-
Notifications
You must be signed in to change notification settings - Fork 1
/
Copy paththdncalculator.py
129 lines (107 loc) · 3.92 KB
/
thdncalculator.py
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
from __future__ import division
import sys
from scipy.signal import blackmanharris
from numpy.fft import rfft, irfft
from numpy import argmax, sqrt, mean, absolute, arange, log10
import numpy as np
try:
import soundfile as sf
except ImportError:
from scikits.audiolab import Sndfile
def rms_flat(a):
"""
Return the root mean square of all the elements of *a*, flattened out.
"""
return sqrt(mean(absolute(a)**2))
def find_range(f, x):
"""
Find range between nearest local minima from peak at index x
"""
# for i in arange(x+1, len(f)):
# if f[i+1] >= f[i]:
# uppermin = i
# break
# for i in arange(x-1, 0, -1):
# if f[i] <= f[i-1]:
# lowermin = i + 1
# break
return (int(x*0.9), int(x*1.1))
def THDN(signal, sample_rate):
"""
Measure the THD+N for a signal and print the results
Prints the estimated fundamental frequency and the measured THD+N. This is
calculated from the ratio of the entire signal before and after
notch-filtering.
Currently this tries to find the "skirt" around the fundamental and notch
out the entire thing. A fixed-width filter would probably be just as good,
if not better.
"""
# Get rid of DC and window the signal
signal -= mean(signal) # TODO: Do this in the frequency domain, and take any skirts with it?
windowed = signal * blackmanharris(len(signal)) # TODO Kaiser?
# Measure the total signal before filtering but after windowing
total_rms = rms_flat(windowed)
# Find the peak of the frequency spectrum (fundamental frequency), and
# filter the signal by throwing away values between the nearest local
# minima
f = rfft(windowed)
i = argmax(abs(f))
print 'Frequency: %f Hz' % (sample_rate * (i / len(windowed))) # Not exact
lowermin, uppermin = find_range(abs(f), i)
f[lowermin: uppermin] = 0
# Transform noise back into the signal domain and measure it
# TODO: Could probably calculate the RMS directly in the frequency domain instead
noise = irfft(f)
THDN = rms_flat(noise) / total_rms
print "THD+N: %.8f%% or %.1f dB" % (THDN * 100, 20 * log10(THDN))
sf.write('new_file.wav', noise, sample_rate, 'PCM_24')
def load(filename):
"""
Load a wave file and return the signal, sample rate and number of channels.
Can be any format that libsndfile supports, like .wav, .flac, etc.
"""
try:
wave_file = sf.SoundFile(filename)
signal = wave_file.read()
except ImportError:
wave_file = Sndfile(filename, 'r')
signal = wave_file.read_frames(wave_file.nframes)
channels = wave_file.channels
sample_rate = wave_file.samplerate
return signal, sample_rate, channels
def analyze_channels(filename, function):
"""
Given a filename, run the given analyzer function on each channel of the
file
"""
signal, sample_rate, channels = load(filename)
print 'Analyzing "' + filename + '"...'
if channels == 1:
# Monaural
function(signal, sample_rate)
elif channels == 2:
# Stereo
if np.array_equal(signal[:, 0], signal[:, 1]):
print '-- Left and Right channels are identical --'
function(signal[:, 0], sample_rate)
else:
print '-- Left channel --'
function(signal[:, 0], sample_rate)
print '-- Right channel --'
function(signal[:, 1], sample_rate)
else:
# Multi-channel
for ch_no, channel in enumerate(signal.transpose()):
print '-- Channel %d --' % (ch_no + 1)
function(channel, sample_rate)
files = sys.argv[1:]
if files:
for filename in files:
try:
analyze_channels(filename, THDN)
except Exception as e:
print 'Couldn\'t analyze "' + filename + '"'
print e
print ''
else:
sys.exit("You must provide at least one file to analyze")