mirror of
https://github.com/vsariola/sointu.git
synced 2026-02-02 13:50:15 -05:00
361 lines
9.0 KiB
Go
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 }
|