feat: add the ability to use Sointu as a sync-tracker

There is a new "sync" opcode that saves the top-most signal every 256 samples to the new "syncBuffer" output. Additionally, you can enable saving the current fractional row as sync[0], avoiding calculating the beat in the shader, but also calculating the beat correctly when the beat is modulated.
This commit is contained in:
vsariola 2021-03-09 23:47:27 +02:00
parent a3bdf565fd
commit 99dbdfe223
30 changed files with 375 additions and 88 deletions

View File

@ -313,6 +313,20 @@ New features since fork
- **A bytecode interpreter written in pure go**. It's slightly slower than the - **A bytecode interpreter written in pure go**. It's slightly slower than the
hand-written assembly code by sointu compiler, but with this, the tracker is hand-written assembly code by sointu compiler, but with this, the tracker is
ultraportable and does not need cgo calls. ultraportable and does not need cgo calls.
- **Using Sointu as a sync-tracker**. Similar to [GNU
Rocket](https://github.com/yupferris/gnurocket), but (ab)using the tracker
we already have for music. We use the Go "rpc" package to send current sync
values from the new "sync" opcode + optionally the current fractional row
the song is on. The syncs are saved every 256th sample (approximately 172
Hz). For 4k intro development, the idea is to write a debug version of the
intro that merely loads the shader and listens to the RPC messages, and then
draws the shader with those as the uniforms. Then, during the actual 4k
intro, one can get sync the data from Sointu: when using syncs,
su_render_song takes two buffer parameters, one for sound, another for
syncs. These can then be sent to the shader as a uniform float array. A
track with two voices, triggering an instrument with a single envelope and a
slow filter can even be used as a cheap smooth interpolation mechanism,
provided the syncs are added to each other in the shader.
Future goals Future goals
------------ ------------
@ -327,15 +341,6 @@ Future goals
bit flag in the existing filter bit flag in the existing filter
- Arbitrary envelopes; for easier automation. - Arbitrary envelopes; for easier automation.
- **MIDI support for the tracker**. - **MIDI support for the tracker**.
- **Reintroduce the sync mechanism**. 4klang could export the envelopes of all
instruments at a 256 times lower frequency, with the purpose of using them
as sync data. This feature was removed at some point, but should be
reintroduced at some point. Need to investigate the best way to implement
this; maybe a "sync" opcode that save the current signal from the stack? Or
reusing sends/outs and having special sync output ports, allowing easily
combining multiple signals into one sync. Oh, and we probably should dump
the whole thing also as a texture to the shader; to fly through the song, in
a very literal way.
- **Find a solution for denormalized signals**. Denormalized floating point - **Find a solution for denormalized signals**. Denormalized floating point
numbers (floating point numbers that are very very small) can result in 100x numbers (floating point numbers that are very very small) can result in 100x
CPU slow down. We got hit by this already: the damp filters in delay units CPU slow down. We got hit by this already: the damp filters in delay units
@ -347,16 +352,6 @@ Future goals
Crazy ideas Crazy ideas
----------- -----------
- **Using Sointu as a sync-tracker**. Similar to [GNU
Rocket](https://github.com/yupferris/gnurocket), but (ab)using the tracker
we already have for music. We could define a generic RPC protocol for Sointu
tracker send current sync values and time; one could then write a debug
version of a 4k intro that merely loads the shader and listens to the RPC
messages, and then draws the shader with those as the uniforms. Then, during
the actual 4k intro, just render song, get sync data from Sointu and send as
uniforms to shader. A track with two voices, triggering an instrument with a
single envelope and a slow filter can even be used as a cheap smooth
interpolation mechanism.
- **Hack deeper into audio sources from the OS**. Speech synthesis, I'm eyeing - **Hack deeper into audio sources from the OS**. Speech synthesis, I'm eyeing
at you. at you.

View File

@ -33,6 +33,7 @@ func main() {
list := flag.Bool("l", false, "Do not write files; just list files that would change instead.") list := flag.Bool("l", false, "Do not write files; just list files that would change instead.")
stdout := flag.Bool("s", false, "Do not write files; write to standard output instead.") stdout := flag.Bool("s", false, "Do not write files; write to standard output instead.")
help := flag.Bool("h", false, "Show help.") help := flag.Bool("h", false, "Show help.")
rowsync := flag.Bool("r", false, "Write the current fractional row as sync #0")
library := flag.Bool("a", false, "Compile Sointu into a library. Input files are not needed.") library := flag.Bool("a", false, "Compile Sointu into a library. Input files are not needed.")
jsonOut := flag.Bool("j", false, "Output the song as .json file instead of compiling.") jsonOut := flag.Bool("j", false, "Output the song as .json file instead of compiling.")
yamlOut := flag.Bool("y", false, "Output the song as .yml file instead of compiling.") yamlOut := flag.Bool("y", false, "Output the song as .yml file instead of compiling.")
@ -53,9 +54,9 @@ func main() {
if compile || *library { if compile || *library {
var err error var err error
if *tmplDir != "" { if *tmplDir != "" {
comp, err = compiler.NewFromTemplates(*targetOs, *targetArch, *output16bit, *tmplDir) comp, err = compiler.NewFromTemplates(*targetOs, *targetArch, *output16bit, *rowsync, *tmplDir)
} else { } else {
comp, err = compiler.New(*targetOs, *targetArch, *output16bit) comp, err = compiler.New(*targetOs, *targetArch, *output16bit, *rowsync)
} }
if err != nil { if err != nil {
fmt.Fprintf(os.Stderr, `error creating compiler: %v`, err) fmt.Fprintf(os.Stderr, `error creating compiler: %v`, err)

View File

@ -17,5 +17,7 @@ func main() {
} }
defer audioContext.Close() defer audioContext.Close()
synthService := bridge.BridgeService{} synthService := bridge.BridgeService{}
gioui.Main(audioContext, synthService) // TODO: native track does not support syncing at the moment (which is why
// we pass nil), as the native bridge does not support sync data
gioui.Main(audioContext, synthService, nil)
} }

View File

@ -92,7 +92,7 @@ func main() {
synth.Release(i) synth.Release(i)
} }
} }
buffer, err := sointu.Play(synth, song) // render the song to calculate its length buffer, _, err := sointu.Play(synth, song) // render the song to calculate its length
if err != nil { if err != nil {
return fmt.Errorf("sointu.Play failed: %v", err) return fmt.Errorf("sointu.Play failed: %v", err)
} }

View File

@ -1,21 +1,33 @@
package main package main
import ( import (
"flag"
"fmt" "fmt"
"os" "os"
"github.com/vsariola/sointu/oto" "github.com/vsariola/sointu/oto"
"github.com/vsariola/sointu/rpc"
"github.com/vsariola/sointu/tracker/gioui" "github.com/vsariola/sointu/tracker/gioui"
"github.com/vsariola/sointu/vm" "github.com/vsariola/sointu/vm"
) )
func main() { func main() {
syncAddress := flag.String("address", "", "remote RPC server where to send sync data")
flag.Parse()
audioContext, err := oto.NewContext() audioContext, err := oto.NewContext()
if err != nil { if err != nil {
fmt.Println(err) fmt.Println(err)
os.Exit(1) os.Exit(1)
} }
defer audioContext.Close() defer audioContext.Close()
var syncChannel chan<- []float32
if *syncAddress != "" {
syncChannel, err = rpc.Sender(*syncAddress)
if err != nil {
fmt.Println(err)
os.Exit(1)
}
}
synthService := vm.SynthService{} synthService := vm.SynthService{}
gioui.Main(audioContext, synthService) gioui.Main(audioContext, synthService, syncChannel)
} }

View File

@ -37,6 +37,18 @@ func (p Patch) NumDelayLines() int {
return total return total
} }
func (p Patch) NumSyncs() int {
total := 0
for _, instr := range p {
for _, unit := range instr.Units {
if unit.Type == "sync" {
total += instr.NumVoices
}
}
}
return total
}
func (p Patch) FirstVoiceForInstrument(instrIndex int) int { func (p Patch) FirstVoiceForInstrument(instrIndex int) int {
ret := 0 ret := 0
for _, t := range p[:instrIndex] { for _, t := range p[:instrIndex] {

57
rpc/rpc.go Normal file
View File

@ -0,0 +1,57 @@
package rpc
import (
"fmt"
"log"
"net"
"net/http"
"net/rpc"
)
type SyncServer struct {
channel chan []float32
}
func (s *SyncServer) Sync(syncData []float32, reply *int) error {
select {
case s.channel <- syncData:
default:
}
return nil
}
func Receiver() (<-chan []float32, error) {
c := make(chan []float32, 1)
server := &SyncServer{channel: c}
rpc.Register(server)
rpc.HandleHTTP()
l, e := net.Listen("tcp", ":31337")
if e != nil {
log.Fatal("listen error:", e)
return nil, fmt.Errorf("net.listen failed: %v", e)
}
go func() {
defer close(c)
http.Serve(l, nil)
}()
return c, nil
}
func Sender(serverAddress string) (chan<- []float32, error) {
c := make(chan []float32, 256)
client, err := rpc.DialHTTP("tcp", serverAddress+":31337")
if err != nil {
log.Fatal("dialing:", err)
return nil, fmt.Errorf("rpc.DialHTTP failed: %v", err)
}
go func() {
for msg := range c {
var reply int
err = client.Call("SyncServer.Sync", msg, &reply)
if err != nil {
log.Fatal("SyncServer.Sync error:", err)
}
}
}()
return c, nil
}

24
rpc/rpc_test.go Normal file
View File

@ -0,0 +1,24 @@
package rpc_test
import (
"testing"
"github.com/vsariola/sointu/rpc"
)
func TestSendReceive(t *testing.T) {
receiver, err := rpc.Receiver()
if err != nil {
t.Fatalf("rpc.Receiver error: %v", err)
}
sender, err := rpc.Sender("127.0.0.1")
if err != nil {
t.Fatalf("rpc.Sender error: %v", err)
}
value := []float32{42}
sender <- value
valueGot := <-receiver
if valueGot[0] != value[0] {
t.Fatalf("rpc.Sender error: %v", err)
}
}

View File

@ -7,7 +7,7 @@ import (
) )
type Synth interface { type Synth interface {
Render(buffer []float32, maxtime int) (int, int, error) Render(buffer []float32, syncBuffer []float32, maxtime int) (sample int, syncs int, time int, err error)
Update(patch Patch) error Update(patch Patch) error
Trigger(voice int, note byte) Trigger(voice int, note byte)
Release(voice int) Release(voice int)
@ -18,7 +18,7 @@ type SynthService interface {
} }
func Render(synth Synth, buffer []float32) error { func Render(synth Synth, buffer []float32) error {
s, _, err := synth.Render(buffer, math.MaxInt32) s, _, _, err := synth.Render(buffer, nil, math.MaxInt32)
if err != nil { if err != nil {
return fmt.Errorf("sointu.Render failed: %v", err) return fmt.Errorf("sointu.Render failed: %v", err)
} }
@ -28,10 +28,10 @@ func Render(synth Synth, buffer []float32) error {
return nil return nil
} }
func Play(synth Synth, song Song) ([]float32, error) { func Play(synth Synth, song Song) ([]float32, []float32, error) {
err := song.Validate() err := song.Validate()
if err != nil { if err != nil {
return nil, err return nil, nil, err
} }
curVoices := make([]int, len(song.Score.Tracks)) curVoices := make([]int, len(song.Score.Tracks))
for i := range curVoices { for i := range curVoices {
@ -39,7 +39,10 @@ func Play(synth Synth, song Song) ([]float32, error) {
} }
initialCapacity := song.Score.LengthInRows() * song.SamplesPerRow() * 2 initialCapacity := song.Score.LengthInRows() * song.SamplesPerRow() * 2
buffer := make([]float32, 0, initialCapacity) buffer := make([]float32, 0, initialCapacity)
syncBuffer := make([]float32, 0, initialCapacity)
rowbuffer := make([]float32, song.SamplesPerRow()*2) rowbuffer := make([]float32, song.SamplesPerRow()*2)
numSyncs := song.Patch.NumSyncs()
syncRowBuffer := make([]float32, ((song.SamplesPerRow()+255)/256)*(1+numSyncs))
for row := 0; row < song.Score.LengthInRows(); row++ { for row := 0; row < song.Score.LengthInRows(); row++ {
patternRow := row % song.Score.RowsPerPattern patternRow := row % song.Score.RowsPerPattern
pattern := row / song.Score.RowsPerPattern pattern := row / song.Score.RowsPerPattern
@ -73,16 +76,22 @@ func Play(synth Synth, song Song) ([]float32, error) {
} }
tries := 0 tries := 0
for rowtime := 0; rowtime < song.SamplesPerRow(); { for rowtime := 0; rowtime < song.SamplesPerRow(); {
samples, time, err := synth.Render(rowbuffer, song.SamplesPerRow()-rowtime) samples, syncs, time, err := synth.Render(rowbuffer, syncRowBuffer, song.SamplesPerRow()-rowtime)
for i := 0; i < syncs; i++ {
t := syncRowBuffer[i*(1+numSyncs)]
t = (t+float32(rowtime))/(float32(song.SamplesPerRow())) + float32(row)
syncRowBuffer[i*(1+numSyncs)] = t
}
if err != nil { if err != nil {
return buffer, fmt.Errorf("render failed: %v", err) return buffer, syncBuffer, fmt.Errorf("render failed: %v", err)
} }
rowtime += time rowtime += time
buffer = append(buffer, rowbuffer[:samples*2]...) buffer = append(buffer, rowbuffer[:samples*2]...)
syncBuffer = append(syncBuffer, syncRowBuffer[:syncs]...)
if tries > 100 { if tries > 100 {
return nil, fmt.Errorf("Song speed modulation likely so slow that row never advances; error at pattern %v, row %v", pattern, patternRow) return nil, nil, fmt.Errorf("Song speed modulation likely so slow that row never advances; error at pattern %v, row %v", pattern, patternRow)
} }
} }
} }
return buffer, nil return buffer, syncBuffer, nil
} }

View File

@ -21,3 +21,24 @@
fstp dword [{{.WRK}}] ; save the remainder for future fstp dword [{{.WRK}}] ; save the remainder for future
ret ret
{{end}} {{end}}
{{- if or .RowSync (.HasOp "sync")}}
;-------------------------------------------------------------------------------
; SYNC opcode: save the stack top to sync buffer
;-------------------------------------------------------------------------------
{{.Func "su_op_sync" "Opcode"}}
{{- if not .Library}}
; TODO: syncs are NOPs when compiling as library, should figure out a way to
; make them work when compiling to use the native track also
mov {{.AX}}, [{{.Stack "GlobalTick"}}]
test al, al
jne su_op_sync_skip
xchg {{.AX}}, [{{.Stack "SyncBufPtr"}}]
fst dword [{{.AX}}]
add {{.AX}}, 4
xchg {{.AX}}, [{{.Stack "SyncBufPtr"}}]
su_op_sync_skip:
{{- end}}
ret
{{end}}

View File

@ -14,6 +14,14 @@
;------------------------------------------------------------------------------- ;-------------------------------------------------------------------------------
{{.Func "su_run_vm"}} {{.Func "su_run_vm"}}
{{- .PushRegs .CX "DelayWorkSpace" .DX "Synth" .COM "CommandStream" .WRK "Voice" .VAL "ValueStream" | indent 4}} {{- .PushRegs .CX "DelayWorkSpace" .DX "Synth" .COM "CommandStream" .WRK "Voice" .VAL "ValueStream" | indent 4}}
{{- if .RowSync}}
fild dword [{{.Stack "Sample"}}]
{{.Int .Song.SamplesPerRow | .Prepare | indent 8}}
fidiv dword [{{.Int .Song.SamplesPerRow | .Use}}]
fiadd dword [{{.Stack "Row"}}]
{{.Call "su_op_sync"}}
fstp st0
{{- end}}
su_run_vm_loop: ; loop until all voices done su_run_vm_loop: ; loop until all voices done
movzx edi, byte [{{.COM}}] ; edi = command byte movzx edi, byte [{{.COM}}] ; edi = command byte
inc {{.COM}} ; move to next instruction inc {{.COM}} ; move to next instruction

View File

@ -14,12 +14,22 @@ su_synth_obj:
; the output buffer. Renders the compile time hard-coded song to the buffer. ; the output buffer. Renders the compile time hard-coded song to the buffer.
; Stack: output_ptr ; Stack: output_ptr
;------------------------------------------------------------------------------- ;-------------------------------------------------------------------------------
{{- if or .RowSync (.HasOp "sync")}}
{{.ExportFunc "su_render_song" "OutputBufPtr" "SyncBufPtr"}}
{{- else}}
{{.ExportFunc "su_render_song" "OutputBufPtr"}} {{.ExportFunc "su_render_song" "OutputBufPtr"}}
{{- end}}
{{- if .Amd64}} {{- if .Amd64}}
{{- if eq .OS "windows"}} {{- if eq .OS "windows"}}
{{- .PushRegs "rcx" "OutputBufPtr" "rdi" "NonVolatileRsi" "rsi" "NonVolatile" "rbx" "NonVolatileRbx" "rbp" "NonVolatileRbp" | indent 4}} ; rcx = ptr to buf. rdi,rsi,rbx,rbp nonvolatile {{- .PushRegs "rcx" "OutputBufPtr" "rdi" "NonVolatileRsi" "rsi" "NonVolatile" "rbx" "NonVolatileRbx" "rbp" "NonVolatileRbp" | indent 4}} ; rcx = ptr to buf. rdi,rsi,rbx,rbp nonvolatile
{{- if or .RowSync (.HasOp "sync")}}
{{- .PushRegs "rdx" "SyncBufPtr" | indent 4}}
{{- end}}
{{- else}} ; SystemV amd64 ABI, linux mac or hopefully something similar {{- else}} ; SystemV amd64 ABI, linux mac or hopefully something similar
{{- .PushRegs "rdi" "OutputBufPtr" "rbx" "NonVolatileRbx" "rbp" "NonVolatileRbp" | indent 4}} {{- .PushRegs "rdi" "OutputBufPtr" "rbx" "NonVolatileRbx" "rbp" "NonVolatileRbp" | indent 4}}
{{- if or .RowSync (.HasOp "sync")}}
{{- .PushRegs "rsi" "SyncBufPtr" | indent 4}}
{{- end}}
{{- end}} {{- end}}
{{- else}} {{- else}}
{{- .PushRegs | indent 4}} {{- .PushRegs | indent 4}}
@ -68,6 +78,9 @@ su_render_sampleloop: ; loop through every sample in the row
{{$.Pop $.AX}} {{$.Pop $.AX}}
{{- end}} {{- end}}
{{- if .Amd64}} {{- if .Amd64}}
{{- if or .RowSync (.HasOp "sync")}}
{{.Pop .AX}} ; pop the sync buf ptr away
{{- end}}
{{- if eq .OS "windows"}} {{- if eq .OS "windows"}}
; Windows64 ABI, rdi rsi rbx rbp non-volatile ; Windows64 ABI, rdi rsi rbx rbp non-volatile
{{- .PopRegs "rcx" "rdi" "rsi" "rbx" "rbp" | indent 4}} {{- .PopRegs "rcx" "rdi" "rsi" "rbx" "rbp" | indent 4}}
@ -78,8 +91,12 @@ su_render_sampleloop: ; loop through every sample in the row
ret ret
{{- else}} {{- else}}
{{- .PopRegs | indent 4}} {{- .PopRegs | indent 4}}
{{- if or .RowSync (.HasOp "sync")}}
ret 8
{{- else}}
ret 4 ret 4
{{- end}} {{- end}}
{{- end}}
;------------------------------------------------------------------------------- ;-------------------------------------------------------------------------------
; su_update_voices function: polyphonic & chord implementation ; su_update_voices function: polyphonic & chord implementation

View File

@ -13,6 +13,15 @@
#define SU_LENGTH_IN_ROWS (SU_LENGTH_IN_PATTERNS*SU_PATTERN_SIZE) #define SU_LENGTH_IN_ROWS (SU_LENGTH_IN_PATTERNS*SU_PATTERN_SIZE)
#define SU_SAMPLES_PER_ROW (SU_SAMPLE_RATE*60/(SU_BPM*SU_ROWS_PER_BEAT)) #define SU_SAMPLES_PER_ROW (SU_SAMPLE_RATE*60/(SU_BPM*SU_ROWS_PER_BEAT))
{{- if or .RowSync (.HasOp "sync")}}
{{- if .RowSync}}
#define SU_NUMSYNCS {{add1 .Song.Patch.NumSyncs}}
{{- else}}
#define SU_NUMSYNCS {{.Song.Patch.NumSyncs}}
{{- end}}
#define SU_SYNCBUFFER_LENGTH ((SU_LENGTH_IN_SAMPLES+255)>>8)*SU_NUMSYNCS
{{- end}}
#include <stdint.h> #include <stdint.h>
#if UINTPTR_MAX == 0xffffffff #if UINTPTR_MAX == 0xffffffff
#if defined(__clang__) || defined(__GNUC__) #if defined(__clang__) || defined(__GNUC__)
@ -39,7 +48,12 @@ typedef float SUsample;
extern "C" { extern "C" {
#endif #endif
{{- if or .RowSync (.HasOp "sync")}}
void SU_CALLCONV su_render_song(SUsample *buffer,float *syncBuffer);
#define SU_SYNC
{{- else}}
void SU_CALLCONV su_render_song(SUsample *buffer); void SU_CALLCONV su_render_song(SUsample *buffer);
{{- end}}
{{- if gt (.SampleOffsets | len) 0}} {{- if gt (.SampleOffsets | len) 0}}
void SU_CALLCONV su_load_gmdls(); void SU_CALLCONV su_load_gmdls();
#define SU_LOAD_GMDLS #define SU_LOAD_GMDLS

View File

@ -1,10 +1,13 @@
function(regression_test testname) function(regression_test testname)
if(${ARGC} LESS 6) if(ARGV5)
if(${ARGC} LESS 4) set(source ${ARGV5})
add_executable(${testname} ${source} test_renderer.c)
else()
if(ARGV3)
set(source ${ARGV3}.yml)
else()
set(source ${testname}.yml) set(source ${testname}.yml)
else()
set(source ${ARGV3}.yml)
endif() endif()
set(asmfile ${testname}.asm) set(asmfile ${testname}.asm)
@ -19,7 +22,7 @@ function(regression_test testname)
add_executable(${testname} test_renderer.c ${asmfile}) add_executable(${testname} test_renderer.c ${asmfile})
target_compile_definitions(${testname} PUBLIC TEST_HEADER=<${testname}.h>) target_compile_definitions(${testname} PUBLIC TEST_HEADER=<${testname}.h>)
if (NODE AND WAT2WASM AND NOT ${testname} MATCHES "sample") if (NODE AND WAT2WASM AND NOT ${testname} MATCHES "sample" AND NOT ${testname} MATCHES "sync")
set(wasmfile ${CMAKE_CURRENT_BINARY_DIR}/${testname}.wasm) set(wasmfile ${CMAKE_CURRENT_BINARY_DIR}/${testname}.wasm)
set(watfile ${CMAKE_CURRENT_BINARY_DIR}/${testname}.wat) set(watfile ${CMAKE_CURRENT_BINARY_DIR}/${testname}.wat)
set(wasmtarget wasm_${testname}) set(wasmtarget wasm_${testname})
@ -29,31 +32,29 @@ function(regression_test testname)
DEPENDS sointu-compiler DEPENDS sointu-compiler
) )
add_test(${wasmtarget} ${NODE} ${CMAKE_CURRENT_SOURCE_DIR}/wasm_test_renderer.es6 ${wasmfile} ${CMAKE_CURRENT_SOURCE_DIR}/expected_output/${testname}.raw) add_test(${wasmtarget} ${NODE} ${CMAKE_CURRENT_SOURCE_DIR}/wasm_test_renderer.es6 ${wasmfile} ${CMAKE_CURRENT_SOURCE_DIR}/expected_output/${testname}.raw)
endif() endif()
else()
set(source ${ARGV5})
add_executable(${testname} ${source} test_renderer.c)
endif() endif()
add_test(${testname} ${testname} ${CMAKE_CURRENT_SOURCE_DIR}/expected_output/${testname}.raw) if (${testname} MATCHES "sync")
add_test(${testname} ${testname} ${CMAKE_CURRENT_SOURCE_DIR}/expected_output/${testname}.raw ${CMAKE_CURRENT_SOURCE_DIR}/expected_output/${testname}_syncbuf.raw)
else()
add_test(${testname} ${testname} ${CMAKE_CURRENT_SOURCE_DIR}/expected_output/${testname}.raw)
endif()
target_link_libraries(${testname} ${HEADERLIB}) target_link_libraries(${testname} ${HEADERLIB})
target_include_directories(${testname} PUBLIC ${CMAKE_CURRENT_BINARY_DIR}) target_include_directories(${testname} PUBLIC ${CMAKE_CURRENT_BINARY_DIR})
target_compile_definitions(${testname} PUBLIC TEST_NAME="${testname}") target_compile_definitions(${testname} PUBLIC TEST_NAME="${testname}")
if(ARGC GREATER 1) if (ARGV1)
if (ARGV1) message("${testname} requires ${ARGV1}")
message("${testname} requires ${ARGV1}") set_tests_properties(${testname} PROPERTIES FIXTURES_REQUIRED "${ARGV1}")
set_tests_properties(${testname} PROPERTIES FIXTURES_REQUIRED "${ARGV1}") endif()
endif()
if (ARGV2)
message("${testname} setups ${ARGV2}")
set_tests_properties(${testname} PROPERTIES FIXTURES_SETUP "${ARGV2}")
endif() endif()
if(ARGC GREATER 2)
if (ARGV2)
message("${testname} setups ${ARGV2}")
set_tests_properties(${testname} PROPERTIES FIXTURES_SETUP "${ARGV2}")
endif()
endif()
endfunction(regression_test) endfunction(regression_test)
regression_test(test_envelope "" ENVELOPE) regression_test(test_envelope "" ENVELOPE)
@ -157,6 +158,7 @@ regression_test(test_envelope_16bit ENVELOPE "" test_envelope "-i")
regression_test(test_polyphony "ENVELOPE;VCO_SINE") regression_test(test_polyphony "ENVELOPE;VCO_SINE")
regression_test(test_chords "ENVELOPE;VCO_SINE") regression_test(test_chords "ENVELOPE;VCO_SINE")
regression_test(test_speed "ENVELOPE;VCO_SINE") regression_test(test_speed "ENVELOPE;VCO_SINE")
regression_test(test_sync "ENVELOPE" "" "" "-r")
regression_test(test_render_samples ENVELOPE "" "" "" test_render_samples.c) regression_test(test_render_samples ENVELOPE "" "" "" test_render_samples.c)
target_link_libraries(test_render_samples ${STATICLIB}) target_link_libraries(test_render_samples ${STATICLIB})

Binary file not shown.

Binary file not shown.

View File

@ -16,6 +16,11 @@
#include TEST_HEADER #include TEST_HEADER
SUsample buf[SU_BUFFER_LENGTH]; SUsample buf[SU_BUFFER_LENGTH];
SUsample filebuf[SU_BUFFER_LENGTH]; SUsample filebuf[SU_BUFFER_LENGTH];
#ifdef SU_SYNC
float syncBuf[SU_SYNCBUFFER_LENGTH];
float fileSyncBuf[SU_BUFFER_LENGTH];
#endif
int main(int argc, char* argv[]) { int main(int argc, char* argv[]) {
FILE* f; FILE* f;
@ -36,7 +41,11 @@ int main(int argc, char* argv[]) {
su_load_gmdls(); su_load_gmdls();
#endif #endif
#ifdef SU_SYNC
su_render_song(buf, syncBuf);
#else
su_render_song(buf); su_render_song(buf);
#endif
#if defined (_WIN32) #if defined (_WIN32)
CreateDirectory(actual_output_folder, NULL); CreateDirectory(actual_output_folder, NULL);
@ -49,6 +58,13 @@ int main(int argc, char* argv[]) {
fwrite((void*)buf, sizeof(SUsample), SU_BUFFER_LENGTH, f); fwrite((void*)buf, sizeof(SUsample), SU_BUFFER_LENGTH, f);
fclose(f); fclose(f);
#ifdef SU_SYNC
snprintf(filename, sizeof filename, "%s%s%s", actual_output_folder, test_name, "_syncbuf.raw");
f = fopen(filename, "wb");
fwrite((void*)syncBuf, sizeof(float), SU_SYNCBUFFER_LENGTH, f);
fclose(f);
#endif
f = fopen(argv[1], "rb"); f = fopen(argv[1], "rb");
if (f == NULL) { if (f == NULL) {
@ -92,6 +108,42 @@ int main(int argc, char* argv[]) {
printf("Warning: Sointu rendered almost correct wave, but a small maximum error of %f\n",max_diff); printf("Warning: Sointu rendered almost correct wave, but a small maximum error of %f\n",max_diff);
} }
#ifdef SU_SYNC
f = fopen(argv[2], "rb");
if (f == NULL) {
printf("No expected sync waveform found!\n");
goto fail;
}
fseek(f, 0, SEEK_END);
fsize = ftell(f);
fseek(f, 0, SEEK_SET);
if (SU_SYNCBUFFER_LENGTH * sizeof(float) < fsize) {
printf("Sointu rendered shorter sync wave than expected\n");
goto fail;
}
if (SU_SYNCBUFFER_LENGTH * sizeof(float) > fsize) {
printf("Sointu rendered longer sync wave than expected\n");
goto fail;
}
fread((void*)fileSyncBuf, fsize, 1, f);
fclose(f);
f = NULL;
max_diff = 0.0f;
for (n = 0; n < SU_SYNCBUFFER_LENGTH; n++) {
diff = (float)fabs(syncBuf[n] - fileSyncBuf[n]);
if (diff > 1e-3f || isnan(diff)) {
printf("Sointu rendered different sync wave than expected\n");
goto fail;
}
}
#endif
return 0; return 0;
fail: fail:

20
tests/test_sync.yml Normal file
View File

@ -0,0 +1,20 @@
bpm: 100
rowsperbeat: 4
score:
rowsperpattern: 16
length: 2
tracks:
- numvoices: 1
order: [0, 0]
patterns: [[64, 1, 1, 1, 1, 1, 1, 1, 0, 0, 0, 0, 0, 0, 0, 0]]
patch:
- numvoices: 1
units:
- type: envelope
parameters: {attack: 64, decay: 64, gain: 128, release: 80, stereo: 0, sustain: 64}
- type: sync
- type: envelope
parameters: {attack: 95, decay: 64, gain: 128, release: 80, stereo: 0, sustain: 64}
- type: sync
- type: out
parameters: {gain: 128, stereo: 1}

View File

@ -40,6 +40,7 @@ var defaultUnits = map[string]sointu.Unit{
"speed": {Type: "speed", Parameters: map[string]int{}}, "speed": {Type: "speed", Parameters: map[string]int{}},
"compressor": {Type: "compressor", Parameters: map[string]int{"stereo": 0, "attack": 64, "release": 64, "invgain": 64, "threshold": 64, "ratio": 64}}, "compressor": {Type: "compressor", Parameters: map[string]int{"stereo": 0, "attack": 64, "release": 64, "invgain": 64, "threshold": 64, "ratio": 64}},
"send": {Type: "send", Parameters: map[string]int{"stereo": 0, "amount": 128, "voice": 0, "unit": 0, "port": 0, "sendpop": 1}}, "send": {Type: "send", Parameters: map[string]int{"stereo": 0, "amount": 128, "voice": 0, "unit": 0, "port": 0, "sendpop": 1}},
"sync": {Type: "sync", Parameters: map[string]int{}},
} }
var defaultInstrument = sointu.Instrument{ var defaultInstrument = sointu.Instrument{

View File

@ -51,13 +51,13 @@ func (t *Tracker) Run(w *app.Window) error {
} }
} }
func Main(audioContext sointu.AudioContext, synthService sointu.SynthService) { func Main(audioContext sointu.AudioContext, synthService sointu.SynthService, syncChannel chan<- []float32) {
go func() { go func() {
w := app.NewWindow( w := app.NewWindow(
app.Size(unit.Dp(800), unit.Dp(600)), app.Size(unit.Dp(800), unit.Dp(600)),
app.Title("Sointu Tracker"), app.Title("Sointu Tracker"),
) )
t := New(audioContext, synthService) t := New(audioContext, synthService, syncChannel)
defer t.Close() defer t.Close()
if err := t.Run(w); err != nil { if err := t.Run(w); err != nil {
fmt.Println(err) fmt.Println(err)

View File

@ -99,7 +99,7 @@ func (t *Tracker) Close() {
t.audioContext.Close() t.audioContext.Close()
} }
func New(audioContext sointu.AudioContext, synthService sointu.SynthService) *Tracker { func New(audioContext sointu.AudioContext, synthService sointu.SynthService, syncChannel chan<- []float32) *Tracker {
t := &Tracker{ t := &Tracker{
Theme: material.NewTheme(gofont.Collection()), Theme: material.NewTheme(gofont.Collection()),
audioContext: audioContext, audioContext: audioContext,
@ -160,7 +160,7 @@ func New(audioContext sointu.AudioContext, synthService sointu.SynthService) *Tr
sprObserver := make(chan int, 16) sprObserver := make(chan int, 16)
t.AddSamplesPerRowObserver(sprObserver) t.AddSamplesPerRowObserver(sprObserver)
audioChannel := make(chan []float32) audioChannel := make(chan []float32)
t.player = tracker.NewPlayer(synthService, t.playerCloser, patchObserver, scoreObserver, sprObserver, t.refresh, audioChannel, vuBufferObserver) t.player = tracker.NewPlayer(synthService, t.playerCloser, patchObserver, scoreObserver, sprObserver, t.refresh, syncChannel, audioChannel, vuBufferObserver)
audioOut := audioContext.Output() audioOut := audioContext.Output()
go func() { go func() {
for buf := range audioChannel { for buf := range audioChannel {

View File

@ -66,13 +66,16 @@ func (p *Player) Enabled() bool {
return atomic.LoadInt32(&p.synthNotNil) == 1 return atomic.LoadInt32(&p.synthNotNil) == 1
} }
func NewPlayer(service sointu.SynthService, closer <-chan struct{}, patchs <-chan sointu.Patch, scores <-chan sointu.Score, samplesPerRows <-chan int, posChanged chan<- struct{}, outputs ...chan<- []float32) *Player { func NewPlayer(service sointu.SynthService, closer <-chan struct{}, patchs <-chan sointu.Patch, scores <-chan sointu.Score, samplesPerRows <-chan int, posChanged chan<- struct{}, syncOutput chan<- []float32, outputs ...chan<- []float32) *Player {
p := &Player{playCmds: make(chan uint64, 16)} p := &Player{playCmds: make(chan uint64, 16)}
go func() { go func() {
var score sointu.Score var score sointu.Score
buffer := make([]float32, 2048) buffer := make([]float32, 2048)
buffer2 := make([]float32, 2048) buffer2 := make([]float32, 2048)
zeros := make([]float32, 2048) zeros := make([]float32, 2048)
totalSyncs := 1 // just the beat
syncBuffer := make([]float32, (2048+255)/256*totalSyncs)
syncBuffer2 := make([]float32, (2048+255)/256*totalSyncs)
rowTime := 0 rowTime := 0
samplesPerRow := math.MaxInt32 samplesPerRow := math.MaxInt32
var trackIDs []uint32 var trackIDs []uint32
@ -103,6 +106,9 @@ func NewPlayer(service sointu.SynthService, closer <-chan struct{}, patchs <-cha
} }
} }
} }
totalSyncs = 1 + p.patch.NumSyncs()
syncBuffer = make([]float32, ((2048+255)/256)*totalSyncs)
syncBuffer2 = make([]float32, ((2048+255)/256)*totalSyncs)
p.mutex.Unlock() p.mutex.Unlock()
case score = <-scores: case score = <-scores:
if row, playing := p.Position(); playing { if row, playing := p.Position(); playing {
@ -165,17 +171,29 @@ func NewPlayer(service sointu.SynthService, closer <-chan struct{}, patchs <-cha
renderTime = math.MaxInt32 renderTime = math.MaxInt32
} }
p.mutex.Lock() p.mutex.Lock()
rendered, timeAdvanced, err := p.synth.Render(buffer, renderTime) rendered, syncs, timeAdvanced, err := p.synth.Render(buffer, syncBuffer, renderTime)
if err != nil { if err != nil {
p.synth = nil p.synth = nil
atomic.StoreInt32(&p.synthNotNil, 0) atomic.StoreInt32(&p.synthNotNil, 0)
} }
p.mutex.Unlock() p.mutex.Unlock()
for i := 0; i < syncs; i++ {
a := syncBuffer[i*totalSyncs]
b := (a+float32(rowTime))/float32(samplesPerRow) + float32(row.Pattern*score.RowsPerPattern+row.Row)
syncBuffer[i*totalSyncs] = b
}
rowTime += timeAdvanced rowTime += timeAdvanced
for window := syncBuffer[:totalSyncs*syncs]; len(window) > 0; window = window[totalSyncs:] {
select {
case syncOutput <- window[:totalSyncs]:
default:
}
}
for _, o := range outputs { for _, o := range outputs {
o <- buffer[:rendered*2] o <- buffer[:rendered*2]
} }
buffer2, buffer = buffer, buffer2 buffer2, buffer = buffer, buffer2
syncBuffer2, syncBuffer = syncBuffer, syncBuffer2
} else { } else {
rowTime += len(zeros) / 2 rowTime += len(zeros) / 2
for _, o := range outputs { for _, o := range outputs {

View File

@ -131,6 +131,7 @@ var UnitTypes = map[string]([]UnitParameter){
"in": []UnitParameter{ "in": []UnitParameter{
{Name: "stereo", MinValue: 0, MaxValue: 1, CanSet: true, CanModulate: false}, {Name: "stereo", MinValue: 0, MaxValue: 1, CanSet: true, CanModulate: false},
{Name: "channel", MinValue: 0, MaxValue: 6, CanSet: true, CanModulate: false}}, {Name: "channel", MinValue: 0, MaxValue: 6, CanSet: true, CanModulate: false}},
"sync": []UnitParameter{},
} }
var Ports = make(map[string]([]string)) var Ports = make(map[string]([]string))

View File

@ -70,17 +70,18 @@ func Synth(patch sointu.Patch) (*C.Synth, error) {
// time > maxtime, as it is modulated and the time could advance by 2 or more, so the loop // time > maxtime, as it is modulated and the time could advance by 2 or more, so the loop
// exit condition would fire when the time is already past maxtime. // exit condition would fire when the time is already past maxtime.
// Under no conditions, nsamples >= len(buffer)/2 i.e. guaranteed to never overwrite the buffer. // Under no conditions, nsamples >= len(buffer)/2 i.e. guaranteed to never overwrite the buffer.
func (synth *C.Synth) Render(buffer []float32, maxtime int) (int, int, error) { func (synth *C.Synth) Render(buffer []float32, syncBuffer []float32, maxtime int) (int, int, int, error) {
// TODO: syncBuffer is not getting passed to cgo; do we want to even try to support the syncing with the native bridge
if len(buffer)%1 == 1 { if len(buffer)%1 == 1 {
return -1, -1, errors.New("RenderTime writes stereo signals, so buffer should have even length") return -1, -1, -1, errors.New("RenderTime writes stereo signals, so buffer should have even length")
} }
samples := C.int(len(buffer) / 2) samples := C.int(len(buffer) / 2)
time := C.int(maxtime) time := C.int(maxtime)
errcode := int(C.su_render(synth, (*C.float)(&buffer[0]), &samples, &time)) errcode := int(C.su_render(synth, (*C.float)(&buffer[0]), &samples, &time))
if errcode > 0 { if errcode > 0 {
return int(samples), int(time), &RenderError{errcode: errcode} return int(samples), 0, int(time), &RenderError{errcode: errcode}
} }
return int(samples), int(time), nil return int(samples), 0, int(time), nil
} }
// Trigger is part of C.Synths' implementation of sointu.Synth interface // Trigger is part of C.Synths' implementation of sointu.Synth interface

View File

@ -44,7 +44,7 @@ func TestOscillatSine(t *testing.T) {
if err != nil { if err != nil {
t.Fatalf("Compiling patch failed: %v", err) t.Fatalf("Compiling patch failed: %v", err)
} }
buffer, err := sointu.Play(synth, song) buffer, _, err := sointu.Play(synth, song)
if err != nil { if err != nil {
t.Fatalf("Render failed: %v", err) t.Fatalf("Render failed: %v", err)
} }
@ -103,7 +103,7 @@ func TestAllRegressionTests(t *testing.T) {
if err != nil { if err != nil {
t.Fatalf("Compiling patch failed: %v", err) t.Fatalf("Compiling patch failed: %v", err)
} }
buffer, err := sointu.Play(synth, song) buffer, _, err := sointu.Play(synth, song)
buffer = buffer[:song.Score.LengthInRows()*song.SamplesPerRow()*2] // extend to the nominal length always. buffer = buffer[:song.Score.LengthInRows()*song.SamplesPerRow()*2] // extend to the nominal length always.
if err != nil { if err != nil {
t.Fatalf("Play failed: %v", err) t.Fatalf("Play failed: %v", err)

View File

@ -18,10 +18,11 @@ type Compiler struct {
OS string OS string
Arch string Arch string
Output16Bit bool Output16Bit bool
RowSync bool
} }
// New returns a new compiler using the default .asm templates // New returns a new compiler using the default .asm templates
func New(os string, arch string, output16Bit bool) (*Compiler, error) { func New(os string, arch string, output16Bit bool, rowsync bool) (*Compiler, error) {
_, myname, _, _ := runtime.Caller(0) _, myname, _, _ := runtime.Caller(0)
var subdir string var subdir string
if arch == "386" || arch == "amd64" { if arch == "386" || arch == "amd64" {
@ -32,17 +33,17 @@ func New(os string, arch string, output16Bit bool) (*Compiler, error) {
return nil, fmt.Errorf("compiler.New failed, because only amd64, 386 and wasm archs are supported (targeted architecture was %v)", arch) return nil, fmt.Errorf("compiler.New failed, because only amd64, 386 and wasm archs are supported (targeted architecture was %v)", arch)
} }
templateDir := filepath.Join(path.Dir(myname), "..", "..", "templates", subdir) templateDir := filepath.Join(path.Dir(myname), "..", "..", "templates", subdir)
compiler, err := NewFromTemplates(os, arch, output16Bit, templateDir) compiler, err := NewFromTemplates(os, arch, output16Bit, rowsync, templateDir)
return compiler, err return compiler, err
} }
func NewFromTemplates(os string, arch string, output16Bit bool, templateDirectory string) (*Compiler, error) { func NewFromTemplates(os string, arch string, output16Bit bool, rowsync bool, templateDirectory string) (*Compiler, error) {
globPtrn := filepath.Join(templateDirectory, "*.*") globPtrn := filepath.Join(templateDirectory, "*.*")
tmpl, err := template.New("base").Funcs(sprig.TxtFuncMap()).ParseGlob(globPtrn) tmpl, err := template.New("base").Funcs(sprig.TxtFuncMap()).ParseGlob(globPtrn)
if err != nil { if err != nil {
return nil, fmt.Errorf(`could not create template based on directory "%v": %v`, templateDirectory, err) return nil, fmt.Errorf(`could not create template based on directory "%v": %v`, templateDirectory, err)
} }
return &Compiler{Template: tmpl, OS: os, Arch: arch, Output16Bit: output16Bit}, nil return &Compiler{Template: tmpl, OS: os, Arch: arch, RowSync: rowsync, Output16Bit: output16Bit}, nil
} }
func (com *Compiler) Library() (map[string]string, error) { func (com *Compiler) Library() (map[string]string, error) {

View File

@ -354,15 +354,25 @@ func (p *X86Macros) FmtStack() string {
} }
func (p *X86Macros) ExportFunc(name string, params ...string) string { func (p *X86Macros) ExportFunc(name string, params ...string) string {
if !p.Amd64 { numRegisters := 0 // in 32-bit systems, we use stdcall: everything in stack
reverseParams := make([]string, len(params)) switch {
for i, param := range params { case p.Amd64 && p.OS == "windows":
reverseParams[len(params)-1-i] = param numRegisters = 4 // 64-bit windows has 4 parameters in registers, rest in stack
} case p.Amd64:
p.Stacklocs = append(reverseParams, "retaddr_"+name) // in 32-bit, we use stdcall and parameters are in the stack numRegisters = 6 // System V ABI has 6 parameters in registers, rest in stack
if p.OS == "windows" { }
return fmt.Sprintf("%[1]v\nglobal _%[2]v@%[3]v\n_%[2]v@%[3]v:", p.SectText(name), name, len(params)*4) if len(params) > numRegisters {
} params = params[numRegisters:]
} else {
params = nil
}
reverseParams := make([]string, len(params))
for i, param := range params {
reverseParams[len(params)-1-i] = param
}
p.Stacklocs = append(reverseParams, "retaddr_"+name) // in 32-bit, we use stdcall and parameters are in the stack
if !p.Amd64 && p.OS == "windows" {
return fmt.Sprintf("%[1]v\nglobal _%[2]v@%[3]v\n_%[2]v@%[3]v:", p.SectText(name), name, len(params)*4)
} }
if p.OS == "darwin" { if p.OS == "darwin" {
return fmt.Sprintf("%[1]v\nglobal _%[2]v\n_%[2]v:", p.SectText(name), name) return fmt.Sprintf("%[1]v\nglobal _%[2]v\n_%[2]v:", p.SectText(name), name)

View File

@ -115,7 +115,7 @@ func (s *Interpreter) Update(patch sointu.Patch) error {
return nil return nil
} }
func (s *Interpreter) Render(buffer []float32, maxtime int) (samples int, time int, renderError error) { func (s *Interpreter) Render(buffer []float32, syncBuf []float32, maxtime int) (samples int, syncs int, time int, renderError error) {
defer func() { defer func() {
if err := recover(); err != nil { if err := recover(); err != nil {
renderError = fmt.Errorf("render panicced: %v", err) renderError = fmt.Errorf("render panicced: %v", err)
@ -133,6 +133,10 @@ func (s *Interpreter) Render(buffer []float32, maxtime int) (samples int, time i
voicesRemaining := s.bytePatch.NumVoices voicesRemaining := s.bytePatch.NumVoices
voices := s.synth.voices[:] voices := s.synth.voices[:]
units := voices[0].units[:] units := voices[0].units[:]
if byte(s.synth.globalTime) == 0 { // every 256 samples
syncBuf[0], syncBuf = float32(time), syncBuf[1:]
syncs++
}
for voicesRemaining > 0 { for voicesRemaining > 0 {
op := commands[0] op := commands[0]
commands = commands[1:] commands = commands[1:]
@ -152,7 +156,7 @@ func (s *Interpreter) Render(buffer []float32, maxtime int) (samples int, time i
} }
tcount := transformCounts[opNoStereo-1] tcount := transformCounts[opNoStereo-1]
if len(values) < tcount { if len(values) < tcount {
return samples, time, errors.New("value stream ended prematurely") return samples, syncs, time, errors.New("value stream ended prematurely")
} }
voice := &voices[0] voice := &voices[0]
unit := &units[0] unit := &units[0]
@ -523,16 +527,20 @@ func (s *Interpreter) Render(buffer []float32, maxtime int) (samples int, time i
if stereo { if stereo {
stack = append(stack, gain) stack = append(stack, gain)
} }
case opSync:
if byte(s.synth.globalTime) == 0 { // every 256 samples
syncBuf[0], syncBuf = float32(stack[l-1]), syncBuf[1:]
}
default: default:
return samples, time, errors.New("invalid / unimplemented opcode") return samples, syncs, time, errors.New("invalid / unimplemented opcode")
} }
units = units[1:] units = units[1:]
} }
if len(stack) < 4 { if len(stack) < 4 {
return samples, time, errors.New("stack underflow") return samples, syncs, time, errors.New("stack underflow")
} }
if len(stack) > 4 { if len(stack) > 4 {
return samples, time, errors.New("stack not empty") return samples, syncs, time, errors.New("stack not empty")
} }
buffer[0] = synth.outputs[0] buffer[0] = synth.outputs[0]
buffer[1] = synth.outputs[1] buffer[1] = synth.outputs[1]
@ -544,7 +552,7 @@ func (s *Interpreter) Render(buffer []float32, maxtime int) (samples int, time i
s.synth.globalTime++ s.synth.globalTime++
} }
s.stack = stack[:0] s.stack = stack[:0]
return samples, time, nil return samples, syncs, time, nil
} }
func (s *synth) rand() float32 { func (s *synth) rand() float32 {

View File

@ -45,7 +45,7 @@ func TestAllRegressionTests(t *testing.T) {
if err != nil { if err != nil {
t.Fatalf("Compiling patch failed: %v", err) t.Fatalf("Compiling patch failed: %v", err)
} }
buffer, err := sointu.Play(synth, song) buffer, _, err := sointu.Play(synth, song)
buffer = buffer[:song.Score.LengthInRows()*song.SamplesPerRow()*2] // extend to the nominal length always. buffer = buffer[:song.Score.LengthInRows()*song.SamplesPerRow()*2] // extend to the nominal length always.
if err != nil { if err != nil {
t.Fatalf("Play failed: %v", err) t.Fatalf("Play failed: %v", err)

View File

@ -30,7 +30,8 @@ const (
opReceive = 26 opReceive = 26
opSend = 27 opSend = 27
opSpeed = 28 opSpeed = 28
opXch = 29 opSync = 29
opXch = 30
) )
var transformCounts = [...]int{0, 0, 1, 0, 5, 1, 4, 1, 5, 2, 1, 1, 0, 1, 0, 1, 0, 0, 2, 6, 1, 2, 1, 0, 0, 0, 1, 0, 0} var transformCounts = [...]int{0, 0, 1, 0, 5, 1, 4, 1, 5, 2, 1, 1, 0, 1, 0, 1, 0, 0, 2, 6, 1, 2, 1, 0, 0, 0, 1, 0, 0, 0}