• WANTED: Happy members who like to discuss audio and other topics related to our interest. Desire to learn and share knowledge of science required. 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!

MM vs MI vs MC

Balle Clorin

Major Contributor
Joined
Dec 26, 2017
Messages
1,383
Likes
1,256
Here's an updated script. Also includes some rude scaling logic and config to override and set a custom ylim. I haven't tested this version on Windows so there'll likely be a couple compatibility issues with the file attribute handling.

Code:
"""
PLEASE READ
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. Frequencies below 1kHz are
ignored since virually all cartridges are well behaved below 1kHz.
Python does not read 24bit packed .wav's 16 bit is more than enough here.

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"

plotstyle =

                1 - traditional
                2 - dual axis (twinx)
                3 - dual plot



VERSION 6BClean


"""

from scipy import signal
from scipy.io.wavfile import read
from pathlib import Path
import matplotlib.pyplot as plt
import numpy as np
import datetime
import os




#edit here to add a HOME directory, etc.

#HOME = '/Users/jjones/Documents/polar/' #THIS IS NOT NEEDED IF RAN FROM A FILE

_FILE = 'v15v-str100.wav'

infoline =  'V15VMR / 47k 350pF / STR-100'


roundlvl = 2
plotstyle = 2
plotdataout = 0


ovdylim = 0
ovdylimvalue = [-25,5]

#end Edit

print('Input File:   ' + str(_FILE))
print('Info Line:    ' + str(infoline))


def align_yaxis(ax1, ax2):
    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)

    # normalize both axes
    y_mags = (y_lims[:,1] - y_lims[:,0]).reshape(len(y_lims),1)
    y_lims_normalized = y_lims / y_mags

    # find combined range
    y_new_lims_normalized = np.array([np.min(y_lims_normalized), np.max(y_lims_normalized)])

    # denormalize combined range to get new axes
    new_lim1, new_lim2 = y_new_lims_normalized * y_mags
    ax1.set_ylim(new_lim1)
    ax2.set_ylim(new_lim2)



#Needed to steal Matlab's flat top window

def ft_window(n):
    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

try:
    input = raw_input
except NameError:
    pass


y = read(_FILE)
Fs = int(y[0])
input_sig = y[1]


filelength = input_sig.shape[0] / Fs
print(f"Length:       {filelength}s")
print('Sample Rate:  ' + str("{:,}".format(Fs) + 'Hz'))




frequency = []
amplitude = []

frequency2h = []
amplitude2h = []

frequency3h = []
amplitude3h = []

freqout = []
ampout = []

freqout2h = []
ampout2h = []

freqout3h = []
ampout3h = []

F = int(Fs/100)
win = ft_window(F)

#For Fs sampling use small Fs/100 FFT's so frequency is f/100
#All common sampling rates are divisible by 100
#First try, no overlap and flat top window

#Find the minimum and maximum frequencies and flip the data if the sweep
#is going down in frequency.

y = abs(np.fft.rfft(input_sig[0:F]*win))
minf = np.argmax(y)

y = abs(np.fft.rfft(input_sig[len(input_sig)-F:len(input_sig)]*win))
maxf = np.argmax(y)

if maxf < minf:
    maxf,minf = minf,maxf
    input_sig = np.flipud(input_sig)



for x in range(0,len(input_sig)-F,F):
    y = abs(np.fft.rfft(input_sig[x:x+F]*win))
    f = np.argmax(y) #use largest bin
    if f >=10:
        frequency.append(f*100)
        amplitude.append(y[f])
    if 2*f<F/2-2 and f >= 10:
        f2 = np.argmax(y[(2*f)-2:(2*f)+2])
        frequency2h.append(f*100)
        amplitude2h.append(y[2*f-2+f2])
    if 3*f<F/2-2 and f >= 10:
        f3 = np.argmax(y[(3*f)-2:(3*f)+2])
        frequency3h.append(f*100)
        amplitude3h.append(y[3*f-2+f3])
 
amp = 0
count = 0
x = minf*100

for x in range(minf*100,(maxf+1)*100,100):
    for y in range(0,len(frequency)):
        if frequency[y] == x:
            amp = amp + amplitude[y]
            count = count + 1
    if count != 0:
        freqout.append(x)
        ampout.append(20*np.log10(amp/count))
    amp = 0
    count = 0

amp = 0
count = 0
x = minf*100

for x in range(minf*100,(maxf+1)*100,100):
    for y in range(0,len(frequency2h)):
        if frequency2h[y] == x:
            amp = amp + amplitude2h[y]
            count = count + 1
    if count != 0:
        freqout2h.append(x)
        ampout2h.append(20*np.log10(amp/count))
    amp = 0
    count = 0
 

amp = 0
count = 0
x = minf*100

