音階を作る


ここでは、音階データから簡単な音楽を流します。


1オクターブ分の周期の計算
タイマーにセットする値を求める
差分の計算
元のタイマーの値に戻す
単音を出す
音階を奏でる
CdSに手をかざすとスピーカーから「猫踏んじゃった」

音階とは

 音律にはピタゴラス音律、純正律、平均律とあるようですが、ここではコンピュータで簡単に計算可能な平均律を用います。
 歌を歌えてもドと上のドの周波数はどう違うのか? という問いには普通答えられないと思います。音楽の世界ではドと上のドの差を一オクターブと言います。octaveのoctは8という意味でドレミファソラシドが8つの音からきています。しかしながら、半音までちゃんと数えると全部で12になります。1オクターブは周波数で2倍になっています。その間の半音は等比数列で1/12分割されています。
数式で書くと、
音程(オクターブ)=LOG2(周波数比)
半音の音程は
半音の音程(オクターブ)=LOG2(21/12) =1/12
これを周波数で換算してみます。基本の周波数(ラの音)は440Hzと決まっています。ですから、上のラの周波数は880Hzになります。
ここでi番目を0とした時の周波数を440Hz、12番目の周波数を880Hzとすると、次のような式になります。
f(Hz)=440*2i/12
これをC言語の式に翻訳すると
f = 440*pow(2.0, i/12.0);
となります。
実際に純粋な周波数は以下の図のsine波です。しかし、コンピュータが得意とする(簡単という意味)ではSquare波(方形波、矩形波)です。ここでは、非力なCPUであるPICを使いますので、当然方形波を用います。デジタルI/Oをon、offするだけなので簡単です。

ここで必要になるのは周波数ではなく周期、あるいは波形を作るためのon/offのタイミングです。
周期と周波数は逆数の関係にあります。数式で書くと次のようになります。
T(秒) = 1/f(Hz)
これを上のC言語式に代入してみます
T(秒) = 1/(440*pow(2.0, i/12.0));
これは現実的ではありません。どこが現実的でないかと言えば単位が秒というのはTが整数でなければいけないので、いつもゼロになるからです。従って取りあえず100万倍してマイクロ秒単位で考えてみましょう。
T(μ秒) = 1.0e6/(440*pow(2.0, i/12.0));
1.0e6は1.0×106という意味です。
この式でi=0つまり440Hzを計算してみると2272.7になりますが、小数点の計算をすると時間がかかるのと現実問題無理なので四捨五入しますが、on/offタイミングから考えるとこの半分の時間になり、1136(μ秒)となります。
ここで、3オクターブ上と4オクターブ上のラの音の時間を求めてみましょう。
1136/(23) = 1136/8 = 142
1136/(24) = 1136/16 = 71
142と71を12等分(等比で)しないといけないのでとてもきつくなります。はっきり言って聞けたものではありません。音痴です。
どこが原因かといえば、単位を1μ秒にしたからです。これを0.1μ秒とか、0.01μ秒単位にすればかなり聞ける音になります。ここで0.01μ秒としましょう。すると1136が113636となります。残念ながら時間を計測するタイマーは65535までしか数えられないので不可能です。ですから、1136はこだわらずに、できるだけ大きい数字で、聞きやすいようになる数字を選びます。
そこで、例えば65535を選んだとします。すると4オクターブ上の音の数字は次のようになります。
65535/(24) = 65535/16 = 4096
聞きやすいかどうかは別にして、ある程度可能に思います。しかしながら、非力なCPUであるPICではこれもきつかったりします。1つの音をだすのに16bit、つまり2byteも使うのです。あっという間にデータメモリーが無くなってしまいます。これを頭を使って1byte、つまり8bit、値で0~255までの数字に変換できないかです。
4オクターブだったら4*12+1で49種類あれば可能なのでなんとかいけるかもしれません。
以下のプログラムで、1191という値は任意です。このぐらいが良いかも。で勝手に決めています。

1オクターブ分の周期の計算

1オクターブ分の周期の計算
#include <stdio.h>
#include <math.h>

main()
{
  int i, x;
  int m=1191;
  int n=0x10000-m;       // 65536-m
  for(i=0; i<=12; i++) {
    x = (int)round(n/pow(2.0, i/12.));
    if(i%12==0) printf("\n");
    printf("%d,", x);
  }
}
計算結果
64345,60734,57325,54107,51071,48204,45499,42945,40535,38260,36112,34086, 32173,

タイマーにセットする値を求める

数字を眺めれば、大きい数字ばかりです。しかしながら、実際にタイマーに設定する数字はmカウントさせるには、65536-mをタイマーにセットすることになります。従って、タイマーにセットさせるために必要な値は次のようになります。
タイマーにセットする値を求める
#include <stdio.h>
#include <math.h>

main()
{
  int i, x;
  int m=1191;
  for(i=0; i<=12; i++) {
    x = (int)round(m/pow(2.0, i/12.));
    if(i%12==0) printf("\n");
    printf("%d,", x);
  }
}
計算結果
1191,1124,1061,1002,945,892,842,795,750,708,668,631, 596,

