Sounddevice Output Overflow

75 views Asked by At

I have troubles of unknown kind with the sounddevice module for Python. The code below outputs a GUI where you can output a sine wave of a user-defined frequency. Additionaly, one can modulate its amplitude with a sine wave of a user-defined frequency and amplitude/amount. Upon pressing the "Play"-button, I receive an output overflow and the audio starts lagging immediately. The problem does not occur if I remove the option of amplitude modulation in the code and I hear a smooth sine signal.

I understand that this code might use too much CPU time for the audio signal to be smooth.

Has anybody encountered a similar problem in a similar project? Or is anybody familiar enough with the sounddevice module to help me out?

A good part of the code is copy-paste from this example on Github.

I am by no means an experienced programmer.

Any help is dearly appreciated!

import argparse
import sys

import tkinter as tk
import sounddevice as sd
import numpy as np

# the following function and parsing part is copy-paste from the link above 
# and with a few modifications of the remaining code wouldn't be crucial I guess.
# I left it there because I wanted to spare myself the modifications.

def int_or_str(text):
    """Helper function for argument parsing."""
    try:
        return int(text)
    except ValueError:
        return text

parser = argparse.ArgumentParser(add_help=False)
parser.add_argument(
    '-l', '--list-devices', action='store_true',
    help='show list of audio devices and exit')
args, remaining = parser.parse_known_args()
if args.list_devices:
    print(sd.query_devices())
    parser.exit(0)
parser = argparse.ArgumentParser(
    description=__doc__,
    formatter_class=argparse.RawDescriptionHelpFormatter,
    parents=[parser])
parser.add_argument(
    'frequency', nargs='?', metavar='FREQUENCY', type=float, default=500,
    help='frequency in Hz (default: %(default)s)')
parser.add_argument(
    '-d', '--device', type=int_or_str,
    help='output device (numeric ID or substring)')
parser.add_argument(
    '-a', '--amplitude', type=float, default=0.2,
    help='amplitude (default: %(default)s)')
args = parser.parse_args(remaining)

start_idx = 0

def play():
    samplerate = sd.query_devices(args.device, 'output')['default_samplerate']

    # the callback function is basically copy-paste from the link abouve 
    # except I added the modulating sine wave
    def callback(outdata, frames, time, status): 
        if status:
            print(status, file=sys.stderr)
        global start_idx
        t = (start_idx + np.arange(frames)) / samplerate
        t = t.reshape(-1, 1)
        am_amount = float(am_amt_1.get())
        a_modulator = np.sin(2*np.pi*float(am_freq_1.get())*t)*am_amount+1-am_amount
        carrier = np.sin(2 * np.pi * float(freq_1.get()) * t)
        outdata[:] = carrier*a_modulator
        start_idx += frames

    with sd.OutputStream(device=args.device, channels=1, callback=callback,
                         samplerate=samplerate):
        input() #don't really know what this is doing but there's no audio at all without it

# setting up the GUI
  
main = tk.Tk()
main.title('Test')
main.geometry('10000x10000')

# entries for frequency in hz and am amount

freq_1 = tk.Entry(main)
freq_1.grid(row = 0, column = 1)

am_freq_1 = tk.Entry(main)
am_freq_1.grid(row=1,column=1)

am_amt_1 = tk.Entry(main)
am_amt_1.grid(row=2,column=1)

# labels

f1 = tk.Label(main,text='Frequency')
f1.grid(row=0,column=0)

amf_1 = tk.Label(main,text='AM Frequency')
amf_1.grid(row=1,column=0)

amamt_1 = tk.Label(main,text='AM Amount')
amamt_1.grid(row=2,column=0)

# play button executing the above play function

pl = tk.Button(main, text='Play',command = play)
pl.grid(row=3,column=1)

main.mainloop()

I tried the program with both the built-in output device from my laptop and the M-Audio Fast Track Pro.

1

There are 1 answers

3
AKX On

As I kind of suspected, the issue is resolved as soon as you don't call tkinter functions within the callback; if you pass the values in to the play function, things seem to work fine.

I also refactored the rest a bit for brevity, too. The reason you need an input() in the with is that the device is otherwise immediately closed; I replaced that with a sleep here, so the sound plays for 2 seconds and then exits.

If your sound device does not contain Speakers in the name, you'll need to change that in the device=... line.

