builtin-programs/audio.folk
# Provides WAV, FLAC, and MP3 file playback using the miniaudio library. Requires
# ALSA, PulseAudio, or JACK drivers.
#
# See https://miniaud.io/. We are using Miniaudio v0.11.23.
#
# Examples:
#
# Play a sound file in assets/sounds/ or user-programs/$hostname/sounds:
#
# Wish to play audio drums.wav
#
# Play a sound file by absolute path:
#
# Wish to play audio /home/folk/sounds/drums.wav
set cc [C]
$cc cflags -I./vendor/miniaudio
if {![catch {exec which sclang}]} {
# HACK: We're running msuic.folk and therefore are running JACK,
# so we should force miniaudio to not use ALSA directly (because
# that won't work).
$cc code {
#define MA_NO_ALSA
#define MA_NO_PULSEAUDIO
}
}
$cc code {
#define MINIAUDIO_IMPLEMENTATION
}
$cc include <pthread.h>
$cc include <stdio.h>
$cc include <stdint.h>
$cc include <stdlib.h>
$cc include <unistd.h>
$cc include <miniaudio.h>
if {$::tcl_platform(os) ne "Darwin"} {
$cc endcflags -lpthread -lm -ldl
}
$cc code {
static ma_context g_ctx;
static ma_engine g_engine;
static ma_sound_group g_group;
static bool g_ctx_initialized = false;
static bool g_engine_initialized = false;
static bool g_engine_started = false;
static bool g_group_initialized = false;
static pthread_mutex_t g_state_mtx = PTHREAD_MUTEX_INITIALIZER;
typedef struct SoundNode {
ma_sound* snd;
struct SoundNode* next;
} SoundNode;
static SoundNode* g_head = NULL;
static pthread_mutex_t g_list_mtx = PTHREAD_MUTEX_INITIALIZER;
static bool g_reaper_running = false;
static pthread_t g_reaper_thr;
static bool g_shutdown_reaper = false;
/* Maintains linked list of active sounds */
static bool registry_add(ma_sound* snd) {
SoundNode* node = (SoundNode*)malloc(sizeof *node);
if (!node) {
FOLK_ERROR("miniaudio: registry_add: alloc failed\n");
return false;
}
node->snd = snd;
pthread_mutex_lock(&g_list_mtx);
node->next = g_head;
g_head = node;
pthread_mutex_unlock(&g_list_mtx);
return true;
}
/* Removes and uninitializes all sounds from the registry */
static void registry_clear_locked(void) {
SoundNode* node = g_head;
while (node) {
SoundNode* next = node->next;
if (node->snd) {
ma_sound_uninit(node->snd);
free(node->snd);
}
free(node);
node = next;
}
g_head = NULL;
}
/* Background thread that periodically removes finished sounds from the registry */
static void* reaper_main(void* arg) {
(void)arg;
while (!g_shutdown_reaper) {
pthread_mutex_lock(&g_list_mtx);
SoundNode** pprev = &g_head;
SoundNode* node = g_head;
while (node) {
ma_sound* snd = node->snd;
bool remove = false;
if (snd && !ma_sound_is_playing(snd) && !ma_sound_is_looping(snd)) {
/* Collect finished sounds */
ma_sound_uninit(snd);
free(snd);
remove = true;
}
if (remove) {
SoundNode* to_free = node;
*pprev = node->next;
node = node->next;
free(to_free);
continue;
}
pprev = &node->next;
node = node->next;
}
pthread_mutex_unlock(&g_list_mtx);
usleep(50 * 1000);
}
pthread_mutex_lock(&g_list_mtx);
registry_clear_locked();
pthread_mutex_unlock(&g_list_mtx);
g_reaper_running = false;
return NULL;
}
static int reaper_start(void) {
if (g_reaper_running) return 0;
g_shutdown_reaper = false;
int err = pthread_create(&g_reaper_thr, NULL, reaper_main, NULL);
if (err == 0) {
g_reaper_running = true;
}
return err;
}
/* Initializes the audio backend and sound group, and starts the reaper thread */
static bool audio_init_impl(void) {
ma_result r;
if (!g_ctx_initialized) {
r = ma_context_init(NULL, 0, NULL, &g_ctx);
if (r != MA_SUCCESS) {
FOLK_ERROR("miniaudio: context init failed: %s (%d)\n",
ma_result_description(r), (int)r);
return false;
}
g_ctx_initialized = true;
}
if (!g_engine_initialized) {
ma_engine_config cfg = ma_engine_config_init();
cfg.pContext = &g_ctx;
r = ma_engine_init(&cfg, &g_engine);
if (r != MA_SUCCESS) {
FOLK_ERROR("miniaudio: engine init failed: %s (%d)\n",
ma_result_description(r), (int)r);
return false;
}
fprintf(stderr, "miniaudio: engine initialized with %s backend\n",
ma_get_backend_name(g_ctx.backend));
g_engine_initialized = true;
}
if (!g_engine_started) {
r = ma_engine_start(&g_engine);
if (r != MA_SUCCESS) {
FOLK_ERROR("miniaudio: engine start failed: %s (%d)\n",
ma_result_description(r), (int)r);
return false;
}
g_engine_started = true;
}
if (!g_group_initialized) {
r = ma_sound_group_init(&g_engine, 0, NULL, &g_group);
if (r != MA_SUCCESS) {
FOLK_ERROR("miniaudio: group init failed: %s (%d)\n",
ma_result_description(r), (int)r);
/* Unwind engine on group failure; context remains for future attempts */
if (g_engine_initialized) {
ma_engine_uninit(&g_engine);
memset(&g_engine, 0, sizeof g_engine);
g_engine_initialized = false;
}
g_engine_started = false;
return false;
}
g_group_initialized = true;
}
int err = reaper_start();
if (err != 0) {
FOLK_ERROR("audio: reaper thread create failed: %d\n", err);
return false;
}
return true;
}
}
$cc proc audioInit {} bool {
pthread_mutex_lock(&g_state_mtx);
bool success = audio_init_impl();
pthread_mutex_unlock(&g_state_mtx);
if (success) {
return true;
}
FOLK_ERROR("miniaudio: audio init failed\n");
return false;
}
$cc proc audioStop {ma_sound* target} void {
SoundNode* to_free = NULL;
ma_sound* snd = NULL;
pthread_mutex_lock(&g_list_mtx);
SoundNode** pprev = &g_head;
SoundNode* node = g_head;
/* Find the sound to stop. TODO: this duplicates traversal logic from the reaper thread */
while (node) {
if (node->snd == target) {
snd = node->snd;
node->snd = NULL;
*pprev = node->next;
to_free = node;
break;
}
pprev = &node->next;
node = node->next;
}
pthread_mutex_unlock(&g_list_mtx);
/* Sound already reaped or never registered */
if (!to_free) return;
if (snd) {
ma_sound_stop(snd);
ma_sound_uninit(snd);
free(snd);
snd = NULL;
}
free(to_free);
}
$cc proc playSound {char* path} ma_sound* {
ma_sound* snd = (ma_sound*)malloc(sizeof *snd);
if (!snd) {
FOLK_ERROR("miniaudio: playSound: alloc sound failed\n");
}
ma_result r = ma_sound_init_from_file(&g_engine,
path,
MA_SOUND_FLAG_DECODE | MA_SOUND_FLAG_NO_SPATIALIZATION,
&g_group,
NULL,
snd);
if (r != MA_SUCCESS) {
free(snd);
FOLK_ERROR("miniaudio: playSound: init failed for %s: %s (%d)\n",
path,
ma_result_description(r),
(int)r);
}
r = ma_sound_start(snd);
if (r != MA_SUCCESS) {
ma_sound_uninit(snd);
free(snd);
FOLK_ERROR("miniaudio: playSound: start failed for %s: %s (%d)\n",
path,
ma_result_description(r),
(int)r);
}
if (!registry_add(snd)) {
ma_sound_stop(snd);
ma_sound_uninit(snd);
free(snd);
FOLK_ERROR("miniaudio: playSound: registry add failed for %s\n", path);
}
fprintf(stderr, "miniaudio: playing %s\n", path);
return snd;
}
try {
set audioLib [$cc compile]
set success [$audioLib audioInit]
if {!$success} {
puts stderr "audio: init failed"
return
}
Claim the audio library is $audioLib
} on error e {
puts stderr "audio: compile failed: $e"
}
When the audio library is /audioLib/ &\
/someone/ wishes to play audio /sound/ {
# Check if the sound file exists in the user-programs/$hostname/sounds directory
# or in the working directory's assets/sounds subdirectory. Otherwise, assume
# it's an absolute path.
proc resolveSoundPath {filename} {
set scriptDir [file dirname [info script]]
set projectRoot [pwd]
set hostname [info hostname]
set path "$projectRoot/user-programs/$hostname/audio/$filename"
if {[file exists $path]} { return $path }
set path "$projectRoot/audio/$filename"
if {[file exists $path]} { return $path }
# treat as an absolute path
return $filename
}
set path [resolveSoundPath $sound]
if {![file exists $path]} {
puts stderr "audio: File not found '$path'"
return
}
set handle [$audioLib playSound $path]
if {$handle == 0} {
puts stderr "audio: Failed to play '$path'"
return
}
On unmatch {
puts "audio: stopping audio '$path'"
$audioLib audioStop $handle
}
}