mirror of
https://github.com/vsariola/sointu.git
synced 2026-04-03 12:43:01 -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]
|
||||
### 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
|
||||
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
|
||||
@ -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
|
||||
[i121]: https://github.com/vsariola/sointu/issues/121
|
||||
[i122]: https://github.com/vsariola/sointu/issues/122
|
||||
[i124]: https://github.com/vsariola/sointu/issues/124
|
||||
[i125]: https://github.com/vsariola/sointu/issues/125
|
||||
[i128]: https://github.com/vsariola/sointu/issues/128
|
||||
[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
|
||||
[i210]: https://github.com/vsariola/sointu/issues/210
|
||||
[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
|
||||
// 1 is done so that the default value 0 means bit mask 0b0001 i.e. only
|
||||
// thread 1 is rendering the instrument.
|
||||
ThreadMaskM1 int `yaml:",omitempty"`
|
||||
ThreadMaskM1 int `yaml:",omitempty"`
|
||||
MIDI MIDI `yaml:",flow,omitempty"`
|
||||
Units []Unit
|
||||
}
|
||||
|
||||
@ -64,6 +65,16 @@ type (
|
||||
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
|
||||
|
||||
// UnitParameter documents one parameter that an unit takes
|
||||
|
||||
@ -2,6 +2,7 @@ package tracker
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"strconv"
|
||||
"time"
|
||||
|
||||
"github.com/vsariola/sointu"
|
||||
@ -45,6 +46,7 @@ type (
|
||||
railWidth int
|
||||
params [][]Parameter
|
||||
paramsWidth int
|
||||
title string
|
||||
}
|
||||
|
||||
derivedTrack struct {
|
||||
@ -72,6 +74,25 @@ func (m *Model) updateDeriveData(changeType ChangeType) {
|
||||
m.updateParams()
|
||||
m.updateRails()
|
||||
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
|
||||
splitInstrumentBtn *Clickable
|
||||
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(),
|
||||
splitInstrumentBtn: 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.unsoloHint = makeHint("Unsolo", " (%s)", "SoloToggle")
|
||||
@ -81,8 +96,9 @@ func (ip *InstrumentProperties) layout(gtx C) D {
|
||||
layout.Rigid(thread4btn.Layout),
|
||||
)
|
||||
}
|
||||
|
||||
return ip.list.Layout(gtx, 11, func(gtx C, index int) D {
|
||||
gtx.Constraints.Max.X = min(gtx.Dp(300), gtx.Constraints.Max.X)
|
||||
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 {
|
||||
case 0:
|
||||
return layoutInstrumentPropertyLine(gtx, "Name", func(gtx C) D {
|
||||
@ -93,12 +109,42 @@ func (ip *InstrumentProperties) layout(gtx C) D {
|
||||
case 4:
|
||||
muteBtn := ToggleIconBtn(tr.Instrument().Mute(), tr.Theme, ip.muteBtn, icons.ToggleCheckBoxOutlineBlank, icons.ToggleCheckBox, ip.muteHint, ip.unmuteHint)
|
||||
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)
|
||||
return layoutInstrumentPropertyLine(gtx, "Solo", soloBtn.Layout)
|
||||
case 8:
|
||||
case 7:
|
||||
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:
|
||||
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 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 {
|
||||
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)
|
||||
return layout.Flex{Axis: layout.Horizontal, Alignment: layout.Middle}.Layout(gtx,
|
||||
layout.Rigid(layout.Spacer{Width: 6, Height: 36}.Layout),
|
||||
|
||||
@ -5,7 +5,6 @@ import (
|
||||
"image"
|
||||
"image/color"
|
||||
"io"
|
||||
"strconv"
|
||||
|
||||
"gioui.org/io/clipboard"
|
||||
"gioui.org/io/event"
|
||||
@ -251,9 +250,9 @@ func (il *InstrumentList) Layout(gtx C) D {
|
||||
gtx.Constraints.Max.Y = gtx.Dp(36)
|
||||
gtx.Constraints.Min.Y = gtx.Dp(36)
|
||||
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 {
|
||||
name, level, mute, ok := t.Instrument().Item(i)
|
||||
if !ok {
|
||||
labelStyle := Label(t.Theme, &t.Theme.InstrumentEditor.InstrumentList.Number, "")
|
||||
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.
|
||||
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) {
|
||||
return "", 0, false, false
|
||||
return "", "", 0, false, false
|
||||
}
|
||||
name = v.d.Song.Patch[i].Name
|
||||
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
|
||||
return
|
||||
}
|
||||
@ -478,6 +481,7 @@ success:
|
||||
}
|
||||
instrument.NumVoices = clamp(instrument.NumVoices, 1, 32-numVoices)
|
||||
(*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
|
||||
return true
|
||||
}
|
||||
|
||||
235
tracker/midi.go
235
tracker/midi.go
@ -3,6 +3,8 @@ package tracker
|
||||
import (
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
|
||||
"github.com/vsariola/sointu"
|
||||
)
|
||||
|
||||
type MIDIModel Model
|
||||
@ -144,6 +146,239 @@ func (m *midiInputtingNotes) SetValue(val bool) {
|
||||
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
|
||||
// host.
|
||||
type MIDIMessage struct {
|
||||
|
||||
@ -90,7 +90,8 @@ type (
|
||||
|
||||
broker *Broker
|
||||
|
||||
midi midiState
|
||||
midi midiState
|
||||
midiAssign midiAssigns
|
||||
|
||||
presetData presetData
|
||||
}
|
||||
@ -177,6 +178,7 @@ func NewModel(broker *Broker, synthers []sointu.Synther, midiContext MIDIContext
|
||||
m := new(Model)
|
||||
m.synthers = synthers
|
||||
m.midi = midiState{context: midiContext}
|
||||
m.midiAssign = midiAssigns{ctoi: map[midiAssignKey][]midiAssignRange{}}
|
||||
m.broker = broker
|
||||
m.d.Octave = 4
|
||||
m.linkInstrTrack = true
|
||||
|
||||
@ -30,7 +30,9 @@ type (
|
||||
frameDeltas map[any]int64 // Player.frame (approx.)= event.Timestamp + frameDeltas[event.Source]
|
||||
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
|
||||
|
||||
@ -64,7 +66,7 @@ type (
|
||||
NoteEvent struct {
|
||||
Timestamp int64 // in frames, relative to whatever clock the source is using
|
||||
On bool
|
||||
Channel int
|
||||
Channel int // which track or instrument is triggered, depending on IsTrack
|
||||
Note byte
|
||||
IsTrack bool // true if "Channel" means track number, false if it means instrument number
|
||||
Source any
|
||||
@ -90,6 +92,7 @@ func NewPlayer(broker *Broker, synther sointu.Synther) *Player {
|
||||
broker: broker,
|
||||
synther: synther,
|
||||
frameDeltas: make(map[any]int64),
|
||||
midiAssigns: midiAssigns{ctoi: map[midiAssignKey][]midiAssignRange{}},
|
||||
}
|
||||
}
|
||||
|
||||
@ -183,6 +186,7 @@ func (p *Player) destroySynth() {
|
||||
p.synth.Close()
|
||||
p.synth = nil
|
||||
}
|
||||
p.prevVal = p.prevVal[:0]
|
||||
}
|
||||
|
||||
func (p *Player) advanceRow() {
|
||||
@ -281,16 +285,31 @@ loop:
|
||||
case *NoteEvent:
|
||||
p.events = append(p.events, *m)
|
||||
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 {
|
||||
p.events = append(p.events, NoteEvent{
|
||||
Timestamp: m.Timestamp,
|
||||
Channel: int(m.Data[0] & 0x0F),
|
||||
Note: m.Data[1],
|
||||
On: m.Data[0] >= 0x90,
|
||||
Source: m.Source})
|
||||
chn := int(m.Data[0]&0x0F) + 1
|
||||
note := m.Data[1]
|
||||
velocity := m.Data[2]
|
||||
on := m.Data[0] >= 0x90
|
||||
cb := func(i int, v byte) {
|
||||
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:
|
||||
p.midiRouter = m
|
||||
@ -401,6 +420,7 @@ func (p *Player) compileOrUpdateSynth() {
|
||||
}
|
||||
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
|
||||
|
||||
@ -167,6 +167,7 @@ func (m *presetResultList) SetSelected(i int) {
|
||||
newInstr := m.presetData.cache.results[i].instr.Copy()
|
||||
newInstr.NumVoices = clamp(m.d.Song.Patch[m.d.InstrIndex].NumVoices, 1, vm.MAX_VOICES)
|
||||
(*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
|
||||
}
|
||||
|
||||
|
||||
@ -175,7 +175,7 @@ func (recording *Recording) Score(patch sointu.Patch, rowsPerBeat, rowsPerPatter
|
||||
order[k] = len(patterns)
|
||||
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)
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user