ESP32-WROOM-32EでMicroPythonを使い開発(I2S編)

〔ESP-WROOM-02〕 〔マイコンのトップに戻る〕
[準備編] [module編] [GPIO編] [通信編]   [WiFi編] [BLE編1] [BLE編2] [BLE編3] [色々編]


I2S(Inter-IC Sound:IC間サウンド)デバイス間でオーディオ・データをシリアルバスのプロトコルで
データ転送を行う規格です。

《音の再生》

I2S通信の概念図1
I2S通信の概念図
wavファイルからデータを読み出し、
ESP32で音楽データを取り出しI2S規格のシリアル・データに変換して
それをDACモジュールがアナログ信号に変換し音楽が再生されるといった感じぃ。

I2Sは、3本の信号線から構成され、これらは同期してシリアル転送されます。

LRCLK
 WDCLK(Word Clock - ワードクロック)と記述される時もあります。
 LR Clock(エルアールクロック) 2チャンネルステレオにおいて、音楽信号のLチャネルとRチャネルを
 選択する為の信号。信号レベルが0V(LOW)がLチャンネル、HIGHがRチャンネルとされています。

BCLK
 SCLK(Serial Clock - シリアルクロック)と記述される時もあります。
 Bit Clock(ビットクロック)サンプル辺りのビット数が32ビットならL(R)の1チャンネル辺り32クロック
 でラッチしSDATAの信号を読み取れる様にします
 サンプル周波数が1チャンネル辺り44.1KHzは、44.1KHz×2CH×32bit=2.8224MHzの処理能力が必要

SDATA
 Serial Data(シリアルデータ)デジタル化された音声データのビット列です。

・MCLK
 DACモジュールによっては、基準クロック用の信号(MCLK)を必要とする場合があるらしい。
 SYSCLK(System Clock - システムクロック)と記述される時もあります。
 Master Clock(マスタークロック)デジタル信号の動作基準となるクロック信号。
 MCLKを必要としない物は、内蔵クロックから適切なクロックを生成するらしい。

オーディオに使用される周波数について(サンプリング周波数、PCM,DSD等)”も一読しましょう。

〔DACモジュール〕

DACモジュール
 ”UDA1334A搭載 I2S ステレオDACモジュール
 スイッチサイエンスから購入。
 Adafruitの製品です。仕様書等はこちらです。
 3.3 - 5 Vのロジックレベルで使用できます。
 SDATA、LRCLK、BCLK を接続するだけで使用可能です。
 3.5 mmステレオヘッドフォンジャックと音声出力ピンが
 有ります。

ピンの構成一覧
ピン名称
機能
VIN
3V - 5V の電源を接続、ICは3.3V、ピンは3V - 5Vトレラント
3VO
内蔵レギュレータ(MIC5225)から3.3Vを出力(150mA)します
GND
グランド
WSEL
LRCLK データが左チャンネル、右チャンネルをDACに伝えるピンです
DIN
SDATA 実際のデータが入ってくるピンです
BCLK
BCLK データピン上のデータを読み取るタイミングを指示するピンです
Lout
左ライン出力 出力に47uFブロッキングコンデンサ有り
AGND
アナログ用グランド
Rout
右ライン出力 アンプで使用する様に3KΩ負荷に対して設計されている
SCLK
MCLK システムクロック入力か27MHz「ビデオモード」入力
SF1
入力データ形式を設定するために使用されます(下記参照)
MUTE
HIGH接続でミュート
SF0
入力データ形式を設定するために使用されます(下記参照)(3.3Vのみ)
PLL
LOWでオーディオモード、HIGHでビデオモード(3.3Vのみ)
DEEM
ディエンファシス フィルターを使用する為のピン、
ビデオモードでは、これがクロック出力ピン

入力データ形式の設定表
SF0
SF1
機能
LOW
LOW
I2S (デフォルト)
LOW
HIGH
16Bit  左詰めデータ
HIGH
LOW
20Bit 左詰めデータ
HIGH
HIGH
24Bit 左詰めデータ

〔I2Sクラスモジュール〕

