Skip to content
Merged
Show file tree
Hide file tree
Changes from 1 commit
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
17 changes: 17 additions & 0 deletions CMakeLists.txt
Original file line number Diff line number Diff line change
Expand Up @@ -66,6 +66,12 @@ OPTION(WANT_OPENAL "Include OpenAL (Cross Platform) support" OFF)

OPTION(WANT_DEVTEST "Build WildMIDI DevTest file to check files" OFF)

OPTION(WANT_SF2 "SoundFont2 (SF2) support via TinySoundFont" ON)
IF (WANT_SF2 AND CMAKE_VERSION VERSION_LESS 3.14)
MESSAGE(WARNING "WANT_SF2 requires CMake 3.14+ (FetchContent); disabling SF2 support")
SET(WANT_SF2 OFF)
ENDIF()

CMAKE_DEPENDENT_OPTION(WANT_MP_BUILD "Build with Multiple Processes (/MP)" OFF "WIN32;MSVC" OFF)
CMAKE_DEPENDENT_OPTION(WANT_OSX_DEPLOYMENT "OSX Deployment" OFF "APPLE" OFF)

Expand Down Expand Up @@ -299,6 +305,17 @@ IF(WANT_PLAYER OR WANT_PLAYERSTATIC)
MESSAGE(STATUS "Enabled audio output backends: ${ENABLED_OUTPUT}")
ENDIF()

IF (WANT_SF2)
INCLUDE(FetchContent)
FetchContent_Declare(tinysoundfont
GIT_REPOSITORY https://github.com/schellingb/TinySoundFont.git
GIT_TAG fbc913531b85f5707f49115110bb86b1cd583885 # 2025-07-19
)
FetchContent_MakeAvailable(tinysoundfont)
INCLUDE_DIRECTORIES(SYSTEM "${tinysoundfont_SOURCE_DIR}")
SET(WILDMIDI_SF2 1)
Comment thread
coderabbitai[bot] marked this conversation as resolved.
ENDIF()

# Setup up our config file
CONFIGURE_FILE("${PROJECT_SOURCE_DIR}/include/config.h.cmake" "${PROJECT_BINARY_DIR}/include/config.h")

Expand Down
69 changes: 69 additions & 0 deletions docs/SF2.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,69 @@
# SoundFont2 (SF2) Support

