mirror of
https://github.com/vsariola/sointu.git
synced 2026-07-13 14:50:55 -04:00
feat: MIDI velocity, keyboard splits, and fixing instrument channel
Closes #124 Closes #215 Closes #221
This commit is contained in:
parent
0179b24fd4
commit
b349474c4d
@@ -5,6 +5,10 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/).
|
|||||||
|
|
||||||
## [Unreleased]
|
## [Unreleased]
|
||||||
### Added
|
### Added
|
||||||
|
- MIDI velocity, keyboard splitting, forcing specific instrument to use
|
||||||
|
particular MIDI channel, and ability to transpose the incoming note values.
|
||||||
|
These settings can be configured under instrument properties. ([#124][i124],
|
||||||
|
[#215][i215], [#221][i221])
|
||||||
- Ability to bind MIDI controllers to specific parameters. The MIDI menu has the
|
- 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,
|
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
|
Sointu waits for the next MIDI Control Change event and binds the currently
|
||||||
@@ -378,6 +382,7 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/).
|
|||||||
[i120]: https://github.com/vsariola/sointu/issues/120
|
[i120]: https://github.com/vsariola/sointu/issues/120
|
||||||
[i121]: https://github.com/vsariola/sointu/issues/121
|
[i121]: https://github.com/vsariola/sointu/issues/121
|
||||||
[i122]: https://github.com/vsariola/sointu/issues/122
|
[i122]: https://github.com/vsariola/sointu/issues/122
|
||||||
|
[i124]: https://github.com/vsariola/sointu/issues/124
|
||||||
[i125]: https://github.com/vsariola/sointu/issues/125
|
[i125]: https://github.com/vsariola/sointu/issues/125
|
||||||
[i128]: https://github.com/vsariola/sointu/issues/128
|
[i128]: https://github.com/vsariola/sointu/issues/128
|
||||||
[i129]: https://github.com/vsariola/sointu/issues/129
|
[i129]: https://github.com/vsariola/sointu/issues/129
|
||||||
@@ -417,3 +422,5 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/).
|
|||||||
[i200]: https://github.com/vsariola/sointu/issues/200
|
[i200]: https://github.com/vsariola/sointu/issues/200
|
||||||
[i210]: https://github.com/vsariola/sointu/issues/210
|
[i210]: https://github.com/vsariola/sointu/issues/210
|
||||||
[i211]: https://github.com/vsariola/sointu/issues/211
|
[i211]: https://github.com/vsariola/sointu/issues/211
|
||||||
|
[i215]: https://github.com/vsariola/sointu/issues/215
|
||||||
|
[i221]: https://github.com/vsariola/sointu/issues/221
|
||||||
|
|||||||
13
patch.go
13
patch.go
@@ -24,7 +24,8 @@ type (
|
|||||||
// ThreadMaskM1 is a bit mask of which threads are used, minus 1. Minus
|
// ThreadMaskM1 is a bit mask of which threads are used, minus 1. Minus
|
||||||
// 1 is done so that the default value 0 means bit mask 0b0001 i.e. only
|
// 1 is done so that the default value 0 means bit mask 0b0001 i.e. only
|
||||||
// thread 1 is rendering the instrument.
|
// thread 1 is rendering the instrument.
|
||||||
ThreadMaskM1 int `yaml:",omitempty"`
|
ThreadMaskM1 int `yaml:",omitempty"`
|
||||||
|
MIDI MIDI `yaml:",flow,omitempty"`
|
||||||
Units []Unit
|
Units []Unit
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -64,6 +65,16 @@ type (
|
|||||||
Comment string `yaml:",omitempty"`
|
Comment string `yaml:",omitempty"`
|
||||||
}
|
}
|
||||||
|
|
||||||
|
MIDI struct { // contains info on how MIDI events should trigger this instrument; if empty, then the instrument is not triggered by MIDI events
|
||||||
|
Channel int `yaml:",omitempty"` // 0 means automatically assigned channel, 1-16 means MIDI channel 1-16
|
||||||
|
Start int `yaml:",omitempty"` // MIDI note number to start on, 0-127
|
||||||
|
End int `yaml:",omitempty"` // MIDI note number to end on, counted backwards from 127, done so that the default number of 0 corresponds to "full keyboard", without any splittings
|
||||||
|
Transpose int `yaml:",omitempty"` // value to be added to the MIDI note/velocity number, can be negative
|
||||||
|
Velocity bool `yaml:",omitempty"` // is this instrument triggered by midi event velocity or note
|
||||||
|
NoRetrigger bool `yaml:",omitempty"` // if true, then this instrument does not retrigger if two consecutive events have the same value
|
||||||
|
IgnoreNoteOff bool `yaml:",omitempty"` // if true, then this instrument should ignore note off events, i.e. notes never release
|
||||||
|
}
|
||||||
|
|
||||||
ParamMap map[string]int
|
ParamMap map[string]int
|
||||||
|
|
||||||
// UnitParameter documents one parameter that an unit takes
|
// UnitParameter documents one parameter that an unit takes
|
||||||
|
|||||||
@@ -2,6 +2,7 @@ package tracker
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"fmt"
|
"fmt"
|
||||||
|
"strconv"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
"github.com/vsariola/sointu"
|
"github.com/vsariola/sointu"
|
||||||
@@ -45,6 +46,7 @@ type (
|
|||||||
railWidth int
|
railWidth int
|
||||||
params [][]Parameter
|
params [][]Parameter
|
||||||
paramsWidth int
|
paramsWidth int
|
||||||
|
title string
|
||||||
}
|
}
|
||||||
|
|
||||||
derivedTrack struct {
|
derivedTrack struct {
|
||||||
@@ -72,6 +74,25 @@ func (m *Model) updateDeriveData(changeType ChangeType) {
|
|||||||
m.updateParams()
|
m.updateParams()
|
||||||
m.updateRails()
|
m.updateRails()
|
||||||
m.updateWires()
|
m.updateWires()
|
||||||
|
m.buildInstrumentTitles()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (m *Model) buildInstrumentTitles() {
|
||||||
|
m.midiAssign.update(m.d.Song.Patch)
|
||||||
|
for i, instr := range m.d.Song.Patch {
|
||||||
|
if i >= len(m.midiAssign.itoc) || m.midiAssign.itoc[i] == 0 {
|
||||||
|
m.derived.patch[i].title = "---"
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
t := strconv.Itoa(m.midiAssign.itoc[i])
|
||||||
|
if instr.MIDI.Velocity {
|
||||||
|
t = t + " vel"
|
||||||
|
}
|
||||||
|
if instr.MIDI.Start > 0 || instr.MIDI.End > 0 {
|
||||||
|
t = t + fmt.Sprintf(" [%d-%d]", instr.MIDI.Start, 127-instr.MIDI.End)
|
||||||
|
}
|
||||||
|
m.derived.patch[i].title = t
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -27,6 +27,14 @@ type (
|
|||||||
voices *NumericUpDownState
|
voices *NumericUpDownState
|
||||||
splitInstrumentBtn *Clickable
|
splitInstrumentBtn *Clickable
|
||||||
splitInstrumentHint string
|
splitInstrumentHint string
|
||||||
|
|
||||||
|
ignoreNoteOff *Clickable
|
||||||
|
velocity *Clickable
|
||||||
|
change *Clickable
|
||||||
|
noteStart *NumericUpDownState
|
||||||
|
noteEnd *NumericUpDownState
|
||||||
|
transpose *NumericUpDownState
|
||||||
|
midiChannel *NumericUpDownState
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -40,6 +48,13 @@ func NewInstrumentProperties() *InstrumentProperties {
|
|||||||
voices: NewNumericUpDownState(),
|
voices: NewNumericUpDownState(),
|
||||||
splitInstrumentBtn: new(Clickable),
|
splitInstrumentBtn: new(Clickable),
|
||||||
threadBtns: [4]*Clickable{new(Clickable), new(Clickable), new(Clickable), new(Clickable)},
|
threadBtns: [4]*Clickable{new(Clickable), new(Clickable), new(Clickable), new(Clickable)},
|
||||||
|
ignoreNoteOff: new(Clickable),
|
||||||
|
velocity: new(Clickable),
|
||||||
|
change: new(Clickable),
|
||||||
|
noteStart: NewNumericUpDownState(),
|
||||||
|
noteEnd: NewNumericUpDownState(),
|
||||||
|
transpose: NewNumericUpDownState(),
|
||||||
|
midiChannel: NewNumericUpDownState(),
|
||||||
}
|
}
|
||||||
ret.soloHint = makeHint("Solo", " (%s)", "SoloToggle")
|
ret.soloHint = makeHint("Solo", " (%s)", "SoloToggle")
|
||||||
ret.unsoloHint = makeHint("Unsolo", " (%s)", "SoloToggle")
|
ret.unsoloHint = makeHint("Unsolo", " (%s)", "SoloToggle")
|
||||||
@@ -81,8 +96,9 @@ func (ip *InstrumentProperties) layout(gtx C) D {
|
|||||||
layout.Rigid(thread4btn.Layout),
|
layout.Rigid(thread4btn.Layout),
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
gtx.Constraints.Max.X = min(gtx.Dp(300), gtx.Constraints.Max.X)
|
||||||
return ip.list.Layout(gtx, 11, func(gtx C, index int) D {
|
gtx.Constraints.Min.X = min(gtx.Constraints.Max.X, gtx.Constraints.Min.X)
|
||||||
|
return ip.list.Layout(gtx, 18, func(gtx C, index int) D {
|
||||||
switch index {
|
switch index {
|
||||||
case 0:
|
case 0:
|
||||||
return layoutInstrumentPropertyLine(gtx, "Name", func(gtx C) D {
|
return layoutInstrumentPropertyLine(gtx, "Name", func(gtx C) D {
|
||||||
@@ -93,12 +109,42 @@ func (ip *InstrumentProperties) layout(gtx C) D {
|
|||||||
case 4:
|
case 4:
|
||||||
muteBtn := ToggleIconBtn(tr.Instrument().Mute(), tr.Theme, ip.muteBtn, icons.ToggleCheckBoxOutlineBlank, icons.ToggleCheckBox, ip.muteHint, ip.unmuteHint)
|
muteBtn := ToggleIconBtn(tr.Instrument().Mute(), tr.Theme, ip.muteBtn, icons.ToggleCheckBoxOutlineBlank, icons.ToggleCheckBox, ip.muteHint, ip.unmuteHint)
|
||||||
return layoutInstrumentPropertyLine(gtx, "Mute", muteBtn.Layout)
|
return layoutInstrumentPropertyLine(gtx, "Mute", muteBtn.Layout)
|
||||||
case 6:
|
case 5:
|
||||||
soloBtn := ToggleIconBtn(tr.Instrument().Solo(), tr.Theme, ip.soloBtn, icons.ToggleCheckBoxOutlineBlank, icons.ToggleCheckBox, ip.soloHint, ip.unsoloHint)
|
soloBtn := ToggleIconBtn(tr.Instrument().Solo(), tr.Theme, ip.soloBtn, icons.ToggleCheckBoxOutlineBlank, icons.ToggleCheckBox, ip.soloHint, ip.unsoloHint)
|
||||||
return layoutInstrumentPropertyLine(gtx, "Solo", soloBtn.Layout)
|
return layoutInstrumentPropertyLine(gtx, "Solo", soloBtn.Layout)
|
||||||
case 8:
|
case 7:
|
||||||
return layoutInstrumentPropertyLine(gtx, "Thread", threadbtnline)
|
return layoutInstrumentPropertyLine(gtx, "Thread", threadbtnline)
|
||||||
|
case 9:
|
||||||
|
l := Label(tr.Theme, &tr.Theme.InstrumentEditor.Properties.Label, "MIDI")
|
||||||
|
l.Alignment = text.Middle
|
||||||
|
return l.Layout(gtx)
|
||||||
case 10:
|
case 10:
|
||||||
|
channelLine := NumUpDown(tr.MIDI().Channel(), tr.Theme, ip.midiChannel, "0 = automatic")
|
||||||
|
return layoutInstrumentPropertyLine(gtx, "Channel", channelLine.Layout)
|
||||||
|
case 11:
|
||||||
|
start := NumUpDown(tr.MIDI().NoteStart(), tr.Theme, ip.noteStart, "Lowest note triggering\nthis instrument")
|
||||||
|
end := NumUpDown(tr.MIDI().NoteEnd(), tr.Theme, ip.noteEnd, "Highest note triggering\nthis instrument")
|
||||||
|
noteRangeLine := func(gtx C) D {
|
||||||
|
return layout.Flex{}.Layout(gtx,
|
||||||
|
layout.Rigid(start.Layout),
|
||||||
|
layout.Rigid(layout.Spacer{Width: 6}.Layout),
|
||||||
|
layout.Rigid(end.Layout),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
return layoutInstrumentPropertyLine(gtx, "Note range", noteRangeLine)
|
||||||
|
case 12:
|
||||||
|
transpose := NumUpDown(tr.MIDI().Transpose(), tr.Theme, ip.transpose, "Transpose of the MIDI values")
|
||||||
|
return layoutInstrumentPropertyLine(gtx, "Transpose", transpose.Layout)
|
||||||
|
case 13:
|
||||||
|
velocityBtn := ToggleIconBtn(tr.MIDI().Velocity(), tr.Theme, ip.velocity, icons.ToggleCheckBoxOutlineBlank, icons.ToggleCheckBox, "Instrument triggered by\nMIDI note", "Instrument triggered by\nMIDI velocity")
|
||||||
|
return layoutInstrumentPropertyLine(gtx, "Velocity", velocityBtn.Layout)
|
||||||
|
case 14:
|
||||||
|
retriggerBtn := ToggleIconBtn(tr.MIDI().Change(), tr.Theme, ip.change, icons.ToggleCheckBoxOutlineBlank, icons.ToggleCheckBox, "Every note/velocity retriggers", "Retrigger only when\nnote/velocity changes")
|
||||||
|
return layoutInstrumentPropertyLine(gtx, "No retrigger", retriggerBtn.Layout)
|
||||||
|
case 15:
|
||||||
|
noteOff := ToggleIconBtn(tr.MIDI().IgnoreNoteOff(), tr.Theme, ip.ignoreNoteOff, icons.ToggleCheckBoxOutlineBlank, icons.ToggleCheckBox, "Notes released", "Notes never released")
|
||||||
|
return layoutInstrumentPropertyLine(gtx, "Ignore note off", noteOff.Layout)
|
||||||
|
case 17:
|
||||||
return layout.UniformInset(unit.Dp(6)).Layout(gtx, func(gtx C) D {
|
return layout.UniformInset(unit.Dp(6)).Layout(gtx, func(gtx C) D {
|
||||||
return ip.commentEditor.Layout(gtx, tr.Instrument().Comment(), tr.Theme, &tr.Theme.InstrumentEditor.InstrumentComment, "Comment")
|
return ip.commentEditor.Layout(gtx, tr.Instrument().Comment(), tr.Theme, &tr.Theme.InstrumentEditor.InstrumentComment, "Comment")
|
||||||
})
|
})
|
||||||
@@ -112,7 +158,6 @@ func (ip *InstrumentProperties) layout(gtx C) D {
|
|||||||
|
|
||||||
func layoutInstrumentPropertyLine(gtx C, text string, content layout.Widget) D {
|
func layoutInstrumentPropertyLine(gtx C, text string, content layout.Widget) D {
|
||||||
tr := TrackerFromContext(gtx)
|
tr := TrackerFromContext(gtx)
|
||||||
gtx.Constraints.Max.X = min(gtx.Dp(300), gtx.Constraints.Max.X)
|
|
||||||
label := Label(tr.Theme, &tr.Theme.InstrumentEditor.Properties.Label, text)
|
label := Label(tr.Theme, &tr.Theme.InstrumentEditor.Properties.Label, text)
|
||||||
return layout.Flex{Axis: layout.Horizontal, Alignment: layout.Middle}.Layout(gtx,
|
return layout.Flex{Axis: layout.Horizontal, Alignment: layout.Middle}.Layout(gtx,
|
||||||
layout.Rigid(layout.Spacer{Width: 6, Height: 36}.Layout),
|
layout.Rigid(layout.Spacer{Width: 6, Height: 36}.Layout),
|
||||||
|
|||||||
@@ -5,7 +5,6 @@ import (
|
|||||||
"image"
|
"image"
|
||||||
"image/color"
|
"image/color"
|
||||||
"io"
|
"io"
|
||||||
"strconv"
|
|
||||||
|
|
||||||
"gioui.org/io/clipboard"
|
"gioui.org/io/clipboard"
|
||||||
"gioui.org/io/event"
|
"gioui.org/io/event"
|
||||||
@@ -251,9 +250,9 @@ func (il *InstrumentList) Layout(gtx C) D {
|
|||||||
gtx.Constraints.Max.Y = gtx.Dp(36)
|
gtx.Constraints.Max.Y = gtx.Dp(36)
|
||||||
gtx.Constraints.Min.Y = gtx.Dp(36)
|
gtx.Constraints.Min.Y = gtx.Dp(36)
|
||||||
element := func(gtx C, i int) D {
|
element := func(gtx C, i int) D {
|
||||||
grabhandle := Label(t.Theme, &t.Theme.InstrumentEditor.InstrumentList.Number, strconv.Itoa(i+1))
|
name, title, level, mute, ok := t.Instrument().Item(i)
|
||||||
|
grabhandle := Label(t.Theme, &t.Theme.InstrumentEditor.InstrumentList.Number, title)
|
||||||
label := func(gtx C) D {
|
label := func(gtx C) D {
|
||||||
name, level, mute, ok := t.Instrument().Item(i)
|
|
||||||
if !ok {
|
if !ok {
|
||||||
labelStyle := Label(t.Theme, &t.Theme.InstrumentEditor.InstrumentList.Number, "")
|
labelStyle := Label(t.Theme, &t.Theme.InstrumentEditor.InstrumentList.Number, "")
|
||||||
return layout.Center.Layout(gtx, labelStyle.Layout)
|
return layout.Center.Layout(gtx, labelStyle.Layout)
|
||||||
|
|||||||
@@ -75,9 +75,9 @@ func (m *splitInstrument) Do() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Item returns information about the instrument at a given index.
|
// Item returns information about the instrument at a given index.
|
||||||
func (v *InstrModel) Item(i int) (name string, maxLevel float32, mute bool, ok bool) {
|
func (v *InstrModel) Item(i int) (name, title string, maxLevel float32, mute bool, ok bool) {
|
||||||
if i < 0 || i >= len(v.d.Song.Patch) {
|
if i < 0 || i >= len(v.d.Song.Patch) {
|
||||||
return "", 0, false, false
|
return "", "", 0, false, false
|
||||||
}
|
}
|
||||||
name = v.d.Song.Patch[i].Name
|
name = v.d.Song.Patch[i].Name
|
||||||
mute = v.d.Song.Patch[i].Mute
|
mute = v.d.Song.Patch[i].Mute
|
||||||
@@ -93,6 +93,9 @@ func (v *InstrModel) Item(i int) (name string, maxLevel float32, mute bool, ok b
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
if i >= 0 && i < len(v.derived.patch) {
|
||||||
|
title = v.derived.patch[i].title
|
||||||
|
}
|
||||||
ok = true
|
ok = true
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
@@ -478,6 +481,7 @@ success:
|
|||||||
}
|
}
|
||||||
instrument.NumVoices = clamp(instrument.NumVoices, 1, 32-numVoices)
|
instrument.NumVoices = clamp(instrument.NumVoices, 1, 32-numVoices)
|
||||||
(*Model)(m).assignUnitIDs(instrument.Units)
|
(*Model)(m).assignUnitIDs(instrument.Units)
|
||||||
|
instrument.MIDI = m.d.Song.Patch[m.d.InstrIndex].MIDI // keep the MIDI config of the current instrument
|
||||||
m.d.Song.Patch[m.d.InstrIndex] = instrument
|
m.d.Song.Patch[m.d.InstrIndex] = instrument
|
||||||
return true
|
return true
|
||||||
}
|
}
|
||||||
|
|||||||
235
tracker/midi.go
235
tracker/midi.go
@@ -3,6 +3,8 @@ package tracker
|
|||||||
import (
|
import (
|
||||||
"encoding/json"
|
"encoding/json"
|
||||||
"fmt"
|
"fmt"
|
||||||
|
|
||||||
|
"github.com/vsariola/sointu"
|
||||||
)
|
)
|
||||||
|
|
||||||
type MIDIModel Model
|
type MIDIModel Model
|
||||||
@@ -144,6 +146,239 @@ func (m *midiInputtingNotes) SetValue(val bool) {
|
|||||||
TrySend(m.broker.ToPlayer, any(m.midi.router))
|
TrySend(m.broker.ToPlayer, any(m.midi.router))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Transpose returns an Int controlling the MIDI transpose value of the
|
||||||
|
// currently selected instrument.
|
||||||
|
func (m *MIDIModel) Transpose() Int { return MakeInt((*midiTranspose)(m)) }
|
||||||
|
|
||||||
|
type midiTranspose MIDIModel
|
||||||
|
|
||||||
|
func (m *midiTranspose) Value() int {
|
||||||
|
i := m.d.InstrIndex
|
||||||
|
if i < 0 || i >= len(m.d.Song.Patch) {
|
||||||
|
return 0
|
||||||
|
}
|
||||||
|
return m.d.Song.Patch[i].MIDI.Transpose
|
||||||
|
}
|
||||||
|
func (m *midiTranspose) SetValue(val int) bool {
|
||||||
|
i := m.d.InstrIndex
|
||||||
|
if i < 0 || i >= len(m.d.Song.Patch) {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
defer (*Model)(m).change("MIDITranspose", PatchChange, MinorChange)()
|
||||||
|
m.d.Song.Patch[i].MIDI.Transpose = val
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
func (m *midiTranspose) Range() RangeInclusive { return RangeInclusive{-127, 127} }
|
||||||
|
|
||||||
|
// NoteStart returns an Int controlling the MIDI note start value of the
|
||||||
|
// currently selected instrument.
|
||||||
|
func (m *MIDIModel) NoteStart() Int { return MakeInt((*midiNoteStart)(m)) }
|
||||||
|
|
||||||
|
type midiNoteStart MIDIModel
|
||||||
|
|
||||||
|
func (m *midiNoteStart) Value() int {
|
||||||
|
i := m.d.InstrIndex
|
||||||
|
if i < 0 || i >= len(m.d.Song.Patch) {
|
||||||
|
return 0
|
||||||
|
}
|
||||||
|
return m.d.Song.Patch[i].MIDI.Start
|
||||||
|
}
|
||||||
|
func (m *midiNoteStart) SetValue(val int) bool {
|
||||||
|
i := m.d.InstrIndex
|
||||||
|
if i < 0 || i >= len(m.d.Song.Patch) {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
defer (*Model)(m).change("MIDINoteStart", PatchChange, MinorChange)()
|
||||||
|
m.d.Song.Patch[i].MIDI.Start = val
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
func (m *midiNoteStart) Range() RangeInclusive { return RangeInclusive{0, 127} }
|
||||||
|
|
||||||
|
// NoteEnd returns an Int controlling the MIDI note end value of the
|
||||||
|
// currently selected instrument.
|
||||||
|
func (m *MIDIModel) NoteEnd() Int { return MakeInt((*midiNoteEnd)(m)) }
|
||||||
|
|
||||||
|
type midiNoteEnd MIDIModel
|
||||||
|
|
||||||
|
func (m *midiNoteEnd) Value() int {
|
||||||
|
i := m.d.InstrIndex
|
||||||
|
if i < 0 || i >= len(m.d.Song.Patch) {
|
||||||
|
return 0
|
||||||
|
}
|
||||||
|
return 127 - m.d.Song.Patch[i].MIDI.End
|
||||||
|
}
|
||||||
|
func (m *midiNoteEnd) SetValue(val int) bool {
|
||||||
|
i := m.d.InstrIndex
|
||||||
|
if i < 0 || i >= len(m.d.Song.Patch) {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
defer (*Model)(m).change("MIDINoteEnd", PatchChange, MinorChange)()
|
||||||
|
m.d.Song.Patch[i].MIDI.End = 127 - val
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
func (m *midiNoteEnd) Range() RangeInclusive { return RangeInclusive{0, 127} }
|
||||||
|
|
||||||
|
// Velocity returns a Bool controlling whether the velocity value from MIDI
|
||||||
|
// event is used instead of the normal note value
|
||||||
|
func (m *MIDIModel) Velocity() Bool { return MakeBool((*midiVelocity)(m)) }
|
||||||
|
|
||||||
|
type midiVelocity MIDIModel
|
||||||
|
|
||||||
|
func (m *midiVelocity) Value() bool {
|
||||||
|
i := m.d.InstrIndex
|
||||||
|
if i < 0 || i >= len(m.d.Song.Patch) {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
return m.d.Song.Patch[i].MIDI.Velocity
|
||||||
|
}
|
||||||
|
func (m *midiVelocity) SetValue(val bool) {
|
||||||
|
i := m.d.InstrIndex
|
||||||
|
if i < 0 || i >= len(m.d.Song.Patch) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
defer (*Model)(m).change("MIDIVelocity", PatchChange, MinorChange)()
|
||||||
|
m.d.Song.Patch[i].MIDI.Velocity = val
|
||||||
|
}
|
||||||
|
|
||||||
|
// Change returns a Bool controlling whether only the change in note or velocity value is used
|
||||||
|
func (m *MIDIModel) Change() Bool { return MakeBool((*midiChange)(m)) }
|
||||||
|
|
||||||
|
type midiChange MIDIModel
|
||||||
|
|
||||||
|
func (m *midiChange) Value() bool {
|
||||||
|
i := m.d.InstrIndex
|
||||||
|
if i < 0 || i >= len(m.d.Song.Patch) {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
return m.d.Song.Patch[i].MIDI.NoRetrigger
|
||||||
|
}
|
||||||
|
func (m *midiChange) SetValue(val bool) {
|
||||||
|
i := m.d.InstrIndex
|
||||||
|
if i < 0 || i >= len(m.d.Song.Patch) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
defer (*Model)(m).change("MIDIChange", PatchChange, MinorChange)()
|
||||||
|
m.d.Song.Patch[i].MIDI.NoRetrigger = val
|
||||||
|
}
|
||||||
|
|
||||||
|
// IgnoreNoteOff returns a Bool controlling whether note off events are ignored
|
||||||
|
func (m *MIDIModel) IgnoreNoteOff() Bool { return MakeBool((*midiIgnoreNoteOff)(m)) }
|
||||||
|
|
||||||
|
type midiIgnoreNoteOff MIDIModel
|
||||||
|
|
||||||
|
func (m *midiIgnoreNoteOff) Value() bool {
|
||||||
|
i := m.d.InstrIndex
|
||||||
|
if i < 0 || i >= len(m.d.Song.Patch) {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
return m.d.Song.Patch[i].MIDI.IgnoreNoteOff
|
||||||
|
}
|
||||||
|
func (m *midiIgnoreNoteOff) SetValue(val bool) {
|
||||||
|
i := m.d.InstrIndex
|
||||||
|
if i < 0 || i >= len(m.d.Song.Patch) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
defer (*Model)(m).change("MIDIIgnoreNoteOff", PatchChange, MinorChange)()
|
||||||
|
m.d.Song.Patch[i].MIDI.IgnoreNoteOff = val
|
||||||
|
}
|
||||||
|
|
||||||
|
// Channel returns an Int controlling the MIDI channel of the currently selected
|
||||||
|
// instrument. 0 = automatically selected, 1-16 fixed to specific MIDI channel
|
||||||
|
func (m *MIDIModel) Channel() Int { return MakeInt((*midiChannel)(m)) }
|
||||||
|
|
||||||
|
type midiChannel MIDIModel
|
||||||
|
|
||||||
|
func (m *midiChannel) Value() int {
|
||||||
|
i := m.d.InstrIndex
|
||||||
|
if i < 0 || i >= len(m.d.Song.Patch) {
|
||||||
|
return 0
|
||||||
|
}
|
||||||
|
return m.d.Song.Patch[i].MIDI.Channel
|
||||||
|
}
|
||||||
|
func (m *midiChannel) SetValue(val int) bool {
|
||||||
|
i := m.d.InstrIndex
|
||||||
|
if i < 0 || i >= len(m.d.Song.Patch) {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
defer (*Model)(m).change("MIDIChannel", PatchChange, MinorChange)()
|
||||||
|
m.d.Song.Patch[i].MIDI.Channel = val
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
func (m *midiChannel) Range() RangeInclusive { return RangeInclusive{0, 16} }
|
||||||
|
|
||||||
|
type (
|
||||||
|
midiAssigns struct {
|
||||||
|
ctoi map[midiAssignKey][]midiAssignRange // map to quickly find which instruments to trigger
|
||||||
|
itoc []int // slice to quickly find which MIDI channel was assigned for a given instrument
|
||||||
|
}
|
||||||
|
midiAssignKey struct {
|
||||||
|
Channel int
|
||||||
|
Velocity bool
|
||||||
|
}
|
||||||
|
midiAssignRange struct {
|
||||||
|
Start, End byte
|
||||||
|
Instr int
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
const MAX_MIDI_CHANNELS = 16
|
||||||
|
|
||||||
|
// update tries to assign MIDI channels to instruments that have MIDI channel 0
|
||||||
|
// (automatic) in a way that minimizes the number of channels used. It also
|
||||||
|
// updates the ctoi and itoc maps for quick lookup when routing MIDI messages.
|
||||||
|
// The algorithm iterates through the instruments and, for those with automatic
|
||||||
|
// channel, it tries to find the lowest MIDI channel that doesn't have an
|
||||||
|
// overlapping note range with any of the already assigned instruments with the
|
||||||
|
// same velocity setting. If it runs out of channels, it leaves the rest of the
|
||||||
|
// instruments unassigned (channel 0).
|
||||||
|
func (a *midiAssigns) update(p sointu.Patch) {
|
||||||
|
for k := range a.ctoi {
|
||||||
|
a.ctoi[k] = a.ctoi[k][:0] // clear all slices, keeping their allocated memory
|
||||||
|
}
|
||||||
|
a.itoc = a.itoc[:0]
|
||||||
|
for i, instr := range p {
|
||||||
|
if instr.MIDI.Channel != 0 {
|
||||||
|
k := midiAssignKey{Channel: instr.MIDI.Channel, Velocity: instr.MIDI.Velocity}
|
||||||
|
v := midiAssignRange{Start: byte(max(instr.MIDI.Start, 0)), End: byte(min(127-instr.MIDI.End, 127)), Instr: i}
|
||||||
|
a.ctoi[k] = append(a.ctoi[k], v)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
k := midiAssignKey{Channel: 1, Velocity: false}
|
||||||
|
outer:
|
||||||
|
for i, e := range p {
|
||||||
|
if e.MIDI.Channel > 0 { // already assigned to a specific channel, so skip automatic assignment
|
||||||
|
a.itoc = append(a.itoc, e.MIDI.Channel)
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
k.Velocity = e.MIDI.Velocity
|
||||||
|
x := midiAssignRange{Start: byte(max(e.MIDI.Start, 0)), End: byte(min(127-e.MIDI.End, 127)), Instr: i}
|
||||||
|
inner:
|
||||||
|
for {
|
||||||
|
for _, y := range a.ctoi[k] {
|
||||||
|
if max(x.Start, y.Start) <= min(x.End, y.End) {
|
||||||
|
if k.Channel >= MAX_MIDI_CHANNELS {
|
||||||
|
break outer // we've ran out of channels, leave the rest unassigned
|
||||||
|
}
|
||||||
|
k.Channel++
|
||||||
|
continue inner // this channel is already taken for the overlapping range, try next channel
|
||||||
|
}
|
||||||
|
}
|
||||||
|
break // this channel had no overlaps with the already assigned ranges, so we can use it
|
||||||
|
}
|
||||||
|
a.ctoi[k] = append(a.ctoi[k], x)
|
||||||
|
a.itoc = append(a.itoc, k.Channel)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (a *midiAssigns) forEach(chn int, vel bool, val byte, cb func(instr int, val byte)) {
|
||||||
|
k := midiAssignKey{Channel: chn, Velocity: vel}
|
||||||
|
for _, r := range a.ctoi[k] {
|
||||||
|
if r.Start <= val && val <= r.End {
|
||||||
|
cb(r.Instr, val)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// MIDIMessage represents a MIDI message received from a MIDI input port or VST
|
// MIDIMessage represents a MIDI message received from a MIDI input port or VST
|
||||||
// host.
|
// host.
|
||||||
type MIDIMessage struct {
|
type MIDIMessage struct {
|
||||||
|
|||||||
@@ -90,7 +90,8 @@ type (
|
|||||||
|
|
||||||
broker *Broker
|
broker *Broker
|
||||||
|
|
||||||
midi midiState
|
midi midiState
|
||||||
|
midiAssign midiAssigns
|
||||||
|
|
||||||
presetData presetData
|
presetData presetData
|
||||||
}
|
}
|
||||||
@@ -177,6 +178,7 @@ func NewModel(broker *Broker, synthers []sointu.Synther, midiContext MIDIContext
|
|||||||
m := new(Model)
|
m := new(Model)
|
||||||
m.synthers = synthers
|
m.synthers = synthers
|
||||||
m.midi = midiState{context: midiContext}
|
m.midi = midiState{context: midiContext}
|
||||||
|
m.midiAssign = midiAssigns{ctoi: map[midiAssignKey][]midiAssignRange{}}
|
||||||
m.broker = broker
|
m.broker = broker
|
||||||
m.d.Octave = 4
|
m.d.Octave = 4
|
||||||
m.linkInstrTrack = true
|
m.linkInstrTrack = true
|
||||||
|
|||||||
@@ -30,7 +30,9 @@ type (
|
|||||||
frameDeltas map[any]int64 // Player.frame (approx.)= event.Timestamp + frameDeltas[event.Source]
|
frameDeltas map[any]int64 // Player.frame (approx.)= event.Timestamp + frameDeltas[event.Source]
|
||||||
events NoteEventList
|
events NoteEventList
|
||||||
|
|
||||||
midiRouter midiRouter
|
midiRouter midiRouter
|
||||||
|
midiAssigns midiAssigns
|
||||||
|
prevVal []byte
|
||||||
|
|
||||||
status PlayerStatus // the part of the Player state that is communicated to the model to visualize what Player is doing
|
status PlayerStatus // the part of the Player state that is communicated to the model to visualize what Player is doing
|
||||||
|
|
||||||
@@ -64,7 +66,7 @@ type (
|
|||||||
NoteEvent struct {
|
NoteEvent struct {
|
||||||
Timestamp int64 // in frames, relative to whatever clock the source is using
|
Timestamp int64 // in frames, relative to whatever clock the source is using
|
||||||
On bool
|
On bool
|
||||||
Channel int
|
Channel int // which track or instrument is triggered, depending on IsTrack
|
||||||
Note byte
|
Note byte
|
||||||
IsTrack bool // true if "Channel" means track number, false if it means instrument number
|
IsTrack bool // true if "Channel" means track number, false if it means instrument number
|
||||||
Source any
|
Source any
|
||||||
@@ -90,6 +92,7 @@ func NewPlayer(broker *Broker, synther sointu.Synther) *Player {
|
|||||||
broker: broker,
|
broker: broker,
|
||||||
synther: synther,
|
synther: synther,
|
||||||
frameDeltas: make(map[any]int64),
|
frameDeltas: make(map[any]int64),
|
||||||
|
midiAssigns: midiAssigns{ctoi: map[midiAssignKey][]midiAssignRange{}},
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -183,6 +186,7 @@ func (p *Player) destroySynth() {
|
|||||||
p.synth.Close()
|
p.synth.Close()
|
||||||
p.synth = nil
|
p.synth = nil
|
||||||
}
|
}
|
||||||
|
p.prevVal = p.prevVal[:0]
|
||||||
}
|
}
|
||||||
|
|
||||||
func (p *Player) advanceRow() {
|
func (p *Player) advanceRow() {
|
||||||
@@ -281,16 +285,31 @@ loop:
|
|||||||
case *NoteEvent:
|
case *NoteEvent:
|
||||||
p.events = append(p.events, *m)
|
p.events = append(p.events, *m)
|
||||||
case *MIDIMessage:
|
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 {
|
if m.Data[0] >= 0x80 && m.Data[0] <= 0x9F {
|
||||||
p.events = append(p.events, NoteEvent{
|
chn := int(m.Data[0]&0x0F) + 1
|
||||||
Timestamp: m.Timestamp,
|
note := m.Data[1]
|
||||||
Channel: int(m.Data[0] & 0x0F),
|
velocity := m.Data[2]
|
||||||
Note: m.Data[1],
|
on := m.Data[0] >= 0x90
|
||||||
On: m.Data[0] >= 0x90,
|
cb := func(i int, v byte) {
|
||||||
Source: m.Source})
|
if i < 0 || i >= len(p.song.Patch) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
instr := p.song.Patch[i]
|
||||||
|
if instr.MIDI.IgnoreNoteOff && !on {
|
||||||
|
return // instruments configured to ignore note offs never release
|
||||||
|
}
|
||||||
|
n := byte(min(max(int(v)+instr.MIDI.Transpose, 2), 255)) // 0 and 1 have special meaning
|
||||||
|
if instr.MIDI.NoRetrigger && on && i < len(p.prevVal) && p.prevVal[i] == n {
|
||||||
|
return // the instrument is configured to respond only to changes in values and there was no change
|
||||||
|
}
|
||||||
|
p.events = append(p.events, NoteEvent{Timestamp: m.Timestamp, Channel: i, Note: n, On: on, Source: m.Source})
|
||||||
|
for len(p.prevVal) <= i {
|
||||||
|
p.prevVal = append(p.prevVal, 0)
|
||||||
|
}
|
||||||
|
p.prevVal[i] = n
|
||||||
|
}
|
||||||
|
p.midiAssigns.forEach(chn, false, note, cb) // trigger instruments that are configured to respond to this midi channel's note events
|
||||||
|
p.midiAssigns.forEach(chn, true, velocity, cb) // trigger instruments that are configured to respond to this midi channel's velocity events
|
||||||
}
|
}
|
||||||
case midiRouter:
|
case midiRouter:
|
||||||
p.midiRouter = m
|
p.midiRouter = m
|
||||||
@@ -401,6 +420,7 @@ func (p *Player) compileOrUpdateSynth() {
|
|||||||
}
|
}
|
||||||
voice += instr.NumVoices
|
voice += instr.NumVoices
|
||||||
}
|
}
|
||||||
|
p.midiAssigns.update(p.song.Patch)
|
||||||
}
|
}
|
||||||
|
|
||||||
// all sendTargets from player are always non-blocking, to ensure that the player thread cannot end up in a dead-lock
|
// all sendTargets from player are always non-blocking, to ensure that the player thread cannot end up in a dead-lock
|
||||||
|
|||||||
@@ -167,6 +167,7 @@ func (m *presetResultList) SetSelected(i int) {
|
|||||||
newInstr := m.presetData.cache.results[i].instr.Copy()
|
newInstr := m.presetData.cache.results[i].instr.Copy()
|
||||||
newInstr.NumVoices = clamp(m.d.Song.Patch[m.d.InstrIndex].NumVoices, 1, vm.MAX_VOICES)
|
newInstr.NumVoices = clamp(m.d.Song.Patch[m.d.InstrIndex].NumVoices, 1, vm.MAX_VOICES)
|
||||||
(*Model)(m).assignUnitIDs(newInstr.Units)
|
(*Model)(m).assignUnitIDs(newInstr.Units)
|
||||||
|
newInstr.MIDI = m.d.Song.Patch[m.d.InstrIndex].MIDI // keep the MIDI config of the current instrument
|
||||||
m.d.Song.Patch[m.d.InstrIndex] = newInstr
|
m.d.Song.Patch[m.d.InstrIndex] = newInstr
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -175,7 +175,7 @@ func (recording *Recording) Score(patch sointu.Patch, rowsPerBeat, rowsPerPatter
|
|||||||
order[k] = len(patterns)
|
order[k] = len(patterns)
|
||||||
patterns = append(patterns, newPat)
|
patterns = append(patterns, newPat)
|
||||||
}
|
}
|
||||||
track := sointu.Track{NumVoices: numVoices, Effect: false, Order: order, Patterns: patterns}
|
track := sointu.Track{NumVoices: numVoices, Effect: patch[i].MIDI.Velocity, Order: order, Patterns: patterns}
|
||||||
songTracks = append(songTracks, track)
|
songTracks = append(songTracks, track)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user