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.
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 thewith
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 thedevice=...
line.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: