音楽を演奏したい - Wave Table編 [Arduino]
今回は、正弦波を出してみたいと思います。
・仕組み
こんな感じの添え字が0~255までで、値が0~255までの配列をつくりました。
正弦波というか、位相をずらして0から始まるようにしてあります。(音の鳴り始めのポップノイズをなくすためです。)
音の周波数によって、配列の添え字の進め方を変えて値を読み取ります。
たとえば、A4 = 440Hzだと 16MHz, 9bit割り込み(512clock)の場合、
256 / (16000000 / 512 / 440) = 3.60448
ということで、3.60448ずつ配列の添え字を進めていけばいいのですが、処理能力的にマイコンで少数を扱うのはやめたほうがいいと思います。
そこで仮に配列が大きく、添え字が0~32767まであったとしたら、128倍して、461.37344となり、小数点以下を切り捨てて461としても大きな問題がなさそうです。
ただ、配列を大きくするとメモリが足りなくなるので、配列に添え字を入れる段階で128で割る=7ビット右シフトすることで誤差を許容範囲にできるという寸法です。
256倍にしなかったのは、添え字を足していき65535を超えたたらまた添え字を0にするというようにしてしまうと、16ビット内で計算ができないためです。
・問題点
波形の性質なのか、音が減衰しきっていないときに新しい音がくるとポップノイズがでる。
波形の性質なのか、ハードウェアのせいなのか、音量が小さい。
波形の種類や減衰量などを選べるようにと機能を欲張ったら、2和音しか出せなくなった。
処理能力については、配列を使わないようにするとか、変数を使いまわすとか、いろいろテクニックはあるらしい。
とりあえず個人的にWave Tableに興味がなくなってしまったので、覚えとしての和音なしでのスケッチ。
・仕組み
こんな感じの添え字が0~255までで、値が0~255までの配列をつくりました。
正弦波というか、位相をずらして0から始まるようにしてあります。(音の鳴り始めのポップノイズをなくすためです。)
音の周波数によって、配列の添え字の進め方を変えて値を読み取ります。
たとえば、A4 = 440Hzだと 16MHz, 9bit割り込み(512clock)の場合、
256 / (16000000 / 512 / 440) = 3.60448
ということで、3.60448ずつ配列の添え字を進めていけばいいのですが、処理能力的にマイコンで少数を扱うのはやめたほうがいいと思います。
そこで仮に配列が大きく、添え字が0~32767まであったとしたら、128倍して、461.37344となり、小数点以下を切り捨てて461としても大きな問題がなさそうです。
ただ、配列を大きくするとメモリが足りなくなるので、配列に添え字を入れる段階で128で割る=7ビット右シフトすることで誤差を許容範囲にできるという寸法です。
256倍にしなかったのは、添え字を足していき65535を超えたたらまた添え字を0にするというようにしてしまうと、16ビット内で計算ができないためです。
・問題点
波形の性質なのか、音が減衰しきっていないときに新しい音がくるとポップノイズがでる。
波形の性質なのか、ハードウェアのせいなのか、音量が小さい。
波形の種類や減衰量などを選べるようにと機能を欲張ったら、2和音しか出せなくなった。
処理能力については、配列を使わないようにするとか、変数を使いまわすとか、いろいろテクニックはあるらしい。
とりあえず個人的にWave Tableに興味がなくなってしまったので、覚えとしての和音なしでのスケッチ。
// Arduino/ATmega328P,32U4 減衰正弦波オルゴール
// Arduino Uno, Leonardo 両対応
#include "notes.h" // 音符の定義データ(音階と音価(長さ))
#include "Gurenge.h" // 曲データ
static uint16_t NoteCycle; // 基準音符(96,48分音符)の長さに必要な割り込みサイクル数
static uint16_t *NoteP; // 楽譜のポインタ
// 基本の12音階の波長となるサイクル数とwave tableの換算スケール
// 第10オクターブ, 1サイクル(@16MHz,9bit高速=512clock)で1周期(Wave Tableで0-255)進むときの添字の128倍)
static const uint16_t WT_SCALE[] = { 17557, 18601, 19708, 20879, 22121, 23436, 24830, 26306, 27871, 29528, 31284, 33144 };
// 正弦波データ (音の出だしでノイズが出ないように0から開始)
static const uint8_t SinWave[] = {
0, 0, 0, 0, 1, 1, 1, 2, 2, 3, 4, 5, 5, 6, 7, 9, 10, 11, 12, 14, 15, 17, 18, 20,
21, 23, 25, 27, 29, 31, 33, 35, 37, 40, 42, 44, 47, 49, 52, 54, 57, 59, 62, 65, 67, 70, 73, 76,
79, 82, 85, 88, 90, 93, 97,100,103,106,109,112,115,118,121,124,128,131,134,137,140,143,146,149,
152,155,158,162,165,167,170,173,176,179,182,185,188,190,193,196,198,201,203,206,208,211,213,215,
218,220,222,224,226,228,230,232,234,235,237,238,240,241,243,244,245,246,248,249,250,250,251,252,
253,253,254,254,254,255,255,255,255,255,255,255,254,254,254,253,253,252,251,250,250,249,248,246,
245,244,243,241,240,238,237,235,234,232,230,228,226,224,222,220,218,215,213,211,208,206,203,201,
198,196,193,190,188,185,182,179,176,173,170,167,165,162,158,155,152,149,146,143,140,137,134,131,
128,124,121,118,115,112,109,106,103,100, 97, 93, 90, 88, 85, 82, 79, 76, 73, 70, 67, 65, 62, 59,
57, 54, 52, 49, 47, 44, 42, 40, 37, 35, 33, 31, 29, 27, 25, 23, 21, 20, 18, 17, 15, 14, 12, 11,
10, 9, 7, 6, 5, 5, 4, 3, 2, 2, 1, 1, 1, 0, 0, 0};
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)
}
void loop(){
playOC1A( Gurenge ); // 演奏開始
while( TIMSK1 & _BV(TOIE1) ); // 演奏終了(割り込み停止)まで待機(ループ内delay禁止(誤作動の原因))
delay(5000);
}
void playOC1A(const uint16_t *d){
NoteCycle = F_CPU / 512 *4*60 / pgm_read_word_near( d ) / MIN_NOTE; // 基準音符に必要なサイクル数
NoteP = d + 2; // 楽譜の開始位置を取得
TCCR1A |= B10100000; // 波形出力(OCR1A(D9), OCR1B(D10)ともコンペアマッチでLow)
TIMSK1 |= _BV(TOIE1); // タイマ1のオーバーフロー割り込み(TOIE1)
}
ISR(TIMER1_OVF_vect){ // タイマ1オーバーフロー:カウンタ最大値を超えたとき割り込み(F_CPU/9bit(Hz)→32usec毎)
static uint16_t n = 1; // 基準音符毎に処理に入るカウンタ
static uint16_t note; // PROGMEMから読み込んだ音符(音階+音価)情報
static uint8_t len = 1; // 音符の長さ(96,48分音符の何個分の長さか)
static uint16_t cyc, c; // 音の波長 → 1サイクルでWaveTableの添字を進める量x128とそのカウンタ
static uint16_t env; // 音の振幅(envelope)
static uint8_t lap; // 1音の経過サイクルカウンタ(減衰計算用), 32μs*16=480μsごとに減衰させる
// *** 楽譜の処理 ***
if( !--n ) { // 基準音符長毎に楽譜の処理
n = NoteCycle;
if( !--len ) { // 1音終わったら次の音の処理
note = pgm_read_word_near( NoteP++ );
len = note & 0x00ff; // 下位8ビットが音の長さ(96,48分音符の何倍の長さか)
if( note & 0xff00 ) { // 休符でなければ
cyc = WT_SCALE[ (note>>8) & 0x0f ] >> (10 - (note>>12)); // 1サイクルで進めるWave Tabele添字の数の128倍
c = 0; // 正弦波1周期をつくるためのサイクル数のカウンタを初期化
lap = 0; // 音の経過サイクル(laptime)
env = 0xffff; // 初めは最大振幅
}
if( !note ) { // 0なら演奏終了
OCR1A = OCR1B = 0; // 出力を0にする
TCCR1A &= B00001111; // 波形出力なし(COM1A1=0, COM1A0=0, COM1B1=0, COM1B0=0)
TIMSK1 &= ~_BV(TOIE1); // タイマ1のオーバーフロー割り込み(TOIE1)=0
n = len = 1; // カウンタを初期化して終了
return;
}
}
}
// *** 波形の処理・出力 ***
if( c > 32767 ) c = 0; // 添字x128が、添字の範囲[0-255]を超えたらリセット
OCR1A = OCR1B = (SinWave[ c>>7 ] * (env>>8)) >> 7;
c += cyc;
if( !(lap++ & 0x0f) ) env -= (env>>8); // 32us*16=480us毎に振幅減衰(調整可)
}
音楽を演奏したい - 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)
音楽を演奏したい - digitalWrite編 [Arduino]
今回は、tone()ではなく、digitalWrite()を使って波形をつくろうと思います。
tone()では、和音が出せないこともないようですが、特殊なやり方のようで、スピーカーも和音の数だけ必要らしい。
・仕組み
和音での演奏をするのにあたり、tone()を使わないで、digitalWrite()のOn/Offで矩形波をで作ることにしました。
tone()では周波数を指定して矩形波の音を出していましたが、digitalWrite()の場合、周波数よりも、矩形波の長さ=1波形の時間=周期、で指定するのが簡便かと考えました。
ちょうど基本の12音階(ド~シ)で第0オクターブの周期(usec; μ秒)が、以下のように16ビット符号なし整数型にいい感じで収まります。
const uint16_t T_SCALE[] = { 61156, 57724, 54484, 51426, 48540, 45815, 43244, 40817, 38526, 36364, 34323, 32396};
あとは、ループ内で各パート(トラック)ごとに、音が終わる時刻(usExp)が現在時刻(usCur)を過ぎたら新しい音の読み込み。新しい音の読み込みのときには、その音の終了時刻(usExp)と、周期(T)と、周期のうち出力Onにする時間(Ton)を指定する。また、矩形波を作るため、ループ内で現在時刻(usCur)を周期(T)で割った余り(0→(T-1)で変化を繰り返す)が、出力On時間(Ton)内なら出力フラグを立てる。ということにしました。
#include "notes.h" // 音符の定義データ(音階と音価(長さ))
#include "Gurenge.h" // 曲データ
#define MAX_TRACK 4 // 最大トラック数
const uint16_t T_SCALE[] = { 61156, 57724, 54484, 51426, 48540, 45815, 43244, 40817, 38526, 36364, 34323, 32396};
// 基本の12音階(ド~シ:C0からB0まで)の波長となる周期(usec)
void setup(){
pinMode( 9, OUTPUT );
pinMode( A1, INPUT_PULLUP );
}
void loop(){
playDW( Gurenge );
while(digitalRead(A1));
delay(1000);
}
void playDW(const uint16_t *d){ // digitalWriteで
uint8_t t, track = 0; // 楽譜のトラック数(track) とトラックのカウンタ(t)
uint8_t output; // digitalWriteの出力
uint16_t *p[MAX_TRACK]; // 楽譜のトラック毎のポインタ(メモリ上の絶対的な位置)
uint16_t note; // PROGMEMから読み込んだ音符(音階+音価)情報
uint32_t usRef, usCur, usExp[MAX_TRACK]; // 基準音符の長さ, 現時刻, 音の終了時刻(usec)
uint32_t T[MAX_TRACK], Ton[MAX_TRACK]; // 周期, 周期のうちONの時間(usec)
usRef = 1000000*60*4 / MIN_NOTE / pgm_read_word_near(d++); // テンポから基準96分音符の長さ(usec)を計算
for(;;) { // 曲データからトラック数と各トラックの開始位置を取得
if( pgm_read_word_near(d++) != 0 ) continue; // 区切りが来るまで飛ばす
if( pgm_read_word_near(d) == 0 ) break; // 0が2つ続いたらデータの終了
p[ track++ ] = d; // メモリ上の開始位置を取得、track数をカウントアップ
}
for( t = 0; t < track; t++ ) usExp[t] = micros(); // 音の終了時刻の初期化
do { // 曲データを読み込みながら再生
output = LOW;
usCur = micros();
for( t = 0; t < track; t++ ) {
if( usCur > usExp[t] ) { // 1音の出力時間を過ぎたら次の1音の準備
if( !(note = pgm_read_word_near(p[t])) ) continue; // note = 0なら演奏終了
p[t]++; // 音符データのポインタを進める
usExp[t] += (note & 0x00ff) * usRef; // 下位8ビットが音の長さ(96分音符の何倍の長さか)
if( note & 0xff00 ) { // 休符でなければ、、
T[t] = T_SCALE[ (note>>8) & 0x0f ] >> (note>>12); // 上位4ビットがオクターブ、次の4ビットがピッチクラス(0-11)
Ton[t] = T[t] >> 3; // duty比を1/8とした
} else T[t] = 0; // 休符であれば∞にしたいとことだが、0で。
}
if( T[t] && usCur % T[t] < Ton[t] ) output = HIGH; // 休符でなく波形のON部分なら出力
}
digitalWrite( 9, output );
} while( note );
}
無駄だと思うのは、曲の再生時にトラック数と、各トラックの開始位置を探る処理です。曲データはリアルタイムで変わるものではないので事前に定数として持っていればいいのですが、、、でも、楽譜データ作成の簡便さを優先ということで。
音楽を演奏したい - tone編 [Arduino]
作った楽譜が再生できるかの確認です。
とりあえず、和音なし、Arduino標準のtone()関数を使った再生です。
tone関数は、周波数で音の高さを指定します。
基本の12音階(ド~シ)の周波数(Hz)をスケールとして配列で持っておき、1オクターブは上がると周波数は倍に、1オクターブ下がると周波数は半分になるので、シフト演算でいけます。16ビット符号なし整数型に収まる最大が第11オクターブになるので、ここから希望のオクターブまで右シフトすることで希望のオクターブの12音階(ド~シ)のそれぞれの周波数が得られるという仕組みにしました。
右ビットシフトは切り捨てになりますが、R_BITRSHIFT という四捨五入できるビットシフトマクロを作ってみましたが、それを聞き分けられるほどの耳ではないし、そもそもtone()関数の周波数は整数指定なので気にしなくてもよさそう。
#include "notes.h" // 音符の定義データ(音階と音価(長さ))
#include "Gurenge.h" // 曲データ
#define R_BITRSHIFT(X,S) ((((X)>>((S)-1))+1)>>1) // 四捨五入(ROUND)する右シフト(BITRSHIFT)マクロ
const uint16_t F_SCALE[] = { 33488, 35479, 37589, 39824, 42192, 44701, 47359, 50175, 53159, 56320, 59669, 63217 };
// 基本の12音階(ド~シ:C11からB11まで)の周波数(Hz)(16ビット符号なし整数型に収まる最大が第11オクターブ)
void setup(){
pinMode( 9, OUTPUT ); // tone()の中から呼び出されるのでなくていいが、明示的に
pinMode( A1, INPUT_PULLUP ); // タクトスイッチ(PULLUPしてあり、押すとGND=LOWに)
}
void loop(){
playTone( Gurenge );
delay(1000);
while( digitalRead(A1) ); // 演奏が終わったらスイッチが押される(LOWになる)まで待つ
delay(1000);
}
void playTone(const uint16_t *d){
uint16_t note, freq; // PROGMEMから読み込んだ音符(音階+音価)情報(16bit), 周波数(Hz)
uint32_t usExp, usDur, usRef; // 音の切れる(expire)時刻, 音の持続時間(duration), 基準音符の長さ(usec)
usRef = 1000000 * 60 * 4 / MIN_NOTE / pgm_read_word_near(d++); // テンポから基準96,48分音符の長さ(usec)を計算
usExp = micros(); // 音の終了時刻の初期化
do { // 曲データを読み込みながら再生
if( micros() > usExp ) { // 1音の出力時間を過ぎたら次の1音の準備
if( !(note = pgm_read_word_near(++d)) ) continue; // note = 0なら演奏終了
usDur = (note & 0x00ff) * usRef; // 下位8ビットが音の長さ(基準音符の何倍の長さか)
usExp += usDur; // 音の切れる時間を更新
if(note & 0xff00) { // 休符でなければ、、
freq = R_BITRSHIFT( F_SCALE[(note>>8)&0x0f], 11-(note>>12) ); // 上位4ビットがオクターブ、次の4ビットがピッチクラス(0-11)
tone( 9, freq, usDur>>10 ); // 10bitシフト=(1/1024):usからmsに変換し,やや音を短く
}
}
if( !digitalRead(A1) ) break; // タクトスイッチが押されたら演奏終了
} while( note );
}
マクロ使わない場合。
freq = F_SCALE[(note>>8)&0x0f] >> (11-(note>>12)); // 上位4ビットがオクターブ、次の4ビットがピッチクラス(0-11)
音楽を演奏したい - ハード編 [Arduino]
音を鳴らすためのハードウェアの作成です。
・設計
とりあえず実験用として作ります。
digital 9番は、Arduino Uno (ATmega328P)と、Leonardo (ATmega32u4)で、PWMのピンがOC1Aで同じなので、ここにスピーカーをつなぐことにしました。
スピーカとの接続には一応トランジスタを1つ使ってみました。(あまり意味なかった?)
なにかの操作用のボタンは、「再生」と「なにかの+(プラス)」、「なにかの-(マイナス)」の3つ作ることにしました。analogReadと抵抗値を変えることで1ピンだけ使用する方法もあるようですが、ピンは余ってるし、単純化のためそれぞれピンにつなぐことにし、アナログ信号を使用するわけではないけど、わかりやすいようにA0, A1, A2ピンに割り当てることにしました。
Arduino側でPull upすることにして、ボタンを押したときGNDに落とすことでLOWになるようにしました。
・パーツ選び
100円ショップのスピーカーを使用しました。
マイコンは、小さくてUSBも使える Arduino Leonardo互換のPro Microを使用しました。
可変抵抗は平たいのがなかなか売っていなくて、AliExpress.comで探しました。(volumeでは出てこないのでpotentiometer(ポテンショメータ)で探す。)
・ケース加工とパーツ加工
スピーカーのケースの溝にカッターナイフを差し込み、接着部分をはがすことで分解。
ケースにピンバイスで穴をあけ、パーツをあてがっては広げて、あてがっては整えを繰り返す。
タクトスイッチの基板と、Pro Microがわずかに干渉するので、Pro Microをぎりぎりまで削る。
ケースに収まるのを確認して配線のはんだ付け。(一応この時点で動作確認を)
ホットボンドで固定。(再度動作確認を)
瞬間接着剤で閉鎖。
・反省点
ホットボンドで固定してから接触不良を確認した。動作確認をこまめにしておけばよかった。
電源LEDがいらない。スピーカー前面のあみあみから漏れ出る光はまだ許せるが、白いケースを透過して漏れ出る光はいまいち。電源LEDは除去しておくべきでした。
・設計
とりあえず実験用として作ります。
digital 9番は、Arduino Uno (ATmega328P)と、Leonardo (ATmega32u4)で、PWMのピンがOC1Aで同じなので、ここにスピーカーをつなぐことにしました。
スピーカとの接続には一応トランジスタを1つ使ってみました。(あまり意味なかった?)
なにかの操作用のボタンは、「再生」と「なにかの+(プラス)」、「なにかの-(マイナス)」の3つ作ることにしました。analogReadと抵抗値を変えることで1ピンだけ使用する方法もあるようですが、ピンは余ってるし、単純化のためそれぞれピンにつなぐことにし、アナログ信号を使用するわけではないけど、わかりやすいようにA0, A1, A2ピンに割り当てることにしました。
Arduino側でPull upすることにして、ボタンを押したときGNDに落とすことでLOWになるようにしました。
・パーツ選び
100円ショップのスピーカーを使用しました。
マイコンは、小さくてUSBも使える Arduino Leonardo互換のPro Microを使用しました。
可変抵抗は平たいのがなかなか売っていなくて、AliExpress.comで探しました。(volumeでは出てこないのでpotentiometer(ポテンショメータ)で探す。)
・ケース加工とパーツ加工
スピーカーのケースの溝にカッターナイフを差し込み、接着部分をはがすことで分解。
ケースにピンバイスで穴をあけ、パーツをあてがっては広げて、あてがっては整えを繰り返す。
タクトスイッチの基板と、Pro Microがわずかに干渉するので、Pro Microをぎりぎりまで削る。
ケースに収まるのを確認して配線のはんだ付け。(一応この時点で動作確認を)
ホットボンドで固定。(再度動作確認を)
瞬間接着剤で閉鎖。
・反省点
ホットボンドで固定してから接触不良を確認した。動作確認をこまめにしておけばよかった。
電源LEDがいらない。スピーカー前面のあみあみから漏れ出る光はまだ許せるが、白いケースを透過して漏れ出る光はいまいち。電源LEDは除去しておくべきでした。
データの全ての組み合わせ(CROSS JOIN) [JavaScript]
データの全ての組み合わせをつくることを、交差結合とか、クロス結合とか、CROSS JOINとかいうようです。
音価、音高の組み合わせを作るのに作ったスクリプトです。
ネットで拾ったものから適当に作りました。雑ですみません。
次のコードをテキストエディタでhtmlとして保存します。Unicode(UTF-8)です。
私がふだん使っているfirefoxではスクリプトでコピーが使えないので、[Ctrl]+[C]でコピーです。
音価、音高の組み合わせを作るのに作ったスクリプトです。
ネットで拾ったものから適当に作りました。雑ですみません。
次のコードをテキストエディタでhtmlとして保存します。Unicode(UTF-8)です。
<!DOCTYPE html>
<html lang="ja">
<head>
<meta charset="UTF-8">
<title>組み合わせる</title>
<script>
function crossjoin() {
var lines1 = document.getElementById('list1').value.replace(/\r\n|\r/g, "\n").split( '\n' );
var lines2 = document.getElementById('list2').value.replace(/\r\n|\r/g, "\n").split( '\n' );
document.getElementById("result").value = "";
for ( var i = 0; i < lines1.length; i++ ) {
if ( lines1[i] == '' ) continue;
for ( var j = 0; j < lines2.length; j++ ) {
if ( lines2[j] == '' ) continue;
document.getElementById("result").value += lines1[i] + '_' + lines2[j] + '\n';
}
}
document.getElementById("result").select();
}
</script>
</head>
<body>
<h1>組み合わせる(クロス結合)</h1>
<form>
<p>
<textarea rows=8 cols=30 id="list1">
醤油ラーメン
味噌ラーメン
豚骨ラーメン
</textarea>
<textarea rows=8 cols=30 id="list2">
小盛
並盛
大盛
</textarea>
<p>
<textarea rows=12 cols=64 id="result" readonly placeholder=" *** 結果はここに表示 ***">
</textarea>
<p>
<input type=button value="組み合わせる" onclick="crossjoin()" />
<input type=button value="選択範囲をコピー" onclick="document.execCommand('copy')" />
<input type=reset value="クリア" />
</form>
</body>
</html>
私がふだん使っているfirefoxではスクリプトでコピーが使えないので、[Ctrl]+[C]でコピーです。
音楽を演奏したい - 楽譜編 [Arduino]
久しぶりです。
Arduinoで和音を使った音楽を演奏したいと思いました。
さて、データの形式をどうしましょうか?
マイコンでの実装なので極力データは少なく、扱いやすい形式がいいです。
また、わかりやすいという面ではいわゆる西洋音楽の楽譜をベースにしたいと思います。
一つ一つの音を音符で表していくのがよさそう。
音符については wikipedia によると、、
「音符は五線譜などの中で、相対的な音の長さ(音価)と時間的な位置、および高さ(音高)を表す。また、音価によってその形が異なる。」
ということです。時間的な位置については、データの位置ということになるので、音の長さ(音価)と高さ(音高)について考えていきます。
オルゴールでは、音の長さ(音価)はどの音も同じですが、時間的な位置というものを音の長さ(音価)として扱ってもいいような気がします。
◆音の長さ(音価)
表し方としては、
(1) 絶対的な長さ(マイクロ秒とか、ミリ秒とか)
(2) 相対的な長さ(ある音符を1として長さを比較)
(3) 使用する音の長さ(音価)にIDを振る
さて、それぞれの特徴から、
(1)はわかりやすが、データが大きくなる、楽譜という法則と疎遠な感じ
そもそも一つの曲で使われる音符の長さの種類は限られているので(3)の方法だと少ないデータで済むと思います。ただそのIDと結びつくデータベースを別に持っていないといけません。
ということで、(2)です。テンポの情報は必要ですが、プログラムからの操作は容易になります。
◆音の高さ(音高)
表し方としては、
(1) 絶対的な値(周波数や周期)
(2) 相対的な値(ある音高から順番にID)
(3) 使用する音の高さ(音高)にIDを振る
さて、それぞれの特徴から、
(1)これは場合によってはプログラム上扱いやすいかもしれないが、音高ごとの周波数の比率は2の12乗根倍の等比数列であり、使われる周波数はきまっているためきわめて無駄が多い。(3)は、最小のデータになる可能性が高いが、音の長さ(音価)の場合と同じでデータベースが必要。ということで、(2)です。
◆音符データの形式
音価、音高ふくめて1バイトにおさめるとなると両者とも(3)の方法しかないのですが、曲ごとにデータベースを作るのは面倒なので、とりあえず2バイト(16ビット、uint16_t)でなんとかしたい。上位8ビットを音高、下位8ビットを音値だとわかりやすいだろうと考えました。
・音の長さ(音価)
一番長い音符はwikipediaによると倍全音符ですが、短い音符にはきりがありません。
口でタタタとか、トゥクトゥクとかで表現できるのは16~32分音符くらい、ボカロの曲とかだと256分音符などもあると聞きましたが、マイコンなのである程度の切り捨てが必要です。
あと、3連符、5連符、7連符とか2で割り切れないものや、付点音符、複付点音符、3重付点音の扱いもある程度の切り捨てが必要です。
ということで考えました。
水戸黄門のテーマや、ドラえもんのオープニングなどでも3連符はでてくるので3連符までは押さえておく。一般的な曲ではせいぜい16分音符。余裕をもって32分音符まで対応する。付点は1個まで。
ということで96分音符というものの長さを1とします。32分音符は3となり、4分音符は24となり、付点4分音符は36となり、一番長い倍全音符は192となり255までに収まります。通常の音符はすべて3の倍数になるので、3で割って3連符を表現できるというわけです。
・音の高さ(音高)
MIDI番号というのがあって、C-1, 8.2Hzを0番として、A4, 440.0Hz を69番、G9, 12543.9Hzの127番までとなっています。これを使うのが一見よさそうに見えますが、2の12乗根のべき乗計算や割り算や剰余計算はマイコンには酷です。そこで音高をオクターブと音名(C, C#, D, D#, … ,Bの12個)にわけます。1オクターブごとに周波数は2倍(周期は半分)になることを利用し、あらかじめ12個だけ周波数(あるいは周期)をデータとして持っておき、音名からデータを取り出し、オクターブ情報でビットシフト計算するというのがいいんじゃないかなぁと考えました。それぞれ4ビットごと使用します。音名は12個のデータに4ビット(16個のデータ)を割り当てるので多少の無駄はあります。オクターブも0~7までなら3ビットで済み1bit余ります。多少の無駄はありますが、データ処理が容易になるので現実的な方法だと思います。
例:A4(ラ 440Hz)、4分音符の場合
(MSB) 0100 1001 0001 1000 (LSB) = 0x4918
* 上位4ビットがオクターブ(4)
* 次の4ビットが音階スケール(ド~シ; C, C#, D, D#, … ,B)(9)
* 下位8ビットが音の長さ0x18(=24)(1/96音符を1とする)
と定義すれば、
= H_A4 | L_4
というように、ORでくっつけることで音符が完成です。
トータルで2バイトなので pgm_read_word_near で読みだせるし、ビットマスクとビットシフト、配列操作がメインで高速動作が期待できるんじゃないかと。
ここまでは理論の話。
実際に楽譜(ためしに鬼滅の刃の主題歌「紅蓮華」)を入れてみたところ、、
と、面倒でした。
はじめから、すべての組み合わせを定義したところ、ちょっとすっきりし、入力も楽になりました。
・音符から楽譜へ
音符を連ねて楽譜にしていくのですが、データの形式としては単音の音符(音高と音価)を連続していくのですが、そうすると和音がだせません。和音は別のパート、トラックとして扱おうと思います。
パート毎に配列を作ると2次元配列とした場合には無駄が出るし、、。ということで、1次元の配列で、区切りを入れることとしました。
0を区切りとして、0が2個続いたらデータ終了という感じ。
以下、作成した定義ファイル「notes.h」
Arduinoで和音を使った音楽を演奏したいと思いました。
さて、データの形式をどうしましょうか?
マイコンでの実装なので極力データは少なく、扱いやすい形式がいいです。
また、わかりやすいという面ではいわゆる西洋音楽の楽譜をベースにしたいと思います。
一つ一つの音を音符で表していくのがよさそう。
音符については wikipedia によると、、
「音符は五線譜などの中で、相対的な音の長さ(音価)と時間的な位置、および高さ(音高)を表す。また、音価によってその形が異なる。」
ということです。時間的な位置については、データの位置ということになるので、音の長さ(音価)と高さ(音高)について考えていきます。
オルゴールでは、音の長さ(音価)はどの音も同じですが、時間的な位置というものを音の長さ(音価)として扱ってもいいような気がします。
◆音の長さ(音価)
表し方としては、
(1) 絶対的な長さ(マイクロ秒とか、ミリ秒とか)
(2) 相対的な長さ(ある音符を1として長さを比較)
(3) 使用する音の長さ(音価)にIDを振る
さて、それぞれの特徴から、
(1)はわかりやすが、データが大きくなる、楽譜という法則と疎遠な感じ
そもそも一つの曲で使われる音符の長さの種類は限られているので(3)の方法だと少ないデータで済むと思います。ただそのIDと結びつくデータベースを別に持っていないといけません。
ということで、(2)です。テンポの情報は必要ですが、プログラムからの操作は容易になります。
◆音の高さ(音高)
表し方としては、
(1) 絶対的な値(周波数や周期)
(2) 相対的な値(ある音高から順番にID)
(3) 使用する音の高さ(音高)にIDを振る
さて、それぞれの特徴から、
(1)これは場合によってはプログラム上扱いやすいかもしれないが、音高ごとの周波数の比率は2の12乗根倍の等比数列であり、使われる周波数はきまっているためきわめて無駄が多い。(3)は、最小のデータになる可能性が高いが、音の長さ(音価)の場合と同じでデータベースが必要。ということで、(2)です。
◆音符データの形式
音価、音高ふくめて1バイトにおさめるとなると両者とも(3)の方法しかないのですが、曲ごとにデータベースを作るのは面倒なので、とりあえず2バイト(16ビット、uint16_t)でなんとかしたい。上位8ビットを音高、下位8ビットを音値だとわかりやすいだろうと考えました。
・音の長さ(音価)
一番長い音符はwikipediaによると倍全音符ですが、短い音符にはきりがありません。
口でタタタとか、トゥクトゥクとかで表現できるのは16~32分音符くらい、ボカロの曲とかだと256分音符などもあると聞きましたが、マイコンなのである程度の切り捨てが必要です。
あと、3連符、5連符、7連符とか2で割り切れないものや、付点音符、複付点音符、3重付点音の扱いもある程度の切り捨てが必要です。
ということで考えました。
水戸黄門のテーマや、ドラえもんのオープニングなどでも3連符はでてくるので3連符までは押さえておく。一般的な曲ではせいぜい16分音符。余裕をもって32分音符まで対応する。付点は1個まで。
ということで96分音符というものの長さを1とします。32分音符は3となり、4分音符は24となり、付点4分音符は36となり、一番長い倍全音符は192となり255までに収まります。通常の音符はすべて3の倍数になるので、3で割って3連符を表現できるというわけです。
・音の高さ(音高)
MIDI番号というのがあって、C-1, 8.2Hzを0番として、A4, 440.0Hz を69番、G9, 12543.9Hzの127番までとなっています。これを使うのが一見よさそうに見えますが、2の12乗根のべき乗計算や割り算や剰余計算はマイコンには酷です。そこで音高をオクターブと音名(C, C#, D, D#, … ,Bの12個)にわけます。1オクターブごとに周波数は2倍(周期は半分)になることを利用し、あらかじめ12個だけ周波数(あるいは周期)をデータとして持っておき、音名からデータを取り出し、オクターブ情報でビットシフト計算するというのがいいんじゃないかなぁと考えました。それぞれ4ビットごと使用します。音名は12個のデータに4ビット(16個のデータ)を割り当てるので多少の無駄はあります。オクターブも0~7までなら3ビットで済み1bit余ります。多少の無駄はありますが、データ処理が容易になるので現実的な方法だと思います。
例:A4(ラ 440Hz)、4分音符の場合
(MSB) 0100 1001 0001 1000 (LSB) = 0x4918
* 上位4ビットがオクターブ(4)
* 次の4ビットが音階スケール(ド~シ; C, C#, D, D#, … ,B)(9)
* 下位8ビットが音の長さ0x18(=24)(1/96音符を1とする)
#define H_A4 0x4900
#define L_4 24
と定義すれば、
= H_A4 | L_4
というように、ORでくっつけることで音符が完成です。
トータルで2バイトなので pgm_read_word_near で読みだせるし、ビットマスクとビットシフト、配列操作がメインで高速動作が期待できるんじゃないかと。
ここまでは理論の話。
実際に楽譜(ためしに鬼滅の刃の主題歌「紅蓮華」)を入れてみたところ、、
PROGMEM const uint16_t Gurenge[] = {
135 , 0 , // テンポ 135
H_G4 | L_8d, H_FS4| L_8d , H_G4 | (L_8+L_2),
H_G4 | L_8d, H_FS4| L_8d , H_G4 | (L_8+L_2),
H_G4 | L_8d, H_FS4| L_8d , H_E4 | L_4d, H_D4 | L_8, H_D4 | (L_8+L_2),
H_RST| L_8 , H_B3 | L_4, H_D4 | L_8,
H_E4 | L_2 , H_RST| L_8 , H_E4 | L_4, H_G4 | L_8,
H_A4 | L_2 , H_RST| L_8 , H_G4 | L_4, H_A4 | L_8,
...
と、面倒でした。
はじめから、すべての組み合わせを定義したところ、ちょっとすっきりし、入力も楽になりました。
PROGMEM const uint16_t Gurenge[] = {
135 , 0, // テンポ 135
G4_8d , FS4_8d, G4_8+L_2,
G4_8d , FS4_8d, G4_8+L_2,
G4_8d , FS4_8d, E4_4d , D4_8 , D4_8+L_2,
RST_8 , B3_4 , D4_8 ,
E4_2 , RST_8 , E4_4 , G4_8 ,
A4_2 , RST_8 , G4_4 , A4_8 ,
...
・音符から楽譜へ
音符を連ねて楽譜にしていくのですが、データの形式としては単音の音符(音高と音価)を連続していくのですが、そうすると和音がだせません。和音は別のパート、トラックとして扱おうと思います。
パート毎に配列を作ると2次元配列とした場合には無駄が出るし、、。ということで、1次元の配列で、区切りを入れることとしました。
配列名[] = { テンポ, 0, パート1のデータ … , 0, パート2のデータ … , 0, パートnのデータ … , 0, 0 }
0を区切りとして、0が2個続いたらデータ終了という感じ。
以下、作成した定義ファイル「notes.h」