Index Types & Handles Window & Input GDI System & Files File I/O Multimedia

Multimedia — MIDI, MCI & Timers

Free API implements MIDI music playback via TinySoundFont + TinyMidiLoader over an SDL3 audio stream, the MCI command API for sequencer devices, multimedia periodic timers, and MIDI output volume control.

Include mmsystem.h is included automatically by <windows.h>. You only need #include <mmsystem.h> explicitly if you are not including <windows.h>.

MIDI playback architecture

Game calls mciSendCommand(MCI_OPEN)
    │
    ├─ TinyMidiLoader parses .mid file (Type 0 or Type 1)
    ├─ TinySoundFont loads .sf2 SoundFont for synthesis
    └─ SDL3 audio device opened (one shared device for all tracks)

Game calls mciSendCommand(MCI_PLAY)
    │
    ├─ Mixer thread starts
    │     ├─ advances MIDI time tick by tick
    │     ├─ sends Note On/Off, Program Change, Control Change to TinySoundFont
    │     └─ TinySoundFont synthesises stereo float PCM
    └─ SDL3 audio stream feeds PCM to OS audio driver

Song ends → MM_MCINOTIFY posted to notification window

Game calls mciSendCommand(MCI_CLOSE)
    └─ stops mixer thread, frees MIDI data
      

Vendored libraries

LibraryFileVersionPurpose
TinySoundFontexternal/tsf.hv0.9SoundFont 2 synthesiser — converts MIDI events to PCM audio. MIT license by Bernhard Schelling.
TinyMidiLoaderexternal/tml.hv0.7MIDI file parser (Type 0 and Type 1). MIT license by Bernhard Schelling.

SoundFont 2 (.sf2) requirement

MIDI synthesis requires a SoundFont 2 file that maps MIDI instrument numbers to audio samples. No SoundFont is bundled with Free API (copyright reasons).

Free API searches for a SoundFont in this order (first found wins):

  1. Environment variable: FREE_API_SOUNDFONT=/path/to/file.sf2
  2. Relative path: assets/soundfont/default.sf2
  3. Relative path: soundfont/default.sf2

If no SoundFont is found, MCI_OPEN still succeeds but playback is silent. A warning is printed to the SDL log:

SDL log warning (no SoundFont)
[midi] WARNING: No SoundFont found. Music will be silent.
Set FREE_API_SOUNDFONT=/path/to/file.sf2 or place default.sf2
in assets/soundfont/ or soundfont/.
Free SoundFonts suitable for testing GeneralUser GS (~30 MB, freely redistributable) — schristiancollins.com
FluidR3_GM — Debian/Ubuntu: sudo apt install fluid-soundfont-gm
Path on Debian: /usr/share/sounds/sf2/FluidR3_GM.sf2
bash — set SoundFont via environment variable
# One-shot
FREE_API_SOUNDFONT=/usr/share/sounds/sf2/FluidR3_GM.sf2 ./MyGame

# Or export for the session
export FREE_API_SOUNDFONT=/usr/share/sounds/sf2/FluidR3_GM.sf2
./MyGame
      
C++ — set SoundFont path from code
// Must be called before the first MCI_OPEN for it to take effect
SetEnvironmentVariableA("FREE_API_SOUNDFONT",
    "/usr/share/sounds/sf2/FluidR3_GM.sf2");
      

MCI types and result codes

TypeUnderlyingStatusDescription
MMRESULTUINTIMPLEMENTEDReturn type for WinMM API functions.
MCIDEVICEIDUINTPARTIALIdentifies an open MCI device (sequencer). Assigned by MCI_OPEN, freed by MCI_CLOSE.
MCIERRORDWORDPARTIALError code returned by mciSendCommandA.
HMIDIOUTHANDLESTUBOpaque MIDI output device handle. Real audio goes through MCI; this is a dummy wrapper for volume control.
LPTIMECALLBACKfunction pointerPARTIALType of callback passed to timeSetEvent. Signature: void CALLBACK f(UINT, UINT, DWORD_PTR, DWORD_PTR, DWORD_PTR).

Result code constants

ConstantValueMeaning
MMSYSERR_NOERROR0Success.
MCIERR_INVALID_DEVICE_ID259The MCI device ID is not valid.
MCIERR_UNSUPPORTED_FUNCTION268The device type or command is not supported (e.g. cdaudio).
MCIERR_INTERNAL305Internal Free API error (e.g. TinyMidiLoader parse failure).

MCI parameter structures

MCI_OPEN_PARMSA — open a device
typedef struct _MCI_OPEN_PARMSA {
    DWORD_PTR  dwCallback;      // notification window (ignored for MCI_OPEN)
    MCIDEVICEID wDeviceID;      // filled by Free API on MCI_OPEN success
    LPCSTR     lpstrDeviceType; // "sequencer" for MIDI files
    LPCSTR     lpstrElementName;// path to the MIDI file
    LPCSTR     lpstrAlias;      // optional alias (ignored)
} MCI_OPEN_PARMSA;
typedef MCI_OPEN_PARMSA MCI_OPEN_PARMS;
      
MCI_PLAY_PARMS — start playback
typedef struct _MCI_PLAY_PARMS {
    DWORD_PTR dwCallback; // HWND to receive MM_MCINOTIFY when song ends
    DWORD_PTR dwFrom;     // start position (ignored — always starts from 0)
    DWORD_PTR dwTo;       // end position (ignored)
} MCI_PLAY_PARMS;
      
MCI_SET_PARMS — set device parameters
typedef struct _MCI_SET_PARMS {
    DWORD_PTR dwCallback;   // ignored
    DWORD     dwTimeFormat; // ignored (no-op)
    DWORD     dwAudio;      // ignored
} MCI_SET_PARMS;
      

MCI_OPEN — open a MIDI file

Signature
MCIERROR WINAPI mciSendCommandA(
    MCIDEVICEID mciId,      // 0 for MCI_OPEN
    UINT        uMsg,       // MCI_OPEN
    DWORD_PTR   fdwCommand, // flags: MCI_OPEN_TYPE | MCI_OPEN_ELEMENT
    DWORD_PTR   dwParam     // pointer to MCI_OPEN_PARMS
);
// #define mciSendCommand mciSendCommandA
      

MCI_OPEN with lpstrDeviceType = "sequencer" loads the MIDI file specified by lpstrElementName into memory via TinyMidiLoader. On success, op.wDeviceID is filled with a non-zero ID that must be used in subsequent MCI_PLAY and MCI_CLOSE calls.

Specifying lpstrDeviceType = "cdaudio" returns MCIERR_UNSUPPORTED_FUNCTION gracefully — it will not crash.

FlagValueMeaning
MCI_OPEN_TYPE0x00002000lpstrDeviceType is a string (required for "sequencer").
MCI_OPEN_TYPE_ID0x00001000lpstrDeviceType is an integer ID (alternative form, not used by Blupi).
MCI_OPEN_ELEMENT0x00000200lpstrElementName is a file path.
C++ — open a MIDI file
#include <windows.h>   // includes mmsystem.h

MCIDEVICEID g_musicId = 0;

BOOL OpenMidiFile(const char* path)
{
    // Close any previously open track
    if (g_musicId) {
        mciSendCommand(g_musicId, MCI_CLOSE, 0, 0);
        g_musicId = 0;
    }

    MCI_OPEN_PARMS op = {};
    op.lpstrDeviceType  = "sequencer";
    op.lpstrElementName = path;

    MCIERROR err = mciSendCommand(0, MCI_OPEN,
        MCI_OPEN_TYPE | MCI_OPEN_ELEMENT,
        (DWORD_PTR)&op);

    if (err != MMSYSERR_NOERROR) {
        char msg[256];
        mciGetErrorString(err, msg, sizeof(msg));
        OutputDebugString(msg);
        return FALSE;
    }

    g_musicId = op.wDeviceID;
    return TRUE;
}
      

MCI_PLAY — start playback

Starts the SDL3 audio stream and launches the mixer thread. Playback runs asynchronously — mciSendCommand returns immediately. When the song ends, MM_MCINOTIFY is posted to the window specified in dwCallback.

