Created
June 10, 2024 15:46
-
-
Save fuxoft/af9b7c40df13910cbd6ea85730c7ac01 to your computer and use it in GitHub Desktop.
Audio generator using coroutines
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
# Sound device for Chromebook: hw:CARD=SoundCard,DEV=0 | |
(def sample-rate 48000) | |
(defn is-generator? [x] | |
(and (dictionary? x) (x :is-generator))) | |
(defn- msg-format [args] | |
(string/join | |
(map | |
(fn [thing] | |
(if (= :string (type thing)) | |
thing | |
(if (is-generator? thing) | |
(string "[" (thing :name) "]") | |
(string/format "%p" thing)))) args))) | |
(defn debug [& args] | |
(print "DBG> " (msg-format args))) | |
(defn ferror [& args] | |
(error (msg-format args))) | |
(defn interpolate [from1 from2 to1 to2 x] | |
(+ to1 (* (- x from1) (/ (- to2 to1) (- from2 from1))))) | |
(def unique-number-channel (ev/thread-chan)) | |
(ev/spawn-thread | |
(for n 1 math/inf | |
(ev/give unique-number-channel n))) | |
(defn unique-number [] | |
(ev/take unique-number-channel)) | |
(def math/2pi (* 2 math/pi)) | |
(def math/root-2-12 (math/pow 2 (/ 1 12))) | |
(defn note-to-step [note] | |
"Converts midi note number to step size | |
(i.e. what fraction of one cycle is advanced during the rendering of one sample)." | |
# note 69 = 440 Hz. This formula is hardcoded for 48000 Hz sample rate!!! | |
(* 0.00017033 (math/exp2 (/ note 12)))) | |
(defn float-to-word | |
"Converts float between -1 and 1 to two bytes." | |
[float] | |
(assert (and (<= float 1) (>= float -1)) "Audio clipping.") | |
(def signed (math/round (* 32767 float))) | |
(def bytes (if (neg? signed) (+ 0x10000 signed) signed)) | |
(def upper (brshift bytes 8)) | |
(def lower (band 0xff bytes)) | |
# (print lower " " upper) | |
(def buf (buffer/push @"" lower upper)) | |
#(pp buf) | |
buf) | |
(def aplay-channel (ev/chan)) | |
(def fname "out.raw") | |
(ev/spawn | |
(file/close (file/open fname :w)) | |
# 16 bit little endian, 48000, stereo | |
(def args @["aplay" "-f" "S16_LE" "-c" "2" "-r" (string sample-rate)]) | |
# Command line argument "chrome" switches output to Chrome sound device | |
(if (= "chrome" (get (dyn *args*) 1)) | |
(array/insert args 1 "-Dhw:CARD=SoundCard,DEV=0")) | |
(def proc (os/spawn args :px {:in :pipe})) | |
(def stream (assert (proc :in))) | |
(forever | |
(def word (float-to-word (ev/take aplay-channel))) | |
# First comes left channel, then the right | |
(def f (file/open fname :ab)) | |
(file/write f word) | |
(file/close f) | |
(ev/write stream word))) | |
(defn- table/filter [fun tbl] | |
(def result @{}) | |
(eachp [k v] tbl | |
(if (fun v) | |
(put result k v))) | |
result) | |
(defn new-generator | |
[coro0 &opt name] | |
(assert (= :fiber (type coro0))) | |
(def obj @{:is-generator true}) | |
(def cor | |
(coro | |
(def self obj) #todo | |
(forever | |
(when (empty? (self :inputs)) | |
(yield :bye) | |
(error "Should never reach here!")) | |
(var min-wait math/inf) | |
(each inp (self :inputs) | |
(if (< (inp :wait) min-wait) | |
(set min-wait (inp :wait)))) | |
(if (neg? min-wait) (error "Negative :wait value.")) | |
(if (zero? min-wait) | |
#Some input(s) can execute | |
(do | |
# Find all inputs with lowest :wait | |
(def lowest-waits | |
(table/filter | |
(fn [inp] (= (inp :wait) min-wait)) (self :inputs))) | |
(assert (not (empty? lowest-waits))) | |
(def current-key | |
(do | |
(var [found highest] [false math/-inf]) | |
(eachp [k v] lowest-waits | |
(when (> (v :priority) highest) | |
(set found k) | |
(set highest (v :priority)))) | |
found)) | |
(assert current-key) | |
(def current-coro (get-in self [:inputs current-key :coroutine])) | |
(def incoming (resume current-coro)) | |
#(debug self " received " incoming " from its " current-key) | |
(match incoming | |
nil | |
(ferror self "'s " current-key " has terminated without yielding :bye.") | |
:bye | |
# Input coro has finished, disconnect this input | |
(put (self :inputs) current-key nil) | |
[:do fun] | |
(fun self) | |
[:set-stream cor] | |
(do | |
#(debug "Output stream for " current-key " in " self " set to " cor) | |
(put-in self [:inputs current-key :stream] cor)) | |
[:wait time] | |
(put-in self [:inputs current-key :wait] time) | |
(ferror "Invalid incoming directive for " self ": " incoming))) | |
#All inputs are waiting for at least <min-wait> ticks | |
(do | |
#(def stream-coro (self :stream-coroutine)) | |
(def streams @{}) | |
(eachp [k v] (self :inputs) | |
(if (v :stream) | |
(put streams k v))) | |
#(debug self " inputs: " (self :inputs)) | |
#(debug "Streams: " streams) | |
(if (empty? streams) | |
(comment) | |
(if (self :combine-streams) | |
(:combine-streams self streams) | |
(ferror "There are " (length streams) " active streams in " self | |
" but :combine-streams is undefined."))) | |
(yield [:wait min-wait]) | |
# (debug self " has waited for " min-wait " ticks.") | |
(eachp [k v] (self :inputs) | |
(assert (>= (v :wait) min-wait)) | |
(put v :wait (- (v :wait) min-wait)))))))) | |
#(def obj (table/setproto @{} proto)) | |
(put obj :output cor) | |
(default name "Generator") | |
(put obj :name (string name " #" (unique-number))) | |
(def inputs @{}) | |
(put obj :inputs inputs) | |
(put inputs :init @{:priority 0 :coroutine coro0 :wait 0}) | |
obj) | |
# (envelope [[1 0] [0.5 0.2] [0.5 0.8] [0 1] :hold] ... returns coro | |
# Unless there is :hold at the end, it REPEATS FOREVER | |
(defn envelope-coroutine | |
[env] | |
(coro | |
(forever | |
(var value 0) | |
(each item env | |
(match item | |
:hold | |
(forever (yield value)) | |
[new-value time] | |
(do | |
(assert (>= time 0)) | |
(if (zero? time) | |
(set value new-value) | |
(let [step (/ (- new-value value) time)] | |
(if (zero? step) | |
(repeat time | |
(yield value)) | |
(do | |
(repeat time | |
(yield value) | |
(set value (+ value step))) | |
(set value new-value)))))) | |
(ferror "Invalid envelope pair: item")))))) | |
(defn oscillator [note-n] | |
(coro | |
(def env (envelope-coroutine [[0.2 0] [0.07 20000] :hold])) | |
(def mo (envelope-coroutine [[0.1 0] [0.5 10000] [0.1 10000]])) | |
(def step (note-to-step note-n)) | |
(var position 0) | |
(forever | |
#(yield (* (resume env) (math/sin (* position math/2pi)))) | |
#(yield (* (dec (* 2 position)) (resume env))) # saw | |
#(yield (* (resume env) (if (< position 0.5) (dec (* 4 position)) (+ 3 (* -4 position))))) #triangle | |
(yield (if (< position (resume mo)) (- (resume env)) (resume env))) #mod | |
(set position (% (+ position step) 1)) | |
#(print (string (self :name) " " position)) | |
#(set count (if (> count 1) (- count 2) count)) | |
))) | |
(defn gen-do [fun] | |
(yield [:do fun])) | |
(defn gen-bye [] | |
(yield :bye)) | |
# Sets the audio stream for THIS WHOLE GENERATOR (not just this coroutine) in the parent generator. | |
(defn gen-my-stream [cor] | |
(yield [:do (fn [self] | |
(assert (fiber? cor)) | |
(yield [:set-stream cor]))])) | |
(defn gen-wait [time] | |
(yield [:wait time])) | |
(defn gen-wait-forever [] | |
(yield [:wait math/inf])) | |
(defn gen-add-input [thing &opt name?] | |
(gen-do | |
(fn [self] | |
(def [name cor] | |
(if (is-generator? thing) | |
[(string "<Gen:" (thing :name) ">") (thing :output)] | |
[(or name? (string "Coro#" (unique-number))) thing])) | |
(assert (= :fiber (type cor))) | |
(def name2 | |
(if (get-in self [:inputs name]) | |
(string name " " (unique-number)) | |
name)) | |
#(debug "Connected input " name2 " to " self) | |
(put-in self [:inputs name2] @{:coroutine cor :priority 0 :wait 0})))) | |
(defn play-note [note-n len] | |
(def release 10000) | |
(def note-gen | |
(new-generator | |
(coro | |
(def wavegen (oscillator note-n)) | |
(def wavegen2 (oscillator (- note-n 0.01))) | |
(gen-my-stream | |
(coro | |
(repeat len | |
(yield (+ (resume wavegen) (resume wavegen2)))) | |
(def fader (envelope-coroutine [[1 0] [0 release]])) | |
# (debug "release " release) | |
(repeat release | |
(def f (resume fader)) | |
(yield (* f (+ (resume wavegen) (resume wavegen2))))) | |
(forever (yield 0)))) | |
(gen-wait (+ len release)) | |
(gen-bye)) | |
"Note")) | |
(gen-add-input note-gen)) | |
(def mixer1 | |
(new-generator | |
(coro | |
# Reverb and filter test | |
(def buf @[]) | |
(def rng (math/rng)) | |
(repeat 65536 | |
(array/push buf 0)) | |
(var rev-pointer 0) | |
(defn rev-add [index value multiply] | |
(def ind (band 65535 (+ rev-pointer index))) | |
(put buf ind (+ (* multiply value) (buf ind)))) | |
(var s0 0) | |
(def mod (envelope-coroutine [[1 0] [0.01 300000] [1 300000]])) | |
(defn reverb [input] | |
(coro | |
(forever | |
(def inp (resume input)) | |
(def out (+ inp (buf rev-pointer))) | |
# (rev-add 7941 out 0.3) | |
# (rev-add 6141 out 0.3) | |
# (rev-add 5141 out 0.3) | |
#(def out2 (+ (* (resume mod) (- out s0)) s0)) | |
(yield out) | |
#(set s0 out2) | |
(put buf rev-pointer 0) | |
(set rev-pointer (band 65535 (inc rev-pointer)))))) | |
(gen-do | |
# Initialize mixer | |
(fn [self] | |
# (debug "Setting combine-streams hook.") | |
(put self :combine-streams | |
(fn [self streams] | |
# (debug "Mixing streams!!!!") | |
(def mixed-stream | |
(if (empty? streams) | |
(coro (forever (yield 0))) | |
(if (= 1 (length streams)) | |
(assert (get-in streams [(next streams) :stream])) | |
(coro | |
(def mix | |
(map (fn [st] (get st :stream)) streams)) | |
(forever | |
(yield | |
(reduce (fn [acc co] (+ acc (resume co))) 0 mix))))))) | |
(assert mixed-stream) | |
# (debug "Streams mixed into " mixed-stream) | |
(gen-my-stream (reverb mixed-stream)))))) | |
# End of mixer init | |
(forever | |
(each base [60 62 64] | |
(each note [0 4 7] | |
(play-note (+ base note) 30000) | |
(gen-wait 18000))))) | |
"Mixer")) | |
(def master-out | |
(new-generator | |
(coro | |
(gen-add-input mixer1) | |
(gen-wait-forever)) | |
"Master out")) | |
(var st nil) | |
(var cnt 0) | |
(def time0 (os/clock)) | |
(forever | |
(def incoming (resume (master-out :output))) | |
# (debug "Incoming from master-out: " incoming) | |
(match incoming | |
[:set-stream str] | |
(set st str) | |
[:wait samples] | |
(do | |
(repeat samples | |
(def mono-sample (resume st)) | |
(++ cnt) | |
# (print cnt " " mono-sample) | |
(when (> cnt 48000000000000) | |
(print "10 seconds rendered in " (- (os/clock) time0)) | |
(os/exit)) | |
#(print "mono sample " mono-sample) | |
(ev/give aplay-channel mono-sample) | |
(ev/give aplay-channel mono-sample) | |
#23 notes, took 1.29-1.34 secs | |
)))) |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment