部品 · 読了まで18分

Arduinoのサウンドセンサー(KY-038):拍手スイッチと音の大きさの測定

KY-038/KY-037サウンドセンサーはArduinoでどう動くのでしょうか。ポテンショメーターの調整、ピークツーピークでの音量測定、2回拍手のライト、LEDバー、ESP32とPicoのコード、そしてdBの真実を解説します。

Arduinoのサウンドセンサーといえば、たいていはKY-038のことです。小さなエレクトレットマイク、LM393コンパレーター、青い感度調整用ポテンショメーターを1枚の基板にまとめた安価なモジュールです。大きなマイクを積んだ兄弟分のKY-037も同じように動きます。ライブラリは必要ありません。DOピンは音がしきい値を超えると状態が変わり、digitalRead()で読み取ります。AOピンはマイクの生の信号を出し、analogRead()で読み取ります。拍手で点くライト、騒音アラーム、音に反応するイルミネーションは、どれもこのモジュールで作れます。

ただし、このセンサーには多くのチュートリアルが触れない重要な限界があります。KY-038は音があるかないかを検知するセンサーです。音を録音することも、話し声を認識することもできず、デシベルを測ることもできません。この記事では、モジュールの回路のしくみ、ポテンショメーターの正しい調整方法、アナログ値がほとんど変わらないように見える理由、そしてピークツーピーク法で音の大きさを正しく測る方法を学びます。そのうえで、2回の拍手で切り替わるライトと8個のLEDによる音量バーを作り、同じことをESP32とRaspberry Pi Picoで行い、本格的な音の測定が必要なときにどのモジュールへ移ればよいかも紹介します。

KY-038サウンドセンサーモジュール(エレクトレットマイクとLM393)
KY-038サウンドセンサー:右側にエレクトレットマイク、中央にLM393コンパレーターと青い感度調整用ポテンショメーター、左側にAO、G、+、DOのピンがあります。

サウンドセンサーとは?しくみを解説

基板の端にある銀色の円筒はエレクトレットコンデンサーマイクです。内部では、薄い振動膜が永久的な電荷を持つ板と向かい合い、小さなコンデンサを作っています。音は空気の圧力の波で、振動膜を前後に押すたびに静電容量が変わり、ごくわずかな電圧の変化が生まれます。カプセル内の小さなJFETがこの信号をバッファするため、エレクトレットマイクは抵抗を通して給電し、極性もあります。その結果、マイクから出てくるのは、一定の直流電圧に乗ったわずか数ミリボルトの音の波です。

基板の中央にある8ピンのLM393チップには2つのコンパレーターが入っていて、モジュールはそのうち1つを使います。コンパレーターは2つの電圧、つまりマイクからの信号とポテンショメーターで設定したしきい値を比べます。信号がしきい値を越えた瞬間に出力の状態が変わり、DOピンと基板上の2つ目のLEDが反応します。ここで非常に大切な点があります。コンパレーターはアンプではありません。信号を大きくするのではなく、「しきい値より上か下か」という問いに答えるだけです。AOピンに出ているのはマイクの増幅されていない生の信号で、アナログ値がとても弱いのはそのためです。

項目値
マイクエレクトレットコンデンサーマイクのカプセル、無指向性
コンパレーターLM393(2回路入りのうち1つを使用)
電源3.3〜5 V
電流数mA(LED込みで約5〜10 mA)
出力AO:マイクの生の信号、DO:しきい値の出力
感度調整青い多回転型の半固定抵抗(トリマー、たいてい3296型)
LED1つは電源用、1つはDOの状態表示用
ピンの並びAO、G、+、DO

KY-038とKY-037の違い

KY-038とKY-037は同じ回路、同じLM393、同じピンの並びを使っているので、この記事のスケッチはどちらでも変更なしで動きます。違いはマイクのカプセルです。KY-037はカプセルが大きく「高感度サウンドセンサー」として売られており、KY-038はカプセルが小さく、センサーキットに最もよく入っているモデルです。実際には、どちらも近くの大きな音はよく拾いますが、部屋の反対側で話している声は聞き取れません。デジタル出力だけの3ピンのLM393サウンドモジュールもあり、その多くは音がすると出力がLOWになります。

モデルピンマイクメモ
KY-038AO、G、+、DO小型カプセルセンサーキットで最も一般的なモデル
KY-037AO、G、+、DO大型カプセル回路もコードも同じ。たいてい少し感度が高い
3ピンのLM393サウンドモジュールVCC、GND、OUT小型カプセルデジタル出力のみ。OUTはたいてい音がするとLOWになる

サウンドセンサーの配線:Arduino Uno、ESP32、Pico

モジュールのピンArduino UnoESP32Raspberry Pi Pico
+5V3V33V3(OUT)、36番ピン
GGNDGNDGND、38番ピン
AOA0GPIO 34(ADC1)GP26(ADC0)、31番ピン
DOD2GPIO 27GP15、20番ピン
ESP32とPicoのピンは3.3 V用に設計されています。これらのボードでは、サウンドセンサーを5V、VIN、VBUSではなく3V3ピンから給電してください。そうしないとDO出力が5 Vまで上がり、入力ピンを傷めるおそれがあります。ESP32では、AOにADC1のピン(GPIO 32〜39)のどれかを使います。ADC2のピンは、Wi-Fiがオンの間はanalogRead()で読み取れません。

デジタル出力(DO)とポテンショメーターの調整

DOピンが返すのは「はい」か「いいえ」の答えです。多くのKY-038基板では、音がしきい値を超えるとDOがHIGHになり、2つ目のLEDが点灯します。一部の互換品や3ピンのモジュールではその逆です。そのため、スケッチでは「音あり」を意味するレベルをSOUND_ACTIVEという定数で持っています。もう1つの大事な点は、DOが拍手1回につき1本の長いパルスを出すわけではないことです。音の波は1秒間に何百回もしきい値を上下するので、出力も同じ速さでオンとオフを繰り返します。1回の拍手で、数十ミリ秒続く短いパルスの連続が生まれます。ほとんどの基板ではコンパレーターにヒステリシスがないため、しきい値に近い音でも出力がばたつきます。そのため、DOはdelay()のない速いループか割り込みで読み取り、間隔の近いパルスは1つの出来事として扱います。

  1. モジュールを接続し、センサーを実際に使う場所に、普段どおりの生活音がある時間帯に置きます。調整は必ず実際の環境で行ってください。
  2. 基板のDO用LEDを見ます。静かなときでも点いたままなら、しきい値が敏感すぎます。小さなドライバーでLEDが消えるまでポテンショメーターを回します。
  3. LEDがまったく点かない場合は、逆方向に回して点灯し始める位置を探します。
  4. その位置から、LEDが消えたままになる側へ半回転〜1回転戻します。こうすると、冷蔵庫や扇風機のような一定の音では反応しなくなります。
  5. 約1メートル離れて拍手します。LEDが一瞬光れば正解です。普通の話し声でも光る場合は感度を下げる方向へ少し回し、拍手で光らない場合は感度を上げる方向へ少し回します。
これらの基板の青いポテンショメーターは、たいてい端から端まで約25回転する3296型の多回転トリマーです。何回か回してもLEDに変化がなくても、故障ではありません。さらに回し続ける必要があるだけです。回しきるとねじが空回りし始めるので、無理に回す必要はありません。
C++ (Arduino IDE)
1// KY-038 DO testi: sesi algıla ve dahili LED'i yak / KY-038 DO test: detect sound and light the built-in LED
2const int SOUND_DO = 2;               // modülün DO pini / module DO pin
3const int SOUND_ACTIVE = HIGH;        // ses gelince HIGH; kartın tersse LOW / HIGH on sound; LOW if your board is inverted
4const unsigned long HOLD_MS = 200;    // yeni olay için gereken sessizlik / silence needed for a new event
5
6unsigned long lastSound = 0;
7unsigned long events = 0;
8
9void setup() {
10  Serial.begin(9600);
11  pinMode(SOUND_DO, INPUT);
12  pinMode(LED_BUILTIN, OUTPUT);
13}
14
15void loop() {
16  unsigned long now = millis();
17  if (digitalRead(SOUND_DO) == SOUND_ACTIVE) {
18    if (now - lastSound > HOLD_MS) {  // sessizlikten sonra ilk darbe / first pulse after silence
19      events++;
20      Serial.print(F("Ses algilandi / Sound detected #"));
21      Serial.println(events);
22    }
23    lastSound = now;                  // darbe yağmuru süreyi uzatır / the pulse burst extends the time
24  }
25  digitalWrite(LED_BUILTIN, now - lastSound < HOLD_MS ? HIGH : LOW);  // LED en az 200 ms yanar / LED stays on at least 200 ms
26}

このスケッチは、delay()を使わずにループのたびにDOピンを読み取ります。DOがアクティブになるたびにlastSoundの時刻が更新され、200 msの無音が続いて初めて新しい出来事として数えられます。そのため、拍手1回分のパルスの連続は、シリアルモニタでは1行として表示されます。内蔵LEDも最低200 ms点灯させています。そうしないと、ミリ秒単位のパルスは目で見えません。静かなのにカウンターが増え続ける場合は、しきい値が敏感すぎるか、基板のDO出力が反転しています。後者の場合はSOUND_ACTIVEをLOWにしてください。

アナログ値(AO)がとても弱いのはなぜ?

初心者の多くはAOをA0につないでanalogRead()の値を表示し、500前後の数値が音でもほとんど変わらないのを目にします。センサーは壊れていません。問題はスケールです。Arduino Unoの10ビットADCは0〜5 Vを1024段階に分けるので、1段階は約4.9 mVです。普通の音では、マイクが出す信号はこの1段階よりずっと小さいのです。

音おおよそのレベルマイクの出力(ピークツーピーク)UnoでのADCの段階数
ささやき声30 dB SPL約0.01 mV0
普通の話し声(1 m)60 dB SPL約0.4 mV0(見えない)
大音量のテレビ、交通騒音80 dB SPL約3.6 mV0〜1
とても大きな音(1 Pa)94 dB SPL約18 mV3〜4
すぐ近くでの鋭い拍手や車のクラクション110 dB SPL約110 mV約23

この表は、一般的なエレクトレットカプセルの感度である−44 dBV/Paを使い、音を正弦波と仮定して計算しました。基板上のマイクのバイアス抵抗によっては実際の値が数倍大きくなることもありますが、結論は変わりません。AOがはっきりとらえられるのは近くの大きな音だけで、普通の話し声やささやき声はUnoの分解能を下回ります。ADCのしくみはADCとは?で詳しく紹介しています。

2つ目の理由は音の性質です。音は直流のレベルを中心に上下に振れる波です。1回のanalogRead()は、その波のランダムな瞬間をとらえます。山に当たることも、谷に当たることも、ちょうど真ん中に当たることもあります。つまり、1回の読み取り値の大きさからは音の大きさについて何もわかりません。3つ目の理由は速さです。Unoはアナログ読み取り1回に約100マイクロ秒かかるので、1秒間に最大でも約10,000回しかサンプリングできません。音の大きさを追うには十分ですが、音を録音したり周波数を分析したりするには足りません。最後に、USBからの5 V電源のノイズがそのままAOに入り込みます。最初の2つの問題は、正しい測定方法で解決できます。

ピークツーピーク振幅の測定:音の大きさを正しく読む

正しい方法は、短い時間の窓の中でできるだけ多くのサンプルをとり、最大値と最小値を見つけることです。その差がピークツーピーク振幅で、直流のレベルに関係なく音の強さを表します。窓の長さは50 msがちょうどよい値です。人間の耳に聞こえる最も低い周波数である20 Hzの1周期がちょうど50 msなので、いちばん低い音でも1波長がまるごと窓に収まります。Unoはその間に約450〜500回サンプリングします。

