Skip to content

Commit da4e818

Browse files
satraclaude
andauthored
Fix lab plots: EMA time axis, pitch filter, figure sizes (#485)
* Fix lab plots: EMA time axis, pitch filter, figure sizes - EMA plot: auto-detect (frames, channels) vs (channels, frames) orientation — was showing 0.22s instead of 5s - Pitch contour: filter artifacts above 250Hz (onset detection noise) - Acoustic analysis plot: figsize increased to (16, 14) - Comparison plot: figsize increased to (16, 18) Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> * Fix pitch filter: 350Hz threshold, consistent across all plots - Raised pitch filter from 250Hz to 350Hz (women/children can exceed 250Hz) - Applied (50, 350) filter consistently in all 3 visualization cells - Filter SPARC pitch zeros (unvoiced frames) in comparison data Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> --------- Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
1 parent 2acd58f commit da4e818

1 file changed

Lines changed: 84 additions & 22 deletions

File tree

tutorials/audio/speech_representations_lab.ipynb

Lines changed: 84 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -51,7 +51,43 @@
5151
"execution_count": null,
5252
"metadata": {},
5353
"outputs": [],
54-
"source": "import os\nimport urllib.request\nfrom pathlib import Path\n\nimport matplotlib\nimport matplotlib.pyplot as plt\nimport numpy as np\nimport torch\nimport torchaudio.transforms as T\n\nfrom senselab.audio.data_structures import Audio\nfrom senselab.audio.tasks.features_extraction.ppg import (\n extract_ppg_segments,\n to_frame_major_posteriorgram,\n extract_mean_phoneme_durations,\n extract_ppgs_from_audios,\n plot_ppg_phoneme_timeline,\n)\nfrom senselab.audio.tasks.features_extraction.sparc import SparcFeatureExtractor\nfrom senselab.audio.tasks.features_extraction.torchaudio import extract_pitch_from_audios\nfrom senselab.audio.tasks.plotting.plotting import play_audio, plot_aligned_panels, plot_specgram, plot_waveform\nfrom senselab.audio.tasks.preprocessing.preprocessing import (\n downmix_audios_to_mono,\n resample_audios,\n)\nfrom senselab.utils.data_structures import DeviceType\n\n# Auto-detect device\nif torch.cuda.is_available():\n device = DeviceType.CUDA\nelif hasattr(torch.backends, \"mps\") and torch.backends.mps.is_available():\n device = DeviceType.CPU # MPS not supported by all backends\nelse:\n device = DeviceType.CPU\nprint(f\"Using device: {device.value}\")"
54+
"source": [
55+
"import os\n",
56+
"import urllib.request\n",
57+
"from pathlib import Path\n",
58+
"\n",
59+
"import matplotlib\n",
60+
"import matplotlib.pyplot as plt\n",
61+
"import numpy as np\n",
62+
"import torch\n",
63+
"import torchaudio.transforms as T\n",
64+
"\n",
65+
"from senselab.audio.data_structures import Audio\n",
66+
"from senselab.audio.tasks.features_extraction.ppg import (\n",
67+
" extract_ppg_segments,\n",
68+
" to_frame_major_posteriorgram,\n",
69+
" extract_mean_phoneme_durations,\n",
70+
" extract_ppgs_from_audios,\n",
71+
" plot_ppg_phoneme_timeline,\n",
72+
")\n",
73+
"from senselab.audio.tasks.features_extraction.sparc import SparcFeatureExtractor\n",
74+
"from senselab.audio.tasks.features_extraction.torchaudio import extract_pitch_from_audios\n",
75+
"from senselab.audio.tasks.plotting.plotting import play_audio, plot_aligned_panels, plot_specgram, plot_waveform\n",
76+
"from senselab.audio.tasks.preprocessing.preprocessing import (\n",
77+
" downmix_audios_to_mono,\n",
78+
" resample_audios,\n",
79+
")\n",
80+
"from senselab.utils.data_structures import DeviceType\n",
81+
"\n",
82+
"# Auto-detect device\n",
83+
"if torch.cuda.is_available():\n",
84+
" device = DeviceType.CUDA\n",
85+
"elif hasattr(torch.backends, \"mps\") and torch.backends.mps.is_available():\n",
86+
" device = DeviceType.CPU # MPS not supported by all backends\n",
87+
"else:\n",
88+
" device = DeviceType.CPU\n",
89+
"print(f\"Using device: {device.value}\")"
90+
]
5591
},
5692
{
5793
"cell_type": "markdown",
@@ -140,7 +176,43 @@
140176
"metadata": {},
141177
"outputs": [],
142178
"source": [
143-
"from pathlib import Path\n\n# Load recorded audio if available, otherwise download sample files\nimport urllib.request\n\nbase_url = \"https://github.com/sensein/senselab/raw/main/src/tests/data_for_testing\"\n\n# Download sample files as fallback\nfor fname in [\"audio_48khz_mono_16bits.wav\", \"audio_48khz_stereo_16bits.wav\"]:\n dest = Path(\"tutorial_audio_files\") / fname\n if not dest.exists():\n urllib.request.urlretrieve(f\"{base_url}/{fname}\", str(dest))\n\n# Sentence 1: use recording if available\nrec1 = Path(\"tutorial_audio_files/recording_1.wav\")\nif rec1.exists() and rec1.stat().st_size > 1000:\n sentence1 = Audio(filepath=str(rec1.resolve()))\n print(\"Sentence 1: using your recording\")\nelse:\n sentence1 = Audio(filepath=os.path.abspath(\"tutorial_audio_files/audio_48khz_mono_16bits.wav\"))\n print(\"Sentence 1: using sample audio\")\n\n# Sentence 2: use recording if available\nrec2 = Path(\"tutorial_audio_files/recording_2.wav\")\nif rec2.exists() and rec2.stat().st_size > 1000:\n sentence2_raw = Audio(filepath=str(rec2.resolve()))\n print(\"Sentence 2: using your recording\")\nelse:\n sentence2_raw = Audio(filepath=os.path.abspath(\"tutorial_audio_files/audio_48khz_stereo_16bits.wav\"))\n print(\"Sentence 2: using sample audio\")\n\n# Preprocess: mono + 16kHz\nsentence1_16k = resample_audios(downmix_audios_to_mono([sentence1]), resample_rate=16000)[0]\nsentence2_16k = resample_audios(downmix_audios_to_mono([sentence2_raw]), resample_rate=16000)[0]\n\nprint(f\"Sentence 1: {sentence1_16k.waveform.shape[1] / 16000:.2f}s at 16kHz\")\nprint(f\"Sentence 2: {sentence2_16k.waveform.shape[1] / 16000:.2f}s at 16kHz\")"
179+
"from pathlib import Path\n",
180+
"\n",
181+
"# Load recorded audio if available, otherwise download sample files\n",
182+
"import urllib.request\n",
183+
"\n",
184+
"base_url = \"https://github.com/sensein/senselab/raw/main/src/tests/data_for_testing\"\n",
185+
"\n",
186+
"# Download sample files as fallback\n",
187+
"for fname in [\"audio_48khz_mono_16bits.wav\", \"audio_48khz_stereo_16bits.wav\"]:\n",
188+
" dest = Path(\"tutorial_audio_files\") / fname\n",
189+
" if not dest.exists():\n",
190+
" urllib.request.urlretrieve(f\"{base_url}/{fname}\", str(dest))\n",
191+
"\n",
192+
"# Sentence 1: use recording if available\n",
193+
"rec1 = Path(\"tutorial_audio_files/recording_1.wav\")\n",
194+
"if rec1.exists() and rec1.stat().st_size > 1000:\n",
195+
" sentence1 = Audio(filepath=str(rec1.resolve()))\n",
196+
" print(\"Sentence 1: using your recording\")\n",
197+
"else:\n",
198+
" sentence1 = Audio(filepath=os.path.abspath(\"tutorial_audio_files/audio_48khz_mono_16bits.wav\"))\n",
199+
" print(\"Sentence 1: using sample audio\")\n",
200+
"\n",
201+
"# Sentence 2: use recording if available\n",
202+
"rec2 = Path(\"tutorial_audio_files/recording_2.wav\")\n",
203+
"if rec2.exists() and rec2.stat().st_size > 1000:\n",
204+
" sentence2_raw = Audio(filepath=str(rec2.resolve()))\n",
205+
" print(\"Sentence 2: using your recording\")\n",
206+
"else:\n",
207+
" sentence2_raw = Audio(filepath=os.path.abspath(\"tutorial_audio_files/audio_48khz_stereo_16bits.wav\"))\n",
208+
" print(\"Sentence 2: using sample audio\")\n",
209+
"\n",
210+
"# Preprocess: mono + 16kHz\n",
211+
"sentence1_16k = resample_audios(downmix_audios_to_mono([sentence1]), resample_rate=16000)[0]\n",
212+
"sentence2_16k = resample_audios(downmix_audios_to_mono([sentence2_raw]), resample_rate=16000)[0]\n",
213+
"\n",
214+
"print(f\"Sentence 1: {sentence1_16k.waveform.shape[1] / 16000:.2f}s at 16kHz\")\n",
215+
"print(f\"Sentence 2: {sentence2_16k.waveform.shape[1] / 16000:.2f}s at 16kHz\")"
144216
]
145217
},
146218
{
@@ -277,6 +349,9 @@
277349
"]\n",
278350
"\n",
279351
"ema = features1[\"ema\"].numpy()\n",
352+
"# EMA shape is (frames, channels) \u2014 detect orientation\n",
353+
"if ema.shape[0] > ema.shape[1]: # (frames, channels)\n",
354+
" ema = ema.T # transpose to (channels, frames)\n",
280355
"n_channels = min(ema.shape[0], len(CHANNEL_NAMES))\n",
281356
"n_frames = ema.shape[1]\n",
282357
"time_ema = np.arange(n_frames) / 50.0 # SPARC runs at 50 Hz\n",
@@ -444,24 +519,7 @@
444519
"metadata": {},
445520
"outputs": [],
446521
"source": [
447-
"# Extract acoustic pitch contour\n",
448-
"pitch_result = extract_pitch_from_audios([sentence1_16k], freq_low=80, freq_high=500)\n",
449-
"pitch_contour = pitch_result[0][\"pitch\"].squeeze()\n",
450-
"\n",
451-
"# Build time axis (default hop length for torchaudio pitch detection)\n",
452-
"hop_length = 160 # default for 16 kHz\n",
453-
"time_pitch = torch.arange(len(pitch_contour)) * hop_length / 16000\n",
454-
"\n",
455-
"# Plot only voiced frames (pitch > 0)\n",
456-
"plt.figure(figsize=(12, 4))\n",
457-
"voiced = pitch_contour > 0\n",
458-
"plt.scatter(time_pitch[voiced].numpy(), pitch_contour[voiced].numpy(), s=2, c=\"blue\")\n",
459-
"plt.xlabel(\"Time (seconds)\")\n",
460-
"plt.ylabel(\"Frequency (Hz)\")\n",
461-
"plt.title(\"Acoustic Pitch (F0) Contour\")\n",
462-
"plt.grid(True, alpha=0.3)\n",
463-
"plt.tight_layout()\n",
464-
"plt.show()"
522+
"# Extract acoustic pitch contour\npitch_result = extract_pitch_from_audios([sentence1_16k], freq_low=80, freq_high=500)\npitch_contour = pitch_result[0][\"pitch\"].squeeze()\n\n# Build time axis (default hop length for torchaudio pitch detection)\nhop_length = 160 # default for 16 kHz\ntime_pitch = torch.arange(len(pitch_contour)) * hop_length / 16000\n\n# Plot only voiced frames (pitch > 0)\nplt.figure(figsize=(12, 4))\nvoiced = (pitch_contour > 50) & (pitch_contour < 350) # filter artifacts\nplt.scatter(time_pitch[voiced].numpy(), pitch_contour[voiced].numpy(), s=2, c=\"blue\")\nplt.xlabel(\"Time (seconds)\")\nplt.ylabel(\"Frequency (Hz)\")\nplt.title(\"Acoustic Pitch (F0) Contour\")\nplt.grid(True, alpha=0.3)\nplt.tight_layout()\nplt.show()"
465523
]
466524
},
467525
{
@@ -535,7 +593,9 @@
535593
"execution_count": null,
536594
"metadata": {},
537595
"outputs": [],
538-
"source": "# Standard spectrogram params: 10ms window, 5ms hop at 16kHz\nspec_params = {\"n_fft\": 256, \"hop_length\": 80, \"win_length\": 160}\n\n# Get PPG segments for plotting\nframe_major = to_frame_major_posteriorgram(ppg1)\nsegments = extract_ppg_segments(sentence1_16k, frame_major)\n\n# Convert PPG segments to plot_aligned_panels format\nppg_segments = [\n {\"label\": seg[\"phoneme\"], \"start\": seg[\"start_seconds\"], \"end\": seg[\"start_seconds\"] + seg[\"duration_seconds\"]}\n for seg in segments\n]\n\n# Build pitch overlay data\nvoiced_mask = pitch_contour > 0\npitch_data = [(time_pitch[voiced_mask], pitch_contour[voiced_mask], \"F0\", \"steelblue\")]\n\nplot_aligned_panels(\n sentence1_16k,\n [\n {\"type\": \"waveform\"},\n {\"type\": \"spectrogram\", \"mel\": True},\n {\"type\": \"features\", \"data\": pitch_data},\n {\"type\": \"segments\", \"segments\": ppg_segments},\n ],\n title=\"Acoustic Analysis: Waveform + Spectrogram + Pitch + PPG\",\n spectrogram_params=spec_params,\n)\nplt.show()"
596+
"source": [
597+
"# Standard spectrogram params: 10ms window, 5ms hop at 16kHz\nspec_params = {\"n_fft\": 256, \"hop_length\": 80, \"win_length\": 160}\n\n# Get PPG segments for plotting\nframe_major = to_frame_major_posteriorgram(ppg1)\nsegments = extract_ppg_segments(sentence1_16k, frame_major)\n\n# Convert PPG segments to plot_aligned_panels format\nppg_segments = [\n {\"label\": seg[\"phoneme\"], \"start\": seg[\"start_seconds\"], \"end\": seg[\"start_seconds\"] + seg[\"duration_seconds\"]}\n for seg in segments\n]\n\n# Build pitch overlay data\nvoiced_mask = (pitch_contour > 50) & (pitch_contour < 350)\npitch_data = [(time_pitch[voiced_mask], pitch_contour[voiced_mask], \"F0\", \"steelblue\")]\n\nplot_aligned_panels(\n sentence1_16k,\n [\n {\"type\": \"waveform\"},\n {\"type\": \"spectrogram\", \"mel\": True},\n {\"type\": \"features\", \"data\": pitch_data},\n {\"type\": \"segments\", \"segments\": ppg_segments},\n ],\n title=\"Acoustic Analysis: Waveform + Spectrogram + Pitch + PPG\",\n spectrogram_params=spec_params,\n figsize=(16, 14),\n)\nplt.show()"
598+
]
539599
},
540600
{
541601
"cell_type": "markdown",
@@ -564,7 +624,9 @@
564624
"execution_count": null,
565625
"metadata": {},
566626
"outputs": [],
567-
"source": "# Get PPG segments for the comparison\nframe_major = to_frame_major_posteriorgram(ppg1)\nsegments = extract_ppg_segments(sentence1_16k, frame_major)\nppg_segments = [\n {\"label\": seg[\"phoneme\"], \"start\": seg[\"start_seconds\"], \"end\": seg[\"start_seconds\"] + seg[\"duration_seconds\"]}\n for seg in segments\n]\n\n# Build SPARC pitch and acoustic pitch as feature overlays\nsparc_pitch_time = np.arange(len(pitch_sparc)) / 50.0\nvoiced_mask = pitch_contour > 0\n\npitch_comparison_data = [\n (sparc_pitch_time, pitch_sparc, \"SPARC pitch\", \"coral\"),\n (time_pitch[voiced_mask], pitch_contour[voiced_mask], \"Acoustic F0\", \"steelblue\"),\n]\n\nloudness_data = [(loud_time, loudness_sparc, \"SPARC Loudness\", \"coral\")]\n\n# Selected EMA trajectories\nselected_channels = [(\"TT_y\", 1), (\"TB_y\", 3), (\"UL_y\", 7), (\"LL_y\", 9)]\nema_data = []\nfor name, ch_idx in selected_channels:\n artic = name.split(\"_\")[0]\n color_map = {\n \"TT\": \"#43B96B\",\n \"TB\": \"#3EADD8\",\n \"UL\": \"#EE3A5B\",\n \"LL\": \"#FFD155\",\n }\n signal = ema[ch_idx] - ema[ch_idx].mean()\n ema_data.append((time_ema, signal, name, color_map.get(artic, \"gray\")))\n\nplot_aligned_panels(\n sentence1_16k,\n [\n {\"type\": \"waveform\"},\n {\"type\": \"features\", \"data\": pitch_comparison_data},\n {\"type\": \"features\", \"data\": loudness_data},\n {\"type\": \"features\", \"data\": ema_data},\n {\"type\": \"segments\", \"segments\": ppg_segments},\n ],\n title=\"Acoustic vs Articulatory Representations (side by side)\",\n spectrogram_params=spec_params,\n)\nplt.show()"
627+
"source": [
628+
"# Get PPG segments for the comparison\nframe_major = to_frame_major_posteriorgram(ppg1)\nsegments = extract_ppg_segments(sentence1_16k, frame_major)\nppg_segments = [\n {\"label\": seg[\"phoneme\"], \"start\": seg[\"start_seconds\"], \"end\": seg[\"start_seconds\"] + seg[\"duration_seconds\"]}\n for seg in segments\n]\n\n# Build SPARC pitch and acoustic pitch as feature overlays\nsparc_pitch_time = np.arange(len(pitch_sparc)) / 50.0\nvoiced_mask = (pitch_contour > 50) & (pitch_contour < 350)\n\npitch_comparison_data = [\n (sparc_pitch_time, pitch_sparc, \"SPARC pitch\", \"coral\"),\n (time_pitch[voiced_mask], pitch_contour[voiced_mask], \"Acoustic F0\", \"steelblue\"),\n]\n\nloudness_data = [(loud_time, loudness_sparc, \"SPARC Loudness\", \"coral\")]\n\n# Selected EMA trajectories\nselected_channels = [(\"TT_y\", 1), (\"TB_y\", 3), (\"UL_y\", 7), (\"LL_y\", 9)]\nema_data = []\nfor name, ch_idx in selected_channels:\n artic = name.split(\"_\")[0]\n color_map = {\n \"TT\": \"#43B96B\",\n \"TB\": \"#3EADD8\",\n \"UL\": \"#EE3A5B\",\n \"LL\": \"#FFD155\",\n }\n signal = ema[ch_idx] - ema[ch_idx].mean()\n ema_data.append((time_ema, signal, name, color_map.get(artic, \"gray\")))\n\nplot_aligned_panels(\n sentence1_16k,\n [\n {\"type\": \"waveform\"},\n {\"type\": \"features\", \"data\": pitch_comparison_data},\n {\"type\": \"features\", \"data\": loudness_data},\n {\"type\": \"features\", \"data\": ema_data},\n {\"type\": \"segments\", \"segments\": ppg_segments},\n ],\n title=\"Acoustic vs Articulatory Representations (side by side)\",\n spectrogram_params=spec_params,\n figsize=(16, 18),\n)\nplt.show()"
629+
]
568630
},
569631
{
570632
"cell_type": "markdown",

0 commit comments

Comments
 (0)