0

I'm trying to code a digital wavetable synthesizer that will eventually incorporate real-time pitch/volume input, but I'm stuck making the audio work properly. The basic idea is that the program checks for pitch and volume input about 100 times per second, uses scipy's interpolation function to alter the recorded waveform period's pitch and volume, then writes a signal consisting of the altered waveform period repeated to a duration of at least 0.01s to the Pyaudio stream. I originally tried using pygame, but pygame crashed too easily.

Trying to use callback mode and sleep() to time each cycle resulted in really choppy and rough sound due to audio cliffs or cycle signal overlap, and even when I tested pitches that wouldn't have audio cliffs there's a strange high-pitch component that wasn't in the original waveform.

Using blocking mode resulted in a more faithful signal because there wasn't a need to time the cycles; the previous cycle had to complete before execution resumed. However, the computation to generate each cycle's signal did result in a skip, changing the timbre of the waveform. The change is small because the computation time is much smaller than the 0.01s cycle, but it would be worse if I wanted to increase the pitch/volume update frequency of the program.

Is there a way to make the stream.write() function return ASAP and allow the signal computation but pause execution at the next stream.write() until all samples are played?

import scipy as sp
from scipy import interpolate
from time import sleep, perf_counter
import pyaudio

def pyaudioplay():
    # sampled waveform and linear interpolation
    waveform = sp.array([ 884,   8343,  18370,  27897,  32767,  30861,  23163,  12692,
         2225,  -6780, -13815, -17404, -16557, -12404,  -6286,    792,
         7448,  12458,  15086,  15770,  15699,  15031,  13224,  10705,
         8734,   7790,   7621,   7800,   7784,   6964,   5444,   4174,
         4435,   6335,   8696,  10156,  10460,  10352,   9950,   9006,
         7594,   5835,   3680,    -21,  -6574, -15732, -25302, -31550,
       -31968, -25916, -15352,  -3702,   6536,  14429,  19103,  19244,
        15471,   9717,   2882,  -4359, -10835, -15449, -17816, -18788,
       -18685, -16665, -12778,  -8588,  -5434,  -3593,  -2882,  -3311,
        -4370,  -5157,  -5531,  -6112,  -7410,  -9000, -10042, -10042,
        -9206,  -8213,  -7268,  -6188,  -4489,  -1167], dtype=sp.int16)
    size = len(waveform)
    wavefunc = interpolate.interp1d(range(size), waveform, kind = 'linear', axis = -1, copy = True, bounds_error = False, fill_value = (waveform[0], waveform[-1]))
    # pitch and volume 1.5s time series for testing
    length = 150; sample_pitch = 512.79069767441854; max_volume = 1.
    t = sp.linspace(0., 3*sp.pi, num = length, endpoint = False)
    pitches = (sample_pitch*3/4)+(sample_pitch*1/4)*sp.cos(t) # pitch goes down and up
    volumes = max_volume*sp.ones(length) # hold at same volume
    # PyAudio stream: blocking mode
    p = pyaudio.PyAudio()
    stream = p.open(format = pyaudio.paInt16,
                    channels = 1,
                    rate = 44100,
                    output = True,
                    frames_per_buffer = 0) # also specifies frame_count int for callback
    updatePeriod = 441
    begin = perf_counter()
    record = 0
    index = 0
    while index < length:
        start = perf_counter()
        # get pitch and volume
        Apitch = pitches[index]
        Avolume = volumes[index]
        # period with altered width and amplitude to change pitch and volume
        Aperiod = (Avolume*wavefunc(sp.arange(0, size, Apitch/sample_pitch))).astype(sp.int16)
        cycle = sp.tile(Aperiod, 1+updatePeriod//len(Aperiod)).tobytes()
        index += 1
        runtime = perf_counter() - start
        if runtime > record:
            record = runtime
        stream.write(cycle)
    print('total time:', perf_counter()-begin)
    print('largest runtime lag:', record)
    stream.stop_stream()
    stream.close()
    p.terminate()
BatWannaBe
  • 4,330
  • 1
  • 14
  • 23
  • You should use callback mode, but don't use `sleep()`! The calculation in the callback function must not take longer than the duration of one block, but it may finish much quicker, no problem. – Matthias Sep 20 '16 at 16:57
  • Thanks, worked like a charm – BatWannaBe Sep 25 '16 at 20:08

0 Answers0