• Welcome to ASR. There are many reviews of audio hardware and expert members to help answer your questions. Click here to have your audio equipment measured for free!

Phono Cartridge Response Measurement Script

The bumps on my CA-TRS1007 A side, is not out of phase, so not related to non-flatness or vertical bump. It is probably a pressing error, since it does not show up in stereo track.

Skärmavbild 2023-03-25 kl. 10.00.34.png
 
That’s modulated so it‘s not going show up as amplitude.
But only in 45° L direction? Perhaps you wrote is somewhere, but how large is bin size vs. f?

Skärmavbild 2023-03-25 kl. 14.10.18.png
 
Last edited:
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






Thanks for pointing med too QR2009 FR sweep, even if my record is badly treated the FR seem good.
View attachment 274554
I use a record flattener like record pi or vinyl flat.

I wonder now if some of the oscillations may have to do with insufficient tracking force.
 
But only in 45° L direction? Perhaps you wrote is somewhere, but how large is bin size vs. f?

View attachment 274614

The cycle time of the first picture is about .09s, so 11.1Hz. The second is ~.07s, so 14.3Hz.

The script is taking slices of the signal and doing an FFT on each slice, and is then taking only the single largest bin of that slice. Amplitudes for multiple bins at the same frequency are averaged. Slices for the frequencies we're plotting are:

20-45Hz - Fs/5 = 5Hz steps
50-90Hz - Fs/10 = 10Hz steps.
100-980Hz - Fs/20 = 20Hz steps.
1000Hz+ - Fs/100 - 100Hz steps.

If we take a 96kHz file and run the rfft function for fmin = 20, fmax = 45, and fstep = 5 (Fs/5), It'll take Fs/5 (19200 sample) slices of the file and collect the frequency and amplitude of the largest bin for each slice where the frequency is between 20Hz and 45Hz. This is the actual frequency data from a sweep file:

Code:
[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]

The interpolate function (bad name) then takes that frequency and amplitude data and averages all the amplitudes:

Code:
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
*The numbers in brackets [] are the position index from the preceding array.


You then have the frequency and amplitude arrays that are the plot data for the 20-45Hz range:

Code:
[25, 30, 35, 40, 45]

[732.1964401517267, 678.4332700476149, 668.9802320110238, 655.8348731536851, 670.57387650922]
*amplitude is converted to dB for the plot which I didn't do here.

As the slices for lower frequencies are so large you'll commonly see mixed frequency for the largest bin in each slice as you do above (25, 30, 25, 25, 25, 25, 30, 25, 30, 25, 30). This behavior is pretty much gone by the time you hit around 500Hz, and I've never seen it jump more than one or two frequency steps. Very rarely two.

When you're at Fs/100 it can't detect those low frequencies anyway. If you add print(freq) above the return line for the rfft function (around line 238) it'll dump the frequency arrays to the console and you can see what's being detected as the largest bins as it slices through the file.

Code:
                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
 
Last edited:
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

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)
 
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.

AT33PTG2X_SUT_STR 100.png
 
Azimuth is probably off. STR-100 is no good for checking that.
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.
 
Measurement floor on that record is -20dB, where your cartridge should be -30dB or better. You need a record with good tracks to use to know what adjustment is necessary, and the adjustment will vary per record as show in a recent post.
 
Got the script to do low shelf coefficients working. Bad news is I haven't figured out how to get the right slope and rolloff yet.


Shelf Filter Test.png
 
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 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
;-)
 
Last edited:
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
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 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?
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.
 
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)
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.

Here are my latest results (click to magnify):
Audio-Technica AT7V² - Denon DP-35F - CA¹ - 1.png
Audio-Technica AT7V² - Denon DP-35F - CA¹ - 2.png


Audio-Technica AT7V² - Denon DP-35F - CA¹ - 3.png

Important to note is that I still need to measure this on a higher-mass tonearm given the 2g tracking force. I'd like to see if there is a difference or if 2g is fine for lighter-mass tonearms, which it really should be. I've noticed the difference with 3g. Perhaps the measurement at 20Hz will tamper down a little. I'll take out my Sony PS-X50 to do this, but not for a while. I also have another one with slightly different inductance measurements that I want to check.

It's pretty much as good as the much loved and very expensive AT150MLX, but with a smaller boost in the highs. It should sound neutral with the peak tastefully making up for any hearing loss. Having seen measurements of the cartridges you mentioned, I know that--for me--this one would win all day, every day. While I would never use this for archiving (my main purpose for all of this), I still enthusiastically endorse this for normal listening.

The great thing here is that because my old CBS test record was slightly off in the right way my old measurements compare favorably and are still quite useful:
AT7V (2) · ATN7V (NOS) - Sony PS-X50 2.png


EDIT: I don't see much FR fluctuation in the highs here like with my Shure V15 V-MR. Perhaps the Shure shows it because of incorrect tracking. Given its brush and given its age, perhaps it needs a little more tracking force. I've oiled the hinge but maybe it is still sticky enough to make a small difference. Just a thought.

Shure V15 V-MR⁴ - Denon DP-35F - CA¹ - 3.png
 
Last edited:
I 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
;-)
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 :p

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 :-)
 

Attachments

Perhaps a bit premature, given John's to-do/wants list, but trying to follow along and reading to 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, JP. As he [JP] states, the real updates are tracked >here<.

#!/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()
 
Last edited:
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 :p

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 :)
I'll try to figure it all out thank you.
;-)
to be honnete with an old mm , better than -40db to 1k without this electronic stage, this one necessarily paying on other points ...
I don't really feel interested in this approach.
;-)
 
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()
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.
 
Back
Top Bottom