Skip to content

Instantly share code, notes, and snippets.

@tam17aki
Created November 7, 2025 12:47
Show Gist options
  • Select an option

  • Save tam17aki/1adf19685e0dbd367fab606ab8d4c6ab to your computer and use it in GitHub Desktop.

Select an option

Save tam17aki/1adf19685e0dbd367fab606ab8d4c6ab to your computer and use it in GitHub Desktop.
# -*- coding: utf-8 -*-
"""Plots vocal tract area functions for Japanese vowels.
This script processes vocal tract diameter data from Arai (2007) to
generate and display interpolated area function graphs for the five
standard Japanese vowels.
Copyright (C) 2025 by Akira TAMAMORI
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.
"""
import matplotlib.pyplot as plt
import numpy as np
import numpy.typing as npt
from scipy.interpolate import PchipInterpolator
def get_vowel_area_data() -> dict[str, npt.NDArray[np.float64]]:
"""Provides vocal tract diameter data for the five Japanese vowels.
Returns:
dict[str, np.ndarray]: A dictionary where keys are vowel names
(e.g., '/a/') and values are numpy arrays
of diameters in millimeters.
"""
# Diameters (in mm) for the five Japanese vowels from Arai (2007),
# Table 1. Index 1 corresponds to the lips, and index 16 to the
# glottis.
vowel_data = {
"/i/": np.array(
[24, 14, 12, 10, 10, 10, 16, 24, 32, 32, 32, 32, 32, 32, 12, 12]
),
"/e/": np.array(
[24, 22, 22, 20, 18, 16, 16, 18, 24, 28, 30, 30, 30, 30, 12, 12]
),
"/a/": np.array(
[32, 28, 30, 34, 38, 38, 34, 30, 26, 20, 14, 12, 16, 26, 12, 12]
),
"/o/": np.array(
[14, 22, 26, 32, 38, 38, 34, 28, 22, 16, 14, 16, 22, 30, 12, 12]
),
"/u/": np.array(
[16, 14, 20, 22, 24, 26, 22, 14, 18, 26, 30, 30, 30, 30, 12, 12]
),
}
return vowel_data
def process_diameter_data(
diameter_mm: npt.NDArray[np.float64],
num_sections: int = 16,
section_length_m: float = 0.01,
) -> tuple[npt.NDArray[np.float64], npt.NDArray[np.float64]]:
"""Processes diameter data into position and area arrays.
Args:
diameter_mm (np.ndarray):
Array of vocal tract diameters in millimeters.
num_sections (int):
The number of sections in the vocal tract model.
section_length_m (float):
The length of each section in meters.
Returns:
Tuple[np.ndarray, np.ndarray]:
A tuple containing:
- pos_m: Position from the glottis in meters.
- area_cm2: Cross-sectional area in square centimeters.
Raises:
ValueError: If the input diameter is not the number of sections.
"""
if len(diameter_mm) != num_sections:
raise ValueError(
f"Input diameter data must have {num_sections} elements."
)
index = np.arange(1, num_sections + 1)
# Position from glottis [m] The center of section `i` (from glottis)
# is at (i-0.5)*section_length Index 16 is section 1, Index 1 is
# section 16.
pos_m = (num_sections + 0.5 - index) * section_length_m
# Cross-sectional area [cm^2] for plotting
radius_cm = (diameter_mm / 2) / 10
area_cm2 = np.pi * radius_cm**2
return pos_m.astype(np.float64), area_cm2
def create_interpolator(
pos_m: np.ndarray, area_cm2: np.ndarray
) -> PchipInterpolator:
"""Creates a PchipInterpolator from position and area data.
Args:
pos_m (np.ndarray): Position data in meters.
area_cm2 (np.ndarray): Area data in square centimeters.
Returns:
PchipInterpolator: An interpolator function.
"""
# Sort data by position, as required by the interpolator
sort_indices = np.argsort(pos_m)
pos_m_sorted = pos_m[sort_indices]
area_cm2_sorted = area_cm2[sort_indices]
return PchipInterpolator(pos_m_sorted, area_cm2_sorted)
def plot_vocal_tract_area(
vowel_name: str,
pos_m_data: npt.NDArray[np.float64],
area_cm2_data: npt.NDArray[np.float64],
interpolator: PchipInterpolator,
l_vt_m: float,
) -> None:
"""Processes diameter data into position and area arrays.
Args:
vowel_name (str):
The name of the vowel (e.g., '/a/').
pos_m_data (np.ndarray):
Original position data points in meters.
area_cm2_data (np.ndarray):
Original area data points in square centimeters.
interpolator (PchipInterpolator):
The interpolator function.
l_vt_m (float):
Total length of the vocal tract in meters.
"""
# Generate smooth data for plotting the interpolated curve
pos_m_smooth = np.linspace(0, l_vt_m, 1000)
area_cm2_smooth = interpolator(pos_m_smooth)
# Create the plot
plt.figure(figsize=(10, 6))
# Plot interpolated curve
plt.plot(
pos_m_smooth * 100,
area_cm2_smooth,
label="PCHIP Interpolation",
color="royalblue",
linewidth=2,
)
# Plot original data points
plt.plot(
pos_m_data * 100,
area_cm2_data,
"o",
label="Original Data Points (Arai, 2007)",
color="crimson",
markersize=8,
)
# Formatting
plt.title(f"Vocal Tract Area Function for Vowel {vowel_name}", fontsize=16)
plt.xlabel("Position from Glottis [cm]", fontsize=12)
plt.ylabel("Cross-sectional Area [cm$^2$]", fontsize=12)
plt.grid(True, linestyle="--", alpha=0.6)
plt.legend(fontsize=11)
plt.xlim(0, l_vt_m * 100)
plt.ylim(bottom=0)
# Add text labels
max_area = max(area_cm2_data) * 0.9
plt.text(1, max_area, "Glottis side", ha="left")
plt.text(l_vt_m * 100 - 1, max_area, "Lips side", ha="right")
plt.show()
def main() -> None:
"""Perform plot."""
target_vowels = ["/i/", "/e/", "/a/", "/o/", "/u/"] # Plot all vowels
num_sections = 16
section_length_m = 0.01
vocal_tract_length_m = num_sections * section_length_m
# --- Main Logic ---
all_vowel_data = get_vowel_area_data()
for vowel in target_vowels:
if vowel in all_vowel_data:
# 1. Get raw diameter data
diameter_data = all_vowel_data[vowel]
# 2. Process data to get position and area
pos_data, area_data = process_diameter_data(
diameter_data, num_sections, section_length_m
)
# 3. Create an interpolator
interpolator_func = create_interpolator(pos_data, area_data)
# 4. Plot the results
plot_vocal_tract_area(
vowel,
pos_data,
area_data,
interpolator_func,
vocal_tract_length_m,
)
else:
print(f"Warning: Data for vowel '{vowel}' not found.")
if __name__ == "__main__":
main()
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment