Change in beat frequency in isochronic tones in python - python

I just started learning about isochronic tones and started writing a basic python script to generate the same. Following is a script that I have written for doing the following:
Generate a trapezoidal wave of beat frequency
Modulate a sine wave of base frequency, with the trapezoidal wave of beat frequency
#!/usr/bin/env python
# encoding: utf-8
"""
Small program for creating Isochronic Tones of desired base frequency, beat frequency and ramp frequencies
"""
import math
import wave
import struct
import array
import sys
def make_isochronic_wave(beat_freq, beat_ramp_percent, beat_sampl_rate, beat_time, base_freq, amplitude):
#
# The time for which the trapezoidal beat frequency wave is present in the entire cycle
#
up_time = 1 / beat_freq
#
# Gap between consequtive trapezoidal beats
#
gap_percent = up_time * 0.15
#
# To accomodate gaps
#
up_time = up_time * 0.85
#
# Total number of samples
#
data_size = beat_sampl_rate * beat_time
#
# No. of gaps per sec = No. of beats per sec
#
no_of_gaps = beat_freq
#
# Samples per gap = percentage of total time allocated for gaps * No. of samples per sec
#
sampls_per_gap = gap_percent * beat_sampl_rate
#
# Total number of samples in all the gaps
#
gap_sampls = no_of_gaps * sampls_per_gap
#
# Change the beat sample rate to accomodate gaps
#
beat_sampl_rate = beat_sampl_rate - gap_sampls
#
# nsps = Number of Samples Per Second
#
# NOTE: Please see the image at:
#
beat_nsps_defined = beat_sampl_rate * up_time
beat_nsps_inc = beat_nsps_defined * beat_ramp_percent
beat_nsps_dec = beat_nsps_defined * beat_ramp_percent
beat_nsps_stable = beat_nsps_defined - (beat_nsps_inc + beat_nsps_dec)
beat_nsps_undefined = beat_sampl_rate - beat_nsps_defined
#
# Trapezoidal values
#
values = []
#
# Trapezoidal * sine == Isochronic values
#
isoch_values = []
#
# Samples constructed
#
sampls_const = 0
#
# Iterate till all the samples in data_size are constructed
#
while sampls_const < data_size:
prev_sampl_value_inc = 0.0
prev_sampl_value_dec = 1.0
#
# Construct one trapezoidal beat wave (remember this is not the entire sample)
#
for sampl_itr in range(0, int(beat_sampl_rate)):
if sampl_itr < beat_nsps_inc:
value = prev_sampl_value_inc + (1 / beat_nsps_inc)
prev_sampl_value_inc = value
values.append(value)
if sampl_itr > beat_nsps_inc:
if sampl_itr < (beat_nsps_inc + beat_nsps_stable):
value = 1
values.append(value)
elif (sampl_itr > (beat_nsps_inc + beat_nsps_stable)) and (sampl_itr < (beat_nsps_inc + beat_nsps_stable + beat_nsps_dec)):
value = prev_sampl_value_dec - (1 / beat_nsps_dec)
prev_sampl_value_dec = value
values.append(value)
#
# Add the gap cycles
#
for gap_iter in range(0, int(sampls_per_gap)):
values.append(0)
#
# Increment the number of samples constructed to reflect the values
#
sampls_const = sampls_const + beat_nsps_defined + gap_sampls
#
# Open the wave file
#
wav_file = wave.open("beat_wave_%s_%s.wav" % (base_freq, beat_freq), "w")
#
# Define parameters
#
nchannels = 2
sampwidth = 2
framerate = beat_sampl_rate
nframes = data_size
comptype = "NONE"
compname = "not compressed"
wav_file.setparams((nchannels, sampwidth, framerate, nframes, comptype, compname))
#
# Calculate isochronic wave point values
#
value_iter = 0
for value in values:
isoch_value = value * math.sin(2 * math.pi * base_freq * (value_iter / beat_sampl_rate))
value_iter = value_iter + 1
isoch_values.append(isoch_value)
#
# Create the wave file (in .wav format)
#
for value in isoch_values:
data = array.array('h')
data.append(int(value * amplitude / 2)) # left channel
data.append(int(value * amplitude / 2)) # right channel
wav_file.writeframes(data.tostring())
wav_file.close()
try:
base_freq = int(sys.argv[1], 10)
beat_freq = int(sys.argv[2], 10)
sample_rate = int(sys.argv[3], 10)
output_time = int(sys.argv[4], 10)
ramp_percent = float(sys.argv[5])
amplitude = float(sys.argv[6])
make_isochronic_wave(beat_freq, ramp_percent, sample_rate, output_time, base_freq, amplitude)
except:
msg = """
<program> <base freqency> <beat frequency> <sample rate> <output time> <ramp percent> <amplitude>
"""
print (msg)
The above code works fine, and I get a wave of the below format:
The above format being similar to what is generated using audacity using IsoMod plugin. However, I would like to generate a tone which has the beat frequency reduce as a ramp. For this, I have enhanced the above script to call the trapezoidal wave generation in a loop, once every second. However, setting the parameters for wav file for writing multiple times (with changes in data_size due to change in beat_freq across the ramp), I am getting the following error
G:\>python gen_isochronic_tones.py 70 10 5 11025 5 0.15 8000
gen_isochronic_tones.py:195: DeprecationWarning: tostring() is deprecated. Use tobytes() instead.
make_isochronic_wave(beat_freq_start, beat_freq_end, ramp_percent, sample_rate, output_time, base_freq, amplitude)
Traceback (most recent call last):
File "gen_isochronic_tones.py", line 195, in <module>
make_isochronic_wave(beat_freq_start, beat_freq_end, ramp_percent, sample_rate, output_time, base_freq, amplitude)
File "gen_isochronic_tones.py", line 156, in make_isochronic_wave
wav_file.setparams((nchannels, sampwidth, framerate, data_size, comptype, compname))
File "G:\Python37-32\lib\wave.py", line 399, in setparams
raise Error('cannot change parameters after starting to write')
wave.Error: cannot change parameters after starting to write
Looks like wave module allows the paramters (namely data_size above) to be changed only once. Any idea how to make this work for changing data_size?

