Implementing an F# Signal Generator

Written by Liam McLennan

signal generator

As part of a larger project I have been working on a signal generator. A signal generator is a device (in this case an F# program) that is often used to generate waves at a particular frequency that can be interpreted as sound (by applying to a speaker as a voltage).

For my project I want to generate pitches and then convert the signal to PCM audio data, wrapped in the windows WAVE file format. PCM format means that the signal is represented as a series of positive or negative values within a certain range. The number of samples per second is called the sample rate (44,100 for CD audio). The range of the values is governed by the bit depth, again I will stick with the CD audio standard 16 bit audio, giving me a range of -32768 to 32767.

Generating Signals

The simplest and purest representation of a particular frequency is given by a sine wave.

A sine wave

This graph was generated using FSharp.Charting and the following fsx file

#load "../packages/FSharp.Charting.0.90.7/FSharp.Charting.fsx"
open FSharp.Charting
let c = seq { for x in [1.0..0.1..20.0] do yield (x, sin x) } |> Chart.Line

Note that the wave has a range from -1 to 1 and a recurring regular frequency. But what is that regular frequency? The F# sin function operates on an angle given in radians. Experimenting in the F# repl (fsi) we can find that sin of PI radians is 0

> sin System.Math.PI;;
val it : float = 1.224606354e-16

Interesting. What about the sin of 2 PI radians?

> sin (2. * System.Math.PI);;
val it : float = -2.449212708e-16

Those accustomed to working with floating point numbers will recognise that anything x 10^-16 is floating points version of 0. So sin (n * PI) is 0. This tells us, or at least suggests, that the graph crosses the x axis every PI change in x. Now let’s look at mid-points between x intercepts:

> sin (1.5 * System.Math.PI);;
val it : float = -1.0
> sin (2.5 * System.Math.PI);;
val it : float = 1.0
> sin (3.5 * System.Math.PI);;
val it : float = -1.0

Half way between PI and 2 x PI the y value is -1. Half way between 2 x PI and 3 x PI the y value is 1. Half way between 3 x PI and 4 x PI the y value is back to -1, so we see that the graph repeats every 2 x PI.

Now we can start generating PCM data. First we need to list the samples we want to create:

let requiredSamples = seq { 1.0..(seconds * sampleRate) }

We need sampleRate samples for every second. Next we plot those points using the sin function, and scale them by the expected range for 16 bit audio.

let samples = Seq.map 
                (fun x -> x |> sin |> (*) 32767 |> int16) 
                requiredSamples

Now we have PCM audio samples that we can pack and send to an audio player for listening pleasure.

Packing PCM data into a WAVE File

The WAVE file format is a binary format defined as part of Microsoft’s RIFF specification. The details of the format are not very interesting. It has some headers that describe the format in use, and details of the audio data, such as sample rate, bit depth and number of channels. To encode 16 bit mono samples at 44,100the following is sufficient:

let pack (d:int16[]) = 
    let stream = new MemoryStream();
    let writer = new BinaryWriter(stream, System.Text.Encoding.ASCII);
    let dataLength = Array.length d * 2

    // RIFF
    writer.Write(System.Text.Encoding.ASCII.GetBytes("RIFF"))
    writer.Write(Array.length d)
    writer.Write(System.Text.Encoding.ASCII.GetBytes("WAVE"))

    // fmt
    writer.Write(System.Text.Encoding.ASCII.GetBytes("fmt "))
    writer.Write(16)
    writer.Write(1s)        // PCM
    writer.Write(1s)        // mono
    writer.Write(44100)     // sample rate
    writer.Write(44100 * 16 / 8)     // byte rate
    writer.Write(2s)        // bytes per sample
    writer.Write(16s)       // bits per sample

    // data
    writer.Write(System.Text.Encoding.ASCII.GetBytes("data"))
    writer.Write(dataLength)
    let data:byte[] = Array.zeroCreate dataLength
    System.Buffer.BlockCopy(d, 0, data, 0, data.Length)
    writer.Write(data)
    stream

Tuning

So far we have a signal generator that generates audio data following a sine wave, but we have no control of the pitch (frequency) of the sound. We know that the wave repeats every 2 x PI, and we know that we have 44,100 samples per second. What we need is a way to expand and contract the wave horizontally to hit target frequencies. The graph transformation that controls horizontal stretch is achieved by multiplying the argument by a factor prior to applying the original function. ie

let square x = x * x
let squareStretched x = (2 * x) * (2 * x)

The frequence of the wave is

freq = 44100 / period

and the period is 2 x PI. To add a horizontal stretching factor we multiply by some factor.

freq = 44100 / 2 x PI x factor

solving for the factor

factor = 44100 / 2 * PI * freq

Now we can update our signal generating function to be tuneable using the inverse of factor

let frequency = 440.
let samples = Seq.map 
                (fun x -> x 
                |> (*) (2. * System.Math.PI * frequency / 44100.)
                |> sin 
                |> (*) 32767 
                |> int16) 
                requiredSamples

Now we can tune our data to frequencies. Above I have used 440Hz (concert A). To pack into a WAVE and save to a file

let write (ms:MemoryStream) =
    use fs = new FileStream(Path.Combine(__SOURCE_DIRECTORY__,"test.wav"), FileMode.Create)
    ms.WriteTo(fs)

samples |> pack |> write

Admiring the Results

What is the result of all that effort? A few seconds of concert pitch.