1
1
mirror of https://github.com/wader/fq.git synced 2024-11-22 07:16:49 +03:00
This commit is contained in:
twystd 2024-09-17 22:23:22 -07:00
parent f150085735
commit 00ae470d48
6 changed files with 58 additions and 6 deletions

View File

@ -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",
@ -114,6 +115,7 @@ var notes = scalar.UintMapSymStr{
21: "A0",
}
// Map of key signature values to key signature names.
const (
keyCMajor = 0x0000
keyGMajor = 0x0100

13
format/midi/doc.go Normal file
View File

@ -0,0 +1,13 @@
/*
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

View File

@ -5,6 +5,7 @@ import (
"github.com/wader/fq/pkg/scalar"
)
// MIDI meta-event status byte values.
const (
SequenceNumber uint64 = 0x00
Text uint64 = 0x01
@ -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",
@ -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,
@ -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
@ -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")

View File

@ -12,15 +12,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,
@ -33,6 +36,10 @@ 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
@ -53,6 +60,12 @@ func decodeMIDI(d *decode.D) any {
return nil
}
// decodeMThd decodes an MThd MIDI header chunk into a 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) {
if !bytes.Equal(d.PeekBytes(4), []byte("MThd")) {
d.Errorf("missing MThd tag")
@ -76,6 +89,10 @@ func decodeMThd(d *decode.D) {
})
}
// 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")
@ -99,6 +116,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)
@ -113,6 +134,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
@ -161,13 +183,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)
@ -185,7 +208,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)
@ -200,7 +223,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)
@ -211,6 +234,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...)

View File

@ -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

View File

@ -28,6 +28,9 @@ lint:
fuzz: build
make -f Makefile fuzz GROUP=midi
godoc:
godoc -http=:80 -index_interval=60s
events: build
go run . -d midi dv format/midi/testdata/events/sequence-number.mid
go run . -d midi dv format/midi/testdata/events/text.mid