Related

Noise Attenuation with multiple files via Fourier Transformation

I'm trying to implement the following formulas in a Jupyter notebook:
)
Below, I have the code for loading the Noisy (Y) and Noise files (D).
# Read audio data from file
noisy_speech = AudioSegment.from_wav('NoisySignal/Station/sp01_station_sn5.wav')
y = noisy_speech.get_array_of_samples() # samples x(t)
y_f = noisy_speech.frame_rate # sampling rate f
#window size: the number of samples per frame, each frame is of 30ms
win_length = int(y_f * 0.03)
#number of samples between two consecutive frames, by default, hop_length = win_length / 4
hop_length = int(win_length / 2)
Y = librosa.stft(np.float32(y), n_fft = 2048, window = 'hann', hop_length = hop_length, win_length = win_length)
mag_Y = abs(Y)
angle = np.angle(Y)
print(Y.shape)
# Read audio data from file
n_speech = AudioSegment.from_wav('Noise/Station/Station_1.wav')
d = n_speech.get_array_of_samples() # samples x(t)
d_f = n_speech.frame_rate # sampling rate f
#window size: the number of samples per frame, each frame is of 30ms
win_length = int(d_f * 0.03)
#number of samples between two consecutive frames, by default, hop_length = win_length / 4
hop_length = int(win_length / 2)
D = librosa.stft(np.float32(y), n_fft = 2048, window = 'hann', hop_length = hop_length, win_length = win_length)
mag_D = abs(D)
means_mag_D = np.mean(mag_D, axis = 1)
So the array in Y & D, each column is a frame.
How would I implement the above formula for S-hat?
Is there a library that can do it for me, if not, how would I write it from scratch?
Also, if anyone has a link to a video or document for writing formulas to code, that would be helpful as well.
Thank you.
The formula doesn't involve any calculus or anything like that so you can simply use numpy. Work out how to do each of the individual mathematical operations using numpy and then put them all together.
import numpy as np
y = np.random.random(20)
d = np.random.random(20)
right = 1 - (d**2)/(y**2) # Calculate the right side of the max equation
right[right < 0] = 0 # Equivalent to max(0, right)
h = right**0.5
s_hat = h*y

