2026年2月12日 星期四

[OTTO GO] 喇叭測試-老虎張嘴出聲

 


Arduino:




#include #include #include // ===== TFT pins (OTTO GO) ===== #define TFT_MOSI 19 #define TFT_SCLK 18 #define TFT_CS 5 #define TFT_DC 16 #define TFT_RST 23 // ===== Button (OTTO GO) ===== #define KEY1_PIN 35 // 使用者按鈕 P35 // ===== I2S AMP pins (OTTO GO) ===== #include "driver/i2s.h" #define I2S_BCLK 2 // I2S BCLK #define I2S_LRCK 17 // I2S LRCK/WS #define I2S_DOUT 32 // 圖上寫 DIN:這裡是送到功放的資料輸出 Adafruit_ST7789 tft = Adafruit_ST7789(TFT_CS, TFT_DC, TFT_RST); static inline uint16_t RGB565(uint8_t r, uint8_t g, uint8_t b) { return ((r & 0xF8) << 8) | ((g & 0xFC) << 3) | (b >> 3); } // ================== 虎嘴繪圖 ================== void drawMuzzleBase(int cx, int cy) { uint16_t furLight = RGB565(235, 228, 210); uint16_t shadow = RGB565(160, 150, 135); uint16_t dotDark = RGB565(70, 70, 70); tft.fillScreen(ST77XX_BLACK); tft.fillCircle(cx - 55, cy + 10, 55, furLight); tft.fillCircle(cx + 55, cy + 10, 55, furLight); tft.fillRoundRect(cx - 90, cy - 25, 180, 110, 28, furLight); tft.fillRoundRect(cx - 85, cy + 60, 170, 22, 12, shadow); tft.fillRoundRect(cx - 40, cy + 55, 80, 10, 5, RGB565(120,110,100)); int dotY1 = cy + 6; int dotY2 = cy + 26; for (int k = 0; k < 3; k++) { tft.fillCircle(cx - 55 - k * 12, dotY1 + k * 2, 2, dotDark); tft.fillCircle(cx - 55 - k * 12, dotY2 + k * 2, 2, dotDark); tft.fillCircle(cx + 55 + k * 12, dotY1 + k * 2, 2, dotDark); tft.fillCircle(cx + 55 + k * 12, dotY2 + k * 2, 2, dotDark); } } void drawMouthClosed(int cx, int cy) { uint16_t lineDark = RGB565(40, 40, 40); tft.drawLine(cx, cy - 5, cx, cy + 20, lineDark); tft.drawLine(cx - 1, cy - 5, cx - 1, cy + 20, RGB565(80,80,80)); int mouthY = cy + 18; for (int i = 0; i < 34; i++) { int y = mouthY + (i * i) / 95; tft.drawPixel(cx - i, y, lineDark); tft.drawPixel(cx - i, y + 1, lineDark); tft.drawPixel(cx + i, y, lineDark); tft.drawPixel(cx + i, y + 1, lineDark); } tft.fillCircle(cx - 36, mouthY + 12, 3, lineDark); tft.fillCircle(cx + 36, mouthY + 12, 3, lineDark); tft.fillRoundRect(cx - 10, mouthY + 20, 20, 4, 2, RGB565(90,80,70)); } void drawMouthOpen(int cx, int cy) { uint16_t lineDark = RGB565(40, 40, 40); uint16_t mouthIn = RGB565(15, 15, 15); uint16_t tongue = RGB565(210, 120, 120); tft.drawLine(cx, cy - 5, cx, cy + 10, lineDark); tft.drawLine(cx - 1, cy - 5, cx - 1, cy + 10, RGB565(80,80,80)); int mouthY = cy + 16; for (int i = 0; i < 36; i++) { int y = mouthY + (i * i) / 85; tft.drawPixel(cx - i, y, lineDark); tft.drawPixel(cx - i, y + 1, lineDark); tft.drawPixel(cx + i, y, lineDark); tft.drawPixel(cx + i, y + 1, lineDark); } tft.fillCircle(cx - 38, mouthY + 14, 3, lineDark); tft.fillCircle(cx + 38, mouthY + 14, 3, lineDark); int w = 90, h = 45; int x = cx - w / 2; int y0 = cy + 28; tft.fillRoundRect(x, y0, w, h, 18, mouthIn); tft.fillCircle(cx, y0 + h, 22, mouthIn); tft.fillRoundRect(cx - 22, y0 + 18, 44, 20, 10, tongue); tft.fillCircle(cx, y0 + 36, 14, tongue); } void drawTigerMouth(bool open) { int cx = tft.width() / 2; int cy = tft.height() / 2; drawMuzzleBase(cx, cy); if (open) drawMouthOpen(cx, cy); else drawMouthClosed(cx, cy); } // ================== I2S 老虎吼叫(合成音) ================== // 簡單偽隨機(噪聲用) static uint32_t rng = 1; static inline int16_t noise16() { rng = rng * 1664525UL + 1013904223UL; return (int16_t)(rng >> 16); // -32768..32767 } void i2sInit() { const i2s_port_t I2S_PORT = I2S_NUM_0; i2s_config_t cfg = { .mode = (i2s_mode_t)(I2S_MODE_MASTER | I2S_MODE_TX), .sample_rate = 22050, .bits_per_sample = I2S_BITS_PER_SAMPLE_16BIT, .channel_format = I2S_CHANNEL_FMT_ONLY_LEFT, // 單聲道 .communication_format = I2S_COMM_FORMAT_I2S_MSB, .intr_alloc_flags = ESP_INTR_FLAG_LEVEL1, .dma_buf_count = 6, .dma_buf_len = 256, .use_apll = false, .tx_desc_auto_clear = true, .fixed_mclk = 0 }; i2s_pin_config_t pin_cfg = { .bck_io_num = I2S_BCLK, .ws_io_num = I2S_LRCK, .data_out_num = I2S_DOUT, .data_in_num = I2S_PIN_NO_CHANGE }; i2s_driver_install(I2S_PORT, &cfg, 0, NULL); i2s_set_pin(I2S_PORT, &pin_cfg); i2s_zero_dma_buffer(I2S_PORT); } void playTigerRoarOnce() { const i2s_port_t I2S_PORT = I2S_NUM_0; // 0.7 秒左右的咆哮 const int sampleRate = 22050; const float durSec = 0.7f; const int total = (int)(sampleRate * durSec); // 每次寫一小段 const int N = 256; int16_t buf[N]; // 低頻咆哮(80~120Hz 變化) + 噪聲(牙擦感) + 包絡(先強後弱) float phase = 0.0f; for (int i = 0; i < total; ) { int n = (total - i > N) ? N : (total - i); for (int k = 0; k < n; k++) { float t = (float)(i + k) / (float)total; // 0..1 // 頻率滑動:更像吼叫 float f = 120.0f - 50.0f * t; // 120 -> 70 Hz phase += 2.0f * 3.1415926f * f / (float)sampleRate; if (phase > 2.0f * 3.1415926f) phase -= 2.0f * 3.1415926f; // 包絡:快速起音 + 慢慢衰減 float env = (t < 0.08f) ? (t / 0.08f) : (1.0f - (t - 0.08f) / 0.92f); if (env < 0) env = 0; // 主音 + 低頻加粗 + 噪聲 float s = sinf(phase) * 0.65f + sinf(phase * 0.5f) * 0.35f; float nz = (noise16() / 32768.0f) * 0.35f; float out = (s + nz) * env; // 音量(可調):0.0~1.0 float gain = 0.9f; int32_t v = (int32_t)(out * gain * 30000.0f); if (v > 32767) v = 32767; if (v < -32768) v = -32768; buf[k] = (int16_t)v; } size_t written = 0; i2s_write(I2S_PORT, (const char*)buf, n * sizeof(int16_t), &written, portMAX_DELAY); i += n; } } // ================== 按鍵去彈跳 + 事件觸發 ================== bool mouthOpen = false; // 若你按鍵按下讀到 LOW,維持 true;若相反就改成 false const bool ACTIVE_LOW = true; int lastStable = HIGH; int lastRead = HIGH; unsigned long lastDebounceMs = 0; const unsigned long DEBOUNCE_MS = 30; static inline bool isPressedLevel(int level) { return ACTIVE_LOW ? (level == LOW) : (level == HIGH); } void setup() { // TFT SPI.begin(TFT_SCLK, -1, TFT_MOSI, TFT_CS); tft.init(240, 320); // 若你確定是 170x320,改成 tft.init(170, 320) tft.setRotation(1); // Button pinMode(KEY1_PIN, INPUT); // P35 無內建上拉下拉 // I2S i2sInit(); // 初始畫面:嘴巴閉合 drawTigerMouth(mouthOpen); } void loop() { int reading = digitalRead(KEY1_PIN); if (reading != lastRead) { lastDebounceMs = millis(); lastRead = reading; } if (millis() - lastDebounceMs > DEBOUNCE_MS) { if (reading != lastStable) { lastStable = reading; if (isPressedLevel(lastStable)) { bool prev = mouthOpen; mouthOpen = !mouthOpen; drawTigerMouth(mouthOpen); // 只有「切到張開」時才吼叫一次 if (!prev && mouthOpen) { playTigerRoarOnce(); } } } } }

沒有留言:

張貼留言