// The MIT License (MIT) // // Copyright (C) 2026 Scott Duensing // // 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. // cpSounds.c -- Control Panel's Sounds page (see cpSounds.h). // // The sound library is looked up by name when the page is built, so the // Control Panel still works where it is not installed (the page then says // so). The sound files offered are those in the default sounds folder, // the ones DVX.INI names and any the user browses to; "(None)" is always // first. The volume applies at once (Cancel puts DVX.INI's back); the // sounds, the driver and the MIDI choice are written by OK, and the // driver and MIDI choice take effect when DVX next starts. #include "cpSounds.h" #include "audio/audio.h" #include "audio/audioDrv.h" #include "dvxDlg.h" #include "dvxPlat.h" #include "box/box.h" #include "button/button.h" #include "dropdown/dropdown.h" #include "label/label.h" #include "listBox/listBox.h" #include "slider/slider.h" #include "stb_ds_wrap.h" #include #include #include #include #include #include #define CPS_LABEL_W 100 #define CPS_GAP 4 #define CPS_BTN_W 70 #define CPS_LIST_H 148 // the event list: nine rows #define CPS_VOL_LBL_W 40 #define CPS_TEXT_BUF 128 #define CPS_DRV_ITEMS (AUDIO_DRIVERS_MAX + 2) // Automatic, the drivers, DVX.INI's own choice #define PERCENT_MAX 100 // The sound library's functions (NULL when it is not installed) typedef struct { __typeof__(audioDeskFile) *file; __typeof__(audioDeskLabel) *label; __typeof__(audioDeskPlayFile) *playFile; __typeof__(audioDeskReload) *reload; __typeof__(audioDeskSetFile) *setFile; __typeof__(audioDriverNames) *driverNames; __typeof__(audioErrorText) *errorText; __typeof__(audioGetInfo) *getInfo; const PrefsChoiceT *midiChoices; __typeof__(audioPercentToVolume) *toVolume; __typeof__(audioSetMasterVolume) *setVolume; __typeof__(audioSettingDriver) *driver; __typeof__(audioSettingMidi) *midi; __typeof__(audioSettingsStore) *store; __typeof__(audioSettingVolume) *volume; } SndFnsT; typedef struct { const char *name; size_t offset; } SndFnSymT; #define SND_FN_SYM(field, fn) { DVX_SYM(#fn), offsetof(SndFnsT, field) } static const SndFnSymT sSyms[] = { SND_FN_SYM(file, audioDeskFile), SND_FN_SYM(label, audioDeskLabel), SND_FN_SYM(playFile, audioDeskPlayFile), SND_FN_SYM(reload, audioDeskReload), SND_FN_SYM(setFile, audioDeskSetFile), SND_FN_SYM(driverNames, audioDriverNames), SND_FN_SYM(errorText, audioErrorText), SND_FN_SYM(getInfo, audioGetInfo), SND_FN_SYM(midiChoices, audioMidiChoices), SND_FN_SYM(toVolume, audioPercentToVolume), SND_FN_SYM(setVolume, audioSetMasterVolume), SND_FN_SYM(driver, audioSettingDriver), SND_FN_SYM(midi, audioSettingMidi), SND_FN_SYM(store, audioSettingsStore), SND_FN_SYM(volume, audioSettingVolume) }; // The sound files listed from the sounds folder static const char *sSoundExts[] = { ".wav", ".voc" }; // ============================================================ // Module state // ============================================================ static SndFnsT sSnd; static AppContextT *sAc = NULL; static PrefsHandleT *sPrefs = NULL; static WidgetT *sEventList = NULL; static WidgetT *sFileDrop = NULL; static WidgetT *sVolSldr = NULL; static WidgetT *sVolLbl = NULL; static WidgetT *sDrvDrop = NULL; static WidgetT *sMidiDrop = NULL; static char **sFileValues = NULL; // stb_ds: paths as stored ("" for none), owned static const char **sFileLabels = NULL; // stb_ds: parallel labels (into sFileValues or literals) static char sDrvNames[CPS_DRV_ITEMS][AUDIO_NAME_MAX]; // as stored static const char *sDrvLabels[CPS_DRV_ITEMS]; static int32_t sDrvCount = 0; static const char *sEventLabels[SoundCountE]; static const char *sMidiLabels[AUDIO_MIDI_CHOICE_COUNT]; static int32_t sChoice[SoundCountE]; // ============================================================ // Prototypes // ============================================================ static int32_t addDriver(const char *name, const char *label); static int32_t addFile(const char *path); static WidgetT *addRow(WidgetT *parent, const char *label); static void addSoundsDir(void); static int compareNames(const void *a, const void *b); void cpSoundsApply(void); void cpSoundsBuild(AppContextT *ac, WidgetT *page, PrefsHandleT *prefs); void cpSoundsFree(void); void cpSoundsRevert(void); void cpSoundsStore(PrefsHandleT *prefs); static void onBrowse(WidgetT *w); static void onEvent(WidgetT *w); static void onFile(WidgetT *w); static void onTest(WidgetT *w); static void onVolume(WidgetT *w); static bool resolveLibrary(void); static void showVolume(void); // A driver item: the name stored and, when label is NULL, shown. static int32_t addDriver(const char *name, const char *label) { snprintf(sDrvNames[sDrvCount], AUDIO_NAME_MAX, "%s", name); sDrvLabels[sDrvCount] = label ? label : sDrvNames[sDrvCount]; return sDrvCount++; } // The item of a sound file, added when it is not listed yet ("" is // "(None)"). Files are told apart ignoring case: DOS names have none. static int32_t addFile(const char *path) { for (int32_t i = 0; i < (int32_t)arrlen(sFileValues); i++) { if (strcasecmp(sFileValues[i], path) == 0) { return i; } } char *value = strdup(path); if (!value) { return 0; } size_t dirLen = strlen(AUDIO_SOUNDS_DIR); const char *label = value; if (!value[0]) { label = "(None)"; } else if (strncasecmp(value, AUDIO_SOUNDS_DIR, dirLen) == 0 && value[dirLen] == '/') { label = value + dirLen + 1; } arrput(sFileValues, value); arrput(sFileLabels, label); return (int32_t)arrlen(sFileValues) - 1; } // "Label:" and the row for its controls. static WidgetT *addRow(WidgetT *parent, const char *label) { WidgetT *row = wgtHBox(parent); WidgetT *lbl = wgtLabel(row, label); row->spacing = wgtPixels(CPS_GAP); lbl->prefW = wgtPixels(CPS_LABEL_W); return row; } // The sound files of the default set, by name. static void addSoundsDir(void) { char **names = dvxReadDir(AUDIO_SOUNDS_DIR); if (!names) { return; } qsort(names, arrlen(names), sizeof(names[0]), compareNames); for (int32_t i = 0; i < (int32_t)arrlen(names); i++) { for (int32_t e = 0; e < DVX_ARRAY_LEN(sSoundExts); e++) { if (dvxHasExt(names[i], sSoundExts[e])) { char path[DVX_MAX_PATH]; snprintf(path, sizeof(path), "%s/%s", AUDIO_SOUNDS_DIR, names[i]); addFile(path); } } } dvxReadDirFree(names); } static int compareNames(const void *a, const void *b) { return strcasecmp(*(const char * const *)a, *(const char * const *)b); } void cpSoundsApply(void) { if (sSnd.reload) { sSnd.reload(); } } void cpSoundsBuild(AppContextT *ac, WidgetT *page, PrefsHandleT *prefs) { sAc = ac; sPrefs = prefs; if (!resolveLibrary()) { wgtLabel(page, "Sound is not installed."); return; } addFile(""); addSoundsDir(); for (int32_t i = 0; i < SoundCountE; i++) { sEventLabels[i] = sSnd.label((DvxSoundE)i); sChoice[i] = addFile(sSnd.file(prefs, (DvxSoundE)i)); } wgtLabel(page, "Events:"); sEventList = wgtListBox(page); sEventList->prefH = wgtPixels(CPS_LIST_H); sEventList->onChange = onEvent; wgtListBoxSetItems(sEventList, sEventLabels, SoundCountE); WidgetT *row = addRow(page, "Sound:"); sFileDrop = wgtDropdown(row); sFileDrop->weight = WGT_WEIGHT_FILL; sFileDrop->onChange = onFile; wgtDropdownSetItems(sFileDrop, sFileLabels, (int32_t)arrlen(sFileLabels)); WidgetT *btn = wgtButton(row, "Browse..."); btn->onClick = onBrowse; btn->prefW = wgtPixels(CPS_BTN_W); btn = wgtButton(row, "Test"); btn->onClick = onTest; btn->prefW = wgtPixels(CPS_BTN_W); row = addRow(page, "Volume:"); sVolSldr = wgtSlider(row, 0, PERCENT_MAX); sVolSldr->weight = WGT_WEIGHT_FILL; sVolSldr->onChange = onVolume; sVolLbl = wgtLabel(row, ""); sVolLbl->prefW = wgtPixels(CPS_VOL_LBL_W); wgtSliderSetValue(sVolSldr, sSnd.volume(prefs)); showVolume(); // Driver: "Automatic", every installed driver, and DVX.INI's own // choice even when that driver is missing (so OK keeps it). char names[AUDIO_DRIVERS_MAX][AUDIO_NAME_MAX]; int32_t count = sSnd.driverNames(names, AUDIO_DRIVERS_MAX); const char *want = sSnd.driver(prefs); int32_t drvSel = -1; sDrvCount = 0; addDriver(AUDIO_DRIVER_AUTO, "Automatic"); for (int32_t i = 0; i < count; i++) { addDriver(names[i], NULL); } for (int32_t i = 0; i < sDrvCount; i++) { drvSel = strcasecmp(sDrvNames[i], want) == 0 ? i : drvSel; } if (drvSel < 0) { drvSel = addDriver(want, NULL); } sDrvDrop = wgtDropdown(addRow(page, "Sound driver:")); wgtDropdownSetItems(sDrvDrop, sDrvLabels, sDrvCount); wgtDropdownSetSelected(sDrvDrop, drvSel); prefsChoiceLabels(sSnd.midiChoices, AUDIO_MIDI_CHOICE_COUNT, sMidiLabels); sMidiDrop = wgtDropdown(addRow(page, "MIDI music:")); wgtDropdownSetItems(sMidiDrop, sMidiLabels, AUDIO_MIDI_CHOICE_COUNT); wgtDropdownSetSelected(sMidiDrop, sSnd.midi(prefs)); AudioInfoT info; char text[CPS_TEXT_BUF]; sSnd.getInfo(&info); snprintf(text, sizeof(text), "Sound device: %s", info.running ? info.description : "none running"); wgtLabel(page, text); wgtLabel(page, "A new driver or MIDI choice takes effect when DVX next starts."); wgtListBoxSetSelected(sEventList, 0); onEvent(sEventList); } void cpSoundsFree(void) { for (int32_t i = 0; i < (int32_t)arrlen(sFileValues); i++) { free(sFileValues[i]); } arrfree(sFileValues); arrfree(sFileLabels); sFileValues = NULL; sFileLabels = NULL; } void cpSoundsRevert(void) { if (sSnd.setVolume) { sSnd.setVolume(sSnd.toVolume(sSnd.volume(sPrefs))); } } void cpSoundsStore(PrefsHandleT *prefs) { if (!sSnd.store) { return; } for (int32_t i = 0; i < SoundCountE; i++) { sSnd.setFile(prefs, (DvxSoundE)i, sFileValues[sChoice[i]]); } sSnd.store(prefs, sDrvNames[wgtDropdownGetSelected(sDrvDrop)], wgtDropdownGetSelected(sMidiDrop), wgtSliderGetValue(sVolSldr)); } // Choose any sound file for the selected event. static void onBrowse(WidgetT *w) { (void)w; FileFilterT filters[] = { { "Sounds (*.wav;*.voc)" }, { "All Files (*.*)" } }; char path[DVX_MAX_PATH]; if (!dvxFileDialog(sAc, "Choose a Sound", FD_OPEN, AUDIO_SOUNDS_DIR, filters, DVX_ARRAY_LEN(filters), path, sizeof(path))) { return; } int32_t item = addFile(path); wgtDropdownSetItems(sFileDrop, sFileLabels, (int32_t)arrlen(sFileLabels)); wgtDropdownSetSelected(sFileDrop, item); onFile(sFileDrop); } // Show the selected event's sound. static void onEvent(WidgetT *w) { int32_t ev = wgtListBoxGetSelected(w); if (ev >= 0 && ev < SoundCountE) { wgtDropdownSetSelected(sFileDrop, sChoice[ev]); } } static void onFile(WidgetT *w) { int32_t ev = wgtListBoxGetSelected(sEventList); int32_t item = wgtDropdownGetSelected(w); if (ev >= 0 && ev < SoundCountE && item >= 0) { sChoice[ev] = item; } } // Play the sound chosen in the dropdown. static void onTest(WidgetT *w) { (void)w; int32_t item = wgtDropdownGetSelected(sFileDrop); if (item <= 0) { return; } int32_t rc = sSnd.playFile(sFileValues[item]); if (rc < 0) { dvxMessageBox(sAc, "Sounds", sSnd.errorText(rc), MB_OK | MB_ICONWARNING); } } static void onVolume(WidgetT *w) { sSnd.setVolume(sSnd.toVolume(wgtSliderGetValue(w))); showVolume(); } static bool resolveLibrary(void) { for (int32_t i = 0; i < DVX_ARRAY_LEN(sSyms); i++) { void *fn = dlsym(RTLD_DEFAULT, sSyms[i].name); if (!fn) { memset(&sSnd, 0, sizeof(sSnd)); return false; } memcpy((char *)&sSnd + sSyms[i].offset, &fn, sizeof(fn)); } return true; } static void showVolume(void) { char text[CPS_TEXT_BUF]; snprintf(text, sizeof(text), "%ld%%", (long)wgtSliderGetValue(sVolSldr)); wgtSetText(sVolLbl, text); }