• 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!

Phono Cartridge Response Measurement Script

That's a big jump from conda 4 to 23. Is that for anaconda in general. Big leaps like that can be a problem.What version of python are you using? I recommend you start a new anaconda environment. Start fresh (without killing your old environment if you are using it). Base it off python 3.10. You'll have to search for and install most libraries as well as Spyder and CMD.
Many thanks but I am sorry I am not understanding the terminology, environment* etc or what to actually do. The original script in the other thread runs fine, I installed Anaconda and Spyder came with it, I googled my way to install som of the "libraries" and "sub programs" ,and made the simpler scripts work, after some trial end error.


I tried this in the CMD promt but no success
C:\Users\ersol>pip install librosa
'pip' is not recognized as an internal or external command,
operable program or batch file.



, but WHERE? do I write it?
1674920404868.png



"New Anaconda environment" ?: Do you mean that I should install Anaconda once more but in another folder?

1674916444650.png


"base off Python" 3.10? I thought Spyder was the program that runs Python, I only installed Anaconda and start Spyder. I have a folder on C named IPython.... and now I find this 3-8 folders. Does that mean that I run Python 3.8 when I use Spyder? EDIT found out that I have Python 3.8.7
1674917234391.png


I apologize for being such a Python dummy,but I really like the plots your scrips produce
1674917460506.png


* environment https://packaging.python.org/en/latest/tutorials/installing-packages/
 
Last edited:
Many thanks but I am sorry I am not understanding the terminology, environment* etc or what to actually do. The original script in the other thread runs fine, I installed Anaconda and Spyder came with it, I googled my way to install som of the "libraries" and "sub programs" ,and made the simpler scripts work, after some trial end error.

"New Anaconda environment" ?: Do you mean that I should install Anaconda once more but in another folder?

View attachment 260600

"base off Python" 3.10? I thought Spyder was the program that runs Python, I only installed Anaconda and start Spyder. I have a folder on C named IPython.... and now I find this 3-8 folders. Does that mean that I run Python 3.8 when I use Spyder? EDIT found out that I have Python 3.8.7
View attachment 260602

I apologize for being such a Python dummy,but I really like the plots your scrips produce
View attachment 260605

