Created
December 22, 2022 10:50
-
-
Save Munksgaard/6bafba77a9f8e0f888d9adb46d590139 to your computer and use it in GitHub Desktop.
A futhark literate script showing how we can create audio
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
-- # 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