
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>
233 строки
7,1 КиБ
Go
233 строки
7,1 КиБ
Go
package midi
|
|
|
|
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 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
|
|
}
|
|
|
|
// 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
|
|
}
|