Standard library · Encoding and data

audio/midi

Reading, writing and playing standard MIDI files.

import audio.midi

A MIDI file holds notes, not sound: which note, when, how hard and on what instrument. That makes it tiny, and it makes it editable in a way a recording is not.

This package reads and writes standard MIDI files, and Render turns one into sound through audio/synth, so a file of notes becomes a clip you can save as WAV or FLAC.

Things to know

  • Everything is in ticks, and division says how many ticks a quarter note is. TicksToSeconds needs the tempo as well, which is why it takes the whole song.
  • A note is two events, on and off. AddNote writes both, and Notes pairs them back up and gives you lengths in seconds.
  • A note on with velocity 0 means a note off, and the reader turns it into one so you never have to check.
  • Release a song with FreeSong: it owns a vector per track plus the vector of tracks.
  • Render uses a single plain envelope for every note. For anything more, take Notes and render it yourself.

Types

Event

struct Event
One MIDI event, with the tick it happens at.
NameTypeDescription
tickint = 0When it happens, in ticks from the start.
statusint = 0Which kind of event it is.
channelint = 0Which of the sixteen channels it is on.
data1int = 0The first byte, a note number or a controller.
data2int = 0The second byte, a velocity or a value.
metaint = 0Which meta event it is, when the status says meta.
valuei64 = 0The number a meta event carries, such as a tempo.
midi-Write.salam
import audio.midi
import compress

func main:
    mut f := midi.NewSong(480)
    println f.division, f.format, f.tempo as int
    println midi.DefaultDivision

    mut t := midi.NewTrack()
    midi.AddTempo(t, 0, 120.0)
    midi.AddProgram(t, 0, 0, 0)
    midi.AddControl(t, 0, 7, 100, 0)
    mut s := 0
    until s < 4:
        midi.AddNote(t, s * 480, 240, 60 + s * 2, 100, 0)
        s += 1
    end
    println t.events.len()
    midi.AddTrack(f, t)
    println f.tracks.len(), midi.TotalTicks(f)

    one := t.events.get(1)
    println one.tick, one.status == midi.NoteOn, one.data1, one.data2, one.channel

    println midi.NoteOn, midi.NoteOff, midi.ControlChange, midi.ProgramChange, midi.MetaEvent

    enc := midi.EncodeMIDI(f)
    println compress.BufLen(enc) > 22, midi.IsMIDI(enc.data, enc.len)
    mut empty := midi.NewSong(480)
    println compress.BufLen(midi.EncodeMIDI(empty))
    midi.FreeSong(empty)

    compress.BufFree(enc)
    midi.FreeSong(f)
end
Output480 1 500000 480 11 1 1680 0 false 0 0 0 144 128 176 192 255 true true 0

Track

struct Track
One track: a list of events in tick order.
NameTypeDescription
eventsVectorThe events.

The example for this item is shown with the ones above: midi-Write.salam

Song

struct Song
A whole MIDI file: its format, its timing and its tracks.
NameTypeDescription
formatint = 0Which SMF format it is, 0 for one track and 1 for several.
divisionint = 0Ticks per quarter note, which is what every tick is measured in.
tempoi64 = 500000Microseconds per quarter note. 500000 is 120 beats per minute.
tracksVectorThe tracks themselves.

The example for this item is shown with the ones above: midi-Write.salam

Constants and variables

NoteOff

const NoteOff: i32 = 128
A note ending. A note on with velocity 0 is read as one of these.

The example for this item is shown with the ones above: midi-Write.salam

NoteOn

const NoteOn: i32 = 144
A note starting.

The example for this item is shown with the ones above: midi-Write.salam

ControlChange

const ControlChange: i32 = 176
A controller moving, such as a volume pedal.

The example for this item is shown with the ones above: midi-Write.salam

ProgramChange

const ProgramChange: i32 = 192
The instrument changing.

The example for this item is shown with the ones above: midi-Write.salam

Meta event numbers Meta…

Which meta event an event carries when its status says meta.
NameValue
MetaEvent255
MetaTempo81
MetaTrackName3
MetaEndOfTrack47
MetaTimeSignature88

The example for this item is shown with the ones above: midi-Write.salam

DefaultDivision

const DefaultDivision: i32 = 480
The ticks per quarter note a new song gets, 480.

The example for this item is shown with the ones above: midi-Write.salam

Functions

IsMIDI

func IsMIDI(p: void*, n: int): bool
True when these bytes start with the MThd header.
midi-Read.salam
import audio
import audio.midi
import compress
import filepath
import math
import mem
import os

