GameLib is my library for making games
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.

622 lines
26 KiB

(import &comptime-only "Options.cake" "Macros.cake" "GamelibMacros.cake")
(import "Ogre.cake" "OgreInitialize.cake" "SDL.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>")
;; These are read and written to from different threads (currently, without locking)
(var audio-is-recording bool false)
(var audio-input-buffer (* Uint8) null)
(var audio-input-buffer-size int 0)
(var audio-input-write-head int 0)
(var audio-input-read-head int 0)
;; (var enable-audio bool false)
(var enable-audio bool true)
;; 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)
(var dest-file (* FILE) (fopen output-filename "w"))
(unless dest-file
(printf "Could not open file to write data\n")
(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-input-buffer-initialize ()
(set audio-input-buffer-size 44100)
(set audio-input-buffer (type-cast (calloc audio-input-buffer-size (sizeof Uint8))
(* Uint8)))
(var i int 0)
(while (< i audio-input-buffer-size)
;; TODO: Use silence value from SDL audio spec
(set (at i audio-input-buffer) 127)
(incr i)))
(defun-local audio-input-buffer-destroy ()
(free audio-input-buffer)
(set audio-input-buffer null))
(defun-local audio-output-callback (userdata (* void) stream (* Uint8) stream-length int)
;; (printf "Audio len %d\n" stream-length)
(static-var up bool false)
(set up (not up))
(var i int 0)
(var pi (const float) 3.14159f)
(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 pi) (type-cast samples-per-channel float))))))
;; Loop playback
(if audio-is-recording
(set mono-sample 127) ;; Silence
(block ;; Else, play the recording
(var recording-index int (+ (/ i num-channels) audio-input-read-head))
(set recording-index (mod recording-index audio-input-buffer-size))
(set mono-sample (at recording-index audio-input-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 audio-input-read-head
(mod (+ audio-input-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)
(unless audio-is-recording
;; (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 audio-input-write-head audio-input-buffer) (at i stream))
(if (= audio-input-write-head (- audio-input-buffer-size 1))
(set audio-input-write-head 0)
(incr audio-input-write-head))
(incr i)))
;; Allocates both names and array
;; Returns number of devices in device-names-out
(defun-local sdl-audio-get-devices (device-names-out (* (* (* (const char))))
is-capture bool &return int)
(var num-devices int (SDL_GetNumAudioDevices is-capture))
(set (deref device-names-out) (type-cast
(calloc (sizeof (type (* (const char)))) num-devices)
(* (* (const char)))))
(printf "Available %s devices:\n" (? is-capture "recording" "playback"))
(var i int 0)
(while (< i num-devices)
(var device-name (* (const char)) (SDL_GetAudioDeviceName i is-capture))
(when device-name
(printf "\t[%d] %s\n" i device-name)
(set (at i (deref device-names-out)) (strdup device-name)))
(incr i))
(return num-devices))
(defun-local sdl-audio-list-specification (spec (* SDL_AudioSpec))
(printf "freq: %d\n" (path spec > freq))
(printf "format: %d\n" (path spec > format))
(printf "channels: %d\n" (path spec > channels))
(printf "samples: %d\n" (path spec > samples)))
(defun-local sdl-audio-free-device-list (device-names (* (* (const char))) num-devices int)
(var i int 0)
(while (< i num-devices)
(free (type-cast (at i device-names) (* void)))
(incr i))
(free device-names))
(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)
(var audio-driver (* (const char)) (SDL_GetCurrentAudioDriver))
(scope ;; 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 ;; 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-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)
false ;; iscapture
(addr desired-spec) (addr obtained-output-spec)
(if (>= output-device-id valid-device-start-num)
(block (set selected-output-device device-name)
(set (deref output-device-spec-out) obtained-output-spec))
(block (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 ;; 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-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)
true ;; iscapture
(addr desired-spec) (addr obtained-input-spec)
(when (< input-device-id valid-device-start-num)
(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))
(return (and output-device-id input-device-id)))
(defun-local sdl-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)))
(var g-window (* SDL_Window) null)
(var g-output-device SDL_AudioDeviceID 0)
(var g-input-device SDL_AudioDeviceID 0)
(var g-output-device-spec SDL_AudioSpec)
(var g-input-device-spec SDL_AudioSpec)
(var monkey-mesh mesh-handle)
(var monkey-node scene-node)
(var g-light-node scene-node)
(var reload-sentinel int 2)
(var initialized bool false)
(forward-declare (namespace Ogre
(class Root)
(class SceneManager)))
ogreCreatePbsSpheres (root (* (in Ogre Root))
scene-manager (* (in Ogre SceneManager))))
(defun-local app-main (&return int)
(unless initialized
;; (defun main (&return int)
(unless (sdl-initialize-for-3d (addr g-window))
(return 1))
;; 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)
(return 1))
(set monkey-mesh (ogre-load-mesh "Suzanne.mesh"))
(set monkey-node (ogre-node-from-item monkey-mesh))
(set g-light-node (ogre-create-light))
(when g-ogre-root
(printf "Creating PBS spheres\n")
(ogreCreatePbsSpheres g-ogre-root (ogre-get-scene-manager))
(printf "Creating PBS spheres done\n"))
(set initialized true))
;; Audio needs to be re-initialized due to reload removing callbacks
;; TODO: Some way to avoid the big wait time required when initializing audio would be good
;; It actually seems pretty quick now
(when enable-audio
(unless (initialize-audio (addr g-output-device) (addr g-input-device)
(addr g-output-device-spec) (addr g-input-device-spec))
(sdl-shutdown g-window)
(sdl-audio-close g-output-device g-input-device)
(return 1)))
(var exit-reason (* (const char)) null)
(var x float -10.f)
(var y float 0.f)
(var z float 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)
;; Main loop
(while (not exit-reason)
(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
(sdl-audio-close g-output-device
(return reload-sentinel))
(var delta-position ([] 3 float) (array 0))
(when (at SDL_SCANCODE_RIGHT currentKeyStates)
(set (at 0 delta-position) (+ (at 0 delta-position) move-speed)))
(when (at SDL_SCANCODE_LEFT currentKeyStates)
(set (at 0 delta-position) (- (at 0 delta-position) move-speed)))
(when (at SDL_SCANCODE_UP currentKeyStates)
(set (at 1 delta-position) (+ (at 1 delta-position) move-speed)))
(when (at SDL_SCANCODE_DOWN currentKeyStates)
(set (at 1 delta-position) (- (at 1 delta-position) move-speed)))
(var prev-recording-value bool audio-is-recording)
(set audio-is-recording (at SDL_SCANCODE_SPACE currentKeyStates))
(when (and (!= prev-recording-value audio-is-recording)
(not audio-is-recording))
(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-input-device-spec format)
(field g-input-device-spec channels)
(field g-input-device-spec freq)
;; Dest
(field g-output-device-spec format)
1 ;; (field g-output-device-spec channels) ;; We convert to stereo in playback callback
(field g-output-device-spec freq))
(set (field audio-conversion-settings len) audio-input-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"
(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 audio-input-buffer-size)
(set (at i (field audio-conversion-settings buf)) (at i audio-input-buffer))
(incr i)))
(unless (= 0 (SDL_ConvertAudio (addr audio-conversion-settings)))
(set exit-reason "Failed to convert audio")
(free (field audio-conversion-settings buf))
;; 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 audio-input-buffer-size)
(set (at i audio-input-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 (- audio-input-buffer-size 3900))
(set (at i audio-input-buffer) 127))
(incr i)))
(free (field audio-conversion-settings buf)))
;; 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 audio-input-buffer-size)
(var current-value int (- (type-cast (at i audio-input-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 audio-input-buffer-size)
(var current-value int (- (type-cast (at i audio-input-buffer) int) 127))
(set (at i audio-input-buffer) (type-cast
;; Map back to 0-255
(+ 127.f
(* current-value scaling-factor))
(incr i)))))
(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
(/ audio-input-read-head
(type-cast audio-input-buffer-size float))))
(var audio-volume-to-world float (* 7.f
(/ (- (at audio-input-read-head audio-input-buffer) 127)
(set x (+ x (* delta-time (at 0 delta-position))))
(set y (+ y (* delta-time (at 1 delta-position))))
(set z (+ z (* delta-time (at 2 delta-position))))
(ogre-node-set-position (addr monkey-node) (+ audio-read-head-to-world x)
(+ audio-volume-to-world z))
(set last-frame-perf-count (SDL_GetPerformanceCounter))
(unless (ogre-render-frame)
(set exit-reason "Failed to render frame")
(audio-dump-recorded-buffer "out.dat"
audio-input-buffer audio-input-buffer-size)
(sdl-audio-close g-output-device
(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))
((= reload-sentinel result)
(return true)))
(return false))
;; Assets
;; TODO Improvements:
;; - Check blender command for changes to cause rebuild
;; - Check linked Blender files (would need to hard code because blender is too slow to start)
;; - Don't rebuild due to different build configuration (assets remain unchanged)
;; - Actually check assets instead of just cache file
(defun-comptime process-3d-assets (manager (& ModuleManager) module (* Module) &return bool)
(scope ;; Models/meshes
;; Make sure output dir exists and we have an absolute path to it
(var model-relative-dir (* (const char)) "test/data/Models")
(makeDirectory model-relative-dir)
;; Output must be absolute or OgreMeshTool will fail (probably due to different working dir)
(var model-output-dir (* (const char)) (makeAbsolutePath_Allocated null model-relative-dir))
(unless model-output-dir
(Logf "Asset-Building: could not make directory %s absolute\n" model-relative-dir)
(return false))
(var model-assets ([] (* (const char))) (array "Monkey"))
(var i int 0)
(while (< i (array-size model-assets))
(var blend-asset ([] MAX_PATH_LENGTH char) (array 0))
(PrintfBuffer blend-asset "test/assets/%s.blend" (at i model-assets))
;; It is too slow to check Blender for all the files the blend will export, then check whether
;; the .blend file is more recently modified. Instead, create a file in the cache to represent
;; the last time the .blend was known to have been exported. Hack, especially because other
;; configurations don't need to rebuild assets. Probably better to just leave asset building to
;; another executable
(var cache-reference-filename ([] MAX_PATH_LENGTH char) (array 0))
(unless (outputFilenameFromSourceFilename (on-call (field manager buildOutputDir) c_str)
"txt" ;; Add to end of file for type
cache-reference-filename (sizeof cache-reference-filename))
(free (type-cast model-output-dir (* void)))
(return false))
(unless (fileIsMoreRecentlyModified blend-asset cache-reference-filename)
(incr i)
"--background" blend-asset
"--python-exit-code" "1" ;; If there's a python exception, return 1
"--python" "tools/"
"--" model-output-dir)
(Log "Asset-Building: failed to build 3D asset. Is Blender on your path? Is blender2ogre set
up on your Blender default preferences? See for setup\n")
(free (type-cast model-output-dir (* void)))
(return false))
(scope ;; Write reference file
(var cache-reference (* FILE) (fopen cache-reference-filename "w"))
(unless cache-reference
(Logf "Asset-Building: failed to open cache reference file %s\n" cache-reference-filename)
(free (type-cast model-output-dir (* void)))
(return false))
(fprintf cache-reference "%s exported\n" blend-asset)
(fclose cache-reference))
(incr i))
(free (type-cast model-output-dir (* void))))
(scope ;; Textures
(var texture-assets ([] (* (const char))) (array "Monkey_Texture"))
(var i int 0)
(while (< i (array-size texture-assets))
(var texture-asset ([] MAX_PATH_LENGTH char) (array 0))
(PrintfBuffer texture-asset "test/assets/%s.png" (at i texture-assets))
(var texture-converted ([] MAX_PATH_LENGTH char) (array 0))
(PrintfBuffer texture-converted "test/data/Materials/Textures/" (at i texture-assets))
(unless (fileIsMoreRecentlyModified texture-asset texture-converted)
(incr i)
("convert" texture-asset texture-converted)
(Log "Asset-Building: failed to convert 2D texture. Is `convert` on your path? You may need to
install ImageMagick. See\n")
(return false))
(incr i)))
(return true))
;; TODO: This should be a post-build hook
(add-compile-time-hook-module pre-build process-3d-assets)
;; Building
(set-cakelisp-option executable-output "test/VocalGame")
;; TODO: Somehow inherit this from SDL.cake?
(use-ogre-build-options) ;; Only needed for the dependency
(add-cpp-build-dependency "OgreHlms.cpp")