feat: Delays and samples are now working through the bridge.

One should call bridge.Init() once during the initialization of the program to load the static sample table. On linux, bridge.Init() does nothing.
This commit is contained in:
Veikko Sariola
2020-11-08 16:03:10 +02:00
parent e65b08d2b3
commit bcbb5aaf19
12 changed files with 155 additions and 50 deletions

View File

@ -17,6 +17,8 @@ func ParseAsm(reader io.Reader) (*Song, error) {
tracks := make([]Track, 0)
var patch Patch
var instr Instrument
var delayTimes []int
var sampleOffsets [][]int
paramReg, err := regexp.Compile(`([a-zA-Z]\w*)\s*\(\s*([0-9]+)\s*\)`) // matches FOO(42), groups "FOO" and "42"
if err != nil {
return nil, err
@ -158,6 +160,20 @@ func ParseAsm(reader io.Reader) (*Song, error) {
instr = Instrument{NumVoices: ints[0], Units: []Unit{}}
case "END_INSTRUMENT":
patch = append(patch, instr)
case "DELTIME":
ints, err := parseNumbers(rest)
if err != nil {
return nil, err
}
for _, v := range ints {
delayTimes = append(delayTimes, v)
}
case "SAMPLE_OFFSET":
ints, err := parseNumbers(rest)
if err != nil {
return nil, err
}
sampleOffsets = append(sampleOffsets, ints)
}
if unittype, ok := unitNameMap[word]; ok {
instrMatch := wordReg.FindStringSubmatch(rest)
@ -214,6 +230,12 @@ func ParseAsm(reader io.Reader) (*Song, error) {
} else {
parameters["pop"] = 0
}
} else if unittype == "delay" {
if flags["NOTETRACKING"] {
parameters["notetracking"] = 1
} else {
parameters["notetracking"] = 0
}
}
unit := Unit{Type: unittype, Stereo: stereo, Parameters: parameters}
instr.Units = append(instr.Units, unit)
@ -221,6 +243,26 @@ func ParseAsm(reader io.Reader) (*Song, error) {
}
}
}
for i := range patch {
for u := range patch[i].Units {
if patch[i].Units[u].Type == "delay" {
s := patch[i].Units[u].Parameters["delay"]
e := patch[i].Units[u].Parameters["count"]
if patch[i].Units[u].Stereo {
e *= 2 // stereo delays use 'count' number of delaytimes, but for both channels
}
patch[i].Units[u].DelayTimes = append(patch[i].Units[u].DelayTimes, delayTimes[s:e]...)
delete(patch[i].Units[u].Parameters, "delay")
delete(patch[i].Units[u].Parameters, "count")
} else if patch[i].Units[u].Type == "oscillator" && patch[i].Units[u].Parameters["type"] == Sample {
sampleno := patch[i].Units[u].Parameters["color"]
patch[i].Units[u].Parameters["start"] = sampleOffsets[sampleno][0]
patch[i].Units[u].Parameters["loopstart"] = sampleOffsets[sampleno][1]
patch[i].Units[u].Parameters["looplength"] = sampleOffsets[sampleno][2]
delete(patch[i].Units[u].Parameters, "color")
}
}
}
s := Song{BPM: bpm, Patterns: patterns, Tracks: tracks, Patch: patch, SongLength: -1}
return &s, nil
}

View File

@ -18,6 +18,7 @@ import (
)
func TestAllAsmFiles(t *testing.T) {
bridge.Init()
_, myname, _, _ := runtime.Caller(0)
files, err := filepath.Glob(path.Join(path.Dir(myname), "..", "tests", "*.asm"))
if err != nil {
@ -27,9 +28,6 @@ func TestAllAsmFiles(t *testing.T) {
basename := filepath.Base(filename)
testname := strings.TrimSuffix(basename, path.Ext(basename))
t.Run(testname, func(t *testing.T) {
if strings.Contains(testname, "delay") || strings.Contains(testname, "sample") {
return // delays and samples are not implemented yet in the bridge, so skip them for now
}
file, err := os.Open(filename)
if err != nil {
t.Fatalf("cannot read the .asm file: %v", filename)

View File

@ -34,7 +34,7 @@ var opcodeTable = map[string]opTableEntry{
"filter": opTableEntry{C.su_filter_id, []string{"frequency", "resonance"}},
"clip": opTableEntry{C.su_clip_id, []string{}},
"pan": opTableEntry{C.su_pan_id, []string{"panning"}},
"delay": opTableEntry{C.su_delay_id, []string{"pregain", "dry", "feedback", "damp", "damp", "delay", "count"}},
"delay": opTableEntry{C.su_delay_id, []string{"pregain", "dry", "feedback", "damp", "delaycount"}},
"compressor": opTableEntry{C.su_compres_id, []string{"attack", "release", "invgain", "threshold", "ratio"}},
"speed": opTableEntry{C.su_speed_id, []string{}},
"out": opTableEntry{C.su_out_id, []string{"gain"}},
@ -80,6 +80,9 @@ func (synth *C.Synth) Render(buffer []float32, maxtime int) (int, int, error) {
}
func Synth(patch go4k.Patch) (*C.Synth, error) {
s := new(C.Synth)
sampleno := 0
delaytimeno := 0
totalVoices := 0
commands := make([]byte, 0)
values := make([]byte, 0)
@ -99,13 +102,31 @@ func Synth(patch go4k.Patch) (*C.Synth, error) {
}
commands = append(commands, byte(opCode))
for _, paramname := range val.parameterList {
if pval, ok := unit.Parameters[paramname]; ok {
if unit.Type == "delay" && paramname == "count" {
pval = pval*2 - 1
if val, ok := unit.Parameters["notetracking"]; ok && val == 1 {
pval++
}
if unit.Type == "delay" && paramname == "delaycount" {
if unit.Stereo && len(unit.DelayTimes)%2 != 0 {
return nil, errors.New("Stereo delays should have even number of delaytimes")
}
values = append(values, byte(delaytimeno))
for _, v := range unit.DelayTimes {
s.DelayTimes[delaytimeno] = C.ushort(v)
delaytimeno++
}
count := len(unit.DelayTimes)
if unit.Stereo {
count /= 2
}
count = count*2 - 1
if unit.Parameters["notetracking"] == 1 {
count++
}
values = append(values, byte(count))
} else if unit.Type == "oscillator" && unit.Parameters["type"] == go4k.Sample && paramname == "color" {
values = append(values, byte(sampleno))
s.SampleOffsets[sampleno].Start = (C.uint)(unit.Parameters["start"])
s.SampleOffsets[sampleno].LoopStart = (C.ushort)(unit.Parameters["loopstart"])
s.SampleOffsets[sampleno].LoopLength = (C.ushort)(unit.Parameters["looplength"])
sampleno++
} else if pval, ok := unit.Parameters[paramname]; ok {
values = append(values, byte(pval))
} else {
return nil, fmt.Errorf("Unit parameter undefined: %v (at instrument %v, unit %v)", paramname, insid, unitid)
@ -178,7 +199,6 @@ func Synth(patch go4k.Patch) (*C.Synth, error) {
if len(values) > 16384 { // TODO: 16384 could probably be pulled automatically from cgo
return nil, errors.New("The patch would result in more than 16384 values")
}
s := new(C.Synth)
for i := range commands {
s.Commands[i] = (C.uchar)(commands[i])
}

View File

@ -24,9 +24,9 @@ const su_max_samples = SAMPLES_PER_ROW * TOTAL_ROWS
func TestBridge(t *testing.T) {
patch := []go4k.Instrument{
go4k.Instrument{1, []go4k.Unit{
go4k.Unit{"envelope", false, map[string]int{"attack": 64, "decay": 64, "sustain": 64, "release": 80, "gain": 128}},
go4k.Unit{"envelope", false, map[string]int{"attack": 95, "decay": 64, "sustain": 64, "release": 80, "gain": 128}},
go4k.Unit{"out", true, map[string]int{"gain": 128}},
go4k.Unit{"envelope", false, map[string]int{"attack": 64, "decay": 64, "sustain": 64, "release": 80, "gain": 128}, []int{}},
go4k.Unit{"envelope", false, map[string]int{"attack": 95, "decay": 64, "sustain": 64, "release": 80, "gain": 128}, []int{}},
go4k.Unit{"out", true, map[string]int{"gain": 128}, []int{}},
}}}
synth, err := bridge.Synth(patch)
if err != nil {

View File

@ -0,0 +1,6 @@
// +build !windows
package bridge
func Init() {
}

12
go4k/bridge/init_win.go Normal file
View File

@ -0,0 +1,12 @@
// +build windows
package bridge
// #cgo CFLAGS: -I"${SRCDIR}/../../include/sointu"
// #cgo LDFLAGS: "${SRCDIR}/../../build/libsointu.a"
// #include <sointu.h>
import "C"
func Init() {
C.su_load_gmdls() // GM.DLS is an windows specific sound bank so samples work currently only on windows
}

View File

@ -10,6 +10,7 @@ type Unit struct {
Type string
Stereo bool
Parameters map[string]int
DelayTimes []int
}
const (

View File

@ -2,12 +2,13 @@ package go4k_test
import (
"encoding/json"
"github.com/vsariola/sointu/go4k"
"reflect"
"testing"
"github.com/vsariola/sointu/go4k"
)
const expectedMarshaled = "{\"BPM\":100,\"Patterns\":[\"QABEACAAAABLAE4AAAAAAA==\"],\"Tracks\":[{\"NumVoices\":1,\"Sequence\":\"AA==\"}],\"SongLength\":0,\"Patch\":[{\"NumVoices\":1,\"Units\":[{\"Type\":\"envelope\",\"Stereo\":false,\"Parameters\":{\"attack\":32,\"decay\":32,\"gain\":128,\"release\":64,\"sustain\":64}},{\"Type\":\"oscillator\",\"Stereo\":false,\"Parameters\":{\"color\":96,\"detune\":64,\"flags\":64,\"gain\":128,\"phase\":0,\"shape\":64,\"transpose\":64}},{\"Type\":\"mulp\",\"Stereo\":false,\"Parameters\":{}},{\"Type\":\"envelope\",\"Stereo\":false,\"Parameters\":{\"attack\":32,\"decay\":32,\"gain\":128,\"release\":64,\"sustain\":64}},{\"Type\":\"oscillator\",\"Stereo\":false,\"Parameters\":{\"color\":64,\"detune\":64,\"flags\":64,\"gain\":128,\"phase\":64,\"shape\":96,\"transpose\":72}},{\"Type\":\"mulp\",\"Stereo\":false,\"Parameters\":{}},{\"Type\":\"out\",\"Stereo\":true,\"Parameters\":{\"gain\":128}}]}]}"
const expectedMarshaled = "{\"BPM\":100,\"Patterns\":[\"QABEACAAAABLAE4AAAAAAA==\"],\"Tracks\":[{\"NumVoices\":1,\"Sequence\":\"AA==\"}],\"SongLength\":0,\"Patch\":[{\"NumVoices\":1,\"Units\":[{\"Type\":\"envelope\",\"Stereo\":false,\"Parameters\":{\"attack\":32,\"decay\":32,\"gain\":128,\"release\":64,\"sustain\":64},\"DelayTimes\":[]},{\"Type\":\"oscillator\",\"Stereo\":false,\"Parameters\":{\"color\":96,\"detune\":64,\"flags\":64,\"gain\":128,\"phase\":0,\"shape\":64,\"transpose\":64},\"DelayTimes\":[]},{\"Type\":\"mulp\",\"Stereo\":false,\"Parameters\":{},\"DelayTimes\":[]},{\"Type\":\"envelope\",\"Stereo\":false,\"Parameters\":{\"attack\":32,\"decay\":32,\"gain\":128,\"release\":64,\"sustain\":64},\"DelayTimes\":[]},{\"Type\":\"oscillator\",\"Stereo\":false,\"Parameters\":{\"color\":64,\"detune\":64,\"flags\":64,\"gain\":128,\"phase\":64,\"shape\":96,\"transpose\":72},\"DelayTimes\":[]},{\"Type\":\"mulp\",\"Stereo\":false,\"Parameters\":{},\"DelayTimes\":[]},{\"Type\":\"out\",\"Stereo\":true,\"Parameters\":{\"gain\":128},\"DelayTimes\":[]}]}]}"
var testSong = go4k.Song{
BPM: 100,
@ -18,13 +19,13 @@ var testSong = go4k.Song{
SongLength: 0,
Patch: go4k.Patch{
go4k.Instrument{NumVoices: 1, Units: []go4k.Unit{
{"envelope", false, map[string]int{"attack": 32, "decay": 32, "sustain": 64, "release": 64, "gain": 128}},
{"oscillator", false, map[string]int{"transpose": 64, "detune": 64, "phase": 0, "color": 96, "shape": 64, "gain": 128, "flags": 0x40}},
{"mulp", false, map[string]int{}},
{"envelope", false, map[string]int{"attack": 32, "decay": 32, "sustain": 64, "release": 64, "gain": 128}},
{"oscillator", false, map[string]int{"transpose": 72, "detune": 64, "phase": 64, "color": 64, "shape": 96, "gain": 128, "flags": 0x40}},
{"mulp", false, map[string]int{}},
{"out", true, map[string]int{"gain": 128}},
{"envelope", false, map[string]int{"attack": 32, "decay": 32, "sustain": 64, "release": 64, "gain": 128}, []int{}},
{"oscillator", false, map[string]int{"transpose": 64, "detune": 64, "phase": 0, "color": 96, "shape": 64, "gain": 128, "flags": 0x40}, []int{}},
{"mulp", false, map[string]int{}, []int{}},
{"envelope", false, map[string]int{"attack": 32, "decay": 32, "sustain": 64, "release": 64, "gain": 128}, []int{}},
{"oscillator", false, map[string]int{"transpose": 72, "detune": 64, "phase": 64, "color": 64, "shape": 96, "gain": 128, "flags": 0x40}, []int{}},
{"mulp", false, map[string]int{}, []int{}},
{"out", true, map[string]int{"gain": 128}, []int{}},
}},
},
}

View File

@ -23,14 +23,14 @@ const su_max_samples = SAMPLES_PER_ROW * TOTAL_ROWS
// const bufsize = su_max_samples * 2
func TestPlayer(t *testing.T) {
patch := go4k.Patch{go4k.Instrument{1, []go4k.Unit{
go4k.Unit{"envelope", false, map[string]int{"attack": 32, "decay": 32, "sustain": 64, "release": 64, "gain": 128}},
go4k.Unit{"oscillator", false, map[string]int{"transpose": 64, "detune": 64, "phase": 0, "color": 96, "shape": 64, "gain": 128, "type": go4k.Sine, "lfo": 0, "unison": 0}},
go4k.Unit{"mulp", false, map[string]int{}},
go4k.Unit{"envelope", false, map[string]int{"attack": 32, "decay": 32, "sustain": 64, "release": 64, "gain": 128}},
go4k.Unit{"oscillator", false, map[string]int{"transpose": 72, "detune": 64, "phase": 64, "color": 64, "shape": 96, "gain": 128, "type": go4k.Sine, "lfo": 0, "unison": 0}},
go4k.Unit{"mulp", false, map[string]int{}},
go4k.Unit{"out", true, map[string]int{"gain": 128}},
patch := []go4k.Instrument{go4k.Instrument{1, []go4k.Unit{
go4k.Unit{"envelope", false, map[string]int{"attack": 32, "decay": 32, "sustain": 64, "release": 64, "gain": 128}, []int{}},
go4k.Unit{"oscillator", false, map[string]int{"transpose": 64, "detune": 64, "phase": 0, "color": 96, "shape": 64, "gain": 128, "type": go4k.Sine, "lfo": 0, "unison": 0}, []int{}},
go4k.Unit{"mulp", false, map[string]int{}, []int{}},
go4k.Unit{"envelope", false, map[string]int{"attack": 32, "decay": 32, "sustain": 64, "release": 64, "gain": 128}, []int{}},
go4k.Unit{"oscillator", false, map[string]int{"transpose": 72, "detune": 64, "phase": 64, "color": 64, "shape": 96, "gain": 128, "type": go4k.Sine, "lfo": 0, "unison": 0}, []int{}},
go4k.Unit{"mulp", false, map[string]int{}, []int{}},
go4k.Unit{"out", true, map[string]int{"gain": 128}, []int{}},
}}}
patterns := [][]byte{{64, 0, 68, 0, 32, 0, 0, 0, 75, 0, 78, 0, 0, 0, 0, 0}}
tracks := []go4k.Track{go4k.Track{1, []byte{0}}}

View File

@ -15,13 +15,13 @@ var defaultSong = go4k.Song{
SongLength: 0,
Patch: go4k.Patch{
go4k.Instrument{NumVoices: 2, Units: []go4k.Unit{
{"envelope", false, map[string]int{"attack": 32, "decay": 32, "sustain": 64, "release": 64, "gain": 128}},
{"oscillator", false, map[string]int{"transpose": 64, "detune": 64, "phase": 0, "color": 96, "shape": 64, "gain": 128, "type": go4k.Sine}},
{"mulp", false, map[string]int{}},
{"envelope", false, map[string]int{"attack": 32, "decay": 32, "sustain": 64, "release": 64, "gain": 128}},
{"oscillator", false, map[string]int{"transpose": 72, "detune": 64, "phase": 64, "color": 64, "shape": 96, "gain": 128, "type": go4k.Sine}},
{"mulp", false, map[string]int{}},
{"out", true, map[string]int{"gain": 128}},
{"envelope", false, map[string]int{"attack": 32, "decay": 32, "sustain": 64, "release": 64, "gain": 128}, []int{}},
{"oscillator", false, map[string]int{"transpose": 64, "detune": 64, "phase": 0, "color": 96, "shape": 64, "gain": 128, "type": go4k.Sine}, []int{}},
{"mulp", false, map[string]int{}, []int{}},
{"envelope", false, map[string]int{"attack": 32, "decay": 32, "sustain": 64, "release": 64, "gain": 128}, []int{}},
{"oscillator", false, map[string]int{"transpose": 72, "detune": 64, "phase": 64, "color": 64, "shape": 96, "gain": 128, "type": go4k.Sine}, []int{}},
{"mulp", false, map[string]int{}, []int{}},
{"out", true, map[string]int{"gain": 128}, []int{}},
}},
},
}

View File

@ -1,7 +1,7 @@
#ifndef _SOINTU_H
#define _SOINTU_H
#pragma pack(push,4) // this should be fine for both Go and assembly
#pragma pack(push,1) // this should be fine for both Go and assembly
typedef struct Unit {
float State[8];
float Ports[8];
@ -30,9 +30,17 @@ typedef struct SynthWorkspace {
struct Voice Voices[32];
} SynthWorkspace;
typedef struct SampleOffset {
unsigned int Start;
unsigned short LoopStart;
unsigned short LoopLength;
} SampleOffset;
typedef struct Synth {
struct SynthWorkspace SynthWrk;
struct DelayWorkspace DelayWrks[64]; // let's keep this as 64 for now, so the delays take 16 meg. If that's too little or too much, we can change this in future.
unsigned short DelayTimes[768];
struct SampleOffset SampleOffsets[256];
unsigned int RandSeed;
unsigned int GlobalTick;
unsigned char Commands[32 * 64];
@ -52,9 +60,7 @@ typedef struct Synth {
#define CALLCONV // the asm will use honor honor correct x64 ABI on all 64-bit platforms
#endif
#ifdef INCLUDE_GMDLS
extern void CALLCONV su_load_gmdls(void);
#endif
void CALLCONV su_load_gmdls(void);
// int su_render(Synth* synth, float* buffer, int* samples, int* time):
// Renders samples until 'samples' number of samples are reached or 'time' number of

View File

@ -70,6 +70,7 @@ USE_IN
%define INCLUDE_SINE
%define INCLUDE_PULSE
%define INCLUDE_GATE
%define INCLUDE_SAMPLES
%define INCLUDE_UNISONS
%define INCLUDE_POLYPHONY
%define INCLUDE_MULTIVOICE_TRACKS
@ -80,14 +81,26 @@ USE_IN
%define INCLUDE_NEGBANDPASS
%define INCLUDE_NEGHIGHPASS
%define INCLUDE_GLOBAL_SEND
%define INCLUDE_DELAY_NOTETRACKING
%define INCLUDE_DELAY_FLOAT_TIME
%define INCLUDE_GMDLS
%include "sointu/footer.inc"
section .text
struc su_sampleoff
.start resd 1
.loopstart resw 1
.looplength resw 1
.size:
endstruc
struc su_synth
.synthwrk resb su_synthworkspace.size
.delaywrks resb su_delayline_wrk.size * 64
.delaytimes resw 768
.sampleoffs resb su_sampleoff.size * 256
.randseed resd 1
.globaltime resd 1
.commands resb 32 * 64
@ -125,6 +138,10 @@ EXPORT MANGLE_FUNC(su_render,16)
push _SI ; push bufsize
push _DX ; push bufptr
push _CX ; this takes place of the voicetrack
lea _AX, [_CX + su_synth.sampleoffs]
push _AX
lea _AX, [_CX + su_synth.delaytimes]
push _AX
mov eax, [_CX + su_synth.randseed]
push _AX ; randseed
mov eax, [_CX + su_synth.globaltime]
@ -135,12 +152,12 @@ EXPORT MANGLE_FUNC(su_render,16)
su_render_samples_loop:
cmp eax, [_SP] ; if rowtick >= maxtime
jge su_render_samples_time_finish ; goto finish
mov ecx, [_SP + PTRSIZE*5] ; ecx = buffer length in samples
cmp [_SP + PTRSIZE*6], ecx ; if samples >= maxsamples
mov ecx, [_SP + PTRSIZE*7] ; ecx = buffer length in samples
cmp [_SP + PTRSIZE*8], ecx ; if samples >= maxsamples
jge su_render_samples_time_finish ; goto finish
inc eax ; time++
inc dword [_SP + PTRSIZE*6] ; samples++
mov _CX, [_SP + PTRSIZE*3]
inc dword [_SP + PTRSIZE*8] ; samples++
mov _CX, [_SP + PTRSIZE*5]
push _AX ; push rowtick
mov eax, [_CX + su_synth.polyphony]
push _AX ;polyphony
@ -154,12 +171,12 @@ su_render_samples_loop:
call MANGLE_FUNC(su_run_vm,0)
pop _AX
pop _AX
mov _DI, [_SP + PTRSIZE*5] ; edi containts buffer ptr
mov _CX, [_SP + PTRSIZE*4]
mov _DI, [_SP + PTRSIZE*7] ; edi containts buffer ptr
mov _CX, [_SP + PTRSIZE*6]
lea _SI, [_CX + su_synth.synthwrk + su_synthworkspace.left]
movsd ; copy left channel to output buffer
movsd ; copy right channel to output buffer
mov [_SP + PTRSIZE*5], _DI ; save back the updated ptr
mov [_SP + PTRSIZE*7], _DI ; save back the updated ptr
lea _DI, [_SI-8]
xor eax, eax
stosd ; clear left channel so the VM is ready to write them again
@ -170,7 +187,9 @@ su_render_samples_loop:
su_render_samples_time_finish:
pop _CX
pop _BX
pop _DX
pop _DX
pop _CX ; discard delaytimes ptr
pop _CX ; discard samplesoffs ptr
pop _CX
mov [_CX + su_synth.randseed], edx
mov [_CX + su_synth.globaltime], ebx