refactor(tracker): harmonize naming and use iterators in MIDI

using iterators requires go 1.23
This commit is contained in:
5684185+vsariola@users.noreply.github.com 2024-10-14 15:00:55 +03:00
parent 577265b250
commit c07d8000c6
5 changed files with 95 additions and 87 deletions

View File

@ -54,8 +54,8 @@ func main() {
recoveryFile = filepath.Join(configDir, "Sointu", "sointu-track-recovery") recoveryFile = filepath.Join(configDir, "Sointu", "sointu-track-recovery")
} }
model, player := tracker.NewModelPlayer(cmd.MainSynther, recoveryFile) model, player := tracker.NewModelPlayer(cmd.MainSynther, recoveryFile)
model.MIDI = gomidi.CreateContext() model.MIDI = gomidi.NewContext()
defer model.MIDI.DestroyContext() defer model.MIDI.Close()
if a := flag.Args(); len(a) > 0 { if a := flag.Args(); len(a) > 0 {
f, err := os.Open(a[0]) f, err := os.Open(a[0])
if err == nil { if err == nil {

2
go.mod
View File

@ -1,6 +1,6 @@
module github.com/vsariola/sointu module github.com/vsariola/sointu
go 1.22.2 go 1.23
require ( require (
gioui.org v0.7.1 gioui.org v0.7.1

View File

@ -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) Export() Action { return Allow(func() { m.dialog = Export }) }
func (m *Model) ExportFloat() Action { return Allow(func() { m.dialog = ExportFloatExplorer }) } 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) 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() { return Allow(func() {
if !m.MIDI.OpenInputDevice(item) { if err := item.Open(); err != nil {
message := fmt.Sprintf("Could not open MIDI device %s\n", item) message := fmt.Sprintf("Could not open MIDI device: %s", item)
m.Alerts().Add(message, Error) m.Alerts().Add(message, Error)
} else {
message := fmt.Sprintf("Opened MIDI device: %s", item)
m.Alerts().Add(message, Info)
} }
}) })
} }

View File

@ -1,8 +1,8 @@
package gomidi package gomidi
import ( import (
"errors"
"fmt" "fmt"
"time"
"github.com/vsariola/sointu/tracker" "github.com/vsariola/sointu/tracker"
"gitlab.com/gomidi/midi/v2" "gitlab.com/gomidi/midi/v2"
@ -11,105 +11,111 @@ import (
) )
type ( type (
MIDIContext struct { RTMIDIContext struct {
driver *rtmididrv.Driver driver *rtmididrv.Driver
inputAvailable bool currentIn drivers.In
driverAvailable bool events chan midi.Message
currentIn MIDIDevicer }
events chan midi.Message
RTMIDIDevice struct {
context *RTMIDIContext
in drivers.In
} }
MIDIDevicer drivers.In
) )
func (m *MIDIContext) ListInputDevices() <-chan tracker.MIDIDevicer { func (m *RTMIDIContext) ListInputDevices() func(yield func(tracker.MIDIDevice) bool) {
return func(yield func(tracker.MIDIDevice) bool) {
ins, err := m.driver.Ins() if m.driver == nil {
channel := make(chan tracker.MIDIDevicer, len(ins)) return
if err != nil {
m.driver.Close()
m.driverAvailable = false
return nil
}
go func() {
for i := 0; i < len(ins); i++ {
channel <- ins[i].(MIDIDevicer)
} }
close(channel) ins, err := m.driver.Ins()
}() if err != nil {
return channel return
}
for i := 0; i < len(ins); i++ {
device := RTMIDIDevice{context: m, in: ins[i]}
if !yield(device) {
break
}
}
}
} }
// Open the driver. // Open the driver.
func CreateContext() *MIDIContext { func NewContext() *RTMIDIContext {
m := MIDIContext{} m := RTMIDIContext{events: make(chan midi.Message, 1024)}
var err error // there's not much we can do if this fails, so just use m.driver = nil to
m.driver, err = rtmididrv.New() // indicate no driver available
m.driverAvailable = err == nil m.driver, _ = rtmididrv.New()
if m.driverAvailable {
m.events = make(chan midi.Message)
}
return &m return &m
} }
// Open an input device while closing the currently open if necessary. // Open an input device while closing the currently open if necessary.
func (m *MIDIContext) OpenInputDevice(in tracker.MIDIDevicer) bool { func (m RTMIDIDevice) Open() error {
fmt.Printf("Opening midi device %s\n.", in) if m.context.currentIn == m.in {
if m.driverAvailable { return nil
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
}
} }
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) { func (d RTMIDIDevice) String() string {
go func() { return d.in.String()
m.events <- msg
time.Sleep(time.Nanosecond)
}()
} }
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 { select {
case msg := <-c.events: case msg := <-c.events:
{ var channel uint8
var channel uint8 var velocity uint8
var velocity uint8 var key uint8
var key uint8 if msg.GetNoteOn(&channel, &key, &velocity) {
var controller uint8 return tracker.MIDINoteEvent{Frame: 0, On: true, Channel: int(channel), Note: key}, true
var value uint8 } else if msg.GetNoteOff(&channel, &key, &velocity) {
if msg.GetNoteOn(&channel, &key, &velocity) { return tracker.MIDINoteEvent{Frame: 0, On: false, Channel: int(channel), Note: key}, true
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)
}
} }
// TODO: handle control messages with something like:
// if msg.GetControlChange(&channel, &controller, &value) {
// ....
default: default:
// Note (@LeStahL): This empty select case is needed to make the implementation non-blocking. // Note (@LeStahL): This empty select case is needed to make the implementation non-blocking.
} }
return tracker.MIDINoteEvent{}, false return tracker.MIDINoteEvent{}, false
} }
func (c *MIDIContext) BPM() (bpm float64, ok bool) { func (c *RTMIDIContext) BPM() (bpm float64, ok bool) {
return 0, false return 0, false
} }
func (c *MIDIContext) DestroyContext() { func (c *RTMIDIContext) Close() {
close(c.events) if c.driver == nil {
c.currentIn.Close() return
}
if c.currentIn != nil && c.currentIn.IsOpen() {
c.currentIn.Close()
}
c.driver.Close() c.driver.Close()
} }

View File

@ -76,7 +76,7 @@ type (
PlayerMessages chan PlayerMsg PlayerMessages chan PlayerMsg
modelMessages chan<- interface{} modelMessages chan<- interface{}
MIDI MIDIContexter MIDI MIDIContext
} }
// Cursor identifies a row and a track in a song score. // Cursor identifies a row and a track in a song score.
@ -123,16 +123,15 @@ type (
Dialog int Dialog int
MIDIContexter interface { MIDIContext interface {
ListInputDevices() <-chan MIDIDevicer ListInputDevices() func(yield func(MIDIDevice) bool)
OpenInputDevice(item MIDIDevicer) bool Close()
DestroyContext() PlayerProcessContext
BPM() (bpm float64, ok bool)
NextEvent() (event MIDINoteEvent, ok bool)
} }
MIDIDevicer interface { MIDIDevice interface {
String() string String() string
Open() error
} }
) )