But only in 45° L direction? Perhaps you wrote is somewhere, but how large is bin size vs. f?That’s modulated so it‘s not going show up as amplitude.
I use a record flattener like record pi or vinyl flat.Yes my Clearaudio CA-TRS is visual flat but the cart makes a small bump every revolution it can be seen as a wiggle in the recording, if I filter below 25hz it goes away
How do you flatten your CA record?. The strange thing is that my ATOC9ML/ii does not have the oscillating FR curve the AT33PTG/II gets on CA TRS1007 record. The sam thing is seen for PTG on Stereoplays post I link to
View attachment 274553
![]()
Frequency Response Test Records
Frequency Response Test Records I was wondering how my few test records measure compared to each other and which one is the best for measuring crosstalk. Used Pickup: Ortofon Vinyl Master Silver (47K//240pF, 17,5mN) Used Turntable: Mitsubishi LT-20 First I adjusted the pickup exaktly...audiosciencereview.com
Thanks for pointing med too QR2009 FR sweep, even if my record is badly treated the FR seem good.
View attachment 274554
But only in 45° L direction? Perhaps you wrote is somewhere, but how large is bin size vs. f?
View attachment 274614
[25, 30, 25, 25, 25, 25, 30, 25, 30, 25, 30, 30, 30, 35, 35, 35, 35, 35, 35, 35, 40, 40, 40, 40, 45, 40, 45, 45, 45, 45]
f[0]: 25
a[0]: 714.0505025957801
f[2]: 25
a[2]: 744.5318299600603
f[3]: 25
a[3]: 724.1118547340114
f[4]: 25
a[4]: 735.016179146185
f[5]: 25
a[5]: 741.9490647758521
f[7]: 25
a[7]: 752.5597109199148
f[9]: 25
a[9]: 713.155938930283
f_out append 25
a_out append 732.1964401517267
f[1]: 30
a[1]: 664.9557610951268
f[6]: 30
a[6]: 731.9400263363799
f[8]: 30
a[8]: 688.1360526883085
f[10]: 30
a[10]: 666.4466943939625
f[11]: 30
a[11]: 668.0342292470842
f[12]: 30
a[12]: 651.0868565248278
f_out append 30
a_out append 678.4332700476149
f[13]: 35
a[13]: 653.061517255442
f[14]: 35
a[14]: 659.905914872235
f[15]: 35
a[15]: 666.5882270151418
f[16]: 35
a[16]: 655.9677975202726
f[17]: 35
a[17]: 688.8555461797697
f[18]: 35
a[18]: 695.7629451510218
f[19]: 35
a[19]: 662.7196760832829
f_out append 35
a_out append 668.9802320110238
f[20]: 40
a[20]: 654.0369192800038
f[21]: 40
a[21]: 642.221874040165
f[22]: 40
a[22]: 676.4778060561893
f[23]: 40
a[23]: 654.5196023544113
f[25]: 40
a[25]: 651.9181640376564
f_out append 40
a_out append 655.8348731536851
f[24]: 45
a[24]: 677.5937776607718
f[26]: 45
a[26]: 683.9640262044979
f[27]: 45
a[27]: 666.5412955785015
f[28]: 45
a[28]: 674.0879870076102
f[29]: 45
a[29]: 650.6822960947187
f_out append 45
a_out append 670.57387650922
[25, 30, 35, 40, 45]
[732.1964401517267, 678.4332700476149, 668.9802320110238, 655.8348731536851, 670.57387650922]
if 2*f0<F/2-2 and f0 > minf/fstep and f0 < maxf/fstep:
f2 = np.argmax(y0[(2*f0)-2:(2*f0)+2])
freq2h.append(f0*fstep)
amp2h.append(y0[2*f0-2+f2])
if 3*f0<F/2-2 and f0 > minf/fstep and f0 < maxf/fstep:
f3 = np.argmax(y0[(3*f0)-2:(3*f0)+2])
freq3h.append(f0*fstep)
amp3h.append(y0[3*f0-2+f3])
print(freq)
return freq, amp, freqx, ampx, freq2h, amp2h, freq3h, amp3h
ATN150MLX stylus on Signet body. I am playing around with that body type (AT150, AT7V, AT120, etc.) and various styli to see how inductance affects the frequency response. Essentially Signet cartridges are premium AT cartridges with higher inductance. These Signets spec at 550mH while the AT150 line specs at around 350mH. As inductance mostly affects the high end, my bet is that with a lot of AT cartridges you don't need to match the bodies and styli, and in fact you may be better off with a unique and possibly cheaper combination. People do this all the time on other boards, which makes sense as AT is notorious for terminating and replacing cartridge lines, but, of course, they do so blindly so I sure as hell don't trust what they say works.
Edit: here is a NON-FINAL, WORKING example:
View attachment 267830
Edit 2: I want to redo the AT7V measurement as it is quite old, but I am hoping I can end up with something like this:
View attachment 267834
I though Azimuth showed up in cross talk which looks OK to me. Why is STR-100 no good for cross talk? Assuming it is Azimuth which way do I turn cart based on the measured distortion? (clockwise or counter clockwise looking at front of cart). Thanks again.Azimuth is probably off. STR-100 is no good for checking that.
I do not own and know the "pcc" approach of denon, this late 70', this disc and the use of this active module...I've been playing around with my Denon PCC test record and my AT132/ATN152 combo, trying to get it to do 40dB+ of separation again by means of active compensation. First step is to measure crosstalk as is, of course. But I noticed something weird. LtoR crosstalk was only at around 20dB, while RtoL was about 32, just like it used to be for both channels the last time I measured. I didn't use it for about one year but it did not get damaged. Does anyone have an idea what might cause this? The preamp I use is different than last time, though I can hardly imagine it being bad enough to cause that much crosstalk on only one channel. Pretty odd.
I don’t have the STR100 but I think there are some common patterns seen between different messurements which then translates to record itself and not cartridge or measurement setup. Did you compare to other results?I got the latest script working and it seems to work OK but there is a big drop in distortion in one channel just below 1000 Hz. I tried another test record and it was the same. I also tried playing with anti-skate and no difference. Is this an issue with my set up or with script or is it normal? In any case thank you very much @JP and @USER for all the work.
View attachment 275082
Looking at the JP tests using STR-100 type 3 new there are some similarities but my measurements seem a little more extreme. I did play with Azimuth and got the channels much more balanced when it comes to cross talk even if they are not as low was would be expected. Now I am wondering if I am driving blind with the STR 100 when it comes to azimuth.... seems like it unfortunately.I don’t have the STR100 but I think there are some common patterns seen between different messurements which then translates to record itself and not cartridge or measurement setup. Did you compare to other results?
It's not surprising that you keep coming back to it. It performs very well. I like it a lot too and it is my favorite 2g tracking cartridge. Among Audio-Technica enthusiasts, it is a favorite as well. As I have come to learn about audio in general, higher price doesn't mean better. Hopefully, with these measurements, people will put more value into what really matters.Did you end up re-doing the AT7V measurement?
I keep coming back to it for subjective reasons.
I'm curious to see what the measurements say, especially given how cheap it is vs the much more expensive carts I'm regularly comparing it to (Nagoaka MP-500, ART9XA)
This article from a 1979 issue of Elector Magazine should explain it a lot better than I can. Yes, I know it's from Vinyl Engine, but I'm the one who uploaded itI do not own and know the "pcc" approach of denon, this late 70', this disc and the use of this active module...
could you explain it to us?
thank you
;-)
#!/usr/bin/env python3
'''
#################
# VER 16.4 [BETA]
#################
__author__ = "Scott Wurcer, John P. Jones III [JP]"
__copyright__ = "Copyright 2023"
__credits__ = ["Scott Wurcer", "John P. Jones III [JP]"]
__license__ = "MIT"
__version__ = "16.4"
__maintainer__ = "John P. Jones III [JP]"
__email__ = "[email protected]"
__status__ = "Development"
__title__ = "Phono Graphing Script"
__name__ = "phono_freq_plot_{{ revision }}.py"
__modified__ = 22MAR23
__misc__ = "CFP"
+-------+-------+-------+-------+--------------------------------------------------------------+
| VER | Major | Minor | Micro | DESCRIPTION |
|=======|=======|=======|=======|==============================================================|
| 16.4B | | | X | Improve documentation/PEP |
+-------+-------+-------+-------+--------------------------------------------------------------+
| 16.3B | | | X | Removed prefilter |
+-------+-------+-------+-------+--------------------------------------------------------------+
| 16.2B | | | X | Fixed copy-paste error with plot style 1 |
+-------+-------+-------+-------+--------------------------------------------------------------+
| 16.1B | | X | | Added equipment info line; Crosstalk 1kHz outputs on the |
| | | | | console line; Adjusted font sizes. |
+-------+-------+-------+-------+--------------------------------------------------------------+
| 16.0B | X | | | Process 2x .wav; Aability to REF_1KHZ; Fixed cosmetic and |
| | | | | layout issues; Plotdataout will output data for both files; |
| | | | | Timestamp now included |
+-------+-------+-------+-------+--------------------------------------------------------------+
| 15.3A | | X | | Added Butterworth HP pre-filter; prefilterorder is the order |
| | | | | of the filter set by prefilterfreq = {{BOOL}} |
+-------+-------+-------+-------+--------------------------------------------------------------+
| 15.2A | | X | | Let matplotlib decide the location for the legend box in |
| | | | | plot style 2 |
+-------+-------+-------+-------+--------------------------------------------------------------+
| 15.1A | | | X | Fixed axis colors for plot style 1 |
+-------+-------+-------+-------+--------------------------------------------------------------+
| 15.0A | X | | | Detects mono or stereo file; Script (Librosa Lib) limited to |
| | | | | 96kHz; ONEKHZ_START will start plot 1kH;Plot style 3 will not|
| | | | | plot crosstalk |
+-------+-------+-------+-------+--------------------------------------------------------------+
| 14.0B | | | | Internal Only |
+-------+-------+-------+-------+--------------------------------------------------------------+
| 13.0B | X | | | Full-Range plots; Compensation for STR-100; RIAA and inverse |
| | | | | RIAA implemented separately for bass and treble; SRC all |
| | | | | files to 96kHz (temporary to make filtering easier). |
+-------+-------+-------+-------+--------------------------------------------------------------+
PLEASE READ
###########
INSTRUCTIONS
------------
After mounting and calibrating your phono cartridge, play a test record of a
stereo '20Hz-20Khz Frequency Sweep' and record/save the playback as a *.wav
type file. If an RIAA phono-preamp was used in the chain, then toggle the
RIAA_MODE [described below]. The script was designed around the JVC TRS-1007
test record, but the CBS-STR100 test record can also be used (but must be
selected).
To use this script you need to edit HOME to the directory where the .wav file is.
The .wav file can be any monotonic frequency sweep either up or down in
frequency but it must be trimmed at both ends to remove any leading silence
[aka:"file should start on signal", but filter 1Kz beeps].
The info_line should be alpha-numeric with entries separated by " / " only.
The script will save a .png file that is named from the info line,
replacing " / " with "_". As example "this / is / a / test" will create a file
named "this_is_a_test.png"
PLOT_STYLE = 1 - traditional
2 - dual axis (twinx)
3 - dual plot
RIAA_MODE = 0 - off
1 - bass emphasis
2 - trebel de-emphasis
3 - both
RIAA_INV = 0 - disable
1 - inverse RIAA EQ per RIAA_MODE setting
CBS_STR100 = 0 - disable
1 - enable 6dB/oct correction from 500Hz to 40Hz
REF_1KHZ = 1000 [Frequency in Hz to set as 0dB in the plot]
FILE0_NORM = 0 - REF_1KHZ both files independently
1 - REF_1KHZ both files to file 0 level
'''
##################################################
# IMPORTED PYTHON LIBS
##################################################
import os
from pathlib import Path
from itertools import chain
from datetime import datetime
from scipy import signal
from scipy.io.wavfile import read
from matplotlib.legend_handler import HandlerBase
import matplotlib.pyplot as plt
import numpy as np
import librosa
##################################################
# Edit here to add a HOME directory, etc.
##################################################
#HOME = '/home/xxx/polar' #THIS IS NOT NEEDED IF RAN FROM A FILE
FILE_0 = 'file.wav'
FILE_1 = ''
##################################################
# NOTE: Maintain a space between the [/] slashes
##################################################
INFO_LINE = 'Cart / Load / Record'
EQUIP_INFO = 'Arm -> Phonostage -> ADC'
ROUND_LVL = 1
PLOT_STYLE = 2
PLOT_DATA_OUT = 0
RIAA_MODE = 0
RIAA_INV = 0
CBS_STR100 = 0
REF_1KHZ = 1000
FILE0_NORM = 1
ONEKHZ_START = 0
END_FREQ = 20000
OV_DYN_LIMIT = 0
OV_DYN_LIMIT_VALUE = [-35, 5]
TOP_DB_VAR = 100
FRAME_LENGTH_VAR = 1024
HOP_LENGTH_VAR = 256
#global FILE_OPEN_IDX
FILE_OPEN_IDX = 0
##################################################
# FUNCTION [00]
##################################################
def align_yaxis(ax1, ax2):
"""_summary_
Args:
ax1 (_type_): _description_
ax2 (_type_): _description_
Returns:
_type_: _description_
"""
y_lims = np.array([ax.get_ylim() for ax in [ax1, ax2]])
# force 0 to appear on both axes, comment if don't need
y_lims[:, 0] = y_lims[:, 0].clip(None, 0)
y_lims[:, 1] = y_lims[:, 1].clip(0, None)
# REF_1KHZ both axes
y_mags = (y_lims[:, 1] - y_lims[:, 0]).reshape(len(y_lims), 1)
y_lims_REF_1KHZd = y_lims / y_mags
# find combined range
y_new_lims_REF_1KHZd = np.array(
[np.min(y_lims_REF_1KHZd),
np.max(y_lims_REF_1KHZd)])
# deREF_1KHZ combined range to get new axes
new_lim1, new_lim2 = y_new_lims_REF_1KHZd * y_mags
return new_lim1, new_lim2
##################################################
# CLASS [00]
##################################################
class AnyObjectHandler(HandlerBase):
"""_summary_
"""
##################################################
# SUB-FUNCTION [00]
##################################################
def create_artists(self, legend, orig_handle, x0, y0, width, height,
fontsize, trans):
l1 = plt.Line2D([x0, y0 + width], [0.7 * height, 0.7 * height],
color=orig_handle[0],
linestyle=orig_handle[1])
l2 = plt.Line2D([x0, y0 + width], [0.3 * height, 0.3 * height],
color=orig_handle[2],
linestyle=orig_handle[3])
return [l1, l2]
##################################################
# FUNCTION [01]
##################################################
def ft_window(n): #Matlab's flat top window
"""_summary_
Args:
n (_type_): _description_
Returns:
_type_: _description_
"""
w = []
a0 = 0.21557895
a1 = 0.41663158
a2 = 0.277263158
a3 = 0.083578947
a4 = 0.006947368
pi = np.pi
for x in range(0, n):
w.append(a0 - a1 * np.cos(2 * pi * x / (n - 1)) +
a2 * np.cos(4 * pi * x / (n - 1)) -
a3 * np.cos(6 * pi * x / (n - 1)) + a4 * np.cos(8 * pi * x /
(n - 1)))
return w
##################################################
# FUNCTION [02]
##################################################
def find_nearest(array, value):
"""_summary_
Args:
array (_type_): _description_
value (_type_): _description_
Returns:
_type_: _description_
"""
array = np.asarray(array)
idx = (np.abs(array - value)).argmin()
return idx
##################################################
# FUNCTION [03]
##################################################
def createplotdata(insig, Fs):
"""_summary_
Args:
insig (_type_): _description_
Fs (_type_): _description_
Returns:
_type_: _description_
"""
fout = []
aout = []
foutx = []
aoutx = []
fout2 = []
aout2 = []
fout3 = []
aout3 = []
global norm
##################################################
# SUB-FUNCTION [01]
##################################################
def interpolate(f, a, minf, maxf, fstep):
"""_summary_
Args:
f (_type_): _description_
a (_type_): _description_
minf (_type_): _description_
maxf (_type_): _description_
fstep (_type_): _description_
Returns:
_type_: _description_
"""
f_out = []
a_out = []
amp = 0
count = 0
for x in range(minf, (maxf) + 1, fstep):
for y in range(0, len(f)):
if f[y] == x:
amp = amp + a[y]
count = count + 1
if count != 0:
f_out.append(x)
a_out.append(20 * np.log10(amp / count))
amp = 0
count = 0
return f_out, a_out
##################################################
# SUB-FUNCTION [02]
##################################################
def rfft(insig, Fs, minf, maxf, fstep):
"""_summary_
Args:
insig (_type_): _description_
Fs (_type_): _description_
minf (_type_): _description_
maxf (_type_): _description_
fstep (_type_): _description_
Returns:
_type_: _description_
"""
freq = []
amp = []
freqx = []
ampx = []
freq2h = []
amp2h = []
freq3h = []
amp3h = []
F = int(Fs / fstep)
win = ft_window(F)
if chinfile == 1:
for x in range(0, len(insig) - F, F):
y = abs(np.fft.rfft(insig[x:x + F] * win))
f = np.argmax(y) #use largest bin
if f >= minf / fstep and f <= maxf / fstep:
freq.append(f * fstep)
amp.append(y[f])
if 2 * f < F / 2 - 2 and f > minf / fstep and f < maxf / fstep:
f2 = np.argmax(y[(2 * f) - 2:(2 * f) + 2])
freq2h.append(f * fstep)
amp2h.append(y[2 * f - 2 + f2])
if 3 * f < F / 2 - 2 and f > minf / fstep and f < maxf / fstep:
f3 = np.argmax(y[(3 * f) - 2:(3 * f) + 2])
freq3h.append(f * fstep)
amp3h.append(y[3 * f - 2 + f3])
else:
for x in range(0, len(insig[0]) - F, F):
y0 = abs(np.fft.rfft(insig[0, x:x + F] * win))
y1 = abs(np.fft.rfft(insig[1, x:x + F] * win))
f0 = np.argmax(y0) #use largest bin
f1 = np.argmax(y1) #use largest bin
if f0 >= minf / fstep and f0 <= maxf / fstep:
freq.append(f0 * fstep)
freqx.append(f1 * fstep)
amp.append(y0[f0])
ampx.append(y1[f1])
if 2 * f0 < F / 2 - 2 and f0 > minf / fstep and f0 < maxf / fstep:
f2 = np.argmax(y0[(2 * f0) - 2:(2 * f0) + 2])
freq2h.append(f0 * fstep)
amp2h.append(y0[2 * f0 - 2 + f2])
if 3 * f0 < F / 2 - 2 and f0 > minf / fstep and f0 < maxf / fstep:
f3 = np.argmax(y0[(3 * f0) - 2:(3 * f0) + 2])
freq3h.append(f0 * fstep)
amp3h.append(y0[3 * f0 - 2 + f3])
return freq, amp, freqx, ampx, freq2h, amp2h, freq3h, amp3h
##################################################
# SUB-FUNCTION [03]
##################################################
def NORMCBS_STR100(f, a):
"""_summary_
Args:
f (_type_): _description_
a (_type_): _description_
Returns:
_type_: _description_
"""
fmin = 40
fmax = 500
slope = -6.02
for x in range(find_nearest(f, fmin), (find_nearest(f, fmax))):
a[x] = a[x] + 20 * np.log10(1 * ((f[x]) / fmax)**(
(slope / 20) / np.log10(2)))
return a
##################################################
# SUB-FUNCTION [04]
##################################################
def chunk(insig, Fs, fmin, fmax, step, offset):
"""_summary_
Args:
insig (_type_): _description_
Fs (_type_): _description_
fmin (_type_): _description_
fmax (_type_): _description_
step (_type_): _description_
offset (_type_): _description_
Returns:
_type_: _description_
"""
f, a, fx, ax, f2, a2, f3, a3 = rfft(insig, Fs, fmin, fmax, step)
f, a = interpolate(f, a, fmin, fmax, step)
fx, ax = interpolate(fx, ax, fmin, fmax, step)
f2, a2 = interpolate(f2, a2, fmin, fmax, step)
f3, a3 = interpolate(f3, a3, fmin, fmax, step)
a = [x - offset for x in a]
ax = [x - offset for x in ax]
a2 = [x - offset for x in a2]
a3 = [x - offset for x in a3]
return f, a, fx, ax, f2, a2, f3, a3
##################################################
# SUB-FUNCTION [05]
##################################################
def concat(f, a, fx, ax, f2, a2, f3, a3, fout, aout, foutx, aoutx, fout2,
aout2, fout3, aout3):
"""_summary_
Args:
f (_type_): _description_
a (_type_): _description_
fx (_type_): _description_
ax (_type_): _description_
f2 (_type_): _description_
a2 (_type_): _description_
f3 (_type_): _description_
a3 (_type_): _description_
fout (_type_): _description_
aout (_type_): _description_
foutx (_type_): _description_
aoutx (_type_): _description_
fout2 (_type_): _description_
aout2 (_type_): _description_
fout3 (_type_): _description_
aout3 (_type_): _description_
Returns:
_type_: _description_
"""
fout = fout + f
aout = aout + a
foutx = foutx + fx
aoutx = aoutx + ax
fout2 = fout2 + f2
aout2 = aout2 + a2
fout3 = fout3 + f3
aout3 = aout3 + a3
return fout, aout, foutx, aoutx, fout2, aout2, fout3, aout3
if ONEKHZ_START == 0:
f, a, fx, ax, f2, a2, f3, a3 = chunk(insig, Fs, 20, 45, 5, 26.03)
fout, aout, foutx, aoutx, fout2, aout2, fout3, aout3 = concat(
f, a, fx, ax, f2, a2, f3, a3, fout, aout, foutx, aoutx, fout2,
aout2, fout3, aout3)
f, a, fx, ax, f2, a2, f3, a3 = chunk(insig, Fs, 50, 90, 10, 19.995)
fout, aout, foutx, aoutx, fout2, aout2, fout3, aout3 = concat(
f, a, fx, ax, f2, a2, f3, a3, fout, aout, foutx, aoutx, fout2,
aout2, fout3, aout3)
f, a, fx, ax, f2, a2, f3, a3 = chunk(insig, Fs, 100, 980, 20, 13.99)
fout, aout, foutx, aoutx, fout2, aout2, fout3, aout3 = concat(
f, a, fx, ax, f2, a2, f3, a3, fout, aout, foutx, aoutx, fout2,
aout2, fout3, aout3)
f, a, fx, ax, f2, a2, f3, a3 = chunk(insig, Fs, 1000, END_FREQ, 100, 0)
fout, aout, foutx, aoutx, fout2, aout2, fout3, aout3 = concat(
f, a, fx, ax, f2, a2, f3, a3, fout, aout, foutx, aoutx, fout2, aout2,
fout3, aout3)
if CBS_STR100 == 1:
aout = NORMCBS_STR100(fout, aout)
aout2 = NORMCBS_STR100(fout2, aout2)
aout3 = NORMCBS_STR100(fout3, aout3)
if chinfile == 2:
aoutx = NORMCBS_STR100(foutx, aoutx)
if FILE0_NORM == 1 and FILE_OPEN_IDX == 1:
i = find_nearest(fout, REF_1KHZ)
norm = aout[i]
elif FILE0_NORM == 0:
i = find_nearest(fout, REF_1KHZ)
norm = aout[i]
aout = aout - norm #amplitude is in dB so REF_1KHZ by subtraction at [i]
aoutx = aoutx - norm
aout2 = aout2 - norm
aout3 = aout3 - norm
sos = signal.iirfilter(3, .5, btype='lowpass',
output='sos') #filter some noise
aout = signal.sosfiltfilt(sos, aout)
aout2 = signal.sosfiltfilt(sos, aout2)
aout3 = signal.sosfiltfilt(sos, aout3)
if chinfile == 2 and len(aoutx) > 1:
aoutx = signal.sosfiltfilt(sos, aoutx)
return fout, aout, foutx, aoutx, fout2, aout2, fout3, aout3
##################################################
# FUNCTION [04]
##################################################
def ordersignal(sig, Fs):
"""_summary_
Args:
sig (_type_): _description_
Fs (_type_): _description_
Returns:
_type_: _description_
"""
F = int(Fs / 100)
win = ft_window(F)
if chinfile == 1:
y = abs(np.fft.rfft(sig[0:F] * win))
minf = np.argmax(y)
y = abs(np.fft.rfft(sig[len(sig) - F:len(sig)] * win))
maxf = np.argmax(y)
else:
y = abs(np.fft.rfft(sig[0, 0:F] * win))
minf = np.argmax(y)
y = abs(np.fft.rfft(sig[0][len(sig[0]) - F:len(sig[0])] * win))
maxf = np.argmax(y)
if maxf < minf:
maxf, minf = minf, maxf
sig = np.flipud(sig)
return sig, minf, maxf
##################################################
# FUNCTION [05]
##################################################
def riaaiir(sig, Fs, mode, inv):
"""_summary_
Args:
sig (_type_): _description_
Fs (_type_): _description_
mode (_type_): _description_
inv (_type_): _description_
Returns:
_type_: _description_
"""
if Fs == 96000:
at = [1, -0.66168391, -0.18158841]
bt = [0.1254979638905360, 0.0458786797031512, 0.0018820452752401]
ars = [1, -0.60450091, -0.39094593]
brs = [0.90861261463964900, -0.52293147388301200, -0.34491369168550900]
if inv == 1:
at, bt = bt, at
ars, brs = brs, ars
if mode == 1:
sig = signal.lfilter(brs, ars, sig)
if mode == 2:
sig = signal.lfilter(bt, at, sig)
if mode == 3:
sig = signal.lfilter(bt, at, sig)
sig = signal.lfilter(brs, ars, sig)
return sig
##################################################
# FUNCTION [06]
##################################################
def openaudio(_FILE):
"""_summary_
Args:
_FILE (_type_): _description_
Returns:
_type_: _description_
"""
global chinfile
global FILE_OPEN_IDX
chinfile = 1
srinfile = librosa.get_samplerate(_FILE)
audio, Fs = librosa.load(_FILE, sr=None, mono=False)
if len(audio.shape) == 2:
chinfile = 2
filelength = audio.shape[1] / Fs
else:
filelength = audio.shape[0] / Fs
print('Input File: ' + str(_FILE))
print('Sample Rate: ' + str("{:,}".format(srinfile) + 'Hz'))
if Fs < 96000:
print(' Resampling to 96,000Hz')
audio = librosa.resample(audio, orig_sr=Fs, target_sr=96000)
Fs = 96000
print('Channels: ' + str(chinfile))
print(f"Length: {filelength}s")
if RIAA_MODE != 0:
audio = riaaiir(audio, Fs, RIAA_MODE, RIAA_INV)
audio, index = librosa.effects.trim(audio,
top_db=TOP_DB_VAR,
frame_length=FRAME_LENGTH_VAR,
hop_length=HOP_LENGTH_VAR)
print(f"In/Out (s): {index / Fs}")
audio, minf, maxf = ordersignal(audio, Fs)
print('Min Freq: ' + str("{:,}".format(minf * 100) + 'Hz'))
print('Max Freq: ' + str("{:,}".format(maxf * 100) + 'Hz\n'))
FILE_OPEN_IDX += 1
return audio, Fs, minf, maxf
##################################################
# (MAIN) FUNCTION [07]
##################################################
def main():
""" MAIN: CALL AND ORCHESTRATE ALL FUNCTIONS"""
# CALL FNC1: Open and convert the wav file using librosa
input_sig, Fs, minf, maxf = openaudio(FILE_0)
# CALL FNC3: plot the the librosa data
fo0, ao0, fox0, aox0, fo2h0, ao2h0, fo3h0, ao3h0 = createplotdata(
input_sig, Fs)
# MANIPULATE RETURNED VALUES
deltah0 = round((max(ao0)), ROUND_LVL)
deltal0 = abs(round((min(ao0)), ROUND_LVL))
if aox0.size > 0:
idx_fox0 = find_nearest(fox0, 1000)
print('X-talk @1kHz: ' + (str(round(aox0[idx_fox0], 2))) + 'dB\n\n')
if FILE_1:
input_sig, Fs, minf, maxf = openaudio(FILE_1)
fo1, ao1, fox1, aox1, fo2h1, ao2h1, fo3h1, ao3h1 = createplotdata(
input_sig, Fs)
deltah1 = round((max(ao1)), ROUND_LVL)
deltal1 = abs(round((min(ao1)), ROUND_LVL))
if aox1.size > 0:
idx_fox1 = find_nearest(fox1, 1000)
print('X-talk @1kHz: ' + (str(round(aox1[idx_fox1], 2))) +
'dB\n\n')
if PLOT_DATA_OUT == 1:
dao0 = [*ao0, *[''] * (len(fo0) - len(ao0))]
daox0 = [*aox0, *[''] * (len(fo0) - len(aox0))]
dao2h0 = [*ao2h0, *[''] * (len(fo0) - len(ao2h0))]
dao3h0 = [*ao3h0, *[''] * (len(fo0) - len(ao3h0))]
print('\n\nFile 0 Plot Data: (freq, ampl, x-talk, 2h, 3h)\n\n')
dataout = list(zip(fo0, dao0, daox0, dao2h0, dao3h0))
for fo, ao, aox, ao2, ao3 in dataout:
print(fo, ao, aox, ao2, ao3, sep=', ')
if FILE_1:
dao1 = [*ao1, *[''] * (len(fo1) - len(ao1))]
daox1 = [*aox1, *[''] * (len(fo1) - len(aox1))]
dao2h1 = [*ao2h1, *[''] * (len(fo1) - len(ao2h1))]
dao3h1 = [*ao3h1, *[''] * (len(fo1) - len(ao3h1))]
print('\n\nFile 1 Plot Data: (freq, ampl, x-talk, 2h, 3h)\n\n')
dataout = list(zip(fo1, dao1, daox1, dao2h1, dao3h1))
for fo, ao, aox, ao2, ao3 in dataout:
print(fo, ao, aox, ao2, ao3, sep=', ')
plt.rcParams["xtick.minor.visible"] = True
plt.rcParams["ytick.minor.visible"] = True
if PLOT_STYLE == 1:
fig, ax1 = plt.subplots(1, 1, figsize=(14, 6))
ax1.semilogx(fo0, ao0, color='#0000ff', label='Freq Response')
ax1.semilogx(fo2h0,
ao2h0,
color='#0080ff',
label='2ⁿᵈ Harmonic',
alpha=1,
linewidth=0.75)
ax1.semilogx(fo3h0,
ao3h0,
color='#00dfff',
label='3ʳᵈ Harmonic',
alpha=1,
linewidth=0.75)
ax1.semilogx(fox0,
aox0,
color='#0000ff',
linestyle=(0, (3, 1, 1, 1)),
label='Crosstalk')
if FILE_1:
ax1.semilogx(fo1, ao1, color='#ff0000', label='Freq Response')
ax1.semilogx(fo2h1,
ao2h1,
color='#ff8000',
label='2ⁿᵈ Harmonic',
alpha=1,
linewidth=0.75)
ax1.semilogx(fo3h1,
ao3h1,
color='#ffdf00',
label='3ʳᵈ Harmonic',
alpha=1,
linewidth=0.75)
ax1.semilogx(fox1,
aox1,
color='#ff0000',
linestyle=(0, (3, 1, 1, 1)),
label='Crosstalk')
plt.legend(
[("#0000ff", "-", "#ff0000", "-"),
("#0000ff", (0, (3, 1, 1, 1)), "#ff0000", (0, (3, 1, 1, 1))),
("#0080ff", "-", "#ff8000", "-"),
("#00dfff", "-", "#ffdf00", "-")],
['Freq Response', 'Crosstalk', '2ⁿᵈ Harmonic', '3ʳᵈ Harmonic'],
handler_map={tuple: AnyObjectHandler()},
loc=4)
ax1.set_ylim((min(chain(aox0, aox1)) - 2),
(max(chain(ao0, ao1)) + 2))
else:
plt.legend(loc=4)
ax1.set_ylabel("Amplitude (dB)")
ax1.set_xlabel("Frequency (Hz)")
plt.autoscale(enable=True, axis='y')
if OV_DYN_LIMIT == 1:
ax1.set_ylim(*OV_DYN_LIMIT_VALUE)
if PLOT_STYLE == 2:
fig, ax1 = plt.subplots(1, 1, figsize=(14, 6))
ax2 = ax1.twinx()
if max(ao0) < 7:
ax1.set_ylim(-25, 7)
if max(ao0) < 4:
ax1.set_ylim(-25, 5)
if max(ao0) < 2:
ax1.set_ylim(-29, 3)
if max(ao0) < 0.5:
ax1.set_ylim(-30, 2)
if aox0.size > 0:
if FILE_1:
ax1.set_ylim((min(chain(aox0, aox1)) - 2),
(max(chain(ao0, ao1)) + 2))
else:
ax1.set_ylim((min(aox0) - 2), (max(ao0) + 2))
if OV_DYN_LIMIT == 1:
ax1.set_ylim(*OV_DYN_LIMIT_VALUE)
ax1.semilogx(fo0, ao0, color='#0000ff', label='Freq Response')
ax2.semilogx(fo2h0,
ao2h0,
color='#0080ff',
label='2ⁿᵈ Harmonic',
alpha=1,
linewidth=0.75)
ax2.semilogx(fo3h0,
ao3h0,
color='#00dfff',
label='3ʳᵈ Harmonic',
alpha=1,
linewidth=0.75)
ax1.semilogx(fox0,
aox0,
color='#0000ff',
linestyle=(0, (3, 1, 1, 1)),
label='Crosstalk')
if FILE_1:
ax1.semilogx(fo1, ao1, color='#ff0000', label='Freq Response')
ax2.semilogx(fo2h1,
ao2h1,
color='#ff8000',
label='2ⁿᵈ Harmonic',
alpha=1,
linewidth=0.75)
ax2.semilogx(fo3h1,
ao3h1,
color='#ffdf00',
label='3ʳᵈ Harmonic',
alpha=1,
linewidth=0.75)
ax1.semilogx(fox1,
aox1,
color='#ff0000',
linestyle=(0, (3, 1, 1, 1)),
label='Crosstalk')
plt.legend(
[("#0000ff", "-", "#ff0000", "-"),
("#0000ff", (0, (3, 1, 1, 1)), "#ff0000", (0, (3, 1, 1, 1))),
("#0080ff", "-", "#ff8000", "-"),
("#00dfff", "-", "#ffdf00", "-")],
['Freq Response', 'Crosstalk', '2ⁿᵈ Harmonic', '3ʳᵈ Harmonic'],
handler_map={tuple: AnyObjectHandler()},
loc=4)
ax1.set_ylim((min(chain(aox0, aox1)) - 2),
(max(chain(ao0, ao1)) + 2))
else:
lines1, labels1 = ax1.get_legend_handles_labels()
lines2, labels2 = ax2.get_legend_handles_labels()
plt.legend(lines1 + lines2, labels1 + labels2, loc=4)
new_lim1, new_lim2 = align_yaxis(ax1, ax2)
ax1.set_ylim(new_lim1)
ax2.set_ylim(new_lim2)
ax1.set_ylabel("Amplitude (dB)")
ax2.set_ylabel("Distortion (dB)")
ax1.set_xlabel("Frequency (Hz)")
if PLOT_STYLE == 3:
fig, (ax1, ax2) = plt.subplots(2, 1, sharex=True, figsize=(14, 6))
ax2.grid(True, which="major", axis="both", ls="-", color="black")
ax2.grid(True, which="minor", axis="both", ls="-", color="gainsboro")
ax1.set_ylim(-5, 5)
if FILE_1:
if (min(chain(ao0, ao1)) < -5) or (max(chain(ao0, ao1)) > 5):
ax1.autoscale(enable=True, axis='y')
elif (min(ao0) < -5) or (max(ao0) > 5):
ax1.autoscale(enable=True, axis='y')
if OV_DYN_LIMIT == 1:
ax1.set_ylim(*OV_DYN_LIMIT_VALUE)
ax1.semilogx(fo0, ao0, color='#0000ff', label='Freq Response')
ax2.semilogx(fo2h0, ao2h0, color='#0080ff', label='2nd Harmonic')
ax2.semilogx(fo3h0, ao3h0, color='#00dfff', label='3rd Harmonic')
if FILE_1:
ax1.semilogx(fo1, ao1, color='#ff0000', label='Freq Response')
ax2.semilogx(fo2h1, ao2h1, color='#ff8000', label='2ⁿᵈ Harmonic')
ax2.semilogx(fo3h1, ao3h1, color='#ffdf00', label='3ʳᵈ Harmonic')
ax1.legend([
("#0000ff", "-", "#ff0000", "-"),
], ['Freq Response'],
handler_map={tuple: AnyObjectHandler()},
loc=4)
ax2.legend([("#0080ff", "-", "#ff8000", "-"),
("#00dfff", "-", "#ffdf00", "-")],
['2ⁿᵈ Harmonic', '3ʳᵈ Harmonic'],
handler_map={tuple: AnyObjectHandler()},
loc=4)
else:
ax1.legend(loc=4)
ax2.legend(loc=4)
ax1.set_ylabel("Amplitude (dB)")
ax2.set_ylabel("Distortion (dB)")
ax2.set_xlabel("Frequency (Hz)")
ax1.grid(True, which="major", axis="both", ls="-", color="black")
ax1.grid(True, which="minor", axis="both", ls="-", color="gainsboro")
bbox_args = dict(boxstyle="round",
color='b',
fc='w',
ec='b',
alpha=1,
pad=.15)
ax1.annotate('+' + str(deltah0) + ', ' + u"\u2212" + str(deltal0) +
' dB',color = 'b',\
xy=(fo0[0],(ao0[0]-1)), xycoords='data', \
xytext=(-10, -20), textcoords='offset points', \
ha="left", va="center", bbox=bbox_args)
if FILE_1:
bbox_args = dict(boxstyle="round",
color='b',
fc='w',
ec='r',
alpha=1,
pad=.15)
ax1.annotate('+' + str(deltah1) + ', ' + u"\u2212" + str(deltal1) +
' dB',color = 'r',\
xy=(fo0[0],(ao0[0]-1)), xycoords='data', \
xytext=(-10, -34.5), textcoords='offset points', \
ha="left", va="center", bbox=bbox_args)
ax1.set_xticks(
[0, 20, 50, 100, 500, 1000, 5000, 10000, 20000, 50000, 100000])
ax1.set_xticklabels([
'0', '20', '50', '100', '500', '1k', '5k', '10k', '20k', '50k', '100k'
])
plt.autoscale(enable=True, axis='x')
ax1.set_title(INFO_LINE + "\n", fontsize=16)
now = datetime.now()
if FILE_1:
plt.figtext(.17, .118, INFO_LINE + "\n" + FILE_0 + "\n" + FILE_1 +
"\n" + \
now.strftime("%b %d, %Y %H:%M"), fontsize=6)
else:
plt.figtext(.17, .118, INFO_LINE + "\n" + FILE_0 + "\n" + \
now.strftime("%b %d, %Y %H:%M"), fontsize=6)
plt.figtext(.125, 0, EQUIP_INFO, alpha=.5, fontsize=6)
plt.savefig(INFO_LINE.replace(' / ', '_') + '.png',
bbox_inches='tight',
pad_inches=.5,
dpi=96)
plt.show()
print('\nDone!')
##################################################
# INVOKE MAIN
##################################################
if __name__ == '__main__':
main()
I'll try to figure it all out thank you.This article from a 1979 issue of Elector Magazine should explain it a lot better than I can. Yes, I know it's from Vinyl Engine, but I'm the one who uploaded it
All I can add to it is that stereo image improves by a lot, if your speaker setup is up to it. If channel volume and listening distance for both speakers are equal and frequency responses match, you can switch PCC on and hear the stereo image extend beyond the speakers (like with the CD copy of the same track) or switch it off and have the stereo image collapse to where it is squished between the speakers - like the way we're used to with vinyl. You can clearly hear that it's not an artificial enhanced stereo but it actually reduces a certain type of distortion.
The only thing that bothers me about the PCC 1000 is that it's yet another power hungry box. So I tried to recreate it in software and it worked![]()
Maybe show this to JP privately before posting something named REV16.4 and confusing the hell out of everyone? It seems disrespectful to go about this this way.Perhaps a bit premature, given John's to-do/wants list, but trying to follow along and understand -- As a general comment, I think we can use dicts {} and lists [] (list comprehension) a bit more to take the congestion out-- looking at other Librosa examples to see what can be done. But great work, JL. As he [JL] states, the real updates are tracked >here<.
#!/usr/bin/env python3 ''' ################# # VER 16.4 [BETA] ################# __author__ = "Scott Wurcer, John Pendergast" __copyright__ = "Copyright 2023" __credits__ = ["Scott Wurcer, John Pendergast"] __license__ = "MIT" __version__ = "16.4" __maintainer__ = "John Pendergast" __email__ = "[email protected]" __status__ = "Development" __title__ = "Phono Graphing Script" __modified__ = 22MAR23 __misc__ = "CFP" +-------+-------+-------+-------+--------------------------------------------------------------+ | VER | Major | Minor | Micro | DESCRIPTION | |=======|=======|=======|=======|==============================================================| | 16.4B | | | X | Improve documentation/PEP | +-------+-------+-------+-------+--------------------------------------------------------------+ | 16.3B | | | X | Removed prefilter | +-------+-------+-------+-------+--------------------------------------------------------------+ | 16.2B | | | X | Fixed copy-paste error with plot style 1 | +-------+-------+-------+-------+--------------------------------------------------------------+ | 16.1B | | X | | Added equipment info line; Crosstalk 1kHz outputs on the | | | | | | console line; Adjusted font sizes. | +-------+-------+-------+-------+--------------------------------------------------------------+ | 16.0B | X | | | Process 2x .wav; Aability to REF_1KHZ; Fixed cosmetic and | | | | | | layout issues; Plotdataout will output data for both files; | | | | | | Timestamp now included | +-------+-------+-------+-------+--------------------------------------------------------------+ | 15.3A | | X | | Added Butterworth HP pre-filter; prefilterorder is the order | | | | | | of the filter set by prefilterfreq = {{BOOL}} | +-------+-------+-------+-------+--------------------------------------------------------------+ | 15.2A | | X | | Let matplotlib decide the location for the legend box in | | | | | | plot style 2 | +-------+-------+-------+-------+--------------------------------------------------------------+ | 15.1A | | | X | Fixed axis colors for plot style 1 | +-------+-------+-------+-------+--------------------------------------------------------------+ | 15.0A | X | | | Detects mono or stereo file; Script (Librosa Lib) limited to | | | | | | 96kHz; ONEKHZ_START will start plot 1kH;Plot style 3 will not| | | | | | plot crosstalk | +-------+-------+-------+-------+--------------------------------------------------------------+ | 14.0B | | | | Internal Only | +-------+-------+-------+-------+--------------------------------------------------------------+ | 13.0B | X | | | Full-Range plots; Compensation for STR-100; RIAA and inverse | | | | | | RIAA implemented separately for bass and treble; SRC all | | | | | | files to 96kHz (temporary to make filtering easier). | +-------+-------+-------+-------+--------------------------------------------------------------+ PLEASE READ ########### INSTRUCTIONS ------------ After mounting and calibrating your phono cartridge, play a test record of a stereo '20Hz-20Khz Frequency Sweep' and record/save the playback as a *.wav type file. If an RIAA phono-preamp was used in the chain, then toggle the RIAA_MODE [described below]. The script was designed around the JVC TRS-1007 test record, but the CBS-STR100 test record can also be used (but must be selected). To use this script you need to edit HOME to the directory where the .wav file is. The .wav file can be any monotonic frequency sweep either up or down in frequency but it must be trimmed at both ends to remove any leading silence [aka:"file should start on signal", but filter 1Kz beeps]. The info_line should be alpha-numeric with entries separated by " / " only. The script will save a .png file that is named from the info line, replacing " / " with "_". As example "this / is / a / test" will create a file named "this_is_a_test.png" PLOT_STYLE = 1 - traditional 2 - dual axis (twinx) 3 - dual plot RIAA_MODE = 0 - off 1 - bass emphasis 2 - trebel de-emphasis 3 - both RIAA_INV = 0 - disable 1 - inverse RIAA EQ per RIAA_MODE setting CBS_STR100 = 0 - disable 1 - enable 6dB/oct correction from 500Hz to 40Hz REF_1KHZ = 1000 [Frequency in Hz to set as 0dB in the plot] FILE0_NORM = 0 - REF_1KHZ both files independently 1 - REF_1KHZ both files to file 0 level ''' ################################################## # IMPORTED PYTHON LIBS ################################################## import os from pathlib import Path from itertools import chain from datetime import datetime from scipy import signal from scipy.io.wavfile import read from matplotlib.legend_handler import HandlerBase import matplotlib.pyplot as plt import numpy as np import librosa ################################################## # Edit here to add a HOME directory, etc. ################################################## #HOME = '/home/xxx/polar' #THIS IS NOT NEEDED IF RAN FROM A FILE FILE_0 = 'file.wav' FILE_1 = '' ################################################## # NOTE: Maintain a space between the [/] slashes ################################################## INFO_LINE = 'Cart / Load / Record' EQUIP_INFO = 'Arm -> Phonostage -> ADC' ROUND_LVL = 1 PLOT_STYLE = 2 PLOT_DATA_OUT = 0 RIAA_MODE = 0 RIAA_INV = 0 CBS_STR100 = 0 REF_1KHZ = 1000 FILE0_NORM = 1 ONEKHZ_START = 0 END_FREQ = 20000 OV_DYN_LIMIT = 0 OV_DYN_LIMIT_VALUE = [-35, 5] TOP_DB_VAR = 100 FRAME_LENGTH_VAR = 1024 HOP_LENGTH_VAR = 256 #global FILE_OPEN_IDX FILE_OPEN_IDX = 0 ################################################## # FUNCTION [00] ################################################## def align_yaxis(ax1, ax2): """_summary_ Args: ax1 (_type_): _description_ ax2 (_type_): _description_ Returns: _type_: _description_ """ y_lims = np.array([ax.get_ylim() for ax in [ax1, ax2]]) # force 0 to appear on both axes, comment if don't need y_lims[:, 0] = y_lims[:, 0].clip(None, 0) y_lims[:, 1] = y_lims[:, 1].clip(0, None) # REF_1KHZ both axes y_mags = (y_lims[:, 1] - y_lims[:, 0]).reshape(len(y_lims), 1) y_lims_REF_1KHZd = y_lims / y_mags # find combined range y_new_lims_REF_1KHZd = np.array( [np.min(y_lims_REF_1KHZd), np.max(y_lims_REF_1KHZd)]) # deREF_1KHZ combined range to get new axes new_lim1, new_lim2 = y_new_lims_REF_1KHZd * y_mags return new_lim1, new_lim2 ################################################## # CLASS [00] ################################################## class AnyObjectHandler(HandlerBase): """_summary_ """ ################################################## # SUB-FUNCTION [00] ################################################## def create_artists(self, legend, orig_handle, x0, y0, width, height, fontsize, trans): l1 = plt.Line2D([x0, y0 + width], [0.7 * height, 0.7 * height], color=orig_handle[0], linestyle=orig_handle[1]) l2 = plt.Line2D([x0, y0 + width], [0.3 * height, 0.3 * height], color=orig_handle[2], linestyle=orig_handle[3]) return [l1, l2] ################################################## # FUNCTION [01] ################################################## def ft_window(n): #Matlab's flat top window """_summary_ Args: n (_type_): _description_ Returns: _type_: _description_ """ w = [] a0 = 0.21557895 a1 = 0.41663158 a2 = 0.277263158 a3 = 0.083578947 a4 = 0.006947368 pi = np.pi for x in range(0, n): w.append(a0 - a1 * np.cos(2 * pi * x / (n - 1)) + a2 * np.cos(4 * pi * x / (n - 1)) - a3 * np.cos(6 * pi * x / (n - 1)) + a4 * np.cos(8 * pi * x / (n - 1))) return w ################################################## # FUNCTION [02] ################################################## def find_nearest(array, value): """_summary_ Args: array (_type_): _description_ value (_type_): _description_ Returns: _type_: _description_ """ array = np.asarray(array) idx = (np.abs(array - value)).argmin() return idx ################################################## # FUNCTION [03] ################################################## def createplotdata(insig, Fs): """_summary_ Args: insig (_type_): _description_ Fs (_type_): _description_ Returns: _type_: _description_ """ fout = [] aout = [] foutx = [] aoutx = [] fout2 = [] aout2 = [] fout3 = [] aout3 = [] global norm ################################################## # SUB-FUNCTION [01] ################################################## def interpolate(f, a, minf, maxf, fstep): """_summary_ Args: f (_type_): _description_ a (_type_): _description_ minf (_type_): _description_ maxf (_type_): _description_ fstep (_type_): _description_ Returns: _type_: _description_ """ f_out = [] a_out = [] amp = 0 count = 0 for x in range(minf, (maxf) + 1, fstep): for y in range(0, len(f)): if f[y] == x: amp = amp + a[y] count = count + 1 if count != 0: f_out.append(x) a_out.append(20 * np.log10(amp / count)) amp = 0 count = 0 return f_out, a_out ################################################## # SUB-FUNCTION [02] ################################################## def rfft(insig, Fs, minf, maxf, fstep): """_summary_ Args: insig (_type_): _description_ Fs (_type_): _description_ minf (_type_): _description_ maxf (_type_): _description_ fstep (_type_): _description_ Returns: _type_: _description_ """ freq = [] amp = [] freqx = [] ampx = [] freq2h = [] amp2h = [] freq3h = [] amp3h = [] F = int(Fs / fstep) win = ft_window(F) if chinfile == 1: for x in range(0, len(insig) - F, F): y = abs(np.fft.rfft(insig[x:x + F] * win)) f = np.argmax(y) #use largest bin if f >= minf / fstep and f <= maxf / fstep: freq.append(f * fstep) amp.append(y[f]) if 2 * f < F / 2 - 2 and f > minf / fstep and f < maxf / fstep: f2 = np.argmax(y[(2 * f) - 2:(2 * f) + 2]) freq2h.append(f * fstep) amp2h.append(y[2 * f - 2 + f2]) if 3 * f < F / 2 - 2 and f > minf / fstep and f < maxf / fstep: f3 = np.argmax(y[(3 * f) - 2:(3 * f) + 2]) freq3h.append(f * fstep) amp3h.append(y[3 * f - 2 + f3]) else: for x in range(0, len(insig[0]) - F, F): y0 = abs(np.fft.rfft(insig[0, x:x + F] * win)) y1 = abs(np.fft.rfft(insig[1, x:x + F] * win)) f0 = np.argmax(y0) #use largest bin f1 = np.argmax(y1) #use largest bin if f0 >= minf / fstep and f0 <= maxf / fstep: freq.append(f0 * fstep) freqx.append(f1 * fstep) amp.append(y0[f0]) ampx.append(y1[f1]) if 2 * f0 < F / 2 - 2 and f0 > minf / fstep and f0 < maxf / fstep: f2 = np.argmax(y0[(2 * f0) - 2:(2 * f0) + 2]) freq2h.append(f0 * fstep) amp2h.append(y0[2 * f0 - 2 + f2]) if 3 * f0 < F / 2 - 2 and f0 > minf / fstep and f0 < maxf / fstep: f3 = np.argmax(y0[(3 * f0) - 2:(3 * f0) + 2]) freq3h.append(f0 * fstep) amp3h.append(y0[3 * f0 - 2 + f3]) return freq, amp, freqx, ampx, freq2h, amp2h, freq3h, amp3h ################################################## # SUB-FUNCTION [03] ################################################## def NORMCBS_STR100(f, a): """_summary_ Args: f (_type_): _description_ a (_type_): _description_ Returns: _type_: _description_ """ fmin = 40 fmax = 500 slope = -6.02 for x in range(find_nearest(f, fmin), (find_nearest(f, fmax))): a[x] = a[x] + 20 * np.log10(1 * ((f[x]) / fmax)**( (slope / 20) / np.log10(2))) return a ################################################## # SUB-FUNCTION [04] ################################################## def chunk(insig, Fs, fmin, fmax, step, offset): """_summary_ Args: insig (_type_): _description_ Fs (_type_): _description_ fmin (_type_): _description_ fmax (_type_): _description_ step (_type_): _description_ offset (_type_): _description_ Returns: _type_: _description_ """ f, a, fx, ax, f2, a2, f3, a3 = rfft(insig, Fs, fmin, fmax, step) f, a = interpolate(f, a, fmin, fmax, step) fx, ax = interpolate(fx, ax, fmin, fmax, step) f2, a2 = interpolate(f2, a2, fmin, fmax, step) f3, a3 = interpolate(f3, a3, fmin, fmax, step) a = [x - offset for x in a] ax = [x - offset for x in ax] a2 = [x - offset for x in a2] a3 = [x - offset for x in a3] return f, a, fx, ax, f2, a2, f3, a3 ################################################## # SUB-FUNCTION [05] ################################################## def concat(f, a, fx, ax, f2, a2, f3, a3, fout, aout, foutx, aoutx, fout2, aout2, fout3, aout3): """_summary_ Args: f (_type_): _description_ a (_type_): _description_ fx (_type_): _description_ ax (_type_): _description_ f2 (_type_): _description_ a2 (_type_): _description_ f3 (_type_): _description_ a3 (_type_): _description_ fout (_type_): _description_ aout (_type_): _description_ foutx (_type_): _description_ aoutx (_type_): _description_ fout2 (_type_): _description_ aout2 (_type_): _description_ fout3 (_type_): _description_ aout3 (_type_): _description_ Returns: _type_: _description_ """ fout = fout + f aout = aout + a foutx = foutx + fx aoutx = aoutx + ax fout2 = fout2 + f2 aout2 = aout2 + a2 fout3 = fout3 + f3 aout3 = aout3 + a3 return fout, aout, foutx, aoutx, fout2, aout2, fout3, aout3 if ONEKHZ_START == 0: f, a, fx, ax, f2, a2, f3, a3 = chunk(insig, Fs, 20, 45, 5, 26.03) fout, aout, foutx, aoutx, fout2, aout2, fout3, aout3 = concat( f, a, fx, ax, f2, a2, f3, a3, fout, aout, foutx, aoutx, fout2, aout2, fout3, aout3) f, a, fx, ax, f2, a2, f3, a3 = chunk(insig, Fs, 50, 90, 10, 19.995) fout, aout, foutx, aoutx, fout2, aout2, fout3, aout3 = concat( f, a, fx, ax, f2, a2, f3, a3, fout, aout, foutx, aoutx, fout2, aout2, fout3, aout3) f, a, fx, ax, f2, a2, f3, a3 = chunk(insig, Fs, 100, 980, 20, 13.99) fout, aout, foutx, aoutx, fout2, aout2, fout3, aout3 = concat( f, a, fx, ax, f2, a2, f3, a3, fout, aout, foutx, aoutx, fout2, aout2, fout3, aout3) f, a, fx, ax, f2, a2, f3, a3 = chunk(insig, Fs, 1000, END_FREQ, 100, 0) fout, aout, foutx, aoutx, fout2, aout2, fout3, aout3 = concat( f, a, fx, ax, f2, a2, f3, a3, fout, aout, foutx, aoutx, fout2, aout2, fout3, aout3) if CBS_STR100 == 1: aout = NORMCBS_STR100(fout, aout) aout2 = NORMCBS_STR100(fout2, aout2) aout3 = NORMCBS_STR100(fout3, aout3) if chinfile == 2: aoutx = NORMCBS_STR100(foutx, aoutx) if FILE0_NORM == 1 and FILE_OPEN_IDX == 1: i = find_nearest(fout, REF_1KHZ) norm = aout[i] elif FILE0_NORM == 0: i = find_nearest(fout, REF_1KHZ) norm = aout[i] aout = aout - norm #amplitude is in dB so REF_1KHZ by subtraction at [i] aoutx = aoutx - norm aout2 = aout2 - norm aout3 = aout3 - norm sos = signal.iirfilter(3, .5, btype='lowpass', output='sos') #filter some noise aout = signal.sosfiltfilt(sos, aout) aout2 = signal.sosfiltfilt(sos, aout2) aout3 = signal.sosfiltfilt(sos, aout3) if chinfile == 2 and len(aoutx) > 1: aoutx = signal.sosfiltfilt(sos, aoutx) return fout, aout, foutx, aoutx, fout2, aout2, fout3, aout3 ################################################## # FUNCTION [04] ################################################## def ordersignal(sig, Fs): """_summary_ Args: sig (_type_): _description_ Fs (_type_): _description_ Returns: _type_: _description_ """ F = int(Fs / 100) win = ft_window(F) if chinfile == 1: y = abs(np.fft.rfft(sig[0:F] * win)) minf = np.argmax(y) y = abs(np.fft.rfft(sig[len(sig) - F:len(sig)] * win)) maxf = np.argmax(y) else: y = abs(np.fft.rfft(sig[0, 0:F] * win)) minf = np.argmax(y) y = abs(np.fft.rfft(sig[0][len(sig[0]) - F:len(sig[0])] * win)) maxf = np.argmax(y) if maxf < minf: maxf, minf = minf, maxf sig = np.flipud(sig) return sig, minf, maxf ################################################## # FUNCTION [05] ################################################## def riaaiir(sig, Fs, mode, inv): """_summary_ Args: sig (_type_): _description_ Fs (_type_): _description_ mode (_type_): _description_ inv (_type_): _description_ Returns: _type_: _description_ """ if Fs == 96000: at = [1, -0.66168391, -0.18158841] bt = [0.1254979638905360, 0.0458786797031512, 0.0018820452752401] ars = [1, -0.60450091, -0.39094593] brs = [0.90861261463964900, -0.52293147388301200, -0.34491369168550900] if inv == 1: at, bt = bt, at ars, brs = brs, ars if mode == 1: sig = signal.lfilter(brs, ars, sig) if mode == 2: sig = signal.lfilter(bt, at, sig) if mode == 3: sig = signal.lfilter(bt, at, sig) sig = signal.lfilter(brs, ars, sig) return sig ################################################## # FUNCTION [06] ################################################## def openaudio(_FILE): """_summary_ Args: _FILE (_type_): _description_ Returns: _type_: _description_ """ global chinfile global FILE_OPEN_IDX chinfile = 1 srinfile = librosa.get_samplerate(_FILE) audio, Fs = librosa.load(_FILE, sr=None, mono=False) if len(audio.shape) == 2: chinfile = 2 filelength = audio.shape[1] / Fs else: filelength = audio.shape[0] / Fs print('Input File: ' + str(_FILE)) print('Sample Rate: ' + str("{:,}".format(srinfile) + 'Hz')) if Fs < 96000: print(' Resampling to 96,000Hz') audio = librosa.resample(audio, orig_sr=Fs, target_sr=96000) Fs = 96000 print('Channels: ' + str(chinfile)) print(f"Length: {filelength}s") if RIAA_MODE != 0: audio = riaaiir(audio, Fs, RIAA_MODE, RIAA_INV) audio, index = librosa.effects.trim(audio, top_db=TOP_DB_VAR, frame_length=FRAME_LENGTH_VAR, hop_length=HOP_LENGTH_VAR) print(f"In/Out (s): {index / Fs}") audio, minf, maxf = ordersignal(audio, Fs) print('Min Freq: ' + str("{:,}".format(minf * 100) + 'Hz')) print('Max Freq: ' + str("{:,}".format(maxf * 100) + 'Hz\n')) FILE_OPEN_IDX += 1 return audio, Fs, minf, maxf ################################################## # (MAIN) FUNCTION [07] ################################################## def main(): """ MAIN: CALL AND ORCHESTRATE ALL FUNCTIONS""" # CALL FNC1: Open and convert the wav file using librosa input_sig, Fs, minf, maxf = openaudio(FILE_0) # CALL FNC3: plot the the librosa data fo0, ao0, fox0, aox0, fo2h0, ao2h0, fo3h0, ao3h0 = createplotdata( input_sig, Fs) # MANIPULATE RETURNED VALUES deltah0 = round((max(ao0)), ROUND_LVL) deltal0 = abs(round((min(ao0)), ROUND_LVL)) if aox0.size > 0: idx_fox0 = find_nearest(fox0, 1000) print('X-talk @1kHz: ' + (str(round(aox0[idx_fox0], 2))) + 'dB\n\n') if FILE_1: input_sig, Fs, minf, maxf = openaudio(FILE_1) fo1, ao1, fox1, aox1, fo2h1, ao2h1, fo3h1, ao3h1 = createplotdata( input_sig, Fs) deltah1 = round((max(ao1)), ROUND_LVL) deltal1 = abs(round((min(ao1)), ROUND_LVL)) if aox1.size > 0: idx_fox1 = find_nearest(fox1, 1000) print('X-talk @1kHz: ' + (str(round(aox1[idx_fox1], 2))) + 'dB\n\n') if PLOT_DATA_OUT == 1: dao0 = [*ao0, *[''] * (len(fo0) - len(ao0))] daox0 = [*aox0, *[''] * (len(fo0) - len(aox0))] dao2h0 = [*ao2h0, *[''] * (len(fo0) - len(ao2h0))] dao3h0 = [*ao3h0, *[''] * (len(fo0) - len(ao3h0))] print('\n\nFile 0 Plot Data: (freq, ampl, x-talk, 2h, 3h)\n\n') dataout = list(zip(fo0, dao0, daox0, dao2h0, dao3h0)) for fo, ao, aox, ao2, ao3 in dataout: print(fo, ao, aox, ao2, ao3, sep=', ') if FILE_1: dao1 = [*ao1, *[''] * (len(fo1) - len(ao1))] daox1 = [*aox1, *[''] * (len(fo1) - len(aox1))] dao2h1 = [*ao2h1, *[''] * (len(fo1) - len(ao2h1))] dao3h1 = [*ao3h1, *[''] * (len(fo1) - len(ao3h1))] print('\n\nFile 1 Plot Data: (freq, ampl, x-talk, 2h, 3h)\n\n') dataout = list(zip(fo1, dao1, daox1, dao2h1, dao3h1)) for fo, ao, aox, ao2, ao3 in dataout: print(fo, ao, aox, ao2, ao3, sep=', ') plt.rcParams["xtick.minor.visible"] = True plt.rcParams["ytick.minor.visible"] = True if PLOT_STYLE == 1: fig, ax1 = plt.subplots(1, 1, figsize=(14, 6)) ax1.semilogx(fo0, ao0, color='#0000ff', label='Freq Response') ax1.semilogx(fo2h0, ao2h0, color='#0080ff', label='2ⁿᵈ Harmonic', alpha=1, linewidth=0.75) ax1.semilogx(fo3h0, ao3h0, color='#00dfff', label='3ʳᵈ Harmonic', alpha=1, linewidth=0.75) ax1.semilogx(fox0, aox0, color='#0000ff', linestyle=(0, (3, 1, 1, 1)), label='Crosstalk') if FILE_1: ax1.semilogx(fo1, ao1, color='#ff0000', label='Freq Response') ax1.semilogx(fo2h1, ao2h1, color='#ff8000', label='2ⁿᵈ Harmonic', alpha=1, linewidth=0.75) ax1.semilogx(fo3h1, ao3h1, color='#ffdf00', label='3ʳᵈ Harmonic', alpha=1, linewidth=0.75) ax1.semilogx(fox1, aox1, color='#ff0000', linestyle=(0, (3, 1, 1, 1)), label='Crosstalk') plt.legend( [("#0000ff", "-", "#ff0000", "-"), ("#0000ff", (0, (3, 1, 1, 1)), "#ff0000", (0, (3, 1, 1, 1))), ("#0080ff", "-", "#ff8000", "-"), ("#00dfff", "-", "#ffdf00", "-")], ['Freq Response', 'Crosstalk', '2ⁿᵈ Harmonic', '3ʳᵈ Harmonic'], handler_map={tuple: AnyObjectHandler()}, loc=4) ax1.set_ylim((min(chain(aox0, aox1)) - 2), (max(chain(ao0, ao1)) + 2)) else: plt.legend(loc=4) ax1.set_ylabel("Amplitude (dB)") ax1.set_xlabel("Frequency (Hz)") plt.autoscale(enable=True, axis='y') if OV_DYN_LIMIT == 1: ax1.set_ylim(*OV_DYN_LIMIT_VALUE) if PLOT_STYLE == 2: fig, ax1 = plt.subplots(1, 1, figsize=(14, 6)) ax2 = ax1.twinx() if max(ao0) < 7: ax1.set_ylim(-25, 7) if max(ao0) < 4: ax1.set_ylim(-25, 5) if max(ao0) < 2: ax1.set_ylim(-29, 3) if max(ao0) < 0.5: ax1.set_ylim(-30, 2) if aox0.size > 0: if FILE_1: ax1.set_ylim((min(chain(aox0, aox1)) - 2), (max(chain(ao0, ao1)) + 2)) else: ax1.set_ylim((min(aox0) - 2), (max(ao0) + 2)) if OV_DYN_LIMIT == 1: ax1.set_ylim(*OV_DYN_LIMIT_VALUE) ax1.semilogx(fo0, ao0, color='#0000ff', label='Freq Response') ax2.semilogx(fo2h0, ao2h0, color='#0080ff', label='2ⁿᵈ Harmonic', alpha=1, linewidth=0.75) ax2.semilogx(fo3h0, ao3h0, color='#00dfff', label='3ʳᵈ Harmonic', alpha=1, linewidth=0.75) ax1.semilogx(fox0, aox0, color='#0000ff', linestyle=(0, (3, 1, 1, 1)), label='Crosstalk') if FILE_1: ax1.semilogx(fo1, ao1, color='#ff0000', label='Freq Response') ax2.semilogx(fo2h1, ao2h1, color='#ff8000', label='2ⁿᵈ Harmonic', alpha=1, linewidth=0.75) ax2.semilogx(fo3h1, ao3h1, color='#ffdf00', label='3ʳᵈ Harmonic', alpha=1, linewidth=0.75) ax1.semilogx(fox1, aox1, color='#ff0000', linestyle=(0, (3, 1, 1, 1)), label='Crosstalk') plt.legend( [("#0000ff", "-", "#ff0000", "-"), ("#0000ff", (0, (3, 1, 1, 1)), "#ff0000", (0, (3, 1, 1, 1))), ("#0080ff", "-", "#ff8000", "-"), ("#00dfff", "-", "#ffdf00", "-")], ['Freq Response', 'Crosstalk', '2ⁿᵈ Harmonic', '3ʳᵈ Harmonic'], handler_map={tuple: AnyObjectHandler()}, loc=4) ax1.set_ylim((min(chain(aox0, aox1)) - 2), (max(chain(ao0, ao1)) + 2)) else: lines1, labels1 = ax1.get_legend_handles_labels() lines2, labels2 = ax2.get_legend_handles_labels() plt.legend(lines1 + lines2, labels1 + labels2, loc=4) new_lim1, new_lim2 = align_yaxis(ax1, ax2) ax1.set_ylim(new_lim1) ax2.set_ylim(new_lim2) ax1.set_ylabel("Amplitude (dB)") ax2.set_ylabel("Distortion (dB)") ax1.set_xlabel("Frequency (Hz)") if PLOT_STYLE == 3: fig, (ax1, ax2) = plt.subplots(2, 1, sharex=True, figsize=(14, 6)) ax2.grid(True, which="major", axis="both", ls="-", color="black") ax2.grid(True, which="minor", axis="both", ls="-", color="gainsboro") ax1.set_ylim(-5, 5) if FILE_1: if (min(chain(ao0, ao1)) < -5) or (max(chain(ao0, ao1)) > 5): ax1.autoscale(enable=True, axis='y') elif (min(ao0) < -5) or (max(ao0) > 5): ax1.autoscale(enable=True, axis='y') if OV_DYN_LIMIT == 1: ax1.set_ylim(*OV_DYN_LIMIT_VALUE) ax1.semilogx(fo0, ao0, color='#0000ff', label='Freq Response') ax2.semilogx(fo2h0, ao2h0, color='#0080ff', label='2nd Harmonic') ax2.semilogx(fo3h0, ao3h0, color='#00dfff', label='3rd Harmonic') if FILE_1: ax1.semilogx(fo1, ao1, color='#ff0000', label='Freq Response') ax2.semilogx(fo2h1, ao2h1, color='#ff8000', label='2ⁿᵈ Harmonic') ax2.semilogx(fo3h1, ao3h1, color='#ffdf00', label='3ʳᵈ Harmonic') ax1.legend([ ("#0000ff", "-", "#ff0000", "-"), ], ['Freq Response'], handler_map={tuple: AnyObjectHandler()}, loc=4) ax2.legend([("#0080ff", "-", "#ff8000", "-"), ("#00dfff", "-", "#ffdf00", "-")], ['2ⁿᵈ Harmonic', '3ʳᵈ Harmonic'], handler_map={tuple: AnyObjectHandler()}, loc=4) else: ax1.legend(loc=4) ax2.legend(loc=4) ax1.set_ylabel("Amplitude (dB)") ax2.set_ylabel("Distortion (dB)") ax2.set_xlabel("Frequency (Hz)") ax1.grid(True, which="major", axis="both", ls="-", color="black") ax1.grid(True, which="minor", axis="both", ls="-", color="gainsboro") bbox_args = dict(boxstyle="round", color='b', fc='w', ec='b', alpha=1, pad=.15) ax1.annotate('+' + str(deltah0) + ', ' + u"\u2212" + str(deltal0) + ' dB',color = 'b',\ xy=(fo0[0],(ao0[0]-1)), xycoords='data', \ xytext=(-10, -20), textcoords='offset points', \ ha="left", va="center", bbox=bbox_args) if FILE_1: bbox_args = dict(boxstyle="round", color='b', fc='w', ec='r', alpha=1, pad=.15) ax1.annotate('+' + str(deltah1) + ', ' + u"\u2212" + str(deltal1) + ' dB',color = 'r',\ xy=(fo0[0],(ao0[0]-1)), xycoords='data', \ xytext=(-10, -34.5), textcoords='offset points', \ ha="left", va="center", bbox=bbox_args) ax1.set_xticks( [0, 20, 50, 100, 500, 1000, 5000, 10000, 20000, 50000, 100000]) ax1.set_xticklabels([ '0', '20', '50', '100', '500', '1k', '5k', '10k', '20k', '50k', '100k' ]) plt.autoscale(enable=True, axis='x') ax1.set_title(INFO_LINE + "\n", fontsize=16) now = datetime.now() if FILE_1: plt.figtext(.17, .118, INFO_LINE + "\n" + FILE_0 + "\n" + FILE_1 + "\n" + \ now.strftime("%b %d, %Y %H:%M"), fontsize=6) else: plt.figtext(.17, .118, INFO_LINE + "\n" + FILE_0 + "\n" + \ now.strftime("%b %d, %Y %H:%M"), fontsize=6) plt.figtext(.125, 0, EQUIP_INFO, alpha=.5, fontsize=6) plt.savefig(INFO_LINE.replace(' / ', '_') + '.png', bbox_inches='tight', pad_inches=.5, dpi=96) plt.show() print('\nDone!') ################################################## # INVOKE MAIN ################################################## if __name__ == '__main__': main()