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 <ron@hybridgroup.com>
Этот коммит содержится в:
родитель
4643401a1d
коммит
9d6eb1ff06
3 изменённых файлов: 278 добавлений и 33 удалений
|
@ -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)
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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
|
||||
}
|
||||
|
||||
// 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[:])
|
||||
// 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
|
||||
}
|
||||
|
||||
// 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
|
||||
}
|
||||
|
|
|
@ -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
|
||||
|
|
Загрузка…
Создание таблицы
Сослаться в новой задаче