📋 專案概述
六個月的娃只有「連續低頻嘶聲」能睡:安全座椅能睡、推車能睡、浴廁排氣風扇下也能睡——唯獨床上不行,因為你家夜裡的安靜跟車裡差一個世界:冰箱壓縮機每隔 40 分鐘轟然啟動、樓上 22:30 的拖動椅子聲、你太太翻個身床架吱呀一聲——每一聲對大人是背景,對淺眠的嬰兒是開戰宣言。你下載白噪音 App:20 分鐘後跳出「升級解鎖完整雨聲」的訂閱窗(NT$990/年,年度方案比實體機還貴);手機放床頭,第三天充電線就被扯壞一條。買嬰兒房品牌聲機:NT$2,400,六段出廠取樣、音量只有四格、最關鍵的是它不懂淡出——定時到點「咔」一聲掐斷,四分鐘後你聽到了那個熟悉的、被嚇醒的哭腔。你半夜摸黑起來關機器的次數,比它替你省的睡眠計時器錢多得多。
市售商品:
- 嬰兒聲機品牌款:NT$2,000–3,500,出廠取樣循環(細聽有接縫爆點)、音量階梯粗、淡出功能是行銷照片裡才存在的東西。
- 手機 App:訂閱制收割中;且手機在嬰兒床邊=藍光+通知+「媽媽我要手機」三連擊。
- 風扇/空氣清淨機轉到最大:NT$0 但整年電費+濾芯+你先生的哀怨眼神;而且它们不會在你需要的時候剛好是那個音量。
本專案的哲學:噪音是最好合成的東西——一個好的聲景不需要任何取樣檔。五種聲景全部由演算法即時生成(跑 20 年也不會聽到「接縫」,因為它根本不會重複),加上一台真正懂睡覺的機器該有的修養:
- 五種程序化聲景:白噪(xorshift32)/粉噪(Paul Kellet 級聯濾波,聽感像遠處瀑布)/棕噪(漏積分器,低頻厚——蓋冰箱轟聲首選)/雨聲(棕噪底+泊松過程雨滴+隨機共振包絡,每滴都一樣又每滴都不一樣)/海浪(0.07Hz LFO 調幅棕噪+浪頭嘶聲)
- I2S + MAX98357A、環形 DMA 綁核供數:音頻填充任務釘在 core1、Wi-Fi 協議棧趕去 core0——i2s_channel_write 永遠準時,44.1kHz 無一幀遲到,無爆音
- 30 秒線性淡出睡眠定時:15/30/45/60 分鐘;到點不是「咔」,是把音量在 30 秒內融化掉——這是本機與 NT$2,400 商品的核心差異
- 觸控音量+靜音:S3 電容觸控 pad,輕觸加檔、滿格回零(半夜不用睜眼找按鍵);切聲源長觸自動淡入淡出,無瞬態爆音
- 夜間減亮:60 秒無操作 OLED 降到 8% 亮度——嬰兒房裡不該有任何發光體
- NTP 倒數+NAS 週報:面板顯示剩餘分鐘;每週一出「本週聲景小時數」報告——順便證明「雨聲 11 小時」不是你在下面聽 Podcast
💰 成本效益:全機 ~NT$585。App 訂閱一年 NT$990;嬰兒聲機最低款也是本機 4 倍價。而真正值錢的是「淡出」讓孩子在淺眠週期不被嚇醒的那 0.3 個睡回籠覺/週。
為什麼選 ESP32-S3?
| 需求 | S3 的優勢 |
|---|---|
| 音頻不能遲到 | 雙核:音頻 fill task 以 FreeRTOS 最高優先級綁核 1,Wi-Fi/BT 協議棧全部趕去核 0——單核的 C3/C6 在 Wi-Fi 掃描瞬間就是爆音預支 |
| 觸控互動 | S3 內建 14 通道電容觸控(GPIO1-14),音量 pad 直接長在外殼上,無外加觸控晶片 |
| 留了後路 | N16R8 的 PSRAM 養得起將來的 TinyML 哭鬧偵測自動開機、本地語音「音量小聲點」 |
| 靜音要真靜 | MAX98357A 的 SD 腳可由 GPIO 硬mute——淡出到零之後連功放的靜態嘶聲都收掉 |
🧱 硬體清單
| 項目 | 型號 | 數量 | 預估單價 | 備註 |
|---|---|---|---|---|
| 主控板 | ESP32-S3 DevKitC-1 N16R8 | 1 | ~230 元 | 雙核+觸控+PSRAM |
| 功放 | MAX98357A 3W I2S 模組 | 1 | ~60 元 | GAIN 腳懸空=6dB;接 GND=9dB(本機接 GND) |
| 喇叭 | 4Ω 3W 40mm 全頻 | 1 | ~40 元 | 短音圈軟懸邊——低頻噪音不糊 |
| 螢幕 | SSD1306 0.96" I2C | 1 | ~90 元 | 夜間 8% 亮度模式 |
| 電源 | 5V 2A 線性 adaptor | 1 | ~80 元 | 開關電源的高頻紋波在深夜=蟬鳴 |
| 列印材料 | PLA ~90g+吸音棉 | — | ~25 元 | 室內機,PLA 可 |
| 小件 | 防振矽膠腳×4、100µF/25V、開關帽 | — | ~60 元 | 100µF 併功放電源端 |
| 合計 | ~585 元 | 品牌嬰兒聲機的 24% |
🔌 接線圖
詳細接線
| ESP32-S3 腳位 | 連接 | 說明 |
|---|---|---|
| GPIO12 / 13 / 14 | MAX98357A BCLK / WS / DIN | I2S 標準輸出 44.1kHz・16bit・立體聲 |
| GPIO10 | MAX98357A SD | 靜音腳:淡出到零後硬 mute 功放底噪 |
| GPIO2 | 銅箔觸控 pad(殼內側) | S3 touch:輕觸音量+1 檔、滿格回零 |
| GPIO3 | 按鈕(對 GND) | 短按切聲源(淡入淡出)、長按循環定時 |
| GPIO8 / GPIO9 | OLED SDA / SCL | 夜間 8% 亮度 |
| 5V / GND | 線性 adaptor | 功放電源端併 100µF;地單點匯接 |
ESP32-S3 DevKitC-1 MAX98357A 喇叭
GPIO12(BCLK) ────────── BCLK
GPIO13(WS) ────────── WS
GPIO14(DOUT) ────────── DIN
GPIO10 ────────── SD(靜音)
5V(獨立線) ──┬───────── VIN(+100µF∥)
GND ─────────┴───────── GND / OUT± ────── 4Ω 3W
GPIO2 ←銅箔 pad(音量) GPIO3 ←按鈕→GND
GPIO8/9 ──── OLED SDA/SCL
⚠️ 功放電源不要從板子 3V3 拿:3W 滿輸出 時峰值近 0.6A,會把整個板子的地紋波抬高(表現為「音量越大越有嗡嗡聲」)。VIN 走獨立線回 adaptor,地單點匯接。
🖨️ 3D 列印設計
| 零件 | 材質/設定 | 數量 | 備註 |
|---|---|---|---|
| shell-top 頂蓋 | PLA/0.2mm/2 壁/15% | 1 | 蜂窩穿孔開孔率 32%——開孔率就是音量頻響曲線 |
| diffuser-dome 擴散穹頂 | PLA/0.2mm/1.2 壁/0%(實壁薄殼) | 1 | 喇叭正上方的半球:高頻不「指著你」而是散開 |
| shell-base 底座 | PLA/0.2mm/2.5 壁/25% | 1 | 四角矽膠腳槽+纜線出孔+OLED 12° 傾窗 |
| pad-plate 觸控壓片 | PLA/0.2mm/2 壁/20% | 1 | 銅箔倉,壓片厚度決定觸控靈敏度 |
| bottom-vent 底透音條 | PLA/0.24mm/2 壁/10% | 1 | 棕噪的低頻需要下方出音口才厚 |
列印總重 ~90g。殼內壁貼一半面積吸音棉(對角線分區:一半吸高頻毛刺、一半留反射)——「雨聲裡的那層沙沙」有沒有,就差這一步。銅箔觸控 pad 與外殼距離 ≤2.5mm,超了靈敏度不夠就貼一片鋁箔到殼外應急。
💻 Arduino 程式碼
Arduino IDE 2.x+espressif 3.x core(開發板 ESP32S3 Dev Module,PSRAM 選 OPI);I2S 直接用 core 內建 ESP-IDF i2s_std 驅動,函式庫僅 Adafruit SSD1306+GFX。
/* ============================================================
* 白噪音睡眠聲機 sleep-sound.ino
* ------------------------------------------------------------
* 功能:五種程序化聲景、觸控音量、睡眠定時 30s 淡出、
* 夜間減亮、NTP 時鐘、NAS 週報
* 硬體:ESP32-S3 DevKitC-1|I2S: 12=BCLK 13=WS 14=DOUT
* 10=功放 SD、2=觸控音量、3=按鈕、8/9=I2C OLED
* 供電:5V 線性 adaptor,功放電源獨立線+100µF
* 相依:Adafruit_SSD1306 / GFX(core 3.x,i2s_std API)
* ============================================================ */
#include <Wire.h>
#include <Adafruit_SSD1306.h>
#include <WiFi.h>
#include <HTTPClient.h>
#include "driver/i2s_std.h"
/* --- 0. 配置 --- */
const char* WIFI_SSID = "YOUR_SSID";
const char* WIFI_PASS = "YOUR_PASS";
const char* NAS_URL = "http://192.168.8.60:1880/sleep-sound";
const char* NAS_TOKEN = "change-me";
#define PIN_SD 10
#define PIN_TOUCH 2
#define PIN_BTN 3
#define SR 44100
#define FRAMES 512 // 每緩衝 512 訊框 ≈11.6ms,低延遲低佔用
enum Mode { WHITE, PINK, BROWN, RAIN, WAVE, MODE_N };
const char* modeName[MODE_N] = {"WHITE", "PINK ", "BROWN", "RAIN ", "WAVE "};
Mode mode = PINK;
uint8_t volume = 10; // 0..16 檔(≈2dB/檔)
uint32_t timerEnd = 0; // 定時結束 millis;0=未定時
const uint32_t FADE_MS = 30000; // 淡出 30 秒——本機的修養
float fade = 1.0f; // 全域淡入淡出包絡
bool touching = false;
uint32_t lastUser = 0; // 最近互動時刻(夜間減亮用)
/* --- 1. 噪聲生成器狀態 --- */
uint32_t xs = 0x9E3779B9; // xorshift 種子
float pk[7] = {0}; // Kellet 粉噪級聯
float brownLast = 0;
float dripEnv = 0, dripFreq = 0, dripNext = 0; // 雨滴
float wavePhase = 0;
static inline float whiteF() { // 白噪:xorshift32,比 rand 快一個數量級
xs ^= xs << 13; xs ^= xs >> 17; xs ^= xs << 5;
return (float)((int32_t)xs) / 2147483648.0f;
}
float pinkF() { // Paul Kellet 級聯近似 -3dB/oct
float w = whiteF();
pk[0] = 0.99886f*pk[0] + w*0.0555179f; pk[1] = 0.99332f*pk[1] + w*0.0750759f;
pk[2] = 0.96900f*pk[2] + w*0.1538520f; pk[3] = 0.86650f*pk[3] + w*0.3104856f;
pk[4] = 0.55000f*pk[4] + w*0.5329522f; pk[5] = -0.7616f*pk[5] - w*0.0168980f;
return (pk[0]+pk[1]+pk[2]+pk[3]+pk[4]+pk[5]+pk[6]+w*0.5362f) * 0.11f;
}
float brownF() { // 棕噪:漏積分器,-6dB/oct
brownLast = (brownLast + 0.02f * whiteF()) / 1.02f;
return brownLast * 3.5f;
}
/* --- 2. 聲景取樣器:每個聲源的「一個取樣」 ---
* 參數:t 取樣序号|回傳:單聲道 -1..1
* 為什麼即時合成:零取樣檔、永不重複循環、ROM 零佔用 */
float sampleAt(uint64_t t) {
switch (mode) {
case WHITE: return whiteF();
case PINK: return pinkF();
case BROWN: return brownF();
case RAIN: {
float s = brownF() * 0.45f; // 雨幕底
if (t > dripNext && dripEnv < 0.05f) { // 泊松到點:新的一滴
dripEnv = 0.5f + 0.5f * fabsf(whiteF());
dripFreq = 900 + 1800 * fabsf(whiteF());
dripNext = t + (uint64_t)(SR * (0.02f + 0.16f * fabsf(whiteF())));
}
dripEnv *= 0.9985f; // ~60ms 衰減
s += dripEnv * 0.5f * sinf(6.2832f * dripFreq * t / SR);
return constrain(s, -1, 1);
}
case WAVE: {
wavePhase += 6.2832f * 0.07f / SR; // 0.07Hz ≈ 每 14 秒一浪
float swell = 0.4f + 0.35f * sinf(wavePhase);
float s = brownF() * swell;
if (sinf(wavePhase) > 0.75f) s += pinkF() * 0.10f; // 浪頭嘶聲
return constrain(s, -1, 1);
}
default: return 0;
}
}
/* --- 3. 音頻填充任務:綁核 1、最高優先級 ---
* 參數:arg 未用|回傳:無
* 為什麼綁核:core0 是 Wi-Fi/BT 協議棧地盤;音頻與它們搶時間=爆音 */
i2s_chan_handle_t gI2s;
void audioTask(void*) {
static int16_t buf[FRAMES * 2];
uint64_t t0 = 0;
size_t w;
for (;;) {
float fadeNow = fade;
if (timerEnd && millis() > timerEnd) { // 定時到點:30s 線性淡出
fadeNow = constrain(1.0f - (millis() - timerEnd) / (float)FADE_MS, 0, 1);
if (fadeNow <= 0.001f) { digitalWrite(PIN_SD, LOW); fade = 0; } // 硬 mute
}
float g = (volume ? volume / 16.0f : 0.0f) * fadeNow * 26000.0f;
for (int i = 0; i < FRAMES; i++) {
float s = sampleAt(t0 + i);
int16_t v = (int16_t)(s * g);
buf[i*2] = v; buf[i*2+1] = v; // 單聲道複製成立體
}
t0 += FRAMES;
i2s_channel_write(gI2s, buf, sizeof buf, &w, portMAX_DELAY); // 寫滿即阻塞=天然節流
}
}
/* --- 4. I2S 初始化(ESP-IDF std 驅動,core 3.x) --- */
void i2sInit() {
i2s_chan_config_t cc = I2S_CHANNEL_DEFAULT_CONFIG(I2S_NUM_0, I2S_ROLE_MASTER);
cc.dma_desc_num = 6; cc.dma_frame_num = 256; // 6×256 深 DMA:Wi-Fi 干擾緩衝池
i2s_new_channel(&cc, &gI2s, NULL);
i2s_std_config_t sc = {
.clk_cfg = I2S_STD_CLK_DEFAULT_CONFIG(SR),
.slot_cfg = I2S_STD_PHILIPS_SLOT_DEFAULT_CONFIG(I2S_DATA_BIT_WIDTH_16BIT,
I2S_SLOT_MODE_STEREO),
.gpio_cfg = { .mclk = I2S_GPIO_UNUSED, .bclk = 12, .ws = 13,
.dout = 14, .din = I2S_GPIO_UNUSED },
};
i2s_channel_init_std_mode(gI2s, &sc);
i2s_channel_enable(gI2s);
}
/* --- 5. 觸控音量:輕觸+1 檔、滿格回零;靜音腳跟隨 --- */
void onTouch() {
static uint32_t last = 0;
if (millis() - last < 400) return; // 消抖
last = millis();
lastUser = millis(); // 任何互動都喚醒面板
volume = (volume + 1) > 16 ? 0 : volume + 1;
if (volume > 0 && timerEnd == 0) digitalWrite(PIN_SD, HIGH);
}
/* --- 6. 按鈕:短按切聲源(淡出→換→淡入);長按循環定時 --- */
uint32_t btnHold = 0, btnLast = HIGH;
void buttonStep(uint32_t m) {
uint8_t b = digitalRead(PIN_BTN);
static uint32_t tBtn = 0;
if (b == LOW && btnLast == HIGH) { btnHold = m; }
if (b == HIGH && btnLast == LOW) {
if (m - btnHold < 450) { // 短按:聲源淡切
fade = 0; mode = (Mode)((mode + 1) % MODE_N);
timerEnd = 0; digitalWrite(PIN_SD, HIGH);
for (uint8_t i = 0; i <= 20; i++) { fade = i / 20.0f; delay(12); } // 240ms 淡入
} else if (m - btnHold > 800) { // 長按:15/30/45/60/關
static const uint32_t mins[] = {15, 30, 45, 60, 0};
static uint8_t k = 0; k = (k + 1) % 5;
timerEnd = mins[k] ? m + mins[k] * 60000UL : 0;
if (timerEnd) { fade = 1; digitalWrite(PIN_SD, HIGH); }
}
}
btnLast = b;
}
/* --- 7. 面板與夜間減亮 --- */
Adafruit_SSD1306 oled(128, 64, &Wire, -1);
void drawPanel(uint32_t m) {
bool dim = (m - lastUser > 60000);
oled.clearDisplay(); oled.setTextSize(1); oled.setCursor(0, 0);
oled.printf("%s vol%2u %s\n", modeName[mode], volume,
WiFi.status() == WL_CONNECTED ? "net" : "wifi..");
if (timerEnd && m < timerEnd) oled.printf("timer %2lum%02lus\n",
(timerEnd - m) / 60000UL, ((timerEnd - m) / 1000UL) % 60);
else if (timerEnd) oled.printf("fading...\n");
else oled.printf("no timer\n");
struct tm ti; time_t now; time(&now); localtime_r(&now, &ti);
oled.printf("%02d:%02d\n", ti.tm_hour, ti.tm_min);
oled.display();
/* 夜間減亮:setContrast 128→16(約 8% 亮度)——嬰兒房裡不該有燈泡 */
static bool dimSet = false;
if (dim != dimSet) { dimSet = dim; oled.setContrast(dim ? 16 : 128); }
}
/* --- 8. NAS 週報:每週一 07:00 上報本週聲景分佈 --- */
float hrs[MODE_N] = {0}; uint32_t hAcc = 0;
void pushWeekly() {
if (WiFi.status() != WL_CONNECTED) return;
HTTPClient h; h.begin(NAS_URL);
h.addHeader("Content-Type", "application/json");
h.addHeader("X-Device-Token", NAS_TOKEN);
String b = "{";
const char q = char(34);
for (int i = 0; i < MODE_N; i++) {
b += q; b += modeName[i]; b += q; b += ":"; // key 用聲源名,NAS 端免維護索引表
b += String(hrs[i], 1); b += (i < MODE_N - 1) ? "," : "";
}
String body = "{"; body += q; body += "uptimeH"; body += q;
body += ":" + String(millis()/3600000UL) + ",";
body += q; body += "hrs"; body += q; body += ":" + b + "}}";
h.POST(body);
h.end();
for (int i = 0; i < MODE_N; i++) hrs[i] = 0;
}
/* --- 9. setup --- */
void setup() {
Serial.begin(115200);
pinMode(PIN_SD, OUTPUT); digitalWrite(PIN_SD, LOW);
pinMode(PIN_BTN, INPUT_PULLUP);
Wire.begin(8, 9); oled.begin(SSD1306_SWITCHCAPVCC, 0x3C);
oled.setTextColor(SSD1306_WHITE);
touchAttachInterrupt(PIN_TOUCH, onTouch, 40); // 殼貼式:門檻親調
i2sInit();
xTaskCreatePinnedToCore(audioTask, "audio", 4096, NULL, 24, NULL, 1);
digitalWrite(PIN_SD, HIGH);
WiFi.begin(WIFI_SSID, WIFI_PASS);
configTime(8 * 3600, 0, "pool.ntp.org");
lastUser = millis();
}
/* --- 10. loop:UI 拍 50ms/小時統計/週日計 --- */
void loop() {
static uint32_t t50 = 0, tHour = 0, tMon = 0;
uint32_t m = millis();
buttonStep(m);
if (m - t50 >= 500) { t50 = m; drawPanel(m); }
if (m - tHour >= 3600000UL) { tHour = m; hrs[mode] += (fade > 0.01 && volume) ? 1 : 0; }
if (m - tMon >= 7UL * 24 * 3600 * 1000) { tMon = m; pushWeekly(); }
}
⚠️ 兩個坑:①
touchAttachInterrupt 門檻(本機 40)受外殼材質與濕手影響大,上殼後一定要重調——靈敏過頭會自己加音量(枕頭誤觸)。②setContrast 若你的 Adafruit_SSD1306 版本沒有,改發 0x81 對比度指令(註解標明處);先確認能編譯再上機。📦 NAS 端(Node-RED):每週一 07:05 收週報 → Telegram「本週聲景:粉噪 21h、雨聲 14h、海浪 2h——比上週少哭鬧時段 3 小時」;另可加規則:週報缺席(設備離線 26 小時以上)→「聲機是不是被你老婆收進抽屜了」。
🏗️ 組裝步驟
📘 圖文版組裝說明書:每一步附示意圖、預估時間與難度標記,可直接列印 A4 帶到工作檯 → 開啟 HTML 組裝說明書
- 列印後處理:頂蓋蜂窩孔逐孔通針(支撐殘渣是殺手)、殼內一半面積貼吸音棉(對角分區)
- 功放前測:MAX98357A 先裸測——100µF 焊在模組電源端,GAIN 接 GND(9dB),SD 接 GPIO10 驗證靜音
- 喇叭減震:喇叭法蘭墊 2mm 泡棉圈鎖固——硬鎖底座的殼會跟共振
- 觸控 pad 製作:40×25mm 銅箔膠帶貼 pad-plate 內側、飛線 GPIO2,膠帶全包邊防潮
- 地線單點匯接:功放 GND 與板 GND 各走獨立線到 adaptor 地端子(星形),拒走板對板跳線
- 燒錄+爆音掃描: Serial 開著跑 10 分鐘,Wi-Fi 掃描時段(重連瞬間)側耳聽有無 tick——有則加深 DMA(dma_desc_num 6→8)
- 觸控門檻調校:上蓋狀態跑 touch 原始值觀察(函式庫附範例),閒置<15、隔殼觸>60,門檻設中間
- 淡出聽感驗收:定時 15 分鐘縮時版(改 FADE_MS 測)——淡出全程應聽不出「結束點」;聽得出=淡出太短,誠實加長
🔧 問題排除
| 問題 | 原因 | 解決方法 |
|---|---|---|
| 完全無聲 | SD 腳沒拉高(功放默認靜音)或 I2S 腳位錯 | SD 必須 HIGH 才有輸出——這是新手 90% 的「無聲」;再查 12/13/14 三線序 |
| 間歇 tick/pop 爆音 | 音頻任務被 Wi-Fi 搶時間或 DMA 太淺 | 確認任務已綁核 1 且優先級 24;dma_desc_num 6→8;仍發作就把 OLED 刷新降頻(I2C 中斷也搶) |
| 音量越大越嗡嗡 | 功放電源走了板端 3V3/地線走線繞路 | VIN 獨立線回 adaptor、地單點匯接、100µF 就近——三件套缺一不可 |
| 自己會加音量 | 觸控門檻太靈/線纜耦合 | 門檻調高、pad 飛線包地、殼旁別壓充電線;枕頭邊的機器加防誤觸(連觸兩次生效) |
| 棕噪聽起來沒低頻 | 全封閉殼+頂蓋孔太小,低頻悶死 | 加底透音條(本設計留了槽);低頻要「漏」,高頻才要「散」 |
| 淡出後有輕微嘶聲 | 功放淡出到 0 仍在線 | fade 歸零時 digitalWrite(PIN_SD, LOW) 硬 mute——本韌體已做,別註解掉 |
| NTP 時間差 8 小時 | configTime 時區參數錯 | 台北:configTime(8*3600, 0, "pool.ntp.org");倒數計時走 millis 不受影響,只有面板時鐘會錯 |
🎯 進階延伸
- 哭鬧偵測開機:INMP441+8k 採樣+1D-CNN(TinyML)偵測哭聲自動淡入指定聲源——PSRAM 就是為這一步留的
- 雙機星狀:嬰兒房+主卧各一台,NAS 同步聲源與音量——車上補聽用同一份週報
- 日落自動場景:NTP 日落時刻自動從「白天雨聲」切「夜間粉噪+音量減兩檔」;早上 6:50 淡出叫醒
- 本地語音:S3+離線喚醒(esp-sr)——「小白,小聲一點」,不經任何雲端
📦 檔案清單
sleep-sound/
├── src/
│ └── sleep-sound.ino # ESP32 韌體(本頁完整程式)
├── nas/
│ └── node-red-flow.json # 週報接收/離線告警
├── stl/
│ ├── shell-top.stl # 蜂窩穿孔頂蓋
│ ├── diffuser-dome.stl # 擴散穹頂
│ ├── shell-base.stl # 減震底座
│ ├── pad-plate.stl # 觸控壓片
│ └── bottom-vent.stl # 底透音條
├── f3d/
│ └── sleepsound-design.f3d # Fusion 360 原始檔
├── test/
│ └── touch-cal.ino # 觸控原始值觀察
└── manual/
└── assembly.html # 圖文組裝說明書