この記事は「2025 Japan AWS Jr. Chanpions 夏のQiitaリレー」の31日目の記事です!!
宣伝と過去の投稿(リンク集)はこちら👇
はじめに
スケーラブルなリモートスピーカーシステムを作りたいと思い、ATOM S3 LiteとAWS IoT Coreを組み合わせてみました。イマーシブシアターの小道具や空間演出での利用を想定しています。
この記事で実現すること
- AWS IoT Core経由でのリモート音声再生
- 複数デバイスへの拡張可能なアーキテクチャ
イマーシブシアターについて知りたい方はこちら
使用機材・サービス
ハードウェア
- ATOM S3 Lite:ESP32-S3ベースの小型マイコン
- ATOM SPK:専用スピーカーユニット
開発環境
- PlatformIO IDE(VS Code拡張)
- macOSでの開発
AWS環境
- AWS IoT Core:MQTT通信とデバイス管理
- Amazon S3:音声ファイルホスティング
- AWS Lambda:制御ロジック
作成した構成
- AWS Lambda が IoT Core に {cmd, url} を Publish
- Atom S3+SPK Base が MQTT トピックを Subscribe → コマンド受信
- Atom S3 が AWS S3 へ HTTP GET リクエスト
- AWS S3 から MP3 データを Atom S3 に Streaming で返送
- 内蔵スピーカーへ出力
<ポイント>
Lambdaによるプログラム制御のおかげで、
1台のPCから、複数のスピーカーに、違う指示を送ることができる!!
開発環境のセットアップ
PlatformIO IDEの導入
Arduino IDEでも開発可能らしいですが、VS Codeに慣れているのでPlatformIO IDEを選びました。
初期設定はこちらのブログを参考に進めました。
今回は、S3 liteを使っているのでAtom S3を選択しプロジェクトを作成しました。
プロジェクト設定
ポートの確認(mac terminalで実行)
ls /dev/cu.usb*
# → /dev/cu.usbmodem101 が表示された
platformio.ini
の設定内容
[env:m5stack-atoms3]
platform = espressif32
board = m5stack-atoms3
framework = arduino
upload_port = /dev/cu.usbmodem101
upload_speed = 1500000
monitor_speed = 115200
; --- USB 関連 --------------------------------------------------
build_flags =
-DARDUINO_USB_MODE=1 ; 0=JTAG, 1=CDC, 2=MSC+CDC
-DARDUINO_USB_CDC_ON_BOOT=1 ; ← ← ← ここが正しいスイッチ
-DBOARD_HAS_PSRAM ; ← ESP32‑S3 の PSRAM 8 MB を有効に
-mfix-esp32-psram-cache-issue
lib_deps =
m5stack/M5Unified@
m5stack/M5Atomic-EchoBase@^1
https://github.com/schreibfaul1/ESP32-audioI2S.git#2.0.6
bblanchon/ArduinoJson
FastLED
WiFi
knolleary/PubSubClient@^2.8
重要
-
ARDUINO_USB_CDC_ON_BOOT=1
の設定がないとSerialの出力がモニタに表示されません。 -
ESP32-audioI2S
はバージョン3だと動作しないため、2.0.6を指定しています。
AWS IoT Coreの設定
デバイス登録
-
Thing(モノ)の作成
- AWS IoT Coreコンソールで「モノを作成」
- 証明書は「自動生成」を選択
-
証明書のダウンロード
- デバイス証明書(
xxx-certificate.pem.crt
) - プライベートキー(
xxx-private.pem.key
) - ルートCA証明書(
AmazonRootCA1.pem
)
- デバイス証明書(
-
ポリシーの作成と紐付け
AWS IoT > 管理 > セキュリティ > ポリシー で作成する。{ "Version": "2012-10-17", "Statement": [ { "Effect": "Allow", "Action": [ "iot:Connect", "iot:Publish", "iot:Receive", "iot:Subscribe" ], "Resource": "arn:aws:iot:*:*:*" } ] }
証明書の埋め込み
src/certs.h
ファイルを作成し、ダウンロードした証明書を埋め込みます。
#pragma once
static const char AWS_ROOT_CA[] PROGMEM = R"EOF(
-----BEGIN CERTIFICATE-----
(AmazonRootCA1.pemの内容)
-----END CERTIFICATE-----
)EOF";
static const char CLIENT_CERT[] PROGMEM = R"EOF(
-----BEGIN CERTIFICATE-----
(xxx-certificate.pem.crtの内容)
-----END CERTIFICATE-----
)EOF";
static const char CLIENT_KEY[] PROGMEM = R"EOF(
-----BEGIN RSA PRIVATE KEY-----
(xxx-private.pem.keyの内容)
-----END RSA PRIVATE KEY-----
)EOF";
注意
ファイル内容をコピーする際は、PEM本文のみを貼り付けてください。
ファームウェア実装
メインコード
#include <WiFi.h>
#include <WiFiClientSecure.h>
#include <PubSubClient.h>
#include <FastLED.h>
#include <Audio.h>
#include <ArduinoJson.h>
#include "certs.h"
/* Wi‑Fi */
const char* WIFI_SSID = "XXXXXXXXXXXX";
const char* WIFI_PASS = "XXXXXXXXXXXX";
/* AWS IoT Core */
const char* AWS_EP = "XXXXXXXXXXXXXXXXXXXXXXXXx";
const char* SUB_TOPIC = "music/control";
/* LED */
#define LED_PIN 35
#define PIXEL_PWR 21
CRGB leds[1];
/* SPK AMP enable (ATOM SPK v2 = GPIO25, v1=26) */
#define AMP_EN 25 // ★ 追加
/* MQTT */
WiFiClientSecure net;
PubSubClient mqtt(net);
/* Audio */
Audio audio;
/* ---------- util ------------------------------------------------ */
void connectWiFi() {
WiFi.begin(WIFI_SSID, WIFI_PASS);
for (int i = 0; i < 100 && WiFi.status() != WL_CONNECTED; ++i) delay(100);
Serial.println(WiFi.status() == WL_CONNECTED ?
"WiFi OK IP=" + WiFi.localIP().toString() : "WiFi FAIL");
}
void connectAWS() {
net.setCACert(AWS_ROOT_CA);
net.setCertificate(CLIENT_CERT);
net.setPrivateKey(CLIENT_KEY);
mqtt.setServer(AWS_EP, 8883);
mqtt.setCallback([](char* topic, byte* payload, unsigned len) {
StaticJsonDocument<256> doc;
deserializeJson(doc, payload, len);
const char* cmd = doc["cmd"] | "";
if (!strcmp(cmd, "led")) { // LED 制御
const char* c = doc["color"] | "off";
if (!strcmp(c,"red")) leds[0] = CRGB::Red;
else if (!strcmp(c,"blue")) leds[0] = CRGB::Blue;
else if (!strcmp(c,"green")) leds[0] = CRGB::Green;
else leds[0] = CRGB::Black;
FastLED.show();
}
else if (!strcmp(cmd, "play")) { // 曲再生
const char* url = doc["url"] | "";
if (*url) audio.connecttohost(url);
}
else if (!strcmp(cmd, "stop")) { // 再生停止
audio.stopSong(); // ← ここで止める
}
Serial.printf("RX %s: %.*s\n", topic, len, payload);
});
while (!mqtt.connected()) {
if (mqtt.connect("AtomS3Lite")) {
mqtt.subscribe(SUB_TOPIC);
Serial.println("MQTT connected ✓");
} else {
Serial.printf("MQTT err %d\n", mqtt.state());
delay(2000);
}
}
}
/* ---------------------------------------------------------------- */
void setup() {
pinMode(PIXEL_PWR, OUTPUT); digitalWrite(PIXEL_PWR, HIGH);
pinMode(AMP_EN , OUTPUT); digitalWrite(AMP_EN, LOW); // ★ 初期はミュート
FastLED.addLeds<WS2812, LED_PIN, GRB>(leds, 1);
FastLED.setBrightness(40);
Serial.begin(115200);
delay(1500);
Serial.println("Boot OK");
connectWiFi();
if (WiFi.status() != WL_CONNECTED) return; // Wi‑Fi失敗時は止める
connectAWS();
/* ---- I²S 設定 ---- */
audio.setPinout(5, 39, 38); // BCLK, LRCK, DATA
audio.setVolume(18);
audio.setBufsize(32*1024, 64*1024);
/*-------------------*/
}
void loop() {
mqtt.loop();
audio.loop();
}
制御コマンド形式
JSON形式でコマンドを送信すれば動くようになっています
// LED制御(テスト用)
{"cmd":"led","color":"blue"}
// 音声再生
{"cmd":"play","url":"http://XXXXXXX/audio.mp3"}
// 音声停止
{"cmd":"stop"}
音声ファイル配信システム
S3バケットの設定
音声ファイルをS3に配置し、静的ウェブサイトホスティングを有効にします。
こちらを参考にしました。
httpsにしたい場合は、cloud frontを利用すればできます。こちらが参考になります。
セキュリティ注意
本実装では簡易化のためバケットを公開していますが、本番環境ではCloudFrontや署名付きURLの利用を推奨します。
バケットポリシー例
{
"Version": "2012-10-17",
"Statement": [
{
"Sid": "PublicReadGetObject",
"Effect": "Allow",
"Principal": "*",
"Action": "s3:GetObject",
"Resource": "arn:aws:s3:::your-bucket/*"
}
]
}
Lambda制御システム
デバイス制御用のLambda関数を作成します
import json
import os
import boto3
IOT_ENDPOINT = os.environ["IOT_ENDPOINT"]
TOPIC = os.environ["IOT_TOPIC"]
client = boto3.client("iot-data", endpoint_url=f"https://{IOT_ENDPOINT}")
def lambda_handler(event, context):
cmd = event.get("cmd", "")
url = event.get("url")
color = event.get("color")
payload = {"cmd": cmd}
if url:
payload["url"] = url
if color:
payload["color"] = color
resp = client.publish(
topic=TOPIC,
qos=0,
payload=json.dumps(payload)
)
return {
"statusCode": 200,
"body": json.dumps({"published": payload})
}
環境変数設定:
-
IOT_ENDPOINT
: AWS IoTのエンドポイント -
IOT_TOPIC
:music/control
はAWS IoTと一致させてください
エンドポイントは、ドメイン設定
から取得できます!
動作確認と結果
テスト実行
Lambdaのテスト実行で次のJSONを送信する
{"cmd":"led","color":"red"}
{"cmd":"play","url":"http://XXXXXXXXXXXXXX/audio.mp3"}
{"cmd":"stop"}
無事に音が鳴ります!!!嬉しいですね!!
実現できたこと
- リモートでの音声制御
- 低遅延での音声再生(体感では遅延はほとんど感じられない)
- WiFi経由での安定した通信
- 小型・軽量で電池駆動可能
課題と今後の改善点
現在の課題
-
音質の問題
- イヤホンジャック経由でノイズが発生
- メインスピーカーとしては音質が不十分
-
セキュリティ
- S3バケットの公開設定
- 証明書・Wifiパスワードがハードコーディング
今後の改善案
-
音質向上
- イヤホンジャックから音声を出す
-
セキュリティ強化
- CloudFront + Signed URLの利用
- 証明書の外部保存
- デバイス認証の強化
-
機能拡張
- 複数スピーカーを同時にコントロール
- 音量調整機能
まとめ
ATOM S3 LiteとAWS IoT Coreを組み合わせることで、低遅延でスケーラブルなリモートスピーカーシステムを構築できました。
本取り組みでできたこと・わかったこと
- 小型で軽量な電池駆動スピーカー
- AWS IoTによる柔軟な制御システム
- 複数デバイスへの拡張可能性
- イマーシブシアターなどの空間演出への応用可能性
メインスピーカーとしては音質面で課題がありますが、空間演出や小道具としては十分な性能を発揮します。小型スピーカーを部屋のあちこちに仕込むことで、正しい場所から正しく音を鳴らせることができます。適切な場所から音が鳴る(=音が聞こえる)ことで、より没入感のある体験を創出できそうです。
コスト面でのメリット
- ATOM S3 Lite + ATOM SPEAKER: 約3,000円程度
- AWS IoT Core: 少量使用なら月数円程度
- 市販のWiFiスピーカーと比較して大幅にコストダウン
音響技術でのメリット
- 1台のPCから同時に複数のスピーカーを制御可能になった
- PCからの配線が不要になった(不安定なBlueToothではなくWifiを利用できる)
- 小さいのでちょっとした隙間に仕込みやすい
今後は音質改善、スピーカー同時制御に取り組み、よりクオリティの高い音響システムを目指していきます。
余談
初めてIoT触りました。購入からドッキドキです。
そんな超初心者でも、GPT先生が手順をほぼ完璧に教えてくれたので作れました!感謝!!
みなさんもGPT先生に聞いて、やりたいことを挑戦してみてください!!!