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 | ||||
| 		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 | ||||
| 	} | ||||
| 
 | ||||
| // 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[:]) | ||||
| 	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 | ||||
| } | ||||
| 
 | ||||
| // 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[:]) | ||||
| // 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 | ||||
| } | ||||
|  |  | |||
|  | @ -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 | ||||
| } | ||||
| 
 | ||||
| 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]) | ||||
| // SetTxHandler sets the handler function for outgoing MIDI messages. | ||||
| func (m *midi) SetTxHandler(txHandler func()) { | ||||
| 	m.txHandler = txHandler | ||||
| } | ||||
| 	return i, nil | ||||
| 
 | ||||
| func (m *midi) Write(b []byte) (n int, err error) { | ||||
| 	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 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 | ||||
|  |  | |||
		Загрузка…
	
	Создание таблицы
		
		Сослаться в новой задаче
	
	 deadprogram
						deadprogram