belangeo / pyo

Python DSP module
GNU Lesser General Public License v3.0
1.28k stars 130 forks source link

Looper Stutter When Loading Table? #214

Closed jsl303 closed 3 years ago

jsl303 commented 3 years ago

I have the following simple code that extract random parts of soundfiles from a folder. However, each load, the sound stutters with the last few milliseconds before playing the next. I use librosa to load m4a files into numpy, and then load into DataTable. I'd appreciate any suggestion.

from pyo import *
import random
from glob import glob
import librosa

def load():
    path = files[random.randint(0,len(files))]
    print(path)
    length = librosa.get_duration(filename=path)
    dur = random.uniform(5, 10)
    start = random.uniform(0, length-dur)
    a, sr = librosa.load(path, sr=44100, offset=start, duration=dur, mono=False)
    if len(a.shape) > 1:
        loop.setTable(DataTable(a.shape[1], chnls=a.shape[0], init=a.tolist()))
    else:
        loop.setTable(DataTable(a.shape[0], chnls=1, init=a.tolist()))
    loop.reset()

files = glob('sounds/**/*.m4a', recursive=True)
s = Server(duplex=0)
s.boot()
s.start()
table = DataTable(size=0, chnls=2)
fade = Fader(fadein=0.005, fadeout=0.005, dur=0, mul=0.7)
fade.play()
loop = Looper(table, xfade=5, startfromloop=True, mul=fade).out()
trig = TrigFunc(loop['trig'], load)
input("Running")
belangeo commented 3 years ago

Ouch... A file to decompressed (m4a) into a numpy array, then copied into a pyo table, while the audio is running, will surely get you into trouble. You can't guarantee that all this work will be done fast enough so that every audio buffer (asked by the audio driver) will be filled with valid samples.

Few advises:

1) It's always better to preload all your sounds into different buffers at the beginning of the program. Switching which buffer to read is very cheap.

2) It's better to use uncompressed soundfiles (wav or aiff), instead of any compressed format, because they are faster to read.

3) Try to avoid multiple copies at runtime. If you absolutely need to use numpy array, take a look at the buffer protocol, implemented in the PyoTableObject.

http://ajaxsoundstudio.com/pyodoc/api/classes/_core.html?highlight=pyotableobject#pyo.PyoTableObject.getBuffer

In the documentation, this line:

t = SndTable(SNDS_PATH+”/transparent.aif”) arr = numpy.asarray(t.getBuffer()) should be read:

t = SndTable(SNDS_PATH+”/transparent.aif”)
arr = numpy.asarray(t.getBuffer())
jsl303 commented 3 years ago

Thanks! I modified the snippet from the multiprocessing example, and separated Loader and Player with Queue mechanism. It now runs without stutter even with m4a.

from pyo import *
import multiprocessing as mp
import random
from glob import glob
import librosa
import time
import warnings

class Player(mp.Process):
    def __init__(self, q):
        super(Player, self).__init__()
        self.daemon = True
        self._terminated = False
        self.q = q

    def run(self):
        self.server = Server(duplex=0)
        self.server.boot()
        self.server.start()

        while not self._terminated:
            d = self.q.get()
            print(d['path'])
            shape = d['shape']
            if len(shape) > 1: length = shape[1]
            else: length = shape[0]
            dur = length/44100
            table = DataTable(length, chnls=2, init=d['y'])
            osc = Looper(table, mode=0, dur=dur, xfade=1).out()
            time.sleep(dur)
        self.server.stop()

    def stop(self):
        self._terminated = True

class Loader(mp.Process):
    def __init__(self, q):
        super(Loader, self).__init__()
        self.daemon = True
        self._terminated = False
        self.q = q

    def run(self):
        files = glob('sounds/**/*.m4a', recursive=True)
        while not self._terminated:
            while self.q.qsize()>20: time.sleep(0.1)
            try:
                with warnings.catch_warnings():
                    warnings.simplefilter("ignore")
                    d = {}
                    path = files[random.randint(0,len(files))]
                    d['path'] = path
                    length = librosa.get_duration(filename=path)
                    dur = random.uniform(3,5)
                    d['dur'] = dur
                    start = random.uniform(0, length-dur)
                    y, sr = librosa.load(path, sr=44100, offset=start, duration=dur, mono=False)
                    d['shape'] = y.shape
                    d['y'] = y.tolist()
                    self.q.put(d)
            except Exception as e: print(e)

    def stop(self):
        self._terminated = True

if __name__ == '__main__':
    q = mp.Queue()
    loader = Loader(q)
    loader.start()
    player = Player(q)
    player.start()
    input("Running")
    player.stop()
    loader.stop()
    exit()