mirror of
https://github.com/vsariola/sointu.git
synced 2025-05-25 18:00:37 -04:00
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:
parent
a3bdf565fd
commit
99dbdfe223
33
README.md
33
README.md
@ -313,6 +313,20 @@ New features since fork
|
||||
- **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
|
||||
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
|
||||
------------
|
||||
@ -327,15 +341,6 @@ Future goals
|
||||
bit flag in the existing filter
|
||||
- Arbitrary envelopes; for easier automation.
|
||||
- **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
|
||||
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
|
||||
@ -347,16 +352,6 @@ Future goals
|
||||
|
||||
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
|
||||
at you.
|
||||
|
||||
|
@ -33,6 +33,7 @@ func main() {
|
||||
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.")
|
||||
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.")
|
||||
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.")
|
||||
@ -53,9 +54,9 @@ func main() {
|
||||
if compile || *library {
|
||||
var err error
|
||||
if *tmplDir != "" {
|
||||
comp, err = compiler.NewFromTemplates(*targetOs, *targetArch, *output16bit, *tmplDir)
|
||||
comp, err = compiler.NewFromTemplates(*targetOs, *targetArch, *output16bit, *rowsync, *tmplDir)
|
||||
} else {
|
||||
comp, err = compiler.New(*targetOs, *targetArch, *output16bit)
|
||||
comp, err = compiler.New(*targetOs, *targetArch, *output16bit, *rowsync)
|
||||
}
|
||||
if err != nil {
|
||||
fmt.Fprintf(os.Stderr, `error creating compiler: %v`, err)
|
||||
|
@ -17,5 +17,7 @@ func main() {
|
||||
}
|
||||
defer audioContext.Close()
|
||||
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)
|
||||
}
|
||||
|
@ -92,7 +92,7 @@ func main() {
|
||||
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 {
|
||||
return fmt.Errorf("sointu.Play failed: %v", err)
|
||||
}
|
||||
|
@ -1,21 +1,33 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"flag"
|
||||
"fmt"
|
||||
"os"
|
||||
|
||||
"github.com/vsariola/sointu/oto"
|
||||
"github.com/vsariola/sointu/rpc"
|
||||
"github.com/vsariola/sointu/tracker/gioui"
|
||||
"github.com/vsariola/sointu/vm"
|
||||
)
|
||||
|
||||
func main() {
|
||||
syncAddress := flag.String("address", "", "remote RPC server where to send sync data")
|
||||
flag.Parse()
|
||||
audioContext, err := oto.NewContext()
|
||||
if err != nil {
|
||||
fmt.Println(err)
|
||||
os.Exit(1)
|
||||
}
|
||||
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{}
|
||||
gioui.Main(audioContext, synthService)
|
||||
gioui.Main(audioContext, synthService, syncChannel)
|
||||
}
|
||||
|
12
patch.go
12
patch.go
@ -37,6 +37,18 @@ func (p Patch) NumDelayLines() int {
|
||||
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 {
|
||||
ret := 0
|
||||
for _, t := range p[:instrIndex] {
|
||||
|
57
rpc/rpc.go
Normal file
57
rpc/rpc.go
Normal 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
24
rpc/rpc_test.go
Normal 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)
|
||||
}
|
||||
}
|
25
synth.go
25
synth.go
@ -7,7 +7,7 @@ import (
|
||||
)
|
||||
|
||||
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
|
||||
Trigger(voice int, note byte)
|
||||
Release(voice int)
|
||||
@ -18,7 +18,7 @@ type SynthService interface {
|
||||
}
|
||||
|
||||
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 {
|
||||
return fmt.Errorf("sointu.Render failed: %v", err)
|
||||
}
|
||||
@ -28,10 +28,10 @@ func Render(synth Synth, buffer []float32) error {
|
||||
return nil
|
||||
}
|
||||
|
||||
func Play(synth Synth, song Song) ([]float32, error) {
|
||||
func Play(synth Synth, song Song) ([]float32, []float32, error) {
|
||||
err := song.Validate()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
return nil, nil, err
|
||||
}
|
||||
curVoices := make([]int, len(song.Score.Tracks))
|
||||
for i := range curVoices {
|
||||
@ -39,7 +39,10 @@ func Play(synth Synth, song Song) ([]float32, error) {
|
||||
}
|
||||
initialCapacity := song.Score.LengthInRows() * song.SamplesPerRow() * 2
|
||||
buffer := make([]float32, 0, initialCapacity)
|
||||
syncBuffer := make([]float32, 0, initialCapacity)
|
||||
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++ {
|
||||
patternRow := row % song.Score.RowsPerPattern
|
||||
pattern := row / song.Score.RowsPerPattern
|
||||
@ -73,16 +76,22 @@ func Play(synth Synth, song Song) ([]float32, error) {
|
||||
}
|
||||
tries := 0
|
||||
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 {
|
||||
return buffer, fmt.Errorf("render failed: %v", err)
|
||||
return buffer, syncBuffer, fmt.Errorf("render failed: %v", err)
|
||||
}
|
||||
rowtime += time
|
||||
buffer = append(buffer, rowbuffer[:samples*2]...)
|
||||
syncBuffer = append(syncBuffer, syncRowBuffer[:syncs]...)
|
||||
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
|
||||
}
|
||||
|
@ -21,3 +21,24 @@
|
||||
fstp dword [{{.WRK}}] ; save the remainder for future
|
||||
ret
|
||||
{{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}}
|
||||
|
@ -14,6 +14,14 @@
|
||||
;-------------------------------------------------------------------------------
|
||||
{{.Func "su_run_vm"}}
|
||||
{{- .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
|
||||
movzx edi, byte [{{.COM}}] ; edi = command byte
|
||||
inc {{.COM}} ; move to next instruction
|
||||
|
@ -14,12 +14,22 @@ su_synth_obj:
|
||||
; the output buffer. Renders the compile time hard-coded song to the buffer.
|
||||
; Stack: output_ptr
|
||||
;-------------------------------------------------------------------------------
|
||||
{{- if or .RowSync (.HasOp "sync")}}
|
||||
{{.ExportFunc "su_render_song" "OutputBufPtr" "SyncBufPtr"}}
|
||||
{{- else}}
|
||||
{{.ExportFunc "su_render_song" "OutputBufPtr"}}
|
||||
{{- end}}
|
||||
{{- if .Amd64}}
|
||||
{{- 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
|
||||
{{- if or .RowSync (.HasOp "sync")}}
|
||||
{{- .PushRegs "rdx" "SyncBufPtr" | indent 4}}
|
||||
{{- end}}
|
||||
{{- else}} ; SystemV amd64 ABI, linux mac or hopefully something similar
|
||||
{{- .PushRegs "rdi" "OutputBufPtr" "rbx" "NonVolatileRbx" "rbp" "NonVolatileRbp" | indent 4}}
|
||||
{{- if or .RowSync (.HasOp "sync")}}
|
||||
{{- .PushRegs "rsi" "SyncBufPtr" | indent 4}}
|
||||
{{- end}}
|
||||
{{- end}}
|
||||
{{- else}}
|
||||
{{- .PushRegs | indent 4}}
|
||||
@ -68,6 +78,9 @@ su_render_sampleloop: ; loop through every sample in the row
|
||||
{{$.Pop $.AX}}
|
||||
{{- end}}
|
||||
{{- if .Amd64}}
|
||||
{{- if or .RowSync (.HasOp "sync")}}
|
||||
{{.Pop .AX}} ; pop the sync buf ptr away
|
||||
{{- end}}
|
||||
{{- if eq .OS "windows"}}
|
||||
; Windows64 ABI, rdi rsi rbx rbp non-volatile
|
||||
{{- .PopRegs "rcx" "rdi" "rsi" "rbx" "rbp" | indent 4}}
|
||||
@ -78,8 +91,12 @@ su_render_sampleloop: ; loop through every sample in the row
|
||||
ret
|
||||
{{- else}}
|
||||
{{- .PopRegs | indent 4}}
|
||||
{{- if or .RowSync (.HasOp "sync")}}
|
||||
ret 8
|
||||
{{- else}}
|
||||
ret 4
|
||||
{{- end}}
|
||||
{{- end}}
|
||||
|
||||
;-------------------------------------------------------------------------------
|
||||
; su_update_voices function: polyphonic & chord implementation
|
||||
|
@ -13,6 +13,15 @@
|
||||
#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))
|
||||
|
||||
{{- 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>
|
||||
#if UINTPTR_MAX == 0xffffffff
|
||||
#if defined(__clang__) || defined(__GNUC__)
|
||||
@ -39,7 +48,12 @@ typedef float SUsample;
|
||||
extern "C" {
|
||||
#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);
|
||||
{{- end}}
|
||||
{{- if gt (.SampleOffsets | len) 0}}
|
||||
void SU_CALLCONV su_load_gmdls();
|
||||
#define SU_LOAD_GMDLS
|
||||
|
@ -1,10 +1,13 @@
|
||||
function(regression_test testname)
|
||||
|
||||
if(${ARGC} LESS 6)
|
||||
if(${ARGC} LESS 4)
|
||||
set(source ${testname}.yml)
|
||||
if(ARGV5)
|
||||
set(source ${ARGV5})
|
||||
add_executable(${testname} ${source} test_renderer.c)
|
||||
else()
|
||||
if(ARGV3)
|
||||
set(source ${ARGV3}.yml)
|
||||
else()
|
||||
set(source ${testname}.yml)
|
||||
endif()
|
||||
|
||||
set(asmfile ${testname}.asm)
|
||||
@ -19,7 +22,7 @@ function(regression_test testname)
|
||||
add_executable(${testname} test_renderer.c ${asmfile})
|
||||
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(watfile ${CMAKE_CURRENT_BINARY_DIR}/${testname}.wat)
|
||||
set(wasmtarget wasm_${testname})
|
||||
@ -30,30 +33,28 @@ function(regression_test testname)
|
||||
)
|
||||
add_test(${wasmtarget} ${NODE} ${CMAKE_CURRENT_SOURCE_DIR}/wasm_test_renderer.es6 ${wasmfile} ${CMAKE_CURRENT_SOURCE_DIR}/expected_output/${testname}.raw)
|
||||
endif()
|
||||
else()
|
||||
set(source ${ARGV5})
|
||||
add_executable(${testname} ${source} test_renderer.c)
|
||||
endif()
|
||||
|
||||
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_include_directories(${testname} PUBLIC ${CMAKE_CURRENT_BINARY_DIR})
|
||||
target_compile_definitions(${testname} PUBLIC TEST_NAME="${testname}")
|
||||
|
||||
if(ARGC GREATER 1)
|
||||
if (ARGV1)
|
||||
message("${testname} requires ${ARGV1}")
|
||||
set_tests_properties(${testname} PROPERTIES FIXTURES_REQUIRED "${ARGV1}")
|
||||
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)
|
||||
|
||||
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_chords "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)
|
||||
target_link_libraries(test_render_samples ${STATICLIB})
|
||||
|
BIN
tests/expected_output/test_sync.raw
Normal file
BIN
tests/expected_output/test_sync.raw
Normal file
Binary file not shown.
BIN
tests/expected_output/test_sync_syncbuf.raw
Normal file
BIN
tests/expected_output/test_sync_syncbuf.raw
Normal file
Binary file not shown.
@ -16,6 +16,11 @@
|
||||
#include TEST_HEADER
|
||||
SUsample buf[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[]) {
|
||||
FILE* f;
|
||||
@ -36,7 +41,11 @@ int main(int argc, char* argv[]) {
|
||||
su_load_gmdls();
|
||||
#endif
|
||||
|
||||
#ifdef SU_SYNC
|
||||
su_render_song(buf, syncBuf);
|
||||
#else
|
||||
su_render_song(buf);
|
||||
#endif
|
||||
|
||||
#if defined (_WIN32)
|
||||
CreateDirectory(actual_output_folder, NULL);
|
||||
@ -49,6 +58,13 @@ int main(int argc, char* argv[]) {
|
||||
fwrite((void*)buf, sizeof(SUsample), SU_BUFFER_LENGTH, 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");
|
||||
|
||||
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);
|
||||
}
|
||||
|
||||
#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;
|
||||
|
||||
fail:
|
||||
|
20
tests/test_sync.yml
Normal file
20
tests/test_sync.yml
Normal 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}
|
@ -40,6 +40,7 @@ var defaultUnits = map[string]sointu.Unit{
|
||||
"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}},
|
||||
"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{
|
||||
|
@ -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() {
|
||||
w := app.NewWindow(
|
||||
app.Size(unit.Dp(800), unit.Dp(600)),
|
||||
app.Title("Sointu Tracker"),
|
||||
)
|
||||
t := New(audioContext, synthService)
|
||||
t := New(audioContext, synthService, syncChannel)
|
||||
defer t.Close()
|
||||
if err := t.Run(w); err != nil {
|
||||
fmt.Println(err)
|
||||
|
@ -99,7 +99,7 @@ func (t *Tracker) 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{
|
||||
Theme: material.NewTheme(gofont.Collection()),
|
||||
audioContext: audioContext,
|
||||
@ -160,7 +160,7 @@ func New(audioContext sointu.AudioContext, synthService sointu.SynthService) *Tr
|
||||
sprObserver := make(chan int, 16)
|
||||
t.AddSamplesPerRowObserver(sprObserver)
|
||||
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()
|
||||
go func() {
|
||||
for buf := range audioChannel {
|
||||
|
@ -66,13 +66,16 @@ func (p *Player) Enabled() bool {
|
||||
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)}
|
||||
go func() {
|
||||
var score sointu.Score
|
||||
buffer := make([]float32, 2048)
|
||||
buffer2 := 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
|
||||
samplesPerRow := math.MaxInt32
|
||||
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()
|
||||
case score = <-scores:
|
||||
if row, playing := p.Position(); playing {
|
||||
@ -165,17 +171,29 @@ func NewPlayer(service sointu.SynthService, closer <-chan struct{}, patchs <-cha
|
||||
renderTime = math.MaxInt32
|
||||
}
|
||||
p.mutex.Lock()
|
||||
rendered, timeAdvanced, err := p.synth.Render(buffer, renderTime)
|
||||
rendered, syncs, timeAdvanced, err := p.synth.Render(buffer, syncBuffer, renderTime)
|
||||
if err != nil {
|
||||
p.synth = nil
|
||||
atomic.StoreInt32(&p.synthNotNil, 0)
|
||||
}
|
||||
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
|
||||
for window := syncBuffer[:totalSyncs*syncs]; len(window) > 0; window = window[totalSyncs:] {
|
||||
select {
|
||||
case syncOutput <- window[:totalSyncs]:
|
||||
default:
|
||||
}
|
||||
}
|
||||
for _, o := range outputs {
|
||||
o <- buffer[:rendered*2]
|
||||
}
|
||||
buffer2, buffer = buffer, buffer2
|
||||
syncBuffer2, syncBuffer = syncBuffer, syncBuffer2
|
||||
} else {
|
||||
rowTime += len(zeros) / 2
|
||||
for _, o := range outputs {
|
||||
|
@ -131,6 +131,7 @@ var UnitTypes = map[string]([]UnitParameter){
|
||||
"in": []UnitParameter{
|
||||
{Name: "stereo", MinValue: 0, MaxValue: 1, CanSet: true, CanModulate: false},
|
||||
{Name: "channel", MinValue: 0, MaxValue: 6, CanSet: true, CanModulate: false}},
|
||||
"sync": []UnitParameter{},
|
||||
}
|
||||
|
||||
var Ports = make(map[string]([]string))
|
||||
|
@ -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
|
||||
// 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.
|
||||
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 {
|
||||
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)
|
||||
time := C.int(maxtime)
|
||||
errcode := int(C.su_render(synth, (*C.float)(&buffer[0]), &samples, &time))
|
||||
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
|
||||
|
@ -44,7 +44,7 @@ func TestOscillatSine(t *testing.T) {
|
||||
if err != nil {
|
||||
t.Fatalf("Compiling patch failed: %v", err)
|
||||
}
|
||||
buffer, err := sointu.Play(synth, song)
|
||||
buffer, _, err := sointu.Play(synth, song)
|
||||
if err != nil {
|
||||
t.Fatalf("Render failed: %v", err)
|
||||
}
|
||||
@ -103,7 +103,7 @@ func TestAllRegressionTests(t *testing.T) {
|
||||
if err != nil {
|
||||
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.
|
||||
if err != nil {
|
||||
t.Fatalf("Play failed: %v", err)
|
||||
|
@ -18,10 +18,11 @@ type Compiler struct {
|
||||
OS string
|
||||
Arch string
|
||||
Output16Bit bool
|
||||
RowSync bool
|
||||
}
|
||||
|
||||
// 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)
|
||||
var subdir string
|
||||
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)
|
||||
}
|
||||
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
|
||||
}
|
||||
|
||||
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, "*.*")
|
||||
tmpl, err := template.New("base").Funcs(sprig.TxtFuncMap()).ParseGlob(globPtrn)
|
||||
if err != nil {
|
||||
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) {
|
||||
|
@ -354,16 +354,26 @@ func (p *X86Macros) FmtStack() 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
|
||||
switch {
|
||||
case p.Amd64 && p.OS == "windows":
|
||||
numRegisters = 4 // 64-bit windows has 4 parameters in registers, rest in stack
|
||||
case p.Amd64:
|
||||
numRegisters = 6 // System V ABI has 6 parameters in registers, rest in stack
|
||||
}
|
||||
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.OS == "windows" {
|
||||
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" {
|
||||
return fmt.Sprintf("%[1]v\nglobal _%[2]v\n_%[2]v:", p.SectText(name), name)
|
||||
}
|
||||
|
@ -115,7 +115,7 @@ func (s *Interpreter) Update(patch sointu.Patch) error {
|
||||
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() {
|
||||
if err := recover(); err != nil {
|
||||
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
|
||||
voices := s.synth.voices[:]
|
||||
units := voices[0].units[:]
|
||||
if byte(s.synth.globalTime) == 0 { // every 256 samples
|
||||
syncBuf[0], syncBuf = float32(time), syncBuf[1:]
|
||||
syncs++
|
||||
}
|
||||
for voicesRemaining > 0 {
|
||||
op := commands[0]
|
||||
commands = commands[1:]
|
||||
@ -152,7 +156,7 @@ func (s *Interpreter) Render(buffer []float32, maxtime int) (samples int, time i
|
||||
}
|
||||
tcount := transformCounts[opNoStereo-1]
|
||||
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]
|
||||
unit := &units[0]
|
||||
@ -523,16 +527,20 @@ func (s *Interpreter) Render(buffer []float32, maxtime int) (samples int, time i
|
||||
if stereo {
|
||||
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:
|
||||
return samples, time, errors.New("invalid / unimplemented opcode")
|
||||
return samples, syncs, time, errors.New("invalid / unimplemented opcode")
|
||||
}
|
||||
units = units[1:]
|
||||
}
|
||||
if len(stack) < 4 {
|
||||
return samples, time, errors.New("stack underflow")
|
||||
return samples, syncs, time, errors.New("stack underflow")
|
||||
}
|
||||
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[1] = synth.outputs[1]
|
||||
@ -544,7 +552,7 @@ func (s *Interpreter) Render(buffer []float32, maxtime int) (samples int, time i
|
||||
s.synth.globalTime++
|
||||
}
|
||||
s.stack = stack[:0]
|
||||
return samples, time, nil
|
||||
return samples, syncs, time, nil
|
||||
}
|
||||
|
||||
func (s *synth) rand() float32 {
|
||||
|
@ -45,7 +45,7 @@ func TestAllRegressionTests(t *testing.T) {
|
||||
if err != nil {
|
||||
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.
|
||||
if err != nil {
|
||||
t.Fatalf("Play failed: %v", err)
|
||||
|
@ -30,7 +30,8 @@ const (
|
||||
opReceive = 26
|
||||
opSend = 27
|
||||
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}
|
||||
|
Loading…
Reference in New Issue
Block a user