diff options
| author | KonAki <konaki@anokonaki.org> | 2026-10-10 12:58:59 +0800 |
|---|---|---|
| committer | KonAki <konaki@anokonaki.org> | 2026-10-10 12:58:59 +0800 |
| commit | 4965c61e5576b516c9611acc2955057f0c6ad417 (patch) | |
| tree | 954e56842af67f1d696bd35bf5585593a79d6963 | |
| parent | f6003ea8bce29b930ca03687c1e9088858680415 (diff) | |
rename voices to audio, add loops, pitch, buses, fades, positional sound and mic module
| -rw-r--r-- | Makefile | 2 | ||||
| -rwxr-xr-x | build.sh | 8 | ||||
| -rw-r--r-- | examples/sound.c | 387 | ||||
| -rw-r--r-- | kon/konaudio.c | 479 | ||||
| -rw-r--r-- | kon/konaudio.h | 73 | ||||
| -rw-r--r-- | kon/konmic.c | 254 | ||||
| -rw-r--r-- | kon/konmic.h | 68 |
7 files changed, 1189 insertions, 82 deletions
@@ -22,7 +22,7 @@ PACKFLAGS ?= BIN = $(OBJDIR)/$(PROGRAM)$(EXE) PACKER = $(OBJDIR)/konpack$(EXE) MODELER = $(OBJDIR)/konmodel$(EXE) -EXAMPLES = modelview +EXAMPLES = modelview sound EXAMPLEBINS = $(EXAMPLES:%=$(OBJDIR)/%$(EXE)) PACKFILE = $(OBJDIR)/data.kpk CORE = $(OBJDIR)/engine.o $(OBJDIR)/konwinlib.o $(OBJDIR)/konsofren.o \ @@ -12,7 +12,7 @@ developer=0 usage() { echo "usage: build.sh [-d] [-D] [-p platform] [-m module,...] [-o objdir] [target ...]" - echo "modules: font toml tile pack ui debug assets image audio model" + echo "modules: font toml tile pack ui debug assets image audio mic model" echo "platforms: native linux bsd windows" echo "targets: all run clean tools pack examples" exit 1 @@ -57,7 +57,7 @@ windows) esac if [ $mode = debug ]; then - MODEFLAGS="-g -O0 -DKON_DEBUG" + MODEFLAGS="-g -O1 -DKON_DEBUG" developer=1 else MODEFLAGS="-O2 -DNDEBUG" @@ -81,9 +81,9 @@ for m in $modules; do assets) addobj konassets ;; model) addobj konmodel; addobj konassets ;; image) addobj stb_image ;; - audio) + audio|mic) addobj miniaudio - addobj konaudio + addobj "kon$m" case $platform in linux) LDLIBS="$LDLIBS -lpthread -ldl" ;; bsd) LDLIBS="$LDLIBS -lpthread" ;; diff --git a/examples/sound.c b/examples/sound.c new file mode 100644 index 0000000..f61ec06 --- /dev/null +++ b/examples/sound.c @@ -0,0 +1,387 @@ +/* + * sound: audio and mic playground, every sound is made in code so it needs no files + * + * build: ./build.sh -d -m audio,mic,debug examples + * run: obj/sound + * + * top down view, you are the green dot looking up the screen, the orange one circles around you + * and hums, so you can hear it go past. arrow keys move you, click puts a thump in the world + * space beeps, 1 and 2 crossfade the music, 0 stops it, m mutes the mic, r plays back the last 3 seconds of the mic + * the console (`) knows music, bus, and the variables master sfxvol musicvol pitch orbit micgain + */ + +#include <stdio.h> +#include <stdlib.h> +#include <string.h> +#include <math.h> + +#include "konprogram.h" +#include "konaudio.h" +#include "konmic.h" + +#if !defined(KON_MODULE_AUDIO) || !defined(KON_MODULE_MIC) || !defined(KON_MODULE_DEBUG) +#error "build with: ./build.sh -d -m audio,mic,debug examples" +#endif + +#define RATE 44100 +#define SCALE 12.0f + +typedef struct state { + kon_program_t *program; + kon_audio_t *audio; + kon_mic_t *mic; + + kon_sound_t beep, thump, hum, musicA, musicB; + kon_audioHandle_t humHandle, echoHandle; + kon_sound_t *echo; + + float listenerX, listenerZ; + float angle; + int moveX, moveZ; + float masterVolume, sfxVolume, musicVolume, pitch, orbit, micGain; + float pings[8][3]; +} state_t; + +/*** sounds made in code ***/ + +static int makeSound(kon_sound_t *sound, int frames) +{ + sound->frames = calloc((size_t)frames, sizeof(int16_t)); + sound->frameCount = (uint64_t)frames; + sound->channels = 1; + sound->sampleRate = RATE; + return sound->frames != NULL; +} + +static int16_t toSample(float x) +{ + if (x > 1.0f) x = 1.0f; + if (x < -1.0f) x = -1.0f; + return (int16_t)(x * 32000.0f); +} + +static void makeBeep(kon_sound_t *sound) +{ + int i, n = RATE / 4; + + makeSound(sound, n); + for (i = 0; i < n; i++) { + float t = (float)i / RATE; + sound->frames[i] = toSample(0.6f * sinf(6.2831853f * 880.0f * t) * expf(-t * 14.0f)); + } +} + +static void makeThump(kon_sound_t *sound) +{ + int i, n = RATE / 2; + float low = 0.0f; + unsigned seed = 12345; + + makeSound(sound, n); + for (i = 0; i < n; i++) { + float t = (float)i / RATE; + float noise; + + seed = seed * 1664525u + 1013904223u; + noise = (float)(seed >> 8) / 8388608.0f - 1.0f; + low += (noise - low) * 0.08f; + sound->frames[i] = toSample((low * 3.0f + sinf(6.2831853f * 60.0f * t) * 0.8f) * expf(-t * 9.0f)); + } +} + +/* one second that loops cleanly, every wave has a whole number of cycles in it */ +static void makeHum(kon_sound_t *sound) +{ + int i, n = RATE; + + makeSound(sound, n); + for (i = 0; i < n; i++) { + float t = (float)i / RATE; + float pulse = 0.6f + 0.4f * sinf(6.2831853f * 4.0f * t); + + sound->frames[i] = toSample(0.3f * pulse * (sinf(6.2831853f * 220.0f * t) + 0.5f * sinf(6.2831853f * 330.0f * t))); + } +} + +/* four seconds of an arpeggio over a chord, rootA and rootB are the frequency of the first note */ +static void makeMusic(kon_sound_t *sound, float root, const float *ratios) +{ + int i, n = RATE * 4; + + makeSound(sound, n); + for (i = 0; i < n; i++) { + float t = (float)i / RATE; + int step = (int)(t * 4.0f); + float local = t * 4.0f - (float)step; + float freq = root * ratios[step % 4]; + float pad = sinf(6.2831853f * root * 0.5f * t) * 0.15f; + float note = sinf(6.2831853f * freq * t) * expf(-local * 3.0f) * 0.35f; + + sound->frames[i] = toSample(pad + note); + } +} + +/*** commands ***/ + +static void cmdMusic(kon_debug_t *debug, void *user, int argc, char **argv) +{ + state_t *state = user; + + if (argc < 2) { + kon_debugLog(debug, "music a|b|off [fade seconds]"); + return; + } + if (strcmp(argv[1], "a") == 0) { + kon_audioMusic(state->audio, &state->musicA, NULL, argc > 2 ? (float)atof(argv[2]) : 1.5f); + } else if (strcmp(argv[1], "b") == 0) { + kon_audioMusic(state->audio, &state->musicB, NULL, argc > 2 ? (float)atof(argv[2]) : 1.5f); + } else if (strcmp(argv[1], "off") == 0) { + kon_audioMusicStop(state->audio, argc > 2 ? (float)atof(argv[2]) : 1.5f); + } else { + kon_debugLog(debug, "music a|b|off [fade seconds]"); + } +} + +static void cmdBus(kon_debug_t *debug, void *user, int argc, char **argv) +{ + static const char *names[KON_AUDIO_BUSES] = { "sfx", "music", "voice", "ui" }; + state_t *state = user; + int i; + + if (argc < 3) { + kon_debugLog(debug, "bus sfx|music|voice|ui <volume>"); + return; + } + for (i = 0; i < KON_AUDIO_BUSES; i++) { + if (strcmp(argv[1], names[i]) == 0) { + kon_audioSetBusVolume(state->audio, i, (float)atof(argv[2])); + return; + } + } + kon_debugLog(debug, "no bus called %s", argv[1]); +} + +/*** program ***/ + +static void toWorld(int screenX, int screenY, float *x, float *z) +{ + *x = ((float)screenX - 320.0f) / SCALE; + *z = ((float)screenY - 180.0f) / SCALE; +} + +static int event(kon_program_t *program, const kon_event_t *ev, void *user) +{ + state_t *state = user; + + if (ev->type == KON_EVENT_MOUSE_DOWN && ev->mouseButton == KON_MOUSE_LEFT) { + int gx, gy; + + if (kon_windowToGame(program->display, ev->mouseX, ev->mouseY, &gx, &gy)) { + float x, z; + int i; + + toWorld(gx, gy, &x, &z); + x += state->listenerX; + z += state->listenerZ; + kon_audioPlayAt(state->audio, &state->thump, 1.0f, x, 0.0f, z); + for (i = 7; i > 0; i--) memcpy(state->pings[i], state->pings[i - 1], sizeof state->pings[i]); + state->pings[0][0] = x; + state->pings[0][1] = z; + state->pings[0][2] = 1.0f; + } + } else if (ev->type == KON_EVENT_KEY_DOWN || ev->type == KON_EVENT_KEY_UP) { + int down = ev->type == KON_EVENT_KEY_DOWN; + + switch (ev->key) { + case KON_KEY_LEFT: state->moveX = down ? -1 : (state->moveX < 0 ? 0 : state->moveX); break; + case KON_KEY_RIGHT: state->moveX = down ? 1 : (state->moveX > 0 ? 0 : state->moveX); break; + case KON_KEY_UP: state->moveZ = down ? -1 : (state->moveZ < 0 ? 0 : state->moveZ); break; + case KON_KEY_DOWN: state->moveZ = down ? 1 : (state->moveZ > 0 ? 0 : state->moveZ); break; + default: break; + } + if (!down) return 1; + + if (ev->key == KON_KEY_SPACE) { + kon_audioParams_t p = kon_audioParamsDefault(); + + p.bus = KON_BUS_UI; + p.pan = ((float)rand() / (float)RAND_MAX) * 2.0f - 1.0f; + kon_audioPlayEx(state->audio, &state->beep, &p); + } else if (ev->key == KON_KEY_1) { + kon_audioMusic(state->audio, &state->musicA, NULL, 1.5f); + } else if (ev->key == KON_KEY_2) { + kon_audioMusic(state->audio, &state->musicB, NULL, 1.5f); + } else if (ev->key == KON_KEY_0) { + kon_audioMusicStop(state->audio, 1.5f); + } else if (ev->key == KON_KEY_M && state->mic) { + static int muted; + + muted = !muted; + kon_micMute(state->mic, muted); + kon_debugLog(program->debug, muted ? "mic muted" : "mic live"); + } else if (ev->key == KON_KEY_R && state->mic) { + kon_sound_t *heard = kon_micSnapshot(state->mic, 3.0f); + + if (heard) { + /* the sound has to stay alive while it plays, so the old one goes only when we are done with it */ + if (state->echo) { + kon_audioStop(state->audio, state->echoHandle); + free(state->echo); + } + state->echo = heard; + state->echoHandle = kon_audioPlayAt(state->audio, heard, 1.0f, state->listenerX + 6.0f, 0.0f, state->listenerZ - 6.0f); + } else { + kon_debugLog(program->debug, "the mic has not heard anything yet"); + } + } + } else { + return 0; + } + return 1; +} + +static void update(kon_program_t *program, double dt, void *user) +{ + state_t *state = user; + float ex, ez; + int i; + + (void)program; + state->angle += (float)dt * state->orbit; + state->listenerX += (float)state->moveX * 8.0f * (float)dt; + state->listenerZ += (float)state->moveZ * 8.0f * (float)dt; + + ex = state->listenerX + cosf(state->angle) * 10.0f; + ez = state->listenerZ + sinf(state->angle) * 10.0f; + + kon_audioSetListener(state->audio, state->listenerX, 0.0f, state->listenerZ, 0.0f, 0.0f, -1.0f, 0.0f, 1.0f, 0.0f); + kon_audioHandleSetPosition(state->audio, state->humHandle, ex, 0.0f, ez); + kon_audioHandleSetPitch(state->audio, state->humHandle, state->pitch); + + kon_audioSetVolume(state->audio, state->masterVolume); + kon_audioSetBusVolume(state->audio, KON_BUS_SFX, state->sfxVolume); + kon_audioSetBusVolume(state->audio, KON_BUS_MUSIC, state->musicVolume); + if (state->mic) kon_micSetGain(state->mic, state->micGain); + + for (i = 0; i < 8; i++) { + state->pings[i][2] -= (float)dt; + if (state->pings[i][2] < 0.0f) state->pings[i][2] = 0.0f; + } +} + +static void draw(kon_program_t *program, kon_framebuffer_t *game, void *user) +{ + state_t *state = user; + char text[96]; + int i, ex, ez, bar; + + kon_clearFramebuffer(game, 0xFF14161C); + + /* the range of the hum, full volume inside the small ring and nothing outside the big one */ + kon_drawCircle(game, 320, 180, (int)(2.0f * SCALE), 0xFF2A2E3A); + kon_drawCircle(game, 320, 180, (int)(22.0f * SCALE), 0xFF2A2E3A); + + for (i = 0; i < 8; i++) { + if (state->pings[i][2] <= 0.0f) continue; + kon_drawCircle(game, 320 + (int)((state->pings[i][0] - state->listenerX) * SCALE), 180 + (int)((state->pings[i][1] - state->listenerZ) * SCALE), (int)((1.0f - state->pings[i][2]) * 40.0f), 0xFF6A7FD0); + } + + ex = 320 + (int)(cosf(state->angle) * 10.0f * SCALE); + ez = 180 + (int)(sinf(state->angle) * 10.0f * SCALE); + kon_fillCircle(game, ex, ez, 6, 0xFFE0903A); + kon_fillCircle(game, 320, 180, 5, 0xFF5CD06A); + kon_drawLine(game, 320, 180, 320, 164, 0xFF5CD06A); + + if (state->mic) { + bar = (int)(kon_micLevel(state->mic) * 4.0f * 200.0f); + if (bar > 200) bar = 200; + kon_drawRectangle(game, 10, 340, 201, 9, 0xFF8890A0); + kon_fillRectangle(game, 11, 341, bar, 7, kon_micTalking(state->mic) ? 0xFF5CD06A : 0xFF6A7FD0); + } + + if (program->font) { + snprintf(text, sizeof text, "playing %d peak %.2f", kon_audioPlayingCount(state->audio), kon_audioPeak(state->audio)); + kon_drawText(game, program->font, 10, 20, text, 0xFFE0E0E0); + kon_drawText(game, program->font, 10, 36, "arrows move, click thump, space beep, 1 2 0 music, m r mic", 0xFF8890A0); + if (state->mic) kon_drawText(game, program->font, 220, 348, kon_micTalking(state->mic) ? "talking" : "quiet", 0xFFE0E0E0); + else kon_drawText(game, program->font, 10, 348, "no mic", 0xFF8890A0); + } +} + +static int init(kon_program_t *program, void *user) +{ + state_t *state = user; + kon_audioParams_t p = kon_audioParamsDefault(); + static const float majorRatios[4] = { 1.0f, 1.25f, 1.5f, 2.0f }; + static const float minorRatios[4] = { 1.0f, 1.2f, 1.5f, 1.8f }; + + state->program = program; + state->audio = kon_audioCreate(0); + if (!state->audio) { + fprintf(stderr, "sound: could not open an audio device\n"); + return 0; + } + state->mic = kon_micCreate(0, 5.0f); + if (!state->mic) kon_debugLog(program->debug, "no mic, the mic parts are off"); + + makeBeep(&state->beep); + makeThump(&state->thump); + makeHum(&state->hum); + makeMusic(&state->musicA, 220.0f, majorRatios); + makeMusic(&state->musicB, 196.0f, minorRatios); + + state->masterVolume = state->sfxVolume = state->musicVolume = state->pitch = state->micGain = 1.0f; + state->orbit = 0.7f; + state->angle = 0.0f; + + p.loop = 1; + p.positional = 1; + p.x = 10.0f; + p.minDistance = 2.0f; + p.maxDistance = 22.0f; + state->humHandle = kon_audioPlayEx(state->audio, &state->hum, &p); + + kon_debugCommand(program->debug, "music", "music a|b|off [fade seconds]", cmdMusic, state); + kon_debugCommand(program->debug, "bus", "bus sfx|music|voice|ui <volume>", cmdBus, state); + kon_debugVar(program->debug, "master", KON_VAR_FLOAT, &state->masterVolume); + kon_debugVar(program->debug, "sfxvol", KON_VAR_FLOAT, &state->sfxVolume); + kon_debugVar(program->debug, "musicvol", KON_VAR_FLOAT, &state->musicVolume); + kon_debugVar(program->debug, "pitch", KON_VAR_FLOAT, &state->pitch); + kon_debugVar(program->debug, "orbit", KON_VAR_FLOAT, &state->orbit); + kon_debugVar(program->debug, "micgain", KON_VAR_FLOAT, &state->micGain); + return 1; +} + +static void shutdown(kon_program_t *program, void *user) +{ + state_t *state = user; + + (void)program; + if (state->mic) kon_micDestroy(state->mic); + if (state->audio) kon_audioDestroy(state->audio); + free(state->echo); + free(state->beep.frames); + free(state->thump.frames); + free(state->hum.frames); + free(state->musicA.frames); + free(state->musicB.frames); +} + +int main(void) +{ + state_t state = {0}; + kon_programConfig_t config = {0}; + + config.title = "sound"; + config.gameWidth = 640; + config.gameHeight = 360; + config.windowWidth = 960; + config.windowHeight = 540; + config.user = &state; + config.init = init; + config.event = event; + config.update = update; + config.draw = draw; + config.shutdown = shutdown; + return kon_programRun(&config); +} diff --git a/kon/konaudio.c b/kon/konaudio.c index 94fb811..eba5ccc 100644 --- a/kon/konaudio.c +++ b/kon/konaudio.c @@ -26,31 +26,44 @@ #include "konaudio.h" +#include <math.h> #include <stdlib.h> #include <string.h> #include "miniaudio.h" -typedef struct kon_voiceSlot { +/*** types ***/ + +typedef struct kon_slot { const kon_sound_t *sound; double position; double step; - float volume; - float pan; - int loop; + kon_audioParams_t p; + float curL, curR; + float fade, fadeTarget, fadeStep; + int fadeStop; + int fresh; int active; uint32_t generation; -} kon_voiceSlot_t; +} kon_slot_t; struct kon_audio { - kon_voiceSlot_t voices[KON_AUDIO_VOICES]; - float master; + kon_slot_t slots[KON_AUDIO_MAX_PLAYING]; + float master, masterCur; + float bus[KON_AUDIO_BUSES]; + float lx, ly, lz; + float rx, ry, rz; + float peak; + kon_audioHandle_t music; + const kon_sound_t *musicSound; uint32_t rate; int hasDevice; ma_mutex lock; ma_device device; }; +/*** setup ***/ + static void kon_audioCallback_(ma_device *device, void *out, const void *in, ma_uint32 frames) { kon_audio_t *audio = device->pUserData; @@ -62,13 +75,16 @@ static void kon_audioCallback_(ma_device *device, void *out, const void *in, ma_ static kon_audio_t *kon_audioNew_(uint32_t sampleRate) { kon_audio_t *audio = calloc(1, sizeof(kon_audio_t)); + int i; if (!audio) return NULL; if (ma_mutex_init(&audio->lock) != MA_SUCCESS) { free(audio); return NULL; } - audio->master = 1.0f; + audio->master = audio->masterCur = 1.0f; + for (i = 0; i < KON_AUDIO_BUSES; i++) audio->bus[i] = 1.0f; + audio->rx = 1.0f; audio->rate = sampleRate ? sampleRate : 48000; return audio; } @@ -120,55 +136,113 @@ uint32_t kon_audioRate(const kon_audio_t *audio) return audio->rate; } -kon_voice_t kon_audioPlay(kon_audio_t *audio, const kon_sound_t *sound, float volume, float pan, int loop) +/*** playing ***/ + +kon_audioParams_t kon_audioParamsDefault(void) +{ + kon_audioParams_t p; + + memset(&p, 0, sizeof(p)); + p.volume = 1.0f; + p.pitch = 1.0f; + p.bus = KON_BUS_SFX; + p.minDistance = 1.0f; + p.maxDistance = 20.0f; + return p; +} + +static float kon_clampf_(float x, float lo, float hi) +{ + return x < lo ? lo : (x > hi ? hi : x); +} + +static kon_slot_t *kon_find_(kon_audio_t *audio, kon_audioHandle_t handle) +{ + uint32_t index = handle & 0xFF; + kon_slot_t *s; + + if (index == 0 || index > KON_AUDIO_MAX_PLAYING) return NULL; + s = &audio->slots[index - 1]; + if (!s->active || s->generation != (handle >> 8)) return NULL; + return s; +} + +/* lock has to be held */ +static kon_audioHandle_t kon_start_(kon_audio_t *audio, const kon_sound_t *sound, const kon_audioParams_t *params) { - kon_voice_t handle = 0; int i; - if (!audio || !sound || !sound->frames || sound->frameCount == 0 || sound->channels == 0 || sound->sampleRate == 0) return 0; + if (!sound || !sound->frames || sound->frameCount == 0 || sound->channels == 0 || sound->sampleRate == 0) return 0; + + for (i = 0; i < KON_AUDIO_MAX_PLAYING; i++) { + kon_slot_t *s = &audio->slots[i]; + + if (s->active) continue; + s->generation++; + if (s->generation == 0) s->generation = 1; + s->sound = sound; + s->position = 0.0; + s->p = *params; + s->p.volume = s->p.volume < 0.0f ? 0.0f : s->p.volume; + s->p.pan = kon_clampf_(s->p.pan, -1.0f, 1.0f); + s->p.pitch = kon_clampf_(s->p.pitch, 0.05f, 8.0f); + if (s->p.bus < 0 || s->p.bus >= KON_AUDIO_BUSES) s->p.bus = KON_BUS_SFX; + if (s->p.loopStart >= sound->frameCount) s->p.loopStart = 0; + if (s->p.loopEnd > sound->frameCount || s->p.loopEnd <= s->p.loopStart) s->p.loopEnd = 0; + if (s->p.minDistance < 0.0f) s->p.minDistance = 0.0f; + if (s->p.maxDistance <= s->p.minDistance) s->p.maxDistance = s->p.minDistance + 0.001f; + s->step = (double)sound->sampleRate / (double)audio->rate; + s->fade = s->fadeTarget = 1.0f; + s->fadeStep = 0.0f; + s->fadeStop = 0; + s->fresh = 1; + s->active = 1; + return (s->generation << 8) | (uint32_t)(i + 1); + } + return 0; +} - if (pan < -1.0f) pan = -1.0f; - if (pan > 1.0f) pan = 1.0f; +kon_audioHandle_t kon_audioPlayEx(kon_audio_t *audio, const kon_sound_t *sound, const kon_audioParams_t *params) +{ + kon_audioParams_t defaults = kon_audioParamsDefault(); + kon_audioHandle_t handle; + if (!audio) return 0; ma_mutex_lock(&audio->lock); - for (i = 0; i < KON_AUDIO_VOICES; i++) { - kon_voiceSlot_t *v = &audio->voices[i]; - - if (v->active) continue; - v->generation++; - if (v->generation == 0) v->generation = 1; - v->sound = sound; - v->position = 0.0; - v->step = (double)sound->sampleRate / (double)audio->rate; - v->volume = volume; - v->pan = pan; - v->loop = loop; - v->active = 1; - handle = (v->generation << 8) | (uint32_t)(i + 1); - break; - } + handle = kon_start_(audio, sound, params ? params : &defaults); ma_mutex_unlock(&audio->lock); return handle; } -static kon_voiceSlot_t *kon_audioFind_(kon_audio_t *audio, kon_voice_t handle) +kon_audioHandle_t kon_audioPlay(kon_audio_t *audio, const kon_sound_t *sound, float volume, float pan, int loop) { - uint32_t index = handle & 0xFF; - kon_voiceSlot_t *v; + kon_audioParams_t p = kon_audioParamsDefault(); - if (index == 0 || index > KON_AUDIO_VOICES) return NULL; - v = &audio->voices[index - 1]; - if (!v->active || v->generation != (handle >> 8)) return NULL; - return v; + p.volume = volume; + p.pan = pan; + p.loop = loop; + return kon_audioPlayEx(audio, sound, &p); } -void kon_audioStop(kon_audio_t *audio, kon_voice_t voice) +kon_audioHandle_t kon_audioPlayAt(kon_audio_t *audio, const kon_sound_t *sound, float volume, float x, float y, float z) { - kon_voiceSlot_t *v; + kon_audioParams_t p = kon_audioParamsDefault(); + + p.volume = volume; + p.positional = 1; + p.x = x; + p.y = y; + p.z = z; + return kon_audioPlayEx(audio, sound, &p); +} + +void kon_audioStop(kon_audio_t *audio, kon_audioHandle_t handle) +{ + kon_slot_t *s; ma_mutex_lock(&audio->lock); - v = kon_audioFind_(audio, voice); - if (v) v->active = 0; + s = kon_find_(audio, handle); + if (s) s->active = 0; ma_mutex_unlock(&audio->lock); } @@ -177,8 +251,8 @@ void kon_audioStopSound(kon_audio_t *audio, const kon_sound_t *sound) int i; ma_mutex_lock(&audio->lock); - for (i = 0; i < KON_AUDIO_VOICES; i++) { - if (audio->voices[i].active && audio->voices[i].sound == sound) audio->voices[i].active = 0; + for (i = 0; i < KON_AUDIO_MAX_PLAYING; i++) { + if (audio->slots[i].active && audio->slots[i].sound == sound) audio->slots[i].active = 0; } ma_mutex_unlock(&audio->lock); } @@ -188,30 +262,167 @@ void kon_audioStopAll(kon_audio_t *audio) int i; ma_mutex_lock(&audio->lock); - for (i = 0; i < KON_AUDIO_VOICES; i++) audio->voices[i].active = 0; + for (i = 0; i < KON_AUDIO_MAX_PLAYING; i++) audio->slots[i].active = 0; ma_mutex_unlock(&audio->lock); } -int kon_audioIsPlaying(kon_audio_t *audio, kon_voice_t voice) +int kon_audioIsPlaying(kon_audio_t *audio, kon_audioHandle_t handle) { int playing; ma_mutex_lock(&audio->lock); - playing = kon_audioFind_(audio, voice) != NULL; + playing = kon_find_(audio, handle) != NULL; ma_mutex_unlock(&audio->lock); return playing; } -int kon_audioVoiceCount(kon_audio_t *audio) +int kon_audioPlayingCount(kon_audio_t *audio) { int i, count = 0; ma_mutex_lock(&audio->lock); - for (i = 0; i < KON_AUDIO_VOICES; i++) count += audio->voices[i].active; + for (i = 0; i < KON_AUDIO_MAX_PLAYING; i++) count += audio->slots[i].active; ma_mutex_unlock(&audio->lock); return count; } +void kon_audioHandleSetVolume(kon_audio_t *audio, kon_audioHandle_t handle, float volume) +{ + kon_slot_t *s; + + ma_mutex_lock(&audio->lock); + s = kon_find_(audio, handle); + if (s) s->p.volume = volume < 0.0f ? 0.0f : volume; + ma_mutex_unlock(&audio->lock); +} + +void kon_audioHandleSetPan(kon_audio_t *audio, kon_audioHandle_t handle, float pan) +{ + kon_slot_t *s; + + ma_mutex_lock(&audio->lock); + s = kon_find_(audio, handle); + if (s) s->p.pan = kon_clampf_(pan, -1.0f, 1.0f); + ma_mutex_unlock(&audio->lock); +} + +void kon_audioHandleSetPitch(kon_audio_t *audio, kon_audioHandle_t handle, float pitch) +{ + kon_slot_t *s; + + ma_mutex_lock(&audio->lock); + s = kon_find_(audio, handle); + if (s) s->p.pitch = kon_clampf_(pitch, 0.05f, 8.0f); + ma_mutex_unlock(&audio->lock); +} + +void kon_audioHandleSetPosition(kon_audio_t *audio, kon_audioHandle_t handle, float x, float y, float z) +{ + kon_slot_t *s; + + ma_mutex_lock(&audio->lock); + s = kon_find_(audio, handle); + if (s) { + s->p.positional = 1; + s->p.x = x; + s->p.y = y; + s->p.z = z; + } + ma_mutex_unlock(&audio->lock); +} + +/* lock has to be held */ +static void kon_fade_(kon_audio_t *audio, kon_slot_t *s, float target, float seconds, int stopAtEnd) +{ + float frames = seconds * (float)audio->rate; + + target = kon_clampf_(target, 0.0f, 1.0f); + s->fadeTarget = target; + s->fadeStop = stopAtEnd; + + if (frames < 1.0f) { + s->fade = target; + s->fadeStep = 0.0f; + if (stopAtEnd) s->active = 0; + return; + } + s->fadeStep = (target - s->fade) / frames; + if (s->fadeStep == 0.0f && stopAtEnd) s->active = 0; +} + +void kon_audioHandleFade(kon_audio_t *audio, kon_audioHandle_t handle, float target, float seconds, int stopAtEnd) +{ + kon_slot_t *s; + + ma_mutex_lock(&audio->lock); + s = kon_find_(audio, handle); + if (s) kon_fade_(audio, s, target, seconds, stopAtEnd); + ma_mutex_unlock(&audio->lock); +} + +/*** music ***/ + +kon_audioHandle_t kon_audioMusic(kon_audio_t *audio, const kon_sound_t *sound, const kon_audioParams_t *params, float fadeSeconds) +{ + kon_audioParams_t p = params ? *params : kon_audioParamsDefault(); + kon_slot_t *old, *s; + kon_audioHandle_t handle; + + ma_mutex_lock(&audio->lock); + old = kon_find_(audio, audio->music); + + if (old && audio->musicSound == sound && !(old->fadeStop && old->fadeTarget == 0.0f)) { + handle = audio->music; + ma_mutex_unlock(&audio->lock); + return handle; + } + + p.loop = 1; + p.bus = KON_BUS_MUSIC; + p.positional = 0; + handle = kon_start_(audio, sound, &p); + if (!handle) { + ma_mutex_unlock(&audio->lock); + return 0; + } + if (old) kon_fade_(audio, old, 0.0f, fadeSeconds, 1); + s = kon_find_(audio, handle); + if (fadeSeconds > 0.0f) { + s->fade = 0.0f; + kon_fade_(audio, s, 1.0f, fadeSeconds, 0); + } + audio->music = handle; + audio->musicSound = sound; + ma_mutex_unlock(&audio->lock); + return handle; +} + +void kon_audioMusicStop(kon_audio_t *audio, float fadeSeconds) +{ + kon_slot_t *s; + + ma_mutex_lock(&audio->lock); + s = kon_find_(audio, audio->music); + if (s) kon_fade_(audio, s, 0.0f, fadeSeconds, 1); + audio->music = 0; + audio->musicSound = NULL; + ma_mutex_unlock(&audio->lock); +} + +const kon_sound_t *kon_audioMusicCurrent(kon_audio_t *audio) +{ + const kon_sound_t *sound = NULL; + kon_slot_t *s; + + ma_mutex_lock(&audio->lock); + s = kon_find_(audio, audio->music); + if (s && !(s->fadeStop && s->fadeTarget == 0.0f)) sound = audio->musicSound; + ma_mutex_unlock(&audio->lock); + return sound; +} + +/*** mixing ***/ + void kon_audioSetVolume(kon_audio_t *audio, float volume) { if (volume < 0.0f) volume = 0.0f; @@ -220,58 +431,188 @@ void kon_audioSetVolume(kon_audio_t *audio, float volume) ma_mutex_unlock(&audio->lock); } +void kon_audioSetBusVolume(kon_audio_t *audio, int bus, float volume) +{ + if (bus < 0 || bus >= KON_AUDIO_BUSES) return; + ma_mutex_lock(&audio->lock); + audio->bus[bus] = volume < 0.0f ? 0.0f : volume; + ma_mutex_unlock(&audio->lock); +} + +void kon_audioSetListener(kon_audio_t *audio, float x, float y, float z, float fx, float fy, float fz, float ux, float uy, float uz) +{ + float rx = fy * uz - fz * uy; + float ry = fz * ux - fx * uz; + float rz = fx * uy - fy * ux; + float len = sqrtf(rx * rx + ry * ry + rz * rz); + + if (len < 1e-6f) { + rx = 1.0f; + ry = rz = 0.0f; + } else { + rx /= len; + ry /= len; + rz /= len; + } + + ma_mutex_lock(&audio->lock); + audio->lx = x; + audio->ly = y; + audio->lz = z; + audio->rx = rx; + audio->ry = ry; + audio->rz = rz; + ma_mutex_unlock(&audio->lock); +} + +float kon_audioPeak(kon_audio_t *audio) +{ + float peak; + + ma_mutex_lock(&audio->lock); + peak = audio->peak; + ma_mutex_unlock(&audio->lock); + return peak; +} + +/* distance falloff and pan for one positional sound */ +static void kon_spatial_(const kon_audio_t *audio, const kon_audioParams_t *p, float *gain, float *pan) +{ + float dx = p->x - audio->lx, dy = p->y - audio->ly, dz = p->z - audio->lz; + float dist = sqrtf(dx * dx + dy * dy + dz * dz); + float t; + + if (dist <= p->minDistance) { + t = 0.0f; + } else if (dist >= p->maxDistance) { + t = 1.0f; + } else { + t = (dist - p->minDistance) / (p->maxDistance - p->minDistance); + } + *gain = (1.0f - t) * (1.0f - t); + + if (dist < 1e-4f) { + *pan = 0.0f; + return; + } + *pan = (dx * audio->rx + dy * audio->ry + dz * audio->rz) / dist; + + /* right on top of the listener there is no side to be on */ + if (dist < p->minDistance && p->minDistance > 0.0f) *pan *= dist / p->minDistance; +} + void kon_audioMix(kon_audio_t *audio, float *out, uint32_t frames) { uint32_t f; int i; + float peak = 0.0f; + float invFrames = frames ? 1.0f / (float)frames : 0.0f; memset(out, 0, (size_t)frames * 2 * sizeof(float)); ma_mutex_lock(&audio->lock); - for (i = 0; i < KON_AUDIO_VOICES; i++) { - kon_voiceSlot_t *v = &audio->voices[i]; + for (i = 0; i < KON_AUDIO_MAX_PLAYING; i++) { + kon_slot_t *v = &audio->slots[i]; const kon_sound_t *s = v->sound; - float gainL, gainR; + const kon_audioParams_t *p = &v->p; + uint64_t end, loopLen; + float vol, pan, targetL, targetR, gl, gr, dgl, dgr; + double step; if (!v->active) continue; - gainL = v->volume * (v->pan > 0.0f ? 1.0f - v->pan : 1.0f); - gainR = v->volume * (v->pan < 0.0f ? 1.0f + v->pan : 1.0f); + vol = p->volume * audio->bus[p->bus]; + pan = p->pan; + if (p->positional) { + float g; + + kon_spatial_(audio, p, &g, &pan); + vol *= g; + } + targetL = vol * (pan > 0.0f ? 1.0f - pan : 1.0f); + targetR = vol * (pan < 0.0f ? 1.0f + pan : 1.0f); + + if (v->fresh) { + v->curL = targetL; + v->curR = targetR; + v->fresh = 0; + } + gl = v->curL; + gr = v->curR; + dgl = (targetL - gl) * invFrames; + dgr = (targetR - gr) * invFrames; + + step = v->step * p->pitch; + end = p->loop && p->loopEnd ? p->loopEnd : s->frameCount; + loopLen = end - p->loopStart; for (f = 0; f < frames; f++) { uint64_t a = (uint64_t)v->position, b; - float t, l0, r0, l1, r1; - const int16_t *p; + float t, l0, r0, l1, r1, fade = v->fade; + const int16_t *q; - if (a >= s->frameCount) { - if (!v->loop) { + if (a >= end) { + if (!p->loop) { v->active = 0; break; } - v->position -= (double)s->frameCount; + v->position -= (double)loopLen * (double)((uint64_t)((v->position - (double)p->loopStart) / (double)loopLen)); a = (uint64_t)v->position; } b = a + 1; - if (b >= s->frameCount) b = v->loop ? 0 : a; + if (b >= end) b = p->loop ? p->loopStart : a; t = (float)(v->position - (double)a); - p = s->frames + a * s->channels; - l0 = p[0] / 32768.0f; - r0 = (s->channels > 1 ? p[1] : p[0]) / 32768.0f; - p = s->frames + b * s->channels; - l1 = p[0] / 32768.0f; - r1 = (s->channels > 1 ? p[1] : p[0]) / 32768.0f; + q = s->frames + a * s->channels; + l0 = q[0] / 32768.0f; + r0 = (s->channels > 1 ? q[1] : q[0]) / 32768.0f; + q = s->frames + b * s->channels; + l1 = q[0] / 32768.0f; + r1 = (s->channels > 1 ? q[1] : q[0]) / 32768.0f; + + gl += dgl; + gr += dgr; + out[f * 2] += (l0 + (l1 - l0) * t) * gl * fade; + out[f * 2 + 1] += (r0 + (r1 - r0) * t) * gr * fade; + v->position += step; + + if (v->fadeStep != 0.0f) { + v->fade += v->fadeStep; + if ((v->fadeStep > 0.0f && v->fade >= v->fadeTarget) || (v->fadeStep < 0.0f && v->fade <= v->fadeTarget)) { + v->fade = v->fadeTarget; + v->fadeStep = 0.0f; + if (v->fadeStop) { + v->active = 0; + break; + } + } + } + } + v->curL = targetL; + v->curR = targetR; + } + + { + float g = audio->masterCur; + float dg = (audio->master - g) * invFrames; + + for (f = 0; f < frames; f++) { + float x; - out[f * 2] += (l0 + (l1 - l0) * t) * gainL; - out[f * 2 + 1] += (r0 + (r1 - r0) * t) * gainR; - v->position += v->step; + g += dg; + x = out[f * 2] * g; + out[f * 2] = x > 1.0f ? 1.0f : (x < -1.0f ? -1.0f : x); + x = out[f * 2 + 1] * g; + out[f * 2 + 1] = x > 1.0f ? 1.0f : (x < -1.0f ? -1.0f : x); } + audio->masterCur = audio->master; } for (f = 0; f < frames * 2; f++) { - float x = out[f] * audio->master; + float x = out[f] < 0.0f ? -out[f] : out[f]; - out[f] = x > 1.0f ? 1.0f : (x < -1.0f ? -1.0f : x); + if (x > peak) peak = x; } + audio->peak = peak; ma_mutex_unlock(&audio->lock); } diff --git a/kon/konaudio.h b/kon/konaudio.h index f40e190..ef8b957 100644 --- a/kon/konaudio.h +++ b/kon/konaudio.h @@ -32,10 +32,31 @@ /*** definition ***/ -#define KON_AUDIO_VOICES 32 +#define KON_AUDIO_MAX_PLAYING 64 +#define KON_AUDIO_BUSES 4 + +/* buses are just groups with their own volume. what they mean is up to you */ +#define KON_BUS_SFX 0 +#define KON_BUS_MUSIC 1 +#define KON_BUS_VOICE 2 +#define KON_BUS_UI 3 typedef struct kon_audio kon_audio_t; -typedef uint32_t kon_voice_t; /* 0 means no voice */ +typedef uint32_t kon_audioHandle_t; /* 0 means nothing is playing */ + +typedef struct kon_audioParams { + float volume; /* 1 */ + float pan; /* -1 left to 1 right, ignored when positional */ + float pitch; /* 1 is normal, 2 is an octave up and twice as fast */ + int loop; + uint64_t loopStart; /* frame to jump back to */ + uint64_t loopEnd; /* frame to jump from, 0 means the end of the sound */ + int bus; + int positional; + float x, y, z; + float minDistance; /* full volume inside this */ + float maxDistance; /* silent outside this */ +} kon_audioParams_t; /* opens the default output device. the device decides the real rate, see kon_audioRate */ kon_audio_t *kon_audioCreate(uint32_t sampleRate); @@ -46,19 +67,55 @@ void kon_audioDestroy(kon_audio_t *audio); uint32_t kon_audioRate(const kon_audio_t *audio); -/* pan goes from -1 (left) to 1 (right). returns 0 when all voices are busy. - the sound has to stay alive until the voice is done or stopped, see kon_audioStopSound */ -kon_voice_t kon_audioPlay(kon_audio_t *audio, const kon_sound_t *sound, float volume, float pan, int loop); +/*** playing ***/ + +/* volume 1, pan 0, pitch 1, sfx bus, positional range 1 to 20 */ +kon_audioParams_t kon_audioParamsDefault(void); -void kon_audioStop(kon_audio_t *audio, kon_voice_t voice); +/* pan goes from -1 (left) to 1 (right). returns 0 when everything is busy. + the sound has to stay alive until it is done or stopped, see kon_audioStopSound */ +kon_audioHandle_t kon_audioPlay(kon_audio_t *audio, const kon_sound_t *sound, float volume, float pan, int loop); +kon_audioHandle_t kon_audioPlayEx(kon_audio_t *audio, const kon_sound_t *sound, const kon_audioParams_t *params); + +/* shortcut for a sound in the world, use mono sounds for these */ +kon_audioHandle_t kon_audioPlayAt(kon_audio_t *audio, const kon_sound_t *sound, float volume, float x, float y, float z); + +void kon_audioStop(kon_audio_t *audio, kon_audioHandle_t handle); void kon_audioStopSound(kon_audio_t *audio, const kon_sound_t *sound); void kon_audioStopAll(kon_audio_t *audio); -int kon_audioIsPlaying(kon_audio_t *audio, kon_voice_t voice); -int kon_audioVoiceCount(kon_audio_t *audio); +int kon_audioIsPlaying(kon_audio_t *audio, kon_audioHandle_t handle); +int kon_audioPlayingCount(kon_audio_t *audio); + +/* all of these do nothing on a handle that is already finished */ +void kon_audioHandleSetVolume(kon_audio_t *audio, kon_audioHandle_t handle, float volume); +void kon_audioHandleSetPan(kon_audio_t *audio, kon_audioHandle_t handle, float pan); +void kon_audioHandleSetPitch(kon_audio_t *audio, kon_audioHandle_t handle, float pitch); +void kon_audioHandleSetPosition(kon_audio_t *audio, kon_audioHandle_t handle, float x, float y, float z); + +/* moves the handle's own fade to target (0 to 1) over some seconds. stopAtEnd ends it once it arrives */ +void kon_audioHandleFade(kon_audio_t *audio, kon_audioHandle_t handle, float target, float seconds, int stopAtEnd); + +/*** mixing ***/ void kon_audioSetVolume(kon_audio_t *audio, float volume); +void kon_audioSetBusVolume(kon_audio_t *audio, int bus, float volume); + +/* the ears for positional sounds. forward and up do not have to be normalised. + starts at the origin looking down -z with y up, so +x is right */ +void kon_audioSetListener(kon_audio_t *audio, float x, float y, float z, float fx, float fy, float fz, float ux, float uy, float uz); + +/* biggest sample of the last mix, after master volume. for meters */ +float kon_audioPeak(kon_audio_t *audio); /* mixes interleaved stereo float frames. the device calls this, offline you call it */ void kon_audioMix(kon_audio_t *audio, float *out, uint32_t frames); +/*** music ***/ + +/* one looping track on the music bus. a new sound crossfades from the old one. + params can be NULL, loop and bus are always set for you. asking for the track that already plays does nothing */ +kon_audioHandle_t kon_audioMusic(kon_audio_t *audio, const kon_sound_t *sound, const kon_audioParams_t *params, float fadeSeconds); +void kon_audioMusicStop(kon_audio_t *audio, float fadeSeconds); +const kon_sound_t *kon_audioMusicCurrent(kon_audio_t *audio); + #endif diff --git a/kon/konmic.c b/kon/konmic.c new file mode 100644 index 0000000..d8628c4 --- /dev/null +++ b/kon/konmic.c @@ -0,0 +1,254 @@ +/* SPDX-License-Identifier: MIT */ +/* + * konmic.c + * + * Copyright (c) 2026 KonAki + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all + * copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE + * SOFTWARE. + */ + + +#include "konmic.h" + +#include <math.h> +#include <stdlib.h> +#include <string.h> + +#include "miniaudio.h" + +/*** types ***/ + +struct kon_mic { + float *ring; + uint32_t size; + uint32_t head; + uint64_t total; + float gain; + float level; + float threshold; + float hold; + float holdLeft; + int muted; + uint32_t rate; + int hasDevice; + ma_mutex lock; + ma_device device; +}; + +/*** setup ***/ + +static void kon_micCallback_(ma_device *device, void *out, const void *in, ma_uint32 frames) +{ + (void)out; + kon_micFeed(device->pUserData, in, frames); +} + +static kon_mic_t *kon_micNew_(uint32_t sampleRate, float historySeconds) +{ + kon_mic_t *mic = calloc(1, sizeof(kon_mic_t)); + + if (!mic) return NULL; + mic->rate = sampleRate ? sampleRate : 48000; + if (historySeconds < 0.1f) historySeconds = 0.1f; + if (historySeconds > 600.0f) historySeconds = 600.0f; + mic->size = (uint32_t)(historySeconds * (float)mic->rate); + mic->ring = calloc(mic->size, sizeof(float)); + if (!mic->ring || ma_mutex_init(&mic->lock) != MA_SUCCESS) { + free(mic->ring); + free(mic); + return NULL; + } + mic->gain = 1.0f; + mic->threshold = 0.02f; + mic->hold = 0.25f; + return mic; +} + +kon_mic_t *kon_micCreateOffline(uint32_t sampleRate, float historySeconds) +{ + return kon_micNew_(sampleRate, historySeconds); +} + +kon_mic_t *kon_micCreate(uint32_t sampleRate, float historySeconds) +{ + kon_mic_t *mic = kon_micNew_(sampleRate, historySeconds); + ma_device_config config; + + if (!mic) return NULL; + + config = ma_device_config_init(ma_device_type_capture); + config.capture.format = ma_format_f32; + config.capture.channels = 1; + config.sampleRate = mic->rate; + config.dataCallback = kon_micCallback_; + config.pUserData = mic; + + if (ma_device_init(NULL, &config, &mic->device) != MA_SUCCESS) { + ma_mutex_uninit(&mic->lock); + free(mic->ring); + free(mic); + return NULL; + } + mic->rate = mic->device.sampleRate; + mic->hasDevice = 1; + + if (ma_device_start(&mic->device) != MA_SUCCESS) { + kon_micDestroy(mic); + return NULL; + } + return mic; +} + +void kon_micDestroy(kon_mic_t *mic) +{ + if (!mic) return; + if (mic->hasDevice) ma_device_uninit(&mic->device); + ma_mutex_uninit(&mic->lock); + free(mic->ring); + free(mic); +} + +uint32_t kon_micRate(const kon_mic_t *mic) +{ + return mic->rate; +} + +/*** input ***/ + +void kon_micFeed(kon_mic_t *mic, const float *samples, uint32_t count) +{ + uint32_t i; + double sum = 0.0; + float rms, dt, tau; + + if (!mic || !samples || count == 0) return; + + ma_mutex_lock(&mic->lock); + for (i = 0; i < count; i++) { + float x = mic->muted ? 0.0f : samples[i] * mic->gain; + + if (x > 1.0f) x = 1.0f; + if (x < -1.0f) x = -1.0f; + mic->ring[mic->head] = x; + mic->head = mic->head + 1 == mic->size ? 0 : mic->head + 1; + sum += (double)x * (double)x; + } + mic->total += count; + + rms = (float)sqrt(sum / (double)count); + dt = (float)count / (float)mic->rate; + tau = rms > mic->level ? 0.02f : 0.15f; + mic->level += (rms - mic->level) * (1.0f - expf(-dt / tau)); + if (mic->level < 1e-5f) mic->level = 0.0f; + + if (rms >= mic->threshold && rms > 0.0f) { + mic->holdLeft = mic->hold; + } else { + mic->holdLeft -= dt; + if (mic->holdLeft < 0.0f) mic->holdLeft = 0.0f; + } + if (mic->muted) { + mic->level = 0.0f; + mic->holdLeft = 0.0f; + } + ma_mutex_unlock(&mic->lock); +} + +void kon_micSetGain(kon_mic_t *mic, float gain) +{ + ma_mutex_lock(&mic->lock); + mic->gain = gain < 0.0f ? 0.0f : gain; + ma_mutex_unlock(&mic->lock); +} + +void kon_micMute(kon_mic_t *mic, int muted) +{ + ma_mutex_lock(&mic->lock); + mic->muted = muted != 0; + ma_mutex_unlock(&mic->lock); +} + +void kon_micSetTalking(kon_mic_t *mic, float threshold, float holdSeconds) +{ + ma_mutex_lock(&mic->lock); + mic->threshold = threshold < 0.0f ? 0.0f : threshold; + mic->hold = holdSeconds < 0.0f ? 0.0f : holdSeconds; + ma_mutex_unlock(&mic->lock); +} + +/*** reading ***/ + +float kon_micLevel(kon_mic_t *mic) +{ + float level; + + ma_mutex_lock(&mic->lock); + level = mic->level; + ma_mutex_unlock(&mic->lock); + return level; +} + +int kon_micTalking(kon_mic_t *mic) +{ + int talking; + + ma_mutex_lock(&mic->lock); + talking = mic->holdLeft > 0.0f; + ma_mutex_unlock(&mic->lock); + return talking; +} + +kon_sound_t *kon_micSnapshot(kon_mic_t *mic, float seconds) +{ + kon_sound_t *sound; + int16_t *frames; + uint64_t want, have; + uint32_t start, i; + + if (seconds <= 0.0f) return NULL; + + ma_mutex_lock(&mic->lock); + have = mic->total < mic->size ? mic->total : mic->size; + want = (uint64_t)(seconds * (float)mic->rate); + if (want > have) want = have; + if (want == 0) { + ma_mutex_unlock(&mic->lock); + return NULL; + } + + sound = malloc(sizeof(kon_sound_t) + (size_t)want * sizeof(int16_t)); + if (!sound) { + ma_mutex_unlock(&mic->lock); + return NULL; + } + frames = (int16_t *)(sound + 1); + start = (uint32_t)((mic->head + mic->size - want) % mic->size); + for (i = 0; i < want; i++) { + float x = mic->ring[(start + i) % mic->size]; + + frames[i] = (int16_t)(x * 32767.0f); + } + ma_mutex_unlock(&mic->lock); + + sound->frames = frames; + sound->frameCount = want; + sound->channels = 1; + sound->sampleRate = mic->rate; + return sound; +} diff --git a/kon/konmic.h b/kon/konmic.h new file mode 100644 index 0000000..5a6a5a3 --- /dev/null +++ b/kon/konmic.h @@ -0,0 +1,68 @@ +/* SPDX-License-Identifier: MIT */ +/* + * konmic.h + * + * Copyright (c) 2026 KonAki + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all + * copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE + * SOFTWARE. + */ + +#ifndef KONMIC_H +#define KONMIC_H + +#include <stdint.h> + +#include "konassets.h" + +/*** definition ***/ + +typedef struct kon_mic kon_mic_t; + +/* opens the default capture device in mono. keeps the last historySeconds of sound around. + returns NULL when there is no mic or the system says no */ +kon_mic_t *kon_micCreate(uint32_t sampleRate, float historySeconds); + +/* no device. you push samples yourself with kon_micFeed, for tests or recorded input */ +kon_mic_t *kon_micCreateOffline(uint32_t sampleRate, float historySeconds); +void kon_micDestroy(kon_mic_t *mic); + +uint32_t kon_micRate(const kon_mic_t *mic); + +/* mono float samples. the device calls this, offline you call it */ +void kon_micFeed(kon_mic_t *mic, const float *samples, uint32_t count); + +/* multiplies what comes in, 1 by default */ +void kon_micSetGain(kon_mic_t *mic, float gain); + +/* a muted mic hears silence, level and talking drop to zero */ +void kon_micMute(kon_mic_t *mic, int muted); + +/* talking is true while the sound is over threshold, and for holdSeconds after it drops. + defaults are 0.02 and 0.25 */ +void kon_micSetTalking(kon_mic_t *mic, float threshold, float holdSeconds); + +/* how loud it is right now, 0 to 1. rises fast and falls slowly so it works for a mouth */ +float kon_micLevel(kon_mic_t *mic); +int kon_micTalking(kon_mic_t *mic); + +/* the last few seconds as a sound you can hand to konaudio. NULL when nothing was heard yet. + it is a single allocation, free() it when you are done */ +kon_sound_t *kon_micSnapshot(kon_mic_t *mic, float seconds); + +#endif |
