• 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

If you are looking for opinions, I like the small x to note that it is not being used. I think the purple line begins to make things confusing for someone new to the graphs, especially as there is a lot going on in the graphs and often distortion and crosstalk results are messy. Is a slightly thicker black an option? It doesn't need to be obvious, I think, but findable if one wants to see it, which will likely not often be the case. More, is there any good reason not to use 1kHz given that, historically, it seems the privileged frequency? Does it even need to be an option for a general version of the script? Just some questions.
 
It's not a matter of being used though - mode 0 normalizes the files individually so that 0dBFS is is at the normalization frequency for each file and mode 1 normalizes the first file so that 0dBFS is at the normalization frequency, and the does the same adjustment to the second file. This preserves channel imbalance.

The line at 1kHz just changes the existing line to magenta.
 
Yeah, but I'm just thinking the magenta can make the graph a bit too busy. But that's just me. I do understand making it known if the graph does not show channel imbalance. I agree with the sentiment.
 
@stereoplay, how did you optimize this filter? It's 0.5dB off by 20kHz:

TEST XG-7001.png
 
Nice, thanks!
Shure V15Vx JICO SAS B_47 kOhm 140 pF_Denon XG-7001.png
 
Can you send me that file? The FR annotation seems off.
I just used what I found in my file library, but here you are (left and right, swapped so that both are in left channel).


 
Consider showing phase correlation between the fundamental and crosstalk
Old magazines sometimes plotted phase difference L to R channel vs frequency. But with Signal in both channel ( not crosstalk tracks) . I think that could be used to measure zenith/HTA alignment of stylus if record position and tonerarm alignment is known, For crosstalk tracks I guess the track should be out of phase ideally. , for optimal Azimuth..
Many thanks for the Denon XG-7001 EQ
 
Old magazines sometimes plotted phase difference L to R channel vs frequency. But with Signal in both channel ( not crosstalk tracks) . I think that could be used to measure zenith/HTA alignment of stylus if record position and tonerarm alignment is known, For crosstalk tracks I guess the track should be out of phase ideally. , for optimal Azimuth..
Many thanks for the Denon XG-7001 EQ

Scott was working on some real-time tools before he unplugged, though I never saw any of them. I may pick up that torch, if only some hack-y stuff for me to aid in alignment since we added the ability to measure crosstalk. I've also been taking about a Tek WFM and WVR unit for the digital audio monitoring portion so I can have better level monitoring pre- and post-RIAA. My current setup adds a good deal of digital gain with the RIAA filters and I've no way to see overs, which has bitten me a couple times.

Unrelated, for the script I played a bit with the filtering of the data. I'm not sure I'd want to do this, but it seems retain the salient info, and makes the full plots a hell of a lot easier to read. I'd need to "window" the filter so that information isn't lost as frequency declines.

TEST Filter Default.png
TEST Filter 2k.png
 
This is what I did:
1701272365622.png


2nd Harmonics fat and 3rd Harmonics thin lines. I also prefer distortion measurements in % instead of dB...

1701272521128.png
 
Automated pre-processing of recorded Test-Tracks

Hey guys, i was working on something similar like the Measurement Script used here and faced the problem of being lazy. Cutting the recorded test tracks for my purpose by hand was just too much of a hassle, so i wrote python scripts to automate the cropping for me.

Since some may be interested in this, i will post my scripts here.

One is for the typical L+R/L+R Track Nr. 3 on the 1007 Records. It outputs two cropped mono files (names set already so it fits the Measurement-Scripts standards used here).

The other one takes two files, the L and R file (Track 1 and 2 on 1007 Records), crops them, switches channels on the the second track and outputs two stereo tracks (names set already so it fits the Measurement-Scripts standards used here) - this is to produce a Graph that includes Crosstalk.


Depending on your setup (and Test Record), you may have to change the post_pilot_duration variable value.
What the scripts do is look for the 1kHz Test-Tone end, cut the file at this point, and then cut the file again post_pilot_duration specified seconds later. 50s is the official Sweep length on 1007 Records, but even after perfectly adjusting the speed of the test TT i had to set this to 50.5 to "catch" the whole sweep and not running into the end of the track. Test and fit to your requirements.

Settings (basically all is commented, but as quick reference):

These settings are at the end of the script, you can use paths too:

set "source_audio_file" ("left_audio_file" "right_audio_file") to whatever filenames you are saving your captured tracks to.

