Change the Go API to have two versions: Render(buffer []float32), which always fill the whole buffer, and RenderTime(buffer []float32,int maxtime), which ends either when the buffer is full, or modulated time is reached.

This commit is contained in:
Veikko Sariola 2020-10-27 17:26:08 +02:00
parent 7a9ac3489b
commit 5f4b85b0a4
3 changed files with 49 additions and 30 deletions

View File

@ -82,25 +82,55 @@ func (o Opcode) Mono() Opcode {
}
// Render tries to fill the buffer with samples rendered by Sointu.
// Use this version if you are not interested in time modulation. Will always
// fill the buffer.
// Parameters:
// buffer float32 slice to fill with rendered samples. Stereo signal, so
// should have even length.
// Returns a tuple (int, bool, error), consisting of the number samples
// rendered (len(buffer)/2 in the case where buffer was filled, less or equal
// if row end was reached before buffer was full), and bool indicating if row
// has ended
func (s *SynthState) Render(buffer []float32) (int, bool, error) {
// Returns an error if something went wrong.
func (s *SynthState) Render(buffer []float32) error {
if len(buffer)%1 == 1 {
return -1, false, errors.New("Render writes stereo signals, so buffer should have even length")
return errors.New("Render writes stereo signals, so buffer should have even length")
}
maxSamples := len(buffer) / 2
cs := (*C.SynthState)(s)
cs.SamplesPerRow = C.uint(math.MaxInt32)
cs.RowTick = 0
C.su_render_samples((*C.SynthState)(s), C.int(maxSamples), (*C.float)(&buffer[0]))
return nil
}
// RenderTime renders until the buffer is full or the modulated time is reached, whichever
// happens first.
// Parameters:
// buffer float32 slice to fill with rendered samples. Stereo signal, so
// should have even length.
// maxtime how long nominal time to render in samples. Speed unit might modulate time
// so the actual number of samples rendered is not the
// Returns a tuple (int, int, error), consisting of:
// samples number of samples rendered in the buffer
// time how much the time advanced
// error potential error
// In practice, if nsamples = len(buffer)/2, then time <= maxtime. If maxtime was reached
// first, then nsamples <= len(buffer)/2 and time >= maxtime. Note that it could happen that
// 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 (s *SynthState) RenderTime(buffer []float32, maxtime int) (int, int, error) {
if len(buffer)%1 == 1 {
return -1, -1, errors.New("Render writes stereo signals, so buffer should have even length")
}
maxSamples := len(buffer) / 2
cs := (*C.SynthState)(s)
cs.SamplesPerRow = C.uint(maxtime) // these two lines are here just because the C-API is not
cs.RowTick = 0 // updated. SamplesPerRow should be "maxtime" and passed as a parameter
retval := int(C.su_render_samples((*C.SynthState)(s), C.int(maxSamples), (*C.float)(&buffer[0])))
if retval < 0 {
return maxSamples, false, nil
if retval < 0 { // this ugliness is just because the C-API is not updated yet
return maxSamples, int(cs.RowTick), nil
} else if retval == 0 {
return maxSamples, true, nil
return maxSamples, int(cs.RowTick), nil
} else {
return maxSamples - retval, true, nil
return maxSamples - retval, int(cs.RowTick), nil
}
}
@ -159,10 +189,6 @@ func (s *SynthState) Release(voice int) {
cs.Synth.Voices[voice].Release = 1
}
func (s *SynthState) SetSamplesPerRow(spr int) {
s.SamplesPerRow = C.uint(spr)
}
func NewSynthState() *SynthState {
s := new(SynthState)
s.RandSeed = 1

View File

@ -30,22 +30,15 @@ func TestBridge(t *testing.T) {
}},
})
s.Trigger(0, 64)
s.SamplesPerRow = SAMPLES_PER_ROW * 8 // this song is two blocks of 8 rows, release before second block start
buffer := make([]float32, 2*su_max_samples)
n, rowend, err := s.Render(buffer)
if n < su_max_samples/2 {
t.Fatalf("render should have filled half of the buffer on first call, %v samples rendered, %v expected", n, su_max_samples/2)
}
if rowend != true {
t.Fatalf("Row end should have been hit (rowend should have been true) on the first call to Render")
err := s.Render(buffer[:len(buffer)/2])
if err != nil {
t.Fatalf("first render gave an error")
}
s.Release(0)
n, rowend, err = s.Render(buffer[(n * 2):])
if n < su_max_samples/2 {
t.Fatalf("render should have filled second half of the buffer on the second call, %v samples rendered, %v expected", n, su_max_samples/2)
}
if rowend != true {
t.Fatalf("Row end should have been hit (rowend should have been true) on the second call to Render")
err = s.Render(buffer[len(buffer)/2:])
if err != nil {
t.Fatalf("first render gave an error")
}
_, filename, _, _ := runtime.Caller(0)
expectedb, err := ioutil.ReadFile(path.Join(path.Dir(filename), "..", "tests", "expected_output", "test_render_samples.raw"))

View File

@ -93,7 +93,6 @@ func (s *Song) Render() ([]float32, error) {
}
synth := bridge.NewSynthState()
synth.SetPatch(s.Patch)
synth.SetSamplesPerRow(44100 * 60 / (s.BPM * 4))
curVoices := make([]int, len(s.Tracks))
for i := range curVoices {
curVoices[i] = s.FirstTrackVoice(i)
@ -104,6 +103,7 @@ func (s *Song) Render() ([]float32, error) {
}
buffer := make([]float32, samples*2)
totaln := 0
rowtime := 44100 * 60 / (s.BPM * 4)
for row := 0; row < s.TotalRows(); row++ {
patternRow := row % s.PatternRows()
pattern := row / s.PatternRows()
@ -123,8 +123,8 @@ func (s *Song) Render() ([]float32, error) {
synth.Trigger(curVoices[t], note)
}
}
n, _, _ := synth.Render(buffer[2*totaln:])
totaln += n
samples, _, _ := synth.RenderTime(buffer[2*totaln:], rowtime)
totaln += samples
}
return buffer, nil
}