|
| 1 | +// SPDX-License-Identifier: GPL-2.0-or-later |
| 2 | +// Go port of chocolate-doom midifile.c / GENMIDI handling (Simon Howard et al.). |
| 3 | +// Ported for the go-doom/engine authors. |
| 4 | + |
| 5 | +// Package genmidi parses the DMX GENMIDI lump, the OPL instrument bank used by |
| 6 | +// DOOM's Adlib/OPL music playback. The layout is a faithful port of the |
| 7 | +// genmidi_instr_t table read by chocolate-doom's i_oplmusic.c |
| 8 | +// LoadInstrumentTable. |
| 9 | +package genmidi |
| 10 | + |
| 11 | +import ( |
| 12 | + "encoding/binary" |
| 13 | + "errors" |
| 14 | +) |
| 15 | + |
| 16 | +// Header is the 8-byte GENMIDI magic ("#OPL_II#"). |
| 17 | +const Header = "#OPL_II#" |
| 18 | + |
| 19 | +// Instrument counts, matching i_oplmusic.c. |
| 20 | +const ( |
| 21 | + NumInstruments = 128 // GENMIDI_NUM_INSTRS: melodic instruments |
| 22 | + NumPercussion = 47 // GENMIDI_NUM_PERCUSSION |
| 23 | + // NumEntries is the total number of instrument entries in the lump, the |
| 24 | + // melodic instruments followed by the percussion instruments. |
| 25 | + NumEntries = NumInstruments + NumPercussion // 175 |
| 26 | +) |
| 27 | + |
| 28 | +// Instrument flag bits (genmidi_instr_t.flags). |
| 29 | +const ( |
| 30 | + FlagFixed = 0x0001 // fixed pitch |
| 31 | + Flag2Voice = 0x0004 // double voice (OPL3) |
| 32 | +) |
| 33 | + |
| 34 | +// Byte sizes of the packed on-disk structures. |
| 35 | +const ( |
| 36 | + operatorSize = 6 // genmidi_op_t |
| 37 | + voiceSize = operatorSize + 1 + operatorSize + 1 + 2 // genmidi_voice_t = 16 |
| 38 | + instrumentSize = 2 + 1 + 1 + 2*voiceSize // genmidi_instr_t = 36 |
| 39 | + nameSize = 32 |
| 40 | +) |
| 41 | + |
| 42 | +// Operator holds the six per-operator bytes of a genmidi_op_t. |
| 43 | +type Operator struct { |
| 44 | + TremoloVibrato uint8 // tremolo |
| 45 | + AttackDecay uint8 // attack |
| 46 | + SustainRelease uint8 // sustain |
| 47 | + Waveform uint8 // waveform |
| 48 | + Scale uint8 // scale |
| 49 | + Level uint8 // level |
| 50 | +} |
| 51 | + |
| 52 | +// Voice is a genmidi_voice_t: a modulator/carrier operator pair plus the |
| 53 | +// feedback byte, an unused byte and the signed base note offset. |
| 54 | +type Voice struct { |
| 55 | + Modulator Operator |
| 56 | + Feedback uint8 |
| 57 | + Carrier Operator |
| 58 | + Unused uint8 |
| 59 | + BaseNoteOffset int16 |
| 60 | +} |
| 61 | + |
| 62 | +// Instrument is a genmidi_instr_t: flags, tuning and two voices. |
| 63 | +type Instrument struct { |
| 64 | + Flags uint16 |
| 65 | + FineTuning uint8 |
| 66 | + FixedNote uint8 |
| 67 | + Voices [2]Voice |
| 68 | +} |
| 69 | + |
| 70 | +// Bank is a parsed GENMIDI lump. Instruments 0..127 are the melodic |
| 71 | +// instruments; 128..174 are the percussion instruments. Names, when present in |
| 72 | +// the lump, are the trailing 32-byte name strings in the same order. |
| 73 | +type Bank struct { |
| 74 | + Instruments [NumEntries]Instrument |
| 75 | + // Names holds the trailing 32-byte name strings if the lump includes |
| 76 | + // them; it is nil when the lump is truncated to just the instrument data |
| 77 | + // (both layouts are accepted, following DMX/i_oplmusic behaviour). |
| 78 | + Names [][nameSize]byte |
| 79 | +} |
| 80 | + |
| 81 | +// Errors returned by Load. |
| 82 | +var ( |
| 83 | + ErrBadHeader = errors.New("genmidi: missing '#OPL_II#' header") |
| 84 | + ErrShort = errors.New("genmidi: lump too short for instrument table") |
| 85 | +) |
| 86 | + |
| 87 | +// Load parses a GENMIDI lump. It requires the 8-byte header and the 175 |
| 88 | +// instrument entries. The trailing name block is parsed when present but is |
| 89 | +// optional, matching the two GENMIDI layouts seen in the wild. |
| 90 | +func Load(b []byte) (*Bank, error) { |
| 91 | + if len(b) < len(Header) { |
| 92 | + return nil, ErrShort |
| 93 | + } |
| 94 | + if string(b[:len(Header)]) != Header { |
| 95 | + return nil, ErrBadHeader |
| 96 | + } |
| 97 | + |
| 98 | + off := len(Header) |
| 99 | + need := off + NumEntries*instrumentSize |
| 100 | + if len(b) < need { |
| 101 | + return nil, ErrShort |
| 102 | + } |
| 103 | + |
| 104 | + bank := &Bank{} |
| 105 | + for i := 0; i < NumEntries; i++ { |
| 106 | + bank.Instruments[i] = parseInstrument(b[off : off+instrumentSize]) |
| 107 | + off += instrumentSize |
| 108 | + } |
| 109 | + |
| 110 | + // Trailing names are optional. Parse as many complete 32-byte names as the |
| 111 | + // lump provides (up to one per entry). |
| 112 | + if len(b) >= off+nameSize { |
| 113 | + names := make([][nameSize]byte, 0, NumEntries) |
| 114 | + for i := 0; i < NumEntries && len(b) >= off+nameSize; i++ { |
| 115 | + var n [nameSize]byte |
| 116 | + copy(n[:], b[off:off+nameSize]) |
| 117 | + names = append(names, n) |
| 118 | + off += nameSize |
| 119 | + } |
| 120 | + bank.Names = names |
| 121 | + } |
| 122 | + |
| 123 | + return bank, nil |
| 124 | +} |
| 125 | + |
| 126 | +func parseInstrument(b []byte) Instrument { |
| 127 | + var instr Instrument |
| 128 | + instr.Flags = binary.LittleEndian.Uint16(b[0:2]) |
| 129 | + instr.FineTuning = b[2] |
| 130 | + instr.FixedNote = b[3] |
| 131 | + instr.Voices[0] = parseVoice(b[4 : 4+voiceSize]) |
| 132 | + instr.Voices[1] = parseVoice(b[4+voiceSize : 4+2*voiceSize]) |
| 133 | + return instr |
| 134 | +} |
| 135 | + |
| 136 | +func parseVoice(b []byte) Voice { |
| 137 | + var v Voice |
| 138 | + v.Modulator = parseOperator(b[0:operatorSize]) |
| 139 | + v.Feedback = b[operatorSize] |
| 140 | + v.Carrier = parseOperator(b[operatorSize+1 : 2*operatorSize+1]) |
| 141 | + v.Unused = b[2*operatorSize+1] |
| 142 | + v.BaseNoteOffset = int16(binary.LittleEndian.Uint16(b[2*operatorSize+2 : 2*operatorSize+4])) |
| 143 | + return v |
| 144 | +} |
| 145 | + |
| 146 | +func parseOperator(b []byte) Operator { |
| 147 | + return Operator{ |
| 148 | + TremoloVibrato: b[0], |
| 149 | + AttackDecay: b[1], |
| 150 | + SustainRelease: b[2], |
| 151 | + Waveform: b[3], |
| 152 | + Scale: b[4], |
| 153 | + Level: b[5], |
| 154 | + } |
| 155 | +} |
0 commit comments