ESP32 オーディオシグナルジェネレータ

無線機の調整用に特化したオーディオシグナルジェネレータの自作をみて私も作ろうとChatGPTにコードを作ってもらいました。ESP32+ILI9488でタッチパネル操作で頼んでみました。何度かやり取りをして最終的にこんな感じになりました。

操作は問題なく動いてくれますが一部表示周波数と出力周波数が一致しない事があり最終的に20Hz~20KHzで完全に動作はしていません。ChatGPTよりESP32 GPIO25の内蔵DACをそのまま使用し、2段RCローパスフィルターで波形を滑らかにする方法の提案がありためしてみようと思います。

/*
  ASG-10 ESP32 AUDIO SIGNAL GENERATOR
  STABLE AUDIO TASK VERSION

  Board   : ESP32 DevKit V1
  Display : ILI9488 480x320
  Touch   : XPT2046
  DAC     : Internal DAC GPIO25
  Output  : I2S DMA

  Arduino ESP32 Core 2.0.17
*/

#include <Arduino.h>
#include <SPI.h>
#include <TFT_eSPI.h>
#include <XPT2046_Touchscreen.h>
#include <driver/i2s.h>
#include <math.h>

// =================================================
// PIN SETTINGS
// =================================================

#define TOUCH_CS   5
#define TOUCH_IRQ  34
#define DAC_PIN    25

#define SCREEN_W   480
#define SCREEN_H   320

TFT_eSPI tft = TFT_eSPI();

XPT2046_Touchscreen touch(
  TOUCH_CS,
  TOUCH_IRQ
);

// =================================================
// I2S SETTINGS
// =================================================

// 従来のサンプリング設定を維持
#define SAMPLE_RATE 192000
#define BUFFER_SIZE 512

#define I2S_PORT I2S_NUM_0
#define DMA_COUNT 8

uint16_t audioBuffer[BUFFER_SIZE];

// =================================================
// DDS SETTINGS
// =================================================

#define SINE_TABLE_SIZE 2048

int16_t sineTable[SINE_TABLE_SIZE];

uint32_t phase1 = 0;
uint32_t phase2 = 0;
uint32_t envelopePhase = 0;

uint32_t phaseStep1 = 0;
uint32_t phaseStep2 = 0;

// 音声タスクとの共有保護
portMUX_TYPE audioMux = portMUX_INITIALIZER_UNLOCKED;

TaskHandle_t audioTaskHandle = NULL;

bool dacReady = false;

// =================================================
// GENERATOR SETTINGS
// =================================================

enum WaveType {
  SINE,
  SQUARE,
  TRIANGLE,
  TWO_TONE,
  TRAPEZOID
};

WaveType waveMode = SINE;

bool outputEnabled = false;

float frequency = 1000.0f;
float frequency2 = 1500.0f;

int amplitude = 100;

int frequencyStep = 100;

const float MIN_FREQ = 20.0f;
const float MAX_FREQ = 20000.0f;
const float MAX_TWO_TONE = 5000.0f;

// =================================================
// TOUCH CALIBRATION
// =================================================

#define TOUCH_X_MIN 200
#define TOUCH_X_MAX 3900
#define TOUCH_Y_MIN 200
#define TOUCH_Y_MAX 3900

// =================================================
// COLORS
// =================================================

#define BG       TFT_BLACK
#define PANEL    0x10A2
#define WHITE_C  TFT_WHITE
#define CYAN_C   TFT_CYAN
#define YELLOW_C TFT_YELLOW
#define GREEN_C  TFT_GREEN
#define RED_C    TFT_RED
#define GRAY_C   0x8410
#define BLUE_C   TFT_BLUE

// =================================================
// SINE TABLE
// =================================================

void makeSineTable() {

  for (int i = 0; i < SINE_TABLE_SIZE; i++) {

    float a =
      2.0f * PI * (float)i /
      (float)SINE_TABLE_SIZE;

    sineTable[i] =
      (int16_t)(32767.0f * sinf(a));
  }
}

// =================================================
// FREQUENCY SETTING
// =================================================

// 以前の1/2補正を維持
// 1kHzで実測999.994~1000.005Hzを確認した設定

uint32_t frequencyToStep(float f) {

  return (uint32_t)(
    ((double)f * 4294967296.0) /
    ((double)SAMPLE_RATE * 2.0)
  );
}