[準備編]でインストールしたMicroPythonのファームウエアには"I2Sクラス"は構築されていません
だからぁ、私はGitHubのここからダウンロードしました、ここの、
"firmware"フォルダーの"esp32-idf4-0-2-i2s-2021-02-27-v1-14.bin"をインストールし直しました
これは、"esp32-idf4-20210202-v1.14.bin"を使い"I2S"を構築した物らしい。

I2Sクラスのドキュメントは、 GitHubの"README.md"かここを参考にしましょう。

REPLで操作help(I2S)

machine.I2S
ESP32には、2つの I2S 周辺機器(0Ch,1Ch)があります。

 ・i2s = I2S(id,
       bck, ws, [sdin], [sdout],
       [standard=I2S.PHILIPS], mode,
       dataformat, channelformat,
       samplerate,
       [dmacount=16], [dmalen=64],
       [apllrate=0])

  指定された引数を使用して新しいI2Sオブジェクトを作成して返します。
  id      = 使用するI2Sの周辺機器番号(NUM0:0 / NUM1:1)
  bck   = BCLK(ビットクロック)出力に使用するピンを指定
  ws    = LRCLK(ワード選択)出力に使用するピンを指定
  sdout = シリアルデータ(SDATA)出力に使用するピンを指定(sdin/sdout何方か指定)
  sdin    = シリアルデータ(SDATA)入力に使用するピンを指定(マイク等から録音する場合に指定)
  standard  = I2Sで使用されるプロトコルを指定(I2S.PHILIPS / I2S.LSB)
  mode      = 送信(sdout:I2S.MASTER_TX)か受信(sdin:I2S.MASTER_RX)を指定
  dataformat      = 各サンプルのビット数を指定(I2S.B16 / I2S.B24 / I2S.B32)
  channelformat = オーディオ形式を指定、例:ステレオ、モノラルなど
          (I2S.RIGHT_LEFT/I2S.ALL_RIGHT/I2S.ALL_LEFT/I2S.ONLY_RIGHT/
           I2S.ONLY_LEFT)
  samplerate = オーディオサンプリングレートを指定
  dmacount     = リンクされたDMAバッファーの数(サンプル/秒)
  dmalen         = 各DMAバッファーの長さ(サンプル単位)
  apllrate        = オーディオPLLサンプリングレート(サンプル/秒)

 ・i2s.deinit()
  I2Sの初期化を解除(Off)します。

 ・i2s.write(buf, [timeout = -1])
  指定された引数を使用して、オーディオデータをI2Sに書き込みます。
  buf = 送信バッファはbytearray型やarray型のバッファを指定する必要があります
  timeout = タイムアウトはDMAバッファを待機する最大時間(ms単位)
          指定ms秒待っても使用可能なDMAバッファがない場合、関数は戻ります。
          timeout指定なしは、bufを書き込むまで永久に待機します。(デフォルト)
          timeout = 0の時は、送信バッファをコピーする為の空きDMAメモリがない場合、
          関数はすぐに戻ります。
  Return  = 書き込まれたバイト数を返します

 ・i2s.readinto(buf, [timeout = -1])
  指定された引数を使用して、I2Sからオーディオデータを読み取ります。
  buf = 受信バッファはbytearray型やarray型のバッファを指定する必要があります
  timeout = タイムアウトはDMAバッファを待機する最大時間(ms単位)
          タイムアウトms秒待っても使用可能なDMAバッファがない場合、関数は戻ります。
          timeout指定なしは、bufがいっぱいになるまで永遠に待ちます。(デフォル)
          timeout = 0の場合、指定されたバッファにコピーするデータがDMAメモリにない場合
          関数はすぐに戻ります。
  Return  = 読み取られたバイト数を返します

〔配線図(再生)〕

配線図(再生)
電源はUSB接続で、回路電源はボード出力3.3Vです。
GPIO32 - DIN、GPIO33 - BCLK、GPIO25 - WSELを配線しています。

〔サンプル1(flash)〕

上記でダウンロードした、"micropython-esp32-i2s-examples-master"にある
"examples"フォルダーの"play-stereo-wav-from-internal-flash.py"ファイルをそのまま動作させました

@ "wav_files"フォルダーにある"side-to-side-8k-16bits-stereo.wav"ファイルを"PyCharm"の
  プロジェクトディレクトリ(ここでは"espTest"を作成している)へコピーします。