差分の計算

少し小さい値になりましたが、まだ8bit以上です。ここで隣との差をとってみましょう。
差分の計算
#include <stdio.h>
#include <math.h>

main()
{
  int i, x, y;
  int m=1191;
  y=m;
  for(i=0; i<=12; i++) {
    x = (int)round(m/pow(2.0, i/12.));
    if(i%12==0) printf("\n");
    printf("%d,", y-x);
    y=x;
  }
}
計算結果
0,67,63,59,57,53,50,47,45,42,40,37, 35,

元のタイマーの値に戻す

取りあえず、1byteの範囲の値になりました。これを用いて元の時間データに戻してみます。
元のタイマーの値に戻す
#include <stdio.h>
#include <math.h>

#define FirstPeriod 64345

unsigned char diff[]={
67,63,59,57,53,50,47,45,42,40,37,35};

main()
{
  unsigned peri, term;
  int j, tone;

  for(tone=0; tone=12; tone++) {
    peri = FirstPeriod;
    for(j=0; j<tone; j++) peri += diff[j];
      term = 0xffff-peri+1;
      printf("%d, ", term);
  }
}
計算結果
1191,1124,1061,1002,945,892,842,795,750,708,668,631, 596,

ということで、戻すことが出来ました。

単音を出す

ここで良く考えてください、1つの音符の中にその周波数が何周期必要かという計算が必要になります。それは、発音時間/周期となります。
また、実際に楽譜を数値に直していく時に悩むのは休符です。ですから、休符の値として255としました。
単音を出す
// ファイル名 tone.c
// 単音を出す
#include <htc.h>
__CONFIG(PWRTEN&HS&WDTDIS&UNPROTECT&MCLRDIS&BORDIS&IESODIS&FCMDIS);

#define _XTAL_FREQ 20000000
#define FirstPeriod 64345

typedef unsigned char byte;

void SoundPlay(byte tone, byte dulation);
void Delay500();

main()
{
  OPTION = 3;                 // FOSC/4 プリスケーラ1/16
  T1CON = 0x31;               // Timer1 settings
  TMR1IF = 0;                 // clear TMR1IF
  TMR1H = 0xf8;               // Initialize Timer1 register
  TMR1L = 0x00;
  TRISB = 0x00;               // スピーカー
  PORTB = 0;

  while(1) {
    Delay500();
    SoundPlay(51, 2);
  }
}

byte diff[]={
67,63,59,57,53,50,47,45,42,40,37,35,
34,31,30,28,27,25,24,22,21,20,19,17,
17,16,15,14,13,12,12,11,11,10, 9, 9,
 8, 8, 8, 7, 6, 7, 6, 5, 5, 5, 5, 5,
 4, 4, 3, 4};

void SoundPlay(byte tone, byte dulation)
{
   unsigned i, peri, n;
   byte j, k=0;
   byte H, L;
   unsigned term;
   unsigned all  = 1191;//0xffff-FirstPeriod+1;
   peri = FirstPeriod;
   if(tone == 255) {    // 255 は休符
      tone = 0;
      k = 1;
   }
   for(j=0; j<tone; j++) peri += diff[j];
   term = 0xffff-peri+1;
   n = 4*dulation*all/term*10;
   H = peri >> 8;
   L = peri & 0xff;
   for(i=0; i<n; ) {
     if(TMR1IF) {
       if(!k) PORTB = ~PORTB;
       TMR1H = H;               // Initialize Timer1 register
       TMR1L = L;
       TMR1IF = 0;
       i++;
    }
  }
}

void Delay500()               // 500m秒ディレイ
{
  int i;
  for(i=0; i<50; i++)        
    __delay_ms(10); 
}
void SoundPlay(byte tone, byte dulation)
音程toneの音をdulationの間発生させる。

音階を奏でる

 実際にPICで音階を4オクターブ分発音させてみます。
音階を奏でる
// 音階を奏でる
#include <htc.h>  
__CONFIG(PWRTEN&HS&WDTDIS&UNPROTECT&MCLRDIS&BORDIS&IESODIS&FCMDIS);

#define _XTAL_FREQ 20000000
#define FirstPeriod 64345

typedef unsigned char byte;

void SoundPlay(unsigned tone, byte dulation);

main()
{
  unsigned int i;
  unsigned int value;
  byte tone[]={
   3, 5, 7, 8,10,12,14,
  15,17,19,20,22,24,26,
  27,29,31,32,34,36,38,
  39,41,43,44,46,48,50,51};

  OPTION = 3;                 // FOSC/4 プリスケーラ1/16
  T1CON = 0x31;               // Timer1 settings
  TMR1IF = 0;                 // clear TMR1IF
  TMR1H = 0xf8;               // Initialize Timer1 register
  TMR1L = 0x00;
  TRISB = 0x00;              // スピーカー
  PORTB = 0;

  while(1) {
    for(i=0; i<29; i++) 
      SoundPlay(tone[i], 5);
  }
}

