summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorKonAki <konaki@anokonaki.org>2026-10-14 18:42:43 +0800
committerKonAki <konaki@anokonaki.org>2026-10-14 18:42:43 +0800
commit1daa5b5f2d712490e4518729726fe0efa606d6bc (patch)
treeeca254d21fdb37812186ee77e9e65c509d558df3
parent1299523d78a3739c57dac164d29efb3b592cd666 (diff)
add sound module: RPG Maker style audio channels
-rw-r--r--kon/konsound.c392
-rw-r--r--kon/konsound.h113
2 files changed, 505 insertions, 0 deletions
diff --git a/kon/konsound.c b/kon/konsound.c
new file mode 100644
index 0000000..e65c3cf
--- /dev/null
+++ b/kon/konsound.c
@@ -0,0 +1,392 @@
+/* SPDX-License-Identifier: MIT */
+/*
+ * konsound.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 "konsound.h"
+
+#include <string.h>
+
+#define REPLAY_FADE 0.5f
+#define OGG_SCAN 32768
+
+/*** helpers ***/
+
+static int clampInt(int value, int low, int high)
+{
+ return value < low ? low : value > high ? high : value;
+}
+
+static void setName(kon_soundChannel_t *channel, const char *name)
+{
+ size_t length = name ? strlen(name) : 0;
+
+ if (length >= KON_SOUND_NAME) length = KON_SOUND_NAME - 1;
+ if (length) memcpy(channel->name, name, length);
+ channel->name[length] = '\0';
+}
+
+static void setParams(kon_soundChannel_t *channel, const char *name, int volume, int pitch, int pan)
+{
+ setName(channel, name);
+ channel->volume = clampInt(volume, 0, 100);
+ channel->pitch = clampInt(pitch, 50, 150);
+ channel->pan = clampInt(pan, -100, 100);
+}
+
+static kon_audioParams_t makeParams(const kon_soundChannel_t *channel, int loop, int bus, uint64_t loopStart, uint64_t loopEnd)
+{
+ kon_audioParams_t params = kon_audioParamsDefault();
+
+ params.volume = (float)channel->volume / 100.0f;
+ params.pitch = (float)channel->pitch / 100.0f;
+ params.pan = (float)channel->pan / 100.0f;
+ params.loop = loop;
+ params.loopStart = loopStart;
+ params.loopEnd = loopEnd;
+ params.bus = bus;
+ return params;
+}
+
+static const kon_sound_t *find(kon_soundDeck_t *deck, int kind, const char *name, uint64_t *loopStart, uint64_t *loopEnd)
+{
+ *loopStart = 0;
+ *loopEnd = 0;
+ if (!deck->lookup || !name || !name[0]) return NULL;
+ return deck->lookup(kind, name, loopStart, loopEnd, deck->user);
+}
+
+static void startBgm(kon_soundDeck_t *deck, const kon_soundChannel_t *wanted, float fade)
+{
+ uint64_t loopStart, loopEnd;
+ const kon_sound_t *sound = find(deck, KON_SOUND_BGM, wanted->name, &loopStart, &loopEnd);
+ kon_audioParams_t params;
+
+ deck->bgm = *wanted;
+ deck->bgm.handle = 0;
+ if (!sound) {
+ kon_audioMusicStop(deck->audio, 0.0f);
+ deck->bgm.name[0] = '\0';
+ return;
+ }
+ params = makeParams(wanted, 1, KON_BUS_MUSIC, loopStart, loopEnd);
+ deck->bgm.handle = kon_audioMusic(deck->audio, sound, &params, fade);
+}
+
+/*** deck ***/
+
+void kon_soundDeckInit(kon_soundDeck_t *deck, kon_audio_t *audio, kon_soundLookup_t lookup, void *user)
+{
+ if (!deck) return;
+ memset(deck, 0, sizeof *deck);
+ deck->audio = audio;
+ deck->lookup = lookup;
+ deck->user = user;
+}
+
+void kon_soundDeckStop(kon_soundDeck_t *deck)
+{
+ int i;
+
+ if (!deck || !deck->audio) return;
+ kon_audioMusicStop(deck->audio, 0.0f);
+ kon_audioStop(deck->audio, deck->bgs.handle);
+ kon_audioStop(deck->audio, deck->me.handle);
+ for (i = 0; i < KON_SOUND_SE_TRACKED; i++) kon_audioStop(deck->audio, deck->se[i]);
+ memset(deck->se, 0, sizeof deck->se);
+ deck->bgm.name[0] = deck->bgs.name[0] = deck->me.name[0] = '\0';
+ deck->bgm.handle = deck->bgs.handle = deck->me.handle = 0;
+ deck->meHeld = 0;
+}
+
+void kon_soundUpdate(kon_soundDeck_t *deck)
+{
+ if (!deck || !deck->audio || !deck->meHeld) return;
+ if (deck->me.handle && kon_audioIsPlaying(deck->audio, deck->me.handle)) return;
+
+ deck->meHeld = 0;
+ deck->me.handle = 0;
+ deck->me.name[0] = '\0';
+ if (deck->resumeBgm.name[0]) startBgm(deck, &deck->resumeBgm, REPLAY_FADE);
+}
+
+/*** channels ***/
+
+void kon_soundPlayBgm(kon_soundDeck_t *deck, const char *name, int volume, int pitch, int pan)
+{
+ kon_soundChannel_t wanted;
+
+ if (!deck || !deck->audio) return;
+ memset(&wanted, 0, sizeof wanted);
+ setParams(&wanted, name, volume, pitch, pan);
+
+ if (deck->meHeld) {
+ deck->resumeBgm = wanted;
+ return;
+ }
+ if (!wanted.name[0]) {
+ kon_audioMusicStop(deck->audio, 0.0f);
+ deck->bgm.name[0] = '\0';
+ deck->bgm.handle = 0;
+ return;
+ }
+ if (deck->bgm.name[0] && strcmp(deck->bgm.name, wanted.name) == 0 && kon_audioIsPlaying(deck->audio, deck->bgm.handle)) {
+ deck->bgm.volume = wanted.volume;
+ deck->bgm.pitch = wanted.pitch;
+ deck->bgm.pan = wanted.pan;
+ kon_audioHandleSetVolume(deck->audio, deck->bgm.handle, (float)wanted.volume / 100.0f);
+ kon_audioHandleSetPitch(deck->audio, deck->bgm.handle, (float)wanted.pitch / 100.0f);
+ kon_audioHandleSetPan(deck->audio, deck->bgm.handle, (float)wanted.pan / 100.0f);
+ return;
+ }
+ startBgm(deck, &wanted, 0.0f);
+}
+
+void kon_soundPlayBgs(kon_soundDeck_t *deck, const char *name, int volume, int pitch, int pan)
+{
+ kon_soundChannel_t wanted;
+ uint64_t loopStart, loopEnd;
+ const kon_sound_t *sound;
+ kon_audioParams_t params;
+
+ if (!deck || !deck->audio) return;
+ memset(&wanted, 0, sizeof wanted);
+ setParams(&wanted, name, volume, pitch, pan);
+
+ if (wanted.name[0] && strcmp(deck->bgs.name, wanted.name) == 0 && kon_audioIsPlaying(deck->audio, deck->bgs.handle)) {
+ deck->bgs.volume = wanted.volume;
+ deck->bgs.pitch = wanted.pitch;
+ deck->bgs.pan = wanted.pan;
+ kon_audioHandleSetVolume(deck->audio, deck->bgs.handle, (float)wanted.volume / 100.0f);
+ kon_audioHandleSetPitch(deck->audio, deck->bgs.handle, (float)wanted.pitch / 100.0f);
+ kon_audioHandleSetPan(deck->audio, deck->bgs.handle, (float)wanted.pan / 100.0f);
+ return;
+ }
+
+ kon_audioStop(deck->audio, deck->bgs.handle);
+ deck->bgs = wanted;
+ deck->bgs.handle = 0;
+ sound = find(deck, KON_SOUND_BGS, wanted.name, &loopStart, &loopEnd);
+ if (!sound) {
+ deck->bgs.name[0] = '\0';
+ return;
+ }
+ params = makeParams(&wanted, 1, KON_BUS_MUSIC, loopStart, loopEnd);
+ deck->bgs.handle = kon_audioPlayEx(deck->audio, sound, &params);
+}
+
+void kon_soundFadeBgm(kon_soundDeck_t *deck, int seconds)
+{
+ if (!deck || !deck->audio) return;
+ if (deck->meHeld) deck->resumeBgm.name[0] = '\0';
+ kon_audioMusicStop(deck->audio, (float)clampInt(seconds, 0, 3600));
+ deck->bgm.name[0] = '\0';
+ deck->bgm.handle = 0;
+}
+
+void kon_soundFadeBgs(kon_soundDeck_t *deck, int seconds)
+{
+ if (!deck || !deck->audio) return;
+ kon_audioHandleFade(deck->audio, deck->bgs.handle, 0.0f, (float)clampInt(seconds, 0, 3600), 1);
+ deck->bgs.name[0] = '\0';
+ deck->bgs.handle = 0;
+}
+
+void kon_soundSaveBgm(kon_soundDeck_t *deck)
+{
+ if (!deck) return;
+ deck->savedBgm = deck->meHeld ? deck->resumeBgm : deck->bgm;
+ deck->savedBgm.handle = 0;
+}
+
+void kon_soundReplayBgm(kon_soundDeck_t *deck)
+{
+ if (!deck || !deck->audio) return;
+ if (!deck->savedBgm.name[0]) {
+ kon_soundFadeBgm(deck, 0);
+ return;
+ }
+ if (deck->meHeld) {
+ deck->resumeBgm = deck->savedBgm;
+ return;
+ }
+ if (strcmp(deck->bgm.name, deck->savedBgm.name) == 0 && kon_audioIsPlaying(deck->audio, deck->bgm.handle)) return;
+ startBgm(deck, &deck->savedBgm, 0.0f);
+}
+
+void kon_soundPlayMe(kon_soundDeck_t *deck, const char *name, int volume, int pitch, int pan)
+{
+ kon_soundChannel_t wanted;
+ uint64_t loopStart, loopEnd;
+ const kon_sound_t *sound;
+ kon_audioParams_t params;
+
+ if (!deck || !deck->audio) return;
+ memset(&wanted, 0, sizeof wanted);
+ setParams(&wanted, name, volume, pitch, pan);
+
+ kon_audioStop(deck->audio, deck->me.handle);
+ deck->me.handle = 0;
+ deck->me.name[0] = '\0';
+ sound = find(deck, KON_SOUND_ME, wanted.name, &loopStart, &loopEnd);
+
+ if (!sound) {
+ /* an empty or missing ME ends the pause, so the music comes back */
+ if (deck->meHeld) {
+ deck->meHeld = 0;
+ if (deck->resumeBgm.name[0]) startBgm(deck, &deck->resumeBgm, REPLAY_FADE);
+ }
+ return;
+ }
+
+ if (!deck->meHeld && deck->bgm.name[0]) {
+ deck->resumeBgm = deck->bgm;
+ deck->resumeBgm.handle = 0;
+ kon_audioMusicStop(deck->audio, 0.0f);
+ deck->bgm.name[0] = '\0';
+ deck->bgm.handle = 0;
+ deck->meHeld = 1;
+ } else if (!deck->meHeld) {
+ deck->resumeBgm.name[0] = '\0';
+ deck->meHeld = 1;
+ }
+
+ params = makeParams(&wanted, 0, KON_BUS_MUSIC, 0, 0);
+ deck->me = wanted;
+ deck->me.handle = kon_audioPlayEx(deck->audio, sound, &params);
+}
+
+void kon_soundPlaySe(kon_soundDeck_t *deck, const char *name, int volume, int pitch, int pan)
+{
+ kon_soundChannel_t wanted;
+ uint64_t loopStart, loopEnd;
+ const kon_sound_t *sound;
+ kon_audioParams_t params;
+ kon_audioHandle_t handle;
+
+ if (!deck || !deck->audio) return;
+ memset(&wanted, 0, sizeof wanted);
+ setParams(&wanted, name, volume, pitch, pan);
+ sound = find(deck, KON_SOUND_SE, wanted.name, &loopStart, &loopEnd);
+ if (!sound) return;
+
+ params = makeParams(&wanted, 0, KON_BUS_SFX, 0, 0);
+ handle = kon_audioPlayEx(deck->audio, sound, &params);
+ if (!handle) return;
+ deck->se[deck->seNext] = handle;
+ deck->seNext = (deck->seNext + 1) % KON_SOUND_SE_TRACKED;
+}
+
+void kon_soundStopSe(kon_soundDeck_t *deck)
+{
+ int i;
+
+ if (!deck || !deck->audio) return;
+ for (i = 0; i < KON_SOUND_SE_TRACKED; i++) {
+ kon_audioStop(deck->audio, deck->se[i]);
+ deck->se[i] = 0;
+ }
+}
+
+/*** konscript host commands ***/
+
+int kon_soundHost(kon_soundDeck_t *deck, int code, int b, int c, int d, const char *text)
+{
+ char name[KON_SOUND_NAME];
+ size_t length = 0;
+
+ switch (code) {
+ case 241: case 245: case 249: case 250:
+ while (text && text[length] && text[length] != '\t' && length < KON_SOUND_NAME - 1) {
+ name[length] = text[length];
+ length++;
+ }
+ name[length] = '\0';
+ if (code == 241) kon_soundPlayBgm(deck, name, b, c, d);
+ else if (code == 245) kon_soundPlayBgs(deck, name, b, c, d);
+ else if (code == 249) kon_soundPlayMe(deck, name, b, c, d);
+ else kon_soundPlaySe(deck, name, b, c, d);
+ return 1;
+ case 242: kon_soundFadeBgm(deck, b); return 1;
+ case 243: kon_soundSaveBgm(deck); return 1;
+ case 244: kon_soundReplayBgm(deck); return 1;
+ case 246: kon_soundFadeBgs(deck, b); return 1;
+ case 251: kon_soundStopSe(deck); return 1;
+ default: return 0;
+ }
+}
+
+/*** ogg loop points ***/
+
+static int matchTag(const uint8_t *p, const uint8_t *end, const char *tag)
+{
+ while (*tag) {
+ uint8_t ch;
+
+ if (p >= end) return 0;
+ ch = *p;
+ if (ch >= 'a' && ch <= 'z') ch = (uint8_t)(ch - 'a' + 'A');
+ if (ch != (uint8_t)*tag) return 0;
+ p++;
+ tag++;
+ }
+ return 1;
+}
+
+static int readNumber(const uint8_t *p, const uint8_t *end, uint64_t *out)
+{
+ uint64_t value = 0;
+ int digits = 0;
+
+ while (p < end && *p >= '0' && *p <= '9' && digits < 18) {
+ value = value * 10 + (uint64_t)(*p - '0');
+ p++;
+ digits++;
+ }
+ if (!digits) return 0;
+ *out = value;
+ return 1;
+}
+
+int kon_soundOggLoop(const uint8_t *data, size_t size, uint64_t *start, uint64_t *end)
+{
+ const uint8_t *p, *stop;
+ uint64_t loopStart = 0, loopLength = 0;
+ int haveStart = 0, haveLength = 0;
+
+ if (!data || !start || !end || size < 8 || memcmp(data, "OggS", 4) != 0) return 0;
+ stop = data + (size < OGG_SCAN ? size : OGG_SCAN);
+
+ for (p = data; p < stop; p++) {
+ if (!haveStart && matchTag(p, stop, "LOOPSTART=")) {
+ haveStart = readNumber(p + 10, stop, &loopStart);
+ } else if (!haveLength && matchTag(p, stop, "LOOPLENGTH=")) {
+ haveLength = readNumber(p + 11, stop, &loopLength);
+ }
+ }
+
+ if (!haveStart) return 0;
+ *start = loopStart;
+ *end = haveLength ? loopStart + loopLength : 0;
+ return 1;
+}
diff --git a/kon/konsound.h b/kon/konsound.h
new file mode 100644
index 0000000..296c691
--- /dev/null
+++ b/kon/konsound.h
@@ -0,0 +1,113 @@
+/* SPDX-License-Identifier: MIT */
+/*
+ * konsound.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 KONSOUND_H
+#define KONSOUND_H
+
+#include <stddef.h>
+#include <stdint.h>
+
+#include "konaudio.h"
+
+#ifdef __cplusplus
+extern "C" {
+#endif
+
+/*** definition ***/
+
+#define KON_SOUND_NAME 64
+#define KON_SOUND_SE_TRACKED 16
+
+#define KON_SOUND_BGM 0
+#define KON_SOUND_BGS 1
+#define KON_SOUND_ME 2
+#define KON_SOUND_SE 3
+
+/* finds the sound for a name. loopStart and loopEnd are frames, leave them 0 for no loop points.
+ the sound has to stay alive while it plays. return NULL for a name you do not have */
+typedef const kon_sound_t *(*kon_soundLookup_t)(int kind, const char *name, uint64_t *loopStart, uint64_t *loopEnd, void *user);
+
+typedef struct kon_soundChannel {
+ kon_audioHandle_t handle;
+ char name[KON_SOUND_NAME];
+ int volume, pitch, pan;
+} kon_soundChannel_t;
+
+/* volumes, pitches and pans are the RPG Maker numbers: volume 0 to 100, pitch 50 to 150 (100 is normal), pan -100 to 100 */
+typedef struct kon_soundDeck {
+ kon_audio_t *audio;
+ kon_soundLookup_t lookup;
+ void *user;
+ kon_soundChannel_t bgm, bgs, me;
+ kon_soundChannel_t savedBgm;
+ kon_soundChannel_t resumeBgm; /* the music that was playing when a ME started */
+ int meHeld;
+ kon_audioHandle_t se[KON_SOUND_SE_TRACKED];
+ int seNext;
+} kon_soundDeck_t;
+
+/*** deck ***/
+
+void kon_soundDeckInit(kon_soundDeck_t *deck, kon_audio_t *audio, kon_soundLookup_t lookup, void *user);
+
+/* stops everything this deck started */
+void kon_soundDeckStop(kon_soundDeck_t *deck);
+
+/* call once per frame. brings the music back when a ME is over */
+void kon_soundUpdate(kon_soundDeck_t *deck);
+
+/*** channels ***/
+
+/* a new name replaces the music, the same name only changes volume, pitch and pan. an empty name stops it */
+void kon_soundPlayBgm(kon_soundDeck_t *deck, const char *name, int volume, int pitch, int pan);
+void kon_soundPlayBgs(kon_soundDeck_t *deck, const char *name, int volume, int pitch, int pan);
+void kon_soundFadeBgm(kon_soundDeck_t *deck, int seconds);
+void kon_soundFadeBgs(kon_soundDeck_t *deck, int seconds);
+
+/* remembers the music, and starts it again from the beginning (the position is not kept) */
+void kon_soundSaveBgm(kon_soundDeck_t *deck);
+void kon_soundReplayBgm(kon_soundDeck_t *deck);
+
+/* plays once. the music pauses until it is over */
+void kon_soundPlayMe(kon_soundDeck_t *deck, const char *name, int volume, int pitch, int pan);
+void kon_soundPlaySe(kon_soundDeck_t *deck, const char *name, int volume, int pitch, int pan);
+void kon_soundStopSe(kon_soundDeck_t *deck);
+
+/*** konscript host commands ***/
+
+/* handles the RPG Maker audio commands 241 to 246, 249, 250 and 251 the way mzscript writes them.
+ returns 1 when the code was one of those, 0 when it is not yours to handle */
+int kon_soundHost(kon_soundDeck_t *deck, int code, int b, int c, int d, const char *text);
+
+/*** ogg loop points ***/
+
+/* looks for LOOPSTART and LOOPLENGTH tags in the start of an ogg file, the way RPG Maker games write them.
+ returns 1 and sets start and end (frames) when there is a loop start, else 0 */
+int kon_soundOggLoop(const uint8_t *data, size_t size, uint64_t *start, uint64_t *end);
+
+#ifdef __cplusplus
+}
+#endif
+#endif