synth icon indicating copy to clipboard operation
synth copied to clipboard

Using __iter__ to retrigger the oscillators can cause unexpected problems

Open ali1234 opened this issue 3 years ago • 0 comments

Iterable objects usually just return self from the __iter__ method, and quite a lot of code expects this to be the case. Resetting the oscillator there can lead to surprising results, for example:

import miniaudio
import numpy as np

from oscillators import SineOscillator


def stream_pcm(source):
    required_frames = yield b""  # generator initialization
    while True:
        buf = np.fromiter(source, dtype=float, count=required_frames)*32767
        required_frames = yield buf.astype(np.int16).tobytes()


if __name__ == '__main__':
    sample_rate = 44100

    with miniaudio.PlaybackDevice(output_format=miniaudio.SampleFormat.SIGNED16,
                              nchannels=1, sample_rate=sample_rate) as device:

        source = SineOscillator(sample_rate=sample_rate)
        stream = stream_pcm(source)
        next(stream)  # start the generator
        device.start(stream)
        input("Audio file playing in the background. Enter to stop playback: ")

It turns out that np.fromiter() calls iter() on whatever you pass it before calling next() count times, so the oscillator gets reset back to zero on every buffer callback. This causes glitchy playback.

An easy work around is just to hide the custom __iter__ method with a wrapper:

def wrapper(source):
    yield from source

I think it would be better to have an explicit trigger or reset method, rather than re-purposing __iter__ for this.

ali1234 avatar May 13 '22 01:05 ali1234