* environment https://packaging.python.org/en/latest/tutorials/installing-packages/
You are not using Anaconda. My instructions are for it only. Using Anaconda is the easiest way for a newbie to use the script as the gui is simplified (it's pretty nice, TBH) and so my instructions are for it. I'm not calling you a newbie, I'm simply saying that while the underlying architecture is same updating the resources would be different for you, especially as some of your elements are quite old and have different names (e.g. conda/anaconda). When I refer to Environments, I am specifically talking about Anaconda.

Download from the first link and continue the instructions from there. Environments, should you need it, is on the left side.

Untitled.jpg

Untitled3.jpg

Untitled4.jpg

Untitleds.jpg
 
Last edited:
Mac seems so much easier. I’ll need to take a look at windows.

You can likely run without librosa, just won’t be able to just the trim function.
 
The latest code for the script can be found here. I intend to get this on GitHub at some point, but as I'm not a developer I've only read code from repos and have never had to use them for workflow, so I've a learning curve. Patience appreciated.

You will need Python to run this with scipy, matplotlib, numpy, and librosa libraries installed. Note that I work on a Mac so there may be some methods and nomenclatures that are OS dependent, particularly around file handling. In the future I'll test against Windows as well to ensure it is agnostic.

Also note that indents are important with Python, so you want to ensure the formatting doesn't change when you copy/paste.

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!')
-Version 8C, Dec 23, 2022-


This is an example plot done with this version of the code:

View attachment 258361

We'll put some type of instructional here on how to install Python and get this working. If someone wants to volunteer to (help) write that, I'd be very appreciative. We'd need one for Windows, and one for Mac. I'm going to assume Linux folks can figure it out on their own. :cool:
So what the hell were Paul Messenger\Martin Colloms doing when he tested and damned with faint praise the V15VMR in HiFi Choice? If anyone's interested, I can scan and post (sorry, not strictly on topic), but his (pen) plots showed flat to 10k then a 4dB drop to 20kHz with added capacitance and falling 4dB or so from 1 - 20kHz with low capacitance loading... The V15VMR was tested in a Linn LP12/Basik LVX arm (spit) and into an Ortofon TC 3000 test centre.

Sorry to divert, but there weren't half some self appointed 'gurus' in this industry acting as subtle influencers back then :(
 
Last edited:
So what the hell were Paul Messenger\Martin Colloms doing when he tested and damned with faint praise the V15VMR in HiFi Choice? If anyone's interested, I can scan and post (sorry, not strictly on topic), but his (pen) plots showed flat to 10k then a 4dB drop to 20kHz with added capacitance and falling 4dB or so from 1 - 20kHz with low capacitance loading... The V15VMR was tested in a Linn LP12/Basik LVX arm (spit) and into an Ortofon TC 3000 test centre.

Sorry to divert, but there weren't half some self appointed 'gurus' in this industry acting as subtle influencers back then :(
I think that would be best for this thread: https://www.audiosciencereview.com/forum/index.php?threads/mm-vs-mi-vs-mc.18636/page-65

I'd be interested in seeing the review.
 
I am very much a Newbie..thanks for your patience
Followed the procedure and have Anaconda installed, and Librosa confirmed installed, but it seems like "Librosa" is hiding in a folder where the script/Python cannot find it. Someting missing in my PATH definitions maybe?

1674922701371.png



But after doing the install I can just run Spyder as a separate program right? Not sure what the Anaconda navigator is used for after the install procedure is done.

1674922928385.png



1674922134426.png
 
I am very much a Newbie..thanks for your patience
Followed the procedure and have Anaconda installed, and Librosa confirmed installed, but it seems like "Librosa" is hiding in a folder where the script/Python cannot find it. Someting missing in my PATH definitions maybe?

View attachment 260624


But after doing the install I can just run Spyder as a separate program right? Not sure what the Anaconda navigator is used for after the install procedure is done.


View attachment 260623

I am not sure what your first image is. I updated (both) my instructions with images and everything to set up should be done from Anaconda Navigator. Look them over again. No need to do anything resembling your first image. It's much simpler than that. In fact, if it is a fresh install, you likely don't have to do anything with Environments.

And, yes, after it is set-up you don't need to use Anaconda Navigator. However be mindful that you seem to have a separate, previously installed version of Spyder. Don't use that one. Make sure you use the Anaconda linked version of Spyder. (As this is likely Python 3.11, right click and select run as administrator.)

Untitled5.jpg
 
Last edited:
So what the hell were Paul Messenger\Martin Colloms doing when he tested and damned with faint praise the V15VMR in HiFi Choice? If anyone's interested, I can scan and post (sorry, not strictly on topic), but his (pen) plots showed flat to 10k then a 4dB drop to 20kHz with added capacitance and falling 4dB or so from 1 - 20kHz with low capacitance loading... The V15VMR was tested in a Linn LP12/Basik LVX arm (spit) and into an Ortofon TC 3000 test centre.

Sorry to divert, but there weren't half some self appointed 'gurus' in this industry acting as subtle influencers back then :(

All I could find along these lines: https://www.hifi-classic.net/review/shure-v15-type-v-282.html

The cartridge’s frequency response was measured with a range of load capacitances in parallel with a 47,000-ohm resistance. The output dropped about 2 dB in the 5,000- to 15,000-Hz range with a 70-picofarad load, and with 440 pF it was almost perfectly flat ( ± 0.3 dB) up to 12,500 Hz but fell off 4 dB at 20,000 Hz. We used 280 pF for our other tests, since it gave the best combination of flatness and extended high-frequency response. With the CBS STR 100 test record, the response of the poorer of the two channels was +0,-1 dB from 40 to 17,000 Hz, and it was +0, —1 dB from 40 to 20,000 Hz on the other channel.

This seems to generally agree with what I've seen though I have seen a lot of variability which in all likelihood is due to suspension age. Wouldn't mind seeing this article if you've time to scan it.
 
Tangentially related, while waiting for my better half to get ready to go out I was playing with plot layout. It's a lot of data to convey while trying to maintain legibility. Going to have to work on that.

Not quite a coincidence I used V-15VMR data, also looking at crosstalk vs. distortion @Thomas_A.

V15-VMR_47k 350pF_TRS-1007.png
 
Tangentially related, while waiting for my better half to get ready to go out I was playing with plot layout. It's a lot of data to convey while trying to maintain legibility. Going to have to work on that.

Not quite a coincidence I used V-15VMR data, also looking at crosstalk vs. distortion @Thomas_A.

View attachment 260699
If I understand correctly you have -33 and -18 dB crosstalk at 1 kHz? And lower crosstalk at higher f for one channel? If so a bit weird result. The same for a 1kHz signal?
 
Indeed. Right channel is what I’d expect to see. Have looked further - off to dinner at a friends place.
 
Indeed. Right channel is what I’d expect to see. Have looked further - off to dinner at a friends place.
Ok - enjoy dinner. Would like to know more what’s causing this.
 
@Balle Clorin Python installs/envs are definitely hell to manage (and wrap your head around)

If the issue is still not resolved: 1) open Spyder, 2) type following (yes, with ! in the beginning) in the bottom right part, 3) press Enter.
Code:
!conda install -y librosa
This should install librosa inside the Python env Spyder is using. !pip also works if you ever need it
 