Adjust your output file names if you want to deviate from the scripts standard "wavefile1/2"

Pretty much at the beginning of the script you'll find the post_pilot_duration variable in the detect_sweep_start function.
Here you can adjust the lenghts of the Track to your requirements.

The scripts are also available as .py files in the attached .zip.

First the "simple" Script for Track 3 (L+R):

Python:
import os
import numpy as np
from scipy.io import wavfile
from scipy.signal import stft, butter, sosfilt

def bandpass_filter(data, sample_rate, lowcut=18, highcut=22, order=5):
    sos = butter(order, [lowcut / (0.5 * sample_rate), highcut / (0.5 * sample_rate)], btype='band', output='sos')
    return sosfilt(sos, data)

    # fine tune post_pilot_duration (in s) to your setup! this can vary, for example because of Turntable speed variations or your Test Record!
def detect_sweep_start(data, sample_rate, post_pilot_duration=50.5):
    # Apply bandpass filter
    filtered_data = bandpass_filter(data, sample_rate)

    # Perform STFT
    f, t, Zxx = stft(filtered_data, fs=sample_rate, nperseg=1024)

    # Find the index of the frequency closest to 20 Hz
    target_freq_index = np.argmin(np.abs(f - 20))

    # Get the magnitude of the STFT at 20 Hz
    magnitude = np.abs(Zxx[target_freq_index, :])
    threshold = np.max(magnitude) * 0.1  # Arbitrary threshold, may need adjustment

    # Detect the start of the sweep
    sweep_start_idx = np.where(magnitude > threshold)[0][0]
    sweep_start_time = t[sweep_start_idx]

    # Calculate the end time, 40 seconds after the pilot tone ends
    sweep_end_time = sweep_start_time + post_pilot_duration

    return sweep_start_time, sweep_end_time

def save_trimmed_audio(data, sample_rate, start_time, end_time, source_file, file_prefix='wavefile'):
    start_sample = int(start_time * sample_rate)
    end_sample = int(end_time * sample_rate)

    # Trim the data
    trimmed_data_left = data[start_sample:end_sample, 0]  # Left channel
    trimmed_data_right = data[start_sample:end_sample, 1]  # Right channel

    # Determine the output directory based on the source file
    output_dir = os.path.dirname(source_file) if source_file else '.'

    # Save the trimmed data as mono files
    left_output_path = os.path.join(output_dir, f"{file_prefix}1.wav")
    right_output_path = os.path.join(output_dir, f"{file_prefix}2.wav")

    wavfile.write(left_output_path, sample_rate, trimmed_data_left)
    wavfile.write(right_output_path, sample_rate, trimmed_data_right)

    return left_output_path, right_output_path

# Usage example
source_audio_file = '1007_L+R_Downsample.wav'  # Replace with your file path and file name
sample_rate, data = wavfile.read(source_audio_file)
mono_data = data.mean(axis=1) if data.ndim > 1 else data

sweep_start_time, sweep_end_time = detect_sweep_start(mono_data, sample_rate)
left_file, right_file = save_trimmed_audio(data, sample_rate, sweep_start_time, sweep_end_time, source_audio_file)
print(f"Trimmed audio files saved as {left_file} and {right_file}")

and the second one for Track 1+2 with Crosstalk:

Python:
import os
import numpy as np
from scipy.io import wavfile
from scipy.signal import stft, butter, sosfilt

def bandpass_filter(data, sample_rate, lowcut=18, highcut=22, order=5):
    sos = butter(order, [lowcut / (0.5 * sample_rate), highcut / (0.5 * sample_rate)], btype='band', output='sos')
    return sosfilt(sos, data)

    # fine tune post_pilot_duration (in s) to your setup! this can vary, for example because of Turntable speed variations or your Test Record!
def detect_sweep_start(data, sample_rate, post_pilot_duration=50.5):
    # Apply bandpass filter
    filtered_data = bandpass_filter(data, sample_rate)

    # Perform STFT
    f, t, Zxx = stft(filtered_data, fs=sample_rate, nperseg=1024)

    # Find the index of the frequency closest to 20 Hz
    target_freq_index = np.argmin(np.abs(f - 20))

    # Get the magnitude of the STFT at 20 Hz
    magnitude = np.abs(Zxx[target_freq_index, :])
    threshold = np.max(magnitude) * 0.1  # Arbitrary threshold, may need adjustment

    # Detect the start of the sweep
    sweep_start_idx = np.where(magnitude > threshold)[0][0]
    sweep_start_time = t[sweep_start_idx]

    # Calculate the end time, 50 seconds after the sweep starts
    sweep_end_time = sweep_start_time + post_pilot_duration

    return sweep_start_time, sweep_end_time

