SMF Conversion Tool (Smf2GoFMML)
A standalone command-line tool that reads a Standard MIDI File (SMF) and converts it
into a go-fmml .mml file.
Overview
Smf2GoFMML is a standalone command-line app under tools/smf2gofmml, separate
from go-fmml's core (the FM/PCM synthesis engine and MML playback). It reads a Standard MIDI File
(assumed to be Format 1), parses each track's notes, tempo, and track name, and writes out a
.mml text file following go-fmml's own MML spec. It doesn't need go-fmml's audio engine
(oto/v3 etc.) at all - it only performs the file conversion.
Treat the output as a starting draft, not a finished song. Anything an SMF file has no way of
expressing - voice IDs, mute, reverb sends, loop settings, a conductor part, and so on - is filled
in with fixed default values, so the expected workflow is to hand-edit the resulting
.mml afterward, referring to the MML File Structure and
MML Command Reference pages, and pair it with voice files you author
separately.
Building
From the repository's tools/smf2gofmml directory, build the executable straight
into the cmd subfolder (the tool resolves its input/output folders relative to its own
executable's location, so this placement matters).
cd tools/smf2gofmml
go build -o cmd/Smf2GoFMML.exe .
Usage
Next to the built executable (cmd/Smf2GoFMML.exe) live an input smf
folder and an output convert folder (created automatically if missing).
# Run from inside the cmd folder
cd tools/smf2gofmml/cmd
# A bare file name is looked up under the sibling smf folder
Smf2GoFMML.exe bgm1.mid # resolves to ./smf/bgm1.mid
# An absolute path is read as-is
Smf2GoFMML.exe C:\path\to\bgm1.mid
| Item | Details |
|---|---|
| Argument | The path to the SMF file to convert - either a full path, or a bare file
name resolved against the smf folder next to the executable. |
| Output location | The convert folder next to the executable (created
automatically if it doesn't exist). |
| Output file name | <song name>.mml. The song name comes from the
SMF's sequence/track-name meta event (characters invalid in a Windows file name are replaced with
_; falls back to untitled if none is found). |
| On error | Prints the reason to the console and exits (e.g. the file can't be read, the SMF itself is malformed, or no track contains any note data). |
| On warning | Conversion continues; details are printed to the console prefixed with
warning: (e.g. a track skipped for exceeding a part-count limit). |
Track-name tags
Prefixing a track's name (its sequence/track-name meta event) with one of the following tags controls what kind of part it converts into.
| Tag | Converts to |
|---|---|
[FM] | An FM voice part ([partN]) |
[PCM long] | A PCM voice part, long type ([pcmPartX],
partType: long) |
[PCM oneShot] | A PCM voice part, oneShot type ([pcmPartX],
partType: oneShot) |
| No tag | Treated as an FM voice part (the default) |
[PCM oneShot ] or [ PCM oneShot ]) is ignored - a bit of inconsistency from
hand-typed track names is tolerated. The words themselves still need to match
(PCM / long / oneShot). The remainder of the track name (tag
stripped) is automatically written as a comment (#) directly above that part's settings
entry and body section in the output .mml.
# Example track names
[FM]Lead
[PCM long]Strings
[PCM oneShot]Drums
Bass # no tag -> treated as an FM voice part
What gets converted
The [global] section
| Field | How it's derived |
|---|---|
tempo | The first tempo meta event found anywhere in the SMF (decimal part
dropped). Defaults to 120 if none is found. |
sequenceID | The first track name (sequence name) found in the SMF, with
any double-quote characters stripped. Defaults to untitled if none is found. |
loop | Always false (fixed default). |
volume | Always 100 (fixed default). |
reverb | Always false (fixed default). |
reverbType | Always normal (fixed default). |
reverbTime | Always 2.0 (fixed default). |
reverbLevel | Always 100 (fixed default). |
Part assignment
- Following SMF track order, tracks are numbered per their tag: FM voice parts get 1, 2, 3...16, PCM voice parts get A, B, C...P (the FM and PCM counters are independent).
- A track with no note data at all is skipped (not converted).
- A track that would exceed either type's part limit (16 FM, 16 PCM) is skipped with a warning, rather than aborting the whole conversion.
- Every part is written with
voiceID: 0andmute: falsealways (an SMF has no concept of a go-fmml voice).
Part volume and pan
- If a track's early data includes CC (control change) 7 (volume), that value (0-127) is
used directly as the part's
volume. Otherwise100. - If a track's early data includes CC 10 (pan), MIDI's 0-127 range is converted to
go-fmml's -16(left)-16(right) scale and used as
pan. Otherwise0(center). - Volume/pan changes later in the track, plus other controller data (expression, pitch bend, portamento, etc.), are ignored entirely - only the early value is used.
Notes → MML commands (FM / PCM long)
- The SMF's own time division is scaled to go-fmml's internal tick resolution.
- Multiple notes sharing the exact same onset are grouped into a chord (
[ ]). If a chord's member notes have different lengths, the whole chord is unified to the longest one; the other notes' own length information is discarded. - Each note's (or chord's) length - based on the gap to the next onset - is greedily decomposed
into go-fmml's MML length tokens (whole note, half, quarter, ... 32nd, dotted, triplet), longest
first. A length too long for one token is split across several, tied together with
&. Any remainder shorter than a 32nd note is simply dropped. - Staccato (
Q) is snapped to one of 4 levels (Q1=25%, Q2=50%, Q3=75%, Q4=100%), based on the ratio between how long the note actually sounds (note-on to note-off) and the gap to the next note. - A span with no sound is written as a rest (
R). - Velocity is ignored; every note is written with a fixed
V127.
Notes → MML commands (PCM oneShot)
- A oneShot track converts into the dedicated syntax described on the
PCM(oneShot) part commands page: each distinct pitch used
gets its own independent
<note name>{ ... }block, written as a timeline that starts at tick 0 and runs in parallel with every other pitch's block. - Within each block, the same length-decomposition logic turns that pitch's onset gaps into a
sequence of triggers (
X) and rests (R). - Every pitch's final trigger is extended to match the whole track's own last onset (rather than each pitch tacking on its own independent padding) - this avoids inflating the song's overall loop length with arbitrary per-pitch trailing silence.
Caveats
.mml against the MML File Structure page, and
pairing it with voice files you author using the
FM Voice Parameters / PCM Voice Parameters
specs.
- SMPTE-based time division (frames-per-second timing) is not supported.
- The tool is written against Format 1 SMF files, per spec. Other formats may happen to parse structurally, but only Format 1 is tested and supported.
- Because the executable resolves its
smf/convertfolders relative to its own path, copying just the executable elsewhere requires recreating that same folder layout alongside it. - Characters invalid in a file name (
\ / : * ? " < > |, etc.) found in the source song name are replaced with an underscore (_) in the output file name.