メインコンテンツへスキップ

音声を特徴量に、音素列を時間軸に:ciglet・liblrhsmm・SHIROの仕組みとRustでの再実装

音声強制アライメントSHIRO、信号処理ciglet、HSMM推論エンジンliblrhsmmをRustで再実装。音声の数値化や継続時間モデリングの数理を整理し、Rustの型システムによる設計刷新と互換性検証のプロセスをまとめました。

音声波形とテキスト(音素列)の時間的な対応関係を自動推定する強制アライメント(Forced Alignment)。その処理パイプラインを支える3つのC/Lua製プロジェクト——信号処理ライブラリ ciglet、隠れセミマルコフモデル(HSMM)推論・学習エンジン liblrhsmm、そしてこれらを統合するツール群 SHIRO——を、すべてRustで再実装しました。

再実装した3プロジェクトの 0.1.0 は、Rustクレートとして利用できるだけでなく、C ABI、WebAssembly(ブラウザ実行)、Pythonバインディング、Node.jsネイティブアドオンとしても提供しています。

本稿では、元のC/Lua実装が音声をどのように数値化し、状態の継続時間をどう推定していたのかを整理した上で、Cコードの課題をRustの型システムや設計パターンでどのように解消し、互換性を検証したのかについてまとめました。

1. 音声と音素列の間にある、未知の時間境界

音声認識と強制アライメントの違い

SHIROが解く中心的な課題は、強制アライメント(Forced Alignment) です。 音声をテキスト化する一般的な音声認識(ASR)とは異なり、強制アライメントでは「発話内容(音素列)はすでに分かっている」という前提に立ちます。探索対象となるのはテキストではなく、「各音素が音声のどの区間(何ミリ秒から何ミリ秒まで)で発音されたか」という時間境界です。

例えば、録音に対応する音素列が k a s a(傘)であると既知であっても、無音から子音 k への立ち上がり、母音 a への移行点、語中の遷移タイミングなどは波形から自明には分かりません。SHIROは音声波形から音響特徴量の時系列を抽出し、統計モデルを用いて各音素に対応する状態が時間軸上のどのフレームを占めているかを最適化(アライメント)します。

1音素=1状態ではない:状態遷移による音響変化の表現

音声学的に、1つの音素であってもその音響特性は時間とともに変化します。例えば破裂音 k では、閉鎖区間、破裂の瞬間、後続母音へのわたり(Transition)でまったく異なる周波数特性が現れます。

そのため、SHIROでは1つの音素に対して複数の内部状態(通常3〜5状態)を左から右へ(Left-to-Right)遷移させる構成をとっています。これにより、音の立ち上がり、中央の定常部、次の音への渡りを個別の確率分布でモデル化できます。SHIROの音素定義ファイルは、この各音素の状態数や共有関係を宣言的に指定する仕組みになっています。

パイプライン全体の構成と3つのプロジェクト

処理パイプラインは、次のように接続されます:

flowchart TD
    W[録音 WAV] --> P[前処理・フレーム化]
    P --> X[音響特徴量時系列]
    T[既知の音素列] --> S[状態列・分布の参照定義]
    M[音響分布・継続時間分布] --> I[HMM / HSMM 推論]
    X --> I
    S --> I
    I --> B[状態境界・音素ラベル]
    X --> E[事後確率・統計量の収集]
    S --> E
    M --> E
    E --> U[モデルのパラメータ再推定]

このパイプラインにおいて、信号処理を担うのが ciglet、確率モデルの推論・学習を担うのが liblrhsmm、ファイルフォーマットや学習手順を統括してエンドツーエンドのCLIを提供するのが SHIRO です。元プロジェクトの責務分離に倣い、Rust版でも3つの独立したリポジトリとして再実装しました。

元プロジェクト

主な責務・関心事

Rust版リポジトリ

ciglet

音声信号処理、周波数変換、フィルタバンク、LPC、瞬時周波数、声門モデル

ciglet-rs

liblrhsmm

隠れセミマルコフモデル(HSMM)、GMM、動的計画法推論(Viterbi/Forward-Backward)、EM学習

liblrhsmm-rs

SHIRO

音素定義、特徴抽出パイプライン、学習ワークフロー、セグメンテーション、ラベル出力

shiro-rs

元のSHIROの設計は、タスクを強制アライメントとそのモデル学習に特化させているため、巨大な汎用ASRフレームワークに比べて信号処理・推論エンジン・ツール群の依存関係が見通しやすい構成になっています。

2. ciglet:音声信号処理の計算部品群

cigletは、音声・音響信号処理で頻出するプリミティブをまとめたC言語ライブラリです。SHIROの特徴抽出で用いられる関数群はもちろん、線形予測分析(LPC)、位相操作、瞬時周波数推定、スペクトル包絡推定、さらには音声合成のための声門音源モデル(LFモデル)まで含んでいます。

研究用途のプロトタイプ(MATLABやOctave)からCへ移植された経緯を持ち、低レイヤの数値計算と音声特有のドメイン処理が凝縮された構成が特徴です。

2.1 任意周波数間隔を解析するチャープZ変換(CZT)

通常の離散フーリエ変換(DFT/FFT)は、単位円上を等間隔に分割した周波数ビンを観測します。これに対し、cigletは任意の等角周波数間隔でZ平面上を評価できる Chirp Z-Transform(CZT、チャープZ変換)(cig_czt)を備えています。

CZTは、定義通りに計算すると O(N²) の計算量を要しますが、Bluesteinのアルゴリズムを用いることで畳み込み形式に帰着させ、FFTにより O(N log N) で計算できます。この変換では、指数部分の積 kn を2次形式へ分解する恒等式を利用します:

kn = (k² + n² - (k - n)²) / 2

この変形により、離散値の乗算は「入力信号へのチャープ系列の乗算」「チャープ系列との巡回畳み込み」「出力へのチャープ系列の乗算」の3ステップに分解されます。

オリジナルの cig_czt では、ゼロ埋めした単一の連続バッファ内に実部・虚部が交互に並ぶ作業領域を割り当て、2回の順変換・周波数領域での複素乗算・1回の逆変換をインプレースに処理していました。

Rust版での実装方針

Rust版では、FFTエンジンとして RustFFT を採用しました。 RustFFTは内部でプラン(FftPlanner)と作業用スクラッチバッファを管理し、メモリアロケーションを抑えます。RustFFTの仕様では逆変換時に自動での正規化(1/N による除算)を行わないため、ciglet本来の数学的出力と一致させるための正規化スケーリングを適用しています。

