Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions CMakeLists.txt
Original file line number Diff line number Diff line change
Expand Up @@ -207,6 +207,7 @@ ENDIF()
check_symbol_exists(powf math.h HAVE_POWF)
check_symbol_exists(expf math.h HAVE_EXPF)
check_symbol_exists(sqrtf math.h HAVE_SQRTF)
check_symbol_exists(sinf math.h HAVE_SINF)
cmake_pop_check_state()

# ######### General setup ##########
Expand Down
2 changes: 1 addition & 1 deletion amiga/Makefile
Original file line number Diff line number Diff line change
Expand Up @@ -55,7 +55,7 @@ endif
$(CC) -c $(CFLAGS) -o $@ $<

# Objects
LIB_OBJ= wm_error.o file_io.o lock.o wildmidi_lib.o reverb.o gus_pat.o f_xmidi.o f_mus.o f_hmp.o f_midi.o f_hmi.o mus2mid.o xmi2mid.o internal_midi.o patches.o sample.o sf2.o
LIB_OBJ= wm_error.o file_io.o lock.o wildmidi_lib.o reverb.o gus_pat.o f_xmidi.o f_mus.o f_hmp.o f_midi.o f_hmi.o mus2mid.o xmi2mid.o internal_midi.o patches.o sample.o sf2.o synth.o
PLAYER_OBJ= amiga.o wm_tty.o msleep.o getopt_long.o out_none.o out_wave.o out_ahi.o wildmidi.o

# Build targets
Expand Down
2 changes: 1 addition & 1 deletion amiga/Makefile.vbcc
Original file line number Diff line number Diff line change
Expand Up @@ -34,7 +34,7 @@ endif
$(CC) -c $(CFLAGS) -o $@ $<

# Objects
LIB_OBJ= wm_error.o file_io.o lock.o wildmidi_lib.o reverb.o gus_pat.o f_xmidi.o f_mus.o f_hmp.o f_midi.o f_hmi.o mus2mid.o xmi2mid.o internal_midi.o patches.o sample.o sf2.o
LIB_OBJ= wm_error.o file_io.o lock.o wildmidi_lib.o reverb.o gus_pat.o f_xmidi.o f_mus.o f_hmp.o f_midi.o f_hmi.o mus2mid.o xmi2mid.o internal_midi.o patches.o sample.o sf2.o synth.o
PLAYER_OBJ= amiga.o wm_tty.o msleep.o getopt_long.o out_none.o out_wave.o out_ahi.o wildmidi.o

# Build targets
Expand Down
3 changes: 2 additions & 1 deletion amiga/config.h
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
/* wildmidi config for amigaos variants */

#define WILDMIDI_AMIGA 1

#define WILDMIDI_CFG "wildmidi.cfg"
Expand All @@ -20,8 +21,8 @@
/* #undef HAVE_EXPF */
/* #undef HAVE_SQRTF */
/* #undef HAVE_POWF */
/* #undef HAVE_SINF */

#define WILDMIDI_SF2 1

#define AUDIODRV_AHI 1

1 change: 1 addition & 0 deletions android/jni/Android.mk
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@ LOCAL_SRC_FILES := \
src/reverb.c \
src/sample.c \
src/sf2.c \
src/synth.c \
src/wildmidi_lib.c \
src/wm_error.c \
src/xmi2mid.c
Expand Down
2 changes: 1 addition & 1 deletion android/jni/config.h
Original file line number Diff line number Diff line change
Expand Up @@ -64,8 +64,8 @@
#define HAVE_EXPF 1
#define HAVE_SQRTF 1
#define HAVE_POWF 1
#define HAVE_SINF 1

#define WILDMIDI_SF2 1

/* Define our audio drivers */

2 changes: 1 addition & 1 deletion djgpp/Makefile
Original file line number Diff line number Diff line change
Expand Up @@ -57,7 +57,7 @@ endif


