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. | // Try it easily by opening the following site in Chrome. | ||||||
| // https://www.onlinemusictools.com/kb/ | // https://www.onlinemusictools.com/kb/ | ||||||
| 
 | 
 | ||||||
|  | const ( | ||||||
|  | 	cable    = 0 | ||||||
|  | 	channel  = 1 | ||||||
|  | 	velocity = 0x40 | ||||||
|  | ) | ||||||
|  | 
 | ||||||
| func main() { | func main() { | ||||||
| 	led := machine.LED | 	led := machine.LED | ||||||
| 	led.Configure(machine.PinConfig{Mode: machine.PinOutput}) | 	led.Configure(machine.PinConfig{Mode: machine.PinOutput}) | ||||||
| 
 | 
 | ||||||
| 	button := machine.BUTTON | 	button := machine.BUTTON | ||||||
| 	button.Configure(machine.PinConfig{Mode: machine.PinInputPulldown}) | 	button.Configure(machine.PinConfig{Mode: machine.PinInputPullup}) | ||||||
| 
 | 
 | ||||||
| 	m := midi.Port() | 	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()) | 		led.Set(!led.Get()) | ||||||
| 		m.Write(b) |  | ||||||
| 	}) | 	}) | ||||||
| 
 | 
 | ||||||
| 	prev := true | 	prev := true | ||||||
| 	chords := []struct { | 	chords := []struct { | ||||||
| 		name string | 		name  string | ||||||
| 		keys []midi.Note | 		notes []midi.Note | ||||||
| 	}{ | 	}{ | ||||||
| 		{name: "C ", keys: []midi.Note{midi.C4, midi.E4, midi.G4}}, | 		{name: "C ", notes: []midi.Note{midi.C4, midi.E4, midi.G4}}, | ||||||
| 		{name: "G ", keys: []midi.Note{midi.G3, midi.B3, midi.D4}}, | 		{name: "G ", notes: []midi.Note{midi.G3, midi.B3, midi.D4}}, | ||||||
| 		{name: "Am", keys: []midi.Note{midi.A3, midi.C4, midi.E4}}, | 		{name: "Am", notes: []midi.Note{midi.A3, midi.C4, midi.E4}}, | ||||||
| 		{name: "F ", keys: []midi.Note{midi.F3, midi.A3, midi.C4}}, | 		{name: "F ", notes: []midi.Note{midi.F3, midi.A3, midi.C4}}, | ||||||
| 	} | 	} | ||||||
| 	index := 0 | 	index := 0 | ||||||
| 
 | 
 | ||||||
| 	for { | 	for { | ||||||
| 		current := button.Get() | 		current := button.Get() | ||||||
| 		if prev != current { | 		if prev != current { | ||||||
| 			led.Set(current) |  | ||||||
| 			if current { | 			if current { | ||||||
| 				for _, c := range chords[index].keys { | 				for _, note := range chords[index].notes { | ||||||
| 					m.NoteOff(0, 0, c, 0x40) | 					m.NoteOff(cable, channel, note, velocity) | ||||||
| 				} | 				} | ||||||
| 				index = (index + 1) % len(chords) | 				index = (index + 1) % len(chords) | ||||||
| 			} else { | 			} else { | ||||||
| 				for _, c := range chords[index].keys { | 				for _, note := range chords[index].notes { | ||||||
| 					m.NoteOn(0, 0, c, 0x40) | 					m.NoteOn(cable, channel, note, velocity) | ||||||
| 				} | 				} | ||||||
| 			} | 			} | ||||||
| 			prev = current | 			prev = current | ||||||
| 		} | 		} | ||||||
| 		time.Sleep(10 * time.Millisecond) | 		time.Sleep(100 * time.Millisecond) | ||||||
| 	} | 	} | ||||||
| } | } | ||||||
|  |  | ||||||
|  | @ -1,19 +1,233 @@ | ||||||
| package midi | package midi | ||||||
| 
 | 
 | ||||||