C++ (Arduino IDE)
1// Tepe-tepe genlik ölçümü (Seri Çizici için) / Peak-to-peak amplitude (for the Serial Plotter)
2const int SOUND_AO = A0;                 // modülün AO pini / module AO pin
3const unsigned long WINDOW_MS = 50;      // 50 ms = 20 Hz'in bir periyodu / one period of 20 Hz
4
5void setup() {
6  Serial.begin(115200);
7}
8
9void loop() {
10  int minV = 1023, maxV = 0;
11  long sum = 0;
12  int count = 0;
13  unsigned long start = millis();
14  while (millis() - start < WINDOW_MS) { // yaklaşık 450-500 örnek / about 450-500 samples
15    int v = analogRead(SOUND_AO);
16    if (v < minV) minV = v;
17    if (v > maxV) maxV = v;
18    sum += v;
19    count++;
20  }
21  int p2p = maxV - minV;                 // tepe-tepe genlik / peak-to-peak amplitude
22  float mv = p2p * 5000.0 / 1024.0;      // milivolt karşılığı / in millivolts
23
24  Serial.print(F("p2p:"));
25  Serial.print(p2p);
26  Serial.print(F(" ortalama_avg:"));     // AO'nun DC seviyesi; istemezsen bu iki satırı sil / DC level of AO; delete these two lines if not needed
27  Serial.print(sum / count);
28  Serial.print(F(" mV:"));
29  Serial.println(mv, 1);
30}

スケッチを書き込んだら、「ツール」メニューからシリアルプロッタを開き、速度を115200にします。静かな部屋ではp2pはたいてい0〜3の間にとどまり、マイクのそばで話すと数ポイント、近くで拍手すると数十ポイント上がります。ortalama_avgの値はAOの直流レベルで、グラフのスケールを広げてしまうので、p2pを見やすく追いたい場合はortalama_avgの2行を削除してもかまいません。基板によってはポテンショメーターでこの直流レベルも動きます。平均が0か1023に張り付いている場合は信号がクリップしているので、値が中央付近に来るまでポテンショメーターを回してください。シリアルプロッタが初めてなら、Arduinoのシリアルモニタをご覧ください。

ステップごとに作る:2回の拍手でライトを切り替える

拍手で点くライトは最もよく知られたサウンドセンサーのプロジェクトですが、1回の拍手で動く回路は、ドアを強く閉める音や本を落とした音、せきにも反応し、ライトが何度も点いたり消えたりしてしまいます。解決策はリズムを見ることです。下のスケッチは、0.12〜0.7秒の間隔で鳴ったちょうど2回の拍手にだけ反応します。1回だけの音や、3回以上の音(音楽、大勢の拍手、犬の鳴き声)は無視します。

  1. KY-038の+ピンを5Vに、GをGNDに、DOをD2につなぎます。
  2. リレーモジュールのVCCピンを5Vに、GNDをGNDに、INをD7につなぎます。リレーのしくみと負荷側の配線はリレーモジュールとは?で紹介しています。最初のテストはライトではなく、リレーに付いているLEDで行ってください。
  3. スケッチを書き込み、シリアルモニタを9600ボーで開きます。
  4. 1〜2メートル離れて2回拍手します。拍手1回につき「Alkis / Clap」の行が1行だけ表示されるはずです。1回の拍手が2回以上数えられる場合は、ポテンショメーターを感度が下がる方向へ少し回します。
  5. 2回目の拍手から約0.7秒後にライトの状態が切り替わるのを確認します。
C++ (Arduino IDE)
1// Çift alkışla lamba (röle) / Double-clap lamp (relay)
2const int SOUND_DO = 2;                     // KY-038 DO pini / KY-038 DO pin
3const int RELAY = 7;                        // röle modülü IN / relay module IN
4const int SOUND_ACTIVE = HIGH;              // kartın tersse LOW / LOW if your board is inverted
5const int RELAY_ON = LOW;                   // aktif LOW röle modülü / active-LOW relay module
6const int RELAY_OFF = HIGH;
7const unsigned long QUIET_GAP_MS = 120;     // yeni alkış için gereken sessizlik / silence needed before a new clap
8const unsigned long CLAP_WINDOW_MS = 700;   // dizinin bittiğine karar verme süresi / time to decide the series is over
9const unsigned long LOCKOUT_MS = 1000;      // röle tıkırtısını yok say / ignore the relay click
10
11int clapCount = 0;
12unsigned long lastPulse = 0;                // DO'nun son aktif olduğu an / last time DO was active
13unsigned long lastToggle = 0;               // lambanın son değiştiği an / last time the lamp changed
14bool lampOn = false;
15
16void setup() {
17  Serial.begin(9600);
18  pinMode(SOUND_DO, INPUT);
19  pinMode(RELAY, OUTPUT);
20  digitalWrite(RELAY, RELAY_OFF);           // lamba kapalı başlar / lamp starts off
21}
22
23void loop() {
24  unsigned long now = millis();
25
26  if (digitalRead(SOUND_DO) == SOUND_ACTIVE) {
27    bool locked = now - lastToggle < LOCKOUT_MS;
28    if (!locked && now - lastPulse >= QUIET_GAP_MS) {  // sessizlikten sonra ilk darbe = yeni alkış / first pulse after silence = new clap
29      clapCount++;
30      Serial.print(F("Alkis / Clap: "));
31      Serial.println(clapCount);
32    }
33    lastPulse = now;                        // aynı alkışın darbeleri süreyi uzatır / pulses of the same clap extend the time
34  }
35
36  if (clapCount > 0 && now - lastPulse > CLAP_WINDOW_MS) {  // dizi bitti / the series is over
37    if (clapCount == 2) {                   // yalnız tam iki alkış / exactly two claps only
38      lampOn = !lampOn;
39      digitalWrite(RELAY, lampOn ? RELAY_ON : RELAY_OFF);
40      lastToggle = now;
41      if (lampOn) Serial.println(F("Lamba ACIK / Lamp ON"));
42      else Serial.println(F("Lamba KAPALI / Lamp OFF"));
43    }
44    clapCount = 0;                          // 1, 3 ve fazlası yok sayılır / 1, 3 or more are ignored
45  }
46}
定数値目的
SOUND_ACTIVEHIGH音がしたときのDOのレベル。基板が反転している場合はLOWにする
QUIET_GAP_MS120 ms新しい拍手として数えるまでに必要な無音の時間。拍手1回分のパルスの連続と反響は1回として数える
CLAP_WINDOW_MS700 ms最後の音からこの時間だけ無音が続くと、一連の拍手が終わったとみなして判定する
LOCKOUT_MS1000 msライトが切り替わったあとは音を無視し、リレーのカチッという音を新しい拍手と間違えないようにする
RELAY_ON / RELAY_OFFLOW / HIGHアクティブLOWのリレーモジュール用。アクティブHIGHの基板では入れ替える

