diff --git a/audio.go b/audio.go index d32c5a0..2860a4b 100644 --- a/audio.go +++ b/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 } diff --git a/audioexport.go b/audioexport.go index d5166dc..b853552 100644 --- a/audioexport.go +++ b/audioexport.go @@ -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 { diff --git a/cmd/sointu-track/main.go b/cmd/sointu-track/main.go index c1bfd5b..b9680f3 100644 --- a/cmd/sointu-track/main.go +++ b/cmd/sointu-track/main.go @@ -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) diff --git a/cmd/sointu-vsti/main.go b/cmd/sointu-vsti/main.go index 3aafb2e..4f3670e 100644 --- a/cmd/sointu-vsti/main.go +++ b/cmd/sointu-vsti/main.go @@ -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] }, diff --git a/oto/convertbuffer.go b/oto/convertbuffer.go index 9ac72c6..cbe9610 100644 --- a/oto/convertbuffer.go +++ b/oto/convertbuffer.go @@ -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) +} diff --git a/oto/convertbuffer_test.go b/oto/convertbuffer_test.go index 4c5ac2c..f5697d4 100644 --- a/oto/convertbuffer_test.go +++ b/oto/convertbuffer_test.go @@ -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 { diff --git a/oto/oto.go b/oto/oto.go index 0d24ee3..17e7183 100644 --- a/oto/oto.go +++ b/oto/oto.go @@ -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]) diff --git a/synth.go b/synth.go index 2242e2b..246e4eb 100644 --- a/synth.go +++ b/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) } diff --git a/tracker/player.go b/tracker/player.go index 372b738..fbef8ed 100644 --- a/tracker/player.go +++ b/tracker/player.go @@ -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 { diff --git a/tracker/volume.go b/tracker/volume.go index fedfe2c..e00da42 100644 --- a/tracker/volume.go +++ b/tracker/volume.go @@ -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") diff --git a/vm/compiler/bridge/bridge.go b/vm/compiler/bridge/bridge.go index cdc1f77..1cca9a7 100644 --- a/vm/compiler/bridge/bridge.go +++ b/vm/compiler/bridge/bridge.go @@ -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} } diff --git a/vm/compiler/bridge/bridge_test.go b/vm/compiler/bridge/bridge_test.go index 360584f..1e8fa13 100644 --- a/vm/compiler/bridge/bridge_test.go +++ b/vm/compiler/bridge/bridge_test.go @@ -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) + } } } } diff --git a/vm/interpreter.go b/vm/interpreter.go index 9ff778c..59e633f 100644 --- a/vm/interpreter.go +++ b/vm/interpreter.go @@ -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++ diff --git a/vm/interpreter_test.go b/vm/interpreter_test.go index b714c24..0f6d0a1 100644 --- a/vm/interpreter_test.go +++ b/vm/interpreter_test.go @@ -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) + } } } }