void updateFrequency() {

  uint32_t newStep1;
  uint32_t newStep2;

  if (waveMode == TWO_TONE) {

    newStep1 = frequencyToStep(700.0f);

    newStep2 = frequencyToStep(frequency);

  }
  else {

    newStep1 = frequencyToStep(frequency);

    newStep2 = frequencyToStep(frequency2);

  }

  portENTER_CRITICAL(&audioMux);

  phaseStep1 = newStep1;
  phaseStep2 = newStep2;

  portEXIT_CRITICAL(&audioMux);
}

// =================================================
// I2S DAC INITIALIZATION
// =================================================

bool setupDAC() {

  i2s_config_t config = {};

  config.mode =
    (i2s_mode_t)(
      I2S_MODE_MASTER |
      I2S_MODE_TX |
      I2S_MODE_DAC_BUILT_IN
    );

  config.sample_rate = SAMPLE_RATE;

  config.bits_per_sample =
    I2S_BITS_PER_SAMPLE_16BIT;

  config.channel_format =
    I2S_CHANNEL_FMT_ONLY_RIGHT;

  config.communication_format =
    I2S_COMM_FORMAT_STAND_I2S;

  config.intr_alloc_flags = 0;

  config.dma_buf_count = DMA_COUNT;

  config.dma_buf_len = BUFFER_SIZE;

  config.use_apll = false;

  config.tx_desc_auto_clear = true;

  esp_err_t result = i2s_driver_install(
    I2S_PORT,
    &config,
    0,
    NULL
  );

  if (result != ESP_OK) {

    Serial.printf(
      "I2S INSTALL ERROR: %d\n",
      result
    );

    return false;
  }

  result = i2s_set_dac_mode(
    I2S_DAC_CHANNEL_RIGHT_EN
  );

  if (result != ESP_OK) {

    Serial.printf(
      "DAC MODE ERROR: %d\n",
      result
    );

    i2s_driver_uninstall(I2S_PORT);

    return false;
  }

  i2s_zero_dma_buffer(I2S_PORT);

  updateFrequency();

  Serial.println("INTERNAL DAC READY");

  return true;
}

// =================================================
// DDS WAVE GENERATION
// =================================================

int32_t getSine(uint32_t phase) {

  uint16_t index =
    phase >> 21;

  return sineTable[index];
}

// =================================================
// TRIANGLE
// =================================================

int32_t getTriangle(uint32_t phase) {

  uint32_t p = phase >> 16;

  if (p < 32768) {

    return (int32_t)(p * 2) - 32768;
  }

  return 98303 - (int32_t)(p * 2);
}

// =================================================
// WAVE SAMPLE GENERATION
// =================================================

int32_t getWaveSample(
  WaveType mode,
  uint32_t step1,
  uint32_t step2
) {

  int32_t value = 0;

  switch (mode) {

    // -----------------------------
    // SINE
    // -----------------------------

    case SINE:

      value = getSine(phase1);

      break;

    // -----------------------------
    // SQUARE
    // -----------------------------

    case SQUARE:

      value =
        (phase1 & 0x80000000UL)
        ? 32767 : -32767;

      break;

    // -----------------------------
    // TRIANGLE
    // -----------------------------

    case TRIANGLE:

      value = getTriangle(phase1);

      break;

    // -----------------------------
    // TWO TONE
    // -----------------------------

    case TWO_TONE:

      value = (
        getSine(phase1) +
        getSine(phase2)
      ) / 2;

      break;

    // -----------------------------
    // TRAPEZOID
    // -----------------------------

    case TRAPEZOID: {

      uint32_t envelopeStep = 44739UL;

      envelopePhase += envelopeStep;

      uint32_t envelope =
        envelopePhase >> 16;

      value =
        (int32_t)(
          ((int64_t)getSine(phase1) *
           envelope) / 65535
        );

      break;
    }
  }

  // DDS phase update
  phase1 += step1;
  phase2 += step2;

  return value;
}

// =================================================
// AUDIO TASK
// =================================================

// 音声処理を専用タスクで実行
// TFT描画・タッチ処理から独立

void audioTask(void *parameter) {

  Serial.println("AUDIO TASK START");

  while (true) {

    // -----------------------------
    // 設定値を安全に取得
    // -----------------------------

    uint32_t localStep1;
    uint32_t localStep2;

    int localAmplitude;

    bool localOutput;

    WaveType localMode;

    portENTER_CRITICAL(&audioMux);

    localStep1 = phaseStep1;
    localStep2 = phaseStep2;

    localAmplitude = amplitude;

    localOutput = outputEnabled;

    localMode = waveMode;

    portEXIT_CRITICAL(&audioMux);

    // -----------------------------
    // Generate audio buffer
    // -----------------------------

    for (int i = 0; i < BUFFER_SIZE; i++) {

      int32_t sample = 0;

      if (localOutput) {

        sample = getWaveSample(
          localMode,
          localStep1,
          localStep2
        );

        sample =
          (int32_t)(
            (int64_t)sample *
            localAmplitude / 127
          );

      }
      else {

        // 出力停止中も位相を進める
        phase1 += localStep1;
        phase2 += localStep2;

      }

      // -----------------------------
      // Signed 16bit to 8bit DAC
      // -----------------------------

      int32_t dacValue =
        128 + sample / 256;

      dacValue = constrain(
        dacValue,
        0,
        255
      );

      audioBuffer[i] =
        (uint16_t)(dacValue << 8);
    }

    // -----------------------------
    // I2S DMA output
    // -----------------------------

    size_t bytesWritten = 0;

    esp_err_t result = i2s_write(
      I2S_PORT,
      audioBuffer,
      sizeof(audioBuffer),
      &bytesWritten,
      portMAX_DELAY
    );

    if (result != ESP_OK) {

      Serial.println("I2S WRITE ERROR");

      vTaskDelay(1);
    }
  }
}

// =================================================
// WAVEFORM PREVIEW
// =================================================

void drawWaveformPreview() {

  const int x0 = 248;
  const int y0 = 70;
  const int w = 208;
  const int h = 48;

  const int centerY = y0 + h / 2;

  tft.fillRect(
    x0, y0, w, h, TFT_BLACK
  );

  tft.drawFastHLine(
    x0, centerY, w, TFT_DARKGREY
  );

  tft.drawRect(
    x0 - 2,
    y0 - 2,
    w + 4,
    h + 4,
    TFT_WHITE
  );

  int previousX = x0;
  int previousY = centerY;

  for (int i = 0; i < w; i++) {

    float t = (float)i / (float)w;

    float value = 0.0f;

    switch (waveMode) {

      case SINE:

        value =
          sinf(2.0f * PI * 2.0f * t);

        break;

      case SQUARE:

        value =
          (fmodf(t * 2.0f, 1.0f) < 0.5f)
          ? 1.0f : -1.0f;

        break;

      case TRIANGLE: {

        float p = fmodf(t * 2.0f, 1.0f);

        if (p < 0.5f) {

          value = -1.0f + p * 4.0f;

        }
        else {

          value = 3.0f - p * 4.0f;

        }

        break;
      }

      case TWO_TONE: {

        float time = t * 0.004f;

        value = (
          sinf(2.0f * PI * frequency * time) +
          sinf(2.0f * PI * 700.0f * time)
        ) / 2.0f;

        break;
      }

      case TRAPEZOID: {

        float p = fmodf(t * 2.0f, 1.0f);

        if (p < 0.2f) {

          value = -1.0f + p * 5.0f;

        }
        else if (p < 0.7f) {

          value = 0.0f;

        }
        else {

          value = (p - 0.7f) * 3.333f - 1.0f;

        }

        break;
      }
    }

    int currentX = x0 + i;

    int currentY =
      centerY - (int)(value * (h / 2 - 3));

    currentY = constrain(
      currentY,
      y0 + 2,
      y0 + h - 2
    );

    if (i > 0) {

      tft.drawLine(
        previousX,
        previousY,
        currentX,
        currentY,
        TFT_CYAN
      );

    }

    previousX = currentX;
    previousY = currentY;
  }
}

// =================================================
// DISPLAY HEADER
// =================================================

void drawHeader() {

  tft.fillRect(
    0, 0, SCREEN_W, 42, PANEL
  );

  tft.setTextColor(
    WHITE_C, PANEL
  );

  tft.drawCentreString(
    "ASG-10  ESP32 AUDIO GENERATOR",
    240, 12, 2
  );
}

// =================================================
// FREQUENCY DISPLAY
// =================================================

void drawFrequency() {

  tft.fillRoundRect(
    15, 52, 215, 75, 8, BG
  );

  tft.drawRoundRect(
    15, 52, 215, 75, 8, CYAN_C
  );

  char buf[32];

  snprintf(
    buf,
    sizeof(buf),
    "%.1f",
    frequency
  );

  tft.setTextColor(
    YELLOW_C, BG
  );

  tft.drawCentreString(
    buf, 122, 57, 7
  );

  int cursorIndex = 0;

  if (frequencyStep == 10) {
    cursorIndex = 2;
  }
  else if (frequencyStep == 100) {
    cursorIndex = 1;
  }
  else {
    cursorIndex = 0;
  }

  int totalWidth = tft.textWidth(buf, 7);

  int startX = 122 - totalWidth / 2;

  int cursorX = startX +
    tft.textWidth(
      String(buf).substring(0, cursorIndex), 7
    );

  tft.fillRect(
    cursorX + 5,
    107,
    20,
    4,
    TFT_CYAN
  );

  tft.fillRoundRect(
    240, 52, 225, 75, 8, BG
  );

  tft.drawRoundRect(
    240, 52, 225, 75, 8, CYAN_C
  );

  tft.setTextColor(
    WHITE_C, BG
  );

  tft.drawString(
    "WAVEFORM",
    248, 55, 1
  );

  drawWaveformPreview();
}

// =================================================
// BUTTON DRAWING
// =================================================

void drawButton(
  int x,
  int y,
  int w,
  int h,
  const char *label,
  uint16_t color
) {

  tft.fillRoundRect(
    x, y, w, h, 7, color
  );

  tft.drawRoundRect(
    x, y, w, h, 7, WHITE_C
  );

  tft.setTextColor(
    WHITE_C, color
  );

  tft.drawCentreString(
    label,
    x + w / 2,
    y + h / 2 - 8,
    2
  );
}

// =================================================
// WAVEFORM BUTTONS
// =================================================

void drawWaveButtons() {

  const char *names[] = {
    "SINE",
    "SQUARE",
    "TRIANGLE",
    "2-TONE",
    "TRAPEZOID"
  };

  for (int i = 0; i < 5; i++) {

    uint16_t color =
      (waveMode == i)
      ? 0x03EF : PANEL;

    drawButton(
      10 + i * 94,
      145,
      88,
      42,
      names[i],
      color
    );
  }
}

// =================================================
// FREQUENCY CONTROL BUTTONS
// =================================================

void drawFrequencyButtons() {

  drawButton(
    15, 202, 90, 42,
    "- STEP", RED_C
  );

  drawButton(
    115, 202, 90, 42,
    "+ STEP", BLUE_C
  );

  drawButton(
    220, 202, 110, 42,
    "STEP", PANEL
  );

  drawButton(
    345, 202, 120, 42,
    "AMP +", PANEL
  );

  drawButton(
    15, 253, 90, 42,
    "AMP -", PANEL
  );

  drawButton(
    115, 253, 150, 42,
    "START / STOP",
    outputEnabled ? GREEN_C : RED_C
  );

  drawButton(
    280, 253, 185, 42,
    "RESET 1kHz", PANEL
  );
}

// =================================================
// STATUS DISPLAY
// =================================================

void drawStatus() {

  tft.fillRect(
    0, 300, 480, 20, BG
  );

  tft.setTextColor(
    outputEnabled ? GREEN_C : RED_C,
    BG
  );

  tft.drawString(
    outputEnabled ? "OUTPUT ON" : "OUTPUT OFF",
    10, 301, 2
  );

  tft.setTextColor(
    WHITE_C, BG
  );

  char buf[32];

  snprintf(
    buf,
    sizeof(buf),
    "LEVEL: %d/127",
    amplitude
  );

  tft.drawRightString(
    buf, 470, 301, 2
  );
}

// =================================================
// FULL SCREEN
// =================================================

void drawScreen() {

  tft.fillScreen(BG);

  drawHeader();

  drawFrequency();

  drawWaveButtons();

  drawFrequencyButtons();

  drawStatus();
}

// =================================================
// TOUCH READING
// =================================================

bool getTouchXY(int &x, int &y) {

  if (!touch.touched()) {

    return false;
  }

  TS_Point p = touch.getPoint();

  int rawX = constrain(
    p.x,
    TOUCH_X_MIN,
    TOUCH_X_MAX
  );

  int rawY = constrain(
    p.y,
    TOUCH_Y_MIN,
    TOUCH_Y_MAX
  );

  x = map(
    rawX,
    TOUCH_X_MIN,
    TOUCH_X_MAX,
    0,
    SCREEN_W - 1
  );

  y = map(
    rawY,
    TOUCH_Y_MIN,
    TOUCH_Y_MAX,
    0,
    SCREEN_H - 1
  );

  return true;
}

// =================================================
// TOUCH CONTROL
// =================================================

void processTouch() {

  static uint32_t lastTouch = 0;

  if (millis() - lastTouch < 180) {

    return;
  }

  int x, y;

  if (!getTouchXY(x, y)) {

    return;
  }

  lastTouch = millis();

  Serial.printf(
    "TOUCH X=%d Y=%d\n",
    x, y
  );

  // -----------------------------
  // Waveform selection
  // -----------------------------

  if (y >= 140 && y <= 195) {

    int index = x / 94;

    if (index >= 0 && index < 5) {

      portENTER_CRITICAL(&audioMux);

      waveMode = (WaveType)index;

      portEXIT_CRITICAL(&audioMux);

      updateFrequency();

      drawScreen();
    }

    return;
  }

  // -----------------------------
  // Frequency minus
  // -----------------------------

  if (y >= 200 && y <= 247 &&
      x >= 10 && x <= 110) {

    frequency -= frequencyStep;

    frequency = constrain(
      frequency,
      MIN_FREQ,
      MAX_FREQ
    );

    updateFrequency();

    drawScreen();

    return;
  }

  // -----------------------------
  // Frequency plus
  // -----------------------------

  if (y >= 200 && y <= 247 &&
      x >= 110 && x <= 210) {

    frequency += frequencyStep;

    frequency = constrain(
      frequency,
      MIN_FREQ,
      MAX_FREQ
    );

    updateFrequency();

    drawScreen();

    return;
  }

  // -----------------------------
  // Step selection
  // -----------------------------

  if (y >= 200 && y <= 247 &&
      x >= 215 && x <= 335) {

    if (frequencyStep == 10) {

      frequencyStep = 100;

    }
    else if (frequencyStep == 100) {

      frequencyStep = 1000;

    }
    else if (frequencyStep == 1000) {

      frequencyStep = 10000;

    }
    else {

      frequencyStep = 10;

    }

    drawScreen();

    return;
  }

  // -----------------------------
  // Amplitude plus
  // -----------------------------

  if (y >= 200 && y <= 247 &&
      x >= 340) {

    portENTER_CRITICAL(&audioMux);

    amplitude = min(
      amplitude + 5,
      127
    );

    portEXIT_CRITICAL(&audioMux);

    drawScreen();

    return;
  }

  // -----------------------------
  // Amplitude minus
  // -----------------------------

  if (y >= 250 && y <= 299 &&
      x <= 110) {

    portENTER_CRITICAL(&audioMux);

    amplitude = max(
      amplitude - 5,
      0
    );

    portEXIT_CRITICAL(&audioMux);

    drawScreen();

    return;
  }

  // -----------------------------
  // START / STOP
  // -----------------------------

  if (y >= 250 && y <= 299 &&
      x >= 110 && x <= 270) {

    portENTER_CRITICAL(&audioMux);

    outputEnabled = !outputEnabled;

    portEXIT_CRITICAL(&audioMux);

    drawScreen();

    return;
  }

  // -----------------------------
  // RESET
  // -----------------------------

  if (y >= 250 && y <= 299 &&
      x >= 275) {

    frequency = 1000.0f;

    updateFrequency();

    drawScreen();

    return;
  }
}

// =================================================
// SETUP
// =================================================

void setup() {

  Serial.begin(115200);

  delay(500);

  Serial.println();

  Serial.println("ASG-10 ESP32 START");

  // -----------------------------
  // Display
  // -----------------------------

  tft.init();

  tft.setRotation(3);

  tft.fillScreen(BG);

  // -----------------------------
  // Touch
  // -----------------------------

  SPI.begin(
    18, 19, 23, TOUCH_CS
  );

  touch.begin();

  touch.setRotation(1);

  // -----------------------------
  // Sine table
  // -----------------------------

  makeSineTable();

  // -----------------------------
  // DAC
  // -----------------------------

  dacReady = setupDAC();

  if (!dacReady) {

    Serial.println("DAC INITIALIZATION FAILED");

    tft.fillScreen(TFT_BLACK);

    tft.setTextColor(TFT_RED);

    tft.drawCentreString(
      "DAC ERROR",
      240, 150, 4
    );

    return;
  }

  // -----------------------------
  // Audio task
  // -----------------------------

  BaseType_t taskResult =
    xTaskCreatePinnedToCore(
      audioTask,
      "ASG_AUDIO",
      4096,
      NULL,
      3,
      &audioTaskHandle,
      0
    );

  if (taskResult != pdPASS) {

    Serial.println("AUDIO TASK CREATE ERROR");

    tft.fillScreen(TFT_BLACK);

    tft.setTextColor(TFT_RED);

    tft.drawCentreString(
      "AUDIO TASK ERROR",
      240, 150, 4
    );

    return;
  }

  // -----------------------------
  // UI
  // -----------------------------

  drawScreen();

  Serial.println("SYSTEM READY");
}

// =================================================
// MAIN LOOP
// =================================================

void loop() {

  if (!dacReady || audioTaskHandle == NULL) {

    delay(100);

    return;
  }

  // Touch control only
  processTouch();

  delay(1);
}

コメント

タイトルとURLをコピーしました