feat(sointu, tracker,...): restructure domain & tracker models

send targets are now by ID and Song has "Score" part, which is the notes for it. also, moved the model part separate of the actual gioui dependend stuff.

sorry to my future self about the code bomb; ended up too far and did not find an easy way to rewrite the history to make the steps smaller, so in the end, just squashed everything.
This commit is contained in:
vsariola
2021-02-23 23:55:42 +02:00
parent fd1d018e82
commit adcf3ebce8
155 changed files with 5520 additions and 4914 deletions

View File

@ -82,7 +82,7 @@ func (com *Compiler) Song(song *sointu.Song) (map[string]string, error) {
}
features := NecessaryFeaturesFor(song.Patch)
retmap := map[string]string{}
encodedPatch, err := Encode(&song.Patch, features)
encodedPatch, err := Encode(song.Patch, features)
if err != nil {
return nil, fmt.Errorf(`could not encode patch: %v`, err)
}

View File

@ -22,16 +22,22 @@ type SampleOffset struct {
LoopLength uint16
}
func Encode(patch *sointu.Patch, featureSet FeatureSet) (*EncodedPatch, error) {
func Encode(patch sointu.Patch, featureSet FeatureSet) (*EncodedPatch, error) {
var c EncodedPatch
sampleOffsetMap := map[SampleOffset]int{}
for instrIndex, instr := range patch.Instruments {
globalAddrs := map[int]uint16{}
globalFixups := map[int]([]int){}
voiceNo := 0
for instrIndex, instr := range patch {
if len(instr.Units) > 63 {
return nil, errors.New("An instrument can have a maximum of 63 units")
}
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 _, unit := range instr.Units {
if unit.Type == "" { // empty units are just ignored & skipped
continue
@ -113,44 +119,62 @@ func Encode(patch *sointu.Patch, featureSet FeatureSet) (*EncodedPatch, error) {
}
values = append(values, byte(flags))
} else if unit.Type == "send" {
targetID := unit.Parameters["target"]
targetInstrIndex, _, err := patch.FindSendTarget(targetID)
targetVoice := unit.Parameters["voice"]
var targetInstrument int
if targetVoice == 0 {
targetInstrument = instrIndex
} else {
var err error
targetInstrument, err = patch.InstrumentForVoice(targetVoice - 1)
if err != nil {
return nil, fmt.Errorf("send targeted a voice %v, which out of the range of instruments", targetVoice-1)
}
}
origTarget := unit.Parameters["unit"]
targetUnit := origTarget
for k := 0; k < origTarget; k++ {
units := patch.Instruments[targetInstrument].Units
if k >= len(units) {
break
}
if units[k].Type == "" {
targetUnit--
}
}
address := ((targetUnit + 1) << 4) + unit.Parameters["port"] // each unit is 16 dwords, 8 workspace followed by 8 ports. +1 is for skipping the note/release/inputs
if unit.Parameters["voice"] > 0 {
address += 0x8000 + 16 + (unit.Parameters["voice"]-1)*1024 // global send, +16 is for skipping the out/aux ports
}
var addr uint16 = uint16(unit.Parameters["port"]) & 7
if unit.Parameters["sendpop"] == 1 {
address += 0x8
addr += 0x8
}
values = append(values, byte(address&255), byte(address>>8))
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 v, ok := localAddrs[targetID]; ok {
addr += v
} else {
localFixups[targetID] = append(localFixups[targetID], len(c.Values)+len(values))
}
} 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)
}
if v, ok := globalAddrs[targetID]; ok {
addr += v
} else {
globalFixups[targetID] = append(globalFixups[targetID], len(c.Values)+len(values))
}
}
} 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 |= 0xFFF7
}
values = append(values, byte(addr&255), byte(addr>>8))
} else if unit.Type == "delay" {
countTrack := (unit.Parameters["count"] << 1) - 1 + unit.Parameters["notetracking"] // 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(unit.Parameters["delay"]), byte(countTrack))
}
c.Commands = append(c.Commands, byte(opcode+unit.Parameters["stereo"]))
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
}
localUnitNo++ // a command in command stream means the wrkspace addr gets also increased
}
c.Commands = append(c.Commands, byte(0)) // advance
voiceNo += instr.NumVoices
c.NumVoices += uint32(instr.NumVoices)
for k := 0; k < instr.NumVoices-1; k++ {
c.PolyphonyBitmask = (c.PolyphonyBitmask << 1) + 1
@ -163,3 +187,12 @@ func Encode(patch *sointu.Patch, featureSet FeatureSet) (*EncodedPatch, error) {
return &c, nil
}
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)
}
}

View File