2.2 聴覚フィルタバンクにおける離散化と境界条件の再現

人間の聴覚特性を模擬するため、周波数軸を非線形に配置する メル尺度(Mel scale) や Bark尺度 のフィルタバンクが使われます。これらはFFTの周波数スペクトルを、人間の耳の分解能に合わせた複数の帯域エネルギーへと集約します。

メルフィルタバンクでは、各帯域の中心周波数と両端をFFTの離散ビン番号へ射影し、三角形の重み関数(Triangle filters)を構成します。cigletの実装は、全周波数ビンに対してゼロ乗算を繰り返す愚直な行列演算を避け、各帯域が実際に非ゼロとなるビン範囲(支持区間:support range)のみを保持して局所的に積和演算を行う設計になっています。

エネルギー
  ^
  |        / \             / \
  |       /   \           /   \
  |      /     \         /     \
  +-----+-------+-------+-------+-----> 周波数ビン
     left     center  right

移植における数値的留意点

音響特徴量の互換性を担保する上で、周波数ビンの離散化処理は慎重に扱う必要があります。 元実装では周波数座標の算出に nf / fnyq を使用し、各三角フィルタの端点インデックスを floor、ceil、round で丸めています。これを安易に (nf - 1) / fnyq に変更したり丸め関数を統一したりすると、特定の三角窓の頂点位置が1ビンずれ、結果として得られる特徴量ベクトル全体が学習済みモデルのパラメータと乖離してしまいます。

また、Bark系フィルタバンクでは帯域エネルギーの圧縮に pow(sum, 0.33) や近似対数が使われますが、この指数 0.33 を数学的に厳密な 1.0 / 3.0 に変更するだけでもモデル推論に影響が及びます。

さらに、Cの元実装を検査したところ、極端に小さいFFTサイズを指定した場合に中心ビンの計算値が0となり、負のインデックス参照(バッファアンダーラン)を引き起こす未定義動作が存在することが分かりました。Rust版では、境界チェックを入れて未定義動作を防止しつつ、有効な範囲内では元実装と完全に一致する係数算出を再現しています。

2.3 線形予測分析(LPC)とLevinson-Durbinの再帰法

線形予測符号化(Linear Predictive Coding: LPC)は、音声信号の現在サンプル x[n] を、過去 p 個のサンプルの線形結合としてモデル化します:

e[n] = x[n] + Σ[k=1..p] a_k * x[n-k]

予測誤差 e[n] の自乗平均を最小化する係数列 a_k を求める連立方程式は、自己相関関数を要素とするToeplitz行列を係数行列に持ちます。cigletでは、この対称Toeplitz性を利用して計算量を O(p³) から O(p²) に削減する Levinson-Durbinの再帰アルゴリズム を採用しています。

アルゴリズムは、低次の予測係数と反射係数(PARCOR係数)、および残差エネルギーを1次ずつ逐次的に更新していきます。C言語の実装では配列のインプレース書き換えが行われていましたが、更新の過程で前段階の係数を参照する必要があるため、参照順序を慎重に扱う必要がありました。

Rust版では、作業用バッファを安全に再利用しながら、アルゴリズムの各ステージで参照の競合が起きないクリーンな実装に再構成しました。また、自己相関行列が特異行列に近づくのを防ぐための微小な正則化項(対角成分への 1.000001 の乗算)や、単精度・倍精度の混合演算順序もそのままトレースしています。テストでは再帰アルゴリズム自体を二重実装して比較するのではなく、求まった係数 a_k を元の連立方程式に代入して方程式が成立するかを独立して検証するテストハーネスを用意しました。

2.4 位相アンラッピングを行わない瞬時周波数推定

瞬時周波数(Instantaneous Frequency)は、信号の各時刻における局所的な振動数を示します。通常、瞬時周波数は解析信号の位相角を atan2 で求め、その時間微分(位相の差分)を計算することで得られますが、この方法では位相反転や位相アンラッピング(2π の不連続性の補正)の処理が必要になり、ノイズの影響を受けやすくなります。

cigletの瞬時周波数検出器(cig_ifdetector_estimate)は、複素フィルタのインパルス応答とその時間微分応答を同時に畳み込む というアプローチをとっています。

入力信号に対する複素フィルタの応答を y(t) = y_r(t) + iy_i(t)、その微分フィルタによる応答を y'(t) = y'_r(t) + iy'_i(t) とすると、位相の時間微分 f(t) は三角関数や逆正接を使わず、次式のように代数的に計算できます:

f(t) = (1 / 2π) * (y_r(t) * y'_i(t) - y_i(t) * y'_r(t)) / (y_r²(t) + y_i²(t))

元実装は、実部・虚部およびそれぞれの微分に対応する4種類の畳み込みカーネルをあらかじめ生成しておき、入力サンプルとの4本の内積から上式を直接計算します。

この方式は位相アンラッピング処理が不要で高速ですが、無音区間など分母の包絡線エネルギー y_r² + y_i² がゼロに近づく極限で数値的に不安定になります。Rust版では、4本のカーネルを安全に管理しつつ、過大振幅入力でのオーバーフローを抑制するスケーリング経路を整備しました。

2.5 基本周波数(F0)に連動したスペクトル包絡推定

有声音のスペクトルには、声帯振動の基本周波数(F0)の整数倍ごとに鋭いピークが並ぶ調波構造(Harmonic structure)が現れます。しかし、音声認識や音素アライメントで必要な情報は声道の共鳴特性(フォルマント)であり、声帯のピッチによる櫛の歯状の微細構造は平滑化して取り除く必要があります。この大まかな輪郭を取り出す処理が スペクトル包絡(Spectral Envelope)推定 です。

cigletの cig_spec2env は、推定された F0 の値に基づいて周波数領域での平滑化窓幅を適応的に決定します。

対数振幅
  ^       調波構造(有声音のピーク群)
  |       /|   /|   /|   /|   /|
  |      / |  / |  / |  / |  / |        --- スペクトル包絡(声道特性)
  |     /  | /  | /  | /  | /  |
  +----+---+----+----+----+----+----> 周波数
       <-F0->
  1. 基本周波数連動の平滑化: F0 に応じた移動平均により調波の谷を埋めます。
  2. ケプストラム領域でのリフタリング: 対数スペクトルを逆FFTして ケプストラム(Quefrency領域) へ変換します。ケプストラム領域において、F0 周期に依存したsinc関数型の重み(リフタ)を乗算することで、高周波の調波成分(声帯振動由来)を遮断し低周波の声道伝達特性のみを残します。
  3. 対数包絡への復帰: 再度FFTを行い、平滑化された自然対数スペクトル包絡を得ます。

Rust版では、この対数包絡とケプストラム係数を返す仕様を再現し、ナイキスト周波数ビンの端点対称化処理に関する設定もサポートしました。

2.6 音声合成・声門音源モデリング(LFモデル)

cigletは音声の「分析」だけでなく、音声を「生成」するための声門波形モデルである Liljencrants–Fant(LF)モデル も内包しています。

音響生成の音源・フィルタ理論において、肺からの呼気が声帯を通過する際に生じる空気流(声門流)の微分波形を数式でモデル化したものがLFモデルです。開口相(Glottal opening)、閉口相(Glottal closure)、および急激な遮断の後の戻り相(Return phase)が区分非線形関数で定義されます。

cigletでは、形状パラメータ Rd から各時間パラメータへの変換を行い、モデルの内部係数を求めるためにニュートン・ラフソン法(Newton-Raphson method)による非線形方程式の求解を行います。

Cの元実装ではこのニュートン法が最大8回の固定反復で打ち切られており、特定の極端なパラメータ設定では方程式が十分に収束しないケースがありました。Rust版では、元のC実装と一致するパラメータ算出経路を保ちつつ、方程式の残差を監査できる診断機能を備えています。

3. liblrhsmm:状態終了時刻を評価するHSMM推論・学習エンジン

liblrhsmmは、Left-to-Right型の 隠れセミマルコフモデル(Hidden Semi-Markov Model: HSMM) の推論およびパラメータ学習(EMアルゴリズム)を行う専用エンジンです。

一般的な隠れマルコフモデル(HMM)では、状態の継続時間が幾何分布に従うという制約(無記憶性)があり、「ある音素が一定の長さだけ持続しやすい」という音声の物理的特性を表現しにくい面があります。HSMMは、各状態の 継続時間(Duration) そのものを陽な確率分布(Explicit Duration)として扱えるよう拡張したモデルです。

3.1 発話状態列と分布実体の疎結合化(State Tying)

liblrhsmmの設計上の特徴は、発話に現れる 「状態の順序(Topology)」 と、状態が参照する 「確率分布(Parameter)」 を分離している点にあります。

発話中の各状態ノードは、以下の情報を参照として保持します:

[発話内の状態列]
  状態 0 (kの立ち上がり) ───> 出力分布 ID: 12, 継続時間分布 ID: 3
  状態 1 (kの定常部)     ───> 出力分布 ID: 13, 継続時間分布 ID: 4
  状態 2 (aの定常部)     ───> 出力分布 ID: 45, 継続時間分布 ID: 8
  状態 3 (sの立ち上がり) ───> 出力分布 ID: 60, 継続時間分布 ID: 12
  状態 4 (aの定常部:再登場) ─> 出力分布 ID: 45, 継続時間分布 ID: 9

このように状態と分布をIDで参照解決する構造を State Tying(状態共有) と呼びます。 同じ母音 a が文中に何度出現しても音響モデルの出力分布は共有でき、一方で前後の音素コンテキストに応じて継続時間分布だけを個別に割り当てるといった柔軟なモデリングが可能です。

Rust版でも、状態ノードの中にモデルパラメータを直接インラインで持たせるような設計は避け、モデルがパラメータの所有権を持ち、発話セグメント側は検証済みのインデックスを参照する疎結合な設計を引き継ぎました。

3.2 複数特徴ストリーム対応の対角共分散GMM

観測特徴量の確率密度分布には、混合ガウスモデル(Gaussian Mixture Model: GMM) を採用しています。共分散行列は全要素を持たず、各次元が独立であると仮定する 対角共分散(Diagonal covariance) です。

対角共分散ガウス分布の1成分における対数尤度は次式で計算されます:

log N(x; μ, diag(σ²)) = -0.5 * Σ_q [ log(2π * σ_q²) + (x_q - μ_q)² / σ_q² ]

分散 σ_q² のみに依存する正規化項(定数項)は、入力フレーム x に依存しないため、フレーム評価の前に一度だけ事前計算(事前キャッシュ)しておくことで、フレームごとの計算量を次元数 D の積和演算にまで削減できます。

さらに、liblrhsmmは入力特徴量を複数の独立した ストリーム(Stream) に分割して扱います(例えば、静的特徴ストリーム、1次差分ストリーム、2次差分ストリームなど)。各ストリームに対数重み λ_s を割り当て、観測対数尤度を次のように線形結合します:

ℓ_j(t) = Σ_s λ_s * log[ Σ_k w_jsk * N(x_t^(s); μ_jsk, Σ_jsk) ]

Cの元実装では、計算を最適化するため、混合数が1(単一正規分布)の場合に指数関数を通さず直接対数尤度を足し合わせるショートカット分岐や、複数混合時の近似指数・近似対数テーブルが組み込まれていました。

3.3 HMMとHSMMにおける継続時間モデリングの違い

HMMにおける自己遷移確率を a_jj = 1 - p、次状態への遷移確率を p と置くと、その状態に d フレーム滞在する確率は幾何分布に従います:

P(D = d) = (1 - p)^(d-1) * p    (d ≥ 1)

この幾何分布は「すでに d - 1 フレーム滞在した」という履歴に影響されず、常に次のフレームで状態を抜ける確率が一定(無記憶性)です。しかし実際の音素は、「短すぎても長すぎても不自然であり、ある特定の持続長に集中する」という特性を持ちます。

HSMMではこの制約を緩和し、継続時間 d に対して明示的な確率分布 g_j(d) を割り当てます。liblrhsmmでは、継続時間分布として1次元のガウス分布を用い、整数フレーム数 d に対する評価値に、最小・最大継続時間のハード制約やスケーリング係数を適用します。

なお、liblrhsmmはHSMMだけでなく通常のHMM推論も実行できます。HMMモードの遷移確率は、与えられた平均継続時間 d_mean から p = 1 / max(1.01, d_mean) として構成され、初期アライメント用の高速なパスとして活用されます。

3.4 状態の開始と終了を分離した動的計画法(Forward / Viterbi)

HSMMの推論では、「どの状態に遷移したか」だけでなく「その状態に何フレーム滞在したか」という時間次元の探索が加わります。素朴に実装すると計算量が増大しますが、liblrhsmmは 状態への進入(Begin) と 状態からの脱出(Finish) の2つの動的計画法テーブルを分離することで効率的な計算を実現しています。

時刻 t において状態 j が「終了」する対数重みを F(t, j)、状態 j に「入る直前」の対数重みを B(u, j) とすると、基本構造は次のようになります:

B(u, j) = Σ_{i -> j} F(u, i) * a_ij

F(t, j) = Σ_{d ≥ 1} B(t - d, j) * g_j(d) * Π_{r = t-d+1..t} b_j(x_r)

ここで a_ij は状態遷移確率、g_j(d) は継続時間確率、b_j(x_r) はフレーム x_r における観測確率です。

時刻:     t-d                     t
          │                       │
状態 i: ──● (F(t-d, i)で終了)
          │
          └──遷移 a_ij ──> B(t-d, j) (状態jへ進入)
                           │
                           ├── 継続時間 d の探索 (観測スコアを累積)
                           │
状態 j: ───────────────────● (F(t, j)で終了)

元実装の前向き推論では、継続時間 d を伸ばしながら観測の対数尤度を累積加算していくことで、同一区間の尤度を最初から再計算する無駄を省いています。

また元のCコードでは、マクロ定義(VITERBI の有無)によって、確率の総和(Forwardアルゴリズム:Sum演算)と、最良パスの探索(Viterbiアルゴリズム:Max演算+バックポインタ保存)を同一のループ構造から生成して二重管理を避けていました。

3.5 前段アライメント情報を活用した動的枝刈り(Pruning)

状態数を S、総フレーム数を T、最大継続時間を D とすると、HSMMの探索空間は O(T × S × D) に達します。長大な音声ファイルでは全探索のコストが無視できません。

liblrhsmmは、あらかじめ与えられた初期アライメント(または前段のHMM推論で得られた粗い時間境界)を活用し、各時刻 t で評価すべき状態の範囲をその近傍のみに限定する 動的枝刈り を行います。

探索する継続時間の上限も、モデルの平均値・分散、および既存アライメントの長さから適応的に算出されます。SHIROはこの機構を使い、「まず軽量なHMMで大まかなアライメントを求め、その境界周辺のみをHSMMで精密に探索・補正する」という2段階パイプラインによって計算量を抑えています。

3.6 ルックアップテーブル(LUT)による対数加算(Log-Sum-Exp)の高速化

Forwardアルゴリズムや事後確率の算出では、対数確率空間での加算(Log-Sum-Exp)が頻繁に繰り返されます:

log(e^a + e^b) = m + log(1 + e^(-|a - b|))    (ただし m = max(a, b))

この演算で毎回標準ライブラリの exp や log を呼ぶと無視できないオーバーヘッドになります。liblrhsmmでは、右辺第2項の関数 f(Δ) = log(1 + e^(-Δ))(Δ = |a - b| ≥ 0)を、刻み幅 0.001、要素数 7000 の事前計算ルックアップテーブルで高速参照します。Δ > 6.91 の領域では e^(-Δ) ≈ 0 とみなして補正項をゼロと扱い、テーブル参照もスキップします。

また、負の対数尤度から確率値へ逆変換する処理にも、刻み幅 0.002、要素数 7000 のテーブルが用意されており、-13.9 以下の値はゼロとして切り捨てられます。

コンパイラ最適化と量子化インデックスのズレ

こうしたルックアップテーブル方式では、「浮動小数点値を整数インデックスへキャストする処理」の丸め動作が結果を左右します。 移植中の検証において、delta / 0.001 という除算をコンパイラの最適化(-Ofast や -ffast-math)によって delta * 1000.0 の乗算に置き換えた際、境界値付近で浮動小数点誤差によりキャスト後のインデックスが 42 から 43 にずれる現象が確認されました。

Rust版では、元実装のテーブル生成ルールとインデックス量子化ロジックを忠実に再現し、Cコンパイラの最適化フラグによる挙動の差異まで確認した上で決定論的な結果が得られるようにしました。

3.7 EMアルゴリズムによるパラメータ学習と十分統計量の集約

liblrhsmmのパラメータ学習は、Viterbiによる単一の最良パスのみを使うハードアライメント学習ではなく、すべての遷移パスの確率を考慮する Baum-Welchアルゴリズム(EM法) に基づいています。

  1. Eステップ(期待値の計算): Forward-Backwardアルゴリズムから、時刻 t における各状態の占有事後確率 γ_t(j)、および混合ガウス成分ごとの負担率を計算します。
  2. 十分統計量の収集: 各GMM成分について、以下の重み付き統計量を集約します:
    • 0次統計量(有効サンプル数): N_k = Σ_t γ_tk
    • 1次統計量(重み付き和): S_kq = Σ_t γ_tk * x_tq
    • 2次統計量(重み付き二乗和): Q_kq = Σ_t γ_tk * x_tq²
  3. Mステップ(パラメータ更新): 集約された統計量から、新たな平均 μ = S / N および分散 σ² = Q / N - μ² を算出します。

この統計量収集フェーズが独立しているため、複数発話・複数音声ファイルの統計量を並列に集約し、最後に一括してモデルを更新できます。 また、学習が進むにつれて特定成分の分散が極小化して数値計算が破綻するのを防ぐ 分散下限値(Variance Floor) や、学習済みGMMの成分を分裂させて表現力を高める 混合数増加(Mixture Splitting) 機能も備わっています。

3.8 決定論的焼きなましEM(DAEM)とキャッシュの整合性

局所解に陥りやすいEMアルゴリズムの弱点を緩和するため、SHIROは Deterministic Annealing EM(DAEM、決定論的焼きなましEM法) を採用しています。

学習の初期段階では、事後確率計算の対数スコアに 1 より小さい係数 β(逆温度)を掛けることでエントロピーを高め(確率分布をブロードにし)、大局的な最適解へ誘導します。反復回数 i(0 ≤ i < I)に伴い、係数 β は次式に従って徐々に 1 へ近づきます:

β_i = sqrt((i + 1) / I)

継続時間分布の事前計算キャッシュには、この β の値が直接反映されます。そのため、モデルの平均・分散が同一であっても、学習ステップが進んで β が変化した場合はキャッシュを破棄・再生成しなければなりません。Rust版では、このキャッシュの依存関係を管理し、古い β のキャッシュを誤って参照するバグを構造的に防ぐ設計にしました。

3.9 メモリプール(Arena)による事後確率テーブルの管理

HSMMのEM学習では、全フレーム・全状態・全候補継続時間の組み合わせに対する事後確率を保持する必要があり、単純な3次元配列として確保するとメモリ消費が大きくなります。

Cの元実装では、各時刻・状態ごとに必要な探索区間長のみを表現する可変長スライス構造体を用い、それらのメモリを ページ単位でメモリブロックを切り出す独自アロケータ(Memory Pool / Arena) から確保していました。また、事後確率テーブルの末尾にある微小な値(閾値 1e-5 未満)を走査して後続ループの上限を切り詰める最適化も施されていました。

Rust版では、このメモリプールの概念を、ライフタイムによって解放タイミングが保証された安全なバッファプールへと再設計しました。生ポインタのキャストを避けつつ、アロケーションのオーバーヘッドを抑える構成をとっています。

4. SHIRO:信号処理から推論・ラベル生成までをつなぐツール群

SHIROは、cigletとliblrhsmmを組み合わせ、実際の音声データからモデルを学習し、音素の時間境界ラベル(AudacityラベルやTextGrid形式)を出力する統合パイプラインです。

オリジナル版は、数値計算およびバイナリモデル操作をCで実装し、音素辞書解析、JSON生成、学習ループの制御、外部スクリプト連携を Lua で記述する構成をとっていました。Rust版では、これら14個に及ぶCLIツールとLuaスクリプトの全機能を、純粋なRust実装へと統合・移植しました。

録音 WAV ─────────────> [shiro-xxcc] ─────────────> 特徴量ファイル (.feat)
                           ▲
                           │ (ciglet 信号処理)
                           │
音素定義・設定 ───────> [shiro-mkpm / pm2md] ────> 初期モデル定義 (.json / .mdl)
                           │
                           ▼
                      [shiro-train] <─────────────┐ (反復学習ループ)
                           │ (liblrhsmm 推論・学習)│
                           ▼                      │
                      [shiro-align] ──────────────┘
                           │
                           ▼
                      音素ラベルファイル (.txt / Audacity形式)

4.1 特徴抽出パイプライン(`shiro-xxcc`)の仕様

特徴抽出ツール shiro-xxcc は、MFCC(メル周波数ケプストラム係数)、MFBE(メル帯域エネルギー)、および PLPCC(知覚線形予測ケプストラム係数) の抽出をサポートします。

ここで留意すべき点は、「MFCC」や「PLP」という名称が同じであっても、ツールごとに細部の信号処理が異なる点です。shiro-xxcc の実装フローは以下の通りです:

  1. フレーム切り出し: ホップ幅とフレームインデックスから中央時刻を算出し、入力波形から矩形フレームを切り出します(フレーム長が奇数の場合もサポート)。
  2. 窓関数: Blackman窓を乗算します。
  3. 振幅スペクトルの算出: 複素FFTを適用し、「振幅スペクトル(絶対値 |X|)」 を算出します。
    • ※一般的なMFCC実装(Kaldiやlibrosaなど)では「パワースペクトル(|X|²)」が使われることが多いですが、SHIROは一貫して振幅スペクトルを使用します。ここを取り違えると学習済みモデルとの互換性が損なわれます。
  4. フィルタバンク適用: メル尺度またはBark尺度の三角フィルタバンクを乗算します。
  5. 非線形圧縮: 対数変換、またはBark尺度における等ラウドネス圧縮を行います。
  6. 離散コサイン変換(DCT): タイプIIのDCTを適用して低次のケプストラム係数を抽出します。

Rust版では、この一連の変換パイプラインを忠実に実装し、窓掛けの境界パディングやDCTの0次係数、エネルギー項の挿入位置に至るまで、C実装と同一の結果を出力するよう構築しました。

4.2 動的特徴量(Δ, ΔΔ)のストリーム結合

静的なスペクトル特徴量に加え、音声の時間的変化を捉えるために1次差分(Δ: デルタ / 速度)と2次差分(ΔΔ: 加速度)を計算します。

SHIROの差分計算は、前後のフレームに対する以下の固定カーネルの畳み込みで行われます:

  • 1次差分(3点カーネル): [-0.5, 0.0, 0.5]
  • 2次差分(5点カーネル): [0.25, 0.0, -0.5, 0.0, 0.25]

境界外のフレームはゼロパディングとして扱われます。例えば、12次元の静的MFCCに1次差分12次元、2次差分12次元を加えると、1フレームあたり合計36次元の特徴ベクトルが生成されます。

liblrhsmmの推論エンジン側では、この36次元ベクトルを単一のGMMで評価するのではなく、「静的(12次元)」「1次差分(12次元)」「2次差分(12次元)」という3つの独立した特徴ストリーム として受け取り、それぞれのストリーム重み λ_s に基づいて尤度を統合します。

4.3 音素トポロジーの構築と決定論的な制約解決

SHIROは、テキスト形式の音素一覧ファイルから音素マップを生成し、各発話に対応する状態遷移グラフ(トポロジー)を構築します。

  • shiro-mkpm: 音素ごとの状態数、ストリーム数、継続時間制約、スキップ遷移を定義。
  • shiro-pm2md: 参照関係からモデルに必要な総分布数を割り当て。
  • shiro-mkseg: 各発話の音素列を状態列へと展開し、初期区間とジャンプ遷移を設定。

Lua版の制約解決における不確定性の解消

元のLuaスクリプトでは、同一の継続時間分布に対して複数の制約(最小・最大フレーム数)が指定された際、Luaのハッシュテーブルの走査順序に依存して制約の上書きが行われていました。その結果、同一の入力ファイルであっても実行環境によってモデルの制約値が変わってしまうケースがありました。

Rust版では、有効な下限値の最大値(max)と上限値の最小値(min)を決定論的にマージし、論理的矛盾(下限 > 上限)が発生した場合はエラーを返す仕様に改めました。

4.4 Flat Start初期化からHSMMへの段階的学習

初期状態では音声中のどこにどの音素があるかという境界情報は存在しません。SHIROは Flat Start(平坦初期化) により初期モデルを構築します。

  1. 初期配分: 全フレームを発話中の全状態に対して均等に割り当てます。
  2. グローバル分布共有: すべての状態が同一の広い音響分布を参照する状態からスタートし、大まかな音響統計量を収集します。
  3. HMM反復学習: 幾何継続時間モデル(HMM)としてEM学習を回し、音素の大まかな時間境界を獲得します。
  4. HSMM精密学習: 得られた境界情報をアンカーとして、明示的継続時間モデル(HSMM)へと切り替え、動的枝刈りを行いながら精密な再推定を行います。

Rust版では、初期化 → HMM学習 → モデル保存・読込 → HSMM再推定 → アライメント出力に至る全パイプラインを一気通貫の統合テストとして組み込み、各ステップでの出力モデルがC版の学習結果と互換であることを確認しています。

4.5 モデルの独立化(Untying)

モデル学習の初期段階では、過学習を防ぎ統計量を集めるために同一音素の分布を共有(Tying)させます。しかし、学習が進んだ段階では、発話内の位置や前後の音脈に応じて分布を独立させたい場合があります。これを担うのが shiro-untie です。

shiro-untie は、共有されていた分布パラメータを各出現位置ごとに複製し、新たな分布IDを割り振ります。 オリジナルのCツールには、この複製処理の際にストリームの重み設定が既定値へリセットされてしまう不具合がありましたが、Rust版ではストリーム重みを保ったまま分布の複製が行われるよう修正しました。

4.6 音声区間検出(VAD)への応用:`shiro-wavsplit`

SHIROに含まれる shiro-wavsplit は、録音波形から無音部分を検出し、発話区間ごとにスプリットするためのツールです。

内部ではSHIROの推論・学習エンジンがそのまま利用されています。「無音状態」と「音声状態」が交互に並ぶ2状態モデルを定義し、音響特徴量の抽出とHSMMによるアライメントを実行することで音声区間検出(Voice Activity Detection: VAD)を行います。信号処理と確率推論を分離した設計の柔軟性が現れている部分と言えます。

5. 移植における「互換性」の多層的な基準

CコードをRustへ再実装する際、単にCの振る舞いを機械的に模倣するだけでは、潜在的なメモリ破壊や境界外アクセスといった未定義動作まで仕様として固定化してしまう恐れがあります。 本プロジェクトでは、互換性の対象を以下の6つの階層に分類して検証基準を定義しました。

検証階層

検証対象

主な検証方法

バイナリ互換

モデルファイル、設定データ

C/Rust双方での相互読み書き、バイナリ往復(Round-trip)比較

トポロジー互換

状態数、フレーム数、遷移パス

整数値およびアライメント境界の完全一致検証

数値計算互換

浮動小数点出力、尤度スコア

入力と環境を固定した参照出力とのイプシロン比較

特殊値の整合性

NaN、±∞、符号付きゼロ

IEEE 754のビット表現および分類カテゴリの追跡

安全性の保証

メモリ解放、所有権、エラー境界

不正データ投入時のクリーンなエラー復旧検証

バグの修正

Cの未定義動作、境界外アクセス

パッチ適用済みC参照コードとの比較、不具合再現テスト

5.1 MessagePackシリアライゼーションとバイナリの後方互換

liblrhsmmはモデルデータの永続化にバイナリ形式である MessagePack を使用しています。しかし、汎用のMessagePackシリアライザをそのまま適用するだけでは十分な互換性が得られません。

  • 歴史的フォーマットへの対応: 初期のliblrhsmmには「分散下限(Variance floor)」フィールドが存在せず、後から追加された経緯があります。Rust版のデコーダは、古い形式と新しい形式の双方を判別して読み込めるようにしました。
  • 配列ヘッダーの癖: C実装のシリアライザには、構造体のサイズ宣言や配列要素の順序に固有の仕様が存在しました。これらを安易に整えると既存モデルがロードできなくなるため、バイトレベルで互換性を持つ専用コーデックを実装しました。
  • Cのメモリ破壊バグの修正: 元のC実装において、倍精度の境界データを読み込む際に32ビット浮動小数点の値を64ビット変数領域へポインタキャストで直接書き込み、隣接メモリを破壊するバグが存在しました。Rust版では適切な型変換を挟むことでバイナリ形式を維持しつつ安全にデコードしています。

5.2 数値安定性と後方互換のポリシー分離(Legacy vs Stable)

信号処理のアルゴリズムには、「Cの元実装と全く同一の丸め誤差を再現したいケース」と、「数値的に安定したアルゴリズムを用いたいケース」の双方が存在します。ciglet-rsでは、これを Legacy と Stable の2つの実行ポリシーとしてAPIレベルで選択できるようにしました。

// 利用用途に応じて明示的に数値ポリシーを選択可能
let env_legacy = spec2env(spectrum, f0, Policy::Legacy)?;
let env_stable = spec2env(spectrum, f0, Policy::Stable)?;
  • 分散の算出: 元実装の E[X²] - (E[X])² は、平均値が大きく分散が極めて小さいデータに対して桁落ちを起こします。Stable ポリシーではWelfordのアルゴリズム等の補償演算を適用します。
  • sinc関数の零点付近: リサンプリング処理におけるsinc補間カーネルにおいて、元実装の近似式はゼロ近傍で分母が極小になり巨大な外れ値を生む欠陥がありました。Stable ではテーラー展開によるパッチを適用し、連続性を確保しています。

5.3 コンパイラ最適化(`-Ofast`)による浮動小数点の挙動分析

C言語版との出力比較を行う中で、コンパイラの最適化オプションによって出力される浮動小数点の振る舞いが変化することが分かりました。

例えば、完全な無音フレームに対する対数エネルギー計算では -∞ が発生します。その後の差分計算カーネルにおいて、重み 0.0 との積 0.0 × (-∞) を評価すると、IEEE 754の規格上は NaN(Not a Number) が生成されます。 しかし、Cコードを -Ofast や -ffast-math を有効にしてGCCやClangでコンパイルした場合、コンパイラは「非有限値は発生しない」と仮定して数式を変形するため、このNaNが消失して別の数値へと置き換わっていました。

Rust版の検証では、単に許容誤差(イプシロン)を緩めて済ませるのではなく、「厳密演算のC」「最適化されたC」「Rust実装」 を明確に切り分け、どの差異がアルゴリズムの違いであり、どの差異が処理系のフラグによるものかを特定した上で照合を行いました。

5.4 バグ修正に対する独立検証テストハーネス

Cの元実装に論理バグが存在する場合、「元のCと出力が一致すること」だけを合格基準にしてしまうと未定義動作まで仕様として引き継ぐことになってしまいます。

例として、liblrhsmmの後向き推論において、状態番号の上限チェックを誤って「総フレーム数」と比較してしまっており、状態数よりもフレーム数が短い極小発話で有効な遷移が破棄されるバグがありました。 このようなケースでは、Cのソースコード側に修正パッチを当てた「修正版Cリファレンス」を作成し、さらに数式から直接導出した独立なユニットテストを併用することで、バグを解消しつつアルゴリズムの正当性を確認しました。

6. Rustの型システムによる安全性と設計の刷新

Rustへの移植にあたっては、Cの関数をそのまま置き換えるのではなく、所有権やライフタイム、代数的データ型を活用して「不正な状態を表現できない構造」に整理し直しました。

6.1 キャッシュの不整合を防ぐPrepared Modelパターン

liblrhsmmでは、モデルのパラメータ(平均・分散など)から導出される事前計算キャッシュ(ガウス正規化項やルックアップテーブル)が多く使われます。 C実装ではモデル構造体の中にパラメータとキャッシュ配列が生ポインタとして同居していたため、「パラメータだけを更新してキャッシュが更新されず、不整合な状態で推論が走る」という危険がありました。

Rust版では、この問題を Prepared Modelパターン で解決しました:

// モデル定義そのもの(純粋なパラメータデータ)
pub struct Model {
    pub states: Vec<StateDefinition>,
    pub distributions: Vec<Distribution>,
}

// キャッシュを包含し、元モデルを不変借用する推論専用型
pub struct PreparedModel<'a> {
    model: &'a Model,
    gmm_cache: Vec<GmmPrecomputed>,
    duration_cache: Vec<DurationLut>,
}

impl Model {
    /// パラメータを検証し、整合性が保証された推論用オブジェクトを生成
    pub fn prepare(&self) -> Result<PreparedModel<'_>, ModelError> {
        // キャッシュの事前計算ロジック
    }
}

PreparedModel が存在する間は、ライフタイム 'a によって元の Model は不変借用され、外部からパラメータを変更することはできません。パラメータを更新したい場合は、推論オブジェクトを破棄してからモデルを書き換え、再度 .prepare() を呼ぶ流れになります。これにより、キャッシュの不整合をコンパイル時に防ぐことができます。

6.2 不完全な状態を排除するBuilderパターン

C実装では、モデルの初期化時に各フィールドのポインタを malloc し、設定前の段階ではポインタを NULL にしていました。もし呼び出し側が一部の設定を忘れたまま推論関数へ渡すと、NULLポインタデリファレンスが発生します。

Rust版では Builderパターン を導入し、全スロットに値が設定され、かつモデルの構造(トポロジーと分布の参照整合性)がすべて検証されるまで、完成した Model インスタンスを取得できないようにしました。実行時にNULLチェックを挟むのではなく、インスタンスが存在する時点で不変条件が満たされていることを型レベルで保証しています。

6.3 確率と対数尤度を取り違えないNewtypeパターン

確率計算においては、「通常の確率値(0.0 ≤ p ≤ 1.0)」と「対数尤度(-∞ < ℓ ≤ 0.0)」が混在します。C言語ではどちらも同じ double 型であるため、対数尤度を渡すべき関数に誤って通常の確率値を渡してしまってもコンパイラは検知できません。

Rust版では、これらを Newtypeパターン で区別しました:

#[derive(Copy, Clone, Debug, PartialEq, PartialOrd)]
pub struct Probability(f64);

#[derive(Copy, Clone, Debug, PartialEq, PartialOrd)]
pub struct LogLikelihood(f64);

impl LogLikelihood {
    /// 確率値への変換
    pub fn to_probability(self) -> Probability {
        Probability(self.0.exp())
    }
}

型が異なるため、対数尤度を直接確率値として足し合わせようとするコードはコンパイルエラーになります。誤った値の混入をコンパイル時に検知できるようにしました。

6.4 トランザクション的なパラメータ更新

学習ループにおいて、観測統計量から新しい平均や分散を計算する際、途中のストリームで不正な値(分散が負になるなど)が検知される場合があります。 モデルのメモリをその場で書き換えていると、途中でエラーが発生した際に一部のパラメータだけが更新された不整合な状態が残ってしまいます。

Rust版では、更新用の一時バッファを作成し、すべての検証が通過して初めてモデル実体へ適用する トランザクション的な更新設計 を採用しました。これにより、エラー発生時にもモデルの一貫性が保たれます。

6.5 マルチスレッド並列学習における決定論的な集約

SHIROのオリジナル版は、OpenMPを用いて複数発話の統計量計算をマルチスレッド並列化していました。しかし、尤度スコアの加算処理においてスレッド間の排他制御が欠落しており、競合状態を引き起こす箇所が存在しました。

Rust版では並列処理ライブラリ Rayon を採用しました。 発話ごとの統計量を独立して計算させた後、「入力ファイルの固定順序に従って決定論的(Deterministic)に集約(Reduce)する」 構成をとっています。

浮動小数点の加算は結合法則が成り立たないため、スレッドの完了順序に任せて足し合わせると実行ごとにモデルの数値が微妙に変化してしまいます。集約順序を固定することで、マルチスレッド並列学習であっても同一の結果が得られる決定論的な動作を担保しました。

7. 実音声コーパスと既存モデルによるエンドツーエンド検証

単体テストの通過にとどまらず、実際の音声コーパスと既存の学習済みモデルを接続したエンドツーエンドのアライメント検証を実施しました。

7.1 レガシーモデル特有の「未使用分布」への対処

検証には、音声研究で用いられる CMU ARCTICコーパス(SLT話者) の録音データと、SHIROの公式学習済みモデルを使用しました。

この既存モデルを調査したところ、モデル全体としては数千個の分布を保持しているものの、対象の音声に含まれない音素の継続時間分布には「分散がゼロ」のまま初期化された未使用スロットが含まれていました。 もし推論開始時にモデル内の全分布を一括して事前計算する設計にすると、対象発話では使われないはずの分布のせいで推論全体がエラーになってしまいます。

Rust版では、推論で実際に参照される分布のみをオンデマンドに検証・事前計算する仕組みを導入し、既存のモデル資産をそのまま利用できるようにしました。

7.2 特徴量・状態パス・境界ラベルの一致検証

実音声(16 kHzサンプリング、PCM 16bitモノラル)から512サンプル長(32 ms)、80サンプルホップ(5 ms)で特徴量を抽出し、C版とRust版の出力を比較しました。

[録音波形] ─┬─> C版特徴抽出    ─> 74,268個の特徴値 ─┐
            │                                        ├─> 相対誤差 < 5e-5 で合致
            └─> Rust版特徴抽出 ─> 74,268個の特徴値 ─┘

抽出された合計74,268個の浮動小数点特徴量に対し、以下の相対誤差 ε を測定しました:

ε = |x_Rust - x_C| / max(1.0, |x_C|)

測定の結果、相対誤差の最大値は 約 4.35e-5(4.35 × 10⁻⁵)に収まり、設定した許容基準(5e-5)をクリアしました。さらに、これらの特徴量を推論エンジンに入力して得られた音素境界の時間ラベル(ミリ秒単位の開始・終了時刻)および通過状態の遷移パスは、C言語リファレンスとフレーム単位で完全に一致することを確認しました。

7.3 32ビット環境テストによる潜在バグの炙り出し

ネイティブ環境のテストマトリクスには、WindowsおよびLinuxの64ビット環境だけでなく、32ビット(x86)環境 も含めました(MSVCおよびGNUツールチェーンの計5ターゲット)。

32ビット環境でのテストは、ポインタ幅が4バイトになるため、メモリサイズ計算の潜在的なバグを検知しやすくなります。実際、オリジナルのcigletの周波数変換コードにおいて、バッファ確保のサイズ計算に sizeof(double) ではなく誤ってポインタのサイズ sizeof(double*) を使っていた箇所が見つかりました。64ビット環境ではポインタも double も8バイトであるため偶然正常に動作していましたが、32ビット環境では必要なメモリの半分(4バイト)しか確保されず、バッファオーバーフローを引き起こす状態でした。

Rust版では、型付きのスライスやベクタの利用によりこうしたポインタと型のサイズ違いによるミスを解消した上で、巨大配列のインデックス計算でオーバーフローが発生しないよう防御的なチェックを入れています。

8. 多言語バインディングとクロスプラットフォーム展開

Rustで実装したコアエンジンを活用するため、各リポジトリを Cargo Workspace として構成し、計算コアと各ターゲット環境向けバインディングを分離しました。

shiro-rs/
├── Cargo.toml
├── crates/
│   ├── shiro-rs/         # コアエンジン(Pure Rust / no_std対応可能)
│   ├── shiro-rs-capi/    # 外部連携用 C ABI / ヘッダー生成
│   ├── shiro-rs-wasm/    # WebAssemblyバインディング(ブラウザ実行)
│   ├── shiro-rs-node/    # Node.jsネイティブアドオン(napi-rs)
│   └── shiro-rs-python/  # Python拡張モジュール(PyO3 / maturin)
├── docs/
└── tests/

8.1 WebAssembly(WASM)によるブラウザ完結の音声アライメント

WebAssembly向けバインディング(-wasm)では、サーバーを介さずブラウザのJavaScript環境だけで音声アライメントを完結できます。

WASM環境において、ネイティブの生ポインタをJavaScript側へ漏洩させるのは望ましくありません。そのため、WASM境界では音声データを Float32Array、モデル定義を Uint8Array(MessagePackバイナリ)として受け渡し、安全でシンプルなAPI設計としました。

8.2 パッケージエコシステムへの配布構成

各プラットフォームで導入しやすいよう、以下の形式でパッケージを整理しました。

配布チャネル

ciglet

liblrhsmm

SHIRO

備考

crates.io

ciglet-rs

liblrhsmm-rs

shiro-rs

Pure Rustコアクレート

PyPI (Python)

ciglet-rs

liblrhsmm-rs

shiro-rs

CPython 3.11〜3.15向けビルド済みWheel

npm (Node.js)

@ciglet-rs/node

@liblrhsmm-rs/node

@shiro-rs/node

napi-rsによる各OS向けネイティブバイナリ

npm (Browser)

@ciglet-rs/wasm

@liblrhsmm-rs/wasm

@shiro-rs/wasm

web および bundler 向けWASMパッケージ

Node.jsのネイティブパッケージは、主要OS・アーキテクチャ(Windows, macOS, Linux / x64, arm64等)向けにコンパイル済みのバイナリパッケージをオプショナル依存として自動解決する構成をとっています。

9. CI/CDとサプライチェーン・セキュリティ

再実装プロジェクトの品質を担保するため、GitHub Actionsを用いた自動ビルド・リリースパイプラインを構築しました。

9.1 段階的なクレート公開と依存解決

3つのプロジェクトには ciglet → liblrhsmm → SHIRO という依存関係があります。 crates.ioへの公開時には、まず基盤となる ciglet-rs と liblrhsmm-rs を先行して公開し、SHIRO側の依存記述をローカルのパス指定からcrates.ioの公開バージョンへと切り替えた上でテストを再実行し、最後に shiro-rs を公開するという手順を踏みました。

9.2 OIDCとTrusted Publishingによるセキュアな自動リリース

crates.io、PyPI、npmのパッケージレジストリにおいて、長期有効な固定トークンを使わない Trusted Publishing(OpenID Connect: OIDC認証) を導入しました。

GitHub Actionsの実行環境から発行される短時間有効なOIDCトークンを各レジストリが検証してリリースを承認するため、レジストリのAPIキー漏洩リスクを抑えられます。さらに、npmの Provenance やPyPIの Attestation による暗号署名を生成し、配布されているバイナリがどのGitHubリポジトリのどのコミットからビルドされたものかを検証できるようにしています。

9.3 実機マトリクスでのインストール・実行テスト

リリースアーティファクトが生成された後、ビルドの成否だけでなく、クリーンなテスト環境(Windows/Linux実機コンテナ)において pip install や npm install を実行し、公開APIを呼び出すインテグレーションテストを自動化しました。 ブラウザWASMについても、Headless Chromium、Firefox、WebKitの3つのブラウザエンジン上でWASMモジュールをロードして推論が完走することを確認しています。

10. 振り返りとまとめ

今回の再実装では、C言語で培われた信号処理や時系列モデルの計算構造を尊重しつつ、Rustの言語機能を活かして扱いやすいライブラリへと再構成しました。

CZTのチャープ分解、瞬時周波数の解析的計算、HSMMの開始・終了分離テーブル、ルックアップテーブルによる高速化など、元実装には音声データを効率的に扱うための実践的な工夫が多く含まれていました。

一方で、手動メモリ管理や生ポインタに依存していた箇所については、ライフタイムによるキャッシュ整合性の保証、BuilderやNewtypeによる不正な状態の防止、決定論的な並列集約といったアプローチで現代的な設計に置き換えました。

学術研究やプロトタイプに端を発するCコードを安全にモダナイズする試みとして、一定の知見が得られたと考えています。今後は、さらに多様な音声コーパスでの検証や、各処理単位での詳細なベンチマーク測定などを進めていく予定です。