From 0a5ab6af037fe695aeaed29c67aa4dc8ac5a80f5 Mon Sep 17 00:00:00 2001 From: Brian Stark Date: Tue, 12 Apr 2022 10:23:25 -0600 Subject: [PATCH 1/6] Fix port close defer in Find[In|Out]Port. --- v2/port.go | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/v2/port.go b/v2/port.go index 2ace81b..124d2d1 100644 --- a/v2/port.go +++ b/v2/port.go @@ -8,10 +8,10 @@ import ( // It returns -1, if the port can't be found. func FindInPort(name string) int { in, err := drivers.InByName(name) - defer in.Close() if err != nil { return -1 } + defer in.Close() return in.Number() } @@ -28,10 +28,10 @@ func CloseInPort(num int) error { // It returns -1, if the port can't be found. func FindOutPort(name string) int { out, err := drivers.OutByName(name) - defer out.Close() if err != nil { return -1 } + defer out.Close() return out.Number() } -- GitLab From 5b37b9de4eef115c2370476b83335ff5556db9c7 Mon Sep 17 00:00:00 2001 From: Brian Stark Date: Tue, 12 Apr 2022 16:30:54 -0600 Subject: [PATCH 2/6] initial switch to using typed messages --- v2/channel.go | 273 +++++++++++++++------- v2/channel_test.go | 85 +------ v2/error.go | 47 ++++ v2/example/logger/main.go | 16 +- v2/example/simple/main.go | 4 +- v2/example_test.go | 44 ++-- v2/helpers.go | 28 +-- v2/internal/utils/utils.go | 4 - v2/io.go | 2 +- v2/listen.go | 130 ++--------- v2/message.go | 329 +++++++-------------------- v2/pitchbend_test.go | 18 +- v2/smf/example_test.go | 13 +- v2/smf/message.go | 69 +----- v2/smf/reader.go | 2 +- v2/smf/reader_test.go | 18 +- v2/smf/writer_test.go | 22 +- v2/syscommon.go | 68 +++++- v2/syscommon_test.go | 8 +- v2/sysex.go | 28 ++- v2/tools/midicat/cmd/midicat/main.go | 2 +- v2/tools/midispy/cmd/midispy/main.go | 2 +- v2/tools/midispy/midispy.go | 4 +- v2/tools/smflyrics/main.go | 6 +- v2/type.go | 12 +- 25 files changed, 528 insertions(+), 706 deletions(-) create mode 100644 v2/error.go diff --git a/v2/channel.go b/v2/channel.go index 98a2a3f..741d0e4 100644 --- a/v2/channel.go +++ b/v2/channel.go @@ -2,6 +2,7 @@ package midi import ( "encoding/binary" + "fmt" "gitlab.com/gomidi/midi/v2/internal/utils" ) @@ -9,130 +10,236 @@ import ( const ( // PitchReset is the pitch bend value to reset the pitch wheel to zero PitchReset = 0 - // PitchLowest is the lowest possible value of the pitch bending - PitchLowest = -8192 - + PitchMinimum = -8192 // PitchHighest is the highest possible value of the pitch bending - PitchHighest = 8191 + PitchMaximum = 8191 ) -// Pitchbend returns a pitch bend message. -// If value is > 8191 (max), it will be set to 8191. If value is < -8192, it will be set to -8192. -// A value of 0 is considered as neutral position. -func Pitchbend(channel uint8, value int16) Message { - if channel > 15 { - channel = 15 +func legalizePitch(value int16) int16 { + switch { + case value < PitchMinimum: + return PitchMinimum + case value > PitchMaximum: + return PitchMaximum + default: + return value } +} - if value > PitchHighest { - value = PitchHighest +func legalizeUint8(value uint8) uint8 { + if value > 127 { + return 127 } + return value +} - if value < PitchLowest { - value = PitchLowest - } +// Pitchbend returns a pitch bend message. +// If value is > 8191 (max), it will be set to 8191. If value is < -8192, it will be set to -8192. +// A value of 0 is considered as neutral position. +func Pitchbend(channel uint8, value int16) Message { + assertChannel(channel) + value = legalizePitch(value) r := utils.MsbLsbSigned(value) var b = make([]byte, 2) binary.BigEndian.PutUint16(b, r) - return channelMessage2(channel, 14, b[0], b[1]) + return channelMessages(channel, 14, b[0], b[1]) } // PolyAfterTouch returns a polyphonic aftertouch message. func PolyAfterTouch(channel, key, pressure uint8) Message { - if channel > 15 { - channel = 15 - } + assertChannel(channel) + key = legalizeUint8(key) + pressure = legalizeUint8(pressure) - if key > 127 { - key = 127 - } - if pressure > 127 { - pressure = 127 - } - return channelMessage2(channel, 10, key, pressure) + return channelMessages(channel, 10, key, pressure) } // NoteOn returns a note on message. func NoteOn(channel, key, velocity uint8) Message { - if channel > 15 { - channel = 15 - } + assertChannel(channel) + key = legalizeUint8(key) + velocity = legalizeUint8(velocity) - if key > 127 { - key = 127 - } - if velocity > 127 { - velocity = 127 - } - return channelMessage2(channel, 9, key, velocity) + return channelMessages(channel, 9, key, velocity) } // NoteOffVelocity returns a note off message with velocity. -func NoteOffVelocity(channel, key, velocity uint8) Message { - if channel > 15 { - channel = 15 - } +func NoteOff(channel, key, velocity uint8) Message { + assertChannel(channel) + key = legalizeUint8(key) + velocity = legalizeUint8(velocity) - if key > 127 { - key = 127 - } - if velocity > 127 { - velocity = 127 - } - return channelMessage2(channel, 8, key, velocity) -} - -// NoteOff returns a note off message. -func NoteOff(channel, key uint8) Message { - if channel > 15 { - channel = 15 - } - - if key > 127 { - key = 127 - } - return channelMessage2(channel, 8, key, 0) + return channelMessages(channel, 8, key, velocity) } // ProgramChange returns a program change message. func ProgramChange(channel, program uint8) Message { - if channel > 15 { - channel = 15 - } + assertChannel(channel) + program = legalizeUint8(program) - if program > 127 { - program = 127 - } - return channelMessage1(channel, 12, program) + return channelMessages(channel, 12, program) } // AfterTouch returns an aftertouch message. func AfterTouch(channel, pressure uint8) Message { - if channel > 15 { - channel = 15 - } + assertChannel(channel) + pressure = legalizeUint8(pressure) - if pressure > 127 { - pressure = 127 - } - return channelMessage1(channel, 13, pressure) + return channelMessages(channel, 13, pressure) } // ControlChange returns a control change message. func ControlChange(channel, controller, value uint8) Message { - if channel > 15 { - channel = 15 - } + assertChannel(channel) + controller = legalizeUint8(controller) + value = legalizeUint8(value) - if controller > 127 { - controller = 127 - } - if value > 127 { - value = 127 - } - return channelMessage2(channel, 11, controller, value) + return channelMessages(channel, 11, controller, value) +} + +func statusChannelOnly(_ uint8, c uint8) uint8 { + return c +} + +type AfterTouchMessage Message + +func (m AfterTouchMessage) Channel() uint8 { + assertMessageLength(Message(m), 2) + return statusChannelOnly(utils.ParseStatus(m[0])) +} + +func (m AfterTouchMessage) Pressure() uint8 { + assertMessageLength(Message(m), 2) + return utils.ParseUint7(m[1]) +} + +func (m AfterTouchMessage) String() string { + return fmt.Sprintf("%s channel: %d pressure: %d", AfterTouchMsg, m.Channel(), m.Pressure()) +} + +type ProgramChangeMessage Message + +func (m ProgramChangeMessage) Channel() uint8 { + assertMessageLength(Message(m), 2) + return statusChannelOnly(utils.ParseStatus(m[0])) +} + +func (m ProgramChangeMessage) Program() uint8 { + assertMessageLength(Message(m), 2) + return utils.ParseUint7(m[1]) +} + +func (m ProgramChangeMessage) String() string { + return fmt.Sprintf("%s channel: %d program: %d", ProgramChangeMsg, m.Channel(), m.Program()) +} + +type ControlChangeMessage Message + +func (m ControlChangeMessage) Channel() uint8 { + assertMessageLength(Message(m), 3) + return statusChannelOnly(utils.ParseStatus(m[0])) +} + +func (m ControlChangeMessage) Controller() uint8 { + assertMessageLength(Message(m), 3) + return utils.ParseUint7(m[1]) +} + +func (m ControlChangeMessage) Value() uint8 { + assertMessageLength(Message(m), 3) + return utils.ParseUint7(m[2]) +} + +func (m ControlChangeMessage) String() string { + return fmt.Sprintf("%s channel: %d controller: %d value: %d", ControlChangeMsg, m.Channel(), m.Controller(), m.Value()) +} + +type NoteOnMessage Message + +func (m NoteOnMessage) Channel() uint8 { + assertMessageLength(Message(m), 3) + return statusChannelOnly(utils.ParseStatus(m[0])) +} + +func (m NoteOnMessage) Key() uint8 { + assertMessageLength(Message(m), 3) + return utils.ParseUint7(m[1]) +} + +func (m NoteOnMessage) Velocity() uint8 { + assertMessageLength(Message(m), 3) + return utils.ParseUint7(m[2]) +} + +func (m NoteOnMessage) String() string { + return fmt.Sprintf("%s channel: %d key: %d velocity: %d", NoteOnMsg, m.Channel(), m.Key(), m.Velocity()) +} + +type NoteOffMessage Message + +func (m NoteOffMessage) Channel() uint8 { + assertMessageLength(Message(m), 3) + return statusChannelOnly(utils.ParseStatus(m[0])) +} + +func (m NoteOffMessage) Key() uint8 { + assertMessageLength(Message(m), 3) + return utils.ParseUint7(m[1]) +} + +func (m NoteOffMessage) Velocity() uint8 { + assertMessageLength(Message(m), 3) + return utils.ParseUint7(m[2]) +} + +func (m NoteOffMessage) String() string { + return fmt.Sprintf("%s channel: %d key: %d velocity: %d", NoteOffMsg, m.Channel(), m.Key(), m.Velocity()) +} + +type PolyAfterTouchMessage Message + +func (m PolyAfterTouchMessage) Channel() uint8 { + assertMessageLength(Message(m), 3) + return statusChannelOnly(utils.ParseStatus(m[0])) +} + +func (m PolyAfterTouchMessage) Key() uint8 { + assertMessageLength(Message(m), 3) + return utils.ParseUint7(m[1]) +} + +func (m PolyAfterTouchMessage) Pressure() uint8 { + assertMessageLength(Message(m), 3) + return utils.ParseUint7(m[2]) +} + +func (m PolyAfterTouchMessage) String() string { + return fmt.Sprintf("%s channel: %d key: %d pressure: %d", PolyAfterTouchMsg, m.Channel(), m.Key(), m.Pressure()) +} + +type PitchBendMessage Message + +func (m PitchBendMessage) Channel() uint8 { + assertMessageLength(Message(m), 3) + return statusChannelOnly(utils.ParseStatus(m[0])) +} + +func (m PitchBendMessage) Pitch() int16 { + assertMessageLength(Message(m), 3) + rel, _ := utils.ParsePitchWheelVals(m[1], m[2]) + return rel +} + +func (m PitchBendMessage) Absolute() uint16 { + assertMessageLength(Message(m), 3) + _, abs := utils.ParsePitchWheelVals(m[1], m[2]) + return abs +} + +func (m PitchBendMessage) String() string { + return fmt.Sprintf("%s channel: %d pitch: %d (%d)", PitchBendMsg, m.Channel(), m.Pitch(), m.Absolute()) } diff --git a/v2/channel_test.go b/v2/channel_test.go index 3378057..1a2a873 100644 --- a/v2/channel_test.go +++ b/v2/channel_test.go @@ -18,7 +18,6 @@ func TestChannelString(t *testing.T) { }, { ControlChange(8, 7, 110), - //"ControlChange channel: 8 controller: 7 (\"Volume (MSB)\") value 110", "ControlChange channel: 8 controller: 7 value: 110", }, { @@ -26,11 +25,11 @@ func TestChannelString(t *testing.T) { "NoteOn channel: 2 key: 100 velocity: 80", }, { - NoteOff(3, 80), - "NoteOff channel: 3 key: 80", + NoteOff(3, 80, 0), + "NoteOff channel: 3 key: 80 velocity: 0", }, { - NoteOffVelocity(4, 80, 20), + NoteOff(4, 80, 20), "NoteOff channel: 4 key: 80 velocity: 20", }, { @@ -53,7 +52,6 @@ func TestChannelString(t *testing.T) { }, { ControlChange(8, 137, 130), - //"ControlChange channel: 8 controller: 127 (\"Poly Operation\") value 127", "ControlChange channel: 8 controller: 127 value: 127", }, { @@ -61,11 +59,11 @@ func TestChannelString(t *testing.T) { "NoteOn channel: 2 key: 127 velocity: 127", }, { - NoteOff(3, 180), - "NoteOff channel: 3 key: 127", + NoteOff(3, 180, 0), + "NoteOff channel: 3 key: 127 velocity: 0", }, { - NoteOffVelocity(4, 180, 220), + NoteOff(4, 180, 220), "NoteOff channel: 4 key: 127 velocity: 127", }, { @@ -114,11 +112,11 @@ func TestChannelRaw(t *testing.T) { "92 64 50", }, { // 3 - NoteOff(3, 80), + NoteOff(3, 80, 0), "83 50 00", }, { - NoteOffVelocity(4, 80, 20), + NoteOff(4, 80, 20), "84 50 14", }, { @@ -145,71 +143,4 @@ func TestChannelRaw(t *testing.T) { t.Errorf("[%v] got: %#v; wanted %#v", i, got, want) } } - -} - -/* -func TestSetChannel(t *testing.T) { - - tests := []struct { - input Message - toChannel uint8 - expected string - }{ - { - Channel1.Aftertouch(120), - 5, - "channel.Aftertouch channel 5 pressure 120", - }, - { - Channel8.ControlChange(7, 110), - 9, - "channel.ControlChange channel 9 controller 7 (\"Volume (MSB)\") value 110", - }, - { - Channel2.NoteOn(100, 80), - 0, - "channel.NoteOn channel 0 key 100 velocity 80", - }, - { - Channel3.NoteOff(80), - 2, - "channel.NoteOff channel 2 key 80", - }, - { - Channel4.NoteOffVelocity(80, 20), - 11, - "channel.NoteOffVelocity channel 11 key 80 velocity 20", - }, - { - Channel4.Pitchbend(300), - 14, - "channel.Pitchbend channel 14 value 300 absValue 0", - }, - { - Channel4.PolyAftertouch(86, 109), - 2, - "channel.PolyAftertouch channel 2 key 86 pressure 109", - }, - { - Channel4.ProgramChange(83), - 0, - "channel.ProgramChange channel 0 program 83", - }, - } - - for _, test := range tests { - - var bf bytes.Buffer - - msg := SetChannel(test.input, test.toChannel) - - bf.WriteString(msg.String()) - - if got, want := bf.String(), test.expected; got != want { - t.Errorf("got: %#v; wanted %#v", got, want) - } - } - } -*/ diff --git a/v2/error.go b/v2/error.go new file mode 100644 index 0000000..72bbdef --- /dev/null +++ b/v2/error.go @@ -0,0 +1,47 @@ +package midi + +import ( + "fmt" +) + +type Error string + +func (err Error) Error() string { + return string(err) +} + +const ( + ErrMessageLength Error = "message length not expected" + ErrChannelInvalid Error = "channel not in range 0-15" + ErrSysExHeaderInvalid Error = "sysex message header not expected" + ErrSysExTrailerInvalid Error = "sysex message trailer not expected" + ErrSysExCallback Error = "driver error: received 0xF0 in non sysex callback" +) + +func assertMessageLength(msg Message, expLen int) { + if l := len(msg); l != expLen { + panic(fmt.Errorf("%w: %d != %d", ErrMessageLength, l, expLen)) + } +} + +func assertMessageLengthMinimum(msg Message, minLen int) { + if l := len(msg); l < minLen { + panic(fmt.Errorf("%w: %d < %d", ErrMessageLength, l, minLen)) + } +} + +func assertChannel(channel uint8) { + if channel > 15 { + panic(fmt.Errorf("%w: %d > 15", ErrChannelInvalid, channel)) + } +} + +func assertSysExFormat(msg Message) { + assertMessageLengthMinimum(msg, 3) + if v := msg[0]; v != SysExHeader { + panic(fmt.Errorf("%w: 0x%02x != 0x%02x", ErrSysExHeaderInvalid, v, SysExHeader)) + } + if v := msg[len(msg)-1]; v != SysExTrailer { + panic(fmt.Errorf("%w: 0x%02x != 0x%02x", ErrSysExTrailerInvalid, v, SysExTrailer)) + } +} diff --git a/v2/example/logger/main.go b/v2/example/logger/main.go index 79c1030..55ae021 100644 --- a/v2/example/logger/main.go +++ b/v2/example/logger/main.go @@ -18,15 +18,13 @@ func main() { } stop, err := midi.ListenTo(in, func(msg midi.Message, timestampms int32) { - var bt []byte - var ch, key, vel uint8 - switch { - case msg.GetSysEx(&bt): - fmt.Printf("got sysex: % X\n", bt) - case msg.GetNoteStart(&ch, &key, &vel): - fmt.Printf("starting note %s on channel %v with velocity %v\n", midi.Note(key), ch, vel) - case msg.GetNoteEnd(&ch, &key): - fmt.Printf("ending note %s on channel %v\n", midi.Note(key), ch) + switch m := msg.Parse().(type) { + case midi.SysExMessage: + fmt.Printf("got sysex: % X\n", m.Data()) + case midi.NoteOnMessage: + fmt.Printf("starting note %s on channel %d with velocity %d\n", midi.Note(m.Key()), m.Channel(), m.Velocity()) + case midi.NoteOffMessage: + fmt.Printf("ending note %s on channel %v\n", midi.Note(m.Key()), m.Channel()) default: // ignore } diff --git a/v2/example/simple/main.go b/v2/example/simple/main.go index f40bc33..c1c6351 100644 --- a/v2/example/simple/main.go +++ b/v2/example/simple/main.go @@ -49,8 +49,8 @@ func mkSMF() []byte { tr.Add(0, midi.NoteOn(0, midi.Ab(3), 120)) tr.Add(clock.Ticks8th(), midi.NoteOn(0, midi.C(4), 120)) // duration: a quarter note (96 ticks in our case) - tr.Add(clock.Ticks4th()*2, midi.NoteOff(0, midi.Ab(3))) - tr.Add(0, midi.NoteOff(0, midi.C(4))) + tr.Add(clock.Ticks4th()*2, midi.NoteOff(0, midi.Ab(3), 0)) + tr.Add(0, midi.NoteOff(0, midi.C(4), 0)) tr.Close(0) // create the SMF and add the tracks diff --git a/v2/example_test.go b/v2/example_test.go index 084f6e0..096a9cf 100644 --- a/v2/example_test.go +++ b/v2/example_test.go @@ -20,22 +20,17 @@ func Example() { // ignore realtime messages return } - var channel, key, velocity, cc, val uint8 - switch { - - // is better, than to use GetNoteOn (handles note on messages with velocity of 0 as expected) - case msg.GetNoteStart(&channel, &key, &velocity): - fmt.Printf("note started at %vms channel: %v key: %v velocity: %v\n", timestampms, channel, key, velocity) - - // is better, than to use GetNoteOff (handles note on messages with velocity of 0 as expected) - case msg.GetNoteEnd(&channel, &key): - fmt.Printf("note ended at %vms channel: %v key: %v\n", timestampms, channel, key) - - case msg.GetControlChange(&channel, &cc, &val): - fmt.Printf("control change %v %q channel: %v value: %v at %vms\n", cc, ControlChangeName[cc], channel, val, timestampms) + switch m := msg.Parse().(type) { + case NoteOnMessage: + fmt.Printf("note started at %vms channel: %v key: %v velocity: %v\n", timestampms, m.Channel(), m.Key(), m.Velocity()) + case NoteOffMessage: + fmt.Printf("note ended at %vms channel: %v key: %v\n", timestampms, m.Channel(), m.Key()) + case ControlChangeMessage: + cc := m.Controller() + fmt.Printf("control change %v %q channel: %v value: %v at %vms\n", cc, ControlChangeName[cc], m.Channel(), m.Value(), timestampms) default: - fmt.Printf("received %s at %vms\n", msg, timestampms) + fmt.Printf("received %s at %vms\n", m, timestampms) } } @@ -71,16 +66,16 @@ func Example() { // any running status bytes are converted and only complete messages are passed to the eachMessage. stop, _ := ListenTo(in, eachMessage) - { // send some messages - send(NoteOn(0, Db(4), 100)) - time.Sleep(time.Millisecond * 30) - send(NoteOff(0, Db(4))) - send(Pitchbend(0, -12)) - time.Sleep(time.Millisecond * 20) - send(ProgramChange(1, 12)) - send(ControlChange(2, FootPedalMSB, On)) - } - + // send some messages + send(NoteOn(0, Db(4), 100)) + <-time.After(29 * time.Millisecond) + // time.Sleep(time.Millisecond * 30) + send(NoteOff(0, Db(4), 0)) + send(Pitchbend(0, -12)) + <-time.After(19 * time.Millisecond) + // time.Sleep(time.Millisecond * 20) + send(ProgramChange(1, 12)) + send(ControlChange(2, FootPedalMSB, On)) // stops listening stop() @@ -90,5 +85,4 @@ func Example() { // received PitchBend channel: 0 pitch: -12 (8180) at 30ms // received ProgramChange channel: 1 program: 12 at 50ms // control change 4 "Foot Pedal (MSB)" channel: 2 value: 127 at 50ms - } diff --git a/v2/helpers.go b/v2/helpers.go index 61f85ef..bc4b400 100644 --- a/v2/helpers.go +++ b/v2/helpers.go @@ -6,21 +6,17 @@ import ( "gitlab.com/gomidi/midi/v2/internal/utils" ) -// channelMessage1 returns the bytes for a single byte channel message -func channelMessage1(c uint8, status, msg byte) []byte { +// channelMessage returns the bytes for a single byte channel message +func channelMessages(c uint8, status uint8, msgs ...byte) []byte { cm := &channelMessage{channel: c, status: status} - cm.data[0] = msg - bt := cm.bytes() - //return NewMessage(bt) - return bt -} - -// channelMessage2 returns the bytes for a two bytes channel message -func channelMessage2(c uint8, status, msg1 byte, msg2 byte) []byte { - cm := &channelMessage{channel: c, status: status} - cm.data[0] = msg1 - cm.data[1] = msg2 - cm.twoBytes = true + switch len(msgs) { + case 1: + cm.data[0] = msgs[0] + case 2: + cm.twoBytes = true + cm.data[0] = msgs[0] + cm.data[1] = msgs[1] + } bt := cm.bytes() //return NewMessage(bt) return bt @@ -93,7 +89,7 @@ func getMsg1(typ uint8, channel uint8, arg uint8) (m Message) { //m.MsgType = GetChannelMsgType(typ) //m.MsgType = ChannelMsg.Set(channelType[channel]) //m.Data = channelMessage1(channel, typ, arg) - m = channelMessage1(channel, typ, arg) + m = channelMessages(channel, typ, arg) /* switch typ { case byteProgramChange: @@ -110,7 +106,7 @@ func getMsg1(typ uint8, channel uint8, arg uint8) (m Message) { // getMsg2 returns a 2-byte channel message (noteon/noteoff, poly aftertouch, control change or pitchbend) func getMsg2(typ uint8, channel uint8, arg1 uint8, arg2 uint8) (msg Message) { //msg.MsgType = ChannelMsg.Set(channelType[channel]) - msg = channelMessage2(channel, typ, arg1, arg2) + msg = channelMessages(channel, typ, arg1, arg2) /* switch typ { diff --git a/v2/internal/utils/utils.go b/v2/internal/utils/utils.go index a85d5ea..fa7810f 100644 --- a/v2/internal/utils/utils.go +++ b/v2/internal/utils/utils.go @@ -222,10 +222,6 @@ func ClearBitU8(n uint8, pos uint8) uint8 { return n } -func ParseTwoUint7(b1, b2 byte) (uint8, uint8) { - return (b1 & 0x7f), (b2 & 0x7f) -} - func ParseUint7(b byte) uint8 { return b & 0x7f } diff --git a/v2/io.go b/v2/io.go index 099a55e..5fe6240 100644 --- a/v2/io.go +++ b/v2/io.go @@ -25,7 +25,7 @@ func SendTo(portno int) (func(msg Message) error, error) { } } return func(msg Message) error { - return out.Send(msg.Bytes()) + return out.Send([]byte(msg)) }, nil } diff --git a/v2/listen.go b/v2/listen.go index a48f676..c9f1ef8 100644 --- a/v2/listen.go +++ b/v2/listen.go @@ -4,87 +4,43 @@ import ( "fmt" "gitlab.com/gomidi/midi/v2/drivers" - midilib "gitlab.com/gomidi/midi/v2/internal/utils" ) -func _channelMessage(typ, channel, data1, data2 byte) Message { - switch typ { - case byteChannelPressure: - return AfterTouch(channel, data1) - case byteProgramChange: - return ProgramChange(channel, data1) - case byteControlChange: - return ControlChange(channel, data1, data2) - case byteNoteOn: - return NoteOn(channel, data1, data2) - case byteNoteOff: - return NoteOffVelocity(channel, data1, data2) - case bytePolyphonicKeyPressure: - return PolyAfterTouch(channel, data1, data2) - case bytePitchWheel: - rel, _ := midilib.ParsePitchWheelVals(data1, data2) - return Pitchbend(channel, rel) - default: - panic("unknown typ") - } -} - -// listeningOptions are the options for the listening -type listeningOptions struct { - - // TimeCode lets the timecode messages pass through, if set - TimeCode bool - - // ActiveSense lets the active sense messages pass through, if set - ActiveSense bool - - // SysEx lets the system exclusive messages pass through, if set - SysEx bool - - // SysExBufferSize defines the size of the buffer for sysex messages (in bytes). - // SysEx messages larger than this size will be ignored. - // When SysExBufferSize is 0, the default buffersize (1024) is used. - SysExBufferSize uint32 - - // OnError handles occuring errors - OnError func(error) -} - // Option is an option for listening -type Option func(*listeningOptions) +type Option func(*drivers.ListenConfig) // UseTimeCode is an option to receive time code messages func UseTimeCode() Option { - return func(l *listeningOptions) { + return func(l *drivers.ListenConfig) { l.TimeCode = true } } // UseActiveSense is an option to receive active sense messages func UseActiveSense() Option { - return func(l *listeningOptions) { + return func(l *drivers.ListenConfig) { l.ActiveSense = true } } // UseSysEx is an option to receive system exclusive messages func UseSysEx() Option { - return func(l *listeningOptions) { + return func(l *drivers.ListenConfig) { l.SysEx = true } } // SysExBufferSize is an option to set the buffer size for sysex messages func SysExBufferSize(size uint32) Option { - return func(l *listeningOptions) { + return func(l *drivers.ListenConfig) { l.SysExBufferSize = size } } // HandleError sets an error handler when receiving messages func HandleError(cb func(error)) Option { - return func(l *listeningOptions) { - l.OnError = cb + return func(l *drivers.ListenConfig) { + l.OnErr = cb } } @@ -107,73 +63,25 @@ func ListenTo(portno int, recv func(msg Message, timestampms int32), opts ...Opt } } - var opt listeningOptions - for _, o := range opts { - o(&opt) - } - var conf drivers.ListenConfig - conf.SysExBufferSize = opt.SysExBufferSize - conf.TimeCode = opt.TimeCode - conf.ActiveSense = opt.ActiveSense - conf.SysEx = opt.SysEx - conf.OnErr = opt.OnError - - var isStatusSet bool - var typ, channel byte + for _, opt := range opts { + opt(&conf) + } var onMsg = func(data []byte, millisec int32) { - status := data[0] - //var msg []byte - var msg Message switch { - // realtime message - case status >= 0xF8: - //msg = NewMessage([]byte{status}) - msg = []byte{status} - // here we clear for System Common Category messages - case status > 0xF0 && status < 0xF7: - isStatusSet = false - switch status { - case byteSysTuneRequest: - msg = Tune() - case byteMIDITimingCodeMessage: - msg = MTC(data[1]) - case byteSysSongPositionPointer: - _, abs := midilib.ParsePitchWheelVals(data[1], data[2]) - msg = SPP(abs) - case byteSysSongSelect: - msg = SongSelect(data[1]) - default: - // undefined syscommon message - // msg = NewUndefined() - } - - // [MIDI] permits 0xF7 octets that are not part of a (0xF0, 0xF7) pair - // to appear on a MIDI 1.0 DIN cable. Unpaired 0xF7 octets have no - // semantic meaning in MIDI apart from cancelling running status. - case status == 0xF7: - isStatusSet = false - case status == 0xF0: - errmsg := fmt.Sprintf("error in driver: %q receiving 0xF0 in non sysex callback", drivers.Get().String()) - if conf.OnErr != nil { - conf.OnErr(fmt.Errorf(errmsg)) - } else { - // TODO: maybe log - panic(errmsg) - } - case status >= 0x80 && status <= 0xEF: - isStatusSet = true - typ, channel = midilib.ParseStatus(status) - msg = _channelMessage(typ, channel, data[1], data[2]) - default: - // running status - if isStatusSet { - msg = _channelMessage(typ, channel, data[1], data[2]) + case len(data) == 0: + return + case data[0] == 0xF0: + err := fmt.Errorf("%w: %s", ErrSysExCallback, drivers.Get().String()) + if conf.OnErr == nil { + panic(err) } + conf.OnErr(err) + return } - recv(msg, millisec) + recv(Message(data), millisec) } return in.Listen(onMsg, conf) diff --git a/v2/message.go b/v2/message.go index 3e1e31f..287cc03 100644 --- a/v2/message.go +++ b/v2/message.go @@ -1,179 +1,59 @@ package midi import ( - "bytes" "fmt" - - "gitlab.com/gomidi/midi/v2/internal/utils" ) // Message is a complete midi message (not including meta messages) type Message []byte -// Bytes returns the underlying bytes of the message. -func (m Message) Bytes() []byte { - return []byte(m) -} - -// IsPlayable returns, if the message can be send to an instrument. -func (m Message) IsPlayable() bool { - if m.Type() <= UnknownMsg { - return false - } - - return m.Type() < firstMetaMsg -} - -// IsOneOf returns true, if the message has one of the given types. -func (m Message) IsOneOf(checkers ...Type) bool { - for _, checker := range checkers { - if m.Is(checker) { - return true - } +func (m Message) Parse() interface{} { + switch t := getType(m); t { + case ProgramChangeMsg: + return ProgramChangeMessage(m) + case AfterTouchMsg: + return AfterTouchMessage(m) + case NoteOffMsg: + return NoteOffMessage(m) + case NoteOnMsg: + return NoteOnMessage(m) + case PolyAfterTouchMsg: + return PolyAfterTouchMessage(m) + case ControlChangeMsg: + return ControlChangeMessage(m) + case PitchBendMsg: + return PitchBendMessage(m) + case MTCMsg: + return MTCMessage(m) + case SongPositionMsg: + return SongPositionMessage(m) + case SongSelectMsg: + return SongSelectMessage(m) + case TuneMsg: + return TuneMessage(m) + default: + return m } - return false } -// Type returns the type of the message. -func (m Message) Type() Type { +func (m Message) MessageType() Type { return getType(m) } -// Is returns true, if the message is of the given type. -func (m Message) Is(t Type) bool { - return m.Type().Is(t) -} - -// GetNoteOn returns true if (and only if) the message is a NoteOnMsg. -// Then it also extracts the data to the given arguments. -func (m Message) GetNoteOn(channel, key, velocity *uint8) (is bool) { - if !m.Is(NoteOnMsg) { - return false - } - - _, *channel = utils.ParseStatus(m[0]) - *key, *velocity = utils.ParseTwoUint7(m[1], m[2]) - return true -} - -// GetNoteStart returns true if (and only if) the message is a NoteOnMsg with a velocity > 0. -// Then it also extracts the data to the given arguments. -func (m Message) GetNoteStart(channel, key, velocity *uint8) (is bool) { - if !m.Is(NoteOnMsg) { - return false - } - - _, *channel = utils.ParseStatus(m[0]) - *key, *velocity = utils.ParseTwoUint7(m[1], m[2]) - if *velocity == 0 { - return false - } - return true -} - -// GetNoteOff returns true if (and only if) the message is a NoteOffMsg. -// Then it also extracts the data to the given arguments. -func (m Message) GetNoteOff(channel, key, velocity *uint8) (is bool) { - if !m.Is(NoteOffMsg) { - return false - } - - _, *channel = utils.ParseStatus(m[0]) - *key, *velocity = utils.ParseTwoUint7(m[1], m[2]) - return true -} - -// GetChannel returns true if (and only if) the message is a ChannelMsg. -// Then it also extracts the channel to the given argument. -func (m Message) GetChannel(channel *uint8) (is bool) { - if !m.Is(ChannelMsg) { - return false - } - - _, *channel = utils.ParseStatus(m[0]) - return true -} - -// GetNoteEnd returns true if (and only if) the message is a NoteOnMsg with a velocity == 0 or a NoteOffMsg. -// Then it also extracts the data to the given arguments. -func (m Message) GetNoteEnd(channel, key *uint8) (is bool) { - if !m.Is(NoteOnMsg) && !m.Is(NoteOffMsg) { - return false - } - - var velocity uint8 - - _, *channel = utils.ParseStatus(m[0]) - *key, velocity = utils.ParseTwoUint7(m[1], m[2]) - return m.Is(NoteOffMsg) || velocity == 0 -} - -// GetPolyAfterTouch returns true if (and only if) the message is a PolyAfterTouchMsg. -// Then it also extracts the data to the given arguments. -func (m Message) GetPolyAfterTouch(channel, key, pressure *uint8) (is bool) { - if !m.Is(PolyAfterTouchMsg) { - return false - } - - _, *channel = utils.ParseStatus(m[0]) - *key, *pressure = utils.ParseTwoUint7(m[1], m[2]) - return true -} - -// GetAfterTouch returns true if (and only if) the message is a AfterTouchMsg. -// Then it also extracts the data to the given arguments. -func (m Message) GetAfterTouch(channel, pressure *uint8) (is bool) { - if !m.Is(AfterTouchMsg) { - return false - } - - _, *channel = utils.ParseStatus(m[0]) - *pressure = utils.ParseUint7(m[1]) - return true -} - -// GetProgramChange returns true if (and only if) the message is a ProgramChangeMsg. -// Then it also extracts the data to the given arguments. -func (m Message) GetProgramChange(channel, program *uint8) (is bool) { - if !m.Is(ProgramChangeMsg) { - return false - } - - _, *channel = utils.ParseStatus(m[0]) - *program = utils.ParseUint7(m[1]) - return true -} - -// GetPitchBend returns true if (and only if) the message is a PitchBendMsg. -// Then it also extracts the data to the given arguments. -// Either relative or absolute may be nil, if not needed. -func (m Message) GetPitchBend(channel *uint8, relative *int16, absolute *uint16) (is bool) { - if !m.Is(PitchBendMsg) { +// IsPlayable returns, if the message can be send to an instrument. +func (m Message) IsPlayable() bool { + t := getType(m) + if t <= UnknownMsg { return false } - _, *channel = utils.ParseStatus(m[0]) - - rel, abs := utils.ParsePitchWheelVals(m[1], m[2]) - if relative != nil { - *relative = rel - } - if absolute != nil { - *absolute = abs - } - return true + return t < firstMetaMsg } -// GetControlChange returns true if (and only if) the message is a ControlChangeMsg. -// Then it also extracts the data to the given arguments. -func (m Message) GetControlChange(channel, controller, value *uint8) (is bool) { - if !m.Is(ControlChangeMsg) { - return false - } - - _, *channel = utils.ParseStatus(m[0]) - *controller, *value = utils.ParseTwoUint7(m[1], m[2]) - return true +// Is returns true, if the message is of the given type. +func (m Message) Is(t Type) bool { + ty := getType(m) + return ty.Is(t) } /* @@ -199,98 +79,65 @@ cdefg = Hours (0-23) 8 0111 0abc Frame Rate, and Hours MSB */ -// GetMTC returns true if (and only if) the message is a MTCMsg. -// Then it also extracts the data to the given arguments. -func (m Message) GetMTC(quarterframe *uint8) (is bool) { - if !m.Is(MTCMsg) { - return false - } - - *quarterframe = utils.ParseUint7(m[1]) - return true -} - -// GetSongSelect returns true if (and only if) the message is a SongSelectMsg. -// Then it also extracts the song number to the given argument. -func (m Message) GetSongSelect(song *uint8) (is bool) { - if !m.Is(SongSelectMsg) { - return false - } - - *song = utils.ParseUint7(m[1]) - return true -} - -// GetSPP returns true if (and only if) the message is a SPPMsg. -// Then it also extracts the spp to the given argument. -func (m Message) GetSPP(spp *uint16) (is bool) { - if !m.Is(SPPMsg) { - return false - } - - _, *spp = utils.ParsePitchWheelVals(m[2], m[1]) - return true -} +// // GetMTC returns true if (and only if) the message is a MTCMsg. +// // Then it also extracts the data to the given arguments. +// func (m Message) GetMTC(quarterframe *uint8) (is bool) { +// if !m.Is(MTCMsg) { +// return false +// } + +// *quarterframe = utils.ParseUint7(m[1]) +// return true +// } + +// // GetSongSelect returns true if (and only if) the message is a SongSelectMsg. +// // Then it also extracts the song number to the given argument. +// func (m Message) GetSongSelect(song *uint8) (is bool) { +// if !m.Is(SongSelectMsg) { +// return false +// } + +// *song = utils.ParseUint7(m[1]) +// return true +// } + +// // GetSPP returns true if (and only if) the message is a SPPMsg. +// // Then it also extracts the spp to the given argument. +// func (m Message) GetSPP(spp *uint16) (is bool) { +// if !m.Is(SPPMsg) { +// return false +// } + +// _, *spp = utils.ParsePitchWheelVals(m[2], m[1]) +// return true +// } // String represents the Message as a string that contains the Type and its properties. func (m Message) String() string { - var bf bytes.Buffer - fmt.Fprintf(&bf, m.Type().String()) - - var channel, val1, val2 uint8 - var pitchabs uint16 - var pitchrel int16 - var spp uint16 - var sysex []byte - - switch { - case m.GetNoteOn(&channel, &val1, &val2): - fmt.Fprintf(&bf, " channel: %v key: %v velocity: %v", channel, val1, val2) - case m.GetNoteOff(&channel, &val1, &val2): - if val2 > 0 { - fmt.Fprintf(&bf, " channel: %v key: %v velocity: %v", channel, val1, val2) - } else { - fmt.Fprintf(&bf, " channel: %v key: %v", channel, val1) - } - case m.GetPolyAfterTouch(&channel, &val1, &val2): - fmt.Fprintf(&bf, " channel: %v key: %v pressure: %v", channel, val1, val2) - case m.GetAfterTouch(&channel, &val1): - fmt.Fprintf(&bf, " channel: %v pressure: %v", channel, val1) - case m.GetControlChange(&channel, &val1, &val2): - fmt.Fprintf(&bf, " channel: %v controller: %v value: %v", channel, val1, val2) - case m.GetProgramChange(&channel, &val1): - fmt.Fprintf(&bf, " channel: %v program: %v", channel, val1) - case m.GetPitchBend(&channel, &pitchrel, &pitchabs): - fmt.Fprintf(&bf, " channel: %v pitch: %v (%v)", channel, pitchrel, pitchabs) - case m.GetMTC(&val1): - fmt.Fprintf(&bf, " mtc: %v", val1) - case m.GetSPP(&spp): - fmt.Fprintf(&bf, " spp: %v", spp) - case m.GetSongSelect(&val1): - fmt.Fprintf(&bf, " song: %v", val1) - case m.GetSysEx(&sysex): - fmt.Fprintf(&bf, " data: % X", sysex) - default: + tm := m.Parse() + if tm, ok := tm.(fmt.Stringer); ok { + return tm.String() } - return bf.String() + t := getType(m) + return fmt.Sprintf("%s %v", t, tm) } -// GetSysEx returns true, if the message is a sysex message. -// Then it extracts the inner bytes to the given slice. -func (m Message) GetSysEx(bt *[]byte) bool { - if len(m) < 3 { - return false - } +// // GetSysEx returns true, if the message is a sysex message. +// // Then it extracts the inner bytes to the given slice. +// func (m Message) GetSysEx(bt *[]byte) bool { +// if len(m) < 3 { +// return false +// } - if !m.Is(SysExMsg) { - return false - } +// if !m.Is(SysExMsg) { +// return false +// } - if m[0] == 0xF0 && m[len(m)-1] == 0xF7 { - *bt = m[1 : len(m)-1] - return true - } +// if m[0] == 0xF0 && m[len(m)-1] == 0xF7 { +// *bt = m[1 : len(m)-1] +// return true +// } - return false -} +// return false +// } diff --git a/v2/pitchbend_test.go b/v2/pitchbend_test.go index a2e4c4a..e87a6db 100644 --- a/v2/pitchbend_test.go +++ b/v2/pitchbend_test.go @@ -15,32 +15,28 @@ func TestPitchbend(t *testing.T) { expected: 8192, }, { - in: PitchHighest, + in: PitchMaximum, expected: 16383, }, { - in: PitchHighest + 1, + in: PitchMaximum + 1, expected: 16383, }, { - in: PitchLowest, + in: PitchMinimum, expected: 0, }, { - in: PitchLowest - 1, + in: PitchMinimum - 1, expected: 0, }, } for _, test := range tests { - m := Pitchbend(0, test.in) + m := Pitchbend(0, test.in).Parse().(PitchBendMessage) - var got uint16 - var ch uint8 - Message(m).GetPitchBend(&ch, nil, &got) - - if got != test.expected { - t.Errorf("Pitchbend(%v).absValue = %v; wanted %v", test.in, got, test.expected) + if abs := m.Absolute(); abs != test.expected { + t.Errorf("Pitchbend(%v).absValue = %v; wanted %v", test.in, abs, test.expected) } } } diff --git a/v2/smf/example_test.go b/v2/smf/example_test.go index 6fff24d..38a5fc5 100644 --- a/v2/smf/example_test.go +++ b/v2/smf/example_test.go @@ -30,13 +30,13 @@ func Example() { piano.Add(0, midi.ProgramChange(0, gm.Instr_HonkytonkPiano.Value())) piano.Add(0, midi.NoteOn(0, 76, 120)) // duration: a quarter note (96 ticks in our case) - piano.Add(clock.Ticks4th(), midi.NoteOff(0, 76)) + piano.Add(clock.Ticks4th(), midi.NoteOff(0, 76, 0)) piano.Close(0) bass.Add(0, smf.MetaInstrument("Bass")) bass.Add(0, midi.ProgramChange(1, gm.Instr_AcousticBass.Value())) bass.Add(clock.Ticks4th(), midi.NoteOn(1, 47, 64)) - bass.Add(clock.Ticks4th()*3, midi.NoteOff(1, 47)) + bass.Add(clock.Ticks4th()*3, midi.NoteOff(1, 47, 0)) bass.Close(0) // create the SMF and add the tracks @@ -70,7 +70,6 @@ func Example() { var absTicks uint64 var trackname string - var channel, program uint8 var gm_name string for _, ev := range track { @@ -85,10 +84,12 @@ func Example() { switch { case msg.GetMetaTrackName(&trackname): // set the trackname case msg.GetMetaInstrument(&trackname): // set the trackname based on instrument name - case msg.GetProgramChange(&channel, &program): - gm_name = "(" + gm.Instr(program).String() + ")" default: - fmt.Printf("track %v %s %s @%v %s\n", no, trackname, gm_name, absTicks, ev.Message) + if m, ok := midi.Message(msg).Parse().(midi.ProgramChangeMessage); ok { + gm_name = "(" + gm.Instr(m.Program()).String() + ")" + } else { + fmt.Printf("track %v %s %s @%v %s\n", no, trackname, gm_name, absTicks, ev.Message) + } } } } diff --git a/v2/smf/message.go b/v2/smf/message.go index 748601d..3cfe832 100644 --- a/v2/smf/message.go +++ b/v2/smf/message.go @@ -48,7 +48,7 @@ func getType(msg []byte) midi.Type { if Message(msg).IsMeta() { return getMetaType(msg[1]) } else { - return midi.Message(msg).Type() + return midi.Message(msg).MessageType() } } @@ -67,73 +67,6 @@ func (m Message) IsOneOf(checkers ...midi.Type) bool { return false } -// GetSysEx returns true, if the message is a sysex message. -// Then it extracts the inner bytes to the given slice. -func (m Message) GetSysEx(bt *[]byte) bool { - return midi.Message(m).GetSysEx(bt) -} - -// GetNoteOn returns true if (and only if) the message is a NoteOnMsg. -// Then it also extracts the data to the given arguments -func (m Message) GetNoteOn(channel, key, velocity *uint8) (is bool) { - return midi.Message(m).GetNoteOn(channel, key, velocity) -} - -// GetNoteStart returns true if (and only if) the message is a NoteOnMsg with a velocity > 0. -// Then it also extracts the data to the given arguments -func (m Message) GetNoteStart(channel, key, velocity *uint8) (is bool) { - return midi.Message(m).GetNoteStart(channel, key, velocity) -} - -// GetNoteOff returns true if (and only if) the message is a NoteOffMsg. -// Then it also extracts the data to the given arguments -func (m Message) GetNoteOff(channel, key, velocity *uint8) (is bool) { - return midi.Message(m).GetNoteOff(channel, key, velocity) -} - -// GetChannel returns true if (and only if) the message is a ChannelMsg. -// Then it also extracts the data to the given arguments -func (m Message) GetChannel(channel *uint8) (is bool) { - return midi.Message(m).GetChannel(channel) -} - -// GetNoteEnd returns true if (and only if) the message is a NoteOnMsg with a velocity == 0 or a NoteOffMsg. -// Then it also extracts the data to the given arguments -func (m Message) GetNoteEnd(channel, key *uint8) (is bool) { - return midi.Message(m).GetNoteEnd(channel, key) -} - -// GetPolyAfterTouch returns true if (and only if) the message is a PolyAfterTouchMsg. -// Then it also extracts the data to the given arguments -func (m Message) GetPolyAfterTouch(channel, key, pressure *uint8) (is bool) { - return midi.Message(m).GetPolyAfterTouch(channel, key, pressure) -} - -// GetAfterTouch returns true if (and only if) the message is a AfterTouchMsg. -// Then it also extracts the data to the given arguments -func (m Message) GetAfterTouch(channel, pressure *uint8) (is bool) { - return midi.Message(m).GetAfterTouch(channel, pressure) -} - -// GetProgramChange returns true if (and only if) the message is a ProgramChangeMsg. -// Then it also extracts the data to the given arguments -func (m Message) GetProgramChange(channel, program *uint8) (is bool) { - return midi.Message(m).GetProgramChange(channel, program) -} - -// GetPitchBend returns true if (and only if) the message is a PitchBendMsg. -// Then it also extracts the data to the given arguments -// Either relative or absolute may be nil, if not needed. -func (m Message) GetPitchBend(channel *uint8, relative *int16, absolute *uint16) (is bool) { - return midi.Message(m).GetPitchBend(channel, relative, absolute) -} - -// GetControlChange returns true if (and only if) the message is a ControlChangeMsg. -// Then it also extracts the data to the given arguments -func (m Message) GetControlChange(channel, controller, value *uint8) (is bool) { - return midi.Message(m).GetControlChange(channel, controller, value) -} - // String represents the Message as a string that contains the Type and its properties. func (m Message) String() string { diff --git a/v2/smf/reader.go b/v2/smf/reader.go index 11e7ed0..7ca7620 100644 --- a/v2/smf/reader.go +++ b/v2/smf/reader.go @@ -417,7 +417,7 @@ func (r *reader) _readEvent(canary byte) (m Message, err error) { var mim midi.Message mim, err = midi.ReadChannelMessage(status, arg1, r.input) - m = mim.Bytes() + m = []byte(mim) // since every possible status is covered by a voice message type, m can't be nil //m, err = r.channelReader.Read(status, arg1) diff --git a/v2/smf/reader_test.go b/v2/smf/reader_test.go index 38bcc3f..d07e0d1 100644 --- a/v2/smf/reader_test.go +++ b/v2/smf/reader_test.go @@ -7,6 +7,8 @@ import ( // "os" //"log" "testing" + + "gitlab.com/gomidi/midi/v2" ) func testRead(t *testing.T, input []byte) string { @@ -275,13 +277,17 @@ func TestX(t *testing.T) { tr := rd.Tracks[0] - ///fmt.Printf("%s\n", tr[0].Message) - var ch, key, vel uint8 - if len(tr) > 0 && tr[0].Message.GetNoteOn(&ch, &key, &vel) { - if key != 50 || vel != 33 || ch != 0 { - t.Errorf("got %s", tr[0].Message) + if len(tr) > 0 { + if m, ok := midi.Message(tr[0].Message).Parse().(midi.NoteOnMessage); ok { + ///fmt.Printf("%s\n", tr[0].Message) + var ch, key, vel uint8 = m.Channel(), m.Key(), m.Velocity() + if key != 50 || vel != 33 || ch != 0 { + t.Errorf("got %s", tr[0].Message) + } + } else { + t.Errorf("got no noteon message") } } else { - t.Errorf("got no noteon message") + t.Errorf("no tracks") } } diff --git a/v2/smf/writer_test.go b/v2/smf/writer_test.go index 2ea5269..7967e92 100644 --- a/v2/smf/writer_test.go +++ b/v2/smf/writer_test.go @@ -132,11 +132,11 @@ func TestWriteSMF0(t *testing.T) { tr.Add(res.Ticks4th(), midi.NoteOn(1, 67, 64)) tr.Add(res.Ticks4th(), midi.NoteOn(0, 76, 32)) - tr.Add(res.Ticks4th()*2, midi.NoteOffVelocity(2, 48, 64)) + tr.Add(res.Ticks4th()*2, midi.NoteOff(2, 48, 64)) - tr.Add(0, midi.NoteOffVelocity(2, 60, 64)) - tr.Add(0, midi.NoteOffVelocity(1, 67, 64)) - tr.Add(0, midi.NoteOffVelocity(0, 76, 64)) + tr.Add(0, midi.NoteOff(2, 60, 64)) + tr.Add(0, midi.NoteOff(1, 67, 64)) + tr.Add(0, midi.NoteOff(0, 76, 64)) tr.Close(0) @@ -307,7 +307,7 @@ func TestWriteSysEx(t *testing.T) { var tr Track tr.Add(0, midi.NoteOn(2, 65, 90)) tr.Add(10, midi.SysEx([]byte{0x90, 0x51})) - tr.Add(1, midi.NoteOff(2, 65)) + tr.Add(1, midi.NoteOff(2, 65, 0)) tr.Close(0) smf.Tracks = append(smf.Tracks, tr) @@ -333,17 +333,15 @@ func TestWriteSysEx(t *testing.T) { trrd := rd.Tracks[0] - var ch, key, velocity uint8 - var res bytes.Buffer res.WriteString("\n") for _, ev := range trrd { - switch { - case ev.Message.GetNoteOn(&ch, &key, &velocity): - fmt.Fprintf(&res, "[%v] NoteOn at channel %v: key %v velocity: %v\n", ev.Delta, ch, key, velocity) - case ev.Message.GetNoteOff(&ch, &key, &velocity): - fmt.Fprintf(&res, "[%v] NoteOff at channel %v: key %v\n", ev.Delta, ch, key) + switch m := midi.Message(ev.Message).Parse().(type) { + case midi.NoteOnMessage: + fmt.Fprintf(&res, "[%v] NoteOn at channel %v: key %v velocity: %v\n", ev.Delta, m.Channel(), m.Key(), m.Velocity()) + case midi.NoteOffMessage: + fmt.Fprintf(&res, "[%v] NoteOff at channel %v: key %v\n", ev.Delta, m.Channel(), m.Key()) default: if ev.Message.Is(midi.SysExMsg) { fmt.Fprintf(&res, "[%v] Sysex: % X\n", ev.Delta, ev.Message.Bytes()) diff --git a/v2/syscommon.go b/v2/syscommon.go index 71ce2c7..78b3d85 100644 --- a/v2/syscommon.go +++ b/v2/syscommon.go @@ -1,5 +1,11 @@ package midi +import ( + "fmt" + + "gitlab.com/gomidi/midi/v2/internal/utils" +) + /* System Common Message Status Byte Number of Data Bytes @@ -12,15 +18,15 @@ Tune Request F6 None */ const ( - byteMIDITimingCodeMessage = byte(0xF1) - byteSysSongPositionPointer = byte(0xF2) - byteSysSongSelect = byte(0xF3) - byteSysTuneRequest = byte(0xF6) + byteMIDITimingCodeMessage = byte(0xF1) + byteSysSongPosition = byte(0xF2) + byteSysSongSelect = byte(0xF3) + byteSysTuneRequest = byte(0xF6) ) var syscommMessages = map[byte]Type{ byteMIDITimingCodeMessage:/* SysCommonMsg.Set(MTCMsg), */ MTCMsg, - byteSysSongPositionPointer:/* SysCommonMsg.Set(SPPMsg), */ SPPMsg, + byteSysSongPosition:/* SysCommonMsg.Set(SPPMsg), */ SongPositionMsg, byteSysSongSelect:/* SysCommonMsg.Set(SongSelectMsg), */ SongSelectMsg, byteSysTuneRequest:/* SysCommonMsg.Set(TuneMsg), */ TuneMsg, } @@ -31,13 +37,31 @@ func Tune() Message { return []byte{byteSysTuneRequest} } +type TuneMessage Message + +func (m TuneMessage) String() string { + return fmt.Sprintf("%s", TuneMsg) +} + // SPP returns a song position pointer message -func SPP(pointer uint16) Message { - var b = make([]byte, 2) - b[1] = byte(pointer & 0x7F) - b[0] = byte((pointer >> 7) & 0x7F) - //return NewMessage([]byte{byteSysSongPositionPointer, b[0], b[1]}) - return []byte{byteSysSongPositionPointer, b[0], b[1]} +func SongPosition(pointer uint16) Message { + return []byte{ + byteSysSongPosition, + byte(pointer & 0x7F), + byte((pointer >> 7) & 0x7F), + } +} + +type SongPositionMessage Message + +func (m SongPositionMessage) Pointer() uint16 { + assertMessageLength(Message(m), 3) + _, spp := utils.ParsePitchWheelVals(m[1], m[2]) + return spp +} + +func (m SongPositionMessage) String() string { + return fmt.Sprintf("%s pointer: %d", SongPositionMsg, m.Pointer()) } // SongSelect returns a song select message @@ -47,6 +71,17 @@ func SongSelect(song uint8) Message { return []byte{byteSysSongSelect, song} } +type SongSelectMessage Message + +func (m SongSelectMessage) Song() uint16 { + assertMessageLength(Message(m), 2) + return uint16(utils.ParseUint7(m[1])) +} + +func (m SongSelectMessage) String() string { + return fmt.Sprintf("%s song: %d", SongSelectMsg, m.Song()) +} + /* MTC Quarter Frame @@ -77,3 +112,14 @@ func MTC(m uint8) Message { //return NewMessage([]byte{byteMIDITimingCodeMessage, byte(m)}) return []byte{byteMIDITimingCodeMessage, byte(m)} } + +type MTCMessage Message + +func (m MTCMessage) QuarterFrame() uint8 { + assertMessageLength(Message(m), 2) + return utils.ParseUint7(m[1]) +} + +func (m MTCMessage) String() string { + return fmt.Sprintf("%s mtc: %d", MTCMsg, m.QuarterFrame()) +} diff --git a/v2/syscommon_test.go b/v2/syscommon_test.go index 62fa3b2..dcb963d 100644 --- a/v2/syscommon_test.go +++ b/v2/syscommon_test.go @@ -25,12 +25,12 @@ func TestSysCommon(t *testing.T) { "SongSelect song: 5", }, { - midi.SPP(4), - "SPP spp: 4", + midi.SongPosition(4), + "SongPosition pointer: 4", }, { - midi.SPP(4000), - "SPP spp: 4000", + midi.SongPosition(4000), + "SongPosition pointer: 4000", }, } diff --git a/v2/sysex.go b/v2/sysex.go index 482c809..3652bd0 100644 --- a/v2/sysex.go +++ b/v2/sysex.go @@ -1,15 +1,33 @@ package midi +import "fmt" + +const ( + SysExHeader uint8 = 0xf0 + SysExTrailer uint8 = 0xf7 +) + // SysEx returns a system exclusive message. Only the inner bytes must be passed, // the bytes that represent the start and end of a sysex message are added. -func SysEx(bt []byte) (m Message) { - var b = []byte{0xF0} - b = append(b, []byte(bt)...) - m = append(b, 0xF7) - //m.Type = SysExType +func SysEx(data []byte) Message { + m := make([]byte, len(data)+2) + m[0] = SysExHeader + copy(m[1:len(m)-1], data[:]) + m[len(m)-1] = SysExTrailer return m } +type SysExMessage Message + +func (m SysExMessage) Data() []byte { + assertSysExFormat(Message(m)) + return m[1 : len(m)-1] +} + +func (m SysExMessage) String() string { + return fmt.Sprintf("%s data: % X", SysExMsg, m.Data()) +} + /* import ( "io" diff --git a/v2/tools/midicat/cmd/midicat/main.go b/v2/tools/midicat/cmd/midicat/main.go index 8b44ec7..9dfb6fa 100644 --- a/v2/tools/midicat/cmd/midicat/main.go +++ b/v2/tools/midicat/cmd/midicat/main.go @@ -174,7 +174,7 @@ func runIn() (err error) { recv := func(msg midi.Message, absmillisec int32) { //fmt.Printf("got message %s from in port %v\n", msg.String(), in) - msgChan <- timestampedMsg{absmillisec: absmillisec, msg: msg.Bytes()} + msgChan <- timestampedMsg{absmillisec: absmillisec, msg: []byte(msg)} } go func() { diff --git a/v2/tools/midispy/cmd/midispy/main.go b/v2/tools/midispy/cmd/midispy/main.go index 95610b3..d49e9cc 100644 --- a/v2/tools/midispy/cmd/midispy/main.go +++ b/v2/tools/midispy/cmd/midispy/main.go @@ -122,7 +122,7 @@ func startSpying(shouldlog bool) error { logfn = logger(in, 0) } - var recv midi.ReceiverFunc + var recv func(midi.Message, int32) if shouldlog { recv = func(m midi.Message, absmillisec int32) { diff --git a/v2/tools/midispy/midispy.go b/v2/tools/midispy/midispy.go index 0db8bda..51d7f02 100644 --- a/v2/tools/midispy/midispy.go +++ b/v2/tools/midispy/midispy.go @@ -12,7 +12,7 @@ import ( // that is >= 0, write them to the out port. // All given port must be opened. Run will not close any ports. // Stop the spying by closing the ports. -func Run(in drivers.In, out drivers.Out, recv midi.Receiver) error { +func Run(in drivers.In, out drivers.Out, recv func(midi.Message, int32)) error { if in == nil { panic("MIDI in port is nil") } @@ -21,7 +21,7 @@ func Run(in drivers.In, out drivers.Out, recv midi.Receiver) error { In: in, } - return s.SetListener(recv.Receive) + return s.SetListener(recv) } // spy connects a MIDI in port and a MIDI out port and allows diff --git a/v2/tools/smflyrics/main.go b/v2/tools/smflyrics/main.go index cb8704b..e82a223 100644 --- a/v2/tools/smflyrics/main.go +++ b/v2/tools/smflyrics/main.go @@ -87,9 +87,9 @@ type Json struct { } func (p *Json) ReadSMF() { - for _, tr := range p.mf.Tracks() { + for _, tr := range p.mf.Tracks { if p.requestedTrack < 0 || int(p.requestedTrack) == p.trackNo { - p.ReadTrack(tr) + p.ReadTrack(&tr) } p.trackNo++ } @@ -97,7 +97,7 @@ func (p *Json) ReadSMF() { func (t *Json) ReadTrack(tr *smf.Track) { var text string - for _, ev := range tr.Events { + for _, ev := range *tr { if !ev.Message.IsMeta() { continue } diff --git a/v2/type.go b/v2/type.go index 7e6a683..a7cfef4 100644 --- a/v2/type.go +++ b/v2/type.go @@ -91,10 +91,10 @@ var typeNames = map[Type]string{ reservedChannelMsg15: "reservedChannelMsg15", reservedChannelMsg16: "reservedChannelMsg16", - MTCMsg: "MTC", - SongSelectMsg: "SongSelect", - SPPMsg: "SPP", - TuneMsg: "Tune", + MTCMsg: "MTC", + SongSelectMsg: "SongSelect", + SongPositionMsg: "SongPosition", + TuneMsg: "Tune", reservedSysCommonMsg5: "reservedSysCommon5", reservedSysCommonMsg6: "reservedSysCommon6", @@ -238,9 +238,9 @@ const ( // TODO add method to Message to get the song number and document it. SongSelectMsg - // SPP is a MIDI song position pointer (SPP) system common message. + // SongPosition is a MIDI song position pointer (SPP) system common message. // TODO add method to Message to get the song position pointer and document it. - SPPMsg + SongPositionMsg // Tune is a MIDI tune request system common message. // There is no further data associated with messages of this type. -- GitLab From a8a6725b698d830ea93d9e7ddbc3f0961fa7e31f Mon Sep 17 00:00:00 2001 From: Brian Stark Date: Wed, 13 Apr 2022 00:40:57 -0600 Subject: [PATCH 3/6] update message types --- v2/channel.go | 289 ++++++++++-------- v2/channel_test.go | 6 +- v2/error.go | 15 +- v2/example/logger/main.go | 2 +- v2/example_test.go | 18 +- v2/gm/reset.go | 6 +- v2/helpers.go | 246 ++++++++------- v2/internal/runningstatus/runningstatus.go | 4 +- v2/message.go | 272 +++++++++++++---- v2/nrpn/nrpn.go | 18 +- v2/pitchbend_test.go | 2 +- v2/realtime.go | 118 ++++--- v2/realtime_test.go | 22 +- v2/rpn/rpn.go | 28 +- v2/smf/example_test.go | 4 +- v2/smf/message.go | 18 +- v2/smf/meta.go | 23 +- v2/smf/reader.go | 2 +- v2/smf/reader_test.go | 2 +- v2/smf/track.go | 8 +- v2/smf/writer_test.go | 8 +- v2/syscommon.go | 117 ++++--- v2/syscommon_test.go | 20 +- v2/sysex.go | 19 +- v2/type.go | 339 --------------------- 25 files changed, 728 insertions(+), 878 deletions(-) delete mode 100644 v2/type.go diff --git a/v2/channel.go b/v2/channel.go index 741d0e4..e9fdf23 100644 --- a/v2/channel.go +++ b/v2/channel.go @@ -7,141 +7,136 @@ import ( "gitlab.com/gomidi/midi/v2/internal/utils" ) -const ( - // PitchReset is the pitch bend value to reset the pitch wheel to zero - PitchReset = 0 - // PitchLowest is the lowest possible value of the pitch bending - PitchMinimum = -8192 - // PitchHighest is the highest possible value of the pitch bending - PitchMaximum = 8191 -) +func ChannelMessageStatus(t MessageType, c uint8) byte { + assertChannel(c) + assertMessageCategory(t, MessageCategoryChannel) + return byte(t) | byte(c) +} -func legalizePitch(value int16) int16 { - switch { - case value < PitchMinimum: - return PitchMinimum - case value > PitchMaximum: - return PitchMaximum - default: - return value - } +func ChannelMessageChannel(b byte) uint8 { + assertMessageCategory(MessageTypeFromByte(b), MessageCategoryChannel) + return b & byte(MessageCategoryChannel) } -func legalizeUint8(value uint8) uint8 { +func legalizeUint7(value uint8) uint8 { if value > 127 { return 127 } return value } -// Pitchbend returns a pitch bend message. -// If value is > 8191 (max), it will be set to 8191. If value is < -8192, it will be set to -8192. -// A value of 0 is considered as neutral position. -func Pitchbend(channel uint8, value int16) Message { - assertChannel(channel) - value = legalizePitch(value) - - r := utils.MsbLsbSigned(value) +// NoteOff returns a note off message. +func NoteOff(channel, key, velocity uint8) NoteOffMessage { + return NoteOffMessage([]byte{ + ChannelMessageStatus(MessageTypeNoteOff, channel), + legalizeUint7(key), + legalizeUint7(velocity)}) +} - var b = make([]byte, 2) +type NoteOffMessage Message - binary.BigEndian.PutUint16(b, r) - return channelMessages(channel, 14, b[0], b[1]) +func (m NoteOffMessage) Channel() uint8 { + assertMessageLength(Message(m), 3) + return ChannelMessageChannel(m[0]) } -// PolyAfterTouch returns a polyphonic aftertouch message. -func PolyAfterTouch(channel, key, pressure uint8) Message { - assertChannel(channel) - key = legalizeUint8(key) - pressure = legalizeUint8(pressure) - - return channelMessages(channel, 10, key, pressure) +func (m NoteOffMessage) Key() uint8 { + assertMessageLength(Message(m), 3) + return utils.ParseUint7(m[1]) } -// NoteOn returns a note on message. -func NoteOn(channel, key, velocity uint8) Message { - assertChannel(channel) - key = legalizeUint8(key) - velocity = legalizeUint8(velocity) +func (m NoteOffMessage) Velocity() uint8 { + assertMessageLength(Message(m), 3) + return utils.ParseUint7(m[2]) +} - return channelMessages(channel, 9, key, velocity) +func (m NoteOffMessage) Message() Message { + return Message(m) } -// NoteOffVelocity returns a note off message with velocity. -func NoteOff(channel, key, velocity uint8) Message { - assertChannel(channel) - key = legalizeUint8(key) - velocity = legalizeUint8(velocity) +func (m NoteOffMessage) String() string { + return fmt.Sprintf("%s channel: %d key: %d velocity: %d", MessageTypeNoteOff, m.Channel(), m.Key(), m.Velocity()) +} - return channelMessages(channel, 8, key, velocity) +// NoteOn returns a note on message. +func NoteOn(channel, key, velocity uint8) NoteOnMessage { + return NoteOnMessage([]byte{ + ChannelMessageStatus(MessageTypeNoteOn, channel), + legalizeUint7(key), + legalizeUint7(velocity)}) } -// ProgramChange returns a program change message. -func ProgramChange(channel, program uint8) Message { - assertChannel(channel) - program = legalizeUint8(program) +type NoteOnMessage Message - return channelMessages(channel, 12, program) +func (m NoteOnMessage) Channel() uint8 { + assertMessageLength(Message(m), 3) + return ChannelMessageChannel(m[0]) } -// AfterTouch returns an aftertouch message. -func AfterTouch(channel, pressure uint8) Message { - assertChannel(channel) - pressure = legalizeUint8(pressure) +func (m NoteOnMessage) Key() uint8 { + assertMessageLength(Message(m), 3) + return utils.ParseUint7(m[1]) +} - return channelMessages(channel, 13, pressure) +func (m NoteOnMessage) Velocity() uint8 { + assertMessageLength(Message(m), 3) + return utils.ParseUint7(m[2]) } -// ControlChange returns a control change message. -func ControlChange(channel, controller, value uint8) Message { - assertChannel(channel) - controller = legalizeUint8(controller) - value = legalizeUint8(value) +func (m NoteOnMessage) Message() Message { + return Message(m) +} - return channelMessages(channel, 11, controller, value) +func (m NoteOnMessage) String() string { + return fmt.Sprintf("%s channel: %d key: %d velocity: %d", MessageTypeNoteOn, m.Channel(), m.Key(), m.Velocity()) } -func statusChannelOnly(_ uint8, c uint8) uint8 { - return c +// PolyAfterTouch returns a polyphonic aftertouch message. +func PolyAfterTouch(channel, key, pressure uint8) PolyAfterTouchMessage { + return PolyAfterTouchMessage([]byte{ + ChannelMessageStatus(MessageTypePolyAfterTouch, channel), + legalizeUint7(key), + legalizeUint7(pressure)}) } -type AfterTouchMessage Message +type PolyAfterTouchMessage Message -func (m AfterTouchMessage) Channel() uint8 { - assertMessageLength(Message(m), 2) - return statusChannelOnly(utils.ParseStatus(m[0])) +func (m PolyAfterTouchMessage) Channel() uint8 { + assertMessageLength(Message(m), 3) + return ChannelMessageChannel(m[0]) } -func (m AfterTouchMessage) Pressure() uint8 { - assertMessageLength(Message(m), 2) +func (m PolyAfterTouchMessage) Key() uint8 { + assertMessageLength(Message(m), 3) return utils.ParseUint7(m[1]) } -func (m AfterTouchMessage) String() string { - return fmt.Sprintf("%s channel: %d pressure: %d", AfterTouchMsg, m.Channel(), m.Pressure()) +func (m PolyAfterTouchMessage) Pressure() uint8 { + assertMessageLength(Message(m), 3) + return utils.ParseUint7(m[2]) } -type ProgramChangeMessage Message - -func (m ProgramChangeMessage) Channel() uint8 { - assertMessageLength(Message(m), 2) - return statusChannelOnly(utils.ParseStatus(m[0])) +func (m PolyAfterTouchMessage) Message() Message { + return Message(m) } -func (m ProgramChangeMessage) Program() uint8 { - assertMessageLength(Message(m), 2) - return utils.ParseUint7(m[1]) +func (m PolyAfterTouchMessage) String() string { + return fmt.Sprintf("%s channel: %d key: %d pressure: %d", MessageTypePolyAfterTouch, m.Channel(), m.Key(), m.Pressure()) } -func (m ProgramChangeMessage) String() string { - return fmt.Sprintf("%s channel: %d program: %d", ProgramChangeMsg, m.Channel(), m.Program()) +// ControlChange returns a control change message. +func ControlChange(channel, controller, value uint8) ControlChangeMessage { + return ControlChangeMessage([]byte{ + ChannelMessageStatus(MessageTypeControlChange, channel), + legalizeUint7(controller), + legalizeUint7(value)}) } type ControlChangeMessage Message func (m ControlChangeMessage) Channel() uint8 { assertMessageLength(Message(m), 3) - return statusChannelOnly(utils.ParseStatus(m[0])) + return ChannelMessageChannel(m[0]) } func (m ControlChangeMessage) Controller() uint8 { @@ -154,78 +149,86 @@ func (m ControlChangeMessage) Value() uint8 { return utils.ParseUint7(m[2]) } -func (m ControlChangeMessage) String() string { - return fmt.Sprintf("%s channel: %d controller: %d value: %d", ControlChangeMsg, m.Channel(), m.Controller(), m.Value()) +func (m ControlChangeMessage) Message() Message { + return Message(m) } -type NoteOnMessage Message - -func (m NoteOnMessage) Channel() uint8 { - assertMessageLength(Message(m), 3) - return statusChannelOnly(utils.ParseStatus(m[0])) +func (m ControlChangeMessage) String() string { + return fmt.Sprintf("%s channel: %d controller: %d value: %d", MessageTypeControlChange, m.Channel(), m.Controller(), m.Value()) } -func (m NoteOnMessage) Key() uint8 { - assertMessageLength(Message(m), 3) - return utils.ParseUint7(m[1]) +// ProgramChange returns a program change message. +func ProgramChange(channel, program uint8) ProgramChangeMessage { + return ProgramChangeMessage([]byte{ + ChannelMessageStatus(MessageTypeProgramChange, channel), + legalizeUint7(program)}) } -func (m NoteOnMessage) Velocity() uint8 { - assertMessageLength(Message(m), 3) - return utils.ParseUint7(m[2]) -} +type ProgramChangeMessage Message -func (m NoteOnMessage) String() string { - return fmt.Sprintf("%s channel: %d key: %d velocity: %d", NoteOnMsg, m.Channel(), m.Key(), m.Velocity()) +func (m ProgramChangeMessage) Channel() uint8 { + assertMessageLength(Message(m), 2) + return ChannelMessageChannel(m[0]) } -type NoteOffMessage Message - -func (m NoteOffMessage) Channel() uint8 { - assertMessageLength(Message(m), 3) - return statusChannelOnly(utils.ParseStatus(m[0])) +func (m ProgramChangeMessage) Program() uint8 { + assertMessageLength(Message(m), 2) + return utils.ParseUint7(m[1]) } -func (m NoteOffMessage) Key() uint8 { - assertMessageLength(Message(m), 3) - return utils.ParseUint7(m[1]) +func (m ProgramChangeMessage) Message() Message { + return Message(m) } -func (m NoteOffMessage) Velocity() uint8 { - assertMessageLength(Message(m), 3) - return utils.ParseUint7(m[2]) +func (m ProgramChangeMessage) String() string { + return fmt.Sprintf("%s channel: %d program: %d", MessageTypeProgramChange, m.Channel(), m.Program()) } -func (m NoteOffMessage) String() string { - return fmt.Sprintf("%s channel: %d key: %d velocity: %d", NoteOffMsg, m.Channel(), m.Key(), m.Velocity()) +// AfterTouch returns an aftertouch message. +func AfterTouch(channel, pressure uint8) AfterTouchMessage { + return AfterTouchMessage([]byte{ + ChannelMessageStatus(MessageTypeAfterTouch, channel), + legalizeUint7(pressure)}) } -type PolyAfterTouchMessage Message +type AfterTouchMessage Message -func (m PolyAfterTouchMessage) Channel() uint8 { - assertMessageLength(Message(m), 3) - return statusChannelOnly(utils.ParseStatus(m[0])) +func (m AfterTouchMessage) Channel() uint8 { + assertMessageLength(Message(m), 2) + return ChannelMessageChannel(m[0]) } -func (m PolyAfterTouchMessage) Key() uint8 { - assertMessageLength(Message(m), 3) +func (m AfterTouchMessage) Pressure() uint8 { + assertMessageLength(Message(m), 2) return utils.ParseUint7(m[1]) } -func (m PolyAfterTouchMessage) Pressure() uint8 { - assertMessageLength(Message(m), 3) - return utils.ParseUint7(m[2]) +func (m AfterTouchMessage) Message() Message { + return Message(m) } -func (m PolyAfterTouchMessage) String() string { - return fmt.Sprintf("%s channel: %d key: %d pressure: %d", PolyAfterTouchMsg, m.Channel(), m.Key(), m.Pressure()) +func (m AfterTouchMessage) String() string { + return fmt.Sprintf("%s channel: %d pressure: %d", MessageTypeAfterTouch, m.Channel(), m.Pressure()) +} + +// Pitchbend returns a pitch bend message. +// If value is > 8191 (max), it will be set to 8191. If value is < -8192, it will be set to -8192. +// A value of 0 is considered as neutral position. +func PitchBend(channel uint8, value int16) PitchBendMessage { + var b = make([]byte, 2) + binary.BigEndian.PutUint16(b, uint14(value)) + + return PitchBendMessage([]byte{ + ChannelMessageStatus(MessageTypePitchBend, channel), + b[0], + b[1]}) } type PitchBendMessage Message func (m PitchBendMessage) Channel() uint8 { assertMessageLength(Message(m), 3) - return statusChannelOnly(utils.ParseStatus(m[0])) + return ChannelMessageChannel(m[0]) } func (m PitchBendMessage) Pitch() int16 { @@ -240,6 +243,38 @@ func (m PitchBendMessage) Absolute() uint16 { return abs } +func (m PitchBendMessage) Message() Message { + return Message(m) +} + func (m PitchBendMessage) String() string { - return fmt.Sprintf("%s channel: %d pitch: %d (%d)", PitchBendMsg, m.Channel(), m.Pitch(), m.Absolute()) + return fmt.Sprintf("%s channel: %d pitch: %d (%d)", MessageTypePitchBend, m.Channel(), m.Pitch(), m.Absolute()) +} + +const ( + // PitchReset is the pitch bend value to reset the pitch wheel to zero + PitchReset = 0 + // PitchLowest is the lowest possible value of the pitch bending + PitchMinimum = -8192 + // PitchHighest is the highest possible value of the pitch bending + PitchMaximum = 8191 + pitchClearLSB = 0x7f00 + pitchClearMSB = 0x007f +) + +func legalizePitch(value int16) uint16 { + switch { + case value < PitchMinimum: + value = PitchMinimum + case value > PitchMaximum: + value = PitchMaximum + } + return uint16(value - PitchMinimum) +} + +func uint14(value int16) uint16 { + n := legalizePitch(value) + lsb := (n << 8) & pitchClearLSB + msb := (n >> 7) & pitchClearMSB + return lsb | msb } diff --git a/v2/channel_test.go b/v2/channel_test.go index 1a2a873..e6e6f18 100644 --- a/v2/channel_test.go +++ b/v2/channel_test.go @@ -33,7 +33,7 @@ func TestChannelString(t *testing.T) { "NoteOff channel: 4 key: 80 velocity: 20", }, { - Pitchbend(4, 300), + PitchBend(4, 300), "PitchBend channel: 4 pitch: 300 (8492)", }, { @@ -67,7 +67,7 @@ func TestChannelString(t *testing.T) { "NoteOff channel: 4 key: 127 velocity: 127", }, { - Pitchbend(4, 12300), + PitchBend(4, 12300), "PitchBend channel: 4 pitch: 8191 (16383)", }, { @@ -120,7 +120,7 @@ func TestChannelRaw(t *testing.T) { "84 50 14", }, { - Pitchbend(4, 300), + PitchBend(4, 300), "E4 2C 42", }, { diff --git a/v2/error.go b/v2/error.go index 72bbdef..3ee5b6e 100644 --- a/v2/error.go +++ b/v2/error.go @@ -16,6 +16,7 @@ const ( ErrSysExHeaderInvalid Error = "sysex message header not expected" ErrSysExTrailerInvalid Error = "sysex message trailer not expected" ErrSysExCallback Error = "driver error: received 0xF0 in non sysex callback" + ErrMessageCategory Error = "message category not expected" ) func assertMessageLength(msg Message, expLen int) { @@ -36,12 +37,18 @@ func assertChannel(channel uint8) { } } +func assertMessageCategory(t MessageType, c MessageCategory) { + if tc := Category(byte(t)); tc != c { + panic(fmt.Errorf("%w: %s != %s for %s", ErrMessageCategory, tc, c, tc.MessageType(byte(t)))) + } +} + func assertSysExFormat(msg Message) { assertMessageLengthMinimum(msg, 3) - if v := msg[0]; v != SysExHeader { - panic(fmt.Errorf("%w: 0x%02x != 0x%02x", ErrSysExHeaderInvalid, v, SysExHeader)) + if v := msg[0]; v != byte(MessageTypeSysEx) { + panic(fmt.Errorf("%w: 0x%02x != 0x%02x", ErrSysExHeaderInvalid, v, MessageTypeSysEx)) } - if v := msg[len(msg)-1]; v != SysExTrailer { - panic(fmt.Errorf("%w: 0x%02x != 0x%02x", ErrSysExTrailerInvalid, v, SysExTrailer)) + if v := msg[len(msg)-1]; v != byte(MessageTypeSysExEnd) { + panic(fmt.Errorf("%w: 0x%02x != 0x%02x", ErrSysExTrailerInvalid, v, MessageTypeSysExEnd)) } } diff --git a/v2/example/logger/main.go b/v2/example/logger/main.go index 55ae021..fd1ee8d 100644 --- a/v2/example/logger/main.go +++ b/v2/example/logger/main.go @@ -18,7 +18,7 @@ func main() { } stop, err := midi.ListenTo(in, func(msg midi.Message, timestampms int32) { - switch m := msg.Parse().(type) { + switch m := msg.TypedMessage().(type) { case midi.SysExMessage: fmt.Printf("got sysex: % X\n", m.Data()) case midi.NoteOnMessage: diff --git a/v2/example_test.go b/v2/example_test.go index 096a9cf..493bad7 100644 --- a/v2/example_test.go +++ b/v2/example_test.go @@ -3,6 +3,7 @@ package midi_test import ( "fmt" "os" + "testing" "time" . "gitlab.com/gomidi/midi/v2" @@ -13,15 +14,14 @@ import ( // when using rtmidi ("for real"), replace with the line above ) -func Example() { +func TestExample(t *testing.T) { var eachMessage = func(msg Message, timestampms int32) { - if msg.Is(RealTimeMsg) { - // ignore realtime messages + if c := Category(byte(MessageTypeFromMessage(msg))); c == MessageCategoryRealtime { return } - switch m := msg.Parse().(type) { + switch m := msg.TypedMessage().(type) { case NoteOnMessage: fmt.Printf("note started at %vms channel: %v key: %v velocity: %v\n", timestampms, m.Channel(), m.Key(), m.Velocity()) case NoteOffMessage: @@ -67,15 +67,15 @@ func Example() { stop, _ := ListenTo(in, eachMessage) // send some messages - send(NoteOn(0, Db(4), 100)) + send(Message(NoteOn(0, Db(4), 100))) <-time.After(29 * time.Millisecond) // time.Sleep(time.Millisecond * 30) - send(NoteOff(0, Db(4), 0)) - send(Pitchbend(0, -12)) + send(Message(NoteOff(0, Db(4), 0))) + send(Message(PitchBend(0, -12))) <-time.After(19 * time.Millisecond) // time.Sleep(time.Millisecond * 20) - send(ProgramChange(1, 12)) - send(ControlChange(2, FootPedalMSB, On)) + send(Message(ProgramChange(1, 12))) + send(Message(ControlChange(2, FootPedalMSB, On))) // stops listening stop() diff --git a/v2/gm/reset.go b/v2/gm/reset.go index c0ed438..1d72931 100644 --- a/v2/gm/reset.go +++ b/v2/gm/reset.go @@ -6,7 +6,7 @@ import ( // GMProgram is a shortcut to write GM bank select control change message followed // by a program change. -func GMProgram(ch, prog uint8) (msgs []midi.Message) { +func GMProgram(ch, prog uint8) (msgs []midi.Messager) { //c := channel.Channel(ch) msgs = append(msgs, midi.ControlChange(ch, midi.BankSelectMSB, 0)) msgs = append(msgs, midi.ProgramChange(ch, prog)) @@ -25,8 +25,8 @@ func GMProgram(ch, prog uint8) (msgs []midi.Message) { cc hold pedal 0 cc pan position 64 */ -func Reset(ch, prog uint8) []midi.Message { - return []midi.Message{ +func Reset(ch, prog uint8) []midi.Messager { + return []midi.Messager{ midi.ControlChange(ch, midi.BankSelectMSB, 0), midi.ProgramChange(ch, prog), midi.ControlChange(ch, midi.AllControllersOff, 0), diff --git a/v2/helpers.go b/v2/helpers.go index bc4b400..d3e5b6e 100644 --- a/v2/helpers.go +++ b/v2/helpers.go @@ -1,128 +1,122 @@ package midi -import ( - "io" - - "gitlab.com/gomidi/midi/v2/internal/utils" -) - -// channelMessage returns the bytes for a single byte channel message -func channelMessages(c uint8, status uint8, msgs ...byte) []byte { - cm := &channelMessage{channel: c, status: status} - switch len(msgs) { - case 1: - cm.data[0] = msgs[0] - case 2: - cm.twoBytes = true - cm.data[0] = msgs[0] - cm.data[1] = msgs[1] - } - bt := cm.bytes() - //return NewMessage(bt) - return bt -} - -type channelMessage struct { - status uint8 - channel uint8 - twoBytes bool - data [2]byte -} - -func (m *channelMessage) getCompleteStatus() uint8 { - s := m.status << 4 - s = utils.ClearBitU8(s, 0) - s = utils.ClearBitU8(s, 1) - s = utils.ClearBitU8(s, 2) - s = utils.ClearBitU8(s, 3) - s = s | m.channel - return s -} - -func (m *channelMessage) bytes() []byte { - if m.twoBytes { - return []byte{m.getCompleteStatus(), m.data[0], m.data[1]} - } - return []byte{m.getCompleteStatus(), m.data[0]} -} - -const ( - byteProgramChange = 0xC - byteChannelPressure = 0xD - byteNoteOff = 0x8 - byteNoteOn = 0x9 - bytePolyphonicKeyPressure = 0xA - byteControlChange = 0xB - bytePitchWheel = 0xE -) - -// ReadChannelMessage reads a channel message for the given status byte from the given reader. -// Don't use this function as a user, it is only internal to the library. -func ReadChannelMessage(status byte, arg1 byte, rd io.Reader) (m Message, err error) { - typ, channel := utils.ParseStatus(status) - - if err != nil { - return - } - - switch typ { - - // one argument only - case byteProgramChange, byteChannelPressure: - m = getMsg1(typ, channel, arg1) - - // two Arguments needed - default: - var arg2 byte - arg2, err = utils.ReadByte(rd) - - if err != nil { - return - } - m = getMsg2(typ, channel, arg1, arg2) - } - return -} - -// getMsg1 returns a 1-byte channel message (program change or aftertouch) -func getMsg1(typ uint8, channel uint8, arg uint8) (m Message) { - //m.MsgType = GetChannelMsgType(typ) - //m.MsgType = ChannelMsg.Set(channelType[channel]) - //m.Data = channelMessage1(channel, typ, arg) - m = channelMessages(channel, typ, arg) - /* - switch typ { - case byteProgramChange: - m.Type = ProgramChange - case byteChannelPressure: - m.Type = AfterTouch - default: - panic(fmt.Sprintf("must not happen (typ % X is not an channel message with one argument)", typ)) - } - */ - return -} - -// getMsg2 returns a 2-byte channel message (noteon/noteoff, poly aftertouch, control change or pitchbend) -func getMsg2(typ uint8, channel uint8, arg1 uint8, arg2 uint8) (msg Message) { - //msg.MsgType = ChannelMsg.Set(channelType[channel]) - msg = channelMessages(channel, typ, arg1, arg2) - - /* - switch typ { - case byteNoteOff: - msg.Type = NoteOff - case byteNoteOn: - msg.Type = NoteOn - case bytePolyphonicKeyPressure: - msg.Type = PolyAfterTouch - case byteControlChange: - msg.Type = ControlChange - case bytePitchWheel: - msg.Type = PitchBend - default: - panic(fmt.Sprintf("must not happen (typ % X is not an channel message with two arguments)", typ)) - } - */ - return -} +// // channelMessage returns the bytes for a single byte channel message +// func channelMessages(c uint8, status uint8, msgs ...byte) []byte { +// cm := &channelMessage{channel: c, status: status} +// switch len(msgs) { +// case 1: +// cm.data[0] = msgs[0] +// case 2: +// cm.twoBytes = true +// cm.data[0] = msgs[0] +// cm.data[1] = msgs[1] +// } +// bt := cm.bytes() +// //return NewMessage(bt) +// return bt +// } + +// type channelMessage struct { +// status uint8 +// channel uint8 +// twoBytes bool +// data [2]byte +// } + +// func (m *channelMessage) getCompleteStatus() uint8 { +// s := m.status << 4 +// s = utils.ClearBitU8(s, 0) +// s = utils.ClearBitU8(s, 1) +// s = utils.ClearBitU8(s, 2) +// s = utils.ClearBitU8(s, 3) +// s = s | m.channel +// return s +// } + +// func (m *channelMessage) bytes() []byte { +// if m.twoBytes { +// return []byte{m.getCompleteStatus(), m.data[0], m.data[1]} +// } +// return []byte{m.getCompleteStatus(), m.data[0]} +// } + +// const ( +// byteProgramChange = 0xC +// byteChannelPressure = 0xD +// byteNoteOff = 0x8 +// byteNoteOn = 0x9 +// bytePolyphonicKeyPressure = 0xA +// byteControlChange = 0xB +// bytePitchWheel = 0xE +// ) + +// // ReadChannelMessage reads a channel message for the given status byte from the given reader. +// // Don't use this function as a user, it is only internal to the library. +// func ReadChannelMessage(status byte, arg1 byte, rd io.Reader) (m Message, err error) { +// typ, channel := utils.ParseStatus(status) + +// if err != nil { +// return +// } + +// switch typ { + +// // one argument only +// case byteProgramChange, byteChannelPressure: +// m = getMsg1(typ, channel, arg1) + +// // two Arguments needed +// default: +// var arg2 byte +// arg2, err = utils.ReadByte(rd) + +// if err != nil { +// return +// } +// m = getMsg2(typ, channel, arg1, arg2) +// } +// return +// } + +// // getMsg1 returns a 1-byte channel message (program change or aftertouch) +// func getMsg1(typ uint8, channel uint8, arg uint8) (m Message) { +// //m.MsgType = GetChannelMsgType(typ) +// //m.MsgType = ChannelMsg.Set(channelType[channel]) +// //m.Data = channelMessage1(channel, typ, arg) +// m = channelMessages(channel, typ, arg) +// /* +// switch typ { +// case byteProgramChange: +// m.Type = ProgramChange +// case byteChannelPressure: +// m.Type = AfterTouch +// default: +// panic(fmt.Sprintf("must not happen (typ % X is not an channel message with one argument)", typ)) +// } +// */ +// return +// } + +// // getMsg2 returns a 2-byte channel message (noteon/noteoff, poly aftertouch, control change or pitchbend) +// func getMsg2(typ uint8, channel uint8, arg1 uint8, arg2 uint8) (msg Message) { +// //msg.MsgType = ChannelMsg.Set(channelType[channel]) +// msg = channelMessages(channel, typ, arg1, arg2) + +// /* +// switch typ { +// case byteNoteOff: +// msg.Type = NoteOff +// case byteNoteOn: +// msg.Type = NoteOn +// case bytePolyphonicKeyPressure: +// msg.Type = PolyAfterTouch +// case byteControlChange: +// msg.Type = ControlChange +// case bytePitchWheel: +// msg.Type = PitchBend +// default: +// panic(fmt.Sprintf("must not happen (typ % X is not an channel message with two arguments)", typ)) +// } +// */ +// return +// } diff --git a/v2/internal/runningstatus/runningstatus.go b/v2/internal/runningstatus/runningstatus.go index ad38e1b..ee99fa6 100644 --- a/v2/internal/runningstatus/runningstatus.go +++ b/v2/internal/runningstatus/runningstatus.go @@ -126,7 +126,7 @@ func (w *smfwriter) Write(raw []byte) []byte { */ // for non channel messages, reset status and write whole message //if !midilib.IsChannelMessage(firstByte) { - if !midi.Message(raw).Is(midi.ChannelMsg) { + if midi.Message(raw).MessageCategory() != midi.MessageCategoryChannel { // if midi.GetMsgType(raw).Category() != midi.ChannelMessages { //fmt.Printf("is no channel message, resetting status\n") w.status = 0 @@ -176,7 +176,7 @@ func (w *liveWriter) Write(m []byte) (int, error) { // for non channel messages, reset status and write whole message //if !midilib.IsChannelMessage(msg[0]) { //if midi.GetMsgType(m).Category() != midi.ChannelMessages { - if !midi.Message(m).Is(midi.ChannelMsg) { + if midi.Message(m).MessageCategory() != midi.MessageCategoryChannel { // fmt.Printf("is no channel message, resetting status\n") w.status = 0 return w.write(m) diff --git a/v2/message.go b/v2/message.go index 287cc03..a7a3ba2 100644 --- a/v2/message.go +++ b/v2/message.go @@ -4,56 +4,240 @@ import ( "fmt" ) +//go:generate stringer -type=MessageCategory,MessageType -trimprefix=MessageCategory,MessageType -output stringer.go + +type MessageCategory byte +type MessageCategoryTypeFunc func(byte) MessageType + +const ( + MessageCategoryUnknown MessageCategory = 0x00 + MessageCategoryChannel MessageCategory = 0x0f + MessageCategoryCommon MessageCategory = 0xf7 + MessageCategoryRealtime MessageCategory = 0xff +) + +func Category(b byte) MessageCategory { + switch { + case b >= byte(MessageTypeNoteOff) && b < byte(MessageTypeSysEx): + return MessageCategoryChannel + case b >= byte(MessageTypeSysEx) && b < byte(MessageTypeBeatClock): + return MessageCategoryCommon + case b >= byte(MessageTypeBeatClock): + return MessageCategoryRealtime + default: + return MessageCategoryUnknown + } +} + +func (c MessageCategory) MessageType(b byte) MessageType { + switch { + case c == MessageCategoryChannel: + return messageCategoryChannelType(b) + case c == MessageCategoryCommon: + return messageCategoryCommonType(b) + case c == MessageCategoryRealtime: + return messageCategoryRealtimeType(b) + default: + return messageCategoryUnknownType(b) + } +} + +func (c MessageCategory) String() string { + switch c { + case MessageCategoryChannel: + return "Channel" + case MessageCategoryCommon: + return "Common" + case MessageCategoryRealtime: + return "Realtime" + default: + return "Unknown" + } +} + +func messageCategoryUnknownType(b byte) MessageType { + return MessageTypeUnknown +} + +func messageCategoryChannelType(b byte) MessageType { + return MessageType(b & byte(MessageTypeSysEx)) +} + +func messageCategoryCommonType(b byte) MessageType { + return MessageType(b) +} + +func messageCategoryRealtimeType(b byte) MessageType { + return MessageType(b) +} + +func MessageTypeFromByte(b byte) MessageType { + return Category(b).MessageType(b) +} + +func MessageTypeFromMessage(m Message) MessageType { + if len(m) == 0 { + return MessageTypeUnknown + } + return MessageTypeFromByte(m[0]) +} + +// MessageType is the type of a midi message +type MessageType byte + +const ( + MessageTypeUnknown MessageType = 0x00 + MessageTypeNoteOff MessageType = 0x80 // Channel Messages + MessageTypeNoteOn MessageType = 0x90 + MessageTypePolyAfterTouch MessageType = 0xa0 + MessageTypeControlChange MessageType = 0xb0 + MessageTypeProgramChange MessageType = 0xc0 + MessageTypeAfterTouch MessageType = 0xd0 + MessageTypePitchBend MessageType = 0xe0 + MessageTypeSysEx MessageType = 0xf0 // System Common + messageTypeReservedF1 MessageType = 0xf1 + MessageTypeSongPosition MessageType = 0xf2 + MessageTypeSongSelect MessageType = 0xf3 + messageTypeReservedF4 MessageType = 0xf4 + messageTypeReservedF5 MessageType = 0xf5 + MessageTypeTuneRequest MessageType = 0xf6 + MessageTypeSysExEnd MessageType = 0xf7 + MessageTypeBeatClock MessageType = 0xf8 // System Realtime + messageTypeReservedF9 MessageType = 0xf9 + MessageTypeStart MessageType = 0xfa + MessageTypeContinue MessageType = 0xfb + MessageTypeStop MessageType = 0xfc + messageTypeReservedFD MessageType = 0xfd + MessageTypeActiveSense MessageType = 0xfe + MessageTypeReset MessageType = 0xff +) + +func (t MessageType) String() string { + switch t { + case MessageTypeNoteOff: + return "NoteOff" + case MessageTypeNoteOn: + return "NoteOn" + case MessageTypePolyAfterTouch: + return "PolyAfterTouch" + case MessageTypeControlChange: + return "ControlChange" + case MessageTypeProgramChange: + return "ProgramChange" + case MessageTypeAfterTouch: + return "AfterTouch" + case MessageTypePitchBend: + return "PitchBend" + case MessageTypeSysEx: + return "SysEx" + case messageTypeReservedF1: + return "ReservedF1" + case MessageTypeSongPosition: + return "SongPosition" + case MessageTypeSongSelect: + return "SongSelect" + case messageTypeReservedF4: + return "ReservedF4" + case messageTypeReservedF5: + return "ReservedF5" + case MessageTypeTuneRequest: + return "TuneRequest" + case MessageTypeSysExEnd: + return "SysExEnd" + case MessageTypeBeatClock: + return "BeatClock" + case messageTypeReservedF9: + return "ReservedF9" + case MessageTypeStart: + return "Start" + case MessageTypeContinue: + return "Continue" + case MessageTypeStop: + return "Stop" + case messageTypeReservedFD: + return "ReservedFD" + case MessageTypeActiveSense: + return "ActiveSense" + case MessageTypeReset: + return "Reset" + default: + return "Unknown" + } +} + +type Manufacturer byte + +const ( + ManufacturerUniversal Manufacturer = 0x7f +) + // Message is a complete midi message (not including meta messages) type Message []byte -func (m Message) Parse() interface{} { - switch t := getType(m); t { - case ProgramChangeMsg: - return ProgramChangeMessage(m) - case AfterTouchMsg: - return AfterTouchMessage(m) - case NoteOffMsg: +func (m Message) TypedMessage() interface{} { + switch t := MessageTypeFromMessage(m); t { + case MessageTypeNoteOff: return NoteOffMessage(m) - case NoteOnMsg: + case MessageTypeNoteOn: return NoteOnMessage(m) - case PolyAfterTouchMsg: + case MessageTypePolyAfterTouch: return PolyAfterTouchMessage(m) - case ControlChangeMsg: + case MessageTypeControlChange: return ControlChangeMessage(m) - case PitchBendMsg: + case MessageTypeProgramChange: + return ProgramChangeMessage(m) + case MessageTypeAfterTouch: + return AfterTouchMessage(m) + case MessageTypePitchBend: return PitchBendMessage(m) - case MTCMsg: - return MTCMessage(m) - case SongPositionMsg: + case MessageTypeSysEx: + return SysExMessage(m) + case MessageTypeSongPosition: return SongPositionMessage(m) - case SongSelectMsg: + case MessageTypeSongSelect: return SongSelectMessage(m) - case TuneMsg: - return TuneMessage(m) + case MessageTypeTuneRequest: + return TuneRequestMessage(m) + case MessageTypeBeatClock: + return BeatClockMessage(m) + case MessageTypeStart: + return StartMessage(m) + case MessageTypeStop: + return StopMessage(m) + case MessageTypeActiveSense: + return ActiveSenseMessage(m) + case MessageTypeReset: + return ResetMessage(m) default: return m } } -func (m Message) MessageType() Type { - return getType(m) +func (m Message) MessageType() MessageType { + return MessageTypeFromMessage(m) +} + +func (m Message) MessageCategory() MessageCategory { + return Category(byte(m.MessageType())) } -// IsPlayable returns, if the message can be send to an instrument. -func (m Message) IsPlayable() bool { - t := getType(m) - if t <= UnknownMsg { - return false +func (m Message) Message() Message { + return m +} + +// String represents the Message as a string that contains the Type and its properties. +func (m Message) String() string { + tm := m.TypedMessage() + if tm, ok := tm.(fmt.Stringer); ok { + return tm.String() } - return t < firstMetaMsg + t := m.MessageType() + return fmt.Sprintf("%s %v", t, tm) } -// Is returns true, if the message is of the given type. -func (m Message) Is(t Type) bool { - ty := getType(m) - return ty.Is(t) +type Messager interface { + Message() Message } /* @@ -111,33 +295,3 @@ cdefg = Hours (0-23) // _, *spp = utils.ParsePitchWheelVals(m[2], m[1]) // return true // } - -// String represents the Message as a string that contains the Type and its properties. -func (m Message) String() string { - tm := m.Parse() - if tm, ok := tm.(fmt.Stringer); ok { - return tm.String() - } - - t := getType(m) - return fmt.Sprintf("%s %v", t, tm) -} - -// // GetSysEx returns true, if the message is a sysex message. -// // Then it extracts the inner bytes to the given slice. -// func (m Message) GetSysEx(bt *[]byte) bool { -// if len(m) < 3 { -// return false -// } - -// if !m.Is(SysExMsg) { -// return false -// } - -// if m[0] == 0xF0 && m[len(m)-1] == 0xF7 { -// *bt = m[1 : len(m)-1] -// return true -// } - -// return false -// } diff --git a/v2/nrpn/nrpn.go b/v2/nrpn/nrpn.go index 3929260..a2d654a 100644 --- a/v2/nrpn/nrpn.go +++ b/v2/nrpn/nrpn.go @@ -4,20 +4,20 @@ import ( "gitlab.com/gomidi/midi/v2" ) -func cc(channel, ctl, val uint8) midi.Message { +func cc(channel, ctl, val uint8) midi.Messager { return midi.ControlChange(channel, ctl, val) } // Reset aka Null -func Reset(channel uint8) []midi.Message { - return append([]midi.Message{}, +func Reset(channel uint8) []midi.Messager { + return append([]midi.Messager{}, cc(channel, 99, 127), cc(channel, 98, 127), ) } -func Increment(channel uint8, val99, val98 uint8) []midi.Message { - msgs := append([]midi.Message{}, +func Increment(channel uint8, val99, val98 uint8) []midi.Messager { + msgs := append([]midi.Messager{}, cc(channel, 99, val99), cc(channel, 98, val98), cc(channel, 96, 0)) @@ -25,8 +25,8 @@ func Increment(channel uint8, val99, val98 uint8) []midi.Message { return append(msgs, Reset(channel)...) } -func Decrement(channel uint8, val99, val98 uint8) []midi.Message { - msgs := append([]midi.Message{}, +func Decrement(channel uint8, val99, val98 uint8) []midi.Messager { + msgs := append([]midi.Messager{}, cc(channel, 99, val99), cc(channel, 98, val98), cc(channel, 97, 0)) @@ -35,8 +35,8 @@ func Decrement(channel uint8, val99, val98 uint8) []midi.Message { } // NRPN message consisting of a val99 and val98 to identify the NRPN and a msb and lsb for the value -func NRPN(channel uint8, val99, val98, msbVal, lsbVal uint8) []midi.Message { - msgs := append([]midi.Message{}, +func NRPN(channel uint8, val99, val98, msbVal, lsbVal uint8) []midi.Messager { + msgs := append([]midi.Messager{}, cc(channel, 99, val99), cc(channel, 98, val98), cc(channel, 6, msbVal), diff --git a/v2/pitchbend_test.go b/v2/pitchbend_test.go index e87a6db..2ebce54 100644 --- a/v2/pitchbend_test.go +++ b/v2/pitchbend_test.go @@ -33,7 +33,7 @@ func TestPitchbend(t *testing.T) { } for _, test := range tests { - m := Pitchbend(0, test.in).Parse().(PitchBendMessage) + m := PitchBend(0, test.in) if abs := m.Absolute(); abs != test.expected { t.Errorf("Pitchbend(%v).absValue = %v; wanted %v", test.in, abs, test.expected) diff --git a/v2/realtime.go b/v2/realtime.go index ea94de4..3df91ed 100644 --- a/v2/realtime.go +++ b/v2/realtime.go @@ -1,65 +1,93 @@ package midi -const ( - byteTimingClock = 0xF8 - byteTick = 0xF9 - byteStart = 0xFA - byteContinue = 0xFB - byteStop = 0xFC - byteUndefined4 = 0xFD - byteActivesense = 0xFE - byteReset = 0xFF -) - -var rtMessages = map[byte]Type{ - byteTimingClock:/* RealTimeMsg.Set(TimingClockMsg), */ TimingClockMsg, - byteTick:/* RealTimeMsg.Set(TickMsg), */ TickMsg, - byteStart:/* RealTimeMsg.Set(StartMsg), */ StartMsg, - byteContinue:/* RealTimeMsg.Set(ContinueMsg), */ ContinueMsg, - byteStop:/* RealTimeMsg.Set(StopMsg), */ StopMsg, - byteUndefined4:/* RealTimeMsg.Set(UndefinedMsg), */ UnknownMsg, - byteActivesense:/* RealTimeMsg.Set(ActiveSenseMsg), */ ActiveSenseMsg, - byteReset:/* RealTimeMsg.Set(ResetMsg), */ ResetMsg, -} - -// TimingClock returns a timing clock message -func TimingClock() Message { - return []byte{byteTimingClock} -} - -// Tick returns a tick message -func Tick() Message { - return []byte{byteTick} +import "fmt" + +// BeatClock returns a timing clock message +func BeatClock() BeatClockMessage { + return BeatClockMessage([]byte{byte(MessageTypeBeatClock)}) +} + +type BeatClockMessage Message + +func (m BeatClockMessage) Message() Message { + return Message(m) +} + +func (m BeatClockMessage) String() string { + return fmt.Sprintf("%s", MessageTypeBeatClock) } // Start returns a start message -func Start() Message { - return []byte{byteStart} +func Start() StartMessage { + return StartMessage([]byte{byte(MessageTypeStart)}) +} + +type StartMessage Message + +func (m StartMessage) Message() Message { + return Message(m) +} + +func (m StartMessage) String() string { + return fmt.Sprintf("%s", MessageTypeStart) } // Continue returns a continue message -func Continue() Message { - return []byte{byteContinue} +func Continue() ContinueMessage { + return ContinueMessage([]byte{byte(MessageTypeContinue)}) +} + +type ContinueMessage Message + +func (m ContinueMessage) Message() Message { + return Message(m) +} + +func (m ContinueMessage) String() string { + return fmt.Sprintf("%s", MessageTypeContinue) } // Stop returns a stop message -func Stop() Message { - return []byte{byteStop} +func Stop() StopMessage { + return StopMessage([]byte{byte(MessageTypeStop)}) +} + +type StopMessage Message + +func (m StopMessage) Message() Message { + return Message(m) } -/* -// NewUndefined returns an undefined realtime message -func NewUndefined() Message { - return NewMsg([]byte{byteUndefined4}) +func (m StopMessage) String() string { + return fmt.Sprintf("%s", MessageTypeStop) } -*/ // Activesense returns an active sensing message -func Activesense() Message { - return []byte{byteActivesense} +func ActiveSense() ActiveSenseMessage { + return ActiveSenseMessage([]byte{byte(MessageTypeActiveSense)}) +} + +type ActiveSenseMessage Message + +func (m ActiveSenseMessage) Message() Message { + return Message(m) +} + +func (m ActiveSenseMessage) String() string { + return fmt.Sprintf("%s", MessageTypeActiveSense) } // Reset returns a reset message -func Reset() Message { - return []byte{byteReset} +func Reset() ResetMessage { + return ResetMessage([]byte{byte(MessageTypeReset)}) +} + +type ResetMessage Message + +func (m ResetMessage) Message() Message { + return Message(m) +} + +func (m ResetMessage) String() string { + return fmt.Sprintf("%s", MessageTypeReset) } diff --git a/v2/realtime_test.go b/v2/realtime_test.go index 8a0729c..299b9a3 100644 --- a/v2/realtime_test.go +++ b/v2/realtime_test.go @@ -1,6 +1,7 @@ package midi_test import ( + "fmt" "testing" "gitlab.com/gomidi/midi/v2" @@ -9,16 +10,12 @@ import ( func TestRealTime(t *testing.T) { tests := []struct { - msg []byte + msg interface{} expected string }{ { - midi.TimingClock(), - "TimingClock", - }, - { - midi.Tick(), - "Tick", + midi.BeatClock(), + "BeatClock", }, { midi.Start(), @@ -32,14 +29,8 @@ func TestRealTime(t *testing.T) { midi.Stop(), "Stop", }, - /* - { - midi.NewUndefined(), - "UnknownType", - }, - */ { - midi.Activesense(), + midi.ActiveSense(), "ActiveSense", }, { @@ -49,11 +40,10 @@ func TestRealTime(t *testing.T) { } for n, test := range tests { - m := midi.Message(test.msg) + m := test.msg.(fmt.Stringer) if got, want := m.String(), test.expected; got != want { t.Errorf("[%v] (% X).String() = %#v; want %#v", n, test.msg, got, want) } - } } diff --git a/v2/rpn/rpn.go b/v2/rpn/rpn.go index e7df1e2..4662b80 100644 --- a/v2/rpn/rpn.go +++ b/v2/rpn/rpn.go @@ -10,46 +10,46 @@ CC100 00 Selects pitch bend as the parameter you want to adjust. CC06 XX Sensitivity in half steps. The range is 0-24. */ -func cc(channel, ctl uint8, val uint8) midi.Message { +func cc(channel, ctl uint8, val uint8) midi.Messager { return midi.ControlChange(channel, ctl, val) } // PitchBendSensitivity sets the pitch bend range via RPN -func PitchBendSensitivity(channel, msbVal, lsbVal uint8) []midi.Message { +func PitchBendSensitivity(channel, msbVal, lsbVal uint8) []midi.Messager { return RPN(channel, 0, 0, msbVal, lsbVal) } // FineTuning -func FineTuning(channel, msbVal, lsbVal uint8) []midi.Message { +func FineTuning(channel, msbVal, lsbVal uint8) []midi.Messager { return RPN(channel, 0, 1, msbVal, lsbVal) } // CoarseTuning -func CoarseTuning(channel, msbVal, lsbVal uint8) []midi.Message { +func CoarseTuning(channel, msbVal, lsbVal uint8) []midi.Messager { return RPN(channel, 0, 2, msbVal, lsbVal) } // TuningProgramSelect -func TuningProgramSelect(channel, msbVal, lsbVal uint8) []midi.Message { +func TuningProgramSelect(channel, msbVal, lsbVal uint8) []midi.Messager { return RPN(channel, 0, 3, msbVal, lsbVal) } // TuningBankSelect -func TuningBankSelect(channel, msbVal, lsbVal uint8) []midi.Message { +func TuningBankSelect(channel, msbVal, lsbVal uint8) []midi.Messager { return RPN(channel, 0, 4, msbVal, lsbVal) } // Reset aka Null -func Reset(channel uint8) []midi.Message { - return append([]midi.Message{}, +func Reset(channel uint8) []midi.Messager { + return append([]midi.Messager{}, cc(channel, 101, 127), cc(channel, 100, 127), ) } // RPN message consisting of a val101 and val100 to identify the RPN and a msb and lsb for the value -func RPN(channel, val101, val100, msbVal, lsbVal uint8) []midi.Message { - msgs := append([]midi.Message{}, +func RPN(channel, val101, val100, msbVal, lsbVal uint8) []midi.Messager { + msgs := append([]midi.Messager{}, cc(channel, 101, val101), cc(channel, 100, val100), cc(channel, 6, msbVal), @@ -58,8 +58,8 @@ func RPN(channel, val101, val100, msbVal, lsbVal uint8) []midi.Message { return append(msgs, Reset(channel)...) } -func Increment(channel, val101, val100 uint8) []midi.Message { - msgs := append([]midi.Message{}, +func Increment(channel, val101, val100 uint8) []midi.Messager { + msgs := append([]midi.Messager{}, cc(channel, 101, val101), cc(channel, 100, val100), cc(channel, 96, 0)) @@ -67,8 +67,8 @@ func Increment(channel, val101, val100 uint8) []midi.Message { return append(msgs, Reset(channel)...) } -func Decrement(channel, val101, val100 uint8) []midi.Message { - msgs := append([]midi.Message{}, +func Decrement(channel, val101, val100 uint8) []midi.Messager { + msgs := append([]midi.Messager{}, cc(channel, 101, val101), cc(channel, 100, val100), cc(channel, 97, 0)) diff --git a/v2/smf/example_test.go b/v2/smf/example_test.go index 38a5fc5..2048b66 100644 --- a/v2/smf/example_test.go +++ b/v2/smf/example_test.go @@ -76,7 +76,7 @@ func Example() { absTicks += uint64(ev.Delta) msg := ev.Message - if msg.Type() == smf.MetaEndOfTrackMsg { + if msg.MessageType() == smf.MetaEndOfTrackMsg { // ignore continue } @@ -85,7 +85,7 @@ func Example() { case msg.GetMetaTrackName(&trackname): // set the trackname case msg.GetMetaInstrument(&trackname): // set the trackname based on instrument name default: - if m, ok := midi.Message(msg).Parse().(midi.ProgramChangeMessage); ok { + if m, ok := midi.Message(msg).TypedMessage().(midi.ProgramChangeMessage); ok { gm_name = "(" + gm.Instr(m.Program()).String() + ")" } else { fmt.Printf("track %v %s %s @%v %s\n", no, trackname, gm_name, absTicks, ev.Message) diff --git a/v2/smf/message.go b/v2/smf/message.go index 3cfe832..f4c239f 100644 --- a/v2/smf/message.go +++ b/v2/smf/message.go @@ -22,7 +22,7 @@ func (m Message) IsPlayable() bool { return false } - if m.Type() <= midi.UnknownMsg { + if m.MessageType() <= midi.MessageTypeUnknown { return false } return true @@ -37,13 +37,13 @@ func (m Message) IsMeta() bool { } // Type returns the type of the message. -func (m Message) Type() midi.Type { +func (m Message) MessageType() midi.MessageType { return getType(m) } -func getType(msg []byte) midi.Type { +func getType(msg []byte) midi.MessageType { if len(msg) == 0 { - return midi.UnknownMsg + return midi.MessageTypeUnknown } if Message(msg).IsMeta() { return getMetaType(msg[1]) @@ -53,12 +53,12 @@ func getType(msg []byte) midi.Type { } // Is returns true, if the message is of the given type. -func (m Message) Is(t midi.Type) bool { - return m.Type().Is(t) +func (m Message) Is(t midi.MessageType) bool { + return m.MessageType() == t } // IsOneOf returns true, if the message is one of the given types. -func (m Message) IsOneOf(checkers ...midi.Type) bool { +func (m Message) IsOneOf(checkers ...midi.MessageType) bool { for _, checker := range checkers { if m.Is(checker) { return true @@ -72,7 +72,7 @@ func (m Message) String() string { if m.IsMeta() { var bf bytes.Buffer - fmt.Fprintf(&bf, m.Type().String()) + fmt.Fprintf(&bf, m.MessageType().String()) var val1 uint8 var val2 uint8 @@ -104,7 +104,7 @@ func (m Message) String() string { case m.GetMetaKeySig(&val1, &val2, &bl1, &bl2): fmt.Fprintf(&bf, " key: %v num: %v ismajor: %v isflat: %v", val1, val2, bl1, bl2) default: - switch m.Type() { + switch m.MessageType() { case MetaLyricMsg, MetaMarkerMsg, MetaCopyrightMsg, MetaTextMsg, MetaCuepointMsg, MetaDeviceMsg, MetaInstrumentMsg, MetaProgramNameMsg, MetaTrackNameMsg: m.text(&text) fmt.Fprintf(&bf, " text: %q", text) diff --git a/v2/smf/meta.go b/v2/smf/meta.go index 282faa1..2b7711f 100644 --- a/v2/smf/meta.go +++ b/v2/smf/meta.go @@ -12,13 +12,13 @@ import ( ) const ( - MetaMsg midi.Type = -5 + MetaMsg midi.MessageCategory = 0x01 ) const ( // MetaChannelMsg is a MIDI channel meta message - MetaChannelMsg midi.Type = 70 + iota + MetaChannelMsg midi.MessageType = 70 + iota // MetaCopyrightMsg is a MIDI copyright meta message MetaCopyrightMsg @@ -75,8 +75,7 @@ const ( MetaProgramNameMsg ) -var msgTypeString = map[midi.Type]string{ - MetaMsg: "Meta", +var msgTypeString = map[midi.MessageType]string{ MetaChannelMsg: "MetaChannel", MetaCopyrightMsg: "MetaCopyright", MetaCuepointMsg: "MetaCuepoint", @@ -98,13 +97,13 @@ var msgTypeString = map[midi.Type]string{ MetaProgramNameMsg: "MetaProgramName", } -func init() { - for ty, name := range msgTypeString { - midi.AddTypeName(ty, name) - } -} +// func init() { +// for ty, name := range msgTypeString { +// midi.AddTypeName(ty, name) +// } +// } -func readMetaData(tp midi.Type, rd io.Reader) (data []byte, err error) { +func readMetaData(tp midi.MessageType, rd io.Reader) (data []byte, err error) { return utils.ReadVarLengthData(rd) } @@ -132,7 +131,7 @@ const ( byteProgramName = byte(0x8) ) -var metaMessages = map[byte]midi.Type{ +var metaMessages = map[byte]midi.MessageType{ byteEndOfTrack: MetaEndOfTrackMsg, byteSequenceNumber: MetaSeqNumberMsg, byteText: MetaTextMsg, @@ -154,7 +153,7 @@ var metaMessages = map[byte]midi.Type{ } // GetMetaType returns the MetaType of a meta message. It should not be used by the end consumer. -func getMetaType(b byte) midi.Type { +func getMetaType(b byte) midi.MessageType { return metaMessages[b] } diff --git a/v2/smf/reader.go b/v2/smf/reader.go index 7ca7620..fb8e197 100644 --- a/v2/smf/reader.go +++ b/v2/smf/reader.go @@ -395,7 +395,7 @@ func (r *reader) _readEvent(canary byte) (m Message, err error) { // all (event unknown) meta messages must be handled by the meta dispatcher //m, err = newMetaReader(r.input, typ).Read() //r.log("got meta: %T data: % X", m.MsgType, m.Data) - r.log("got meta: %s data: % X\n", mm.Type(), mm) + r.log("got meta: %s data: % X\n", mm.MessageType(), mm) //fmt.Printf("got meta: %s\n", mm) m = mm default: diff --git a/v2/smf/reader_test.go b/v2/smf/reader_test.go index d07e0d1..56ba56d 100644 --- a/v2/smf/reader_test.go +++ b/v2/smf/reader_test.go @@ -278,7 +278,7 @@ func TestX(t *testing.T) { tr := rd.Tracks[0] if len(tr) > 0 { - if m, ok := midi.Message(tr[0].Message).Parse().(midi.NoteOnMessage); ok { + if m, ok := midi.Message(tr[0].Message).TypedMessage().(midi.NoteOnMessage); ok { ///fmt.Printf("%s\n", tr[0].Message) var ch, key, vel uint8 = m.Channel(), m.Key(), m.Velocity() if key != 50 || vel != 33 || ch != 0 { diff --git a/v2/smf/track.go b/v2/smf/track.go index 483dc33..058a459 100644 --- a/v2/smf/track.go +++ b/v2/smf/track.go @@ -109,7 +109,7 @@ func (t *Track) SendTo(resolution MetricTicks, tc TempoChanges, receiver func(m type TracksReader struct { smf *SMF tracks map[int]bool - filter []midi.Type + filter []midi.MessageType err error } @@ -158,7 +158,7 @@ func ReadTracksFrom(rd io.Reader, tracks ...int) *TracksReader { return t } -func (t *TracksReader) Only(mtypes ...midi.Type) *TracksReader { +func (t *TracksReader) Only(mtypes ...midi.MessageType) *TracksReader { t.filter = mtypes return t } @@ -290,10 +290,10 @@ func (t *TracksReader) Do(fn func(TrackEvent)) *TracksReader { } */ msg := ev.Message - ty := msg.Type() + ty := msg.MessageType() for _, f := range t.filter { //fmt.Printf("%s [%s] %s [%s]\n", f, f.Kind().String(), ty, ty.Kind().String()) - if ty.Is(f) { + if ty == f { //if Is(f, ty) { //fn(no, msg, d, dmsec) fn(te) diff --git a/v2/smf/writer_test.go b/v2/smf/writer_test.go index 7967e92..7089a9a 100644 --- a/v2/smf/writer_test.go +++ b/v2/smf/writer_test.go @@ -337,15 +337,13 @@ func TestWriteSysEx(t *testing.T) { res.WriteString("\n") for _, ev := range trrd { - switch m := midi.Message(ev.Message).Parse().(type) { + switch m := midi.Message(ev.Message).TypedMessage().(type) { case midi.NoteOnMessage: fmt.Fprintf(&res, "[%v] NoteOn at channel %v: key %v velocity: %v\n", ev.Delta, m.Channel(), m.Key(), m.Velocity()) case midi.NoteOffMessage: fmt.Fprintf(&res, "[%v] NoteOff at channel %v: key %v\n", ev.Delta, m.Channel(), m.Key()) - default: - if ev.Message.Is(midi.SysExMsg) { - fmt.Fprintf(&res, "[%v] Sysex: % X\n", ev.Delta, ev.Message.Bytes()) - } + case midi.SysExMessage: + fmt.Fprintf(&res, "[%v] Sysex: % X\n", ev.Delta, m.Data()) } } diff --git a/v2/syscommon.go b/v2/syscommon.go index 78b3d85..5d3b98e 100644 --- a/v2/syscommon.go +++ b/v2/syscommon.go @@ -6,50 +6,12 @@ import ( "gitlab.com/gomidi/midi/v2/internal/utils" ) -/* - -System Common Message Status Byte Number of Data Bytes ---------------------- ----------- -------------------- -MIDI Timing Code F1 1 -Song Position Pointer F2 2 -Song Select F3 1 -Tune Request F6 None - -*/ - -const ( - byteMIDITimingCodeMessage = byte(0xF1) - byteSysSongPosition = byte(0xF2) - byteSysSongSelect = byte(0xF3) - byteSysTuneRequest = byte(0xF6) -) - -var syscommMessages = map[byte]Type{ - byteMIDITimingCodeMessage:/* SysCommonMsg.Set(MTCMsg), */ MTCMsg, - byteSysSongPosition:/* SysCommonMsg.Set(SPPMsg), */ SongPositionMsg, - byteSysSongSelect:/* SysCommonMsg.Set(SongSelectMsg), */ SongSelectMsg, - byteSysTuneRequest:/* SysCommonMsg.Set(TuneMsg), */ TuneMsg, -} - -// Tune returns a tune message -func Tune() Message { - //return NewMessage([]byte{byteSysTuneRequest}) - return []byte{byteSysTuneRequest} -} - -type TuneMessage Message - -func (m TuneMessage) String() string { - return fmt.Sprintf("%s", TuneMsg) -} - -// SPP returns a song position pointer message -func SongPosition(pointer uint16) Message { - return []byte{ - byteSysSongPosition, +// SongPosition returns a song position message +func SongPosition(pointer uint16) SongPositionMessage { + return SongPositionMessage([]byte{ + byte(MessageTypeSongPosition), byte(pointer & 0x7F), - byte((pointer >> 7) & 0x7F), - } + byte((pointer >> 7) & 0x7F)}) } type SongPositionMessage Message @@ -61,25 +23,48 @@ func (m SongPositionMessage) Pointer() uint16 { } func (m SongPositionMessage) String() string { - return fmt.Sprintf("%s pointer: %d", SongPositionMsg, m.Pointer()) + return fmt.Sprintf("%s pointer: %d", MessageTypeSongPosition, m.Pointer()) +} + +func (m SongPositionMessage) Message() Message { + return Message(m) } // SongSelect returns a song select message -func SongSelect(song uint8) Message { - // TODO check - it is a guess - //return NewMessage([]byte{byteSysSongSelect, song}) - return []byte{byteSysSongSelect, song} +func SongSelect(song uint8) SongSelectMessage { + return SongSelectMessage([]byte{ + byte(MessageTypeSongSelect), + song}) } type SongSelectMessage Message -func (m SongSelectMessage) Song() uint16 { +func (m SongSelectMessage) Song() uint8 { assertMessageLength(Message(m), 2) - return uint16(utils.ParseUint7(m[1])) + return utils.ParseUint7(m[1]) +} + +func (m SongSelectMessage) Message() Message { + return Message(m) } func (m SongSelectMessage) String() string { - return fmt.Sprintf("%s song: %d", SongSelectMsg, m.Song()) + return fmt.Sprintf("%s song: %d", MessageTypeSongSelect, m.Song()) +} + +// TuneRequest returns a tune request message +func TuneRequest() TuneRequestMessage { + return TuneRequestMessage([]byte{byte(MessageTypeTuneRequest)}) +} + +type TuneRequestMessage Message + +func (m TuneRequestMessage) Message() Message { + return Message(m) +} + +func (m TuneRequestMessage) String() string { + return fmt.Sprintf("%s", MessageTypeTuneRequest) } /* @@ -105,21 +90,21 @@ cdefg = Hours (0-23) 8 0111 0abc Frame Rate, and Hours MSB */ -// MTC returns a timing code message (quarter frame) -func MTC(m uint8) Message { - // TODO check - it is a guess - // TODO provide a better abstraction for MTC - //return NewMessage([]byte{byteMIDITimingCodeMessage, byte(m)}) - return []byte{byteMIDITimingCodeMessage, byte(m)} -} +// // MTC returns a timing code message (quarter frame) +// func MTC(m uint8) Message { +// // TODO check - it is a guess +// // TODO provide a better abstraction for MTC +// //return NewMessage([]byte{byteMIDITimingCodeMessage, byte(m)}) +// return []byte{byteMIDITimingCodeMessage, byte(m)} +// } -type MTCMessage Message +// type MTCMessage Message -func (m MTCMessage) QuarterFrame() uint8 { - assertMessageLength(Message(m), 2) - return utils.ParseUint7(m[1]) -} +// func (m MTCMessage) QuarterFrame() uint8 { +// assertMessageLength(Message(m), 2) +// return utils.ParseUint7(m[1]) +// } -func (m MTCMessage) String() string { - return fmt.Sprintf("%s mtc: %d", MTCMsg, m.QuarterFrame()) -} +// func (m MTCMessage) String() string { +// return fmt.Sprintf("%s mtc: %d", MTCMsg, m.QuarterFrame()) +// } diff --git a/v2/syscommon_test.go b/v2/syscommon_test.go index dcb963d..1649720 100644 --- a/v2/syscommon_test.go +++ b/v2/syscommon_test.go @@ -1,6 +1,7 @@ package midi_test import ( + "fmt" "testing" "gitlab.com/gomidi/midi/v2" @@ -9,34 +10,33 @@ import ( func TestSysCommon(t *testing.T) { tests := []struct { - msg midi.Message + msg interface{} expected string }{ { - midi.MTC(3), - "MTC mtc: 3", + midi.Message(midi.BeatClock()), + "BeatClock", }, { - midi.Tune(), - "Tune", + midi.Message(midi.TuneRequest()), + "TuneRequest", }, { - midi.SongSelect(5), + midi.Message(midi.SongSelect(5)), "SongSelect song: 5", }, { - midi.SongPosition(4), + midi.Message(midi.SongPosition(4)), "SongPosition pointer: 4", }, { - midi.SongPosition(4000), + midi.Message(midi.SongPosition(4000)), "SongPosition pointer: 4000", }, } for n, test := range tests { - //m := midi.Message(test.msg) - m := test.msg + m := test.msg.(fmt.Stringer) if got, want := m.String(), test.expected; got != want { t.Errorf("[%v] (% X).String() = %#v; want %#v", n, test.msg, got, want) diff --git a/v2/sysex.go b/v2/sysex.go index 3652bd0..5c9f1b4 100644 --- a/v2/sysex.go +++ b/v2/sysex.go @@ -2,19 +2,14 @@ package midi import "fmt" -const ( - SysExHeader uint8 = 0xf0 - SysExTrailer uint8 = 0xf7 -) - // SysEx returns a system exclusive message. Only the inner bytes must be passed, // the bytes that represent the start and end of a sysex message are added. -func SysEx(data []byte) Message { +func SysEx(data []byte) SysExMessage { m := make([]byte, len(data)+2) - m[0] = SysExHeader + m[0] = byte(MessageTypeSysEx) copy(m[1:len(m)-1], data[:]) - m[len(m)-1] = SysExTrailer - return m + m[len(m)-1] = byte(MessageTypeSysExEnd) + return SysExMessage(m) } type SysExMessage Message @@ -24,8 +19,12 @@ func (m SysExMessage) Data() []byte { return m[1 : len(m)-1] } +func (m SysExMessage) Message() Message { + return Message(m) +} + func (m SysExMessage) String() string { - return fmt.Sprintf("%s data: % X", SysExMsg, m.Data()) + return fmt.Sprintf("%s data: % X", MessageTypeSysEx, m.Data()) } /* diff --git a/v2/type.go b/v2/type.go deleted file mode 100644 index a7cfef4..0000000 --- a/v2/type.go +++ /dev/null @@ -1,339 +0,0 @@ -package midi - -import ( - "gitlab.com/gomidi/midi/v2/internal/utils" -) - -// Type is the type of a midi message -type Type int8 - -// Is returns true, if the type correspond to the given type. -func (t Type) Is(checker Type) bool { - - switch { - case t == UnknownMsg: - return checker == UnknownMsg - case t == SysExMsg: - return checker == SysExMsg - case t < UnknownMsg: - return false - case checker == UnknownMsg: - return false - case checker > UnknownMsg: - return t == checker - default: - switch checker { - case RealTimeMsg: - return t <= reservedRealTimeMsg14 - case SysCommonMsg: - return t >= MTCMsg && t <= reservedSysCommonMsg10 - case ChannelMsg: - return t >= NoteOnMsg && t <= reservedChannelMsg16 - case metaMsg: - return t >= firstMetaMsg - default: - return false - } - } -} - -// AddTypeName adds names for new types that are not part of this package (e.g. meta types from the smf package). -// Don't use this function as a user, it is only internal to the library. -// Returns false, if the type already has been named, and true on success. -func AddTypeName(m Type, name string) bool { - if _, has := typeNames[m]; has { - return false - } - typeNames[m] = name - return true -} - -var typeNames = map[Type]string{ - - UnknownMsg: "UnknownType", - RealTimeMsg: "RealTimeType", - SysCommonMsg: "SysCommonType", - ChannelMsg: "ChannelType", - SysExMsg: "SysExType", - //metaMsg: "MetaType", - - TickMsg: "Tick", - TimingClockMsg: "TimingClock", - StartMsg: "Start", - ContinueMsg: "Continue", - StopMsg: "Stop", - ActiveSenseMsg: "ActiveSense", - ResetMsg: "Reset", - - reservedRealTimeMsg8: "reservedRealTime8", - reservedRealTimeMsg9: "reservedRealTime9", - reservedRealTimeMsg10: "reservedRealTime10", - reservedRealTimeMsg11: "reservedRealTime11", - reservedRealTimeMsg12: "reservedRealTime12", - reservedRealTimeMsg13: "reservedRealTime13", - reservedRealTimeMsg14: "reservedRealTime14", - - NoteOnMsg: "NoteOn", - NoteOffMsg: "NoteOff", - ControlChangeMsg: "ControlChange", - PitchBendMsg: "PitchBend", - AfterTouchMsg: "AfterTouch", - PolyAfterTouchMsg: "PolyAfterTouch", - ProgramChangeMsg: "ProgramChange", - - reservedChannelMsg8: "reservedChannelMsg8", - reservedChannelMsg9: "reservedChannelMsg9", - reservedChannelMsg10: "reservedChannelMsg10", - reservedChannelMsg11: "reservedChannelMsg11", - reservedChannelMsg12: "reservedChannelMsg12", - reservedChannelMsg13: "reservedChannelMsg13", - reservedChannelMsg14: "reservedChannelMsg14", - reservedChannelMsg15: "reservedChannelMsg15", - reservedChannelMsg16: "reservedChannelMsg16", - - MTCMsg: "MTC", - SongSelectMsg: "SongSelect", - SongPositionMsg: "SongPosition", - TuneMsg: "Tune", - - reservedSysCommonMsg5: "reservedSysCommon5", - reservedSysCommonMsg6: "reservedSysCommon6", - reservedSysCommonMsg7: "reservedSysCommon7", - reservedSysCommonMsg8: "reservedSysCommon8", - reservedSysCommonMsg9: "reservedSysCommon9", - reservedSysCommonMsg10: "reservedSysCommon10", -} - -// String returns the name of the type. -func (t Type) String() string { - if s, has := typeNames[t]; has { - return s - } - - if t >= firstMetaMsg { - return typeNames[metaMsg] - } - - return "user defined" -} - -/* -func init() { - fmt.Printf("RT1: %v SysC10: %v Mt30: %v\n", RT1, SysC10, Mt30) -} -*/ - -const ( - // UnknownMsg is an invalid or unknown MIDI message - UnknownMsg Type = 0 - - // RealTimeMsg is a MIDI realtime message. It can only be used over the wire. - RealTimeMsg Type = -1 - - // SysCommonMsg is a MIDI system common message. It can only be used over the wire. - SysCommonMsg Type = -2 - - // ChannelMsg is a MIDI channel message. It can be used in SMF and over the wire. - ChannelMsg Type = -3 - - // SysExMsg is a MIDI system exclusive message. It can be used in SMF and over the wire. - SysExMsg Type = -4 - - // metaMsg is a MIDI meta message (used in SMF = Simple MIDI Files) - metaMsg Type = -5 -) - -const ( - - // Tick is a MIDI tick realtime message (which is a RealTimeMsg). - // There is no further data associated with messages of this type. - TickMsg Type = 1 + iota - - // TimingClock is a MIDI timing clock realtime message (which is a RealTimeMsg). - // There is no further data associated with messages of this type. - TimingClockMsg - - // Start is a MIDI start realtime message (which is a RealTimeMsg). - // There is no further data associated with messages of this type. - StartMsg - - // Continue is a MIDI continue realtime message (which is a RealTimeMsg). - // There is no further data associated with messages of this type. - ContinueMsg - - // Stop is a MIDI stop realtime message (which is a RealTimeMsg). - // There is no further data associated with messages of this type. - StopMsg - - // ActiveSense is a MIDI active sense realtime message (which is a RealTimeMsg). - // There is no further data associated with messages of this type. - ActiveSenseMsg - - // Reset is a MIDI reset realtime message (which is a RealTimeMsg). - // There is no further data associated with messages of this type. - ResetMsg - - reservedRealTimeMsg8 - reservedRealTimeMsg9 - reservedRealTimeMsg10 - reservedRealTimeMsg11 - reservedRealTimeMsg12 - reservedRealTimeMsg13 - reservedRealTimeMsg14 - - // NoteOn is a MIDI note on message (which is a ChannelMsg). - // The channel of a concrete Message of this type can be retrieved via the Channel method of the Message. - // The velocity of a concrete Message of this type can be retrieved via the Velocity method of the Message. - // The key of a concrete Message of this type can be retrieved via the Key method of the Message. - NoteOnMsg - - // NoteOff is a MIDI note off message (which is a ChannelMsg). - // The channel of a concrete Message of this type can be retrieved via the Channel method of the Message. - // The velocity of a concrete Message of this type can be retrieved via the Velocity method of the Message. - // The key of a concrete Message of this type can be retrieved via the Key method of the Message. - NoteOffMsg - - // ControlChange is a MIDI control change message (which is a ChannelMsg). - // The channel of a concrete Message of this type can be retrieved via the Channel method of the Message. - // The controller of a concrete Message of this type can be retrieved via the Controller method of the Message. - // The change of a concrete Message of this type can be retrieved via the Change method of the Message. - ControlChangeMsg - - // PitchBend is a MIDI pitch bend message (which is a ChannelMsg). - // The channel of a concrete Message of this type can be retrieved via the Channel method of the Message. - // The absolute and releative pitch of a concrete Message of this type can be retrieved via the Pitch method of the Message. - PitchBendMsg - - // AfterTouch is a MIDI after touch message (which is a ChannelMsg). - // The channel of a concrete Message of this type can be retrieved via the Channel method of the Message. - // The pressure of a concrete Message of this type can be retrieved via the Pressure method of the Message. - AfterTouchMsg - - // PolyAfterTouch is a polyphonic MIDI after touch message (which is a ChannelMsg). - // The channel of a concrete Message of this type can be retrieved via the Channel method of the Message. - // The key of a concrete Message of this type can be retrieved via the Key method of the Message. - // The pressure of a concrete Message of this type can be retrieved via the Pressure method of the Message. - PolyAfterTouchMsg - - // ProgramChange is a MIDI program change message (which is a ChannelMsg). - // The channel of a concrete Message of this type can be retrieved via the Channel method of the Message. - // The program number of a concrete Message of this type can be retrieved via the Program method of the Message. - ProgramChangeMsg - - reservedChannelMsg8 - reservedChannelMsg9 - reservedChannelMsg10 - reservedChannelMsg11 - reservedChannelMsg12 - reservedChannelMsg13 - reservedChannelMsg14 - reservedChannelMsg15 - reservedChannelMsg16 - - // MTC is a MIDI MTC system common message. - // TODO add method to Message to get the quarter frame and document it. - MTCMsg - - // SongSelect is a MIDI song select system common message. - // TODO add method to Message to get the song number and document it. - SongSelectMsg - - // SongPosition is a MIDI song position pointer (SPP) system common message. - // TODO add method to Message to get the song position pointer and document it. - SongPositionMsg - - // Tune is a MIDI tune request system common message. - // There is no further data associated with messages of this type. - TuneMsg - - reservedSysCommonMsg5 - reservedSysCommonMsg6 - reservedSysCommonMsg7 - reservedSysCommonMsg8 - reservedSysCommonMsg9 - reservedSysCommonMsg10 -) - -const ( - // everything >= firstMeta are meta messages - firstMetaMsg Type = 70 -) - -/* -GetMsgType returns the message type for the given message (bytes that must include a status byte - no running status). - -The returned MsgType will be a combination of message types, if appropriate (binary flags). For example: -A note on message on channel 0 will have a message type that is a combination of a ChannelMsg, a Channel0Msg, and a NoteOnMsg. -A tempo meta message of a SMF file will have a message type that is a combination of a MetaMsg, and a MetaTempoMsg. -*/ -func getType(bt []byte) (mType Type) { - //fmt.Printf("GetMsgType % X\n", msg) - if len(bt) == 0 { - return UnknownMsg - } - byte1 := bt[0] - - switch { - // channel/Voice Category Status - case byte1 >= 0x80 && byte1 <= 0xEF: - return getChannelType(byte1) - case byte1 == 0xF0, byte1 == 0xF7: - // TODO what about sysex start stop etc. - return SysExMsg - case byte1 == 0xFF: - /* - if byte2 > 0 { - return MetaMsgType - } - */ - return getRealtimeType(byte1) - case byte1 < 0xF7: - return getSysCommonType(byte1) - case byte1 > 0xF7: - return getRealtimeType(byte1) - default: - return UnknownMsg - } -} - -// GetChannelMsgType returns the MsgType of a channel message. It should not be used by the end consumer. -func getChannelType(canary byte) (mType Type) { - tp, _ := utils.ParseStatus(canary) - - switch tp { - case 0xC: - return ProgramChangeMsg - case 0xD: - return AfterTouchMsg - case 0x8: - return NoteOffMsg - case 0x9: - return NoteOnMsg - case 0xA: - return PolyAfterTouchMsg - case 0xB: - return ControlChangeMsg - case 0xE: - return PitchBendMsg - default: - return UnknownMsg - } -} - -// getRealtimeMsgType returns the MsgType of a realtime message. It should not be used by the end consumer. -func getRealtimeType(b byte) Type { - ty, has := rtMessages[b] - if !has { - return UnknownMsg - } - return ty -} - -// getSysCommonMsgType returns the MsgType of a sys common message. It should not be used by the end consumer. -func getSysCommonType(b byte) Type { - ty, has := syscommMessages[b] - if !has { - return UnknownMsg - } - return ty -} -- GitLab From 5650c83477baee59b0b951c926c24e895b27f8d8 Mon Sep 17 00:00:00 2001 From: Brian Stark Date: Fri, 15 Apr 2022 02:32:02 -0600 Subject: [PATCH 4/6] update to use context --- v2/channel_test.go | 37 +++ v2/doc.go | 16 +- v2/drivers/midicatdrv/in.go | 19 +- v2/drivers/port.go | 7 +- v2/drivers/portmididrv/in.go | 15 +- .../rtmididrv/imported/rtmidi/rtmidi.go | 10 +- v2/drivers/rtmididrv/in.go | 303 +----------------- v2/drivers/testdrv/driver.go | 10 +- v2/drivers/webmididrv/in.go | 13 +- v2/error.go | 1 + v2/example/logger/main.go | 11 +- v2/example/smfrecorder/main.go | 8 +- v2/example_test.go | 9 +- v2/itter_test.go | 37 +++ v2/listen.go | 29 +- v2/pitchbend_test.go | 42 --- v2/smf/smf.go | 34 +- v2/smf/track.go | 5 +- 18 files changed, 191 insertions(+), 415 deletions(-) create mode 100644 v2/itter_test.go delete mode 100644 v2/pitchbend_test.go diff --git a/v2/channel_test.go b/v2/channel_test.go index e6e6f18..5d2d22b 100644 --- a/v2/channel_test.go +++ b/v2/channel_test.go @@ -144,3 +144,40 @@ func TestChannelRaw(t *testing.T) { } } } + +func TestPitchbend(t *testing.T) { + + tests := []struct { + in int16 + expected uint16 + }{ + { + in: 0, + expected: 8192, + }, + { + in: PitchMaximum, + expected: 16383, + }, + { + in: PitchMaximum + 1, + expected: 16383, + }, + { + in: PitchMinimum, + expected: 0, + }, + { + in: PitchMinimum - 1, + expected: 0, + }, + } + + for _, test := range tests { + m := PitchBend(0, test.in) + + if abs := m.Absolute(); abs != test.expected { + t.Errorf("Pitchbend(%v).absValue = %v; wanted %v", test.in, abs, test.expected) + } + } +} diff --git a/v2/doc.go b/v2/doc.go index 30e3dc8..638f04a 100644 --- a/v2/doc.go +++ b/v2/doc.go @@ -15,20 +15,22 @@ The data can be retrieved with the corresponding Get* method. // convert to Message type msg := midi.Message(b) - var channel, key, velocity uint8 - if msg.GetNoteOn(&channel, &key, &velocity) { - fmt.Printf("got %s: channel: %v key: %v, velocity: %v\n", msg.Type(), channel, key, velocity) + if m, ok := msg.TypedMessage().(midi.NoteOnMessage); ok { + fmt.Printf("got %s: channel: %v key: %v, velocity: %v\n", msg.MessageType(), m.Channel(), m.Key(), m.Velocity()) + } else { + fmt.Printf("noteon message was not valid") } Received messages can be categorized by their type, e.g. - switch msg.Type() { - case midi.NoteOnMsg, midi.NoteOffMsg: + switch t := msg.MessageType(); t { + case midi.MessageTypeNoteOn, midi.MessageTypeNoteOff: // do something - case midi.ControlChangeMsg: + case midi.MessageTypeControlChange: // do some other thing default: - if msg.Is(midi.RealTimeMsg) || msg.Is(midi.SysCommonMsg) || msg.Is(midi.SysExMsg) { + c := t.MessageCategory() + if c == midi.MessageCategoryRealtime || c == midi.MessageCategoryCommon { // ignore } } diff --git a/v2/drivers/midicatdrv/in.go b/v2/drivers/midicatdrv/in.go index 5d0d17d..b45ea39 100644 --- a/v2/drivers/midicatdrv/in.go +++ b/v2/drivers/midicatdrv/in.go @@ -1,11 +1,13 @@ package midicatdrv import ( + "context" "fmt" "io" "runtime" "sync" + "gitlab.com/gomidi/midi/v2" "gitlab.com/gomidi/midi/v2/drivers" lib "gitlab.com/gomidi/midi/v2/tools/midicat" ) @@ -163,8 +165,8 @@ func newIn(driver *Driver, number int, name string) drivers.In { return &in{driver: driver, number: number, name: name} } -func (i *in) Listen(onMsg func(msg []byte, absmilliseconds int32), config drivers.ListenConfig) (stopFn func(), err error) { - stopFn = func() { +func (i *in) Listen(ctx context.Context, onMsg func(msg []byte, absmilliseconds int32), config drivers.ListenConfig) error { + stopFn := func() { if !i.IsOpen() { return } @@ -172,18 +174,23 @@ func (i *in) Listen(onMsg func(msg []byte, absmilliseconds int32), config driver <-i.didStopListening } + go func(ctx context.Context) { + <-ctx.Done() + stopFn() + }(ctx) + if !i.IsOpen() { - return nil, drivers.ErrPortClosed + return drivers.ErrPortClosed } if onMsg == nil { - return nil, fmt.Errorf("onMsg callback must not be nil") + return midi.ErrMessageCallbackNil } i.RLock() if i.listener != nil { i.RUnlock() - return nil, fmt.Errorf("listener already set") + return fmt.Errorf("listener already set") } i.RUnlock() @@ -196,7 +203,7 @@ func (i *in) Listen(onMsg func(msg []byte, absmilliseconds int32), config driver } i.Unlock() - return stopFn, nil + return nil } /* diff --git a/v2/drivers/port.go b/v2/drivers/port.go index 572a071..2373756 100644 --- a/v2/drivers/port.go +++ b/v2/drivers/port.go @@ -1,6 +1,7 @@ package drivers import ( + "context" "fmt" "strings" ) @@ -63,12 +64,10 @@ type In interface { // The config defines further listening options (see ListenConfig) // The listening must be stopped before the port may be closed. Listen( + ctx context.Context, onMsg func(msg []byte, milliseconds int32), config ListenConfig, - ) ( - stopFn func(), - err error, - ) + ) error } // Out is an interface for a MIDI output port. diff --git a/v2/drivers/portmididrv/in.go b/v2/drivers/portmididrv/in.go index 0d9e265..d643b65 100644 --- a/v2/drivers/portmididrv/in.go +++ b/v2/drivers/portmididrv/in.go @@ -2,10 +2,12 @@ package portmididrv import ( "bytes" + "context" "fmt" "sync/atomic" "time" + "gitlab.com/gomidi/midi/v2" "gitlab.com/gomidi/midi/v2/drivers" "gitlab.com/gomidi/midi/v2/drivers/portmididrv/imported/portmidi" ) @@ -76,10 +78,10 @@ func (i *in) Open() (err error) { } // Listen -func (i *in) Listen(onMsg func(msg []byte, milliseconds int32), config drivers.ListenConfig) (stopFn func(), err error) { +func (i *in) Listen(ctx context.Context, onMsg func(msg []byte, milliseconds int32), config drivers.ListenConfig) error { if onMsg == nil { - return nil, fmt.Errorf("onMsg callback must not be nil") + return midi.ErrMessageCallbackNil } var filters []int @@ -115,12 +117,17 @@ func (i *in) Listen(onMsg func(msg []byte, milliseconds int32), config drivers.L var stop int32 stopWait := i.driver.sleepingTime * 2 - stopFn = func() { + stopFn := func() { // lockless sync atomic.StoreInt32(&stop, 1) time.Sleep(stopWait) } + go func(ctx context.Context) { + <-ctx.Done() + stopFn() + }(ctx) + go func() { var stopped int32 @@ -278,5 +285,5 @@ func (i *in) Listen(onMsg func(msg []byte, milliseconds int32), config drivers.L }() time.Sleep(time.Millisecond * 2) - return + return nil } diff --git a/v2/drivers/rtmididrv/imported/rtmidi/rtmidi.go b/v2/drivers/rtmididrv/imported/rtmidi/rtmidi.go index f98402b..88cd4c5 100644 --- a/v2/drivers/rtmididrv/imported/rtmidi/rtmidi.go +++ b/v2/drivers/rtmididrv/imported/rtmidi/rtmidi.go @@ -39,15 +39,15 @@ const ( // APIUnspecified searches for a working compiled API. APIUnspecified API = C.RTMIDI_API_UNSPECIFIED // APIMacOSXCore uses Macintosh OS-X CoreMIDI API. - APIMacOSXCore = C.RTMIDI_API_MACOSX_CORE + APIMacOSXCore API = C.RTMIDI_API_MACOSX_CORE // APILinuxALSA uses the Advanced Linux Sound Architecture API. - APILinuxALSA = C.RTMIDI_API_LINUX_ALSA + APILinuxALSA API = C.RTMIDI_API_LINUX_ALSA // APIUnixJack uses the JACK Low-Latency MIDI Server API. - APIUnixJack = C.RTMIDI_API_UNIX_JACK + APIUnixJack API = C.RTMIDI_API_UNIX_JACK // APIWindowsMM uses the Microsoft Multimedia MIDI API. - APIWindowsMM = C.RTMIDI_API_WINDOWS_MM + APIWindowsMM API = C.RTMIDI_API_WINDOWS_MM // APIDummy is a compilable but non-functional API. - APIDummy = C.RTMIDI_API_RTMIDI_DUMMY + APIDummy API = C.RTMIDI_API_RTMIDI_DUMMY ) func (api API) String() string { diff --git a/v2/drivers/rtmididrv/in.go b/v2/drivers/rtmididrv/in.go index 8c906b1..c4e0a30 100644 --- a/v2/drivers/rtmididrv/in.go +++ b/v2/drivers/rtmididrv/in.go @@ -1,9 +1,11 @@ package rtmididrv import ( + "context" "fmt" "math" + "gitlab.com/gomidi/midi/v2" "gitlab.com/gomidi/midi/v2/drivers" "gitlab.com/gomidi/midi/v2/drivers/rtmididrv/imported/rtmidi" ) @@ -121,315 +123,30 @@ func newIn(driver *Driver, number int, name string) drivers.In { return &in{driver: driver, number: number, name: name} } -func (i *in) Listen(onMsg func(msg []byte, milliseconds int32), config drivers.ListenConfig) (stopFn func(), err error) { +func (i *in) Listen(ctx context.Context, onMsg func(msg []byte, milliseconds int32), config drivers.ListenConfig) error { if onMsg == nil { - return nil, fmt.Errorf("onMsg callback must not be nil") + return midi.ErrMessageCallbackNil } i.midiIn.IgnoreTypes(!config.SysEx, !config.TimeCode, !config.ActiveSense) - //var inSysEx bool if config.SysExBufferSize == 0 { config.SysExBufferSize = 1024 } - /* - maxlenSysex := int(config.SysExBufferSize) - var sysexBf = make([]byte, maxlenSysex) - var sysexlen int - - var ts_ms int32 - //var stop int32 - var state = readerStateClean - var statusByte uint8 - //var channel uint8 - //var bf [2]byte // first: is set, second: the byte - var issetBf bool - var bf byte - var typ uint8 - */ - var rd = drivers.NewReader(config, onMsg) - /* - rd.OnErr = config.OnErr - rd.OnSysEx = config.OnSysEx - rd.OnMsg = onMsg - rd.SysExBufferSize = config.SysExBufferSize - */ - - //stopWait := i.driver.sleepingTime * 2 - stopFn = func() { - // lockless sync - // atomic.StoreInt32(&stop, 1) - // fmt.Println("stopping") - i.midiIn.CancelCallback() - //time.Sleep(stopWait) - } - - /* - withinChannelMessage := func(b byte) { - switch typ { - case byteChannelPressure: - issetBf = false - state = readerStateClean - //p.receiver.Receive(Channel(p.channel).Aftertouch(b), p.timestamp) - onMsg([3]byte{statusByte, b, 0}, ts_ms) - case byteProgramChange: - issetBf = false // first: is set, second: the byte - state = readerStateClean - //p.receiver.Receive(Channel(p.channel).ProgramChange(b), p.timestamp) - onMsg([3]byte{statusByte, b, 0}, ts_ms) - case byteControlChange: - if issetBf { - issetBf = false // first: is set, second: the byte - state = readerStateClean - //p.receiver.Receive(Channel(p.channel).ControlChange(p.getBf(), b), p.timestamp) - onMsg([3]byte{statusByte, b, bf}, ts_ms) - } else { - issetBf = true - bf = b - } - case byteNoteOn: - if issetBf { - issetBf = false // first: is set, second: the byte - state = readerStateClean - //p.receiver.Receive(Channel(p.channel).NoteOn(p.getBf(), b), p.timestamp) - onMsg([3]byte{statusByte, b, bf}, ts_ms) - } else { - issetBf = true - bf = b - } - case byteNoteOff: - if issetBf { - issetBf = false // first: is set, second: the byte - state = readerStateClean - //p.receiver.Receive(Channel(p.channel).NoteOffVelocity(p.getBf(), b), p.timestamp) - onMsg([3]byte{statusByte, b, bf}, ts_ms) - } else { - issetBf = true - bf = b - } - case bytePolyphonicKeyPressure: - if issetBf { - issetBf = false // first: is set, second: the byte - state = readerStateClean - //p.receiver.Receive(Channel(p.channel).PolyAftertouch(p.getBf(), b), p.timestamp) - onMsg([3]byte{statusByte, b, bf}, ts_ms) - } else { - issetBf = true - bf = b - } - case bytePitchWheel: - if issetBf { - //rel, abs := midilib.ParsePitchWheelVals(bf, b) - //_ = abs - issetBf = false // first: is set, second: the byte - state = readerStateClean - //p.receiver.Receive(Channel(p.channel).Pitchbend(rel), p.timestamp) - onMsg([3]byte{statusByte, b, bf}, ts_ms) - } else { - issetBf = true - bf = b - } - default: - panic("unknown typ") - } - } - */ - - /* - cleanState := func(b byte) { - switch { - - // start sysex - case b == 0xF0: - statusByte = 0 - sysexBf = make([]byte, maxlenSysex) - //sysexBf.Reset() - //sysexBf.WriteByte(b) - sysexBf[0] = b - sysexlen = 1 - state = readerStateInSysEx - // end sysex - // [MIDI] permits 0xF7 octets that are not part of a (0xF0, 0xF7) pair - // to appear on a MIDI 1.0 DIN cable. Unpaired 0xF7 octets have no - // semantic meaning in MIDI apart from cancelling running status. - case b == 0xF7: - sysexBf = nil - sysexlen = 0 - statusByte = 0 - onMsg([3]byte{b, 0, 0}, ts_ms) - - // here we clear for System Common Category messages - case b > 0xF0 && b < 0xF7: - statusByte = 0 - issetBf = false // reset buffer - //fmt.Printf("sys common msg started\n") - switch b { - case byteMIDITimingCodeMessage, byteSysSongPositionPointer, byteSysSongSelect: - state = readerStateWithinSysCommon - typ = b - case byteSysTuneRequest: - onMsg([3]byte{b, 0, 0}, ts_ms) - // - // if p.syscommonHander != nil { - // p.syscommonHander(Tune(), p.timestamp) - // } - - return - default: - // 0xF4, 0xF5, or 0xFD - state = readerStateWithinUnknown - return - } - - // channel message with status byte - case b >= 0x80 && b <= 0xEF: - statusByte = b - issetBf = false // reset buffer - //typ, channel = midilib.ParseStatus(statusByte) - typ, _ = midilib.ParseStatus(statusByte) - state = readerStateWithinChannelMessage - default: - if statusByte != 0 { - state = readerStateWithinChannelMessage - withinChannelMessage(b) - } - } - } - */ - go i.midiIn.SetCallback(func(in rtmidi.MIDIIn, bt []byte, deltaSeconds float64) { - /* - var stopped int32 + go func(ctx context.Context) { + defer i.Close() - // lockless sync - stopped = atomic.LoadInt32(&stop) - - if stopped == 1 { - if config.OnErr != nil { - config.OnErr(drivers.ErrListenStopped) - } - fmt.Printf("stopped") - in.CancelCallback() - return - } - */ + <-ctx.Done() + i.midiIn.CancelCallback() + }(ctx) + return i.midiIn.SetCallback(func(in rtmidi.MIDIIn, bt []byte, deltaSeconds float64) { rd.EachMessage(bt, int32(math.Round(deltaSeconds*1000))) - - /* - // TODO: verify - // assume that each call is without running state - statusByte = 0 - issetBf = false // first: is set, second: the byte - - ts_ms += int32(math.Round(deltaSeconds * 1000)) - - // fmt.Printf("got % X\n", bt) - - for _, b := range bt { - // => realtime message - if b >= 0xF8 { - onMsg([3]byte{b, 0, 0}, ts_ms) - continue - } - - //fmt.Printf("state: %v\n", p.state) - - switch state { - case readerStateInSysEx: - // interrupted sysex, discard old data - if b == 0xF0 { - statusByte = 0 - sysexBf = make([]byte, maxlenSysex) - //sysexBf.Reset() - //sysexBf.WriteByte(b) - sysexBf[0] = b - sysexlen = 1 - state = readerStateInSysEx - continue - } - - if b == 0xF7 { - state = readerStateClean - if config.OnSysEx != nil { - sysexBf[sysexlen] = b - sysexlen++ - go func(bb []byte, l int) { - var _bt = make([]byte, l) - - for i := 0; i < l; i++ { - _bt[i] = bb[i] - } - config.OnSysEx(_bt) - }(sysexBf, sysexlen) - } - sysexBf = nil - sysexlen = 0 - continue - } - if midilib.IsStatusByte(b) { - //p.sysexBf.Reset() - sysexBf = nil - sysexlen = 0 - state = readerStateClean - cleanState(b) - continue - } - - if config.OnSysEx != nil { - sysexBf[sysexlen] = b - sysexlen++ - } - - case readerStateClean: - cleanState(b) - case readerStateWithinUnknown: - //p.withinUnknown(b) - if midilib.IsStatusByte(b) { - state = readerStateClean - cleanState(b) - } - case readerStateWithinSysCommon: - switch typ { - case byteMIDITimingCodeMessage: - issetBf = false - state = readerStateClean - onMsg([3]byte{typ, b, 0}, ts_ms) - case byteSysSongPositionPointer: - if issetBf { - issetBf = false - state = readerStateClean - onMsg([3]byte{typ, b, bf}, ts_ms) - } else { - issetBf = true - bf = b - } - case byteSysSongSelect: - issetBf = false - state = readerStateClean - onMsg([3]byte{typ, b, 0}, ts_ms) - case byteSysTuneRequest: - //panic("must not be handled here, but within clean state") - default: - if config.OnErr != nil { - config.OnErr(fmt.Errorf("unknown syscommon message: % X", b)) - } - //panic("unknown syscommon") - } - case readerStateWithinChannelMessage: - withinChannelMessage(b) - default: - panic(fmt.Sprintf("unknown state %v, must not happen", state)) - } - } - */ - }) - - return stopFn, nil } /* diff --git a/v2/drivers/testdrv/driver.go b/v2/drivers/testdrv/driver.go index e3381c6..8b80f55 100644 --- a/v2/drivers/testdrv/driver.go +++ b/v2/drivers/testdrv/driver.go @@ -9,6 +9,7 @@ Package testdrv provides a Driver for testing. package testdrv import ( + "context" "time" "gitlab.com/gomidi/midi/v2/drivers" @@ -53,16 +54,17 @@ func (f *in) Number() int { return f.number } func (f *in) IsOpen() bool { return f.isOpen } func (f *in) Underlying() interface{} { return nil } -func (f *in) Listen(onMsg func([]byte, int32), conf drivers.ListenConfig) (func(), error) { +func (f *in) Listen(ctx context.Context, onMsg func([]byte, int32), conf drivers.ListenConfig) error { f.driver.last = time.Now() - stopper := func() { + go func(ctx context.Context) { + <-ctx.Done() f.driver.stopListening = true - } + }(ctx) f.driver.rd = drivers.NewReader(conf, onMsg) - return stopper, nil + return nil } func (f *in) Close() error { diff --git a/v2/drivers/webmididrv/in.go b/v2/drivers/webmididrv/in.go index e80da6c..20c6f87 100644 --- a/v2/drivers/webmididrv/in.go +++ b/v2/drivers/webmididrv/in.go @@ -1,6 +1,7 @@ package webmididrv import ( + "context" "math" "sync" "sync/atomic" @@ -105,16 +106,14 @@ func newIn(driver *Driver, number int, name string, jsport js.Value) drivers.In */ -func (i *in) Listen(onMsg func(msg []byte, milliseconds int32), config drivers.ListenConfig) (stopFn func(), err error) { +func (i *in) Listen(ctx context.Context, onMsg func(msg []byte, milliseconds int32), config drivers.ListenConfig) error { var stop int32 - //stopWait := i.driver.sleepingTime * 2 - stopFn = func() { - // lockless sync + go func(ctx context.Context) { + <-ctx.Done() atomic.StoreInt32(&stop, 1) - //time.Sleep(stopWait) - } + }(ctx) i.Lock() @@ -147,7 +146,7 @@ func (i *in) Listen(onMsg func(msg []byte, milliseconds int32), config drivers.L go i.jsport.Call("addEventListener", "midimessage", jsCallback) i.Unlock() - return + return nil } /* diff --git a/v2/error.go b/v2/error.go index 3ee5b6e..d111bf3 100644 --- a/v2/error.go +++ b/v2/error.go @@ -17,6 +17,7 @@ const ( ErrSysExTrailerInvalid Error = "sysex message trailer not expected" ErrSysExCallback Error = "driver error: received 0xF0 in non sysex callback" ErrMessageCategory Error = "message category not expected" + ErrMessageCallbackNil Error = "message callback must be supplied" ) func assertMessageLength(msg Message, expLen int) { diff --git a/v2/example/logger/main.go b/v2/example/logger/main.go index fd1ee8d..baae39e 100644 --- a/v2/example/logger/main.go +++ b/v2/example/logger/main.go @@ -1,6 +1,7 @@ package main import ( + "context" "fmt" "time" @@ -17,7 +18,9 @@ func main() { return } - stop, err := midi.ListenTo(in, func(msg midi.Message, timestampms int32) { + ctx, cancel := context.WithCancel(context.Background()) + + if err := midi.Listen(ctx, midi.FindInPortByName("VMPK"), func(msg midi.Message, timestampms int32) { switch m := msg.TypedMessage().(type) { case midi.SysExMessage: fmt.Printf("got sysex: % X\n", m.Data()) @@ -28,14 +31,12 @@ func main() { default: // ignore } - }, midi.UseSysEx()) - - if err != nil { + }, midi.UseSysEx()); err != nil { fmt.Printf("ERROR: %s\n", err) return } time.Sleep(time.Second * 5) - stop() + cancel() } diff --git a/v2/example/smfrecorder/main.go b/v2/example/smfrecorder/main.go index 3217001..244feac 100644 --- a/v2/example/smfrecorder/main.go +++ b/v2/example/smfrecorder/main.go @@ -1,6 +1,7 @@ package main import ( + "context" "fmt" "time" @@ -28,15 +29,16 @@ func run() error { return fmt.Errorf("can't find MIDI in port %q", "VMPK") } - stop, err := smf.RecordTo(in, 120, "recordedx.mid") + ctx, cancel := context.WithCancel(context.Background()) + defer cancel() - if err != nil { + if err := smf.RecordTo(ctx, in, 120, "recordedx.mid"); err != nil { return err } time.Sleep(5 * time.Second) - stop() + cancel() out := midi.FindOutPort("qsynth") if out < 0 { diff --git a/v2/example_test.go b/v2/example_test.go index 493bad7..21d97e5 100644 --- a/v2/example_test.go +++ b/v2/example_test.go @@ -1,6 +1,7 @@ package midi_test import ( + "context" "fmt" "os" "testing" @@ -62,9 +63,13 @@ func TestExample(t *testing.T) { // here we take first in port, for real, consider // var in = InByName("my midi keyboard") + ctx, cancel := context.WithCancel(context.Background()) + // listens to the in port and calls eachMessage for every message. // any running status bytes are converted and only complete messages are passed to the eachMessage. - stop, _ := ListenTo(in, eachMessage) + if err := Listen(ctx, FindInPortByNumber(in), eachMessage); err != nil { + t.Fatal(err) + } // send some messages send(Message(NoteOn(0, Db(4), 100))) @@ -77,7 +82,7 @@ func TestExample(t *testing.T) { send(Message(ProgramChange(1, 12))) send(Message(ControlChange(2, FootPedalMSB, On))) // stops listening - stop() + cancel() // Output: // note started at 0ms channel: 0 key: 61 velocity: 100 diff --git a/v2/itter_test.go b/v2/itter_test.go new file mode 100644 index 0000000..72456ea --- /dev/null +++ b/v2/itter_test.go @@ -0,0 +1,37 @@ +package midi + +import "testing" + +type S [12]int64 + +var sX = make([]S, 1000) +var sY = make([]S, 1000) +var sZ = make([]S, 1000) +var sumX, sumY, sumZ int64 + +func Benchmark_Loop(b *testing.B) { + for i := 0; i < b.N; i++ { + sumX = 0 + for j := 0; j < len(sX); j++ { + sumX += sX[j][0] + } + } +} + +func Benchmark_Range_OneIterVar(b *testing.B) { + for i := 0; i < b.N; i++ { + sumY = 0 + for j := range sY { + sumY += sY[j][0] + } + } +} + +func Benchmark_Range_TwoIterVar(b *testing.B) { + for i := 0; i < b.N; i++ { + sumZ = 0 + for _, v := range sZ { + sumZ += v[0] + } + } +} diff --git a/v2/listen.go b/v2/listen.go index c9f1ef8..6ff4475 100644 --- a/v2/listen.go +++ b/v2/listen.go @@ -1,6 +1,7 @@ package midi import ( + "context" "fmt" "gitlab.com/gomidi/midi/v2/drivers" @@ -47,19 +48,31 @@ func HandleError(cb func(error)) Option { var ErrPortClosed = drivers.ErrPortClosed var ErrListenStopped = drivers.ErrListenStopped +type ListenPortFinderFunc func() (drivers.In, error) + +func FindInPortByName(name string) ListenPortFinderFunc { + return ListenPortFinderFunc(func() (drivers.In, error) { + return drivers.InByName(name) + }) +} + +func FindInPortByNumber(portNum int) ListenPortFinderFunc { + return ListenPortFinderFunc(func() (drivers.In, error) { + return drivers.InByNumber(portNum) + }) +} + // ListenTo listens on the given port number and passes the received MIDI data to the given receiver. // It returns a stop function that may be called to stop the listening. -func ListenTo(portno int, recv func(msg Message, timestampms int32), opts ...Option) (stop func(), err error) { - in, err := drivers.InByNumber(portno) +func Listen(ctx context.Context, inPort ListenPortFinderFunc, recv func(msg Message, timestampms int32), opts ...Option) error { + in, err := inPort() if err != nil { - return nil, err + return err } if !in.IsOpen() { - err = in.Open() - - if err != nil { - return nil, err + if err := in.Open(); err != nil { + return err } } @@ -84,5 +97,5 @@ func ListenTo(portno int, recv func(msg Message, timestampms int32), opts ...Opt recv(Message(data), millisec) } - return in.Listen(onMsg, conf) + return in.Listen(ctx, onMsg, conf) } diff --git a/v2/pitchbend_test.go b/v2/pitchbend_test.go deleted file mode 100644 index 2ebce54..0000000 --- a/v2/pitchbend_test.go +++ /dev/null @@ -1,42 +0,0 @@ -package midi - -import ( - "testing" -) - -func TestPitchbend(t *testing.T) { - - tests := []struct { - in int16 - expected uint16 - }{ - { - in: 0, - expected: 8192, - }, - { - in: PitchMaximum, - expected: 16383, - }, - { - in: PitchMaximum + 1, - expected: 16383, - }, - { - in: PitchMinimum, - expected: 0, - }, - { - in: PitchMinimum - 1, - expected: 0, - }, - } - - for _, test := range tests { - m := PitchBend(0, test.in) - - if abs := m.Absolute(); abs != test.expected { - t.Errorf("Pitchbend(%v).absValue = %v; wanted %v", test.in, abs, test.expected) - } - } -} diff --git a/v2/smf/smf.go b/v2/smf/smf.go index 58d88ac..4c6aa57 100644 --- a/v2/smf/smf.go +++ b/v2/smf/smf.go @@ -1,6 +1,7 @@ package smf import ( + "context" "fmt" "io" "os" @@ -61,48 +62,35 @@ type SMF struct { // RecordTo records from the given midi in port into the given filename with the given tempo. // It returns a stop function that must be called to stop the recording. The file is then completed and saved. -func RecordTo(inport int, bpm float64, filename string) (stop func() error, err error) { +func RecordTo(ctx context.Context, inport int, bpm float64, filename string) error { file := New() - _stop, _err := file.RecordFrom(inport, bpm) - - if _err != nil { - _stop() - return nil, _err + if err := file.RecordFrom(ctx, inport, bpm); err != nil { + return err } - - return func() error { - _stop() - return file.WriteFile(filename) - }, nil + return nil } // Record records from the given midi in port into a new track. // It returns a stop function that must be called to stop the recording. // It is up to the user to save the SMF. -func (s *SMF) RecordFrom(inport int, bpm float64) (stop func(), err error) { +func (s *SMF) RecordFrom(ctx context.Context, inport int, bpm float64) error { ticks := s.TimeFormat.(MetricTicks) //tr := NewTrack() var tr Track - _stop, _err := tr.RecordFrom(inport, ticks, bpm) + ctx, cancel := context.WithCancel(ctx) - if _err != nil { - _stop() + if err := tr.RecordFrom(ctx, inport, ticks, bpm); err != nil { + cancel() time.Sleep(time.Second) //s.AddAndClose(0, tr) tr.Close(0) s.Tracks = append(s.Tracks, tr) - return nil, _err + return err } - return func() { - _stop() - time.Sleep(time.Second) - //s.AddAndClose(0, tr) - tr.Close(0) - s.Tracks = append(s.Tracks, tr) - }, nil + return nil } func (s *SMF) TempoChanges() TempoChanges { diff --git a/v2/smf/track.go b/v2/smf/track.go index 058a459..4c518c0 100644 --- a/v2/smf/track.go +++ b/v2/smf/track.go @@ -1,6 +1,7 @@ package smf import ( + "context" "fmt" "io" "sort" @@ -80,11 +81,11 @@ func (t *Track) Add(deltaticks uint32, msgs ...[]byte) { } } -func (t *Track) RecordFrom(portno int, ticks MetricTicks, bpm float64) (stop func(), err error) { +func (t *Track) RecordFrom(ctx context.Context, portno int, ticks MetricTicks, bpm float64) error { t.Add(0, MetaTempo(bpm)) var absmillisec int32 //ticks := file.TimeFormat.(smf.MetricTicks) - return midi.ListenTo(portno, func(msg midi.Message, absms int32) { + return midi.Listen(ctx, midi.FindInPortByNumber(portno), func(msg midi.Message, absms int32) { deltams := absms - absmillisec absmillisec = absms //fmt.Printf("[%v] %s\n", deltams, msg.String()) -- GitLab From c6ad60277cc026cba44bb1f26745f42ec1d83ed7 Mon Sep 17 00:00:00 2001 From: Brian Stark Date: Sat, 16 Apr 2022 03:47:00 -0600 Subject: [PATCH 5/6] move driver --- v2/channel.go | 108 ++++------ v2/channel_test.go | 150 ++------------ v2/driver.go | 94 +++++++++ v2/drivers/.gitignore | 1 - v2/drivers/driver.go | 50 ----- v2/drivers/midicatdrv/driver.go | 11 +- v2/drivers/midicatdrv/helpers.go | 6 +- v2/drivers/midicatdrv/in.go | 13 +- v2/drivers/midicatdrv/out.go | 6 +- v2/drivers/port.go | 217 --------------------- v2/drivers/portmididrv/driver.go | 20 +- v2/drivers/portmididrv/in.go | 7 +- v2/drivers/portmididrv/out.go | 8 +- v2/drivers/rtmididrv/driver.go | 14 +- v2/drivers/rtmididrv/in.go | 27 ++- v2/drivers/rtmididrv/out.go | 56 +++--- v2/drivers/testdrv/driver.go | 22 +-- v2/drivers/webmididrv/driver.go | 10 +- v2/drivers/webmididrv/helpers.go | 6 +- v2/drivers/webmididrv/in.go | 10 +- v2/drivers/webmididrv/out.go | 6 +- v2/error.go | 34 +++- v2/example/logger/main.go | 6 +- v2/example/simple/main.go | 2 +- v2/example/smfplayer/main.go | 2 +- v2/example/smfrecorder/main.go | 2 +- v2/example_test.go | 18 +- v2/gm/reset.go | 13 +- v2/go.mod | 2 +- v2/internal/runningstatus/runningstatus.go | 4 +- v2/io.go | 19 +- v2/listen.go | 35 ++-- v2/message.go | 155 +++++---------- v2/nrpn/nrpn.go | 18 +- v2/port.go | 206 ++++++++++++++++++- v2/{drivers => }/reader.go | 6 +- v2/realtime.go | 50 ++--- v2/rpn/rpn.go | 28 +-- v2/syscommon.go | 24 +-- v2/syscommon_test.go | 10 +- v2/sysex.go | 83 +------- 41 files changed, 626 insertions(+), 933 deletions(-) create mode 100644 v2/driver.go delete mode 100644 v2/drivers/.gitignore delete mode 100644 v2/drivers/driver.go delete mode 100644 v2/drivers/port.go rename v2/{drivers => }/reader.go (99%) diff --git a/v2/channel.go b/v2/channel.go index e9fdf23..ca67c7b 100644 --- a/v2/channel.go +++ b/v2/channel.go @@ -7,17 +7,16 @@ import ( "gitlab.com/gomidi/midi/v2/internal/utils" ) +func ChannelMessageChannel(msg []byte) uint8 { + assertMessageCategory(msg, MessageCategoryChannel) + return msg[0] & byte(MessageCategoryChannel) +} + func ChannelMessageStatus(t MessageType, c uint8) byte { assertChannel(c) - assertMessageCategory(t, MessageCategoryChannel) return byte(t) | byte(c) } -func ChannelMessageChannel(b byte) uint8 { - assertMessageCategory(MessageTypeFromByte(b), MessageCategoryChannel) - return b & byte(MessageCategoryChannel) -} - func legalizeUint7(value uint8) uint8 { if value > 127 { return 127 @@ -33,27 +32,22 @@ func NoteOff(channel, key, velocity uint8) NoteOffMessage { legalizeUint7(velocity)}) } -type NoteOffMessage Message +type NoteOffMessage []byte func (m NoteOffMessage) Channel() uint8 { - assertMessageLength(Message(m), 3) - return ChannelMessageChannel(m[0]) + return ChannelMessageChannel(m) } func (m NoteOffMessage) Key() uint8 { - assertMessageLength(Message(m), 3) + assertMessageLength(m, 3) return utils.ParseUint7(m[1]) } func (m NoteOffMessage) Velocity() uint8 { - assertMessageLength(Message(m), 3) + assertMessageLength(m, 3) return utils.ParseUint7(m[2]) } -func (m NoteOffMessage) Message() Message { - return Message(m) -} - func (m NoteOffMessage) String() string { return fmt.Sprintf("%s channel: %d key: %d velocity: %d", MessageTypeNoteOff, m.Channel(), m.Key(), m.Velocity()) } @@ -66,27 +60,22 @@ func NoteOn(channel, key, velocity uint8) NoteOnMessage { legalizeUint7(velocity)}) } -type NoteOnMessage Message +type NoteOnMessage []byte func (m NoteOnMessage) Channel() uint8 { - assertMessageLength(Message(m), 3) - return ChannelMessageChannel(m[0]) + return ChannelMessageChannel(m) } func (m NoteOnMessage) Key() uint8 { - assertMessageLength(Message(m), 3) + assertMessageLength(m, 3) return utils.ParseUint7(m[1]) } func (m NoteOnMessage) Velocity() uint8 { - assertMessageLength(Message(m), 3) + assertMessageLength(m, 3) return utils.ParseUint7(m[2]) } -func (m NoteOnMessage) Message() Message { - return Message(m) -} - func (m NoteOnMessage) String() string { return fmt.Sprintf("%s channel: %d key: %d velocity: %d", MessageTypeNoteOn, m.Channel(), m.Key(), m.Velocity()) } @@ -99,27 +88,22 @@ func PolyAfterTouch(channel, key, pressure uint8) PolyAfterTouchMessage { legalizeUint7(pressure)}) } -type PolyAfterTouchMessage Message +type PolyAfterTouchMessage []byte func (m PolyAfterTouchMessage) Channel() uint8 { - assertMessageLength(Message(m), 3) - return ChannelMessageChannel(m[0]) + return ChannelMessageChannel(m) } func (m PolyAfterTouchMessage) Key() uint8 { - assertMessageLength(Message(m), 3) + assertMessageLength(m, 3) return utils.ParseUint7(m[1]) } func (m PolyAfterTouchMessage) Pressure() uint8 { - assertMessageLength(Message(m), 3) + assertMessageLength(m, 3) return utils.ParseUint7(m[2]) } -func (m PolyAfterTouchMessage) Message() Message { - return Message(m) -} - func (m PolyAfterTouchMessage) String() string { return fmt.Sprintf("%s channel: %d key: %d pressure: %d", MessageTypePolyAfterTouch, m.Channel(), m.Key(), m.Pressure()) } @@ -132,27 +116,22 @@ func ControlChange(channel, controller, value uint8) ControlChangeMessage { legalizeUint7(value)}) } -type ControlChangeMessage Message +type ControlChangeMessage []byte func (m ControlChangeMessage) Channel() uint8 { - assertMessageLength(Message(m), 3) - return ChannelMessageChannel(m[0]) + return ChannelMessageChannel(m) } func (m ControlChangeMessage) Controller() uint8 { - assertMessageLength(Message(m), 3) + assertMessageLength(m, 3) return utils.ParseUint7(m[1]) } func (m ControlChangeMessage) Value() uint8 { - assertMessageLength(Message(m), 3) + assertMessageLength(m, 3) return utils.ParseUint7(m[2]) } -func (m ControlChangeMessage) Message() Message { - return Message(m) -} - func (m ControlChangeMessage) String() string { return fmt.Sprintf("%s channel: %d controller: %d value: %d", MessageTypeControlChange, m.Channel(), m.Controller(), m.Value()) } @@ -164,22 +143,17 @@ func ProgramChange(channel, program uint8) ProgramChangeMessage { legalizeUint7(program)}) } -type ProgramChangeMessage Message +type ProgramChangeMessage []byte func (m ProgramChangeMessage) Channel() uint8 { - assertMessageLength(Message(m), 2) - return ChannelMessageChannel(m[0]) + return ChannelMessageChannel(m) } func (m ProgramChangeMessage) Program() uint8 { - assertMessageLength(Message(m), 2) + assertMessageLength(m, 2) return utils.ParseUint7(m[1]) } -func (m ProgramChangeMessage) Message() Message { - return Message(m) -} - func (m ProgramChangeMessage) String() string { return fmt.Sprintf("%s channel: %d program: %d", MessageTypeProgramChange, m.Channel(), m.Program()) } @@ -191,22 +165,17 @@ func AfterTouch(channel, pressure uint8) AfterTouchMessage { legalizeUint7(pressure)}) } -type AfterTouchMessage Message +type AfterTouchMessage []byte func (m AfterTouchMessage) Channel() uint8 { - assertMessageLength(Message(m), 2) - return ChannelMessageChannel(m[0]) + return ChannelMessageChannel(m) } func (m AfterTouchMessage) Pressure() uint8 { - assertMessageLength(Message(m), 2) + assertMessageLength(m, 2) return utils.ParseUint7(m[1]) } -func (m AfterTouchMessage) Message() Message { - return Message(m) -} - func (m AfterTouchMessage) String() string { return fmt.Sprintf("%s channel: %d pressure: %d", MessageTypeAfterTouch, m.Channel(), m.Pressure()) } @@ -215,38 +184,29 @@ func (m AfterTouchMessage) String() string { // If value is > 8191 (max), it will be set to 8191. If value is < -8192, it will be set to -8192. // A value of 0 is considered as neutral position. func PitchBend(channel uint8, value int16) PitchBendMessage { - var b = make([]byte, 2) - binary.BigEndian.PutUint16(b, uint14(value)) - - return PitchBendMessage([]byte{ - ChannelMessageStatus(MessageTypePitchBend, channel), - b[0], - b[1]}) + msg := []byte{ChannelMessageStatus(MessageTypePitchBend, channel), 0x00, 0x00} + binary.BigEndian.PutUint16(msg[1:], uint14(value)) + return PitchBendMessage(msg) } -type PitchBendMessage Message +type PitchBendMessage []byte func (m PitchBendMessage) Channel() uint8 { - assertMessageLength(Message(m), 3) - return ChannelMessageChannel(m[0]) + return ChannelMessageChannel(m) } func (m PitchBendMessage) Pitch() int16 { - assertMessageLength(Message(m), 3) + assertMessageLength(m, 3) rel, _ := utils.ParsePitchWheelVals(m[1], m[2]) return rel } func (m PitchBendMessage) Absolute() uint16 { - assertMessageLength(Message(m), 3) + assertMessageLength(m, 3) _, abs := utils.ParsePitchWheelVals(m[1], m[2]) return abs } -func (m PitchBendMessage) Message() Message { - return Message(m) -} - func (m PitchBendMessage) String() string { return fmt.Sprintf("%s channel: %d pitch: %d (%d)", MessageTypePitchBend, m.Channel(), m.Pitch(), m.Absolute()) } diff --git a/v2/channel_test.go b/v2/channel_test.go index 5d2d22b..a823f7b 100644 --- a/v2/channel_test.go +++ b/v2/channel_test.go @@ -1,146 +1,34 @@ package midi import ( - "bytes" "fmt" + "reflect" "testing" ) -func TestChannelString(t *testing.T) { +func TestChannelMessages(t *testing.T) { tests := []struct { - input []byte - expected string + msg []byte // [S ~[]E, E byte] + expBytes []byte + expString string + desc MessageType }{ - { - AfterTouch(1, 120), - "AfterTouch channel: 1 pressure: 120", - }, - { - ControlChange(8, 7, 110), - "ControlChange channel: 8 controller: 7 value: 110", - }, - { - NoteOn(2, 100, 80), - "NoteOn channel: 2 key: 100 velocity: 80", - }, - { - NoteOff(3, 80, 0), - "NoteOff channel: 3 key: 80 velocity: 0", - }, - { - NoteOff(4, 80, 20), - "NoteOff channel: 4 key: 80 velocity: 20", - }, - { - PitchBend(4, 300), - "PitchBend channel: 4 pitch: 300 (8492)", - }, - { - PolyAfterTouch(4, 86, 109), - "PolyAfterTouch channel: 4 key: 86 pressure: 109", - }, - { - ProgramChange(4, 83), - "ProgramChange channel: 4 program: 83", - }, - - // too high values - { - AfterTouch(1, 130), - "AfterTouch channel: 1 pressure: 127", - }, - { - ControlChange(8, 137, 130), - "ControlChange channel: 8 controller: 127 value: 127", - }, - { - NoteOn(2, 130, 130), - "NoteOn channel: 2 key: 127 velocity: 127", - }, - { - NoteOff(3, 180, 0), - "NoteOff channel: 3 key: 127 velocity: 0", - }, - { - NoteOff(4, 180, 220), - "NoteOff channel: 4 key: 127 velocity: 127", - }, - { - PitchBend(4, 12300), - "PitchBend channel: 4 pitch: 8191 (16383)", - }, - { - PolyAfterTouch(4, 186, 190), - "PolyAfterTouch channel: 4 key: 127 pressure: 127", - }, - { - ProgramChange(4, 183), - "ProgramChange channel: 4 program: 127", - }, + {NoteOff(3, 80, 0), []byte{0x83, 0x50, 0x00}, "NoteOff channel: 3 key: 80 velocity: 0", MessageTypeNoteOff}, + {NoteOn(2, 100, 80), []byte{0x92, 0x64, 0x50}, "NoteOn channel: 2 key: 100 velocity: 80", MessageTypeNoteOn}, + {PolyAfterTouch(4, 86, 109), []byte{0xa4, 0x56, 0x6d}, "PolyAfterTouch channel: 4 key: 86 pressure: 109", MessageTypePolyAfterTouch}, + {ControlChange(8, 7, 110), []byte{0xb8, 0x07, 0x6e}, "ControlChange channel: 8 controller: 7 value: 110", MessageTypeControlChange}, + {ProgramChange(4, 83), []byte{0xc4, 0x53}, "ProgramChange channel: 4 program: 83", MessageTypeProgramChange}, + {AfterTouch(1, 120), []byte{0xd1, 0x78}, "AfterTouch channel: 1 pressure: 120", MessageTypeAfterTouch}, + {PitchBend(4, 300), []byte{0xe4, 0x2c, 0x42}, "PitchBend channel: 4 pitch: 300 (8492)", MessageTypePitchBend}, } - for _, test := range tests { - - var bf bytes.Buffer - - bf.WriteString(Message(test.input).String()) - - if got, want := bf.String(), test.expected; got != want { - t.Errorf("got: %#v; wanted %#v", got, want) + for i, tst := range tests { + if !reflect.DeepEqual(tst.msg, tst.expBytes) { + t.Errorf("(%d) %s: msg not expected: [% x] != [% x]", i, tst.desc, tst.msg, tst.expBytes) } - } - -} - -func TestChannelRaw(t *testing.T) { - - tests := []struct { - input []byte - expected string - }{ - { // 0 - AfterTouch(1, 120), - "D1 78", - }, - { // 1 - ControlChange(8, 7, 110), - "B8 07 6E", - }, - { // 2 - NoteOn(2, 100, 80), - "92 64 50", - }, - { // 3 - NoteOff(3, 80, 0), - "83 50 00", - }, - { - NoteOff(4, 80, 20), - "84 50 14", - }, - { - PitchBend(4, 300), - "E4 2C 42", - }, - { - PolyAfterTouch(4, 86, 109), - "A4 56 6D", - }, - { - ProgramChange(4, 83), - "C4 53", - }, - } - - for i, test := range tests { - - var bf bytes.Buffer - - bf.Write(test.input) - - if got, want := fmt.Sprintf("% X", bf.Bytes()), test.expected; got != want { - t.Errorf("[%v] got: %#v; wanted %#v", i, got, want) + if s := fmt.Sprintf("%s", Message(tst.msg)); s != tst.expString { + t.Errorf("(%d) %s: string not expected: %s != %s", i, tst.desc, s, tst.expString) } } } @@ -177,7 +65,7 @@ func TestPitchbend(t *testing.T) { m := PitchBend(0, test.in) if abs := m.Absolute(); abs != test.expected { - t.Errorf("Pitchbend(%v).absValue = %v; wanted %v", test.in, abs, test.expected) + t.Errorf("PitchBend(%v).absValue = %v; wanted %v", test.in, abs, test.expected) } } } diff --git a/v2/driver.go b/v2/driver.go new file mode 100644 index 0000000..d981e43 --- /dev/null +++ b/v2/driver.go @@ -0,0 +1,94 @@ +package midi + +import ( + "context" +) + +// Driver is a driver for MIDI connections. +// It may provide the timing delta to the previous message in micro seconds. +// It must send the given MIDI data immediately. +type Driver interface { + + // Ins returns the available MIDI input ports. + Ins() ([]In, error) + + // Outs returns the available MIDI output ports. + Outs() ([]Out, error) + + // Close closes the driver. Must be called for cleanup at the end of a session. + Close() error +} + +type DriverRegistry struct { + drivers []Driver +} + +// RegisterDriver register a driver +func (dr *DriverRegistry) Register(d Driver) { + dr.drivers = append(dr.drivers, d) +} + +// Get returns the first available driver +func (dr *DriverRegistry) Default() Driver { + if len(dr.drivers) == 0 { + return &NoOpDriver{} + } + return dr.drivers[0] +} + +func (dr *DriverRegistry) Close() error { + var errs Errors + for i := range dr.drivers { + if err := dr.drivers[i].Close(); err != nil { + errs = append(errs, err) + } + } + return errs +} + +var defaultRegistry DriverRegistry + +func Register(d Driver) { + defaultRegistry.Register(d) +} + +func Default() Driver { + return defaultRegistry.Default() +} + +func Close() error { + return defaultRegistry.Close() +} + +type NoOpDriver struct { +} + +func (d *NoOpDriver) Close() error { return nil } +func (f *NoOpDriver) Ins() ([]In, error) { return []In{&NoOpInPort{}}, nil } +func (f *NoOpDriver) Outs() ([]Out, error) { return []Out{&NoOpOutPort{}}, nil } + +type NoOpInPort struct { +} + +func (p *NoOpInPort) Open() error { return nil } +func (p *NoOpInPort) Close() error { return nil } +func (p *NoOpInPort) String() string { return "NoOpInPort" } +func (p *NoOpInPort) Number() int { return 0 } +func (p *NoOpInPort) IsOpen() bool { return false } +func (p *NoOpInPort) Underlying() interface{} { return p } + +func (p *NoOpInPort) Listen(ctx context.Context, onMsg func([]byte, int32), conf ListenConfig) error { + <-ctx.Done() + return nil +} + +type NoOpOutPort struct { +} + +func (p *NoOpOutPort) Open() error { return nil } +func (p *NoOpOutPort) Close() error { return nil } +func (p *NoOpOutPort) String() string { return "NoOpOutPort" } +func (p *NoOpOutPort) Number() int { return 0 } +func (p *NoOpOutPort) IsOpen() bool { return false } +func (p *NoOpOutPort) Underlying() interface{} { return p } +func (p *NoOpOutPort) Send(bt []byte) error { return nil } diff --git a/v2/drivers/.gitignore b/v2/drivers/.gitignore deleted file mode 100644 index 8d6077b..0000000 --- a/v2/drivers/.gitignore +++ /dev/null @@ -1 +0,0 @@ -amididrv diff --git a/v2/drivers/driver.go b/v2/drivers/driver.go deleted file mode 100644 index ffbc598..0000000 --- a/v2/drivers/driver.go +++ /dev/null @@ -1,50 +0,0 @@ -package drivers - -var ( - firstDriver string - - // REGISTRY is the registry for MIDI drivers - REGISTRY = map[string]Driver{} -) - -// RegisterDriver register a driver -func Register(d Driver) { - if len(REGISTRY) == 0 { - firstDriver = d.String() - } - REGISTRY[d.String()] = d -} - -// Get returns the first available driver -func Get() Driver { - if len(REGISTRY) == 0 { - return nil - } - return REGISTRY[firstDriver] -} - -// Close closes the first available driver -func Close() { - d := Get() - if d != nil { - d.Close() - } -} - -// Driver is a driver for MIDI connections. -// It may provide the timing delta to the previous message in micro seconds. -// It must send the given MIDI data immediately. -type Driver interface { - - // Ins returns the available MIDI input ports. - Ins() ([]In, error) - - // Outs returns the available MIDI output ports. - Outs() ([]Out, error) - - // String returns the name of the driver. - String() string - - // Close closes the driver. Must be called for cleanup at the end of a session. - Close() error -} diff --git a/v2/drivers/midicatdrv/driver.go b/v2/drivers/midicatdrv/driver.go index ebecb29..7538547 100644 --- a/v2/drivers/midicatdrv/driver.go +++ b/v2/drivers/midicatdrv/driver.go @@ -10,8 +10,7 @@ import ( "strings" "sync" - "gitlab.com/gomidi/midi/v2/drivers" - // "gitlab.com/metakeule/config" + "gitlab.com/gomidi/midi/v2" ) func init() { @@ -19,11 +18,11 @@ func init() { if err != nil { panic(fmt.Sprintf("could not register midicatdrv: %s", err.Error())) } - drivers.Register(drv) + midi.Register(drv) } type Driver struct { - opened []drivers.Port + opened []midi.Port sync.RWMutex } @@ -134,7 +133,7 @@ func New() (*Driver, error) { } // Ins returns the available MIDI input ports -func (d *Driver) Ins() (ins []drivers.In, err error) { +func (d *Driver) Ins() (ins []midi.In, err error) { c := midiCatCmd("ins --json") res, err := c.Output() @@ -168,7 +167,7 @@ func (d *Driver) Ins() (ins []drivers.In, err error) { } // Outs returns the available MIDI output ports -func (d *Driver) Outs() (outs []drivers.Out, err error) { +func (d *Driver) Outs() (outs []midi.Out, err error) { c := midiCatCmd("outs --json") res, err := c.Output() diff --git a/v2/drivers/midicatdrv/helpers.go b/v2/drivers/midicatdrv/helpers.go index 9aeccf7..558733f 100644 --- a/v2/drivers/midicatdrv/helpers.go +++ b/v2/drivers/midicatdrv/helpers.go @@ -1,10 +1,10 @@ package midicatdrv import ( - "gitlab.com/gomidi/midi/v2/drivers" + "gitlab.com/gomidi/midi/v2" ) -type inPorts []drivers.In +type inPorts []midi.In func (i inPorts) Len() int { return len(i) @@ -18,7 +18,7 @@ func (i inPorts) Less(a, b int) bool { return i[a].Number() < i[b].Number() } -type outPorts []drivers.Out +type outPorts []midi.Out func (i outPorts) Len() int { return len(i) diff --git a/v2/drivers/midicatdrv/in.go b/v2/drivers/midicatdrv/in.go index b45ea39..2efd64b 100644 --- a/v2/drivers/midicatdrv/in.go +++ b/v2/drivers/midicatdrv/in.go @@ -8,7 +8,6 @@ import ( "sync" "gitlab.com/gomidi/midi/v2" - "gitlab.com/gomidi/midi/v2/drivers" lib "gitlab.com/gomidi/midi/v2/tools/midicat" ) @@ -161,11 +160,11 @@ func (i *in) Open() (err error) { return nil } -func newIn(driver *Driver, number int, name string) drivers.In { +func newIn(driver *Driver, number int, name string) midi.In { return &in{driver: driver, number: number, name: name} } -func (i *in) Listen(ctx context.Context, onMsg func(msg []byte, absmilliseconds int32), config drivers.ListenConfig) error { +func (i *in) Listen(ctx context.Context, onMsg func(msg []byte, absmilliseconds int32), config midi.ListenConfig) error { stopFn := func() { if !i.IsOpen() { return @@ -180,7 +179,7 @@ func (i *in) Listen(ctx context.Context, onMsg func(msg []byte, absmilliseconds }(ctx) if !i.IsOpen() { - return drivers.ErrPortClosed + return midi.ErrPortClosed } if onMsg == nil { @@ -194,7 +193,7 @@ func (i *in) Listen(ctx context.Context, onMsg func(msg []byte, absmilliseconds } i.RUnlock() - //var rd = drivers.NewReader(config, onMsg) + //var rd = midi.NewReader(config, onMsg) i.Lock() i.listener = func(data []byte, absmilliseconds int32) { //rd.EachMessage(data, deltamillisecs) @@ -210,7 +209,7 @@ func (i *in) Listen(ctx context.Context, onMsg func(msg []byte, absmilliseconds // SendTo makes the listener listen to the in port func (i *in) StartListening(cb func([]byte, int32)) (err error) { if !i.IsOpen() { - return drivers.ErrPortClosed + return midi.ErrPortClosed } i.RLock() @@ -229,7 +228,7 @@ func (i *in) StartListening(cb func([]byte, int32)) (err error) { // StopListening cancels the listening func (i *in) StopListening() (err error) { if !i.IsOpen() { - return drivers.ErrPortClosed + return midi.ErrPortClosed } i.shouldStopListening <- true diff --git a/v2/drivers/midicatdrv/out.go b/v2/drivers/midicatdrv/out.go index 98ce814..fefb09a 100644 --- a/v2/drivers/midicatdrv/out.go +++ b/v2/drivers/midicatdrv/out.go @@ -6,10 +6,10 @@ import ( "os/exec" "sync" - "gitlab.com/gomidi/midi/v2/drivers" + "gitlab.com/gomidi/midi/v2" ) -func newOut(driver *Driver, number int, name string) drivers.Out { +func newOut(driver *Driver, number int, name string) midi.Out { o := &out{driver: driver, number: number, name: name} return o } @@ -61,7 +61,7 @@ func (o *out) Send(b []byte) error { defer o.Unlock() if o.cmd == nil { fmt.Println("port closed") - return drivers.ErrPortClosed + return midi.ErrPortClosed } //fmt.Printf("% X\n", b) _, err := fmt.Fprintf(o.wr, "%d %X\n", 0, b) diff --git a/v2/drivers/port.go b/v2/drivers/port.go deleted file mode 100644 index 2373756..0000000 --- a/v2/drivers/port.go +++ /dev/null @@ -1,217 +0,0 @@ -package drivers - -import ( - "context" - "fmt" - "strings" -) - -var ErrPortClosed = fmt.Errorf("ERROR: port is closed") -var ErrListenStopped = fmt.Errorf("ERROR: stopped listening") - -// Port is an interface for a MIDI port. -// In order to be lockless (for realtime), a port is not threadsafe, so none of its method may be called -// from different goroutines. -type Port interface { - - // Open opens the MIDI port. An implementation should save the open state to make it - // save to call open when the port is already open without getting an error. - Open() error - - // Close closes the MIDI port. An implementation should save the open state to make it - // save to call close when the port is already closed without getting an error. - Close() error - - // IsOpen returns wether the MIDI port is open. - IsOpen() bool - - // Number returns the number of the MIDI port. It is only guaranteed that the numbers are unique within - // MIDI port groups i.e. within MIDI input ports and MIDI output ports. So there may be the same number - // for a given MIDI input port and some MIDI output port. Or not - that depends on the underlying driver. - Number() int - - // String represents the MIDI port by a string, aka name. - String() string -} - -// ListenConfig defines the configuration for in port listening -type ListenConfig struct { - - // TimeCode lets the timecode messages pass through, if set - TimeCode bool - - // ActiveSense lets the active sense messages pass through, if set - ActiveSense bool - - // SysEx lets the sysex messaes pass through, if set - SysEx bool - - // SysExBufferSize defines the size of the buffer for sysex messages (in bytes). - // SysEx messages larger than this size will be ignored. - // When SysExBufferSize is 0, the default buffersize (1024) is used. - SysExBufferSize uint32 - - // OnErr is the callback that is called for any error happening during the listening. - OnErr func(error) -} - -// In is an interface for a MIDI input port -type In interface { - Port - - // Listen listens for incoming messages. It returns a function that must be used to stop listening. - // The onMsg callback is called for every non-sysex message. The onMsg callback must not be nil. - // The config defines further listening options (see ListenConfig) - // The listening must be stopped before the port may be closed. - Listen( - ctx context.Context, - onMsg func(msg []byte, milliseconds int32), - config ListenConfig, - ) error -} - -// Out is an interface for a MIDI output port. -type Out interface { - Port - - //Send(data [3]byte) error - Send(data []byte) error -} - -// Ins return the available MIDI in ports -func Ins() ([]In, error) { - d := Get() - if d == nil { - return nil, fmt.Errorf("no driver registered") - } - return d.Ins() -} - -// Outs return the available MIDI out ports -func Outs() ([]Out, error) { - d := Get() - if d == nil { - return nil, fmt.Errorf("no driver registered") - } - return d.Outs() -} - -// InByName opens the first midi in port that contains the given name -func InByName(portName string) (in In, err error) { - drv := Get() - if drv == nil { - return nil, fmt.Errorf("no driver registered") - } - return openIn(drv, -1, portName) -} - -// InByNumber opens the midi in port with the given number -func InByNumber(portNumber int) (in In, err error) { - drv := Get() - if drv == nil { - return nil, fmt.Errorf("no driver registered") - } - return openIn(drv, portNumber, "") -} - -// OutByName opens the first midi out port that contains the given name -func OutByName(portName string) (out Out, err error) { - drv := Get() - if drv == nil { - return nil, fmt.Errorf("no driver registered") - } - return openOut(drv, -1, portName) -} - -// OutByNumber opens the midi out port with the given number -func OutByNumber(portNumber int) (out Out, err error) { - drv := Get() - if drv == nil { - return nil, fmt.Errorf("no driver registered") - } - return openOut(drv, portNumber, "") -} - -// openIn opens a MIDI input port with the help of the given driver. -// To find the port by port number, pass a number >= 0. -// To find the port by port name, pass a number < 0 and a non empty string. -func openIn(d Driver, number int, name string) (in In, err error) { - ins, err := d.Ins() - if err != nil { - return nil, fmt.Errorf("can't find MIDI input ports: %v", err) - } - - if number >= 0 { - for _, port := range ins { - if number == port.Number() { - in = port - break - } - } - if in == nil { - return nil, fmt.Errorf("can't find MIDI input port %v", number) - } - } else { - if name != "" { - for _, port := range ins { - if strings.Contains(port.String(), name) { - in = port - break - } - } - } - if in == nil { - return nil, fmt.Errorf("can't find MIDI input port %v", name) - } - } - - // should not happen here, since we already returned above - if in == nil { - panic("unreachable") - } - - err = in.Open() - return -} - -// openOut opens a MIDI output port with the help of the given driver. -// To find the port by port number, pass a number >= 0. -// To find the port by port name, pass a number < 0 and a non empty string. -func openOut(d Driver, number int, name string) (out Out, err error) { - outs, err := d.Outs() - if err != nil { - return nil, fmt.Errorf("can't find MIDI output ports: %v", err) - } - - if number >= 0 { - for _, port := range outs { - if number == port.Number() { - out = port - break - } - } - if out == nil { - return nil, fmt.Errorf("can't find MIDI output port %v", number) - } - } else { - if name != "" { - for _, port := range outs { - if strings.Contains(port.String(), name) { - out = port - break - } - } - } - if out == nil { - return nil, fmt.Errorf("can't find MIDI output port %v", name) - } - } - - // should not happen here, since we already returned above - if out == nil { - panic("unreachable") - } - - err = out.Open() - return -} diff --git a/v2/drivers/portmididrv/driver.go b/v2/drivers/portmididrv/driver.go index e9b6661..1ae1b33 100644 --- a/v2/drivers/portmididrv/driver.go +++ b/v2/drivers/portmididrv/driver.go @@ -5,7 +5,7 @@ import ( "sync" "time" - "gitlab.com/gomidi/midi/v2/drivers" + "gitlab.com/gomidi/midi/v2" "gitlab.com/gomidi/midi/v2/drivers/portmididrv/imported/portmidi" ) @@ -14,15 +14,15 @@ func init() { if err != nil { panic(fmt.Sprintf("could not register portmididrv: %s", err.Error())) } - drivers.Register(drv) + midi.Register(drv) } -var _ drivers.Driver = &Driver{} +var _ midi.Driver = &Driver{} -//var _ drivers.SysExListener = &in{} -//var _ drivers.SysCommonListener = &in{} -//var _ drivers.RealtimeListener = &in{} -//var _ drivers.SysExSender = &out{} +//var _ midi.SysExListener = &in{} +//var _ midi.SysCommonListener = &in{} +//var _ midi.RealtimeListener = &in{} +//var _ midi.SysExSender = &out{} type Driver struct { buffersizeRead int @@ -30,7 +30,7 @@ type Driver struct { buffersizeOut int64 sleepingTime time.Duration sync.Mutex - opened []drivers.Port + opened []midi.Port } func (d *Driver) String() string { @@ -98,7 +98,7 @@ func New(options ...Option) (*Driver, error) { } // Ins returns the available MIDI in ports -func (d *Driver) Ins() (ins []drivers.In, err error) { +func (d *Driver) Ins() (ins []midi.In, err error) { var num int for i := 0; i < portmidi.CountDevices(); i++ { info := portmidi.Info(portmidi.DeviceID(i)) @@ -111,7 +111,7 @@ func (d *Driver) Ins() (ins []drivers.In, err error) { } // Outs returns the available MIDI out ports -func (d *Driver) Outs() (outs []drivers.Out, err error) { +func (d *Driver) Outs() (outs []midi.Out, err error) { var num int for i := 0; i < portmidi.CountDevices(); i++ { info := portmidi.Info(portmidi.DeviceID(i)) diff --git a/v2/drivers/portmididrv/in.go b/v2/drivers/portmididrv/in.go index d643b65..17b97ff 100644 --- a/v2/drivers/portmididrv/in.go +++ b/v2/drivers/portmididrv/in.go @@ -8,11 +8,10 @@ import ( "time" "gitlab.com/gomidi/midi/v2" - "gitlab.com/gomidi/midi/v2/drivers" "gitlab.com/gomidi/midi/v2/drivers/portmididrv/imported/portmidi" ) -func newIn(driver *Driver, deviceid portmidi.DeviceID, id int, name string) drivers.In { +func newIn(driver *Driver, deviceid portmidi.DeviceID, id int, name string) midi.In { return &in{driver: driver, id: id, name: name, deviceid: deviceid} } @@ -78,7 +77,7 @@ func (i *in) Open() (err error) { } // Listen -func (i *in) Listen(ctx context.Context, onMsg func(msg []byte, milliseconds int32), config drivers.ListenConfig) error { +func (i *in) Listen(ctx context.Context, onMsg func(msg []byte, milliseconds int32), config midi.ListenConfig) error { if onMsg == nil { return midi.ErrMessageCallbackNil @@ -133,7 +132,7 @@ func (i *in) Listen(ctx context.Context, onMsg func(msg []byte, milliseconds int defer func() { if config.OnErr != nil { - config.OnErr(drivers.ErrListenStopped) + config.OnErr(midi.ErrListenStopped) } }() diff --git a/v2/drivers/portmididrv/out.go b/v2/drivers/portmididrv/out.go index 368ccb5..ee6ea22 100644 --- a/v2/drivers/portmididrv/out.go +++ b/v2/drivers/portmididrv/out.go @@ -3,11 +3,11 @@ package portmididrv import ( "fmt" - "gitlab.com/gomidi/midi/v2/drivers" + "gitlab.com/gomidi/midi/v2" "gitlab.com/gomidi/midi/v2/drivers/portmididrv/imported/portmidi" ) -func newOut(driver *Driver, deviceid portmidi.DeviceID, id int, name string) drivers.Out { +func newOut(driver *Driver, deviceid portmidi.DeviceID, id int, name string) midi.Out { return &out{driver: driver, id: id, name: name, deviceid: deviceid} } @@ -34,7 +34,7 @@ func (o *out) SendSysEx(data []byte) error { //o.mx.RLock() if o.stream == nil { //o.mx.RUnlock() - return drivers.ErrPortClosed + return midi.ErrPortClosed } //o.mx.RUnlock() @@ -59,7 +59,7 @@ func (o *out) Send(b []byte) error { //o.mx.RLock() if o.stream == nil { //o.mx.RUnlock() - return drivers.ErrPortClosed + return midi.ErrPortClosed } // sysex diff --git a/v2/drivers/rtmididrv/driver.go b/v2/drivers/rtmididrv/driver.go index 852a254..7330dd0 100644 --- a/v2/drivers/rtmididrv/driver.go +++ b/v2/drivers/rtmididrv/driver.go @@ -4,7 +4,7 @@ import ( "fmt" "strings" - "gitlab.com/gomidi/midi/v2/drivers" + "gitlab.com/gomidi/midi/v2" "gitlab.com/gomidi/midi/v2/drivers/rtmididrv/imported/rtmidi" ) @@ -13,11 +13,11 @@ func init() { if err != nil { panic(fmt.Sprintf("could not register rtmididrv: %s", err.Error())) } - drivers.Register(drv) + midi.Register(drv) } type Driver struct { - opened []drivers.Port + opened []midi.Port //ignoreSysex bool //ignoreTimeCode bool //ignoreActiveSense bool @@ -78,7 +78,7 @@ func New() (*Driver, error) { } // OpenVirtualIn opens and returns a virtual MIDI in. We can't get the port number, so set it to -1. -func (d *Driver) OpenVirtualIn(name string) (drivers.In, error) { +func (d *Driver) OpenVirtualIn(name string) (midi.In, error) { _in, err := rtmidi.NewMIDIInDefault() if err != nil { return nil, fmt.Errorf("can't open default MIDI in: %v", err) @@ -99,7 +99,7 @@ func (d *Driver) OpenVirtualIn(name string) (drivers.In, error) { } // OpenVirtualOut opens and returns a virtual MIDI out. We can't get the port number, so set it to -1. -func (d *Driver) OpenVirtualOut(name string) (drivers.Out, error) { +func (d *Driver) OpenVirtualOut(name string) (midi.Out, error) { _out, err := rtmidi.NewMIDIOutDefault() if err != nil { return nil, fmt.Errorf("can't open default MIDI out: %v", err) @@ -119,7 +119,7 @@ func (d *Driver) OpenVirtualOut(name string) (drivers.Out, error) { } // Ins returns the available MIDI input ports -func (d *Driver) Ins() (ins []drivers.In, err error) { +func (d *Driver) Ins() (ins []midi.In, err error) { var in rtmidi.MIDIIn in, err = rtmidi.NewMIDIInDefault() if err != nil { @@ -146,7 +146,7 @@ func (d *Driver) Ins() (ins []drivers.In, err error) { } // Outs returns the available MIDI output ports -func (d *Driver) Outs() (outs []drivers.Out, err error) { +func (d *Driver) Outs() (outs []midi.Out, err error) { var out rtmidi.MIDIOut out, err = rtmidi.NewMIDIOutDefault() if err != nil { diff --git a/v2/drivers/rtmididrv/in.go b/v2/drivers/rtmididrv/in.go index c4e0a30..3ba2d16 100644 --- a/v2/drivers/rtmididrv/in.go +++ b/v2/drivers/rtmididrv/in.go @@ -6,7 +6,6 @@ import ( "math" "gitlab.com/gomidi/midi/v2" - "gitlab.com/gomidi/midi/v2/drivers" "gitlab.com/gomidi/midi/v2/drivers/rtmididrv/imported/rtmidi" ) @@ -119,11 +118,11 @@ const ( ) */ -func newIn(driver *Driver, number int, name string) drivers.In { +func newIn(driver *Driver, number int, name string) midi.In { return &in{driver: driver, number: number, name: name} } -func (i *in) Listen(ctx context.Context, onMsg func(msg []byte, milliseconds int32), config drivers.ListenConfig) error { +func (i *in) Listen(ctx context.Context, onMsg func(msg []byte, milliseconds int32), config midi.ListenConfig) error { if onMsg == nil { return midi.ErrMessageCallbackNil @@ -135,25 +134,25 @@ func (i *in) Listen(ctx context.Context, onMsg func(msg []byte, milliseconds int config.SysExBufferSize = 1024 } - var rd = drivers.NewReader(config, onMsg) + var rd = midi.NewReader(config, onMsg) - go func(ctx context.Context) { - defer i.Close() + defer i.Close() - <-ctx.Done() - i.midiIn.CancelCallback() - }(ctx) - - return i.midiIn.SetCallback(func(in rtmidi.MIDIIn, bt []byte, deltaSeconds float64) { + if err := i.midiIn.SetCallback(func(in rtmidi.MIDIIn, bt []byte, deltaSeconds float64) { rd.EachMessage(bt, int32(math.Round(deltaSeconds*1000))) - }) + }); err != nil { + return err + } + + <-ctx.Done() + return i.midiIn.CancelCallback() } /* func (i *in) StartListening(callback func(data []byte, deltadecimilliseconds int32)) error { if !i.IsOpen() { //fmt.Printf("post closed\n") - return drivers.ErrPortClosed + return midi.ErrPortClosed } i.RLock() @@ -190,7 +189,7 @@ func (i *in) StartListening(callback func(data []byte, deltadecimilliseconds int // StopListening cancels the listening func (i *in) StopListening() (err error) { if !i.IsOpen() { - return drivers.ErrPortClosed + return midi.ErrPortClosed } i.Lock() if i.listenerSet { diff --git a/v2/drivers/rtmididrv/out.go b/v2/drivers/rtmididrv/out.go index a14ad9d..e398601 100644 --- a/v2/drivers/rtmididrv/out.go +++ b/v2/drivers/rtmididrv/out.go @@ -3,11 +3,11 @@ package rtmididrv import ( "fmt" - "gitlab.com/gomidi/midi/v2/drivers" + "gitlab.com/gomidi/midi/v2" "gitlab.com/gomidi/midi/v2/drivers/rtmididrv/imported/rtmidi" ) -func newOut(driver *Driver, number int, name string) drivers.Out { +func newOut(driver *Driver, number int, name string) midi.Out { o := &out{driver: driver, number: number, name: name} return o } @@ -28,35 +28,35 @@ func (o *out) IsOpen() (open bool) { return } -// Send writes a MIDI sysex message to the outut port -func (o *out) SendSysEx(data []byte) error { - //fmt.Printf("try to send sysex\n") - - if o.midiOut == nil { - //o.RUnlock() - return drivers.ErrPortClosed - } - //o.mx.RUnlock() - - // since we always open the outputstream with a latency of 0 - // the timestamp is ignored - //var ts portmidi.Timestamp // or portmidi.Time() - - //o.mx.Lock() - // defer o.mx.Unlock() - //fmt.Printf("sending sysex % X\n", data) - //err := o.stream.WriteSysExBytes(ts, data) - err := o.midiOut.SendMessage(data) - if err != nil { - return fmt.Errorf("could not send sysex message to MIDI out %v (%s): %v", o.Number(), o, err) - } - return nil -} +// // Send writes a MIDI sysex message to the outut port +// func (o *out) SendSysEx(data []byte) error { +// //fmt.Printf("try to send sysex\n") + +// if o.midiOut == nil { +// //o.RUnlock() +// return midi.ErrPortClosed +// } +// //o.mx.RUnlock() + +// // since we always open the outputstream with a latency of 0 +// // the timestamp is ignored +// //var ts portmidi.Timestamp // or portmidi.Time() + +// //o.mx.Lock() +// // defer o.mx.Unlock() +// //fmt.Printf("sending sysex % X\n", data) +// //err := o.stream.WriteSysExBytes(ts, data) +// err := o.midiOut.SendMessage(data) +// if err != nil { +// return fmt.Errorf("could not send sysex message to MIDI out %v (%s): %v", o.Number(), o, err) +// } +// return nil +// } func (o *out) Send(b []byte) error { if o.midiOut == nil { //o.RUnlock() - return drivers.ErrPortClosed + return midi.ErrPortClosed } // o.RUnlock() @@ -92,7 +92,7 @@ func (o *out) send(bt []byte) error { defer o.Unlock() if o.midiOut == nil { //o.RUnlock() - return drivers.ErrPortClosed + return midi.ErrPortClosed } // o.RUnlock() diff --git a/v2/drivers/testdrv/driver.go b/v2/drivers/testdrv/driver.go index 8b80f55..a931b22 100644 --- a/v2/drivers/testdrv/driver.go +++ b/v2/drivers/testdrv/driver.go @@ -12,12 +12,12 @@ import ( "context" "time" - "gitlab.com/gomidi/midi/v2/drivers" + "gitlab.com/gomidi/midi/v2" ) func init() { drv := New("testdrv") - drivers.Register(drv) + midi.Register(drv) } type Driver struct { @@ -26,10 +26,10 @@ type Driver struct { name string last time.Time stopListening bool - rd *drivers.Reader + rd *midi.Reader } -func New(name string) drivers.Driver { +func New(name string) midi.Driver { d := &Driver{name: name} d.in = &in{name: name + "-in", driver: d, number: 0} d.out = &out{name: name + "-out", driver: d, number: 0} @@ -37,10 +37,10 @@ func New(name string) drivers.Driver { return d } -func (f *Driver) String() string { return f.name } -func (f *Driver) Close() error { return nil } -func (f *Driver) Ins() ([]drivers.In, error) { return []drivers.In{f.in}, nil } -func (f *Driver) Outs() ([]drivers.Out, error) { return []drivers.Out{f.out}, nil } +func (f *Driver) String() string { return f.name } +func (f *Driver) Close() error { return nil } +func (f *Driver) Ins() ([]midi.In, error) { return []midi.In{f.in}, nil } +func (f *Driver) Outs() ([]midi.Out, error) { return []midi.Out{f.out}, nil } type in struct { number int @@ -54,7 +54,7 @@ func (f *in) Number() int { return f.number } func (f *in) IsOpen() bool { return f.isOpen } func (f *in) Underlying() interface{} { return nil } -func (f *in) Listen(ctx context.Context, onMsg func([]byte, int32), conf drivers.ListenConfig) error { +func (f *in) Listen(ctx context.Context, onMsg func([]byte, int32), conf midi.ListenConfig) error { f.driver.last = time.Now() go func(ctx context.Context) { @@ -62,7 +62,7 @@ func (f *in) Listen(ctx context.Context, onMsg func([]byte, int32), conf drivers f.driver.stopListening = true }(ctx) - f.driver.rd = drivers.NewReader(conf, onMsg) + f.driver.rd = midi.NewReader(conf, onMsg) return nil } @@ -105,7 +105,7 @@ func (f *out) Close() error { func (f *out) Send(bt []byte) error { if !f.isOpen { - return drivers.ErrPortClosed + return midi.ErrPortClosed } if f.driver.stopListening { diff --git a/v2/drivers/webmididrv/driver.go b/v2/drivers/webmididrv/driver.go index f18eeec..2af139a 100644 --- a/v2/drivers/webmididrv/driver.go +++ b/v2/drivers/webmididrv/driver.go @@ -6,7 +6,7 @@ import ( "sync" "syscall/js" - "gitlab.com/gomidi/midi/v2/drivers" + "gitlab.com/gomidi/midi/v2" ) func init() { @@ -14,11 +14,11 @@ func init() { if err != nil { panic(fmt.Sprintf("could not register webmididrv: %s", err.Error())) } - drivers.Register(drv) + midi.Register(drv) } type Driver struct { - opened []drivers.Port + opened []midi.Port sync.RWMutex inputsJS js.Value outputsJS js.Value @@ -99,7 +99,7 @@ func (d *Driver) onMIDIFailure() js.Func { } // Ins returns the available MIDI input ports -func (d *Driver) Ins() (ins []drivers.In, err error) { +func (d *Driver) Ins() (ins []midi.In, err error) { if d.Err != nil { return nil, err } @@ -123,7 +123,7 @@ func (d *Driver) Ins() (ins []drivers.In, err error) { } // Outs returns the available MIDI output ports -func (d *Driver) Outs() (outs []drivers.Out, err error) { +func (d *Driver) Outs() (outs []midi.Out, err error) { if d.Err != nil { return nil, err } diff --git a/v2/drivers/webmididrv/helpers.go b/v2/drivers/webmididrv/helpers.go index 90c544a..03b8162 100644 --- a/v2/drivers/webmididrv/helpers.go +++ b/v2/drivers/webmididrv/helpers.go @@ -3,7 +3,7 @@ package webmididrv import ( "syscall/js" - "gitlab.com/gomidi/midi/v2/drivers" + "gitlab.com/gomidi/midi/v2" ) func log(s string) { @@ -16,7 +16,7 @@ func log(s string) { jsConsole.Call("log", js.ValueOf(s)) } -type inPorts []drivers.In +type inPorts []midi.In func (i inPorts) Len() int { return len(i) @@ -30,7 +30,7 @@ func (i inPorts) Less(a, b int) bool { return i[a].Number() < i[b].Number() } -type outPorts []drivers.Out +type outPorts []midi.Out func (i outPorts) Len() int { return len(i) diff --git a/v2/drivers/webmididrv/in.go b/v2/drivers/webmididrv/in.go index 20c6f87..08efc00 100644 --- a/v2/drivers/webmididrv/in.go +++ b/v2/drivers/webmididrv/in.go @@ -7,7 +7,7 @@ import ( "sync/atomic" "syscall/js" - "gitlab.com/gomidi/midi/v2/drivers" + "gitlab.com/gomidi/midi/v2" ) type in struct { @@ -75,7 +75,7 @@ func (i *in) Open() (err error) { return nil } -func newIn(driver *Driver, number int, name string, jsport js.Value) drivers.In { +func newIn(driver *Driver, number int, name string, jsport js.Value) midi.In { return &in{driver: driver, number: number, name: name, jsport: jsport} } @@ -106,7 +106,7 @@ func newIn(driver *Driver, number int, name string, jsport js.Value) drivers.In */ -func (i *in) Listen(ctx context.Context, onMsg func(msg []byte, milliseconds int32), config drivers.ListenConfig) error { +func (i *in) Listen(ctx context.Context, onMsg func(msg []byte, milliseconds int32), config midi.ListenConfig) error { var stop int32 @@ -153,7 +153,7 @@ func (i *in) Listen(ctx context.Context, onMsg func(msg []byte, milliseconds int // SendTo func (i *in) StartListening(cb func(data []byte, timestamp int32)) (err error) { if !i.IsOpen() { - return drivers.ErrPortClosed + return midi.ErrPortClosed } i.RLock() @@ -192,7 +192,7 @@ func (i *in) StartListening(cb func(data []byte, timestamp int32)) (err error) { // StopListening cancels the listening func (i *in) StopListening() (err error) { if !i.IsOpen() { - return drivers.ErrPortClosed + return midi.ErrPortClosed } // TODO diff --git a/v2/drivers/webmididrv/out.go b/v2/drivers/webmididrv/out.go index 2e98305..9e6e30f 100644 --- a/v2/drivers/webmididrv/out.go +++ b/v2/drivers/webmididrv/out.go @@ -4,10 +4,10 @@ import ( "sync" "syscall/js" - "gitlab.com/gomidi/midi/v2/drivers" + "gitlab.com/gomidi/midi/v2" ) -func newOut(driver *Driver, number int, name string, jsport js.Value) drivers.Out { +func newOut(driver *Driver, number int, name string, jsport js.Value) midi.Out { o := &out{driver: driver, number: number, name: name, jsport: jsport} return o } @@ -35,7 +35,7 @@ func (o *out) Send(b []byte) error { o.RLock() if !o.isOpen { o.RUnlock() - return drivers.ErrPortClosed + return midi.ErrPortClosed } o.RUnlock() diff --git a/v2/error.go b/v2/error.go index d111bf3..88bfed5 100644 --- a/v2/error.go +++ b/v2/error.go @@ -2,6 +2,7 @@ package midi import ( "fmt" + "strings" ) type Error string @@ -10,6 +11,25 @@ func (err Error) Error() string { return string(err) } +type Errors []error + +func (errs Errors) Error() string { + switch l := len(errs); l { + case 0: + return "" + case 1: + return errs[0].Error() + } + + sb := strings.Builder{} + for _, err := range errs { + sb.WriteRune('[') + sb.WriteString(err.Error()) + sb.WriteRune(']') + } + return sb.String() +} + const ( ErrMessageLength Error = "message length not expected" ErrChannelInvalid Error = "channel not in range 0-15" @@ -18,15 +38,17 @@ const ( ErrSysExCallback Error = "driver error: received 0xF0 in non sysex callback" ErrMessageCategory Error = "message category not expected" ErrMessageCallbackNil Error = "message callback must be supplied" + ErrPortClosed Error = "port is closed" + ErrListenStopped Error = "stopped listening" ) -func assertMessageLength(msg Message, expLen int) { +func assertMessageLength(msg []byte, expLen int) { if l := len(msg); l != expLen { panic(fmt.Errorf("%w: %d != %d", ErrMessageLength, l, expLen)) } } -func assertMessageLengthMinimum(msg Message, minLen int) { +func assertMessageLengthMinimum(msg []byte, minLen int) { if l := len(msg); l < minLen { panic(fmt.Errorf("%w: %d < %d", ErrMessageLength, l, minLen)) } @@ -38,13 +60,13 @@ func assertChannel(channel uint8) { } } -func assertMessageCategory(t MessageType, c MessageCategory) { - if tc := Category(byte(t)); tc != c { - panic(fmt.Errorf("%w: %s != %s for %s", ErrMessageCategory, tc, c, tc.MessageType(byte(t)))) +func assertMessageCategory(m []byte, c MessageCategory) { + if tc := Category(m); tc != c { + panic(fmt.Errorf("%w: %s != %s for %s", ErrMessageCategory, tc, c, Type(m))) } } -func assertSysExFormat(msg Message) { +func assertSysExFormat(msg []byte) { assertMessageLengthMinimum(msg, 3) if v := msg[0]; v != byte(MessageTypeSysEx) { panic(fmt.Errorf("%w: 0x%02x != 0x%02x", ErrSysExHeaderInvalid, v, MessageTypeSysEx)) diff --git a/v2/example/logger/main.go b/v2/example/logger/main.go index baae39e..fbc786d 100644 --- a/v2/example/logger/main.go +++ b/v2/example/logger/main.go @@ -10,7 +10,7 @@ import ( ) func main() { - defer midi.CloseDriver() + defer midi.Close() in := midi.FindInPort("VMPK") if in < 0 { @@ -20,8 +20,8 @@ func main() { ctx, cancel := context.WithCancel(context.Background()) - if err := midi.Listen(ctx, midi.FindInPortByName("VMPK"), func(msg midi.Message, timestampms int32) { - switch m := msg.TypedMessage().(type) { + if err := midi.Listen(ctx, midi.FindInPortByName("VMPK"), func(msg []byte, timestampms int32) { + switch m := midi.Message(msg).(type) { case midi.SysExMessage: fmt.Printf("got sysex: % X\n", m.Data()) case midi.NoteOnMessage: diff --git a/v2/example/simple/main.go b/v2/example/simple/main.go index c1c6351..20edaaf 100644 --- a/v2/example/simple/main.go +++ b/v2/example/simple/main.go @@ -12,7 +12,7 @@ import ( ) func main() { - defer midi.CloseDriver() + defer midi.Close() for _, o := range midi.OutPorts() { fmt.Printf("out: %s\n", o) diff --git a/v2/example/smfplayer/main.go b/v2/example/smfplayer/main.go index d09e4c3..bb04116 100644 --- a/v2/example/smfplayer/main.go +++ b/v2/example/smfplayer/main.go @@ -61,7 +61,7 @@ func run() error { } func main() { - defer midi.CloseDriver() + defer midi.Close() err := run() if err != nil { diff --git a/v2/example/smfrecorder/main.go b/v2/example/smfrecorder/main.go index 244feac..b8cc9c7 100644 --- a/v2/example/smfrecorder/main.go +++ b/v2/example/smfrecorder/main.go @@ -22,7 +22,7 @@ func main() { func run() error { - defer midi.CloseDriver() + defer midi.Close() in := midi.FindInPort("VMPK") if in < 0 { diff --git a/v2/example_test.go b/v2/example_test.go index 21d97e5..22bdad7 100644 --- a/v2/example_test.go +++ b/v2/example_test.go @@ -17,12 +17,12 @@ import ( func TestExample(t *testing.T) { - var eachMessage = func(msg Message, timestampms int32) { - if c := Category(byte(MessageTypeFromMessage(msg))); c == MessageCategoryRealtime { + var eachMessage = func(msg []byte, timestampms int32) { + if c := Category(msg); c == MessageCategoryRealtime { return } - switch m := msg.TypedMessage().(type) { + switch m := Message(msg).(type) { case NoteOnMessage: fmt.Printf("note started at %vms channel: %v key: %v velocity: %v\n", timestampms, m.Channel(), m.Key(), m.Velocity()) case NoteOffMessage: @@ -36,7 +36,7 @@ func TestExample(t *testing.T) { } // always good to close the driver at the end - defer CloseDriver() + defer Close() // allows you to get the ports when using "real" drivers like rtmididrv or portmididrv if len(os.Args) == 2 && os.Args[1] == "list" { @@ -72,15 +72,15 @@ func TestExample(t *testing.T) { } // send some messages - send(Message(NoteOn(0, Db(4), 100))) + send(NoteOn(0, Db(4), 100)) <-time.After(29 * time.Millisecond) // time.Sleep(time.Millisecond * 30) - send(Message(NoteOff(0, Db(4), 0))) - send(Message(PitchBend(0, -12))) + send(NoteOff(0, Db(4), 0)) + send(PitchBend(0, -12)) <-time.After(19 * time.Millisecond) // time.Sleep(time.Millisecond * 20) - send(Message(ProgramChange(1, 12))) - send(Message(ControlChange(2, FootPedalMSB, On))) + send(ProgramChange(1, 12)) + send(ControlChange(2, FootPedalMSB, On)) // stops listening cancel() diff --git a/v2/gm/reset.go b/v2/gm/reset.go index 1d72931..66c6986 100644 --- a/v2/gm/reset.go +++ b/v2/gm/reset.go @@ -6,11 +6,10 @@ import ( // GMProgram is a shortcut to write GM bank select control change message followed // by a program change. -func GMProgram(ch, prog uint8) (msgs []midi.Messager) { - //c := channel.Channel(ch) - msgs = append(msgs, midi.ControlChange(ch, midi.BankSelectMSB, 0)) - msgs = append(msgs, midi.ProgramChange(ch, prog)) - return +func GMProgram(ch, prog uint8) [][]byte { + return [][]byte{ + midi.ControlChange(ch, midi.BankSelectMSB, 0), + midi.ProgramChange(ch, prog)} } // Reset writes a kind of somewhat homegrown GM/GS reset message. @@ -25,8 +24,8 @@ func GMProgram(ch, prog uint8) (msgs []midi.Messager) { cc hold pedal 0 cc pan position 64 */ -func Reset(ch, prog uint8) []midi.Messager { - return []midi.Messager{ +func Reset(ch, prog uint8) [][]byte { + return [][]byte{ midi.ControlChange(ch, midi.BankSelectMSB, 0), midi.ProgramChange(ch, prog), midi.ControlChange(ch, midi.AllControllersOff, 0), diff --git a/v2/go.mod b/v2/go.mod index 8fe8a25..548d54f 100644 --- a/v2/go.mod +++ b/v2/go.mod @@ -1,4 +1,4 @@ module gitlab.com/gomidi/midi/v2 -go 1.16 +go 1.18 diff --git a/v2/internal/runningstatus/runningstatus.go b/v2/internal/runningstatus/runningstatus.go index ee99fa6..4b7e03b 100644 --- a/v2/internal/runningstatus/runningstatus.go +++ b/v2/internal/runningstatus/runningstatus.go @@ -126,7 +126,7 @@ func (w *smfwriter) Write(raw []byte) []byte { */ // for non channel messages, reset status and write whole message //if !midilib.IsChannelMessage(firstByte) { - if midi.Message(raw).MessageCategory() != midi.MessageCategoryChannel { + if midi.Category(raw) != midi.MessageCategoryChannel { // if midi.GetMsgType(raw).Category() != midi.ChannelMessages { //fmt.Printf("is no channel message, resetting status\n") w.status = 0 @@ -176,7 +176,7 @@ func (w *liveWriter) Write(m []byte) (int, error) { // for non channel messages, reset status and write whole message //if !midilib.IsChannelMessage(msg[0]) { //if midi.GetMsgType(m).Category() != midi.ChannelMessages { - if midi.Message(m).MessageCategory() != midi.MessageCategoryChannel { + if midi.Category(m) != midi.MessageCategoryChannel { // fmt.Printf("is no channel message, resetting status\n") w.status = 0 return w.write(m) diff --git a/v2/io.go b/v2/io.go index 5fe6240..5f1161a 100644 --- a/v2/io.go +++ b/v2/io.go @@ -3,18 +3,11 @@ package midi import ( "fmt" "os" - - "gitlab.com/gomidi/midi/v2/drivers" ) -// CloseDriver closes the default driver. -func CloseDriver() { - drivers.Close() -} - // SendTo returns a function that can be used to send messages to the given midi port. -func SendTo(portno int) (func(msg Message) error, error) { - out, err := drivers.OutByNumber(portno) +func SendTo(portno int) (func([]byte) error, error) { + out, err := OutByNumber(portno) if err != nil { return nil, err } @@ -24,14 +17,14 @@ func SendTo(portno int) (func(msg Message) error, error) { return nil, err } } - return func(msg Message) error { - return out.Send([]byte(msg)) + return func(msg []byte) error { + return out.Send(msg) }, nil } // InPorts returns the MIDI input ports func InPorts() []string { - ins, err := drivers.Ins() + ins, err := Ins() if err != nil { fmt.Fprintf(os.Stderr, "can't get midi in ports: %s\n", err.Error()) @@ -49,7 +42,7 @@ func InPorts() []string { // OutPorts returns the MIDI output ports func OutPorts() []string { - outs, err := drivers.Outs() + outs, err := Outs() if err != nil { fmt.Fprintf(os.Stderr, "can't get midi out ports: %s\n", err.Error()) diff --git a/v2/listen.go b/v2/listen.go index 6ff4475..0b4ebe9 100644 --- a/v2/listen.go +++ b/v2/listen.go @@ -3,68 +3,63 @@ package midi import ( "context" "fmt" - - "gitlab.com/gomidi/midi/v2/drivers" ) // Option is an option for listening -type Option func(*drivers.ListenConfig) +type Option func(*ListenConfig) // UseTimeCode is an option to receive time code messages func UseTimeCode() Option { - return func(l *drivers.ListenConfig) { + return func(l *ListenConfig) { l.TimeCode = true } } // UseActiveSense is an option to receive active sense messages func UseActiveSense() Option { - return func(l *drivers.ListenConfig) { + return func(l *ListenConfig) { l.ActiveSense = true } } // UseSysEx is an option to receive system exclusive messages func UseSysEx() Option { - return func(l *drivers.ListenConfig) { + return func(l *ListenConfig) { l.SysEx = true } } // SysExBufferSize is an option to set the buffer size for sysex messages func SysExBufferSize(size uint32) Option { - return func(l *drivers.ListenConfig) { + return func(l *ListenConfig) { l.SysExBufferSize = size } } // HandleError sets an error handler when receiving messages func HandleError(cb func(error)) Option { - return func(l *drivers.ListenConfig) { + return func(l *ListenConfig) { l.OnErr = cb } } -var ErrPortClosed = drivers.ErrPortClosed -var ErrListenStopped = drivers.ErrListenStopped - -type ListenPortFinderFunc func() (drivers.In, error) +type ListenPortFinderFunc func() (In, error) func FindInPortByName(name string) ListenPortFinderFunc { - return ListenPortFinderFunc(func() (drivers.In, error) { - return drivers.InByName(name) + return ListenPortFinderFunc(func() (In, error) { + return InByName(name) }) } func FindInPortByNumber(portNum int) ListenPortFinderFunc { - return ListenPortFinderFunc(func() (drivers.In, error) { - return drivers.InByNumber(portNum) + return ListenPortFinderFunc(func() (In, error) { + return InByNumber(portNum) }) } // ListenTo listens on the given port number and passes the received MIDI data to the given receiver. // It returns a stop function that may be called to stop the listening. -func Listen(ctx context.Context, inPort ListenPortFinderFunc, recv func(msg Message, timestampms int32), opts ...Option) error { +func Listen(ctx context.Context, inPort ListenPortFinderFunc, recv func(msg []byte, timestampms int32), opts ...Option) error { in, err := inPort() if err != nil { return err @@ -76,7 +71,7 @@ func Listen(ctx context.Context, inPort ListenPortFinderFunc, recv func(msg Mess } } - var conf drivers.ListenConfig + var conf ListenConfig for _, opt := range opts { opt(&conf) } @@ -86,7 +81,7 @@ func Listen(ctx context.Context, inPort ListenPortFinderFunc, recv func(msg Mess case len(data) == 0: return case data[0] == 0xF0: - err := fmt.Errorf("%w: %s", ErrSysExCallback, drivers.Get().String()) + err := fmt.Errorf("%w: %T", ErrSysExCallback, Default()) if conf.OnErr == nil { panic(err) } @@ -94,7 +89,7 @@ func Listen(ctx context.Context, inPort ListenPortFinderFunc, recv func(msg Mess return } - recv(Message(data), millisec) + recv(data, millisec) } return in.Listen(ctx, onMsg, conf) diff --git a/v2/message.go b/v2/message.go index a7a3ba2..dbe4599 100644 --- a/v2/message.go +++ b/v2/message.go @@ -1,13 +1,8 @@ package midi -import ( - "fmt" -) - //go:generate stringer -type=MessageCategory,MessageType -trimprefix=MessageCategory,MessageType -output stringer.go type MessageCategory byte -type MessageCategoryTypeFunc func(byte) MessageType const ( MessageCategoryUnknown MessageCategory = 0x00 @@ -16,30 +11,18 @@ const ( MessageCategoryRealtime MessageCategory = 0xff ) -func Category(b byte) MessageCategory { - switch { - case b >= byte(MessageTypeNoteOff) && b < byte(MessageTypeSysEx): - return MessageCategoryChannel - case b >= byte(MessageTypeSysEx) && b < byte(MessageTypeBeatClock): - return MessageCategoryCommon - case b >= byte(MessageTypeBeatClock): - return MessageCategoryRealtime - default: - return MessageCategoryUnknown - } -} - -func (c MessageCategory) MessageType(b byte) MessageType { - switch { - case c == MessageCategoryChannel: - return messageCategoryChannelType(b) - case c == MessageCategoryCommon: - return messageCategoryCommonType(b) - case c == MessageCategoryRealtime: - return messageCategoryRealtimeType(b) - default: - return messageCategoryUnknownType(b) +func Category(msg []byte) MessageCategory { + if len(msg) > 0 { + switch b := msg[0]; { + case b >= byte(MessageTypeNoteOff) && b < byte(MessageTypeSysEx): + return MessageCategoryChannel + case b >= byte(MessageTypeSysEx) && b < byte(MessageTypeBeatClock): + return MessageCategoryCommon + case b >= byte(MessageTypeBeatClock): + return MessageCategoryRealtime + } } + return MessageCategoryUnknown } func (c MessageCategory) String() string { @@ -50,39 +33,22 @@ func (c MessageCategory) String() string { return "Common" case MessageCategoryRealtime: return "Realtime" - default: - return "Unknown" } + return "Unknown" } -func messageCategoryUnknownType(b byte) MessageType { - return MessageTypeUnknown -} - -func messageCategoryChannelType(b byte) MessageType { - return MessageType(b & byte(MessageTypeSysEx)) -} - -func messageCategoryCommonType(b byte) MessageType { - return MessageType(b) -} - -func messageCategoryRealtimeType(b byte) MessageType { - return MessageType(b) -} - -func MessageTypeFromByte(b byte) MessageType { - return Category(b).MessageType(b) -} - -func MessageTypeFromMessage(m Message) MessageType { - if len(m) == 0 { - return MessageTypeUnknown +func Type(msg []byte) MessageType { + switch c := Category(msg); c { + case MessageCategoryChannel: + return MessageType(msg[0] & byte(MessageTypeSysEx)) + case MessageCategoryCommon: + return MessageType(msg[0]) + case MessageCategoryRealtime: + return MessageType(msg[0]) } - return MessageTypeFromByte(m[0]) + return MessageTypeUnknown } -// MessageType is the type of a midi message type MessageType byte const ( @@ -160,85 +126,54 @@ func (t MessageType) String() string { return "ActiveSense" case MessageTypeReset: return "Reset" - default: - return "Unknown" } + return "Unknown" } -type Manufacturer byte - -const ( - ManufacturerUniversal Manufacturer = 0x7f -) - -// Message is a complete midi message (not including meta messages) -type Message []byte - -func (m Message) TypedMessage() interface{} { - switch t := MessageTypeFromMessage(m); t { +func Message(msg []byte) interface{} { + switch t := Type(msg); t { case MessageTypeNoteOff: - return NoteOffMessage(m) + return NoteOffMessage(msg) case MessageTypeNoteOn: - return NoteOnMessage(m) + return NoteOnMessage(msg) case MessageTypePolyAfterTouch: - return PolyAfterTouchMessage(m) + return PolyAfterTouchMessage(msg) case MessageTypeControlChange: - return ControlChangeMessage(m) + return ControlChangeMessage(msg) case MessageTypeProgramChange: - return ProgramChangeMessage(m) + return ProgramChangeMessage(msg) case MessageTypeAfterTouch: - return AfterTouchMessage(m) + return AfterTouchMessage(msg) case MessageTypePitchBend: - return PitchBendMessage(m) + return PitchBendMessage(msg) case MessageTypeSysEx: - return SysExMessage(m) + return SysExMessage(msg) case MessageTypeSongPosition: - return SongPositionMessage(m) + return SongPositionMessage(msg) case MessageTypeSongSelect: - return SongSelectMessage(m) + return SongSelectMessage(msg) case MessageTypeTuneRequest: - return TuneRequestMessage(m) + return TuneRequestMessage(msg) case MessageTypeBeatClock: - return BeatClockMessage(m) + return BeatClockMessage(msg) case MessageTypeStart: - return StartMessage(m) + return StartMessage(msg) case MessageTypeStop: - return StopMessage(m) + return StopMessage(msg) case MessageTypeActiveSense: - return ActiveSenseMessage(m) + return ActiveSenseMessage(msg) case MessageTypeReset: - return ResetMessage(m) + return ResetMessage(msg) default: - return m + return msg } } -func (m Message) MessageType() MessageType { - return MessageTypeFromMessage(m) -} - -func (m Message) MessageCategory() MessageCategory { - return Category(byte(m.MessageType())) -} - -func (m Message) Message() Message { - return m -} - -// String represents the Message as a string that contains the Type and its properties. -func (m Message) String() string { - tm := m.TypedMessage() - if tm, ok := tm.(fmt.Stringer); ok { - return tm.String() - } - - t := m.MessageType() - return fmt.Sprintf("%s %v", t, tm) -} +type Manufacturer byte -type Messager interface { - Message() Message -} +const ( + ManufacturerUniversal Manufacturer = 0x7f +) /* MTC Quarter Frame diff --git a/v2/nrpn/nrpn.go b/v2/nrpn/nrpn.go index a2d654a..d765f38 100644 --- a/v2/nrpn/nrpn.go +++ b/v2/nrpn/nrpn.go @@ -4,20 +4,20 @@ import ( "gitlab.com/gomidi/midi/v2" ) -func cc(channel, ctl, val uint8) midi.Messager { +func cc(channel, ctl, val uint8) []byte { return midi.ControlChange(channel, ctl, val) } // Reset aka Null -func Reset(channel uint8) []midi.Messager { - return append([]midi.Messager{}, +func Reset(channel uint8) [][]byte { + return append([][]byte{}, cc(channel, 99, 127), cc(channel, 98, 127), ) } -func Increment(channel uint8, val99, val98 uint8) []midi.Messager { - msgs := append([]midi.Messager{}, +func Increment(channel uint8, val99, val98 uint8) [][]byte { + msgs := append([][]byte{}, cc(channel, 99, val99), cc(channel, 98, val98), cc(channel, 96, 0)) @@ -25,8 +25,8 @@ func Increment(channel uint8, val99, val98 uint8) []midi.Messager { return append(msgs, Reset(channel)...) } -func Decrement(channel uint8, val99, val98 uint8) []midi.Messager { - msgs := append([]midi.Messager{}, +func Decrement(channel uint8, val99, val98 uint8) [][]byte { + msgs := append([][]byte{}, cc(channel, 99, val99), cc(channel, 98, val98), cc(channel, 97, 0)) @@ -35,8 +35,8 @@ func Decrement(channel uint8, val99, val98 uint8) []midi.Messager { } // NRPN message consisting of a val99 and val98 to identify the NRPN and a msb and lsb for the value -func NRPN(channel uint8, val99, val98, msbVal, lsbVal uint8) []midi.Messager { - msgs := append([]midi.Messager{}, +func NRPN(channel uint8, val99, val98, msbVal, lsbVal uint8) [][]byte { + msgs := append([][]byte{}, cc(channel, 99, val99), cc(channel, 98, val98), cc(channel, 6, msbVal), diff --git a/v2/port.go b/v2/port.go index 124d2d1..a6c2b8e 100644 --- a/v2/port.go +++ b/v2/port.go @@ -1,33 +1,152 @@ package midi import ( - "gitlab.com/gomidi/midi/v2/drivers" + "context" + "fmt" + "io" + "strings" ) // FindInPort returns the number of the midi in port with the given name // It returns -1, if the port can't be found. func FindInPort(name string) int { - in, err := drivers.InByName(name) + in, err := InByName(name) if err != nil { return -1 } defer in.Close() return in.Number() + } // CloseInPort closes the in port func CloseInPort(num int) error { - in, err := drivers.InByNumber(num) + in, err := InByNumber(num) if err != nil { return err } return in.Close() } +// Port is an interface for a MIDI port. +// In order to be lockless (for realtime), a port is not threadsafe, so none of its method may be called +// from different goroutines. +type Port interface { + io.Closer + + // Open opens the MIDI port. An implementation should save the open state to make it + // save to call open when the port is already open without getting an error. + Open() error + + // IsOpen returns wether the MIDI port is open. + IsOpen() bool + + // Number returns the number of the MIDI port. It is only guaranteed that the numbers are unique within + // MIDI port groups i.e. within MIDI input ports and MIDI output ports. So there may be the same number + // for a given MIDI input port and some MIDI output port. Or not - that depends on the underlying driver. + Number() int + + // String represents the MIDI port by a string, aka name. + String() string +} + +// ListenConfig defines the configuration for in port listening +type ListenConfig struct { + + // TimeCode lets the timecode messages pass through, if set + TimeCode bool + + // ActiveSense lets the active sense messages pass through, if set + ActiveSense bool + + // SysEx lets the sysex messaes pass through, if set + SysEx bool + + // SysExBufferSize defines the size of the buffer for sysex messages (in bytes). + // SysEx messages larger than this size will be ignored. + // When SysExBufferSize is 0, the default buffersize (1024) is used. + SysExBufferSize uint32 + + // OnErr is the callback that is called for any error happening during the listening. + OnErr func(error) +} + +// In is an interface for a MIDI input port +type In interface { + Port + + // Listen listens for incoming messages. It returns a function that must be used to stop listening. + // The onMsg callback is called for every non-sysex message. The onMsg callback must not be nil. + // The config defines further listening options (see ListenConfig) + // The listening must be stopped before the port may be closed. + Listen( + ctx context.Context, + onMsg func(msg []byte, milliseconds int32), + config ListenConfig, + ) error +} + +// Ins return the available MIDI in ports +func Ins() ([]In, error) { + return Default().Ins() +} + +// InByName opens the first midi in port that contains the given name +func InByName(portName string) (in In, err error) { + return openIn(Default(), -1, portName) +} + +// InByNumber opens the midi in port with the given number +func InByNumber(portNumber int) (in In, err error) { + return openIn(Default(), portNumber, "") +} + +// openIn opens a MIDI input port with the help of the given driver. +// To find the port by port number, pass a number >= 0. +// To find the port by port name, pass a number < 0 and a non empty string. +func openIn(d Driver, number int, name string) (in In, err error) { + ins, err := d.Ins() + if err != nil { + return nil, fmt.Errorf("can't find MIDI input ports: %v", err) + } + + if number >= 0 { + for _, port := range ins { + if number == port.Number() { + in = port + break + } + } + if in == nil { + return nil, fmt.Errorf("can't find MIDI input port %v", number) + } + } else { + if name != "" { + for _, port := range ins { + if strings.Contains(port.String(), name) { + in = port + break + } + } + } + if in == nil { + return nil, fmt.Errorf("can't find MIDI input port %v", name) + } + } + + // should not happen here, since we already returned above + if in == nil { + panic("unreachable") + } + + err = in.Open() + return +} + // FindOutPort returns the number of the midi out port with the given name // It returns -1, if the port can't be found. func FindOutPort(name string) int { - out, err := drivers.OutByName(name) + out, err := OutByName(name) if err != nil { return -1 } @@ -37,9 +156,86 @@ func FindOutPort(name string) int { // CloseOutPort closes the out port func CloseOutPort(num int) error { - out, err := drivers.OutByNumber(num) + out, err := OutByNumber(num) if err != nil { return err } return out.Close() } + +// Out is an interface for a MIDI output port. +type Out interface { + Port + + //Send(data [3]byte) error + Send(data []byte) error +} + +// Outs return the available MIDI out ports +func Outs() ([]Out, error) { + d := Default() + if d == nil { + return nil, fmt.Errorf("no driver registered") + } + return d.Outs() +} + +// OutByName opens the first midi out port that contains the given name +func OutByName(portName string) (out Out, err error) { + drv := Default() + if drv == nil { + return nil, fmt.Errorf("no driver registered") + } + return openOut(drv, -1, portName) +} + +// OutByNumber opens the midi out port with the given number +func OutByNumber(portNumber int) (out Out, err error) { + drv := Default() + if drv == nil { + return nil, fmt.Errorf("no driver registered") + } + return openOut(drv, portNumber, "") +} + +// openOut opens a MIDI output port with the help of the given driver. +// To find the port by port number, pass a number >= 0. +// To find the port by port name, pass a number < 0 and a non empty string. +func openOut(d Driver, number int, name string) (out Out, err error) { + outs, err := d.Outs() + if err != nil { + return nil, fmt.Errorf("can't find MIDI output ports: %v", err) + } + + if number >= 0 { + for _, port := range outs { + if number == port.Number() { + out = port + break + } + } + if out == nil { + return nil, fmt.Errorf("can't find MIDI output port %v", number) + } + } else { + if name != "" { + for _, port := range outs { + if strings.Contains(port.String(), name) { + out = port + break + } + } + } + if out == nil { + return nil, fmt.Errorf("can't find MIDI output port %v", name) + } + } + + // should not happen here, since we already returned above + if out == nil { + panic("unreachable") + } + + err = out.Open() + return +} diff --git a/v2/drivers/reader.go b/v2/reader.go similarity index 99% rename from v2/drivers/reader.go rename to v2/reader.go index 3880e7b..a812d55 100644 --- a/v2/drivers/reader.go +++ b/v2/reader.go @@ -1,4 +1,4 @@ -package drivers +package midi import ( "fmt" @@ -49,7 +49,7 @@ type Reader struct { SysExBufferSize uint32 OnMsg func([]byte, int32) HandleSysex bool - OnErr func(error) + OnErr func(error) } func (r *Reader) withinChannelMessage(b byte) { @@ -187,7 +187,7 @@ func (r *Reader) cleanState(b byte) { func (r *Reader) eachByte(b byte) { if b >= 0xF8 { - r.OnMsg([]byte{b, 0, 0}, r.ts_ms) + r.OnMsg([]byte{b}, r.ts_ms) return } diff --git a/v2/realtime.go b/v2/realtime.go index 3df91ed..8bc0c44 100644 --- a/v2/realtime.go +++ b/v2/realtime.go @@ -1,20 +1,14 @@ package midi -import "fmt" - // BeatClock returns a timing clock message func BeatClock() BeatClockMessage { return BeatClockMessage([]byte{byte(MessageTypeBeatClock)}) } -type BeatClockMessage Message - -func (m BeatClockMessage) Message() Message { - return Message(m) -} +type BeatClockMessage []byte func (m BeatClockMessage) String() string { - return fmt.Sprintf("%s", MessageTypeBeatClock) + return MessageTypeBeatClock.String() } // Start returns a start message @@ -22,14 +16,10 @@ func Start() StartMessage { return StartMessage([]byte{byte(MessageTypeStart)}) } -type StartMessage Message - -func (m StartMessage) Message() Message { - return Message(m) -} +type StartMessage []byte func (m StartMessage) String() string { - return fmt.Sprintf("%s", MessageTypeStart) + return MessageTypeStart.String() } // Continue returns a continue message @@ -37,14 +27,10 @@ func Continue() ContinueMessage { return ContinueMessage([]byte{byte(MessageTypeContinue)}) } -type ContinueMessage Message - -func (m ContinueMessage) Message() Message { - return Message(m) -} +type ContinueMessage []byte func (m ContinueMessage) String() string { - return fmt.Sprintf("%s", MessageTypeContinue) + return MessageTypeContinue.String() } // Stop returns a stop message @@ -52,14 +38,10 @@ func Stop() StopMessage { return StopMessage([]byte{byte(MessageTypeStop)}) } -type StopMessage Message - -func (m StopMessage) Message() Message { - return Message(m) -} +type StopMessage []byte func (m StopMessage) String() string { - return fmt.Sprintf("%s", MessageTypeStop) + return MessageTypeStop.String() } // Activesense returns an active sensing message @@ -67,14 +49,10 @@ func ActiveSense() ActiveSenseMessage { return ActiveSenseMessage([]byte{byte(MessageTypeActiveSense)}) } -type ActiveSenseMessage Message - -func (m ActiveSenseMessage) Message() Message { - return Message(m) -} +type ActiveSenseMessage []byte func (m ActiveSenseMessage) String() string { - return fmt.Sprintf("%s", MessageTypeActiveSense) + return MessageTypeActiveSense.String() } // Reset returns a reset message @@ -82,12 +60,8 @@ func Reset() ResetMessage { return ResetMessage([]byte{byte(MessageTypeReset)}) } -type ResetMessage Message - -func (m ResetMessage) Message() Message { - return Message(m) -} +type ResetMessage []byte func (m ResetMessage) String() string { - return fmt.Sprintf("%s", MessageTypeReset) + return MessageTypeReset.String() } diff --git a/v2/rpn/rpn.go b/v2/rpn/rpn.go index 4662b80..57a2e45 100644 --- a/v2/rpn/rpn.go +++ b/v2/rpn/rpn.go @@ -10,46 +10,46 @@ CC100 00 Selects pitch bend as the parameter you want to adjust. CC06 XX Sensitivity in half steps. The range is 0-24. */ -func cc(channel, ctl uint8, val uint8) midi.Messager { +func cc(channel, ctl uint8, val uint8) []byte { return midi.ControlChange(channel, ctl, val) } // PitchBendSensitivity sets the pitch bend range via RPN -func PitchBendSensitivity(channel, msbVal, lsbVal uint8) []midi.Messager { +func PitchBendSensitivity(channel, msbVal, lsbVal uint8) [][]byte { return RPN(channel, 0, 0, msbVal, lsbVal) } // FineTuning -func FineTuning(channel, msbVal, lsbVal uint8) []midi.Messager { +func FineTuning(channel, msbVal, lsbVal uint8) [][]byte { return RPN(channel, 0, 1, msbVal, lsbVal) } // CoarseTuning -func CoarseTuning(channel, msbVal, lsbVal uint8) []midi.Messager { +func CoarseTuning(channel, msbVal, lsbVal uint8) [][]byte { return RPN(channel, 0, 2, msbVal, lsbVal) } // TuningProgramSelect -func TuningProgramSelect(channel, msbVal, lsbVal uint8) []midi.Messager { +func TuningProgramSelect(channel, msbVal, lsbVal uint8) [][]byte { return RPN(channel, 0, 3, msbVal, lsbVal) } // TuningBankSelect -func TuningBankSelect(channel, msbVal, lsbVal uint8) []midi.Messager { +func TuningBankSelect(channel, msbVal, lsbVal uint8) [][]byte { return RPN(channel, 0, 4, msbVal, lsbVal) } // Reset aka Null -func Reset(channel uint8) []midi.Messager { - return append([]midi.Messager{}, +func Reset(channel uint8) [][]byte { + return append([][]byte{}, cc(channel, 101, 127), cc(channel, 100, 127), ) } // RPN message consisting of a val101 and val100 to identify the RPN and a msb and lsb for the value -func RPN(channel, val101, val100, msbVal, lsbVal uint8) []midi.Messager { - msgs := append([]midi.Messager{}, +func RPN(channel, val101, val100, msbVal, lsbVal uint8) [][]byte { + msgs := append([][]byte{}, cc(channel, 101, val101), cc(channel, 100, val100), cc(channel, 6, msbVal), @@ -58,8 +58,8 @@ func RPN(channel, val101, val100, msbVal, lsbVal uint8) []midi.Messager { return append(msgs, Reset(channel)...) } -func Increment(channel, val101, val100 uint8) []midi.Messager { - msgs := append([]midi.Messager{}, +func Increment(channel, val101, val100 uint8) [][]byte { + msgs := append([][]byte{}, cc(channel, 101, val101), cc(channel, 100, val100), cc(channel, 96, 0)) @@ -67,8 +67,8 @@ func Increment(channel, val101, val100 uint8) []midi.Messager { return append(msgs, Reset(channel)...) } -func Decrement(channel, val101, val100 uint8) []midi.Messager { - msgs := append([]midi.Messager{}, +func Decrement(channel, val101, val100 uint8) [][]byte { + msgs := append([][]byte{}, cc(channel, 101, val101), cc(channel, 100, val100), cc(channel, 97, 0)) diff --git a/v2/syscommon.go b/v2/syscommon.go index 5d3b98e..83a1bbc 100644 --- a/v2/syscommon.go +++ b/v2/syscommon.go @@ -14,10 +14,10 @@ func SongPosition(pointer uint16) SongPositionMessage { byte((pointer >> 7) & 0x7F)}) } -type SongPositionMessage Message +type SongPositionMessage []byte func (m SongPositionMessage) Pointer() uint16 { - assertMessageLength(Message(m), 3) + assertMessageLength(m, 3) _, spp := utils.ParsePitchWheelVals(m[1], m[2]) return spp } @@ -26,10 +26,6 @@ func (m SongPositionMessage) String() string { return fmt.Sprintf("%s pointer: %d", MessageTypeSongPosition, m.Pointer()) } -func (m SongPositionMessage) Message() Message { - return Message(m) -} - // SongSelect returns a song select message func SongSelect(song uint8) SongSelectMessage { return SongSelectMessage([]byte{ @@ -37,17 +33,13 @@ func SongSelect(song uint8) SongSelectMessage { song}) } -type SongSelectMessage Message +type SongSelectMessage []byte func (m SongSelectMessage) Song() uint8 { - assertMessageLength(Message(m), 2) + assertMessageLength(m, 2) return utils.ParseUint7(m[1]) } -func (m SongSelectMessage) Message() Message { - return Message(m) -} - func (m SongSelectMessage) String() string { return fmt.Sprintf("%s song: %d", MessageTypeSongSelect, m.Song()) } @@ -57,14 +49,10 @@ func TuneRequest() TuneRequestMessage { return TuneRequestMessage([]byte{byte(MessageTypeTuneRequest)}) } -type TuneRequestMessage Message - -func (m TuneRequestMessage) Message() Message { - return Message(m) -} +type TuneRequestMessage []byte func (m TuneRequestMessage) String() string { - return fmt.Sprintf("%s", MessageTypeTuneRequest) + return MessageTypeTuneRequest.String() } /* diff --git a/v2/syscommon_test.go b/v2/syscommon_test.go index 1649720..d7207bb 100644 --- a/v2/syscommon_test.go +++ b/v2/syscommon_test.go @@ -14,23 +14,23 @@ func TestSysCommon(t *testing.T) { expected string }{ { - midi.Message(midi.BeatClock()), + midi.BeatClock(), "BeatClock", }, { - midi.Message(midi.TuneRequest()), + midi.TuneRequest(), "TuneRequest", }, { - midi.Message(midi.SongSelect(5)), + midi.SongSelect(5), "SongSelect song: 5", }, { - midi.Message(midi.SongPosition(4)), + midi.SongPosition(4), "SongPosition pointer: 4", }, { - midi.Message(midi.SongPosition(4000)), + midi.SongPosition(4000), "SongPosition pointer: 4000", }, } diff --git a/v2/sysex.go b/v2/sysex.go index 5c9f1b4..f39d208 100644 --- a/v2/sysex.go +++ b/v2/sysex.go @@ -12,96 +12,17 @@ func SysEx(data []byte) SysExMessage { return SysExMessage(m) } -type SysExMessage Message +type SysExMessage []byte func (m SysExMessage) Data() []byte { - assertSysExFormat(Message(m)) + assertSysExFormat(m) return m[1 : len(m)-1] } -func (m SysExMessage) Message() Message { - return Message(m) -} - func (m SysExMessage) String() string { return fmt.Sprintf("%s data: % X", MessageTypeSysEx, m.Data()) } -/* -import ( - "io" - - "gitlab.com/gomidi/midi2/msg" -) - -func newSysexReader() *sysexReader { - return &sysexReader{} -} - -type sysexReader struct { - inSequence bool -} -*/ - -//func (s *sysexReader) Read(startcode byte, rd io.Reader) (sys msg.Message, err error) { -/* - what this means to us is relatively simple: - we read the data after the startcode based of the following length - and return the sysex chunk with the start code. - If it ends with F7 or not, is not our business (the device has to deal with it). - Also, if there are multiple sysexes belonging to each other yada yada. -*/ -/* - switch startcode { - case 0xF0: - // fmt.Println("read sysex with startcode 0xF0") - var data []byte - data, err = midilib.ReadVarLengthData(rd) - - if err != nil { - return nil, err - } - - // complete sysex - if data[len(data)-1] == 0xF7 { - s.inSequence = false - return sysex.SysEx(data[0 : len(data)-1]), nil - } - - // casio style - s.inSequence = true - return sysex.Start(data), nil - - case 0xF7: - var data []byte - data, err = midilib.ReadVarLengthData(rd) - - if err != nil { - return nil, err - } - - // End of sysex sequence - if data[len(data)-1] == 0xF7 { - // casio style - if s.inSequence { - s.inSequence = false - return sysex.End(data[0 : len(data)-1]), nil - } - return sysex.Escape(data), nil - - } - // casio style - if s.inSequence { - return sysex.Continue(data), nil - } - return sysex.Escape(data), nil - - default: - panic("sysex in SMF must start with F0 or F7") - } - -} -*/ /* F0 -- GitLab From df4efae492f19c74615f5b405211b25f639c870d Mon Sep 17 00:00:00 2001 From: Brian Stark Date: Sat, 16 Apr 2022 14:21:46 -0600 Subject: [PATCH 6/6] update rtmidi --- v2/drivers/rtmididrv/.travis.yml | 15 - v2/drivers/rtmididrv/CONTRIBUTING | 5 - v2/drivers/rtmididrv/go.mod | 13 - v2/drivers/rtmididrv/go.sum | 23 - .../rtmididrv/imported/rtmidi/cpp/RtMidi.cpp | 707 +++++++++++++++--- .../rtmididrv/imported/rtmidi/cpp/RtMidi.h | 41 +- .../imported/rtmidi/cpp/rtmidi_c.cpp | 113 ++- v2/drivers/rtmididrv/imported/rtmidi/go.mod | 3 - .../rtmididrv/imported/rtmidi/rtmidi.go | 55 +- .../rtmididrv/imported/rtmidi/rtmidi_c.h | 8 +- .../rtmididrv/imported/rtmidi/rtmidi_stub.cpp | 1 + v2/drivers/rtmididrv/in.go | 4 +- v2/listen.go | 1 + 13 files changed, 772 insertions(+), 217 deletions(-) delete mode 100644 v2/drivers/rtmididrv/.travis.yml delete mode 100644 v2/drivers/rtmididrv/CONTRIBUTING delete mode 100644 v2/drivers/rtmididrv/go.mod delete mode 100644 v2/drivers/rtmididrv/go.sum delete mode 100644 v2/drivers/rtmididrv/imported/rtmidi/go.mod diff --git a/v2/drivers/rtmididrv/.travis.yml b/v2/drivers/rtmididrv/.travis.yml deleted file mode 100644 index 802efbc..0000000 --- a/v2/drivers/rtmididrv/.travis.yml +++ /dev/null @@ -1,15 +0,0 @@ -language: go -go: - - "1.11.x" - - tip - -sudo: false -#before_install: -# - go get github.com/mattn/goveralls -#script: -# - $HOME/gopath/bin/goveralls -service=travis-ci - -before_install: - - sudo apt-get install libasound2-dev -env: - - GO111MODULE=on \ No newline at end of file diff --git a/v2/drivers/rtmididrv/CONTRIBUTING b/v2/drivers/rtmididrv/CONTRIBUTING deleted file mode 100644 index a945964..0000000 --- a/v2/drivers/rtmididrv/CONTRIBUTING +++ /dev/null @@ -1,5 +0,0 @@ -All development activity takes place on Gitlab. - -If you think you've discovered a bug, or you would like -to make a feature request please do this through Gitlab. - diff --git a/v2/drivers/rtmididrv/go.mod b/v2/drivers/rtmididrv/go.mod deleted file mode 100644 index 746586e..0000000 --- a/v2/drivers/rtmididrv/go.mod +++ /dev/null @@ -1,13 +0,0 @@ -module gitlab.com/gomidi/midi/v2/drivers/rtmididrv - -go 1.16 - -require ( - gitlab.com/gomidi/midi/v2 v2.0.0-alpha.15 - gitlab.com/gomidi/midi/v2/drivers/rtmididrv/imported/rtmidi v0.0.0-20210425073027-dcb5d7eb9e83 // indirect -) - -replace ( - gitlab.com/gomidi/midi/v2 => ../../ - gitlab.com/gomidi/midi/v2/drivers/rtmididrv/imported/rtmidi => ./imported/rtmidi -) diff --git a/v2/drivers/rtmididrv/go.sum b/v2/drivers/rtmididrv/go.sum deleted file mode 100644 index 55d29aa..0000000 --- a/v2/drivers/rtmididrv/go.sum +++ /dev/null @@ -1,23 +0,0 @@ -gitlab.com/gomidi/midi v1.7.4 h1:wJFrJpD+oDvilYhNvyutG2cjdvx2wnAB1s11Kxdk4Tg= -gitlab.com/gomidi/midi v1.7.4/go.mod h1:Geyousi3Lg15GJRg0CnpxQMQIgDLapqJKq2Cbf7LJfs= -gitlab.com/gomidi/midi v1.9.0 h1:nLwy9vnnlFiaaLBznLnxLMaqz/yrCbf7ajfn60koIaQ= -gitlab.com/gomidi/midi v1.9.0/go.mod h1:Geyousi3Lg15GJRg0CnpxQMQIgDLapqJKq2Cbf7LJfs= -gitlab.com/gomidi/midi v1.10.0 h1:zh1vPE1oAPCs1vkFwGGS8iftsQzlChjMxhIQOj9ReSE= -gitlab.com/gomidi/midi v1.10.0/go.mod h1:Geyousi3Lg15GJRg0CnpxQMQIgDLapqJKq2Cbf7LJfs= -gitlab.com/gomidi/midi v1.14.1 h1:+I3MHxspu3H1o2ejAdehBLmIUBTOIdlOlmkdKvCJlME= -gitlab.com/gomidi/midi v1.14.1/go.mod h1:3ohtNOhqoSakkuLG/Li1OI6I3J1c2LErnJF5o/VBq1c= -gitlab.com/gomidi/midi v1.14.4 h1:tL6YI1vlUC7frWJukrg1JRsDzrDSZDF1k2N3QAXMOb4= -gitlab.com/gomidi/midi v1.14.4/go.mod h1:3ohtNOhqoSakkuLG/Li1OI6I3J1c2LErnJF5o/VBq1c= -gitlab.com/gomidi/midi v1.15.0 h1:eGSQqAGLB+OUr8tFtKOLWJE3jRAoIZorRVeKEATemRo= -gitlab.com/gomidi/midi v1.15.0/go.mod h1:3ohtNOhqoSakkuLG/Li1OI6I3J1c2LErnJF5o/VBq1c= -gitlab.com/gomidi/midi v1.15.3 h1:PSgwBOnB+8cY2IeMaBL3gKfehGt4q9B3vc70ISHJvvs= -gitlab.com/gomidi/midi v1.15.3/go.mod h1:3ohtNOhqoSakkuLG/Li1OI6I3J1c2LErnJF5o/VBq1c= -gitlab.com/gomidi/midi v1.16.2 h1:Z+UNzTMoKRHIGXPr5cq0l02mMswbOL33SsYJMDhIIDA= -gitlab.com/gomidi/midi v1.16.2/go.mod h1:3ohtNOhqoSakkuLG/Li1OI6I3J1c2LErnJF5o/VBq1c= -gitlab.com/gomidi/midi v1.16.4 h1:Cl2HrJh+ww7ivNlxyKbhEI7q3j9nqv7y/FF+8KFwz3s= -gitlab.com/gomidi/midi v1.16.4/go.mod h1:3ohtNOhqoSakkuLG/Li1OI6I3J1c2LErnJF5o/VBq1c= -gitlab.com/gomidi/midi v1.21.0 h1:eyoUlx7/PTRUcmWWWD3OKxUYuRWbcDa2rCvRYY7Y4yc= -gitlab.com/gomidi/midi v1.21.0/go.mod h1:3ohtNOhqoSakkuLG/Li1OI6I3J1c2LErnJF5o/VBq1c= -gitlab.com/gomidi/rtmididrv/imported/rtmidi v0.0.0-20181030132923-7607b12e13d8/go.mod h1:FYVFN2H23IsX56VntiDF9DgCIekHh359wW+iMl1W8rQ= -gitlab.com/gomidi/rtmididrv/imported/rtmidi v0.0.0-20191025100939-514fe0ed97a6 h1:0XqAH/BAxH5TTBzIWkdlZqpp6VUx6DFcQnMWW6G6hIc= -gitlab.com/gomidi/rtmididrv/imported/rtmidi v0.0.0-20191025100939-514fe0ed97a6/go.mod h1:FYVFN2H23IsX56VntiDF9DgCIekHh359wW+iMl1W8rQ= diff --git a/v2/drivers/rtmididrv/imported/rtmidi/cpp/RtMidi.cpp b/v2/drivers/rtmididrv/imported/rtmidi/cpp/RtMidi.cpp index fb16ac4..cfd4263 100644 --- a/v2/drivers/rtmididrv/imported/rtmidi/cpp/RtMidi.cpp +++ b/v2/drivers/rtmididrv/imported/rtmidi/cpp/RtMidi.cpp @@ -9,7 +9,7 @@ RtMidi WWW site: http://www.music.mcgill.ca/~gary/rtmidi/ RtMidi: realtime MIDI i/o C++ classes - Copyright (c) 2003-2019 Gary P. Scavone + Copyright (c) 2003-2021 Gary P. Scavone Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files @@ -39,12 +39,35 @@ #include "RtMidi.h" #include +#if defined(__APPLE__) +#include +#endif + +#if (TARGET_OS_IPHONE == 1) -#if defined(__MACOSX_CORE__) - #if TARGET_OS_IPHONE #define AudioGetCurrentHostTime CAHostTimeBase::GetCurrentTime #define AudioConvertHostTimeToNanos CAHostTimeBase::ConvertToNanos - #endif + + #include + class CTime2nsFactor + { + public: + CTime2nsFactor() + { + mach_timebase_info_data_t tinfo; + mach_timebase_info(&tinfo); + Factor = (double)tinfo.numer / tinfo.denom; + } + static double Factor; + }; + double CTime2nsFactor::Factor; + static CTime2nsFactor InitTime2nsFactor; + #undef AudioGetCurrentHostTime + #undef AudioConvertHostTimeToNanos + #define AudioGetCurrentHostTime (uint64_t) mach_absolute_time + #define AudioConvertHostTimeToNanos(t) t *CTime2nsFactor::Factor + #define EndianS32_BtoN(n) n + #endif // Default for Windows is to add an identifier to the port names; this @@ -57,11 +80,12 @@ // // **************************************************************** // -#if !defined(__LINUX_ALSA__) && !defined(__UNIX_JACK__) && !defined(__MACOSX_CORE__) && !defined(__WINDOWS_MM__) +#if !defined(__LINUX_ALSA__) && !defined(__UNIX_JACK__) && !defined(__MACOSX_CORE__) && !defined(__WINDOWS_MM__) && !defined(TARGET_IPHONE_OS) && !defined(__WEB_MIDI_API__) #define __RTMIDI_DUMMY__ #endif #if defined(__MACOSX_CORE__) +#include class MidiInCore: public MidiInApi { @@ -78,6 +102,7 @@ class MidiInCore: public MidiInApi std::string getPortName( unsigned int portNumber ); protected: + MIDIClientRef getCoreMidiClientSingleton(const std::string& clientName) throw(); void initialize( const std::string& clientName ); }; @@ -97,6 +122,7 @@ class MidiOutCore: public MidiOutApi void sendMessage( const unsigned char *message, size_t size ); protected: + MIDIClientRef getCoreMidiClientSingleton(const std::string& clientName) throw(); void initialize( const std::string& clientName ); }; @@ -231,6 +257,57 @@ class MidiOutWinMM: public MidiOutApi #endif +#if defined(__WEB_MIDI_API__) + +class MidiInWeb : public MidiInApi +{ + std::string client_name{}; + std::string web_midi_id{}; + int open_port_number{-1}; + + public: + MidiInWeb(const std::string &/*clientName*/, unsigned int queueSizeLimit ); + ~MidiInWeb( void ); + RtMidi::Api getCurrentApi( void ) { return RtMidi::WEB_MIDI_API; }; + void openPort( unsigned int portNumber, const std::string &portName ); + void openVirtualPort( const std::string &portName ); + void closePort( void ); + void setClientName( const std::string &clientName ); + void setPortName( const std::string &portName ); + unsigned int getPortCount( void ); + std::string getPortName( unsigned int portNumber ); + + void onMidiMessage( uint8_t* data, double domHishResTimeStamp ); + + protected: + void initialize( const std::string& clientName ); +}; + +class MidiOutWeb: public MidiOutApi +{ + std::string client_name{}; + std::string web_midi_id{}; + int open_port_number{-1}; + + public: + MidiOutWeb( const std::string &clientName ); + ~MidiOutWeb( void ); + RtMidi::Api getCurrentApi( void ) { return RtMidi::WEB_MIDI_API; }; + void openPort( unsigned int portNumber, const std::string &portName ); + void openVirtualPort( const std::string &portName ); + void closePort( void ); + void setClientName( const std::string &clientName ); + void setPortName( const std::string &portName ); + unsigned int getPortCount( void ); + std::string getPortName( unsigned int portNumber ); + void sendMessage( const unsigned char *message, size_t size ); + + protected: + void initialize( const std::string& clientName ); +}; + +#endif + #if defined(__RTMIDI_DUMMY__) class MidiInDummy: public MidiInApi @@ -285,6 +362,11 @@ RtMidi :: ~RtMidi() rtapi_ = 0; } +RtMidi::RtMidi(RtMidi&& other) noexcept { + rtapi_ = other.rtapi_; + other.rtapi_ = nullptr; +} + std::string RtMidi :: getVersion( void ) throw() { return std::string( RTMIDI_VERSION ); @@ -299,6 +381,7 @@ const char* rtmidi_api_names[][2] = { { "alsa" , "ALSA" }, { "jack" , "Jack" }, { "winmm" , "Windows MultiMedia" }, + { "web" , "Web MIDI API" }, { "dummy" , "Dummy" }, }; const unsigned int rtmidi_num_api_names = @@ -319,6 +402,9 @@ extern "C" const RtMidi::Api rtmidi_compiled_apis[] = { #if defined(__WINDOWS_MM__) RtMidi::WINDOWS_MM, #endif +#if defined(__WEB_MIDI_API__) + RtMidi::WEB_MIDI_API, +#endif #if defined(__RTMIDI_DUMMY__) RtMidi::RTMIDI_DUMMY, #endif @@ -344,14 +430,14 @@ void RtMidi :: getCompiledApi( std::vector &apis ) throw() std::string RtMidi :: getApiName( RtMidi::Api api ) { - if (api < 0 || api >= RtMidi::NUM_APIS) + if (api < RtMidi::UNSPECIFIED || api >= RtMidi::NUM_APIS) return ""; return rtmidi_api_names[api][0]; } std::string RtMidi :: getApiDisplayName( RtMidi::Api api ) { - if (api < 0 || api >= RtMidi::NUM_APIS) + if (api < RtMidi::UNSPECIFIED || api >= RtMidi::NUM_APIS) return "Unknown"; return rtmidi_api_names[api][1]; } @@ -401,6 +487,10 @@ void RtMidiIn :: openMidiApi( RtMidi::Api api, const std::string &clientName, un if ( api == MACOSX_CORE ) rtapi_ = new MidiInCore( clientName, queueSizeLimit ); #endif +#if defined(__WEB_MIDI_API__) + if ( api == WEB_MIDI_API ) + rtapi_ = new MidiInWeb( clientName, queueSizeLimit ); +#endif #if defined(__RTMIDI_DUMMY__) if ( api == RTMIDI_DUMMY ) rtapi_ = new MidiInDummy( clientName, queueSizeLimit ); @@ -469,6 +559,10 @@ void RtMidiOut :: openMidiApi( RtMidi::Api api, const std::string &clientName ) if ( api == MACOSX_CORE ) rtapi_ = new MidiOutCore( clientName ); #endif +#if defined(__WEB_MIDI_API__) + if ( api == WEB_MIDI_API ) + rtapi_ = new MidiOutWeb( clientName ); +#endif #if defined(__RTMIDI_DUMMY__) if ( api == RTMIDI_DUMMY ) rtapi_ = new MidiOutDummy( clientName ); @@ -634,6 +728,12 @@ double MidiInApi :: getMessage( std::vector *message ) return timeStamp; } +void MidiInApi :: setBufferSize( unsigned int size, unsigned int count ) +{ + inputData_.bufferSize = size; + inputData_.bufferCount = count; +} + unsigned int MidiInApi::MidiQueue::size( unsigned int *__back, unsigned int *__front ) { @@ -716,10 +816,11 @@ MidiOutApi :: ~MidiOutApi( void ) // MIDI input. We convert the system specific time stamps to delta // time values. -// OS-X CoreMIDI header files. -#include -#include -#include +// These are not available on iOS. +#if (TARGET_OS_IPHONE == 0) + #include + #include +#endif // A structure to hold variables related to the CoreMIDI API // implementation. @@ -732,6 +833,20 @@ struct CoreMidiData { MIDISysexSendRequest sysexreq; }; +static MIDIClientRef CoreMidiClientSingleton = 0; + +void RtMidi_setCoreMidiClientSingleton(MIDIClientRef client){ + CoreMidiClientSingleton = client; +} + +void RtMidi_disposeCoreMidiClientSingleton(){ + if (CoreMidiClientSingleton == 0){ + return; + } + MIDIClientDispose( CoreMidiClientSingleton ); + CoreMidiClientSingleton = 0; +} + //*********************************************************************// // API: OS-X // Class Definitions: MidiInCore @@ -899,24 +1014,37 @@ MidiInCore :: ~MidiInCore( void ) // Cleanup. CoreMidiData *data = static_cast (apiData_); - MIDIClientDispose( data->client ); if ( data->endpoint ) MIDIEndpointDispose( data->endpoint ); delete data; } +MIDIClientRef MidiInCore::getCoreMidiClientSingleton(const std::string& clientName) throw() { + + if (CoreMidiClientSingleton == 0){ + // Set up our client. + MIDIClientRef client; + + CFStringRef name = CFStringCreateWithCString( NULL, clientName.c_str(), kCFStringEncodingASCII ); + OSStatus result = MIDIClientCreate(name, NULL, NULL, &client ); + if ( result != noErr ) { + std::ostringstream ost; + ost << "MidiInCore::initialize: error creating OS-X MIDI client object (" << result << ")."; + errorString_ = ost.str(); + error( RtMidiError::DRIVER_ERROR, errorString_ ); + return 0; + } + CFRelease( name ); + + CoreMidiClientSingleton = client; + } + + return CoreMidiClientSingleton; +} + void MidiInCore :: initialize( const std::string& clientName ) { // Set up our client. - MIDIClientRef client; - CFStringRef name = CFStringCreateWithCString( NULL, clientName.c_str(), kCFStringEncodingASCII ); - OSStatus result = MIDIClientCreate(name, NULL, NULL, &client ); - if ( result != noErr ) { - std::ostringstream ost; - ost << "MidiInCore::initialize: error creating OS-X MIDI client object (" << result << ")."; - errorString_ = ost.str(); - error( RtMidiError::DRIVER_ERROR, errorString_ ); - return; - } + MIDIClientRef client = getCoreMidiClientSingleton(clientName); // Save our api-specific connection information. CoreMidiData *data = (CoreMidiData *) new CoreMidiData; @@ -924,7 +1052,6 @@ void MidiInCore :: initialize( const std::string& clientName ) data->endpoint = 0; apiData_ = (void *) data; inputData_.apiData = (void *) data; - CFRelease( name ); } void MidiInCore :: openPort( unsigned int portNumber, const std::string &portName ) @@ -960,7 +1087,6 @@ void MidiInCore :: openPort( unsigned int portNumber, const std::string &portNam CFRelease( portNameRef ); if ( result != noErr ) { - MIDIClientDispose( data->client ); errorString_ = "MidiInCore::openPort: error creating OS-X MIDI input port."; error( RtMidiError::DRIVER_ERROR, errorString_ ); return; @@ -970,7 +1096,6 @@ void MidiInCore :: openPort( unsigned int portNumber, const std::string &portNam MIDIEndpointRef endpoint = MIDIGetSource( portNumber ); if ( endpoint == 0 ) { MIDIPortDispose( port ); - MIDIClientDispose( data->client ); errorString_ = "MidiInCore::openPort: error getting MIDI input source reference."; error( RtMidiError::DRIVER_ERROR, errorString_ ); return; @@ -980,7 +1105,6 @@ void MidiInCore :: openPort( unsigned int portNumber, const std::string &portNam result = MIDIPortConnectSource( port, endpoint, NULL ); if ( result != noErr ) { MIDIPortDispose( port ); - MIDIClientDispose( data->client ); errorString_ = "MidiInCore::openPort: error connecting OS-X MIDI input port."; error( RtMidiError::DRIVER_ERROR, errorString_ ); return; @@ -1068,6 +1192,11 @@ CFStringRef EndpointName( MIDIEndpointRef endpoint, bool isExternal ) CFRelease( str ); } + // some MIDI devices have a leading space in endpoint name. trim + CFStringRef space = CFStringCreateWithCString(NULL, " ", kCFStringEncodingUTF8); + CFStringTrim(result, space); + CFRelease(space); + MIDIEntityRef entity = 0; MIDIEndpointGetEntity( endpoint, &entity ); if ( entity == 0 ) @@ -1223,31 +1352,43 @@ MidiOutCore :: ~MidiOutCore( void ) // Cleanup. CoreMidiData *data = static_cast (apiData_); - MIDIClientDispose( data->client ); if ( data->endpoint ) MIDIEndpointDispose( data->endpoint ); delete data; } +MIDIClientRef MidiOutCore::getCoreMidiClientSingleton(const std::string& clientName) throw() { + + if (CoreMidiClientSingleton == 0){ + // Set up our client. + MIDIClientRef client; + + CFStringRef name = CFStringCreateWithCString( NULL, clientName.c_str(), kCFStringEncodingASCII ); + OSStatus result = MIDIClientCreate(name, NULL, NULL, &client ); + if ( result != noErr ) { + std::ostringstream ost; + ost << "MidiInCore::initialize: error creating OS-X MIDI client object (" << result << ")."; + errorString_ = ost.str(); + error( RtMidiError::DRIVER_ERROR, errorString_ ); + return 0; + } + CFRelease( name ); + + CoreMidiClientSingleton = client; + } + + return CoreMidiClientSingleton; +} + void MidiOutCore :: initialize( const std::string& clientName ) { // Set up our client. - MIDIClientRef client; - CFStringRef name = CFStringCreateWithCString( NULL, clientName.c_str(), kCFStringEncodingASCII ); - OSStatus result = MIDIClientCreate(name, NULL, NULL, &client ); - if ( result != noErr ) { - std::ostringstream ost; - ost << "MidiInCore::initialize: error creating OS-X MIDI client object (" << result << ")."; - errorString_ = ost.str(); - error( RtMidiError::DRIVER_ERROR, errorString_ ); - return; - } + MIDIClientRef client = getCoreMidiClientSingleton(clientName); // Save our api-specific connection information. CoreMidiData *data = (CoreMidiData *) new CoreMidiData; data->client = client; data->endpoint = 0; apiData_ = (void *) data; - CFRelease( name ); } unsigned int MidiOutCore :: getPortCount() @@ -1310,7 +1451,6 @@ void MidiOutCore :: openPort( unsigned int portNumber, const std::string &portNa OSStatus result = MIDIOutputPortCreate( data->client, portNameRef, &port ); CFRelease( portNameRef ); if ( result != noErr ) { - MIDIClientDispose( data->client ); errorString_ = "MidiOutCore::openPort: error creating OS-X MIDI output port."; error( RtMidiError::DRIVER_ERROR, errorString_ ); return; @@ -1320,7 +1460,6 @@ void MidiOutCore :: openPort( unsigned int portNumber, const std::string &portNa MIDIEndpointRef destination = MIDIGetDestination( portNumber ); if ( destination == 0 ) { MIDIPortDispose( port ); - MIDIClientDispose( data->client ); errorString_ = "MidiOutCore::openPort: error getting MIDI output destination reference."; error( RtMidiError::DRIVER_ERROR, errorString_ ); return; @@ -1402,50 +1541,54 @@ void MidiOutCore :: sendMessage( const unsigned char *message, size_t size ) return; } - MIDITimeStamp timeStamp = AudioGetCurrentHostTime(); - CoreMidiData *data = static_cast (apiData_); - OSStatus result; - if ( message[0] != 0xF0 && nBytes > 3 ) { errorString_ = "MidiOutCore::sendMessage: message format problem ... not sysex but > 3 bytes?"; error( RtMidiError::WARNING, errorString_ ); return; } - Byte buffer[nBytes+(sizeof( MIDIPacketList ))]; + MIDITimeStamp timeStamp = AudioGetCurrentHostTime(); + CoreMidiData *data = static_cast (apiData_); + OSStatus result; + + ByteCount bufsize = nBytes > 65535 ? 65535 : nBytes; + Byte buffer[bufsize+16]; // pad for other struct members ByteCount listSize = sizeof( buffer ); MIDIPacketList *packetList = (MIDIPacketList*)buffer; - MIDIPacket *packet = MIDIPacketListInit( packetList ); ByteCount remainingBytes = nBytes; - while ( remainingBytes && packet ) { - ByteCount bytesForPacket = remainingBytes > 65535 ? 65535 : remainingBytes; // 65535 = maximum size of a MIDIPacket + while ( remainingBytes ) { + MIDIPacket *packet = MIDIPacketListInit( packetList ); + // A MIDIPacketList can only contain a maximum of 64K of data, so if our message is longer, + // break it up into chunks of 64K or less and send out as a MIDIPacketList with only one + // MIDIPacket. Here, we reuse the memory allocated above on the stack for all. + ByteCount bytesForPacket = remainingBytes > 65535 ? 65535 : remainingBytes; const Byte* dataStartPtr = (const Byte *) &message[nBytes - remainingBytes]; packet = MIDIPacketListAdd( packetList, listSize, packet, timeStamp, bytesForPacket, dataStartPtr ); remainingBytes -= bytesForPacket; - } - if ( !packet ) { - errorString_ = "MidiOutCore::sendMessage: could not allocate packet list"; - error( RtMidiError::DRIVER_ERROR, errorString_ ); - return; - } + if ( !packet ) { + errorString_ = "MidiOutCore::sendMessage: could not allocate packet list"; + error( RtMidiError::DRIVER_ERROR, errorString_ ); + return; + } - // Send to any destinations that may have connected to us. - if ( data->endpoint ) { - result = MIDIReceived( data->endpoint, packetList ); - if ( result != noErr ) { - errorString_ = "MidiOutCore::sendMessage: error sending MIDI to virtual destinations."; - error( RtMidiError::WARNING, errorString_ ); + // Send to any destinations that may have connected to us. + if ( data->endpoint ) { + result = MIDIReceived( data->endpoint, packetList ); + if ( result != noErr ) { + errorString_ = "MidiOutCore::sendMessage: error sending MIDI to virtual destinations."; + error( RtMidiError::WARNING, errorString_ ); + } } - } - // And send to an explicit destination port if we're connected. - if ( connected_ ) { - result = MIDISend( data->port, data->destinationId, packetList ); - if ( result != noErr ) { - errorString_ = "MidiOutCore::sendMessage: error sending MIDI message to port."; - error( RtMidiError::WARNING, errorString_ ); + // And send to an explicit destination port if we're connected. + if ( connected_ ) { + result = MIDISend( data->port, data->destinationId, packetList ); + if ( result != noErr ) { + errorString_ = "MidiOutCore::sendMessage: error sending MIDI message to port."; + error( RtMidiError::WARNING, errorString_ ); + } } } } @@ -1487,6 +1630,7 @@ struct AlsaMidiData { snd_seq_port_subscribe_t *subscription; snd_midi_event_t *coder; unsigned int bufferSize; + unsigned int requestedBufferSize; unsigned char *buffer; pthread_t thread; pthread_t dummy_thread_id; @@ -1517,7 +1661,6 @@ static void *alsaMidiHandler( void *ptr ) snd_seq_event_t *ev; int result; - apiData->bufferSize = 32; result = snd_midi_event_new( 0, &apiData->coder ); if ( result < 0 ) { data->doInput = false; @@ -1769,6 +1912,7 @@ void MidiInAlsa :: initialize( const std::string& clientName ) data->thread = data->dummy_thread_id; data->trigger_fds[0] = -1; data->trigger_fds[1] = -1; + data->bufferSize = inputData_.bufferSize; apiData_ = (void *) data; inputData_.apiData = (void *) data; @@ -2293,7 +2437,7 @@ void MidiOutAlsa :: openVirtualPort( const std::string &portName ) void MidiOutAlsa :: sendMessage( const unsigned char *message, size_t size ) { - int result; + long result; AlsaMidiData *data = static_cast (apiData_); unsigned int nBytes = static_cast (size); if ( nBytes > data->bufferSize ) { @@ -2313,25 +2457,38 @@ void MidiOutAlsa :: sendMessage( const unsigned char *message, size_t size ) } } - snd_seq_event_t ev; - snd_seq_ev_clear( &ev ); - snd_seq_ev_set_source( &ev, data->vport ); - snd_seq_ev_set_subs( &ev ); - snd_seq_ev_set_direct( &ev ); for ( unsigned int i=0; ibuffer[i] = message[i]; - result = snd_midi_event_encode( data->coder, data->buffer, (long)nBytes, &ev ); - if ( result < (int)nBytes ) { - errorString_ = "MidiOutAlsa::sendMessage: event parsing error!"; - error( RtMidiError::WARNING, errorString_ ); - return; - } - // Send the event. - result = snd_seq_event_output( data->seq, &ev ); - if ( result < 0 ) { - errorString_ = "MidiOutAlsa::sendMessage: error sending MIDI message to port."; - error( RtMidiError::WARNING, errorString_ ); - return; + unsigned int offset = 0; + while (offset < nBytes) { + snd_seq_event_t ev; + snd_seq_ev_clear( &ev ); + snd_seq_ev_set_source( &ev, data->vport ); + snd_seq_ev_set_subs( &ev ); + snd_seq_ev_set_direct( &ev ); + result = snd_midi_event_encode( data->coder, data->buffer + offset, + (long)(nBytes - offset), &ev ); + if ( result < 0 ) { + errorString_ = "MidiOutAlsa::sendMessage: event parsing error!"; + error( RtMidiError::WARNING, errorString_ ); + return; + } + + if ( ev.type == SND_SEQ_EVENT_NONE ) { + errorString_ = "MidiOutAlsa::sendMessage: incomplete message!"; + error( RtMidiError::WARNING, errorString_ ); + return; + } + + offset += result; + + // Send the event. + result = snd_seq_event_output( data->seq, &ev ); + if ( result < 0 ) { + errorString_ = "MidiOutAlsa::sendMessage: error sending MIDI message to port."; + error( RtMidiError::WARNING, errorString_ ); + return; + } } snd_seq_drain_output( data->seq ); } @@ -2386,9 +2543,6 @@ static std::string ConvertToUTF8(const TCHAR *str) return u8str; } -#define RT_SYSEX_BUFFER_SIZE 1024 -#define RT_SYSEX_BUFFER_COUNT 4 - // A structure to hold variables related to the CoreMIDI API // implementation. struct WinMidiData { @@ -2396,7 +2550,7 @@ struct WinMidiData { HMIDIOUT outHandle; // Handle to Midi Output Device DWORD lastTime; MidiInApi::MidiMessage message; - LPMIDIHDR sysexBuffer[RT_SYSEX_BUFFER_COUNT]; + std::vector sysexBuffer; CRITICAL_SECTION _mutex; // [Patrice] see https://groups.google.com/forum/#!topic/mididev/6OUjHutMpEo }; @@ -2576,10 +2730,11 @@ void MidiInWinMM :: openPort( unsigned int portNumber, const std::string &/*port } // Allocate and init the sysex buffers. - for ( int i=0; isysexBuffer.resize( inputData_.bufferCount ); + for ( unsigned int i=0; i < inputData_.bufferCount; ++i ) { data->sysexBuffer[i] = (MIDIHDR*) new char[ sizeof(MIDIHDR) ]; - data->sysexBuffer[i]->lpData = new char[ RT_SYSEX_BUFFER_SIZE ]; - data->sysexBuffer[i]->dwBufferLength = RT_SYSEX_BUFFER_SIZE; + data->sysexBuffer[i]->lpData = new char[ inputData_.bufferSize ]; + data->sysexBuffer[i]->dwBufferLength = inputData_.bufferSize; data->sysexBuffer[i]->dwUser = i; // We use the dwUser parameter as buffer indicator data->sysexBuffer[i]->dwFlags = 0; @@ -2630,7 +2785,7 @@ void MidiInWinMM :: closePort( void ) midiInReset( data->inHandle ); midiInStop( data->inHandle ); - for ( int i=0; isysexBuffer.size(); ++i ) { int result = midiInUnprepareHeader(data->inHandle, data->sysexBuffer[i], sizeof(MIDIHDR)); delete [] data->sysexBuffer[i]->lpData; delete [] data->sysexBuffer[i]; @@ -2811,7 +2966,10 @@ void MidiOutWinMM :: closePort( void ) { if ( connected_ ) { WinMidiData *data = static_cast (apiData_); - midiOutReset( data->outHandle ); + // Disabled because midiOutReset triggers 0x7b (if any note was ON) and 0x79 "Reset All + // Controllers" (to all 16 channels) CC messages which is undesirable (see issue #222) + // midiOutReset( data->outHandle ); + midiOutClose( data->outHandle ); data->outHandle = 0; connected_ = false; @@ -2936,6 +3094,8 @@ void MidiOutWinMM :: sendMessage( const unsigned char *message, size_t size ) #include #include #include +#include +#include #ifdef HAVE_SEMAPHORE #include #endif @@ -2945,8 +3105,8 @@ void MidiOutWinMM :: sendMessage( const unsigned char *message, size_t size ) struct JackMidiData { jack_client_t *client; jack_port_t *port; - jack_ringbuffer_t *buffSize; - jack_ringbuffer_t *buffMessage; + jack_ringbuffer_t *buff; + int buffMaxWrite; // actual writable size, usually 1 less than ringbuffer jack_time_t lastTime; #ifdef HAVE_SEMAPHORE sem_t sem_cleanup; @@ -3101,6 +3261,8 @@ void MidiInJack :: openPort( unsigned int portNumber, const std::string &portNam if ( data->port == NULL ) { errorString_ = "MidiInJack::openPort: JACK error creating port"; + if (portName.size() >= (size_t)jack_port_name_size()) + errorString_ += " (port name too long?)"; error( RtMidiError::DRIVER_ERROR, errorString_ ); return; } @@ -3123,6 +3285,8 @@ void MidiInJack :: openVirtualPort( const std::string &portName ) if ( data->port == NULL ) { errorString_ = "MidiInJack::openVirtualPort: JACK error creating virtual port"; + if (portName.size() >= (size_t)jack_port_name_size()) + errorString_ += " (port name too long?)"; error( RtMidiError::DRIVER_ERROR, errorString_ ); } } @@ -3226,11 +3390,15 @@ static int jackProcessOut( jack_nframes_t nframes, void *arg ) void *buff = jack_port_get_buffer( data->port, nframes ); jack_midi_clear_buffer( buff ); - while ( jack_ringbuffer_read_space( data->buffSize ) > 0 ) { - jack_ringbuffer_read( data->buffSize, (char *) &space, (size_t) sizeof( space ) ); - midiData = jack_midi_event_reserve( buff, 0, space ); + while ( jack_ringbuffer_peek( data->buff, (char *) &space, sizeof( space ) ) == sizeof(space) && + jack_ringbuffer_read_space( data->buff ) >= sizeof(space) + space ) { + jack_ringbuffer_read_advance( data->buff, sizeof(space) ); - jack_ringbuffer_read( data->buffMessage, (char *) midiData, (size_t) space ); + midiData = jack_midi_event_reserve( buff, 0, space ); + if ( midiData ) + jack_ringbuffer_read( data->buff, (char *) midiData, (size_t) space ); + else + jack_ringbuffer_read_advance( data->buff, (size_t) space ); } #ifdef HAVE_SEMAPHORE @@ -3269,8 +3437,8 @@ void MidiOutJack :: connect() return; // Initialize output ringbuffers - data->buffSize = jack_ringbuffer_create( JACK_RINGBUFFER_SIZE ); - data->buffMessage = jack_ringbuffer_create( JACK_RINGBUFFER_SIZE ); + data->buff = jack_ringbuffer_create( JACK_RINGBUFFER_SIZE ); + data->buffMaxWrite = (int) jack_ringbuffer_write_space( data->buff ); // Initialize JACK client if ( ( data->client = jack_client_open( clientName.c_str(), JackNoStartServer, NULL ) ) == 0 ) { @@ -3289,8 +3457,7 @@ MidiOutJack :: ~MidiOutJack() MidiOutJack::closePort(); // Cleanup - jack_ringbuffer_free( data->buffSize ); - jack_ringbuffer_free( data->buffMessage ); + jack_ringbuffer_free( data->buff ); if ( data->client ) { jack_client_close( data->client ); } @@ -3316,6 +3483,8 @@ void MidiOutJack :: openPort( unsigned int portNumber, const std::string &portNa if ( data->port == NULL ) { errorString_ = "MidiOutJack::openPort: JACK error creating port"; + if (portName.size() >= (size_t)jack_port_name_size()) + errorString_ += " (port name too long?)"; error( RtMidiError::DRIVER_ERROR, errorString_ ); return; } @@ -3338,6 +3507,8 @@ void MidiOutJack :: openVirtualPort( const std::string &portName ) if ( data->port == NULL ) { errorString_ = "MidiOutJack::openVirtualPort: JACK error creating virtual port"; + if (portName.size() >= (size_t)jack_port_name_size()) + errorString_ += " (port name too long?)"; error( RtMidiError::DRIVER_ERROR, errorString_ ); } } @@ -3437,9 +3608,331 @@ void MidiOutJack :: sendMessage( const unsigned char *message, size_t size ) int nBytes = static_cast(size); JackMidiData *data = static_cast (apiData_); + if ( size + sizeof(nBytes) > (size_t) data->buffMaxWrite ) + return; + + while ( jack_ringbuffer_write_space(data->buff) < sizeof(nBytes) + size ) + sched_yield(); + // Write full message to buffer - jack_ringbuffer_write( data->buffMessage, ( const char * ) message, nBytes ); - jack_ringbuffer_write( data->buffSize, ( char * ) &nBytes, sizeof( nBytes ) ); + jack_ringbuffer_write( data->buff, ( char * ) &nBytes, sizeof( nBytes ) ); + jack_ringbuffer_write( data->buff, ( const char * ) message, nBytes ); } #endif // __UNIX_JACK__ + +//*********************************************************************// +// API: Web MIDI +// +// Written primarily by Atsushi Eno, February 2020. +// +// *********************************************************************// + +#if defined(__WEB_MIDI_API__) + +#include + +//*********************************************************************// +// API: WEB MIDI +// Class Definitions: WebMidiAccessShim +//*********************************************************************// + +class WebMidiAccessShim +{ +public: + WebMidiAccessShim(); + ~WebMidiAccessShim(); + std::string getPortName( unsigned int portNumber, bool isInput ); +}; + +std::unique_ptr shim{nullptr}; + +void ensureShim() +{ + if ( shim.get() != nullptr ) + return; + shim.reset( new WebMidiAccessShim() ); +} + +bool checkWebMidiAvailability() +{ + ensureShim(); + + return MAIN_THREAD_EM_ASM_INT( { + if ( typeof window._rtmidi_internals_waiting === "undefined" ) { + console.log ( "Attempted to use Web MIDI API without trying to open it." ); + return false; + } + if ( window._rtmidi_internals_waiting ) { + console.log ( "Attempted to use Web MIDI API while it is being queried." ); + return false; + } + if ( _rtmidi_internals_midi_access == null ) { + console.log ( "Attempted to use Web MIDI API while it already turned out to be unavailable." ); + return false; + } + return true; + } ); +} + +WebMidiAccessShim::WebMidiAccessShim() +{ + MAIN_THREAD_ASYNC_EM_ASM( { + if( typeof window._rtmidi_internals_midi_access !== "undefined" ) + return; + if( typeof window._rtmidi_internals_waiting !== "undefined" ) { + console.log( "MIDI Access was requested while another request is in progress." ); + return; + } + + // define functions + window._rtmidi_internals_get_port_by_number = function( portNumber, isInput ) { + var midi = window._rtmidi_internals_midi_access; + var devices = isInput ? midi.inputs : midi.outputs; + var i = 0; + for (var device of devices.values()) { + if ( i == portNumber ) + return device; + i++; + } + console.log( "MIDI " + (isInput ? "input" : "output") + " device of portNumber " + portNumber + " is not found."); + return null; + }; + + window._rtmidi_internals_waiting = true; + window.navigator.requestMIDIAccess( {"sysex": true} ).then( (midiAccess) => { + window._rtmidi_internals_midi_access = midiAccess; + window._rtmidi_internals_latest_message_timestamp = 0.0; + window._rtmidi_internals_waiting = false; + if( midiAccess == null ) { + console.log ( "Could not get access to MIDI API" ); + } + } ); + } ); +} + +WebMidiAccessShim::~WebMidiAccessShim() +{ +} + +std::string WebMidiAccessShim::getPortName( unsigned int portNumber, bool isInput ) +{ + if( !checkWebMidiAvailability() ) + return ""; + char *ret = (char*) MAIN_THREAD_EM_ASM_INT( { + var port = window._rtmidi_internals_get_port_by_number($0, $1); + if( port == null) + return null; + var length = lengthBytesUTF8(port.name) + 1; + var ret = _malloc(length); + stringToUTF8(port.name, ret, length); + return ret; + }, portNumber, isInput); + if (ret == nullptr) + return ""; + std::string s = ret; + free(ret); + return s; +} + +//*********************************************************************// +// API: WEB MIDI +// Class Definitions: MidiInWeb +//*********************************************************************// + +MidiInWeb::MidiInWeb( const std::string &clientName, unsigned int queueSizeLimit ) + : MidiInApi( queueSizeLimit ) +{ + initialize( clientName ); +} + +MidiInWeb::~MidiInWeb( void ) +{ + closePort(); +} + +extern "C" void EMSCRIPTEN_KEEPALIVE rtmidi_onMidiMessageProc( MidiInApi::RtMidiInData* data, uint8_t* inputBytes, int32_t length, double domHighResTimeStamp ) +{ + auto &message = data->message; + message.bytes.resize(message.bytes.size() + length); + memcpy(message.bytes.data(), inputBytes, length); + // FIXME: handle timestamp + if ( data->usingCallback ) { + RtMidiIn::RtMidiCallback callback = (RtMidiIn::RtMidiCallback) data->userCallback; + callback( message.timeStamp, &message.bytes, data->userData ); + } +} + +void MidiInWeb::openPort( unsigned int portNumber, const std::string &portName ) +{ + if( !checkWebMidiAvailability() ) + return; + if (open_port_number >= 0) + return; + + MAIN_THREAD_EM_ASM( { + // In Web MIDI API world, there is no step to open a port, but we have to register the input callback instead. + var input = window._rtmidi_internals_get_port_by_number($0, true); + input.onmidimessage = function(e) { + // In RtMidi world, timestamps are delta time from previous message, while in Web MIDI world + // timestamps are relative to window creation time (i.e. kind of absolute time with window "epoch" time). + var rtmidiTimestamp = window._rtmidi_internals_latest_message_timestamp == 0.0 ? 0.0 : e.timeStamp - window._rtmidi_internals_latest_message_timestamp; + window._rtmidi_internals_latest_message_timestamp = e.timeStamp; + Module.ccall( 'rtmidi_onMidiMessageProc', 'void', ['number', 'array', 'number', 'number'], [$1, e.data, e.data.length, rtmidiTimestamp] ); + }; + }, portNumber, &inputData_ ); + open_port_number = portNumber; +} + +void MidiInWeb::openVirtualPort( const std::string &portName ) +{ + + errorString_ = "MidiInWeb::openVirtualPort: this function is not implemented for the Web MIDI API!"; + error( RtMidiError::WARNING, errorString_ ); + +} + +void MidiInWeb::closePort( void ) +{ + if( open_port_number < 0 ) + return; + + MAIN_THREAD_EM_ASM( { + var input = _rtmidi_internals_get_port_by_number($0, true); + if( input == null ) { + console.log( "Port #" + $0 + " could not be found."); + return; + } + // unregister event handler + input.onmidimessage = null; + }, open_port_number ); + open_port_number = -1; +} + +void MidiInWeb::setClientName( const std::string &clientName ) +{ + client_name = clientName; +} + +void MidiInWeb::setPortName( const std::string &portName ) +{ + + errorString_ = "MidiInWeb::setPortName: this function is not implemented for the Web MIDI API!"; + error( RtMidiError::WARNING, errorString_ ); + +} + +unsigned int MidiInWeb::getPortCount( void ) +{ + if( !checkWebMidiAvailability() ) + return 0; + return MAIN_THREAD_EM_ASM_INT( { return _rtmidi_internals_midi_access.inputs.size; } ); +} + +std::string MidiInWeb::getPortName( unsigned int portNumber ) +{ + if( !checkWebMidiAvailability() ) + return ""; + return shim->getPortName( portNumber, true ); +} + +void MidiInWeb::initialize( const std::string& clientName ) +{ + ensureShim(); + setClientName( clientName ); +} + +//*********************************************************************// +// API: WEB MIDI +// Class Definitions: MidiOutWeb +//*********************************************************************// + +MidiOutWeb::MidiOutWeb( const std::string &clientName ) +{ + initialize( clientName ); +} + +MidiOutWeb::~MidiOutWeb( void ) +{ + closePort(); +} + +void MidiOutWeb::openPort( unsigned int portNumber, const std::string &portName ) +{ + if( !checkWebMidiAvailability() ) + return; + if (open_port_number >= 0) + return; + // In Web MIDI API world, there is no step to open a port. + + open_port_number = portNumber; +} + +void MidiOutWeb::openVirtualPort( const std::string &portName ) +{ + + errorString_ = "MidiOutWeb::openVirtualPort: this function is not implemented for the Web MIDI API!"; + error( RtMidiError::WARNING, errorString_ ); + +} + +void MidiOutWeb::closePort( void ) +{ + // there is really nothing to do for output at JS side. + open_port_number = -1; +} + +void MidiOutWeb::setClientName( const std::string &clientName ) +{ + client_name = clientName; +} + +void MidiOutWeb::setPortName( const std::string &portName ) +{ + + errorString_ = "MidiOutWeb::setPortName: this function is not implemented for the Web MIDI API!"; + error( RtMidiError::WARNING, errorString_ ); + +} + +unsigned int MidiOutWeb::getPortCount( void ) +{ + if( !checkWebMidiAvailability() ) + return 0; + return MAIN_THREAD_EM_ASM_INT( { return _rtmidi_internals_midi_access.outputs.size; } ); +} + +std::string MidiOutWeb::getPortName( unsigned int portNumber ) +{ + if( !checkWebMidiAvailability() ) + return ""; + return shim->getPortName( portNumber, false ); +} + +void MidiOutWeb::sendMessage( const unsigned char *message, size_t size ) +{ + if( open_port_number < 0 ) + return; + + MAIN_THREAD_EM_ASM( { + var output = _rtmidi_internals_get_port_by_number( $0, false ); + if( output == null ) { + console.log( "Port #" + $0 + " could not be found."); + return; + } + var buf = new ArrayBuffer ($2); + var msg = new Uint8Array( buf ); + msg.set( new Uint8Array( Module.HEAPU8.buffer.slice( $1, $1 + $2 ) ) ); + output.send( msg ); + }, open_port_number, message, size ); +} + +void MidiOutWeb::initialize( const std::string& clientName ) +{ + if ( shim.get() != nullptr ) + return; + shim.reset( new WebMidiAccessShim() ); + setClientName( clientName ); +} + +#endif // __WEB_MIDI_API__ diff --git a/v2/drivers/rtmididrv/imported/rtmidi/cpp/RtMidi.h b/v2/drivers/rtmididrv/imported/rtmidi/cpp/RtMidi.h index 495e088..a6f5b79 100644 --- a/v2/drivers/rtmididrv/imported/rtmidi/cpp/RtMidi.h +++ b/v2/drivers/rtmididrv/imported/rtmidi/cpp/RtMidi.h @@ -9,7 +9,7 @@ RtMidi WWW site: http://www.music.mcgill.ca/~gary/rtmidi/ RtMidi: realtime MIDI i/o C++ classes - Copyright (c) 2003-2019 Gary P. Scavone + Copyright (c) 2003-2021 Gary P. Scavone Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files @@ -58,13 +58,14 @@ #endif #endif -#define RTMIDI_VERSION "4.0.0" +#define RTMIDI_VERSION "5.0.0" #include #include #include #include + /************************************************************************/ /*! \class RtMidiError \brief Exception handling class for RtMidi. @@ -132,6 +133,8 @@ class MidiApi; class RTMIDI_DLL_PUBLIC RtMidi { public: + + RtMidi(RtMidi&& other) noexcept; //! MIDI API specifier arguments. enum Api { UNSPECIFIED, /*!< Search for a working compiled API. */ @@ -140,6 +143,7 @@ class RTMIDI_DLL_PUBLIC RtMidi UNIX_JACK, /*!< The JACK Low-Latency MIDI Server API. */ WINDOWS_MM, /*!< The Microsoft Multimedia MIDI API. */ RTMIDI_DUMMY, /*!< A compilable but non-functional API. */ + WEB_MIDI_API, /*!< W3C Web MIDI API. */ NUM_APIS /*!< Number of values in this enum. */ }; @@ -213,6 +217,10 @@ class RTMIDI_DLL_PUBLIC RtMidi RtMidi(); virtual ~RtMidi(); MidiApi *rtapi_; + + /* Make the class non-copyable */ + RtMidi(RtMidi& other) = delete; + RtMidi& operator=(RtMidi& other) = delete; }; /**********************************************************************/ @@ -228,8 +236,6 @@ class RTMIDI_DLL_PUBLIC RtMidi time. With the OS-X, Linux ALSA, and JACK MIDI APIs, it is also possible to open a virtual input port to which other MIDI software clients can connect. - - by Gary P. Scavone, 2003-2017. */ /**********************************************************************/ @@ -250,7 +256,6 @@ class RTMIDI_DLL_PUBLIC RtMidi class RTMIDI_DLL_PUBLIC RtMidiIn : public RtMidi { public: - //! User callback function type definition. typedef void (*RtMidiCallback)( double timeStamp, std::vector *message, void *userData ); @@ -276,6 +281,8 @@ class RTMIDI_DLL_PUBLIC RtMidiIn : public RtMidi const std::string& clientName = "RtMidi Input Client", unsigned int queueSizeLimit = 100 ); + RtMidiIn(RtMidiIn&& other) noexcept : RtMidi(std::move(other)) { } + //! If a MIDI connection is still open, it will be closed by the destructor. ~RtMidiIn ( void ) throw(); @@ -373,6 +380,19 @@ class RTMIDI_DLL_PUBLIC RtMidiIn : public RtMidi */ virtual void setErrorCallback( RtMidiErrorCallback errorCallback = NULL, void *userData = 0 ); + //! Set maximum expected incoming message size. + /*! + For APIs that require manual buffer management, it can be useful to set the buffer + size and buffer count when expecting to receive large SysEx messages. Note that + currently this function has no effect when called after openPort(). The default + buffer size is 1024 with a count of 4 buffers, which should be sufficient for most + cases; as mentioned, this does not affect all API backends, since most either support + dynamically scalable buffers or take care of buffer handling themselves. It is + principally intended for users of the Windows MM backend who must support receiving + especially large messages. + */ + virtual void setBufferSize( unsigned int size, unsigned int count ); + protected: void openMidiApi( RtMidi::Api api, const std::string &clientName, unsigned int queueSizeLimit ); }; @@ -388,8 +408,6 @@ class RTMIDI_DLL_PUBLIC RtMidiIn : public RtMidi connect to more than one MIDI device at the same time. With the OS-X, Linux ALSA and JACK MIDI APIs, it is also possible to open a virtual port to which other MIDI software clients can connect. - - by Gary P. Scavone, 2003-2017. */ /**********************************************************************/ @@ -407,6 +425,8 @@ class RTMIDI_DLL_PUBLIC RtMidiOut : public RtMidi RtMidiOut( RtMidi::Api api=UNSPECIFIED, const std::string& clientName = "RtMidi Output Client" ); + RtMidiOut(RtMidiOut&& other) noexcept : RtMidi(std::move(other)) { } + //! The destructor closes any open MIDI connections. ~RtMidiOut( void ) throw(); @@ -527,6 +547,7 @@ protected: RtMidiErrorCallback errorCallback_; bool firstErrorOccurred_; void *errorCallbackUserData_; + }; class RTMIDI_DLL_PUBLIC MidiInApi : public MidiApi @@ -539,6 +560,7 @@ class RTMIDI_DLL_PUBLIC MidiInApi : public MidiApi void cancelCallback( void ); virtual void ignoreTypes( bool midiSysex, bool midiTime, bool midiSense ); double getMessage( std::vector *message ); + virtual void setBufferSize( unsigned int size, unsigned int count ); // A MIDI structure used internally by the class to store incoming // messages. Each message represents one and only one MIDI message. @@ -580,11 +602,13 @@ class RTMIDI_DLL_PUBLIC MidiInApi : public MidiApi RtMidiIn::RtMidiCallback userCallback; void *userData; bool continueSysex; + unsigned int bufferSize; + unsigned int bufferCount; // Default constructor. RtMidiInData() : ignoreFlags(7), doInput(false), firstMessage(true), apiData(0), usingCallback(false), - userCallback(0), userData(0), continueSysex(false) {} + userCallback(0), userData(0), continueSysex(false), bufferSize(1024), bufferCount(4) {} }; protected: @@ -618,6 +642,7 @@ inline std::string RtMidiIn :: getPortName( unsigned int portNumber ) { return r inline void RtMidiIn :: ignoreTypes( bool midiSysex, bool midiTime, bool midiSense ) { static_cast(rtapi_)->ignoreTypes( midiSysex, midiTime, midiSense ); } inline double RtMidiIn :: getMessage( std::vector *message ) { return static_cast(rtapi_)->getMessage( message ); } inline void RtMidiIn :: setErrorCallback( RtMidiErrorCallback errorCallback, void *userData ) { rtapi_->setErrorCallback(errorCallback, userData); } +inline void RtMidiIn :: setBufferSize( unsigned int size, unsigned int count ) { static_cast(rtapi_)->setBufferSize(size, count); } inline RtMidi::Api RtMidiOut :: getCurrentApi( void ) throw() { return rtapi_->getCurrentApi(); } inline void RtMidiOut :: openPort( unsigned int portNumber, const std::string &portName ) { rtapi_->openPort( portNumber, portName ); } diff --git a/v2/drivers/rtmididrv/imported/rtmidi/cpp/rtmidi_c.cpp b/v2/drivers/rtmididrv/imported/rtmidi/cpp/rtmidi_c.cpp index 6c73194..af3a034 100644 --- a/v2/drivers/rtmididrv/imported/rtmidi/cpp/rtmidi_c.cpp +++ b/v2/drivers/rtmididrv/imported/rtmidi/cpp/rtmidi_c.cpp @@ -3,17 +3,48 @@ #include "rtmidi_c.h" #include "RtMidi.h" +/* Compile-time assertions that will break if the enums are changed in + * the future without synchronizing them properly. If you get (g++) + * "error: ‘StaticEnumAssert::StaticEnumAssert() [with bool b = false]’ + * is private within this context", it means enums are not aligned. */ +template class StaticEnumAssert { private: StaticEnumAssert() {} }; +template<> class StaticEnumAssert{ public: StaticEnumAssert() {} }; +#define ENUM_EQUAL(x,y) StaticEnumAssert<(int)x==(int)y>() +class StaticEnumAssertions { StaticEnumAssertions() { + ENUM_EQUAL( RTMIDI_API_UNSPECIFIED, RtMidi::UNSPECIFIED ); + ENUM_EQUAL( RTMIDI_API_MACOSX_CORE, RtMidi::MACOSX_CORE ); + ENUM_EQUAL( RTMIDI_API_LINUX_ALSA, RtMidi::LINUX_ALSA ); + ENUM_EQUAL( RTMIDI_API_UNIX_JACK, RtMidi::UNIX_JACK ); + ENUM_EQUAL( RTMIDI_API_WINDOWS_MM, RtMidi::WINDOWS_MM ); + ENUM_EQUAL( RTMIDI_API_RTMIDI_DUMMY, RtMidi::RTMIDI_DUMMY ); + + ENUM_EQUAL( RTMIDI_ERROR_WARNING, RtMidiError::WARNING ); + ENUM_EQUAL( RTMIDI_ERROR_DEBUG_WARNING, RtMidiError::DEBUG_WARNING ); + ENUM_EQUAL( RTMIDI_ERROR_UNSPECIFIED, RtMidiError::UNSPECIFIED ); + ENUM_EQUAL( RTMIDI_ERROR_NO_DEVICES_FOUND, RtMidiError::NO_DEVICES_FOUND ); + ENUM_EQUAL( RTMIDI_ERROR_INVALID_DEVICE, RtMidiError::INVALID_DEVICE ); + ENUM_EQUAL( RTMIDI_ERROR_MEMORY_ERROR, RtMidiError::MEMORY_ERROR ); + ENUM_EQUAL( RTMIDI_ERROR_INVALID_PARAMETER, RtMidiError::INVALID_PARAMETER ); + ENUM_EQUAL( RTMIDI_ERROR_INVALID_USE, RtMidiError::INVALID_USE ); + ENUM_EQUAL( RTMIDI_ERROR_DRIVER_ERROR, RtMidiError::DRIVER_ERROR ); + ENUM_EQUAL( RTMIDI_ERROR_SYSTEM_ERROR, RtMidiError::SYSTEM_ERROR ); + ENUM_EQUAL( RTMIDI_ERROR_THREAD_ERROR, RtMidiError::THREAD_ERROR ); +}}; + class CallbackProxyUserData { public: - CallbackProxyUserData (RtMidiCCallback cCallback, void *userData) - : c_callback (cCallback), user_data (userData) - { - } - RtMidiCCallback c_callback; - void *user_data; + CallbackProxyUserData (RtMidiCCallback cCallback, void *userData) + : c_callback (cCallback), user_data (userData) + { + } + RtMidiCCallback c_callback; + void *user_data; }; +#ifndef RTMIDI_SOURCE_INCLUDED + extern "C" const enum RtMidiApi rtmidi_compiled_apis[]; // casting from RtMidi::Api[] +#endif extern "C" const unsigned int rtmidi_num_compiled_apis; /* RtMidi API */ @@ -41,10 +72,18 @@ const char *rtmidi_api_display_name(enum RtMidiApi api) return rtmidi_api_names[api][1]; } +enum RtMidiApi rtmidi_compiled_api_by_name(const char *name) { + RtMidi::Api api = RtMidi::UNSPECIFIED; + if (name) { + api = RtMidi::getCompiledApiByName(name); + } + return (enum RtMidiApi)api; +} + void rtmidi_error (MidiApi *api, enum RtMidiErrorType type, const char* errorString) { - std::string msg = errorString; - api->error ((RtMidiError::Type) type, msg); + std::string msg = errorString; + api->error ((RtMidiError::Type) type, msg); } void rtmidi_open_port (RtMidiPtr device, unsigned int portNumber, const char *portName) @@ -52,7 +91,7 @@ void rtmidi_open_port (RtMidiPtr device, unsigned int portNumber, const char *po std::string name = portName; try { ((RtMidi*) device->ptr)->openPort (portNumber, name); - + } catch (const RtMidiError & err) { device->ok = false; device->msg = err.what (); @@ -64,7 +103,7 @@ void rtmidi_open_virtual_port (RtMidiPtr device, const char *portName) std::string name = portName; try { ((RtMidi*) device->ptr)->openVirtualPort (name); - + } catch (const RtMidiError & err) { device->ok = false; device->msg = err.what (); @@ -74,7 +113,7 @@ void rtmidi_open_virtual_port (RtMidiPtr device, const char *portName) void rtmidi_close_port (RtMidiPtr device) { - try { + try { ((RtMidi*) device->ptr)->closePort (); } catch (const RtMidiError & err) { @@ -95,32 +134,42 @@ unsigned int rtmidi_get_port_count (RtMidiPtr device) } } -const char* rtmidi_get_port_name (RtMidiPtr device, unsigned int portNumber) +int rtmidi_get_port_name (RtMidiPtr device, unsigned int portNumber, char * bufOut, int * bufLen) { + if (bufOut == nullptr && bufLen == nullptr) { + return -1; + } + + std::string name; try { - std::string name = ((RtMidi*) device->ptr)->getPortName (portNumber); - return strdup (name.c_str ()); - + name = ((RtMidi*) device->ptr)->getPortName (portNumber); } catch (const RtMidiError & err) { device->ok = false; device->msg = err.what (); - return ""; + return -1; + } + + if (bufOut == nullptr) { + *bufLen = static_cast(name.size()) + 1; + return 0; } + + return snprintf(bufOut, static_cast(*bufLen), "%s", name.c_str()); } /* RtMidiIn API */ RtMidiInPtr rtmidi_in_create_default () { RtMidiWrapper* wrp = new RtMidiWrapper; - + try { RtMidiIn* rIn = new RtMidiIn (); - + wrp->ptr = (void*) rIn; wrp->data = 0; wrp->ok = true; wrp->msg = ""; - + } catch (const RtMidiError & err) { wrp->ptr = 0; wrp->data = 0; @@ -135,10 +184,10 @@ RtMidiInPtr rtmidi_in_create (enum RtMidiApi api, const char *clientName, unsign { std::string name = clientName; RtMidiWrapper* wrp = new RtMidiWrapper; - + try { RtMidiIn* rIn = new RtMidiIn ((RtMidi::Api) api, name, queueSizeLimit); - + wrp->ptr = (void*) rIn; wrp->data = 0; wrp->ok = true; @@ -166,7 +215,7 @@ enum RtMidiApi rtmidi_in_get_current_api (RtMidiPtr device) { try { return (RtMidiApi) ((RtMidiIn*) device->ptr)->getCurrentApi (); - + } catch (const RtMidiError & err) { device->ok = false; device->msg = err.what (); @@ -178,8 +227,8 @@ enum RtMidiApi rtmidi_in_get_current_api (RtMidiPtr device) static void callback_proxy (double timeStamp, std::vector *message, void *userData) { - CallbackProxyUserData* data = reinterpret_cast (userData); - data->c_callback (timeStamp, message->data (), message->size (), data->user_data); + CallbackProxyUserData* data = reinterpret_cast (userData); + data->c_callback (timeStamp, message->data (), message->size (), data->user_data); } void rtmidi_in_set_callback (RtMidiInPtr device, RtMidiCCallback callback, void *userData) @@ -209,10 +258,10 @@ void rtmidi_in_cancel_callback (RtMidiInPtr device) void rtmidi_in_ignore_types (RtMidiInPtr device, bool midiSysex, bool midiTime, bool midiSense) { - ((RtMidiIn*) device->ptr)->ignoreTypes (midiSysex, midiTime, midiSense); + ((RtMidiIn*) device->ptr)->ignoreTypes (midiSysex, midiTime, midiSense); } -double rtmidi_in_get_message (RtMidiInPtr device, +double rtmidi_in_get_message (RtMidiInPtr device, unsigned char *message, size_t *size) { @@ -227,7 +276,7 @@ double rtmidi_in_get_message (RtMidiInPtr device, *size = v.size(); return ret; - } + } catch (const RtMidiError & err) { device->ok = false; device->msg = err.what (); @@ -247,12 +296,12 @@ RtMidiOutPtr rtmidi_out_create_default () try { RtMidiOut* rOut = new RtMidiOut (); - + wrp->ptr = (void*) rOut; wrp->data = 0; wrp->ok = true; wrp->msg = ""; - + } catch (const RtMidiError & err) { wrp->ptr = 0; wrp->data = 0; @@ -270,12 +319,12 @@ RtMidiOutPtr rtmidi_out_create (enum RtMidiApi api, const char *clientName) try { RtMidiOut* rOut = new RtMidiOut ((RtMidi::Api) api, name); - + wrp->ptr = (void*) rOut; wrp->data = 0; wrp->ok = true; wrp->msg = ""; - + } catch (const RtMidiError & err) { wrp->ptr = 0; wrp->data = 0; @@ -322,4 +371,4 @@ int rtmidi_out_send_message (RtMidiOutPtr device, const unsigned char *message, device->msg = "Unknown error"; return -1; } -} +} \ No newline at end of file diff --git a/v2/drivers/rtmididrv/imported/rtmidi/go.mod b/v2/drivers/rtmididrv/imported/rtmidi/go.mod deleted file mode 100644 index eeb84eb..0000000 --- a/v2/drivers/rtmididrv/imported/rtmidi/go.mod +++ /dev/null @@ -1,3 +0,0 @@ -module gitlab.com/gomidi/midi/v2/drivers/rtmididrv/imported/rtmidi - -go 1.16 diff --git a/v2/drivers/rtmididrv/imported/rtmidi/rtmidi.go b/v2/drivers/rtmididrv/imported/rtmidi/rtmidi.go index 88cd4c5..a8a5a5c 100644 --- a/v2/drivers/rtmididrv/imported/rtmidi/rtmidi.go +++ b/v2/drivers/rtmididrv/imported/rtmidi/rtmidi.go @@ -1,7 +1,7 @@ package rtmidi /* -#cgo CXXFLAGS: -g +#cgo CXXFLAGS: -g -std=c++11 #cgo LDFLAGS: -g #cgo linux CXXFLAGS: -D__LINUX_ALSA__ @@ -50,6 +50,7 @@ const ( APIDummy API = C.RTMIDI_API_RTMIDI_DUMMY ) +// Format an API as a string func (api API) String() string { switch api { case APIUnspecified: @@ -121,6 +122,7 @@ type midi struct { midi C.RtMidiPtr } +// Open a MIDI input connection given by enumeration number. func (m *midi) OpenPort(port int, name string) error { p := C.CString(name) defer C.free(unsafe.Pointer(p)) @@ -131,6 +133,8 @@ func (m *midi) OpenPort(port int, name string) error { return nil } +// Create a virtual input port, with optional name, to allow software connections +// (OS X, JACK and ALSA only). func (m *midi) OpenVirtualPort(name string) error { p := C.CString(name) defer C.free(unsafe.Pointer(p)) @@ -141,15 +145,31 @@ func (m *midi) OpenVirtualPort(name string) error { return nil } +// Return a string identifier for the specified MIDI input port number. func (m *midi) PortName(port int) (string, error) { - p := C.rtmidi_get_port_name(m.midi, C.uint(port)) + bufLen := C.int(0) + + C.rtmidi_get_port_name(m.midi, C.uint(port), nil, &bufLen) if !m.midi.ok { return "", errors.New(C.GoString(m.midi.msg)) } - defer C.free(unsafe.Pointer(p)) - return C.GoString(p), nil + + if bufLen < 1 { + return "", nil + } + + bufOut := make([]byte, int(bufLen)) + p := (*C.char)(unsafe.Pointer(&bufOut[0])) + + C.rtmidi_get_port_name(m.midi, C.uint(port), p, &bufLen) + if !m.midi.ok { + return "", errors.New(C.GoString(m.midi.msg)) + } + + return string(bufOut[0 : bufLen-1]), nil } +// Return the number of available MIDI input ports. func (m *midi) PortCount() (int, error) { n := C.rtmidi_get_port_count(m.midi) if !m.midi.ok { @@ -158,6 +178,7 @@ func (m *midi) PortCount() (int, error) { return int(n), nil } +// Close an open MIDI connection. func (m *midi) Close() error { C.rtmidi_close_port(C.RtMidiPtr(m.midi)) if !m.midi.ok { @@ -177,7 +198,7 @@ type midiOut struct { out C.RtMidiOutPtr } -// NewMIDIInDefault opens a default MIDIIn port. +// Open a default MIDIIn port. func NewMIDIInDefault() (MIDIIn, error) { in := C.rtmidi_in_create_default() if !in.ok { @@ -187,7 +208,7 @@ func NewMIDIInDefault() (MIDIIn, error) { return &midiIn{in: in, midi: midi{midi: C.RtMidiPtr(in)}}, nil } -// NewMIDIIn opens a single MIDIIn port using the given API. One can provide a +// Open a single MIDIIn port using the given API. One can provide a // custom port name and a desired queue size for the incomming MIDI messages. func NewMIDIIn(api API, name string, queueSize int) (MIDIIn, error) { p := C.CString(name) @@ -200,6 +221,7 @@ func NewMIDIIn(api API, name string, queueSize int) (MIDIIn, error) { return &midiIn{in: in, midi: midi{midi: C.RtMidiPtr(in)}}, nil } +// Return the MIDI API specifier for the current instance of RtMidiIn. func (m *midiIn) API() (API, error) { api := C.rtmidi_in_get_current_api(m.in) if !m.in.ok { @@ -208,6 +230,7 @@ func (m *midiIn) API() (API, error) { return API(api), nil } +// Close an open MIDI connection (if one exists). func (m *midiIn) Close() error { unregisterMIDIIn(m) if err := m.midi.Close(); err != nil { @@ -217,6 +240,13 @@ func (m *midiIn) Close() error { return nil } +// Specify whether certain MIDI message types should be queued or ignored during input. +// +// By default, MIDI timing and active sensing messages are ignored +// during message input because of their relative high data rates. +// MIDI sysex messages are ignored by default as well. Variable +// values of "true" imply that the respective message type will be +// ignored. func (m *midiIn) IgnoreTypes(midiSysex bool, midiTime bool, midiSense bool) error { C.rtmidi_in_ignore_types(m.in, C._Bool(midiSysex), C._Bool(midiTime), C._Bool(midiSense)) if !m.in.ok { @@ -265,6 +295,7 @@ func goMIDIInCallback(ts C.double, msg *C.uchar, msgsz C.size_t, arg unsafe.Poin m.cb(m, C.GoBytes(unsafe.Pointer(msg), C.int(msgsz)), float64(ts)) } +// Set a callback function to be invoked for incoming MIDI messages. func (m *midiIn) SetCallback(cb func(MIDIIn, []byte, float64)) error { k := registerMIDIIn(m) m.cb = cb @@ -275,6 +306,7 @@ func (m *midiIn) SetCallback(cb func(MIDIIn, []byte, float64)) error { return nil } +// Cancel use of the current callback function (if one exists). func (m *midiIn) CancelCallback() error { unregisterMIDIIn(m) C.rtmidi_in_cancel_callback(m.in) @@ -284,6 +316,10 @@ func (m *midiIn) CancelCallback() error { return nil } +// Fill a byte buffer with the next available MIDI message in the input queue +// and return the event delta-time in seconds. +// +// This function returns immediately whether a new message is available or not. func (m *midiIn) Message() ([]byte, float64, error) { msg := make([]C.uchar, 64*1024, 64*1024) sz := C.size_t(len(msg)) @@ -302,7 +338,7 @@ func (m *midiIn) Destroy() { C.rtmidi_in_free(m.in) } -// NewMIDIOutDefault opens a default MIDIOut port. +// Open a default MIDIOut port. func NewMIDIOutDefault() (MIDIOut, error) { out := C.rtmidi_out_create_default() if !out.ok { @@ -312,7 +348,7 @@ func NewMIDIOutDefault() (MIDIOut, error) { return &midiOut{out: out, midi: midi{midi: C.RtMidiPtr(out)}}, nil } -// NewMIDIOut opens a single MIDIIn port using the given API with the given port name. +// Open a single MIDIIn port using the given API with the given port name. func NewMIDIOut(api API, name string) (MIDIOut, error) { p := C.CString(name) defer C.free(unsafe.Pointer(p)) @@ -324,6 +360,7 @@ func NewMIDIOut(api API, name string) (MIDIOut, error) { return &midiOut{out: out, midi: midi{midi: C.RtMidiPtr(out)}}, nil } +// Return the MIDI API specifier for the current instance of RtMidiOut. func (m *midiOut) API() (API, error) { api := C.rtmidi_out_get_current_api(m.out) if !m.out.ok { @@ -332,6 +369,7 @@ func (m *midiOut) API() (API, error) { return API(api), nil } +// Close an open MIDI connection. func (m *midiOut) Close() error { if err := m.midi.Close(); err != nil { return err @@ -340,6 +378,7 @@ func (m *midiOut) Close() error { return nil } +// Immediately send a single message out an open MIDI output port. func (m *midiOut) SendMessage(b []byte) error { p := C.CBytes(b) defer C.free(unsafe.Pointer(p)) diff --git a/v2/drivers/rtmididrv/imported/rtmidi/rtmidi_c.h b/v2/drivers/rtmididrv/imported/rtmidi/rtmidi_c.h index c0526a6..efbf977 100644 --- a/v2/drivers/rtmididrv/imported/rtmidi/rtmidi_c.h +++ b/v2/drivers/rtmididrv/imported/rtmidi/rtmidi_c.h @@ -155,10 +155,14 @@ RTMIDIAPI void rtmidi_close_port (RtMidiPtr device); */ RTMIDIAPI unsigned int rtmidi_get_port_count (RtMidiPtr device); -/*! \brief Return a string identifier for the specified MIDI input port number. +/*! \brief Access a string identifier for the specified MIDI input port number. + * + * To prevent memory leaks a char buffer must be passed to this function. + * NULL can be passed as bufOut parameter, and that will write the required buffer length in the bufLen. + * * See RtMidi::getPortName(). */ -RTMIDIAPI const char* rtmidi_get_port_name (RtMidiPtr device, unsigned int portNumber); +RTMIDIAPI int rtmidi_get_port_name (RtMidiPtr device, unsigned int portNumber, char * bufOut, int * bufLen); /* RtMidiIn API */ diff --git a/v2/drivers/rtmididrv/imported/rtmidi/rtmidi_stub.cpp b/v2/drivers/rtmididrv/imported/rtmidi/rtmidi_stub.cpp index 928e3d3..ccb8cfd 100644 --- a/v2/drivers/rtmididrv/imported/rtmidi/rtmidi_stub.cpp +++ b/v2/drivers/rtmididrv/imported/rtmidi/rtmidi_stub.cpp @@ -1,4 +1,5 @@ #include "cpp/RtMidi.h" +#define RTMIDI_SOURCE_INCLUDED #include "cpp/RtMidi.cpp" #include "cpp/rtmidi_c.cpp" diff --git a/v2/drivers/rtmididrv/in.go b/v2/drivers/rtmididrv/in.go index 3ba2d16..581ad1a 100644 --- a/v2/drivers/rtmididrv/in.go +++ b/v2/drivers/rtmididrv/in.go @@ -3,6 +3,7 @@ package rtmididrv import ( "context" "fmt" + "log" "math" "gitlab.com/gomidi/midi/v2" @@ -145,7 +146,8 @@ func (i *in) Listen(ctx context.Context, onMsg func(msg []byte, milliseconds int } <-ctx.Done() - return i.midiIn.CancelCallback() + log.Printf("rtmidiin done: %T %v", i.midiIn, i.midiIn) + return nil // i.midiIn.CancelCallback() } /* diff --git a/v2/listen.go b/v2/listen.go index 0b4ebe9..0408653 100644 --- a/v2/listen.go +++ b/v2/listen.go @@ -70,6 +70,7 @@ func Listen(ctx context.Context, inPort ListenPortFinderFunc, recv func(msg []by return err } } + defer in.Close() var conf ListenConfig for _, opt := range opts { -- GitLab