@ -17,6 +17,7 @@ type FeatureSet interface {
SupportsParamValueOtherThan(unitType string, paramName string, value int) bool
SupportsModulation(unitType string, paramName string) bool
SupportsPolyphony() bool
SupportsGlobalSend() bool
}
type Instruction struct {
@ -58,6 +59,10 @@ func (_ AllFeatures) SupportsPolyphony() bool {
return true
}
func (_ AllFeatures) SupportsGlobalSend() bool {
return true
}
func (_ AllFeatures) Opcode(unitType string) (int, bool) {
code, ok := allOpcodes[unitType]
return code, ok
@ -114,12 +119,13 @@ type NecessaryFeatures struct {
instructions []string
supportsParamValue map[paramKey](map[int]bool)
supportsModulation map[paramKey]bool
globalSend bool
polyphony bool
}
func NecessaryFeaturesFor(patch sointu.Patch) NecessaryFeatures {
features := NecessaryFeatures{opcodes: map[string]int{}, supportsParamValue: map[paramKey](map[int]bool){}, supportsModulation: map[paramKey]bool{}}
for instrNo, instrument := range patch.Instruments {
for instrIndex, instrument := range patch {
for _, unit := range instrument.Units {
if _, ok := features.opcodes[unit.Type]; !ok {
features.instructions = append(features.instructions, unit.Type)
@ -133,27 +139,20 @@ func NecessaryFeaturesFor(patch sointu.Patch) NecessaryFeatures {
features.supportsParamValue[key][v] = true
}
if unit.Type == "send" {
targetInstrument := instrNo
if unit.Parameters["voice"] > 0 {
v, err := patch.InstrumentForVoice(unit.Parameters["voice"] - 1)
if err != nil {
continue
}
targetInstrument = v
targetInstrIndex, targetUnitIndex, err := patch.FindSendTarget(unit.Parameters["target"])
if err != nil {
continue
}
if unit.Parameters["unit"] < 0 || unit.Parameters["unit"] >= len(patch.Instruments[targetInstrument].Units) {
continue // send is modulating outside the range of the target instrument; probably a bug in patch, but at least it's not modulating the uniType we're after
targetUnit := patch[targetInstrIndex].Units[targetUnitIndex]
portList := sointu.Ports[targetUnit.Type]
portIndex := unit.Parameters["port"]
if portIndex < 0 || portIndex >= len(portList) {
continue
}
targetUnit := patch.Instruments[targetInstrument].Units[unit.Parameters["unit"]]
modulatedPortNo := 0
for _, v := range sointu.UnitTypes[targetUnit.Type] {
if v.CanModulate {
if modulatedPortNo == unit.Parameters["port"] {
features.supportsModulation[paramKey{targetUnit.Type, v.Name}] = true
}
modulatedPortNo++
}
if targetInstrIndex != instrIndex || unit.Parameters["voice"] > 0 {
features.globalSend = true
}
features.supportsModulation[paramKey{targetUnit.Type, portList[portIndex]}] = true
}
}
if instrument.NumVoices > 1 {
@ -204,3 +203,7 @@ func (n NecessaryFeatures) InputNumber(unitType string, paramName string) int {
func (_ NecessaryFeatures) TransformCount(unitType string) int {
return allTransformCounts[unitType]
}
func (n NecessaryFeatures) SupportsGlobalSend() bool {
return n.globalSend
}

View File

@ -32,11 +32,17 @@ func fixPatternLength(patterns [][]byte, fixedLength int) [][]int {
func flattenSequence(patterns [][]int, sequence []int) []int {
sumLen := 0
for _, patIndex := range sequence {
if patIndex < 0 || patIndex >= len(patterns) {
continue
}
sumLen += len(patterns[patIndex])
}
notes := make([]int, sumLen)
window := notes
for _, patIndex := range sequence {
if patIndex < 0 || patIndex >= len(patterns) {
continue
}
elementsCopied := copy(window, patterns[patIndex])
window = window[elementsCopied:]
}
@ -167,12 +173,12 @@ func bytesToInts(array []byte) []int {
}
func ConstructPatterns(song *sointu.Song) ([][]byte, [][]byte, error) {
patLength := song.RowsPerPattern
sequences := make([][]byte, len(song.Tracks))
patLength := song.Score.RowsPerPattern
sequences := make([][]byte, len(song.Score.Tracks))
var patterns [][]int
for i, t := range song.Tracks {
for i, t := range song.Score.Tracks {
fixed := fixPatternLength(t.Patterns, patLength)
flat := flattenSequence(fixed, bytesToInts(t.Sequence))
flat := flattenSequence(fixed, t.Order)
dontCares := markDontCares(flat)
// TODO: we could give the user the possibility to use another length during encoding that during composing
chunks := splitSequence(dontCares, patLength)

View File

@ -10,14 +10,16 @@ import (
func TestPatternReusing(t *testing.T) {
song := sointu.Song{
RowsPerPattern: 8,
Tracks: []sointu.Track{{
Patterns: [][]byte{{64, 1, 1, 1, 0, 0, 0, 0}, {72, 0, 0, 0, 0, 0, 0, 0}},
Sequence: []byte{0, 1},
}, {
Patterns: [][]byte{{64, 1, 1, 1, 0, 0, 0, 0}, {84, 0, 0, 0, 0, 0, 0, 0}},
Sequence: []byte{0, 1},
}},
Score: sointu.Score{
RowsPerPattern: 8,
Tracks: []sointu.Track{{
Patterns: [][]byte{{64, 1, 1, 1, 0, 0, 0, 0}, {72, 0, 0, 0, 0, 0, 0, 0}},
Order: []int{0, 1},
}, {
Patterns: [][]byte{{64, 1, 1, 1, 0, 0, 0, 0}, {84, 0, 0, 0, 0, 0, 0, 0}},
Order: []int{0, 1},
}},
},
}
patterns, sequences, err := compiler.ConstructPatterns(&song)
if err != nil {
@ -35,14 +37,15 @@ func TestPatternReusing(t *testing.T) {
func TestUnnecessaryHolds(t *testing.T) {
song := sointu.Song{
RowsPerPattern: 8,
Tracks: []sointu.Track{{
Patterns: [][]byte{{64, 1, 1, 1, 0, 1, 0, 0}, {72, 0, 1, 0, 1, 0, 0, 0}},
Sequence: []byte{0, 1},
}, {
Patterns: [][]byte{{64, 1, 1, 1, 0, 0, 1, 0}, {84, 0, 0, 0, 1, 1, 0, 0}},
Sequence: []byte{0, 1},
}},
Score: sointu.Score{
RowsPerPattern: 8,
Tracks: []sointu.Track{{
Patterns: [][]byte{{64, 1, 1, 1, 0, 1, 0, 0}, {72, 0, 1, 0, 1, 0, 0, 0}},
Order: []int{0, 1},
}, {
Patterns: [][]byte{{64, 1, 1, 1, 0, 0, 1, 0}, {84, 0, 0, 0, 1, 1, 0, 0}},
Order: []int{0, 1},
}}},
}
patterns, sequences, err := compiler.ConstructPatterns(&song)
if err != nil {
@ -60,14 +63,17 @@ func TestUnnecessaryHolds(t *testing.T) {
func TestDontCares(t *testing.T) {
song := sointu.Song{
RowsPerPattern: 8,
Tracks: []sointu.Track{{
Patterns: [][]byte{{64, 1, 1, 1, 0, 0, 0, 0}, {0, 0, 0, 0, 0, 0, 0, 0}},
Sequence: []byte{0, 1},
}, {
Patterns: [][]byte{{64, 1, 1, 1, 1, 1, 1, 1}, {1, 1, 1, 0, 0, 0, 0, 0}},
Sequence: []byte{0, 1},
}},
Score: sointu.Score{
Length: 2,
RowsPerPattern: 8,
Tracks: []sointu.Track{{
Patterns: [][]byte{{64, 1, 1, 1, 0, 0, 0, 0}, {0, 0, 0, 0, 0, 0, 0, 0}},
Order: []int{0, 1},
}, {
Patterns: [][]byte{{64, 1, 1, 1, 1, 1, 1, 1}, {1, 1, 1, 0, 0, 0, 0, 0}},
Order: []int{0, 1},
}},
},
}
patterns, sequences, err := compiler.ConstructPatterns(&song)
if err != nil {

View File

@ -13,10 +13,10 @@ type SongMacros struct {
}
func NewSongMacros(s *sointu.Song) *SongMacros {
maxSamples := s.SamplesPerRow() * s.TotalRows()
maxSamples := s.SamplesPerRow() * s.Score.LengthInRows()
p := SongMacros{Song: s, MaxSamples: maxSamples}
trackVoiceNumber := 0
for _, t := range s.Tracks {
for _, t := range s.Score.Tracks {
for b := 0; b < t.NumVoices-1; b++ {
p.VoiceTrackBitmask += 1 << trackVoiceNumber
trackVoiceNumber++
@ -28,7 +28,7 @@ func NewSongMacros(s *sointu.Song) *SongMacros {
func (p *SongMacros) NumDelayLines() string {
total := 0
for _, instr := range p.Song.Patch.Instruments {
for _, instr := range p.Song.Patch {
for _, unit := range instr.Units {
if unit.Type == "delay" {
total += unit.Parameters["count"] * (1 + unit.Parameters["stereo"])