mirror of
https://github.com/vsariola/sointu.git
synced 2025-06-04 01:28:45 -04:00
refactor(vm): rewrote BytePatch Encode to use a builder struct
(cherry picked from commit fdf119e50ce62619f508cc423c2ebaa000a1d540)
This commit is contained in:
parent
8c8232f76e
commit
12dd3dada0
298
vm/bytepatch.go
298
vm/bytepatch.go
@ -33,68 +33,50 @@ type SampleOffset struct {
|
||||
LoopLength uint16
|
||||
}
|
||||
|
||||
type bytePatchBuilder struct {
|
||||
sampleOffsetMap map[SampleOffset]int
|
||||
globalAddrs map[int]uint16
|
||||
globalFixups map[int]([]int)
|
||||
localAddrs map[int]uint16
|
||||
localFixups map[int]([]int)
|
||||
voiceNo int
|
||||
delayIndices [][]int
|
||||
unitNo int
|
||||
BytePatch
|
||||
}
|
||||
|
||||
func Encode(patch sointu.Patch, featureSet FeatureSet, bpm int) (*BytePatch, error) {
|
||||
c := BytePatch{PolyphonyBitmask: polyphonyBitmask(patch), NumVoices: uint32(patch.NumVoices())}
|
||||
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){}
|
||||
voiceNo := 0
|
||||
delayTable, delayIndices := constructDelayTimeTable(patch, bpm)
|
||||
c.DelayTimes = make([]uint16, len(delayTable))
|
||||
for i := range delayTable {
|
||||
c.DelayTimes[i] = uint16(delayTable[i])
|
||||
if patch.NumVoices() > 32 {
|
||||
return nil, fmt.Errorf("Sointu does not support more than 32 concurrent voices; patch uses %v", patch.NumVoices())
|
||||
}
|
||||
b := newBytePatchBuilder(patch, bpm)
|
||||
for instrIndex, instr := range patch {
|
||||
instrCommands = instrCommands[:0]
|
||||
if instr.NumVoices < 1 {
|
||||
return nil, errors.New("Each instrument must have at least 1 voice")
|
||||
}
|
||||
localAddrs := map[int]uint16{}
|
||||
localFixups := map[int]([]int){}
|
||||
localUnitNo := 0
|
||||
for unitIndex, unit := range instr.Units {
|
||||
if unit.Type == "" { // empty units are just ignored & skipped
|
||||
continue
|
||||
}
|
||||
if unit.Type == "oscillator" && unit.Parameters["type"] == 4 {
|
||||
s := SampleOffset{Start: uint32(unit.Parameters["samplestart"]), LoopStart: uint16(unit.Parameters["loopstart"]), LoopLength: uint16(unit.Parameters["looplength"])}
|
||||
if s.LoopLength == 0 {
|
||||
// hacky quick fix: looplength 0 causes div by zero so avoid crashing
|
||||
s.LoopLength = 1
|
||||
}
|
||||
index, ok := sampleOffsetMap[s]
|
||||
if !ok {
|
||||
index = len(c.SampleOffsets)
|
||||
sampleOffsetMap[s] = index
|
||||
c.SampleOffsets = append(c.SampleOffsets, s)
|
||||
}
|
||||
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)
|
||||
return nil, fmt.Errorf(`VM is not configured to support unit type "%v"`, unit.Type)
|
||||
}
|
||||
values = values[:0]
|
||||
for _, v := range sointu.UnitTypes[unit.Type] {
|
||||
if v.CanModulate && v.CanSet {
|
||||
values = append(values, byte(unit.Parameters[v.Name]))
|
||||
if unit.ID != 0 {
|
||||
b.idLabel(unit.ID)
|
||||
}
|
||||
p := unit.Parameters
|
||||
switch unit.Type {
|
||||
case "oscillator":
|
||||
color := p["color"]
|
||||
if unit.Parameters["type"] == 4 {
|
||||
color = b.getSampleIndex(unit)
|
||||
if color > 255 {
|
||||
return nil, errors.New("Patch uses over 256 samples")
|
||||
}
|
||||
}
|
||||
}
|
||||
if unit.Type == "aux" {
|
||||
values = append(values, byte(unit.Parameters["channel"]))
|
||||
} else if unit.Type == "in" {
|
||||
values = append(values, byte(unit.Parameters["channel"]))
|
||||
} else if unit.Type == "oscillator" {
|
||||
flags := 0
|
||||
switch unit.Parameters["type"] {
|
||||
switch p["type"] {
|
||||
case sointu.Sine:
|
||||
flags = 0x40
|
||||
case sointu.Trisaw:
|
||||
@ -106,12 +88,29 @@ func Encode(patch sointu.Patch, featureSet FeatureSet, bpm int) (*BytePatch, err
|
||||
case sointu.Sample:
|
||||
flags = 0x80
|
||||
}
|
||||
if unit.Parameters["lfo"] == 1 {
|
||||
if p["lfo"] == 1 {
|
||||
flags += 0x08
|
||||
}
|
||||
flags += unit.Parameters["unison"]
|
||||
values = append(values, byte(flags))
|
||||
} else if unit.Type == "filter" {
|
||||
flags += p["unison"]
|
||||
b.cmd(opcode + p["stereo"])
|
||||
b.vals(p["transpose"], p["detune"], p["phase"], color, p["shape"], p["gain"], flags)
|
||||
case "delay":
|
||||
count := len(unit.VarArgs)
|
||||
if unit.Parameters["stereo"] == 1 {
|
||||
count /= 2
|
||||
}
|
||||
if count == 0 {
|
||||
continue // skip encoding delays without any delay lines
|
||||
}
|
||||
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.
|
||||
b.cmd(opcode + p["stereo"])
|
||||
b.defaultVals(unit)
|
||||
b.vals(b.delayIndices[instrIndex][unitIndex], countTrack)
|
||||
case "aux", "in":
|
||||
b.cmd(opcode + p["stereo"])
|
||||
b.defaultVals(unit)
|
||||
b.vals(unit.Parameters["channel"])
|
||||
case "filter":
|
||||
flags := 0
|
||||
if unit.Parameters["lowpass"] == 1 {
|
||||
flags += 0x40
|
||||
@ -128,13 +127,14 @@ func Encode(patch sointu.Patch, featureSet FeatureSet, bpm int) (*BytePatch, err
|
||||
if unit.Parameters["neghighpass"] == 1 {
|
||||
flags += 0x04
|
||||
}
|
||||
values = append(values, byte(flags))
|
||||
} else if unit.Type == "send" {
|
||||
b.cmd(opcode + p["stereo"])
|
||||
b.defaultVals(unit)
|
||||
b.vals(flags)
|
||||
case "send":
|
||||
targetID := unit.Parameters["target"]
|
||||
targetInstrIndex, _, err := patch.FindSendTarget(targetID)
|
||||
targetVoice := unit.Parameters["voice"]
|
||||
var addr uint16 = uint16(unit.Parameters["port"]) & 7
|
||||
|
||||
addr := unit.Parameters["port"] & 7
|
||||
if err == nil {
|
||||
// local send is only possible if targetVoice is "auto" (0) and
|
||||
// the targeted unit is in the same instrument as send
|
||||
@ -142,12 +142,9 @@ func Encode(patch sointu.Patch, featureSet FeatureSet, bpm int) (*BytePatch, err
|
||||
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))
|
||||
b.cmd(opcode + p["stereo"])
|
||||
b.defaultVals(unit)
|
||||
b.localIDRef(targetID, addr)
|
||||
} else {
|
||||
addr += 0x8000
|
||||
voiceStart := 0
|
||||
@ -156,85 +153,152 @@ func Encode(patch sointu.Patch, featureSet FeatureSet, bpm int) (*BytePatch, err
|
||||
voiceStart = targetVoice - 1
|
||||
voiceEnd = targetVoice
|
||||
}
|
||||
addr += voiceStart * 0x400
|
||||
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))
|
||||
}
|
||||
b.cmd(opcode + p["stereo"])
|
||||
b.defaultVals(unit)
|
||||
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
|
||||
addr += 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))
|
||||
b.globalIDRef(targetID, addr)
|
||||
addr += 0x400
|
||||
}
|
||||
}
|
||||
} 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 = 0
|
||||
addr = 0xFFF7
|
||||
if unit.Parameters["sendpop"] == 1 {
|
||||
addr = 0x8
|
||||
addr |= 0x8
|
||||
}
|
||||
addr |= 0xFFF7
|
||||
values = append(values, byte(addr&255), byte(addr>>8))
|
||||
b.cmd(opcode + p["stereo"])
|
||||
b.defaultVals(unit)
|
||||
b.Values = append(b.Values, byte(addr&255), byte(addr>>8))
|
||||
}
|
||||
} else if unit.Type == "delay" {
|
||||
count := len(unit.VarArgs)
|
||||
if unit.Parameters["stereo"] == 1 {
|
||||
count /= 2
|
||||
}
|
||||
if count == 0 {
|
||||
continue // skip encoding delays without any delay lines
|
||||
}
|
||||
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))
|
||||
default:
|
||||
b.cmd(opcode + p["stereo"])
|
||||
b.defaultVals(unit)
|
||||
}
|
||||
instrCommands = append(instrCommands, commands...)
|
||||
c.Values = append(c.Values, values...)
|
||||
if unit.ID != 0 {
|
||||
localAddr := uint16((localUnitNo + 1) << 4)
|
||||
fixUp(c.Values, localFixups[unit.ID], localAddr)
|
||||
localFixups[unit.ID] = nil
|
||||
localAddrs[unit.ID] = localAddr
|
||||
globalAddr := localAddr + 16 + uint16(voiceNo)*1024
|
||||
fixUp(c.Values, globalFixups[unit.ID], globalAddr)
|
||||
globalFixups[unit.ID] = nil
|
||||
globalAddrs[unit.ID] = globalAddr
|
||||
if b.unitNo > 63 {
|
||||
return nil, fmt.Errorf(`Instrument %v has over 63 units`, instrIndex)
|
||||
}
|
||||
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
|
||||
b.cmdFinish(instr)
|
||||
}
|
||||
return &c, nil
|
||||
return &b.BytePatch, nil
|
||||
}
|
||||
|
||||
func polyphonyBitmask(patch sointu.Patch) uint32 {
|
||||
var ret uint32 = 0
|
||||
func newBytePatchBuilder(patch sointu.Patch, bpm int) *bytePatchBuilder {
|
||||
var polyphonyBitmask uint32 = 0
|
||||
for _, instr := range patch {
|
||||
for j := 0; j < instr.NumVoices-1; j++ {
|
||||
ret = (ret << 1) + 1 // for each instrument, NumVoices - 1 bits are ones
|
||||
polyphonyBitmask = (polyphonyBitmask << 1) + 1 // for each instrument, NumVoices - 1 bits are ones
|
||||
}
|
||||
ret <<= 1 // ...and the last bit is zero, to denote "change instrument"
|
||||
polyphonyBitmask <<= 1 // ...and the last bit is zero, to denote "change instrument"
|
||||
}
|
||||
return ret
|
||||
delayTimesInt, delayIndices := constructDelayTimeTable(patch, bpm)
|
||||
delayTimesU16 := make([]uint16, len(delayTimesInt))
|
||||
for i, d := range delayTimesInt {
|
||||
delayTimesU16[i] = uint16(d)
|
||||
}
|
||||
c := bytePatchBuilder{
|
||||
BytePatch: BytePatch{PolyphonyBitmask: polyphonyBitmask, NumVoices: uint32(patch.NumVoices()), DelayTimes: delayTimesU16},
|
||||
sampleOffsetMap: map[SampleOffset]int{},
|
||||
globalAddrs: map[int]uint16{},
|
||||
globalFixups: map[int]([]int){},
|
||||
localAddrs: map[int]uint16{},
|
||||
localFixups: map[int]([]int){},
|
||||
delayIndices: delayIndices}
|
||||
return &c
|
||||
}
|
||||
|
||||
func fixUp(values []byte, positions []int, delta uint16) {
|
||||
for _, pos := range positions {
|
||||
orig := (uint16(values[pos+1]) << 8) + uint16(values[pos])
|
||||
new := orig + delta
|
||||
values[pos] = byte(new & 255)
|
||||
values[pos+1] = byte(new >> 8)
|
||||
// cmd adds a command to the bytecode, and increments the unit number
|
||||
func (b *bytePatchBuilder) cmd(opcode int) {
|
||||
b.Commands = append(b.Commands, byte(opcode))
|
||||
b.unitNo++
|
||||
}
|
||||
|
||||
// cmdFinish adds a command to the bytecode that marks the end of an instrument, resets the unit number and increments the voice number
|
||||
// local addresses are forgotten when instrument ends
|
||||
func (b *bytePatchBuilder) cmdFinish(instr sointu.Instrument) {
|
||||
b.Commands = append(b.Commands, 0)
|
||||
b.unitNo = 0
|
||||
b.voiceNo += instr.NumVoices
|
||||
b.localAddrs = map[int]uint16{}
|
||||
b.localFixups = map[int]([]int){}
|
||||
}
|
||||
|
||||
// vals appends values to the value stream
|
||||
func (b *bytePatchBuilder) vals(values ...int) {
|
||||
for _, v := range values {
|
||||
b.Values = append(b.Values, byte(v))
|
||||
}
|
||||
}
|
||||
|
||||
// defaultVals appends the values to the value stream for all parameters that can be modulated and set
|
||||
func (b *bytePatchBuilder) defaultVals(unit sointu.Unit) {
|
||||
for _, v := range sointu.UnitTypes[unit.Type] {
|
||||
if v.CanModulate && v.CanSet {
|
||||
b.Values = append(b.Values, byte(unit.Parameters[v.Name]))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// localIDRef adds a reference to a local id label to the value stream; if the targeted ID has not been seen yet, it is added to the fixup list
|
||||
func (b *bytePatchBuilder) localIDRef(id int, addr int) {
|
||||
if v, ok := b.localAddrs[id]; ok {
|
||||
addr += int(v)
|
||||
} else {
|
||||
b.localFixups[id] = append(b.localFixups[id], len(b.Values))
|
||||
}
|
||||
b.Values = append(b.Values, byte(addr&255), byte(addr>>8))
|
||||
}
|
||||
|
||||
// globalIDRef adds a reference to a global id label to the value stream; if the targeted ID has not been seen yet, it is added to the fixup list
|
||||
func (b *bytePatchBuilder) globalIDRef(id int, addr int) {
|
||||
if v, ok := b.globalAddrs[id]; ok {
|
||||
addr += int(v)
|
||||
} else {
|
||||
b.globalFixups[id] = append(b.globalFixups[id], len(b.Values))
|
||||
}
|
||||
b.Values = append(b.Values, byte(addr&255), byte(addr>>8))
|
||||
}
|
||||
|
||||
// idLabel adds a label to the value stream for the given id; all earlier references to the id are fixed up
|
||||
func (b *bytePatchBuilder) idLabel(id int) {
|
||||
localAddr := uint16((b.unitNo + 1) << 4)
|
||||
b.fixUp(b.localFixups[id], localAddr)
|
||||
b.localFixups[id] = nil
|
||||
b.localAddrs[id] = localAddr
|
||||
globalAddr := localAddr + 16 + uint16(b.voiceNo)*1024
|
||||
b.fixUp(b.globalFixups[id], globalAddr)
|
||||
b.globalFixups[id] = nil
|
||||
b.globalAddrs[id] = globalAddr
|
||||
}
|
||||
|
||||
// fixUp fixes up the references to the given id with the given delta
|
||||
func (b *bytePatchBuilder) fixUp(positions []int, delta uint16) {
|
||||
for _, pos := range positions {
|
||||
orig := (uint16(b.Values[pos+1]) << 8) + uint16(b.Values[pos])
|
||||
new := orig + delta
|
||||
b.Values[pos] = byte(new & 255)
|
||||
b.Values[pos+1] = byte(new >> 8)
|
||||
}
|
||||
}
|
||||
|
||||
// getSampleIndex returns the index of the sample in the sample offset table; if the sample has not been seen yet, it is added to the table
|
||||
func (b *bytePatchBuilder) getSampleIndex(unit sointu.Unit) int {
|
||||
s := SampleOffset{Start: uint32(unit.Parameters["samplestart"]), LoopStart: uint16(unit.Parameters["loopstart"]), LoopLength: uint16(unit.Parameters["looplength"])}
|
||||
if s.LoopLength == 0 {
|
||||
// hacky quick fix: looplength 0 causes div by zero so avoid crashing
|
||||
s.LoopLength = 1
|
||||
}
|
||||
index, ok := b.sampleOffsetMap[s]
|
||||
if !ok {
|
||||
index = len(b.SampleOffsets)
|
||||
b.sampleOffsetMap[s] = index
|
||||
b.SampleOffsets = append(b.SampleOffsets, s)
|
||||
}
|
||||
return index
|
||||
}
|
||||
|
Loading…
x
Reference in New Issue
Block a user