mirror of
https://github.com/vsariola/sointu.git
synced 2025-05-28 03:10:24 -04:00
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:
parent
bb0d4d6800
commit
38e9007bf8
7
audio.go
7
audio.go
@ -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
|
||||
}
|
||||
|
||||
|
@ -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 {
|
||||
|
@ -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)
|
||||
|
@ -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]
|
||||
},
|
||||
|
@ -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)
|
||||
}
|
||||
|
@ -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 {
|
||||
|
@ -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])
|
||||
|
26
synth.go
26
synth.go
@ -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)
|
||||
}
|
||||
|
@ -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 {
|
||||
|
@ -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")
|
||||
|
@ -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}
|
||||
}
|
||||
|
@ -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)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -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++
|
||||
|
@ -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)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
Loading…
Reference in New Issue
Block a user