diff --git a/include/mafm.h b/include/mafm.h index 85d4c3aa..92252a8d 100644 --- a/include/mafm.h +++ b/include/mafm.h @@ -49,6 +49,9 @@ void _WM_MAFM_Reset(void *synth); /* Translate a WildMIDI event to the synth. */ void _WM_MAFM_Event(void *synth, struct _mdi *mdi, struct _event *event); +/* Re-apply every channel's gain (after a WM_MO_LOG_VOLUME toggle). */ +void _WM_MAFM_AdjustChannelVolumes(struct _mdi *mdi); + /* Nonzero while notes are still sounding (release tails). */ int _WM_MAFM_ActiveVoices(void *synth); diff --git a/include/sf2.h b/include/sf2.h index ceb859e3..4d126c93 100644 --- a/include/sf2.h +++ b/include/sf2.h @@ -38,7 +38,13 @@ extern int _WM_SF2_Active(void); /* per-mdi synth instances (voices private, sample data shared) */ extern void *_WM_SF2_NewSynth(uint16_t rate); extern void _WM_SF2_FreeSynth(void *synth); -extern void _WM_SF2_Reset(void *synth); +extern void _WM_SF2_Reset(struct _mdi *mdi); + +/* re-apply every channel's volume (after a reset, or a WM_MO_LOG_VOLUME toggle) */ +extern void _WM_SF2_AdjustChannelVolumes(struct _mdi *mdi); + +/* send every sounding voice into its release stage */ +extern void _WM_SF2_ReleaseAll(void *synth); /* translate a wildmidi event to the synth */ extern void _WM_SF2_Event(void *synth, struct _mdi *mdi, struct _event *event); diff --git a/src/f_hmi.c b/src/f_hmi.c index 85833a88..729109c5 100644 --- a/src/f_hmi.c +++ b/src/f_hmi.c @@ -361,7 +361,12 @@ _WM_ParseNewHmi(const uint8_t *hmi_data, uint32_t hmi_size) { smallest_delta = note[hmi_tmp].length; } } else { - _WM_midi_setup_noteoff(hmi_mdi, note[hmi_tmp].channel, j, 0); + /* zero length note: release it at once, as the + * countdown above reads a length of 0 as "not + * sounding". j is the note sweep's counter, left + * at 128 - the off has to name this note. */ + _WM_midi_setup_noteoff(hmi_mdi, note[hmi_tmp].channel, + (uint8_t)(hmi_tmp - (i * 128)), 0); } } else { diff --git a/src/f_xmidi.c b/src/f_xmidi.c index 73657d0d..9a1b79d3 100644 --- a/src/f_xmidi.c +++ b/src/f_xmidi.c @@ -382,7 +382,16 @@ struct _mdi *_WM_ParseNewXmi(const uint8_t *xmi_data, uint32_t xmi_size) { /* store length */ xmi_notelen[128 * xmi_ch + xmi_note] = xmi_tmpdata; - if ((xmi_tmpdata > 0) && ((xmi_lowestdelta == 0) || (xmi_tmpdata < xmi_lowestdelta))) { + if (xmi_tmpdata == 0) { + /* A zero length note never reaches the countdown + * above, where 0 means "not sounding", so it would + * hang until the next note on the same key turned + * it off - the descending triplet at 38s in TES: + * Arena's SUNNYDAY.XMI rings for 1.6s that way. + * Release it at once, which is what xmi2mid.c + * writes out for the same note. */ + _WM_midi_setup_noteoff(xmi_mdi, xmi_ch, xmi_note, 0); + } else if ((xmi_lowestdelta == 0) || (xmi_tmpdata < xmi_lowestdelta)) { xmi_lowestdelta = xmi_tmpdata; } diff --git a/src/internal_midi.c b/src/internal_midi.c index ab855cad..bad281ac 100644 --- a/src/internal_midi.c +++ b/src/internal_midi.c @@ -1406,7 +1406,14 @@ void _WM_do_meta_endoftrack(struct _mdi *mdi, struct _event_data *data) { /* The FM engine keeps its own voices; a sustaining one holds its level * until key-off, so release them here too or a score that ends without * keying every note off rings on to the caller's cut-off. */ +#ifdef WILDMIDI_MAFM if (mdi->mafm_synth) _WM_MAFM_ReleaseAll(mdi->mafm_synth); +#endif +#ifdef WILDMIDI_SF2 + /* Same for the soundfont engine: without this a score that ends on a + * still-held note sustains it until the render loop's 10s tail cap. */ + if (mdi->sf2_synth) _WM_SF2_ReleaseAll(mdi->sf2_synth); +#endif return; } @@ -2149,6 +2156,12 @@ _WM_initMDI(void) { #endif _WM_do_sysex_gm_reset(mdi, NULL); +#ifdef WILDMIDI_SF2 + /* the reset above only touches mdi's own channel state; push its volumes + into the synth too, so a channel that never sends CC7 still plays at + wildmidi's default rather than TSF's unity gain */ + _WM_SF2_AdjustChannelVolumes(mdi); +#endif return (mdi); } diff --git a/src/mafm.c b/src/mafm.c index 3e1ddf33..7e675803 100644 --- a/src/mafm.c +++ b/src/mafm.c @@ -90,7 +90,9 @@ struct mafm_pcm_voice { uint32_t end_pt; /* loop/end point, clamped to len */ double pos; /* fractional read position (samples) */ double step; /* native_fs / output_rate * pitch ratio */ - float gain; /* volume * expression * velocity^2 */ + float gain; /* chan_gain * vel_gain, what render uses */ + float vel_gain; /* velocity^2, kept apart from the channel + * gain so a later CC7/CC11 can recompute */ float pan_l, pan_r; /* per-slot L/R gains from chan_pan CC */ int channel; /* -1 for ATR one-shots (no owner ch) */ int note; @@ -136,6 +138,8 @@ struct mafm_synth { uint8_t chan_program[16]; float chan_volume[16]; float chan_expression[16]; /* CC 0x0B; multiplied with volume */ + float chan_gain[16]; /* volume x expression through the mixer's + * volume curve; see mafm_apply_channel_volume */ int chan_pitch[16]; /* 14-bit pitch wheel, centred 0x2000 */ uint8_t chan_pan[16]; /* 0..127 pan CC, 64 = centre; 0xff = unset */ uint8_t chan_modulation[16]; /* CC 1 mod wheel, drives a 5Hz pitch LFO */ @@ -660,6 +664,7 @@ void *_WM_MAFM_NewSynth(const uint8_t *smaf, uint32_t size, uint16_t rate) { s->chan_program[i] = 0; s->chan_volume[i] = 1.0f; s->chan_expression[i] = 1.0f; + s->chan_gain[i] = 1.0f; s->chan_pitch[i] = 0x2000; s->chan_pan[i] = 0xff; /* sentinel: use patch pan_default */ } @@ -722,6 +727,7 @@ void _WM_MAFM_Reset(void *synth) { s->chan_program[i] = 0; s->chan_volume[i] = 1.0f; s->chan_expression[i] = 1.0f; + s->chan_gain[i] = 1.0f; s->chan_pitch[i] = 0x2000; s->chan_pan[i] = 0xff; /* sentinel: use patch pan_default */ s->chan_modulation[i] = 0; @@ -829,7 +835,7 @@ static double pcm_env_advance(struct mafm_pcm_voice *pv) { * rate (drums fix it to the played note; melodic voices use 60). params is * the voice's env + loop config, or NULL for an unenvelope one-shot. */ static void mafm_start_pcm_full(struct mafm_synth *s, struct mafm_wave *w, - float gain, int channel, int note, + float vel_gain, int channel, int note, int base_note, const struct mafm_pcm_params *params) { struct mafm_pcm_voice *pv = NULL; @@ -860,7 +866,10 @@ static void mafm_start_pcm_full(struct mafm_synth *s, struct mafm_wave *w, ratio = pow(2.0, ((double)(note - base_note) + bend_semitones) / 12.0); pv->step = (double) fs / s->rate * ratio; } - pv->gain = gain; + /* Keep the two halves apart: an ATR one-shot (channel < 0) has no owning + * channel and stays at its own gain, everything else tracks its channel. */ + pv->vel_gain = vel_gain; + pv->gain = (channel >= 0) ? s->chan_gain[channel] * vel_gain : vel_gain; pv->channel = channel; pv->note = note; /* Pan. channel < 0 stays centred; channel >= 0 tracks its chan_pan CC. @@ -1006,7 +1015,7 @@ static void mafm_note_on(struct mafm_synth *s, int ch, int note, int vel) { * converter emits vel=0 to mean "no explicit velocity", which we * treat as 100. */ float pv = (vel ? (float) vel : 100.0f) / 127.0f; - float g = s->chan_volume[ch] * s->chan_expression[ch] * pv * pv; + float g = pv * pv; /* start_pcm_full folds in the channel gain */ /* Fixed pitch for drums (drum_note != 0) means playing the wave * at native rate regardless of the incoming note. A melodic PCM * voice takes root note 60, matching the "root=middle C" default @@ -1035,8 +1044,9 @@ static void mafm_note_on(struct mafm_synth *s, int ch, int note, int vel) { v = mafm_alloc_voice(s); /* Volume x expression, matching the reference mixer. A file that keeps * volume at 100/127 and rides expression for dynamics needs both to - * combine, otherwise the swells never reach the voice. */ - _WM_MAFM_VoiceSetVolume(v, s->chan_volume[ch] * s->chan_expression[ch]); + * combine, otherwise the swells never reach the voice. See + * mafm_apply_channel_volume() for how chan_gain is derived. */ + _WM_MAFM_VoiceSetVolume(v, s->chan_gain[ch]); /* Squared velocity curve. A linear map made every mid-velocity note * nearly full-scale and constantly pushed the limiter; squaring keeps the * musical dynamic range and matches how the chip's own velocity table @@ -1090,6 +1100,43 @@ static void mafm_clear_vibrato(struct mafm_synth *s, uint8_t ch) { } } +/* Push a channel's CC7 x CC11 gain onto every voice sounding on it, so volume + * swells and expression rides reach in-flight notes: without this a long note + * that started quiet stays quiet, missing the crescendo the score encodes. + * WM_MO_LOG_VOLUME squares the gain, the same curve the GUS mixer's + * dBm_volume table (40*log10(v/127)) and the SF2 backend use. */ +static void mafm_apply_channel_volume(struct mafm_synth *s, struct _mdi *mdi, + uint8_t ch) { + float gain = s->chan_volume[ch] * s->chan_expression[ch]; + int i; + if (mdi->extra_info.mixer_options & WM_MO_LOG_VOLUME) { + gain *= gain; + } + s->chan_gain[ch] = gain; /* note-on reads this, so new notes match */ + for (i = 0; i < MAFM_POLYPHONY; i++) { + struct mafm_voice *v = &s->voices[i]; + if (_WM_MAFM_VoiceActive(v) && v->channel == ch) + _WM_MAFM_VoiceSetVolume(v, gain); + } + /* Sampled voices hold a flattened gain rather than reading the channel + * each sample, so they need the same update or a long PCM phrase would + * ignore every CC7/CC11 that arrives after its note-on. */ + for (i = 0; i < MAFM_PCM_POOL; i++) { + struct mafm_pcm_voice *pv = &s->pcm[i]; + if (pv->active && pv->channel == (int)ch) + pv->gain = gain * pv->vel_gain; + } +} + +/* Re-apply every channel's gain, for a WM_MO_LOG_VOLUME toggle mid-playback. */ +void _WM_MAFM_AdjustChannelVolumes(struct _mdi *mdi) { + uint8_t ch; + if (mdi->mafm_synth == NULL) return; + for (ch = 0; ch < 16; ch++) { + mafm_apply_channel_volume((struct mafm_synth *)mdi->mafm_synth, mdi, ch); + } +} + void _WM_MAFM_Event(void *synth, struct _mdi *mdi, struct _event *event) { struct mafm_synth *s = (struct mafm_synth *) synth; uint8_t ch = event->event_data.channel; @@ -1143,22 +1190,11 @@ void _WM_MAFM_Event(void *synth, struct _mdi *mdi, struct _event *event) { } break; case ev_control_channel_volume: case ev_control_channel_expression: { - /* Update ALL currently-sounding voices on this channel so volume - * swells / expression rides reach in-flight notes. Without this a - * long note that started at low volume stays low forever, missing - * the crescendo the score encodes as CC 7/11 rises. */ - int j; - float v_gain; if (event->evtype == ev_control_channel_volume) s->chan_volume[ch] = (float)(val & 0x7F) / 127.0f; else s->chan_expression[ch] = (float)(val & 0x7F) / 127.0f; - v_gain = s->chan_volume[ch] * s->chan_expression[ch]; - for (j = 0; j < MAFM_POLYPHONY; j++) { - struct mafm_voice *vp = &s->voices[j]; - if (_WM_MAFM_VoiceActive(vp) && vp->channel == ch) - _WM_MAFM_VoiceSetVolume(vp, v_gain); - } + mafm_apply_channel_volume(s, mdi, ch); } break; case ev_control_channel_pan: s->chan_pan[ch] = (uint8_t)(val & 0x7F); @@ -1197,6 +1233,9 @@ void _WM_MAFM_Render(void *synth, int32_t *out, uint32_t frames) { * below the 32767 cap to leave headroom for reverb / master volume. */ const double LIM_THRESHOLD = 30000.0; const double LIM_RELEASE = 0.9999; + /* Applied after the limiter, so turning the master volume down does not + * change how hard the limiter works - only how loud its output is. */ + const double master_vol = (double)_WM_MasterVolume / 1024.0; uint32_t f, i; /* Cache per-voice pan gains once per Render call: pan is a mix of the * channel's pan CC and the voice's patch pan_default, both of which are @@ -1267,8 +1306,8 @@ void _WM_MAFM_Render(void *synth, int32_t *out, uint32_t frames) { l *= gain; r *= gain; } - out[f * 2] += (int32_t) l; - out[f * 2 + 1] += (int32_t) r; + out[f * 2] += (int32_t) (l * master_vol); + out[f * 2 + 1] += (int32_t) (r * master_vol); } } diff --git a/src/patches.c b/src/patches.c index 90aeabd5..0b12136d 100644 --- a/src/patches.c +++ b/src/patches.c @@ -80,13 +80,21 @@ _WM_get_patch_data(struct _mdi *mdi, uint16_t patchid) { WMIDI_UNUSED(mdi); _WM_Lock(&_WM_patch_lock); - search_patch = _find_nearest_patch(patchid); + search_patch = _find_matched_patch(patchid); if (search_patch == NULL && (patchid & 0xff00) != 0) { - /* Nothing at all in the requested bank: fall back to bank 0 rather - * than play silence, as a hardware synth does for an unknown bank. - * SMAF needs this - its scores select Yamaha's own voice banks (0x7c - * and friends), which no GUS/SF2 patch set defines, so without the - * fallback every SMAF file that has no custom FM voices is mute. */ + /* A non-zero bank in a timidity.cfg is an overlay: it lists only the + * few programs that differ from bank 0 (eawpats' "bank 8" holds a + * single sine wave, "drumset 8" a single tambourine). Fall back to + * bank 0 for everything it does not define, or the nearest-patch + * search below would answer every request from that bank with its one + * unrelated instrument. This is also what makes SMAF audible: its + * scores select Yamaha's own voice banks (0x7c and friends), which no + * GUS/SF2 patch set defines at all. */ + search_patch = _find_matched_patch(patchid & 0x00ff); + } + if (search_patch == NULL) { + /* Bank 0 has no such program either - a sparse patch set. Nearest + * program is still better than silence. */ search_patch = _find_nearest_patch(patchid & 0x00ff); } _WM_Unlock(&_WM_patch_lock); diff --git a/src/sf2.c b/src/sf2.c index dd2c8d7c..90a1cc7d 100644 --- a/src/sf2.c +++ b/src/sf2.c @@ -69,6 +69,36 @@ typedef char tsf_char20[20]; /* no empty source. */ static tsf *WM_sf2 = NULL; int _WM_sf2_lock = 0; +/* A per-mdi synth is a tsf plus the note offs we are holding back. tsf + * releases a voice from wherever its amplitude envelope has got to, so a note + * switched off while still in its attack never becomes audible at all. XMI + * scores really do carry zero-length notes - the descending triplet at 38s in + * SUNNYDAY.XMI is three of them - and the GUS mixer keeps those by deferring + * the release until the first envelope stage has finished (see the env == 0 + * branch of _WM_do_note_off). Park the note off here and re-issue it from + * the render loop once the voice has left its attack. */ +struct wm_sf2_synth { + tsf *f; + uint8_t held_off[16][128]; /* deferred offs per channel/key, not a flag: + a key retriggered while held has a voice per + note on, and each one needs its own off */ + int held_count; + uint32_t silent_frames; /* consecutive rendered frames that stayed inaudible */ +}; + +/* How long the render has to stay inaudible before the tail counts as over. + * It just has to be longer than a waveform's own zero crossings: 2048 frames + * is 46ms even at 44.1kHz. */ +#define SF2_SILENCE_FRAMES 2048 + +/* What counts as inaudible, in 16bit output counts. A release that has decayed + * this far is 60dB below full scale and another 20dB below anything else the + * score is doing, so waiting for it to reach the last bit only buys seconds of + * dead air: GeneralUser GS runs ~4s past the last note of SUNNYDAY.XMI at 1 + * count, ~2s at 32. Only consulted once the event list is exhausted, so it + * cannot cut a quiet passage short mid-score. */ +#define SF2_SILENCE_LEVEL 32 + int _WM_SF2_Magic(const uint8_t *data, uint32_t size) { return (size >= 12 && !memcmp(data, "RIFF", 4) && !memcmp(data + 8, "sfbk", 4)); } @@ -104,6 +134,20 @@ int _WM_SF2_Active(void) { return (WM_sf2 != NULL); } +/* Channel volume, using wildmidi's own curves rather than tsf's cubic + * default, so WM_MO_LOG_VOLUME does the same thing here as it does for the + * GUS mixer. The linear curve is _WM_lin_volume[v]/1024 == v/127; the log + * curve is the MIDI2 table dBm_volume[v] == 40*log10(v/127), whose gain + * 10^(dBm/20) is just (v/127) squared. */ +static void WM_SF2_ChannelVolume(tsf *f, struct _mdi *mdi, uint8_t ch, + int volume, int expression) { + float gain = (float)((volume * expression) / 127) / 127.0f; + if (mdi->extra_info.mixer_options & WM_MO_LOG_VOLUME) { + gain *= gain; + } + tsf_channel_set_volume(f, ch, gain); +} + static void WM_SF2_InitChannels(tsf *f) { int ch; for (ch = 0; ch < 16; ch++) { @@ -111,7 +155,19 @@ static void WM_SF2_InitChannels(tsf *f) { } } +/* (Re)apply every channel's volume from the mdi's own state. Needed after a + * reset and whenever WM_MO_LOG_VOLUME is toggled mid-playback. */ +void _WM_SF2_AdjustChannelVolumes(struct _mdi *mdi) { + uint8_t ch; + if (mdi->sf2_synth == NULL) return; + for (ch = 0; ch < 16; ch++) { + WM_SF2_ChannelVolume(((struct wm_sf2_synth *)mdi->sf2_synth)->f, mdi, ch, + mdi->channel[ch].volume, mdi->channel[ch].expression); + } +} + void *_WM_SF2_NewSynth(uint16_t rate) { + struct wm_sf2_synth *s; tsf *f; _WM_Lock(&_WM_sf2_lock); f = WM_sf2 ? tsf_copy(WM_sf2) : NULL; @@ -119,46 +175,138 @@ void *_WM_SF2_NewSynth(uint16_t rate) { if (f == NULL) { return NULL; } + s = (struct wm_sf2_synth *) calloc(1, sizeof(struct wm_sf2_synth)); + if (s == NULL) { + tsf_close(f); + return NULL; + } + s->f = f; tsf_set_output(f, TSF_STEREO_INTERLEAVED, rate, 0.0f); WM_SF2_InitChannels(f); - return f; + return s; } void _WM_SF2_FreeSynth(void *synth) { if (synth) { - tsf_close((tsf *)synth); + tsf_close(((struct wm_sf2_synth *)synth)->f); + free(synth); } } -void _WM_SF2_Reset(void *synth) { - tsf *f = (tsf *)synth; +void _WM_SF2_Reset(struct _mdi *mdi) { + struct wm_sf2_synth *s = (struct wm_sf2_synth *)mdi->sf2_synth; + tsf *f; int ch; + if (s == NULL) return; + f = s->f; + memset(s->held_off, 0, sizeof(s->held_off)); + s->held_count = 0; + s->silent_frames = 0; tsf_reset(f); for (ch = 0; ch < 16; ch++) { tsf_channel_midi_control(f, ch, 121, 0); /* reset controllers */ } WM_SF2_InitChannels(f); + /* Seed the gains from _WM_do_sysex_gm_reset()'s own defaults rather than + from mdi->channel[], which makes this independent of when the caller + resets the mdi: WM_GetOutput_SF2()'s loop path resets it just after, + FastSeek/SongSeek just before, and the GM/GS/XG sysex path runs before + do_event() has applied the reset at all. All four land on the same + volume 100 / expression 127 either way. */ + for (ch = 0; ch < 16; ch++) { + WM_SF2_ChannelVolume(f, mdi, (uint8_t)ch, 100, 127); + } +} + +void _WM_SF2_ReleaseAll(void *synth) { + struct wm_sf2_synth *s = (struct wm_sf2_synth *)synth; + memset(s->held_off, 0, sizeof(s->held_off)); /* superseded by the release */ + s->held_count = 0; + tsf_note_off_all(s->f); +} + +/* Would a note off now silence the voice it lands on? tsf_channel_note_off() + * releases the sounding voice with the lowest playIndex - the oldest note on + * the key - so that is the only envelope that matters here. Asking whether + * *any* matching voice is in attack would hold an older voice that is long + * past its own attack for the whole of a newer overlapping note's. */ +static int WM_SF2_InAttack(tsf *f, int ch, int key) { + struct tsf_voice *v = f->voices, *vEnd = v ? v + f->voiceNum : TSF_NULL; + struct tsf_voice *oldest = TSF_NULL; + for (; v != vEnd; v++) { + /* the same filter tsf_channel_note_off() applies when it picks a voice */ + if (v->playingPreset == -1 || v->playingChannel != ch || v->playingKey != key + || v->ampenv.segment >= TSF_SEGMENT_RELEASE || v->heldSustain) continue; + if (!oldest || v->playIndex < oldest->playIndex) oldest = v; + } + /* nothing to release: let the off through, tsf makes it a no-op */ + return (oldest != TSF_NULL && oldest->ampenv.segment <= TSF_SEGMENT_ATTACK); } +static void WM_SF2_NoteOff(struct wm_sf2_synth *s, int ch, int key) { + if (WM_SF2_InAttack(s->f, ch, key)) { + if (s->held_off[ch][key] < 0xFF) { /* a stuck key cannot wrap the count */ + s->held_off[ch][key]++; + s->held_count++; + } + return; + } + tsf_channel_note_off(s->f, ch, key); +} + +/* Re-issue the note offs whose voices have now left their attack. tsf picks + * the oldest voice for the key, which is the one the held off belongs to; if + * the voice is gone the call is a no-op and the entry just clears. Each off + * uncovers the next voice down, which may still be in its own attack, so the + * check has to be repeated rather than draining the whole count at once. */ +static void WM_SF2_FlushHeldOffs(struct wm_sf2_synth *s) { + int ch, key; + for (ch = 0; ch < 16 && s->held_count; ch++) { + for (key = 0; key < 128 && s->held_count; key++) { + while (s->held_off[ch][key] && !WM_SF2_InAttack(s->f, ch, key)) { + s->held_off[ch][key]--; + s->held_count--; + tsf_channel_note_off(s->f, ch, key); + } + } + } +} + +/* tsf holds a voice open for its whole nominal release time, which on a + * soundfont with long releases runs on for seconds after the envelope has + * decayed out of 16bit range - dead air on the end of the render. What has + * actually come out of the mixer settles that better than any envelope + * threshold can, so a run of silent frames ends the tail. */ int _WM_SF2_ActiveVoices(void *synth) { - return tsf_active_voice_count((tsf *)synth); + struct wm_sf2_synth *s = (struct wm_sf2_synth *)synth; + if (s->silent_frames >= SF2_SILENCE_FRAMES) return 0; + return tsf_active_voice_count(s->f); } void _WM_SF2_Event(void *synth, struct _mdi *mdi, struct _event *event) { - tsf *f = (tsf *)synth; - uint8_t ch = event->event_data.channel; + struct wm_sf2_synth *s = (struct wm_sf2_synth *)synth; + tsf *f = s->f; + uint8_t ch = event->event_data.channel & 0x0F; /* held_off[] is indexed by it */ uint32_t val = event->event_data.data.value; switch (event->evtype) { case ev_note_on: if ((val & 0xFF) == 0) { /* velocity 0 == note off */ - tsf_channel_note_off(f, ch, (val >> 8) & 0x7F); + WM_SF2_NoteOff(s, ch, (val >> 8) & 0x7F); } else { - tsf_channel_note_on(f, ch, (val >> 8) & 0x7F, (float)(val & 0x7F) / 127.0f); + uint8_t key = (val >> 8) & 0x7F; + if (s->held_off[ch][key]) { /* retrigger: let the oldest voice go + first, since waiting for the new + note's attack would hold it on */ + s->held_off[ch][key]--; + s->held_count--; + tsf_channel_note_off(f, ch, key); + } + tsf_channel_note_on(f, ch, key, (float)(val & 0x7F) / 127.0f); } break; case ev_note_off: - tsf_channel_note_off(f, ch, (val >> 8) & 0x7F); + WM_SF2_NoteOff(s, ch, (val >> 8) & 0x7F); break; case ev_patch: tsf_channel_set_presetnumber(f, ch, val & 0x7F, mdi->channel[ch].isdrum); @@ -179,7 +327,8 @@ void _WM_SF2_Event(void *synth, struct _mdi *mdi, struct _event *event) { tsf_channel_midi_control(f, ch, 6, val & 0x7F); break; case ev_control_channel_volume: - tsf_channel_midi_control(f, ch, 7, val & 0x7F); + /* do_event() has not run yet, so pass the new value explicitly */ + WM_SF2_ChannelVolume(f, mdi, ch, val & 0x7F, mdi->channel[ch].expression); break; case ev_control_channel_balance: tsf_channel_midi_control(f, ch, 8, val & 0x7F); @@ -188,7 +337,7 @@ void _WM_SF2_Event(void *synth, struct _mdi *mdi, struct _event *event) { tsf_channel_midi_control(f, ch, 10, val & 0x7F); break; case ev_control_channel_expression: - tsf_channel_midi_control(f, ch, 11, val & 0x7F); + WM_SF2_ChannelVolume(f, mdi, ch, mdi->channel[ch].volume, val & 0x7F); break; case ev_control_data_entry_fine: tsf_channel_midi_control(f, ch, 38, val & 0x7F); @@ -213,6 +362,9 @@ void _WM_SF2_Event(void *synth, struct _mdi *mdi, struct _event *event) { break; case ev_control_channel_controllers_off: tsf_channel_midi_control(f, ch, 121, val & 0x7F); + /* CC121 puts tsf's own volume back to unity; restore ours. Like + _WM_do_control_channel_controllers_off(), CC7 survives, CC11 does not. */ + WM_SF2_ChannelVolume(f, mdi, ch, mdi->channel[ch].volume, 127); break; case ev_control_channel_notes_off: tsf_channel_midi_control(f, ch, 123, val & 0x7F); @@ -226,24 +378,45 @@ void _WM_SF2_Event(void *synth, struct _mdi *mdi, struct _event *event) { case ev_sysex_gm_reset: case ev_sysex_roland_reset: case ev_sysex_yamaha_reset: - _WM_SF2_Reset(f); + _WM_SF2_Reset(mdi); break; default: /* meta/timing events don't reach the synth */ break; } } +/* Headroom. A soundfont renders a single note at full velocity close to full + * scale, so a busy score summed at unity gain clips hard. VOL_DIVISOR in + * internal_midi.c uses 4.0 for the GUS mixer; soundfont material has a higher + * crest factor than that covers. Rendering GeneralUser GS's own nine demo + * scores, the loudest (Jump!) needs 4.78 to stay inside 16 bits and the rest + * need 1.43 to 3.67, so 5.0 clears the set. It is also fluidsynth's default + * gain of 0.2, and it puts SUNNYDAY.XMI within 0.1dB of the eawpats render in + * RMS - the GUS and SF2 paths should not change loudness under the listener. + * Denser material than those demos will reach the clamp; turn it down with + * WildMidi_MasterVolume(). */ +#define SF2_VOL_DIVISOR 5.0f + void _WM_SF2_Render(void *synth, int32_t *out, uint32_t frames) { - tsf *f = (tsf *)synth; - short buf[256 * 2]; + struct wm_sf2_synth *s = (struct wm_sf2_synth *)synth; + tsf *f = s->f; + float buf[256 * 2]; + /* Render float, not short: tsf_render_short() clamps to int16 itself, so + scaling its output afterwards would only make the clipping quieter. */ + const float gain = (32767.0f * (float)_WM_MasterVolume / 1024.0f) / SF2_VOL_DIVISOR; uint32_t n, i; while (frames) { + int32_t heard = 0; n = (frames > 256) ? 256 : frames; - tsf_render_short(f, buf, (int)n, 0); + if (s->held_count) WM_SF2_FlushHeldOffs(s); + tsf_render_float(f, buf, (int)n, 0); for (i = 0; i < n * 2; i++) { - out[i] += buf[i]; + int32_t v = (int32_t)(buf[i] * gain); + if (v > SF2_SILENCE_LEVEL || v < -SF2_SILENCE_LEVEL) heard = 1; + out[i] += v; } + s->silent_frames = heard ? 0 : (s->silent_frames + n); out += n * 2; frames -= n; } diff --git a/src/wildmidi_lib.c b/src/wildmidi_lib.c index 8c9c1958..91df9011 100644 --- a/src/wildmidi_lib.c +++ b/src/wildmidi_lib.c @@ -2090,7 +2090,7 @@ WM_SYMBOL int WildMidi_FastSeek(midi * handle, unsigned long int *sample_pos) { #ifdef WILDMIDI_SF2 /* Rewind TSF too so replayed events rebuild its state from scratch. */ if (mdi->sf2_synth) { - _WM_SF2_Reset(mdi->sf2_synth); + _WM_SF2_Reset(mdi); } #endif #ifdef WILDMIDI_MAFM @@ -2217,7 +2217,7 @@ WM_SYMBOL int WildMidi_SongSeek (midi * handle, int8_t nextsong) { event = mdi->events; _WM_ResetToStart((struct _mdi *) handle); #ifdef WILDMIDI_SF2 - if (mdi->sf2_synth) _WM_SF2_Reset(mdi->sf2_synth); + if (mdi->sf2_synth) _WM_SF2_Reset(mdi); #endif #ifdef WILDMIDI_MAFM if (mdi->mafm_synth) _WM_MAFM_Reset(mdi->mafm_synth); @@ -2254,7 +2254,7 @@ WM_SYMBOL int WildMidi_SongSeek (midi * handle, int8_t nextsong) { event = mdi->events; _WM_ResetToStart((struct _mdi *) handle); #ifdef WILDMIDI_SF2 - if (mdi->sf2_synth) _WM_SF2_Reset(mdi->sf2_synth); + if (mdi->sf2_synth) _WM_SF2_Reset(mdi); #endif #ifdef WILDMIDI_MAFM if (mdi->mafm_synth) _WM_MAFM_Reset(mdi->mafm_synth); @@ -2341,7 +2341,7 @@ static int WM_GetOutput_SF2(midi * handle, int8_t *buffer, uint32_t size) { event->do_event(mdi, &event->event_data); if ((mdi->extra_info.mixer_options & WM_MO_LOOP) && (event[0].evtype == ev_meta_endoftrack) && !end_encountered) { end_encountered = 1; /* Avoid an infinite loop. */ - _WM_SF2_Reset(mdi->sf2_synth); + _WM_SF2_Reset(mdi); _WM_ResetToStart(mdi); event = mdi->current_event; } else { @@ -2646,6 +2646,12 @@ WM_SYMBOL int WildMidi_SetOption(midi * handle, uint16_t options, uint16_t setti if (options & WM_MO_LOG_VOLUME) { _WM_AdjustChannelVolumes(mdi, 16); /* Settings greater than 15 adjusts all channels */ +#ifdef WILDMIDI_SF2 + _WM_SF2_AdjustChannelVolumes(mdi); +#endif +#ifdef WILDMIDI_MAFM + _WM_MAFM_AdjustChannelVolumes(mdi); +#endif } else if (options & WM_MO_REVERB) { _WM_reset_reverb(mdi->reverb); } diff --git a/test/CMakeLists.txt b/test/CMakeLists.txt index 53f06ac5..9aee4b66 100644 --- a/test/CMakeLists.txt +++ b/test/CMakeLists.txt @@ -2,6 +2,23 @@ ADD_EXECUTABLE(test_tokenize test_tokenize.c) TARGET_LINK_LIBRARIES(test_tokenize libwildmidi-static ${M_LIBRARY}) ADD_TEST(NAME tokenize COMMAND test_tokenize) +ADD_EXECUTABLE(test_patch_bank test_patch_bank.c) +TARGET_INCLUDE_DIRECTORIES(test_patch_bank PRIVATE ${CMAKE_SOURCE_DIR}/include) +TARGET_LINK_LIBRARIES(test_patch_bank libwildmidi-static ${M_LIBRARY}) +ADD_TEST(NAME patch_bank COMMAND test_patch_bank) + +IF (WANT_SF2) + ADD_EXECUTABLE(test_sf2_noteoff test_sf2_noteoff.c) + TARGET_INCLUDE_DIRECTORIES(test_sf2_noteoff PRIVATE ${CMAKE_SOURCE_DIR}/include) + TARGET_LINK_LIBRARIES(test_sf2_noteoff libwildmidi-static ${M_LIBRARY}) + ADD_TEST(NAME sf2_noteoff COMMAND test_sf2_noteoff) +ENDIF (WANT_SF2) + +ADD_EXECUTABLE(test_xmi_notelen test_xmi_notelen.c) +TARGET_INCLUDE_DIRECTORIES(test_xmi_notelen PRIVATE ${CMAKE_SOURCE_DIR}/include) +TARGET_LINK_LIBRARIES(test_xmi_notelen libwildmidi-static ${M_LIBRARY}) +ADD_TEST(NAME xmi_notelen COMMAND test_xmi_notelen) + ADD_EXECUTABLE(test_smaf_sequ test_smaf_sequ.c) TARGET_LINK_LIBRARIES(test_smaf_sequ libwildmidi-static ${M_LIBRARY}) ADD_TEST(NAME smaf_sequ COMMAND test_smaf_sequ) diff --git a/test/test_ma7_voice.c b/test/test_ma7_voice.c index fe171a6b..3519c636 100644 --- a/test/test_ma7_voice.c +++ b/test/test_ma7_voice.c @@ -5,6 +5,7 @@ * the 7-byte packed VM35 one the older chips use; the decoder undoes the * shuffle Yamaha's middleware applies. See docs/formats/SmafFileFormat.txt. * The bytes below are the pc=0x0a voice out of AB00221GM7.MMF. */ +#undef NDEBUG /* the asserts are the test; keep them in a Release build */ #include #include #include diff --git a/test/test_patch_bank.c b/test/test_patch_bank.c new file mode 100644 index 00000000..d653e784 --- /dev/null +++ b/test/test_patch_bank.c @@ -0,0 +1,52 @@ +/* assert-based test for _WM_get_patch_data()'s bank resolution order. + * + * Regression guard for issue #295: a non-zero bank in a timidity.cfg is a + * sparse overlay on bank 0 (eawpats' "bank 8" defines one program, "drumset 8" + * one note), so a program the overlay does not define has to come from bank 0 + * and not from the nearest-patch search inside the overlay. */ +#undef NDEBUG /* the asserts are the test; keep them in a Release build */ +#include +#include +#include + +#include "patches.h" + +struct _mdi; +extern struct _patch *_WM_get_patch_data(struct _mdi *mdi, uint16_t patchid); + +static struct _patch b0_p0, b0_p24, b0_d38, b8_p80, b8_d54; + +static void add(struct _patch *p, uint16_t patchid) { + memset(p, 0, sizeof(*p)); + p->patchid = patchid; + p->next = _WM_patch[patchid & 0x7F]; + _WM_patch[patchid & 0x7F] = p; +} + +int main(void) { + add(&b0_p0, 0x0000); /* bank 0, program 0 */ + add(&b0_p24, 0x0018); /* bank 0, program 24 */ + add(&b0_d38, 0x00a6); /* drumset 0, note 38 (0x26 | 0x80) */ + add(&b8_p80, 0x0850); /* bank 8, program 80 - the whole overlay */ + add(&b8_d54, 0x08b6); /* drumset 8, note 54 - the whole overlay */ + + /* exact hits win */ + assert(_WM_get_patch_data(NULL, 0x0018) == &b0_p24); + assert(_WM_get_patch_data(NULL, 0x0850) == &b8_p80); + assert(_WM_get_patch_data(NULL, 0x08b6) == &b8_d54); + + /* a program the overlay lacks comes from bank 0, not from the overlay */ + assert(_WM_get_patch_data(NULL, 0x0818) == &b0_p24); + assert(_WM_get_patch_data(NULL, 0x08a6) == &b0_d38); + + /* a bank nothing defines still falls back to bank 0 (SMAF selects + Yamaha's own 0x7c banks, which no GUS patch set has) */ + assert(_WM_get_patch_data(NULL, 0x7c00) == &b0_p0); + + /* only when bank 0 has no such program either does the nearest one + stand in, rather than playing silence */ + assert(_WM_get_patch_data(NULL, 0x0017) == &b0_p24); + assert(_WM_get_patch_data(NULL, 0x0819) == &b0_p24); + + return 0; +} diff --git a/test/test_sf2_noteoff.c b/test/test_sf2_noteoff.c new file mode 100644 index 00000000..93b5b953 --- /dev/null +++ b/test/test_sf2_noteoff.c @@ -0,0 +1,328 @@ +/* assert-based test for the SF2 path's note-off deferral. + * + * Regression guard for issue #295: tsf releases a voice from wherever its + * amplitude envelope has reached, so a note switched off while still in its + * attack is silent. XMI scores carry zero-length notes (the descending + * triplet at 38s in SUNNYDAY.XMI is three of them), which the GUS mixer keeps + * audible by holding the release back until the first envelope stage has run. + * + * The soundfont below is built here rather than shipped: one preset, one + * instrument, one looping square-wave sample, and a one second attack. With + * an attack that long a note released on the same sample can only be heard if + * the release was deferred, so silence is an unambiguous failure. */ +#undef NDEBUG /* the asserts are the test; keep them in a Release build */ +#include +#include +#include +#include + +#include "common.h" +#include "wildmidi_lib.h" +#include "internal_midi.h" +#include "sf2.h" + +#define SAMPLE_FRAMES 128 +#define RENDER_RATE 32072 + +/* SF2 generator operators, from the spec's list */ +#define GEN_ATTACKVOLENV 34 +#define GEN_RELEASEVOLENV 38 +#define GEN_INSTRUMENT 41 +#define GEN_SAMPLEID 53 +#define GEN_SAMPLEMODES 54 + +static uint8_t *sf2; +static uint32_t sf2_len; + +static void put(const void *src, uint32_t len) { + memcpy(sf2 + sf2_len, src, len); + sf2_len += len; +} + +static void put16(uint16_t v) { + uint8_t b[2]; + b[0] = (uint8_t)(v & 0xff); b[1] = (uint8_t)(v >> 8); + put(b, 2); +} + +static void put32(uint32_t v) { + uint8_t b[4]; + b[0] = (uint8_t)(v & 0xff); b[1] = (uint8_t)((v >> 8) & 0xff); + b[2] = (uint8_t)((v >> 16) & 0xff); b[3] = (uint8_t)(v >> 24); + put(b, 4); +} + +/* a 20 byte NUL padded name field */ +static void put_name(const char *s) { + char name[20]; + memset(name, 0, sizeof(name)); + strncpy(name, s, sizeof(name) - 1); + put(name, sizeof(name)); +} + +/* chunk headers are back-patched once the body length is known */ +static uint32_t begin_chunk(const char *id) { + put(id, 4); + put32(0); + return sf2_len; /* body start */ +} + +static void end_chunk(uint32_t body_start) { + uint32_t len = sf2_len - body_start; + uint32_t at = body_start - 4; + sf2[at] = (uint8_t)(len & 0xff); + sf2[at + 1] = (uint8_t)((len >> 8) & 0xff); + sf2[at + 2] = (uint8_t)((len >> 16) & 0xff); + sf2[at + 3] = (uint8_t)(len >> 24); +} + +static uint32_t begin_list(const char *type) { + uint32_t body = begin_chunk("LIST"); + put(type, 4); + return body; +} + +/* attack_tc/release_tc are timecents: seconds == 2^(tc/1200) */ +static void build_sf2(int16_t attack_tc, int16_t release_tc) { + uint32_t riff, list, chunk; + int i; + + sf2 = (uint8_t *) malloc(4096 + SAMPLE_FRAMES * 2); + assert(sf2 != NULL); + sf2_len = 0; + + riff = begin_chunk("RIFF"); + put("sfbk", 4); + + list = begin_list("INFO"); + chunk = begin_chunk("ifil"); put16(2); put16(1); end_chunk(chunk); + chunk = begin_chunk("isng"); put("EMU8000\0", 8); end_chunk(chunk); + chunk = begin_chunk("INAM"); put("wmtest\0\0", 8); end_chunk(chunk); + end_chunk(list); + + list = begin_list("sdta"); + chunk = begin_chunk("smpl"); + for (i = 0; i < SAMPLE_FRAMES; i++) { + put16((uint16_t)(int16_t)(i < SAMPLE_FRAMES / 2 ? 16000 : -16000)); + } + for (i = 0; i < 46; i++) put16(0); /* spec's trailing zero padding */ + end_chunk(chunk); + end_chunk(list); + + list = begin_list("pdta"); + + chunk = begin_chunk("phdr"); + put_name("wmtest"); put16(0); put16(0); put16(0); put32(0); put32(0); put32(0); + put_name("EOP"); put16(0); put16(0); put16(1); put32(0); put32(0); put32(0); + end_chunk(chunk); + + chunk = begin_chunk("pbag"); + put16(0); put16(0); + put16(1); put16(0); /* terminal */ + end_chunk(chunk); + + chunk = begin_chunk("pmod"); + put16(0); put16(0); put16(0); put16(0); put16(0); /* terminal */ + end_chunk(chunk); + + chunk = begin_chunk("pgen"); + put16(GEN_INSTRUMENT); put16(0); + put16(0); put16(0); /* terminal */ + end_chunk(chunk); + + chunk = begin_chunk("inst"); + put_name("wmtest"); put16(0); + put_name("EOI"); put16(1); + end_chunk(chunk); + + chunk = begin_chunk("ibag"); + put16(0); put16(0); + put16(4); put16(0); /* terminal, after the four generators below */ + end_chunk(chunk); + + chunk = begin_chunk("imod"); + put16(0); put16(0); put16(0); put16(0); put16(0); /* terminal */ + end_chunk(chunk); + + chunk = begin_chunk("igen"); + put16(GEN_ATTACKVOLENV); put16((uint16_t)attack_tc); + put16(GEN_RELEASEVOLENV); put16((uint16_t)release_tc); + put16(GEN_SAMPLEMODES); put16(1); /* loop continuously */ + put16(GEN_SAMPLEID); put16(0); /* must come last in the zone */ + put16(0); put16(0); /* terminal */ + end_chunk(chunk); + + chunk = begin_chunk("shdr"); + put_name("wmtest"); + put32(0); put32(SAMPLE_FRAMES); put32(0); put32(SAMPLE_FRAMES); + put32(44100); + put16(60); /* originalPitch 60, pitchCorrection 0 */ + put16(0); put16(1); /* sampleLink, sampleType == monoSample */ + put_name("EOS"); + put32(0); put32(0); put32(0); put32(0); put32(0); put16(0); put16(0); put16(0); + end_chunk(chunk); + + end_chunk(list); + end_chunk(riff); +} + +static struct _mdi mdi; + +static void send(void *synth, uint16_t evtype, uint8_t ch, uint32_t value) { + struct _event event; + memset(&event, 0, sizeof(event)); + event.evtype = evtype; + event.event_data.channel = ch; + event.event_data.data.value = value; + _WM_SF2_Event(synth, &mdi, &event); +} + +/* peak of frames rendered after a note on/off pair separated by `gap` frames */ +static int32_t render_note(uint32_t gap, uint32_t frames) { + uint32_t held = (gap > frames) ? gap : frames; + int32_t *buf = (int32_t *) calloc(held * 2, sizeof(int32_t)); + void *synth = _WM_SF2_NewSynth(RENDER_RATE); + int32_t peak = 0; + uint32_t i; + + assert(buf != NULL); + assert(synth != NULL); + + send(synth, ev_note_on, 0, (60 << 8) | 100); + if (gap) _WM_SF2_Render(synth, buf, gap); + send(synth, ev_note_off, 0, (60 << 8)); + memset(buf, 0, held * 2 * sizeof(int32_t)); /* peak of the tail only */ + _WM_SF2_Render(synth, buf, frames); + + for (i = 0; i < frames * 2; i++) { + int32_t v = buf[i] < 0 ? -buf[i] : buf[i]; + if (v > peak) peak = v; + } + _WM_SF2_FreeSynth(synth); + free(buf); + return peak; +} + +/* Two overlapping voices on one key, both switched off during the attack. + * Every deferred off has to be re-issued, or the surplus voice keeps sounding + * to the end of the render. Returns the peak once both should be long gone. */ +static int32_t render_overlapping_pair(void) { + uint32_t frames = RENDER_RATE * 3; /* attack 1s + release 0.5s, twice over */ + int32_t *buf = (int32_t *) calloc(frames * 2, sizeof(int32_t)); + void *synth = _WM_SF2_NewSynth(RENDER_RATE); + int32_t peak = 0; + uint32_t i; + + assert(buf != NULL); + assert(synth != NULL); + + send(synth, ev_note_on, 0, (60 << 8) | 100); + send(synth, ev_note_on, 0, (60 << 8) | 100); /* second voice, same key */ + send(synth, ev_note_off, 0, (60 << 8)); + send(synth, ev_note_off, 0, (60 << 8)); + _WM_SF2_Render(synth, buf, frames); + + /* only the last tenth of a second matters: by then both releases are over */ + for (i = (frames - RENDER_RATE / 10) * 2; i < frames * 2; i++) { + int32_t v = buf[i] < 0 ? -buf[i] : buf[i]; + if (v > peak) peak = v; + } + _WM_SF2_FreeSynth(synth); + free(buf); + return peak; +} + +/* peak of a lone voice that has finished its attack and is still held down */ +static int32_t render_held(void) { + uint32_t frames = RENDER_RATE / 10; + int32_t *buf = (int32_t *) calloc(RENDER_RATE * 2, sizeof(int32_t)); + void *synth = _WM_SF2_NewSynth(RENDER_RATE); + int32_t peak = 0; + uint32_t i; + + assert(buf != NULL); + assert(synth != NULL); + + send(synth, ev_note_on, 0, (60 << 8) | 100); + _WM_SF2_Render(synth, buf, RENDER_RATE); /* the whole one second attack */ + memset(buf, 0, RENDER_RATE * 2 * sizeof(int32_t)); + _WM_SF2_Render(synth, buf, frames); + + for (i = 0; i < frames * 2; i++) { + int32_t v = buf[i] < 0 ? -buf[i] : buf[i]; + if (v > peak) peak = v; + } + _WM_SF2_FreeSynth(synth); + free(buf); + return peak; +} + +/* Two voices on one key, staggered: the first has finished its attack and the + * second has only just started when a single note off arrives. tsf releases + * the older voice, so nothing should be deferred - deferring would pin the + * older voice at full level for the rest of the newer one's attack. Returns + * the peak 0.4s later, by when a released first voice is 64dB down and the + * second is four tenths of the way up. */ +static int32_t render_staggered_pair(void) { + uint32_t frames = (RENDER_RATE * 2) / 5; + int32_t *buf = (int32_t *) calloc(RENDER_RATE * 2, sizeof(int32_t)); + void *synth = _WM_SF2_NewSynth(RENDER_RATE); + int32_t peak = 0; + uint32_t i; + + assert(buf != NULL); + assert(synth != NULL); + + send(synth, ev_note_on, 0, (60 << 8) | 100); + _WM_SF2_Render(synth, buf, RENDER_RATE); /* first voice reaches full level */ + send(synth, ev_note_on, 0, (60 << 8) | 100); /* second voice, now in attack */ + send(synth, ev_note_off, 0, (60 << 8)); + memset(buf, 0, RENDER_RATE * 2 * sizeof(int32_t)); + _WM_SF2_Render(synth, buf, frames); + + /* the last tenth of a second, once the older voice has had time to go */ + for (i = (frames - RENDER_RATE / 10) * 2; i < frames * 2; i++) { + int32_t v = buf[i] < 0 ? -buf[i] : buf[i]; + if (v > peak) peak = v; + } + _WM_SF2_FreeSynth(synth); + free(buf); + return peak; +} + +int main(void) { + int32_t deferred, sustained; + + memset(&mdi, 0, sizeof(mdi)); + _WM_MasterVolume = 948; /* WildMidi_Init()'s default */ + + /* one second attack (2^0 s), half second release (2^(-1200/1200) s) */ + build_sf2(0, -1200); + assert(_WM_SF2_Load(sf2, sf2_len) == 0); + assert(_WM_SF2_Active()); + + /* Note off on the very sample the note started: only audible because the + * release waits for the attack. Half a second in, the envelope is still + * climbing, so this is well clear of any release tail. */ + deferred = render_note(0, RENDER_RATE / 2); + assert(deferred > 0); + + /* and it is the same note, not some artefact: holding the key for the + * same span gives the same envelope, so the two peaks must agree */ + sustained = render_note(RENDER_RATE / 2, 1); + assert(deferred >= sustained - (sustained / 8)); + assert(deferred <= sustained + (sustained / 8)); + + /* neither voice of a retriggered key may outlive its own note off */ + assert(render_overlapping_pair() == 0); + + /* A note off aimed at an older voice must not wait on a newer one's + * attack. Released, the pair peaks at the second voice's 0.4 alone; held, + * it peaks at 1.4 of a voice, so three quarters separates the two. */ + assert(render_staggered_pair() < (render_held() * 3) / 4); + + _WM_SF2_Unload(); + assert(!_WM_SF2_Active()); + free(sf2); + return 0; +} diff --git a/test/test_smaf_7f23.c b/test/test_smaf_7f23.c index 9fc4b4e6..413aad6a 100644 --- a/test/test_smaf_7f23.c +++ b/test/test_smaf_7f23.c @@ -18,6 +18,7 @@ * loads, and the first assert fails. * * See docs/formats/SmafFileFormat.txt. */ +#undef NDEBUG /* the asserts are the test; keep them in a Release build */ #include #include #include diff --git a/test/test_smaf_mtsp.c b/test/test_smaf_mtsp.c index f9632f6e..ca3cad80 100644 --- a/test/test_smaf_mtsp.c +++ b/test/test_smaf_mtsp.c @@ -9,6 +9,7 @@ * clause stays gated on wave_count rather than on the file parsing. * * See docs/formats/SmafFileFormat.txt. */ +#undef NDEBUG /* the asserts are the test; keep them in a Release build */ #include #include #include diff --git a/test/test_smaf_sequ.c b/test/test_smaf_sequ.c index 4b7e7247..ede228c2 100644 --- a/test/test_smaf_sequ.c +++ b/test/test_smaf_sequ.c @@ -3,6 +3,7 @@ * address 32 channels: bit 7 is the channel bank, bits 6-4 are the event type * (MIDI status nibble 0x8+n), bits 3-0 are the low channel nibble. * See docs/formats/SmafFileFormat.txt. */ +#undef NDEBUG /* the asserts are the test; keep them in a Release build */ #include #include #include diff --git a/test/test_tokenize.c b/test/test_tokenize.c index 99ea4f26..50a55531 100644 --- a/test/test_tokenize.c +++ b/test/test_tokenize.c @@ -1,4 +1,5 @@ /* assert-based smoke test for WM_LC_Tokenize_Line's config-line path handling */ +#undef NDEBUG /* the asserts are the test; keep them in a Release build */ #include #include #include diff --git a/test/test_xmi_notelen.c b/test/test_xmi_notelen.c new file mode 100644 index 00000000..b99d3b87 --- /dev/null +++ b/test/test_xmi_notelen.c @@ -0,0 +1,79 @@ +/* assert-based test for zero-length XMI notes. + * + * Regression guard for issue #295: an XMI note on carries its duration in + * ticks, and _WM_ParseNewXmi() counts that duration down to decide when to + * emit the note off. A duration of 0 is indistinguishable from "this key is + * not sounding" in that countdown, so the note off used to be dropped and the + * note hung until the next note on the same key released it - the descending + * triplet at 38s in TES: Arena's SUNNYDAY.XMI rings for 1.6s that way. + * + * The file below is the smallest XMI that carries one: a single note on with + * a zero duration, a delta, and an end of track. */ +#undef NDEBUG /* the asserts are the test; keep them in a Release build */ +#include +#include +#include + +#include "common.h" +#include "wildmidi_lib.h" +#include "internal_midi.h" +#include "f_xmidi.h" + +#define NOTE 60 +#define VEL 100 + +static const uint8_t xmi[] = { + /* XDIR form: 14 bytes after the length field */ + 'F','O','R','M', 0,0,0,14, + 'X','D','I','R', + 'I','N','F','O', 0,0,0,2, + 1,0, /* one XMID form follows */ + + 'C','A','T',' ', 0,0,0,28, + 'X','M','I','D', + + 'F','O','R','M', 0,0,0,20, + 'X','M','I','D', + + 'E','V','N','T', 0,0,0,8, + 0x90, NOTE, VEL, 0x00, /* note on, duration 0 ticks */ + 0x0a, /* ten ticks pass */ + 0xff, 0x2f, 0x00 /* end of track */ +}; + +int main(void) { + struct _mdi *mdi; + struct _event *ev; + uint32_t on_at = 0, off_at = 0; + int seen_on = 0, seen_off = 0; + uint32_t samples = 0; + + _WM_SampleRate = 32072; + + mdi = _WM_ParseNewXmi(xmi, (uint32_t)sizeof(xmi)); + assert(mdi != NULL); + + for (ev = mdi->events; ev->do_event != NULL; ev++) { + if (ev->evtype == ev_note_on + && ev->event_data.data.value == ((NOTE << 8) | VEL)) { + assert(!seen_on); /* one note on, so any second is a parser bug */ + seen_on = 1; + on_at = samples; + } else if (ev->evtype == ev_note_off + && ((ev->event_data.data.value >> 8) & 0x7f) == NOTE) { + assert(seen_on); + seen_off = 1; + off_at = samples; + } + samples += ev->samples_to_next; + } + + /* the note off has to exist at all... */ + assert(seen_off); + /* ...and land on the same sample as the note on, since that is the + duration the file asked for */ + assert(off_at == on_at); + + _WM_freeMDI(mdi); + return 0; +}