sointu/vm/patterns.go

235 lines
7.0 KiB
Go

package vm
import (
"errors"
"fmt"
"github.com/vsariola/sointu"
)
// fixPatternLength makes sure that every pattern is the same length. During
// composing. Patterns shorter than the given length are padded with 1 / "hold";
// patterns longer than the given length are cropped.
func fixPatternLength(patterns []sointu.Pattern, fixedLength int) [][]int {
patternData := make([]int, len(patterns)*fixedLength)
ret := make([][]int, len(patterns))
for i, pat := range patterns {
for j, note := range pat {
patternData[j] = int(note)
}
for j := len(pat); j < fixedLength; j++ {
patternData[j] = 1 // pad with hold
}
ret[i], patternData = patternData[:fixedLength], patternData[fixedLength:]
}
return ret
}
// flattenSequence looks up a sequence of patterns and concatenates them into a
// single linear array of notes. Note that variable length patterns are
// concatenated as such; call fixPatternLength first if you want every pattern
// to be constant length.
func flattenSequence(patterns [][]int, sequence []int, songLength int, rowsPerPattern int, releaseFirst bool) []int {
sumLen := rowsPerPattern * songLength
notes := make([]int, sumLen)
window := notes
for i := 0; i < songLength; i++ {
patIndex := -1
if i < len(sequence) {
patIndex = sequence[i]
}
var k int
if patIndex >= 0 && patIndex < len(patterns) {
k = copy(window, patterns[patIndex])
}
for ; k < rowsPerPattern; k++ {
window[k] = 1 // pad with holds
}
if i == 0 && window[0] == 1 && releaseFirst {
window[0] = 0
}
window = window[rowsPerPattern:]
}
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 bytesToInts(array []byte) []int {
ret := make([]int, len(array))
for i, v := range array {
ret[i] = int(v)
}
return ret
}
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 {
fixed := fixPatternLength(t.Patterns, song.Score.RowsPerPattern)
flat := flattenSequence(fixed, t.Order, 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
}