From 3888a77e3c124cd19fc01a2e960037aa029ea4d7 Mon Sep 17 00:00:00 2001 From: Bret Curtis Date: Thu, 9 Jul 2026 15:03:29 +0200 Subject: [PATCH 1/5] initial work on software synth --- src/CMakeLists.txt | 2 ++ src/player/wildmidi.c | 8 +++++++- src/sample.c | 8 +++++++- src/wildmidi_lib.c | 13 +++++++++++++ 4 files changed, 29 insertions(+), 2 deletions(-) diff --git a/src/CMakeLists.txt b/src/CMakeLists.txt index 99cbb372..020a0de2 100644 --- a/src/CMakeLists.txt +++ b/src/CMakeLists.txt @@ -14,6 +14,7 @@ SET(wildmidi_library_SRCS f_midi.c f_hmi.c sample.c + emerg_synth.c sf2.c mus2mid.c xmi2mid.c @@ -34,6 +35,7 @@ SET(wildmidi_library_HDRS ../include/internal_midi.h ../include/patches.h ../include/sample.h + ../include/emerg_synth.h ../include/sf2.h ../include/common.h ../include/filenames.h diff --git a/src/player/wildmidi.c b/src/player/wildmidi.c index 852b6556..e4baf7ba 100644 --- a/src/player/wildmidi.c +++ b/src/player/wildmidi.c @@ -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 } }; @@ -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"); } @@ -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; @@ -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); diff --git a/src/sample.c b/src/sample.c index fd4feb62..5c396b7c 100644 --- a/src/sample.c +++ b/src/sample.c @@ -33,6 +33,7 @@ #include "wildmidi_lib.h" #include "internal_midi.h" #include "sample.h" +#include "emerg_synth.h" /* FIXME: Need to decide if this stuff needs to be broken up for different formats. @@ -123,7 +124,12 @@ _WM_load_sample(struct _patch *sample_patch) { /* we only want to try loading the guspat once. */ sample_patch->loaded = 1; - if ((guspat = _WM_load_gus_pat(sample_patch->filename, _WM_fix_release)) == NULL) { + if (sample_patch->filename == NULL) { + /* Emergency-soundbank mode: no file, fabricate a sample. */ + if ((guspat = _WM_synth_patch(sample_patch->patchid)) == NULL) { + return (-1); + } + } else if ((guspat = _WM_load_gus_pat(sample_patch->filename, _WM_fix_release)) == NULL) { return (-1); } diff --git a/src/wildmidi_lib.c b/src/wildmidi_lib.c index 4636b172..7e34bbd1 100644 --- a/src/wildmidi_lib.c +++ b/src/wildmidi_lib.c @@ -49,6 +49,7 @@ #include "f_xmidi.h" #include "patches.h" #include "sample.h" +#include "emerg_synth.h" #include "mus2mid.h" #include "xmi2mid.h" #ifdef WILDMIDI_SF2 @@ -1667,6 +1668,17 @@ static int _WM_Init(const struct _WM_VIO *callbacks, _WM_FreeBufferFile = callbacks->free_file; WM_InitPatches(); + + /* Emergency soundbank sentinel: skip file loading entirely and populate + _WM_patch[] with synthesised GM voices. */ + if (strcmp(config_file, WM_EMERGENCY_CONFIG) == 0) { + if (_WM_emergency_init_patches() < 0) { + WM_FreePatches(); + _WM_GLOBAL_ERROR(WM_ERR_MEM, NULL, 0); + return (-1); + } + goto post_config_load; + } #ifdef WILDMIDI_SF2 { /* a soundfont can be given directly in place of a config file */ @@ -1700,6 +1712,7 @@ static int _WM_Init(const struct _WM_VIO *callbacks, } #endif +post_config_load: if (mixer_options & 0x0FF0) { _WM_GLOBAL_ERROR(WM_ERR_INVALID_ARG, "(invalid option)", 0); From 98b4bcdc30a974de46dd630bf5dadf5ae0bf151e Mon Sep 17 00:00:00 2001 From: Bret Curtis Date: Thu, 9 Jul 2026 15:13:18 +0200 Subject: [PATCH 2/5] phase 2: add tonal and drums --- src/CMakeLists.txt | 4 ++-- src/sample.c | 2 +- src/wildmidi_lib.c | 2 +- 3 files changed, 4 insertions(+), 4 deletions(-) diff --git a/src/CMakeLists.txt b/src/CMakeLists.txt index 020a0de2..ad813311 100644 --- a/src/CMakeLists.txt +++ b/src/CMakeLists.txt @@ -14,7 +14,7 @@ SET(wildmidi_library_SRCS f_midi.c f_hmi.c sample.c - emerg_synth.c + synth.c sf2.c mus2mid.c xmi2mid.c @@ -35,7 +35,7 @@ SET(wildmidi_library_HDRS ../include/internal_midi.h ../include/patches.h ../include/sample.h - ../include/emerg_synth.h + ../include/synth.h ../include/sf2.h ../include/common.h ../include/filenames.h diff --git a/src/sample.c b/src/sample.c index 5c396b7c..4277123a 100644 --- a/src/sample.c +++ b/src/sample.c @@ -33,7 +33,7 @@ #include "wildmidi_lib.h" #include "internal_midi.h" #include "sample.h" -#include "emerg_synth.h" +#include "synth.h" /* FIXME: Need to decide if this stuff needs to be broken up for different formats. diff --git a/src/wildmidi_lib.c b/src/wildmidi_lib.c index 7e34bbd1..fe69b173 100644 --- a/src/wildmidi_lib.c +++ b/src/wildmidi_lib.c @@ -49,7 +49,7 @@ #include "f_xmidi.h" #include "patches.h" #include "sample.h" -#include "emerg_synth.h" +#include "synth.h" #include "mus2mid.h" #include "xmi2mid.h" #ifdef WILDMIDI_SF2 From ba0d0da92f1ee3a4fbcb284a6fdf85a785f5254a Mon Sep 17 00:00:00 2001 From: Bret Curtis Date: Thu, 9 Jul 2026 15:23:39 +0200 Subject: [PATCH 3/5] updated freq_root to get rid of static between notes; tweak drums --- docs/synth_plan.md | 119 +++++++++++ include/synth.h | 33 +++ src/synth.c | 487 +++++++++++++++++++++++++++++++++++++++++++++ 3 files changed, 639 insertions(+) create mode 100644 docs/synth_plan.md create mode 100644 include/synth.h create mode 100644 src/synth.c diff --git a/docs/synth_plan.md b/docs/synth_plan.md new file mode 100644 index 00000000..fb361b81 --- /dev/null +++ b/docs/synth_plan.md @@ -0,0 +1,119 @@ +# 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); } +``` + +Trigger: `--emergency` CLI flag, or automatic fallback when no cfg is found. +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/emerg_synth.c` + `include/emerg_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/emerg_synth.h`, `src/emerg_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 option. +5. Auto‑enable the fallback when no cfg is discoverable (opt‑in via option, + not silent, so debugging stays honest). +6. `test/` — one MIDI file rendered with `-o` to WAV, byte length asserted + non‑zero; that's the "did anything come out" smoke test. diff --git a/include/synth.h b/include/synth.h new file mode 100644 index 00000000..c0f663c9 --- /dev/null +++ b/include/synth.h @@ -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 + +#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 */ diff --git a/src/synth.c b/src/synth.c new file mode 100644 index 00000000..4c13ed34 --- /dev/null +++ b/src/synth.c @@ -0,0 +1,487 @@ +/* + * synth.c -- Built-in synthesiser for WildMIDI's emergency soundbank. + * + * 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. + * + * All code below is original clean-room work: no lift from Gervill's + * EmergencySoundbank (GPL2), csharpsynth, or any other bank. The techniques + * (additive synthesis, filtered noise, pitched sine sweeps for kicks, + * inharmonic partials for cymbals) are textbook. + */ + +#include "config.h" + +#include +#include +#include +#include +#include + +#include "common.h" +#include "wildmidi_lib.h" +#include "wm_error.h" +#include "patches.h" +#include "sample.h" +#include "synth.h" + +#ifndef M_PI +#define M_PI 3.14159265358979323846 +#endif + +extern uint16_t _WM_SampleRate; + +/* ------------------------------------------------------------------ */ +/* Envelopes */ +/* ------------------------------------------------------------------ */ + +static int32_t env_rate_for_seconds(float seconds) { + if (seconds <= 0.0f) seconds = 0.001f; + return (int32_t)(4194303.0f / ((float)_WM_SampleRate * seconds)); +} + +/* Fill the 6+1 mixer envelope stages. 0..3 run while note is held, + 4..6 after note-off. peak/sustain are in mixer fixed-point (4194303 = 1.0). */ +static void set_envelope(struct _sample *s, int32_t sustain, + float attack_s, float decay_s, float release_s) { + const int32_t peak = 4194303; + s->env_target[0] = peak; s->env_rate[0] = env_rate_for_seconds(attack_s); + s->env_target[1] = sustain; s->env_rate[1] = env_rate_for_seconds(decay_s); + s->env_target[2] = sustain; s->env_rate[2] = env_rate_for_seconds(0.5f); + s->env_target[3] = sustain; s->env_rate[3] = env_rate_for_seconds(4.0f); + s->env_target[4] = 0; s->env_rate[4] = env_rate_for_seconds(release_s); + s->env_target[5] = 0; s->env_rate[5] = env_rate_for_seconds(release_s); + s->env_target[6] = 0; s->env_rate[6] = env_rate_for_seconds(0.010f); +} + +/* ------------------------------------------------------------------ */ +/* Per-family harmonic profiles */ +/* ------------------------------------------------------------------ */ + +/* Eight amplitudes: h[k] weights partial k+1. Values live in [0,1]; + normalisation happens after summing to keep the peak at ~28000. */ +typedef struct { + float h[8]; + int32_t sustain; + float attack, decay, release; +} tonal_voice; + +/* Sixteen GM sound groups (program >> 3). Written by ear for family distinction. */ +static const tonal_voice gm_voice[16] = { + /* 0 Piano */ {{1.0f,0.60f,0.40f,0.30f,0.25f,0.20f,0.15f,0.10f}, 1258291, 0.003f, 0.400f, 0.180f}, + /* 1 Chrom.perc */ {{1.0f,0.30f,0.50f,0.20f,0.30f,0.15f,0.20f,0.10f}, 838860, 0.002f, 0.300f, 0.120f}, + /* 2 Organ */ {{1.0f,0.90f,0.50f,0.70f,0.30f,0.40f,0.20f,0.50f}, 4194303, 0.012f, 0.080f, 0.100f}, + /* 3 Guitar */ {{1.0f,0.70f,0.50f,0.40f,0.30f,0.25f,0.20f,0.15f}, 1677721, 0.003f, 0.500f, 0.180f}, + /* 4 Bass */ {{1.0f,0.80f,0.50f,0.35f,0.25f,0.18f,0.12f,0.08f}, 2097151, 0.004f, 0.400f, 0.160f}, + /* 5 Strings */ {{1.0f,0.80f,0.60f,0.50f,0.40f,0.30f,0.25f,0.20f}, 3355443, 0.060f, 0.200f, 0.250f}, + /* 6 Ensemble */ {{1.0f,0.85f,0.65f,0.55f,0.45f,0.35f,0.28f,0.22f}, 3355443, 0.050f, 0.200f, 0.300f}, + /* 7 Brass */ {{0.60f,1.00f,0.90f,0.80f,0.70f,0.50f,0.40f,0.30f}, 3145727, 0.030f, 0.150f, 0.150f}, + /* 8 Reed */ {{1.0f,0.20f,0.70f,0.20f,0.50f,0.15f,0.40f,0.10f}, 3355443, 0.025f, 0.150f, 0.120f}, + /* 9 Pipe */ {{1.0f,0.15f,0.10f,0.05f,0.02f,0.00f,0.00f,0.00f}, 3355443, 0.030f, 0.150f, 0.140f}, + /* 10 Synth Lead */ {{1.0f,0.50f,0.33f,0.25f,0.20f,0.17f,0.14f,0.13f}, 3355443, 0.008f, 0.100f, 0.100f}, + /* 11 Synth Pad */ {{1.0f,0.50f,0.30f,0.20f,0.10f,0.05f,0.02f,0.01f}, 3355443, 0.150f, 0.400f, 0.400f}, + /* 12 Synth FX */ {{1.0f,0.40f,0.60f,0.30f,0.40f,0.20f,0.30f,0.15f}, 2097151, 0.020f, 0.300f, 0.250f}, + /* 13 Ethnic */ {{1.0f,0.55f,0.45f,0.35f,0.25f,0.20f,0.15f,0.10f}, 1677721, 0.005f, 0.400f, 0.180f}, + /* 14 Percussive */ {{1.0f,0.30f,0.45f,0.20f,0.25f,0.15f,0.15f,0.10f}, 838860, 0.002f, 0.300f, 0.120f}, + /* 15 SFX */ {{1.0f,0.40f,0.35f,0.25f,0.20f,0.15f,0.10f,0.08f}, 1677721, 0.005f, 0.400f, 0.200f} +}; + +/* ------------------------------------------------------------------ */ +/* Sample construction helpers */ +/* ------------------------------------------------------------------ */ + +static struct _sample *alloc_sample(uint32_t n) { + struct _sample *s = (struct _sample *)calloc(1, sizeof(struct _sample)); + if (!s) return NULL; + s->data = (int16_t *)calloc((size_t)n + 2, sizeof(int16_t)); + if (!s->data) { free(s); return NULL; } + return s; +} + +/* Populate the mixer-facing fields of a _sample. root_hz is the pitch of the + raw buffer when played at _WM_SampleRate; the mixer pitch-shifts around it. */ +static void configure(struct _sample *s, uint32_t n, double root_hz, int looped) { + s->rate = _WM_SampleRate; + /* freq_root in GUS patches is Hz * 1000: derived from gus_pat's inc_div + formula ((freq_root*512)/rate)*2. Larger values silently overflow uint32 + in freq_root*512, producing pitch-per-note inconsistency. */ + s->freq_root = (uint32_t)(root_hz * 1000.0); + s->freq_low = 0; + s->freq_high = 0xFFFFFFFFu; + s->loop_fraction = 0; + s->inc_div = ((s->freq_root * 512u) / s->rate) * 2u; + s->modes = SAMPLE_16BIT | SAMPLE_ENVELOPE | (looped ? SAMPLE_LOOP : 0); + s->data_length = n << 10; + s->loop_start = 0; + s->loop_end = n << 10; + s->loop_size = s->loop_end - s->loop_start; + s->note_off_decay = _WM_SampleRate; + s->next = NULL; +} + +/* Normalise a float buffer to [-target, +target] and write it as int16. + `tail_fade` linearly fades the last N samples to zero so non-looped one-shots + don't click when the mixer cuts off at data_length. */ +static void normalise_and_write(struct _sample *s, uint32_t n, float *buf, + float target, uint32_t tail_fade) { + float peak = 0.0f; + uint32_t i; + for (i = 0; i < n; i++) { + float a = buf[i] < 0 ? -buf[i] : buf[i]; + if (a > peak) peak = a; + } + if (peak < 1e-6f) peak = 1.0f; + if (tail_fade > n) tail_fade = n; + for (i = 0; i < n; i++) { + float v = buf[i] * target / peak; + if (tail_fade && i >= n - tail_fade) { + v *= (float)(n - 1 - i) / (float)tail_fade; + } + s->data[i] = (int16_t)v; + } + /* gus_pat-style guard samples for the mixer's interpolator. */ + s->data[n] = s->data[0]; + s->data[n + 1] = s->data[1 % n]; +} + +/* ------------------------------------------------------------------ */ +/* Tonal synthesis (additive harmonics per family) */ +/* ------------------------------------------------------------------ */ + +static struct _sample *synth_tonal(uint16_t patchid) { + uint8_t program = patchid & 0x7F; + const tonal_voice *v = &gm_voice[program >> 3]; + /* One fundamental period: loop covers exactly one cycle. All partials + are integer multiples of the fundamental so the boundary is bit-exact. */ + uint32_t period = _WM_SampleRate / 262u; + if (period < 32) period = 32; + uint32_t n = period; + double root_hz = (double)_WM_SampleRate / (double)period; + struct _sample *s; + float *buf; + uint32_t i; + int k; + if (!(s = alloc_sample(n))) return NULL; + buf = (float *)calloc(n, sizeof(float)); + if (!buf) { free(s->data); free(s); return NULL; } + + for (i = 0; i < n; i++) { + double phase = 2.0 * M_PI * (double)i / (double)period; + float acc = 0.0f; + for (k = 0; k < 8; k++) { + if (v->h[k] != 0.0f) { + acc += v->h[k] * (float)sin(phase * (double)(k + 1)); + } + } + buf[i] = acc; + } + /* Tonal is looped; loop_start == data[0] == 0 so no tail fade needed. */ + normalise_and_write(s, n, buf, 28000.0f, 0); + free(buf); + + configure(s, n, root_hz, 1); + set_envelope(s, v->sustain, v->attack, v->decay, v->release); + return s; +} + +/* ------------------------------------------------------------------ */ +/* Drum synthesis */ +/* ------------------------------------------------------------------ */ + +typedef struct { uint32_t s; } rng_t; +static float rng_bipolar(rng_t *r) { + r->s = r->s * 1103515245u + 12345u; + return ((int32_t)(r->s >> 8) & 0xFFFF) / 32768.0f - 1.0f; +} + +/* Kick / tom: rapid pitch drop from f_start Hz to f_end Hz over the buffer, + plus a short attack click. Toms use the same generator, higher pitch. */ +static struct _sample *synth_kick(uint8_t note) { + uint32_t n = _WM_SampleRate * 3u / 10u; /* 300 ms */ + struct _sample *s; + float *buf; + uint32_t i; + float f_start = (note <= 36) ? 150.0f : 120.0f + 6.0f * (float)note; + float f_end = (note <= 36) ? 45.0f : 60.0f + 3.5f * (float)note; + float click = (note <= 36) ? 1.0f : 0.35f; + double phase = 0.0; + if (!(s = alloc_sample(n))) return NULL; + buf = (float *)calloc(n, sizeof(float)); + if (!buf) { free(s->data); free(s); return NULL; } + + for (i = 0; i < n; i++) { + float t = (float)i / (float)n; + float f = f_end + (f_start - f_end) * expf(-6.0f * t); + phase += 2.0 * M_PI * (double)f / (double)_WM_SampleRate; + float body = sinf((float)phase); + float amp = expf(-4.5f * t); + float attack_click = (i < 30) ? click * (1.0f - (float)i / 30.0f) : 0.0f; + buf[i] = amp * body + attack_click; + } + normalise_and_write(s, n, buf, 30000.0f, 128); + free(buf); + + configure(s, n, 100.0, 0); + set_envelope(s, 0, 0.001f, 0.5f, 0.5f); + return s; +} + +/* Snare: mid-freq body + band-limited noise, both decaying quickly. */ +static struct _sample *synth_snare(uint8_t note) { + uint32_t n = _WM_SampleRate * 2u / 10u; /* 200 ms */ + struct _sample *s; + float *buf; + uint32_t i; + rng_t r = { 0xC0FFEEu ^ ((uint32_t)note * 2654435761u) }; + double phase = 0.0; + float tone_freq = (note == 40 || note == 38) ? 180.0f : 220.0f; + float lpf = 0.0f; + if (!(s = alloc_sample(n))) return NULL; + buf = (float *)calloc(n, sizeof(float)); + if (!buf) { free(s->data); free(s); return NULL; } + + for (i = 0; i < n; i++) { + float t = (float)i / (float)n; + phase += 2.0 * M_PI * (double)tone_freq / (double)_WM_SampleRate; + float body = sinf((float)phase) * expf(-6.0f * t); + float x = rng_bipolar(&r); + lpf += 0.45f * (x - lpf); + buf[i] = 0.55f * body + 0.9f * lpf * expf(-4.0f * t); + } + normalise_and_write(s, n, buf, 28000.0f, 128); + free(buf); + + configure(s, n, 200.0, 0); + set_envelope(s, 0, 0.001f, 0.15f, 0.15f); + return s; +} + +/* Hi-hat / shaker / clap: HP-flavoured noise. `open` selects longer decay. */ +static struct _sample *synth_hat(uint8_t note, int open) { + uint32_t n = open ? (_WM_SampleRate * 3u / 10u) : (_WM_SampleRate / 15u); + struct _sample *s; + float *buf; + uint32_t i; + rng_t r = { 0xBADF00Du ^ ((uint32_t)note * 2654435761u) }; + float lpf = 0.0f; + float decay_k = open ? 4.0f : 25.0f; + if (!(s = alloc_sample(n))) return NULL; + buf = (float *)calloc(n, sizeof(float)); + if (!buf) { free(s->data); free(s); return NULL; } + + for (i = 0; i < n; i++) { + float t = (float)i / (float)n; + float x = rng_bipolar(&r); + lpf += 0.15f * (x - lpf); + float hp = x - lpf; + buf[i] = hp * expf(-decay_k * t); + } + normalise_and_write(s, n, buf, 22000.0f, 128); + free(buf); + + configure(s, n, 8000.0, 0); + set_envelope(s, 0, 0.001f, open ? 0.25f : 0.06f, open ? 0.15f : 0.05f); + return s; +} + +/* Cymbal / crash / ride: three inharmonic partials + broadband noise. */ +static struct _sample *synth_cymbal(uint8_t note) { + uint32_t n = _WM_SampleRate * 3u / 5u; /* 600 ms */ + struct _sample *s; + float *buf; + uint32_t i; + rng_t r = { 0xCA55EEu ^ ((uint32_t)note * 2654435761u) }; + const float ratios[3] = { 1.0f, 1.593f, 2.135f }; + const float base = (note == 51 || note == 53 || note == 59) ? 700.0f : 500.0f; + double ph[3] = { 0.0, 0.0, 0.0 }; + float noise_decay = (note == 49 || note == 57) ? 2.5f : 4.5f; + if (!(s = alloc_sample(n))) return NULL; + buf = (float *)calloc(n, sizeof(float)); + if (!buf) { free(s->data); free(s); return NULL; } + + for (i = 0; i < n; i++) { + float t = (float)i / (float)n; + int k; + float partials = 0.0f; + for (k = 0; k < 3; k++) { + ph[k] += 2.0 * M_PI * (double)(base * ratios[k]) / (double)_WM_SampleRate; + partials += sinf((float)ph[k]); + } + float x = rng_bipolar(&r); + buf[i] = 0.5f * partials * expf(-2.0f * t) + 0.7f * x * expf(-noise_decay * t); + } + normalise_and_write(s, n, buf, 26000.0f, 128); + free(buf); + + configure(s, n, 800.0, 0); + set_envelope(s, 0, 0.001f, 0.5f, 0.4f); + return s; +} + +/* Bell-like: fundamental + inharmonic partial. Cowbell / agogo / triangle. */ +static struct _sample *synth_bell(uint8_t note, float base_hz, float decay_s) { + uint32_t n = (uint32_t)((float)_WM_SampleRate * (decay_s + 0.1f)); + struct _sample *s; + float *buf; + uint32_t i; + double ph1 = 0.0, ph2 = 0.0; + (void)note; + if (!(s = alloc_sample(n))) return NULL; + buf = (float *)calloc(n, sizeof(float)); + if (!buf) { free(s->data); free(s); return NULL; } + + for (i = 0; i < n; i++) { + float t = (float)i / (float)n; + ph1 += 2.0 * M_PI * (double)base_hz / (double)_WM_SampleRate; + ph2 += 2.0 * M_PI * (double)(base_hz * 2.76f) / (double)_WM_SampleRate; + buf[i] = (0.7f * sinf((float)ph1) + 0.3f * sinf((float)ph2)) * expf(-3.0f / decay_s * t); + } + normalise_and_write(s, n, buf, 26000.0f, 128); + free(buf); + + configure(s, n, (double)base_hz, 0); + set_envelope(s, 0, 0.001f, decay_s * 0.7f, decay_s * 0.5f); + return s; +} + +/* Short pitched click for wood-block / claves / sticks / bongo / conga. */ +static struct _sample *synth_click(uint8_t note, float freq, float decay_s) { + uint32_t n = (uint32_t)((float)_WM_SampleRate * (decay_s + 0.03f)); + struct _sample *s; + float *buf; + uint32_t i; + double phase = 0.0; + (void)note; + if (!(s = alloc_sample(n))) return NULL; + buf = (float *)calloc(n, sizeof(float)); + if (!buf) { free(s->data); free(s); return NULL; } + + for (i = 0; i < n; i++) { + float t = (float)i / (float)n; + phase += 2.0 * M_PI * (double)freq / (double)_WM_SampleRate; + buf[i] = sinf((float)phase) * expf(-6.0f / decay_s * t); + } + normalise_and_write(s, n, buf, 22000.0f, 128); + free(buf); + + configure(s, n, (double)freq, 0); + set_envelope(s, 0, 0.001f, decay_s * 0.7f, decay_s * 0.5f); + return s; +} + +/* Fallback for any drum key not otherwise handled. */ +static struct _sample *synth_drum_generic(uint8_t note) { + uint32_t n = _WM_SampleRate / 6u; + struct _sample *s; + float *buf; + uint32_t i; + rng_t r = { 0xDEADBEEFu ^ ((uint32_t)note * 2654435761u) }; + float lpf = 0.0f; + if (!(s = alloc_sample(n))) return NULL; + buf = (float *)calloc(n, sizeof(float)); + if (!buf) { free(s->data); free(s); return NULL; } + for (i = 0; i < n; i++) { + float t = (float)i / (float)n; + float x = rng_bipolar(&r); + lpf += 0.4f * (x - lpf); + buf[i] = lpf * expf(-6.0f * t); + } + normalise_and_write(s, n, buf, 22000.0f, 128); + free(buf); + configure(s, n, 300.0, 0); + set_envelope(s, 0, 0.001f, 0.10f, 0.08f); + return s; +} + +/* Dispatch by GM percussion key (General MIDI Level 1 chart). */ +static struct _sample *synth_drum(uint16_t patchid) { + uint8_t k = patchid & 0x7F; + switch (k) { + case 35: case 36: /* kicks */ + case 41: case 43: case 45: case 47: case 48: case 50: /* toms */ + return synth_kick(k); + case 37: /* side stick */ + return synth_click(k, 800.0f, 0.05f); + case 38: case 40: /* snares */ + return synth_snare(k); + case 39: /* hand clap */ + case 42: case 44: /* closed hat / pedal hat */ + case 54: /* tambourine */ + case 69: case 70: case 82: /* cabasa / maracas / shaker */ + return synth_hat(k, 0); + case 46: /* open hat */ + case 74: /* long guiro */ + return synth_hat(k, 1); + case 49: case 51: case 52: case 53: case 55: case 57: case 59: /* crashes / rides / china / splash */ + return synth_cymbal(k); + case 56: /* cowbell */ + return synth_bell(k, 540.0f, 0.20f); + case 60: case 61: /* bongos */ + return synth_click(k, k == 60 ? 900.0f : 600.0f, 0.15f); + case 62: case 63: case 64: /* congas */ + return synth_click(k, k == 64 ? 220.0f : 350.0f, 0.20f); + case 65: case 66: /* timbales */ + return synth_click(k, k == 65 ? 500.0f : 380.0f, 0.15f); + case 67: case 68: /* agogos */ + return synth_bell(k, k == 67 ? 850.0f : 700.0f, 0.20f); + case 73: case 75: /* short guiro / claves */ + return synth_click(k, 2500.0f, 0.04f); + case 76: case 77: /* wood blocks */ + return synth_click(k, k == 76 ? 1200.0f : 850.0f, 0.08f); + case 80: case 81: /* mute / open triangle */ + return synth_bell(k, 5000.0f, k == 81 ? 0.60f : 0.10f); + default: + return synth_drum_generic(k); + } +} + +/* ------------------------------------------------------------------ */ +/* Public entry points */ +/* ------------------------------------------------------------------ */ + +struct _sample *_WM_synth_patch(uint16_t patchid) { + struct _sample *s = (patchid & 0x80) ? synth_drum(patchid) + : synth_tonal(patchid); + if (!s) { + _WM_GLOBAL_ERROR(WM_ERR_MEM, NULL, errno); + } + return s; +} + +static struct _patch *alloc_patch(uint16_t patchid, uint8_t keep) { + struct _patch *p = (struct _patch *)calloc(1, sizeof(struct _patch)); + if (!p) return NULL; + p->patchid = patchid; + p->amp = 1024; + p->keep = keep; + return p; +} + +int _WM_emergency_init_patches(void) { + uint16_t id; + + for (id = 0; id < 128; id++) { + struct _patch *p = alloc_patch(id, 0); + if (!p) return -1; + _WM_patch[id] = p; + } + /* Drum kit chains onto _WM_patch[note & 0x7F] because _find_matched_patch + keys off patchid&0x7F. keep=SAMPLE_ENVELOPE prevents _WM_load_sample from + stripping envelope mode on drums (see sample.c). */ + for (id = 27; id <= 87; id++) { + uint16_t drumid = 0x80u | id; + struct _patch *p = alloc_patch(drumid, SAMPLE_ENVELOPE); + struct _patch *head = _WM_patch[id]; + if (!p) return -1; + while (head->next) head = head->next; + head->next = p; + } + return 0; +} From 71aa9cdc43243a5f3edb7a4e08c06b745a55ee08 Mon Sep 17 00:00:00 2001 From: Ozkan Sezer Date: Thu, 9 Jul 2026 17:23:20 +0300 Subject: [PATCH 4/5] build fixes, and warning fixes for software synth. --- CMakeLists.txt | 1 + amiga/Makefile | 2 +- amiga/Makefile.vbcc | 2 +- amiga/config.h | 3 +- android/jni/Android.mk | 1 + android/jni/config.h | 2 +- djgpp/Makefile | 2 +- djgpp/config.h | 1 + include/config.h.cmake | 1 + macosx/Makefile | 2 +- macosx/config.h | 2 +- mingw/Makefile | 2 +- mingw/config.h | 2 +- msvc/common.mak | 4 ++- msvc/config.h | 2 +- os2/config.h | 2 +- os2/makefile | 2 +- os2/makefile.emx | 2 +- src/synth.c | 75 ++++++++++++++++++++++++++++-------------- 19 files changed, 72 insertions(+), 38 deletions(-) diff --git a/CMakeLists.txt b/CMakeLists.txt index 920bd435..9b2110dd 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -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 ########## diff --git a/amiga/Makefile b/amiga/Makefile index dd15c321..75385b30 100644 --- a/amiga/Makefile +++ b/amiga/Makefile @@ -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 diff --git a/amiga/Makefile.vbcc b/amiga/Makefile.vbcc index f2bf44a7..e4b0c45b 100644 --- a/amiga/Makefile.vbcc +++ b/amiga/Makefile.vbcc @@ -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 diff --git a/amiga/config.h b/amiga/config.h index 7db056b3..9a688915 100644 --- a/amiga/config.h +++ b/amiga/config.h @@ -1,4 +1,5 @@ /* wildmidi config for amigaos variants */ + #define WILDMIDI_AMIGA 1 #define WILDMIDI_CFG "wildmidi.cfg" @@ -20,8 +21,8 @@ /* #undef HAVE_EXPF */ /* #undef HAVE_SQRTF */ /* #undef HAVE_POWF */ +/* #undef HAVE_SINF */ #define WILDMIDI_SF2 1 #define AUDIODRV_AHI 1 - diff --git a/android/jni/Android.mk b/android/jni/Android.mk index a8a03d9d..814437ed 100644 --- a/android/jni/Android.mk +++ b/android/jni/Android.mk @@ -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 diff --git a/android/jni/config.h b/android/jni/config.h index fb49c34e..3e19f7a2 100644 --- a/android/jni/config.h +++ b/android/jni/config.h @@ -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 */ - diff --git a/djgpp/Makefile b/djgpp/Makefile index a8a205f4..6b1a3fb0 100644 --- a/djgpp/Makefile +++ b/djgpp/Makefile @@ -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 diff --git a/djgpp/config.h b/djgpp/config.h index 64e393c1..baf5dbc6 100644 --- a/djgpp/config.h +++ b/djgpp/config.h @@ -19,6 +19,7 @@ /* #undef HAVE_EXPF */ /* #undef HAVE_SQRTF */ /* #undef HAVE_POWF */ +/* #undef HAVE_SINF */ #define WILDMIDI_SF2 1 diff --git a/include/config.h.cmake b/include/config.h.cmake index 8fa94d7a..c63f32e5 100644 --- a/include/config.h.cmake +++ b/include/config.h.cmake @@ -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 diff --git a/macosx/Makefile b/macosx/Makefile index 1d9a6f73..71ded69b 100644 --- a/macosx/Makefile +++ b/macosx/Makefile @@ -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 diff --git a/macosx/config.h b/macosx/config.h index e571391d..9e7f7fb8 100644 --- a/macosx/config.h +++ b/macosx/config.h @@ -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 - diff --git a/mingw/Makefile b/mingw/Makefile index ae428eff..73ef0b79 100644 --- a/mingw/Makefile +++ b/mingw/Makefile @@ -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 diff --git a/mingw/config.h b/mingw/config.h index c25b9890..27e90002 100644 --- a/mingw/config.h +++ b/mingw/config.h @@ -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 - diff --git a/msvc/common.mak b/msvc/common.mak index 0de1767c..d41cce5c 100644 --- a/msvc/common.mak +++ b/msvc/common.mak @@ -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 @@ -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 diff --git a/msvc/config.h b/msvc/config.h index c0845e6b..aa67425f 100644 --- a/msvc/config.h +++ b/msvc/config.h @@ -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 - diff --git a/os2/config.h b/os2/config.h index 5d8a22dc..9b43ebc2 100644 --- a/os2/config.h +++ b/os2/config.h @@ -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 - diff --git a/os2/makefile b/os2/makefile index f146d1a0..7f35026a 100644 --- a/os2/makefile +++ b/os2/makefile @@ -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) diff --git a/os2/makefile.emx b/os2/makefile.emx index d7ad9119..4c2d6eed 100644 --- a/os2/makefile.emx +++ b/os2/makefile.emx @@ -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) diff --git a/src/synth.c b/src/synth.c index 4c13ed34..37a94e4d 100644 --- a/src/synth.c +++ b/src/synth.c @@ -35,8 +35,18 @@ #ifndef M_PI #define M_PI 3.14159265358979323846 #endif - -extern uint16_t _WM_SampleRate; +#ifndef HAVE_EXPF +static float synth_expf(float x) { + return (float) exp(x); +} +#define expf synth_expf +#endif +#ifndef HAVE_SINF +static float synth_sinf(float x) { + return (float) sin(x); +} +#define sinf synth_sinf +#endif /* ------------------------------------------------------------------ */ /* Envelopes */ @@ -160,15 +170,16 @@ static struct _sample *synth_tonal(uint16_t patchid) { const tonal_voice *v = &gm_voice[program >> 3]; /* One fundamental period: loop covers exactly one cycle. All partials are integer multiples of the fundamental so the boundary is bit-exact. */ - uint32_t period = _WM_SampleRate / 262u; - if (period < 32) period = 32; - uint32_t n = period; + const uint32_t period_ = _WM_SampleRate / 262u; + const uint32_t period = (period_ < 32) ? 32 : period_; + const uint32_t n = period; double root_hz = (double)_WM_SampleRate / (double)period; struct _sample *s; float *buf; uint32_t i; int k; - if (!(s = alloc_sample(n))) return NULL; + s = alloc_sample(n); + if (!s) return NULL; buf = (float *)calloc(n, sizeof(float)); if (!buf) { free(s->data); free(s); return NULL; } @@ -212,17 +223,20 @@ static struct _sample *synth_kick(uint8_t note) { float f_end = (note <= 36) ? 45.0f : 60.0f + 3.5f * (float)note; float click = (note <= 36) ? 1.0f : 0.35f; double phase = 0.0; - if (!(s = alloc_sample(n))) return NULL; + s = alloc_sample(n); + if (!s) return NULL; buf = (float *)calloc(n, sizeof(float)); if (!buf) { free(s->data); free(s); return NULL; } for (i = 0; i < n; i++) { float t = (float)i / (float)n; float f = f_end + (f_start - f_end) * expf(-6.0f * t); + float body, amp, attack_click; + phase += 2.0 * M_PI * (double)f / (double)_WM_SampleRate; - float body = sinf((float)phase); - float amp = expf(-4.5f * t); - float attack_click = (i < 30) ? click * (1.0f - (float)i / 30.0f) : 0.0f; + body = sinf((float)phase); + amp = expf(-4.5f * t); + attack_click = (i < 30) ? click * (1.0f - (float)i / 30.0f) : 0.0f; buf[i] = amp * body + attack_click; } normalise_and_write(s, n, buf, 30000.0f, 128); @@ -239,19 +253,22 @@ static struct _sample *synth_snare(uint8_t note) { struct _sample *s; float *buf; uint32_t i; - rng_t r = { 0xC0FFEEu ^ ((uint32_t)note * 2654435761u) }; + rng_t r; double phase = 0.0; float tone_freq = (note == 40 || note == 38) ? 180.0f : 220.0f; float lpf = 0.0f; - if (!(s = alloc_sample(n))) return NULL; + s = alloc_sample(n); + if (!s) return NULL; buf = (float *)calloc(n, sizeof(float)); if (!buf) { free(s->data); free(s); return NULL; } + r.s = 0xC0FFEEu ^ ((uint32_t)note * 2654435761u); for (i = 0; i < n; i++) { float t = (float)i / (float)n; + float body, x; phase += 2.0 * M_PI * (double)tone_freq / (double)_WM_SampleRate; - float body = sinf((float)phase) * expf(-6.0f * t); - float x = rng_bipolar(&r); + body = sinf((float)phase) * expf(-6.0f * t); + x = rng_bipolar(&r); lpf += 0.45f * (x - lpf); buf[i] = 0.55f * body + 0.9f * lpf * expf(-4.0f * t); } @@ -269,18 +286,21 @@ static struct _sample *synth_hat(uint8_t note, int open) { struct _sample *s; float *buf; uint32_t i; - rng_t r = { 0xBADF00Du ^ ((uint32_t)note * 2654435761u) }; + rng_t r; float lpf = 0.0f; float decay_k = open ? 4.0f : 25.0f; - if (!(s = alloc_sample(n))) return NULL; + s = alloc_sample(n); + if (!s) return NULL; buf = (float *)calloc(n, sizeof(float)); if (!buf) { free(s->data); free(s); return NULL; } + r.s = 0xBADF00Du ^ ((uint32_t)note * 2654435761u); for (i = 0; i < n; i++) { float t = (float)i / (float)n; float x = rng_bipolar(&r); + float hp; lpf += 0.15f * (x - lpf); - float hp = x - lpf; + hp = x - lpf; buf[i] = hp * expf(-decay_k * t); } normalise_and_write(s, n, buf, 22000.0f, 128); @@ -297,24 +317,27 @@ static struct _sample *synth_cymbal(uint8_t note) { struct _sample *s; float *buf; uint32_t i; - rng_t r = { 0xCA55EEu ^ ((uint32_t)note * 2654435761u) }; + rng_t r; const float ratios[3] = { 1.0f, 1.593f, 2.135f }; const float base = (note == 51 || note == 53 || note == 59) ? 700.0f : 500.0f; double ph[3] = { 0.0, 0.0, 0.0 }; float noise_decay = (note == 49 || note == 57) ? 2.5f : 4.5f; - if (!(s = alloc_sample(n))) return NULL; + s = alloc_sample(n); + if (!s) return NULL; buf = (float *)calloc(n, sizeof(float)); if (!buf) { free(s->data); free(s); return NULL; } + r.s = 0xCA55EEu ^ ((uint32_t)note * 2654435761u); for (i = 0; i < n; i++) { float t = (float)i / (float)n; + float x; int k; float partials = 0.0f; for (k = 0; k < 3; k++) { ph[k] += 2.0 * M_PI * (double)(base * ratios[k]) / (double)_WM_SampleRate; partials += sinf((float)ph[k]); } - float x = rng_bipolar(&r); + x = rng_bipolar(&r); buf[i] = 0.5f * partials * expf(-2.0f * t) + 0.7f * x * expf(-noise_decay * t); } normalise_and_write(s, n, buf, 26000.0f, 128); @@ -333,7 +356,8 @@ static struct _sample *synth_bell(uint8_t note, float base_hz, float decay_s) { uint32_t i; double ph1 = 0.0, ph2 = 0.0; (void)note; - if (!(s = alloc_sample(n))) return NULL; + s = alloc_sample(n); + if (!s) return NULL; buf = (float *)calloc(n, sizeof(float)); if (!buf) { free(s->data); free(s); return NULL; } @@ -359,7 +383,8 @@ static struct _sample *synth_click(uint8_t note, float freq, float decay_s) { uint32_t i; double phase = 0.0; (void)note; - if (!(s = alloc_sample(n))) return NULL; + s = alloc_sample(n); + if (!s) return NULL; buf = (float *)calloc(n, sizeof(float)); if (!buf) { free(s->data); free(s); return NULL; } @@ -382,11 +407,13 @@ static struct _sample *synth_drum_generic(uint8_t note) { struct _sample *s; float *buf; uint32_t i; - rng_t r = { 0xDEADBEEFu ^ ((uint32_t)note * 2654435761u) }; + rng_t r; float lpf = 0.0f; - if (!(s = alloc_sample(n))) return NULL; + s = alloc_sample(n); + if (!s) return NULL; buf = (float *)calloc(n, sizeof(float)); if (!buf) { free(s->data); free(s); return NULL; } + r.s = 0xDEADBEEFu ^ ((uint32_t)note * 2654435761u); for (i = 0; i < n; i++) { float t = (float)i / (float)n; float x = rng_bipolar(&r); From b15dd5548706f27d0d25dbf22163a3ab283a3538 Mon Sep 17 00:00:00 2001 From: Bret Curtis Date: Thu, 9 Jul 2026 17:19:32 +0200 Subject: [PATCH 5/5] use opt-in only for use with sentinal value, partial roleback --- docs/synth_plan.md | 15 ++++++++------- src/synth.c | 19 +++++++++++++++++-- 2 files changed, 25 insertions(+), 9 deletions(-) diff --git a/docs/synth_plan.md b/docs/synth_plan.md index fb361b81..b55eb091 100644 --- a/docs/synth_plan.md +++ b/docs/synth_plan.md @@ -21,7 +21,9 @@ if (guspat = _WM_load_gus_pat(...)) { /* existing */ } else if (emergency_bank_enabled) { guspat = _WM_synth_patch(patchid); } ``` -Trigger: `--emergency` CLI flag, or automatic fallback when no cfg is found. +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 @@ -87,7 +89,7 @@ if every program is touched. ## Difficulty -- **A**: ~half a day. New `src/emerg_synth.c` + `include/emerg_synth.h`, a +- **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. @@ -106,14 +108,13 @@ volunteers. ## Concrete task list for approach A -1. `include/emerg_synth.h`, `src/emerg_synth.c` — one public function +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 option. -5. Auto‑enable the fallback when no cfg is discoverable (opt‑in via option, - not silent, so debugging stays honest). -6. `test/` — one MIDI file rendered with `-o` to WAV, byte length asserted +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. diff --git a/src/synth.c b/src/synth.c index 37a94e4d..9a50157b 100644 --- a/src/synth.c +++ b/src/synth.c @@ -491,12 +491,27 @@ static struct _patch *alloc_patch(uint16_t patchid, uint8_t keep) { return p; } +/* Undo whatever _WM_emergency_init_patches installed, so a partial-alloc + failure leaves _WM_patch[] in the same empty state WM_InitPatches() left it. */ +static void free_emergency_patches(void) { + uint16_t id; + for (id = 0; id < 128; id++) { + struct _patch *p = _WM_patch[id]; + while (p) { + struct _patch *next = p->next; + free(p); + p = next; + } + _WM_patch[id] = NULL; + } +} + int _WM_emergency_init_patches(void) { uint16_t id; for (id = 0; id < 128; id++) { struct _patch *p = alloc_patch(id, 0); - if (!p) return -1; + if (!p) { free_emergency_patches(); return -1; } _WM_patch[id] = p; } /* Drum kit chains onto _WM_patch[note & 0x7F] because _find_matched_patch @@ -506,7 +521,7 @@ int _WM_emergency_init_patches(void) { uint16_t drumid = 0x80u | id; struct _patch *p = alloc_patch(drumid, SAMPLE_ENVELOPE); struct _patch *head = _WM_patch[id]; - if (!p) return -1; + if (!p) { free_emergency_patches(); return -1; } while (head->next) head = head->next; head->next = p; }