Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

machine/usb/adc/midi: improve implementation #3889

Merged
merged 1 commit into from
Sep 7, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
40 changes: 25 additions & 15 deletions src/examples/usb-midi/main.go
Original file line number Diff line number Diff line change
Expand Up @@ -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)
}
}
238 changes: 226 additions & 12 deletions src/machine/usb/adc/midi/messages.go
Original file line number Diff line number Diff line change
@@ -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
}
33 changes: 27 additions & 6 deletions src/machine/usb/adc/midi/midi.go
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@ type midi struct {
msg [4]byte
buf *RingBuffer
rxHandler func([]byte)
txHandler func()
waitTxc bool
}

Expand Down Expand Up @@ -53,24 +54,40 @@ func newMidi() *midi {
Index: usb.MIDI_ENDPOINT_IN,
IsIn: true,
Type: usb.ENDPOINT_TYPE_BULK,
TxHandler: m.Handler,
TxHandler: m.TxHandler,
},
},
[]usb.SetupConfig{},
)
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()) {
deadprogram marked this conversation as resolved.
Show resolved Hide resolved
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])
deadprogram marked this conversation as resolved.
Show resolved Hide resolved
}
return i, nil
return e, nil
}

// sendUSBPacket sends a MIDIPacket.
Expand All @@ -79,7 +96,11 @@ func (m *midi) sendUSBPacket(b []byte) {
}

// from BulkIn
func (m *midi) Handler() {
func (m *midi) TxHandler() {
deadprogram marked this conversation as resolved.
Show resolved Hide resolved
if m.txHandler != nil {
m.txHandler()
}
deadprogram marked this conversation as resolved.
Show resolved Hide resolved

m.waitTxc = false
if b, ok := m.buf.Get(); ok {
m.waitTxc = true
Expand Down
Loading