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
|
- **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.
|
||||||
|
|
||||||
|
@ -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)
|
||||||
|
@ -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)
|
||||||
}
|
}
|
||||||
|
@ -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)
|
||||||
}
|
}
|
||||||
|
@ -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)
|
||||||
}
|
}
|
||||||
|
12
patch.go
12
patch.go
@ -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
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 {
|
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
|
||||||
}
|
}
|
||||||
|
@ -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}}
|
||||||
|
@ -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
|
||||||
|
@ -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
|
||||||
|
@ -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
|
||||||
|
@ -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})
|
||||||
|
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
|
#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
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{}},
|
"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{
|
||||||
|
@ -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)
|
||||||
|
@ -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 {
|
||||||
|
@ -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 {
|
||||||
|
@ -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))
|
||||||
|
@ -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
|
||||||
|
@ -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)
|
||||||
|
@ -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) {
|
||||||
|
@ -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)
|
||||||
|
@ -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 {
|
||||||
|
@ -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)
|
||||||
|
@ -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}
|
||||||
|
Loading…
Reference in New Issue
Block a user