From 0da46c8a33e7a9b61b564cce5c4c4526fe3d84dc Mon Sep 17 00:00:00 2001 From: Anthony Wang Date: Thu, 16 Mar 2023 22:59:41 -0400 Subject: [PATCH] Clean up code and rename to yue --- .gitignore | 2 - README.md | 15 ++---- music.nim => blend.nim | 61 ++--------------------- example.ogg => blend.ogg | Bin music.py => blend.py | 101 +++++++-------------------------------- music.scm => blend.scm | 4 +- keyboard.py | 39 +++------------ lambeat.scm | 17 ------- musiclib.nim => yue.nim | 45 +++++++++++++++++ yue.py | 69 ++++++++++++++++++++++++++ lib.scm => yue.scm | 15 ++++++ 11 files changed, 165 insertions(+), 203 deletions(-) delete mode 100644 .gitignore rename music.nim => blend.nim (71%) rename example.ogg => blend.ogg (100%) rename music.py => blend.py (54%) rename music.scm => blend.scm (97%) delete mode 100644 lambeat.scm rename musiclib.nim => yue.nim (57%) create mode 100644 yue.py rename lib.scm => yue.scm (53%) 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/music.nim b/blend.nim similarity index 71% rename from music.nim rename to blend.nim index 30c256c..df87253 100644 --- a/music.nim +++ b/blend.nim @@ -1,56 +1,6 @@ -import musiclib, std/[math, sugar] +import yue, std/[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)) +const GAIN_NORMAL = 0.22 var osc: OscFn = (f, t: float) => 0.0 @@ -58,10 +8,6 @@ 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 = [ @@ -334,6 +280,9 @@ 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)) diff --git a/example.ogg b/blend.ogg similarity index 100% rename from example.ogg rename to blend.ogg diff --git a/music.py b/blend.py similarity index 54% rename from music.py rename to blend.py index 74197b6..b1c9df8 100644 --- a/music.py +++ b/blend.py @@ -1,10 +1,4 @@ -import bisect -import math -import struct -import sys - -# Number of times to sample each second -bitrate = 44100 +import yue intro = [ (1,3,3), @@ -250,81 +244,18 @@ outro = [ (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))) +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/music.scm b/blend.scm similarity index 97% rename from music.scm rename to blend.scm index b779eee..1824ca1 100644 --- a/music.scm +++ b/blend.scm @@ -1,4 +1,4 @@ -(include "lib.scm") +(include "yue.scm") ; https://musiclab.chromeexperiments.com/Song-Maker/song/6021372552937472 (define (music t) @@ -73,3 +73,5 @@ (2 3 35 0.5) (2 0 35.5 0.5) )))) + +(play 6 100) 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/musiclib.nim b/yue.nim similarity index 57% rename from musiclib.nim rename to yue.nim index c21cb95..51553a2 100644 --- a/musiclib.nim +++ b/yue.nim @@ -73,3 +73,48 @@ proc at*(music: openArray[ProcessedNote], t: float): int32 = 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/lib.scm b/yue.scm similarity index 53% rename from lib.scm rename to yue.scm index 36a65c4..6b692ab 100644 --- a/lib.scm +++ b/yue.scm @@ -1,3 +1,8 @@ +(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))) @@ -20,3 +25,13 @@ ; 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) + '())))