fix(tracker): Player routes MIDImsgs so always handled in same block

This commit is contained in:
5684185+vsariola@users.noreply.github.com
2026-02-03 21:20:01 +02:00
parent cc8d737f8a
commit 77b27257fe
8 changed files with 176 additions and 111 deletions

View File

@ -32,22 +32,22 @@ type (
// case <-time.After(3 * time.Second):
// }
Broker struct {
ToModel chan MsgToModel
ToPlayer chan any // TODO: consider using a sum type here, for a bit more type safety. See: https://www.jerf.org/iri/post/2917/
ToDetector chan MsgToDetector
ToGUI chan any
ToSpecAn chan MsgToSpecAn
ToMIDIRouter chan any
ToModel chan MsgToModel
ToPlayer chan any // TODO: consider using a sum type here, for a bit more type safety. See: https://www.jerf.org/iri/post/2917/
ToDetector chan MsgToDetector
ToGUI chan any
ToSpecAn chan MsgToSpecAn
ToMIDIHandler chan any
CloseDetector chan struct{}
CloseGUI chan struct{}
CloseSpecAn chan struct{}
CloseMIDIRouter chan struct{}
CloseDetector chan struct{}
CloseGUI chan struct{}
CloseSpecAn chan struct{}
CloseMIDIHandler chan struct{}
FinishedGUI chan struct{}
FinishedDetector chan struct{}
FinishedSpecAn chan struct{}
FinishedMIDIRouter chan struct{}
FinishedGUI chan struct{}
FinishedDetector chan struct{}
FinishedSpecAn chan struct{}
FinishedMIDIHandler chan struct{}
bufferPool sync.Pool
spectrumPool sync.Pool
@ -111,22 +111,22 @@ const (
func NewBroker() *Broker {
return &Broker{
ToPlayer: make(chan any, 1024),
ToModel: make(chan MsgToModel, 1024),
ToDetector: make(chan MsgToDetector, 1024),
ToGUI: make(chan any, 1024),
ToMIDIRouter: make(chan any, 1024),
ToSpecAn: make(chan MsgToSpecAn, 1024),
CloseDetector: make(chan struct{}, 1),
CloseGUI: make(chan struct{}, 1),
CloseSpecAn: make(chan struct{}, 1),
CloseMIDIRouter: make(chan struct{}, 1),
FinishedGUI: make(chan struct{}),
FinishedDetector: make(chan struct{}),
FinishedSpecAn: make(chan struct{}),
FinishedMIDIRouter: make(chan struct{}),
bufferPool: sync.Pool{New: func() any { return &sointu.AudioBuffer{} }},
spectrumPool: sync.Pool{New: func() any { return &Spectrum{} }},
ToPlayer: make(chan any, 1024),
ToModel: make(chan MsgToModel, 1024),
ToDetector: make(chan MsgToDetector, 1024),
ToGUI: make(chan any, 1024),
ToMIDIHandler: make(chan any, 1024),
ToSpecAn: make(chan MsgToSpecAn, 1024),
CloseDetector: make(chan struct{}, 1),
CloseGUI: make(chan struct{}, 1),
CloseSpecAn: make(chan struct{}, 1),
CloseMIDIHandler: make(chan struct{}, 1),
FinishedGUI: make(chan struct{}),
FinishedDetector: make(chan struct{}),
FinishedSpecAn: make(chan struct{}),
FinishedMIDIHandler: make(chan struct{}),
bufferPool: sync.Pool{New: func() any { return &sointu.AudioBuffer{} }},
spectrumPool: sync.Pool{New: func() any { return &Spectrum{} }},
}
}

View File

@ -134,16 +134,20 @@ func (te *NoteEditor) Layout(gtx layout.Context) layout.Dimensions {
}
}
for gtx.Focused(te.scrollTable) && len(t.noteEvents) > 0 {
ev := t.noteEvents[0]
ev.IsTrack = true
ev.Channel = t.Model.Note().Cursor().X
ev.Source = te
for gtx.Focused(te.scrollTable) && len(t.midiMsgs) > 0 {
ev := tracker.NoteEvent{
Timestamp: t.midiMsgs[0].Timestamp,
Note: t.midiMsgs[0].Data[1],
On: t.midiMsgs[0].Data[0]&0xF0 != 0x80,
IsTrack: true,
Channel: t.Model.Note().Cursor().X,
Source: t.midiMsgs[0].Source,
}
if ev.On {
t.Model.Note().Input(ev.Note)
}
copy(t.noteEvents, t.noteEvents[1:])
t.noteEvents = t.noteEvents[:len(t.noteEvents)-1]
copy(t.midiMsgs, t.midiMsgs[1:])
t.midiMsgs = t.midiMsgs[:len(t.midiMsgs)-1]
tracker.TrySend(t.Broker().ToPlayer, any(&ev))
}

View File

@ -51,7 +51,7 @@ type (
SongPanel *SongPanel
filePathString tracker.String
noteEvents []tracker.NoteEvent
midiMsgs []*tracker.MIDIMessage
preferences Preferences
@ -145,8 +145,8 @@ func (t *Tracker) Main() {
select {
case e := <-t.Broker().ToGUI:
switch e := e.(type) {
case *tracker.NoteEvent:
t.noteEvents = append(t.noteEvents, *e)
case *tracker.MIDIMessage:
t.midiMsgs = append(t.midiMsgs, e)
case tracker.MsgToGUI:
switch e.Kind {
case tracker.GUIMessageCenterOnRow:
@ -267,13 +267,17 @@ func (t *Tracker) Layout(gtx layout.Context) {
}
}
// if no-one else handled the note events, we handle them here
for len(t.noteEvents) > 0 {
ev := t.noteEvents[0]
ev.IsTrack = false
ev.Channel = t.Model.Instrument().List().Selected()
ev.Source = t
copy(t.noteEvents, t.noteEvents[1:])
t.noteEvents = t.noteEvents[:len(t.noteEvents)-1]
for len(t.midiMsgs) > 0 {
ev := tracker.NoteEvent{
Timestamp: t.midiMsgs[0].Timestamp,
Note: t.midiMsgs[0].Data[1],
On: t.midiMsgs[0].Data[0]&0xF0 != 0x80,
IsTrack: false,
Channel: t.Model.Instrument().List().Selected(),
Source: t.midiMsgs[0].Source,
}
copy(t.midiMsgs, t.midiMsgs[1:])
t.midiMsgs = t.midiMsgs[:len(t.midiMsgs)-1]
tracker.TrySend(t.Broker().ToPlayer, any(&ev))
}
}

View File

@ -46,7 +46,7 @@ func (m *RTMIDIContext) Inputs(yield func(input tracker.MIDIInputDevice) bool) {
}
for _, in := range ins {
r := RTMIDIInputDevice{In: in, broker: m.broker}
if !yield(r) {
if !yield(&r) {
break
}
}
@ -67,27 +67,21 @@ func (c *RTMIDIContext) Support() tracker.MIDISupport {
}
// Open an input device and starting the listener.
func (m RTMIDIInputDevice) Open() error {
func (m *RTMIDIInputDevice) Open(h func(msg *tracker.MIDIMessage)) error {
if err := m.In.Open(); err != nil {
return fmt.Errorf("opening MIDI input failed: %w", err)
}
if _, err := midi.ListenTo(m.In, m.handleMessage); err != nil {
q := func(msg midi.Message, timestampms int32) {
if len(msg.Bytes()) == 0 || len(msg.Bytes()) > 3 {
return
}
t := tracker.MIDIMessage{Timestamp: int64(timestampms) * 441 / 10, Source: m}
copy(t.Data[:], msg.Bytes())
h(&t)
}
if _, err := midi.ListenTo(m.In, q); err != nil {
m.In.Close()
return fmt.Errorf("listening to MIDI input failed: %w", err)
}
return nil
}
func (m *RTMIDIInputDevice) handleMessage(msg midi.Message, timestampms int32) {
var channel, key, velocity, controller, value uint8
if msg.GetNoteOn(&channel, &key, &velocity) {
ev := tracker.NoteEvent{Timestamp: int64(timestampms) * 441 / 10, On: true, Channel: int(channel), Note: key, Source: m}
tracker.TrySend(m.broker.ToMIDIRouter, any(&ev))
} else if msg.GetNoteOff(&channel, &key, &velocity) {
ev := tracker.NoteEvent{Timestamp: int64(timestampms) * 441 / 10, On: false, Channel: int(channel), Note: key, Source: m}
tracker.TrySend(m.broker.ToMIDIRouter, any(&ev))
} else if msg.GetControlChange(&channel, &controller, &value) {
ev := tracker.ControlChange{Channel: int(channel), Control: int(controller), Value: int(value)}
tracker.TrySend(m.broker.ToMIDIRouter, any(&ev))
}
}

View File

@ -11,8 +11,8 @@ func (m *Model) MIDI() *MIDIModel { return (*MIDIModel)(m) }
type (
midiState struct {
noteEventsToGui bool
binding bool
binding bool
router midiRouter
currentInput MIDIInputDevice
context MIDIContext
@ -26,7 +26,7 @@ type (
}
MIDIInputDevice interface {
Open() error
Open(func(msg *MIDIMessage)) error
Close() error
IsOpen() bool
String() string
@ -56,7 +56,10 @@ func (m *midiRefresh) Do() {
if m.midi.currentInput != nil && i.String() == m.midi.currentInput.String() {
m.midi.currentInput.Close()
m.midi.currentInput = nil
if err := i.Open(); err != nil {
handler := func(msg *MIDIMessage) {
TrySend(m.broker.ToMIDIHandler, any(msg))
}
if err := i.Open(handler); err != nil {
(*Model)(m).Alerts().Add(fmt.Sprintf("Failed to reopen MIDI input port: %s", err.Error()), Error)
continue
}
@ -96,7 +99,10 @@ func (m *midiInputDevices) SetValue(val int) bool {
return true
}
newInput := m.midi.inputs[val-1]
if err := newInput.Open(); err != nil {
handler := func(msg *MIDIMessage) {
TrySend(m.broker.ToMIDIHandler, any(msg))
}
if err := newInput.Open(handler); err != nil {
(*Model)(m).Alerts().Add(fmt.Sprintf("Failed to open MIDI input port: %s", err.Error()), Error)
return false
}
@ -131,34 +137,82 @@ func (m *MIDIModel) InputtingNotes() Bool { return MakeBool((*midiInputtingNotes
type midiInputtingNotes Model
func (m *midiInputtingNotes) Value() bool { return m.midi.noteEventsToGui }
func (m *midiInputtingNotes) Value() bool { return m.midi.router.sendNoteEventsToGUI }
func (m *midiInputtingNotes) SetValue(val bool) {
m.midi.noteEventsToGui = val
TrySend(m.broker.ToMIDIRouter, any(setNoteEventsToGUI(val)))
m.midi.router.sendNoteEventsToGUI = val
TrySend(m.broker.ToMIDIHandler, any(m.midi.router))
TrySend(m.broker.ToPlayer, any(m.midi.router))
}
type setNoteEventsToGUI bool
// MIDIMessage represents a MIDI message received from a MIDI input port or VST
// host.
type MIDIMessage struct {
Timestamp int64 // in samples (at 44100 Hz)
Data [3]byte
Source any // tag to identify the source of the message; any unique pointer will do
}
func runMIDIRouter(broker *Broker) {
noteEventsToGUI := false
func (m *MIDIMessage) isNoteOff() bool { return m.Data[0]&0xF0 == 0x80 }
func (m *MIDIMessage) isNoteOn() bool { return m.Data[0]&0xF0 == 0x90 }
func (m *MIDIMessage) isControlChange() bool { return m.Data[0]&0xF0 == 0xB0 }
func (m *MIDIMessage) getNoteOn() (channel, note, velocity byte, ok bool) {
if !m.isNoteOn() {
return 0, 0, 0, false
}
return m.Data[0] & 0x0F, m.Data[1], m.Data[2], true
}
func (m *MIDIMessage) getNoteOff() (channel, note, velocity byte, ok bool) {
if !m.isNoteOff() {
return 0, 0, 0, false
}
return m.Data[0] & 0x0F, m.Data[1], m.Data[2], true
}
func (m *MIDIMessage) getControlChange() (channel, controller, value byte, ok bool) {
if !m.isControlChange() {
return 0, 0, 0, false
}
return m.Data[0] & 0x0F, m.Data[1], m.Data[2], true
}
// midiRouter encompasses all the necessary information where MIDIMessages
// should be forwarded. MIDIHandler and Player have their own copies of the
// midiRouter so that the messages don't have to pass through other goroutines
// to be routed. Model has also a copy to display a gui to modify it.
type midiRouter struct {
sendNoteEventsToGUI bool
}
func (r *midiRouter) route(b *Broker, msg *MIDIMessage) (ok bool) {
switch {
case msg.isNoteOn() || msg.isNoteOff():
if r.sendNoteEventsToGUI {
return TrySend(b.ToGUI, any(msg))
} else {
return TrySend(b.ToPlayer, any(msg))
}
case msg.isControlChange():
return TrySend(b.ToModel, MsgToModel{Data: msg})
}
return false
}
func runMIDIHandler(b *Broker) {
router := midiRouter{sendNoteEventsToGUI: false}
for {
select {
case <-broker.CloseMIDIRouter:
close(broker.FinishedMIDIRouter)
return
case msg := <-broker.ToMIDIRouter:
switch m := msg.(type) {
case setNoteEventsToGUI:
noteEventsToGUI = bool(m)
case *NoteEvent:
if noteEventsToGUI {
TrySend(broker.ToGUI, msg)
continue
}
TrySend(broker.ToPlayer, msg)
case *ControlChange:
TrySend(broker.ToModel, MsgToModel{Data: msg})
case v := <-b.ToMIDIHandler:
switch msg := v.(type) {
case *MIDIMessage:
router.route(b, msg)
case midiRouter:
router = msg
}
case <-b.CloseMIDIHandler:
close(b.FinishedMIDIHandler)
return
}
}
}
@ -223,8 +277,8 @@ func (m *MIDIModel) selectedParam() (MIDIParam, bool) {
return value, true
}
func (m *MIDIModel) handleControlEvent(e ControlChange) {
key := MIDIControl{Channel: e.Channel, Control: e.Control}
func (m *MIDIModel) handleControlEvent(channel, control, value int) {
key := MIDIControl{Channel: channel, Control: control}
if m.midi.binding {
m.midi.binding = false
value, ok := m.selectedParam()
@ -246,7 +300,7 @@ func (m *MIDIModel) handleControlEvent(e ControlChange) {
// +62 is chose so that the center position of a typical MIDI controller,
// which is 64, maps to 64 of a 0..128 range Sointu parameter. From there
// on, 65 maps to 66 and, importantly, 127 maps to 128.
newVal := (e.Value*(t.Max-t.Min)+62)/127 + t.Min
newVal := (value*(t.Max-t.Min)+62)/127 + t.Min
if m.d.Song.Patch[i].Units[u].Parameters[t.Param] == newVal {
return
}

View File

@ -203,17 +203,17 @@ func NewModel(broker *Broker, synthers []sointu.Synther, midiContext MIDIContext
m.Play().setSynther(0, false)
go runDetector(broker)
go runSpecAnalyzer(broker)
go runMIDIRouter(broker)
go runMIDIHandler(broker)
return m
}
func (m *Model) Close() {
TrySend(m.broker.CloseDetector, struct{}{})
TrySend(m.broker.CloseSpecAn, struct{}{})
TrySend(m.broker.CloseMIDIRouter, struct{}{})
TrySend(m.broker.CloseMIDIHandler, struct{}{})
TimeoutReceive(m.broker.FinishedDetector, 3*time.Second)
TimeoutReceive(m.broker.FinishedSpecAn, 3*time.Second)
TimeoutReceive(m.broker.FinishedMIDIRouter, 3*time.Second)
TimeoutReceive(m.broker.FinishedMIDIHandler, 3*time.Second)
}
// RequestQuit asks the tracker to quit, showing a dialog if there are unsaved
@ -389,8 +389,10 @@ func (m *Model) ProcessMsg(msg MsgToModel) {
case *Spectrum:
m.broker.PutSpectrum(m.spectrum)
m.spectrum = e
case *ControlChange:
m.MIDI().handleControlEvent(*e)
case *MIDIMessage:
if channel, control, value, ok := e.getControlChange(); ok {
m.MIDI().handleControlEvent(int(channel), int(control), int(value))
}
}
}

View File

@ -30,6 +30,8 @@ type (
frameDeltas map[any]int64 // Player.frame (approx.)= event.Timestamp + frameDeltas[event.Source]
events NoteEventList
midiRouter midiRouter
status PlayerStatus // the part of the Player state that is communicated to the model to visualize what Player is doing
synther sointu.Synther // the synther used to create new synths
@ -180,6 +182,8 @@ func (p *Player) Process(buffer sointu.AudioBuffer, context PlayerProcessContext
p.SendAlert("PlayerCrash", fmt.Sprintf("synth did not fill the audio buffer even with %d render calls", numRenderTries), Error)
}
func (p *Player) EmitMIDIMsg(msg *MIDIMessage) bool { return p.midiRouter.route(p.broker, msg) }
func (p *Player) destroySynth() {
if p.synth != nil {
p.synth.Close()
@ -282,6 +286,20 @@ loop:
TrySend(p.broker.ToModel, MsgToModel{Reset: true})
case *NoteEvent:
p.events = append(p.events, *m)
case *MIDIMessage:
// In future, here we should map midi channels to various
// instruments, possible mapping the velocity to another
// instrument as well
if m.Data[0] >= 0x80 && m.Data[0] <= 0x9F {
p.events = append(p.events, NoteEvent{
Timestamp: m.Timestamp,
Channel: int(m.Data[0] & 0x0F),
Note: m.Data[1],
On: m.Data[0] >= 0x90,
Source: m.Source})
}
case midiRouter:
p.midiRouter = m
case RecordingMsg:
if m.bool {
p.recording = Recording{State: RecordingWaitingForNote}