mirror of
https://github.com/vsariola/sointu.git
synced 2026-03-31 11:13:00 -04:00
fix(tracker): Player routes MIDImsgs so always handled in same block
This commit is contained in:
parent
cc8d737f8a
commit
77b27257fe
@ -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{} }},
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@ -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))
|
||||
}
|
||||
|
||||
|
||||
@ -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))
|
||||
}
|
||||
}
|
||||
|
||||
@ -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))
|
||||
}
|
||||
}
|
||||
|
||||
112
tracker/midi.go
112
tracker/midi.go
@ -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
|
||||
}
|
||||
|
||||
@ -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))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@ -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}
|
||||
|
||||
Reference in New Issue
Block a user