From 9d6eb1ff0632c85362eccd129ffa075188952656 Mon Sep 17 00:00:00 2001 From: deadprogram Date: Sun, 3 Sep 2023 12:06:00 +0200 Subject: [PATCH] machine/usb/adc/midi: improve implementation to include several new messages such as program changes and pitch bend. Also add error handling for invalid parameter values such as MIDI channel. This however makes a somewhat breaking change to the current implementation, in that we now use the typical MIDI user system of counting MIDI channels from 1-16 instead of from 0-15 as the lower level USB-MIDI API itself expects. Also add constant values for continuous controller messages, rename SendCC function, and add SysEx function. Signed-off-by: deadprogram --- src/examples/usb-midi/main.go | 40 +++-- src/machine/usb/adc/midi/messages.go | 238 +++++++++++++++++++++++++-- src/machine/usb/adc/midi/midi.go | 33 +++- 3 files changed, 278 insertions(+), 33 deletions(-) diff --git a/src/examples/usb-midi/main.go b/src/examples/usb-midi/main.go index 02ef6b730a..9e0b4b2021 100644 --- a/src/examples/usb-midi/main.go +++ b/src/examples/usb-midi/main.go @@ -10,47 +10,57 @@ import ( // Try it easily by opening the following site in Chrome. // https://www.onlinemusictools.com/kb/ +const ( + cable = 0 + channel = 1 + velocity = 0x40 +) + func main() { led := machine.LED led.Configure(machine.PinConfig{Mode: machine.PinOutput}) button := machine.BUTTON - button.Configure(machine.PinConfig{Mode: machine.PinInputPulldown}) + button.Configure(machine.PinConfig{Mode: machine.PinInputPullup}) m := midi.Port() - m.SetHandler(func(b []byte) { + m.SetRxHandler(func(b []byte) { + // blink when we receive a MIDI message + led.Set(!led.Get()) + }) + + m.SetTxHandler(func() { + // blink when we send a MIDI message led.Set(!led.Get()) - m.Write(b) }) prev := true chords := []struct { - name string - keys []midi.Note + name string + notes []midi.Note }{ - {name: "C ", keys: []midi.Note{midi.C4, midi.E4, midi.G4}}, - {name: "G ", keys: []midi.Note{midi.G3, midi.B3, midi.D4}}, - {name: "Am", keys: []midi.Note{midi.A3, midi.C4, midi.E4}}, - {name: "F ", keys: []midi.Note{midi.F3, midi.A3, midi.C4}}, + {name: "C ", notes: []midi.Note{midi.C4, midi.E4, midi.G4}}, + {name: "G ", notes: []midi.Note{midi.G3, midi.B3, midi.D4}}, + {name: "Am", notes: []midi.Note{midi.A3, midi.C4, midi.E4}}, + {name: "F ", notes: []midi.Note{midi.F3, midi.A3, midi.C4}}, } index := 0 for { current := button.Get() if prev != current { - led.Set(current) if current { - for _, c := range chords[index].keys { - m.NoteOff(0, 0, c, 0x40) + for _, note := range chords[index].notes { + m.NoteOff(cable, channel, note, velocity) } index = (index + 1) % len(chords) } else { - for _, c := range chords[index].keys { - m.NoteOn(0, 0, c, 0x40) + for _, note := range chords[index].notes { + m.NoteOn(cable, channel, note, velocity) } } prev = current } - time.Sleep(10 * time.Millisecond) + time.Sleep(100 * time.Millisecond) } } diff --git a/src/machine/usb/adc/midi/messages.go b/src/machine/usb/adc/midi/messages.go index bf15e25dae..7a681f9bf6 100644 --- a/src/machine/usb/adc/midi/messages.go +++ b/src/machine/usb/adc/midi/messages.go @@ -1,19 +1,233 @@ package midi -// NoteOn sends a note on message. -func (m *midi) NoteOn(cable, channel uint8, note Note, velocity uint8) { - m.msg[0], m.msg[1], m.msg[2], m.msg[3] = (cable&0xf<<4)|0x9, 0x90|(channel&0xf), byte(note)&0x7f, velocity&0x7f - m.Write(m.msg[:]) +import ( + "errors" +) + +// From USB-MIDI section 4.1 "Code Index Number (CIN) Classifications" +const ( + CINSystemCommon2 = 0x2 + CINSystemCommon3 = 0x3 + CINSysExStart = 0x4 + CINSysExEnd1 = 0x5 + CINSysExEnd2 = 0x6 + CINSysExEnd3 = 0x7 + CINNoteOff = 0x8 + CINNoteOn = 0x9 + CINPoly = 0xA + CINControlChange = 0xB + CINProgramChange = 0xC + CINChannelPressure = 0xD + CINPitchBendChange = 0xE + CINSingleByte = 0xF +) + +// Standard MIDI channel messages +const ( + MsgNoteOff = 0x80 + MsgNoteOn = 0x90 + MsgPolyAftertouch = 0xA0 + MsgControlChange = 0xB0 + MsgProgramChange = 0xC0 + MsgChannelAftertouch = 0xD0 + MsgPitchBend = 0xE0 + MsgSysExStart = 0xF0 + MsgSysExEnd = 0xF7 +) + +// Standard MIDI control change messages +const ( + CCModulationWheel = 0x01 + CCBreathController = 0x02 + CCFootPedal = 0x04 + CCPortamentoTime = 0x05 + CCDataEntry = 0x06 + CCVolume = 0x07 + CCBalance = 0x08 + CCPan = 0x0A + CCExpression = 0x0B + CCEffectControl1 = 0x0C + CCEffectControl2 = 0x0D + CCGeneralPurpose1 = 0x10 + CCGeneralPurpose2 = 0x11 + CCGeneralPurpose3 = 0x12 + CCGeneralPurpose4 = 0x13 + CCBankSelect = 0x20 + CCModulationDepthRange = 0x21 + CCBreathControllerDepth = 0x22 + CCFootPedalDepth = 0x24 + CCEffectsLevel = 0x5B + CCTremeloLevel = 0x5C + CCChorusLevel = 0x5D + CCCelesteLevel = 0x5E + CCPhaserLevel = 0x5F + CCDataIncrement = 0x60 + CCDataDecrement = 0x61 + CCNRPNLSB = 0x62 + CCNRPNMSB = 0x63 + CCRPNLSB = 0x64 + CCRPNMSB = 0x65 + CCAllSoundOff = 0x78 + CCResetAllControllers = 0x79 + CCAllNotesOff = 0x7B + CCChannelVolume = 0x7F +) + +var ( + errInvalidMIDICable = errors.New("invalid MIDI cable") + errInvalidMIDIChannel = errors.New("invalid MIDI channel") + errInvalidMIDIVelocity = errors.New("invalid MIDI velocity") + errInvalidMIDIControl = errors.New("invalid MIDI control number") + errInvalidMIDIControlValue = errors.New("invalid MIDI control value") + errInvalidMIDIPatch = errors.New("invalid MIDI patch number") + errInvalidMIDIPitchBend = errors.New("invalid MIDI pitch bend value") + errInvalidMIDISysExData = errors.New("invalid MIDI SysEx data") +) + +// NoteOn sends a channel note on message. +// The cable parameter is the cable number 0-15. +// The channel parameter is the MIDI channel number 1-16. +func (m *midi) NoteOn(cable, channel uint8, note Note, velocity uint8) error { + switch { + case cable > 15: + return errInvalidMIDICable + case channel == 0 || channel > 16: + return errInvalidMIDIChannel + case velocity > 127: + return errInvalidMIDIVelocity + } + + m.msg[0], m.msg[1], m.msg[2], m.msg[3] = (cable&0xf<<4)|CINNoteOn, MsgNoteOn|(channel-1&0xf), byte(note)&0x7f, velocity&0x7f + _, err := m.Write(m.msg[:]) + return err } -// NoteOff sends a note off message. -func (m *midi) NoteOff(cable, channel uint8, note Note, velocity uint8) { - m.msg[0], m.msg[1], m.msg[2], m.msg[3] = (cable&0xf<<4)|0x8, 0x80|(channel&0xf), byte(note)&0x7f, velocity&0x7f - m.Write(m.msg[:]) +// NoteOff sends a channel note off message. +// The cable parameter is the cable number 0-15. +// The channel parameter is the MIDI channel number 1-16. +func (m *midi) NoteOff(cable, channel uint8, note Note, velocity uint8) error { + switch { + case cable > 15: + return errInvalidMIDICable + case channel == 0 || channel > 16: + return errInvalidMIDIChannel + case velocity > 127: + return errInvalidMIDIVelocity + } + + m.msg[0], m.msg[1], m.msg[2], m.msg[3] = (cable&0xf<<4)|CINNoteOff, MsgNoteOff|(channel-1&0xf), byte(note)&0x7f, velocity&0x7f + _, err := m.Write(m.msg[:]) + return err +} + +// ControlChange sends a channel continuous controller message. +// The cable parameter is the cable number 0-15. +// The channel parameter is the MIDI channel number 1-16. +// The control parameter is the controller number 0-127. +// The value parameter is the controller value 0-127. +func (m *midi) ControlChange(cable, channel, control, value uint8) error { + switch { + case cable > 15: + return errInvalidMIDICable + case channel == 0 || channel > 16: + return errInvalidMIDIChannel + case control > 127: + return errInvalidMIDIControl + case value > 127: + return errInvalidMIDIControlValue + } + + m.msg[0], m.msg[1], m.msg[2], m.msg[3] = (cable&0xf<<4)|CINControlChange, MsgControlChange|(channel-1&0xf), control&0x7f, value&0x7f + _, err := m.Write(m.msg[:]) + return err } -// SendCC sends a continuous controller message. -func (m *midi) SendCC(cable, channel, control, value uint8) { - m.msg[0], m.msg[1], m.msg[2], m.msg[3] = (cable&0xf<<4)|0xB, 0xB0|(channel&0xf), control&0x7f, value&0x7f - m.Write(m.msg[:]) +// ProgramChange sends a channel program change message. +// The cable parameter is the cable number 0-15. +// The channel parameter is the MIDI channel number 1-16. +// The patch parameter is the program number 0-127. +func (m *midi) ProgramChange(cable, channel uint8, patch uint8) error { + switch { + case cable > 15: + return errInvalidMIDICable + case channel == 0 || channel > 16: + return errInvalidMIDIChannel + case patch > 127: + return errInvalidMIDIPatch + } + + m.msg[0], m.msg[1], m.msg[2] = (cable&0xf<<4)|CINProgramChange, MsgProgramChange|(channel-1&0xf), patch&0x7f + _, err := m.Write(m.msg[:3]) + return err +} + +// PitchBend sends a channel pitch bend message. +// The cable parameter is the cable number 0-15. +// The channel parameter is the MIDI channel number 1-16. +// The bend parameter is the 14-bit pitch bend value (maximum 0x3FFF). +// Setting bend above 0x2000 (up to 0x3FFF) will increase the pitch. +// Setting bend below 0x2000 (down to 0x0000) will decrease the pitch. +func (m *midi) PitchBend(cable, channel uint8, bend uint16) error { + switch { + case cable > 15: + return errInvalidMIDICable + case channel == 0 || channel > 16: + return errInvalidMIDIChannel + case bend > 0x3FFF: + return errInvalidMIDIPitchBend + } + + m.msg[0], m.msg[1], m.msg[2], m.msg[3] = (cable&0xf<<4)|CINPitchBendChange, MsgPitchBend|(channel-1&0xf), byte(bend&0x7f), byte(bend>>8)&0x7f + _, err := m.Write(m.msg[:]) + return err +} + +// SysEx sends a System Exclusive message. +// The cable parameter is the cable number 0-15. +// The data parameter is a slice with the data to send. +// It needs to start with the manufacturer ID, which is either +// 1 or 3 bytes in length. +// The data slice should not include the SysEx start (0xF0) or +// end (0xF7) bytes, only the data in between. +func (m *midi) SysEx(cable uint8, data []byte) error { + switch { + case cable > 15: + return errInvalidMIDICable + case len(data) < 3: + return errInvalidMIDISysExData + } + + // write start + m.msg[0], m.msg[1] = (cable&0xf<<4)|CINSysExStart, MsgSysExStart + m.msg[2], m.msg[3] = data[0], data[1] + if _, err := m.Write(m.msg[:]); err != nil { + return err + } + + // write middle + i := 2 + for ; i < len(data)-2; i += 3 { + m.msg[0], m.msg[1] = (cable&0xf<<4)|CINSysExStart, data[i] + m.msg[2], m.msg[3] = data[i+1], data[i+2] + if _, err := m.Write(m.msg[:]); err != nil { + return err + } + } + // write end + switch len(data) - i { + case 2: + m.msg[0], m.msg[1] = (cable&0xf<<4)|CINSysExEnd3, data[i] + m.msg[2], m.msg[3] = data[i+1], MsgSysExEnd + case 1: + m.msg[0], m.msg[1] = (cable&0xf<<4)|CINSysExEnd2, data[i] + m.msg[2], m.msg[3] = MsgSysExEnd, 0 + case 0: + m.msg[0], m.msg[1] = (cable&0xf<<4)|CINSysExEnd1, MsgSysExEnd + m.msg[2], m.msg[3] = 0, 0 + } + if _, err := m.Write(m.msg[:]); err != nil { + return err + } + + return nil } diff --git a/src/machine/usb/adc/midi/midi.go b/src/machine/usb/adc/midi/midi.go index 30b645ee23..8ead2e9269 100644 --- a/src/machine/usb/adc/midi/midi.go +++ b/src/machine/usb/adc/midi/midi.go @@ -17,6 +17,7 @@ type midi struct { msg [4]byte buf *RingBuffer rxHandler func([]byte) + txHandler func() waitTxc bool } @@ -53,7 +54,7 @@ func newMidi() *midi { Index: usb.MIDI_ENDPOINT_IN, IsIn: true, Type: usb.ENDPOINT_TYPE_BULK, - TxHandler: m.Handler, + TxHandler: m.TxHandler, }, }, []usb.SetupConfig{}, @@ -61,16 +62,32 @@ func newMidi() *midi { return m } +// SetHandler is now deprecated, please use SetRxHandler(). func (m *midi) SetHandler(rxHandler func([]byte)) { + m.SetRxHandler(rxHandler) +} + +// SetRxHandler sets the handler function for incoming MIDI messages. +func (m *midi) SetRxHandler(rxHandler func([]byte)) { m.rxHandler = rxHandler } +// SetTxHandler sets the handler function for outgoing MIDI messages. +func (m *midi) SetTxHandler(txHandler func()) { + m.txHandler = txHandler +} + func (m *midi) Write(b []byte) (n int, err error) { - i := 0 - for i = 0; i < len(b); i += 4 { - m.tx(b[i : i+4]) + s, e := 0, 0 + for s = 0; s < len(b); s += 4 { + e = s + 4 + if e > len(b) { + e = len(b) + } + + m.tx(b[s:e]) } - return i, nil + return e, nil } // sendUSBPacket sends a MIDIPacket. @@ -79,7 +96,11 @@ func (m *midi) sendUSBPacket(b []byte) { } // from BulkIn -func (m *midi) Handler() { +func (m *midi) TxHandler() { + if m.txHandler != nil { + m.txHandler() + } + m.waitTxc = false if b, ok := m.buf.Get(); ok { m.waitTxc = true