Volumio2のREST APIにgetqueueを追加する方法

 REST APIでは再生キューのリストが取れないため調査したところ、volumio2に数行のコード追加で取得できるように改造可能とわかったのでその方法をメモ。

 内部的には再生キューを生成するWeb Socket用のコードがすでに存在するため、REST APIのコードにgetqueueのハンドラを追加し、再生キューのリストをレスポンスとして返すようにしてあげるだけです。
 getstateの次あたりに追加すると良いんじゃないかと。v2.562で確認しました。

  • 修正するファイル
 /volumio/app/plugins/user_interface/rest_api/index.js
  • 追加するコード
    api.route('/getqueue')
        .get(function (req, res) {
            var response = self.commandRouter.volumioGetQueue();

            if (response != undefined)
                res.json(response);
            else
                res.json(notFound);
        });

 
 選曲はplayコマンドにリストの番号を追加するだけでOK。0オリジンなことに注意。
 3曲目を指定するならN=2を指定する。

/api/v1/commands/?cmd=play&N=2

 Web SocketとREST APIでは応答電文が共通のため頑張れば大部分をREST API化できるかもしれない。REST APIだとcurlコマンドとかで簡単に制御できるので、シェルスクリプトで色々やるのには便利です。

BCLK/LRCLK出力しかないRaspberry Piで旭化成のDAC IC(AK449xシリーズ)を鳴らす実験

 Raspberry PiでDAI/ASRC/CPLDなどのクロック生成周辺回路を用いず、旭化成DAC AK449Xシリーズを使用する方法についてメモ。
 Raspberry PiDACにAK449xシリーズの採用が少ない原因のひとつとして、AK449xの要求するMCLKをRaspberry Piから供給できない点が挙げられます。
 今回は、周辺回路の追加無しで、なんとかAK449xシリーズを鳴らせないかな?という実験となります。

 実験には@blue__sevenさん製作のChiKoiDac-AK4495Sを使用しました。ChiKoiDac-AK4495SはNanoPi NEO2向けの小型DACボードで、NEO2サイズの基板にAK4495S・OPAMP LPF・電源回路を搭載したお手軽高音質DACです。
 NanoPi NEO2では専用品だけあってそのまま乗せるだけですが、Raspberry Pi Zeroでは互換のあるピン部分をそのまま差し込み、不足するI2SのDATA/LRCLK/BCLKをジャンパケーブルで接続することで使用できます。

f:id:tkztkztkz:20190205231002j:plain
Raspberry PI Zero with AK4495S DAC

方法1

  • @blue__sevenさんに教えて頂いた方法です。
  • AK449xは、MCLKにBCLKを入力するだけで音は出ます。仕様範囲外の動作となります。
    • 44.1kHz/32bit時にはBCLK = MCLK = 64fs = 2.8224MHz
  • MCLKが規定のクロックに達していないため、デジフィル(デジタルフィルタ)は正常に動作しません。
  • オシロで確認する限りNOS(Non-Oversampling)出力ではないようで、何らかの簡易フィルタは掛かっているように見えます。
  • デジフィルにショートディレイやNOSを選んでも、出力波形に変化はありません。
    f:id:tkztkztkz:20190205231256p:plain
    Raspberry Pi Zero Internel PLL MCLK=BCLK=64fs (AK4495S) 1kHz Sine Zoom
    f:id:tkztkztkz:20190205231354p:plain
    Raspberry Pi Zero Internal PLL MCLK=BCLK=64fs (AK4495S) 1kHz Square

方法2

  • MCLKとBCLKを同じクロックで入力するところまでは方法1と同様
  • BCLKを標準の32bit再生時64fsではなく、MCLK相当の512fsまでクロックを上げます。
    • 44.1kHz/32bit時にはBCLK = MCLK = 512fs = 22.5792MHz
  • I2Sドライバの改造が必要です。修正内容は下記の感じ
    • BCLKを大幅に上昇させる 64fs->512fs
    • fslenを適切に設定する
  • オシロで確認する限り、デジフィルはちゃんと動いているように見えます。

    f:id:tkztkztkz:20190205231555p:plain
    Raspberry Pi Zero Internel PLL MCLK=BCLK=512fs (AK4495S) 1kHz Sine Zoom DF SSD
    f:id:tkztkztkz:20190205232013p:plain
    Raspberry Pi Zero Internel PLL MCLK=BCLK=512fs (AK4495S) 1kHz Square DF SSD
    f:id:tkztkztkz:20190205231706p:plain
    Raspberry Pi Zero Internel PLL MCLK=BCLK=512fs (AK4495S) 1kHz Sine Zoom DF NOS

  • AK449XシリーズではBCLKをデータの取り込みタイミングだけに使っているから実現できているようです。LRCLKのエッジから少しだけデータを送出していることがわかります。通常64fsのところ8倍の512fsでBCLKを駆動しているため、エッジから1/8くらいデータが出ています。

    f:id:tkztkztkz:20190205232105p:plain
    Raspberry Pi Zero Internel PLL MCLK=BCLK=512fs (AK4495S) LRCLK & DATA

方法2 検証ソース修正例

  • 割とひどいパッチですが実験時のコードを例示します
    • bclk_rateは24.576MHzをfsで割り切れたら24.576MHz。22.5792MHzをfsで割り切れたら22.5792MHz固定出力にします
    • frame_lengthを変更したbclk ratioで再算出
    • tx_ch2_posもそのままでは正しく計算できないので、framesync_length(frame_length/2)から再算出
  • いろいろ改造しているのでdiffのoffsetは狂ってます。参考まで
*** sound/soc/bcm/bcm2835-i2s.c 2019-01-22 01:56:36.880202010 +0000
--- sound/soc/bcm/bcm2835-i2s_old.c     2019-01-22 01:25:58.276303397 +0000
***************
*** 31,37 ****
   * General Public License for more details.
   */

  #include <linux/bitops.h>
  #include <linux/clk.h>
--- 31,39 ----
   * General Public License for more details.
   */

+ #define AKFS

  #include <linux/bitops.h>
  #include <linux/clk.h>
***************
*** 393,398 ****
--- 394,424 ----
                if (bclk_rate < 0)
                        return bclk_rate;
        }
+ #ifdef AKFS
+       if ( !(24576000 % params_rate(params))) {
+               bclk_rate = 24576000;
+       }else
+       if ( !(22579200 % params_rate(params))) {
+               bclk_rate = 22579200;
+       }else{
+               dev_info(dev->dev, "unknownrate = %d", params_rate(params));
+       }
+       frame_length = bclk_rate / params_rate(params);
+       dev_info(dev->dev, "bclk = %d, flen = %d", bclk_rate, frame_length);
+ #endif

        /* Check if data fits into slots */
        if (data_length > slot_width)
***************
*** 520,527 ****
                rx_mask, slot_width, data_delay, odd_slot_offset);
        bcm2835_i2s_calc_channel_pos(&tx_ch1_pos, &tx_ch2_pos,
                tx_mask, slot_width, data_delay, odd_slot_offset);

        /*
         * Transmitting data immediately after frame start, eg
         * in left-justified or DSP mode A, only works stable
--- 546,567 ----
                rx_mask, slot_width, data_delay, odd_slot_offset);
        bcm2835_i2s_calc_channel_pos(&tx_ch1_pos, &tx_ch2_pos,
                tx_mask, slot_width, data_delay, odd_slot_offset);

+
+ #if defined(AKFS)
+       tx_ch2_pos = framesync_length + 1;
+       dev_info(dev->dev, "tx_ch1_pos = %d, tx_ch2_pos = %d", tx_ch1_pos, tx_ch2_pos);
+ #endif
        /*
         * Transmitting data immediately after frame start, eg
         * in left-justified or DSP mode A, only works stable

考察とかなんとか

  • AK449Xの動作とか
    • AK449x系はMCLK/LRCLKの比を見てデジフィルの動作を決めてるっぽい。BCLKは関与していない?
    • 実際仕様書にもMCLKとLRCLKは同期する必要があると記載。BCLK/DATAについては触れていない
    • BCLKはDATAの取り込みタイミングだけに使われてると予想
  • MCLKが低いときのデジフィル動作について
    • 768kHz再生時のデジフィル動作に似ているとのこと
    • ハイレート再生時の簡易フィルタと同じ動きになるのかな?
    • 実際に768kHz/32bit=64fs再生時には、MCLK=BCLK=49.152MHzで再生している
  • 音質は?
    • NEO2のAUDIO PLLに比べると、なんかいまいちっぽい
    • ラズパイのAudio PLLはAccumlated Jitterが発生するのでそれの影響?

コードについて

 今回は検証用にI2Sドライバを無理やり改造したが、dai linkドライバ(hifiberry-dac.cに代わるもの)を新規に起こして、I2Sドライバはオリジナルのままで動作させる方向で開発中です。

おまけ:前提のお話

  • I2Sとは何か?
    • BCLK/LRCLK/DATAの3本の信号線を用いてIC間でオーディオ信号を転送する規格
    • Inter-IC Soundの略でI2S
  • LRCLK(LR Clock)とは何か?
    • I2S I/Fでデータを転送するとき左右どちらのチャネルを転送しているか示す信号
    • I2SではLRCLK信号がLOW区間でLチャネルのデータ、HIGH区間でRチャネルのデータ転送を行なっていることを示しています。
  • クロックのfs表記
    • DAC ICの仕様書でよく出てくる単位。BCLKやMCLKの周波数表記に良く使われる。MCLK=256,512fsなど
    • LRCLK一周期 = 1fsとする。LRCLKの別名がFrame Syncというだけの話
    • 1fsの周波数は、サンプリング周波数と同値になります。44.1kHzのとき1fs=44.1kHz。48kHzのとき1fs=48kHz
    • I2Sドライバ内ではBCLKとLRCLKの比(bclk_ratio)を表すため重要な単位となります。
  • BCLK(Bit Clock)とは何か?
    • I2S I/Fでデータを転送する基準クロック
    • BCLKは多くの場合32/64fsの二択(それぞれ量子化ビット数 16/32bit時の最小転送サイズになるため)
      • I2S 44.1kHz/16bit転送の場合、BCLK = 32fs = 16 * 2 * 44.1kHz = 1.4112MHz
      • I2S 44.1kHz/32bit転送の場合、BCLK = 64fs = 32 * 2 * 44.1kHz = 2.8224MHz
      • 24bit/48fsも時々使うが、クロックの生成が面倒なので古いDAC ICくらいでしか使わない。
  • MCLK(Master Clock)とは何か?
    • I2S I/Fでは必須ではない信号 (I2S自体はBCLK/LRCLK/DATAの3本あれば良い)
    • DAC ICが内部でオーバーサンプリングなどのデジタル信号処理を行う基準クロックとして要求している。別名SCLK(System Clock)
      • 多くの場合BCLKの2/4/8倍くらいを要求します。
      • LRCLK 44.1/48kHz 32bit時にBCLKの8倍だと64fs * 8 = 512fsとなり、MCLKはそれぞれ22.5792MHz/24.576MHz
      • この二つのクロックの最小公倍数は3.6GHzになるため一つの発振器から生成するのは困難
      • よって2系統のクロック発振器を使うか、PLLにて生成する必要がある
    • DAC IC次第だが、MCLKとBCLK and/or LRCLKの同期が必要なケースが多い
      • MCLKから分周してBCLK/LRCLKを生成するか、BCLK/LRCLKを逓倍してMCLKを生成する必要がある
      • 例外としてESSのDACは非同期でもOKなようです。
    • 要するに用意するのが面倒で、量産を考えるとコストが掛かる。
  • Raspberry PiはMCLK入出力機能を持っていない
    • Raspberry Piシリーズでは、内蔵PLLによるBCLK/LRCLKの生成・出力できるが、MCLKの出力機能はない。
    • Allwinner H5(NanoPi NEO2など)は内蔵AUDIO PLLにてMCLKを生成し出力可能。入力はなし。
    • TI AM335X(BBB/PocketBeagleなど)はMCLK入力が可能。出力はなし。入力したMCLKを基にBCLK/LRCLKを生成・出力が可能

Linux向けAK449x Codec Driverの紹介

 旭化成エレクトロニクス製 AK4490/AK4495/AK4497/AK4493向けLinux Driverの紹介です。NanoPi NEO2でAK449xシリーズを鳴らすために作成しました。

f:id:tkztkztkz:20190124231738j:plain
 ALSA SoC Codecドライバとして実装しています。NanoPi NEO2/Raspberry Pi Zeroで動作確認済み。そのほかのSBCでもDAI LINKドライバとdevice-treeを適切に用意すれば使用可能だと思います。Raspberry Pi Zeroで鳴らす手順はそのうちブログに書く予定です。

 ライセンスはLinux Kernel ModuleなのでGPLv2です。自由に組み込んで使用してください。何か面白いブツができたら教えて頂けるとうれしいです。

できること

  • 標準機能
    • PDN制御(DAC IC PDN信号のGPIO出力)
    • 外部MUTE制御(MUTE制御信号のGPIO出力)
    • ソフトMUTE制御(AKレジスタを叩いてのMUTE)
    • 複数Codec同時制御(I2Cバスあたり最大4個まで)
  • Alsa Mixer設定可能項目
    • ディエンファシス設定
    • 電子ボリューム設定
    • デジタルフィルタ設定
    • Sound Control設定
    • HLOAD ON/OFF設定(AK4497のみ)
    • Gain Control設定(AK4493/AK4497のみ)
    • ATT遷移時間設定(AK4493/AK4497のみ)
  • 出力制御機能(device-treeで設定)
    • STEREO出力の左右入れ替え
    • MONOモード設定
    • 出力位相の変更

ドライバのダウンロード

この辺にソースがあります。
https://raw.githubusercontent.com/tkztkztkz/linux/npi-audio-4.11.y/sound/soc/codecs/ak449x.c https://raw.githubusercontent.com/tkztkztkz/linux/npi-audio-4.11.y/sound/soc/codecs/ak449x.h

NanoPi NEO2用のイメージだとこの辺に導入済みです。
NanoPi Neo2用 Volumio2 - _tkz_ memo

dts設定方法

NanoPi NEO2

dtsの設定例 通常のステレオ仕様(I2C ADDR 0x10)
&i2c0 {
        status = "okay";

        ak449x: ak449x-codec@10 {
                #sound-dai-cells = <0>;
                compatible = "asahi-kasei,ak449x";
                reg = <0x10>; /* I2C addr */
                status = "okay";

                chip = "AK4495"; /* AK4490/AK4495/AK4497/AK4493 */
                reset-gpios = <&pio 6 7 GPIO_ACTIVE_LOW>; /* PG7 */ /* pdn pin */
                mute-gpios = <&pio 6 8 GPIO_ACTIVE_LOW>; /* PG8 */ /* external mute */
        };

