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

@ -1,9 +1,14 @@
package sointu
// AudioBuffer is a buffer of stereo audio samples of variable length, each
// sample represented by a slice of [2]float32. [0] is left channel, [1] is
// right
type AudioBuffer [][2]float32
// AudioOutput represents something where we can send audio e.g. audio output.
// WriteAudio should block if not ready to accept audio e.g. buffer full.
type AudioOutput interface {
WriteAudio(buffer []float32) error
WriteAudio(buffer AudioBuffer) error
Close() error
}

View File

@ -12,9 +12,9 @@ import (
//
// If pcm16 is set to true, the samples in the WAV-file will be 16-bit signed
// integers; otherwise the samples will be 32-bit floats
func Wav(buffer []float32, pcm16 bool) ([]byte, error) {
func Wav(buffer AudioBuffer, pcm16 bool) ([]byte, error) {
buf := new(bytes.Buffer)
wavHeader(len(buffer), pcm16, buf)
wavHeader(len(buffer)*2, pcm16, buf)
err := rawToBuffer(buffer, pcm16, buf)
if err != nil {
return nil, fmt.Errorf("Wav failed: %v", err)
@ -27,7 +27,7 @@ func Wav(buffer []float32, pcm16 bool) ([]byte, error) {
//
// If pcm16 is set to true, the samples will be 16-bit signed integers;
// otherwise the samples will be 32-bit floats
func Raw(buffer []float32, pcm16 bool) ([]byte, error) {
func Raw(buffer AudioBuffer, pcm16 bool) ([]byte, error) {
buf := new(bytes.Buffer)
err := rawToBuffer(buffer, pcm16, buf)
if err != nil {
@ -36,12 +36,13 @@ func Raw(buffer []float32, pcm16 bool) ([]byte, error) {
return buf.Bytes(), nil
}
func rawToBuffer(data []float32, pcm16 bool, buf *bytes.Buffer) error {
func rawToBuffer(data AudioBuffer, pcm16 bool, buf *bytes.Buffer) error {
var err error
if pcm16 {
int16data := make([]int16, len(data))
int16data := make([][2]int16, len(data))
for i, v := range data {
int16data[i] = int16(clamp(int(v*math.MaxInt16), math.MinInt16, math.MaxInt16))
int16data[i][0] = int16(clamp(int(v[0]*math.MaxInt16), math.MinInt16, math.MaxInt16))
int16data[i][1] = int16(clamp(int(v[1]*math.MaxInt16), math.MinInt16, math.MaxInt16))
}
err = binary.Write(buf, binary.LittleEndian, int16data)
} else {

View File

@ -10,6 +10,7 @@ import (
"runtime/pprof"
"gioui.org/app"
"github.com/vsariola/sointu"
"github.com/vsariola/sointu/cmd"
"github.com/vsariola/sointu/oto"
"github.com/vsariola/sointu/tracker"
@ -61,7 +62,7 @@ func main() {
output := audioContext.Output()
defer output.Close()
go func() {
buf := make([]float32, 2048)
buf := make(sointu.AudioBuffer, 1024)
ctx := NullContext{}
for {
player.Process(buf, ctx)

View File

@ -8,6 +8,7 @@ import (
"os"
"path/filepath"
"github.com/vsariola/sointu"
"github.com/vsariola/sointu/cmd"
"github.com/vsariola/sointu/tracker"
"github.com/vsariola/sointu/tracker/gioui"
@ -66,7 +67,7 @@ func init() {
tracker.SetInstrEnlarged(true) // start the vsti with the instrument editor enlarged
go tracker.Main()
context := VSTIProcessContext{make([]vst2.MIDIEvent, 100), h}
buf := make([]float32, 2048)
buf := make(sointu.AudioBuffer, 1024)
return vst2.Plugin{
UniqueID: PLUGIN_ID,
Version: version,
@ -79,13 +80,13 @@ func init() {
ProcessFloatFunc: func(in, out vst2.FloatBuffer) {
left := out.Channel(0)
right := out.Channel(1)
if len(buf) < out.Frames*2 {
buf = append(buf, make([]float32, out.Frames*2-len(buf))...)
if len(buf) < out.Frames {
buf = append(buf, make(sointu.AudioBuffer, out.Frames-len(buf))...)
}
buf = buf[:out.Frames*2]
buf = buf[:out.Frames]
player.Process(buf, &context)
for i := 0; i < out.Frames; i++ {
left[i], right[i] = buf[i*2], buf[i*2+1]
left[i], right[i] = buf[i][0], buf[i][1]
}
context.events = context.events[:0]
},

View File

@ -2,6 +2,8 @@ package oto
import (
"math"
"github.com/vsariola/sointu"
)
// FloatBufferTo16BitLE is a naive helper method to convert []float32 buffers to
@ -9,17 +11,22 @@ import (
//
// Appends the encoded bytes into "to" slice, allowing you to preallocate the
// capacity or just use nil
func FloatBufferTo16BitLE(from []float32, to []byte) []byte {
func FloatBufferTo16BitLE(from sointu.AudioBuffer, to []byte) []byte {
for _, v := range from {
var uv int16
if v < -1.0 {
uv = -math.MaxInt16 // we are a bit lazy: -1.0 is encoded as -32767, as this makes math easier, and -32768 is unused
} else if v > 1.0 {
uv = math.MaxInt16
} else {
uv = int16(v * math.MaxInt16)
}
to = append(to, byte(uv&255), byte(uv>>8))
left := to16BitSample(v[0])
right := to16BitSample(v[1])
to = append(to, byte(left&255), byte(left>>8), byte(right&255), byte(right>>8))
}
return to
}
// convert float32 to int16, clamping to min and max
func to16BitSample(v float32) int16 {
if v < -1.0 {
return -math.MaxInt16
}
if v > 1.0 {
return math.MaxInt16
}
return int16(v * math.MaxInt16)
}

View File

@ -4,11 +4,12 @@ import (
"reflect"
"testing"
"github.com/vsariola/sointu"
"github.com/vsariola/sointu/oto"
)
func TestFloatBufferToBytes(t *testing.T) {
floats := []float32{0, 0.000489128, 0, 0.0019555532, 0, 0.0043964, 0, 0.007806882, 0, 0.012180306, 0, 0.017508084, 0, 0.023779746, 0, 0.030982954, 0, 0.039103523, 0, 0.04812544, 0, 0.05803088, 0, 0.068800256, 0, 0.08041221, 0, 0.09284368, 0, 0.10606992, 0, 0.120064534, 0, 0.13479951, 0, 0.1502453, 0, 0.16637078, 0, 0.18314338, 0, 0.20052913, 0, 0.21849263, 0, 0.23699719, 0, 0.2560048, 0, 0.27547634, 0, 0.29537144, 0, 0.31564865, 0, 0.33626547, 0, 0.35717854, 0, 0.37834346, 0, 0.39971504, 0, 0.4212474, 0, 0.4428938, 0, 0.46460703, 0, 0.48633927, 0, 0.50804216, 0, 0.52966696, 0, 0.5511646, 0, 0.57248586, 0, 0.5935812, 0, 0.6144009, 0, 0.63489544, 0, 0.6550152, 0, 0.67471063, 0, 0.6939326, 0, 0.712632, 0, 0.7307603, 0, 0.7482692, 0, 0.7651111, 0, 0.7812389}
floats := sointu.AudioBuffer{{0, 0.000489128}, {0, 0.0019555532}, {0, 0.0043964}, {0, 0.007806882}, {0, 0.012180306}, {0, 0.017508084}, {0, 0.023779746}, {0, 0.030982954}, {0, 0.039103523}, {0, 0.04812544}, {0, 0.05803088}, {0, 0.068800256}, {0, 0.08041221}, {0, 0.09284368}, {0, 0.10606992}, {0, 0.120064534}, {0, 0.13479951}, {0, 0.1502453}, {0, 0.16637078}, {0, 0.18314338}, {0, 0.20052913}, {0, 0.21849263}, {0, 0.23699719}, {0, 0.2560048}, {0, 0.27547634}, {0, 0.29537144}, {0, 0.31564865}, {0, 0.33626547}, {0, 0.35717854}, {0, 0.37834346}, {0, 0.39971504}, {0, 0.4212474}, {0, 0.4428938}, {0, 0.46460703}, {0, 0.48633927}, {0, 0.50804216}, {0, 0.52966696}, {0, 0.5511646}, {0, 0.57248586}, {0, 0.5935812}, {0, 0.6144009}, {0, 0.63489544}, {0, 0.6550152}, {0, 0.67471063}, {0, 0.6939326}, {0, 0.712632}, {0, 0.7307603}, {0, 0.7482692}, {0, 0.7651111}, {0, 0.7812389}}
bytes := []byte{0x0, 0x0, 0x10, 0x0, 0x0, 0x0, 0x40, 0x0, 0x0, 0x0, 0x90, 0x0, 0x0, 0x0, 0xff, 0x0, 0x0, 0x0, 0x8f, 0x1, 0x0, 0x0, 0x3d, 0x2, 0x0, 0x0, 0xb, 0x3, 0x0, 0x0, 0xf7, 0x3, 0x0, 0x0, 0x1, 0x5, 0x0, 0x0, 0x28, 0x6, 0x0, 0x0, 0x6d, 0x7, 0x0, 0x0, 0xce, 0x8, 0x0, 0x0, 0x4a, 0xa, 0x0, 0x0, 0xe2, 0xb, 0x0, 0x0, 0x93, 0xd, 0x0, 0x0, 0x5e, 0xf, 0x0, 0x0, 0x40, 0x11, 0x0, 0x0, 0x3b, 0x13, 0x0, 0x0, 0x4b, 0x15, 0x0, 0x0, 0x71, 0x17, 0x0, 0x0, 0xaa, 0x19, 0x0, 0x0, 0xf7, 0x1b, 0x0, 0x0, 0x55, 0x1e, 0x0, 0x0, 0xc4, 0x20, 0x0, 0x0, 0x42, 0x23, 0x0, 0x0, 0xce, 0x25, 0x0, 0x0, 0x66, 0x28, 0x0, 0x0, 0xa, 0x2b, 0x0, 0x0, 0xb7, 0x2d, 0x0, 0x0, 0x6d, 0x30, 0x0, 0x0, 0x29, 0x33, 0x0, 0x0, 0xeb, 0x35, 0x0, 0x0, 0xb0, 0x38, 0x0, 0x0, 0x77, 0x3b, 0x0, 0x0, 0x3f, 0x3e, 0x0, 0x0, 0x7, 0x41, 0x0, 0x0, 0xcb, 0x43, 0x0, 0x0, 0x8c, 0x46, 0x0, 0x0, 0x46, 0x49, 0x0, 0x0, 0xf9, 0x4b, 0x0, 0x0, 0xa4, 0x4e, 0x0, 0x0, 0x43, 0x51, 0x0, 0x0, 0xd6, 0x53, 0x0, 0x0, 0x5c, 0x56, 0x0, 0x0, 0xd2, 0x58, 0x0, 0x0, 0x36, 0x5b, 0x0, 0x0, 0x88, 0x5d, 0x0, 0x0, 0xc6, 0x5f, 0x0, 0x0, 0xee, 0x61, 0x0, 0x0, 0xfe, 0x63}
converted := oto.FloatBufferTo16BitLE(floats, nil)
for i, v := range converted {
@ -23,13 +24,14 @@ func TestFloatBufferToBytes(t *testing.T) {
}
func TestFloatBufferToBytesLimits(t *testing.T) {
floats := []float32{0, 1, -1, 0.999, -0.999}
floats := sointu.AudioBuffer{{0, 1}, {-1, 0.999}, {-0.999, 0}}
bytes := []byte{
0x0, 0x0,
0xFF, 0x7F, // float 1 = 0x7FFF = 0111111111111111
0x01, 0x80, // float -1 = 0x8001 = 1000000000000001
0xDE, 0x7F, // float 0.999 = 0x7FDE = 0111111111011110
0x22, 0x80, // float -0.999 = 0x8022 = 1000000000100010
0x0, 0x0,
}
converted := oto.FloatBufferTo16BitLE(floats, nil)
for i, v := range converted {

View File

@ -36,7 +36,7 @@ func (c *OtoContext) Close() error {
}
// Play implements the audio.Player interface for OtoPlayer
func (o *OtoOutput) WriteAudio(floatBuffer []float32) (err error) {
func (o *OtoOutput) WriteAudio(floatBuffer sointu.AudioBuffer) (err error) {
// we reuse the old capacity tmpBuffer by setting its length to zero. then,
// we save the tmpBuffer so we can reuse it next time
o.tmpBuffer = FloatBufferTo16BitLE(floatBuffer, o.tmpBuffer[:0])

View File

@ -10,12 +10,12 @@ import (
type Synth interface {
// Render tries to fill a stereo signal buffer with sound from the
// synthesizer, until either the buffer is full or a given number of
// timesteps is advanced. Normally, 1 sample = 1 unit of time, but
// speed modulations may change this. It returns the number of samples
// filled (! in stereo samples, so the buffer will have 2 * sample floats),
// the number of sync outputs written, the number of time steps time
// advanced, and a possible error.
Render(buffer []float32, maxtime int) (sample int, time int, err error)
// timesteps is advanced. Normally, 1 sample = 1 unit of time, but speed
// modulations may change this. It returns the number of samples filled (in
// stereo samples i.e. number of elements of AudioBuffer filled), the
// number of sync outputs written, the number of time steps time advanced,
// and a possible error.
Render(buffer AudioBuffer, maxtime int) (sample int, time int, err error)
// Update recompiles a patch, but should maintain as much as possible of its
// state as reasonable. For example, filters should keep their state and
@ -40,12 +40,12 @@ type SynthService interface {
// Render fills an stereo audio buffer using a Synth, disregarding all syncs and
// time limits.
func Render(synth Synth, buffer []float32) error {
func Render(synth Synth, buffer AudioBuffer) error {
s, _, err := synth.Render(buffer, math.MaxInt32)
if err != nil {
return fmt.Errorf("sointu.Render failed: %v", err)
}
if s != len(buffer)/2 {
if s != len(buffer) {
return errors.New("in sointu.Render, synth.Render should have filled the whole buffer but did not")
}
return nil
@ -57,7 +57,7 @@ func Render(synth Synth, buffer []float32) error {
// created. The default behaviour during runtime rendering is to leave them
// playing, meaning that envelopes start attacking right away unless an explicit
// note release is put to every track.
func Play(synthService SynthService, song Song, release bool) ([]float32, error) {
func Play(synthService SynthService, song Song, release bool) (AudioBuffer, error) {
err := song.Validate()
if err != nil {
return nil, err
@ -75,9 +75,9 @@ func Play(synthService SynthService, song Song, release bool) ([]float32, error)
for i := range curVoices {
curVoices[i] = song.Score.FirstVoiceForTrack(i)
}
initialCapacity := song.Score.LengthInRows() * song.SamplesPerRow() * 2
buffer := make([]float32, 0, initialCapacity)
rowbuffer := make([]float32, song.SamplesPerRow()*2)
initialCapacity := song.Score.LengthInRows() * song.SamplesPerRow()
buffer := make(AudioBuffer, 0, initialCapacity)
rowbuffer := make(AudioBuffer, song.SamplesPerRow())
for row := 0; row < song.Score.LengthInRows(); row++ {
patternRow := row % song.Score.RowsPerPattern
pattern := row / song.Score.RowsPerPattern
@ -116,7 +116,7 @@ func Play(synthService SynthService, song Song, release bool) ([]float32, error)
return buffer, fmt.Errorf("render failed: %v", err)
}
rowtime += time
buffer = append(buffer, rowbuffer[:samples*2]...)
buffer = append(buffer, rowbuffer[:samples]...)
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)
}

View File

@ -95,13 +95,13 @@ func NewPlayer(synthService sointu.SynthService, playerMessages chan<- PlayerMes
return p
}
func (p *Player) Process(buffer []float32, context PlayerProcessContext) {
func (p *Player) Process(buffer sointu.AudioBuffer, context PlayerProcessContext) {
p.processMessages(context)
midi, midiOk := context.NextEvent()
frame := 0
if p.recording && p.recordingNoteArrived {
p.recordingFrames += len(buffer) / 2
p.recordingFrames += len(buffer)
}
oldBuffer := buffer
@ -110,11 +110,11 @@ func (p *Player) Process(buffer []float32, context PlayerProcessContext) {
for midiOk && frame >= midi.Frame {
if p.recording {
if !p.recordingNoteArrived {
p.recordingFrames = len(buffer) / 2
p.recordingFrames = len(buffer)
p.recordingNoteArrived = true
}
midiTotalFrame := midi
midiTotalFrame.Frame = p.recordingFrames - len(buffer)/2
midiTotalFrame.Frame = p.recordingFrames - len(buffer)
p.recordingEvents = append(p.recordingEvents, midiTotalFrame)
}
if midi.On {
@ -124,7 +124,7 @@ func (p *Player) Process(buffer []float32, context PlayerProcessContext) {
}
midi, midiOk = context.NextEvent()
}
framesUntilMidi := len(buffer) / 2
framesUntilMidi := len(buffer)
if delta := midi.Frame - frame; midiOk && delta < framesUntilMidi {
framesUntilMidi = delta
}
@ -138,14 +138,14 @@ func (p *Player) Process(buffer []float32, context PlayerProcessContext) {
var rendered, timeAdvanced int
var err error
if p.synth != nil {
rendered, timeAdvanced, err = p.synth.Render(buffer[:framesUntilMidi*2], timeUntilRowAdvance)
rendered, timeAdvanced, err = p.synth.Render(buffer[:framesUntilMidi], timeUntilRowAdvance)
} else {
mx := framesUntilMidi
if timeUntilRowAdvance < mx {
mx = timeUntilRowAdvance
}
for i := 0; i < mx*2; i++ {
buffer[i] = 0
for i := 0; i < mx; i++ {
buffer[i] = [2]float32{}
}
rendered = mx
timeAdvanced = mx
@ -154,7 +154,7 @@ func (p *Player) Process(buffer []float32, context PlayerProcessContext) {
p.synth = nil
p.trySend(PlayerCrashMessage{fmt.Errorf("synth.Render: %w", err)})
}
buffer = buffer[rendered*2:]
buffer = buffer[rendered:]
frame += rendered
p.rowtime += timeAdvanced
for i := range p.samplesSinceEvent {

View File

@ -3,6 +3,8 @@ package tracker
import (
"errors"
"math"
"github.com/vsariola/sointu"
)
// Volume represents an average and peak volume measurement, in decibels. 0 dB =
@ -25,14 +27,14 @@ type Volume struct {
//
// minVolume and maxVolume are hard limits in decibels to prevent negative
// infinities for volumes
func (v *Volume) Analyze(buffer []float32, tau float64, attack float64, release float64, minVolume float64, maxVolume float64) error {
func (v *Volume) Analyze(buffer sointu.AudioBuffer, tau float64, attack float64, release float64, minVolume float64, maxVolume float64) error {
alpha := 1 - math.Exp(-1.0/(tau*44100)) // from https://en.wikipedia.org/wiki/Exponential_smoothing
alphaAttack := 1 - math.Exp(-1.0/(attack*44100))
alphaRelease := 1 - math.Exp(-1.0/(release*44100))
var err error
for j := 0; j < 2; j++ {
for i := 0; i < len(buffer); i += 2 {
sample2 := float64(buffer[i+j] * buffer[i+j])
for i := 0; i < len(buffer); i++ {
sample2 := float64(buffer[i][j] * buffer[i][j])
if math.IsNaN(sample2) {
if err == nil {
err = errors.New("NaN detected in master output")

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)
}
}
}
}