import sys
import time
import tkinter as tk

import numpy as np
import sounddevice as sd


def play(*, device, freq: float, am_freq: float, am_amount: float):
    samplerate = sd.query_devices(device, "output")["default_samplerate"]
    start_idx = 0

    def callback(outdata, frames, time, status):
        nonlocal start_idx
        if status:
            print(status, file=sys.stderr)
        t = (start_idx + np.arange(frames)) / samplerate
        t = t.reshape(-1, 1)
        a_modulator = (
            np.sin(2 * np.pi * am_freq * t) * am_amount + 1 - am_amount
        )
        carrier = np.sin(2 * np.pi * freq * t)
        outdata[:] = carrier * a_modulator
        start_idx += frames

    with sd.OutputStream(
        device=device,
        channels=1,
        callback=callback,
        samplerate=samplerate,
    ):
        time.sleep(2)  # Play for a while


def make_box(parent, label, value, y):
    box = tk.Entry(parent)
    box.insert(0, value)
    label = tk.Label(parent, text=label)
    label.grid(row=y, column=0)
    box.grid(row=y, column=1)
    return (label, box)


def main():
    root = tk.Tk()
    root.title("Test")
    freq_label, freq_box = make_box(root, "Frequency", "500", 0)
    am_freq_label, am_freq_box = make_box(root, "AM Frequency", "25", 1)
    am_amt_label, am_amt_box = make_box(root, "AM Amount", "0.5", 2)

    pl = tk.Button(
        root,
        text="Play",
        command=(
            lambda: play(
                device="Speakers",
                freq=float(freq_box.get()),
                am_freq=float(am_freq_box.get() or 0),
                am_amount=float(am_amt_box.get() or 0),
            )
        ),
    )
    pl.grid(row=3, column=1)

    root.mainloop()


if __name__ == "__main__":
    main()

EDIT: Here's an additional refactoring that adds event bindings from Tkinter to update the synthesizer state when you hit Return in the entry boxes:

import sys
import tkinter as tk

import numpy as np
import sounddevice as sd


class Synth:
    def __init__(self):
        self.freq = 500
        self.am_freq = 25
        self.am_amount = 0.5
        self.start_idx = 0
        self.stream = None

    def callback(self, outdata, frames, time, status):
        if status:
            print(status, file=sys.stderr)
        t = (self.start_idx + np.arange(frames)) / self.stream.samplerate
        t = t.reshape(-1, 1)
        a_modulator = np.sin(2 * np.pi * self.am_freq * t) * self.am_amount + 1 - self.am_amount
        carrier = np.sin(2 * np.pi * self.freq * t)
        outdata[:] = carrier * a_modulator
        self.start_idx += frames

    def start(self, device):
        if self.stream:
            self.stop()
        samplerate = sd.query_devices(device, "output")["default_samplerate"]
        self.start_idx = 0
        self.stream = sd.OutputStream(
            device=device,
            channels=1,
            callback=self.callback,
            samplerate=samplerate,
        )
        self.stream.start()

    def stop(self):
        if self.stream:
            self.stream.stop()
            self.stream.close()
            self.stream = None


def make_box(parent, label, value, y):
    box = tk.Entry(parent)
    box.insert(0, value)
    label = tk.Label(parent, text=label)
    label.grid(row=y, column=0)
    box.grid(row=y, column=1)
    return (label, box)


def main():
    synth = Synth()

    def update_synth(*_args):
        synth.freq = float(freq_box.get())
        synth.am_freq = float(am_freq_box.get() or 0)
        synth.am_amount = float(am_amt_box.get() or 0)

    root = tk.Tk()
    root.title("Test")
    freq_label, freq_box = make_box(root, "Frequency", "500", 0)
    am_freq_label, am_freq_box = make_box(root, "AM Frequency", "25", 1)
    am_amt_label, am_amt_box = make_box(root, "AM Amount", "0.5", 2)

    freq_box.bind("<Return>", update_synth)
    am_freq_box.bind("<Return>", update_synth)
    am_amt_box.bind("<Return>", update_synth)

    tk.Button(root, text="Start", command=lambda: synth.start("Speakers")).grid(row=3, column=0)
    tk.Button(root, text="Stop", command=synth.stop).grid(row=3, column=2)
    root.mainloop()


if __name__ == "__main__":
    main()