Skip to content

Instantly share code, notes, and snippets.

@Munksgaard
Created December 22, 2022 10:50
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save Munksgaard/6bafba77a9f8e0f888d9adb46d590139 to your computer and use it in GitHub Desktop.
Save Munksgaard/6bafba77a9f8e0f888d9adb46d590139 to your computer and use it in GitHub Desktop.
A futhark literate script showing how we can create audio
-- # Generating audio with literate Futhark
--
-- Sound is vibrations in the air that are picked up by our ears. Vibrations
-- can be described using sine-curves. Computers are ultimately discrete in
-- nature, so in order to produce sound we have to approximate sine-curves using
-- sampling. Let's see how that works in practice.
-- We start by defining some global variables that we're going to use.
-- First, the volume which we wish to output at.
def volume = 0.5f32
-- Then, the output frequency, meaning the number of samples we need to generate
-- per second.
def output_hz = 44100i64
-- The _standard pitch_. This is an agreed-upon number that forms the base of a
-- particular school of music. For instance, most western music is based on a
-- 12-tone chromatic scale, centered around the A₄-note, which is defined to
-- have the frequency 440 Hz. This means that the A₄ can be described as a
-- sine-curve with 440 periods every second.
def standard_pitch = 440.0f32
-- All other notes in the 12-tone chromatic scale can be defined in terms of the
-- A₄ note. Notes are divided into octaves, which is why the standard pitch has
-- that four in it: It is the A-note of the fourth octave. The notes of each
-- octave is exactly double or half that of the notes in the previous or next
-- octave, so the A₅-note has the frequency 880 Hz. The 12-notes in between are
-- then distributed in such a way that this invariant is always true. This
-- function helps us to compute the frequency of different notes based on the
-- standard pitch. So, for instance, the pitch of the next note up from A₄ the
-- A#₄-note, is computed by calling `pitch 1`.
def pitch (i: i64): f32 =
standard_pitch * 2 ** (f32.i64 i/12)
-- To produce notes we need to things: a pitch and a duration.
-- If the duration is given in seconds, for instance 0.5 seconds, we need to
-- turn that into the number of samples resulting in a sound of that length,
-- given the output frequency.
def num_samples (duration: f32): i64 =
i64.f32 (f32.i64 output_hz * duration)
-- Next, we define the `sample` function, which takes a pitch and a sample index
-- in order to generate the frequency of the pitch at that particular point in
-- time. The result is a number between -1.0 and 1.0 corresponding to the
-- sample of the sine curve at that particular point in time. Note that we
-- multiply by $2 \pi$, the period of the sine-function.
def sample (p: f32) (i: i64): f32 =
volume * f32.sin (2 * f32.pi * f32.i64 i * p / f32.i64 output_hz)
-- Finally, we can define the `note` function, which samples the frequency for a
-- particular note for the given duration.
def note (i: i64) (duration: f32): []f32 =
let p = pitch i
let n = num_samples duration
in tabulate n (sample p)
-- Let's also make it possible to insert breaks in our compositions.
def break (duration: f32): []f32 =
replicate (num_samples duration) 0.0
-- Finally, we need a function to turn the samples into signed 8-bit integers,
-- such that `futhark literate` can turn that into music.
def play [n] (samples: [n]f32): [n]i8 =
samples
|> map ((*) (f32.i8 i8.highest))
|> map i8.f32
-- In the spirit of season, let's use what we have defined so far to compose a
-- song, inserting breaks and adjusting the length of notes as necessary. Let's
-- see if you can recognize it.
def seasonal_song =
let c = note 3
let d = note 5
let e = note 7
let g = note 10
in e 0.3
++ break 0.1
++ e 0.3
++ break 0.1
++ e 0.6
++ break 0.2
++ e 0.3
++ break 0.1
++ e 0.3
++ break 0.1
++ e 0.6
++ break 0.2
++ e 0.3
++ break 0.1
++ g 0.3
++ break 0.1
++ c 0.5
++ break 0.05
++ d 0.15
++ break 0.1
++ e 0.6
|> play
-- > :audio seasonal_song
-- There are plenty of things to improve in our song in particular and the music
-- framework in general. For instance, it would be nice to get rid of the
-- popping sound between notes, and at some point we'd like to be able to write
-- chords (multiple notes played at the same time) and so on, but I think this
-- is a nice start. I have no real knowledge about music except for the bits
-- and pieces I've read on Wikipedia, so hopefully someone with more knowledge
-- about music will come along to point out my mistakes.
-- Happy holidays to everyone!
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment