pico 音階発生器を一時間で作ってみた

カテゴリー: シングルボードコンピュータ  タグ:

旧友が「カラオケに行こう」という。そういえばカラオケなんて何年何十年ぶりだろう?。普段聴いている曲を試しに歌ってみた。歌うのも久しぶりである。すると・・・

うっ、歌いだしの音階がわからん!合わん!高音が出ん!

というわけで、目的曲の音階を発生するものをRaspi picoで製作し、少し練習することにした

方形波では能がないので、DDSでサイン波出力

picoのポートにPWMで出力。今どきはD級アンプ構成が主流だが、手元にはレトロなアンプICがいっぱいあるので、平滑してシングルエンドでスピーカー駆動にした

回路図。ブレッドボードを鍵盤がわりに

例によって手書きですが、ご勘弁を

Raspi picoと言いながら、ブレッドボード1枚に鍵盤入力を並べて片側に集中させたいので、使用したのは秋月のAE-RP2040ボードです。picoとはピン配置が違います。

アンプ周りはNJM386BDのDSの推奨回路をそのまま・・・

ソフトウエア

まいど、チャッピーに手伝ってもらいました。とはいっても全部やってもらったわけではありません

「auduino環境でGP0~GP11を鍵盤入力として、A3~E4までのメジャー音階をSIN波で」

「数百~1kB程度のSINテーブル作って、音階周波数になるように間引き出力して」みたいな指示は、詳細にしっかり出しております

#include <Arduino.h>
#include <hardware/pwm.h>
#include <hardware/timer.h>

// ====== スピーカー(差動PWM) ======
constexpr uint gpioA = 15;  // 今回はsingle endで使う
constexpr uint gpioB = 14;  // 使わない

constexpr uint32_t PWM_FREQ    = 250000;  // キャリア
constexpr uint32_t SAMPLE_RATE = 40000;   // サンプル更新

// ====== DDS ======
constexpr uint8_t  TABLE_BITS = 8;        // 256
constexpr uint16_t TABLE_SIZE = 1u << TABLE_BITS;

uint16_t sinTable[TABLE_SIZE];            // 0..255

volatile uint32_t phase = 0;
volatile uint32_t phaseStep = 0;
volatile uint8_t  volume = 180;           // 0..255

repeating_timer_t audioTimer;
uint sliceA, chanA, sliceB, chanB;

// ====== キー ======
constexpr int NUM_KEYS = 12;
const uint8_t keyPins[NUM_KEYS] = {0,1,2,3,4,5,6,7,8,9,10,11};

const float noteHz[NUM_KEYS] = {  // 音階周波数(出力でx2している)
  220.00f, // A3
  246.94f, // B3
  261.63f, // C4
  293.66f, // D4
  329.63f, // E4
  349.23f, // F4
  392.00f, // G4
  440.00f, // A4
  493.88f, // B4
  523.25f, // C5
  587.33f, // D5
  659.25f  // E5
};

static inline void pwm_write(uint slice, uint chan, uint16_t level){
  pwm_set_chan_level(slice, chan, level);
}

// ====== 音生成ISR(40kHz) ======
bool audioCallback(repeating_timer_t*) {
  if (phaseStep == 0) {
    // 無音:両方中点固定(差動なので実質0)
    pwm_write(sliceA, chanA, 128);
    pwm_write(sliceB, chanB, 128);
    return true;
  }

  phase += phaseStep;
  uint16_t idx = phase >> (32 - TABLE_BITS);
  int16_t s = (int16_t)sinTable[idx] - 128;     // -128..127
  int16_t scaled = (s * (int16_t)volume) >> 8;  // 音量
  int16_t out = scaled + 128;   // 0..255
  if(out < 0) out = 0;
  if(out > 255) out = 255;

  uint16_t a = (uint16_t)out;
  uint16_t b = 255 - a;         // 逆相

  pwm_write(sliceA, chanA, a);
  pwm_write(sliceB, chanB, b);
  return true;
}

void setNoteHzOrOff(float hz){
  if (hz <= 0.0f) {
    phaseStep = 0;
    return;
  }
  phaseStep = (uint32_t)((hz * 4294967296.0f) / (float)SAMPLE_RATE);
}

void setupPwmPin(uint gpio, uint &slice, uint &chan){
  gpio_set_function(gpio, GPIO_FUNC_PWM);
  slice = pwm_gpio_to_slice_num(gpio);
  chan  = pwm_gpio_to_channel(gpio);
  pwm_set_wrap(slice, 255);
  float clkdiv = 125000000.0f / (PWM_FREQ * 256.0f);
  pwm_set_clkdiv(slice, clkdiv);
  pwm_set_enabled(slice, true);
}

// ====== キー読み(単音:最初に見つけたキー) ======
int readKeyIndex() {
  for (int i = 0; i < NUM_KEYS; i++) {
    if (digitalRead(keyPins[i]) == LOW) return i; // 押下
  }
  return -1;
}

void setup() {
  for (uint32_t i = 0; i < TABLE_SIZE; i++) { // sinテーブル生成
    float x = 2.0f * PI * (float)i / (float)TABLE_SIZE;
    float v = sinf(x) * 0.5f + 0.5f;
    sinTable[i] = (uint16_t)(v * 255.0f + 0.5f);
  }
  for (int i = 0; i < NUM_KEYS; i++) {  // キー入力ポートイニシャル(内部プルアップ)
    pinMode(keyPins[i], INPUT_PULLUP);
  }
  setupPwmPin(gpioA, sliceA, chanA);
  setupPwmPin(gpioB, sliceB, chanB);
  add_repeating_timer_us(-(int)(1000000 / SAMPLE_RATE), audioCallback, nullptr, &audioTimer);
  setNoteHzOrOff(0); // 最初は無音(出力はHL固定ではなく50%duty)
}

void loop() {
  static int lastKey = -2;
  static uint32_t lastChangeMs = 0;

  int k = readKeyIndex();
  if (k != lastKey) {   // 簡易デバウンス:変化後5ms待って確定
    lastChangeMs = millis();
    lastKey = k;
  }
  if (millis() - lastChangeMs < 5) return;

  // 確定したキーで音を更新
  static int appliedKey = -1;
  if (k != appliedKey) {
    appliedKey = k;
    if (k < 0) {
      setNoteHzOrOff(0);            // 離したら止める
    } else {
      setNoteHzOrOff(noteHz[k]*2);  // 押してる間鳴る(SPの低音弱いのでx2で1オクターブ上げる)
    }
  }
}

なんと、頼んでないのに差動PWMでコーディングしてくれた

まあ、デジタルアンプが主流なので、これが本道でしょう。でも平滑してsingle endで使うことにします

コーディングはVScode、書き込みはpico probeで

Raspberry pi pico/pico2をVScode PlatformIOでC++開発①」の方法で書き込み・デバッグ。platformIO版なので冒頭に「#include <Arduino.h>」がついてます

ほとんど一発で動いたのでデバッグの機会はほとんどありませんでしたが

発案から音がでるまで、一時間で完成

部品がすべて手元にある、という強みはありますが、多少時間のかかるコーディングは、ほとんどチャッピーが出してくれて、ましてほとんど一発で動いたので、本当に1時間で完成してしまいました

保護テープ貼ったり、音階を記入したりで+10分ぐらい追加ですが・・・、そのあと画像撮ったりオシロ波形採取したり、この記事書いたり、のほうがよほど時間かかってますorz

ブレッドボードの穴を鍵盤がわりに・・・

スピーカーの低音特性が悪いので、1オクターブ上げてA3→A4にしました

写真のスピーカーは500Hz以下で急激に下がります。もっと低音が出るスピーカーはガタイが大きく重く手軽さに欠けるので、音階を1オクターブ上げて対処してます(ソースコードにコメントいれてます)

気になる波形は?

黄色:PWM出力ポート(GP15)、緑:アンプ入力

CRだけのフィルタですが、そこそこのSIN波でています

PWM周波数は実測270KHzです。D級駆動にするにはちょっと高すぎるかもしれません。まして普通のアナログアンプではとても追いつきませんね

チャッピーに相談中に!?

なんと!
ご希望であれば、ポリフォニックにしますか?

と聞いてきたもんだ。

うわぁ、picoでシンセ作れるじゃん!

はるか昔、ラ製で手作りシンセ記事に夢中になってやっとモノフォニック版アナログシンセを作った経験を持つオイラにとって、なんともすごい時代だ

そういえば、その時の鍵盤がまだ実家の押し入れに眠ってるなぁ・・・

お気軽にコメントをどうぞ。

日本語が含まれない投稿は無視されますのでご注意ください。(スパム対策)