前回、M5StickC Plusに搭載されたPDMマイクSPM1423から、音声データを読み出す実験を実施しました。
PDMマイクを活用する何か良いアプリケーションは無いかな〜?と、考えを巡らしてみて、「音声メモでも作って見ようか」、と思いました。
皆様、ボイスレコーダ、と言う機械はご存知でしょう。
そんな使い方をしますよね?
音声メモは、ボイスレコーダと似たような動作をしますが、録音できる時間に圧倒的な差があります。
例えば音声メモは、
等と言う、短いメッセージを録音して、相手に伝えるための機械です。メモ紙とペンを持ち出して書き書きするより、ボタン押して録音したら終わり、の方が楽チンですよね。
解決すべき課題は2つあります。
録音については、前回、PDMマイクの予備実験を済ませてあり、準備万端です。ただし、データの記録方法について追加検討を行います。
再生については、M5StickC Plusにはスピーカやアナログサウンド出力用のミニジャックなどが搭載されていないので、ここは、外付けに回路を追加しないとなりません。
前回の実験によって、M5StickC Plus搭載のPDMマイクSPM1423からI2Sユニット(ESP32)経由で読み出したデータは16ビットで、賞味12ビット程度のPCMデータだと判りました。
この場合、録音に必要なデータ領域は以下の通りです。
16ビット×fPCM×Trec
ここで、Trecは録音時間[s]、fPCMはサンプリングレート[Hz]
fPCM=44.1kHz、Trec=10sを想定する場合、約861Kバイトと、ESP32のRAM 320Kバイトをもってしても全く無理なサイズとなります。fPCM=8kHzに落としてようやく、156Kバイトです。fPCM=8kHzだとギリギリの音質なので、可能なら、もう少し高いサンプリングレートで録音したいところです。
録音するデータ量を削減する手立てが必要だと判りました。
検討を進めるに当たって、まず確認です。
M5StickC Plusで録音してPCに吸い上げ、WAVファイルに変換して再生するようなアプリケーションって、使い道あるでしょうか?
無いとは断言しませんが、M5StickC Plusを、ただのマイクとして使うようなものです。データ通信時間も嵩むので、ただのマイクとして使うことに、あまり利点を感じません。M5StickC Plusで録音したら、M5StickC Plusで再生する用途を想定すれば良いでしょう。
再生時に使うESP32のDA変換器は8ビット解像度なので、16ビットデータで録音するのは無駄です。
使い方としては:
を、想定することとします。
これを実現するには、一旦全データを受信しないと、バイアスを知ることができません。これでは結局1サンプル当たり16ビットのデータ領域が必要となってしまいます。つまり、受信しながら、データ量を8ビットに圧縮して、かつ、音質の劣化を最小に抑える録音方法を考案しなくてはならない、と言う事になります。
前回の実験結果をおさらいしておくと、SPM1423+I2Sユニット(ESP32)で読み出せる16ビットデータは、賞味12ビット程度のPCMデータでした(下図)。
赤点線で示したバイアスレベルは、PCMデータの平均値や実効値などを取って算出する必要があるため、全データを舐めないと求めることができません。音量(最大振幅)も、時間帯によって大小が変化するので、全体を舐めて値の範囲を計測しなければ知ることができません。
全体のバイアスや最大振幅を調べることなく、受信したその場で(on the fly)、データを8ビットに圧縮するのが喫緊の課題です。
ここで、時間的に連続するPCMデータ列に於いて、直前のPCMデータと現在のPCMデータの差分を取って見ましょう。差を取ったデータのことをDPCM(Differential PCM)と呼びます。DPCMデータは、直前のPCMデータからの偏差なので、データのバイアスや振幅とは無関係な相対値を表しています。
とある音声データで、DPCMの出現頻度を調べてみたところ、以下図のようなヒストグラムとなりました。
上図は、横軸にDPCMデータ絶対値の区間、縦軸にその出現頻度を取ったヒストグラムです。元の音声データは、SPM1423を使い、fPCM=44.1kHz÷4=11,025Hzで録音した、トータル8,192サンプルのDPCMです。
一見して、差分値DPCMが小さい方に出現頻度(エネルギ)が偏っていることが判ります。
圧縮のアイディアとしては:
圧縮率を追求する、バリバリの「データ圧縮」では、出現頻度の高いデータに短いコード、低いデータに長いコードを割り当てる(ハフマン符号化)、みたいなことをすべきなのですが、上のアイディアは少しアプローチが異なっています。
データを圧縮しながら、音質を確保する為に出現頻度を使っています。
なお、データ圧縮(元のデータを正確に復元可能な可逆圧縮)では、「全てのデータを必ずP[%]に圧縮する」アルゴリズムが存在しないことが証明されています。
上で提案したアルゴリズムでは、「粗く量子化する」事によって、正確に元のデータを復元できない非可逆圧縮と、なっています。これによって、対象の12ビットPCMが、どんな音声データであっても必ず8ビットに圧縮したいと思います。
本節では具体的に、圧縮・符号化方法を検討していきます。
対象となるデータの性格をもう一度確認しておきましょう。
まず、これがスタートラインです。ここから、DPCMを8ビットに圧縮します。
符号化(圧縮):
上の処理を噛み砕いて説明します。
データとしては、12ビットPCMから8ビット圧縮DPCMへ変換できました。圧縮率は以下の通りです。
圧縮率: 8ビット÷12ビット≒66.7%
12ビットデータを2バイト(16ビット)で記録することを前提とすると、
圧縮率: 1バイト÷2バイト=50%
に、圧縮できたことになります。
1サンプル当たり1バイトの記憶領域に圧縮できたので、fPCM=8kHz、Trec=10sなら、1バイト×fPCM×Trec≒約78Kバイトで記憶できます。ESP32のRAMは320Kバイトあるので、もう少し記憶領域を拡張して、録音時間を長くしたり、あるいはサンプリングレート(fPCM)を上げて音質を向上したりすることができます。
ただし、非可逆圧縮です。
結構派手に、12ビットデータを6ビットに圧縮するなどと言う荒業を繰り出しているので、もし、録音した音声に違和感を感じるようなら、非可逆圧縮のせい、かも知れません。
それ(音質の劣化)が許されないアプリケーションの場合には、データ量が×2倍に膨れますが、非圧縮で16ビットデータを記録すれば良いだけです。
ここに示した方法は、少しでも長く、限られたメモリ領域に録音したい場合、への提案です。用途(応用)によって、戦法を変えて見てください。
Arduino UNOやDUEを使って、WAVファイルを再生した時の記事は、以下よりご参照ください。
「#7-9 WAVファイルを再生する」
「#7-10 WAVファイルを再生する(回収編)」
「#12-10 DUEでWAVファイルを再生する」
「#13-8 DUEのWAVファイル再生を向上する」
UNOでは、外付けのDA変換器モジュールを使って再生しました。DUEには、DA変換器が内蔵されているので、内蔵DA変換器を使いました。M5StickC PlusのESP32にもDA変換器が搭載されているので、DUEの例の方が近いです。
これを踏まえて、ESP32の8ビットDA変換器(出力端子はGPIO25またはGPIO26)を使って、サウンド出力回路を外付けに作成します(下図)。
データシートを見ても、ESP32搭載DA変換器出力の電流駆動能力が不明なので、ドライバとしてオペアンプLM358を使いました。オペアンプは1個しか使わないので、もう1個の方は、悪さしないようにボルテージフォロアにして処置しておきましょう。
アンプ無しの回路なので、ミニジャックにはイヤホンか、アンプ内蔵スピーカを接続しないと音が鳴りません。
M5StickC Plusから、3.3VフルスケールのDA変換器で音声データを出力します。バイアスは3.3V÷2=1.65Vです。
PDMマイクSPM1423を使って録音したデータは、8ビット圧縮DPCMデータで、伸長すると12ビットPCMデータに復元されます。録音した音声データのバイアスと最大振幅を以下の様に計測します。
伸長した12ビットPCMデータ列: Pcm12
バイアス: Bias=average (Pcm12)
最大振幅: Amax=max (abs (Pcm12−Bias))
前回の予備実験で、SPM1423+I2Sユニット(ESP32)が出力してくるPCMデータのバイアスレベルが規定されておらず、SPM1423の個体差が見えてくる可能性を指摘しておきました。上のアルゴリズムに従って、動的にバイアスBiasを計測することで、個体差の影響を排除しています。
ESP32に内蔵された8ビットDA変換器を使って出力するので、上で計測したバイアスと最大振幅がフルスケールとなる様に、8ビットPCMへ変換します。
8ビットPCMデータ列: Pcm8=(Pcm12−Bias)×128÷Amax×Volume÷100+128
ここで、出力電圧が適切な振幅となる様に、Volume(=0〜100[%])で調整することが可能です。イヤホンやアンプ内蔵スピーカに合わせて、Volumeを調整してください。
ライン出力だと、Vpp≒1Vくらいでしょうか??だとすると、Volume=Vpp÷2÷3.3V≒15%くらいでしょう。プログラム中では定数rec_VOLUMとしてあります。
const int rec_VOLUM=15; // rec: volume 0~100[%]
イヤホンなどで、音量が大きい/小さい等ありましたら、rec_VOLUM定数で調整してお使いください。
ポイントだけ解説しておきます。
以下を目標仕様とします。
M5StickC Plus(ESP32)のDA変換器は8ビット解像度で、GPIO25(DAC1)とGPIO26(DAC2)で使用可能です。2本とも、M5StickC Plusの拡張ポートに引き出されているので、どちらでも使えます。
Arduinoコアライブラリの中に、dacWrite()関数があるので、これを使って、サンプリングレート(fPCM)毎に対応するPCMデータを出力すれば、音は鳴ります。
I2Sの出力先をESP32内蔵DA変換器に指定すると、DMACを使ってデータ転送させることができ、CPUをフリーにすることができるみたいです。
ただ、DMACで転送するデータとして16ビットデータを指定する必要があるみたいで、8ビットDPCMとして記録した今回のアプリケーションでは、結局CPUがお膳立てしないと動作しないと見たので、使用しませんでした。
使ってみたい方は、ESP-IDFのAPI Referenceに説明があるので、そちらをご参照ください。
目標仕様にて、再生する時、録音した日時を表示することにしたので、RTCから日時データを取得しようと思いました。
M5StickC Plusライブラリの中に、M5.Rtcクラスがあるので、これを使えば、M5StickC Plusに搭載されたRTC(BM8563)を制御することが可能です。が、全然ダメ、でした。多分、ライブラリを作った人、RTCの使い方を知らない……。
日時の読み出し:
M5.Rtc.GetTime()メソッド
M5.Rtc.GetData()メソッド
日時のセット(書き込み):
M5.Rtc.SetTime()メソッド
M5.Rtc.SetData()メソッド
まず、GetData() → GetDate()かな?スペルミスなんて可愛い物で、RTCの場合、読み出しも書き込みも、日付(date)と時刻(time)を別々にアクセスしてはダメ、なのです。
日付の読み出しと、時刻の読み出しの間に、秒カウンタのインクリメントが発生して、23:59:59から00:00:00へ桁上がりしてしまうと、先に読み出した日付と、インクリメント後に読み出した時刻が不整合を起こすから、です。
データシートにも、「そういう使い方はダメよ」って書いてあるはずなんだけど……。
BM8563のデータシートは中国語なので読めないのだけど、互換チップと思われる「NXP社PCF8563データシート」には、「ダメ🚫」って書いてありました。
※筆者がFPGAボードで設計したRTC400ユニットでは、SNAPビットを使ってRTCカウンタとの同期を一時停止し、その間に読み出し/書き込みする機構を取っていました。RTCはカウンタ数が多くて、つい分割して読み出したくなってしまいますが、非同期に動く場合が多いので、必ず何らかの制約があります。それを見落としてはいけないのです。
M5.Rtcクラスが使えない事実が判明したので、以下の関数を自前で起こすことにします。
RTC(BM8563)読み出し・書き込み関数仕様:
上に示したRTC用の関数たちは、M5cp-PSライブラリに追加したので、リンクをたどって最新版のコードに更新してお使いください。
ちなみに、うるう年の周期は400年なのですが、BM8563はC(Century)ビットを含めても200年分の年カウンタしかありません。このため、年カウンタが4の倍数である年が、全てうるう年と判定されます。結果、うるう年から除外すべき「100で割り切れる年」も、うるう年になってしまいます。直近では、2100年がNGなのですが、そんな年まで生きているわけないので、まあ、良いか(1^−^;)。
使い物にならなかったM5StickC Plusライブラリでは、少し変則的なのですが、Cビット=0に2000年代、C=1に1900年代を割り当てて使っていました。1900年も、うるう年ではないのに「うるう年」と誤判定されてしまう年ですが、今更1900年に設定して使う人はいない、と言う判断なのでしょう。
RTC(BM8563)は、I2Cでインターフェイスする外部チップです。M5StickC Plusのボード上で、SCL=GPIO22、SDA=GPIO21としてI2Cバスへ接続されています。M5StickC Plusでは、同じI2Cバス上にPMIC(AXP192)とIMU(MPU6886)も、ぶら下がっています。
これらのチップにアクセスするため、ユーザ用のI2CクラスであるWireクラスとは別に、システム用のWire1クラスが使われています。
M5StickC PlusライブラリをM5.begin()で初期化した後であれば、Wire1クラスがオンボードのI2Cバス用に初期化されているので、「Wire1.begin()」無しで、いきなり使うことができます。BM8563のスレーブアドレスは0x51なので、後はデータシートを見ながら制御するだけでした。
smbus_master.inoの実行結果:【 】内はシリアルモニタからの送信データ
【scan】
OK+ +0 +1 +2 +3 +4 +5 +6 +7 +8 +9 +a +b +c +d +e +f
00: -- -- -- -- -- -- -- --
10: -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- --
20: -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- --
30: -- -- -- -- 34 -- -- -- -- -- -- -- -- -- -- --
40: -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- --
50: -- 51 -- -- -- -- -- -- -- -- -- -- -- -- -- --
60: -- -- -- -- -- -- -- -- 68 -- -- -- -- -- -- --
70: -- -- -- -- -- -- -- --
OK: scan i2c slave address
※プログラム(smbus_master.ino)先頭付近のマクロに「#define WIRE Wire1」と、設定してコンパイル&書き込んでください。
I2Cスレーブアドレス:
以下に、完成したプログラムをリンクしておきます。
完成プログラム:m5cp_voice_memo.ino
※コンパイルには、「#23-5 unicode→S-JIS変換(S/W編)」で作成したM5cp-PSライブラリが必要となります。
コンパイルしてM5StickC Plusへ書き込んでおいてください。
シリアルモニタから以下のコマンドを受け付けています。
シリアルモニタコマンド仕様:
M5StickC Plus内蔵RTC(BM8563)に日時をセットするためのサポートコマンドです。日時を設定する場合以外は、シリアルモニタ無しで自立します。
サウンド出力回路と、M5StickC Plus、イヤホン等を以下表に従って接続してください。
M5StickC Plus | 接続 | サウンド出力回路 | 接続 |
イヤホンまたは、 アンプ内蔵スピーカ |
5V(出力) | → | Vcc | ||
GND | ⇔ | GND | ||
GPIO26(G26) | → | G26 | ||
ミニジャック | → | ミニプラグ |
M5StickC Plusの上部にある拡張ポート(下図)から、配線してください。
以下、実験中の様子です。
実験サンプル:
以下に考察を加えます。
考察:
改定提案:
SW-Bを押すと録音待機状態となって、一時停止中を表すアイコンが表示されます。この状態で、音量レベルが一定以上に上昇すると、その時点から録音を開始します。そんな小技も含め、全体の動作状況は、以下の動画でご確認ください。
外部のシリアルFlashメモリなど、大容量のメモリを活用できる様になれば、音声メモ → ボイスレコーダに昇華させることもできるでしょう。