for x in range(minf*100,(maxf+1)*100,100):
    for y in range(0,len(frequency3h)):
        if frequency3h[y] == x:
            amp = amp + amplitude3h[y]
            count = count + 1
    if count != 0:
        freqout3h.append(x)
        ampout3h.append(20*np.log10(amp/count))
    amp = 0
    count = 0


b, a = signal.iirfilter(3,.5, btype='lowpass') #filter some noise
ampout = signal.filtfilt(b,a,ampout)
ampout2h = signal.filtfilt(b,a,ampout2h)
ampout3h = signal.filtfilt(b,a,ampout3h)



norm = ampout[0]
ampout = ampout-norm #amplitude is in dB so normalize by subtraction at [0]
ampout2h = ampout2h-norm
ampout3h = ampout3h-norm


deltah = round((max(ampout)), roundlvl)
deltal = abs(round((min(ampout)), roundlvl))


print('Min Freq:     ' + str("{:,}".format(minf * 100) + 'Hz'))
print('Max Freq:     ' + str("{:,}".format(maxf * 100) + 'Hz'))

print('Min Ampl:     ' + str(round((min(ampout)), 4)) + 'dB')
print('Max Ampl:     ' + str(round((max(ampout)), 4)) + 'dB')
print('Delta:        ' + str(round((max(ampout) - min(ampout)), 4)) + 'dB')

print('Frequency:    ' + str(len(freqout)) + ' list elements')
print('Amplitude:    ' + str(len(ampout)) + ' list elements')
print('Amplitude 2h: ' + str(len(ampout2h)) + ' list elements')
print('Amplitude 3h: ' + str(len(ampout3h)) + ' list elements')


if plotdataout == 1:

    dampout = [*ampout, *[''] * (len(freqout) - len(ampout))]
    dampout2h = [*ampout2h, *[''] * (len(freqout) - len(ampout2h))]
    dampout3h = [*ampout3h, *[''] * (len(freqout) - len(ampout3h))]

    print('\nPlot Data: (freq, ampl, 2h, 3h)\n')

    dataout = list(zip(freqout, dampout, dampout2h, dampout3h))
    for fo, ao, ao2, ao3 in dataout:
        print(fo, ao, ao2, ao3, sep=', ')



plt.rcParams["xtick.minor.visible"] =  True
plt.rcParams["ytick.minor.visible"] =  True

if plotstyle == 1:
        fig, ax1 = plt.subplots(1, 1, figsize=(14,6))
 
        ax1.semilogx(freqout,ampout,color = 'b', label = 'Freq Response')
        ax1.semilogx(freqout2h,ampout2h,color = 'g', label = '2nd Harmonic')
        ax1.semilogx(freqout3h,ampout3h,color = 'r', label = '3rd Harmonic')

        ax1.set_ylabel("Amplitude (dB)")
        ax1.set_xlabel("Frequency (Hz)")

        ax1.legend(loc=4)
 

if plotstyle == 2:
        fig, ax1 = plt.subplots(1, 1, figsize=(14,6))
        ax2 = ax1.twinx()

        if max(ampout) <7:
            ax1.set_ylim(-25, 7)

        if max(ampout) < 4:
            ax1.set_ylim(-25,5)
  
        if max(ampout) < 2:
            ax1.set_ylim(-29,3)

        if max(ampout) < 0.5:
            ax1.set_ylim(-30,2)

        if ovdylim == 1:
            ax1.set_ylim(*ovdylimvalue)
 
        ax1.semilogx(freqout,ampout,color = 'b', label = 'Freq Response')
        ax2.semilogx(freqout2h,ampout2h,color = 'g', label = '2nd Harmonic')
        ax2.semilogx(freqout3h,ampout3h,color = 'r', label = '3rd Harmonic')

        align_yaxis(ax1, ax2)

        ax1.set_ylabel("Amplitude (dB)")
        ax2.set_ylabel("Distortion (dB)")
        ax1.set_xlabel("Frequency (Hz)")

        fig.legend(loc=4, bbox_to_anchor=(0.899, 0.11))
 

if plotstyle == 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")

        if max(ampout) <= 1.75 and min(ampout) >= -1.75:
                ax1.set_ylim(-2,2)

        ax1.semilogx(freqout,ampout,color = 'b', label = 'Freq Response')
        ax2.semilogx(freqout2h,ampout2h,color = 'g', label = '2nd Harmonic')
        ax2.semilogx(freqout3h,ampout3h,color = 'r', label = '3rd Harmonic')

        ax1.set_ylabel("Amplitude (dB)")
        ax2.set_ylabel("Distortion (dB)")
        ax2.set_xlabel("Frequency (Hz)")

        ax1.legend(loc=4)
        ax2.legend(loc=4)
 

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)
arrow_args = dict(arrowstyle="->")

ax1.annotate('+' + str(deltah) + ', ' + u"\u2212" + str(deltal) + ' dB', color='b', \
             xy=(1000,0), xycoords='data', \
             xytext=(-15, -20), textcoords='offset points', \
             ha="left", va="center", \
             bbox=bbox_args, \
             #arrowprops=arrow_args \
             )


 
ax1.set_xticks([1000,10000])
ax1.set_xticklabels(['1k','10k'])

ax1.set_title(infoline + "\n", fontsize=16)


mod_date = datetime.datetime.fromtimestamp(os.path.getmtime(_FILE))

plt.figtext(.17, .13, infoline + "\n" + _FILE + "\n" + \
            mod_date.strftime("%b %d, %Y %H:%M:%S"), fontsize=8)
  
plt.savefig(infoline.replace(' / ', '_') +'.png', bbox_inches='tight', pad_inches=.75, dpi=96)

plt.show()

print('\nDone!')
Trying to run your frequency response plotting script, I something wrong with my file directory, file location here?
1671787978259.png



Moved my Wav file to the directory where Python is installed and now I get this error?
1671788457491.png



The sweep file is 50 seconds
1671788556038.png


Cut off the last millisec part of the file with low signal and Voila!
1671788840540.png


'

'
1671789964907.png


No Ju just need to find out how to plot the whole frequency range and a script for the Wow&Flutter polar plot

Comparing plots by overlay
1671790299294.png
 

Attachments

  • 1671789638634.png
    1671789638634.png
    32 KB · Views: 35
Last edited:

Balle Clorin

Major Contributor
Joined
Dec 26, 2017
Messages
1,383
Likes
1,256
@JP , this "HOME" thing is drving my crazy I cannot get the file or folder path to work , How can I fix this?
This is the Polar RPM plot script
1671797813837.png
 
Last edited:

JP

Major Contributor
Joined
Jul 4, 2018
Messages
2,332
Likes
2,507
Location
Brookfield, CT
You're concatenating strings. The way the data is entered the result is
Code:
/Users/ersol/Documents/data/HiFi/Michell/RPM WFWFtestwav.wav

You need a trailing "/" on HOME.

For FR, on or around line 161 is where you can modify what F it starts plotting

Code:
for x in range(0,len(input_sig)-F,F):
    y = abs(np.fft.rfft(input_sig[x:x+F]*win))
    f = np.argmax(y) #use largest bin
    if f >=10:                       #!!!!!!!!!!!!!!!!!!!! 10 = 1kHz.  Change to 0 to start in the basement.                       
        frequency.append(f*100)
        amplitude.append(y[f])

Due to the filters the best it can do is 200Hz (not in this version), however as the bins are 100Hz it's really not useful that low. The consensus was that non-broken cartridges have unremarkable response below 1k, and below ~30Hz response really isn't up the cartridge alone, so it wasn't useful to plot below 1k for the intended purpose.

I've played around a little bit with improving that part, but note that while I'm playing caretaker, Scott Wurcer is the original author.
 

JP

Major Contributor
Joined
Jul 4, 2018
Messages
2,332
Likes
2,507
Location
Brookfield, CT
This is an updated version of the FR script that trims the "silence" from the sweep automatically. Requires librosa.

Code:
"""
PLEASE READ
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. Frequencies below 1kHz are
ignored since virually all cartridges are well behaved below 1kHz.

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"

plotstyle =

                1 - traditional
                2 - dual axis (twinx)
                3 - dual plot


VERSION 8C

"""

from scipy import signal
from scipy.io.wavfile import read
from pathlib import Path
import matplotlib.pyplot as plt
import numpy as np
import datetime
import os
import librosa



#edit here to add a HOME directory, etc.

#HOME = '/Users/user/Documents/' #THIS IS NOT NEEDED IF RAN FROM A FILE

_FILE = 'wavefile.wav'

infoline =  'Cart / Load / Record'


roundlvl = 2
plotstyle = 2
plotdataout = 0

ovdylim = 0
ovdylimvalue = [-25,5]

topdb = 20
framelength = 512
hoplength = 128

#end edit



print('Input File:   ' + str(_FILE))
print('Info Line:    ' + str(infoline))


def align_yaxis(ax1, ax2):
    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)

    # normalize both axes
    y_mags = (y_lims[:,1] - y_lims[:,0]).reshape(len(y_lims),1)
    y_lims_normalized = y_lims / y_mags

    # find combined range
    y_new_lims_normalized = np.array([np.min(y_lims_normalized), np.max(y_lims_normalized)])

    # denormalize combined range to get new axes
    new_lim1, new_lim2 = y_new_lims_normalized * y_mags
    ax1.set_ylim(new_lim1)
    ax2.set_ylim(new_lim2)
   

#Needed to steal Matlab's flat top window

def ft_window(n):
    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


audio, Fs = librosa.load(_FILE, sr=None)


