Skip to content
Merged
Show file tree
Hide file tree
Changes from 19 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
17 changes: 17 additions & 0 deletions CMakeLists.txt
Original file line number Diff line number Diff line change
Expand Up @@ -12,10 +12,12 @@ SET(VERSION 2.1.0)
# Find Macros
SET(CMAKE_MODULE_PATH ${PROJECT_SOURCE_DIR}/cmake)

INCLUDE(CMakePushCheckState)
INCLUDE(CMakeDependentOption)
INCLUDE(CheckCCompilerFlag)
INCLUDE(CheckCSourceCompiles)
INCLUDE(CheckIncludeFile)
INCLUDE(CheckSymbolExists)
INCLUDE(GNUInstallDirs)
INCLUDE(TestBigEndian)

Expand Down Expand Up @@ -66,6 +68,8 @@ 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)

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 @@ -189,6 +193,7 @@ CHECK_INCLUDE_FILE(inttypes.h HAVE_INTTYPES_H)
TEST_BIG_ENDIAN(WORDS_BIGENDIAN)

# UNIX-like environments
cmake_push_check_state()
IF (UNIX AND NOT APPLE)
# find our math lib
FIND_LIBRARY(M_LIBRARY m)
Expand All @@ -197,10 +202,16 @@ IF (UNIX AND NOT APPLE)
SET(M_LIBRARY "")
SET(PKG_PRIVATELIBS "")
ELSE()
LIST(APPEND CMAKE_REQUIRED_LIBRARIES m)
SET(PKG_PRIVATELIBS "-lm")
ENDIF()
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)
cmake_pop_check_state()

# ######### General setup ##########
INCLUDE_DIRECTORIES(BEFORE "${PROJECT_SOURCE_DIR}/include" "${PROJECT_BINARY_DIR}/include")
IF (NOT HAVE_STDINT_H) # AND NOT HAVE_INTTYPES_H
Expand Down Expand Up @@ -299,6 +310,12 @@ IF(WANT_PLAYER OR WANT_PLAYERSTATIC)
MESSAGE(STATUS "Enabled audio output backends: ${ENABLED_OUTPUT}")
ENDIF()

IF (WANT_SF2)
SET(TINYSOUNDFONT_DIR "${PROJECT_SOURCE_DIR}/extern/TinySoundFont")
INCLUDE_DIRECTORIES(SYSTEM "${TINYSOUNDFONT_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
67 changes: 67 additions & 0 deletions docs/SF2.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,67 @@
# 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`. No network dependency: TinySoundFont is
vendored at `extern/TinySoundFont/` (MIT licensed, compatible with LGPLv3
linking). Refresh by replacing `tsf.h` with a newer upstream revision.

## 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).
19 changes: 19 additions & 0 deletions extern/TinySoundFont/LICENSE
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
Copyright (C) 2017-2023 Bernhard Schelling (Based on SFZero, Copyright (C) 2012 Steve Folta, https://github.com/stevefolta/SFZero)

Permission is hereby granted, free of charge, to any person obtaining a copy of
this software and associated documentation files (the "Software"), to deal in
the Software without restriction, including without limitation the rights to
use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies
of the Software, and to permit persons to whom the Software is furnished to do
so, subject to the following conditions:

The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.

THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.
11 changes: 11 additions & 0 deletions extern/TinySoundFont/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
# TinySoundFont

Vendored copy of [TinySoundFont](https://github.com/schellingb/TinySoundFont)
at commit `fbc913531b85f5707f49115110bb86b1cd583885` (2025-07-19), used by
`src/sf2.c` when built with `-DWANT_SF2=ON`.

MIT licensed — see `LICENSE`.

To refresh, replace `tsf.h` and `LICENSE` with a newer upstream revision. Only
`tsf.h` is compiled; the rest of the upstream repo (examples, tools) is not
needed.
Loading
Loading