sointu/compiler/encoded_song.go
vsariola 5dd81430b7 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.
2021-01-03 01:06:59 +02:00

147 lines
4.6 KiB
Go

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
}