belangeo / pyo

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

savefileFromTable causes stutter, and other Looper questions #209

Closed dsholes closed 3 years ago

dsholes commented 3 years ago

Hello! First of all, thank you so much for writing such an extensive library, I'm amazed at what's possible with pyo.

I'm hoping to use pyo as a backend for a looping platform. My goal is essentially to be able to record instrument loops and layer them over each other. I would like to save the individual loops to disk as WAV files.

An issue I'm running into right now is that the audio seems to "stutter" as soon as savefileFromTable is triggered. Is there any way to run that function asynchronously to avoid the stutter, or do you have any recommendations for a better way to proceed?

Also, what is the best way to sync multiple Looper objects to a common Metronome? Let me know if you'd like me to split this into multiple issues.

I have the following as a basic example for recording one input from a Scarlett 2i2 on a mac running macOS Catalina (I run it from a jupyter lab interactive session:

def save_table(save_dict):
    # This function is triggered when TableRec is finished
    savefileFromTable(table=save_dict['table'], 
                      path=save_dict['filepath'], 
                      fileformat=0, 
                      sampletype=3)
    print('Done Recording')

song_bpm = 120
sec_per_beat = 1./(song_bpm/60)
beats_per_bar = 4
sec_per_bar = sec_per_beat*beats_per_bar
num_bars_to_rec = 2
sec_to_rec = sec_per_bar*num_bars_to_rec

s = Server(sr=input_info['default sr'],
           buffersize=512)

s.setOutputDevice(output_port)
s.setInputDevice(input_port)
s.boot()

# Path of the recorded sound file.
guit_rec_path = Path('test_pyo_guitar.wav')

# Creates a two seconds stereo empty table. The "feedback" argument
# is the amount of old data to mix with a new recording (overdub).
guitar_table = NewTable(length=sec_to_rec+.1, chnls=2, feedback=0.5)
guit_save_dict = {
    'table': guitar_table,
    'filepath': guit_rec_path.as_posix()
}

guitar_input = Input(chnl=0)
reverb = Freeverb(guitar_input, damp=0.2,size=0.8)
rec = TableRec(reverb, table=guitar_table, fadetime=0.05)

# To create click track
cos_tab = CosTable([(0,0), (50,1), (1000,.25), (8191,0)])
met = Metro(time=sec_per_beat, poly=1)
amp = TrigEnv(met, table=cos_tab, dur=.01)
click = Sine(freq=200, mul=amp)

mm = Mixer(outs=2, chnls=2, time=.025)

mm.addInput(0, reverb)
mm.setAmp(0,0,.5)
mm.setAmp(0,1,.5)

mm.addInput(1, click)
mm.setAmp(1,0,.75)
mm.setAmp(1,1,.75)

mm.out()

met.play() # Start Click track
rec.play(delay=sec_to_rec-.1) # Start Recording

s.start()

Thank you!

belangeo commented 3 years ago

Yes, saving a long table to disk will probably cause buffer underruns... Saving asynchronously could resolve the issue but it's not implemented at the moment. Do you really need to save them on disk? Why not just keep them in tables while you're performing? When you're done, you can call a function that saves all tables recorded during the performance.

dsholes commented 3 years ago

Yes, you're right, I can just save as Tables until the end, and then just save to disk when I'm shutting everything down. I was just wondering if I was doing something wrong. Thank you!