FlagValueMeaning
MCI_NOTIFY0x00000001Post MM_MCINOTIFY to op.dwCallback (HWND) when song ends. Recommended.
MCI_WAIT0x00000002Wait for operation to complete — NOT IMPLEMENTED. Behaves like MCI_NOTIFY.
C++ — play and loop music
void PlayMusic(HWND hWndNotify)
{
    MCI_PLAY_PARMS pp = {};
    pp.dwCallback = (DWORD_PTR)hWndNotify;

    mciSendCommand(g_musicId, MCI_PLAY, MCI_NOTIFY, (DWORD_PTR)&pp);
}

// In WNDPROC — handle end-of-song notification:
case MM_MCINOTIFY:
    if (wParam == MCI_NOTIFY_SUCCESSFUL && g_musicId) {
        // Song finished — restart for looping
        MCI_PLAY_PARMS pp = { (DWORD_PTR)hWnd };
        mciSendCommand(g_musicId, MCI_PLAY, MCI_NOTIFY, (DWORD_PTR)&pp);
    }
    return 0;
      

MCI_CLOSE — stop and free

C++ — stop and close the music track
void StopMusic()
{
    if (g_musicId) {
        mciSendCommand(g_musicId, MCI_CLOSE, 0, 0);
        g_musicId = 0;
    }
}

// Always close in WM_DESTROY:
case WM_DESTROY:
    StopMusic();
    PostQuitMessage(0);
    return 0;
      

All MCI constants

ConstantValueDescription
MCI_OPEN0x0803Open a device.
MCI_CLOSE0x0804Close a device.
MCI_PLAY0x0806Start playback.
MCI_SET0x080DSet device parameters (accepted as no-op).
MCI_NOTIFY0x00000001LPost notification when done.
MCI_WAIT0x00000002LWait flag (not implemented).
MCI_OPEN_TYPE0x00002000LlpstrDeviceType is a string.
MCI_OPEN_TYPE_ID0x00001000LlpstrDeviceType is an ID.
MCI_OPEN_ELEMENT0x00000200LlpstrElementName is a file path.
MCI_SET_TIME_FORMAT0x00000400LSet time format (no-op).
MCI_FORMAT_TMSF10Track/Minute/Second/Frame format constant.
MCI_TRACK0x00000010LTrack flag for position.
MM_MCINOTIFY0x03B9Message posted by MCI to the notification window when an operation completes.
MCI_NOTIFY_SUCCESSFUL0x0001wParam value in MM_MCINOTIFY when operation succeeded.

MIDI output — midiOutOpen, midiOutSetVolume, midiOutClose

The MIDI output API is used primarily to control playback volume. Real note output is not implemented — use MCI for actual playback.

FunctionStatusDescription
midiOutGetNumDevs(void) IMPLEMENTED Returns 1 if the SDL3 audio subsystem is available, 0 otherwise. Games use this to check whether any MIDI device is present before calling midiOutOpen.
midiOutOpen(phmo, uDeviceID, dwCallback, dwInstance, fdwOpen) IMPLEMENTED Allocates a dummy handle and writes it to *phmo. Accepts any uDeviceID (typically 0 or MIDI_MAPPER). Returns MMSYSERR_NOERROR on success.
midiOutSetVolume(hmo, dwVolume) IMPLEMENTED Sets playback volume. dwVolume is a packed 32-bit value: low 16 bits = left channel, high 16 bits = right channel (range 0x0000–0xFFFF). The average of both channels is mapped to a 0.0–1.0 linear gain applied to TinySoundFont.
midiOutClose(hmo) IMPLEMENTED Releases the dummy handle. Returns MMSYSERR_NOERROR.
C++ — MIDI volume control
void SetMusicVolume(int percent)  // 0-100
{
    if (midiOutGetNumDevs() == 0) return;

    HMIDIOUT hmo;
    if (midiOutOpen(&hmo, 0, 0, 0, 0) != MMSYSERR_NOERROR) return;

    // Scale 0-100% to 0x0000-0xFFFF
    WORD vol = (WORD)((percent * 0xFFFF) / 100);
    DWORD packed = (DWORD)vol | ((DWORD)vol << 16);  // same for L and R

    midiOutSetVolume(hmo, packed);
    midiOutClose(hmo);
}

// Examples:
SetMusicVolume(100);   // full volume
SetMusicVolume(50);    // half volume
SetMusicVolume(0);     // muted
      

mciGetErrorString — human-readable error text

