日本語English

go-fmmlgo-fmml

An asynchronous FM + PCM sound synthesis engine in Go (Golang), built for embedding game BGM.

https://github.com/megaak-soft/go-fmml

What is this?

go-fmml combines an original FM synthesis core inspired by the design philosophy of classic YAMAHA FM chips with a PCM core that plays back WAV samples, and lets you author and play sequence data using an original MML (Music Macro Language) text format, as a Go library. It is designed to embed directly into a game project and play BGM asynchronously (never blocking the caller).

This is not chip emulation and not a synthesis-accuracy reproduction project - it is an original implementation focused on "sounding like FM synthesis, letting you write BGM in MML, and being easy to embed in a game." It is not a port of, or derived from, any specific hardware's register map, emulator, or existing open-source implementation.

This project was built with Claude Code, using a "Spec-Driven Development" approach: the engine spec, MML spec, and phase plan were written out in detail in a specification document (CLAUDE.md), and the AI implemented the code from it.

AI-generated code carries a known risk of unintentional "license contamination" - copyleft (e.g. GPL) code patterns leaking in from the model's training data. go-fmml's specification document defines explicit rules to avoid this kind of contamination, and the AI is required to self-check against those rules on every code generation pass. go-fmml's source code itself is provided under the MIT License, and can be used freely, including for commercial use, redistribution, and embedding into games. See the License page for details.

What it can do (feature list)

FM synthesis

4 operators × 8 algorithms, 8 selectable basic waveforms (W1-W8), an LFO (vibrato), up to 16 parts, up to 16-voice polyphony per part.

PCM sound

WAV sample playback. Two types: drum-kit style oneShot (up to 16 samples per part) and pitch-shiftable long (loop/envelope/LFO support), up to 16 parts.

MML sequencer

Plays up to 32 parts (FM + PCM combined) in sample-accurate sync using go-fmml's own MML spec, at a timing resolution of 1/192 of a 4/4 measure.

Master reverb

A stereo reverb selectable between a Schroeder type (lightweight) and an FDN type (higher quality), with per-part and per-oneShot-note send levels.

MML expressiveness

Portamento, pitch-bend up/down, part mute/solo, transpose, and a conductor part (tempo changes, loop jump points).

Playback control

Play/Pause/Resume/Stop/Rewind/SkipPlay, master volume (instant or smooth), fade-in/fade-out/fade-in-and-out playback.

Flexible input

File-based loading (absolute path or embed.FS), plus passing MML/FM voice/PCM voice data directly as Go string literals.

SMF conversion tool

The bundled Smf2GoFMML tool reads a Standard MIDI File (Format 1) and converts it to go-fmml's MML format.

Requirements

ItemRequirement
Go1.24.0 or later (matches the minimum required by ebitengine/oto/v3, which go-fmml depends on)
Supported OSWindows / macOS / Linux / Android / iOS / WebAssembly (matches the platforms supported by ebitengine/oto/v3, used for audio output)

Try the demo

A minimal working example that actually plays sound is included at cmd/mmldemo. It demonstrates the full flow from loading FM voice/MML files to playback.

cd cmd/mmldemo
go run .

Performance characteristics

Audio rendering is sample-by-sample software synthesis. A few things worth knowing:

Relationship to ebitengine/oto/v3

Audio output uses the Ebitengine project's github.com/ebitengine/oto/v3. go-fmml's fmcore.NewEngine internally initializes an oto audio context, and the resulting *fmcore.Engine itself implements the io.Reader (Read([]byte) (int, error)) that oto's oto.Player expects, so it can be handed directly to an oto player.

For apps that already manage their own oto context (for example, one that is also doing other audio playback through Ebitengine at the same time), fmcore.NewEngineWithoutOutput is also provided: go-fmml does not open an audio device itself, and your app keeps owning the oto context. See the code sample below.

Third-party library license notices
go-fmml depends on a few OSS libraries internally (including indirect dependencies). Copyright in each library belongs to its own authors, and each library's own license terms apply independently of go-fmml's MIT license. The full dependency list and go-fmml's own usage terms (MIT License) are collected on the License page.

Code samples (Go)

Basic: go-fmml manages audio output itself

package main

import (
    "log"

    "github.com/megaak-soft/go-fmml/fileio"
    "github.com/megaak-soft/go-fmml/fmcore"
    "github.com/megaak-soft/go-fmml/player"
)

func main() {
    if err := fileio.LoadFMVoiceFile("voice_fm.yaml"); err != nil {
        log.Fatal(err)
    }
    if err := fileio.LoadPCMVoiceFile("voice_pcm.yaml"); err != nil {
        log.Fatal(err)
    }
    if err := fileio.LoadMMLFile("bgm1.mml"); err != nil {
        log.Fatal(err)
    }

    // Opens an audio device at 44.1kHz and initializes an oto/v3 context internally.
    engine, err := fmcore.NewEngine(44100)
    if err != nil {
        log.Fatal(err)
    }
    defer engine.Close()

    onComplete := func() { log.Println("playback finished") }
    if err := player.Play(engine, "bgm1", onComplete); err != nil {
        log.Fatal(err)
    }

    select {} // replace with your app's own wait/game loop
}

Sharing Ebitengine's own oto/v3 context

package main

import (
    "github.com/ebitengine/oto/v3"
    "github.com/megaak-soft/go-fmml/fileio"
    "github.com/megaak-soft/go-fmml/fmcore"
    "github.com/megaak-soft/go-fmml/player"
)

func setupBGM(otoCtx *oto.Context) (*fmcore.Engine, error) {
    if err := fileio.LoadFMVoiceFile("voice_fm.yaml"); err != nil {
        return nil, err
    }
    if err := fileio.LoadMMLFile("bgm1.mml"); err != nil {
        return nil, err
    }

    // Doesn't open an audio device - go-fmml is used purely as an io.Reader.
    engine := fmcore.NewEngineWithoutOutput(44100)

    // Registers go-fmml's engine as a player on your existing oto context.
    p := otoCtx.NewPlayer(engine)
    p.Play()

    return engine, player.Play(engine, "bgm1", nil)
}

Loading MML/voice data directly from a string literal (Phase 7)

const mml = `
[GoFMML File]
[global]
  tempo: 128
  sequenceID: demo
  loop: true
[partSetting]
part:
  - partNo: 1
    voiceID: 0
    volume: 100
    pan: 0
[part1]
  O4 L4 C D E F G A B O5 C
[part1End]
`

fileio.SetMMLTextData(mml)

For the detailed syntax and valid ranges of each parameter, see the spec pages linked in the left navigation.

About the Author

MEGAAK SOFT

MEGAAK SOFT is an indie solo game development project, making retro-style 2D games.

Website / GitHub / X