byte diff[]={
67,63,59,57,53,50,47,45,42,40,37,35,
34,31,30,28,27,25,24,22,21,20,19,17,
17,16,15,14,13,12,12,11,11,10, 9, 9,
 8, 8, 8, 7, 6, 7, 6, 5, 5, 5, 5, 5,
 4, 4, 3, 4};

void SoundPlay(unsigned tone, byte dulation)
{
   unsigned i, peri, n;
   byte j, k=0;
   byte H, L;
   unsigned term;
   unsigned all  = 1191;//0xffff-FirstPeriod+1;
   peri = FirstPeriod;
   if(tone == 255) {    // 255 は休符
      tone = 0;
      k = 1;
   }
   for(j=0; j<tone; j++) peri += diff[j];
   term = 0xffff-peri+1;
   n = 4*dulation*all/term*10;
   H = peri >> 8;
   L = peri & 0xff;
   for(i=0; i<n; ) {
     if(TMR1IF) {
       if(!k) PORTB = ~PORTB;
       TMR1H = H;               // Initialize Timer1 register
       TMR1L = L;
       TMR1IF = 0;
       i++;
    }
  }
}

音階データは次の表のようになります。ここで、先頭の音階はラの音程(A)と仮定しました。
0  1  2  3  4  5  6  7  8  9  10 11
A  A# B  C  C# D  D# E  F  F# G  G#
12 13 14 15 16 17 18 19 20 21 22 23
A  A# B  C  C# D  D# E  F  F# G  G#
24 25 26 27 28 29 30 31 32 33 34 35
A  A# B  C  C# D  D# E  F  F# G  G#
36 37 38 39 40 41 42 43 44 45 46 47
A  A# B  C  C# D  D# E  F  F# G  G#
48 49 50 51 52 53 54 55 56 57 58 59

CdSに手をかざすとスピーカーから「猫踏んじゃった

実際に使う形での練習をしてみます。

CdSに手をかざすとスピーカーから「猫踏んじゃった」
// CdSに手をかざすとスピーカーから「猫踏んじゃった」が流れる
#include <htc.h>  
__CONFIG(PWRTEN&HS&WDTDIS&UNPROTECT&MCLRDIS&BORDIS&IESODIS&FCMDIS);

#define _XTAL_FREQ 20000000
#define ANS4 0x10
#define FirstPeriod 64345

typedef unsigned char byte;

void SoundPlay(unsigned tone, byte dulation);
void musicPlay(byte tones[], byte speed);

main()
{
  unsigned int i;
  unsigned int value;
  byte tone[]={8,14,12, 5, 5,17,255,17,255};

  PORTA = 0;                  // PORTAを0にする
  TRISA = 0;                  // PORTAを出力に設定する
  ANSEL = ANS4;               // アナログ入力をAN4をON
  ADCON0 = 0x11;              // ADFM=0(左詰)  AN4 adOn
  OPTION = 3;                 // FOSC/4 プリスケーラ1/16

  T1CON = 0x31;               // Timer1 settings
  TMR1IF = 0;                 // clear TMR1IF
  TMR1H = 0xf8;               // Initialize Timer1 register
  TMR1L = 0x00;
  TRISB = 0x00;               // スピーカー
  PORTB = 0;

  while(1) {
    GODONE = 1;               // A/D 変換開始
    while(GODONE);            // 変換完了
    value = ADRESH;
    if(value < 30) {
      musicPlay(tone, 3);
      musicPlay(tone, 3);
    }  
  }
}

byte diff[]={
67,63,59,57,53,50,47,45,42,40,37,35,
34,31,30,28,27,25,24,22,21,20,19,17,
17,16,15,14,13,12,12,11,11,10, 9, 9,
 8, 8, 8, 7, 6, 7, 6, 5, 5, 5, 5, 5,
 4, 4, 3, 4};


void SoundPlay(unsigned tone, byte dulation)
{
   unsigned i, peri, n;
   byte j, k=0;
   byte H, L;
   unsigned term;
   unsigned all  = 1191;//0xffff-FirstPeriod+1;
   peri = FirstPeriod;
   if(tone == 255) {    // 255 は休符
      tone = 0;
      k = 1;
   }
   for(j=0; j<tone; j++) peri += diff[j];
   term = 0xffff-peri+1;
   n = 4*dulation*all/term*10;
   H = peri >> 8;
   L = peri & 0xff;
   for(i=0; i<n; ) {
     if(TMR1IF) {
       if(!k) PORTB = ~PORTB;
       TMR1H = H;               // Initialize Timer1 register
       TMR1L = L;
       TMR1IF = 0;
       i++;
    }
  }
}

void musicPlay(byte tones[], byte speed)
{
  byte m, j;
  unsigned int i;

  m = tones[0];
  for(i=1; i<=m; i++) SoundPlay(tones[i], speed);
}

void musicPlay(byte tones[], byte speed)
tones配列で示された楽譜をspeedの速さで奏でる。

目次へ戻る