Equivalent command of "audioplayer" and "play" from MATLAB in python?

How can I write this same code from MATLAB in Python? what modules should I use?
player = audioplayer(y, Fs);
play(player);
% y = Audio signal represented by a vector or two-dimensional array containing
%single, double, int8, uint8, or int16 values.
%Fs = Sampling rate in Hz
The simpleaudio package allows audio playback of numpy vectors with time-domain signals.
Here is a simple usage example taken from the simpleaudio package's documentation:
import numpy as np
import simpleaudio as sa
# calculate note frequencies
A_freq = 440
Csh_freq = A_freq * 2 ** (4 / 12)
E_freq = A_freq * 2 ** (7 / 12)
# get timesteps for each sample, T is note duration in seconds
sample_rate = 44100
T = 0.25
t = np.linspace(0, T, T * sample_rate, False)
# generate sine wave notes
A_note = np.sin(A_freq * t * 2 * np.pi)
Csh_note = np.sin(Csh_freq * t * 2 * np.pi)
E_note = np.sin(E_freq * t * 2 * np.pi)
# concatenate notes
audio = np.hstack((A_note, Csh_note, E_note))
# normalize to 16-bit range
audio *= 32767 / np.max(np.abs(audio))
# convert to 16-bit data
audio = audio.astype(np.int16)
# start playback
play_obj = sa.play_buffer(audio, 1, 2, sample_rate)
# wait for playback to finish before exiting
play_obj.wait_done()

Trying to get the frequencies of a .wav file in Python

