Skip to content
Merged
Show file tree
Hide file tree
Changes from 2 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
6 changes: 3 additions & 3 deletions .github/workflows/bsd.yml
Original file line number Diff line number Diff line change
Expand Up @@ -24,17 +24,17 @@ jobs:
- { name: FreeBSD, os: freebsd, os-version: 14.4, os-arch: x86-64,
cmake_opts: '-DWANT_OSS=ON',
setup_cmd: 'sudo pkg update',
install_cmd: 'sudo pkg install -y cmake',
install_cmd: 'sudo pkg install -y cmake git',
}
- { name: OpenBSD, os: openbsd, os-version: 7.9, os-arch: x86-64,
cmake_opts: '-DWANT_SNDIO=ON',
setup_cmd: 'sudo pkg_add -u',
install_cmd: 'sudo pkg_add cmake',
install_cmd: 'sudo pkg_add cmake git',
}
- { name: NetBSD, os: netbsd, os-version: 10.1, os-arch: x86-64,
cmake_opts: '-DWANT_NETBSD=ON',
setup_cmd: 'export PATH="/usr/pkg/sbin:/usr/pkg/bin:/sbin:$PATH";export PKG_PATH="https://cdn.netBSD.org/pub/pkgsrc/packages/NetBSD/$(uname -p)/$(uname -r|cut -f "1 2" -d.)/All/";echo "PKG_PATH=$PKG_PATH";echo "uname -a -> \"$(uname -a)\"";sudo -E sysctl -w security.pax.aslr.enabled=0;sudo -E sysctl -w security.pax.aslr.global=0;sudo -E pkgin clean;sudo -E pkgin update',
install_cmd: 'sudo -E pkgin -y install cmake',
install_cmd: 'sudo -E pkgin -y install cmake git',
}
steps:
- uses: actions/checkout@v4
Expand Down
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