復元・復旧サービス充実|(有)フロンティア・オンライン

復元・復旧サービス充実|(有)フロンティア・オンライン

【深淵オーディオ】(8)CCC本体:ProAudio仕様PopoDAC(USB DAC) 自作(DIY)

‹ 2026/01/02 ›

こんにちは。8回目、お楽しみDDC中央コントロールセンター(本体)の回がやってきました。

(お待ちかねのショッピングセンターに着きましたよ!ってね)


恐らくみなさんは、「この思想でESP32S3で本当に動くのか?」という疑念と共にここに辿り着いてると思います。


未だに「無理っぽくね?」と思ってる方もおりますでしょう。


結論を先に述べておきます。


PopoDACの完成度はこの執筆時点で既に100%を超えました。


もはや対Windows環境においては、OSだけではなく、その遡上であるYoutube、Winamp、VLC等のアプリケーション個別に起こる反応誤差・ストレスまで感知、制御できる状態までの精度で稼働しています。

もちろんLightning世代のiPhoneにでさえも、99.5%程度の親和性が出てきました。


(というか本物を積むとこんなところまで分かるのか!800円のESP32S3なのに!と私自身驚きを隠せません)

DDC中央コントロールセンターとは何か?

では早速、DDC中央コントロールセンターの役割をordinaryと比較しながら説明します。

前回より少し改良された自転車の図面で見て行きましょう。

DDC-CCC ordinary

この図面は、普通に考えられるCCCの仕事を表しています。


普通のCCCの仕事とは “transfer” 管理です。


・データを受け取る

・データを送る

・必要なら SRC する


これが「普通のUSB-DACのCCCがやっていること」です。

あるいは「やっているつもりになっていること」と言ってもいいかもしれません。


PopoDAC DDC-CCC

前回の通り、大変危うい自転車をコントロールしているのが PopoDAC CCC です。

PopoDAC CCC の役割は Harmony(調和)そのもの。


そしてその調和の管制は、ホスト → DDC → I2S → DAC → 空間 → リスナーへと連続し、ひとつの“時”に結びつきます。


普通のCCCでは見えない世界の果て。

しかし PopoDAC は MASTER TIMELINE によって世界をロックできます。


ロック完了した瞬間、PopoDAC CCC は本来見えない両端を超えた世界に手を伸ばす ようになります。


音源から人に至るまで完全支配し、ひとつの時に結び付ける。

これが ProAudio 仕様の設計思想で生まれる CCC の完成形です。


CCCの本質

で、本質をまとめますと、CCCが最終的に行うことはただひとつ「USB と I2S の世界線を完全に一致させ、その状態を永続的に STUCK(固定化)させること」となります。


この STUCK が成立した瞬間、PopoDAC は「時の操者」になります。

そしてリスナーは、奏者に導かれる世界 に入ります。


CCCを定義する

では具体的に・・・、ここで定義するCCCは、まるでPAXA宇宙指令管制センターのようなものになります。


 ・UACロケット

 ・I2Sロケット

 ・発射準備

 ・プリロール制御

 ・軌道・姿勢制御

 ・時・空間の均衡化


実射に必要な計器類、制御ボタンに相当するものはここで整えます。

もちろん、弾道起動(MASTER TIMELINE)に乗った後の制御も含め、コントロールに必要なものは全てここに備える必要があります。


ddc_control_t構造体

では、早速DDCコントロールセンターの定義です。


typedef struct {

    // audio stream 設定

    audio_quality_t uac_quality;


    // USB 側の状態管理

    bool usb_mounted; // USBマウント状態

    bool uac_alt1_active; // Out EP開閉(SPEAKERストリーム)

    uint8_t uac_alt1_muted; // ミュート有無

    int16_t uac_alt1_volume; // ボリューム


    // Feedbackプリロール、立ち上がりのドリフトをFB軽減

    int16_t uac_fb_inverval_count; /* 送信間隔を(1 << (UAC_EXPLICIT_FB_INTERVAL-1)ms間隔に合わせる */

    bool uac_fb_started; // FB発射開始シグナル

    bool uac_fb_start_pending; // FB発射準備シグナル

    bool uac_fb_locked; // FBロック状態

    int16_t uac_fb_lockcount; // FB 未ロックカウント

    int32_t uac_long_err_mHz; // 長期誤差蓄積(mHz 単位)


    // I2Sプリロール、ALT1立ち上がり直後のドリフト吸収

    bool i2s_starting; // I2S発射準備シグナル

    uint32_t i2s_start_ms; // I2S発射カウントダウンタイマー


    // I2S ASRCコントロ-ラ(SRC & Killer)

    bool i2s_src_enabled; // ASRC 有効/無効(デバッグ用)

    bool i2s_src_inited; // SRC Preroll済みフラグ

    bool i2s_src_locked; // SRC ロック済み

    int16_t i2s_src_lockcount; // SRC 未ロックカウント

    int32_t i2s_pcnt_delta_hz; // SRCに渡す側のデルタHz

    //float i2s_src_clock_ratio; // 制御ループ >> CLOCK側に移動

    float i2s_src_phase_ratio; // resampler 用(USB/I2S)

    float i2s_src_smooth_ratio; // SRC補正レート

    float i2s_src_phase;      // ASRC の位相(補間器の内部状態)

    int i2s_src_in_idx; // 補間器の入力サンプル位置

    

    // カウンタープリロール

    bool counter_warmup_rollout; // カウンター初動完了


    // 監視 健康状態のモニタリング

    int16_t ddc_ring_looses; // DDC ringの渇水、量子化ノイズ、ジッタノイズ源

    float ddc_ring_wl; // DDC ring water level

    int16_t ddc_rx_size; // UAC受信バッファサイズ

    int16_t ddc_tx_size; // I2S送信バッファサイズ

} ddc_control_t;


どうでしょう。

本当に指令管制センターっぽいですね。^^


とても玩具MPUモジュールで自作するUSB-DACの管制塔には思えないくらいギッシリあります。汗


「やたらと複雑化しただけなのでは?」

と、つい思ってしまうほどに・・・。


PopoDACは、きちんと量子(最小単位)を捕まえるDDCをもちますので、このくらい必要になります。

そして、実際の稼働部コードはさほど複雑にはなりません。

寧ろ整然としてすっきり綺麗です。


audio_quality_t構造体

DDCコントロールの冒頭に挿し込まれてますが、これは管制というより単なるプリセットのカレントセットになります。

typedef struct {

    int32_t sample_rate; 

    int8_t channels;

    int8_t resolution_bits;

    int8_t sample_bytes;

    int16_t frame_size;

    int16_t mclk_multi;

    float rms_max;

} audio_quality_t;


特にここでは実パルスウントしますので、sample_rateと共にmclk_multiも支配下に置きます。

rms_maxは、ディスプレイ用にMAXを固定値でセットしてます。


混同しやすいのがsample_bytesとframe_sizeでしょうか。

sample_bytesをここではresolution_bytesという意味で扱っています。

(直すのが面倒)

frame_sizeはper 1ms バイト数で、channels数を反映しています。


clock_counter_t構造体

PCNT周りの細かな情報は、DDCから外して専用の構造体にしました。

typedef struct {

    int src;

    int mclk;

    int16_t mclk_multiple;

    int16_t lrck;

    int bclk;

    int bclk_divn; // 整数部= mclk / bclk

    int bclk_divi; // 端数部= mclk % bclk


    // 以下はカウンタ

    int16_t pcnt_diff;

    int16_t pcnt_cur;

    int16_t pcnt_last;

    int32_t pcnt_fs;

    int16_t pcnt_warmup;

    int32_t pcnt_fs_history[3]; // 3点中央値フィルタ

    int16_t pcnt_fs_factor;


    // 以下はASRC用

    uint32_t last_fb;        // Q14

    int32_t acc_q14;        // fractional 累積用 Q14

    int32_t delta_fb_q;

    float source_clock_ratio;

} clock_counter_t;


「ん?bclk_divi要らなくね?

はい、いりません。


プログラムミス検知用です。汗


中段はカウント計測用、終段は補償用に使います。


log_entry_t構造体

ログ監視用のエントリーです。

typedef struct {

    bool uac_fb_started;  // UAC FB開始後

    bool uac_fb_locked;   // UAC FBロック中

    bool i2s_inited;      // I2S初期化

    bool i2s_locked;      // I2Sロック中

    bool pcnt_countdown;  // 立上げカウントダウン

    bool pcnt_rollupped;  // カウンター実運転中


    int16_t delta_fb_q;    // UAC Feedback用(10.14)

    int32_t delta_hz;      // ASRC入力用

    

    int16_t pcnt_diff;       // PCNT生値

    int32_t pcnt_fs;        // PCNT Hz

    int32_t fs_raw;          // PCNT生→Hz生

    int32_t long_err_mHz;    // FB長期誤差

    float   ratio_clock;     // 制御ループ用(I2S/USB)

    float   ratio_phase;     // resampler 用(USB/I2S)

    float   ratio_smooth;    // ASRC内部補正

    

    float i2s_src_phase;      // ASRC の位相(補間器の内部状態)

    int i2s_src_in_idx; // 補間器の入力サンプル位置


    uint64_t tick_us;   // ★ 追加:ログ時刻(マイクロ秒)

    call_position_e call_position; // 呼び出し元

} log_entry_t;


その名の通りDDCコントロールの計器値のコピーです。

そしてそのコピー履歴を作るためにあります。

