Files
sointu/tracker/midi.go
5684185+vsariola@users.noreply.github.com f2ef57a845 feat(tracker): ability to bind MIDI controllers to parameters
Closes #152
2026-02-01 12:07:00 +02:00

361 lines
9.0 KiB
Go

package tracker
import (
"encoding/json"
"fmt"
)
type MIDIModel Model
func (m *Model) MIDI() *MIDIModel { return (*MIDIModel)(m) }
type (
midiState struct {
noteEventsToGui bool
binding bool
currentInput MIDIInputDevice
context MIDIContext
inputs []MIDIInputDevice
}
MIDIContext interface {
Inputs(yield func(input MIDIInputDevice) bool)
Close()
Support() MIDISupport
}
MIDIInputDevice interface {
Open() error
Close() error
IsOpen() bool
String() string
}
MIDISupport int
)
const (
MIDISupportNotCompiled MIDISupport = iota
MIDISupportNoDriver
MIDISupported
)
// Refresh
func (m *MIDIModel) Refresh() Action { return MakeAction((*midiRefresh)(m)) }
type midiRefresh MIDIModel
func (m *midiRefresh) Do() {
if m.midi.context == nil {
return
}
m.midi.inputs = m.midi.inputs[:0]
for i := range m.midi.context.Inputs {
m.midi.inputs = append(m.midi.inputs, i)
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 {
(*Model)(m).Alerts().Add(fmt.Sprintf("Failed to reopen MIDI input port: %s", err.Error()), Error)
continue
}
m.midi.currentInput = i
}
}
}
// InputDevices can be iterated to get string names of all the MIDI input
// devices.
func (m *MIDIModel) Input() Int { return MakeInt((*midiInputDevices)(m)) }
type midiInputDevices MIDIModel
func (m *midiInputDevices) Value() int {
if m.midi.currentInput == nil {
return 0
}
for i, d := range m.midi.inputs {
if d == m.midi.currentInput {
return i + 1
}
}
return 0
}
func (m *midiInputDevices) SetValue(val int) bool {
if val < 0 || val > len(m.midi.inputs) {
return false
}
if m.midi.currentInput != nil {
if err := m.midi.currentInput.Close(); err != nil {
(*Model)(m).Alerts().Add(fmt.Sprintf("Failed to close current MIDI input port: %s", err.Error()), Error)
}
m.midi.currentInput = nil
}
if val == 0 {
return true
}
newInput := m.midi.inputs[val-1]
if err := newInput.Open(); err != nil {
(*Model)(m).Alerts().Add(fmt.Sprintf("Failed to open MIDI input port: %s", err.Error()), Error)
return false
}
m.midi.currentInput = newInput
(*Model)(m).Alerts().Add(fmt.Sprintf("Opened MIDI input port: %s", newInput.String()), Info)
return true
}
func (m *midiInputDevices) Range() RangeInclusive {
return RangeInclusive{Min: 0, Max: len(m.midi.inputs)}
}
func (m *midiInputDevices) StringOf(value int) string {
if value < 0 || value > len(m.midi.inputs) {
return ""
}
if value == 0 {
switch m.midi.context.Support() {
case MIDISupportNotCompiled:
return "Not compiled"
case MIDISupportNoDriver:
return "No driver"
default:
return "Closed"
}
}
return m.midi.inputs[value-1].String()
}
// InputtingNotes returns a Bool controlling whether the MIDI events are used
// just to trigger instruments, or if the note events are used to input notes to
// the note table.
func (m *MIDIModel) InputtingNotes() Bool { return MakeBool((*midiInputtingNotes)(m)) }
type midiInputtingNotes Model
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.
type NullMIDIContext struct{}
func (m NullMIDIContext) Inputs(yield func(input MIDIInputDevice) bool) {}
func (m NullMIDIContext) Close() {}
func (m NullMIDIContext) Support() MIDISupport { return MIDISupportNotCompiled }