Files
sointu/tracker/midi.go
2026-02-03 21:20:01 +02:00

418 lines
11 KiB
Go

package tracker
import (
"encoding/json"
"fmt"
)
type MIDIModel Model
func (m *Model) MIDI() *MIDIModel { return (*MIDIModel)(m) }
type (
midiState struct {
binding bool
router midiRouter
currentInput MIDIInputDevice
context MIDIContext
inputs []MIDIInputDevice
}
MIDIContext interface {
Inputs(yield func(input MIDIInputDevice) bool)
Close()
Support() MIDISupport
}
MIDIInputDevice interface {
Open(func(msg *MIDIMessage)) 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
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
}
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]
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
}
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.router.sendNoteEventsToGUI }
func (m *midiInputtingNotes) SetValue(val bool) {
m.midi.router.sendNoteEventsToGUI = val
TrySend(m.broker.ToMIDIHandler, any(m.midi.router))
TrySend(m.broker.ToPlayer, any(m.midi.router))
}
// 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 (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 v := <-b.ToMIDIHandler:
switch msg := v.(type) {
case *MIDIMessage:
router.route(b, msg)
case midiRouter:
router = msg
}
case <-b.CloseMIDIHandler:
close(b.FinishedMIDIHandler)
return
}
}
}
// 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(channel, control, value int) {
key := MIDIControl{Channel: channel, Control: 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
}
// +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 := (value*(t.Max-t.Min)+62)/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 }