Indeed. Right channel is what I’d expect to see. Have looked further - off to dinner at a friends place.
I looked at some recorded sweeps; the Ortofon test record sweep with x-talk:
Ortofon test sweep V15VxSAS_B.png



vs the Clearaudio TRS1007 "copy":
Clearaudio TRS1007 V15VxSAS_B.png


So there is something very wrong with the crosstalk of my Clearaudio TRS1007 test record.
 
Quick note that I don't think the XG-7001 comp is correct below ~100Hz. Also really nice to be able to quickly put stuff like the below together. The designated capacitance isn't verified so let's call it +/-25pF of the labeled values.

View attachment 259978

Just a short question, how is the sweep of the XG-7001 recorded? How is the quality vs the TRS-1007?
 
All I could find along these lines: https://www.hifi-classic.net/review/shure-v15-type-v-282.html



This seems to generally agree with what I've seen though I have seen a lot of variability which in all likelihood is due to suspension age. Wouldn't mind seeing this article if you've time to scan it.
Hope the scan comes out readable. I've included the Supex SM100E on the adjacent page as I feel it lives on in the Sumiko Pearl model and is also related closely to the A&R E77 and Rega R100 of the early 80's. Interesting as an aside that massive 5+DB droops in response were tolerated then, but a slight rise at >10khz wasn't. No wonder 'digital' and CD wasn't liked by this set at first, at least on paper as we read them as one guru knew exactly what was going on and used master recordings in his speaker reviews. I loved mine until the cantilever met with an unfortunate accident :( The subjective results are more the mid period turntable and slightly splashy tonearm I feel (looking back with experience and hindsight).

Again, apologies for the drift -

scan0001.jpg
 
Just a short question, how is the sweep of the XG-7001 recorded? How is the quality vs the TRS-1007?

Comparisons are here #45

All my laybacks are flat. For the XG-7001 I applied a "standard" EQ of 30Hz and 250Hz as I had it the biquads for it already, but it seems it should be just 250Hz for that one.
 
Comparisons are here #45

All my laybacks are flat. For the XG-7001 I applied a "standard" EQ of 30Hz and 250Hz as I had it the biquads for it already, but it seems it should be just 250Hz for that one.
Thanks. I just placed an order for one in mint condition. Wonder what crosstalk you get with that one, both ways.
 
Hope the scan comes out readable. I've included the Supex SM100E on the adjacent page as I feel it lives on in the Sumiko Pearl model and is also related closely to the A&R E77 and Rega R100 of the early 80's. Interesting as an aside that massive 5+DB droops in response were tolerated then, but a slight rise at >10khz wasn't. No wonder 'digital' and CD wasn't liked by this set at first, at least on paper as we read them as one guru knew exactly what was going on and used master recordings in his speaker reviews. I loved mine until the cantilever met with an unfortunate accident :( The subjective results are more the mid period turntable and slightly splashy tonearm I feel (looking back with experience and hindsight).

Again, apologies for the drift -

View attachment 260847

They sure didn't do any favors by not being consistent with reference to the plot grids in making it any easier to read. Below is 150pF and 350pF with the Ortofon TC-3000 sweep. The TC-3000 system didn't plot though - only gave amplitudes for 1, 5, 10, 12, 15, 18, and 20 kHz. Not positive that record could be used on a chart recorder as the sweep runs quite a bit faster than typical, but I haven't played with any.

It'd definitely odd to see FR impact below 1kHz from changing C. Granted, they never state what "higher load capacitance" actually is.

V15-VMR_Ortofon 3000.png
 
Thank you for your work



Have you heard of GitHub Gists? Basically code snippets with versioning and comments. Should be more convenient than pasting stuff to a forum and keeping track of versions yourself. No collaboration features like a real git repo though, only comments
Hadn't. I'll check it out. Thanks!
 
Back
Top Bottom