|
51 | 51 | "execution_count": null, |
52 | 52 | "metadata": {}, |
53 | 53 | "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 | + ] |
55 | 91 | }, |
56 | 92 | { |
57 | 93 | "cell_type": "markdown", |
|
140 | 176 | "metadata": {}, |
141 | 177 | "outputs": [], |
142 | 178 | "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\")" |
144 | 216 | ] |
145 | 217 | }, |
146 | 218 | { |
|
277 | 349 | "]\n", |
278 | 350 | "\n", |
279 | 351 | "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", |
280 | 355 | "n_channels = min(ema.shape[0], len(CHANNEL_NAMES))\n", |
281 | 356 | "n_frames = ema.shape[1]\n", |
282 | 357 | "time_ema = np.arange(n_frames) / 50.0 # SPARC runs at 50 Hz\n", |
|
444 | 519 | "metadata": {}, |
445 | 520 | "outputs": [], |
446 | 521 | "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()" |
465 | 523 | ] |
466 | 524 | }, |
467 | 525 | { |
|
535 | 593 | "execution_count": null, |
536 | 594 | "metadata": {}, |
537 | 595 | "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 | + ] |
539 | 599 | }, |
540 | 600 | { |
541 | 601 | "cell_type": "markdown", |
|
564 | 624 | "execution_count": null, |
565 | 625 | "metadata": {}, |
566 | 626 | "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 | + ] |
568 | 630 | }, |
569 | 631 | { |
570 | 632 | "cell_type": "markdown", |
|
0 commit comments