What I'm trying to do seems simple: I want to know exactly what frequencies there are in a .wav file at given times; i.e. "from the time n milliseconds to n + 10 milliseconds, the average frequency of the sound was x hertz". I have seen people talking about Fourier transforms and Goertzel algorithms, as well as various modules, that I can't seem to figure out how to get to do what I've described.
What I'm looking for is a solution like this pseudocode, or at least one that will do something like what the pseudocode is getting at:
import some_module_that_can_help_me_do_this as freq
file = 'output.wav'
start_time = 1000 # Start 1000 milliseconds into the file
end_time = 1010 # End 10 milliseconds thereafter
print("Average frequency = " + str(freq.average(start_time, end_time)) + " hz")
I don't come from a mathematics background, so I don't want to have to understand the implementation details.
If you'd like to detect pitch of a sound (and it seems you do), then in terms of Python libraries your best bet is aubio. Please consult this example for implementation.
import sys
from aubio import source, pitch
win_s = 4096
hop_s = 512
s = source(your_file, samplerate, hop_s)
samplerate = s.samplerate
tolerance = 0.8
pitch_o = pitch("yin", win_s, hop_s, samplerate)
pitch_o.set_unit("midi")
pitch_o.set_tolerance(tolerance)
pitches = []
confidences = []
total_frames = 0
while True:
samples, read = s()
pitch = pitch_o(samples)[0]
pitches += [pitch]
confidence = pitch_o.get_confidence()
confidences += [confidence]
total_frames += read
if read < hop_s: break
print("Average frequency = " + str(np.array(pitches).mean()) + " hz")
Be sure to check docs on pitch detection methods.
I also thought you might be interested in estimation of mean frequency and some other audio parameters without using any special libraries. Let's just use numpy! This should give you much better insight into how such audio features can be calculated. It's based off specprop from seewave package. Check docs for meaning of computed features.
import numpy as np
def spectral_properties(y: np.ndarray, fs: int) -> dict:
spec = np.abs(np.fft.rfft(y))
freq = np.fft.rfftfreq(len(y), d=1 / fs)
spec = np.abs(spec)
amp = spec / spec.sum()
mean = (freq * amp).sum()
sd = np.sqrt(np.sum(amp * ((freq - mean) ** 2)))
amp_cumsum = np.cumsum(amp)
median = freq[len(amp_cumsum[amp_cumsum <= 0.5]) + 1]
mode = freq[amp.argmax()]
Q25 = freq[len(amp_cumsum[amp_cumsum <= 0.25]) + 1]
Q75 = freq[len(amp_cumsum[amp_cumsum <= 0.75]) + 1]
IQR = Q75 - Q25
z = amp - amp.mean()
w = amp.std()
skew = ((z ** 3).sum() / (len(spec) - 1)) / w ** 3
kurt = ((z ** 4).sum() / (len(spec) - 1)) / w ** 4
result_d = {
'mean': mean,
'sd': sd,
'median': median,
'mode': mode,
'Q25': Q25,
'Q75': Q75,
'IQR': IQR,
'skew': skew,
'kurt': kurt
}
return result_d
I felt the OPs frustration - it shouldnt be so hard to find how to get values of the sprectrogram instead of seeing the spectrogram image if someone needs to:
#!/usr/bin/env python
import librosa
import sys
import numpy as np
import matplotlib.pyplot as plt
import librosa.display
np.set_printoptions(threshold=sys.maxsize)
filename = 'filename.wav'
Fs = 44100
clip, sample_rate = librosa.load(filename, sr=Fs)
n_fft = 1024 # frame length
start = 0
hop_length=512
#commented out code to display Spectrogram
X = librosa.stft(clip, n_fft=n_fft, hop_length=hop_length)
#Xdb = librosa.amplitude_to_db(abs(X))
#plt.figure(figsize=(14, 5))
#librosa.display.specshow(Xdb, sr=Fs, x_axis='time', y_axis='hz')
#If to pring log of frequencies
#librosa.display.specshow(Xdb, sr=Fs, x_axis='time', y_axis='log')
#plt.colorbar()
#librosa.display.waveplot(clip, sr=Fs)
#plt.show()
#now print all values
t_samples = np.arange(clip.shape[0]) / Fs
t_frames = np.arange(X.shape[1]) * hop_length / Fs
#f_hertz = np.arange(N / 2 + 1) * Fs / N # Works only when N is even
f_hertz = np.fft.rfftfreq(n_fft, 1 / Fs) # Works also when N is odd
#example
print('Time (seconds) of last sample:', t_samples[-1])
print('Time (seconds) of last frame: ', t_frames[-1])
print('Frequency (Hz) of last bin: ', f_hertz[-1])
print('Time (seconds) :', len(t_samples))
#prints array of time frames
print('Time of frames (seconds) : ', t_frames)
#prints array of frequency bins
print('Frequency (Hz) : ', f_hertz)
print('Number of frames : ', len(t_frames))
print('Number of bins : ', len(f_hertz))
#This code is working to printout frame by frame intensity of each frequency
#on top line gives freq bins
curLine = 'Bins,'
for b in range(1, len(f_hertz)):
curLine += str(f_hertz[b]) + ','
print(curLine)
curLine = ''
for f in range(1, len(t_frames)):
curLine = str(t_frames[f]) + ','
for b in range(1, len(f_hertz)): #for each frame, we get list of bin values printed
curLine += str("%.02f" % np.abs(X[b, f])) + ','
#remove format of the float for full details if needed
#curLine += str(np.abs(X[b, f])) + ','
#print other useful info like phase of frequency bin b at frame f.
#curLine += str("%.02f" % np.angle(X[b, f])) + ','
print(curLine)
This answer is quite late, but you could try this:
(Note: I deserve very little credit for this since I got most of it from other SO posts and this great article on FFT using Python: https://realpython.com/python-scipy-fft/)
import numpy as np
from scipy.fft import *
from scipy.io import wavfile
def freq(file, start_time, end_time):
# Open the file and convert to mono
sr, data = wavfile.read(file)
if data.ndim > 1:
data = data[:, 0]
else:
pass
# Return a slice of the data from start_time to end_time
dataToRead = data[int(start_time * sr / 1000) : int(end_time * sr / 1000) + 1]
# Fourier Transform
N = len(dataToRead)
yf = rfft(dataToRead)
xf = rfftfreq(N, 1 / sr)
# Uncomment these to see the frequency spectrum as a plot
# plt.plot(xf, np.abs(yf))
# plt.show()
# Get the most dominant frequency and return it
idx = np.argmax(np.abs(yf))
freq = xf[idx]
return freq
This code can work for any .wav file, but it may be slightly off since it only returns the most dominant frequency, and also because it only uses the first channel of the audio (if not mono).
If you want to learn more about how the Fourier transform works, check out this video by 3blue1brown with a visual explanation: https://www.youtube.com/watch?v=spUNpyF58BY
Try something along the below, it worked for me with a sine wave file with a freq of 1234 I generated
from this page.
from scipy.io import wavfile
def freq(file, start_time, end_time):
sample_rate, data = wavfile.read(file)
start_point = int(sample_rate * start_time / 1000)
end_point = int(sample_rate * end_time / 1000)
length = (end_time - start_time) / 1000
counter = 0
for i in range(start_point, end_point):
if data[i] < 0 and data[i+1] > 0:
counter += 1
return counter/length
freq("sin.wav", 1000 ,2100)
1231.8181818181818
edited: cleaned up for loop a bit

python - how can I generate a WAV file with beeps?

is there a way in python to generate a continuous series of beeps in increasing amplitude and export it into a WAV file?
I've based this on the answer to the previous question and added a lot of comments. Hopefully this makes it clear. You'll probably want to introduce a for loop to control the number of beeps and the increasing volume.
#!/usr/bin/python
# based on : www.daniweb.com/code/snippet263775.html
import math
import wave
import struct
# Audio will contain a long list of samples (i.e. floating point numbers describing the
# waveform). If you were working with a very long sound you'd want to stream this to
# disk instead of buffering it all in memory list this. But most sounds will fit in
# memory.
audio = []
sample_rate = 44100.0
def append_silence(duration_milliseconds=500):
"""
Adding silence is easy - we add zeros to the end of our array
"""
num_samples = duration_milliseconds * (sample_rate / 1000.0)
for x in range(int(num_samples)):
audio.append(0.0)
return
def append_sinewave(
freq=440.0,
duration_milliseconds=500,
volume=1.0):
"""
The sine wave generated here is the standard beep. If you want something
more aggresive you could try a square or saw tooth waveform. Though there
are some rather complicated issues with making high quality square and
sawtooth waves... which we won't address here :)
"""
global audio # using global variables isn't cool.
num_samples = duration_milliseconds * (sample_rate / 1000.0)
for x in range(int(num_samples)):
audio.append(volume * math.sin(2 * math.pi * freq * ( x / sample_rate )))
return
def save_wav(file_name):
# Open up a wav file
wav_file=wave.open(file_name,"w")
# wav params
nchannels = 1
sampwidth = 2
# 44100 is the industry standard sample rate - CD quality. If you need to
# save on file size you can adjust it downwards. The stanard for low quality
# is 8000 or 8kHz.
nframes = len(audio)
comptype = "NONE"
compname = "not compressed"
wav_file.setparams((nchannels, sampwidth, sample_rate, nframes, comptype, compname))
# WAV files here are using short, 16 bit, signed integers for the
# sample size. So we multiply the floating point data we have by 32767, the
# maximum value for a short integer. NOTE: It is theortically possible to
# use the floating point -1.0 to 1.0 data directly in a WAV file but not
# obvious how to do that using the wave module in python.
for sample in audio:
wav_file.writeframes(struct.pack('h', int( sample * 32767.0 )))
wav_file.close()
return
append_sinewave(volume=0.25)
append_silence()
append_sinewave(volume=0.5)
append_silence()
append_sinewave()
save_wav("output.wav")
I added minor improvements to the JCx code above. As author said, its not cool to use global variables. So I wrapped his solution into class, and it works just fine:
import math
import wave
import struct
class BeepGenerator:
def __init__(self):
# Audio will contain a long list of samples (i.e. floating point numbers describing the
# waveform). If you were working with a very long sound you'd want to stream this to
# disk instead of buffering it all in memory list this. But most sounds will fit in
# memory.
self.audio = []
self.sample_rate = 44100.0
def append_silence(self, duration_milliseconds=500):
"""
Adding silence is easy - we add zeros to the end of our array
"""
num_samples = duration_milliseconds * (self.sample_rate / 1000.0)
for x in range(int(num_samples)):
self.audio.append(0.0)
return
def append_sinewave(
self,
freq=440.0,
duration_milliseconds=500,
volume=1.0):
"""
The sine wave generated here is the standard beep. If you want something
more aggresive you could try a square or saw tooth waveform. Though there
are some rather complicated issues with making high quality square and
sawtooth waves... which we won't address here :)
"""
num_samples = duration_milliseconds * (self.sample_rate / 1000.0)
for x in range(int(num_samples)):
self.audio.append(volume * math.sin(2 * math.pi * freq * ( x / self.sample_rate )))
return
def save_wav(self, file_name):
# Open up a wav file
wav_file=wave.open(file_name,"w")
# wav params
nchannels = 1
sampwidth = 2
# 44100 is the industry standard sample rate - CD quality. If you need to
# save on file size you can adjust it downwards. The stanard for low quality
# is 8000 or 8kHz.
nframes = len(self.audio)
comptype = "NONE"
compname = "not compressed"
wav_file.setparams((nchannels, sampwidth, self.sample_rate, nframes, comptype, compname))
# WAV files here are using short, 16 bit, signed integers for the
# sample size. So we multiply the floating point data we have by 32767, the
# maximum value for a short integer. NOTE: It is theortically possible to
# use the floating point -1.0 to 1.0 data directly in a WAV file but not
# obvious how to do that using the wave module in python.
for sample in self.audio:
wav_file.writeframes(struct.pack('h', int( sample * 32767.0 )))
wav_file.close()
return
if __name__ == "__main__":
bg = BeepGenerator()
bg.append_sinewave(volume=0.25, duration_milliseconds=100)
bg.append_silence()
bg.append_sinewave(volume=0.5, duration_milliseconds=700)
bg.append_silence()
bg.save_wav("output.wav")
I adjusted it a bit further, now it should be a lot faster, and I added a function for playing multiple tones at the same time.
import numpy as np
import scipy.io.wavfile
class BeepGenerator:
def __init__(self):
# Audio will contain a long list of samples (i.e. floating point numbers describing the
# waveform). If you were working with a very long sound you'd want to stream this to
# disk instead of buffering it all in memory list this. But most sounds will fit in
# memory.
self.audio = []
self.sample_rate = 44100.0
def append_silence(self, duration_milliseconds=500):
"""
Adding silence is easy - we add zeros to the end of our array
"""
num_samples = duration_milliseconds * (self.sample_rate / 1000.0)
for x in range(int(num_samples)):
self.audio.append(0.0)
return
def append_sinewave(
self,
freq=440.0,
duration_milliseconds=500,
volume=1.0):
"""
The sine wave generated here is the standard beep. If you want something
more aggressive you could try a square or saw tooth waveform. Though there
are some rather complicated issues with making high quality square and
sawtooth waves... which we won't address here :)
"""
num_samples = duration_milliseconds * (self.sample_rate / 1000.0)
x = np.array([i for i in range(int(num_samples))])
sine_wave = volume * np.sin(2 * np.pi * freq * (x / self.sample_rate))
self.audio.extend(list(sine_wave))
return
def append_sinewaves(
self,
freqs=[440.0],
duration_milliseconds=500,
volumes=[1.0]):
"""
The sine wave generated here is the standard beep. If you want something
more aggressive you could try a square or saw tooth waveform. Though there
are some rather complicated issues with making high quality square and
sawtooth waves... which we won't address here :)
len(freqs) must be the same as len(volumes)
"""
volumes = list(np.array(volumes)/sum(volumes))
num_samples = duration_milliseconds * (self.sample_rate / 1000.0)
x = np.array([i for i in range(int(num_samples))])
first_it = True
for volume, freq in zip(volumes, freqs):
print(freq)
if first_it:
sine_wave = volume * np.sin(2 * np.pi * freq * (x / self.sample_rate))
first_it = False
else:
sine_wave += volume * np.sin(2 * np.pi * freq * (x / self.sample_rate))
self.audio.extend(list(sine_wave))
return
def save_wav(self, file_name):
# Open up a wav file
# wav params
# 44100 is the industry standard sample rate - CD quality. If you need to
# save on file size you can adjust it downwards. The standard for low quality
# is 8000 or 8kHz.
# WAV files here are using short, 16 bit, signed integers for the
# sample size. So we multiply the floating point data we have by 32767, the
# maximum value for a short integer. NOTE: It is theoretically possible to
# use the floating point -1.0 to 1.0 data directly in a WAV file but not
# obvious how to do that using the wave module in python.
self.audio = np.array(self.audio).astype(np.float32)
scipy.io.wavfile.write(file_name, int(self.sample_rate), np.array(self.audio))
return
if __name__ == "__main__":
bg = BeepGenerator()
bg.append_sinewave(volume=1, duration_milliseconds=100)
bg.append_silence()
bg.append_sinewave(volume=0.5, duration_milliseconds=700)
bg.append_silence()
bg.append_sinewaves(volumes=[1, 1], duration_milliseconds=700, freqs=[880, 660])
bg.append_silence()
bg.save_wav("output.wav")

Easy way to implement a Root Raised Cosine (RRC) filter using Python & Numpy

SciPy/Numpy seems to support many filters, but not the root-raised cosine filter. Is there a trick to easily create one rather than calculating the transfer function? An approximation would be fine as well.
The commpy package has several filters included with it. The order of return variables was switched in an earlier version (as of this edit, current version is 0.7.0). To install, foemphasized textllow instructions here or here.
Here's a use example for 1024 symbols of QAM16:
import numpy as np
from commpy.modulation import QAMModem
from commpy.filters import rrcosfilter
N = 1024 # Number of symbols
os = 8 #over sampling factor
# Create modulation. QAM16 makes 4 bits/symbol
mod1 = QAMModem(16)
# Generate the bit stream for N symbols
sB = np.random.randint(0, 2, N*mod1.num_bits_symbol)
# Generate N complex-integer valued symbols
sQ = mod1.modulate(sB)
sQ_upsampled = np.zeros(os*(len(sQ)-1)+1,dtype = np.complex64)
sQ_upsampled[::os] = sQ
# Create a filter with limited bandwidth. Parameters:
# N: Filter length in samples
# 0.8: Roll off factor alpha
# 1: Symbol period in time-units
# 24: Sample rate in 1/time-units
sPSF = rrcosfilter(N, alpha=0.8, Ts=1, Fs=over_sample)[1]
# Analog signal has N/2 leading and trailing near-zero samples
qW = np.convolve(sPSF, sQ_upsampled)
Here's some explanation of the parameters. N is the number of baud samples. You need 4 times as many bits (in the case of QAM) as samples. I made the sPSF array return with N elements so we can see the signal with leading and trailing samples. See the Wikipedia Root-raised-cosine filter page for explanation of parameter alpha. Ts is the symbol period in seconds and Fs is the number of filter samples per Ts. I like to pretend Ts=1 to keep things simple (unit symbol rate). Then Fs is the number of complex waveform samples per baud point.
If you use return element 0 from rrcosfilter to get the sample time indexes, you need to insert the correct symbol period and filter sample rate in Ts and Fs for the index values to be correctly scaled.
It would be nice to have the root-raised cosine filter standardized in a common package. Here is my implementation in the meantime based on commpy. It vectorized with numpy, and normalized without consideration of the symbol rate.
def raised_root_cosine(upsample, num_positive_lobes, alpha):
"""
Root raised cosine (RRC) filter (FIR) impulse response.
upsample: number of samples per symbol
num_positive_lobes: number of positive overlaping symbols
length of filter is 2 * num_positive_lobes + 1 samples
alpha: roll-off factor
"""
N = upsample * (num_positive_lobes * 2 + 1)
t = (np.arange(N) - N / 2) / upsample
# result vector
h_rrc = np.zeros(t.size, dtype=np.float)
# index for special cases
sample_i = np.zeros(t.size, dtype=np.bool)
# deal with special cases
subi = t == 0
sample_i = np.bitwise_or(sample_i, subi)
h_rrc[subi] = 1.0 - alpha + (4 * alpha / np.pi)
subi = np.abs(t) == 1 / (4 * alpha)
sample_i = np.bitwise_or(sample_i, subi)
h_rrc[subi] = (alpha / np.sqrt(2)) \
* (((1 + 2 / np.pi) * (np.sin(np.pi / (4 * alpha))))
+ ((1 - 2 / np.pi) * (np.cos(np.pi / (4 * alpha)))))
# base case
sample_i = np.bitwise_not(sample_i)
ti = t[sample_i]
h_rrc[sample_i] = np.sin(np.pi * ti * (1 - alpha)) \
+ 4 * alpha * ti * np.cos(np.pi * ti * (1 + alpha))
h_rrc[sample_i] /= (np.pi * ti * (1 - (4 * alpha * ti) ** 2))
return h_rrc
commpy doesn't seem to be released yet. But here is my nugget of knowledge.
beta = 0.20 # roll off factor
Tsample = 1.0 # sampling period, should at least twice the rate of the symbol
oversampling_rate = 8 # oversampling of the bit stream, this gives samples per symbol
# must be at least 2X the bit rate
Tsymbol = oversampling_rate * Tsample # pulse duration should be at least 2 * Ts
span = 50 # number of symbols to span, must be even
n = span*oversampling_rate # length of the filter = samples per symbol * symbol span
# t_step must be from -span/2 to +span/2 symbols.
# each symbol has 'sps' number of samples per second.
t_step = Tsample * np.linspace(-n/2,n/2,n+1) # n+1 to include 0 time
BW = (1 + beta) / Tsymbol
a = np.zeros_like(t_step)
for item in list(enumerate(t_step)):
i,t = item
# t is n*Ts
if (1-(2.0*beta*t/Tsymbol)**2) == 0:
a[i] = np.pi/4 * np.sinc(t/Tsymbol)
print 'i = %d' % i
elif t == 0:
a[i] = np.cos(beta * np.pi * t / Tsymbol)/ (1-(2.0*beta*t/Tsymbol)**2)
print 't = 0 captured'
print 'i = %d' % i
else:
numerator = np.sinc( np.pi * t/Tsymbol )*np.cos( np.pi*beta*t/Tsymbol )
denominator = (1.0 - (2.0*beta*t/Tsymbol)**2)
a[i] = numerator / denominator
#a = a/sum(a) # normalize total power
plot_filter = 0
if plot_filter == 1:
w,h = signal.freqz(a)
fig = plt.figure()
plt.subplot(2,1,1)
plt.title('Digital filter (raised cosine) frequency response')
ax1 = fig.add_subplot(211)
plt.plot(w/np.pi, 20*np.log10(abs(h)),'b')
#plt.plot(w/np.pi, abs(h),'b')
plt.ylabel('Amplitude (dB)', color = 'b')
plt.xlabel(r'Normalized Frequency ($\pi$ rad/sample)')
ax2 = ax1.twinx()
angles = np.unwrap(np.angle(h))
plt.plot(w/np.pi, angles, 'g')
plt.ylabel('Angle (radians)', color = 'g')
plt.grid()
plt.axis('tight')
plt.show()
plt.subplot(2,1,2)
plt.stem(a)
plt.show()
I think the correct response is to generate the desire impulse response. For a raised cosine filter the function is
h(n) = (sinc(n/T)*cos(pi * alpha* n /T)) / (1-4*(alpha*n/T)**2)
Select the number of points for your filter and generate the weights.
output = scipy.signal.convolve(signal_in, h)
This is basically the same function as in CommPy but much smaller in code:
def rcosfilter(N, beta, Ts, Fs):
t = (np.arange(N) - N / 2) / Fs
return np.where(np.abs(2*t) == Ts / beta,
np.pi / 4 * np.sinc(t/Ts),
np.sinc(t/Ts) * np.cos(np.pi*beta*t/Ts) / (1 - (2*beta*t/Ts) ** 2))
SciPy will support any filter. Just calculate the impulse response and use any of the appropriate scipy.signal filter/convolve functions.

Categories

Resources