chakokuのブログ(rev4)

テック・コミック・DTM・・・ごくまれにチャリ

I2S接続のSpeaker(M5 STACK社のSpeaker2 Hat (MAX98357))をMicroPython/I2Sで制御して鳴らす

本来はMEMS MICを正しく実装したいのだが、デバッグ等でI2Sバスで音を確認できるといろいろ助かる。また、I2Sで扱うデータ仕様の理解も深まる。
そこで、I2S接続のSpeakerを波形データを作って鳴らしてみる。使ったパーツは、M5 STACK社のSpeaker2 Hat (MAX98357)

作るデータ:440HzのSin波
データ仕様は、16bit、44.1KHzサンプリング、Monoral
高速化等を無視してmath.sin関数でただただ計算する。符号付きint16変換がちょっと怪しいがプログラムは以下
バッファサイズとして、 buffer_size = 44100 * 2  としたので、1秒間鳴るはず (I2Sバスに対して、44.1KHzサンプリング指定) 。

#
# make sin wave
#

import math
from machine import I2S, Pin

def dump2Bls(buf, len=128, onHex=False):
  for i in range(len):
     data = buf[i*2+1] << 8
     data += buf[i*2]
     if onHex:
         print(f'{data:02x} ',end='')
     if data >= 0x8000:
         print(data - 0x10000)
     else:
         print(data)

def toI16(data):
   if data == 0:
      lb = 0
      ub = 0
   elif (data > 0) and (data <= 0x7fff):
      lb = data & 0xff
      ub = (data >> 8) & 0xff
   elif (data < 0) and  (data >= (0x8000 * -1)):
      data = 0x10000 + data
      lb = data & 0xff
      ub = (data >> 8) & 0xff
   else:      
      lb = None
      ub = None
   return (ub, lb)		      

buffer_size = 44100 * 2              # *2 means 2byte(16bit)
buf = bytearray(buffer_size)

max = 0x7fff

for i in range(int(len(buf)/2)):
    rad = math.pi * 2 * i / 100
    data = max * math.sin(rad)
    i16 = toI16(int(data))
    #print(data, i16)
    buf[i * 2] = i16[1]
    buf[i * 2 + 1] = i16[0]

i2s = I2S(0, sck=Pin(18), ws=Pin(19), sd=Pin(20), mode=I2S.TX, bits=16, format=I2S.MONO, rate=44100, ibuf=20000)

i2s.write(buf)

波形データが正しいか、グラフ化して確認

プログラムを実行すると、M5のSpeakerから音は鳴った。ただ、、Sin波のようなこもった音ではなく、矩形波のような尖ったBEEP音であった。これはスピーカが小さいせいか?

■追記
I2Sのバス仕様、波形の信号はMSBから送信される仕様のようだ。

https://www.nxp.com/docs/en/user-manual/UM11732.pdf
音声データのファイルフォーマットとして、マルチバイトの表現は下位バイトから並べるらしい。

WAVファイルのフォーマットについて | Ingenious

■バイトオーダを確認する
I2Sに渡すバッファ内はL_ch→R_ch→L_ch→Rの順番で並べて(モノラルの場合は、L_ch→L_ch→L_ch→L_ch)、各Channnelは下位バイト→上位バイトの順番で並べると理解した。本当にそれで合っているのかを確認
以下のような固定波形を繰り返し出力するプログラムを作成

#
# byte order check
#

import math
from machine import I2S, Pin

def dump(buf,len=4):
  for i in range(len):
     print(f'{buf[i]:02x} ',end='')
     if (i+1) % 16 == 0:
        print('')


buf = bytes((
0x45, 0xA1, 0x09, 0xED,0x45, 0xA1, 0x09, 0xED,
0x45, 0xA1, 0x09, 0xED,0x45, 0xA1, 0x09, 0xED,
0x45, 0xA1, 0x09, 0xED,0x45, 0xA1, 0x09, 0xED,
0x45, 0xA1, 0x09, 0xED,0x45, 0xA1, 0x09, 0xED,
0x45, 0xA1, 0x09, 0xED,0x45, 0xA1, 0x09, 0xED,
0x45, 0xA1, 0x09, 0xED,0x45, 0xA1, 0x09, 0xED,
0x45, 0xA1, 0x09, 0xED,0x45, 0xA1, 0x09, 0xED,
0x45, 0xA1, 0x09, 0xED,0x45, 0xA1, 0x09, 0xED,
0x45, 0xA1, 0x09, 0xED,0x45, 0xA1, 0x09, 0xED,
0x45, 0xA1, 0x09, 0xED,0x45, 0xA1, 0x09, 0xED,
0x45, 0xA1, 0x09, 0xED,0x45, 0xA1, 0x09, 0xED,
0x45, 0xA1, 0x09, 0xED,0x45, 0xA1, 0x09, 0xED,
0x45, 0xA1, 0x09, 0xED,0x45, 0xA1, 0x09, 0xED,
0x45, 0xA1, 0x09, 0xED,0x45, 0xA1, 0x09, 0xED,
0x45, 0xA1, 0x09, 0xED,0x45, 0xA1, 0x09, 0xED,
0x45, 0xA1, 0x09, 0xED,0x45, 0xA1, 0x09, 0xED,
0x45, 0xA1, 0x09, 0xED,0x45, 0xA1, 0x09, 0xED,
0x45, 0xA1, 0x09, 0xED,0x45, 0xA1, 0x09, 0xED,
0x45, 0xA1, 0x09, 0xED,0x45, 0xA1, 0x09, 0xED,
0x45, 0xA1, 0x09, 0xED,0x45, 0xA1, 0x09, 0xED,
0x45, 0xA1, 0x09, 0xED,0x45, 0xA1, 0x09, 0xED,
0x45, 0xA1, 0x09, 0xED,0x45, 0xA1, 0x09, 0xED,
0x45, 0xA1, 0x09, 0xED,0x45, 0xA1, 0x09, 0xED,
))

i2s = I2S(0, sck=Pin(18), ws=Pin(19), sd=Pin(20), mode=I2S.TX, bits=16, format=I2S.MONO, rate=44100, ibuf=20000)

while True:
   i2s.write(buf)
   print('.')

バッファ内が、0x45,0xA1なので、I2Sのバスを観察すると、0xA145のデータがMSBから送信されるはず。オシロで確認するとそのようになってる。

L_ch -> R_chの確認もすべきかもだけど、片側のChannelしか使わないので、省略(出力バスを見ていると、モノラル出力指定したら、L_chの値と同じ値がR_chに出力されているようだ、。)

■追記
MicroPythonのDocumentにバッファのバイトオーダはLittle Endianであると書かれていた。なるほど。
クラス I2S -- IC間サウンド(Inter-IC Sound)バスプロトコル — MicroPython latest ドキュメント