このスケッチはdelay()を使わず、すべてをmillis()で計るので、拍手のタイミングを取りこぼしません。この考え方はmillisとdelayの違いで詳しく説明しています。ライトが2回目の拍手の直後ではなく0.7秒後に切り替わるのは意図的な設計で、3回目の拍手が来るかどうかを待っているのです。もっと速く反応させたい場合はCLAP_WINDOW_MSを400〜500 msに下げられますが、そのぶん拍手も速くする必要があります。部屋の反響が大きく、1回の拍手が2回と数えられる場合は、QUIET_GAP_MSを150〜200 msに上げてください。

リレーで商用電源の電圧(110〜230 V)を切り替えるのは命にかかわる危険があります(日本の商用電源は100V(50/60Hz)ですが、危険性は同じです)。接続はプラグを抜いた状態で行い、むき出しの線の端を残さず、回路は絶縁された密閉ケースに入れてください。自信がない場合は、まず12 VのLEDテープで試しましょう。また、センサーはリレーから離して置きます。リレーのカチッという音がマイクのすぐそばで鳴ると、ロックアウト時間があっても誤動作の原因になります。

音量バー:8個のLEDで作るVUメーター

ピークツーピークの値をそのままLEDに送っても動きますが、自然には見えません。人間の耳は音を対数的に感じるからです。振幅が2倍の音が、2倍の大きさに聞こえるわけではありません。音をデシベル(dB)で測るのはそのためです。振幅の場合の式はdB = 20 × log10(p2p / 基準値)で、振幅が2倍なら+6 dB、10倍なら+20 dBです。下のスケッチは起動時に1秒間、部屋のノイズフロア(暗騒音)を測って基準値とし、LED 1個がそのフロアから3 dB分を表します。LEDはD3〜D10のピンに、それぞれ220 Ωの抵抗を付けてつなぎます。ほかの色のLEDに合った抵抗はLED抵抗の計算ツールで求められます。

C++ (Arduino IDE)
1// 8 LED'li ses seviyesi göstergesi (VU metre) / 8-LED sound level bar (VU meter)
2const int SOUND_AO = A0;                    // modülün AO pini / module AO pin
3const int LED_PINS[8] = {3, 4, 5, 6, 7, 8, 9, 10};  // her LED 220 Ω ile / each LED with 220 Ω
4const float DB_PER_LED = 3.0;               // her LED 3 dB / each LED is 3 dB
5const unsigned long FALL_MS = 80;           // LED'ler bu aralıkla birer birer söner / LEDs drop one by one at this rate
6
7float noiseFloor = 1.0;                     // sessiz ortamın p2p değeri / quiet-room p2p
8int shown = 0;                              // yanan LED sayısı / number of lit LEDs
9unsigned long lastFall = 0;
10
11int readPeakToPeak() {
12  int minV = 1023, maxV = 0;
13  unsigned long start = millis();
14  while (millis() - start < 50) {           // 50 ms pencere / 50 ms window
15    int v = analogRead(SOUND_AO);
16    if (v < minV) minV = v;
17    if (v > maxV) maxV = v;
18  }
19  return maxV - minV;
20}
21
22void setup() {
23  Serial.begin(115200);
24  for (int i = 0; i < 8; i++) pinMode(LED_PINS[i], OUTPUT);
25  long sum = 0;
26  for (int i = 0; i < 20; i++) sum += readPeakToPeak();  // 1 sn sessizlik ölç / measure 1 s of silence
27  noiseFloor = sum / 20.0;
28  if (noiseFloor < 1.0) noiseFloor = 1.0;
29}
30
31void loop() {
32  int p2p = readPeakToPeak();
33  if (p2p < 1) p2p = 1;
34  float db = 20.0 * log10(p2p / noiseFloor);  // tabana göre göreceli dB / relative dB above the floor
35  int level = constrain((int)(db / DB_PER_LED), 0, 8);
36
37  unsigned long now = millis();
38  if (level >= shown) {                     // yükselişte anında / rise instantly
39    shown = level;
40    lastFall = now;
41  } else if (now - lastFall >= FALL_MS) {   // inişte yavaşça / fall slowly
42    shown--;
43    lastFall = now;
44  }
45  for (int i = 0; i < 8; i++) digitalWrite(LED_PINS[i], i < shown ? HIGH : LOW);
46
47  Serial.print(F("dB:"));
48  Serial.println(db, 1);
49}

起動時の1秒間は静かにしていてください。その間に話すとフロアが高く測定され、バーの反応が鈍くなります。バーはすぐに上がりますが、80 msごとにLED 1個ずつゆっくり下がります。本物のVUメーターがちらつかずになめらかに見えるのも同じ理由です。KY-038のアナログ信号は弱いので、LED 1個あたり2〜3 dBが合っています。MAX4466のような増幅回路付きのマイクで同じスケッチを使う場合は、DB_PER_LEDを4〜6 dBに上げられます。カラフルな表示にしたいなら、LEDの代わりにNeoPixelリングを使えます。詳しくはWS2812B NeoPixel LEDをご覧ください。

