Skip to content

Commit 9d6eb1f

Browse files
committed
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 <[email protected]>
1 parent 4643401 commit 9d6eb1f

File tree

3 files changed

+278
-33
lines changed

3 files changed

+278
-33
lines changed

src/examples/usb-midi/main.go

Lines changed: 25 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -10,47 +10,57 @@ import (
1010
// Try it easily by opening the following site in Chrome.
1111
// https://www.onlinemusictools.com/kb/
1212

13+
const (
14+
cable = 0
15+
channel = 1
16+
velocity = 0x40
17+
)
18+
1319
func main() {
1420
led := machine.LED
1521
led.Configure(machine.PinConfig{Mode: machine.PinOutput})
1622

1723
button := machine.BUTTON
18-
button.Configure(machine.PinConfig{Mode: machine.PinInputPulldown})
24+
button.Configure(machine.PinConfig{Mode: machine.PinInputPullup})
1925

2026
m := midi.Port()
21-
m.SetHandler(func(b []byte) {
27+
m.SetRxHandler(func(b []byte) {
28+
// blink when we receive a MIDI message
29+
led.Set(!led.Get())
30+
})
31+
32+
m.SetTxHandler(func() {
33+
// blink when we send a MIDI message
2234
led.Set(!led.Get())
23-
m.Write(b)
2435
})
2536

2637
prev := true
2738
chords := []struct {
28-
name string
29-
keys []midi.Note
39+
name string
40+
notes []midi.Note
3041
}{
31-
{name: "C ", keys: []midi.Note{midi.C4, midi.E4, midi.G4}},
32-
{name: "G ", keys: []midi.Note{midi.G3, midi.B3, midi.D4}},
33-
{name: "Am", keys: []midi.Note{midi.A3, midi.C4, midi.E4}},
34-
{name: "F ", keys: []midi.Note{midi.F3, midi.A3, midi.C4}},
42+
{name: "C ", notes: []midi.Note{midi.C4, midi.E4, midi.G4}},
43+
{name: "G ", notes: []midi.Note{midi.G3, midi.B3, midi.D4}},
44+
{name: "Am", notes: []midi.Note{midi.A3, midi.C4, midi.E4}},
45+
{name: "F ", notes: []midi.Note{midi.F3, midi.A3, midi.C4}},
3546
}
3647
index := 0
3748

3849
for {
3950
current := button.Get()
4051
if prev != current {
41-
led.Set(current)
4252
if current {
43-
for _, c := range chords[index].keys {
44-
m.NoteOff(0, 0, c, 0x40)
53+
for _, note := range chords[index].notes {
54+
m.NoteOff(cable, channel, note, velocity)
4555
}
4656
index = (index + 1) % len(chords)
4757
} else {
48-
for _, c := range chords[index].keys {
49-
m.NoteOn(0, 0, c, 0x40)
58+
for _, note := range chords[index].notes {
59+
m.NoteOn(cable, channel, note, velocity)
5060
}
5161
}
5262
prev = current
5363
}
54-
time.Sleep(10 * time.Millisecond)
64+
time.Sleep(100 * time.Millisecond)
5565
}
5666
}
Lines changed: 226 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -1,19 +1,233 @@
11
package midi
22

3-
// NoteOn sends a note on message.
4-
func (m *midi) NoteOn(cable, channel uint8, note Note, velocity uint8) {
5-
m.msg[0], m.msg[1], m.msg[2], m.msg[3] = (cable&0xf<<4)|0x9, 0x90|(channel&0xf), byte(note)&0x7f, velocity&0x7f
6-
m.Write(m.msg[:])
3+
import (
4+
"errors"
5+
)
6+
7+
// From USB-MIDI section 4.1 "Code Index Number (CIN) Classifications"
8+
const (
9+
CINSystemCommon2 = 0x2
10+
CINSystemCommon3 = 0x3
11+
CINSysExStart = 0x4
12+
CINSysExEnd1 = 0x5
13+
CINSysExEnd2 = 0x6
14+
CINSysExEnd3 = 0x7
15+
CINNoteOff = 0x8
16+
CINNoteOn = 0x9
17+
CINPoly = 0xA
18+
CINControlChange = 0xB
19+
CINProgramChange = 0xC
20+
CINChannelPressure = 0xD
21+
CINPitchBendChange = 0xE
22+
CINSingleByte = 0xF
23+
)
24+
25+
// Standard MIDI channel messages
26+
const (
27+
MsgNoteOff = 0x80
28+
MsgNoteOn = 0x90
29+
MsgPolyAftertouch = 0xA0
30+
MsgControlChange = 0xB0
31+
MsgProgramChange = 0xC0
32+
MsgChannelAftertouch = 0xD0
33+
MsgPitchBend = 0xE0
34+
MsgSysExStart = 0xF0
35+
MsgSysExEnd = 0xF7
36+
)
37+
38+
// Standard MIDI control change messages
39+
const (
40+
CCModulationWheel = 0x01
41+
CCBreathController = 0x02
42+
CCFootPedal = 0x04
43+
CCPortamentoTime = 0x05
44+
CCDataEntry = 0x06
45+
CCVolume = 0x07
46+
CCBalance = 0x08
47+
CCPan = 0x0A
48+
CCExpression = 0x0B
49+
CCEffectControl1 = 0x0C
50+
CCEffectControl2 = 0x0D
51+
CCGeneralPurpose1 = 0x10
52+
CCGeneralPurpose2 = 0x11
53+
CCGeneralPurpose3 = 0x12
54+
CCGeneralPurpose4 = 0x13
55+
CCBankSelect = 0x20
56+
CCModulationDepthRange = 0x21
57+
CCBreathControllerDepth = 0x22
58+
CCFootPedalDepth = 0x24
59+
CCEffectsLevel = 0x5B
60+
CCTremeloLevel = 0x5C
61+
CCChorusLevel = 0x5D
62+
CCCelesteLevel = 0x5E
63+
CCPhaserLevel = 0x5F
64+
CCDataIncrement = 0x60
65+
CCDataDecrement = 0x61
66+
CCNRPNLSB = 0x62
67+
CCNRPNMSB = 0x63
68+
CCRPNLSB = 0x64
69+
CCRPNMSB = 0x65
70+
CCAllSoundOff = 0x78
71+
CCResetAllControllers = 0x79
72+
CCAllNotesOff = 0x7B
73+
CCChannelVolume = 0x7F
74+
)
75+
76+
var (
77+
errInvalidMIDICable = errors.New("invalid MIDI cable")
78+
errInvalidMIDIChannel = errors.New("invalid MIDI channel")
79+
errInvalidMIDIVelocity = errors.New("invalid MIDI velocity")
80+
errInvalidMIDIControl = errors.New("invalid MIDI control number")
81+
errInvalidMIDIControlValue = errors.New("invalid MIDI control value")
82+
errInvalidMIDIPatch = errors.New("invalid MIDI patch number")
83+
errInvalidMIDIPitchBend = errors.New("invalid MIDI pitch bend value")
84+
errInvalidMIDISysExData = errors.New("invalid MIDI SysEx data")
85+
)
86+
87+
// NoteOn sends a channel note on message.
88+
// The cable parameter is the cable number 0-15.
89+
// The channel parameter is the MIDI channel number 1-16.
90+
func (m *midi) NoteOn(cable, channel uint8, note Note, velocity uint8) error {
91+
switch {
92+
case cable > 15:
93+
return errInvalidMIDICable
94+
case channel == 0 || channel > 16:
95+
return errInvalidMIDIChannel
96+
case velocity > 127:
97+
return errInvalidMIDIVelocity
98+
}
99+
100+
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
101+
_, err := m.Write(m.msg[:])
102+
return err
7103
}
8104

9-
// NoteOff sends a note off message.
10-
func (m *midi) NoteOff(cable, channel uint8, note Note, velocity uint8) {
11-
m.msg[0], m.msg[1], m.msg[2], m.msg[3] = (cable&0xf<<4)|0x8, 0x80|(channel&0xf), byte(note)&0x7f, velocity&0x7f
12-
m.Write(m.msg[:])
105+
// NoteOff sends a channel note off message.
106+
// The cable parameter is the cable number 0-15.
107+
// The channel parameter is the MIDI channel number 1-16.
108+
func (m *midi) NoteOff(cable, channel uint8, note Note, velocity uint8) error {
109+
switch {
110+
case cable > 15:
111+
return errInvalidMIDICable
112+
case channel == 0 || channel > 16:
113+
return errInvalidMIDIChannel
114+
case velocity > 127:
115+
return errInvalidMIDIVelocity
116+
}
117+
118+
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
119+
_, err := m.Write(m.msg[:])
120+
return err
121+
}
122+
123+
// ControlChange sends a channel continuous controller message.
124+
// The cable parameter is the cable number 0-15.
125+
// The channel parameter is the MIDI channel number 1-16.
126+
// The control parameter is the controller number 0-127.
127+
// The value parameter is the controller value 0-127.
128+
func (m *midi) ControlChange(cable, channel, control, value uint8) error {
129+
switch {
130+
case cable > 15:
131+
return errInvalidMIDICable
132+
case channel == 0 || channel > 16:
133+
return errInvalidMIDIChannel
134+
case control > 127:
135+
return errInvalidMIDIControl
136+
case value > 127:
137+
return errInvalidMIDIControlValue
138+
}
139+
140+
m.msg[0], m.msg[1], m.msg[2], m.msg[3] = (cable&0xf<<4)|CINControlChange, MsgControlChange|(channel-1&0xf), control&0x7f, value&0x7f
141+
_, err := m.Write(m.msg[:])
142+
return err
13143
}
14144

15-
// SendCC sends a continuous controller message.
16-
func (m *midi) SendCC(cable, channel, control, value uint8) {
17-
m.msg[0], m.msg[1], m.msg[2], m.msg[3] = (cable&0xf<<4)|0xB, 0xB0|(channel&0xf), control&0x7f, value&0x7f
18-
m.Write(m.msg[:])
145+
// ProgramChange sends a channel program change message.
146+
// The cable parameter is the cable number 0-15.
147+
// The channel parameter is the MIDI channel number 1-16.
148+
// The patch parameter is the program number 0-127.
149+
func (m *midi) ProgramChange(cable, channel uint8, patch uint8) error {
150+
switch {
151+
case cable > 15:
152+
return errInvalidMIDICable
153+
case channel == 0 || channel > 16:
154+
return errInvalidMIDIChannel
155+
case patch > 127:
156+
return errInvalidMIDIPatch
157+
}
158+
159+
m.msg[0], m.msg[1], m.msg[2] = (cable&0xf<<4)|CINProgramChange, MsgProgramChange|(channel-1&0xf), patch&0x7f
160+
_, err := m.Write(m.msg[:3])
161+
return err
162+
}
163+
164+
// PitchBend sends a channel pitch bend message.
165+
// The cable parameter is the cable number 0-15.
166+
// The channel parameter is the MIDI channel number 1-16.
167+
// The bend parameter is the 14-bit pitch bend value (maximum 0x3FFF).
168+
// Setting bend above 0x2000 (up to 0x3FFF) will increase the pitch.
169+
// Setting bend below 0x2000 (down to 0x0000) will decrease the pitch.
170+
func (m *midi) PitchBend(cable, channel uint8, bend uint16) error {
171+
switch {
172+
case cable > 15:
173+
return errInvalidMIDICable
174+
case channel == 0 || channel > 16:
175+
return errInvalidMIDIChannel
176+
case bend > 0x3FFF:
177+
return errInvalidMIDIPitchBend
178+
}
179+
180+
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
181+
_, err := m.Write(m.msg[:])
182+
return err
183+
}
184+
185+
// SysEx sends a System Exclusive message.
186+
// The cable parameter is the cable number 0-15.
187+
// The data parameter is a slice with the data to send.
188+
// It needs to start with the manufacturer ID, which is either
189+
// 1 or 3 bytes in length.
190+
// The data slice should not include the SysEx start (0xF0) or
191+
// end (0xF7) bytes, only the data in between.
192+
func (m *midi) SysEx(cable uint8, data []byte) error {
193+
switch {
194+
case cable > 15:
195+
return errInvalidMIDICable
196+
case len(data) < 3:
197+
return errInvalidMIDISysExData
198+
}
199+
200+
// write start
201+
m.msg[0], m.msg[1] = (cable&0xf<<4)|CINSysExStart, MsgSysExStart
202+
m.msg[2], m.msg[3] = data[0], data[1]
203+
if _, err := m.Write(m.msg[:]); err != nil {
204+
return err
205+
}
206+
207+
// write middle
208+
i := 2
209+
for ; i < len(data)-2; i += 3 {
210+
m.msg[0], m.msg[1] = (cable&0xf<<4)|CINSysExStart, data[i]
211+
m.msg[2], m.msg[3] = data[i+1], data[i+2]
212+
if _, err := m.Write(m.msg[:]); err != nil {
213+
return err
214+
}
215+
}
216+
// write end
217+
switch len(data) - i {
218+
case 2:
219+
m.msg[0], m.msg[1] = (cable&0xf<<4)|CINSysExEnd3, data[i]
220+
m.msg[2], m.msg[3] = data[i+1], MsgSysExEnd
221+
case 1:
222+
m.msg[0], m.msg[1] = (cable&0xf<<4)|CINSysExEnd2, data[i]
223+
m.msg[2], m.msg[3] = MsgSysExEnd, 0
224+
case 0:
225+
m.msg[0], m.msg[1] = (cable&0xf<<4)|CINSysExEnd1, MsgSysExEnd
226+
m.msg[2], m.msg[3] = 0, 0
227+
}
228+
if _, err := m.Write(m.msg[:]); err != nil {
229+
return err
230+
}
231+
232+
return nil
19233
}

src/machine/usb/adc/midi/midi.go

Lines changed: 27 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,7 @@ type midi struct {
1717
msg [4]byte
1818
buf *RingBuffer
1919
rxHandler func([]byte)
20+
txHandler func()
2021
waitTxc bool
2122
}
2223

@@ -53,24 +54,40 @@ func newMidi() *midi {
5354
Index: usb.MIDI_ENDPOINT_IN,
5455
IsIn: true,
5556
Type: usb.ENDPOINT_TYPE_BULK,
56-
TxHandler: m.Handler,
57+
TxHandler: m.TxHandler,
5758
},
5859
},
5960
[]usb.SetupConfig{},
6061
)
6162
return m
6263
}
6364

65+
// SetHandler is now deprecated, please use SetRxHandler().
6466
func (m *midi) SetHandler(rxHandler func([]byte)) {
67+
m.SetRxHandler(rxHandler)
68+
}
69+
70+
// SetRxHandler sets the handler function for incoming MIDI messages.
71+
func (m *midi) SetRxHandler(rxHandler func([]byte)) {
6572
m.rxHandler = rxHandler
6673
}
6774

75+
// SetTxHandler sets the handler function for outgoing MIDI messages.
76+
func (m *midi) SetTxHandler(txHandler func()) {
77+
m.txHandler = txHandler
78+
}
79+
6880
func (m *midi) Write(b []byte) (n int, err error) {
69-
i := 0
70-
for i = 0; i < len(b); i += 4 {
71-
m.tx(b[i : i+4])
81+
s, e := 0, 0
82+
for s = 0; s < len(b); s += 4 {
83+
e = s + 4
84+
if e > len(b) {
85+
e = len(b)
86+
}
87+
88+
m.tx(b[s:e])
7289
}
73-
return i, nil
90+
return e, nil
7491
}
7592

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

8198
// from BulkIn
82-
func (m *midi) Handler() {
99+
func (m *midi) TxHandler() {
100+
if m.txHandler != nil {
101+
m.txHandler()
102+
}
103+
83104
m.waitTxc = false
84105
if b, ok := m.buf.Get(); ok {
85106
m.waitTxc = true

0 commit comments

Comments
 (0)