mirror of
https://github.com/vsariola/sointu.git
synced 2025-06-04 01:28:45 -04:00
refactor(compiler): split song encoding logic into smaller reusable functions
This commit is contained in:
parent
5dd81430b7
commit
d328192834
@ -15,112 +15,163 @@ type EncodedSong struct {
|
|||||||
Sequences [][]byte
|
Sequences [][]byte
|
||||||
}
|
}
|
||||||
|
|
||||||
// flattenPatterns returns the track sequences flattened into linear arrays.
|
// fixPatternLength makes sure that every pattern is the same length. During
|
||||||
// Additionally, after first release (value 0), it replaces every release or
|
// composing. Patterns shorter than the given length are padded with 1 / "hold";
|
||||||
// hold with -1, denoting "don't care if it's either release (0) or hold (1)".
|
// patterns longer than the given length are cropped.
|
||||||
// As we reconstruct the pattern table, we may use any pattern that has either 0
|
func fixPatternLength(patterns [][]byte, fixedLength int) [][]int {
|
||||||
// or hold in place of don't cares.
|
patternData := make([]int, len(patterns)*fixedLength)
|
||||||
func flattenPatterns(song *sointu.Song) [][]int {
|
ret := make([][]int, len(patterns))
|
||||||
ret := make([][]int, 0, len(song.Tracks))
|
for i, pat := range patterns {
|
||||||
for _, t := range song.Tracks {
|
for j, note := range pat {
|
||||||
flatSequence := make([]int, 0, song.TotalRows())
|
patternData[j] = int(note)
|
||||||
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)
|
for j := len(pat); j < fixedLength; j++ {
|
||||||
|
patternData[j] = 1 // pad with hold
|
||||||
|
}
|
||||||
|
ret[i], patternData = patternData[:fixedLength], patternData[fixedLength:]
|
||||||
}
|
}
|
||||||
return ret
|
return ret
|
||||||
}
|
}
|
||||||
|
|
||||||
// constructPatterns finds the smallest global pattern table for a given list of
|
// flattenSequence looks up a sequence of patterns and concatenates them into a
|
||||||
// flattened patterns. If the patterns are not divisible with the patternLength,
|
// single linear array of notes. Note that variable length patterns are
|
||||||
// then: a) if the last note of a track is release (0) or don't care (-1), the
|
// concatenated as such; call fixPatternLength first if you want every pattern
|
||||||
// track is extended with don't cares (-1) until the total length of the song is
|
// to be constant length.
|
||||||
// divisible with the patternLength. b) Otherwise, the track is extended with a
|
func flattenSequence(patterns [][]int, sequence []int) []int {
|
||||||
// single release (0), followed by don't care about release & hold (-1).
|
sumLen := 0
|
||||||
//
|
for _, patIndex := range sequence {
|
||||||
// In otherwords: any playing notes are released when the original song ends.
|
sumLen += len(patterns[patIndex])
|
||||||
func constructPatterns(tracks [][]int, patternLength int) ([][]byte, [][]byte, error) {
|
}
|
||||||
patternTable := make([][]int, 0)
|
notes := make([]int, sumLen)
|
||||||
sequences := make([][]byte, 0, len(tracks))
|
window := notes
|
||||||
for _, t := range tracks {
|
for _, patIndex := range sequence {
|
||||||
var sequence []byte
|
elementsCopied := copy(window, patterns[patIndex])
|
||||||
for s := 0; s < len(t); s += patternLength {
|
window = window[elementsCopied:]
|
||||||
pat := t[s : s+patternLength]
|
}
|
||||||
if len(pat) < patternLength {
|
return notes
|
||||||
extension := make([]int, patternLength-len(pat))
|
}
|
||||||
for i := range extension {
|
|
||||||
if pat[len(pat)-1] > 0 && i == 0 {
|
// markDontCares goes through a linear array of notes and marks every hold (1)
|
||||||
extension[i] = 0
|
// or release (0) after the first release (0) as -1 or "don't care". This means
|
||||||
} else {
|
// that for -1:s, we don't care if it's a hold or release; it does not affect
|
||||||
extension[i] = -1
|
// the sound as the note has been already released.
|
||||||
}
|
func markDontCares(notes []int) []int {
|
||||||
}
|
notesWithDontCares := make([]int, len(notes))
|
||||||
pat = append(pat, extension...)
|
dontCare := false
|
||||||
|
for i, n := range notes {
|
||||||
|
if dontCare && n <= 1 {
|
||||||
|
notesWithDontCares[i] = -1
|
||||||
|
} else {
|
||||||
|
notesWithDontCares[i] = n
|
||||||
|
dontCare = n == 0
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return notesWithDontCares
|
||||||
|
}
|
||||||
|
|
||||||
|
// replaceInt replaces all occurrences of needle in the haystack with the value
|
||||||
|
// "with"
|
||||||
|
func replaceInts(haystack []int, needle int, with int) {
|
||||||
|
for i, v := range haystack {
|
||||||
|
if v == needle {
|
||||||
|
haystack[i] = with
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// splitSequence splits a linear sequence of notes into patternLength size
|
||||||
|
// chunks. If the last chunk is shorter than the patternLength, then it is
|
||||||
|
// padded with dontCares (-1).
|
||||||
|
func splitSequence(sequence []int, patternLength int) [][]int {
|
||||||
|
numChunksRoundedUp := (len(sequence) + patternLength - 1) / patternLength
|
||||||
|
chunks := make([][]int, numChunksRoundedUp)
|
||||||
|
for i := range chunks {
|
||||||
|
if len(sequence) >= patternLength {
|
||||||
|
chunks[i], sequence = sequence[:patternLength], sequence[patternLength:]
|
||||||
|
} else {
|
||||||
|
padded := make([]int, patternLength)
|
||||||
|
j := copy(padded, sequence)
|
||||||
|
for ; j < patternLength; j++ {
|
||||||
|
padded[j] = -1
|
||||||
}
|
}
|
||||||
// go through the current pattern table to see if there's already a
|
chunks[i] = padded
|
||||||
// pattern that could be used
|
}
|
||||||
patternIndex := -1
|
}
|
||||||
for j, p := range patternTable {
|
return chunks
|
||||||
match := true
|
}
|
||||||
for k, n := range p {
|
|
||||||
if (n > -1 && pat[k] > -1 && n != pat[k]) ||
|
// addPatternsToTable adds given patterns to the table, checking if existing
|
||||||
(n == -1 && pat[k] > 1) ||
|
// pattern could be used. DontCares are taken into account so a pattern that has
|
||||||
(n > 1 && pat[k] == -1) {
|
// don't care where another has a hold or release is ok. It returns a 1D
|
||||||
match = false
|
// sequence of indices of each added pattern in the updated pattern table & the
|
||||||
break
|
// updated pattern table.
|
||||||
}
|
func addPatternsToTable(patterns [][]int, table [][]int) ([]int, [][]int) {
|
||||||
}
|
updatedTable := make([][]int, len(table))
|
||||||
if match {
|
copy(updatedTable, table) // avoid updating the underlying slices for concurrency safety
|
||||||
// if there was any don't cares in the pattern table where
|
sequence := make([]int, len(patterns))
|
||||||
// the new pattern has non don't cares, copy them to the
|
for i, pat := range patterns {
|
||||||
// patterns that was already in the pattern table
|
// go through the current pattern table to see if there's already a
|
||||||
for k, n := range pat {
|
// pattern that could be used
|
||||||
if n != -1 {
|
patternIndex := -1
|
||||||
patternTable[j][k] = n
|
for j, p := range updatedTable {
|
||||||
}
|
match := true
|
||||||
}
|
identical := true
|
||||||
patternIndex = j
|
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
|
break
|
||||||
}
|
}
|
||||||
|
if n != pat[i] {
|
||||||
|
identical = false
|
||||||
|
}
|
||||||
}
|
}
|
||||||
if patternIndex == -1 {
|
if match {
|
||||||
patternIndex = len(patternTable)
|
if !identical {
|
||||||
patternTable = append(patternTable, pat)
|
// the patterns were not identical; one of them had don't
|
||||||
}
|
// cares where another had hold or release so we make a new
|
||||||
if patternIndex > 255 {
|
// copy with merged data, that essentially is a max of the
|
||||||
return nil, nil, errors.New("encoding the song would result more than 256 different unique patterns")
|
// two patterns
|
||||||
}
|
mergedPat := make([]int, len(p))
|
||||||
sequence = append(sequence, byte(patternIndex))
|
copy(mergedPat, p) // make a copy instead of updating existing, for concurrency safety
|
||||||
}
|
for k, n := range pat {
|
||||||
sequences = append(sequences, sequence)
|
if n != -1 {
|
||||||
}
|
mergedPat[k] = n
|
||||||
// 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 {
|
updatedTable[j] = mergedPat
|
||||||
bytePat := make([]byte, 0, patternLength)
|
}
|
||||||
for _, n := range pat {
|
patternIndex = j
|
||||||
if n >= 0 {
|
break
|
||||||
bytePat = append(bytePat, byte(n))
|
|
||||||
} else {
|
|
||||||
bytePat = append(bytePat, 0)
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
byteTable = append(byteTable, bytePat)
|
if patternIndex == -1 {
|
||||||
|
patternIndex = len(updatedTable)
|
||||||
|
updatedTable = append(updatedTable, pat)
|
||||||
|
}
|
||||||
|
sequence[i] = patternIndex
|
||||||
}
|
}
|
||||||
return byteTable, sequences, nil
|
return sequence, updatedTable
|
||||||
|
}
|
||||||
|
|
||||||
|
func intsToBytes(array []int) ([]byte, error) {
|
||||||
|
ret := make([]byte, len(array))
|
||||||
|
for i, v := range array {
|
||||||
|
if v < 0 || v > 255 {
|
||||||
|
return nil, fmt.Errorf("when converting intsToBytes, all values should be 0 .. 255 (was: %v)", v)
|
||||||
|
}
|
||||||
|
ret[i] = byte(v)
|
||||||
|
}
|
||||||
|
return ret, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func bytesToInts(array []byte) []int {
|
||||||
|
ret := make([]int, len(array))
|
||||||
|
for i, v := range array {
|
||||||
|
ret[i] = int(v)
|
||||||
|
}
|
||||||
|
return ret
|
||||||
}
|
}
|
||||||
|
|
||||||
func (e *EncodedSong) PatternLength() int {
|
func (e *EncodedSong) PatternLength() int {
|
||||||
@ -136,11 +187,31 @@ func (e *EncodedSong) TotalRows() int {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func EncodeSong(song *sointu.Song) (*EncodedSong, error) {
|
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()
|
patLength := song.PatternRows()
|
||||||
patterns, sequences, err := constructPatterns(flattenPatterns(song), patLength)
|
sequences := make([][]byte, len(song.Tracks))
|
||||||
if err != nil {
|
var patterns [][]int
|
||||||
return nil, fmt.Errorf("error during constructPatterns: %v", err)
|
for i, t := range song.Tracks {
|
||||||
|
fixed := fixPatternLength(t.Patterns, patLength)
|
||||||
|
flat := flattenSequence(fixed, bytesToInts(t.Sequence))
|
||||||
|
dontCares := markDontCares(flat)
|
||||||
|
// TODO: we could give the user the possibility to use another length during encoding that during composing
|
||||||
|
chunks := splitSequence(dontCares, patLength)
|
||||||
|
var sequence []int
|
||||||
|
sequence, patterns = addPatternsToTable(chunks, patterns)
|
||||||
|
var err error
|
||||||
|
sequences[i], err = intsToBytes(sequence)
|
||||||
|
if err != nil {
|
||||||
|
return nil, errors.New("the constructed pattern table would result in > 256 unique patterns; only 256 unique patterns are supported")
|
||||||
|
}
|
||||||
}
|
}
|
||||||
return &EncodedSong{Patterns: patterns, Sequences: sequences}, nil
|
bytePatterns := make([][]byte, len(patterns))
|
||||||
|
for i, pat := range patterns {
|
||||||
|
var err error
|
||||||
|
replaceInts(pat, -1, 0) // replace don't cares with releases
|
||||||
|
bytePatterns[i], err = intsToBytes(pat)
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("invalid note in pattern, notes should be 0 .. 255: %v", err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return &EncodedSong{Patterns: bytePatterns, Sequences: sequences}, nil
|
||||||
}
|
}
|
||||||
|
Loading…
x
Reference in New Issue
Block a user