&i2s0 {
        sound-dai = <&ak449x>; /* dai link target */
        status = "okay";
};
dtsの設定例 DualMonoバランス出力仕様(I2C ADDR 0x10/0x11)
&i2c0 {
        ak449x1: ak449x-codec@10 {
                #sound-dai-cells = <0>;
                compatible = "asahi-kasei,ak449x";
                reg = <0x10>; /* I2C addr */
                status = "okay";

                chip = "AK4495"; /* AK4490/AK4495/AK4497/AK4493 */
                chmode = "MONO_LCH"; /* STEREO/STEREO_INVERT/MONO_LCH/MONO_RCH */
                phase = "LIRN";  /* LNRN LNRI LIRN LIRI - N=non-invert I=invert*/
                reset-gpios = <&pio 6 7 GPIO_ACTIVE_LOW>; /* PG7 */ /* pdn pin */
                mute-gpios = <&pio 6 8 GPIO_ACTIVE_LOW>; /* PG8 */ /* external mute */
        };
        ak449x2: ak449x-codec@11 {
                #sound-dai-cells = <0>;
                compatible = "asahi-kasei,ak449x";
                reg = <0x11>; /* I2C addr */
                status = "okay";

                chip = "AK4495"; /* AK4490/AK4495/AK4497/AK4493 */
                chmode = "MONO_RCH"; /* STEREO/STEREO_INVERT/MONO_LCH/MONO_RCH */
                phase = "LIRN"; /* LNRN LNRI LIRN LIRI - N=non-invert I=invert*/
       };
};