Implementation plan and design notes for [issue #8](https://github.com/Mindwerks/wildmidi/issues/8).

## Approach

WildMIDI gains SF2 rendering via [TinySoundFont](https://github.com/schellingb/TinySoundFont)
(`tsf.h`, MIT licensed, single-header, pure C), fetched at configure time with CMake
`FetchContent` and pinned to a known-good commit.

The key insight: WildMIDI's format parsers (MID, XMI, MUS, HMP, HMI) already convert
everything into a single time-stamped event list (`struct _event`), and the renderer is
just the stage that consumes that list. SF2 support swaps the *rendering* stage while
keeping every parser, the timing engine, and the public API untouched. No other MIDI
library renders XMI/MUS/HMP/HMI through SoundFonts — this is WildMIDI's niche.

TinySoundFont's common criticism — "you must do your own MIDI parsing" — is exactly
what makes it the right fit here: its `tsf_channel_*` API maps 1:1 onto WildMIDI's
already-decoded event types.

## How it works

1. **Loading** — `WildMidi_Init()` / `WildMidi_InitVIO()` sniff the config file for the
RIFF/`sfbk` magic (not the file extension). An `.sf2` file given directly as the
"config file" is loaded through the existing VIO buffer callbacks into one global
`tsf*` instance. Additionally, the timidity.cfg parser understands a
`soundfont <file>` directive (relative paths resolved against the config dir, same
as `source`), so `/etc/timidity/fluidr3_gm.cfg` works as-is.
2. **Per-song synth** — each opened midi (`struct _mdi`) gets its own `tsf_copy()` of
the global instance (sample data shared, voices private), created in `_WM_initMDI()`
and freed in `_WM_freeMDI()`. Concurrent handles stay safe.
3. **Rendering** — `WM_GetOutput_SF2()` mirrors the scheduling loop of
`WM_GetOutput_Linear()`. Every event still runs its normal `do_event` (tempo, lyrics,
channel state; GUS note-ons no-op safely since no GUS patches are loaded) and is
additionally translated to the corresponding `tsf_channel_*` call. Audio is rendered
by `tsf_render_short()` into the 32-bit mix buffer, so `WM_MO_REVERB` and the rest of
the output pipeline behave exactly like the GUS path.
4. **Release tails** — when the event list ends, rendering continues while
`tsf_active_voice_count() > 0`, so SF2 release envelopes aren't clipped at
end-of-song.

## Event translation

| WildMIDI event | TSF call |
|---|---|
| note on/off | `tsf_channel_note_on` / `_off` |
| patch change | `tsf_channel_set_presetnumber(..., isdrum)` |
| pitch wheel | `tsf_channel_set_pitchwheel` |
| all controllers (bank, volume, pan, expression, hold, RPN/NRPN, sound/notes off, …) | `tsf_channel_midi_control` (TSF implements the full CC map) |
| Roland drum-track sysex | `tsf_channel_set_bank_preset(ch, 128, 0)` |

Channel 9 defaults to bank 128 (drums) at synth creation, per GM convention.

## Build

* `WANT_SF2` CMake option, default `ON`; requires CMake >= 3.14 (auto-disables with a
warning on older CMake so legacy platforms keep building).
* Offline/distro builds: point `FETCHCONTENT_SOURCE_DIR_TINYSOUNDFONT` at a local
checkout — standard CMake, no extra plumbing.
* License: `tsf.h` is MIT, compatible with linking into the LGPLv3 library.

## Non-goals (first pass)

* Mixing GUS patches and SF2 in one config — if a soundfont is loaded it takes over
rendering entirely.
* SF3 (ogg-compressed) soundfonts — one `#define TSF_SF3` + stb_vorbis away if wanted.
* timidity per-soundfont options (`amp=`, `order=`, `remove`) — parsed token is the
filename only.
* `WildMidi_MasterVolume()` does not affect the SF2 path yet (TSF has its own gain).
3 changes: 3 additions & 0 deletions include/config.h.cmake
Original file line number Diff line number Diff line change
Expand Up @@ -55,6 +55,9 @@
/* define this if building for AmigaOS variants */
#cmakedefine WILDMIDI_AMIGA 1

/* define this to enable SoundFont2 support via TinySoundFont */
#cmakedefine WILDMIDI_SF2 1

/* Define if you have the <stdint.h> header file. */
#cmakedefine HAVE_STDINT_H

Expand Down
2 changes: 2 additions & 0 deletions include/internal_midi.h
Original file line number Diff line number Diff line change
Expand Up @@ -161,6 +161,8 @@ struct _mdi {
uint8_t is_type2;

char *lyric;

void *sf2_synth; /* per-mdi TinySoundFont instance, NULL unless a soundfont is loaded */
};


Expand Down
52 changes: 52 additions & 0 deletions include/sf2.h
Original file line number Diff line number Diff line change
@@ -0,0 +1,52 @@
/*
* sf2.h -- SoundFont2 rendering via TinySoundFont
*
* 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.
*
* WildMIDI is distributed in the hope that it will be useful, but WITHOUT
* ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
* FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License and
* the GNU Lesser General Public License for more details.
*
* You should have received a copy of the GNU General Public License and the
* GNU Lesser General Public License along with WildMIDI. If not, see
* <http://www.gnu.org/licenses/>.
*/

#ifndef __SF2_H
#define __SF2_H

struct _mdi;
struct _event;

/* returns 1 if the buffer looks like a RIFF sfbk (SoundFont2) file */
extern int _WM_SF2_Magic(const uint8_t *data, uint32_t size);

/* load/free the global soundfont instance */
extern int _WM_SF2_Load(const uint8_t *data, uint32_t size);
extern void _WM_SF2_Unload(void);
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);

/* translate a wildmidi event to the synth */
extern void _WM_SF2_Event(void *synth, struct _mdi *mdi, struct _event *event);

/* returns nonzero while notes are still sounding (release tails) */
extern int _WM_SF2_ActiveVoices(void *synth);

/* render stereo frames into the 32bit mix buffer */
extern void _WM_SF2_Render(void *synth, int32_t *out, uint32_t frames);

#endif /* __SF2_H */
5 changes: 5 additions & 0 deletions src/CMakeLists.txt
Original file line number Diff line number Diff line change
Expand Up @@ -41,6 +41,11 @@ SET(wildmidi_library_HDRS
../include/wildplay.h
)

IF (WANT_SF2)
LIST(APPEND wildmidi_library_SRCS sf2.c)
LIST(APPEND wildmidi_library_HDRS ../include/sf2.h)
ENDIF()

# set our library names
IF (MSVC) # windows uses *.lib for both static and dynamic, workaround
SET(LIBRARY_DYN_NAME "libWildMidi")
Expand Down
12 changes: 12 additions & 0 deletions src/internal_midi.c
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,9 @@
#include "wildmidi_lib.h"
#include "patches.h"
#include "internal_midi.h"
#ifdef WILDMIDI_SF2
#include "sf2.h"
#endif

#define HOLD_OFF 0x02

Expand Down Expand Up @@ -1946,6 +1949,12 @@ _WM_initMDI(void) {

mdi->lyric = NULL;

#ifdef WILDMIDI_SF2
if (_WM_SF2_Active()) {
mdi->sf2_synth = _WM_SF2_NewSynth(_WM_SampleRate);
}
#endif

_WM_do_sysex_gm_reset(mdi, NULL);

return (mdi);
Expand Down Expand Up @@ -1996,6 +2005,9 @@ void _WM_freeMDI(struct _mdi *mdi) {
free(mdi->events);
_WM_free_reverb(mdi->reverb);
free(mdi->mix_buffer);
#ifdef WILDMIDI_SF2
_WM_SF2_FreeSynth(mdi->sf2_synth);
#endif
if (mdi->tmp_info) {
free(mdi->tmp_info->copyright);
free(mdi->tmp_info);
Expand Down
Loading
Loading