mirror of
https://github.com/vsariola/sointu.git
synced 2025-09-17 18:54:35 -04:00
feat(sointu): make patterns local to track
The global pattern table is constructed only during compilation. At this point, we can do also all sorts of optimizations / changes e.g. remove unnecessary releases and reuse patterns if there's a pattern already that could be used.
This commit is contained in:
@ -85,6 +85,10 @@ func (com *Compiler) Song(song *sointu.Song) (map[string]string, error) {
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf(`could not encode patch: %v`, err)
|
||||
}
|
||||
encodedSong, err := EncodeSong(song)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf(`could not encode song: %v`, err)
|
||||
}
|
||||
for _, templateName := range templates {
|
||||
compilerMacros := *NewCompilerMacros(*com)
|
||||
featureSetMacros := FeatureSetMacros{features}
|
||||
@ -99,7 +103,8 @@ func (com *Compiler) Song(song *sointu.Song) (map[string]string, error) {
|
||||
X86Macros
|
||||
SongMacros
|
||||
*EncodedPatch
|
||||
}{compilerMacros, featureSetMacros, x86Macros, songMacros, encodedPatch}
|
||||
EncodedSong *EncodedSong
|
||||
}{compilerMacros, featureSetMacros, x86Macros, songMacros, encodedPatch, encodedSong}
|
||||
populatedTemplate, extension, err = com.compile(templateName, &data)
|
||||
} else if com.Arch == "wasm" {
|
||||
wasmMacros := *NewWasmMacros()
|
||||
@ -109,7 +114,8 @@ func (com *Compiler) Song(song *sointu.Song) (map[string]string, error) {
|
||||
WasmMacros
|
||||
SongMacros
|
||||
*EncodedPatch
|
||||
}{compilerMacros, featureSetMacros, wasmMacros, songMacros, encodedPatch}
|
||||
EncodedSong *EncodedSong
|
||||
}{compilerMacros, featureSetMacros, wasmMacros, songMacros, encodedPatch, encodedSong}
|
||||
populatedTemplate, extension, err = com.compile(templateName, &data)
|
||||
}
|
||||
if err != nil {
|
||||
|
146
compiler/encoded_song.go
Normal file
146
compiler/encoded_song.go
Normal file
@ -0,0 +1,146 @@
|
||||
package compiler
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"fmt"
|
||||
|
||||
"github.com/vsariola/sointu"
|
||||
)
|
||||
|
||||
// EncodedSong has a single global pattern table and all track sequences are
|
||||
// indices to this table. This is in contrast with sointu. Song, which has one
|
||||
// pattern table per track.
|
||||
type EncodedSong struct {
|
||||
Patterns [][]byte
|
||||
Sequences [][]byte
|
||||
}
|
||||
|
||||
// flattenPatterns returns the track sequences flattened into linear arrays.
|
||||
// Additionally, after first release (value 0), it replaces every release or
|
||||
// hold with -1, denoting "don't care if it's either release (0) or hold (1)".
|
||||
// As we reconstruct the pattern table, we may use any pattern that has either 0
|
||||
// or hold in place of don't cares.
|
||||
func flattenPatterns(song *sointu.Song) [][]int {
|
||||
ret := make([][]int, 0, len(song.Tracks))
|
||||
for _, t := range song.Tracks {
|
||||
flatSequence := make([]int, 0, song.TotalRows())
|
||||
dontCare := false
|
||||
for _, s := range t.Sequence {
|
||||
for _, note := range t.Patterns[s] {
|
||||
if !dontCare || note > song.Hold {
|
||||
if note == song.Hold {
|
||||
flatSequence = append(flatSequence, 1) // replace holds with 1s, we'll get rid of song.Hold soon and do the hold replacement at the last minute
|
||||
} else {
|
||||
flatSequence = append(flatSequence, int(note))
|
||||
}
|
||||
dontCare = note == 0 // after 0 aka release, we don't care if further releases come along
|
||||
} else {
|
||||
flatSequence = append(flatSequence, -1)
|
||||
}
|
||||
}
|
||||
}
|
||||
ret = append(ret, flatSequence)
|
||||
}
|
||||
return ret
|
||||
}
|
||||
|
||||
// constructPatterns finds the smallest global pattern table for a given list of
|
||||
// flattened patterns. If the patterns are not divisible with the patternLength,
|
||||
// then: a) if the last note of a track is release (0) or don't care (-1), the
|
||||
// track is extended with don't cares (-1) until the total length of the song is
|
||||
// divisible with the patternLength. b) Otherwise, the track is extended with a
|
||||
// single release (0), followed by don't care about release & hold (-1).
|
||||
//
|
||||
// In otherwords: any playing notes are released when the original song ends.
|
||||
func constructPatterns(tracks [][]int, patternLength int) ([][]byte, [][]byte, error) {
|
||||
patternTable := make([][]int, 0)
|
||||
sequences := make([][]byte, 0, len(tracks))
|
||||
for _, t := range tracks {
|
||||
var sequence []byte
|
||||
for s := 0; s < len(t); s += patternLength {
|
||||
pat := t[s : s+patternLength]
|
||||
if len(pat) < patternLength {
|
||||
extension := make([]int, patternLength-len(pat))
|
||||
for i := range extension {
|
||||
if pat[len(pat)-1] > 0 && i == 0 {
|
||||
extension[i] = 0
|
||||
} else {
|
||||
extension[i] = -1
|
||||
}
|
||||
}
|
||||
pat = append(pat, extension...)
|
||||
}
|
||||
// go through the current pattern table to see if there's already a
|
||||
// pattern that could be used
|
||||
patternIndex := -1
|
||||
for j, p := range patternTable {
|
||||
match := true
|
||||
for k, n := range p {
|
||||
if (n > -1 && pat[k] > -1 && n != pat[k]) ||
|
||||
(n == -1 && pat[k] > 1) ||
|
||||
(n > 1 && pat[k] == -1) {
|
||||
match = false
|
||||
break
|
||||
}
|
||||
}
|
||||
if match {
|
||||
// if there was any don't cares in the pattern table where
|
||||
// the new pattern has non don't cares, copy them to the
|
||||
// patterns that was already in the pattern table
|
||||
for k, n := range pat {
|
||||
if n != -1 {
|
||||
patternTable[j][k] = n
|
||||
}
|
||||
}
|
||||
patternIndex = j
|
||||
break
|
||||
}
|
||||
}
|
||||
if patternIndex == -1 {
|
||||
patternIndex = len(patternTable)
|
||||
patternTable = append(patternTable, pat)
|
||||
}
|
||||
if patternIndex > 255 {
|
||||
return nil, nil, errors.New("encoding the song would result more than 256 different unique patterns")
|
||||
}
|
||||
sequence = append(sequence, byte(patternIndex))
|
||||
}
|
||||
sequences = append(sequences, sequence)
|
||||
}
|
||||
// finally, if there are still some don't cares in the table, just replace them with zeros
|
||||
byteTable := make([][]byte, 0, len(patternTable))
|
||||
for _, pat := range patternTable {
|
||||
bytePat := make([]byte, 0, patternLength)
|
||||
for _, n := range pat {
|
||||
if n >= 0 {
|
||||
bytePat = append(bytePat, byte(n))
|
||||
} else {
|
||||
bytePat = append(bytePat, 0)
|
||||
}
|
||||
}
|
||||
byteTable = append(byteTable, bytePat)
|
||||
}
|
||||
return byteTable, sequences, nil
|
||||
}
|
||||
|
||||
func (e *EncodedSong) PatternLength() int {
|
||||
return len(e.Patterns[0])
|
||||
}
|
||||
|
||||
func (e *EncodedSong) SequenceLength() int {
|
||||
return len(e.Sequences[0])
|
||||
}
|
||||
|
||||
func (e *EncodedSong) TotalRows() int {
|
||||
return e.SequenceLength() * e.PatternLength()
|
||||
}
|
||||
|
||||
func EncodeSong(song *sointu.Song) (*EncodedSong, error) {
|
||||
// TODO: we could give the user the possibility to encode the patterns with a different length here also
|
||||
patLength := song.PatternRows()
|
||||
patterns, sequences, err := constructPatterns(flattenPatterns(song), patLength)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("error during constructPatterns: %v", err)
|
||||
}
|
||||
return &EncodedSong{Patterns: patterns, Sequences: sequences}, nil
|
||||
}
|
81
compiler/encoded_song_test.go
Normal file
81
compiler/encoded_song_test.go
Normal file
@ -0,0 +1,81 @@
|
||||
package compiler_test
|
||||
|
||||
import (
|
||||
"reflect"
|
||||
"testing"
|
||||
|
||||
"github.com/vsariola/sointu"
|
||||
"github.com/vsariola/sointu/compiler"
|
||||
)
|
||||
|
||||
func TestPatternReusing(t *testing.T) {
|
||||
song := sointu.Song{
|
||||
Hold: 1,
|
||||
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},
|
||||
}},
|
||||
}
|
||||
encodedSong, err := compiler.EncodeSong(&song)
|
||||
if err != nil {
|
||||
t.Fatalf("song encoding error: %v", err)
|
||||
}
|
||||
expected := compiler.EncodedSong{
|
||||
Sequences: [][]byte{{0, 1}, {0, 2}},
|
||||
Patterns: [][]byte{{64, 1, 1, 1, 0, 0, 0, 0}, {72, 0, 0, 0, 0, 0, 0, 0}, {84, 0, 0, 0, 0, 0, 0, 0}},
|
||||
}
|
||||
if !reflect.DeepEqual(*encodedSong, expected) {
|
||||
t.Fatalf("got different EncodedSong than expected. got: %v expected: %v", *encodedSong, expected)
|
||||
}
|
||||
}
|
||||
|
||||
func TestUnnecessaryHolds(t *testing.T) {
|
||||
song := sointu.Song{
|
||||
Hold: 1,
|
||||
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},
|
||||
}},
|
||||
}
|
||||
encodedSong, err := compiler.EncodeSong(&song)
|
||||
if err != nil {
|
||||
t.Fatalf("song encoding error: %v", err)
|
||||
}
|
||||
expected := compiler.EncodedSong{
|
||||
Sequences: [][]byte{{0, 1}, {0, 2}},
|
||||
Patterns: [][]byte{{64, 1, 1, 1, 0, 0, 0, 0}, {72, 0, 0, 0, 0, 0, 0, 0}, {84, 0, 0, 0, 0, 0, 0, 0}},
|
||||
}
|
||||
if !reflect.DeepEqual(*encodedSong, expected) {
|
||||
t.Fatalf("got different EncodedSong than expected. got: %v expected: %v", *encodedSong, expected)
|
||||
}
|
||||
}
|
||||
|
||||
func TestDontCares(t *testing.T) {
|
||||
song := sointu.Song{
|
||||
Hold: 1,
|
||||
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},
|
||||
}},
|
||||
}
|
||||
encodedSong, err := compiler.EncodeSong(&song)
|
||||
if err != nil {
|
||||
t.Fatalf("song encoding error: %v", err)
|
||||
}
|
||||
expected := compiler.EncodedSong{
|
||||
Sequences: [][]byte{{0, 1}, {2, 1}},
|
||||
Patterns: [][]byte{{64, 1, 1, 1, 0, 0, 0, 0}, {1, 1, 1, 0, 0, 0, 0, 0}, {64, 1, 1, 1, 1, 1, 1, 1}},
|
||||
}
|
||||
if !reflect.DeepEqual(*encodedSong, expected) {
|
||||
t.Fatalf("got different EncodedSong than expected. got: %v expected: %v", *encodedSong, expected)
|
||||
}
|
||||
}
|
Reference in New Issue
Block a user