&i2s0 {
        sound-dai = <&ak449x1 &ak449x2>; /* dai link target */
        status = "okay";
};

Raspberry Pi Zero

dt-overlayの設定例 通常のステレオ仕様(I2C ADDR 0x10)
// Definitions for HiFiBerry DAC
/dts-v1/;
/plugin/;

/ {
        compatible = "brcm,bcm2708";

        fragment@0 {
                target = <&i2s>;
                __overlay__ {
                        status = "okay";
                };
        };
        fragment@1 {
                target = <&i2c1>;
                __overlay__ {
                        #address-cells = <1>;
                        #size-cells = <0>;
                        status = "okay";

                        ak449x: ak449x-codec@10 {
                                #sound-dai-cells = <0>;
                                compatible = "asahi-kasei,ak449x";
                                reg = <0x10>; /* I2C addr */
                                status = "okay";

                                chip = "AK4495"; /* AK4490/AK4495/AK4497/AK4493 */
                                chmode = "STEREO"; /* STEREO/STEREO_INVERT/MONO_LCH/MONO_RCH */
                                reset-gpios = <&gpio 15 1>; /* GPIO15 */ /* pdn pin */
                                mute-gpios = <&gpio 23 1>;  /* GPIO23 */ /* external mute */
                        };
                };
        };

        fragment@2 {
                target = <&sound>;
                __overlay__ {
                        compatible = "hifiberry,hifiberry-dac";
                        i2s-controller = <&i2s>;
                        i2s-codec = <&ak449x>;
                        status = "okay";
                };
        };
};

パラメータについて

  • chip (AK4490/AK4495/AK4497/AK4493)
    • 制御対象チップを設定します
    • デフォルトは4495となります
    • 下記項目の動作が変わります
      • HLOAD(4497のみ)
      • Gain Control(4493/4497のみ)
      • ソフトMUTEの遷移時間
  • chmode (STEREO/STEREO_INVERT/MONO_LCH/MONO_RCH)
    • 動作モードを設定します
    • デフォルトはSTEREOとなります
    • Dual MonoやSTEREOの左右を入れ替える場合に指定します
  • phase (LNRN LNRI LIRN LIRI - N=non-invert I=invert)
    • 出力位相を設定します
    • デフォルトはLNRNとなります。L/R CHとも正相出力。
    • LIRIにするとL/R CHとも逆相出力となります。
    • 出力位相を変えたり、Dual Mono設定時、バランス出力する場合などに指定します
  • reset-gpios
    • PDN制御出力を行うピンを指定します
    • デフォルトは無効です
    • 出力論理はGPIOドライバの指定に従います(出力論理変更ができない場合はActive Highになるはず)
  • mute-gpios
    • 外部MUTE制御出力を行うピンを指定します
    • デフォルトは無効です
    • 出力論理はGPIOドライバの指定に従います(出力論理変更ができない場合はActive Highになるはず)
    • MUTEの制御順は下記となります
      • Power On -> 外部MUTE assert
      • 再生開始 MCLK/LRCLK設定 -> 外部MUTE negate -> ソフトMUTE negate -> wait
      • 再生停止 ソフトMUTE assert -> wait -> 外部MUTE assert -> MCLK/LRCLK設定

備考

  • ポップノイズ回避について
    • AK449xシリーズはMCLK/LRCLKの開始・停止でポップノイズが発生します
    • ソフトMUTE制御だけでは回避できません
    • 外部MUTE制御を使用して、リレーかFETでMUTE回路を組むことをオススメします
  • 複数のAK449Xを制御する場合には、最初に指定するcodecにのみGPIO設定を記述してください
  • PDN制御について
    • 旭化成DAC ICはPDN制御が鬼門だったりします。規定のタイミングを守らないと動かないケースがあります。
    • POWER ONから一定時間LowにしHighを入れる必要があります。
    • PDN信号をCRで鈍らせてプルアップを入れても動かなくはありませんが、場合によっては不安定になるため外部からきっちりリセットパルスを入れたほうが良いです。

Raspberry Pi ZeroのI2Sオーディオドライバに非Codecドライバを追加登録する方法

概要

  • ALSA SoCドライバで電子ボリュームを利用する方法を調べてみた。
  • 今回は電子ボリューム内蔵ヘッドフォンアンプIC TPA6130A2ドライバをhifiberry-dacドライバに登録してみる。

仕組み

  • TPA6130A2のドライバはsound/soc/codecs/tpa6130a2.cを使用する
  • TPA6130A2のようなCodecではないボリューム制御のみのICはaux devとして登録する
  • aux devはcodecではないため、codecとしてdai link登録はできない
  • dai linkではなく、soc_card構造体のaux devsに登録してregisterする

