<jupyter_start><jupyter_text>Timbre AnalysisThis notebook compares the timbre of two violins quantitatively.1. The first violin is a normal, wooden violin. 2. The [second one is 3d-printed](https://timodenk.com/blog/3d-printed-violin).They do sound quite different and the objective here is to quantify that, i.e., to make statements like _"the high frequency harmonics are more amplified for the wooden violin"_ or so.<jupyter_code>from glob import glob
import matplotlib.pyplot as plt
import numpy as np
import os
from scipy import signal
from scipy.io import wavfile
notes = ['g', 'd', 'a', 'e']
freqs = [196., 293.7, 440., 659.3]  # frequences of the notes g, d, a, and e in Hz
wooden_wav_files = sorted(glob(os.path.join('samples', 'wooden-*.wav')))
printed_wav_files = sorted(glob(os.path.join('samples', 'printed-*.wav')))
assert len(wooden_wav_files) == 4 and len(printed_wav_files) == 4<jupyter_output><empty_output><jupyter_text>Loading the `.wav` files. `wavfile.read` returns a tuple consisting of `sample_rate, samples`.<jupyter_code>wooden_wav = [wavfile.read(p) for p in wooden_wav_files]
printed_wav = [wavfile.read(p) for p in printed_wav_files]
min_duration = min([samples.shape[0] for _, samples in wooden_wav + printed_wav])
min_duration<jupyter_output><empty_output><jupyter_text>**Spectrogram** computation<jupyter_code>def get_spectrogram(sample_rate: int, wav_samples: np.array) -> (np.array, np.array):
    assert sample_rate == 44100
    num_samples = wav_samples.shape[0]  # total length of the audio segment in terms of samples
    assert num_samples > 22050, "Duration must be at least half a second"
    assert wav_samples.shape[1] == 2, "Number of channels must be two"
    
    print(f"Got sample of length {round(num_samples/sample_rate, 4)}s ", 
          f"cropping to {round (min_duration/sample_rate, 4)}s")
    num_samples = min_duration
    wav_samples_cropped = wav_samples[:min_duration, :]
    
    ch1 = wav_samples_cropped.T[0]
    ch2 = wav_samples_cropped.T[1]
    
    # nperseg=num_samples ensures that the spectrogram is computed over the entire length
    # so the time resolution will be only a single position
    freqs, _, spec1 = signal.spectrogram(ch1, sample_rate, nperseg=num_samples)
    freqs, _, spec2 = signal.spectrogram(ch2, sample_rate, nperseg=num_samples)
    
    # average spectrograms from both channels
    spectrogram = np.mean(np.array([spec1, spec2]), axis=0)[:, 0]
    spectrogram_normalized = spectrogram / np.linalg.norm(spectrogram)
    return freqs, spectrogram
wooden_spectrograms = [
    get_spectrogram(sample_rate, wav_samples)[1]
    for sample_rate, wav_samples in wooden_wav
]

printed_spectrograms = [
    get_spectrogram(sample_rate, wav_samples)[1]
    for sample_rate, wav_samples in printed_wav
]<jupyter_output>Got sample of length 1.1498s  cropping to 0.9981s
Got sample of length 1.4237s  cropping to 0.9981s
Got sample of length 1.1756s  cropping to 0.9981s
Got sample of length 1.1051s  cropping to 0.9981s
Got sample of length 0.9981s  cropping to 0.9981s
Got sample of length 1.1557s  cropping to 0.9981s
Got sample of length 1.1312s  cropping to 0.9981s
Got sample of length 1.0781s  cropping to 0.9981s<jupyter_text>We determine the frequencies of the spectrogram once, because they are the same for every audio sample, thanks to the cropping in duration that we do inside of `get_spectrogram`.<jupyter_code>freqs = get_spectrogram(*wooden_wav[0])[0]
freqs<jupyter_output>Got sample of length 1.1498s  cropping to 0.9981s<jupyter_text>The frequencies range from 0 Hz up to 22 kHz.  Examaple VisualizationPlotted below is the spectrogram of the wooden violin (blue) and the printed one (red) for the tone E.Note:* It is not clear (but important to find out) whether the open string was being played in both cases.* Violins were not tuned to the same frequency, so the comparison in terms of absolute frequencies is not possible (which is no big deal, just worth mentioning).<jupyter_code>x_offset = 500
x_width = 4000
plt.plot(freqs[x_offset:x_width], wooden_spectrograms[3][x_offset:x_width], c='b')
plt.plot(freqs[x_offset:x_width], printed_spectrograms[3][x_offset:x_width], c='r')
plt.xscale('log'); plt.xlabel('frequency'); plt.ylabel('intensity');
x_offset = 300
plt.plot(freqs[x_offset:x_width], wooden_spectrograms[0][x_offset:x_width], c='b')
plt.plot(freqs[x_offset:x_width], printed_spectrograms[0][x_offset:x_width], c='r')
plt.xscale('log'); plt.xlabel('frequency'); plt.ylabel('intensity');<jupyter_output><empty_output>
{'fraction_non_alphanumeric': 0.1001765225066196, 'fraction_numerical': 0.03508384819064431, 'mean_word_length': 5.304589707927677, 'pattern_counts': {'":': 0, '<': 19, '<?xml version=': 0, '>': 21, 'https://': 1, 'lorem ipsum': 0, 'www.': 0, 'xml': 0}, 'pii_count': 0, 'substrings_counts': 0, 'word_list_counts': {'cursed_substrings.json': 0, 'profanity_word_list.json': 0, 'sexual_word_list.json': 0, 'zh_pornsignals.json': 0}}
{'chain_length': '5', 'dir': 'jupyter-structured-clean-dedup', 'id': '393203', 'license': 'MIT', 'n_tokens_mistral': 1693, 'n_tokens_neox': 1551, 'n_words': 461, 'path': 'audio/violin/timbre-analysis.ipynb', 'repo_name': 'Simsso/Machine-Learning-Tinkering', 'size': '4532'}