2026年2月12日 星期四

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

 


Arduino:


  1
  2
  3
  4
  5
  6
  7
  8
  9
 10
 11
 12
 13
 14
 15
 16
 17
 18
 19
 20
 21
 22
 23
 24
 25
 26
 27
 28
 29
 30
 31
 32
 33
 34
 35
 36
 37
 38
 39
 40
 41
 42
 43
 44
 45
 46
 47
 48
 49
 50
 51
 52
 53
 54
 55
 56
 57
 58
 59
 60
 61
 62
 63
 64
 65
 66
 67
 68
 69
 70
 71
 72
 73
 74
 75
 76
 77
 78
 79
 80
 81
 82
 83
 84
 85
 86
 87
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
#include <SPI.h>
#include <Adafruit_GFX.h>
#include <Adafruit_ST7789.h>

// ===== 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();
        }
      }
    }
  }
}


沒有留言:

張貼留言