sointu/vm/compiler/patterns.go
5684185+vsariola@users.noreply.github.com d92426a100 feat!: rewrote the GUI and model for better testability
The Model was getting unmaintanable mess. This is an attempt to refactor/rewrite the Model so that data of certain type is exposed in standardized way, offering certain standard manipulations for that data type, and on the GUI side, certain standard widgets to tied to that data.

This rewrite closes #72, #106 and #120.
2024-02-17 18:16:06 +02:00

199 lines
5.8 KiB
Go

package compiler
import (
"errors"
"fmt"
"github.com/vsariola/sointu"
)
// flattenSequence returns the notes of a track in a single linear array of
// integer notes.
func flattenSequence(t sointu.Track, songLength int, rowsPerPattern int, releaseFirst bool) []int {
sumLen := rowsPerPattern * songLength
notes := make([]int, sumLen)
k := 0
for i := 0; i < songLength; i++ {
for j := 0; j < rowsPerPattern; j++ {
note := int(t.Note(sointu.SongPos{OrderRow: i, PatternRow: j}))
if releaseFirst && i == 0 && j == 0 && note == 1 {
note = 0
}
notes[k] = note
k++
}
}
return notes
}
// markDontCares goes through a linear array of notes and marks every hold (1)
// or release (0) after the first release (0) as -1 or "don't care". This means
// that for -1:s, we don't care if it's a hold or release; it does not affect
// the sound as the note has been already released.
func markDontCares(notes []int) []int {
notesWithDontCares := make([]int, len(notes))
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
}
chunks[i] = padded
}
}
return chunks
}
// addPatternsToTable adds given patterns to the table, checking if existing
// pattern could be used. DontCares are taken into account so a pattern that has
// don't care where another has a hold or release is ok. It returns a 1D
// sequence of indices of each added pattern in the updated pattern table & the
// updated pattern table.
func addPatternsToTable(patterns [][]int, table [][]int) ([]int, [][]int) {
updatedTable := make([][]int, len(table))
copy(updatedTable, table) // avoid updating the underlying slices for concurrency safety
sequence := make([]int, len(patterns))
for i, pat := range patterns {
// go through the current pattern table to see if there's already a
// pattern that could be used
patternIndex := -1
for j, p := range updatedTable {
match := true
identical := 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 (i < len(pat) && n != pat[i]) || n != 1 {
identical = false
}
}
if match {
if !identical {
// the patterns were not identical; one of them had don't
// cares where another had hold or release so we make a new
// copy with merged data, that essentially is a max of the
// two patterns
mergedPat := make([]int, len(p))
copy(mergedPat, p) // make a copy instead of updating existing, for concurrency safety
for k, n := range pat {
if n != -1 {
mergedPat[k] = n
}
}
updatedTable[j] = mergedPat
}
patternIndex = j
break
}
}
if patternIndex == -1 {
patternIndex = len(updatedTable)
updatedTable = append(updatedTable, pat)
}
sequence[i] = patternIndex
}
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 ConstructPatterns(song *sointu.Song) ([][]byte, [][]byte, error) {
sequences := make([][]byte, len(song.Score.Tracks))
var patterns [][]int
for i, t := range song.Score.Tracks {
flat := flattenSequence(t, song.Score.Length, song.Score.RowsPerPattern, true)
dontCares := markDontCares(flat)
// TODO: we could give the user the possibility to use another length during encoding that during composing
chunks := splitSequence(dontCares, song.Score.RowsPerPattern)
var sequence []int
sequence, patterns = addPatternsToTable(chunks, patterns)
var err error
sequences[i], err = intsToBytes(sequence)
if err != nil {
return nil, nil, errors.New("the constructed pattern table would result in > 256 unique patterns; only 256 unique patterns are supported")
}
}
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, nil, fmt.Errorf("invalid note in pattern, notes should be 0 .. 255: %v", err)
}
}
allZeroIndex := -1
for i, pat := range patterns {
match := true
for _, note := range pat {
if note != 0 {
match = false
break
}
}
if match {
allZeroIndex = i
break
}
}
// if there's a pattern full of zeros...
if allZeroIndex > -1 {
// ...swap that into position 0, as it is likely the most common pattern
bytePatterns[allZeroIndex], bytePatterns[0] = bytePatterns[0], bytePatterns[allZeroIndex]
for _, s := range sequences {
for j, n := range s {
if n == 0 {
s[j] = byte(allZeroIndex)
} else if n == byte(allZeroIndex) {
s[j] = 0
}
}
}
}
return bytePatterns, sequences, nil
}