Browse Source

Detect input pitch using circular buffer

* Added (bad) circular buffer set-up for reading input audio
* Pitch will be very noisily displayed on screen via the monkey's
vertical position
* Changed scripts slightly to use hot-reloading
Macoy Madson 9 months ago
  1. 4
  2. 4
  3. 10
  4. 227


@ -1,5 +1,5 @@
echo "\n\nVocal Game (reloadable)\n\n"
./Dependencies/cakelisp/bin/cakelisp --verbose-processes \
test/src/Config_Linux.cake test/src/MakeHotReload.cake test/src/VocalGame.cake || exit $?
./Dependencies/cakelisp/bin/cakelisp \
test/src/Config_Linux.cake test/src/MakeHotReload.cake test/src/VocalGame.cake || exit $?


@ -20,10 +20,10 @@ echo "\n\nVocal Game (hot reload)\n\n"
./Dependencies/cakelisp/bin/cakelisp --verbose-processes test/src/Config_Linux.cake test/src/MakeHotReload.cake test/src/VocalGame.cake || exit $?
echo "\n\nLoader\n\n"
./Dependencies/cakelisp/bin/cakelisp test/src/Config_Linux.cake test/src/Loader.cake || exit $?
./Dependencies/cakelisp/bin/cakelisp --execute test/src/Config_Linux.cake test/src/Loader.cake || exit $?
echo "\n\nVocal Game (no reload)\n\n"
./Dependencies/cakelisp/bin/cakelisp --execute \
./Dependencies/cakelisp/bin/cakelisp \
test/src/Config_Linux.cake test/src/NoHotReload.cake test/src/VocalGame.cake || exit $?


@ -21,6 +21,16 @@
;; Must also include in source file so HANDMADE_MATH_IMPLEMENTATION is output here
&with-defs "HandmadeMath.h")
;; Fundamental
(defun interpolate-range (start-A float end-A float
start-B float end-B float B-value float
&return float)
(var interpolate-to float (/ (- B-value start-B) (- end-B start-B)))
(return (+ (* interpolate-to (- end-A start-A)) start-A)))
;; Trigonometry