ESP32でサウンドセンサーを使う:割り込みで拍手を検知

ESP32のADCは12ビットで、0〜4095の値を返します。3.3 Vの範囲なら1段階は約0.8 mVで、Unoの約6倍細かくなります。一方でESP32のADCはノイズが多いので、静かな部屋でのp2pの値も高めに出ますが、相対的なdBの計算でこれは自然に相殺されます。下のスケッチでは、DOピンを割り込みに割り当てています。loop()がAOから音の大きさを50 msかけて測っている間でも、拍手のパルスを取りこぼしません。割り込み関数はIRAM_ATTRでRAMに配置して短く保ち、割り込みとloop()の両方で使う変数はvolatileで宣言し、クリティカルセクションで保護しています。

C++ (Arduino IDE)
1// ESP32: kesmeyle çift alkış + ses seviyesi / ESP32: double clap via interrupt + sound level
2const int SOUND_AO = 34;                    // ADC1 pini, Wi-Fi açıkken de çalışır / ADC1 pin, works with Wi-Fi on
3const int SOUND_DO = 27;                    // modülün DO pini / module DO pin
4const int RELAY = 26;                       // röle modülü IN / relay module IN
5const int SOUND_ACTIVE = HIGH;              // kartın tersse LOW / LOW if your board is inverted
6const int RELAY_ON = LOW;                   // aktif LOW röle / active-LOW relay
7const int RELAY_OFF = HIGH;
8const unsigned long QUIET_GAP_MS = 120;     // yeni alkış için sessizlik / silence before a new clap
9const unsigned long CLAP_WINDOW_MS = 700;   // dizinin bitmesi / end of the series
10const unsigned long LOCKOUT_MS = 1000;      // röle tıkırtısını yok say / ignore the relay click
11
12portMUX_TYPE mux = portMUX_INITIALIZER_UNLOCKED;  // kesme ile loop arasında kilit / lock between ISR and loop
13volatile int clapCount = 0;
14volatile unsigned long lastPulse = 0;
15volatile unsigned long lastToggle = 0;
16bool lampOn = false;
17float noiseFloor = 1.0;
18
19void IRAM_ATTR onSound() {                  // kısa tutulur, RAM'den çalışır / kept short, runs from RAM
20  unsigned long now = millis();
21  portENTER_CRITICAL_ISR(&mux);
22  if (now - lastToggle >= LOCKOUT_MS && now - lastPulse >= QUIET_GAP_MS) clapCount++;
23  lastPulse = now;
24  portEXIT_CRITICAL_ISR(&mux);
25}
26
27int readPeakToPeak() {
28  int minV = 4095, maxV = 0;
29  unsigned long start = millis();
30  while (millis() - start < 50) {           // 50 ms pencere / 50 ms window
31    int v = analogRead(SOUND_AO);           // 0-4095 (12 bit)
32    if (v < minV) minV = v;
33    if (v > maxV) maxV = v;
34  }
35  return maxV - minV;
36}
37
38void setup() {
39  Serial.begin(115200);
40  pinMode(SOUND_DO, INPUT);
41  pinMode(RELAY, OUTPUT);
42  digitalWrite(RELAY, RELAY_OFF);
43  long sum = 0;
44  for (int i = 0; i < 20; i++) sum += readPeakToPeak();  // 1 sn sessiz ortam / 1 s of quiet room
45  noiseFloor = sum / 20.0;
46  if (noiseFloor < 1.0) noiseFloor = 1.0;
47  attachInterrupt(digitalPinToInterrupt(SOUND_DO), onSound, SOUND_ACTIVE == HIGH ? RISING : FALLING);
48}
49
50void loop() {
51  int p2p = readPeakToPeak();               // bu sırada alkışları kesme yakalar / the interrupt catches claps meanwhile
52  float db = 20.0 * log10((p2p < 1 ? 1 : p2p) / noiseFloor);
53  Serial.printf("p2p:%d dB:%.1f\n", p2p, db);
54
55  portENTER_CRITICAL(&mux);
56  unsigned long now = millis();
57  int claps = clapCount;
58  bool finished = claps > 0 && now - lastPulse > CLAP_WINDOW_MS;
59  if (finished) clapCount = 0;              // dizi bitti, sayacı sıfırla / series over, reset the counter
60  portEXIT_CRITICAL(&mux);
61
62  if (finished && claps == 2) {             // yalnız tam iki alkış / exactly two claps only
63    lampOn = !lampOn;
64    digitalWrite(RELAY, lampOn ? RELAY_ON : RELAY_OFF);
65    portENTER_CRITICAL(&mux);
66    lastToggle = millis();
67    portEXIT_CRITICAL(&mux);
68    Serial.println(lampOn ? "Lamba ACIK / Lamp ON" : "Lamba KAPALI / Lamp OFF");
69  }
70}

このスケッチはUnoと同じ2回拍手のロジックを割り込みの中で実行し、約50 msごとに音の大きさをdBで表示します。リレーモジュールのINピンはGPIO 26につなぎます。5 Vで給電するアクティブLOWのリレーモジュールの中には、ESP32の3.3 VのHIGHレベルでは完全にオフにならないものがあるので、その場合は3.3 Vで駆動できるモジュールを選んでください。ESP32の本当の強みはWi-Fiです。音の大きさが数秒間しきい値を超えたままなら(赤ちゃんが泣いている、犬が吠えているなど)、スマートフォンに通知を送れます。その方法はESP32のTelegramボットで、ホームオートメーションのシステムにつなぐ方法はESP32のMQTTとHome Assistantで紹介しています。

Raspberry Pi PicoとMicroPythonでサウンドセンサーを使う

Raspberry Pi Picoでは、モジュールを3V3(OUT)ピンから給電し、AOをGP26に、DOをGP15につなぎます。ライトの代わりに、220 Ωの抵抗を付けたLEDをGP14につなぎます。PicoのADCは12ビットですが、MicroPythonのread_u16()は値を0〜65535に換算します。そのためp2pの数値はUnoよりずっと大きく見えますが、相対的なdBの計算は変わりません。DOピンにはPin.irq()で割り込みを設定します。

