You can not select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
691 lines
29 KiB
691 lines
29 KiB
(set-cakelisp-option executable-output "VocalGame")
|
|
|
|
(import "Options.cake" "ComptimeHelpers.cake" "AssetBuilder.cake")
|
|
|
|
(import "Ogre.cake" "OgreInitialize.cake" "SDL.cake" "Math.cake" "Tracy.cake" "Aubio.cake")
|
|
|
|
;; TODO: Should this happen automatically, because import automatically adds current working dir?
|
|
;; Should it add working dir?
|
|
(add-c-search-directory-module ".")
|
|
|
|
(c-import "SDL.h" "SDL_syswm.h" "SDL_timer.h"
|
|
"<math.h>" "<stdio.h>" "<string.h>")
|
|
|
|
(var g-window (* SDL_Window) null)
|
|
|
|
(var g-monkey-mesh mesh-handle)
|
|
(var g-monkey-node scene-node)
|
|
(var g-monkey-anim-handle OgreAnimationHandle)
|
|
(var g-light-node scene-node)
|
|
(var g-pitch-particles particle-system-handle)
|
|
|
|
(var g-reload-sentinel int 2)
|
|
|
|
(var g-initialized bool false)
|
|
|
|
;; These are read and written to from different threads (currently, without locking)
|
|
(var g-audio-is-initialized bool false)
|
|
(var g-audio-is-recording bool false)
|
|
|
|
(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))
|
|
buffer (* Uint8) buffer-size int)
|
|
(ZoneScopedN "Audio dump recording")
|
|
(var dest-file (* FILE) (fopen output-filename "w"))
|
|
(unless dest-file
|
|
(printf "Could not open file to write data\n")
|
|
(return))
|
|
|
|
(var i int 0)
|
|
(while (< i buffer-size)
|
|
(fprintf dest-file "%d %d\n" i (at i buffer))
|
|
(incr i))
|
|
(fclose dest-file))
|
|
|
|
(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-looper-buffer-size)
|
|
;; TODO: Use silence value from SDL audio spec
|
|
(set (at i g-audio-looper-buffer) 127)
|
|
(incr i)))
|
|
|
|
(defun-local audio-looper-buffer-destroy ()
|
|
(free g-audio-looper-buffer)
|
|
(set g-audio-looper-buffer null))
|
|
|
|
(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)
|
|
(set up (not up))
|
|
(var i int 0)
|
|
(var num-channels int 2)
|
|
(var samples-per-channel int (/ stream-length num-channels))
|
|
(while (< i stream-length)
|
|
(var mono-sample int 127)
|
|
;; Square
|
|
;; (set mono-sample (? up 255 0))
|
|
;; Sawtooth
|
|
;; (set mono-sample (mod (/ i 4) 255))
|
|
;; Sine
|
|
;; (set mono-sample
|
|
;; ;; Map to 0-255
|
|
;; (+ 127 (* 127
|
|
;; ;; Map to -1 to +1
|
|
;; (sin
|
|
;; ;; Map from position in buffer to 2pi range
|
|
;; (/ (* i 2 g-pi) (type-cast samples-per-channel float))))))
|
|
|
|
;; Loop playback
|
|
(if g-audio-is-recording
|
|
(set mono-sample 127) ;; Silence
|
|
(scope ;; Else, play the recording
|
|
(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)
|
|
(while (< channel num-channels)
|
|
(set (at (+ i channel) stream) mono-sample)
|
|
;; (printf "[%d][%d] %d\n" (+ i channel) channel (at i stream))
|
|
(incr channel))
|
|
(set i (+ i num-channels)))
|
|
(set g-audio-looper-read-head
|
|
(mod (+ g-audio-looper-read-head samples-per-channel)
|
|
g-audio-looper-buffer-size)))
|
|
|
|
;; 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-looper-input-callback (userdata (* void) stream (* Uint8) stream-length int)
|
|
(ZoneScopedN "Audio input")
|
|
|
|
(unless g-audio-is-recording
|
|
(return))
|
|
;; (printf "received audio %d %d %d\n"
|
|
;; (at 0 stream)
|
|
;; (at (/ stream-length 2) stream)
|
|
;; (at (- stream-length 1) stream))
|
|
(var i int 0)
|
|
(while (< i stream-length)
|
|
(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)
|
|
(break))
|
|
(incr i)))
|
|
|
|
(defun-local initialize-audio (output-device-out (* SDL_AudioDeviceID)
|
|
input-device-out (* SDL_AudioDeviceID)
|
|
output-device-spec-out (* SDL_AudioSpec)
|
|
input-device-spec-out (* SDL_AudioSpec)
|
|
&return bool)
|
|
(ZoneScopedN "Audio intialization")
|
|
|
|
;; This will take a while. Make sure we aren't marked as ready while preparing
|
|
(set g-audio-is-initialized false)
|
|
|
|
(var audio-driver (* (const char)) (SDL_GetCurrentAudioDriver))
|
|
(scope-timed
|
|
"Audio drivers"
|
|
(printf "Available drivers:\n")
|
|
(var num-drivers int (SDL_GetNumAudioDrivers))
|
|
(var i int 0)
|
|
(while (< i num-drivers)
|
|
(var driver-name (* (const char)) (SDL_GetAudioDriver i))
|
|
(when driver-name
|
|
(printf "\t[%d] %s\n" i driver-name))
|
|
(incr i))
|
|
(unless audio-driver
|
|
(printf "No audio driver found")
|
|
(return false))
|
|
(printf "Active audio driver: %s\n" audio-driver))
|
|
|
|
;; Use my USB webcam
|
|
(var macoy-input-device-index int 2)
|
|
;; Use my HDMI output device
|
|
(var macoy-output-device-index int 2)
|
|
;; Pulse audio has different sources
|
|
(when (= 0 (strcmp audio-driver "pulseaudio"))
|
|
(set macoy-input-device-index 0)
|
|
(set macoy-output-device-index 1))
|
|
|
|
(var is-capture bool false)
|
|
|
|
(var devices (* (* (const char))) null)
|
|
(var num-devices int (sdl-audio-get-devices (addr devices) is-capture))
|
|
(unless num-devices (return false))
|
|
|
|
(var selected-output-device (* (const char)) nullptr)
|
|
(var output-device-id SDL_AudioDeviceID 0)
|
|
(var obtained-output-spec SDL_AudioSpec (array 0))
|
|
(scope-timed
|
|
"Audio output device"
|
|
(var desired-spec SDL_AudioSpec (array 0))
|
|
(set (field desired-spec freq) 44100)
|
|
(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-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)
|
|
(return false))
|
|
|
|
(var device-name (* (const char)) (at macoy-output-device-index devices))
|
|
(var valid-device-start-num (const int) 2)
|
|
(set output-device-id (SDL_OpenAudioDevice
|
|
;; null = reasonable default (doesn't work in my case)
|
|
device-name
|
|
false ;; iscapture
|
|
(addr desired-spec) (addr obtained-output-spec)
|
|
(bit-or SDL_AUDIO_ALLOW_FREQUENCY_CHANGE
|
|
SDL_AUDIO_ALLOW_SAMPLES_CHANGE
|
|
SDL_AUDIO_ALLOW_CHANNELS_CHANGE)))
|
|
(if (>= output-device-id valid-device-start-num)
|
|
(scope (set selected-output-device device-name)
|
|
(set (deref output-device-spec-out) obtained-output-spec))
|
|
(scope (sdl-print-error)
|
|
(sdl-audio-free-device-list devices num-devices)
|
|
(return false))))
|
|
|
|
(var selected-input-device (* (const char)) nullptr)
|
|
(var obtained-input-spec SDL_AudioSpec (array 0))
|
|
(var input-device-id SDL_AudioDeviceID 0)
|
|
(scope-timed
|
|
"Audio input device"
|
|
(var desired-spec SDL_AudioSpec (array 0))
|
|
(set (field desired-spec freq) 44100)
|
|
(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-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))
|
|
(unless num-capture-devices (return false))
|
|
|
|
(unless (< macoy-input-device-index num-capture-devices)
|
|
(printf "Could not choose device %d\n" macoy-input-device-index)
|
|
(sdl-audio-free-device-list capture-devices num-capture-devices)
|
|
(return false))
|
|
|
|
(var device-name (* (const char)) (at macoy-input-device-index capture-devices))
|
|
(var valid-device-start-num (const int) 2)
|
|
(set input-device-id (SDL_OpenAudioDevice
|
|
;; null = reasonable default (doesn't work in my case)
|
|
device-name
|
|
true ;; iscapture
|
|
(addr desired-spec) (addr obtained-input-spec)
|
|
(bit-or SDL_AUDIO_ALLOW_FREQUENCY_CHANGE
|
|
SDL_AUDIO_ALLOW_SAMPLES_CHANGE
|
|
SDL_AUDIO_ALLOW_CHANNELS_CHANGE)))
|
|
(when (< input-device-id valid-device-start-num)
|
|
(sdl-print-error)
|
|
(sdl-audio-free-device-list capture-devices num-capture-devices)
|
|
(return false))
|
|
(set (deref input-device-spec-out) obtained-input-spec)
|
|
(set selected-input-device device-name)
|
|
|
|
(when selected-input-device ;; print final settings
|
|
(printf "Final input settings:\n")
|
|
(printf "device: %s\n" selected-input-device)
|
|
(sdl-audio-list-specification (addr obtained-input-spec))
|
|
;; Start playing
|
|
;; Important note: SDL_PauseAudio works on the default device, NOT the opened one
|
|
;; This tripped me up when my audio wasn't actually getting played on my opened one
|
|
(SDL_PauseAudioDevice input-device-id 0))
|
|
(sdl-audio-free-device-list capture-devices num-capture-devices))
|
|
|
|
(when selected-output-device
|
|
(printf "Final output settings:\n")
|
|
(printf "device: %s\n" selected-output-device)
|
|
(sdl-audio-list-specification (addr obtained-output-spec))
|
|
;; Start playing
|
|
;; Important note: SDL_PauseAudio works on the default device, NOT the opened one
|
|
;; This tripped me up when my audio wasn't actually getting played on my opened one
|
|
(SDL_PauseAudioDevice output-device-id 0))
|
|
|
|
(sdl-audio-free-device-list devices num-devices)
|
|
|
|
(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-initialized true)
|
|
|
|
(return (and output-device-id input-device-id)))
|
|
|
|
(defun-local audio-close (output-device SDL_AudioDeviceID
|
|
input-device SDL_AudioDeviceID)
|
|
(when (>= output-device 2) (SDL_CloseAudioDevice output-device))
|
|
(when (>= input-device 2) (SDL_CloseAudioDevice input-device)))
|
|
|
|
(forward-declare (namespace Ogre
|
|
(class Root)
|
|
(class SceneManager)))
|
|
(declare-extern-function
|
|
ogreCreatePbsSpheres (root (* (in Ogre Root))
|
|
scene-manager (* (in Ogre SceneManager))))
|
|
|
|
(defun-local audio-initialize-thread (user-data (* void) &return int)
|
|
(TracyCSetThreadName "Audio Initialization")
|
|
(unless (initialize-audio (addr g-audio-output-device) (addr g-audio-input-device)
|
|
(addr g-audio-output-device-spec) (addr g-audio-input-device-spec))
|
|
;; TODO: Communicate with main thread and close
|
|
(printf "warning: audio failed to initialize\n")
|
|
(return 1))
|
|
(return 0))
|
|
|
|
(defun-local audio-shutdown-thread (user-data (* void) &return int)
|
|
(ZoneScopedN "SDL audio shutdown")
|
|
(audio-close g-audio-output-device
|
|
g-audio-input-device)
|
|
(return 0))
|
|
|
|
(defun-local audio-set-recording-state (new-value bool)
|
|
(scope ;; Toggle recording state
|
|
(SDL_LockAudioDevice g-audio-output-device)
|
|
(SDL_LockAudioDevice g-audio-input-device)
|
|
(set g-audio-is-recording new-value)
|
|
(SDL_UnlockAudioDevice g-audio-input-device)
|
|
(SDL_UnlockAudioDevice g-audio-output-device)))
|
|
|
|
(defun-local app-main (&return int)
|
|
(TracyCZoneN startup-zone "Startup" 1)
|
|
(unless g-initialized ;; Note, continues after audio thread starts up
|
|
(scope-timed
|
|
"SDL initialization"
|
|
;; (defun main (&return int)
|
|
(unless (sdl-initialize-for-3d (addr g-window) "Vocal Game" 1920 1080)
|
|
(TracyCZoneEnd startup-zone)
|
|
(return 1))))
|
|
|
|
;; 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-looper-buffer-initialize)
|
|
;; 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)
|
|
(SDL_CreateThread audio-initialize-thread "AudioInitialization" null))
|
|
;; While we do want to know if this thread fails, we don't want to block to know, so we'll
|
|
;; detach it to not leave it dangling (see SDL_thread.h)
|
|
(SDL_DetachThread audio-init-thread-handle)
|
|
(ignore ;; Single threaded version
|
|
(unless (initialize-audio (addr g-audio-output-device) (addr g-audio-input-device)
|
|
(addr g-audio-output-device-spec) (addr g-audio-input-device-spec))
|
|
(sdl-shutdown g-window)
|
|
(audio-close g-audio-output-device g-audio-input-device)
|
|
(TracyCZoneEnd startup-zone)
|
|
(return 1))))
|
|
|
|
(unless g-initialized
|
|
(scope-timed
|
|
"Ogre initialization"
|
|
;; Ogre uses exceptions for error handling, so we can't gracefully close without getting all that
|
|
;; stuff set up (which I don't really want to do; it belongs in Gamelib)
|
|
(unless (ogre-initialize-sdl)
|
|
(TracyCZoneEnd startup-zone)
|
|
(return 1)))
|
|
|
|
(scope-timed
|
|
"Scene setup/load meshes"
|
|
(set g-monkey-mesh (ogre-load-mesh "Monkey_Mesh.mesh"))
|
|
(set g-monkey-node (ogre-node-from-item g-monkey-mesh))
|
|
;; TODO: How to change this animation's name?
|
|
(set g-monkey-anim-handle
|
|
(ogre-attach-animation g-monkey-mesh "Monkey_Mesh.skeleton" "Monkey_AngleChin"))
|
|
|
|
(set g-light-node (ogre-create-light))
|
|
|
|
(set g-pitch-particles (ogre-create-particle-system))
|
|
|
|
(when g-ogre-root
|
|
(printf "Creating PBS spheres\n")
|
|
(ogreCreatePbsSpheres g-ogre-root (ogre-get-scene-manager))
|
|
(printf "Creating PBS spheres done\n")))
|
|
|
|
(set g-initialized true))
|
|
|
|
(var exit-reason (* (const char)) null)
|
|
|
|
(var tracker-position vec3 (array -10.f 0.f 0.f))
|
|
(var move-speed float 10.f)
|
|
|
|
(var counter-num-ticks-per-second (const Uint64) (SDL_GetPerformanceFrequency))
|
|
(var last-frame-perf-count Uint64 (* 0.016f counter-num-ticks-per-second))
|
|
|
|
;; "Debounce" via only reloading on key release
|
|
(var was-reload-pressed bool false)
|
|
|
|
(TracyCZoneEnd startup-zone)
|
|
|
|
;; Main loop
|
|
(while (not exit-reason)
|
|
(FrameMarkNamed "Frame")
|
|
(ZoneScopedN "Frame")
|
|
|
|
(var event SDL_Event)
|
|
(while (SDL_PollEvent (addr event))
|
|
(when (= (field event type) SDL_QUIT)
|
|
(set exit-reason "Window closed")))
|
|
|
|
;; Note: this requires SDL_PollEvent in order to be up-to-date
|
|
(var currentKeyStates (* (const Uint8)) (SDL_GetKeyboardState null))
|
|
(when (at SDL_SCANCODE_ESCAPE currentKeyStates)
|
|
(set exit-reason "Escape pressed"))
|
|
|
|
(var reload-pressed bool (at SDL_SCANCODE_R currentKeyStates))
|
|
(when reload-pressed
|
|
(set was-reload-pressed true))
|
|
(when (and (not reload-pressed) was-reload-pressed)
|
|
(set was-reload-pressed false)
|
|
(printf "\nReloading\n\n")
|
|
;; Our audio callbacks are going away!
|
|
;; The SDL_QueueAudio() API would make this easier to deal with, but then I need to manage
|
|
;; an audio thread on my own
|
|
(audio-close g-audio-output-device
|
|
g-audio-input-device)
|
|
(return g-reload-sentinel))
|
|
|
|
(var delta-position vec3 (array 0.f 0.f 0.f))
|
|
(when (at SDL_SCANCODE_RIGHT currentKeyStates)
|
|
(set (vec-x delta-position) (+ (vec-x delta-position) move-speed)))
|
|
(when (at SDL_SCANCODE_LEFT currentKeyStates)
|
|
(set (vec-x delta-position) (- (vec-x delta-position) move-speed)))
|
|
(when (at SDL_SCANCODE_UP currentKeyStates)
|
|
(set (vec-y delta-position) (+ (vec-y delta-position) move-speed)))
|
|
(when (at SDL_SCANCODE_DOWN currentKeyStates)
|
|
(set (vec-y delta-position) (- (vec-y delta-position) move-speed)))
|
|
|
|
(scope-timed
|
|
"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)
|
|
(scope-timed
|
|
"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)
|
|
(interpolate-range
|
|
;; 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
|
|
pitch-hz)))))
|
|
|
|
(var prev-recording-value bool g-audio-is-recording)
|
|
(audio-set-recording-state (at SDL_SCANCODE_SPACE currentKeyStates))
|
|
(when (and g-audio-is-initialized
|
|
(!= prev-recording-value g-audio-is-recording)
|
|
(not g-audio-is-recording))
|
|
(ZoneScopedN "Audio conversion")
|
|
(scope ;; Convert audio to playback format.
|
|
;; This was necessary in my case because my microphone records at 48kHz, but my sound card
|
|
;; expects 44.1kHz output
|
|
;; Audio Streams are worth looking at in the future, so it's not all one big batch process
|
|
(var audio-conversion-settings SDL_AudioCVT)
|
|
(SDL_BuildAudioCVT (addr audio-conversion-settings)
|
|
;; Src
|
|
(field g-audio-input-device-spec format)
|
|
(field g-audio-input-device-spec channels)
|
|
(field g-audio-input-device-spec freq)
|
|
;; Dest
|
|
(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-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
|
|
(* (field audio-conversion-settings len)
|
|
(field audio-conversion-settings len_mult)))
|
|
(printf "Converted buffer size: %d bytes because %d len %d len_mult\n"
|
|
converted-buffer-size-bytes
|
|
(field audio-conversion-settings len)
|
|
(field audio-conversion-settings len_mult))
|
|
(set (field audio-conversion-settings buf)
|
|
(type-cast (malloc converted-buffer-size-bytes) (* Uint8)))
|
|
(scope ;; Copy src to in-place conversion buffer
|
|
(var i int 0)
|
|
(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")
|
|
(free (field audio-conversion-settings buf))
|
|
(break))
|
|
;; TODO: This shouldn't be byte size, it should be sample count, to support other formats
|
|
(audio-dump-recorded-buffer "converted.dat"
|
|
(field audio-conversion-settings buf)
|
|
;; Remove the extra buffer used by the converter
|
|
(/ converted-buffer-size-bytes
|
|
(field audio-conversion-settings len_mult)))
|
|
(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-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-looper-buffer-size 3900))
|
|
(set (at i g-audio-looper-buffer) 127))
|
|
(incr i)))
|
|
(free (field audio-conversion-settings buf)))
|
|
|
|
(scope ;; Normalize audio
|
|
;; 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-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))
|
|
(when highest-peak
|
|
;; Get within 90% of the max, to avoid clipping
|
|
(var desired-high-peak (const int) (* (/ 256 2) 0.9f))
|
|
(var scaling-factor float (/ desired-high-peak (type-cast highest-peak float)))
|
|
(printf "Scaling recording %.4f because desired = %d and highest was %d\n"
|
|
scaling-factor desired-high-peak highest-peak)
|
|
(var scaling-limit float 6.f)
|
|
;; If there's only silence, you don't want to go crazy with boosting it
|
|
(when (> scaling-factor scaling-limit)
|
|
(printf "scaling limited to %f\n" scaling-limit)
|
|
(set scaling-factor scaling-limit))
|
|
(when scaling-factor
|
|
(set i 0)
|
|
(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))
|
|
Uint8))
|
|
(incr i)))))
|
|
(scope-timed
|
|
"Detect pitch"
|
|
(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))
|
|
(var delta-time float (/ frame-diff-ticks
|
|
(type-cast counter-num-ticks-per-second float)))
|
|
;; (printf "%lu %f\n" frame-diff-ticks delta-time)
|
|
|
|
;; Visualize audio playback
|
|
(var audio-read-head-to-world float (* 20.f
|
|
(/ g-audio-looper-read-head
|
|
(type-cast g-audio-looper-buffer-size float))))
|
|
(var audio-volume-to-world float (* 7.f
|
|
(/ (- (at g-audio-looper-read-head g-audio-looper-buffer) 127)
|
|
127.f)))
|
|
|
|
(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 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))
|
|
;; This moves the entire particle system, not the emitter
|
|
;; (ogre-node-set-position (addr g-particle-node) (vec-xyz final-pos))
|
|
(ogre-particle-emitter-set-position (addr g-pitch-particles) (vec-xyz final-pos))
|
|
|
|
(ogre-animation-add-time g-monkey-anim-handle delta-time)
|
|
|
|
(set last-frame-perf-count (SDL_GetPerformanceCounter))
|
|
|
|
(scope-timed
|
|
"Ogre render + vsync"
|
|
(unless (ogre-render-frame)
|
|
(set exit-reason "Failed to render frame")
|
|
(break))))
|
|
|
|
(ZoneScopedN "Shutdown")
|
|
|
|
(audio-set-recording-state false)
|
|
|
|
;; Audio shutdown is extremely slow (~2-3 seconds on my machine). Let's do it on a
|
|
;; separate thread to let other things shutdown at the same time
|
|
(var audio-shutdown-thread-handle (* SDL_Thread)
|
|
(SDL_CreateThread audio-shutdown-thread "AudioShutdown" null))
|
|
|
|
(audio-dump-recorded-buffer "out.dat"
|
|
g-audio-looper-buffer g-audio-looper-buffer-size)
|
|
(audio-looper-buffer-destroy)
|
|
|
|
(scope-timed
|
|
"Ogre shutdown"
|
|
(ogre-shutdown))
|
|
|
|
;; Make sure audio is shutdown before destroying SDL
|
|
(var status int -1)
|
|
(SDL_WaitThread audio-shutdown-thread-handle (addr status))
|
|
|
|
(scope-timed
|
|
"SDL shutdown"
|
|
(sdl-shutdown g-window))
|
|
|
|
(when exit-reason
|
|
(printf "Exit reason: %s\n" exit-reason))
|
|
(return 0))
|
|
|
|
;; TODO: Automatically make entry point
|
|
(defun reloadableEntryPoint (&return bool)
|
|
(var result int (app-main))
|
|
(cond ((= 0 result)
|
|
(return false))
|
|
((= 1 result)
|
|
(return false))
|
|
((= g-reload-sentinel result)
|
|
(return true)))
|
|
(return false))
|
|
|
|
;;
|
|
;; Building
|
|
;;
|
|
|
|
(use-ogre-build-options) ;; Only needed for the dependency
|
|
(add-cpp-build-dependency "OgreHlms.cpp")
|
|
|
|
(import "Dependencies.cake")
|
|
|