diff options
| author | KonAki <konaki@anokonaki.org> | 2026-10-14 18:42:43 +0800 |
|---|---|---|
| committer | KonAki <konaki@anokonaki.org> | 2026-10-14 18:42:43 +0800 |
| commit | 1daa5b5f2d712490e4518729726fe0efa606d6bc (patch) | |
| tree | eca254d21fdb37812186ee77e9e65c509d558df3 | |
| parent | 1299523d78a3739c57dac164d29efb3b592cd666 (diff) | |
add sound module: RPG Maker style audio channels
| -rw-r--r-- | kon/konsound.c | 392 | ||||
| -rw-r--r-- | kon/konsound.h | 113 |
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, ¶ms, 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, ¶ms); +} + +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, ¶ms); +} + +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, ¶ms); + 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 |
