Reputation: 3804
I am trying do some sound experiments with Python and I need a decent implementation of a play_tone(freq, dur)
function. I have looked far and wide over a long period of time and have so far found three implementations, only one of which approaches the basic smoothness of sound that I require when playing rapid sequences of notes.
I have not studied the maths/physics of sound generation so am taking a lot of the code on trust - once I know why the best one of these approaches works best, I will use that as a staring point to study further.
So I'm looking for an explanation of why the two "worst" version have so much clipping and clicking while the best version (which uses struct
) is so much smoother. I expect this answer has something to do with it, but I'm not sure how.
Also, I would love to find a way to make the last note of the smoothest version not click on the last note - i.e I want it to end smoothly.
# This is the smoothest version I can find
import math
import struct
import pyaudio
def play_tone(frequency, duration, stream, amplitude=0.5, fs=44100):
N = int(fs / frequency)
T = int(frequency * duration) # repeat for T cycles
dt = 1.0 / fs
# 1 cycle
tone = (amplitude * math.sin(2 * math.pi * frequency * n * dt)
for n in range(N))
# Notice the b to transform the operation in a bytes operation
data = b''.join(struct.pack('f', samp) for samp in tone)
for n in range(T):
stream.write(data)
#Usage
fs = 48000
p = pyaudio.PyAudio()
stream = p.open(
format=pyaudio.paFloat32,
channels=1,
rate=fs,
output=True)
a = 2 ** (1 / 24)
f0 = 110
qts = [f0 * a ** p for p in range(96)]
for i in range(0, len(qts) - 24, 3):
for j in range(i, i + 24, 4):
play_tone(qts[j], 0.1, stream)
stream.close()
p.terminate()
# This is the second smoothest version I can find
import math
import numpy
import pyaudio
def sine(frequency, length, rate):
length = int(length * rate)
factor = float(frequency) * (math.pi * 2) / rate
return numpy.sin(numpy.arange(length) * factor)
def play_tone(stream, frequency=440, length=1, rate=44100):
chunks = []
chunks.append(sine(frequency, length, rate))
chunk = numpy.concatenate(chunks) * 0.25
stream.write(chunk.astype(numpy.float32).tostring())
#Usage
fs = 48000
p = pyaudio.PyAudio()
stream = p.open(
format=pyaudio.paFloat32,
channels=1,
rate=fs,
output=True)
a = 2 ** (1 / 24)
f0 = 110
qts = [f0 * a ** p for p in range(96)]
for i in range(0, len(qts) - 24, 3):
for j in range(i, i + 24, 4):
play_tone(stream, qts[j], 0.1)
stream.close()
p.terminate()
# This is the least smooth version I can find
import numpy as np
import pyaudio
def play_tone(freq, dur, stream, fs=44100):
volume = 0.5 # range [0.0, 1.0]
duration = dur # in seconds, may be float
f = freq # sine frequency, Hz, may be float
# generate samples, note conversion to float32 array
samples = (np.sin(2*np.pi*np.arange(fs*duration)*f/fs)).astype(np.float32)
# play. May repeat with different volume values (if done interactively)
stream.write(volume*samples)
#Usage
fs = 48000
p = pyaudio.PyAudio()
stream = p.open(
format=pyaudio.paFloat32,
channels=1,
rate=fs,
output=True)
a = 2 ** (1 / 24)
f0 = 110
qts = [f0 * a ** p for p in range(96)]
for i in range(0, len(qts) - 24, 3):
for j in range(i, i + 24, 4):
play_tone(qts[j], 0.5, stream)
stream.close()
p.terminate()
Upvotes: 0
Views: 2334
Reputation: 1
I'm not an experienced programmer (also more used to javascript). Also, this is an old question, but I didn't find a good answer anywhere. So I had a shot at this.
import pyaudio
import numpy as np
import math
def play_tone(frequency, dur):
p = pyaudio.PyAudio()
volume = 0.8 # range [0.0, 1.0]
fs = 44100 # sampling rate, Hz, must be integer
# duration = 0.3 # in seconds, may be float
duration=dur #"dur" parameter can be removed and set directly
f=frequency
# We need to ramp up (I used an exponential growth formula)
# from low volume to the volume we want.
# For some reason (I can't bothered to figure that out) the
# following factor is needed to calculate how many steps are
# needed to reach maximum volume:
# 0.693147 = -LN(0.5)
stepstomax = 50
stepstomax_mod = int(round(stepstomax/0.693147))
ramprate = 1/(math.exp(0.5)*stepstomax_mod)
decayrate = 0.9996
#Decay could be programmed better. It doesn't take tone duration into account.
#That means it might not reach an inaudible level before the tone ends.
#sine wave
samples1=(np.sin(2*np.pi*np.arange(0,fs*duration,1)*f/fs))
stepcounter=0
for nums in samples1:
thisnum=samples1[stepcounter]
if stepcounter<stepstomax_mod:
#the ramp up stage
samples1[stepcounter]=volume*thisnum*(pow(ramprate+1,stepcounter+1)-1)
else:
#the decay stage
samples1[stepcounter]=volume*thisnum*(pow(decayrate,stepcounter-stepstomax))
stepcounter+=1
samples = samples1.astype(np.float32).tobytes()
stream = p.open(format=pyaudio.paFloat32,
channels=1,
rate=fs,
output=True)
stream.write(samples)
stream.stop_stream()
stream.close()
p.terminate()
play_tone(261.6, 0.3)
play_tone(329.6, 0.3)
play_tone(392, 0.3)
play_tone(523.3, 0.6)
I was programming something similar in javascript and there is a very good article on pretty much the same problem in webaudio (getting rid of the click; making a nice sound). What I tried to do was to translate the click removal at the beginning of a note from webaudio/javascript to python.
Upvotes: 0
Reputation: 43533
Modify your waveform generator so that the amplitude starts at zero, ramps up to the desired value over a certain time period (say 1/10 of the total duration), and ramps down to zero over the same period at the end.
That way, the signal is always zero at the end and the beginning of each tone, no matter the frequency or phase. That should yield smooth transitions everywhere.
Upvotes: 1