デバッグのためだけに使いますので、Release稼働では使わなくてもよいものとなります。


なぜ必要かということなのですが、DDCはデバッグ中でも堅固なリアルタム性で稼働する必要があるため、開発者側が直接DDCコントロールの状況把握に割って入ることができません。

デバッグ運転中、その不安定の要因は神のみぞ知るということでして、後から取り出し検討するを繰り返すことになります。


ロケット打ち上げ失敗を事後検討するのと一緒です。


ddc_log_print_detail関数

実際のログ出力フォーマットです。

int32_t ddc_log_print_detail(ddc_control_t *ddc, int32_t start, int32_t count)

{

    if (start >= MAX_DDC_LOG)

        return 0;


    // Header

    if (start == 0) {

        ESP_LOGI(TAG,

            "DDC cfg rate=%u bits=%d ch=%d mclk_multi=%d",

            ddc->uac_quality.sample_rate,

            ddc->uac_quality.resolution_bits,

            ddc->uac_quality.channels,

            ddc->uac_quality.mclk_multi

        );

    }


    int tail = start count;

    if (tail > MAX_DDC_LOG)

        tail = MAX_DDC_LOG;


    for (int i = start; i < tail; i ) {

        log_entry_t *L = &g_ddc_log[i];


        // ---- フェーズ判定 ----

        const char *phase =

            (!L->i2s_locked)        ? "PHASE0(I2S_INIT)" :

            (!L->pcnt_rollupped)    ? "PHASE1(PCNT_WARMUP)" :

            (!L->uac_fb_started)    ? "PHASE2(FB_WAIT_START)" :

            (!L->uac_fb_locked)     ? "PHASE3(FB_WAIT_LOCK)" :

                                      "PHASE4(LOCKED)";


        // ---- STUCK 判定 ----

        bool fs_stuck     = (L->fs_raw == ddc->uac_quality.sample_rate);

        bool pcntfs_stuck = (L->pcnt_fs == ddc->uac_quality.sample_rate);

        bool dhz_stuck    = (L->delta_hz == 0);

        bool dfb_stuck    = (L->delta_fb_q == 0);


        ESP_LOGI(TAG,

            "idx=%d tick=%u part=%d %s\n"

            "  I2S: init=%d lock=%d\n"

            "  PCNT: warm=%d up=%d diff=%d fs_raw=%ld%s fs=%d%s dhz=%ld%s\n"

            "  FB: start=%d lock=%d dfb=%d%s long=%ld ratio clock=%.6f\n"

            "  SRC: ratio phase=%.6f smooth=%.6f",

            i,

            (uint32_t)(L->tick_us & 0xFFFFFFFF),

            L->call_position,

            phase,


            // I2S

            L->i2s_inited,

            L->i2s_locked,


            // PCNT

            L->pcnt_countdown,

            L->pcnt_rollupped,

            L->pcnt_diff,

            L->fs_raw,     fs_stuck     ? " (STUCK)" : "",

            L->pcnt_fs,    pcntfs_stuck ? " (STUCK)" : "",

            L->delta_hz,   dhz_stuck    ? " (NO-MOVE)" : "",


            // FB

            L->uac_fb_started,

            L->uac_fb_locked,

            L->delta_fb_q, dfb_stuck ? " (NO-MOVE)" : "",

            L->long_err_mHz,

            L->ratio_clock,


            // SRC

            L->ratio_clock,

            L->ratio_smooth

        );

    }

    return tail - start;

}


PHASE0からPHASE4までの遷移が綺麗に追えます。

ここまで来るとDDCコントロールが管制していること、その状況が掴めてきますね。


CCC 構造体まとめ

まとめると、


・audio_quality_t → 名目設定

・clock_counter_t → 実クロックの観測と補償

・ddc_control_t → 全体の状態と制御の中枢

・log_entry_t → 飛行記録(ブラックボックス)


という役割分担になります。


CCCで実管制する

ここまで装備しますとCCC実用管制に入れます。


重要なのは、これらの構造体を駆使して得られる総合判断可能な整理された情報になります。


・Phase(フェーズ)

・Current Sample Rate(瞬間サンプルレート)

・Plateau Position(Stuck位置)

・Water Level(Ring水位)

・SRC Rating(ARC稼働率)


概ねこの範囲を掴みきれるようになれば、CCCの実管制がどの程度行き届いているのか分かるようになります。

そして同時に、これらが理想通りな計測値を示している時、PopoDACは時を支配しています。


では次回、あなたはついに PopoDACの心臓が実際にどう動いているのか を目撃することになります。

それを見た瞬間、あなたのデジタルオーディオの常識は完全に書き換わります。


そして、どのようにして“時の操者”としてのPopoDACが生まれるのか。

その真実が見えてきます。


お楽しみに♪