diff --git a/compiler/encoded_song.go b/compiler/encoded_song.go index 703da45..2cdd2e3 100644 --- a/compiler/encoded_song.go +++ b/compiler/encoded_song.go @@ -15,112 +15,163 @@ type EncodedSong struct { 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) - } - } +// 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 [][]byte, 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) } - 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 } -// 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...) +// 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) []int { + sumLen := 0 + for _, patIndex := range sequence { + sumLen += len(patterns[patIndex]) + } + notes := make([]int, sumLen) + window := notes + for _, patIndex := range sequence { + elementsCopied := copy(window, patterns[patIndex]) + window = window[elementsCopied:] + } + 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 } - // 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 + 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 n != pat[i] { + identical = false + } } - 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) + 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 } } - 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 { @@ -136,11 +187,31 @@ func (e *EncodedSong) TotalRows() int { } 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) + sequences := make([][]byte, len(song.Tracks)) + var patterns [][]int + 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 }