M5StickC Plusのマイクをmicropythonでいじってみた

# 概要
* 久しぶりにM5StickC Plusで遊んだ。
* 今度はUIFlowではなくmicropythonを使用。
* 内蔵マイクで騒音計（っぽい）ものを作ってみた。
* なんとなく動いているようだが、補正とか単位は何も考えていない。

# 環境
* M5StickC Plus
* UIFlow2 （ファームウェア、開発環境）
* Thonny （開発環境）

# はまったところ
## 前提
* 音声信号というのがなんなのかわかっていない
* micropythonもわかっていない
* なんならメモリの扱いもわかっていない

## マイクが触れない
一番参考にしたのがこの記事。
[M5StickCでdB(デシベル)表示の騒音計を作る](https://qiita.com/tomoto335/items/263b23d9ba156de12857)
micropythonに触れることが主たる目的だったので、写経してからいじるだけのつもりでした。しかし、M5stickC&騒音計&micropython自体は見つけられなかったので、上記のコードをmicropythonで行うのを基本方針としました。

しかし、[micropythonのI2S](https://micropython-docs-ja.readthedocs.io/ja/latest/library/machine.I2S.html)の使い方がよくわかりませんでした。具体的にはコンストラクタで、sckとしてどのピンを指定すれば良いのかが見つけられず断念。

## 公式ドキュメントにたどり着いてようやくデータを取得
どうしてすぐにこれを見なかったのかというのは置いておいて、ようやくマイクのデータにアクセスできるようになったのが、これにたどり着いてからです。
[UIFlow2 Programming Guide / Hardware / Mic](https://uiflow-micropython.readthedocs.io/en/2.1.4/hardware/mic.html)
M5モジュールにあるMicを使ってマイクのデータを取得できるようになったので、UIFlow2でMic周りのところ以外を作って、Micの処理を書いていくことにしました。

## 取得したデータがよくわからないその１
Thonnyで接続してシェルを叩いて試している時に、[UIFlow2 Programming Guide / Hardware / Mic](https://uiflow-micropython.readthedocs.io/en/2.1.4/hardware/mic.html)の記述
> Methods of the Mic Class heavily rely on M5.begin()
> All calls to methods of Mic objects should be placed after M5.begin(), and M5.update()

に、気づかずおかしなデータになっていて、しばらく時間を潰しました。

最終的にはシェルでの動作確認時には、最低これで動いているようでした。
```python
>>> import M5
>>> M5.begin()
>>> samples = bytearray(128*2*8)
>>> M5.Mic.begin()
True
>>> M5.Mic.record(samples, 16000, False)
True
>>> print(samples)
```
## 取得したデータがよくわからないその２
それっぽいデータがようやく取れるようになったので、データのフォーマット向き合う時間。参考に挙げたページの3, 8などを読むと符号付き16ビットのリトルエンディアンということがわかりました。

[MicroPython ドキュメンテーション　クラス I2S -- IC間サウンド(Inter-IC Sound)バスプロトコル](https://micropython-docs-ja.readthedocs.io/ja/latest/library/machine.I2S.html)
>  "buf" のバイトオーダはリトルエンディアンです。

[M5StickCのマイクを使ってみる その3](https://lang-ship.com/blog/work/m5stickc-mic-3/)
> M5StickC内蔵I2Sマイクの録音データは、符号付き16ビットですので、2バイト単位で送信されてきます。

**追記**
この記事（[I2Sと、I2Sもどきと、 S/PDIFについて](https://qiita.com/drafts/a79a1bba80472102b87f/edit)）を読むとデータフォーマットについて上記の理解では誤っているようです。より理解が進んだら改めて修正し直しますが、いまのところはこのままにします。
追記ここまで。

データの転送あたりは[#23-10　PDMマイクを使って見る](http://hello.world.coocan.jp/ARDUINO23/arduino23_a.html)を読んでわかったふりをしました。

これらを踏まえて[M5StickCでdB(デシベル)表示の騒音計を作る](https://qiita.com/tomoto335/items/263b23d9ba156de12857)を参考に、マイクのデータ（buffer）を補正とか無視して音の強さ（っぽいの）への変換するのがこの部分です。
```python
def putSoundLevel(buffer):
  ret = 0.0
  dataSize = len(buffer) // 2
  for i in range(dataSize):
    r = int.from_bytes(buffer[i*2:i*2+2], "little")
    ret += (r if r <= 0x8000 else r - 0x10000)**2
  ret = ret / dataSize
  ret = math.sqrt(ret)
  return ret
```
# コード
できあがったのが次のコード。なんとなくは動くけれど、縦軸、横軸ともに適当です。バッファーの大きさとかこれで良いのかよくわかっていないから、それっぽく出るからひとまずここまで。

```python:soundlevelmeter.py
import M5
from M5 import *
import math
import time

line0 = None
title0 = None
label0 = None
soundLevels = None
isRecording = None

# Describe this function...
def putSoundLevel(buffer):
  ret = 0.0
  dataSize = len(buffer) // 2
  for i in range(dataSize):
    r = int.from_bytes(buffer[i*2:i*2+2], "little")
    ret += (r if r <= 0x8000 else r - 0x10000)**2
  ret = ret / dataSize
  ret = math.sqrt(ret)
  return ret

def putColor(value):
  _value = value / 4096 if value <= 4096 else 1
  if _value <= 0.2:
    r = 0
    g = int(255 * (1 - _value / 0.2))
    b = 255
  elif _value <= 0.8:
    r = int(255 * (_value - 0.2) / 0.6)
    g = 0
    b = int(255 * (1 - (_value - 0.2) / 0.6))
  else:
    r = 255
    g = 0
    b = int(255 * (_value - 0.8) / 0.2)
  return r << 16 | g << 8 | b

def setup():
  global line0, title0, label0, soundLevels, isRecording
  M5.begin()
  Widgets.fillScreen(0xeeeeee)
  line0 = Widgets.Line(120, 30, 120, 230, 0xffffff)
  title0 = Widgets.Title("Sound level meter", 3, 0xffffff, 0x0000FF, Widgets.FONTS.DejaVu12)
  label0 = Widgets.Label("Stop", 0, 16, 1.0, 0x222222, 0xeeeeee, Widgets.FONTS.DejaVu12)
  soundLevels = [0.0 for i in range(200)]
  isRecording = False
  Mic.config(dma_buf_len=256)
  Mic.config(dma_buf_count=2)
  Mic.begin()

def loop():
  global line0, title0, label0, soundLevels, isRecording
  M5.update() 
  samples = bytearray(512)
  if BtnA.wasClicked():
    if isRecording:
      isRecording = False
      title0.setColor(0x000000, 0x00ff00)
      label0.setText("Stop")
    else:
      isRecording = True
  if isRecording:
    Widgets.fillScreen(0xeeeeee)
    title0.setColor(0xffffff, 0xff0000)
    Mic.record(samples, 16000, False)
    while Mic.isRecording():
      time.sleep_ms(10)
    soundLevels.pop(0)
    soundLevels.append(putSoundLevel(samples))
    label0.setText("%.2f"%soundLevels[-1])
    for i in range(199):
      c = putColor((soundLevels[i] + soundLevels[i+1])/2)
      Widgets.Line(int(120-soundLevels[i]/4096*120), 30+i, int(120-soundLevels[i+1]/4096*120), 30+i+1, c)
  time.sleep_ms(10)

if __name__ == '__main__':
  try:
    setup()
    while True:
      loop()
  except (Exception, KeyboardInterrupt) as e:
    try:
      from utility import print_error_msg
      print_error_msg(e)
    except ImportError:
      print("please update to latest firmware")


```

# 感想
参考に挙げたページなどを根気よく読んでいればもっとすんなりできた気がします。「こんなもんやろ」でつまみ食いしながら進めてしまったために、時間を要しました。

# 参考
1. [M5StickCでMicroPython（本家ESP32版＋Thonny編）](https://blog.boochow.com/article/thonny-micropython-m5stickc.html)
2. [M5StickCでdB(デシベル)表示の騒音計を作る](https://qiita.com/tomoto335/items/263b23d9ba156de12857)
3. [MicroPython ドキュメンテーション　クラス I2S -- IC間サウンド(Inter-IC Sound)バスプロトコル](https://micropython-docs-ja.readthedocs.io/ja/latest/library/machine.I2S.html)
4. [MicroPython的午睡 投稿順index](https://jhalfmoon.com/dbc/micropython%e7%9a%84%e5%8d%88%e7%9d%a1-%e6%8a%95%e7%a8%bf%e9%a0%86index/)
5. [#23-10　PDMマイクを使って見る](http://hello.world.coocan.jp/ARDUINO23/arduino23_a.html)
6. [M5StickCのマイクを使ってみる その1](https://lang-ship.com/blog/work/m5stickc-mic/)
7. [M5StickCのマイクを使ってみる その2](https://lang-ship.com/blog/work/m5stickc-mic-2/)
8. [M5StickCのマイクを使ってみる その3](https://lang-ship.com/blog/work/m5stickc-mic-3/)