Forkcasting
As clear as a puddle of mud

Making noise

Building a sine wave generator in Rust

I've never been able to build a tone generator or the lines on any platform. I understood that cat /dev/urandom | /dev/dsp made a white noise generator, but that was it. Times have changed, and I want something more sophisticated.

Tools

We'll use a few tools:

  1. Rust -- because I want to learn it better
  2. pacat -- To send our sound to the speakers
  3. sox and ffmpeg to convert the output to something smaller.

Sound format

First, we need to know what pacat expects as input. The --format argument has many possible variations, but it says it "Defaults to s16ne", which is a signed 16-bit native-endian integer. In other words, every two bytes represents the sample amplitude as a number between -32,768 and 32,767.

We can write our program to spit out bytes representing 16-bit signed integers and pipe that directly into pacat.

Sampling basics

An input signal varies continuously, so how do we get it into a computer which is discrete?

A line graph showing the Sine function

Smooth Sine wave

For the computer: We just measure it at regular intervals. Each measurement is a "sample". How often we sample per second is our "sample rate", often quoted in Hz.

A scatter graph showing a series of points, tracing the outline of the Sine function.

Stepped Sine wave

This looks like it would sound terrible, and it can. If we sample (say) 3,000 times a second, then anything above 1.5 kHz gets distorted or lost. However, if we sample 44,100 times a second, then we can capture input frequencies slightly over 20 kHz. Since the people can't hear anything over 20 kHz, we don't lose any information. Perfect!

Generating a sample

Let's get into some code. We need to know what the amplitude of our signal is at any point in our sequence.

fn sample(sample: i32, sample_rate: i32,
          frequency: i32) -> f64 {
    // Where are we in this second?
    let t = f64::from(sample) / f64::from(sample_rate);

    // What is the frequency in radians/sec?
    let rads_per_sec
        = f64::from(frequency * 2) * std::f64::consts::PI;

    // Calculate the Sine of where we are in the second.
    return (t * rads_per_sec).sin();
}

We can't play this for a few reasons:

  1. It's too quiet. It only goes between -1 and 1, which is nearly silent.
  2. It's not a 16-bit signed integer. pacat doesn't know what do do with it.
  3. On it's own, we only get one sample. We need to be able to loop through the values of sample to get any output. This

I'll show the driver loop here, because that handles scaling the sample and generating many samples.

fn main() {
    let sample_rate = 44100;
    // Middle C
    let frequency = 440;
    let mut samp_count = 0;
    loop {

        // Generate one sample
        let base_sample = sample(samp_count, sample_rate,
                                 frequency);

        // Scale it to (-100..100) and make it a 16-bit
        // signed integer
        let samp = (base_sample * 100_f64).round() as i16;

        // Convert it to bytes
        let samp_bts = bytes(samp);

        // Write those bytes to stdout for pacat
        // Do not use print! for this! It does extra Unicode
        // processing that mangles values less than zero.
        std::io::stdout().write(&samp_bts);

        // Move on to the next sample
        samp_count = samp_count + 1;

        // ... Unless we're at the end of one second,
        // then start again.
        if samp_count >= sample_rate {
            samp_count = 0;
        }
    }
}

Notice that we don't need to wrangle any "real time" work. We just generate samples and send them down. pacat defaults to 44,100 Hz and plays exactly that many samples per second. Neat!

Finally, let's talk about getting the bytes for a sample. Ugh. If I had a newer version of rust I could use i16::to_ne_bytes(), but I don't. I had to write my own bytes(i16) -> [u8; 2].

fn bytes(short : i16) -> [u8; 2] {
   return [ (short >> 7) as u8, short as u8 ];
}

It's not bad, just annoying.

Results

Set your volume low, then run:

./target/debug/rust-sine \
    | pacat --channels=1

You can save the output by redirecting to a file, then using sox to do with it as you please.

# Generate some samples
# Ctrl-C once you've got enough
./target/debug/rust-sine > rust-sine.pcm

# Convert the samples to a WAVE file
sox -t raw -r 44.1k -e signed -b 16 -c 1 \
    rust-sine.pcm rust-sine.wav

# Save it as Opus/Ogg so it's much smaller.
ffmpeg -y -i "rust-sine.wav" \
 -codec:a libopus -b:a 16k \
 -map_metadata 0 \
 "rust-sine.ogg"

Now you can hear my work!

A 440 Hz Sine wave, generated by yours truly