diff --git a/compiler/compiler.go b/compiler/compiler.go index ac30db9..70a63f9 100644 --- a/compiler/compiler.go +++ b/compiler/compiler.go @@ -86,7 +86,7 @@ func (com *Compiler) Song(song *sointu.Song) (map[string]string, error) { if err != nil { return nil, fmt.Errorf(`could not encode patch: %v`, err) } - encodedSong, err := EncodeSong(song) + patterns, sequences, err := ConstructPatterns(song) if err != nil { return nil, fmt.Errorf(`could not encode song: %v`, err) } @@ -104,9 +104,12 @@ func (com *Compiler) Song(song *sointu.Song) (map[string]string, error) { X86Macros SongMacros *EncodedPatch - EncodedSong *EncodedSong - Hold int - }{compilerMacros, featureSetMacros, x86Macros, songMacros, encodedPatch, encodedSong, 1} + Patterns [][]byte + Sequences [][]byte + PatternLength int + SequenceLength int + Hold int + }{compilerMacros, featureSetMacros, x86Macros, songMacros, encodedPatch, patterns, sequences, len(patterns[0]), len(sequences[0]), 1} populatedTemplate, extension, err = com.compile(templateName, &data) } else if com.Arch == "wasm" { wasmMacros := *NewWasmMacros() @@ -116,9 +119,12 @@ func (com *Compiler) Song(song *sointu.Song) (map[string]string, error) { WasmMacros SongMacros *EncodedPatch - EncodedSong *EncodedSong - Hold int - }{compilerMacros, featureSetMacros, wasmMacros, songMacros, encodedPatch, encodedSong, 1} + Patterns [][]byte + Sequences [][]byte + PatternLength int + SequenceLength int + Hold int + }{compilerMacros, featureSetMacros, wasmMacros, songMacros, encodedPatch, patterns, sequences, len(patterns[0]), len(sequences[0]), 1} populatedTemplate, extension, err = com.compile(templateName, &data) } if err != nil { diff --git a/compiler/encoded_song_test.go b/compiler/encoded_song_test.go deleted file mode 100644 index c6c10f8..0000000 --- a/compiler/encoded_song_test.go +++ /dev/null @@ -1,78 +0,0 @@ -package compiler_test - -import ( - "reflect" - "testing" - - "github.com/vsariola/sointu" - "github.com/vsariola/sointu/compiler" -) - -func TestPatternReusing(t *testing.T) { - song := sointu.Song{ - Tracks: []sointu.Track{{ - Patterns: [][]byte{{64, 1, 1, 1, 0, 0, 0, 0}, {72, 0, 0, 0, 0, 0, 0, 0}}, - Sequence: []byte{0, 1}, - }, { - Patterns: [][]byte{{64, 1, 1, 1, 0, 0, 0, 0}, {84, 0, 0, 0, 0, 0, 0, 0}}, - Sequence: []byte{0, 1}, - }}, - } - encodedSong, err := compiler.EncodeSong(&song) - if err != nil { - t.Fatalf("song encoding error: %v", err) - } - expected := compiler.EncodedSong{ - Sequences: [][]byte{{0, 1}, {0, 2}}, - Patterns: [][]byte{{64, 1, 1, 1, 0, 0, 0, 0}, {72, 0, 0, 0, 0, 0, 0, 0}, {84, 0, 0, 0, 0, 0, 0, 0}}, - } - if !reflect.DeepEqual(*encodedSong, expected) { - t.Fatalf("got different EncodedSong than expected. got: %v expected: %v", *encodedSong, expected) - } -} - -func TestUnnecessaryHolds(t *testing.T) { - song := sointu.Song{ - Tracks: []sointu.Track{{ - Patterns: [][]byte{{64, 1, 1, 1, 0, 1, 0, 0}, {72, 0, 1, 0, 1, 0, 0, 0}}, - Sequence: []byte{0, 1}, - }, { - Patterns: [][]byte{{64, 1, 1, 1, 0, 0, 1, 0}, {84, 0, 0, 0, 1, 1, 0, 0}}, - Sequence: []byte{0, 1}, - }}, - } - encodedSong, err := compiler.EncodeSong(&song) - if err != nil { - t.Fatalf("song encoding error: %v", err) - } - expected := compiler.EncodedSong{ - Sequences: [][]byte{{0, 1}, {0, 2}}, - Patterns: [][]byte{{64, 1, 1, 1, 0, 0, 0, 0}, {72, 0, 0, 0, 0, 0, 0, 0}, {84, 0, 0, 0, 0, 0, 0, 0}}, - } - if !reflect.DeepEqual(*encodedSong, expected) { - t.Fatalf("got different EncodedSong than expected. got: %v expected: %v", *encodedSong, expected) - } -} - -func TestDontCares(t *testing.T) { - song := sointu.Song{ - Tracks: []sointu.Track{{ - Patterns: [][]byte{{64, 1, 1, 1, 0, 0, 0, 0}, {0, 0, 0, 0, 0, 0, 0, 0}}, - Sequence: []byte{0, 1}, - }, { - Patterns: [][]byte{{64, 1, 1, 1, 1, 1, 1, 1}, {1, 1, 1, 0, 0, 0, 0, 0}}, - Sequence: []byte{0, 1}, - }}, - } - encodedSong, err := compiler.EncodeSong(&song) - if err != nil { - t.Fatalf("song encoding error: %v", err) - } - expected := compiler.EncodedSong{ - Sequences: [][]byte{{0, 1}, {2, 1}}, - Patterns: [][]byte{{64, 1, 1, 1, 0, 0, 0, 0}, {1, 1, 1, 0, 0, 0, 0, 0}, {64, 1, 1, 1, 1, 1, 1, 1}}, - } - if !reflect.DeepEqual(*encodedSong, expected) { - t.Fatalf("got different EncodedSong than expected. got: %v expected: %v", *encodedSong, expected) - } -} diff --git a/compiler/encoded_song.go b/compiler/patterns.go similarity index 87% rename from compiler/encoded_song.go rename to compiler/patterns.go index 2cdd2e3..b0ed2fc 100644 --- a/compiler/encoded_song.go +++ b/compiler/patterns.go @@ -7,14 +7,6 @@ import ( "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 -} - // 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. @@ -174,19 +166,7 @@ func bytesToInts(array []byte) []int { return ret } -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) { +func ConstructPatterns(song *sointu.Song) ([][]byte, [][]byte, error) { patLength := song.PatternRows() sequences := make([][]byte, len(song.Tracks)) var patterns [][]int @@ -201,7 +181,7 @@ func EncodeSong(song *sointu.Song) (*EncodedSong, error) { 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 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)) @@ -210,8 +190,8 @@ func EncodeSong(song *sointu.Song) (*EncodedSong, 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 nil, nil, fmt.Errorf("invalid note in pattern, notes should be 0 .. 255: %v", err) } } - return &EncodedSong{Patterns: bytePatterns, Sequences: sequences}, nil + return bytePatterns, sequences, nil } diff --git a/compiler/patterns_test.go b/compiler/patterns_test.go new file mode 100644 index 0000000..8d37094 --- /dev/null +++ b/compiler/patterns_test.go @@ -0,0 +1,81 @@ +package compiler_test + +import ( + "reflect" + "testing" + + "github.com/vsariola/sointu" + "github.com/vsariola/sointu/compiler" +) + +func TestPatternReusing(t *testing.T) { + song := sointu.Song{ + Tracks: []sointu.Track{{ + Patterns: [][]byte{{64, 1, 1, 1, 0, 0, 0, 0}, {72, 0, 0, 0, 0, 0, 0, 0}}, + Sequence: []byte{0, 1}, + }, { + Patterns: [][]byte{{64, 1, 1, 1, 0, 0, 0, 0}, {84, 0, 0, 0, 0, 0, 0, 0}}, + Sequence: []byte{0, 1}, + }}, + } + patterns, sequences, err := compiler.ConstructPatterns(&song) + if err != nil { + t.Fatalf("erorr constructing patterns: %v", err) + } + expectedSequences := [][]byte{{0, 1}, {0, 2}} + expectedPatterns := [][]byte{{64, 1, 1, 1, 0, 0, 0, 0}, {72, 0, 0, 0, 0, 0, 0, 0}, {84, 0, 0, 0, 0, 0, 0, 0}} + if !reflect.DeepEqual(patterns, expectedPatterns) { + t.Fatalf("got different patterns than expected. got: %v expected: %v", patterns, expectedPatterns) + } + if !reflect.DeepEqual(sequences, expectedSequences) { + t.Fatalf("got different patterns than expected. got: %v expected: %v", patterns, expectedPatterns) + } +} + +func TestUnnecessaryHolds(t *testing.T) { + song := sointu.Song{ + Tracks: []sointu.Track{{ + Patterns: [][]byte{{64, 1, 1, 1, 0, 1, 0, 0}, {72, 0, 1, 0, 1, 0, 0, 0}}, + Sequence: []byte{0, 1}, + }, { + Patterns: [][]byte{{64, 1, 1, 1, 0, 0, 1, 0}, {84, 0, 0, 0, 1, 1, 0, 0}}, + Sequence: []byte{0, 1}, + }}, + } + patterns, sequences, err := compiler.ConstructPatterns(&song) + if err != nil { + t.Fatalf("erorr constructing patterns: %v", err) + } + expectedSequences := [][]byte{{0, 1}, {0, 2}} + expectedPatterns := [][]byte{{64, 1, 1, 1, 0, 0, 0, 0}, {72, 0, 0, 0, 0, 0, 0, 0}, {84, 0, 0, 0, 0, 0, 0, 0}} + if !reflect.DeepEqual(patterns, expectedPatterns) { + t.Fatalf("got different patterns than expected. got: %v expected: %v", patterns, expectedPatterns) + } + if !reflect.DeepEqual(sequences, expectedSequences) { + t.Fatalf("got different patterns than expected. got: %v expected: %v", patterns, expectedPatterns) + } +} + +func TestDontCares(t *testing.T) { + song := sointu.Song{ + Tracks: []sointu.Track{{ + Patterns: [][]byte{{64, 1, 1, 1, 0, 0, 0, 0}, {0, 0, 0, 0, 0, 0, 0, 0}}, + Sequence: []byte{0, 1}, + }, { + Patterns: [][]byte{{64, 1, 1, 1, 1, 1, 1, 1}, {1, 1, 1, 0, 0, 0, 0, 0}}, + Sequence: []byte{0, 1}, + }}, + } + patterns, sequences, err := compiler.ConstructPatterns(&song) + if err != nil { + t.Fatalf("erorr constructing patterns: %v", err) + } + expectedSequences := [][]byte{{0, 1}, {2, 1}} + expectedPatterns := [][]byte{{64, 1, 1, 1, 0, 0, 0, 0}, {1, 1, 1, 0, 0, 0, 0, 0}, {64, 1, 1, 1, 1, 1, 1, 1}} + if !reflect.DeepEqual(patterns, expectedPatterns) { + t.Fatalf("got different patterns than expected. got: %v expected: %v", patterns, expectedPatterns) + } + if !reflect.DeepEqual(sequences, expectedSequences) { + t.Fatalf("got different patterns than expected. got: %v expected: %v", patterns, expectedPatterns) + } +} diff --git a/templates/amd64-386/player.asm b/templates/amd64-386/player.asm index 66fefd6..bc18a39 100644 --- a/templates/amd64-386/player.asm +++ b/templates/amd64-386/player.asm @@ -61,7 +61,7 @@ su_render_sampleloop: ; loop through every sample in the row jl su_render_sampleloop {{.Pop .AX}} ; Stack: pushad ptr inc eax - cmp eax, {{.EncodedSong.TotalRows}} + cmp eax, {{mul .PatternLength .SequenceLength}} jl su_render_rowloop ; rewind the stack the entropy of multiple pop {{.AX}} is probably lower than add {{- range slice .Stacklocs $prologsize}} @@ -91,7 +91,7 @@ su_render_sampleloop: ; loop through every sample in the row {{- if ne .VoiceTrackBitmask 0}} ; The more complicated implementation: one track can trigger multiple voices xor edx, edx - mov ebx, {{.EncodedSong.PatternLength}} ; we could do xor ebx,ebx; mov bl,PATTERN_SIZE, but that would limit patternsize to 256... + mov ebx, {{.PatternLength}} ; we could do xor ebx,ebx; mov bl,PATTERN_SIZE, but that would limit patternsize to 256... div ebx ; eax = current pattern, edx = current row in pattern {{.Prepare "su_tracks"}} lea {{.SI}}, [{{.Use "su_tracks"}}+{{.AX}}] ; esi points to the pattern data for current track @@ -100,7 +100,7 @@ su_render_sampleloop: ; loop through every sample in the row mov {{.BP}}, {{.PTRWORD}} su_synth_obj ; ebp points to the current_voiceno array su_update_voices_trackloop: movzx eax, byte [{{.SI}}] ; eax = current pattern - imul eax, {{.EncodedSong.PatternLength}} ; eax = offset to current pattern data + imul eax, {{.PatternLength}} ; eax = offset to current pattern data {{- .Prepare "su_patterns" .AX | indent 4}} movzx eax,byte [{{.Use "su_patterns" .AX}},{{.DX}}] ; eax = note push {{.DX}} ; Stack: ptrnrow @@ -138,7 +138,7 @@ su_update_voices_skipreset: su_update_voices_nexttrack: pop {{.BX}} ; ebx=first voice of next instrument, Stack: ptrnrow pop {{.DX}} ; edx=patrnrow - add {{.SI}}, {{.EncodedSong.SequenceLength}} + add {{.SI}}, {{.SequenceLength}} inc {{.BP}} {{- $addrname := len .Song.Tracks | printf "su_synth_obj + %v"}} {{- .Prepare $addrname | indent 8}} @@ -149,7 +149,7 @@ su_update_voices_nexttrack: ; The simple implementation: each track triggers always the same voice xor edx, edx xor ebx, ebx - mov bl, {{.EncodedSong.PatternLength}} ; rows per pattern + mov bl, {{.PatternLength}} ; rows per pattern div ebx ; eax = current pattern, edx = current row in pattern {{- .Prepare "su_tracks" | indent 4}} lea {{.SI}}, [{{.Use "su_tracks"}}+{{.AX}}]; esi points to the pattern data for current track @@ -157,7 +157,7 @@ su_update_voices_nexttrack: mov bl, {{len .Song.Tracks}} ; MAX_TRACKS is always <= 32 so this is ok su_update_voices_trackloop: movzx eax, byte [{{.SI}}] ; eax = current pattern - imul eax, {{.EncodedSong.PatternLength}} ; multiply by rows per pattern, eax = offset to current pattern data + imul eax, {{.PatternLength}} ; multiply by rows per pattern, eax = offset to current pattern data {{- .Prepare "su_patterns" .AX | indent 8}} movzx eax, byte [{{.Use "su_patterns" .AX}} + {{.DX}}] ; ecx = note cmp al, {{.Hold}} ; anything but hold causes action @@ -173,7 +173,7 @@ su_update_voices_retrigger: su_update_voices_nexttrack: add {{.DI}}, su_voice.size su_update_voices_skipadd: - add {{.SI}}, {{.EncodedSong.SequenceLength}} + add {{.SI}}, {{.SequenceLength}} dec ebx jnz short su_update_voices_trackloop ret @@ -185,7 +185,7 @@ su_update_voices_skipadd: ; Patterns ;------------------------------------------------------------------------------- {{.Data "su_patterns"}} -{{- range .EncodedSong.Patterns}} +{{- range .Patterns}} db {{. | toStrings | join ","}} {{- end}} @@ -193,7 +193,7 @@ su_update_voices_skipadd: ; Tracks ;------------------------------------------------------------------------------- {{.Data "su_tracks"}} -{{- range .EncodedSong.Sequences}} +{{- range .Sequences}} db {{. | toStrings | join ","}} {{- end}} diff --git a/templates/wasm/player.wat b/templates/wasm/player.wat index acc41b3..85bd87a 100644 --- a/templates/wasm/player.wat +++ b/templates/wasm/player.wat @@ -7,7 +7,7 @@ */}} {{- .SetLabel "su_patterns"}} {{- $m := .}} -{{- range .EncodedSong.Patterns}} +{{- range .Patterns}} {{- range .}} {{- $.DataB .}} {{- end}} @@ -20,7 +20,7 @@ */}} {{- .SetLabel "su_tracks"}} {{- $m := .}} -{{- range .EncodedSong.Sequences}} +{{- range .Sequences}} {{- range .}} {{- $.DataB .}} {{- end}} @@ -111,7 +111,7 @@ ;; TODO: only export start and length with certain compiler options; in demo use, they can be hard coded ;; in the intro (global $outputStart (export "s") i32 (i32.const 8388608)) ;; TODO: do not hard code, layout memory somehow intelligently -(global $outputLength (export "l") i32 (i32.const {{if .Output16Bit}}{{mul .EncodedSong.TotalRows .Song.SamplesPerRow 4}}{{else}}{{mul .EncodedSong.TotalRows .Song.SamplesPerRow 8}}{{end}})) +(global $outputLength (export "l") i32 (i32.const {{if .Output16Bit}}{{mul .PatternLength .SequenceLength .Song.SamplesPerRow 4}}{{else}}{{mul .PatternLength .SequenceLength .Song.SamplesPerRow 8}}{{end}})) (global $output16bit (export "t") i32 (i32.const {{if .Output16Bit}}1{{else}}0{{end}})) @@ -183,17 +183,17 @@ (br_if $sample_loop (i32.lt_s (global.get $sample) (i32.const {{.Song.SamplesPerRow}}))) end (global.set $row (i32.add (global.get $row) (i32.const 1))) - (br_if $row_loop (i32.lt_s (global.get $row) (i32.const {{.EncodedSong.PatternLength}}))) + (br_if $row_loop (i32.lt_s (global.get $row) (i32.const {{.PatternLength}}))) end (global.set $pattern (i32.add (global.get $pattern) (i32.const 1))) - (br_if $pattern_loop (i32.lt_s (global.get $pattern) (i32.const {{.EncodedSong.SequenceLength}}))) + (br_if $pattern_loop (i32.lt_s (global.get $pattern) (i32.const {{.SequenceLength}}))) end ) {{- if ne .VoiceTrackBitmask 0}} ;; the complex implementation of update_voices: at least one track has more than one voice (func $su_update_voices (local $si i32) (local $di i32) (local $tracksRemaining i32) (local $note i32) (local $firstVoice i32) (local $nextTrackStartsAt i32) (local $numVoices i32) (local $voiceNo i32) - (local.set $tracksRemaining (i32.const {{len .EncodedSong.Sequences}})) + (local.set $tracksRemaining (i32.const {{len .Sequences}})) (local.set $si (global.get $pattern)) (local.set $nextTrackStartsAt (i32.const 0)) loop $track_loop @@ -212,7 +212,7 @@ br_if $voiceLoop end (i32.load8_u offset={{index .Labels "su_tracks"}} (local.get $si)) - (i32.mul (i32.const {{.EncodedSong.PatternLength}})) + (i32.mul (i32.const {{.PatternLength}})) (i32.add (global.get $row)) (i32.load8_u offset={{index .Labels "su_patterns"}}) (local.tee $note) @@ -246,7 +246,7 @@ (i32.store8 offset=768 (local.get $tracksRemaining) (local.get $voiceNo)) )) )) - (local.set $si (i32.add (local.get $si) (i32.const {{.EncodedSong.SequenceLength}}))) + (local.set $si (i32.add (local.get $si) (i32.const {{.SequenceLength}}))) (br_if $track_loop (local.tee $tracksRemaining (i32.sub (local.get $tracksRemaining) (i32.const 1)))) end ) @@ -254,12 +254,12 @@ {{- else}} ;; the simple implementation of update_voices: each track has exactly one voice (func $su_update_voices (local $si i32) (local $di i32) (local $tracksRemaining i32) (local $note i32) - (local.set $tracksRemaining (i32.const {{len .EncodedSong.Sequences}})) + (local.set $tracksRemaining (i32.const {{len .Sequences}})) (local.set $si (global.get $pattern)) (local.set $di (i32.const 4160)) loop $track_loop (i32.load8_u offset={{index .Labels "su_tracks"}} (local.get $si)) - (i32.mul (i32.const {{.EncodedSong.PatternLength}})) + (i32.mul (i32.const {{.PatternLength}})) (i32.add (global.get $row)) (i32.load8_u offset={{index .Labels "su_patterns"}}) (local.tee $note) @@ -271,7 +271,7 @@ )) )) (local.set $di (i32.add (local.get $di) (i32.const 4096))) - (local.set $si (i32.add (local.get $si) (i32.const {{.EncodedSong.SequenceLength}}))) + (local.set $si (i32.add (local.get $si) (i32.const {{.SequenceLength}}))) (br_if $track_loop (local.tee $tracksRemaining (i32.sub (local.get $tracksRemaining) (i32.const 1)))) end )