From c07d8000c6056d391af60d5e84925da56af7af10 Mon Sep 17 00:00:00 2001 From: "5684185+vsariola@users.noreply.github.com" <5684185+vsariola@users.noreply.github.com> Date: Mon, 14 Oct 2024 15:00:55 +0300 Subject: [PATCH] refactor(tracker): harmonize naming and use iterators in MIDI using iterators requires go 1.23 --- cmd/sointu-track/main.go | 4 +- go.mod | 2 +- tracker/action.go | 9 ++- tracker/gomidi/midi.go | 152 ++++++++++++++++++++------------------- tracker/model.go | 15 ++-- 5 files changed, 95 insertions(+), 87 deletions(-) diff --git a/cmd/sointu-track/main.go b/cmd/sointu-track/main.go index ac7fd0b..9989e5f 100644 --- a/cmd/sointu-track/main.go +++ b/cmd/sointu-track/main.go @@ -54,8 +54,8 @@ func main() { recoveryFile = filepath.Join(configDir, "Sointu", "sointu-track-recovery") } model, player := tracker.NewModelPlayer(cmd.MainSynther, recoveryFile) - model.MIDI = gomidi.CreateContext() - defer model.MIDI.DestroyContext() + model.MIDI = gomidi.NewContext() + defer model.MIDI.Close() if a := flag.Args(); len(a) > 0 { f, err := os.Open(a[0]) if err == nil { diff --git a/go.mod b/go.mod index f7fc25c..4a25dc4 100644 --- a/go.mod +++ b/go.mod @@ -1,6 +1,6 @@ module github.com/vsariola/sointu -go 1.22.2 +go 1.23 require ( gioui.org v0.7.1 diff --git a/tracker/action.go b/tracker/action.go index a5e2e46..c772d62 100644 --- a/tracker/action.go +++ b/tracker/action.go @@ -445,11 +445,14 @@ func (m *Model) Cancel() Action { return Allow(func() { m.dialog = NoDialog func (m *Model) Export() Action { return Allow(func() { m.dialog = Export }) } func (m *Model) ExportFloat() Action { return Allow(func() { m.dialog = ExportFloatExplorer }) } func (m *Model) ExportInt16() Action { return Allow(func() { m.dialog = ExportInt16Explorer }) } -func (m *Model) SelectMidiInput(item MIDIDevicer) Action { +func (m *Model) SelectMidiInput(item MIDIDevice) Action { return Allow(func() { - if !m.MIDI.OpenInputDevice(item) { - message := fmt.Sprintf("Could not open MIDI device %s\n", item) + if err := item.Open(); err != nil { + message := fmt.Sprintf("Could not open MIDI device: %s", item) m.Alerts().Add(message, Error) + } else { + message := fmt.Sprintf("Opened MIDI device: %s", item) + m.Alerts().Add(message, Info) } }) } diff --git a/tracker/gomidi/midi.go b/tracker/gomidi/midi.go index 5674c4e..27acccf 100644 --- a/tracker/gomidi/midi.go +++ b/tracker/gomidi/midi.go @@ -1,8 +1,8 @@ package gomidi import ( + "errors" "fmt" - "time" "github.com/vsariola/sointu/tracker" "gitlab.com/gomidi/midi/v2" @@ -11,105 +11,111 @@ import ( ) type ( - MIDIContext struct { - driver *rtmididrv.Driver - inputAvailable bool - driverAvailable bool - currentIn MIDIDevicer - events chan midi.Message + RTMIDIContext struct { + driver *rtmididrv.Driver + currentIn drivers.In + events chan midi.Message + } + + RTMIDIDevice struct { + context *RTMIDIContext + in drivers.In } - MIDIDevicer drivers.In ) -func (m *MIDIContext) ListInputDevices() <-chan tracker.MIDIDevicer { - - ins, err := m.driver.Ins() - channel := make(chan tracker.MIDIDevicer, len(ins)) - if err != nil { - m.driver.Close() - m.driverAvailable = false - return nil - } - go func() { - for i := 0; i < len(ins); i++ { - channel <- ins[i].(MIDIDevicer) +func (m *RTMIDIContext) ListInputDevices() func(yield func(tracker.MIDIDevice) bool) { + return func(yield func(tracker.MIDIDevice) bool) { + if m.driver == nil { + return } - close(channel) - }() - return channel + ins, err := m.driver.Ins() + if err != nil { + return + } + for i := 0; i < len(ins); i++ { + device := RTMIDIDevice{context: m, in: ins[i]} + if !yield(device) { + break + } + } + } } // Open the driver. -func CreateContext() *MIDIContext { - m := MIDIContext{} - var err error - m.driver, err = rtmididrv.New() - m.driverAvailable = err == nil - if m.driverAvailable { - m.events = make(chan midi.Message) - } +func NewContext() *RTMIDIContext { + m := RTMIDIContext{events: make(chan midi.Message, 1024)} + // there's not much we can do if this fails, so just use m.driver = nil to + // indicate no driver available + m.driver, _ = rtmididrv.New() return &m } // Open an input device while closing the currently open if necessary. -func (m *MIDIContext) OpenInputDevice(in tracker.MIDIDevicer) bool { - fmt.Printf("Opening midi device %s\n.", in) - if m.driverAvailable { - if m.currentIn == in { - return false - } - if m.inputAvailable && m.currentIn.IsOpen() { - m.currentIn.Close() - } - m.currentIn = in.(MIDIDevicer) - m.currentIn.Open() - _, err := midi.ListenTo(m.currentIn, m.HandleMessage) - if err != nil { - m.inputAvailable = false - return false - } +func (m RTMIDIDevice) Open() error { + if m.context.currentIn == m.in { + return nil } - return true + if m.context.driver == nil { + return errors.New("no driver available") + } + if m.context.currentIn != nil && m.context.currentIn.IsOpen() { + m.context.currentIn.Close() + } + m.context.currentIn = m.in + err := m.in.Open() + if err != nil { + m.context.currentIn = nil + return fmt.Errorf("opening MIDI input failed: %W", err) + } + _, err = midi.ListenTo(m.in, m.context.HandleMessage) + if err != nil { + m.in.Close() + m.context.currentIn = nil + } + return nil } -func (m *MIDIContext) HandleMessage(msg midi.Message, timestampms int32) { - go func() { - m.events <- msg - time.Sleep(time.Nanosecond) - }() +func (d RTMIDIDevice) String() string { + return d.in.String() } -func (c *MIDIContext) NextEvent() (event tracker.MIDINoteEvent, ok bool) { +func (m *RTMIDIContext) HandleMessage(msg midi.Message, timestampms int32) { + select { + case m.events <- msg: // if the channel is full, just drop the message + default: + } +} + +func (c *RTMIDIContext) NextEvent() (event tracker.MIDINoteEvent, ok bool) { select { case msg := <-c.events: - { - var channel uint8 - var velocity uint8 - var key uint8 - var controller uint8 - var value uint8 - if msg.GetNoteOn(&channel, &key, &velocity) { - return tracker.MIDINoteEvent{Frame: 0, On: true, Channel: int(channel), Note: key}, true - } else if msg.GetNoteOff(&channel, &key, &velocity) { - return tracker.MIDINoteEvent{Frame: 0, On: false, Channel: int(channel), Note: key}, true - } else if msg.GetControlChange(&channel, &controller, &value) { - fmt.Printf("CC @ Channel: %d, Controller: %d, Value: %d\n", channel, controller, value) - } else { - fmt.Printf("Unhandled MIDI message: %s\n", msg) - } + var channel uint8 + var velocity uint8 + var key uint8 + if msg.GetNoteOn(&channel, &key, &velocity) { + return tracker.MIDINoteEvent{Frame: 0, On: true, Channel: int(channel), Note: key}, true + } else if msg.GetNoteOff(&channel, &key, &velocity) { + return tracker.MIDINoteEvent{Frame: 0, On: false, Channel: int(channel), Note: key}, true } + // TODO: handle control messages with something like: + // if msg.GetControlChange(&channel, &controller, &value) { + // .... default: // Note (@LeStahL): This empty select case is needed to make the implementation non-blocking. } return tracker.MIDINoteEvent{}, false } -func (c *MIDIContext) BPM() (bpm float64, ok bool) { +func (c *RTMIDIContext) BPM() (bpm float64, ok bool) { return 0, false } -func (c *MIDIContext) DestroyContext() { - close(c.events) - c.currentIn.Close() +func (c *RTMIDIContext) Close() { + if c.driver == nil { + return + } + if c.currentIn != nil && c.currentIn.IsOpen() { + c.currentIn.Close() + } c.driver.Close() } diff --git a/tracker/model.go b/tracker/model.go index a8a985f..175d5f7 100644 --- a/tracker/model.go +++ b/tracker/model.go @@ -76,7 +76,7 @@ type ( PlayerMessages chan PlayerMsg modelMessages chan<- interface{} - MIDI MIDIContexter + MIDI MIDIContext } // Cursor identifies a row and a track in a song score. @@ -123,16 +123,15 @@ type ( Dialog int - MIDIContexter interface { - ListInputDevices() <-chan MIDIDevicer - OpenInputDevice(item MIDIDevicer) bool - DestroyContext() - BPM() (bpm float64, ok bool) - NextEvent() (event MIDINoteEvent, ok bool) + MIDIContext interface { + ListInputDevices() func(yield func(MIDIDevice) bool) + Close() + PlayerProcessContext } - MIDIDevicer interface { + MIDIDevice interface { String() string + Open() error } )