@ -12,16 +12,67 @@
"<math.h>" "<stdio.h>" "<string.h>")
;; These are read and written to from different threads (currently, without locking)
(var g-audio-is-g-initialized bool false)
(var g-audio-is-initialized bool false)
(var g-audio-is-recording bool false)
(var g-audio-input-buffer (* Uint8) null)
(var g-audio-input-buffer-size int 0)
(var g-audio-input-write-head int 0)
(var g-audio-input-read-head int 0)
(var g-audio-looper-buffer (* Uint8) null)
(var g-audio-looper-buffer-size int 0)
(var g-audio-looper-write-head int 0)
(var g-audio-looper-read-head int 0)
(var g-audio-output-device SDL_AudioDeviceID 0)
(var g-audio-input-device SDL_AudioDeviceID 0)
(var g-audio-output-device-spec SDL_AudioSpec)
(var g-audio-input-device-spec SDL_AudioSpec)
;; (var g-enable-audio bool false)
(var g-enable-audio bool true)
(defstruct-local circular-buffer
data (* Uint8)
size int
read-head int
write-head int)
(defun-local circular-buffer-initialize (buffer (* circular-buffer) desired-size int)
(set (path buffer > data) (type-cast (calloc desired-size (sizeof Uint8)) (* Uint8)))
(set (path buffer > size) desired-size)
(set (path buffer > read-head) 0)
;; To get things started, write-head needs to not equal read
(set (path buffer > write-head) 0))
(defun-local circular-buffer-increment-read (buffer (* circular-buffer))
(incr (path buffer > read-head))
(when (>= (path buffer > read-head) (path buffer > size))
(set (path buffer > read-head) 0)))
(defun-local circular-buffer-increment-write (buffer (* circular-buffer))
(incr (path buffer > write-head))
(when (>= (path buffer > write-head) (path buffer > size))
(set (path buffer > write-head) 0)))
;; TODO: Read in batches
;; Returns false if no new samples, i.e. read head caught up with write head
(defun-local circular-buffer-read (buffer (* circular-buffer) sample-out (* Uint8) &return bool)
(unless (and buffer (!= (path buffer > read-head) (path buffer > write-head)))
;; Waiting for new samples to be written
(return false))
(set (deref sample-out) (at (path buffer > read-head) (path buffer > data)))
(circular-buffer-increment-read buffer)
(return true))
;; Returns false if write-head = read head, and sample will not be written
(defun-local circular-buffer-write (buffer (* circular-buffer) sample-in Uint8 &return bool)
;; TODO This isn't quite right. Write head should stop right before read instead of on it
(circular-buffer-increment-write buffer)
(unless (and buffer (!= (path buffer > write-head) (path buffer > read-head)))
;; Write head reached read head. Samples will be dropped!
(return false))
(set (at (path buffer > write-head) (path buffer > data)) sample-in)
(return true))
(var g-audio-pitch-input-buffer circular-buffer)
(var g-audio-input-samples-lost int 0)
;; Outputs to a format that can be plotted with gnuplot:
;; gnuplot> plot 'out.dat' with lines
(defun-local audio-dump-recorded-buffer (output-filename (* (const char))
@ -38,21 +89,21 @@
(incr i))
(fclose dest-file))
(defun-local audio-input-buffer-initialize ()
(set g-audio-input-buffer-size 44100)
(set g-audio-input-buffer (type-cast (calloc g-audio-input-buffer-size (sizeof Uint8))
(* Uint8)))
(defun-local audio-looper-buffer-initialize ()
(set g-audio-looper-buffer-size 44100)
(set g-audio-looper-buffer (type-cast (calloc g-audio-looper-buffer-size (sizeof Uint8))
(* Uint8)))
(var i int 0)
(while (< i g-audio-input-buffer-size)
(while (< i g-audio-looper-buffer-size)
;; TODO: Use silence value from SDL audio spec
(set (at i g-audio-input-buffer) 127)
(set (at i g-audio-looper-buffer) 127)
(incr i)))
(defun-local audio-input-buffer-destroy ()
(free g-audio-input-buffer)
(set g-audio-input-buffer null))
(defun-local audio-looper-buffer-destroy ()
(free g-audio-looper-buffer)
(set g-audio-looper-buffer null))
(defun-local audio-output-callback (userdata (* void) stream (* Uint8) stream-length int)
(defun-local audio-looper-output-callback (userdata (* void) stream (* Uint8) stream-length int)
(ZoneScopedN "Audio output")
;; (printf "Audio len %d\n" stream-length)
(var-static up bool false)
@ -79,9 +130,9 @@
(if g-audio-is-recording
(set mono-sample 127) ;; Silence
(block ;; Else, play the recording
(var recording-index int (+ (/ i num-channels) g-audio-input-read-head))
(set recording-index (mod recording-index g-audio-input-buffer-size))
(set mono-sample (at recording-index g-audio-input-buffer))))
(var recording-index int (+ (/ i num-channels) g-audio-looper-read-head))
(set recording-index (mod recording-index g-audio-looper-buffer-size))
(set mono-sample (at recording-index g-audio-looper-buffer))))
;; Channels are interleaved, e.g. LRLR, not LLRR
(var channel int 0)
@ -90,14 +141,15 @@
;; (printf "[%d][%d] %d\n" (+ i channel) channel (at i stream))
(incr channel))
(set i (+ i num-channels)))
(set g-audio-input-read-head
(mod (+ g-audio-input-read-head samples-per-channel)
(set g-audio-looper-read-head
(mod (+ g-audio-looper-read-head samples-per-channel)
;; Note: If input is sampled at a different rate, playback will be at a lower pitch. Use SDL's
;; audio conversion functions to handle that properly
(defun-local audio-input-callback (userdata (* void) stream (* Uint8) stream-length int)
(defun-local audio-looper-input-callback (userdata (* void) stream (* Uint8) stream-length int)
(ZoneScopedN "Audio input")
(unless g-audio-is-recording
;; (printf "received audio %d %d %d\n"
@ -106,10 +158,23 @@
;; (at (- stream-length 1) stream))
(var i int 0)
(while (< i stream-length)
(set (at g-audio-input-write-head g-audio-input-buffer) (at i stream))
(if (= g-audio-input-write-head (- g-audio-input-buffer-size 1))
(set g-audio-input-write-head 0)
(incr g-audio-input-write-head))
(set (at g-audio-looper-write-head g-audio-looper-buffer) (at i stream))
(if (= g-audio-looper-write-head (- g-audio-looper-buffer-size 1))
(set g-audio-looper-write-head 0)
(incr g-audio-looper-write-head))
(incr i)))
(defun-local audio-pitch-input-callback (userdata (* void) stream (* Uint8) stream-length int)
(ZoneScopedN "Audio input")
(var i int 0)
(while (< i stream-length)
(var sample-written bool
(circular-buffer-write (addr g-audio-pitch-input-buffer) (at i stream)))
;; Buffer is full; has the read thread gotten behind?
(unless sample-written
(incr g-audio-input-samples-lost)
(incr i)))
;; Allocates both names and array
@ -152,7 +217,7 @@
(ZoneScopedN "Audio intialization")
;; This will take a while. Make sure we aren't marked as ready while preparing
(set g-audio-is-g-initialized false)
(set g-audio-is-initialized false)
(var audio-driver (* (const char)) (SDL_GetCurrentAudioDriver))
@ -195,7 +260,7 @@
(set (field desired-spec format) AUDIO_U8)
(set (field desired-spec channels) 2) ;; 1 = Mono 2 = Stereo
(set (field desired-spec samples) 512)
(set (field desired-spec callback) audio-output-callback)
(set (field desired-spec callback) audio-looper-output-callback)
(unless (< macoy-output-device-index num-devices)
(printf "Could not choose device %d\n" macoy-output-device-index)
(sdl-audio-free-device-list devices num-devices)
@ -228,7 +293,8 @@
(set (field desired-spec format) AUDIO_U8)
(set (field desired-spec channels) 1) ;; 1 = Mono 2 = Stereo
(set (field desired-spec samples) 512)
(set (field desired-spec callback) audio-input-callback)
;; (set (field desired-spec callback) audio-looper-input-callback)
(set (field desired-spec callback) audio-pitch-input-callback)
(var capture-devices (* (* (const char))) null)
(var num-capture-devices int (sdl-audio-get-devices (addr capture-devices) true))
@ -280,7 +346,7 @@
(when output-device-out (set (deref output-device-out) output-device-id))
(when input-device-out (set (deref input-device-out) input-device-id))
(set g-audio-is-g-initialized true)
(set g-audio-is-initialized true)
(return (and output-device-id input-device-id)))
@ -290,10 +356,6 @@
(when (>= input-device 2) (SDL_CloseAudioDevice input-device)))
(var g-window (* SDL_Window) null)
(var g-audio-output-device SDL_AudioDeviceID 0)
(var g-audio-input-device SDL_AudioDeviceID 0)
(var g-audio-output-device-spec SDL_AudioSpec)
(var g-audio-input-device-spec SDL_AudioSpec)
(var g-monkey-mesh mesh-handle)
(var g-monkey-node scene-node)
@ -346,7 +408,9 @@
;; Audio needs to be re-g-initialized due to reload removing callbacks
(when g-enable-audio
;; Allow one second of no reads before we stop dropping samples
(circular-buffer-initialize (addr g-audio-pitch-input-buffer) 44100)
;; Audio initialization is extremely slow (~2-3 seconds on my machine). Let's do it on a
;; separate thread to keep start up responsive
(var audio-init-thread-handle (* SDL_Thread)
@ -401,8 +465,6 @@
(TracyCZoneEnd startup-zone)
(var pitch-hz float 0.f)
;; Main loop
(while (not exit-reason)
(FrameMarkNamed "Frame")
@ -441,9 +503,41 @@
(when (at SDL_SCANCODE_DOWN currentKeyStates)
(set (vec-y delta-position) (- (vec-y delta-position) move-speed)))
"Audio read pitch input"
(var num-samples-read int 0)
(var sample Uint8 0)
(var pitch-buffer-size int 512)
(var pitch-buffer ([] 512 Uint8))
(while (circular-buffer-read (addr g-audio-pitch-input-buffer) (addr sample))
;; TODO use last 512, not first 512
(when (< num-samples-read pitch-buffer-size)
(set (at num-samples-read pitch-buffer) sample))
(incr num-samples-read))
;; (printf "Read %d samples\n" num-samples-read)
(when g-audio-input-samples-lost
(printf "warning: lost %d input samples\n" g-audio-input-samples-lost)
(set g-audio-input-samples-lost 0))
(when (>= num-samples-read pitch-buffer-size)
"Detect pitch"
(var pitch-hz float
(audio-detect-pitch pitch-buffer pitch-buffer-size
(field g-audio-output-device-spec freq)))
(printf "Pitch (hz): %f\n" pitch-hz)
(set (vec-y delta-position)
;; On-camera range
0.f 7.f
;; Human vocal range is ~100hz to ~5000hz
;; 100.f 2000.f ;; Harmonica
100.f 700.f ;; My voice
(var prev-recording-value bool g-audio-is-recording)
(audio-set-recording-state (at SDL_SCANCODE_SPACE currentKeyStates))
(when (and g-audio-is-g-initialized
(when (and g-audio-is-initialized
(!= prev-recording-value g-audio-is-recording)
(not g-audio-is-recording))
(ZoneScopedN "Audio conversion")
@ -461,7 +555,7 @@
(field g-audio-output-device-spec format)
1 ;; (field g-audio-output-device-spec channels) ;; We convert to stereo in playback callback
(field g-audio-output-device-spec freq))
(set (field audio-conversion-settings len) g-audio-input-buffer-size)
(set (field audio-conversion-settings len) g-audio-looper-buffer-size)
;; TODO: Does the buffer really nead to be 8x the src? Am I doing something wrong with my len?
;; Could be conversion to float then 2x that for resampling = 4x * 2x = 8x
(var converted-buffer-size-bytes int
@ -475,8 +569,8 @@
(type-cast (malloc converted-buffer-size-bytes) (* Uint8)))
(scope ;; Copy src to in-place conversion buffer
(var i int 0)
(while (< i g-audio-input-buffer-size)
(set (at i (field audio-conversion-settings buf)) (at i g-audio-input-buffer))
(while (< i g-audio-looper-buffer-size)
(set (at i (field audio-conversion-settings buf)) (at i g-audio-looper-buffer))
(incr i)))
(unless (= 0 (SDL_ConvertAudio (addr audio-conversion-settings)))
(set exit-reason "Failed to convert audio")
@ -491,16 +585,16 @@
(scope ;; Copy back to destination buffer. Note: this could drop samples, because it should
;; use the converted buffer size instead of the fixed array size!
(var i int 0)
(while (< i g-audio-input-buffer-size)
(set (at i g-audio-input-buffer)
(while (< i g-audio-looper-buffer-size)
(set (at i g-audio-looper-buffer)
(at i (type-cast (field audio-conversion-settings buf) (* Uint8))))
;; TODO: If my sample rate is higher when recorded, that means I need to record more
;; samples and play back fewer. Because I use the same buffer, it means I don't have
;; enough samples at the lower playback rate, and start hitting buffer workspace.
;; Zero them out for now. I should record with enough samples to make it exactly fit
;; after conversion
(when (>= i (- g-audio-input-buffer-size 3900))
(set (at i g-audio-input-buffer) 127))
(when (>= i (- g-audio-looper-buffer-size 3900))
(set (at i g-audio-looper-buffer) 127))
(incr i)))
(free (field audio-conversion-settings buf)))
@ -508,8 +602,8 @@
;; Note: peak in both low and high, never greater than range / 2
(var highest-peak Uint8 0)
(var i int 0)
(while (< i g-audio-input-buffer-size)
(var current-value int (- (type-cast (at i g-audio-input-buffer) int) 127))
(while (< i g-audio-looper-buffer-size)
(var current-value int (- (type-cast (at i g-audio-looper-buffer) int) 127))
(when (< highest-peak (abs current-value))
(set highest-peak (abs current-value)))
(incr i))
@ -526,23 +620,19 @@
(set scaling-factor scaling-limit))
(when scaling-factor
(set i 0)
(while (< i g-audio-input-buffer-size)
(var current-value int (- (type-cast (at i g-audio-input-buffer) int) 127))
(set (at i g-audio-input-buffer) (type-cast
;; Map back to 0-255
(+ 127.f
(* current-value scaling-factor))
(while (< i g-audio-looper-buffer-size)
(var current-value int (- (type-cast (at i g-audio-looper-buffer) int) 127))
(set (at i g-audio-looper-buffer) (type-cast
;; Map back to 0-255
(+ 127.f
(* current-value scaling-factor))
(incr i)))))
"Detect pitch"
(set pitch-hz
(audio-detect-pitch g-audio-input-buffer g-audio-input-buffer-size
(field g-audio-output-device-spec freq)))
(printf "Pitch (hz): %f\n" pitch-hz)))
(when (!= 0 pitch-hz)
(printf "Pitch (hz): %f\n" pitch-hz))
(var pitch-hz float
(audio-detect-pitch g-audio-looper-buffer g-audio-looper-buffer-size
(field g-audio-output-device-spec freq)))))
(var current-counter-ticks Uint64 (SDL_GetPerformanceCounter))
(var frame-diff-ticks Uint64 (- current-counter-ticks last-frame-perf-count))
@ -552,16 +642,17 @@
;; Visualize audio playback
(var audio-read-head-to-world float (* 20.f
(/ g-audio-input-read-head
(type-cast g-audio-input-buffer-size float))))
(/ g-audio-looper-read-head
(type-cast g-audio-looper-buffer-size float))))
(var audio-volume-to-world float (* 7.f
(/ (- (at g-audio-input-read-head g-audio-input-buffer) 127)
(/ (- (at g-audio-looper-read-head g-audio-looper-buffer) 127)
(var audio-pos vec3 (array audio-read-head-to-world 0.f audio-volume-to-world))
;; (var final-pos vec3 (vec3-scale-add-vec3 delta-position delta-time tracker-position))
(var final-pos vec3 (vec3-scale-add-vec3 delta-position delta-time
(vec3-add tracker-position audio-pos)))
;; (var final-pos vec3 (vec3-scale-add-vec3 delta-position delta-time
;; (vec3-add tracker-position audio-pos)))
(var final-pos vec3 delta-position)
(when g-audio-is-recording
(set (vec-y final-pos) (+ (vec-y final-pos) -2.f)))
(ogre-node-set-position (addr g-monkey-node) (vec-xyz final-pos))
@ -586,8 +677,8 @@
(SDL_CreateThread audio-shutdown-thread "AudioShutdown" null))
(audio-dump-recorded-buffer "out.dat"
g-audio-input-buffer g-audio-input-buffer-size)
g-audio-looper-buffer g-audio-looper-buffer-size)
"Ogre shutdown"