ソース修正例

  • サウンドカード(soc_card)を登録するDAI LINKドライバにdevice treeのi2s-auxdevエントリ参照処理を追加する
    • device treeにi2s-auxdevエントリがあればそのデバイスノードをaux devに登録する
    • i2s_auxdevに加えてi2s-codecをdevice treeから指定する機能も組み込んでいます。
  • 修正コード sound/soc/bcm/hifiberry_dac.c
@@ -14,6 +14,8 @@
  * General Public License for more details.
  */

+#define DEBUG
+
 #include <linux/module.h>
 #include <linux/platform_device.h>

@@ -22,6 +24,8 @@
 #include <sound/pcm_params.h>
 #include <sound/soc.h>
 #include <sound/jack.h>
+#include <linux/of_device.h>
+#include <linux/of_graph.h>

 static int snd_rpi_hifiberry_dac_init(struct snd_soc_pcm_runtime *rtd)
 {
@@ -72,12 +76,14 @@ static struct snd_soc_card snd_rpi_hifiberry_dac = {
 static int snd_rpi_hifiberry_dac_probe(struct platform_device *pdev)
 {
        int ret = 0;
+       int cnt;

        snd_rpi_hifiberry_dac.dev = &pdev->dev;

        if (pdev->dev.of_node) {
                struct device_node *i2s_node;
                struct snd_soc_dai_link *dai = &snd_rpi_hifiberry_dac_dai[0];
+
                i2s_node = of_parse_phandle(pdev->dev.of_node,
                                        "i2s-controller", 0);

@@ -87,6 +93,67 @@ static int snd_rpi_hifiberry_dac_probe(struct platform_device *pdev)
                        dai->platform_name = NULL;
                        dai->platform_of_node = i2s_node;
                }
+
+               cnt = of_count_phandle_with_args(pdev->dev.of_node, "i2s-codec", "#sound-dai-cells");
+               dev_info(&pdev->dev, "%s: i2s-codec sound-dai = %d", __func__, cnt);
+
+               if ( cnt > 0 ) {
+                       struct snd_soc_dai_link_component *codecs;
+                       struct of_phandle_args args;
+                       int i;
+
+                       dai->codec_name = NULL;
+                       dai->codec_dai_name = NULL;
+                       dai->codec_of_node = NULL;
+
+                       codecs = devm_kzalloc(&pdev->dev, sizeof(struct snd_soc_dai_link_component)*cnt, GFP_KERNEL);
+                       if (!codecs)
+                               return -ENOMEM;
+
+                       for ( i = 0; i < cnt ; i++ ) {
+                               ret = of_parse_phandle_with_args(pdev->dev.of_node, "i2s-codec",
+                                       "#sound-dai-cells", i, &args);
+                               if (ret) {
+                                       dev_err(&pdev->dev, "%s: Can't find codec by \"i2s-codec\"\n", __func__);
+                                       return 0;
+                               }
+                               codecs[i].of_node = args.np;
+                               if (snd_soc_get_dai_name(&args, &codecs[i].dai_name) < 0 ) {
+                                       dev_err(&pdev->dev, "%s: failed to find dai name, use codec's name as dai name.\n", __func__);
+                                       codecs[i].dai_name = codecs[i].of_node->name;
+                               }
+                               dev_dbg(&pdev->dev, "add codec %s @ %s", args.np->name, codecs[i].dai_name);
+                       }
+                       dai->codecs = codecs;
+                       dai->num_codecs = cnt;
+               }
+
+               cnt = of_count_phandle_with_args(pdev->dev.of_node, "i2s-auxdev", "#sound-dai-cells");
+               dev_info(&pdev->dev, "%s: i2c-auxdev sound-dai = %d", __func__, cnt);
+
+               if ( cnt > 0 ){
+                       struct snd_soc_aux_dev *auxdevs;
+                       struct of_phandle_args args;
+                       int i;
+
+                       auxdevs = devm_kzalloc(&pdev->dev, sizeof(struct snd_soc_dai_link_component)*cnt, GFP_KERNEL);
+                       if (!auxdevs)
+                               return -ENOMEM;
+
+                       for ( i = 0; i < cnt ; i++ ) {
+                               ret = of_parse_phandle_with_args(pdev->dev.of_node, "i2s-auxdev",
+                                                               "#sound-dai-cells", i, &args);
+                               if (ret) {
+                                       dev_err(&pdev->dev, "%s: Can't find codec by \"i2s-auxdev\"\n", __func__);
+                                       return 0;
+                               }
+                               auxdevs[i].codec_of_node = args.np;
+                               auxdevs[i].name    = args.np->name;
+                               dev_dbg(&pdev->dev, "add aux dev %s", auxdevs[i].name);
+                       }
+                       snd_rpi_hifiberry_dac.aux_dev = auxdevs;
+                       snd_rpi_hifiberry_dac.num_aux_devs = cnt;
+               }
        }

        ret = snd_soc_register_card(&snd_rpi_hifiberry_dac);

device tree overlay設定例

  • fragment@1でTPA6130A2を定義
  • fragment@3のi2s-auxdevでTPA6130A2を参照するように定義
// Definitions for HiFiBerry DAC
/dts-v1/;
/plugin/;

/ {
        compatible = "brcm,bcm2708";

        fragment@0 {
                target = <&i2s>;
                __overlay__ {
                        status = "okay";
                };
        };

        fragment@1 {
                target = <&i2c1>;
                __overlay__ {
                        #address-cells = <1>;
                        #size-cells = <0>;
                        status = "okay";

                        tpa6130a2: tpa6130a2@60 {
                                #sound-dai-cells = <0>;
                                compatible = "ti,tpa6130a2";
                                reg = <0x60>;
                                status = "okay";
                        };

                };
        };

        fragment@2 {
                target-path = "/";
                __overlay__ {
                        pcm5102a: pcm5102a-codec {
                                #sound-dai-cells = <0>;
                                compatible = "ti,pcm5102a";
                                status = "okay";
                        };
                };
        };


        fragment@3 {
                target = <&sound>;
                __overlay__ {
                        compatible = "hifiberry,hifiberry-dac";
                        i2s-controller = <&i2s>;
                        i2s-codec = <&pcm5102a>;
                        i2s-auxdev = <&tpa6130a2>;
                        status = "okay";
                };
        };
};

NanoPi NEO2用Volumio2の機能切り替え(dtb差し替え)について

大まかな仕組み

  • NEO2用Volumio2に採用しているLinuxカーネルはdevice treeという方法でドライバのパラメータなどを変更し実際の動作を切り替えています
  • device treeの実行時バイナリはdtbとdtboの2種あります。
  • NanoPi NEO2の場合、現時点ではdtbのみを使用しています。
  • 使用するdtbは/boot/boot.scrで指定されている /boot/sun50i-h5-nanopi-neo2.dtb となります。
  • そんなわけで、/boot/sun50i-h5-nanopi-neo2.dtbを差し替えることで動作を変えることができるわけです。

変更方法

  • /boot/sun50i-h5-nanopi-neo2.dtbを任意のdtbで上書きすることで動作が変わります。
  • ここではsun50i-h5-nanopi-neo2-i2s-generic.dtbに変更する例を書いておきます。
  • SSHでログインして変更する方法と、Windows上で変更する方法を紹介します。どちらかお好みの方法で変更してください。

その1 NanoPi NEO2にログインして変更

  • TeraTermputtysshコマンドでログインしdtbを変更する方法
  • まずはWEB UIに接続してみます。ブラウザで接続してみましょう。volumio.local(mDNS経由)での接続ができない場合は、直接IPアドレスを指定して接続してください。
  • http://volumio.local
  • 無事繋がったらSSHを有効化します。ブラウザで開発者メニューを開きsshをEnableにします。イメージを書き込んだ後に一度だけやればOKです。
  • http://volumio.local/dev
  • sshクライアントでvolumio2が動作しているNanoPi NEO2に接続します。
ssh volumio.local
  • ログインできたらコピーコマンドでdtbファイルを差し替えます
cp /boot/sun50i-h5-nanopi-neo2-i2s-generic.dtb /boot/sun50i-h5-nanopi-neo2.dtb
  • あとはリブートして反映します
reboot
  • 再起動したらWEB UIから「プレイバックオプション」を開き、出力デバイスを選択し直してください。

その2 MicroSDをカードリーダでWindows PCに接続し書き換え

  • イメージ書き込み済みのMicroSDをカードリーダでPCに接続してください。
  • イメージを書き込んだ直後は認識できていないことがあるので、一度カードリーダを外して付け直してください。
  • ボリューム名BOOTというドライブを認識するはずです。ドライブを認識しない場合は、ドライブ文字の割り当てがされていない可能性があります。ドライブ文字を変更したいのですが?|Q&A | IODATA アイ・オー・データ機器などを参考にBOOTパーティションのドライブ文字を設定してください。
  • まずはsun50i-h5-nanopi-neo2.dtbを削除
  • sun50i-h5-nanopi-neo2-i2s-generic.dtbを複製
  • 複製したファイルをsun50i-h5-nanopi-neo2.dtbにリネームしてください。
  • 「安全な取り外し」を行なったらNEO2にSDカードを差し込み起動してください。
  • 起動したらWEB UIから「プレイバックオプション」を開き、出力デバイスを選択し直してください。

dtbの簡単な説明

  1. sun50i-h5-nanopi-neo2-i2s-generic.dtb

    • もっとも基本的にI2Sドライバです。
    • PCM5102A/ES9023/TDA1387T/TDA1543などのMCLKが不要なDACを鳴らす場合にはこれを選択してください。
  2. sun50i-h5-nanopi-neo2-i2s-generic_32fs.dtb

    • 基本的なI2Sドライバですが、32fs固定となっています。
    • 16bit固定出力になります。上記dtbに比べるとBCLKの周波数が低くなります。
    • TDA1387T/TDA1543などの16bitが上限のDACで384kHz再生する場合にオススメです。
  3. sun50i-h5-nanopi-neo2-i2s-generic_mclk.dtb

    • 基本的なI2Sドライバですが、BCLK/LRCLKに加えPA6ピンからMCLKを出力します。
    • MCLKが必要なFN1242A/PCM1794などの再生に利用します。
  4. sun50i-h5-nanopi-neo2-pcm_rj16_32fs_mclk.dtb

    • PCM RJ 16bit固定出力とするドライバです。
    • TDA1545A/TDA1543Aなど、RJフォーマットで16bitが上限のDACで再生する場合に利用します。
  5. sun50i-h5-nanopi-neo2-i2s-generic_mclk_ak449x.dtb

    • I2S MCLK有りでAK4490/AK4493/AK4495/AK4497を利用する場合に使用します。
    • I2CでAK449xを制御することができ、mixerから電子ボリュームやデジフィルの変更を行えます。
    • デフォルトのI2Cアドレスは0x10、制御デバイスはAK4495となっています。
    • 他のデバイスを使用する場合にはdtbファイルをdtsに変換し編集の後、dtbに再変換してください。
  6. sun50i-h5-nanopi-neo2-i2s-dacpluspro.dtb

    • I2SでPCM5122+外部クロック対応のDACを利用する場合に使用します。
    • HifiBerry DAC+ PROもしくは互換DACで動作します。
    • 外部回路はHifiBerry DAC+ PRO互換としてください。
  7. sun50i-h5-nanopi-neo2-spdif.dtb

その他情報・メモなど

  • device tree関係

  • mDNS関係

    • volumio.localでのアクセスですが、LinuxMacであれば問題なくアクセスできると思います。
    • 同じネットワーク内に複数Volumio2を動かしているときどうなるかは不明。
    • Volumio2のWEB UIでホスト名を変えるとvolumioの部分が変わりますのでご注意を。
    • WindowsでのmDNSの動作はよくわかりません。Windows10で実装されたようですが完璧では無いようで。
    • Windows利用者でもiTunesを導入している環境だとBonjourサービスがインストールされるため動くことがあるようです。
    • 繋がらない場合はIPアドレスでアクセスしてください。
    • arpコマンドで調べるか、ルータのDHCP振り出し情報を見るか。
    • mDNSを使ってローカルDNSサーバーを廃止する - Qiitaに情報あります。
  • Volumio2でsshの有効化関連

    • /boot/sshがあればsshが動くのでそれでもOK
    • イメージ書き込み後にgui設定と一緒に書いておくのも良いかも
    • ユーザ・パスワード固定かつrootパスも同じなので、パスワードを変えておくなどの配慮は必要です。踏み台にされちゃうぞ!
  • WindowsでUSBドライブが見えない問題

    • 複数のドライブが見えるタイプのカードリーダでガチャガチャやってると発生する模様
    • Windowsのシステム上は認識しているが、ドライブ文字が割り当てられていないだけのようです。
    • 手動でドライブ文字を割り当てれば見えるはず。

Raspberry Pi ZeroのI2S DACとポップノイズの関係 その2 まっとうにMUTE制御

概要

  • NosPiDAC Zeroなどで音楽を楽しんでいるときに気になるポップノイズについて調べたこと・やったことをメモ。
  • その2はCodecドライバを修正し、外部MUTE制御との連動を試してみます。
  • 外部MUTE制御用のサンプルコード付。
  • MUTE機能つきのHPA IC(MAX9722とか)やオペアンプ(OPA1622とか)などで利用できます。

MUTE制御について

  • ALSAではCodecに対してポップ抑制用にMUTE Callbackを用意している。
  • MUTE関連のCallbackはdigital_muteとmute_streamの2種類
    • digital_muteはstream関係なくCallbackされる。mute_stream callbackを登録すると呼ばれない
    • mute_streamはstream番号指定でのMUTE制御。録再別とかチャネル別でMUTEできるとかかな?
    • とりあえずはdigital_muteを使えば良さそう。
  • このMute Callbackを利用してGPIOからHPAのMUTEを制御してポップノイズを抑制できるかを確認してみる。

仕込み

  • 改造ベースはNosPiDac Zero 1543 HPAを使用
  • HPA ICのMAX9722のShutdownピンにGPIO出力を接続
  • NosPiDACのMAX9722 ShutdownピンはVCCに接続されているため、あらかじめカッターなどでパターンをカットする必要があり
  • PCM5102Aドライバを改造してdigital_mute CallbackでGPIO出力を制御するように改造
    f:id:tkztkztkz:20180829213719j:plain
    NosPiDAC Zero 1543 HPAのMAX9722 SDピンに配線した例

結果

  • 曲間でプチる
    • I2Sドライバはそのままで、CodecドライバのMUTE制御だけでポップノイズを防ぐことはできない。
    • MUTE CallbackでMute解除通知後の処理に何かまずいのが居そう。
  • Mute Callbackの様子
    • 再生開始・停止でMute Callbackが飛んでくる
      • prepare時にMute CallbackでMute解除を通知
      • close(shutdownの前)時にMute CallbackでMuteを通知
    • 曲間ではMute解除通知しかこない(Mute通知は来ない)
      • trigger start/stop時にはMute Callbackは呼ばれない
      • 曲間では trigger(stop) - > prepare -> trigger(start)の順で動くため、prepare実行時のMute解除のみ通知される模様
  • 今度はCallシーケンスを洗って、どこでプチってるか確認します。

  • なお、曲再生中にMAX9722のShutdownピンを直接制御してみたところ、ゼロクロス付近ではノイズなし。ゼロクロスから離れていると少々プチノイズが出る感じとなりました。ゼロクロス検出無しのMute回路なので、ここはまあスペック通りかな。

外部MUTE制御サンプル

  • 今回テストに使用した、PCM5102Aドライバを改造してMUTE信号をGPIO出力するサンプルを乗っけておきます。
  • GPIO4にMUTE信号を出力します。Active Low出力なので、LでMUTE、Hでミュート解除になります。
    • init時にLを設定し、ALSAからのMUTE解除でHを出力します。
    • Active Highにする場合は、GPIOF_OUT_INIT_LOWをGPIOF_OUT_INIT_HIGHに変更し、gpio_set_valueの出力論理を反転させてください。
  • prepare時(MUTE解除)とclose時(MUTE)にMUTE制御が飛んできます。
diff --git a/sound/soc/codecs/pcm5102a.c b/sound/soc/codecs/pcm5102a.c
index 8ba322a00363..960d7b83e6ea
--- a/sound/soc/codecs/pcm5102a.c
+++ b/sound/soc/codecs/pcm5102a.c
@@ -14,11 +14,42 @@
  * General Public License for more details.
  */

 #include <linux/init.h>
 #include <linux/module.h>
 #include <linux/platform_device.h>
+#include <linux/gpio.h>

 #include <sound/soc.h>
+static int null_set_dai_mute(struct snd_soc_dai *dai, int mute)
+{
+       struct snd_soc_codec *codec = dai->codec;
+
+       pr_debug("%s mute = %d\n", __FUNCTION__, mute);
+
+       if (mute) {
+               gpio_set_value(4, 0);
+       } else {
+               gpio_set_value(4, 1);
+       }
+
+       return 0;
+}
+
+static struct snd_soc_dai_ops null_dai_ops = {
+       .digital_mute   = null_set_dai_mute,
+};

 static struct snd_soc_dai_driver pcm5102a_dai = {
        .name = "pcm5102a-hifi",
@@ -26,16 +57,20 @@ static struct snd_soc_dai_driver pcm5102a_dai = {
                .channels_min = 2,
                .channels_max = 2,
                .rates = SNDRV_PCM_RATE_8000_192000,
                .formats = SNDRV_PCM_FMTBIT_S16_LE |
                           SNDRV_PCM_FMTBIT_S24_LE |
                           SNDRV_PCM_FMTBIT_S32_LE
        },
+       .ops = &null_dai_ops,
 };

 static struct snd_soc_codec_driver soc_codec_dev_pcm5102a;

 static int pcm5102a_probe(struct platform_device *pdev)
 {
+       gpio_request_one(4, GPIOF_OUT_INIT_LOW, "CODEC MUTE");
+
        return snd_soc_register_codec(&pdev->dev, &soc_codec_dev_pcm5102a,
                        &pcm5102a_dai, 1);
 }
  • サンプルドライバではGPIO決めうちにしているけど、device treeで外部から制御させるように作ると良いかもしれない。

Raspberry Pi ZeroでRJフォーマットDACを鳴らす方法のメモ

概要

  • TDA1545AやTDA1543AなどRJフォーマットを採用しているDACRaspberry Piで鳴らす方法のメモです。
  • NosPiDACでTDA1545Aを鳴らしたかったので、調査・検証してみました。

仕組み

  • 通常ALSAのSoCドライバではdai linkを設定するdriverでPCMフォーマットを設定しているのでそこを直す
  • Raspberry PiのI2S DAC出力を構成するドライバは下記の三種
    • sound/soc/bcm/bcm2835-i2s.c (dai driver)
    • sound/soc/bcm/hifiberry_dac.c (dai link driver)
    • sound/soc/codecs/pcm5102a.c (codec driver)
  • なので、hifiberry_dac.cを修正します。

注意事項

  • Raspberry PiのI2Sドライバは再生ストリームのbit depthと出力bit depthを調整する機能がありません。
    • 再生ストリームのbit depthが出力bit depthになってしまっているようです。
    • RJ16bitのDACを使う場合には、再生bit depthを16bitにしてください。
    • mpdなどで再生bit depthを16bit固定にしてあげるのが簡単です。
      • /etc/mpd.confを下記のように設定 audio_output_format "*:16:*"
  • RJ24bit / RJ32bitに関しては未確認です。
    • RJ32bitはLJ設定32bit固定でも鳴りそうですね。

ソースDiff

diff --git a/sound/soc/bcm/hifiberry_dac.c b/sound/soc/bcm/hifiberry_dac.c
index ee9f13395354..221a8dc820b4
--- a/sound/soc/bcm/hifiberry_dac.c
+++ b/sound/soc/bcm/hifiberry_dac.c
@@ -53,7 +53,8 @@ static struct snd_soc_dai_link snd_rpi_hifiberry_dac_dai[] = {
        .codec_dai_name = "pcm5102a-hifi",
        .platform_name  = "bcm2708-i2s.0",
        .codec_name     = "pcm5102a-codec",
-       .dai_fmt        = SND_SOC_DAIFMT_I2S | SND_SOC_DAIFMT_NB_NF |
+       .dai_fmt        = SND_SOC_DAIFMT_RIGHT_J | SND_SOC_DAIFMT_NB_NF |
                                SND_SOC_DAIFMT_CBS_CFS,
        .ops            = &snd_rpi_hifiberry_dac_ops,
        .init           = snd_rpi_hifiberry_dac_init,

備考

  • hifiberry_dac.cでdevice treeを読んでPCMフォーマットを切り替えられるようになると便利だね。
  • 出力bit depthもDAC合わせて固定したいな。
  • ついでにcodecもdevice treeから読むようにすれば、電子ボリュームとかに簡単に対応できるようになりそう。
  • どっかで時間が取れたら作ってみようかな。