| // NoteOn sends a note on message. | import ( | ||||||
| func (m *midi) NoteOn(cable, channel uint8, note Note, velocity uint8) { | 	"errors" | ||||||
| 	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[:]) | 
 | ||||||
|  | // 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. | // NoteOff sends a channel note off message. | ||||||
| func (m *midi) NoteOff(cable, channel uint8, note Note, velocity uint8) { | // The cable parameter is the cable number 0-15. | ||||||
| 	m.msg[0], m.msg[1], m.msg[2], m.msg[3] = (cable&0xf<<4)|0x8, 0x80|(channel&0xf), byte(note)&0x7f, velocity&0x7f | // The channel parameter is the MIDI channel number 1-16. | ||||||
| 	m.Write(m.msg[:]) | 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. | // ControlChange sends a channel continuous controller message. | ||||||
| func (m *midi) SendCC(cable, channel, control, value uint8) { | // The cable parameter is the cable number 0-15. | ||||||
| 	m.msg[0], m.msg[1], m.msg[2], m.msg[3] = (cable&0xf<<4)|0xB, 0xB0|(channel&0xf), control&0x7f, value&0x7f | // The channel parameter is the MIDI channel number 1-16. | ||||||
| 	m.Write(m.msg[:]) | // 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 | 	msg       [4]byte | ||||||
| 	buf       *RingBuffer | 	buf       *RingBuffer | ||||||
| 	rxHandler func([]byte) | 	rxHandler func([]byte) | ||||||
|  | 	txHandler func() | ||||||
| 	waitTxc   bool | 	waitTxc   bool | ||||||
| } | } | ||||||
| 
 | 
 | ||||||
|  | @ -53,7 +54,7 @@ func newMidi() *midi { | ||||||
| 				Index:     usb.MIDI_ENDPOINT_IN, | 				Index:     usb.MIDI_ENDPOINT_IN, | ||||||
| 				IsIn:      true, | 				IsIn:      true, | ||||||
| 				Type:      usb.ENDPOINT_TYPE_BULK, | 				Type:      usb.ENDPOINT_TYPE_BULK, | ||||||
| 				TxHandler: m.Handler, | 				TxHandler: m.TxHandler, | ||||||
| 			}, | 			}, | ||||||
| 		}, | 		}, | ||||||
| 		[]usb.SetupConfig{}, | 		[]usb.SetupConfig{}, | ||||||
|  | @ -61,16 +62,32 @@ func newMidi() *midi { | ||||||
| 	return m | 	return m | ||||||
| } | } | ||||||
| 
 | 
 | ||||||
|  | // SetHandler is now deprecated, please use SetRxHandler(). | ||||||
| func (m *midi) SetHandler(rxHandler func([]byte)) { | 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 | 	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) { | func (m *midi) Write(b []byte) (n int, err error) { | ||||||
| 	i := 0 | 	s, e := 0, 0 | ||||||
| 	for i = 0; i < len(b); i += 4 { | 	for s = 0; s < len(b); s += 4 { | ||||||
| 		m.tx(b[i : i+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. | // sendUSBPacket sends a MIDIPacket. | ||||||
|  | @ -79,7 +96,11 @@ func (m *midi) sendUSBPacket(b []byte) { | ||||||
| } | } | ||||||
| 
 | 
 | ||||||
| // from BulkIn | // from BulkIn | ||||||
| func (m *midi) Handler() { | func (m *midi) TxHandler() { | ||||||
|  | 	if m.txHandler != nil { | ||||||
|  | 		m.txHandler() | ||||||
|  | 	} | ||||||
|  | 
 | ||||||
| 	m.waitTxc = false | 	m.waitTxc = false | ||||||
| 	if b, ok := m.buf.Get(); ok { | 	if b, ok := m.buf.Get(); ok { | ||||||
| 		m.waitTxc = true | 		m.waitTxc = true | ||||||
|  |  | ||||||
		Загрузка…
	
	Создание таблицы
		
		Сослаться в новой задаче
	
	 deadprogram
						deadprogram