func main:
    mut f := midi.NewSong(480)
    mut t := midi.NewTrack()
    midi.AddTempo(t, 0, 120.0)
    mut s := 0
    until s < 4:
        midi.AddNote(t, s * 480, 240, 60 + s * 2, 100, 0)
        s += 1
    end
    midi.AddTrack(f, t)
    enc := midi.EncodeMIDI(f)

    println midi.IsMIDI(enc.data, enc.len), midi.IsMIDI(enc.data, 8)
    println audio.FormatName(audio.DetectFormat(enc.data, enc.len))
    mut ok := false
    mut back := midi.DecodeMIDI(enc.data, enc.len, ok)
    println ok, back.division, back.tracks.len()
    println math.Abs(midi.Bpm(back) - 120.0) < 0.01
    println midi.TotalTicks(back)
    println midi.TicksToSeconds(back, 480), midi.SecondsToTicks(back, 0.5)

    mut notes := midi.Notes(back)
    println notes.len(), notes.get(0).note, notes.get(3).note
    println notes.get(1).start, notes.get(0).length
    notes.free()

    rendered := midi.Render(back, 22050, 1)
    println rendered.frames > 0, audio.Peak(rendered) > 0.05
    println midi.ProgramName(0), midi.ProgramName(200) == ""

    path := filepath.Join(os.TempDir(), "salam_midi_example.mid")
    defer mem.Free(path as void*)
    println midi.SaveMIDI(f, path)
    mut dok := false
    mut disk := midi.OpenMIDI(path, dok)
    println dok, disk.tracks.len()
    mut gone := false
    missing := filepath.Join(os.TempDir(), "salam_midi_none.mid")
    defer mem.Free(missing as void*)
    midi.OpenMIDI(missing, gone)
    println gone

    mut bad := false
    midi.DecodeMIDI(enc.data, 8, bad)
    println bad

    compress.BufFree(enc)
    mut r := rendered
    audio.Free(r)
    midi.FreeSong(f)
    midi.FreeSong(back)
    midi.FreeSong(disk)
end
Outputtrue false midi true 480 1 true 1680 0.5 480 4 60 66 0.5 0.25 true true Acoustic Grand Piano true true true 1 false false

NewSong

func NewSong(division: int): Song
An empty song with a division you choose.

The example for this item is shown with the ones above: midi-Write.salam

NewTrack

func NewTrack(): Track
An empty track.

The example for this item is shown with the ones above: midi-Write.salam

AddTrack

func AddTrack(f &: Song, t: Track)
Adds a track to a song.

The example for this item is shown with the ones above: midi-Write.salam

AddNote

func AddNote(t &: Track, tick: int, length: int, note: int, velocity: int, channel: int)
Adds a note on and the matching note off, which is what writing a note really means.

The example for this item is shown with the ones above: midi-Write.salam

AddTempo

func AddTempo(t &: Track, tick: int, bpm: f64)
Adds a tempo change in beats per minute.

The example for this item is shown with the ones above: midi-Write.salam

AddProgram

func AddProgram(t &: Track, tick: int, program: int, channel: int)
Adds an instrument change.

The example for this item is shown with the ones above: midi-Write.salam

AddControl

func AddControl(t &: Track, tick: int, controller: int, value: int, channel: int)
Adds a controller change.

The example for this item is shown with the ones above: midi-Write.salam

FreeSong

func FreeSong(f &: Song)
Releases every track and event a song holds.

The example for this item is shown with the ones above: midi-Write.salam

TicksToSeconds

func TicksToSeconds(f: Song, ticks: int): f64
How many seconds a number of ticks lasts at the song's tempo.

The example for this item is shown with the ones above: midi-Read.salam

SecondsToTicks

func SecondsToTicks(f: Song, seconds: f64): int
The other way round.

The example for this item is shown with the ones above: midi-Read.salam

Bpm

func Bpm(f: Song): f64
The tempo in beats per minute.

The example for this item is shown with the ones above: midi-Read.salam

TotalTicks

func TotalTicks(f: Song): int
The tick the last event happens at, so how long the song is.

The example for this item is shown with the ones above: midi-Read.salam

DecodeMIDI

func DecodeMIDI(p: void*, n: int, ok &: bool): Song
Reads a standard MIDI file, following running status and skipping system events.

The example for this item is shown with the ones above: midi-Read.salam

EncodeMIDI

func EncodeMIDI(f: Song): compress.ByteBuf
Writes a song out as a standard MIDI file.

The example for this item is shown with the ones above: midi-Write.salam

OpenMIDI

func OpenMIDI(path: str, ok &: bool): Song
Reads a MIDI file from disk.

The example for this item is shown with the ones above: midi-Read.salam

SaveMIDI

func SaveMIDI(f: Song, path: str): bool
Writes a song to a MIDI file.

The example for this item is shown with the ones above: midi-Write.salam

Notes

func Notes(f: Song): Vector<synth.Note>
Every note in the song as a synth note, with its start and length already in seconds.

The example for this item is shown with the ones above: midi-Read.salam

Render

func Render(f: Song, rate: int, channels: int): audio.Clip
Renders a whole song to a clip through audio/synth.

The example for this item is shown with the ones above: midi-Read.salam

ProgramName

func ProgramName(program: int): str
The General MIDI name of an instrument number, or an empty string for one this package does not name.

The example for this item is shown with the ones above: midi-Read.salam