MicroPython
1# Raspberry Pi Pico: çift alkış + ses seviyesi / double clap + sound level
2from machine import Pin, ADC
3import time, math
4
5sound_ao = ADC(26)                  # GP26 = ADC0, modülün AO pini / module AO pin
6sound_do = Pin(15, Pin.IN)          # modülün DO pini / module DO pin
7lamp = Pin(14, Pin.OUT, value=0)    # LED (220 Ω ile) / LED (with 220 Ω)
8
9QUIET_GAP_MS = 120                  # yeni alkış için sessizlik / silence before a new clap
10CLAP_WINDOW_MS = 700                # dizinin bitmesi / end of the series
11LOCKOUT_MS = 1000                   # değişimden sonra sesleri yok say / ignore sounds after a toggle
12
13claps = 0
14last_pulse = time.ticks_ms()
15last_toggle = time.ticks_ms()
16
17def on_sound(pin):                  # kesme fonksiyonu / interrupt handler
18    global claps, last_pulse
19    now = time.ticks_ms()
20    if (time.ticks_diff(now, last_toggle) >= LOCKOUT_MS
21            and time.ticks_diff(now, last_pulse) >= QUIET_GAP_MS):
22        claps += 1
23    last_pulse = now
24
25# DO sesle LOW oluyorsa IRQ_FALLING yaz / use IRQ_FALLING if DO goes LOW on sound
26sound_do.irq(trigger=Pin.IRQ_RISING, handler=on_sound)
27
28def peak_to_peak(window_ms=50):     # 50 ms boyunca en büyük - en küçük / max - min over 50 ms
29    lo, hi = 65535, 0
30    start = time.ticks_ms()
31    while time.ticks_diff(time.ticks_ms(), start) < window_ms:
32        v = sound_ao.read_u16()     # 0-65535
33        if v < lo:
34            lo = v
35        if v > hi:
36            hi = v
37    return hi - lo
38
39noise_floor = max(1, sum(peak_to_peak() for _ in range(20)) / 20)  # 1 sn sessizlik / 1 s of silence
40
41while True:
42    p2p = peak_to_peak()
43    db = 20 * math.log10(max(p2p, 1) / noise_floor)   # göreceli dB / relative dB
44    print("p2p:", p2p, "dB:", round(db, 1))
45
46    now = time.ticks_ms()
47    if claps and time.ticks_diff(now, last_pulse) > CLAP_WINDOW_MS:
48        if claps == 2:              # yalnız tam iki alkış / exactly two claps only
49            lamp.toggle()
50            last_toggle = now
51            print("Lamba / Lamp:", "ON" if lamp.value() else "OFF")
52        claps = 0

Thonnyでコードを実行すると、シェルにp2pとdBの値が表示されます。グラフで追いたい場合は「表示」メニューから「プロッター」を開きます。基板のDO出力が音でLOWになる場合は、IRQ_RISINGの代わりにIRQ_FALLINGと書きます。Picoに電源を入れたときにプログラムが自動で始まるようにするには、ファイルをmain.pyとしてPicoに保存します。MicroPythonが初めてなら、MicroPythonの基本から始めるのがおすすめです。

サウンドセンサーでデシベルは測れる?

結論から言うと、測れません。ネット上には、KY-038のピークツーピークの値をmap()で49〜90 dBのような範囲に変換するスケッチがたくさんありますが、それは測定ではなく推測です。本当の音圧レベル(dB SPL)を測るには、感度がわかっているマイクと94 dBの校正器で校正したシステム、信号の線形な増幅、決められた時間(Fastで125 ms、Slowで1 s)で計算した実効値(RMS)、そして耳の感度をまねたA特性のフィルタが必要です。騒音計がIEC 61672規格に沿って作られているのはそのためです。

KY-038はこれらの条件を1つも満たしていません。基板のゲインは不明で、マイクのカプセルの感度は個体ごとに数dBばらつき、ポテンショメーターや電源電圧で信号のレベルが変わり、Unoでは普通の音がすべてADCの数段階に押し込められます。いちばん正直にできるのは相対的な測定、つまり「今は静かな部屋より12 dB大きい」と言うことです。それでも、騒音アラームや教室の騒音信号機、VUメーターには十分すぎるほどです。スマートフォンのアプリと1点で比べて大まかに校正することもできますが、その校正はほかの音量や別の基板では意味を失います。

代わりになるモジュール:MAX4466、MAX9814、INMP441

モジュール出力ゲインデシベルの測定対応ボード
KY-038 / KY-037AO(生の信号)+DO(しきい値)なし不可すべて
MAX4466アナログ、VCC/2が中心25〜125倍、調整可能相対値のみすべて
MAX9814アナログ、1.25 Vが中心、最大2 Vpp40 / 50 / 60 dB+自動利得制御(AGC)不可、AGCがレベルを圧縮するすべて
INMP441デジタルI2S、24ビット不要おおよそのdB SPLESP32、Pico(Unoは不可)
完成品の騒音計モジュール(例:DFRobot SEN0232)アナログ、直接dBA工場で校正済み可、30〜130 dBA、±1.5 dBすべて

MAX4466は、同じ種類のエレクトレットマイクとゲインを調整できるオペアンプを組み合わせたモジュールです。出力は無音のとき電源電圧の半分にあり、近くで普通に話すと数百ミリボルトの信号を出します。つまり、KY-038ではADCの数段階だった信号が、数十段階から数百段階になります。この記事のピークツーピークとVUメーターのスケッチは、MAX4466でも変更なしで動きます。ゲインの計算方法が気になるなら、オペアンプのゲイン計算ツールとオペアンプとは?をご覧ください。モジュールをUnoのノイズの少ない3.3Vピンから給電すると、ノイズが減ります。一方のMAX9814は、自動利得制御で遠くの話し声まで使えるレベルに引き上げます。録音や声の検知には最適ですが、静かな環境と騒がしい環境の差を縮めてしまうため、レベルの測定には向きません。

