From 0da46c8a33e7a9b61b564cce5c4c4526fe3d84dc Mon Sep 17 00:00:00 2001 From: Anthony Wang Date: Thu, 16 Mar 2023 22:59:41 -0400 Subject: Clean up code and rename to yue --- .gitignore | 2 - README.md | 15 +-- blend.nim | 289 ++++++++++++++++++++++++++++++++++++++++++++++++++ blend.ogg | Bin 0 -> 432160 bytes blend.py | 261 +++++++++++++++++++++++++++++++++++++++++++++ blend.scm | 77 ++++++++++++++ example.ogg | Bin 432160 -> 0 bytes keyboard.py | 39 ++----- lambeat.scm | 17 --- lib.scm | 22 ---- music.nim | 340 ----------------------------------------------------------- music.py | 330 --------------------------------------------------------- music.scm | 75 ------------- musiclib.nim | 75 ------------- yue.nim | 120 +++++++++++++++++++++ yue.py | 69 ++++++++++++ yue.scm | 37 +++++++ 17 files changed, 865 insertions(+), 903 deletions(-) delete mode 100644 .gitignore create mode 100644 blend.nim create mode 100644 blend.ogg create mode 100644 blend.py create mode 100644 blend.scm delete mode 100644 example.ogg delete mode 100644 lambeat.scm delete mode 100644 lib.scm delete mode 100644 music.nim delete mode 100644 music.py delete mode 100644 music.scm delete mode 100644 musiclib.nim create mode 100644 yue.nim create mode 100644 yue.py create mode 100644 yue.scm diff --git a/.gitignore b/.gitignore deleted file mode 100644 index 37c3b88..0000000 --- a/.gitignore +++ /dev/null @@ -1,2 +0,0 @@ -/music -/music.s32 \ No newline at end of file diff --git a/README.md b/README.md index eb1259f..87fbaae 100644 --- a/README.md +++ b/README.md @@ -1,14 +1,9 @@ -# Lambeat -Lambeat is a new way to make music using functional programming. It's heavily influenced by [Bytebeat](https://dollchan.net/bytebeat) and initially started out as an reimplementation of Bytebeat in Scheme. Since then, it's grown to be a delightful new way to make music with code. +# yue + +yue is a programmatic music library, influenced by [Bytebeat](https://dollchan.net/bytebeat). It is designed to be low-level and enable you to build your own abstractions and functions. Think of it like an assembly language for music. There are three implementations in Python (PyPy recommended for speed), Scheme, and Nim. Check out this [sample code](blend.py) and [listen to it](blend.ogg)! ## Get started -First, install [Sox](https://sox.sourceforge.net/) and clone this repo. Write some music in `music.scm`. Enjoy your music with `guile --fresh-auto-compile lambeat.scm | play -r 8000 -t s16 -`! -For the Python version, use `pypy3 music.py | play -r 44100 -t s32 -` to listen and `pypy3 music.py | sox -r 44100 -t s32 - example.ogg` to save to a file. +First, install [Sox](https://sox.sourceforge.net/) and clone this repo. -For the nim version -``` -nim c --mm:orc -d:release music.nim -./music > music.s32 -play -r 44100 -t s32 music.s32 -``` +To run a program that uses yue, use `pypy file.py`, `guile --fresh-auto-compile file.scm`, or `nim c --mm:orc -d:release file.nim && ./file`, and pipe to `play -r 44100 -t s32 -`. If you would like to save to a file, pipe to `sox -r 44100 -t s32 - file.ogg` instead. diff --git a/blend.nim b/blend.nim new file mode 100644 index 0000000..df87253 --- /dev/null +++ b/blend.nim @@ -0,0 +1,289 @@ +import yue, std/[sugar] + +const GAIN_NORMAL = 0.22 + +var osc: OscFn = (f, t: float) => 0.0 + +proc p*(len, octave, step, vol: float = 1): Note = + ## Note helper constructor + (len, freq(octave, step), vol, osc) + +osc = (f, t: float) => osc_piano(f, t) * GAIN_NORMAL + +let intro = [ + p(1, 3, 3), + p(1, 3, 7), + p(1, 3, 10), + p(6, 4, 2), + + p(1, 3, 1), + p(1, 3, 5), + p(1, 3, 8), + p(3, 4, 0), + + p(1, 2, 11), + p(1, 3, 3), + p(1, 3, 6), + p(3, 3, 10), + + p(1, 2, 8), + p(1, 3, 0), + p(1, 3, 3), + p(8, 3, 7), +] + +let outro = [ + p(1, 3, 3), + p(1, 3, 7), + p(1, 3, 10), + p(1, 4, 2), + p(1, 4, 3), + p(1, 4, 7), + p(2, 4, 8), + + p(1, 3, 1), + p(1, 3, 5), + p(1, 3, 8), + p(1, 4, 0), + p(1, 4, 1), + p(1, 4, 5), + p(2, 4, 8), + + p(1, 2, 11), + p(1, 3, 3), + p(1, 3, 6), + p(1, 3, 10), + p(1.5, 3, 11), + p(1.5, 4, 3), + p(3, 4, 8), + + p(1.5, 2, 8), + p(1.5, 3, 0), + p(2, 3, 3), + p(16, 3, 7, 2), +] + +let melody = [ + p(1, 3, 3), + p(1, 3, 7), + p(1, 3, 10), + p(1, 4, 2), + p(1, 4, 3), + p(1, 4, 7), + p(2, 4, 8), + + p(1, 3, 1), + p(1, 3, 5), + p(1, 3, 8), + p(1, 4, 0), + p(1, 4, 1), + p(1, 4, 5), + p(2, 4, 8), + + p(1, 3, 3), + p(1, 3, 7), + p(1, 3, 10), + p(1, 4, 2), + p(1, 4, 3), + p(1, 4, 10), + p(2, 4, 3), + + p(1, 3, 1), + p(1, 3, 5), + p(1, 3, 8), + p(1, 4, 0), + p(1, 4, 10), + p(1, 4, 8), + p(2, 4, 10), + + + p(1, 3, 3), + p(1, 3, 7), + p(1, 3, 10), + p(1, 4, 2), + p(1, 4, 3), + p(1, 4, 7), + p(2, 4, 8), + + p(1, 3, 1), + p(1, 3, 5), + p(1, 3, 8), + p(1, 4, 0), + p(1, 4, 1), + p(1, 4, 5), + p(2, 4, 1), + + p(1, 3, 3), + p(1, 3, 7), + p(1, 3, 10), + p(1, 4, 2), + p(1, 4, 3), + p(1, 4, 10), + p(1, 4, 8), + p(1, 4, 7), + + p(1, 3, 1), + p(1, 3, 5), + p(1, 3, 8), + p(1, 4, 0), + p(1, 4, 10), + p(1, 4, 8), + p(2, 4, 10), +] + +osc = (f, t: float) => osc_weird_pluck(f, t) * GAIN_NORMAL + +let melody2 = [ + p(1, 0, 0), + p(1, 5, 10), + p(1, 5, 8), + p(1, 5, 7), + p(1, 5, 8), + p(3, 5, 7, 2), + + p(1, 5, 3), + p(1, 4, 10), + p(6, 5, 1, 2), + + p(1/2, 5, 0, 2), + p(1/2, 5, 1, 2), + p(3, 5, 3, 2), + p(1/2, 5, 10, 2), + p(7/2, 5, 3, 2), + + p(8, 0, 0), + + p(1, 0, 0), + p(1, 5, 3), + p(1, 5, 10), + p(1, 5, 10), + p(4/3, 5, 10), + p(4/3, 5, 8), + p(4/3, 5, 7), + + p(1, 0, 0), + p(1, 5, 1), + p(1, 5, 8), + p(1, 5, 8), + p(4/3, 5, 8), + p(4/3, 5, 8), + p(4/3, 5, 10), + + p(8, 0, 0), + + p(1, 0, 0), + p(5, 5, 3, 2), + p(2, 5, 10, 2), +] + +let melody3 = [ + p(1, 0, 0), + p(1, 5, 10), + p(1/2, 5, 8, 2/3), + p(1/2, 5, 7, 2/3), + p(1/4, 5, 8, 1/2), + p(1/4, 5, 7, 1/2), + p(1/4, 5, 8, 1/2), + p(1/4, 5, 7, 1/2), + p(1, 5, 8), + p(3, 5, 7, 2), + + p(1, 5, 3), + p(1, 4, 10), + p(1, 5, 1), + p(5, 5, 7, 2), + + p(1/2, 5, 7), + p(1/2, 5, 10), + p(1/4, 5, 7), + p(1/4, 5, 10), + p(1/4, 5, 7), + p(1/4, 5, 10), + p(1, 6, 3), + p(2, 5, 3, 2), + p(1/2, 6, 3), + p(5/2, 5, 3, 2), + + p(1/2, 5, 10), + p(1/2, 5, 8), + p(1/2, 5, 7), + p(1/2, 5, 8), + p(1/2, 5, 7), + p(1/2, 5, 3), + p(1/2, 4, 10), + p(1/2, 5, 1), + p(1/2, 5, 0), + p(1/2, 4, 10), + p(1/2, 4, 8), + p(1/2, 4, 10), + p(1/2, 5, 3), + p(1/2, 5, 7), + p(1/2, 5, 3), + p(1/2, 5, 10), + + p(4/3, 5, 7), + p(4/3, 6, 3), + p(4/3, 6, 3), + p(4/3, 6, 2), + p(4/3, 5, 10), + p(4/3, 5, 7), + + p(3, 5, 5), + p(2, 5, 7), + p(2, 5, 8), + p(1, 6, 1), + + p(1, 5, 3), + p(1, 5, 5), + p(2, 5, 7), + p(1, 5, 3), + p(1, 5, 8), + p(2, 5, 10), + + p(3/2, 6, 0), + p(3/2, 6, 1), + p(5, 6, 3, 2), +] + +# clip length to 1 second +osc = (f, t: float) => (if t > 1: 0.0 else: + (osc_saw(f, t) * GAIN_NORMAL * 0.06) + (osc_pulse(f, t) * GAIN_NORMAL * 0.3)) + +let bass = [ + p(1, 1, 3), + p(1, 1, 10), + p(1, 1, 1), + p(1, 1, 8), + p(1, 1, 3), + p(1, 2, 3), + p(1, 1, 1), + p(1, 1, 10), +] + +from std/algorithm import sort + +# Process all lists of notes +var music: seq[ProcessedNote] = @[] +music.process(intro, 0, 4) +music.process(melody, 8, 4) +music.process(melody, 24, 4) +music.process(bass, 24) +music.process(bass, 32) +music.process(melody, 40, 4) +music.process(melody2, 40, 4) +music.process(bass, 40) +music.process(bass, 48) +music.process(melody, 56, 4) +music.process(melody3, 56, 4) +music.process(bass, 56) +music.process(bass, 64) +music.process(outro, 72, 4) +music.sortByStart() + +# Number of times to sample each second +const bitrate = 44100 + +# Print out music encoded in s32 to standard output +for i in (0 * bitrate ..< 84 * bitrate): + let bytes = cast[array[4, uint8]](music.at(i / bitrate)) + doAssert 4 == stdout.writeBytes(bytes, 0, 4) diff --git a/blend.ogg b/blend.ogg new file mode 100644 index 0000000..af1b409 Binary files /dev/null and b/blend.ogg differ diff --git a/blend.py b/blend.py new file mode 100644 index 0000000..b1c9df8 --- /dev/null +++ b/blend.py @@ -0,0 +1,261 @@ +import yue + +intro = [ + (1,3,3), + (1,3,7), + (1,3,10), + (6,4,2), + + (1,3,1), + (1,3,5), + (1,3,8), + (3,4,0), + + (1,2,11), + (1,3,3), + (1,3,6), + (3,3,10), + + (1,2,8), + (1,3,0), + (1,3,3), + (8,3,7), +] + +melody = [ + (1,3,3), + (1,3,7), + (1,3,10), + (1,4,2), + (1,4,3), + (1,4,7), + (2,4,8), + + (1,3,1), + (1,3,5), + (1,3,8), + (1,4,0), + (1,4,1), + (1,4,5), + (2,4,8), + + (1,3,3), + (1,3,7), + (1,3,10), + (1,4,2), + (1,4,3), + (1,4,10), + (2,4,3), + + (1,3,1), + (1,3,5), + (1,3,8), + (1,4,0), + (1,4,10), + (1,4,8), + (2,4,10), + + + (1,3,3), + (1,3,7), + (1,3,10), + (1,4,2), + (1,4,3), + (1,4,7), + (2,4,8), + + (1,3,1), + (1,3,5), + (1,3,8), + (1,4,0), + (1,4,1), + (1,4,5), + (2,4,1), + + (1,3,3), + (1,3,7), + (1,3,10), + (1,4,2), + (1,4,3), + (1,4,10), + (1,4,8), + (1,4,7), + + (1,3,1), + (1,3,5), + (1,3,8), + (1,4,0), + (1,4,10), + (1,4,8), + (2,4,10), +] + +bass = [ + (1,1,3), + (1,1,10), + (1,1,1), + (1,1,8), + (1,1,3), + (1,2,3), + (1,1,1), + (1,1,10), +] + +melody2 = [ + (1,0,0), + (1,5,10), + (1,5,8), + (1,5,7), + (1,5,8), + (3,5,7,2), + + (1,5,3), + (1,4,10), + (6,5,1,2), + + (1/2,5,0,2), + (1/2,5,1,2), + (3,5,3,2), + (1/2,5,10,2), + (7/2,5,3,2), + + (8,0,0), + + (1,0,0), + (1,5,3), + (1,5,10), + (1,5,10), + (4/3,5,10), + (4/3,5,8), + (4/3,5,7), + + (1,0,0), + (1,5,1), + (1,5,8), + (1,5,8), + (4/3,5,8), + (4/3,5,8), + (4/3,5,10), + + (8,0,0), + + (1,0,0), + (5,5,3,2), + (2,5,10,2), +] + +melody3 = [ + (1,0,0), + (1,5,10), + (1/2,5,8,2/3), + (1/2,5,7,2/3), + (1/4,5,8,1/2), + (1/4,5,7,1/2), + (1/4,5,8,1/2), + (1/4,5,7,1/2), + (1,5,8), + (3,5,7,2), + + (1,5,3), + (1,4,10), + (1,5,1), + (5,5,7,2), + + (1/2,5,7), + (1/2,5,10), + (1/4,5,7), + (1/4,5,10), + (1/4,5,7), + (1/4,5,10), + (1,6,3), + (2,5,3,2), + (1/2,6,3), + (5/2,5,3,2), + + (1/2,5,10), + (1/2,5,8), + (1/2,5,7), + (1/2,5,8), + (1/2,5,7), + (1/2,5,3), + (1/2,4,10), + (1/2,5,1), + (1/2,5,0), + (1/2,4,10), + (1/2,4,8), + (1/2,4,10), + (1/2,5,3), + (1/2,5,7), + (1/2,5,3), + (1/2,5,10), + + (4/3,5,7), + (4/3,6,3), + (4/3,6,3), + (4/3,6,2), + (4/3,5,10), + (4/3,5,7), + + (3,5,5), + (2,5,7), + (2,5,8), + (1,6,1), + + (1,5,3), + (1,5,5), + (2,5,7), + (1,5,3), + (1,5,8), + (2,5,10), + + (3/2,6,0), + (3/2,6,1), + (5,6,3,2), +] + +outro = [ + (1,3,3), + (1,3,7), + (1,3,10), + (1,4,2), + (1,4,3), + (1,4,7), + (2,4,8), + + (1,3,1), + (1,3,5), + (1,3,8), + (1,4,0), + (1,4,1), + (1,4,5), + (2,4,8), + + (1,2,11), + (1,3,3), + (1,3,6), + (1,3,10), + (1.5,3,11), + (1.5,4,3), + (3,4,8), + + (1.5,2,8), + (1.5,3,0), + (2,3,3), + (16,3,7,2), +] + +yue.process(intro, 0, 4, blend=1) +yue.process(melody, 8, 4, blend=1) +yue.process(melody, 24, 4, blend=1) +yue.process(bass, 24, gain=1.5, blend=1) +yue.process(bass, 32, gain=1.5, blend=1) +yue.process(melody, 40, 4, blend=1) +yue.process(melody2, 40, 4, blend=1) +yue.process(bass, 40, gain=1.5, blend=1) +yue.process(bass, 48, gain=1.5, blend=1) +yue.process(melody, 56, 4, blend=1) +yue.process(melody3, 56, 4, blend=1) +yue.process(bass, 56, gain=1.5, blend=1) +yue.process(bass, 64, gain=1.5, blend=1) +yue.process(outro, 72, 4, blend=1) +yue.play(0, 84) diff --git a/blend.scm b/blend.scm new file mode 100644 index 0000000..1824ca1 --- /dev/null +++ b/blend.scm @@ -0,0 +1,77 @@ +(include "yue.scm") + +; https://musiclab.chromeexperiments.com/Song-Maker/song/6021372552937472 +(define (music t) + (apply + (map (lambda (x) + (apply (lambda (octave pitch start len) ((note (getfreq octave pitch) (/ start 2) (/ len 2)) t)) x)) + '( + ; opening notes + (1 11 0 .2) + (2 3 .2 .2) + (2 6 .4 .2) + (2 10 .6 2) + (1 9 3 .2) + (2 1 3.2 .2) + (2 4 3.4 .2) + (2 8 3.6 1) + (1 7 5 .2) + (1 11 5.2 .2) + (2 2 5.4 .2) + (2 6 5.6 1) + (1 4 7 .2) + (1 8 7.2 .2) + (1 11 7.4 .2) + (2 3 7.6 4.4) + + ; melody + (2 8 12 0.5) + (2 7 12.5 0.5) + (2 3 13 0.5) + (2 0 13.5 0.5) + (2 8 14 0.5) + (2 7 14.5 0.5) + (2 3 15 0.5) + (2 0 15.5 0.5) + (2 8 16 0.5) + (2 7 16.5 0.5) + (2 3 17 0.5) + (1 10 17.5 0.5) + (2 8 18 0.5) + (2 7 18.5 0.5) + (2 3 19 0.5) + (1 8 19.5 0.5) + (2 8 20 0.5) + (2 7 20.5 0.5) + (2 3 21 0.5) + (2 5 21.5 0.5) + (2 8 22 0.5) + (2 7 22.5 0.5) + (2 3 23 0.5) + (2 0 23.5 0.5) + (2 8 24 0.5) + (2 7 24.5 0.5) + (2 3 25 0.5) + (1 10 25.5 0.5) + (2 8 26 0.5) + (2 7 26.5 0.5) + (2 3 27 0.5) + (2 10 27.5 0.5) + (2 8 28 0.5) + (2 7 28.5 0.5) + (2 8 29 0.5) + (2 0 29.5 0.5) + (2 8 30 0.5) + (2 7 30.5 0.5) + (2 3 31 0.5) + (1 10 31.5 0.5) + (2 8 32 0.5) + (2 7 32.5 0.5) + (2 8 33 0.5) + (2 10 33.5 0.5) + (2 8 34 0.5) + (2 7 34.5 0.5) + (2 3 35 0.5) + (2 0 35.5 0.5) + )))) + +(play 6 100) diff --git a/example.ogg b/example.ogg deleted file mode 100644 index af1b409..0000000 Binary files a/example.ogg and /dev/null differ diff --git a/keyboard.py b/keyboard.py index 301c950..73892ea 100644 --- a/keyboard.py +++ b/keyboard.py @@ -1,51 +1,26 @@ import math -import sounddevice as sd import os import sys +import sounddevice as sd +import yue # Number of times to sample each second bitrate = 44100 -def freq(octave, step): - """ - Returns the frequency of a note - """ - return 55 * 2 ** (octave + step / 12 - 1) - - -def tone(f, t): - """ - Returns the intensity of a tone of frequency f sampled at time t - """ - # https://dsp.stackexchange.com/questions/46598/mathematical-equation-for-the-sound-wave-that-a-piano-makes - # https://youtu.be/ogFAHvYatWs?t=254 - # return int(2**13*(1+square(t, 440*2**(math.floor(5*t)/12)))) - # Y = sum([math.sin(2 * i * math.pi * t * f) * math.exp(-0.0004 * 2 * math.pi * t * f) / 2**i for i in range(1, 4)]) - # Y += Y * Y * Y - # Y *= 1 + 16 * t * math.exp(-6 * t) - w = 2 * math.pi * f - Y = 0.6 * math.sin(w * t) * math.exp(-0.001 * w * t) - Y += 0.2 * math.sin(2 * w * t) * math.exp(-0.001 * w * t) - Y += 0.05 * math.sin(3 * w * t) * math.exp(-0.001 * w * t) - Y += Y * Y * Y - Y *= 1 + 16 * t * math.exp(-6 * t) - return Y - - note = [] for i in range(0, 24): - note.append([tone(freq(3, i), j / bitrate) / 4 for j in range(0, 3 * bitrate)]) + note.append([yue.tone(yue.freq(3, i), j / bitrate) / 4 for j in range(0, 3 * bitrate)]) sd.default.samplerate = bitrate -print('READY') +print("READY") while True: - os.system('stty raw -echo') + os.system("stty raw -echo") c = sys.stdin.read(1) - os.system('stty -raw echo') + os.system("stty -raw echo") - x = '`1234567890-~!@#$%^&*()_'.index(c) + x = "`1234567890-~!@#$%^&*()_".index(c) print(x) sd.play(note[x]) diff --git a/lambeat.scm b/lambeat.scm deleted file mode 100644 index 5214b56..0000000 --- a/lambeat.scm +++ /dev/null @@ -1,17 +0,0 @@ -(use-modules (ice-9 binary-ports)) -(include "music.scm") - -; Bitrate is the number time to sample the music function each second -(define bitrate 8000) - -; Get the music as a list sampled at the bitrate -(define (play t end) - (cons ((lambda (a) - (let ((b (modulo (inexact->exact (round (* (+ a 2) 32768))) 65536))) cons - (put-u8 (current-output-port) (modulo b 256)) - (put-u8 (current-output-port) (quotient b 256)))) (music t)) - (if (< t end) - (play (+ t (/ 1 bitrate)) end) - '()))) - -(play 6 100) diff --git a/lib.scm b/lib.scm deleted file mode 100644 index 36a65c4..0000000 --- a/lib.scm +++ /dev/null @@ -1,22 +0,0 @@ -; Triangle wave with a period of 1 second -(define (tri t) - (let ((m (floor-remainder (+ t (/ 1 4)) 1))) - (if (< m 1/2) - (- (* 4 m) 1) - (- 3 (* 4 m))))) - -; Square wave with a period of 1 second -(define (square t) - (let ((m (floor-remainder t 1))) - (if (< m 0.5) 1 -1))) - -; Creates a note -(define (note freq start len) - (lambda (t) ( - if (or (< t start) (>= t (+ start len))) - 0 - (* 1/4 (tri (* t freq)))))) - -; Gets the frequency of a particular pitch -(define (getfreq octave pitch) - (* 55 (ash 1 octave) (expt 2 (/ pitch 12)))) diff --git a/music.nim b/music.nim deleted file mode 100644 index 30c256c..0000000 --- a/music.nim +++ /dev/null @@ -1,340 +0,0 @@ -import musiclib, std/[math, sugar] - -# Number of times to sample each second -const bitrate = 44100 - -func osc_weird_pluck*(f, t: float): float = - # I got this as a bug - let w = 2 * PI * f - let wxt = w * t - let exp_swxt = math.exp(-0.001 * wxt) - let y0 = 0.6 * math.sin(wxt) - let y1 = 0.2 * math.sin(2 * wxt) - let y2 = 0.05 * math.sin(3 * wxt) - let y3 = (y0 + y1 + y2) * exp_swxt - let y4 = (1+y3)*y3*y3 # this line is different - y4 * (1 + 16 * t * math.exp(-6 * t)) - -func osc_piano*(f, t: float): float = - ## Returns the intensity of a tone of frequency f sampled at time t - ## t starts at 0 (note start) - # https://dsp.stackexchange.com/questions/46598/mathematical-equation-for-the-sound-wave-that-a-piano-makes - # https://youtu.be/ogFAHvYatWs?t=254 - # return int(2**13*(1+square(t, 440*2**(math.floor(5*t)/12)))) - # Y = sum([math.sin(2 * i * math.pi * t * f) * math.exp(-0.0004 * 2 * math.pi * t * f) / 2**i for i in range(1, 4)]) - # Y += Y * Y * Y - # Y *= 1 + 16 * t * math.exp(-6 * t) - let w = 2 * PI * f - let ewt = math.exp(-0.001 * w * t) - var Y = 0.6 * math.sin(w * t) * ewt + - 0.2 * math.sin(2 * w * t) * ewt + - 0.05 * math.sin(3 * w * t) * ewt - - let Y2 = Y * (Y * Y + 1) - Y2 * (1 + 16 * t * math.exp(-6 * t)) - -func osc_pulse*(f, t: float, phasedrift: float = 0.0): float = - let doublewidth = 1.0 / f - let width = doublewidth / 2 - let phase: float = (t + doublewidth * phasedrift) mod doublewidth - if phase < width: 1.0 else: -1.0 - -func osc_saw*(f,t:float, phasedrift: float = 0.0):float = - let doublewidth = 1.0 / f - let width = doublewidth / 2 - let phase: float = (t + doublewidth * phasedrift) mod doublewidth - if phase < width: - -1.0 + 2.0 * phase / width - else: - 1.0 - 2.0 * (phase - width) / (1.0 - width) - -func freq*(octave, step: float): float = - ## Returns the frequency of a note - 55 * pow(2, (octave + step / 12 - 1)) - -var osc: OscFn = (f, t: float) => 0.0 - -proc p*(len, octave, step, vol: float = 1): Note = - ## Note helper constructor - (len, freq(octave, step), vol, osc) - -#------- song region ------- - -const GAIN_NORMAL = 0.22 - -osc = (f, t: float) => osc_piano(f, t) * GAIN_NORMAL - -let intro = [ - p(1, 3, 3), - p(1, 3, 7), - p(1, 3, 10), - p(6, 4, 2), - - p(1, 3, 1), - p(1, 3, 5), - p(1, 3, 8), - p(3, 4, 0), - - p(1, 2, 11), - p(1, 3, 3), - p(1, 3, 6), - p(3, 3, 10), - - p(1, 2, 8), - p(1, 3, 0), - p(1, 3, 3), - p(8, 3, 7), -] - -let outro = [ - p(1, 3, 3), - p(1, 3, 7), - p(1, 3, 10), - p(1, 4, 2), - p(1, 4, 3), - p(1, 4, 7), - p(2, 4, 8), - - p(1, 3, 1), - p(1, 3, 5), - p(1, 3, 8), - p(1, 4, 0), - p(1, 4, 1), - p(1, 4, 5), - p(2, 4, 8), - - p(1, 2, 11), - p(1, 3, 3), - p(1, 3, 6), - p(1, 3, 10), - p(1.5, 3, 11), - p(1.5, 4, 3), - p(3, 4, 8), - - p(1.5, 2, 8), - p(1.5, 3, 0), - p(2, 3, 3), - p(16, 3, 7, 2), -] - -let melody = [ - p(1, 3, 3), - p(1, 3, 7), - p(1, 3, 10), - p(1, 4, 2), - p(1, 4, 3), - p(1, 4, 7), - p(2, 4, 8), - - p(1, 3, 1), - p(1, 3, 5), - p(1, 3, 8), - p(1, 4, 0), - p(1, 4, 1), - p(1, 4, 5), - p(2, 4, 8), - - p(1, 3, 3), - p(1, 3, 7), - p(1, 3, 10), - p(1, 4, 2), - p(1, 4, 3), - p(1, 4, 10), - p(2, 4, 3), - - p(1, 3, 1), - p(1, 3, 5), - p(1, 3, 8), - p(1, 4, 0), - p(1, 4, 10), - p(1, 4, 8), - p(2, 4, 10), - - - p(1, 3, 3), - p(1, 3, 7), - p(1, 3, 10), - p(1, 4, 2), - p(1, 4, 3), - p(1, 4, 7), - p(2, 4, 8), - - p(1, 3, 1), - p(1, 3, 5), - p(1, 3, 8), - p(1, 4, 0), - p(1, 4, 1), - p(1, 4, 5), - p(2, 4, 1), - - p(1, 3, 3), - p(1, 3, 7), - p(1, 3, 10), - p(1, 4, 2), - p(1, 4, 3), - p(1, 4, 10), - p(1, 4, 8), - p(1, 4, 7), - - p(1, 3, 1), - p(1, 3, 5), - p(1, 3, 8), - p(1, 4, 0), - p(1, 4, 10), - p(1, 4, 8), - p(2, 4, 10), -] - -osc = (f, t: float) => osc_weird_pluck(f, t) * GAIN_NORMAL - -let melody2 = [ - p(1, 0, 0), - p(1, 5, 10), - p(1, 5, 8), - p(1, 5, 7), - p(1, 5, 8), - p(3, 5, 7, 2), - - p(1, 5, 3), - p(1, 4, 10), - p(6, 5, 1, 2), - - p(1/2, 5, 0, 2), - p(1/2, 5, 1, 2), - p(3, 5, 3, 2), - p(1/2, 5, 10, 2), - p(7/2, 5, 3, 2), - - p(8, 0, 0), - - p(1, 0, 0), - p(1, 5, 3), - p(1, 5, 10), - p(1, 5, 10), - p(4/3, 5, 10), - p(4/3, 5, 8), - p(4/3, 5, 7), - - p(1, 0, 0), - p(1, 5, 1), - p(1, 5, 8), - p(1, 5, 8), - p(4/3, 5, 8), - p(4/3, 5, 8), - p(4/3, 5, 10), - - p(8, 0, 0), - - p(1, 0, 0), - p(5, 5, 3, 2), - p(2, 5, 10, 2), -] - -let melody3 = [ - p(1, 0, 0), - p(1, 5, 10), - p(1/2, 5, 8, 2/3), - p(1/2, 5, 7, 2/3), - p(1/4, 5, 8, 1/2), - p(1/4, 5, 7, 1/2), - p(1/4, 5, 8, 1/2), - p(1/4, 5, 7, 1/2), - p(1, 5, 8), - p(3, 5, 7, 2), - - p(1, 5, 3), - p(1, 4, 10), - p(1, 5, 1), - p(5, 5, 7, 2), - - p(1/2, 5, 7), - p(1/2, 5, 10), - p(1/4, 5, 7), - p(1/4, 5, 10), - p(1/4, 5, 7), - p(1/4, 5, 10), - p(1, 6, 3), - p(2, 5, 3, 2), - p(1/2, 6, 3), - p(5/2, 5, 3, 2), - - p(1/2, 5, 10), - p(1/2, 5, 8), - p(1/2, 5, 7), - p(1/2, 5, 8), - p(1/2, 5, 7), - p(1/2, 5, 3), - p(1/2, 4, 10), - p(1/2, 5, 1), - p(1/2, 5, 0), - p(1/2, 4, 10), - p(1/2, 4, 8), - p(1/2, 4, 10), - p(1/2, 5, 3), - p(1/2, 5, 7), - p(1/2, 5, 3), - p(1/2, 5, 10), - - p(4/3, 5, 7), - p(4/3, 6, 3), - p(4/3, 6, 3), - p(4/3, 6, 2), - p(4/3, 5, 10), - p(4/3, 5, 7), - - p(3, 5, 5), - p(2, 5, 7), - p(2, 5, 8), - p(1, 6, 1), - - p(1, 5, 3), - p(1, 5, 5), - p(2, 5, 7), - p(1, 5, 3), - p(1, 5, 8), - p(2, 5, 10), - - p(3/2, 6, 0), - p(3/2, 6, 1), - p(5, 6, 3, 2), -] - -# clip length to 1 second -osc = (f, t: float) => (if t > 1: 0.0 else: - (osc_saw(f, t) * GAIN_NORMAL * 0.06) + (osc_pulse(f, t) * GAIN_NORMAL * 0.3)) - -let bass = [ - p(1, 1, 3), - p(1, 1, 10), - p(1, 1, 1), - p(1, 1, 8), - p(1, 1, 3), - p(1, 2, 3), - p(1, 1, 1), - p(1, 1, 10), -] - -from std/algorithm import sort - -# Process all lists of notes -var music: seq[ProcessedNote] = @[] -music.process(intro, 0, 4) -music.process(melody, 8, 4) -music.process(melody, 24, 4) -music.process(bass, 24) -music.process(bass, 32) -music.process(melody, 40, 4) -music.process(melody2, 40, 4) -music.process(bass, 40) -music.process(bass, 48) -music.process(melody, 56, 4) -music.process(melody3, 56, 4) -music.process(bass, 56) -music.process(bass, 64) -music.process(outro, 72, 4) -music.sortByStart() - -# Print out music encoded in s32 to standard output -for i in (0 * bitrate ..< 84 * bitrate): - let bytes = cast[array[4, uint8]](music.at(i / bitrate)) - doAssert 4 == stdout.writeBytes(bytes, 0, 4) diff --git a/music.py b/music.py deleted file mode 100644 index 74197b6..0000000 --- a/music.py +++ /dev/null @@ -1,330 +0,0 @@ -import bisect -import math -import struct -import sys - -# Number of times to sample each second -bitrate = 44100 - -intro = [ - (1,3,3), - (1,3,7), - (1,3,10), - (6,4,2), - - (1,3,1), - (1,3,5), - (1,3,8), - (3,4,0), - - (1,2,11), - (1,3,3), - (1,3,6), - (3,3,10), - - (1,2,8), - (1,3,0), - (1,3,3), - (8,3,7), -] - -melody = [ - (1,3,3), - (1,3,7), - (1,3,10), - (1,4,2), - (1,4,3), - (1,4,7), - (2,4,8), - - (1,3,1), - (1,3,5), - (1,3,8), - (1,4,0), - (1,4,1), - (1,4,5), - (2,4,8), - - (1,3,3), - (1,3,7), - (1,3,10), - (1,4,2), - (1,4,3), - (1,4,10), - (2,4,3), - - (1,3,1), - (1,3,5), - (1,3,8), - (1,4,0), - (1,4,10), - (1,4,8), - (2,4,10), - - - (1,3,3), - (1,3,7), - (1,3,10), - (1,4,2), - (1,4,3), - (1,4,7), - (2,4,8), - - (1,3,1), - (1,3,5), - (1,3,8), - (1,4,0), - (1,4,1), - (1,4,5), - (2,4,1), - - (1,3,3), - (1,3,7), - (1,3,10), - (1,4,2), - (1,4,3), - (1,4,10), - (1,4,8), - (1,4,7), - - (1,3,1), - (1,3,5), - (1,3,8), - (1,4,0), - (1,4,10), - (1,4,8), - (2,4,10), -] - -bass = [ - (1,1,3), - (1,1,10), - (1,1,1), - (1,1,8), - (1,1,3), - (1,2,3), - (1,1,1), - (1,1,10), -] - -melody2 = [ - (1,0,0), - (1,5,10), - (1,5,8), - (1,5,7), - (1,5,8), - (3,5,7,2), - - (1,5,3), - (1,4,10), - (6,5,1,2), - - (1/2,5,0,2), - (1/2,5,1,2), - (3,5,3,2), - (1/2,5,10,2), - (7/2,5,3,2), - - (8,0,0), - - (1,0,0), - (1,5,3), - (1,5,10), - (1,5,10), - (4/3,5,10), - (4/3,5,8), - (4/3,5,7), - - (1,0,0), - (1,5,1), - (1,5,8), - (1,5,8), - (4/3,5,8), - (4/3,5,8), - (4/3,5,10), - - (8,0,0), - - (1,0,0), - (5,5,3,2), - (2,5,10,2), -] - -melody3 = [ - (1,0,0), - (1,5,10), - (1/2,5,8,2/3), - (1/2,5,7,2/3), - (1/4,5,8,1/2), - (1/4,5,7,1/2), - (1/4,5,8,1/2), - (1/4,5,7,1/2), - (1,5,8), - (3,5,7,2), - - (1,5,3), - (1,4,10), - (1,5,1), - (5,5,7,2), - - (1/2,5,7), - (1/2,5,10), - (1/4,5,7), - (1/4,5,10), - (1/4,5,7), - (1/4,5,10), - (1,6,3), - (2,5,3,2), - (1/2,6,3), - (5/2,5,3,2), - - (1/2,5,10), - (1/2,5,8), - (1/2,5,7), - (1/2,5,8), - (1/2,5,7), - (1/2,5,3), - (1/2,4,10), - (1/2,5,1), - (1/2,5,0), - (1/2,4,10), - (1/2,4,8), - (1/2,4,10), - (1/2,5,3), - (1/2,5,7), - (1/2,5,3), - (1/2,5,10), - - (4/3,5,7), - (4/3,6,3), - (4/3,6,3), - (4/3,6,2), - (4/3,5,10), - (4/3,5,7), - - (3,5,5), - (2,5,7), - (2,5,8), - (1,6,1), - - (1,5,3), - (1,5,5), - (2,5,7), - (1,5,3), - (1,5,8), - (2,5,10), - - (3/2,6,0), - (3/2,6,1), - (5,6,3,2), -] - -outro = [ - (1,3,3), - (1,3,7), - (1,3,10), - (1,4,2), - (1,4,3), - (1,4,7), - (2,4,8), - - (1,3,1), - (1,3,5), - (1,3,8), - (1,4,0), - (1,4,1), - (1,4,5), - (2,4,8), - - (1,2,11), - (1,3,3), - (1,3,6), - (1,3,10), - (1.5,3,11), - (1.5,4,3), - (3,4,8), - - (1.5,2,8), - (1.5,3,0), - (2,3,3), - (16,3,7,2), -] - -def process(notes, start, speed=1, gain=1): - """ - Adds a list of notes to the music list - """ - t = start - for note in notes: - vol = 1 - if len(note) == 4: - vol = note[3] - start = min(t, t + note[0] / speed) - end = max(t, t + note[0] / speed) - music.append((start, end, note[1], note[2], vol * gain)) - t = end - - -# Process all lists of notes -music = [] -process(intro, 0, 4) -process(melody, 8, 4) -process(melody, 24, 4) -process(bass, 24, gain=1.5) -process(bass, 32, gain=1.5) -process(melody, 40, 4) -process(melody2, 40, 4) -process(bass, 40, gain=1.5) -process(bass, 48, gain=1.5) -process(melody, 56, 4) -process(melody3, 56, 4) -process(bass, 56, gain=1.5) -process(bass, 64, gain=1.5) -process(outro, 72, 4) -music.sort() - - -def freq(octave, step): - """ - Returns the frequency of a note - """ - return 55 * 2 ** (octave + step / 12 - 1) - - -def tone(f, t): - """ - Returns the intensity of a tone of frequency f sampled at time t - """ - # https://dsp.stackexchange.com/questions/46598/mathematical-equation-for-the-sound-wave-that-a-piano-makes - # https://youtu.be/ogFAHvYatWs?t=254 - # return int(2**13*(1+square(t, 440*2**(math.floor(5*t)/12)))) - # Y = sum([math.sin(2 * i * math.pi * t * f) * math.exp(-0.0004 * 2 * math.pi * t * f) / 2**i for i in range(1, 4)]) - # Y += Y * Y * Y - # Y *= 1 + 16 * t * math.exp(-6 * t) - w = 2 * math.pi * f - Y = 0.6 * math.sin(w * t) * math.exp(-0.001 * w * t) - Y += 0.2 * math.sin(2 * w * t) * math.exp(-0.001 * w * t) - Y += 0.05 * math.sin(3 * w * t) * math.exp(-0.001 * w * t) - Y += Y * Y * Y - Y *= 1 + 16 * t * math.exp(-6 * t) - return Y - - -def at(t): - """ - Returns the total intensity of music sampled at time t - """ - i = bisect.bisect(music, (t, 2**31)) - # This is actually pretty efficient ngl - # Because people usually don't have that many overlapping notes - ret = 0 - for j in range(max(i - 32, 0), i): - m = music[j] - # if m[0] + m[1] > t: - ret += m[4] * tone(freq(m[2], m[3]), t - m[0]) - return int(2**28 * ret) - - -# Print out music encoded in s32 to standard output -for i in range(0 * bitrate, 84 * bitrate): - sys.stdout.buffer.write(struct.pack('i', at(i / bitrate))) diff --git a/music.scm b/music.scm deleted file mode 100644 index b779eee..0000000 --- a/music.scm +++ /dev/null @@ -1,75 +0,0 @@ -(include "lib.scm") - -; https://musiclab.chromeexperiments.com/Song-Maker/song/6021372552937472 -(define (music t) - (apply + (map (lambda (x) - (apply (lambda (octave pitch start len) ((note (getfreq octave pitch) (/ start 2) (/ len 2)) t)) x)) - '( - ; opening notes - (1 11 0 .2) - (2 3 .2 .2) - (2 6 .4 .2) - (2 10 .6 2) - (1 9 3 .2) - (2 1 3.2 .2) - (2 4 3.4 .2) - (2 8 3.6 1) - (1 7 5 .2) - (1 11 5.2 .2) - (2 2 5.4 .2) - (2 6 5.6 1) - (1 4 7 .2) - (1 8 7.2 .2) - (1 11 7.4 .2) - (2 3 7.6 4.4) - - ; melody - (2 8 12 0.5) - (2 7 12.5 0.5) - (2 3 13 0.5) - (2 0 13.5 0.5) - (2 8 14 0.5) - (2 7 14.5 0.5) - (2 3 15 0.5) - (2 0 15.5 0.5) - (2 8 16 0.5) - (2 7 16.5 0.5) - (2 3 17 0.5) - (1 10 17.5 0.5) - (2 8 18 0.5) - (2 7 18.5 0.5) - (2 3 19 0.5) - (1 8 19.5 0.5) - (2 8 20 0.5) - (2 7 20.5 0.5) - (2 3 21 0.5) - (2 5 21.5 0.5) - (2 8 22 0.5) - (2 7 22.5 0.5) - (2 3 23 0.5) - (2 0 23.5 0.5) - (2 8 24 0.5) - (2 7 24.5 0.5) - (2 3 25 0.5) - (1 10 25.5 0.5) - (2 8 26 0.5) - (2 7 26.5 0.5) - (2 3 27 0.5) - (2 10 27.5 0.5) - (2 8 28 0.5) - (2 7 28.5 0.5) - (2 8 29 0.5) - (2 0 29.5 0.5) - (2 8 30 0.5) - (2 7 30.5 0.5) - (2 3 31 0.5) - (1 10 31.5 0.5) - (2 8 32 0.5) - (2 7 32.5 0.5) - (2 8 33 0.5) - (2 10 33.5 0.5) - (2 8 34 0.5) - (2 7 34.5 0.5) - (2 3 35 0.5) - (2 0 35.5 0.5) - )))) diff --git a/musiclib.nim b/musiclib.nim deleted file mode 100644 index c21cb95..0000000 --- a/musiclib.nim +++ /dev/null @@ -1,75 +0,0 @@ -import std/[algorithm, math, sugar, strformat, logging] - -type - OscFn* = proc (f: float, t: float): float - - Note* = tuple - len: float ## seconds - freq: float - vol: float - osc: OscFn - - ProcessedNote* = tuple - start: float ## absolute time in seconds - stop: float ## absolute time in seconds - freq: float - vol: float - osc: OscFn - -const HACK_LONGEST_NOTE = 16.0 - -func process*(music: var seq[ProcessedNote], notes: openArray[Note]; start_init: float, speed: float=1) = - ## Adds a list of notes to the music list - ## - ## `notes` sequence of notes with no rests in between - var start = start_init - var t = start - for note in notes: - assert note.len >= 0.0 - assert note.len <= HACK_LONGEST_NOTE, &"note too long: {note.len}" - start = t - let stop = t + note.len / speed - music &= (start, stop, note.freq, note.vol, note.osc) - t = stop - -func sortByStart*(music: var seq[ProcessedNote]) = - music.sort((a, b) => cmp(a.start, b.start)) - -func bisect(music: openArray[ProcessedNote], x: float): int = - ## Return the index where to insert item `x` in list `music` - ## - ## assumes `music` is sorted by `.start` - - music.lowerBound(x, (m, key) => cmp(m.start, key)) - -const GAIN_BIAS: float = pow(2.0, 31.0) - -proc at*(music: openArray[ProcessedNote], t: float): int32 = - ## Returns the total intensity of music sampled at time t - ## - ## assumes `music` is sorted by `.start` - - var i: int = music.bisect(t) - 1 - - var ret: float = 0 - - while i >= 0: - let m = music[i] - assert m.start <= t - if m.start + HACK_LONGEST_NOTE < t: - break - else: - ret += m.vol * m.osc(m.freq, t - m.start) - i -= 1 - - ret *= GAIN_BIAS - - # clip sample - if ret >= int32.high.float: - warn(&"audio clipping at t={t}") - int32.high - elif ret <= int32.low.float: - warn(&"audio clipping at t={t}") - int32.low - else: - int32(ret) diff --git a/yue.nim b/yue.nim new file mode 100644 index 0000000..51553a2 --- /dev/null +++ b/yue.nim @@ -0,0 +1,120 @@ +import std/[algorithm, math, sugar, strformat, logging] + +type + OscFn* = proc (f: float, t: float): float + + Note* = tuple + len: float ## seconds + freq: float + vol: float + osc: OscFn + + ProcessedNote* = tuple + start: float ## absolute time in seconds + stop: float ## absolute time in seconds + freq: float + vol: float + osc: OscFn + +const HACK_LONGEST_NOTE = 16.0 + +func process*(music: var seq[ProcessedNote], notes: openArray[Note]; start_init: float, speed: float=1) = + ## Adds a list of notes to the music list + ## + ## `notes` sequence of notes with no rests in between + var start = start_init + var t = start + for note in notes: + assert note.len >= 0.0 + assert note.len <= HACK_LONGEST_NOTE, &"note too long: {note.len}" + start = t + let stop = t + note.len / speed + music &= (start, stop, note.freq, note.vol, note.osc) + t = stop + +func sortByStart*(music: var seq[ProcessedNote]) = + music.sort((a, b) => cmp(a.start, b.start)) + +func bisect(music: openArray[ProcessedNote], x: float): int = + ## Return the index where to insert item `x` in list `music` + ## + ## assumes `music` is sorted by `.start` + + music.lowerBound(x, (m, key) => cmp(m.start, key)) + +const GAIN_BIAS: float = pow(2.0, 31.0) + +proc at*(music: openArray[ProcessedNote], t: float): int32 = + ## Returns the total intensity of music sampled at time t + ## + ## assumes `music` is sorted by `.start` + + var i: int = music.bisect(t) - 1 + + var ret: float = 0 + + while i >= 0: + let m = music[i] + assert m.start <= t + if m.start + HACK_LONGEST_NOTE < t: + break + else: + ret += m.vol * m.osc(m.freq, t - m.start) + i -= 1 + + ret *= GAIN_BIAS + + # clip sample + if ret >= int32.high.float: + warn(&"audio clipping at t={t}") + int32.high + elif ret <= int32.low.float: + warn(&"audio clipping at t={t}") + int32.low + else: + int32(ret) + +func osc_weird_pluck*(f, t: float): float = + # I got this as a bug + let w = 2 * PI * f + let wxt = w * t + let exp_swxt = math.exp(-0.001 * wxt) + let y0 = 0.6 * math.sin(wxt) + let y1 = 0.2 * math.sin(2 * wxt) + let y2 = 0.05 * math.sin(3 * wxt) + let y3 = (y0 + y1 + y2) * exp_swxt + let y4 = (1+y3)*y3*y3 # this line is different + y4 * (1 + 16 * t * math.exp(-6 * t)) + +func osc_piano*(f, t: float): float = + ## Returns the intensity of a tone of frequency f sampled at time t + ## t starts at 0 (note start) + # https://dsp.stackexchange.com/questions/46598/mathematical-equation-for-the-sound-wave-that-a-piano-makes + # https://youtu.be/ogFAHvYatWs?t=254 + let w = 2 * PI * f + let ewt = math.exp(-0.001 * w * t) + var Y = 0.6 * math.sin(w * t) * ewt + + 0.2 * math.sin(2 * w * t) * ewt + + 0.05 * math.sin(3 * w * t) * ewt + + let Y2 = Y * (Y * Y + 1) + Y2 * (1 + 16 * t * math.exp(-6 * t)) + +func osc_pulse*(f, t: float, phasedrift: float = 0.0): float = + let doublewidth = 1.0 / f + let width = doublewidth / 2 + let phase: float = (t + doublewidth * phasedrift) mod doublewidth + if phase < width: 1.0 else: -1.0 + +func osc_saw*(f,t:float, phasedrift: float = 0.0):float = + let doublewidth = 1.0 / f + let width = doublewidth / 2 + let phase: float = (t + doublewidth * phasedrift) mod doublewidth + if phase < width: + -1.0 + 2.0 * phase / width + else: + 1.0 - 2.0 * (phase - width) / (1.0 - width) + +func freq*(octave, step: float): float = + ## Returns the frequency of a note + 55 * pow(2, (octave + step / 12 - 1)) diff --git a/yue.py b/yue.py new file mode 100644 index 0000000..dda9e79 --- /dev/null +++ b/yue.py @@ -0,0 +1,69 @@ +import bisect +import math +import struct +import sys + +# Number of times to sample each second +bitrate = 44100 +music = [] + + +def process(notes, start, speed=1, gain=1, blend=0): + """ + Adds a list of notes to the music list + """ + t = start + for note in notes: + vol = 1 + if len(note) == 4: + vol = note[3] + start = min(t, t + note[0] / speed) + end = max(t, t + note[0] / speed) + music.append((start, end + 16 * int(blend), note[1], note[2], vol * gain)) + t = end + + +def freq(octave, step): + """ + Returns the frequency of a note + """ + return 55 * 2 ** (octave + step / 12 - 1) + + +def tone(f, t): + """ + Returns the intensity of a tone of frequency f sampled at time t + https://dsp.stackexchange.com/questions/46598/mathematical-equation-for-the-sound-wave-that-a-piano-makes + https://youtu.be/ogFAHvYatWs?t=254 + """ + w = 2 * math.pi * f + Y = 0.6 * math.sin(w * t) * math.exp(-0.001 * w * t) + Y += 0.2 * math.sin(2 * w * t) * math.exp(-0.001 * w * t) + Y += 0.05 * math.sin(3 * w * t) * math.exp(-0.001 * w * t) + Y += Y * Y * Y + Y *= 1 + 16 * t * math.exp(-6 * t) + return Y + + +def at(t): + """ + Returns the total intensity of music sampled at time t + This is actually pretty efficient ngl + Because people usually don't have that many overlapping notes + """ + i = bisect.bisect(music, (t, 2**31)) + ret = 0 + for j in range(max(i - 32, 0), i): + m = music[j] + if m[1] > t: + ret += m[4] * tone(freq(m[2], m[3]), t - m[0]) + return int(2**28 * ret) + + +def play(start, end): + """ + Print music from the start time to end time encoded in s32 to standard output + """ + music.sort() + for i in range(start * bitrate, end * bitrate): + sys.stdout.buffer.write(struct.pack("i", at(i / bitrate))) diff --git a/yue.scm b/yue.scm new file mode 100644 index 0000000..6b692ab --- /dev/null +++ b/yue.scm @@ -0,0 +1,37 @@ +(use-modules (ice-9 binary-ports)) + +; Bitrate is the number time to sample the music function each second +(define bitrate 8000) + +; Triangle wave with a period of 1 second +(define (tri t) + (let ((m (floor-remainder (+ t (/ 1 4)) 1))) + (if (< m 1/2) + (- (* 4 m) 1) + (- 3 (* 4 m))))) + +; Square wave with a period of 1 second +(define (square t) + (let ((m (floor-remainder t 1))) + (if (< m 0.5) 1 -1))) + +; Creates a note +(define (note freq start len) + (lambda (t) ( + if (or (< t start) (>= t (+ start len))) + 0 + (* 1/4 (tri (* t freq)))))) + +; Gets the frequency of a particular pitch +(define (getfreq octave pitch) + (* 55 (ash 1 octave) (expt 2 (/ pitch 12)))) + +; Get the music as a list sampled at the bitrate +(define (play t end) + (cons ((lambda (a) + (let ((b (modulo (inexact->exact (round (* (+ a 2) 32768))) 65536))) cons + (put-u8 (current-output-port) (modulo b 256)) + (put-u8 (current-output-port) (quotient b 256)))) (music t)) + (if (< t end) + (play (+ t (/ 1 bitrate)) end) + '()))) -- cgit v1.2.3-70-g09d2