feat(vm)!: implement cross-instrument modulation of all voices

The "auto" was misleading, as it meant self modulation when targetting a unit within instrument itself and just voice 0 when cross-instrument modulation. This feature changes the "auto" meaning "self" for instruments self-modulating, and "all" voices for cross-instrument modulations. "all" is implemented by compiling a single send into multiple repeated sends, with only the last popping the stack (if necessary).

Closes #107
This commit is contained in:
5684185+vsariola@users.noreply.github.com 2023-10-07 14:07:39 +03:00
parent 7ee43f199a
commit 8c8232f76e
3 changed files with 57 additions and 18 deletions

View File

@ -17,6 +17,16 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/).
- The VSTI waits for the gioui actually have quit when closing the
plugin
### Changed
- BREAKING CHANGE: The meaning of default modulation mode ("auto") has
been changed for cross-instrument modulations: it now means "all"
voices, instead of first voice (which was redundant, as it was same as
defining voice = 0). This means that for cross-instrument modulations,
one "all vocies" send gets actually compiled into multiple sends, one
for each targeted voice. For intra-instrument modulations, the meaning
stays the same, but the label was changed to "self", to highlight that
this means the voice modulates only itself and not other voices.
## v0.2.0
### Added
- Saving and loading instruments

View File

@ -177,7 +177,11 @@ func (p Patch) ParamHintString(instrIndex, unitIndex int, param string) string {
return fmt.Sprintf("%.2f", float32(value)/64-1)
case "voice":
if value == 0 {
return "auto"
targetIndex, _, err := p.FindSendTarget(unit.Parameters["target"])
if err == nil && targetIndex != instrIndex {
return "all"
}
return "self"
}
return fmt.Sprintf("%v", value)
case "target":

View File

@ -38,6 +38,9 @@ func Encode(patch sointu.Patch, featureSet FeatureSet, bpm int) (*BytePatch, err
if c.NumVoices > 32 {
return nil, fmt.Errorf("Sointu does not support more than 32 concurrent voices; patch uses %v", c.NumVoices)
}
var values []byte
var commands []byte
var instrCommands []byte
sampleOffsetMap := map[SampleOffset]int{}
globalAddrs := map[int]uint16{}
globalFixups := map[int]([]int){}
@ -48,9 +51,7 @@ func Encode(patch sointu.Patch, featureSet FeatureSet, bpm int) (*BytePatch, err
c.DelayTimes[i] = uint16(delayTable[i])
}
for instrIndex, instr := range patch {
if len(instr.Units) > 63 {
return nil, errors.New("An instrument can have a maximum of 63 units")
}
instrCommands = instrCommands[:0]
if instr.NumVoices < 1 {
return nil, errors.New("Each instrument must have at least 1 voice")
}
@ -76,10 +77,12 @@ func Encode(patch sointu.Patch, featureSet FeatureSet, bpm int) (*BytePatch, err
unit.Parameters["color"] = index
}
opcode, ok := featureSet.Opcode(unit.Type)
commands = commands[:0]
commands = append(commands, byte(opcode+unit.Parameters["stereo"]))
if !ok {
return nil, fmt.Errorf(`the targeted virtual machine is not configured to support unit type "%v"`, unit.Type)
}
var values []byte
values = values[:0]
for _, v := range sointu.UnitTypes[unit.Type] {
if v.CanModulate && v.CanSet {
values = append(values, byte(unit.Parameters[v.Name]))
@ -131,38 +134,56 @@ func Encode(patch sointu.Patch, featureSet FeatureSet, bpm int) (*BytePatch, err
targetInstrIndex, _, err := patch.FindSendTarget(targetID)
targetVoice := unit.Parameters["voice"]
var addr uint16 = uint16(unit.Parameters["port"]) & 7
if unit.Parameters["sendpop"] == 1 {
addr += 0x8
}
if err == nil {
// local send is only possible if targetVoice is "auto" (0) and
// the targeted unit is in the same instrument as send
if targetInstrIndex == instrIndex && targetVoice == 0 {
if unit.Parameters["sendpop"] == 1 {
addr += 0x8
}
if v, ok := localAddrs[targetID]; ok {
addr += v
} else {
localFixups[targetID] = append(localFixups[targetID], len(c.Values)+len(values))
}
values = append(values, byte(addr&255), byte(addr>>8))
} else {
addr += 0x8000
if targetVoice > 0 { // "auto" (0) means for global send that it targets voice 0 of that instrument
addr += uint16((targetVoice - 1) * 0x400)
voiceStart := 0
voiceEnd := patch[targetInstrIndex].NumVoices
if targetVoice > 0 { // "all" (0) means for global send that it targets all voices of that instrument
voiceStart = targetVoice - 1
voiceEnd = targetVoice
}
if v, ok := globalAddrs[targetID]; ok {
addr += v
} else {
globalFixups[targetID] = append(globalFixups[targetID], len(c.Values)+len(values))
for i := voiceStart; i < voiceEnd; i++ {
if i > voiceStart { // we have already one opcode in commands, but with multiple voices we need to repeat it
commands = append(commands, byte(opcode+unit.Parameters["stereo"]))
values = append(values, byte(unit.Parameters["amount"]))
}
addr2 := addr + uint16(i)*0x400
if v, ok := globalAddrs[targetID]; ok {
addr2 += v
} else {
globalFixups[targetID] = append(globalFixups[targetID], len(c.Values)+len(values))
}
if i == voiceEnd-1 && unit.Parameters["sendpop"] == 1 {
addr2 += 0x8 // when making multi unit send, only the last one should have POP bit set if popping
}
values = append(values, byte(addr2&255), byte(addr2>>8))
}
}
} else {
// if no target will be found, the send will trash some of
// the last values of the last port of the last voice, which
// is unlikely to cause issues. We still honor the POP bit.
addr &= 0x8
addr = 0
if unit.Parameters["sendpop"] == 1 {
addr = 0x8
}
addr |= 0xFFF7
values = append(values, byte(addr&255), byte(addr>>8))
}
values = append(values, byte(addr&255), byte(addr>>8))
} else if unit.Type == "delay" {
count := len(unit.VarArgs)
if unit.Parameters["stereo"] == 1 {
@ -174,7 +195,7 @@ func Encode(patch sointu.Patch, featureSet FeatureSet, bpm int) (*BytePatch, err
countTrack := count*2 - 1 + (unit.Parameters["notetracking"] & 1) // 1 means no note tracking and 1 delay, 2 means notetracking with 1 delay, 3 means no note tracking and 2 delays etc.
values = append(values, byte(delayIndices[instrIndex][unitIndex]), byte(countTrack))
}
c.Commands = append(c.Commands, byte(opcode+unit.Parameters["stereo"]))
instrCommands = append(instrCommands, commands...)
c.Values = append(c.Values, values...)
if unit.ID != 0 {
localAddr := uint16((localUnitNo + 1) << 4)
@ -186,8 +207,12 @@ func Encode(patch sointu.Patch, featureSet FeatureSet, bpm int) (*BytePatch, err
globalFixups[unit.ID] = nil
globalAddrs[unit.ID] = globalAddr
}
localUnitNo++ // a command in command stream means the wrkspace addr gets also increased
if len(instrCommands) > 63 {
return nil, errors.New("An instrument can have a maximum of 63 units")
}
localUnitNo += len(commands) // a command in command stream means the wrkspace addr gets also increased
}
c.Commands = append(c.Commands, instrCommands...)
c.Commands = append(c.Commands, byte(0)) // advance
voiceNo += instr.NumVoices
}