filelength = audio.shape[0] / Fs
print('Sample Rate:  ' + str("{:,}".format(Fs) + 'Hz'))
print(f"Length:       {filelength}s")


trimmedaudio, index = librosa.effects.trim(audio, top_db=topdb, frame_length=framelength, hop_length=hoplength)
input_sig = (trimmedaudio* 32767).astype(int) #scale samples and convert to int

print(f"In/Out (s):   {index / Fs}")


frequency = []
amplitude = []

frequency2h = []
amplitude2h = []

frequency3h = []
amplitude3h = []

freqout = []
ampout = []

freqout2h = []
ampout2h = []

freqout3h = []
ampout3h = []

F = int(Fs/100)
win = ft_window(F)

#For Fs sampling use small Fs/100 FFT's so frequency is f/100
#All common sampling rates are divisible by 100
#First try, no overlap and flat top window

#Find the minimum and maximum frequencies and flip the data if the sweep
#is going down in frequency.

y = abs(np.fft.rfft(input_sig[0:F]*win))
minf = np.argmax(y)

y = abs(np.fft.rfft(input_sig[len(input_sig)-F:len(input_sig)]*win))
maxf = np.argmax(y)

if maxf < minf:
    maxf,minf = minf,maxf
    input_sig = np.flipud(input_sig)



for x in range(0,len(input_sig)-F,F):
    y = abs(np.fft.rfft(input_sig[x:x+F]*win))
    f = np.argmax(y) #use largest bin
    if f >=10:
        frequency.append(f*100)
        amplitude.append(y[f])
    if 2*f<F/2-2 and f >= 10:
        f2 = np.argmax(y[(2*f)-2:(2*f)+2])
        frequency2h.append(f*100)
        amplitude2h.append(y[2*f-2+f2])
    if 3*f<F/2-2 and f >= 10:
        f3 = np.argmax(y[(3*f)-2:(3*f)+2])
        frequency3h.append(f*100)
        amplitude3h.append(y[3*f-2+f3])
   
amp = 0
count = 0
x = minf*100

for x in range(minf*100,(maxf+1)*100,100):
    for y in range(0,len(frequency)):
        if frequency[y] == x:
            amp = amp + amplitude[y]
            count = count + 1
    if count != 0:
        freqout.append(x)
        ampout.append(20*np.log10(amp/count))
    amp = 0
    count = 0

amp = 0
count = 0
x = minf*100

for x in range(minf*100,(maxf+1)*100,100):
    for y in range(0,len(frequency2h)):
        if frequency2h[y] == x:
            amp = amp + amplitude2h[y]
            count = count + 1
    if count != 0:
        freqout2h.append(x)
        ampout2h.append(20*np.log10(amp/count))
    amp = 0
    count = 0
      

amp = 0
count = 0
x = minf*100

for x in range(minf*100,(maxf+1)*100,100):
    for y in range(0,len(frequency3h)):
        if frequency3h[y] == x:
            amp = amp + amplitude3h[y]
            count = count + 1
    if count != 0:
        freqout3h.append(x)
        ampout3h.append(20*np.log10(amp/count))
    amp = 0
    count = 0


b, a = signal.iirfilter(3,.5, btype='lowpass') #filter some noise
ampout = signal.filtfilt(b,a,ampout)
ampout2h = signal.filtfilt(b,a,ampout2h)
ampout3h = signal.filtfilt(b,a,ampout3h)


norm = ampout[0]
ampout = ampout-norm #amplitude is in dB so normalize by subtraction at [0]
ampout2h = ampout2h-norm
ampout3h = ampout3h-norm


deltah = round((max(ampout)), roundlvl)
deltal = abs(round((min(ampout)), roundlvl))


print('Min Freq:     ' + str("{:,}".format(minf * 100) + 'Hz'))
print('Max Freq:     ' + str("{:,}".format(maxf * 100) + 'Hz'))

print('Min Ampl:     ' + str(round((min(ampout)), 4)) + 'dB')
print('Max Ampl:     ' + str(round((max(ampout)), 4)) + 'dB')
print('Delta:        ' + str(round((max(ampout) - min(ampout)), 4)) + 'dB')

print('Frequency:    ' + str(len(freqout)) + ' list elements')
print('Amplitude:    ' + str(len(ampout)) + ' list elements')
print('Amplitude 2h: ' + str(len(ampout2h)) + ' list elements')
print('Amplitude 3h: ' + str(len(ampout3h)) + ' list elements')


if plotdataout == 1:

    dampout = [*ampout, *[''] * (len(freqout) - len(ampout))]
    dampout2h = [*ampout2h, *[''] * (len(freqout) - len(ampout2h))]
    dampout3h = [*ampout3h, *[''] * (len(freqout) - len(ampout3h))]

    print('\nPlot Data: (freq, ampl, 2h, 3h)\n')

    dataout = list(zip(freqout, dampout, dampout2h, dampout3h))
    for fo, ao, ao2, ao3 in dataout:
        print(fo, ao, ao2, ao3, sep=', ')


plt.rcParams["xtick.minor.visible"] =  True
plt.rcParams["ytick.minor.visible"] =  True

if plotstyle == 1:
        fig, ax1 = plt.subplots(1, 1, figsize=(14,6))
       
        ax1.semilogx(freqout,ampout,color = 'b', label = 'Freq Response')
        ax1.semilogx(freqout2h,ampout2h,color = 'g', label = '2nd Harmonic')
        ax1.semilogx(freqout3h,ampout3h,color = 'r', label = '3rd Harmonic')

        ax1.set_ylabel("Amplitude (dB)")
        ax1.set_xlabel("Frequency (Hz)")

        ax1.legend(loc=4)
       

if plotstyle == 2:
        fig, ax1 = plt.subplots(1, 1, figsize=(14,6))
        ax2 = ax1.twinx()

        if max(ampout) <7:
            ax1.set_ylim(-25, 7)

        if max(ampout) < 4:
            ax1.set_ylim(-25,5)
           
        if max(ampout) < 2:
            ax1.set_ylim(-29,3)

        if max(ampout) < 0.5:
            ax1.set_ylim(-30,2)

        if ovdylim == 1:
            ax1.set_ylim(*ovdylimvalue)
        
        ax1.semilogx(freqout,ampout,color = 'b', label = 'Freq Response')
        ax2.semilogx(freqout2h,ampout2h,color = 'g', label = '2nd Harmonic')
        ax2.semilogx(freqout3h,ampout3h,color = 'r', label = '3rd Harmonic')

        align_yaxis(ax1, ax2)

        ax1.set_ylabel("Amplitude (dB)")
        ax2.set_ylabel("Distortion (dB)")
        ax1.set_xlabel("Frequency (Hz)")

        fig.legend(loc=4, bbox_to_anchor=(0.899, 0.11))
       

if plotstyle == 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")

        if max(ampout) <= 1.75 and min(ampout) >= -1.75:
                ax1.set_ylim(-2,2)

        ax1.semilogx(freqout,ampout,color = 'b', label = 'Freq Response')
        ax2.semilogx(freqout2h,ampout2h,color = 'g', label = '2nd Harmonic')
        ax2.semilogx(freqout3h,ampout3h,color = 'r', label = '3rd Harmonic')

        ax1.set_ylabel("Amplitude (dB)")
        ax2.set_ylabel("Distortion (dB)")
        ax2.set_xlabel("Frequency (Hz)")

        ax1.legend(loc=4)
        ax2.legend(loc=4)
      

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)
arrow_args = dict(arrowstyle="->")

ax1.annotate('+' + str(deltah) + ', ' + u"\u2212" + str(deltal) + ' dB', color='b', \
             xy=(1000,0), xycoords='data', \
             xytext=(-15, -20), textcoords='offset points', \
             ha="left", va="center", \
             bbox=bbox_args, \
             #arrowprops=arrow_args \
             )

 
ax1.set_xticks([1000,10000])
ax1.set_xticklabels(['1k','10k'])

ax1.set_title(infoline + "\n", fontsize=16)

mod_date = datetime.datetime.fromtimestamp(os.path.getmtime(_FILE))

plt.figtext(.17, .13, infoline + "\n" + _FILE + "\n" + \
            mod_date.strftime("%b %d, %Y %H:%M:%S"), fontsize=8)
           
plt.savefig(infoline.replace(' / ', '_') +'.png', bbox_inches='tight', pad_inches=.75, dpi=96)

plt.show()

print('\nDone!')
 

Balle Clorin

Major Contributor
Joined
Dec 26, 2017
Messages
1,383
Likes
1,256
I apologize for beeing so stupid but what is wrong with the Polar plow W&flutter script here
1672039838567.png


Googles the error , but I cannot get is correct
1672040013928.png


Putting the file directly without the HOME spesification does not help The file is ther but script refuses to see it, grateful for any help
1672040431705.png



In desperation I put the file in HOME statement, and it ran, but it is not not the intention of Home is it?
1672041262506.png




Clearaudio TRS-1007
1672041617277.png

1672042298753.png


Maybe my best is Bruel&Kjær, a bit off center but conistent

1672043595853.png



'But NONE of these plots are valid evaluation of the test record quality. Since my yable is belt driven and sometimes moody and not A Technict DD that keep the RPM and W&F the same every day and run,

A different run of Clearaudio TRS-1007
1672044185363.png


Going back to the first file the HOME /FILe problem is back, I am baffled!!! Whta is wrong with me??
1672055739657.png
 
Last edited:

JP

Major Contributor
Joined
Jul 4, 2018
Messages
2,332
Likes
2,507
Location
Brookfield, CT
I think the _FILE = input method isn't reliable - you're running an old version of the script. You can add print(HOME + _FILE) above y = read(HOME + _FILE) to see if the concatenated string is correct. I'm guessing it's not.

Changing _FILE = input('FILENAME.EXT') to _FILE = 'FILENAME.EXT' should solve it.

I'm on a Mac and use IDLE with the audio files and scripts in the same directory so I don't use the HOME statements.

Latest WF code:

Code:
#Version 5C

from scipy import signal
from scipy.io.wavfile import read
import matplotlib.pyplot as plt
import numpy as np
import datetime
import os



#edit here to add a HOME directory, etc.

HOME = '/Users/jjones/Documents/polar/'

_FILE = 'FM_WF_SONY.wav'

info_line = 'WF_SONY'

Hz_per_tick = '3'
sec_per_rev = '1.8'  # 1.8 for 33, 1.33 for 45, .766 for 78.26

stereo_channel = '0'    #0 = Left, 1 = Right
filter_freq = '60'

#end edit



stereo_channel = int(stereo_channel)
filter_freq = float(filter_freq)
sec_per_rev = float(sec_per_rev)
Hz_per_tick = float(Hz_per_tick)


def instfreq(sig,Fs,filter_freq):
    z = signal.hilbert(sig)
    rawfreq = Fs/(2*np.pi)*np.diff(np.unwrap(np.angle(z)))
    rawfreq = np.append(rawfreq,rawfreq[len(rawfreq)-1])    #np.diff drops one end point

    b, a = signal.iirfilter(1,filter_freq/(Fs/2), btype='lowpass')
    zi = signal.lfilter_zi(b, a) #Initialize the filter to the mean of the leading edge of the data
    rawfreq,_ = signal.lfilter(b,a,rawfreq,zi=zi*np.mean(rawfreq[0:2000])) #reduces glitch, first pass

    b, a = signal.iirfilter(3,filter_freq/(Fs/2), btype='lowpass')
    instfreq = signal.filtfilt(b,a,rawfreq) #back and forth linear phase IIR filter (6 pole)

    return (instfreq)


y = read(HOME + _FILE)
Fs = float(y[0])
if np.size(y[1][0]) == 2:
    sig = y[1][:,stereo_channel][0:int(Fs*(sec_per_rev*3))] #Grab 3*sec_per_rev of audio from the specified channel
else:
    sig = y[1][0:int(Fs*(sec_per_rev*3))] #mono file

t = np.arange(sec_per_rev,0,-1/Fs)  #Reverse time (theta axis)
theta = t*2*np.pi/sec_per_rev   #Time becomes degrees (1 rev = 2pi radians)
theta = np.roll(theta,int((sec_per_rev*Fs/4)))  #Rotate 90 deg to put 0 on top (sec_per_rev*Fs/4)

freq1 = instfreq(sig,Fs,filter_freq)

freq1 = np.roll(freq1,-int(Fs*.2))#Throw away the first .2sec to guarantee the IIR transient settles
if1 = freq1[0:int(Fs*sec_per_rev)]
if2 = freq1[int(Fs*sec_per_rev):int(2*Fs*sec_per_rev)]

mm = np.mean(if1)
mm2 = np.mean(if2)

for x in range(0,len(if1)):
    if if1[x] > mm+10:
        if1[x] = mm+10
    if if1[x] < mm-10:
        if1[x] = mm-10
for x in range(0,len(if2)):
    if if2[x] > mm2+10:
        if2[x] = mm2+10
    if if2[x] < mm2-10:
        if2[x] = mm2-10


maxf = (max(max(if1),max(if2))+.2) #This shuld be changed to make the maximum frequency an even 10th of a Hz.

r1 = 20.-(maxf-if1)/Hz_per_tick  #20 radial ticks at Hz_per_tick is fixed, adaptive scaling
r2 = 20.-(maxf-if2)/Hz_per_tick  #is an exercise for later

#plt.figure(1)
plt.figure(figsize=(11,11))
ax = plt.subplot(111, projection='polar')
ax.plot(theta,r1)
ax.plot(theta,r2)

dgr = (2*np.pi)/360.
#ax.text(222.*dgr, 27, 'Mean Rev1 {:4.2f}Hz'.format(np.mean(if1)), fontsize=8)
#ax.text(223.*dgr, 27.5, 'Mean Rev2 {:4.2f}Hz'.format(np.mean(if2)), fontsize=8)



mod_date = datetime.datetime.fromtimestamp(os.path.getmtime(_FILE))
ax.text(226.*dgr, 28.5, 'Mean Rev1 {:4.3f}Hz'.format(np.mean(if1)) + "\n" + \
    'Mean Rev2 {:4.3f}Hz'.format(np.mean(if2)) + "\n" + \
    _FILE + "\n" + \
    mod_date.strftime("%b %d, %Y %H:%M:%S"), fontsize=9)


ax.set_rmax(20)
                #Set up the ticks y is radial x is theta, it turns out x and y
                #methods still work in polar projection but sometimes do funny things

tick_loc = np.arange(1,21,1)


myticks = []
for x in range(0,20,1):
    myticks.append('{:4.2f}Hz'.format(maxf-(19*Hz_per_tick)+x*Hz_per_tick))

ax.set_rgrids(tick_loc, labels = myticks, angle = 90, fontsize = 8)

ax.set_xticklabels(['90'+u'\N{DEGREE SIGN}','45'+u'\N{DEGREE SIGN}','0'+u'\N{DEGREE SIGN}',\
                    '315'+u'\N{DEGREE SIGN}','270'+u'\N{DEGREE SIGN}','225'+u'\N{DEGREE SIGN}',\
                    '180'+u'\N{DEGREE SIGN}','135'+u'\N{DEGREE SIGN}'])



ax.grid(True)

ax.set_title(info_line, va='bottom', fontsize=16)



plt.savefig(info_line.replace(' / ', '_') +'.png', bbox_inches='tight', pad_inches=.5)

#plt.savefig(info_line.replace(' / ', '_') +'.png', bbox_inches='tight', pad_inches=.5, transparent=True)

plt.show()
 

JP

Major Contributor
Joined
Jul 4, 2018
Messages
2,332
Likes
2,507
Location
Brookfield, CT
Anyone have an M97xE and STR-100? I'd like to get a few measurements of this cart from different folks as I see a lot of variability in measurements in the wild.
 

JP

Major Contributor
Joined
Jul 4, 2018
Messages
2,332
Likes
2,507
Location
Brookfield, CT
Not sure the hacky way I'm doing this will pass muster, but we may be getting a bit closer to full-range plots.

sweep1323.png
 
Last edited:

JP

Major Contributor
Joined
Jul 4, 2018
Messages
2,332
Likes
2,507
Location
Brookfield, CT
Here's a real cartridge.

I've also built-in RIAA/IRAA correction with the ability to only apply bass turnover and shelf, such as what the TRS-1007 record and others use.

20-20 Test Plot.png
 
Last edited:

JP

Major Contributor
Joined
Jul 4, 2018
Messages
2,332
Likes
2,507
Location
Brookfield, CT
You meant 1007 - yes. That's a 150MLX with an extra 50pF on it from what I normally do.

The biggest learning from this is if you don't apply EQ to records that have lower turnover and shelf, the response from around 4kHz is +1dB from 1kHz. This is the recording characteristic of those records:

V11 Test.png
 
Last edited:

JP

Major Contributor
Joined
Jul 4, 2018
Messages
2,332
Likes
2,507
Location
Brookfield, CT
"Note that in the STR 100 record the 500-Hz break is sharp..."

No kidding.

V11 Test.png
 

USER

Addicted to Fun and Learning
Forum Donor
Joined
Mar 30, 2019
Messages
997
Likes
1,662
:oops: Well, that is going to be the best most of us will be able to do. Still, giving us an extra 500Hz is a generous gift.

What do you think about the CBS record now that you are scrutinizing it further? It *seems* to be matching up better with the 1007 (EQ'ed now, I presume), at least from afar.

I'm hoping the Clearaudio test record is as dependable and resembles your record. I got one last week and will be looking at it very soon.
 

JP

Major Contributor
Joined
Jul 4, 2018
Messages
2,332
Likes
2,507
Location
Brookfield, CT
"Thus, ideally response can be verified if the trace follows two straight lines - one rising at 6dB per octave up to 500Hz and the other horizontal above 500Hz."

We can go full old-school chart-recorder and put guides on the plot for perfect response. I'd hope I can get a filter to correct it but not sure - I'm brute-forcing my way through most of this.

I haven't paid much attention to the difference in records - trying to get the scenarios debugged so I can consolidate the code. For instance, when I apply lower turnover to the STR-100 it breaks the interpolation routine. No idea why.

BTW, if you change the marked value to 5 you can plot 500Hz with the existing code. The downside is that the resolution is 100Hz.

Code:
for x in range(0,len(input_sig)-F,F):
    y = abs(np.fft.rfft(input_sig[x:x+F]*win))
    f = np.argmax(y) #use largest bin
    if f >=10:                                   # <-------- Change this to 5.  f * 100 = Hz.
        frequency.append(f*100)
        amplitude.append(y[f])
    if 2*f<F/2-2 and f >= 10:
        f2 = np.argmax(y[(2*f)-2:(2*f)+2])
        frequency2h.append(f*100)
        amplitude2h.append(y[2*f-2+f2])
    if 3*f<F/2-2 and f >= 10:
        f3 = np.argmax(y[(3*f)-2:(3*f)+2])
        frequency3h.append(f*100)
        amplitude3h.append(y[3*f-2+f3])
V11 Test.png
 

USER

Addicted to Fun and Learning
Forum Donor
Joined
Mar 30, 2019
Messages
997
Likes
1,662
"Thus, ideally response can be verified if the trace follows two straight lines - one rising at 6dB per octave up to 500Hz and the other horizontal above 500Hz."

We can go full old-school chart-recorder and put guides on the plot for perfect response. I'd hope I can get a filter to correct it but not sure - I'm brute-forcing my way through most of this.

I haven't paid much attention to the difference in records - trying to get the scenarios debugged so I can consolidate the code. For instance, when I apply lower turnover to the STR-100 it breaks the interpolation routine. No idea why.

BTW, if you change the marked value to 5 you can plot 500Hz with the existing code. The downside is that the resolution is 100Hz.

Code:
for x in range(0,len(input_sig)-F,F):
    y = abs(np.fft.rfft(input_sig[x:x+F]*win))
    f = np.argmax(y) #use largest bin
    if f >=10:                                   # <-------- Change this to 5.  f * 100 = Hz.
        frequency.append(f*100)
        amplitude.append(y[f])
    if 2*f<F/2-2 and f >= 10:
        f2 = np.argmax(y[(2*f)-2:(2*f)+2])
        frequency2h.append(f*100)
        amplitude2h.append(y[2*f-2+f2])
    if 3*f<F/2-2 and f >= 10:
        f3 = np.argmax(y[(3*f)-2:(3*f)+2])
        frequency3h.append(f*100)
        amplitude3h.append(y[3*f-2+f3])
View attachment 258194
I think I mentioned in a private conversation that changing that number doesn't work for me. I get the following:

Normal "10"

10.png



"5"

5.png


The lower I go, the more off the result. Is there something I am missing? Does this have to do with the resolution you mention? Is this why you got that strange result you showed me for the AT150MLX.
 
Last edited:

JP

Major Contributor
Joined
Jul 4, 2018
Messages
2,332
Likes
2,507
Location
Brookfield, CT
That version normalizes at the lowest f. Find:

Code:
norm = ampout[0]
ampout = ampout-norm #amplitude is in dB so normalize by subtraction at [0]
ampout2h = ampout2h-norm
ampout3h = ampout3h-norm

And replace with:

Code:
def find_nearest(array, value):
    array = np.asarray(array)
    idx = (np.abs(array - value)).argmin()
    return idx

i = find_nearest(freqout, 1000)
norm = ampout[i]
ampout = ampout-norm #amplitude is in dB so normalize by subtraction at [i]
ampout2h = ampout2h-norm
ampout3h = ampout3h-norm

I've this configurable now, just not in a version that I've published.
 

USER

Addicted to Fun and Learning
Forum Donor
Joined
Mar 30, 2019
Messages
997
Likes
1,662
Very cool.

At this point I may as well share the measurements for a private label, black-bodied Stanton cartridge (as far as I can tell a 680EE-S) with a stereohedron stylus.


s-l1600 (5).jpg



Figure 2023-01-18 213024.png
 
Last edited:
  • Like
Reactions: JP

JP

Major Contributor
Joined
Jul 4, 2018
Messages
2,332
Likes
2,507
Location
Brookfield, CT
Cosmetic housekeeping if you want, find:

Code:
ax1.annotate('+' + str(deltah) + ', ' + u"\u2212" + str(deltal) + ' dB', color='b', \
             xy=(1000,0), xycoords='data', \
             xytext=(-15, -20), textcoords='offset points', \
             ha="left", va="center", \
             bbox=bbox_args, \
             #arrowprops=arrow_args \
             )

and replace with

Code:
ax1.annotate('+' + str(deltah) + ', ' + u"\u2212" + str(deltal) + ' dB', color='b', \
             xy=(freqout[0],(ampout[0]-1)), xycoords='data', \
             xytext=(-15, -20), textcoords='offset points', \
             ha="left", va="center", \
             bbox=bbox_args, \
             #arrowprops=arrow_args \
             )

freqout[0] and ampout[0] place the annotation box at the start of the FR plot, and you can adjust the position you want from there with a modifier, such as ampout[0]-1 does to move it down. This will all be in the next version.

EDIT: Actually the xytext args are modifying the position relevant to xy, so xy should be xy=(freqout[0],(ampout[0]) and xytext used as the modifier. Either/and will work, just cleaner this way.
 
Last edited:

VinyLuke

Member
Joined
Oct 19, 2019
Messages
50
Likes
94
Shure V15 III with Jico SAS/B stylus, Technics SL-1210GR with KAB fluid damper and stock headshell.
Latest script used, capacitance of arm and IC was measured with proper tool so it should be very close to real (+/- 10pF).

Shure V15 III + Jico SAS-B_48kΩ_260pF_1.25g_Right Ch_CBS STR-100.png
 
Top Bottom