音楽を演奏したい - OC1A編 [Arduino]
digitalWrite()編では、ピンの位置だけ変更すれば、Arduino IDEを使ったArduinoの派生品ほぼすべてで移植可能ではないかと思います。
しかし、1音の音量が一定というか、音のエンベロープが長方形で、減衰しないのが難点。
そこで、ATmega328p, ATmega32u4の2つのAVRマイコンに依存した方法で音を出してみます。
私だけの電子オルゴール ♪ MyMelo2 ♪
http://siva.cc.hirosaki-u.ac.jp/usr/koyama/mymelo/
大いに参考というか、ISR内の考え方はそのままいただきました。
D/Aコンバータ(DAC)がないので、超高速なPWMで疑似的に作り出しています。
16MHz / 512clock = 31.250 kHz なので人間にはわからない速さです。
OCR1A, OCR1Bに0~511の範囲で値をいれるDACにします。
ISR内のプログラムは、
・基準の音符(96分音符)ごとに楽譜の評価
・矩形波の作成、減衰処理、出力
の2つの役割があります。
512clock内にとりあえず収まるようで、4和音でつくった「紅蓮華」がちゃんと再生できました。
// Arduino/ATmega328P,32U4 減衰矩形波オルゴール
// Arduino Uno, Leonardo 両対応
#include "notes.h" // 音符の定義データ(音階と音価(長さ))
#include "Gurenge.h" // 曲データ
#define MAX_TRACK 4 // 最大トラック数 (省RAMのため必要最低限で)
// 基本の12音階(ド~シ:C0からB0まで)の波長となるサイクル数 (16MHz, 512clock毎の割り込みのとき)
static const uint16_t CYC_SCALE[] = { 1911, 1804, 1703, 1607, 1517, 1432, 1351, 1276, 1204, 1136, 1073, 1012 };
// ISRでのやり取りのためのグローバル変数
static uint8_t Tracks; // 楽譜のトラック数
static uint16_t *NoteP[MAX_TRACK]; // 楽譜のトラック毎のポインタ
static uint16_t NoteCycle; // 基準音符(96,48分音符)の長さに必要な割り込みサイクル数
static uint8_t Reverb = 9; // 残響のレベル(0-16, 小さいと短い音、大きいと長い音)
void setup(){
pinMode( 9, OUTPUT ); // OC1A(Uno:PB1, Leonardo PB5) Uno, Leonardoとも Digital 9番ピン
pinMode( 10, OUTPUT ); // OC1B(Uno:PB2, Leonardo PB6) Uno, Leonardoとも Digital 10番ピン
// 9bit高速PWM(TOP=0x01ff), 分周なし, コンペアマッチでLow (512clock毎に割り込み)
TCCR1A = B10100010; // コンペアマッチでLow(COM1A1=1,COM1A0=0,COM1B1=1,COM1B0=0), タイマモード(一部)(WGM11=1,WGM10=0)
TCCR1B = B00001001; // タイマモード(一部)(WGM13=0,WGM12=1), クロック分周(CS12=0,CS11=0,CS10=1)
pinMode( A0, INPUT_PULLUP ); // タクトスイッチ(左側;音短い)
pinMode( A1, INPUT_PULLUP ); // タクトスイッチ(中央;再生&音標準)
pinMode( A2, INPUT_PULLUP ); // タクトスイッチ(右側;音長い)
}
void loop(){
playOC1A( Gurenge ); // 演奏開始
while( TIMSK1 & _BV(TOIE1) ){ // 演奏終了(割り込み停止)まで待つ 注意:ループ内delay()禁止(誤作動の原因)
if( !digitalRead(A0) ) Reverb = 8; // 減衰幅を大きく残響が短い音
if( !digitalRead(A1) ) Reverb = 9; // 減衰幅中間
if( !digitalRead(A2) ) Reverb = 10; // 減衰幅を小さく残響が長い音
}
while( digitalRead(A1) ); // ボタンが押される(LOWになる)まで待つ
}
void playOC1A(const uint16_t *d){ // 初期化作業のみ、本体はタイマ1オーバーフロー割込み
NoteCycle = F_CPU / 512 *4*60 / pgm_read_word_near(d++) / MIN_NOTE; // 基準音符に必要なサイクル数
for( Tracks = 0; Tracks < MAX_TRACK; ) { // 曲データからトラック数と各トラックの開始位置を取得
if( pgm_read_word_near(d++) != 0 ) continue;// 区切りが来るまで飛ばす
if( pgm_read_word_near(d) == 0 ) break; // 0が2つ続いたらデータの終了
NoteP[ Tracks++ ] = d; // メモリ上の位置を取得、トラック数をカウントアップ
}
TCCR1A |= B10100000; // 波形出力(OCR1A(D9), OCR1B(D10)ともコンペアマッチでLow)
TIMSK1 |= _BV(TOIE1); // タイマ1のオーバーフロー割り込み(TOIE1)
}
ISR(TIMER1_OVF_vect){ // タイマ1オーバーフロー:カウンタ最大値を超えたとき割り込み(F_CPU/9bit(Hz)→32usec毎)
static uint8_t t; // トラックのカウンタ(t)
static uint16_t n; // 基準音符毎に処理に入るカウンタ(n)
static uint16_t note; // PROGMEMから読み込んだ音符(音階+音価)情報
static uint8_t len[MAX_TRACK] = {}; // 音符の長さ(96分音符の何個分の長さか)
static uint16_t c[MAX_TRACK], cyc[MAX_TRACK]; // 音の波長→1周期に必要な割り込みサイクル数(cyc)とそのカウンタ(c)
static uint16_t env[MAX_TRACK]; // 音の振幅(envelope)
static uint16_t out[MAX_TRACK]; // 出力される値
static uint8_t lap[MAX_TRACK]; // 音の経過サイクル(laptime)、32μs*16=480μsごとに減衰させる
// *** 楽譜の処理 ***
if( !n ) { // 基準音符長毎に楽譜の処理
n = NoteCycle;
for( t = 0; t < Tracks; t++ ) {
if( !len[t] ) {
note = pgm_read_word_near( NoteP[t]++ );
if( !note ) { // 音の開始時の初期化 0なら今回の演奏終了
OCR1A = OCR1B = 0; // 出力を0にする
TCCR1A &= B00001111; // 波形出力なし(COM1A1=0, COM1A0=0, COM1B1=0, COM1B0=0)
TIMSK1 &= ~_BV(TOIE1); // タイマ1のオーバーフロー割り込み(TOIE1)=0
return;
}
len[t] = note & 0x00ff; // 下位8bitが音の長さ(96分音符の何倍の長さか)
cyc[t] = (note & 0xff00) ? (CYC_SCALE[ (note>>8) & 0x0f ] >> (note>>12)) : 0;
// 上位4bitがオクターブ、次の4bitがピッチクラス(0-11)
c[t] = 0; // 矩形波1周期をつくるためのサイクル数のカウンタを初期化
lap[t] = 0; // 音の経過サイクル初期化
env[t] = 0xffff; // 初めは最大振幅
}
len[t]--;
}
}
n--;
// *** 波形の処理・出力 ***
for( t = 0; t < Tracks; t++ ) {
if( ++c[t] == cyc[t] ) c[t] = 0; // 周期は16MHz時 32usec*cyc[]
out[t] = ( c[t] < (cyc[t]>>1) ) ? env[t] : 0; // duty比1:1の矩形波
if(!(++lap[t] & 0x0f)) env[t] -= (env[t]>>Reverb); // 32us*16=480us毎に振幅減衰
}
switch( Tracks ) { // OC1A(D9), OC1B(D10) 出力をトラック数毎で変える
case 1: OCR1A = OCR1B = out[0]>>7; break;
case 2: OCR1A = OCR1B = (out[0]>>8) + (out[1]>>8); break;
case 3: OCR1A = OCR1B = (out[0]>>8) + (out[1]>>9) + (out[2]>>9); break;
case 4: OCR1A = OCR1B = (out[0]>>9) + (out[1]>>9) + (out[2]>>9) + (out[3]>>9);
}
}
ちょっと手直ししました。(2020/12/05)
曲データ?(Gurenge.h)はどうやって作成しましたか?また、notes.hの詳細も知りたいです。
by うろ (2022-02-27 21:13)
ブログみていただきありがとうございます。
notes.h は、
・音楽を演奏したい - 楽譜編 [Arduino]
にあります。データの作り方もあります。
曲データのサンプルは、
・音楽を演奏したい - 波形確認編 [Arduino]
にあり、「static const uint16_t PROGMEM Noritz[] = { // テオドール・エステン作曲『人形の夢と目覚め』より」のところです。
紅蓮華は(テキストにすると)データが大きいので別ファイル(Gurenge.h)にしたのと、著作権的にブログに載せていいのかわからなかったので、載せていないだけです。
今思うと、楽譜データを、C4q (C(ドの音)で、オクターブは4で、q(四分音符;Quarter))とか、A5h(A(ラの音)のオクターブは5で、h(二分音符;Half))にしておけば、すっきりして入力もしやすかったと後悔。
by お名前(必須) (2022-03-03 12:24)