INMP441はアナログではなくデジタルのMEMSマイクです。内部で音を24ビットの数値に変換してI2Sバスで送るので、配線に乗るノイズが測定に入りません。最大の利点は、メーカーが感度を狭い公差で規定していることです。94 dB SPLの音が−26 dBFSとして読み取られます。つまり、測定した実効値をdBFSに換算して120を足せば、おおよそのdB SPLの値が得られます。I2SにはESP32かPicoが必要で、Arduino UnoにはI2Sのハードウェアがありません。INMP441のホビー向けモジュールは安くて手に入りやすく、ICS-43434やSPH0645も同様のI2Sマイクです。本当に正確な騒音測定が必要なら、A特性で校正済みの出力を持つ完成品の騒音計モジュールを使うのがいちばん簡単です。たとえばDFRobot SEN0232なら、出力電圧に50を掛けるだけでdBAの値が直接得られます。

サウンドセンサーのプロジェクトのアイデア

  • 拍手ライト:上の2回拍手のスケッチにリレーモジュールを加えて机のライトやLEDテープを操作し、3回の拍手で2台目の機器を操作します。
  • 教室の騒音信号機:相対的なdBの値に応じて緑、黄、赤のLEDを点灯させる騒音表示器です。先生たちに大人気です。
  • 赤ちゃん・ペットモニター:大きな音が5秒以上続いたら、ESP32からスマートフォンにTelegramのメッセージを送ります。
  • 音に反応するライト:音楽のビートに合わせて光るNeoPixelのリングやテープです。
  • ノック検知器:誰かがドアをノックするとブザーやLEDで知らせる仕組みです。ノックを音ではなく振動としてとらえたいなら、SW-420振動センサーもよい選択です。
  • スマートホームのシナリオ:音を人感センサーや光センサーと組み合わせ、部屋に誰もいないときに照明を消します。アイデアはArduinoでスマートホームを作るをご覧ください。

よくある失敗と直し方

症状考えられる原因対策
DO用LEDが常に点いているしきい値が敏感すぎる、または一定の騒音(扇風機、冷蔵庫)がある静かな状態でLEDが消えるまでポテンショメーターを回し、センサーを騒音源から離す
拍手しても何も起きないしきい値が高すぎる、またはDOが違うピンにつながっているまず拍手で基板のLEDが光るかを確かめ、次に配線とコードのピン番号を確認する
静かなときにカウンターが増え、音がすると止まる基板のDO出力がアクティブLOWSOUND_ACTIVEをLOWにする。PicoではIRQ_FALLINGを使う
analogReadが500前後のまま、音で1〜2しか変わらないAOは増幅されておらず、1回ずつの読み取りしかしていないピークツーピーク法を使う、音源を近づける、またはMAX4466に替える
AOが常に0か1023AOとDOを取り違えている、または直流レベルが端に寄っている配線を確認し、ポテンショメーターで平均を中央に戻す
1回の拍手が2回と数えられる部屋の反響が大きい、またはQUIET_GAP_MSが短すぎるQUIET_GAP_MSを150〜200 msにし、しきい値の感度を少し下げる
ライトが勝手に点いたり消えたりするリレーのカチッという音、ドアを強く閉める音、音楽LOCKOUT_MSを長くし、センサーをリレーから離し、2回拍手のルールを維持する
ESP32でWi-FiがオンになるとAOが読めなくなるAOがADC2のピンにつながっているAOをGPIO 32〜39のADC1のピンに移す
サウンドセンサーをテストする便利な方法は、スマートフォンのトーンジェネレーターアプリです。1 kHzの一定の音を鳴らし、スマートフォンをマイクから10、20、40 cmの位置に置きます。開けた空間では距離が2倍になるたびに音圧が半分になるので、p2pは約半分に、dBの値は約6 dB下がるはずです。この簡単な実験で、配線とコードの両方を確認できます。

数本の配線とよく書かれたコードがあれば、KY-038はコンパレーター、アナログ・デジタル変換、割り込み、時間管理、デシベルの計算といった電子工作の基本を一度に教えてくれます。まずDOで拍手を検知し、ピークツーピーク法で音の大きさを測り、本格的な音の測定が必要になったらMAX4466やINMP441に移りましょう。BlueGraysアプリでは、リレーで点けるライト、流れるLED、ESP32のスマート照明、NeoPixelテープのプロジェクトを、回路図とステップごとのコード付きで紹介しています。そこにサウンドセンサーを加えるのが、拍手や音に反応するシステムに変えるいちばん簡単な方法です。

よくある質問

Arduinoのサウンドセンサーはどのようなしくみで動きますか?

サウンドセンサーはLM393コンパレーターを使って、エレクトレットマイクからの小さな電圧とポテンショメーターで設定したしきい値を比べます。音がしきい値を超えるとDOピンの状態が変わり、基板のLEDが点灯します。AOピンはマイクの増幅されていない生の信号を出し、analogRead()で読み取ります。

KY-038とKY-037の違いは何ですか?

どちらも同じLM393の回路と同じAO、G、+、DOのピンの並びを使い、コードもまったく同じです。KY-037はマイクのカプセルが大きく、たいてい少し感度が高めです。KY-038は小さく、センサーキットによく入っています。

サウンドセンサーの感度はどうやって調整しますか?

静かな状態で、DO用LEDがちらつく位置まで青いポテンショメーターを回し、そこからLEDが消えたままになる側へ半回転〜1回転戻します。多回転型のポテンショメーターなので、変化が見えるまで何回も回す必要があるかもしれません。調整は必ずセンサーを使う場所で行ってください。

サウンドセンサーでデシベルは測れますか?