Signature
BOOL WINAPI mciGetErrorStringA(MCIERROR mcierr, LPSTR pszText, UINT cchText);
// #define mciGetErrorString mciGetErrorStringA
      
C++ — error handling pattern
MCIERROR err = mciSendCommand(0, MCI_OPEN,
    MCI_OPEN_TYPE | MCI_OPEN_ELEMENT,
    (DWORD_PTR)&op);

if (err != MMSYSERR_NOERROR) {
    char errorMsg[256];
    mciGetErrorString(err, errorMsg, sizeof(errorMsg));

    char fullMsg[512];
    snprintf(fullMsg, sizeof(fullMsg),
        "MCI error %lu: %s\n", (unsigned long)err, errorMsg);
    OutputDebugString(fullMsg);
    return FALSE;
}
      

timeSetEvent / timeKillEvent — high-frequency periodic timers

Multimedia timers fire a callback on a private SDL timer thread at precise intervals — independent of the message loop. This is the mechanism Speedy Blupi and Planet Blupi use for their main game tick.

Signatures
MMRESULT WINAPI timeSetEvent(
    UINT           uDelay,      // interval in milliseconds (minimum ~1 ms)
    UINT           uResolution, // timer resolution request (0 = as accurate as possible)
    LPTIMECALLBACK lpTimeProc,  // function called at each interval
    DWORD_PTR      dwUser,      // user data passed to callback
    UINT           fuEvent      // must be TIME_PERIODIC
);
// Returns timer ID (non-zero) on success, 0 on failure.

MMRESULT WINAPI timeKillEvent(UINT uTimerID);
// Returns MMSYSERR_NOERROR on success.
      

Callback signature

LPTIMECALLBACK type
void CALLBACK TimerCallback(
    UINT      uTimerID,  // same ID returned by timeSetEvent
    UINT      uMsg,      // reserved (0)
    DWORD_PTR dwUser,    // your dwUser value
    DWORD_PTR dw1,       // reserved (0)
    DWORD_PTR dw2        // reserved (0)
);
      
Callback runs on a separate thread The callback fires on an SDL timer thread, not the main game thread. Free API's internal message queue is mutex-protected so calling PostMessage from the callback is safe. Do not call other Free API functions from the callback unless you have verified they are thread-safe.
C++ — periodic game tick via multimedia timer
#include <windows.h>

UINT g_timerId = 0;

// Callback fires every 50 ms on a private SDL timer thread
void CALLBACK GameTimerCallback(UINT id, UINT msg,
    DWORD_PTR user, DWORD_PTR dw1, DWORD_PTR dw2)
{
    HWND hWnd = (HWND)user;

    // Safe to PostMessage from a timer thread
    PostMessage(hWnd, WM_USER + 1, 0, 0);
}

void StartGameLoop(HWND hWnd)
{
    g_timerId = timeSetEvent(
        50,                    // 50 ms = 20 ticks/second
        1,                     // 1 ms resolution
        GameTimerCallback,
        (DWORD_PTR)hWnd,
        TIME_PERIODIC
    );
    if (!g_timerId)
        OutputDebugString("timeSetEvent failed\n");
}

void StopGameLoop()
{
    if (g_timerId) {
        timeKillEvent(g_timerId);
        g_timerId = 0;
    }
}

// In WNDPROC — handle the tick message posted by the callback:
case WM_USER + 1:
    GameUpdate();  // runs on the main (message loop) thread
    GameRender();
    return 0;
      
ConstantValueDescription
TIME_PERIODIC0x0001Required flag — the timer fires repeatedly at the given interval.

Joystick API (stubs)

Joystick support is not implemented. The functions compile cleanly and return safe values so legacy code that probes for a joystick does not crash.

FunctionStatusReturns
joyGetNumDevs(void)STUB0 — no joystick devices.
joyGetPosEx(uJoyID, JOYINFOEX*)STUBMMSYSERR_NOERROR with zeroed JOYINFOEX struct.

MIDI debug logging

bash — enable per-call MIDI logging
FREE_API_DEBUG_MIDI=1 ./MyGame

With FREE_API_DEBUG_MIDI=1 set, Free API logs every MCI / midiOut call and all mixer-thread events (note on/off, program changes, timing) to the SDL log. Useful for diagnosing why music does not play or sounds wrong.