def save_trimmed_audio(data, sample_rate, start_time, end_time, source_file, file_name, swap_channels=False):
    start_sample = int(start_time * sample_rate)
    end_sample = int(end_time * sample_rate)

    # Trim the data
    trimmed_data = data[start_sample:end_sample]

    # Swap channels if required
    if swap_channels and trimmed_data.shape[1] > 1:
        trimmed_data = trimmed_data[:, [1, 0]]

    # Determine the output directory based on the source file
    output_dir = os.path.dirname(source_file) if source_file else '.'

    # Save the trimmed data
    output_path = os.path.join(output_dir, f"{file_name}.wav")
    wavfile.write(output_path, sample_rate, trimmed_data)

    return output_path

# Usage example
left_audio_file = '1007_L.wav'  # Replace with your file path and file name
right_audio_file = '1007_R.wav'  # Replace with your file path and file name

# Process left channel file
sample_rate, data_left = wavfile.read(left_audio_file)
sweep_start_time, sweep_end_time = detect_sweep_start(data_left.mean(axis=1), sample_rate)
wavefile1_path = save_trimmed_audio(data_left, sample_rate, sweep_start_time, sweep_end_time, left_audio_file, "wavefile1")

# Process right channel file and swap channels
sample_rate, data_right = wavfile.read(right_audio_file)
sweep_start_time, sweep_end_time = detect_sweep_start(data_right.mean(axis=1), sample_rate)
wavefile2_path = save_trimmed_audio(data_right, sample_rate, sweep_start_time, sweep_end_time, right_audio_file, "wavefile2", swap_channels=True)

print(f"Trimmed audio files saved as {wavefile1_path} and {wavefile2_path}")
 

Attachments

  • Crop_Sweeps.zip
    2.4 KB · Views: 38
Thanks - I’ll take a look at what can be incorporated. Librosa has a good detection function that can handle the end of the sweep.
 
With the way i acquired signals in the beginning of my tests i had quite the problems with this end-of-sweep detection. Mainly because the SNR was getting super low in the end until i repurposed a phono stage to match the 2 constant encoding of the 1007 Records. from that time is the approach to use the time-based cut.
Maybe i'll get it more reliably to work now that i have a better way of recording, but the time-based approach is relatively fool-proof (after dialing in) in terms of vulnerability because of weird filtered recordings etc.

I'll also work further on this, but no promises ;)
 
You'd want to do your RIAA corrections first to avoid that issue - 96k filters are in the measurement script.

For the implementation I'd want in the script I'd want to base the start off of one the 1kHz pilot tone stops, as that was the standard operating process that nearly all these records were designed for. Timing the record works for people who are inclined to tweak, but I'd like to do something a bit more fool-proof. It's on the winter projects list, which hasn't started yet. It's a decent day so I'm going to go clean the teak swim platform of the boat so I can sand/oil/seal it over the winter.
 
Hmmm.

But that is what I do? The script detects the pilot tone, in this case 1kHz, and looks where the pilot tone ends. After doing this at the beginning and running I to problems (using spectral density for detection) I switched to 20Hz recognition. This is where it cuts first. THEN it uses the post_pilot_duration to cut again after the pilot tone ended. And it works pretty well and easy actually. Just have to look how long the sweep track is supposed to be (50s on 1007) and put that in. If it does not fit perfectly, give or take a little, and then your sweeps are cut fully automatically.

Or did I not understand you correctly?

I should provide some additional info:

I am correcting the IRIAA of the record directly with a special 2 constant RIAA filter in my measurement phono stages.
So I am getting already "flat" tracks when I record them without further processing =)
Furthermore: I developed these scripts not because I was working with your script, but because I was working on other measurement stuff (including something like you did here). It was just a coincidence that I was reading about this here and adapted it a little so the guys here could use it.

That being said, I like your script a lot, with a couple more improvements I would even use it for my measurements.
Unfortunately, with less than 192kHz capability, 3rd harmonics at the upper end of the spectrum are not possible. But i saw you're working on that already, cool thing! Keep up the good work =)
 
Last edited:
Back
Top Bottom