KY-038とKY-037では本当のデシベル(dB SPL)は測れません。基板のゲインもマイクの感度も不明で、校正もA特性もないからです。計算できるのは、静かな部屋と比べた相対的なdBだけです。本格的に測定するなら、INMP441のように感度がわかっているI2Sマイクか、校正済みの騒音計モジュールを使ってください。

サウンドセンサーのアナログ値が変わらないのはなぜですか?

AO出力は増幅されていないマイクの信号で、普通の音ではUnoのADCの1段階である4.9 mVより小さくなります。また、1回のanalogRead()は音の波のランダムな瞬間を読み取るだけです。50 msの間サンプリングして最大値と最小値の差(ピークツーピーク)を計算すれば、変化がはっきり見えます。

サウンドセンサーはESP32やRaspberry Pi Picoでも使えますか?

はい。モジュールは3.3 Vでも動きます。ESP32とPicoでは、DO出力が3.3 Vを超えないように3V3ピンから給電してください。ESP32ではAOをWi-Fiと競合しないADC1のピン(GPIO 32〜39)に、PicoではGP26、GP27、GP28のどれかにつなぎます。

全プロジェクトを見る
ボタンでLEDの点灯・消灯を切り替え
#10初級

ボタンでLEDの点灯・消灯を切り替え

部屋の照明スイッチのように、ボタンを1回押すと点灯し、もう1回押すと消灯します。Arduinoはボタンが押されるたびにLEDの状態を反転させます。ボタン内部の小さな接点の振動で誤動作しないよう、短い待ち時間も設けます。1つのボタンでオン・オフを切り替える、シンプルな仕組みです。

4.8(100)
167
ESP32でボタンによるLED制御(トグル)
#105初級

ESP32でボタンによるLED制御(トグル)

ボタンを押すたびにLEDの状態が切り替わり、点灯中なら消灯し、消灯中なら点灯します。基本的なトグル動作です。チャタリング対策でボタンの機械的なノイズを除去し、1回の操作で1回だけ切り替わるようにします。

4.6(39)
57
流れるLED
#5中級

流れるLED

LEDが次々に点灯し、光が走るように進みます。Arduinoが6個のLEDを1個ずつ短く点灯して消灯させることで、光が端から端へと移動し、また最初に戻ります。この簡単なプロジェクトで、1つのパターンで複数の光を制御する方法を学びます。

4.6(85)
207
ボタンを押すたびに220V電球の点灯・消灯を切り替え
#20上級
プレミアム

ボタンを押すたびに220V電球の点灯・消灯を切り替え

小さなボタンで、家庭の電球を点灯・消灯してみませんか?ボタンを押すとArduinoがリレーを切り替えて電球を点灯させ、もう一度押すと消灯させます。リレーは、ボードの弱い信号で家庭用電源を安全に切り替える仲介役です。220ボルトを扱う際は、必ず大人に手伝ってもらってください。

4.5(33)
44
Raspberry Pi PicoでボタンによるLEDの点灯・消灯切り替え
#156初級

Raspberry Pi PicoでボタンによるLEDの点灯・消灯切り替え

ボタンとLEDを使って、さらに一歩進みます!基本のボタンプロジェクトと違い、ボタンを押し続ける必要はありません。1回押して離すとLEDが点灯し、もう1回押して離すと消灯します(トグル動作)。回路は同じ(LEDはGP14、ボタンはGP2)ですが、コードは大きく変わります。エッジ検出(1→0に変わる瞬間の検出)、状態の記録、チャタリング対策(機械的な接点の振動を除去)を学びます。MicroPythonのled.toggle()なら1行で状態を切り替えられます。モード切り替え、IRQ、ダブルクリックなど、ボタン操作を使うあらゆるプロジェクトの基礎になります。

0.0(0)
9
ESP32スマート照明(PIR + LDR + リレー + OLED)
#141上級
プレミアム

ESP32スマート照明(PIR + LDR + リレー + OLED)

ESP32で廊下やトイレ、食品庫の照明をスマート化します!PIRモーションセンサー、LDR光センサー、リレーモジュールを使い、「暗い場所で動きを検知すると点灯する」定番の仕組みを作ります。周囲が明るければ動きを検知しても点灯しないため、日中の無駄な電力消費を抑えられます。照明は30秒間点灯し、動きが続くとタイマーがリセットされて点灯時間が延びます。SSD1306 OLEDには大きな文字で状態(ON / DAYTIME / WAITING)とカウントダウンを表示します。

4.3(6)
11
ESP32駐車センサー(HC-SR04 + 5連LEDバー + ブザー + OLED)
#142上級
プレミアム

ESP32駐車センサー(HC-SR04 + 5連LEDバー + ブザー + OLED)

車の駐車センサーをESP32で作ります!HC-SR04超音波センサーで前方の物体までの距離を測ります。距離が縮まるほど5連LEDバーが緑 → 黄 → 赤の順に点灯し、ブザーの鳴る間隔が短くなります。10 cm未満まで近づくとアラームが鳴り続け、すべてのLEDが点灯します。SSD1306 OLEDには現在の距離をcm単位の大きな文字で表示し、状態メッセージ(落ち着いて / 注意 / 接近 / 停止! / 危険)とLEDバーのシミュレーションも表示します。HC-SR04のpulseIn、複数のデジタル出力、ブザーを学べる定番のプロジェクトです。

0.0(0)
14
ESP32 WS2812B NeoPixel LEDテープの制御
#126中級
プレミアム

ESP32 WS2812B NeoPixel LEDテープの制御

WS2812B(NeoPixel)は、1本のデータピンで連結したLEDを個別に制御できるスマートなRGB LEDです。このプロジェクトでは、8個のLEDを備えたWS2812BテープをAdafruit_NeoPixelライブラリで制御し、滑らかな虹色のアニメーションを作ります。ESP32の3.3VロジックレベルはほとんどのWS2812Bのロットで問題なく動作しますが、不安定な場合に備え、470Ωの直列抵抗と1000µFのコンデンサを使う対策も紹介します。アニメーションは、HSV色空間の色相(hue)をずらして作ります。

4.9(19)
30