summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorKonAki <konaki@anokonaki.org>2026-10-10 12:58:59 +0800
committerKonAki <konaki@anokonaki.org>2026-10-10 12:58:59 +0800
commit4965c61e5576b516c9611acc2955057f0c6ad417 (patch)
tree954e56842af67f1d696bd35bf5585593a79d6963
parentf6003ea8bce29b930ca03687c1e9088858680415 (diff)
rename voices to audio, add loops, pitch, buses, fades, positional sound and mic module
-rw-r--r--Makefile2
-rwxr-xr-xbuild.sh8
-rw-r--r--examples/sound.c387
-rw-r--r--kon/konaudio.c479
-rw-r--r--kon/konaudio.h73
-rw-r--r--kon/konmic.c254
-rw-r--r--kon/konmic.h68
7 files changed, 1189 insertions, 82 deletions
diff --git a/Makefile b/Makefile
index ffec53e..954859c 100644
--- a/Makefile
+++ b/Makefile
@@ -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 \
diff --git a/build.sh b/build.sh
index b5cb78e..7f752a6 100755
--- a/build.sh
+++ b/build.sh
@@ -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