A ESP32-WROOM-32EボードをUSBでPCに繋ぎます。
B "PyCharm"を起動させ、作成してあるプロジェクトを開きましょう。
  こちらのCを操作する必要が有ります。(起動時最初のみ設定)
C "main.py"に"play-stereo-wav-from-internal-flash.py"ファイルをそのまま貼り付けます。
D ESP32-WROOM-32Eに書き込みます
  ・左側プロジェクトウインドウから"side-to-side-8k-16bits-stereo.wav"の文字を右クリックをし、
   [実行(U)'Flash side-to-side-8k-16bits-stereo.wav']を選んでクリック、少し時間が掛かります。
  ・左側プロジェクトウインドウから"main.py"の文字を右クリックをして、
   [実行(U)'Flash main.py']を選んでクリックします。
E イヤホンを取り付け聞いてみましょう、ぷぅ〜音が左右交互に鳴ると思います。 (;^_^)//”
  コングラチュレーション!、あなたはI2C再生に成功しました。
F 止める場合は[CTRL]+[C]キーを押しましょう。あ、REPL起動しておかないとぉ止められない!

〔サンプル2(sdcard)〕

SDカードの音楽wavファイルを読み出して再生してみます。

結論から申しますとぉ、実験今一つです。
"wav_files"にあるサンプルは、まあ、出来たと言っても良いでしょう。
だがぁ、音楽データは....音が汚い、
ならぁ、フラッシュメモリに音楽データを入れてみる....随分良くなったがぁ、音が割れる感じぃ。
SDカードの読み出し速度もあるかもだがぁ、DACモジュールが限界かぁ、ESPが限界かぁ?
良く解りません、残念侍!
・・・・・・・・・・・・・・・・・

何だか消化不良みたいなぁ感じぃ....ならぁ、ってんでぇ。
NFJストア楽天市場店より"
I2S入力DAC PCM5102A搭載32bit 384kHz DAC完成基板"の
"PCM510xA-V3"を買っちゃいましたぁ。
結果はいい音でなって〜ますぅ。思わずプレーヤーを作成したくなりましたぁ。d(>_・ )
上のボードと大して値段も変わらないのでぇ最初からこれにして置くべきでした。

PCM510xA-V3 動作モード設定(ディップスイッチ):
SW1は、ONでIIR(無限インパルス応答フィルター)
     OFFでFIR(有限インパルス応答フィルター)
SW2は、ONでオーディオフォーマットは左揃え
     OFFでI2S
必ず電源OFF時にディップスイッチを設定してください。

MCLK出力がない機器の場合はMCLKをGNDに落とせば
内部クロックで動作しますが、落とさない場合でも
ICの仕様により自動的に内部クロックで動作します。
"MUTE"ピンはGND接続で音が出ません。

回路図は割愛します。
SDカードは、Slot1のSD/MMC配線での接続です。詳しくはこちらを参照しましょう。
PCM510xA-V3は、BICK-GPIO33、DATA-GPIO32、LRCK-GPIO25 のI2S接続です。
電源はDC4.2V-12Vなのでぇ別電源となります。ESPボードの5Vは入力専用なので使えません。
、PCM510xA-V3のGNDとESPボードのGND 同士は接続しますよ

使ったスクリプトは下記です。
--------------------------------------------------------------------------------
# "play-mono-wav-from-sdcard.py"をベースに作成しています。
# SDカード上のWAVファイルから16ビットオーディオサンプルを読み取る
# オーディオサンプルをI2SアンプまたはDACモジュールに書き込む
#
from machine import Pin, SDCard
from machine import I2S, freq
import uos

freq(240000000)  # CPUクロックを240MHzに変更

WAV_FILE    = 'Summer-time-magic-16k.wav'
SAMPLE_RATE = 16000

BCLK_pin  = Pin(33)
LRCLK_pin = Pin(25)
SDATA_pin = Pin(32)

# wavファイルのヘッダ部を読み飛ばしてデータ部を探す処理
def find_data():
    wav_f.seek(0)
    while True:
        if wav_f.read(1) == b'd':
            if wav_f.read(1) == b'a':
                if wav_f.read(1) == b't':
                    if wav_f.read(1) == b'a':
                        wav_f.seek(4)
                        break

# チャネルフォーマット設定
i2s = I2S(
    I2S.NUM0,
    bck=BCLK_pin, ws=LRCLK_pin, sdout=SDATA_pin,
    standard=I2S.PHILIPS,
    mode=I2S.MASTER_TX,
    dataformat=I2S.B16,
    channelformat=I2S.RIGHT_LEFT,
    samplerate=SAMPLE_RATE,
    dmacount=16, dmalen=512)

# SDカードの初期化とマウントを行う
# d1/d3のピンは外部でプルアップする事
d0 = Pin(2, Pin.PULL_UP)
d2 = Pin(12, Pin.PULL_UP)
sd = SDCard(slot=1, width=4, freq=40000000)
uos.mount(sd, "/sd")

wav_f = open('/sd/' + WAV_FILE, 'rb')
find_data()

# オーディオデータバッファ配列を割り当てる
# memoryviewを使ってwhileループでヒープ割り当てを減らす為に使用される
wav_buf = bytearray(8192)
wav_buf_mv = memoryview(wav_buf)

print('Starting')
# WAVファイルからオーディオデータを継続的に読み取り、I2Sに書き込みます
while True:
    try:
        read_su = wav_f.readinto(wav_buf_mv)
        written_su = 0
        # WAVファイルの終わりか?
        if read_su == 0:
            # データ部の最初のバイトまでシークします
            find_data()
        else:
            # 全てのオーディオデータがI2Sに書き込まれるまでループします
            while written_su < read_su:
                written_su += i2s.write(wav_buf_mv[written_su:read_su], timeout=0)
    except (KeyboardInterrupt, Exception) as e:
        print('caught exception {} {}'.format(type(e).__name__, e))
        break
# 終了処理
wav_f.close()
uos.umount("/sd")
sd.deinit()
i2s.deinit()
import gc
gc.collect()
print('Done')
--------------------------------------------------------------------------------------------------
”サンプル1”に比べて、バッファを増やしています、又、CPUクロックも240MHzに上げています、
それとぉ、ここで使用した音楽は、MP3からwavに変換 (サンプリングレート=16K、サンプルの
ビット数=16B、ステレオ)した物を使っています。
サンプリングレート=32K でも行ってみた、鳴る鳴るぞぉ、音もいいよぉ、ヾ(≧ω≦)ノ
嬉しくなるなぁ、あ、いかんいかん涙が出てきたぁ。(´;ω;`)

《音の録音》

今度は音を録音してSDカードにwavファイルで記録したいと思います。

I2S通信の概念図2
I2S通信の概念図
"SPH0645LM4H-B"と言うデバイスで、拾った音をI2Sデータとして出力してくれるマイクです。
これを使い(下記図)ESP32に送り、SDカードにwavファイルで保存します。

SPH0645 I2C Mic  ”
SPH0645LM4H搭載 I2S MEMSマイクモジュール
 スイッチサイエンスから購入。
 I2S出力で50Hz-15kHzでオーディオを録音/検出できます
 電源は1.6V-3.6Vで動作し、5 Vでは使用できません。
 搭載のマイクはモノラルです。
 "SEL"ピンは、GND配線で左のチャンネルを選択し、
 +電源で右のチャンネルを選択します。
 データシートはこちらです。
 尚、この写真の裏側がマイクのピン穴が有るので其方を
 上に配線します。

〔配線図(録音)〕

配線図(録音)
電源はUSB接続で、回路電源はボード出力3.3Vです。
GPIO32 - DOUT、GPIO33 - BCLK、GPIO25 - LRCLKを配線しています。
図では割愛していますが、SDカードスロットを接続しています、Slot1のSD/MMC配線での接続です。
詳しくはこちらを参照しましょう。
又、"SEL"ピンは、GND接続なので左チャンネルを選択しています。

〔サンプル3(sdcard)〕

使ったスクリプトは下記ですが、これは、
上記でGitHubから
ダウンロード"したmicropython-esp32-i2s-examples-master"にある
"examples"フォルダーの"record-mono-mic-to-sdcard.py"ファイルを使用し日本語化したコメントと、
SDカードの初期化部分変更と、変数を自分に解かり易い物に変更しています。
又、コードの終了時に"gc.collect()"を入れてメモリの開放を行っています。

--------------------------------------------------------------------------------
# The MIT License (MIT)
# Copyright (c) 2019 Michael Shi
# Copyright (c) 2020 Mike Teachman
# https://opensource.org/licenses/MIT
#
# 目的:
# - I2Sマイクの左チャンネルから32ビットオーディオサンプルを読み取る
# - 各32ビットマイクサンプルから上位16ビットを切り取ります
# - WAV形式を使用して16ビットサンプルをSDカードファイルに書き込む
#
# 記録されたWAVファイルの名前は次のとおりです:
#   "mic_left_channel_16bits.wav"
#
# テスト済みのハードウェア:
# - Adafruit I2S MEMS Microphone Breakout module (SPH0645LM4H)

from machine import Pin
from machine import SDCard
from machine import I2S
import uos

# ======= ユーザー設定  =======
RECORD_TIME_IN_SECONDS = 5  # 録音時間
SAMPLE_RATE_IN_HZ = 22050   # サンプリングレート
# ======= ユーザー設定  =======

WAV_SAMPLE_SIZE_IN_BITS = 16
WAV_SAMPLE_SIZE_IN_BYTES = WAV_SAMPLE_SIZE_IN_BITS // 8
MIC_SAMPLE_BUFFER_SIZE_IN_BYTES = 4096
SDCARD_SAMPLE_BUFFER_SIZE_IN_BYTES = MIC_SAMPLE_BUFFER_SIZE_IN_BYTES // 2  # なぜ2で割るのですか? 唯一の32ビットサンプルの16ビットを使用
NUM_SAMPLE_BYTES_TO_WRITE = RECORD_TIME_IN_SECONDS * SAMPLE_RATE_IN_HZ * WAV_SAMPLE_SIZE_IN_BYTES
NUM_SAMPLES_IN_DMA_BUFFER = 256
NUM_CHANNELS = 1

# snip_16_mono(): 32ビットのモノラルサンプルストリームから16ビットのサンプルを切り取る処理
# 仮定 : モノラルマイクのI2S構成。 例えば I2S channelformat = ONLY_LEFT or ONLY_RIGHT
# example snip:
#   samples_in[] =  [0x44, 0x55, 0xAB, 0x77, 0x99, 0xBB, 0x11, 0x22]
#   samples_out[] = [0xAB, 0x77, 0x11, 0x22]
#   notes:
#       リトルエンディアン形式で配置されたsamples_in []フォーマット:
#           0x77は、32ビットサンプルの最上位バイトです。
#           0x44は、32ビットサンプルの最下位バイトです。
# returns:  切り取られたバイト数(samples_out[])
def snip_16_mono(samples_in, samples_out):
    num_samples = len(samples_in) // 4
    for i in range(num_samples):
        samples_out[2 * i] = samples_in[4 * i + 2]
        samples_out[2 * i + 1] = samples_in[4 * i + 3]

    return num_samples * 2

# wavファイルのヘッダ部を作成する処理
def create_wav_header(sampleRate, bitsPerSample, num_channels, num_samples):
    datasize = num_samples * num_channels * bitsPerSample // 8
    o = bytes("RIFF", 'ascii')   # (4byte) ファイルをRIFF識別子としてマークします
    o += (datasize + 36).to_bytes(4, 'little')  # (4byte) これとRIFF識別子を除くファイルサイズ(バイト単位)
    o += bytes("WAVE", 'ascii')  # (4byte) File type ("WAVE"or"AVI ")
    o += bytes("fmt ", 'ascii')  # (4byte) fmt識別子
    o += (16).to_bytes(4, 'little')  # (4byte) fmtチャンクのバイト数
    o += (1).to_bytes(2, 'little')   # (2byte) 音声フォーマット(1 - PCM)
    o += (num_channels).to_bytes(2, 'little')  # (2byte) チャンネル数(モノラルは1、ステレオは2)
    o += (sampleRate).to_bytes(4, 'little')    # (4byte) サンプリング周波数(Hz)
    o += (sampleRate * num_channels * bitsPerSample // 8).to_bytes(4, 'little')  # (4byte) バイト/秒 1秒間の録音に必要なバイト数
    o += (num_channels * bitsPerSample // 8).to_bytes(2, 'little')  # (2byte) ブロック境界 ステレオ16bitなら、16bit*2=32bit = 4byte
    o += (bitsPerSample).to_bytes(2, 'little')  # (2byte) ビット/サンプル 1サンプルに必要なビット数
    o += bytes("data", 'ascii')  # (4byte) data識別子としてマークします
    o += (datasize).to_bytes(4, 'little')  # (4byte) wavデータサイズ(バイト単位)
    return o

BCLK_pin  = Pin(33)  # BCLK
LRCLK_pin = Pin(25)  # LRCLK
SDATA_pin = Pin(32)  # DOUT

# チャネルフォーマット設定
i2s = I2S(
    I2S.NUM0,
    bck=BCLK_pin, ws=LRCLK_pin, sdin=SDATA_pin,
    standard=I2S.PHILIPS,
    mode=I2S.MASTER_RX,
    dataformat=I2S.B32,
    channelformat=I2S.ONLY_LEFT,
    samplerate=SAMPLE_RATE_IN_HZ,
    dmacount=50,
    dmalen=NUM_SAMPLES_IN_DMA_BUFFER
)

# SDカードの初期化とマウントを行う
# d1/d3のピンは外部でプルアップする事
d0 = Pin(2, Pin.PULL_UP)
d2 = Pin(12, Pin.PULL_UP)
sd = SDCard(slot=1, width=4, freq=40000000)
uos.mount(sd, "/sd")

wav_f = open('/sd/mic_left_channel_16bits.wav', 'wb')

# WAVファイルのヘッダーを作成してSDカードに書き込む
wav_header = create_wav_header(
    SAMPLE_RATE_IN_HZ,
    WAV_SAMPLE_SIZE_IN_BITS,
    NUM_CHANNELS,
    SAMPLE_RATE_IN_HZ * RECORD_TIME_IN_SECONDS
)
num_bytes_written = wav_f.write(wav_header)

# マイクデータバッファ配列を割り当てる
# memoryviewを使ってwhileループでヒープ割り当てを減らす為に使用される
mic_buf = bytearray(MIC_SAMPLE_BUFFER_SIZE_IN_BYTES)
mic_buf_mv = memoryview(mic_buf)  # 32ビットオーディオサンプルのバッファ
wav_buf = bytearray(SDCARD_SAMPLE_BUFFER_SIZE_IN_BYTES)
wav_buf_mv = memoryview(wav_buf)  # 上位16ビットを切り取ったバッファ

num_buf_bytes_written_to_wav = 0

print('Starting')
# I2Sマイクから32ビットサンプルを読み取り、
# 上位16ビットを切り取り、切り取ったサンプルをWAVファイルに書き込みます
while num_buf_bytes_written_to_wav < NUM_SAMPLE_BYTES_TO_WRITE:
    try:
        # I2Sマイクからサンプルのブロックを読み取ってみてください
        # DMAバッファがいっぱいでない場合、readinto() メソッドは0を返します
        num_bytes_read_from_mic = i2s.readinto(mic_buf_mv, timeout=0)
        if num_bytes_read_from_mic > 0:
            # 各32ビットマイクサンプルから上位16ビットを切り取ります
            num_bytes_snipped = snip_16_mono(mic_buf_mv[:num_bytes_read_from_mic], wav_buf_mv)
            num_bytes_to_write = min(num_bytes_snipped, NUM_SAMPLE_BYTES_TO_WRITE - num_buf_bytes_written_to_wav)
            # サンプルをWAVファイルに書き込む
            num_bytes_written = wav_f.write(wav_buf_mv[:num_bytes_to_write])
            num_buf_bytes_written_to_wav += num_bytes_written
    except (KeyboardInterrupt, Exception) as e:
        print('caught exception {} {}'.format(type(e).__name__, e))
        break
# 終了処理
wav_f.close()
uos.umount("/sd")
sd.deinit()
i2s.deinit()
print('Done')
print('%d sample bytes written to WAV file' % num_buf_bytes_written_to_wav)
import gc
gc.collect()
--------------------------------------------------------------------------------------------------



[通信編へ] [ページ上へ] [WiFi編へ]


【きむ茶工房ガレージハウス】
Copyright (C) 2006-2021 Shigehiro Kimura All Rights Reserved.