Repository: TakedaLab/B4Lecture-2025
Pull request #7: 石川雄斗 EX1 解答 python
Author: login: luna-moonlight1412
Labels: name: EX1
Created: 2025-04-16T05:31:21Z | Merged: 2025-04-25T09:36:23Z

# What

- STFTとその逆変換を実装

- 実行コマンド
 `$ python main.py <path/to/input/audio> <path/to/output/result>`

 オプション
| パラメータ    | デフォルト    | 説明         |
|-------------|-------------|-------------|
| --help      | ─           | ヘルプを表示します。 |
| --filter_length      | 1024           | フィルタのサイズを設定します。 |
| --overlap_rate      | 0.5          | オーバーラップの割合を設定します。(0 ~ 1) |
| --window      | hamming          | 窓関数を設定します。(hamming, hann, rect) |

# Result

![result](https://github.com/user-attachments/assets/512ae2cf-8050-4f10-85ec-cff86b5d1e86)

# WIP [オプション]

現在、逆変換の際に重複する部分は後のフィルタで上書きする実装になっているが、正しく逆変換できているのかわからない。(結果の見た目では、オリジナルとほぼ変わらない)



Diff:
diff --git a/ex1/y_ishikawa/.gitignore b/ex1/y_ishikawa/.gitignore
new file mode 100644
index 00000000..3d3f0a80
--- /dev/null
+++ b/ex1/y_ishikawa/.gitignore
@@ -0,0 +1,3 @@
+.DS_Store
+__pycache__/
+.vscode
\ No newline at end of file
diff --git a/ex1/y_ishikawa/audio.wav b/ex1/y_ishikawa/audio.wav
new file mode 100644
index 00000000..ae35bdbf
Binary files /dev/null and b/ex1/y_ishikawa/audio.wav differ
diff --git a/ex1/y_ishikawa/main.py b/ex1/y_ishikawa/main.py
new file mode 100644
index 00000000..759c1b3f
--- /dev/null
+++ b/ex1/y_ishikawa/main.py
@@ -0,0 +1,196 @@
+"""Visualize the STFT spectrogram of an audio file and reconstruct the waveform."""
+
+import argparse
+from pathlib import Path
+from typing import Literal
+
+import matplotlib.gridspec as gridspec
+import matplotlib.pyplot as plt
+import numpy as np
+import soundfile as sf
+from stft import inv_stft, stft
+
+
+def magphase(
+    complex_array: np.ndarray[complex],
+) -> tuple[np.ndarray[float], np.ndarray[float]]:
+    """Split complex to magnitude and phase.
+
+    Parameters
+    ----------
+    complex_array : np.ndarray[complex]
+        Array containing complex numbers.
+
+    Returns
+    -------
+    magnitude : np.ndarray[float]
+        Array containing magnitudes.
+    phase : np.ndarray[float]
+        Array containing phases (in radians).
+    """
+    magnitude = np.abs(complex_array)
+    phase = np.angle(complex_array)
+
+    return magnitude, phase
+
+
+def mag_to_db(magnitude: np.ndarray[float], eps: float = 1e-7) -> np.ndarray[float]:
+    """Convert magnitude to dB.
+
+    Parameters
+    ----------
+    magnitude : np.ndarray[float]
+        Array containing magnitudes.
+
+    eps : float, default 1e-7
+        Small constant added to avoid taking the logarithm of zero.
+
+    Returns
+    -------
+    db : np.ndarray[float]
+        Array converted to dB scale.
+    """
+    magnitude = np.clip(magnitude, eps, None)
+    db = 20 * np.log10(magnitude)
+
+    return db
+
+
+class NameSpace:
+    """Configuration namespace for audio processing parameters.
+
+    Parameters
+    ----------
+    input_path : Path
+        Path to the input audio file. (.wav)
+    output_path : Path
+        Path where the processed output will be saved. (.png)
+    filter_length : int
+        Length of the filter to be applied, in samples.
+    overlap_rate : float
+        Overlap rate between consecutive windows. (0 ~ 1)
+    window : {"hamming", "hann", "rect"}
+        Type of window function to apply during STFT.
+    """
+
+    input_path: Path
+    output_path: Path
+    filter_length: int
+    overlap_rate: float
+    window: Literal["hamming", "hann", "rect"]
+
+
+def parse_args() -> NameSpace:
+    """Parse command-line arguments.
+
+    Returns
+    -------
+    arguments : NameSpace
+        Parsed arguments including input/output paths, filter parameters, and window type.
+    """
+    # prepare arguments
+    parser = argparse.ArgumentParser()
+    parser.add_argument(
+        "input_path", type=Path, help="Path to input audio file. (.wav)"
+    )
+    parser.add_argument(
+        "output_path",
+        type=Path,
+        help="Path to output result file. (.png)",
+    )
+    parser.add_argument(
+        "--filter_length",
+        type=int,
+        default=1024,
+        help="Size of filter for STFT. Default 1024",
+    )
+    parser.add_argument(
+        "--overlap_rate",
+        type=float,
+        default=0.5,
+        help="Rate of overlap for STFT (0 ~ 1). Default 0.5",
+    )
+    parser.add_argument(
+        "--window",
+        type=str,
+        default="hamming",
+        choices=["hamming", "hann", "rect"],
+        help="Using window function (hamming, hann, rect). Default hamming",
+    )
+
+    return parser.parse_args(namespace=NameSpace())
+
+
+if __name__ == "__main__":
+    # get arguments
+    args = parse_args()
+    audio_file_path = args.input_path
+    result_path = args.output_path
+    filter_length = args.filter_length
+    overlap_rate = args.overlap_rate
+    window = args.window
+
+    # load audio
+    data, sample_rate = sf.read(audio_file_path)
+    nyquist_frequency = sample_rate // 2
+    audio_frame = len(data)
+    audio_time = audio_frame / sample_rate
+
+    # process STFT
+    stft_result = stft(data, filter_length, overlap_rate, window)
+    magnitude, _ = magphase(stft_result)
+    spectrogram = mag_to_db(magnitude)
+
+    # remove after nyquist frequency
+    spectrogram = spectrogram[: spectrogram.shape[0] // 2]
+
+    # process ISTFT
+    istft_result = inv_stft(
+        stft_result, audio_frame, filter_length, overlap_rate, window
+    )
+
+    # prepare for graph
+    x = np.linspace(0, audio_time, audio_frame)
+    fig = plt.figure(figsize=(10, 6))
+    gs = gridspec.GridSpec(3, 2, width_ratios=[50, 1], wspace=0.05, hspace=0.7)
+
+    # draw original audio signal
+    ax1 = fig.add_subplot(gs[0, 0])
+    ax1.plot(x, data)
+    ax1.set_xlim(0, audio_time)
+    ax1.set_ylim(-1, 1)
+    ax1.set_xlabel("Time [s]")
+    ax1.set_ylabel("Amplitude")
+    ax1.set_title("Original audio signal")
+
+    # draw spectrogram
+    ax2 = fig.add_subplot(gs[1, 0])
+    image = ax2.imshow(
+        spectrogram,
+        cmap="jet",
+        aspect="auto",
+        extent=[0, audio_time, 0, nyquist_frequency // 1000],
+        origin="lower",
+    )
+    ax2.set_xlim(0, audio_time)
+    ax2.set_ylim(0, nyquist_frequency // 1000)
+    ax2.set_xlabel("Time [s]")
+    ax2.set_ylabel("Frequency [kHz]")
+    ax2.set_title("Spectrogram")
+
+    # draw color bar
+    cax = fig.add_subplot(gs[1, 1])
+    fig.colorbar(image, cax=cax, format="%.0f dB")
+
+    # draw re-synthesized audio signal
+    ax3 = fig.add_subplot(gs[2, 0])
+    ax3.plot(x, istft_result)
+    ax3.set_xlim(0, audio_time)
+    ax3.set_ylim(-1, 1)
+    ax3.set_xlabel("Time [s]")
+    ax3.set_ylabel("Amplitude")
+    ax3.set_title("Re-synthesized audio signal")
+
+    # show graph
+    plt.savefig(result_path)
+    plt.show()
diff --git a/ex1/y_ishikawa/pyproject.toml b/ex1/y_ishikawa/pyproject.toml
new file mode 100644
index 00000000..3a637ce0
--- /dev/null
+++ b/ex1/y_ishikawa/pyproject.toml
@@ -0,0 +1,15 @@
+[tool.ruff]
+target-version = "py310"
+line-length = 88
+indent-width = 4
+
+[tool.ruff.lint]
+select = ["E", "F", "I", "PLR", "B", "ANN"]
+fixable = ["ALL"]
+unfixable = []
+
+[tool.ruff.format]
+quote-style = "double"
+indent-style = "space"
+docstring-code-format = true
+docstring-code-line-length = 60
\ No newline at end of file
diff --git a/ex1/y_ishikawa/requirements.txt b/ex1/y_ishikawa/requirements.txt
new file mode 100644
index 00000000..762760b1
--- /dev/null
+++ b/ex1/y_ishikawa/requirements.txt
@@ -0,0 +1,3 @@
+matplotlib==3.5.2
+numpy==1.26.4
+soundfile==0.13.0
\ No newline at end of file
diff --git a/ex1/y_ishikawa/result.png b/ex1/y_ishikawa/result.png
new file mode 100644
index 00000000..8088cf25
Binary files /dev/null and b/ex1/y_ishikawa/result.png differ
diff --git a/ex1/y_ishikawa/stft.py b/ex1/y_ishikawa/stft.py
new file mode 100644
index 00000000..a1cc8b8c
--- /dev/null
+++ b/ex1/y_ishikawa/stft.py
@@ -0,0 +1,189 @@
+"""Provides STFT and inverse STFT functions for 1D audio signal processing."""
+
+import numpy as np
+
+
+def back_padding(data: np.ndarray, length: int) -> np.ndarray:
+    """Pad a 1D array with zeros at the end.
+
+    Parameters
+    ----------
+    data : np.ndarray
+        1D array to be padded.
+
+    length : int
+        Desired total length of the output array.
+
+    Returns
+    -------
+    padded_data : np.ndarray
+        1D array of length `length`.
+        If `data` is shorter than `length`, zeros are appended.
+    """
+    return np.append(data, np.zeros(length - len(data)))
+
+
+def split_data(
+    data: np.ndarray, length: int = 1024, overlap_rate: float = 0.5
+) -> list[np.ndarray]:
+    """Split a 1D array into overlapping segments.
+
+    Parameters
+    ----------
+    data : np.ndarray
+        1D array
+
+    length : int, default 1024
+        Segment length (number of samples per segment).
+
+    overlap_rate : float, default 0
+        Overlap rate between consecutive segments.
+        Should be in the range [0, 1).
+
+    Returns
+    -------
+    splitted_data : list[np.ndarray]
+        List of 1D arrays, each of length `length`.
+        If the last segment is shorter than `length`, zero-padded at the end.
+    """
+    if not (0 <= overlap_rate < 1):
+        raise ValueError("'overlap_rate' should be in the range [0, 1)")
+
+    step = int((1 - overlap_rate) * length)
+
+    splitted_data = []
+    for i in range(0, len(data), step):
+        splitted_data.append(back_padding(data[i : i + length], length))
+
+    return splitted_data
+
+
+def get_window_func(window: str, length: int) -> np.ndarray:
+    """Get window function.
+
+    Parameters
+    ----------
+    window : str
+        Using window function name. ("hamming", "hann", "rect")
+
+    filter_length: int
+        Size of window function.
+
+    Returns
+    -------
+    window_function : np.ndarray
+        Corresponding window function.
+    """
+    if window == "hamming":
+        return np.hamming(length)
+    elif window == "hann":
+        return np.hanning(length)
+    elif window == "rect":
+        return np.ones(length)
+
+
+def stft(
+    audio_data: np.ndarray,
+    filter_length: int,
+    overlap_rate: float,
+    window: str,
+) -> np.ndarray:
+    """Calculate Short-Time Fourier Transform (STFT) of a 1D audio signal.
+
+    Parameters
+    ----------
+    audio_data : np.ndarray
+        1D array of audio signal.
+
+    filter_length: int
+        Size of filter.
+
+    overlap_rate: float
+        Overlap rate for splitting data. (0 ~ 1)
+
+    window : str
+        Using window function name. ("hamming", "hann", "rect")
+
+    Returns
+    -------
+    spectrogram : np.ndarray
+        2D array of complex STFT coefficients.
+    """
+    # get window function
+    window_func = get_window_func(window, filter_length)
+
+    stft_result = []
+    for splitted_audio_data in split_data(audio_data, filter_length, overlap_rate):
+        # apply window function
+        splitted_audio_data *= window_func
+
+        # process FFT
+        fft_result = np.fft.fft(splitted_audio_data)
+
+        # add to result
+        stft_result.append(fft_result)
+
+    return np.stack(stft_result, axis=1)
+
+
+def inv_stft(
+    stft_result: np.ndarray[complex],
+    original_audio_frame: int,
+    filter_length: int,
+    overlap_rate: float,
+    window: str,
+    eps: float = 1e-7,
+) -> np.ndarray:
+    """Reconstruct a 1D audio signal from STFT result.
+
+    Parameters
+    ----------
+    stft_result : np.ndarray[complex]
+        2D array of complex STFT coefficients.
+
+    original_audio_frame: int
+        Total number of frames in the original audio signal before STFT.
+
+    filter_length: int
+        Size of filter.
+
+    overlap_rate: float
+        Overlap rate for splitting data. (0 ~ 1)
+
+    window : str
+        Using window function name. ("hamming", "hann", "rect")
+
+    eps : float, default 1e-7
+        Small constant added to avoid taking the logarithm of zero.
+
+    Returns
+    -------
+    audio_data : np.ndarray
+        1D array of audio signal.
+    """
+    # get window function
+    window_func = get_window_func(window, filter_length)
+
+    step = int((1 - overlap_rate) * filter_length)
+
+    audio_data = np.zeros(original_audio_frame + filter_length)
+    norm = np.zeros(original_audio_frame + filter_length)
+    for i, fft_result in enumerate(stft_result.T):
+        # process IFFT
+        ifft_result = np.fft.ifft(fft_result)
+
+        # use only real value
+        ifft_result = np.real(ifft_result)
+
+        # prepare to remove window function
+        ifft_result *= window_func
+        norm[step * i : step * i + filter_length] += window_func**2
+
+        # add to result
+        audio_data[step * i : step * i + filter_length] += ifft_result
+
+    # remove window function
+    norm = np.clip(norm, eps, None)
+    audio_data /= norm
+
+    return np.array(audio_data[:original_audio_frame])


Reviews:
user: login: zodiac-18
body: とてもよく書けていると思います.
適切に関数分割・モジュール化されていて, とても読みやすかったです.
argparse, docstring, 型アノテーションも使いこなせていて素晴らしいですね.

以下, 簡単なコメントです. もし私のコメントが間違っていたら教えて下さい.
state: CHANGES_REQUESTED
author_association: COLLABORATOR
submitted_at: 2025-04-20T06:28:43Z
user: login: luna-moonlight1412
state: COMMENTED
author_association: COLLABORATOR
submitted_at: 2025-04-20T08:30:42Z
user: login: luna-moonlight1412
state: COMMENTED
author_association: COLLABORATOR
submitted_at: 2025-04-20T08:32:45Z
user: login: luna-moonlight1412
state: COMMENTED
author_association: COLLABORATOR
submitted_at: 2025-04-20T08:34:00Z
user: login: zodiac-18
state: CHANGES_REQUESTED
author_association: COLLABORATOR
submitted_at: 2025-04-20T08:53:41Z
user: login: zodiac-18
body: お疲れ様でした。
LGTM
state: APPROVED
author_association: COLLABORATOR
submitted_at: 2025-04-20T09:52:53Z

Review comments:
diff_hunk: @@ -0,0 +1,179 @@
+import numpy as np
+
+
+def back_padding(data: np.ndarray, length: int) -> np.ndarray:
+    """Pad a 1D array with zeros at the end.
+
+    Parameters
+    ----------
+    data : np.ndarray
+        1D array to be padded.
+
+    length : int
+        Desired total length of the output array.
+
+    Returns
+    -------
+    padded_data : np.ndarray
+        1D array of length `length`.
+        If `data` is shorter than `length`, zeros are appended.
+    """
+    return np.append(data, np.zeros(length - len(data)))
+
+
+def split_data(
+    data: np.ndarray, length: int = 1024, overlap_rate: float = 0.5
+) -> list[np.ndarray]:
+    """Split a 1D array into overlapping segments.
+
+    Parameters
+    ----------
+    data : np.ndarray
+        1D array
+
+    length : int, default 1024
+        Segment length (number of samples per segment).
+
+    overlap_rate : float, default 0
+        Overlap rate between consecutive segments.
+        Should be in the range [0, 1).
+
+    Returns
+    -------
+    splitted_data : list[np.ndarray]
+        List of 1D arrays, each of length `length`.
+        If the last segment is shorter than `length`, zero-padded at the end.
+    """
+    if not (0 <= overlap_rate < 1):
+        raise ValueError("'overlap_rate' should be in the range [0, 1)")
+
+    step = int((1 - overlap_rate) * length)
+
+    splitted_data = []
+    for i in range(0, len(data), step):
+        splitted_data.append(back_padding(data[i : i + length], length))
+
+    return splitted_data
+
+
+def get_window_func(window: str, length: int) -> np.ndarray:
+    """Get window function.
+
+    Parameters
+    ----------
+    window : str
+        Using window function name. ("hamming", "hann", "rect")
+
+    filter_length: int
+        Size of window function.
+
+    Returns
+    -------
+    window_function : np.ndarray
+        Corresponding window function.
+    """
+    if window == "hamming":
+        return np.hamming(length)
+    elif window == "hann":
+        return np.hanning(length)
+    elif window == "rect":
+        return np.ones(length)
+
+
+def stft(
+    audio_data: np.ndarray,
+    filter_length: int,
+    overlap_rate: float,
+    window: str,
+) -> np.ndarray:
+    """Calculate Short-Time Fourier Transform (STFT) of a 1D audio signal.
+
+    Parameters
+    ----------
+    audio_data : np.ndarray
+        1D array of audio signal.
+
+    filter_length: int
+        Size of filter.
+
+    overlap_rate: float
+        Overlap rate for splitting data. (0 ~ 1)
+
+    window : str
+        Using window function name. ("hamming", "hann", "rect")
+
+    Returns
+    -------
+    spectrogram : np.ndarray
+        2D array of complex STFT coefficients.
+    """
+    # get window function
+    window_func = get_window_func(window, filter_length)
+
+    stft_result = []
+    for splitted_audio_data in split_data(
+        audio_data, filter_length, overlap_rate
+    ):
+        # apply window function
+        splitted_audio_data *= window_func
+
+        # process FFT
+        fft_result = np.fft.fft(splitted_audio_data)
+
+        # add to result
+        stft_result.append(fft_result)
+
+    return np.stack(stft_result, axis=1)
+
+
+def inv_stft(
+    stft_result: np.ndarray[complex],
+    original_audio_frame: int,
+    filter_length: int,
+    overlap_rate: float,
+    window: str,
+) -> np.ndarray:
+    """Reconstruct a 1D audio signal from STFT result.
+
+    Parameters
+    ----------
+    stft_result : np.ndarray[complex]
+        2D array of complex STFT coefficients.
+
+    original_audio_frame: int
+        Total number of frames in the original audio signal before STFT.
+
+    filter_length: int
+        Size of filter.
+
+    overlap_rate: float
+        Overlap rate for splitting data. (0 ~ 1)
+
+    window : str
+        Using window function name. ("hamming", "hann", "rect")
+
+    Returns
+    -------
+    audio_data : np.ndarray
+        1D array of audio signal.
+    """
+    # get window function
+    window_func = get_window_func(window, filter_length)
+
+    step = int((1 - overlap_rate) * filter_length)
+
+    audio_data = np.zeros(original_audio_frame + filter_length)
+    for i, fft_result in enumerate(stft_result.T):
+        # process IFFT
+        ifft_result = np.fft.ifft(fft_result)
+
+        # use only real value
+        ifft_result = np.real(ifft_result)
+
+        # remove window function
+        ifft_result /= window_func
+
+        # add to result
+        audio_data[step * i: step * i + filter_length] = ifft_result
path: ex1/y_ishikawa/stft.py
user: login: zodiac-18
body: 現在はオーバーラップする部分を上書きしていると思いますが、重畳加算するのが正しいと思います。
`audio_data[step * i: step * i + filter_length] += ifft_result`
created_at: 2025-04-20T06:03:49Z
author_association: COLLABORATOR
diff_hunk: @@ -0,0 +1,167 @@
+import argparse
+from pathlib import Path
+from typing import Literal
+
+import matplotlib.gridspec as gridspec
+import matplotlib.pyplot as plt
+import numpy as np
+import soundfile as sf
+
+from stft import inv_stft, stft
+
+
+def magphase(
+    complex_array: np.ndarray[complex],
+) -> tuple[np.ndarray[float], np.ndarray[float]]:
+    """Split complex to magnitude and phase.
+
+    Parameters
+    ----------
+    complex_array : np.ndarray[complex]
+        Array containing complex numbers.
+
+    Returns
+    -------
+    magnitude : np.ndarray[float]
+        Array containing magnitudes.
+    phase : np.ndarray[float]
+        Array containing phases (in radians).
+    """
+    magnitude = np.abs(complex_array)
+    phase = np.angle(complex_array)
+
+    return magnitude, phase
+
+
+def mag_to_db(magnitude: np.ndarray[float]) -> np.ndarray[float]:
+    """Convert magnitude to dB.
+
+    Parameters
+    ----------
+    magnitude : np.ndarray[float]
+        Array containing magnitudes.
+
+    Returns
+    -------
+    db : np.ndarray[float]
+        Array converted to dB scale.
+    """
+    db = 20 * np.log10(magnitude)
path: ex1/y_ishikawa/main.py
user: login: zodiac-18
body: 今回は不要かもしれないですが、この実装だと音声信号の成分が0になったときに-infになってしまいます。そのため、無視できるほどの非常に小さい値でクリッピングしてからdb変換を行うことがよくあります。
```
magnitude = np.clip(magnitude, 1e-7, None)
db = 20 * np.log10(magnitude)
```
created_at: 2025-04-20T06:11:25Z
author_association: COLLABORATOR
diff_hunk: @@ -0,0 +1,167 @@
+import argparse
+from pathlib import Path
+from typing import Literal
+
+import matplotlib.gridspec as gridspec
+import matplotlib.pyplot as plt
+import numpy as np
+import soundfile as sf
+
+from stft import inv_stft, stft
+
+
+def magphase(
+    complex_array: np.ndarray[complex],
+) -> tuple[np.ndarray[float], np.ndarray[float]]:
+    """Split complex to magnitude and phase.
+
+    Parameters
+    ----------
+    complex_array : np.ndarray[complex]
+        Array containing complex numbers.
+
+    Returns
+    -------
+    magnitude : np.ndarray[float]
+        Array containing magnitudes.
+    phase : np.ndarray[float]
+        Array containing phases (in radians).
+    """
+    magnitude = np.abs(complex_array)
+    phase = np.angle(complex_array)
+
+    return magnitude, phase
+
+
+def mag_to_db(magnitude: np.ndarray[float]) -> np.ndarray[float]:
+    """Convert magnitude to dB.
+
+    Parameters
+    ----------
+    magnitude : np.ndarray[float]
+        Array containing magnitudes.
+
+    Returns
+    -------
+    db : np.ndarray[float]
+        Array converted to dB scale.
+    """
+    db = 20 * np.log10(magnitude)
+
+    return db
+
+
+class NameSpace:
+    input_path: Path
+    output_path: Path
+    filter_length: int
+    overlap_rate: float
+    window: Literal["hamming", "hann", "rect"]
+
+
+def parse_args() -> NameSpace:
+    # prepare arguments
+    parser = argparse.ArgumentParser()
+    parser.add_argument(
+        "input_path", type=Path, help="Path to input audio file. (.wav)"
+    )
+    parser.add_argument(
+        "output_path",
+        type=Path,
+        help="Path to output result file. (.png)",
+    )
+    parser.add_argument(
+        "--filter_length",
+        type=int,
+        default=1024,
+        help="Size of filter for STFT. Default 1024",
+    )
+    parser.add_argument(
+        "--overlap_rate",
+        type=float,
+        default=0.5,
+        help="Rate of overlap for STFT (0 ~ 1). Default 0.5",
+    )
+    parser.add_argument(
+        "--window",
+        type=str,
+        default="hamming",
+        choices=["hamming", "hann", "rect"],
+        help="Using window function (hamming, hann, rect). Default hamming",
+    )
+
+    return parser.parse_args(namespace=NameSpace())
+
+
+if __name__ == "__main__":
+    # get arguments
+    args = parse_args()
+    audio_file_path = args.input_path
+    result_path = args.output_path
+    filter_length = args.filter_length
+    overlap_rate = args.overlap_rate
+    window = args.window
+
+    # load audio
+    data, sample_rate = sf.read(audio_file_path)
+    nyquist_frequency = sample_rate // 2
+    audio_frame = len(data)
+    audio_time = audio_frame / sample_rate
+
+    # process STFT
+    stft_result = stft(data, filter_length, overlap_rate, window)
+    magnitude, _ = magphase(stft_result)
+    spectrogram = mag_to_db(magnitude)
+
+    # remove after nyquist frequency
+    spectrogram = spectrogram[spectrogram.shape[0] // 2 :]
path: ex1/y_ishikawa/main.py
user: login: zodiac-18
body: これだと逆にナイキスト周波数以上の周波数が残ってしまう気がします。
正しくは
`spectrogram = spectrogram[:spectrogram.shape[0] // 2]`
ですかね。
created_at: 2025-04-20T06:17:20Z
author_association: COLLABORATOR
diff_hunk: @@ -0,0 +1,179 @@
+import numpy as np
+
+
+def back_padding(data: np.ndarray, length: int) -> np.ndarray:
+    """Pad a 1D array with zeros at the end.
+
+    Parameters
+    ----------
+    data : np.ndarray
+        1D array to be padded.
+
+    length : int
+        Desired total length of the output array.
+
+    Returns
+    -------
+    padded_data : np.ndarray
+        1D array of length `length`.
+        If `data` is shorter than `length`, zeros are appended.
+    """
+    return np.append(data, np.zeros(length - len(data)))
+
+
+def split_data(
+    data: np.ndarray, length: int = 1024, overlap_rate: float = 0.5
+) -> list[np.ndarray]:
+    """Split a 1D array into overlapping segments.
+
+    Parameters
+    ----------
+    data : np.ndarray
+        1D array
+
+    length : int, default 1024
+        Segment length (number of samples per segment).
+
+    overlap_rate : float, default 0
+        Overlap rate between consecutive segments.
+        Should be in the range [0, 1).
+
+    Returns
+    -------
+    splitted_data : list[np.ndarray]
+        List of 1D arrays, each of length `length`.
+        If the last segment is shorter than `length`, zero-padded at the end.
+    """
+    if not (0 <= overlap_rate < 1):
+        raise ValueError("'overlap_rate' should be in the range [0, 1)")
+
+    step = int((1 - overlap_rate) * length)
+
+    splitted_data = []
+    for i in range(0, len(data), step):
+        splitted_data.append(back_padding(data[i : i + length], length))
+
+    return splitted_data
+
+
+def get_window_func(window: str, length: int) -> np.ndarray:
+    """Get window function.
+
+    Parameters
+    ----------
+    window : str
+        Using window function name. ("hamming", "hann", "rect")
+
+    filter_length: int
+        Size of window function.
+
+    Returns
+    -------
+    window_function : np.ndarray
+        Corresponding window function.
+    """
+    if window == "hamming":
+        return np.hamming(length)
+    elif window == "hann":
+        return np.hanning(length)
+    elif window == "rect":
+        return np.ones(length)
+
+
+def stft(
+    audio_data: np.ndarray,
+    filter_length: int,
+    overlap_rate: float,
+    window: str,
+) -> np.ndarray:
+    """Calculate Short-Time Fourier Transform (STFT) of a 1D audio signal.
+
+    Parameters
+    ----------
+    audio_data : np.ndarray
+        1D array of audio signal.
+
+    filter_length: int
+        Size of filter.
+
+    overlap_rate: float
+        Overlap rate for splitting data. (0 ~ 1)
+
+    window : str
+        Using window function name. ("hamming", "hann", "rect")
+
+    Returns
+    -------
+    spectrogram : np.ndarray
+        2D array of complex STFT coefficients.
+    """
+    # get window function
+    window_func = get_window_func(window, filter_length)
+
+    stft_result = []
+    for splitted_audio_data in split_data(
+        audio_data, filter_length, overlap_rate
+    ):
+        # apply window function
+        splitted_audio_data *= window_func
+
+        # process FFT
+        fft_result = np.fft.fft(splitted_audio_data)
+
+        # add to result
+        stft_result.append(fft_result)
+
+    return np.stack(stft_result, axis=1)
+
+
+def inv_stft(
+    stft_result: np.ndarray[complex],
+    original_audio_frame: int,
+    filter_length: int,
+    overlap_rate: float,
+    window: str,
+) -> np.ndarray:
+    """Reconstruct a 1D audio signal from STFT result.
+
+    Parameters
+    ----------
+    stft_result : np.ndarray[complex]
+        2D array of complex STFT coefficients.
+
+    original_audio_frame: int
+        Total number of frames in the original audio signal before STFT.
+
+    filter_length: int
+        Size of filter.
+
+    overlap_rate: float
+        Overlap rate for splitting data. (0 ~ 1)
+
+    window : str
+        Using window function name. ("hamming", "hann", "rect")
+
+    Returns
+    -------
+    audio_data : np.ndarray
+        1D array of audio signal.
+    """
+    # get window function
+    window_func = get_window_func(window, filter_length)
+
+    step = int((1 - overlap_rate) * filter_length)
+
+    audio_data = np.zeros(original_audio_frame + filter_length)
+    for i, fft_result in enumerate(stft_result.T):
+        # process IFFT
+        ifft_result = np.fft.ifft(fft_result)
+
+        # use only real value
+        ifft_result = np.real(ifft_result)
+
+        # remove window function
+        ifft_result /= window_func
+
+        # add to result
+        audio_data[step * i: step * i + filter_length] = ifft_result
path: ex1/y_ishikawa/stft.py
user: login: luna-moonlight1412
body: 単純な加算だと振幅が大きくなってしまいました。

![Figure_1](https://github.com/user-attachments/assets/76b3a953-5160-4ee6-bfcb-bcdb517c30a5)
created_at: 2025-04-20T08:30:41Z
author_association: COLLABORATOR
diff_hunk: @@ -0,0 +1,167 @@
+import argparse
+from pathlib import Path
+from typing import Literal
+
+import matplotlib.gridspec as gridspec
+import matplotlib.pyplot as plt
+import numpy as np
+import soundfile as sf
+
+from stft import inv_stft, stft
+
+
+def magphase(
+    complex_array: np.ndarray[complex],
+) -> tuple[np.ndarray[float], np.ndarray[float]]:
+    """Split complex to magnitude and phase.
+
+    Parameters
+    ----------
+    complex_array : np.ndarray[complex]
+        Array containing complex numbers.
+
+    Returns
+    -------
+    magnitude : np.ndarray[float]
+        Array containing magnitudes.
+    phase : np.ndarray[float]
+        Array containing phases (in radians).
+    """
+    magnitude = np.abs(complex_array)
+    phase = np.angle(complex_array)
+
+    return magnitude, phase
+
+
+def mag_to_db(magnitude: np.ndarray[float]) -> np.ndarray[float]:
+    """Convert magnitude to dB.
+
+    Parameters
+    ----------
+    magnitude : np.ndarray[float]
+        Array containing magnitudes.
+
+    Returns
+    -------
+    db : np.ndarray[float]
+        Array converted to dB scale.
+    """
+    db = 20 * np.log10(magnitude)
+
+    return db
+
+
+class NameSpace:
+    input_path: Path
+    output_path: Path
+    filter_length: int
+    overlap_rate: float
+    window: Literal["hamming", "hann", "rect"]
+
+
+def parse_args() -> NameSpace:
+    # prepare arguments
+    parser = argparse.ArgumentParser()
+    parser.add_argument(
+        "input_path", type=Path, help="Path to input audio file. (.wav)"
+    )
+    parser.add_argument(
+        "output_path",
+        type=Path,
+        help="Path to output result file. (.png)",
+    )
+    parser.add_argument(
+        "--filter_length",
+        type=int,
+        default=1024,
+        help="Size of filter for STFT. Default 1024",
+    )
+    parser.add_argument(
+        "--overlap_rate",
+        type=float,
+        default=0.5,
+        help="Rate of overlap for STFT (0 ~ 1). Default 0.5",
+    )
+    parser.add_argument(
+        "--window",
+        type=str,
+        default="hamming",
+        choices=["hamming", "hann", "rect"],
+        help="Using window function (hamming, hann, rect). Default hamming",
+    )
+
+    return parser.parse_args(namespace=NameSpace())
+
+
+if __name__ == "__main__":
+    # get arguments
+    args = parse_args()
+    audio_file_path = args.input_path
+    result_path = args.output_path
+    filter_length = args.filter_length
+    overlap_rate = args.overlap_rate
+    window = args.window
+
+    # load audio
+    data, sample_rate = sf.read(audio_file_path)
+    nyquist_frequency = sample_rate // 2
+    audio_frame = len(data)
+    audio_time = audio_frame / sample_rate
+
+    # process STFT
+    stft_result = stft(data, filter_length, overlap_rate, window)
+    magnitude, _ = magphase(stft_result)
+    spectrogram = mag_to_db(magnitude)
+
+    # remove after nyquist frequency
+    spectrogram = spectrogram[spectrogram.shape[0] // 2 :]
path: ex1/y_ishikawa/main.py
user: login: luna-moonlight1412
body: 対称性のおかげでimshowの出力結果がたまたま一致していました。
意図していた挙動ではないので直します。
created_at: 2025-04-20T08:32:45Z
author_association: COLLABORATOR
diff_hunk: @@ -0,0 +1,167 @@
+import argparse
+from pathlib import Path
+from typing import Literal
+
+import matplotlib.gridspec as gridspec
+import matplotlib.pyplot as plt
+import numpy as np
+import soundfile as sf
+
+from stft import inv_stft, stft
+
+
+def magphase(
+    complex_array: np.ndarray[complex],
+) -> tuple[np.ndarray[float], np.ndarray[float]]:
+    """Split complex to magnitude and phase.
+
+    Parameters
+    ----------
+    complex_array : np.ndarray[complex]
+        Array containing complex numbers.
+
+    Returns
+    -------
+    magnitude : np.ndarray[float]
+        Array containing magnitudes.
+    phase : np.ndarray[float]
+        Array containing phases (in radians).
+    """
+    magnitude = np.abs(complex_array)
+    phase = np.angle(complex_array)
+
+    return magnitude, phase
+
+
+def mag_to_db(magnitude: np.ndarray[float]) -> np.ndarray[float]:
+    """Convert magnitude to dB.
+
+    Parameters
+    ----------
+    magnitude : np.ndarray[float]
+        Array containing magnitudes.
+
+    Returns
+    -------
+    db : np.ndarray[float]
+        Array converted to dB scale.
+    """
+    db = 20 * np.log10(magnitude)
path: ex1/y_ishikawa/main.py
user: login: luna-moonlight1412
body: 直します。
created_at: 2025-04-20T08:34:00Z
author_association: COLLABORATOR
diff_hunk: @@ -0,0 +1,179 @@
+import numpy as np
+
+
+def back_padding(data: np.ndarray, length: int) -> np.ndarray:
+    """Pad a 1D array with zeros at the end.
+
+    Parameters
+    ----------
+    data : np.ndarray
+        1D array to be padded.
+
+    length : int
+        Desired total length of the output array.
+
+    Returns
+    -------
+    padded_data : np.ndarray
+        1D array of length `length`.
+        If `data` is shorter than `length`, zeros are appended.
+    """
+    return np.append(data, np.zeros(length - len(data)))
+
+
+def split_data(
+    data: np.ndarray, length: int = 1024, overlap_rate: float = 0.5
+) -> list[np.ndarray]:
+    """Split a 1D array into overlapping segments.
+
+    Parameters
+    ----------
+    data : np.ndarray
+        1D array
+
+    length : int, default 1024
+        Segment length (number of samples per segment).
+
+    overlap_rate : float, default 0
+        Overlap rate between consecutive segments.
+        Should be in the range [0, 1).
+
+    Returns
+    -------
+    splitted_data : list[np.ndarray]
+        List of 1D arrays, each of length `length`.
+        If the last segment is shorter than `length`, zero-padded at the end.
+    """
+    if not (0 <= overlap_rate < 1):
+        raise ValueError("'overlap_rate' should be in the range [0, 1)")
+
+    step = int((1 - overlap_rate) * length)
+
+    splitted_data = []
+    for i in range(0, len(data), step):
+        splitted_data.append(back_padding(data[i : i + length], length))
+
+    return splitted_data
+
+
+def get_window_func(window: str, length: int) -> np.ndarray:
+    """Get window function.
+
+    Parameters
+    ----------
+    window : str
+        Using window function name. ("hamming", "hann", "rect")
+
+    filter_length: int
+        Size of window function.
+
+    Returns
+    -------
+    window_function : np.ndarray
+        Corresponding window function.
+    """
+    if window == "hamming":
+        return np.hamming(length)
+    elif window == "hann":
+        return np.hanning(length)
+    elif window == "rect":
+        return np.ones(length)
+
+
+def stft(
+    audio_data: np.ndarray,
+    filter_length: int,
+    overlap_rate: float,
+    window: str,
+) -> np.ndarray:
+    """Calculate Short-Time Fourier Transform (STFT) of a 1D audio signal.
+
+    Parameters
+    ----------
+    audio_data : np.ndarray
+        1D array of audio signal.
+
+    filter_length: int
+        Size of filter.
+
+    overlap_rate: float
+        Overlap rate for splitting data. (0 ~ 1)
+
+    window : str
+        Using window function name. ("hamming", "hann", "rect")
+
+    Returns
+    -------
+    spectrogram : np.ndarray
+        2D array of complex STFT coefficients.
+    """
+    # get window function
+    window_func = get_window_func(window, filter_length)
+
+    stft_result = []
+    for splitted_audio_data in split_data(
+        audio_data, filter_length, overlap_rate
+    ):
+        # apply window function
+        splitted_audio_data *= window_func
+
+        # process FFT
+        fft_result = np.fft.fft(splitted_audio_data)
+
+        # add to result
+        stft_result.append(fft_result)
+
+    return np.stack(stft_result, axis=1)
+
+
+def inv_stft(
+    stft_result: np.ndarray[complex],
+    original_audio_frame: int,
+    filter_length: int,
+    overlap_rate: float,
+    window: str,
+) -> np.ndarray:
+    """Reconstruct a 1D audio signal from STFT result.
+
+    Parameters
+    ----------
+    stft_result : np.ndarray[complex]
+        2D array of complex STFT coefficients.
+
+    original_audio_frame: int
+        Total number of frames in the original audio signal before STFT.
+
+    filter_length: int
+        Size of filter.
+
+    overlap_rate: float
+        Overlap rate for splitting data. (0 ~ 1)
+
+    window : str
+        Using window function name. ("hamming", "hann", "rect")
+
+    Returns
+    -------
+    audio_data : np.ndarray
+        1D array of audio signal.
+    """
+    # get window function
+    window_func = get_window_func(window, filter_length)
+
+    step = int((1 - overlap_rate) * filter_length)
+
+    audio_data = np.zeros(original_audio_frame + filter_length)
+    for i, fft_result in enumerate(stft_result.T):
+        # process IFFT
+        ifft_result = np.fft.ifft(fft_result)
+
+        # use only real value
+        ifft_result = np.real(ifft_result)
+
+        # remove window function
+        ifft_result /= window_func
path: ex1/y_ishikawa/stft.py
user: login: zodiac-18
body: ここです。窓関数をもう一度掛けるほうがoverlap-add法において厳密には正しいはずです（確かですが...）
created_at: 2025-04-20T08:47:29Z
author_association: COLLABORATOR
diff_hunk: @@ -0,0 +1,179 @@
+import numpy as np
+
+
+def back_padding(data: np.ndarray, length: int) -> np.ndarray:
+    """Pad a 1D array with zeros at the end.
+
+    Parameters
+    ----------
+    data : np.ndarray
+        1D array to be padded.
+
+    length : int
+        Desired total length of the output array.
+
+    Returns
+    -------
+    padded_data : np.ndarray
+        1D array of length `length`.
+        If `data` is shorter than `length`, zeros are appended.
+    """
+    return np.append(data, np.zeros(length - len(data)))
+
+
+def split_data(
+    data: np.ndarray, length: int = 1024, overlap_rate: float = 0.5
+) -> list[np.ndarray]:
+    """Split a 1D array into overlapping segments.
+
+    Parameters
+    ----------
+    data : np.ndarray
+        1D array
+
+    length : int, default 1024
+        Segment length (number of samples per segment).
+
+    overlap_rate : float, default 0
+        Overlap rate between consecutive segments.
+        Should be in the range [0, 1).
+
+    Returns
+    -------
+    splitted_data : list[np.ndarray]
+        List of 1D arrays, each of length `length`.
+        If the last segment is shorter than `length`, zero-padded at the end.
+    """
+    if not (0 <= overlap_rate < 1):
+        raise ValueError("'overlap_rate' should be in the range [0, 1)")
+
+    step = int((1 - overlap_rate) * length)
+
+    splitted_data = []
+    for i in range(0, len(data), step):
+        splitted_data.append(back_padding(data[i : i + length], length))
+
+    return splitted_data
+
+
+def get_window_func(window: str, length: int) -> np.ndarray:
+    """Get window function.
+
+    Parameters
+    ----------
+    window : str
+        Using window function name. ("hamming", "hann", "rect")
+
+    filter_length: int
+        Size of window function.
+
+    Returns
+    -------
+    window_function : np.ndarray
+        Corresponding window function.
+    """
+    if window == "hamming":
+        return np.hamming(length)
+    elif window == "hann":
+        return np.hanning(length)
+    elif window == "rect":
+        return np.ones(length)
+
+
+def stft(
+    audio_data: np.ndarray,
+    filter_length: int,
+    overlap_rate: float,
+    window: str,
+) -> np.ndarray:
+    """Calculate Short-Time Fourier Transform (STFT) of a 1D audio signal.
+
+    Parameters
+    ----------
+    audio_data : np.ndarray
+        1D array of audio signal.
+
+    filter_length: int
+        Size of filter.
+
+    overlap_rate: float
+        Overlap rate for splitting data. (0 ~ 1)
+
+    window : str
+        Using window function name. ("hamming", "hann", "rect")
+
+    Returns
+    -------
+    spectrogram : np.ndarray
+        2D array of complex STFT coefficients.
+    """
+    # get window function
+    window_func = get_window_func(window, filter_length)
+
+    stft_result = []
+    for splitted_audio_data in split_data(
+        audio_data, filter_length, overlap_rate
+    ):
+        # apply window function
+        splitted_audio_data *= window_func
+
+        # process FFT
+        fft_result = np.fft.fft(splitted_audio_data)
+
+        # add to result
+        stft_result.append(fft_result)
+
+    return np.stack(stft_result, axis=1)
+
+
+def inv_stft(
+    stft_result: np.ndarray[complex],
+    original_audio_frame: int,
+    filter_length: int,
+    overlap_rate: float,
+    window: str,
+) -> np.ndarray:
+    """Reconstruct a 1D audio signal from STFT result.
+
+    Parameters
+    ----------
+    stft_result : np.ndarray[complex]
+        2D array of complex STFT coefficients.
+
+    original_audio_frame: int
+        Total number of frames in the original audio signal before STFT.
+
+    filter_length: int
+        Size of filter.
+
+    overlap_rate: float
+        Overlap rate for splitting data. (0 ~ 1)
+
+    window : str
+        Using window function name. ("hamming", "hann", "rect")
+
+    Returns
+    -------
+    audio_data : np.ndarray
+        1D array of audio signal.
+    """
+    # get window function
+    window_func = get_window_func(window, filter_length)
+
+    step = int((1 - overlap_rate) * filter_length)
+
+    audio_data = np.zeros(original_audio_frame + filter_length)
+    for i, fft_result in enumerate(stft_result.T):
+        # process IFFT
+        ifft_result = np.fft.ifft(fft_result)
+
+        # use only real value
+        ifft_result = np.real(ifft_result)
+
+        # remove window function
+        ifft_result /= window_func
+
+        # add to result
+        audio_data[step * i: step * i + filter_length] = ifft_result
path: ex1/y_ishikawa/stft.py
user: login: zodiac-18
body: おっしゃるとおりですね、単純な加算だとオーバーラップ部分で値が膨らむので、補正が必要ですね。

ただその前に、L174で窓関数を割っていますが、正確に元の波形を再現するには、実は掛けるほうが正しいという話があった気がします（もしよければ調べてみてください）。
で、その結果を窓関数の二乗和で割ることで振幅を正規化します。（正確には、窓関数=0の可能性があるので非常に小さい値を窓関数に足した値で割る）

実際に、ライブラリのiSTFT関数ではそのような実装になっているはずです （[参考](https://github.com/scipy/scipy/blob/0f1fd4a7268b813fa2b844ca6038e4dfdf90084a/scipy/signal/_spectral_py.py#L1439)）
これを参考にして直してみてください。
created_at: 2025-04-20T08:53:24Z
author_association: COLLABORATOR

PR comments:
user: login: github-actions[bot]
created_at: 2025-04-16T05:31:33Z
body: @luna-moonlight1412
課題の提出お疲れ様です！
レビュアーが自動で割り当てられない場合はお知らせください．M1の方には自動で割り当てられないことがあります．
また，レビュアーが割り振られていても反応がない場合は，気づいていない可能性があるので催促してあげてください．
コーディングは自動でチェックされます．できる範囲で構わないのでエラーメッセージや[こちら](https://github.com/TakedaLab/B4Lecture-2025/blob/master/docs/TIPS.md#github-actionsのciによるコーディングチェック)を参考に修正してみてください．
Approve後も含めて**マージはしないようお願いします．**

---

## 確認事項

PRに関して以下の注意点があるので，毎回必ずご確認をお願いします．
**確認ができたらチェックを入れてください．** 自力で解決が難しい場合はお知らせください．

### 提出するファイルだけ出してください

- [ ] 確認しました

例）第2回の課題を提出する場合
Files changedに`ex2/<your_dir>/**` **だけ**がある状態にしてください．

過去の課題などが混じっていると，ラベルやレビュアー割り当てがおかしくなるので気をつけてください．
もし間違えてしまったときは，内容を修正した後にActionsにてSet PR ReviewersをRe-run all jobsすると解決できます．

### 配布されたファイルは変更しないでください

- [ ] 確認しました

課題で配布されたREADME.mdや図などは変更しないようにしてください．
コンフリクトの原因となる可能性があります．
author_association: NONE
user: login: github-actions[bot]
created_at: 2025-04-16T05:31:47Z
body: @ck5133540 @zodiac-18
このPRのレビュアーに指名されました．
M1、M2はレビューをして，**最後に忘れずapproveをしてください．**
B4は感想や参考になった点などなんでも良いのでコメントしてください．
マージはドクターの先輩が行います．
author_association: NONE
user: login: ck5133540
created_at: 2025-04-20T05:36:23Z
body: ファイルを分けてたりちゃんとクラスを有効活用してたり、コメントもしっかり書いてて凄い丁寧で感心してしまう...。僕なんか単一ファイル...

(追記:コード修正は全部Ruffでできるんですね。いいこと知りました。)
author_association: COLLABORATOR
user: login: github-actions[bot]
created_at: 2025-04-20T09:19:40Z
body: @ck5133540 @zodiac-18
このPRのレビュアーに指名されました．
M1、M2はレビューをして，**最後に忘れずapproveをしてください．**
B4は感想や参考になった点などなんでも良いのでコメントしてください．
マージはドクターの先輩が行います．
author_association: NONE
user: login: github-actions[bot]
created_at: 2025-04-25T09:36:35Z
body: @luna-moonlight1412
以上でこの課題は終了です．お疲れ様でした！
author_association: NONE