refactor: use [][2] as audio buffers, instead of []float32

Throughout sointu, we assume stereo audiobuffers, but were passing
around []float32. This had several issues, including len(buf)/2 and
numSamples*2 type of length conversion in many places. Also, it
caused one bug in a test case, causing it to succeed when it should
have not (the test had +-1 when it should have had +-2). This
refactoring makes it impossible to have odd length buffer issues.
This commit is contained in:
5684185+vsariola@users.noreply.github.com
2023-10-18 13:51:02 +03:00
parent bb0d4d6800
commit 38e9007bf8
14 changed files with 106 additions and 82 deletions

View File

@ -79,15 +79,15 @@ func Synth(patch sointu.Patch, bpm int) (*BridgeSynth, 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 (bridgesynth *BridgeSynth) Render(buffer []float32, maxtime int) (int, int, error) {
func (bridgesynth *BridgeSynth) Render(buffer sointu.AudioBuffer, maxtime int) (int, int, error) {
synth := (*C.Synth)(bridgesynth)
// 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")
}
samples := C.int(len(buffer) / 2)
samples := C.int(len(buffer))
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][0]), &samples, &time))
if errcode > 0 {
return int(samples), int(time), &RenderError{errcode: errcode}
}

View File

@ -59,7 +59,7 @@ func TestRenderSamples(t *testing.T) {
t.Fatalf("bridge compile error: %v", err)
}
synth.Trigger(0, 64)
buffer := make([]float32, 2*su_max_samples)
buffer := make(sointu.AudioBuffer, su_max_samples)
err = sointu.Render(synth, buffer[:len(buffer)/2])
if err != nil {
t.Fatalf("first render gave an error")
@ -96,7 +96,7 @@ func TestAllRegressionTests(t *testing.T) {
t.Fatalf("could not parse the .yml file: %v", err)
}
buffer, err := sointu.Play(bridge.BridgeService{}, song, false)
buffer = buffer[:song.Score.LengthInRows()*song.SamplesPerRow()*2] // extend to the nominal length always.
buffer = buffer[:song.Score.LengthInRows()*song.SamplesPerRow()] // extend to the nominal length always.
if err != nil {
t.Fatalf("Play failed: %v", err)
}
@ -134,7 +134,7 @@ func TestStackUnderflow(t *testing.T) {
if err != nil {
t.Fatalf("bridge compile error: %v", err)
}
buffer := make([]float32, 2)
buffer := make(sointu.AudioBuffer, 1)
err = sointu.Render(synth, buffer)
if err == nil {
t.Fatalf("rendering should have failed due to stack underflow")
@ -150,7 +150,7 @@ func TestStackBalancing(t *testing.T) {
if err != nil {
t.Fatalf("bridge compile error: %v", err)
}
buffer := make([]float32, 2)
buffer := make(sointu.AudioBuffer, 1)
err = sointu.Render(synth, buffer)
if err == nil {
t.Fatalf("rendering should have failed due to unbalanced stack push/pop")
@ -183,7 +183,7 @@ func TestStackOverflow(t *testing.T) {
if err != nil {
t.Fatalf("bridge compile error: %v", err)
}
buffer := make([]float32, 2)
buffer := make(sointu.AudioBuffer, 1)
err = sointu.Render(synth, buffer)
if err == nil {
t.Fatalf("rendering should have failed due to stack overflow, despite balanced push/pops")
@ -200,20 +200,20 @@ func TestDivideByZero(t *testing.T) {
if err != nil {
t.Fatalf("bridge compile error: %v", err)
}
buffer := make([]float32, 2)
buffer := make(sointu.AudioBuffer, 1)
err = sointu.Render(synth, buffer)
if err == nil {
t.Fatalf("rendering should have failed due to divide by zero")
}
}
func compareToRawFloat32(t *testing.T, buffer []float32, rawname string) {
func compareToRawFloat32(t *testing.T, buffer sointu.AudioBuffer, rawname string) {
_, filename, _, _ := runtime.Caller(0)
expectedb, err := ioutil.ReadFile(path.Join(path.Dir(filename), "..", "..", "..", "tests", "expected_output", rawname))
if err != nil {
t.Fatalf("cannot read expected: %v", err)
}
expected := make([]float32, len(expectedb)/4)
expected := make(sointu.AudioBuffer, len(expectedb)/8)
buf := bytes.NewReader(expectedb)
err = binary.Read(buf, binary.LittleEndian, &expected)
if err != nil {
@ -223,8 +223,10 @@ func compareToRawFloat32(t *testing.T, buffer []float32, rawname string) {
t.Fatalf("buffer length mismatch, got %v, expected %v", len(buffer), len(expected))
}
for i, v := range expected {
if math.IsNaN(float64(buffer[i])) || math.Abs(float64(v-buffer[i])) > 1e-6 {
t.Fatalf("error bigger than 1e-6 detected, at sample position %v", i)
for j, s := range v {
if math.IsNaN(float64(buffer[i][j])) || math.Abs(float64(s-buffer[i][j])) > 1e-6 {
t.Fatalf("error bigger than 1e-6 detected, at sample position %v", i)
}
}
}
}

View File

@ -140,7 +140,7 @@ func (s *Interpreter) Update(patch sointu.Patch, bpm int) error {
return nil
}
func (s *Interpreter) Render(buffer []float32, maxtime int) (samples int, time int, renderError error) {
func (s *Interpreter) Render(buffer sointu.AudioBuffer, maxtime int) (samples int, time int, renderError error) {
defer func() {
if err := recover(); err != nil {
renderError = fmt.Errorf("render panicced: %v", err)
@ -150,7 +150,7 @@ func (s *Interpreter) Render(buffer []float32, maxtime int) (samples int, time i
stack := s.stack[:]
stack = append(stack, []float32{0, 0, 0, 0}...)
synth := &s.synth
for time < maxtime && len(buffer) > 1 {
for time < maxtime && len(buffer) > 0 {
commandInstr := s.bytePatch.Commands
valuesInstr := s.bytePatch.Values
commands, values := commandInstr, valuesInstr
@ -580,11 +580,10 @@ func (s *Interpreter) Render(buffer []float32, maxtime int) (samples int, time i
if len(stack) > 4 {
return samples, time, errors.New("stack not empty")
}
buffer[0] = synth.outputs[0]
buffer[1] = synth.outputs[1]
buffer[0][0], buffer[0][1] = synth.outputs[0], synth.outputs[1]
synth.outputs[0] = 0
synth.outputs[1] = 0
buffer = buffer[2:]
buffer = buffer[1:]
samples++
time++
s.synth.globalTime++

View File

@ -18,6 +18,8 @@ import (
"gopkg.in/yaml.v2"
)
const errorThreshold = 1e-2
func TestAllRegressionTests(t *testing.T) {
_, myname, _, _ := runtime.Caller(0)
files, err := filepath.Glob(path.Join(path.Dir(myname), "..", "tests", "*.yml"))
@ -42,7 +44,7 @@ func TestAllRegressionTests(t *testing.T) {
t.Fatalf("could not parse the .yml file: %v", err)
}
buffer, err := sointu.Play(vm.SynthService{}, song, false)
buffer = buffer[:song.Score.LengthInRows()*song.SamplesPerRow()*2] // extend to the nominal length always.
buffer = buffer[:song.Score.LengthInRows()*song.SamplesPerRow()] // extend to the nominal length always.
if err != nil {
t.Fatalf("Play failed: %v", err)
}
@ -80,7 +82,7 @@ func TestStackUnderflow(t *testing.T) {
if err != nil {
t.Fatalf("bridge compile error: %v", err)
}
buffer := make([]float32, 2)
buffer := make(sointu.AudioBuffer, 1)
err = sointu.Render(synth, buffer)
if err == nil {
t.Fatalf("rendering should have failed due to stack underflow")
@ -96,20 +98,20 @@ func TestStackBalancing(t *testing.T) {
if err != nil {
t.Fatalf("bridge compile error: %v", err)
}
buffer := make([]float32, 2)
buffer := make(sointu.AudioBuffer, 1)
err = sointu.Render(synth, buffer)
if err == nil {
t.Fatalf("rendering should have failed due to unbalanced stack push/pop")
}
}
func compareToRawFloat32(t *testing.T, buffer []float32, rawname string) {
func compareToRawFloat32(t *testing.T, buffer sointu.AudioBuffer, rawname string) {
_, filename, _, _ := runtime.Caller(0)
expectedb, err := ioutil.ReadFile(path.Join(path.Dir(filename), "..", "tests", "expected_output", rawname))
if err != nil {
t.Fatalf("cannot read expected: %v", err)
}
expected := make([]float32, len(expectedb)/4)
expected := make(sointu.AudioBuffer, len(expectedb)/8)
buf := bytes.NewReader(expectedb)
err = binary.Read(buf, binary.LittleEndian, &expected)
if err != nil {
@ -121,14 +123,16 @@ func compareToRawFloat32(t *testing.T, buffer []float32, rawname string) {
firsterr := -1
errs := 0
for i, v := range expected[1 : len(expected)-1] {
if math.IsNaN(float64(buffer[i])) || (math.Abs(float64(v-buffer[i])) > 1e-2 &&
math.Abs(float64(v-buffer[i+1])) > 1e-2 && math.Abs(float64(v-buffer[i+2])) > 1e-2) {
errs++
if firsterr == -1 {
firsterr = i
}
if errs > 200 { // we are again quite liberal with rounding errors, as different platforms have minor differences in floating point rounding
t.Fatalf("more than 200 errors bigger than 1e-2 detected, first at sample position %v", firsterr)
for j, s := range v {
if math.IsNaN(float64(buffer[i][j])) || (math.Abs(float64(s-buffer[i][j])) > errorThreshold &&
math.Abs(float64(s-buffer[i+1][j])) > errorThreshold && math.Abs(float64(s-buffer[i+2][j])) > errorThreshold) {
errs++
if firsterr == -1 {
firsterr = i
}
if errs > 200 { // we are again quite liberal with rounding errors, as different platforms have minor differences in floating point rounding
t.Fatalf("more than 200 errors bigger than %v detected, first at sample position %v", errorThreshold, firsterr)
}
}
}
}