Use your keyboard and this three step program to tame some noise using the Rust programming language.
4 min read
·
By Bendik Sem Kvernevik
·
December 19, 2023
Let’s make some noise with Rust! Yes, literal noise. Whats more poetic than starting this journey with the audio to the source of life caressing your eardrums: streams! (google translate “bekk” to discover a coincidence). We will attempt to take some control and tame the noise with keyboard input, using some digital audio signal processing. Disclaimer: This is written by a Rust beginner, and may not apply all best practices.
📢 Friendly advice: Although the software presented here is safe for your ears. I will still recommend as a precaution to not use headphones/earphones (or at least always start low volume) when playing with audio in programming. I speak from experience as I vividly remember almost tearing up some eardrums 10 years ago. I was testing out the Csound programming language while cluelessly choosing numbers for scaling the output volume. In shock I almost punched myself in my face, as I lunged toward the headphones pounding damaging amplitudes of sine waves into my brain. Lesson learned; speakers that don’t go directly into your ear are safer.
Given that you have already installed the rust toolchain. Let’s start by creating the project, we may call it “noisy”.
cargo new noisy
To handle playing sound from your computer we will use the cpal crate (Cross-Platform I/O Audio Library). And to simplify our way around creating (synthesizing) the noise, we utilize another powerful crate: fundsp. «FunDSP is an audio DSP (digital signal processing) library for audio processing and synthesis.» - this seems promising! Run cargo add fundsp cpal
. (dasp (formerly sample) also seems to be a popular crate for audio signal processing, and may be more fitting should you need a more lightweight alternative with focus on separation of concern)
We start by setting up the stream for audio playback in main()
and generate some pink noise in the run()
function, that we pass on to the write_data()
function. write_data()
writes the audio - sample by sample - to the output stream.
Run this with cargo run
and you should hear some beautiful noise from your speakers.
use cpal::traits::{DeviceTrait, HostTrait, StreamTrait};
use cpal::{FromSample, SizedSample};
use fundsp::hacker::{pan, pink};
fn main() {
let host = cpal::default_host();
let device = host
.default_output_device()
.expect("Failed to find a default output device");
let config = device.default_output_config().unwrap();
match config.sample_format() {
cpal::SampleFormat::F32 => run::<f32>(&device, &config.into()).unwrap(),
cpal::SampleFormat::I16 => run::<i16>(&device, &config.into()).unwrap(),
cpal::SampleFormat::U16 => run::<u16>(&device, &config.into()).unwrap(),
_ => panic!("Unsupported format"),
}
}
fn run<T>(device: &cpal::Device, config: &cpal::StreamConfig) -> Result<(), anyhow::Error>
where
T: SizedSample + FromSample<f64>,
{
let sample_rate = config.sample_rate.0 as f64;
let channels = config.channels as usize;
// Create pink noise signal
let signal = pink();
// Pan it to the center
let signal = signal >> pan(0.0);
// Scale it to 10%
let mut signal = signal * 0.1;
signal.set_sample_rate(sample_rate);
let mut next_value = move || signal.get_stereo();
let err_fn = |err| eprintln!("an error occurred on stream: {}", err);
let stream = device.build_output_stream(
config,
move |data: &mut [T], _: &cpal::OutputCallbackInfo| {
write_data(data, channels, &mut next_value)
},
err_fn,
None,
)?;
stream.play()?;
std::thread::sleep(std::time::Duration::from_millis(120_000));
Ok(())
}
fn write_data<T>(output: &mut [T], channels: usize, next_sample: &mut dyn FnMut() -> (f64, f64))
where
T: SizedSample + FromSample<f64>,
{
for frame in output.chunks_mut(channels) {
let sample = next_sample();
let left = T::from_sample(sample.0);
let right: T = T::from_sample(sample.1);
for (channel, sample) in frame.iter_mut().enumerate() {
if channel & 1 == 0 {
*sample = left;
} else {
*sample = right;
}
}
}
}
Noise by itself may become boring after some time. Let’s take some control with the keyboard. We add the crossterm
crate to parse keyboard input: cargo add crossterm
. The raw mode lets the program read each character as it is typed by the user. We register this in the main function. Secondly, we create a loop in the run()
function that listens for keyboard input. For this step pressing any button will play the sound, pressing escape will quit the program. We simulate playing while holding down a button by registering a tone_duration
. You may change this variable to your own preference.
Try running this and pressing buttons — this should give you some more control of the wild noise.
use cpal::traits::{DeviceTrait, HostTrait, StreamTrait};
use cpal::{FromSample, SizedSample};
use crossterm::event::{poll, read, Event, KeyCode, KeyEvent};
use crossterm::terminal::{disable_raw_mode, enable_raw_mode};
use fundsp::hacker::{pan, pink};
use std::io;
use std::time::{Duration, Instant};
fn main() -> io::Result<()> {
enable_raw_mode()?;
let host = cpal::default_host();
let device = host
.default_output_device()
.expect("Failed to find a default output device");
let config = device.default_output_config().unwrap();
match config.sample_format() {
cpal::SampleFormat::F32 => run::<f32>(&device, &config.into()).unwrap(),
cpal::SampleFormat::I16 => run::<i16>(&device, &config.into()).unwrap(),
cpal::SampleFormat::U16 => run::<u16>(&device, &config.into()).unwrap(),
_ => panic!("Unsupported format"),
}
disable_raw_mode()?;
Ok(())
}
fn run<T>(device: &cpal::Device, config: &cpal::StreamConfig) -> Result<(), anyhow::Error>
where
T: SizedSample + FromSample<f64>,
{
let sample_rate = config.sample_rate.0 as f64;
let channels = config.channels as usize;
// Create pink noise signal
let signal = pink();
// Pan it to the center
let signal = signal >> pan(0.0);
// Scale it to 10%
let mut signal = signal * 0.1;
signal.set_sample_rate(sample_rate);
let mut next_value = move || signal.get_stereo();
let err_fn = |err| eprintln!("an error occurred on stream: {}", err);
let stream = device.build_output_stream(
config,
move |data: &mut [T], _: &cpal::OutputCallbackInfo| {
write_data(data, channels, &mut next_value)
},
err_fn,
None,
)?;
stream.pause()?;
let mut last_keypress_time = Instant::now();
let mut is_playing = false;
loop {
if poll(std::time::Duration::from_millis(10))? {
if let Event::Key(KeyEvent {
code,
modifiers,
kind: _,
state: _,
}) = read()?
{
match (code, modifiers) {
(KeyCode::Esc, _) => {
// Press 'Escape' to quit
break;
}
(_, _) => {
if !is_playing {
stream.play()?;
is_playing = true;
}
last_keypress_time = Instant::now();
}
}
}
}
// Change this to your desired duration
let tone_duration = 400;
if last_keypress_time.elapsed() > Duration::from_millis(tone_duration) {
stream.pause()?;
is_playing = false;
}
}
Ok(())
}
fn write_data<T>(output: &mut [T], channels: usize, next_sample: &mut dyn FnMut() -> (f64, f64))
where
T: SizedSample + FromSample<f64>,
{
for frame in output.chunks_mut(channels) {
let sample = next_sample();
let left = T::from_sample(sample.0);
let right: T = T::from_sample(sample.1);
for (channel, sample) in frame.iter_mut().enumerate() {
if channel & 1 == 0 {
*sample = left;
} else {
*sample = right;
}
}
}
}
Alright! Now that we have some keyboard events to hook into, let’s take advantage of it. There are a couple of things added here, so let’s break it down. Firstly, we replace the pink noise with an Oscillator
(borrowed from the cpal examples) to be able to dynamically change the frequencies. The oscillator generates square waves, from a given frequency. Secondly, we create our own little piano on the keyboard. Registering the keys from A-L to represent an octave from C to C (the tone). Check out the code below.
use cpal::traits::{DeviceTrait, HostTrait, StreamTrait};
use cpal::{FromSample, Sample, SizedSample};
use crossterm::event::{poll, read, Event, KeyCode, KeyEvent};
use crossterm::terminal::{disable_raw_mode, enable_raw_mode};
use std::io;
use std::sync::{Arc, Mutex};
use std::time::{Duration, Instant};
fn main() -> io::Result<()> {
enable_raw_mode()?;
let host = cpal::default_host();
let device = host
.default_output_device()
.expect("Failed to find a default output device");
let config = device.default_output_config().unwrap();
match config.sample_format() {
cpal::SampleFormat::F32 => run::<f32>(&device, &config.into()).unwrap(),
cpal::SampleFormat::I16 => run::<i16>(&device, &config.into()).unwrap(),
cpal::SampleFormat::U16 => run::<u16>(&device, &config.into()).unwrap(),
_ => panic!("Unsupported format"),
}
disable_raw_mode()?;
Ok(())
}
pub struct Oscillator {
pub sample_rate: f64,
pub current_sample_index: f64,
pub frequency_hz: f64,
}
impl Oscillator {
fn advance_sample(&mut self) {
self.current_sample_index = (self.current_sample_index + 1.0) % self.sample_rate;
}
fn set_freq(&mut self, frequency_hz: f64) {
self.frequency_hz = frequency_hz;
}
fn calculate_sine_output_from_freq(&self, freq: f64) -> f64 {
let two_pi = 2.0 * std::f64::consts::PI;
(self.current_sample_index * freq * two_pi / self.sample_rate).sin()
}
fn is_multiple_of_freq_above_nyquist(&self, multiple: f64) -> bool {
self.frequency_hz * multiple > self.sample_rate / 2.0
}
fn generative_waveform(&mut self, harmonic_index_increment: i32, gain_exponent: f64) -> f64 {
self.advance_sample();
let mut output = 0.0;
let mut i = 1;
while !self.is_multiple_of_freq_above_nyquist(i as f64) {
let gain = 1.0 / (i as f64).powf(gain_exponent);
output += gain * self.calculate_sine_output_from_freq(self.frequency_hz * i as f64);
i += harmonic_index_increment;
}
output
}
fn square_wave(&mut self) -> f64 {
self.generative_waveform(2, 1.0)
}
fn tick(&mut self) -> f64 {
// scale amplitude to 1%
self.square_wave() * 0.01
}
}
fn run<T>(device: &cpal::Device, config: &cpal::StreamConfig) -> Result<(), anyhow::Error>
where
T: SizedSample + FromSample<f64>,
{
let sample_rate = config.sample_rate.0 as f64;
let channels = config.channels as usize;
let frequency = Arc::new(Mutex::new(440.0));
let freq_clone = Arc::clone(&frequency);
let mut oscillator = Oscillator {
sample_rate,
current_sample_index: 0.0,
frequency_hz: 440.0,
};
let err_fn = |err| eprintln!("an error occurred on stream: {}", err);
let stream = device.build_output_stream(
config,
move |data: &mut [T], _: &cpal::OutputCallbackInfo| {
oscillator.set_freq(*freq_clone.lock().unwrap());
write_data(data, channels, &mut oscillator)
},
err_fn,
None,
)?;
stream.pause()?;
let mut last_keypress_time = Instant::now();
let mut is_playing = false;
loop {
if poll(std::time::Duration::from_millis(10))? {
if let Event::Key(KeyEvent {
code,
modifiers: _,
kind: _,
state: _,
}) = read()?
{
match code {
KeyCode::Esc => {
// Press 'Escape' to quit
break;
}
KeyCode::Char('a') => {
*frequency.lock().unwrap() = 261.63; // C4
}
KeyCode::Char('w') => {
*frequency.lock().unwrap() = 277.18; // C#4/Db4
}
KeyCode::Char('s') => {
*frequency.lock().unwrap() = 293.66; // D4
}
KeyCode::Char('e') => {
*frequency.lock().unwrap() = 311.13; // D#4/Eb4
}
KeyCode::Char('d') => {
*frequency.lock().unwrap() = 329.63; // E4
}
KeyCode::Char('f') => {
*frequency.lock().unwrap() = 349.23; // F4
}
KeyCode::Char('t') => {
*frequency.lock().unwrap() = 369.99; // F#4/Gb4
}
KeyCode::Char('g') => {
*frequency.lock().unwrap() = 392.00; // G4
}
KeyCode::Char('y') => {
*frequency.lock().unwrap() = 415.30; // G#4/Ab4
}
KeyCode::Char('h') => {
*frequency.lock().unwrap() = 440.00; // A4
}
KeyCode::Char('u') => {
*frequency.lock().unwrap() = 466.16; // A#4/Bb4
}
KeyCode::Char('j') => {
*frequency.lock().unwrap() = 493.88; // B4
}
KeyCode::Char('k') => {
*frequency.lock().unwrap() = 523.25; // C5
}
_ => {
continue;
}
}
if !is_playing {
stream.play()?;
is_playing = true;
}
last_keypress_time = Instant::now();
}
}
// Change this to your desired duration
let tone_duration = 400;
if is_playing && last_keypress_time.elapsed() > Duration::from_millis(tone_duration) {
stream.pause()?;
is_playing = false;
}
}
Ok(())
}
fn write_data<SampleType>(
output: &mut [SampleType],
num_channels: usize,
oscillator: &mut Oscillator,
) where
SampleType: Sample + FromSample<f64>,
{
for frame in output.chunks_mut(num_channels) {
let value: SampleType = SampleType::from_sample(oscillator.tick());
for sample in frame.iter_mut() {
*sample = value;
}
}
}
Congrats! 🎉 Now you have created your very own monophonic synthesizer (monophonic → able to play one note at a time)! Have a look at the fundsp examples for more inspiration to play around with digital signal processing. With future endeavours one could for example simplify the audio generation in this example even more by utilizing more of the tools from the mentioned crates.