mirror of
https://github.com/vsariola/sointu.git
synced 2026-02-23 08:33:17 -05:00
feat(tracker): ability to bind MIDI controllers to parameters
Closes #152
This commit is contained in:
parent
6e8acc8f9b
commit
f2ef57a845
@ -5,6 +5,10 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/).
|
||||
|
||||
## [Unreleased]
|
||||
### Added
|
||||
- Ability to bind MIDI controllers to specific parameters. The MIDI menu has the
|
||||
options to bind/unbind parameters. When the user starts binding a parameter,
|
||||
Sointu waits for the next MIDI Control Change event and binds the currently
|
||||
selected parameter to that controller. ([#152][i152])
|
||||
- Plot the envelope shape on top of the oscilloscope when the envelope unit is
|
||||
selected.
|
||||
- Spectrum analyzer showing the spectrum. When the user has a filter or belleq
|
||||
@ -389,6 +393,7 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/).
|
||||
[i149]: https://github.com/vsariola/sointu/issues/149
|
||||
[i150]: https://github.com/vsariola/sointu/issues/150
|
||||
[i151]: https://github.com/vsariola/sointu/issues/151
|
||||
[i152]: https://github.com/vsariola/sointu/issues/152
|
||||
[i153]: https://github.com/vsariola/sointu/issues/153
|
||||
[i154]: https://github.com/vsariola/sointu/issues/154
|
||||
[i155]: https://github.com/vsariola/sointu/issues/155
|
||||
|
||||
@ -97,12 +97,19 @@ func init() {
|
||||
for i := 0; i < events.NumEvents(); i++ {
|
||||
switch ev := events.Event(i).(type) {
|
||||
case *vst2.MIDIEvent:
|
||||
if ev.Data[0] >= 0x80 && ev.Data[0] <= 0x9F {
|
||||
switch {
|
||||
case ev.Data[0] >= 0x80 && ev.Data[0] <= 0x9F:
|
||||
channel := ev.Data[0] & 0x0F
|
||||
note := ev.Data[1]
|
||||
on := ev.Data[0] >= 0x90
|
||||
trackerEvent := tracker.NoteEvent{Timestamp: int64(ev.DeltaFrames) + totalFrames, On: on, Channel: int(channel), Note: note, Source: &context}
|
||||
tracker.TrySend(broker.MIDIChannel(), any(trackerEvent))
|
||||
tracker.TrySend(broker.ToMIDIRouter, any(&trackerEvent))
|
||||
case ev.Data[0] >= 0xB0 && ev.Data[0] <= 0xBF:
|
||||
channel := ev.Data[0] & 0x0F
|
||||
controller := ev.Data[1]
|
||||
value := ev.Data[2]
|
||||
trackerEvent := tracker.ControlChange{Channel: int(channel), Control: int(controller), Value: int(value)}
|
||||
tracker.TrySend(broker.ToMIDIRouter, any(&trackerEvent))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ -2,7 +2,6 @@ package tracker
|
||||
|
||||
import (
|
||||
"sync"
|
||||
"sync/atomic"
|
||||
"time"
|
||||
|
||||
"github.com/vsariola/sointu"
|
||||
@ -33,24 +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
|
||||
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
|
||||
|
||||
CloseDetector chan struct{}
|
||||
CloseGUI chan struct{}
|
||||
CloseSpecAn chan struct{}
|
||||
CloseDetector chan struct{}
|
||||
CloseGUI chan struct{}
|
||||
CloseSpecAn chan struct{}
|
||||
CloseMIDIRouter chan struct{}
|
||||
|
||||
FinishedGUI chan struct{}
|
||||
FinishedDetector chan struct{}
|
||||
FinishedSpecAn chan struct{}
|
||||
|
||||
// mIDIEventsToGUI is true if all MIDI events should be sent to the GUI,
|
||||
// for inputting notes to tracks. If false, they should be sent to the
|
||||
// player instead.
|
||||
mIDIEventsToGUI atomic.Bool
|
||||
FinishedGUI chan struct{}
|
||||
FinishedDetector chan struct{}
|
||||
FinishedSpecAn chan struct{}
|
||||
FinishedMIDIRouter chan struct{}
|
||||
|
||||
bufferPool sync.Pool
|
||||
spectrumPool sync.Pool
|
||||
@ -114,29 +111,25 @@ 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),
|
||||
ToSpecAn: make(chan MsgToSpecAn, 1024),
|
||||
CloseDetector: make(chan struct{}, 1),
|
||||
CloseGUI: make(chan struct{}, 1),
|
||||
CloseSpecAn: make(chan struct{}, 1),
|
||||
FinishedGUI: make(chan struct{}),
|
||||
FinishedDetector: make(chan struct{}),
|
||||
FinishedSpecAn: 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),
|
||||
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{} }},
|
||||
}
|
||||
}
|
||||
|
||||
func (b *Broker) MIDIChannel() chan<- any {
|
||||
if b.mIDIEventsToGUI.Load() {
|
||||
return b.ToGUI
|
||||
}
|
||||
return b.ToPlayer
|
||||
}
|
||||
|
||||
// GetAudioBuffer returns an audio buffer from the buffer pool. The buffer is
|
||||
// guaranteed to be empty. After using the buffer, it should be returned to the
|
||||
// pool with PutAudioBuffer.
|
||||
|
||||
@ -287,6 +287,12 @@ func (t *Tracker) KeyEvent(e key.Event, gtx C) {
|
||||
t.MIDI().Refresh().Do()
|
||||
case "ToggleMIDIInputtingNotes":
|
||||
t.MIDI().InputtingNotes().Toggle()
|
||||
case "ToggleMIDIBinding":
|
||||
t.MIDI().Binding().Toggle()
|
||||
case "MIDIUnbind":
|
||||
t.MIDI().Unbind().Do()
|
||||
case "MIDIUnbindAll":
|
||||
t.MIDI().UnbindAll().Do()
|
||||
default:
|
||||
if len(action) > 4 && action[:4] == "Note" {
|
||||
val, err := strconv.Atoi(string(action[4:]))
|
||||
|
||||
@ -28,6 +28,9 @@
|
||||
- { key: "E", shortcut: true, action: "InstrEnlargedToggle" }
|
||||
- { key: "K", shortcut: true, action: "LinkInstrTrackToggle" }
|
||||
- { key: "W", shortcut: true, action: "Quit" }
|
||||
- { key: "B", shortcut: true, action: "ToggleMIDIBinding" }
|
||||
- { key: "U", shortcut: true, action: "MIDIUnbind" }
|
||||
- { key: "U", shortcut: true, shift: true, action: "MIDIUnbindAll" }
|
||||
- { key: "Space", action: "PlayingToggleUnfollow" }
|
||||
- { key: "Space", shift: true, action: "PlayingToggleFollow" }
|
||||
- { key: "F1", action: "OrderEditorFocus" }
|
||||
|
||||
@ -32,7 +32,7 @@ func (t *Keyboard[T]) Press(key T, ev tracker.NoteEvent) {
|
||||
ev.Source = t // set the source to this keyboard
|
||||
ev.On = true
|
||||
ev.Timestamp = t.now()
|
||||
if tracker.TrySend(t.broker.ToPlayer, any(ev)) {
|
||||
if tracker.TrySend(t.broker.ToPlayer, any(&ev)) {
|
||||
t.pressed[key] = ev
|
||||
}
|
||||
}
|
||||
@ -42,7 +42,7 @@ func (t *Keyboard[T]) Release(key T) {
|
||||
if ev, ok := t.pressed[key]; ok {
|
||||
ev.Timestamp = t.now()
|
||||
ev.On = false // the pressed contains the event we need to send to release the note
|
||||
tracker.TrySend(t.broker.ToPlayer, any(ev))
|
||||
tracker.TrySend(t.broker.ToPlayer, any(&ev))
|
||||
delete(t.pressed, key)
|
||||
}
|
||||
}
|
||||
|
||||
@ -144,7 +144,7 @@ func (te *NoteEditor) Layout(gtx layout.Context) layout.Dimensions {
|
||||
}
|
||||
copy(t.noteEvents, t.noteEvents[1:])
|
||||
t.noteEvents = t.noteEvents[:len(t.noteEvents)-1]
|
||||
tracker.TrySend(t.Broker().ToPlayer, any(ev))
|
||||
tracker.TrySend(t.Broker().ToPlayer, any(&ev))
|
||||
}
|
||||
|
||||
defer clip.Rect(image.Rect(0, 0, gtx.Constraints.Max.X, gtx.Constraints.Max.Y)).Push(gtx.Ops).Pop()
|
||||
|
||||
@ -508,9 +508,13 @@ func (t *MenuBar) Layout(gtx C) D {
|
||||
midiBtn := MenuBtn(&t.MenuStates[2], &t.Clickables[2], "MIDI")
|
||||
midiFC := layout.Rigid(func(gtx C) D {
|
||||
return midiBtn.Layout(gtx,
|
||||
ActionMenuChild(tr.MIDI().Refresh(), "Refresh port list", keyActionMap["MIDIRefresh"], icons.NavigationRefresh),
|
||||
BoolMenuChild(tr.MIDI().InputtingNotes(), "Use for note input", keyActionMap["ToggleMIDIInputtingNotes"], icons.NavigationCheck),
|
||||
BoolMenuChild(tr.MIDI().Binding(), "Bind to controller", keyActionMap["ToggleMIDIBinding"], icons.NavigationCheck),
|
||||
ActionMenuChild(tr.MIDI().Unbind(), "Unbind", keyActionMap["MIDIUnbind"], icons.ImageLeakRemove),
|
||||
ActionMenuChild(tr.MIDI().UnbindAll(), "Unbind all", keyActionMap["MIDIUnbindAll"], icons.ImageLeakRemove),
|
||||
DividerMenuChild(),
|
||||
BoolMenuChild(tr.MIDI().InputtingNotes(), "Input notes", keyActionMap["ToggleMIDIInputtingNotes"], icons.NavigationCheck),
|
||||
DividerMenuChild(),
|
||||
ActionMenuChild(tr.MIDI().Refresh(), "Refresh", keyActionMap["MIDIRefresh"], icons.NavigationRefresh),
|
||||
IntMenuChild(tr.MIDI().Input(), icons.NavigationCheck),
|
||||
)
|
||||
})
|
||||
|
||||
@ -156,7 +156,7 @@ menu:
|
||||
shortcut: { textsize: 16, color: *mediumemphasis, shadowcolor: *black }
|
||||
hover: { r: 100, g: 140, b: 255, a: 48 }
|
||||
disabled: *disabled
|
||||
width: 200
|
||||
width: 240
|
||||
height: 300
|
||||
preset:
|
||||
text: { textsize: 16, color: *highemphasis, shadowcolor: *black }
|
||||
|
||||
@ -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.NoteEvent:
|
||||
t.noteEvents = append(t.noteEvents, *e)
|
||||
case tracker.MsgToGUI:
|
||||
switch e.Kind {
|
||||
case tracker.GUIMessageCenterOnRow:
|
||||
@ -274,7 +274,7 @@ func (t *Tracker) Layout(gtx layout.Context) {
|
||||
ev.Source = t
|
||||
copy(t.noteEvents, t.noteEvents[1:])
|
||||
t.noteEvents = t.noteEvents[:len(t.noteEvents)-1]
|
||||
tracker.TrySend(t.Broker().ToPlayer, any(ev))
|
||||
tracker.TrySend(t.Broker().ToPlayer, any(&ev))
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@ -79,12 +79,15 @@ func (m RTMIDIInputDevice) Open() error {
|
||||
}
|
||||
|
||||
func (m *RTMIDIInputDevice) handleMessage(msg midi.Message, timestampms int32) {
|
||||
var channel, key, velocity uint8
|
||||
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.MIDIChannel(), any(ev))
|
||||
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.MIDIChannel(), any(ev))
|
||||
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))
|
||||
}
|
||||
}
|
||||
|
||||
@ -57,6 +57,7 @@ func (m *historyRedo) Do() {
|
||||
func (m *HistoryModel) MarshalRecovery() []byte {
|
||||
out, err := json.Marshal(m.d)
|
||||
if err != nil {
|
||||
(*Model)(m).Alerts().Add(fmt.Sprintf("Could not marshal recovery data: %s", err.Error()), Error)
|
||||
return nil
|
||||
}
|
||||
if m.d.RecoveryFilePath != "" {
|
||||
|
||||
225
tracker/midi.go
225
tracker/midi.go
@ -1,6 +1,7 @@
|
||||
package tracker
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
)
|
||||
|
||||
@ -10,6 +11,9 @@ func (m *Model) MIDI() *MIDIModel { return (*MIDIModel)(m) }
|
||||
|
||||
type (
|
||||
midiState struct {
|
||||
noteEventsToGui bool
|
||||
binding bool
|
||||
|
||||
currentInput MIDIInputDevice
|
||||
context MIDIContext
|
||||
inputs []MIDIInputDevice
|
||||
@ -127,8 +131,225 @@ func (m *MIDIModel) InputtingNotes() Bool { return MakeBool((*midiInputtingNotes
|
||||
|
||||
type midiInputtingNotes Model
|
||||
|
||||
func (m *midiInputtingNotes) Value() bool { return m.broker.mIDIEventsToGUI.Load() }
|
||||
func (m *midiInputtingNotes) SetValue(val bool) { m.broker.mIDIEventsToGUI.Store(val) }
|
||||
func (m *midiInputtingNotes) Value() bool { return m.midi.noteEventsToGui }
|
||||
func (m *midiInputtingNotes) SetValue(val bool) {
|
||||
m.midi.noteEventsToGui = val
|
||||
TrySend(m.broker.ToMIDIRouter, any(setNoteEventsToGUI(val)))
|
||||
}
|
||||
|
||||
type setNoteEventsToGUI bool
|
||||
|
||||
func runMIDIRouter(broker *Broker) {
|
||||
noteEventsToGUI := 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})
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Binding returns a Bool controlling whether the next received MIDI controller
|
||||
// event is used to bind a parameter.
|
||||
func (m *MIDIModel) Binding() Bool { return MakeBool((*midiBinding)(m)) }
|
||||
|
||||
type midiBinding MIDIModel
|
||||
|
||||
func (m *midiBinding) Value() bool { return m.midi.binding }
|
||||
func (m *midiBinding) SetValue(val bool) {
|
||||
m.midi.binding = val
|
||||
if val {
|
||||
(*Model)(m).Alerts().Add("Move a MIDI controller to bind it to the selected parameter", Info)
|
||||
}
|
||||
}
|
||||
|
||||
// Unbind removes the MIDI binding for the currently selected parameter.
|
||||
func (m *MIDIModel) Unbind() Action { return MakeAction((*midiUnbind)(m)) }
|
||||
|
||||
type midiUnbind MIDIModel
|
||||
|
||||
func (m *midiUnbind) Enabled() bool {
|
||||
p, ok := (*MIDIModel)(m).selectedParam()
|
||||
if !ok {
|
||||
return false
|
||||
}
|
||||
_, ok = m.d.MIDIBindings.GetControl(p)
|
||||
return ok
|
||||
}
|
||||
func (m *midiUnbind) Do() {
|
||||
p, _ := (*MIDIModel)(m).selectedParam()
|
||||
m.d.MIDIBindings.UnlinkParam(p)
|
||||
(*Model)(m).Alerts().Add("Removed MIDI controller bindings for the selected parameter", Info)
|
||||
}
|
||||
|
||||
// UnbindAll removes all MIDI bindings.
|
||||
func (m *MIDIModel) UnbindAll() Action { return MakeAction((*midiUnbindAll)(m)) }
|
||||
|
||||
type midiUnbindAll MIDIModel
|
||||
|
||||
func (m *midiUnbindAll) Enabled() bool { return len(m.d.MIDIBindings.ControlBindings) > 0 }
|
||||
func (m *midiUnbindAll) Do() {
|
||||
m.d.MIDIBindings = MIDIBindings{}
|
||||
(*Model)(m).Alerts().Add("Removed all MIDI controller bindings", Info)
|
||||
}
|
||||
|
||||
func (m *MIDIModel) selectedParam() (MIDIParam, bool) {
|
||||
point := (*Model)(m).Params().Table().Cursor()
|
||||
item := (*Model)(m).Params().Item(point)
|
||||
if _, ok := item.vtable.(*namedParameter); !ok {
|
||||
return MIDIParam{}, false
|
||||
}
|
||||
r := item.Range()
|
||||
value := MIDIParam{
|
||||
Id: item.unit.ID,
|
||||
Param: item.up.Name,
|
||||
Min: r.Min,
|
||||
Max: r.Max,
|
||||
}
|
||||
return value, true
|
||||
}
|
||||
|
||||
func (m *MIDIModel) handleControlEvent(e ControlChange) {
|
||||
key := MIDIControl{Channel: e.Channel, Control: e.Control}
|
||||
if m.midi.binding {
|
||||
m.midi.binding = false
|
||||
value, ok := m.selectedParam()
|
||||
if !ok {
|
||||
(*Model)(m).Alerts().Add("Cannot bind MIDI controller to this parameter type", Warning)
|
||||
return
|
||||
}
|
||||
m.d.MIDIBindings.Link(key, value)
|
||||
(*Model)(m).Alerts().Add(fmt.Sprintf("Bound MIDI CC %d on channel %d to %s", key.Control, key.Channel+1, value.Param), Info)
|
||||
}
|
||||
t, ok := m.d.MIDIBindings.GetParam(key)
|
||||
if !ok {
|
||||
return
|
||||
}
|
||||
i, u, err := m.d.Song.Patch.FindUnit(t.Id)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
newVal := (e.Value*(t.Max-t.Min)+63)/127 + t.Min
|
||||
if m.d.Song.Patch[i].Units[u].Parameters[t.Param] == newVal {
|
||||
return
|
||||
}
|
||||
defer (*Model)(m).change("MIDIControlChange", PatchChange, MinorChange)()
|
||||
m.d.Song.Patch[i].Units[u].Parameters[t.Param] = newVal
|
||||
}
|
||||
|
||||
type (
|
||||
// Two-way map between MIDI controls and parameters that makes sure only one control channel is linked to only one parameter and vice versa.
|
||||
MIDIBindings struct {
|
||||
ControlBindings map[MIDIControl]MIDIParam
|
||||
ParamBindings map[MIDIParam]MIDIControl
|
||||
}
|
||||
|
||||
MIDIParam struct {
|
||||
Id int
|
||||
Param string
|
||||
Min, Max int
|
||||
}
|
||||
|
||||
MIDIControl struct{ Channel, Control int }
|
||||
|
||||
midiControlParam struct {
|
||||
Control MIDIControl
|
||||
Param MIDIParam
|
||||
}
|
||||
)
|
||||
|
||||
// marshal as slice of bindings cause json doesn't support marshaling maps with
|
||||
// struct keys
|
||||
func (t *MIDIBindings) UnmarshalJSON(data []byte) error {
|
||||
var bindings []midiControlParam
|
||||
err := json.Unmarshal(data, &bindings)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
for _, b := range bindings {
|
||||
t.Link(b.Control, b.Param)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (t MIDIBindings) MarshalJSON() ([]byte, error) {
|
||||
var bindings []midiControlParam
|
||||
for k, v := range t.ControlBindings {
|
||||
bindings = append(bindings, midiControlParam{Control: k, Param: v})
|
||||
}
|
||||
return json.Marshal(bindings)
|
||||
}
|
||||
|
||||
func (t *MIDIBindings) GetParam(m MIDIControl) (MIDIParam, bool) {
|
||||
if t.ControlBindings == nil {
|
||||
return MIDIParam{}, false
|
||||
}
|
||||
p, ok := t.ControlBindings[m]
|
||||
return p, ok
|
||||
}
|
||||
|
||||
func (t *MIDIBindings) GetControl(p MIDIParam) (MIDIControl, bool) {
|
||||
if t.ParamBindings == nil {
|
||||
return MIDIControl{}, false
|
||||
}
|
||||
c, ok := t.ParamBindings[p]
|
||||
return c, ok
|
||||
}
|
||||
|
||||
func (t *MIDIBindings) Link(m MIDIControl, p MIDIParam) {
|
||||
if t.ControlBindings == nil {
|
||||
t.ControlBindings = make(map[MIDIControl]MIDIParam)
|
||||
}
|
||||
if t.ParamBindings == nil {
|
||||
t.ParamBindings = make(map[MIDIParam]MIDIControl)
|
||||
}
|
||||
if p, ok := t.ControlBindings[m]; ok {
|
||||
delete(t.ParamBindings, p)
|
||||
}
|
||||
if m, ok := t.ParamBindings[p]; ok {
|
||||
delete(t.ControlBindings, m)
|
||||
}
|
||||
t.ControlBindings[m] = p
|
||||
t.ParamBindings[p] = m
|
||||
}
|
||||
|
||||
func (t *MIDIBindings) UnlinkParam(p MIDIParam) {
|
||||
if t.ParamBindings == nil {
|
||||
return
|
||||
}
|
||||
if c, ok := t.ParamBindings[p]; ok {
|
||||
delete(t.ParamBindings, p)
|
||||
delete(t.ControlBindings, c)
|
||||
}
|
||||
}
|
||||
|
||||
func (t *MIDIBindings) Copy() MIDIBindings {
|
||||
ret := MIDIBindings{
|
||||
ControlBindings: make(map[MIDIControl]MIDIParam, len(t.ControlBindings)),
|
||||
ParamBindings: make(map[MIDIParam]MIDIControl, len(t.ParamBindings)),
|
||||
}
|
||||
for k, v := range t.ControlBindings {
|
||||
ret.ControlBindings[k] = v
|
||||
}
|
||||
for k, v := range t.ParamBindings {
|
||||
ret.ParamBindings[k] = v
|
||||
}
|
||||
return ret
|
||||
}
|
||||
|
||||
// NullMIDIContext is a mockup MIDIContext if you don't want to create a real
|
||||
// one.
|
||||
|
||||
@ -36,6 +36,7 @@ type (
|
||||
SendSource int
|
||||
InstrumentTab InstrumentTab
|
||||
PresetSearchString string
|
||||
MIDIBindings MIDIBindings
|
||||
}
|
||||
|
||||
Model struct {
|
||||
@ -202,14 +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)
|
||||
return m
|
||||
}
|
||||
|
||||
func (m *Model) Close() {
|
||||
TrySend(m.broker.CloseDetector, struct{}{})
|
||||
TrySend(m.broker.CloseSpecAn, struct{}{})
|
||||
TrySend(m.broker.CloseMIDIRouter, struct{}{})
|
||||
TimeoutReceive(m.broker.FinishedDetector, 3*time.Second)
|
||||
TimeoutReceive(m.broker.FinishedSpecAn, 3*time.Second)
|
||||
TimeoutReceive(m.broker.FinishedMIDIRouter, 3*time.Second)
|
||||
}
|
||||
|
||||
// RequestQuit asks the tracker to quit, showing a dialog if there are unsaved
|
||||
@ -385,6 +389,8 @@ func (m *Model) ProcessMsg(msg MsgToModel) {
|
||||
case *Spectrum:
|
||||
m.broker.PutSpectrum(m.spectrum)
|
||||
m.spectrum = e
|
||||
case *ControlChange:
|
||||
m.MIDI().handleControlEvent(*e)
|
||||
}
|
||||
}
|
||||
|
||||
@ -393,6 +399,7 @@ func (m *Model) Broker() *Broker { return m.broker }
|
||||
func (d *modelData) Copy() modelData {
|
||||
ret := *d
|
||||
ret.Song = d.Song.Copy()
|
||||
ret.MIDIBindings = d.MIDIBindings.Copy()
|
||||
return ret
|
||||
}
|
||||
|
||||
|
||||
@ -69,6 +69,12 @@ type (
|
||||
|
||||
playerTimestamp int64 // the timestamp of the event, adjusted to the player's clock, used to sort events
|
||||
}
|
||||
|
||||
ControlChange struct {
|
||||
Channel int
|
||||
Control int
|
||||
Value int
|
||||
}
|
||||
)
|
||||
|
||||
type (
|
||||
@ -274,8 +280,8 @@ loop:
|
||||
}
|
||||
}
|
||||
TrySend(p.broker.ToModel, MsgToModel{Reset: true})
|
||||
case NoteEvent:
|
||||
p.events = append(p.events, m)
|
||||
case *NoteEvent:
|
||||
p.events = append(p.events, *m)
|
||||
case RecordingMsg:
|
||||
if m.bool {
|
||||
p.recording = Recording{State: RecordingWaitingForNote}
|
||||
|
||||
Reference in New Issue
Block a user