# Objects
LIB_OBJ= wm_error.o file_io.o lock.o wildmidi_lib.o reverb.o gus_pat.o f_xmidi.o f_mus.o f_hmp.o f_midi.o f_hmi.o mus2mid.o xmi2mid.o internal_midi.o patches.o sample.o sf2.o
LIB_OBJ= wm_error.o file_io.o lock.o wildmidi_lib.o reverb.o gus_pat.o f_xmidi.o f_mus.o f_hmp.o f_midi.o f_hmi.o mus2mid.o xmi2mid.o internal_midi.o patches.o sample.o sf2.o synth.o
PLAYER_OBJ= wm_tty.o msleep.o getopt_long.o out_none.o dosirq.o dosdma.o dossb.o out_dossb.o out_wave.o wildmidi.o

# Build targets
Expand Down
1 change: 1 addition & 0 deletions djgpp/config.h
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@
/* #undef HAVE_EXPF */
/* #undef HAVE_SQRTF */
/* #undef HAVE_POWF */
/* #undef HAVE_SINF */

#define WILDMIDI_SF2 1

Expand Down
120 changes: 120 additions & 0 deletions docs/synth_plan.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,120 @@
# Emergency Soundbank — Live Synthesis Fallback

Refs: [issue #121](https://github.com/Mindwerks/wildmidi/issues/121).

Goal: when the user has no `.pat` set and no `.sf2`, WildMIDI still makes
sound. Not hi‑fi — audible, recognisable GM instruments generated in RAM at
startup.

## Feasibility: yes

WildMIDI already speaks one internal format: `struct _sample` (see
[include/sample.h](include/sample.h#L45)). Every loader — GUS, SF2 — ends up
handing a `_sample` chain to `sample_patch->first_sample` from inside
[`_WM_load_sample`](src/sample.c#L117). A synthesiser is just another loader
that never touches disk.

Integration point is one branch in `_WM_load_sample`:

```
if (guspat = _WM_load_gus_pat(...)) { /* existing */ }
else if (emergency_bank_enabled) { guspat = _WM_synth_patch(patchid); }
```
Comment on lines +19 to +22

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

📐 Maintainability & Code Quality | 🟡 Minor | ⚡ Quick win

Add a language tag to this fenced example.

The docs lint already flags this block (MD040). Mark it as c so the markdown check passes.

Fix
-```
+```c
 if (guspat = _WM_load_gus_pat(...)) { /* existing */ }
 else if (emergency_bank_enabled)   { guspat = _WM_synth_patch(patchid); }
-```
+```
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
```
if (guspat = _WM_load_gus_pat(...)) { /* existing */ }
else if (emergency_bank_enabled) { guspat = _WM_synth_patch(patchid); }
```
🧰 Tools
🪛 markdownlint-cli2 (0.22.1)

[warning] 19-19: Fenced code blocks should have a language specified

(MD040, fenced-code-language)

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@docs/synth_plan.md` around lines 19 - 22, The fenced example in the markdown
doc is missing a language identifier, which triggers the MD040 lint rule. Update
the fenced block containing the _WM_load_gus_pat and _WM_synth_patch example so
it uses the c language tag on the opening fence, keeping the existing example
content unchanged.

Source: Linters/SAST tools


Trigger: opt-in only — `--emergency` CLI flag, or pass the sentinel
`"@emergency"` as the config path to `WildMidi_Init()`. Never silent, so a
missing cfg still surfaces as an error and debugging stays honest.
No API break; existing users notice nothing.

## Licensing

- `EmergencySoundbank.java` (Gervill / Sun) is **GPL‑2 only** → **cannot lift**. Technique is not copyrightable; a clean reimplementation is fine.
- `csharpsynthproject` is MIT → compatible, safe to port ideas or code.
- AKWF single‑cycle waveforms are **public domain (CC0)** → safe to embed as static tables.
- KissFFT is BSD‑3 → compatible if we want an iFFT approach.
- Sonivox EAS wavetable (`wt_44khz.c`) is Apache 2.0 → compatible with LGPLv3 (WildMIDI's library license), **not** with LGPLv2. WildMIDI is v3+, so it is usable — but only from library code, not from the GPLv3 player if we want to stay strict.

Any lifted code goes in its own file with its original header preserved and gets called out in `docs/license/`.

## Three approaches, laziest first

### A. Detuned sine + ADSR per program family *(recommended first cut)*

One sine oscillator, per‑program pitch envelope, per‑program amplitude
envelope, per‑program brightness (a running average = one‑pole LPF). 128
programs collapse into ~8 families (piano, organ, guitar, bass, strings,
brass, reed, lead/pad, plus percussion tables) with a small parameter table.

- ~200 lines of C, no deps.
- One 1s sample per program → `128 × 44100 × 2 B ≈ 11 MB` worst case; do it lazy per program on first `NoteOn` and it stays under 1 MB in practice.
- Sound quality: chiptune‑ish. Fine as a "the config is missing" safety net.

### B. Additive (harmonic stack) + noise for percussion

Sum of 4–8 sines with a per‑program harmonic amplitude table, noise burst
for drums, same ADSR framework as A. Closer to a real timbre; brass and
strings become distinguishable. csharpsynth's generator is roughly this
shape and MIT‑licensed — that's the model to crib from.

- ~600 lines of C.
- Still no FFT dep.
- Percussion (channel 10) needs its own noise+resonator path — an extra ~100 lines.

### C. Gervill‑style frequency‑domain synthesis

Render weighted Gaussians in the frequency domain, phase‑randomise, iFFT,
window. This is what `EmergencySoundbank` does and why its output sounds
plausible. Requires an FFT — pull in KissFFT (BSD, ~1 kLOC, single header +
single .c).

- ~1200 lines of C + KissFFT.
- Genuinely usable for casual playback.
- Only worth doing if A/B prove insufficient.

## CPU cost

All figures are **one‑shot at startup**, or lazy on first use of each
program. Nothing runs per audio callback — output is a normal `_sample`
that flows through the existing mixer.

| Approach | Per‑patch synth | 128 patches (upper bound) | Runtime cost after warm‑up |
|----------|-----------------|---------------------------|----------------------------|
| A sine+ADSR | ~1 ms | <150 ms | zero |
| B additive | ~5–10 ms | ~1 s | zero |
| C iFFT | ~10–30 ms (44100‑pt) | ~2–4 s | zero |

Memory: 88 KB per 1s mono patch @ 44.1 kHz. Cap generated length per family
(pads = 2s, drums = 0.3s, plucks = 0.5s) and total stays around 4–6 MB even
if every program is touched.

## Difficulty

- **A**: ~half a day. New `src/synth.c` + `include/synth.h`, a
128‑entry program table, one branch in `_WM_load_sample`, CLI flag in
`src/player/wildmidi.c`, one env‑target/rate mapping using the existing
`env_rate[]/env_target[]` machinery.
- **B**: 2–3 days. Same skeleton as A + a harmonic table per family + a
noise+resonator path for drums + real tuning by ear.
- **C**: 1–2 weeks. Add KissFFT (or write a radix‑2 iFFT — ~120 lines),
reproduce the Gaussian‑sum→phase‑rand→iFFT pipeline, tune per family,
handle loop‑point selection on non‑decaying tones.

## Recommendation

Ship **A** as `--emergency` in a first pass. It answers issue #121: the
player never falls silent for lack of patches. If users want it to sound
good rather than just present, escalate to **B**; skip **C** unless someone
volunteers.

## Concrete task list for approach A

1. `include/synth.h`, `src/synth.c` — one public function
`struct _sample *_WM_synth_patch(uint16_t patchid);` returning a
`_sample` chain compatible with the mixer (16‑bit signed, `SAMPLE_LOOP`
set on sustained families, envelope in `env_rate[]/env_target[]`).
2. Program → family table (~30 lines of static data).
3. Wire fallback into [`_WM_load_sample`](src/sample.c#L117) — one `else if`.
4. `--emergency` flag in `src/player/wildmidi.c` and matching library
sentinel `WM_EMERGENCY_CONFIG`.
5. `test/` — one MIDI file rendered with `-o` to WAV, byte length asserted
non‑zero; that's the "did anything come out" smoke test.
1 change: 1 addition & 0 deletions include/config.h.cmake
Original file line number Diff line number Diff line change
Expand Up @@ -52,6 +52,7 @@
#cmakedefine HAVE_POWF
#cmakedefine HAVE_EXPF
#cmakedefine HAVE_SQRTF
#cmakedefine HAVE_SINF

/* define this if you are running a bigendian system (motorola, sparc, etc) */
#cmakedefine WORDS_BIGENDIAN 1
Expand Down
33 changes: 33 additions & 0 deletions include/synth.h
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
/*
* synth.h -- Built-in synthesiser: fabricates _sample chains so WildMIDI
* plays even without a GUS patch set or SoundFont.
*
* Copyright (C) WildMIDI Developers 2026
*
* This file is part of WildMIDI.
*
* WildMIDI is free software: you can redistribute and/or modify the player
* under the terms of the GNU General Public License and you can redistribute
* and/or modify the library under the terms of the GNU Lesser General Public
* License as published by the Free Software Foundation, either version 3 of
* the licenses, or(at your option) any later version.
*/

#ifndef __SYNTH_H
#define __SYNTH_H

#include <stdint.h>

#define WM_EMERGENCY_CONFIG "@emergency"

struct _sample;

/* Fabricate a _sample chain for the given (possibly-drum) patch id.
Uses the current _WM_SampleRate. Returns NULL on OOM. */
extern struct _sample *_WM_synth_patch(uint16_t patchid);

/* Populate _WM_patch[] with 128 tonal + GM drum entries, all with
filename==NULL so _WM_load_sample routes them to _WM_synth_patch. */
extern int _WM_emergency_init_patches(void);

#endif /* __SYNTH_H */
2 changes: 1 addition & 1 deletion macosx/Makefile
Original file line number Diff line number Diff line change
Expand Up @@ -68,7 +68,7 @@ LDLIBS_EXE+=-L. -l$(LIBNAME)

# Objects
LIB_OBJ = wm_error.o file_io.o lock.o wildmidi_lib.o reverb.o gus_pat.o
LIB_OBJ+= f_xmidi.o f_mus.o f_hmp.o f_midi.o f_hmi.o mus2mid.o xmi2mid.o internal_midi.o patches.o sample.o sf2.o
LIB_OBJ+= f_xmidi.o f_mus.o f_hmp.o f_midi.o f_hmi.o mus2mid.o xmi2mid.o internal_midi.o patches.o sample.o sf2.o synth.o
PLAYER_OBJ = wm_tty.o msleep.o out_none.o out_wave.o out_coreaudio.o wildmidi.o
# out_openal.o

Expand Down
2 changes: 1 addition & 1 deletion macosx/config.h
Original file line number Diff line number Diff line change
Expand Up @@ -24,9 +24,9 @@
#define HAVE_EXPF 1
#define HAVE_SQRTF 1
#define HAVE_POWF 1
#define HAVE_SINF 1

#define WILDMIDI_SF2 1

/* #undef AUDIODRV_OPENAL */
#define AUDIODRV_COREAUDIO 1

2 changes: 1 addition & 1 deletion mingw/Makefile
Original file line number Diff line number Diff line change
Expand Up @@ -52,7 +52,7 @@ LDLIBS_EXE+=-L. -l$(LIBNAME)

# Objects
LIB_OBJ = wm_error.o file_io.o lock.o wildmidi_lib.o reverb.o gus_pat.o
LIB_OBJ+= f_xmidi.o f_mus.o f_hmp.o f_midi.o f_hmi.o mus2mid.o xmi2mid.o internal_midi.o patches.o sample.o sf2.o
LIB_OBJ+= f_xmidi.o f_mus.o f_hmp.o f_midi.o f_hmi.o mus2mid.o xmi2mid.o internal_midi.o patches.o sample.o sf2.o synth.o
PLAYER_OBJ = wm_tty.o msleep.o getopt_long.o out_none.o out_wave.o out_win32mm.o wildmidi.o
# out_openal.o

Expand Down
2 changes: 1 addition & 1 deletion mingw/config.h
Original file line number Diff line number Diff line change
Expand Up @@ -20,9 +20,9 @@
#define HAVE_EXPF 1
#define HAVE_SQRTF 1
#define HAVE_POWF 1
#define HAVE_SINF 1

#define WILDMIDI_SF2 1

/* #undef AUDIODRV_OPENAL */
#define AUDIODRV_WINMM 1

4 changes: 3 additions & 1 deletion msvc/common.mak
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@ PLAYER = wildmidi.exe
LIBS_DLL=
LIBS_PLY= $(IMPNAME) winmm.lib

DLL_OBJ = wm_error.obj file_io.obj lock.obj wildmidi_lib.obj reverb.obj gus_pat.obj f_xmidi.obj f_mus.obj f_hmp.obj f_midi.obj f_hmi.obj mus2mid.obj xmi2mid.obj internal_midi.obj patches.obj sample.obj sf2.obj
DLL_OBJ = wm_error.obj file_io.obj lock.obj wildmidi_lib.obj reverb.obj gus_pat.obj f_xmidi.obj f_mus.obj f_hmp.obj f_midi.obj f_hmi.obj mus2mid.obj xmi2mid.obj internal_midi.obj patches.obj sample.obj sf2.obj synth.obj
PLY_OBJ = wm_tty.obj msleep.obj getopt_long.obj out_none.obj out_wave.obj out_win32mm.obj wildmidi.obj
# out_openal.obj

Expand Down Expand Up @@ -57,6 +57,8 @@ sample.obj: ..\src\sample.c
$(CC) $(DLL_FLAGS) $(INCLUDES) -c -Fo$@ $?
sf2.obj: ..\src\sf2.c
$(CC) $(DLL_FLAGS) $(INCLUDES) -c -Fo$@ $?
synth.obj: ..\src\synth.c
$(CC) $(DLL_FLAGS) $(INCLUDES) -c -Fo$@ $?

# player objects:
wildmidi.obj: ..\src\player\wildmidi.c
Expand Down
2 changes: 1 addition & 1 deletion msvc/config.h
Original file line number Diff line number Diff line change
Expand Up @@ -24,10 +24,10 @@
#define HAVE_EXPF 1
#define HAVE_SQRTF 1
#define HAVE_POWF 1
#define HAVE_SINF 1
#endif

#define WILDMIDI_SF2 1

/* #undef AUDIODRV_OPENAL */
#define AUDIODRV_WINMM 1

2 changes: 1 addition & 1 deletion os2/config.h
Original file line number Diff line number Diff line change
Expand Up @@ -18,9 +18,9 @@
#define HAVE_EXPF 1
#define HAVE_SQRTF 1
#define HAVE_POWF 1
#define HAVE_SINF 1
#endif

#define WILDMIDI_SF2 1

#define AUDIODRV_OS2DART 1

2 changes: 1 addition & 1 deletion os2/makefile
Original file line number Diff line number Diff line change
Expand Up @@ -38,7 +38,7 @@ BLD_TARGET=$(DLLNAME) $(PLAYER)
INCPATH=-I"$(%WATCOM)/h/os2" -I"$(%WATCOM)/h"
INCLUDES=$(INCPATH) -I. -I"../include"

OBJ=wm_error.obj file_io.obj lock.obj wildmidi_lib.obj reverb.obj gus_pat.obj f_xmidi.obj f_mus.obj f_hmp.obj f_midi.obj f_hmi.obj mus2mid.obj xmi2mid.obj internal_midi.obj patches.obj sample.obj sf2.obj
OBJ=wm_error.obj file_io.obj lock.obj wildmidi_lib.obj reverb.obj gus_pat.obj f_xmidi.obj f_mus.obj f_hmp.obj f_midi.obj f_hmi.obj mus2mid.obj xmi2mid.obj internal_midi.obj patches.obj sample.obj sf2.obj synth.obj
PLAYER_OBJ=wm_tty.obj msleep.obj getopt_long.obj out_none.obj out_wave.obj out_dart.obj wildmidi.obj

all: $(BLD_TARGET)
Expand Down
2 changes: 1 addition & 1 deletion os2/makefile.emx
Original file line number Diff line number Diff line change
Expand Up @@ -27,7 +27,7 @@ PLAYER_LIBS+=-lmmpm2
CFLAGS_LIB= $(CFLAGS) -DWILDMIDI_BUILD
CFLAGS_EXE= $(CFLAGS)

OBJ=wm_error.o file_io.o lock.o wildmidi_lib.o reverb.o gus_pat.o f_xmidi.o f_mus.o f_hmp.o f_midi.o f_hmi.o mus2mid.o xmi2mid.o internal_midi.o patches.o sample.o sf2.o
OBJ=wm_error.o file_io.o lock.o wildmidi_lib.o reverb.o gus_pat.o f_xmidi.o f_mus.o f_hmp.o f_midi.o f_hmi.o mus2mid.o xmi2mid.o internal_midi.o patches.o sample.o sf2.o synth.o
PLAYER_OBJ=wm_tty.o msleep.o getopt_long.o out_none.o out_wave.o out_dart.o wildmidi.o

all: $(LIBSTATIC) $(PLAYER_STATIC)
Expand Down
2 changes: 2 additions & 0 deletions src/CMakeLists.txt
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@ SET(wildmidi_library_SRCS
f_midi.c
f_hmi.c
sample.c
synth.c
sf2.c
mus2mid.c
xmi2mid.c
Expand All @@ -34,6 +35,7 @@ SET(wildmidi_library_HDRS
../include/internal_midi.h
../include/patches.h
../include/sample.h
../include/synth.h
../include/sf2.h
../include/common.h
../include/filenames.h
Expand Down
8 changes: 7 additions & 1 deletion src/player/wildmidi.c
Original file line number Diff line number Diff line change
Expand Up @@ -264,6 +264,7 @@ static struct option const long_options[] = {
{ "textaslyric", 0, NULL, 'a' },
{ "playfrom", 1, NULL, 'i'},
{ "playto", 1, NULL, 'j'},
{ "emergency", 0, NULL, 'E' },
{ NULL, 0, NULL, 0 }
};

Expand Down Expand Up @@ -300,6 +301,7 @@ static void do_help(void) {
printf(" -r N --rate=N Set sample rate to N samples per second (Hz)\n");
printf(" -c P --config=P Point to your wildmidi.cfg config file name/path\n");
printf(" defaults to: %s\n", WILDMIDI_CFG);
printf(" -E --emergency Use built-in synthesised soundbank (no cfg/sf2 needed)\n");
printf(" -m V --mastervol=V Set the master volume (0..127), default is 100\n");
printf(" -b --reverb Enable final output reverb engine\n\n");
}
Expand Down Expand Up @@ -382,7 +384,7 @@ int main(int argc, char **argv) {

do_version();
while (1) {
i = getopt_long(argc, argv, "0vho:tx:g:P:f:lr:c:m:btak:p:ed:nsi:j:", long_options,
i = getopt_long(argc, argv, "0vho:tx:g:P:f:lr:c:m:btak:p:ed:nsi:j:E", long_options,
&option_index);
if (i == -1)
break;
Expand Down Expand Up @@ -495,6 +497,10 @@ int main(int argc, char **argv) {
case 'j':
play_to = (unsigned long int)(atof(optarg) * (double)rate);
break;
case 'E': /* Emergency built-in soundbank */
strncpy(config_file, "@emergency", sizeof(config_file));
config_file[sizeof(config_file) - 1] = 0;
break;
default:
do_syntax();
return (1);
Expand Down
Loading
Loading