450 lines
14 KiB
C
450 lines
14 KiB
C
// 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 <dlfcn.h>
|
|
#include <stddef.h>
|
|
#include <stdio.h>
|
|
#include <stdlib.h>
|
|
#include <string.h>
|
|
#include <strings.h>
|
|
|
|
#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);
|
|
}
|