Skip to content

Commit

Permalink
Merge pull request #1023 from transcriptaze/midi-v0.2
Browse files Browse the repository at this point in the history
midi: minor fixups
  • Loading branch information
wader authored Nov 9, 2024
2 parents 3917383 + 2b5a52b commit a4372cb
Show file tree
Hide file tree
Showing 24 changed files with 381 additions and 59 deletions.
28 changes: 15 additions & 13 deletions format/midi/constants.go
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ import (
"github.com/wader/fq/pkg/scalar"
)

// Map of note values to note names.
var notes = scalar.UintMapSymStr{
127: "G9",
126: "F♯9/G♭9",
Expand Down Expand Up @@ -114,6 +115,7 @@ var notes = scalar.UintMapSymStr{
21: "A0",
}

// Map of key signature values to key signature names.
const (
keyCMajor = 0x0000
keyGMajor = 0x0100
Expand Down Expand Up @@ -182,7 +184,7 @@ var keys = scalar.UintMapSymStr{
keyAFlatMinor: "A♭ minor",
}

var controllers = scalar.UintMapSymStr{
var controllersMap = scalar.UintMapSymStr{
// High resolution continuous controllers (MSB)
0: "Bank Select (MSB)",
1: "Modulation Wheel (MSB)",
Expand Down Expand Up @@ -269,7 +271,7 @@ var controllers = scalar.UintMapSymStr{
127: "Poly Mode On",
}

var manufacturers = scalar.UintMapSymStr{
var manufacturersMap = scalar.UintMapSymStr{
// special purpose

0x7D: "Non-Commercial",
Expand Down Expand Up @@ -318,7 +320,7 @@ var manufacturers = scalar.UintMapSymStr{
0x52: "Zoom",
}

var manufacturers_extended = scalar.UintMapSymStr{
var manufacturersExtendedMap = scalar.UintMapSymStr{
0x0007: "Digital Music Corporation",
0x0009: "New England Digital",
0x000E: "Alesis",
Expand Down Expand Up @@ -361,16 +363,16 @@ var manufacturers_extended = scalar.UintMapSymStr{
0x2127: "Expert Sleepers",
}

var framerates = scalar.UintMapSymUint{
0: 24,
1: 25,
2: 29,
3: 30,
var frameratesMap = scalar.UintMapSymStr{
0: "24 FPS",
1: "25 FPS",
2: "29.97 FPS DF",
3: "30 FPS",
}

var fps = scalar.UintMapSymUint{
0xe8: 24,
0xe7: 25,
0xe6: 29,
0xe5: 30,
var fpsMap = scalar.SintMapSymStr{
-24: "SMPTE 24 FPS",
-25: "SMPTE 25 FPS",
-29: "SMPTE 29.97 FPS DF",
-30: "SMPTE 30 FPS",
}
14 changes: 10 additions & 4 deletions format/midi/metaevents.go
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ import (
"github.com/wader/fq/pkg/scalar"
)

// MIDI meta-event status byte values.
const (
SequenceNumber uint64 = 0x00
Text uint64 = 0x01
Expand All @@ -26,6 +27,7 @@ const (
SequencerSpecificEvent uint64 = 0x7f
)

// Maps MIDI meta-events to a human readable name.
var metaevents = scalar.UintMapSymStr{
SequenceNumber: "sequence_number",
Text: "text",
Expand All @@ -47,6 +49,7 @@ var metaevents = scalar.UintMapSymStr{
SequencerSpecificEvent: "sequencer_specific_event",
}

// Internal map of MIDI meta-events to the associated event parser.
var metafns = map[uint64]func(d *decode.D){
SequenceNumber: decodeSequenceNumber,
Text: decodeText,
Expand All @@ -68,6 +71,7 @@ var metafns = map[uint64]func(d *decode.D){
SequencerSpecificEvent: decodeSequencerSpecificEvent,
}

// decodeMetaEvent extracts the meta-event delta time, event status and event detail.
func decodeMetaEvent(d *decode.D, event uint8, ctx *context) {
ctx.running = 0x00
ctx.casio = false
Expand All @@ -89,6 +93,8 @@ func decodeMetaEvent(d *decode.D, event uint8, ctx *context) {
}
}

// decodeSequenceNumber parses a Sequence Number MIDI meta event to a struct comprising:
// - sequence_number
func decodeSequenceNumber(d *decode.D) {
d.FieldUintFn("length", vlq, d.UintRequire(2))
d.FieldU16("sequence_number")
Expand Down Expand Up @@ -149,8 +155,8 @@ func decodeSMPTEOffset(d *decode.D) {
d.FieldUintFn("length", vlq, d.UintRequire(5))

d.FieldStruct("smpte_offset", func(d *decode.D) {
d.FieldU2("framerate", framerates)
d.FieldU6("hour")
d.FieldU3("framerate", frameratesMap)
d.FieldU5("hour")
d.FieldU8("minute")
d.FieldU8("second")
d.FieldU8("frames")
Expand Down Expand Up @@ -195,13 +201,13 @@ func decodeSequencerSpecificEvent(d *decode.D) {
b := d.PeekUintBits(8)

if length > 2 && b == 0 {
d.FieldU24("manufacturer", manufacturers_extended)
d.FieldU24("manufacturer", manufacturersExtendedMap)

if length > 3 {
d.FieldRawLen("data", 8*(int64(length)-3))
}
} else if length > 0 {
d.FieldU8("manufacturer", manufacturers)
d.FieldU8("manufacturer", manufacturersMap)

if length > 1 {
d.FieldRawLen("data", 8*(int64(length)-1))
Expand Down
108 changes: 82 additions & 26 deletions format/midi/midi.go
Original file line number Diff line number Diff line change
@@ -1,3 +1,15 @@
/*
Package midi implements an fq plugin to decode [standard MIDI files].
The MIDI decoder is a member of the 'probe' group and fq should automatically invoke the
decoder when opening a MIDI file. The decoder can be explicitly specified with the '-d midi'
command line option.
The decoder currently only supports MIDI 1.0 files and does only basic validation on the
MIDI file structure.
[standard MIDI files]: https://midi.org/standard-midi-files.
*/
package midi

// https://www.midi.org/specifications/item/the-midi-1-0-specification
Expand All @@ -12,15 +24,18 @@ import (
"github.com/wader/fq/pkg/interp"
)

//go:embed midi.md
var midiFS embed.FS

// context is a container struct for the running parse information required to
// decode a MIDI track.
type context struct {
tick uint64
running uint8
casio bool
}

//go:embed midi.md
var midiFS embed.FS

// init registers the MIDI format decoder and adds it to the 'probe' group.
func init() {
interp.RegisterFormat(
format.MIDI,
Expand All @@ -33,17 +48,31 @@ func init() {
interp.RegisterFS(midiFS)
}

// decodeMIDI implements the MIDI file decoder.
//
// The decoder parses the file as a set of chunks, each comprising a 4 character tag
// followed by a uint32 length field. The decoder parses the MTHd and MTrk MIDI chunks.
func decodeMIDI(d *decode.D) any {
d.Endian = decode.BigEndian

// ... decode header
d.FieldStruct("header", decodeMThd)
format, _ := decodeMThd(d)

// ... decode tracks (and other chunks)
d.FieldArray("content", func(d *decode.D) {
tracks := uint16(0)

for d.BitsLeft() > 0 {
if bytes.Equal(d.PeekBytes(4), []byte("MTrk")) {
d.FieldStruct("track", decodeMTrk)
switch {
case format == 0 && tracks > 0: // decode 'extra' format 0 tracks as data
d.FieldStruct("other", decodeOther)

default:
d.FieldStruct("track", decodeMTrk)
}

tracks++
} else {
d.FieldStruct("other", decodeOther)
}
Expand All @@ -53,29 +82,48 @@ func decodeMIDI(d *decode.D) any {
return nil
}

func decodeMThd(d *decode.D) {
if !bytes.Equal(d.PeekBytes(4), []byte("MThd")) {
d.Errorf("missing MThd tag")
}

d.FieldUTF8("tag", 4)
length := d.FieldS32("length")
// decodeMThd decodes an MThd MIDI header chunk into a 'header' struct with the fields:
// - tag "MThd"
// - length Header chunk size
// - format MIDI format (0,1 or 2)
// - tracks Number of tracks
// - division Time division
func decodeMThd(d *decode.D) (uint16, uint16) {
var format uint16
var tracks uint16

f := func(d *decode.D) {
if !bytes.Equal(d.PeekBytes(4), []byte("MThd")) {
d.Errorf("missing MThd tag")
}

d.FramedFn(length*8, func(d *decode.D) {
d.FieldU16("format")
d.FieldU16("tracks")
d.FieldUTF8("tag", 4)
length := d.FieldS32("length")

d.FieldStruct("division", func(d *decode.D) {
if division := d.PeekUintBits(16); division&0x8000 == 0x8000 {
d.FieldU8("fps", fps)
d.FieldU8("resolution")
} else {
d.FieldU16("ppqn")
}
d.FramedFn(length*8, func(d *decode.D) {
format = uint16(d.FieldU16("format"))
tracks = uint16(d.FieldU16("tracks"))

d.FieldStruct("division", func(d *decode.D) {
if division := d.PeekUintBits(16); division&0x8000 == 0x8000 {
d.FieldS8("fps", fpsMap)
d.FieldU8("resolution")
} else {
d.FieldU16("ppqn")
}
})
})
})
}

d.FieldStruct("header", f)

return format, tracks
}

// decodeMTrk decodes an MTrk MIDI track chunk into a struct with the header fields:
// - tag "MTrk"
// - length Track chunk size
// - events List of track events
func decodeMTrk(d *decode.D) {
if !bytes.Equal(d.PeekBytes(4), []byte("MTrk")) {
d.Errorf("missing MTrk tag")
Expand All @@ -99,6 +147,10 @@ func decodeMTrk(d *decode.D) {
})
}

// decodeEvent decodes a single MIDI event as either:
// - Meta event
// - MIDI channel event
// - SysEx system event
func decodeEvent(d *decode.D, ctx *context) {
_, status, event := peekEvent(d)

Expand All @@ -113,6 +165,7 @@ func decodeEvent(d *decode.D, ctx *context) {
}
}

// peekEvent retrieves the type of the next event without moving the reader location.
func peekEvent(d *decode.D) (uint64, uint8, uint8) {
var N int = 1

Expand Down Expand Up @@ -161,13 +214,14 @@ func peekEvent(d *decode.D) (uint64, uint8, uint8) {
}
}

// decodeOther decodes non-MIDI chunks as raw data.
func decodeOther(d *decode.D) {
d.FieldUTF8("tag", 4)
length := d.FieldS32("length")
d.FieldRawLen("data", length*8)
}

// Big endian varint
// vlq decodes a MIDI big-endian varuint.
func vlq(d *decode.D) uint64 {
vlq := uint64(0)

Expand All @@ -185,7 +239,7 @@ func vlq(d *decode.D) uint64 {
return vlq
}

// Byte array with a big endian varint length
// vlf decodes a MIDI byte array prefixed with a varuint length.
func vlf(d *decode.D) ([]uint8, error) {
N := vlq(d)

Expand All @@ -200,7 +254,7 @@ func vlf(d *decode.D) ([]uint8, error) {
}
}

// String with a big endian varint length
// vlstring decodes a MIDI string prefixed with a varuint length.
func vlstring(d *decode.D) string {
if data, err := vlf(d); err != nil {
d.Fatalf("%v", err)
Expand All @@ -211,6 +265,8 @@ func vlstring(d *decode.D) string {
return ""
}

// flush reads and discards any remaining bits in a chunk after encountering an
// invalid event.
func flush(d *decode.D, format string, args ...any) {
d.Errorf(format, args...)

Expand Down
2 changes: 2 additions & 0 deletions format/midi/midi.md
Original file line number Diff line number Diff line change
Expand Up @@ -35,3 +35,5 @@ fq -d midi 'grep_by(.event=="note_on") | [.time.tick, .note_on.note] | join(" ")
2. [Standard MIDI Files](https://midi.org/standard-midi-files)
3. [Standard MIDI File (SMF) Format](http://midi.teragonaudio.com/tech/midifile.htm)
4. [MIDI Files Specification](http://www.somascape.org/midi/tech/mfile.html)
5. [MIDI SMPTE Offset meta message](https://www.recordingblogs.com/wiki/midi-smpte-offset-meta-message)
6. [Somascape MIDI Files Specification](http://www.somascape.org/midi/tech/mfile.html#meta)
5 changes: 4 additions & 1 deletion format/midi/midievents.go
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,9 @@ import (
"github.com/wader/fq/pkg/scalar"
)

// MIDI event status byte values. A MIDI event status byte is a composite byte
// composed of the event type in the high order nibble and the event channel
// (0 to 15) in the low order nibble.
const (
NoteOff uint64 = 0x80
NoteOn uint64 = 0x90
Expand Down Expand Up @@ -94,7 +97,7 @@ func decodePolyphonicPressure(d *decode.D) {

func decodeController(d *decode.D) {
d.FieldStruct("controller", func(d *decode.D) {
d.FieldU8("controller", controllers)
d.FieldU8("controller", controllersMap)
d.FieldU8("value")
})
}
Expand Down
2 changes: 1 addition & 1 deletion format/midi/sysex.go
Original file line number Diff line number Diff line change
Expand Up @@ -58,7 +58,7 @@ func decodeSysExMessage(d *decode.D, ctx *context) {
}

d.FieldStruct("message", func(d *decode.D) {
d.FieldU8("manufacturer", manufacturers)
d.FieldU8("manufacturer", manufacturersMap)

if length < 1 {
ctx.casio = true
Expand Down
17 changes: 17 additions & 0 deletions format/midi/testdata/CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
# CHANGELOG

## Unreleased

### Added
1. Minimal package godoc.

### Updated
1. Reworked MThd SMPTE field to mirror the wording of the MIDI spec.
2. Decoded 'extra' format 0 file tracks as data.


## [0.1.0](https://github.com/wader/fq/releases/tag/v0.13.0) - 2024-09-21

1. Initial release


Loading

0 comments on commit a4372cb

Please sign in to comment.