relic-se / CircuitPython_SynthVoice

Advanced synthio voices.
MIT License
2 stars 0 forks source link

Calculated time is too short when `synthvoice.sample.Sample.looping` is set to `True` #1

Closed relic-se closed 1 week ago

relic-se commented 1 month ago

The calculation within Sample.duration is incorrect and produces undesirable playback length when Sample.looping is enabled.

https://github.com/dcooperdalrymple/CircuitPython_SynthVoice/blob/0c3a7b7fb29a2b8385cfef551e38d269dee93eda/synthvoice/sample.py#L193-L203

relic-se commented 1 week ago

Here's my initial test of this problem. It seems that everything is actually calculating out correctly. It may be that the max_size property is too for the sample in question (examples/test.wav) and it's sounding too short because of that. It could also be that the update routine needs to run very often or else it'll stop the note too late.

import asyncio
import audiobusio
import board
import math
import synthio
import time

from synthvoice.sample import Sample

synth = synthio.Synthesizer(sample_rate=44100)
voice = Sample(synth, looping=False, file="/test.wav")

audio = audiobusio.I2SOut(board.GP0, board.GP1, board.GP2)
audio.play(synth)

print("frames: {:d}, sample_rate: {:d}, source duration: {:f}".format(len(voice._note.waveform), voice._sample_rate, voice._source_duration))
print("fftfreq: {:f}, source tuning: {:f}".format(voice._root, voice._source_tune))
print("initial duration: {:f}".format(initial := voice.duration))

async def test(ratio: float) -> None:
    voice.press(1) # Force triggers glide envelope
    print("\ndesired ratio = {:f}".format(ratio))
    voice.frequency = voice._root * ratio
    await asyncio.sleep(1) # Wait for glide to finish (should be very fast)
    print("frequency: {:f}, duration: {:f}, ratio: {:f}".format(voice.frequency, voice.duration, voice.duration / initial))
    voice.release()

async def run_tests() -> None:
    for i in range(2, -3, -1):
        await test(math.pow(2, i))

async def update_voice() -> None:
    while True:
        voice.update()
        await asyncio.sleep(0.001)

async def main() -> None:
    await asyncio.gather(run_tests(), update_voice())

asyncio.run(main())

I also added a print statement within synthvoice.sample.Sample.update to output the timing of the release call.

Output:

frames: 4096, sample_rate: 11025, source duration: 0.371519
fftfreq: 124.301453, source tuning: -5.529207
initial duration: 0.104955

desired ratio = 4.000000
desired = 0.026239, actual = 0.027344
frequency: 2.000000, duration: 0.026239, ratio: 0.250000

desired ratio = 2.000000
desired = 0.052478, actual = 0.054688
frequency: 1.000000, duration: 0.052478, ratio: 0.500000

desired ratio = 1.000000
desired = 0.104955, actual = 0.105469
frequency: 0.000000, duration: 0.104955, ratio: 1.000000

desired ratio = 0.500000
desired = 0.209911, actual = 0.210938
frequency: -1.000000, duration: 0.209911, ratio: 2.000000

desired ratio = 0.250000
desired = 0.419822, actual = 0.421875
frequency: -2.000000, duration: 0.419822, ratio: 4.000000

The release call lags slightly behind. This could potentially be mediated with one of the following:

relic-se commented 1 week ago

This PR resolves this issue: https://github.com/relic-se/CircuitPython_SynthVoice/pull/4

Using something like voice.release_time=0.01 helps smooth out sample playback.