I’m loser, baby.

So why don't you kill me?

mbed LPC1114手習い04 / もう少しちゃんとPWMしてみる

PWMで音量、音程がコントロールできることは確認したので、取り敢えずもう少しちゃんとプログラムしたくて1から書き直しつつ途中で混乱したりしたので、自分用のメモ兼ねてまとめてみます。

パルス幅変調(PWM)とは

フーリエ変換は全ての波形をサイン波の合成で表す方法ですが、パルス幅変調は全ての波形をパルス波で表そう、という考え方です。
デジタルで音を扱う時、通常のPCM方式だとサンプリング周波数とサンプリングレートの組み合わせで音をデジタルデータ化します。
例えば下のノコギリ波の例で見ると、アナログデータが縦軸の音の強弱がサンプリングレート、横軸の音の変化がサンプリング周波数によってデジタルデータ化されます。
ちなみに8bit11kHzの音であれば音の強弱は8bit=256で0から255までの256段階で表されます。

  *
音 **
の ***
強 ****
弱 *****
  ******
  時間経過

それに対しPWMでは、音の強弱はオン/オフの0/1で表します。つまり全てパルス波にしてしまいます。8bit分の音の強弱情報は1/11kHz周期内のオン/オフ比率の中に繰り込んでしまい、出力された音をローパスフィルターに通すことでアナログデータを再現します。また1周期内のオン/オフの比率をデューティ比と呼びます。
ちなみにPWM方式では8bit11kHzの情報を扱うのには256×11=2816kHzで約2.8MHzの周波数が必要となります。

| 上の*一つ分 |
|******     |*****      |****       |***        |**         |*          |
   -------------------- 上の時間経過と同じ時間 -------------------------

この方式だと周波数はPCM方式より大きい値が必要となりますが、詳細は省略しますがデジタル/アナログ変換の回路が非常に単純になるというメリットがあります。
つまり安価でクロック48MHzのLPC1114で音を出すには良い方法となります。正直、もう少し上のクラスのシリーズであればAnalogOutがあるのでこんなことしなくても良いのですが、LPC1114はDIP方式で1個150円前後なので素人が入門用に遊ぶのは一番よいのです。他のシリーズだとdipじゃないので気軽に実機が作れない感じ。評価ボード込みで2000円とかになっちゃうんですよね。

mbed1114でPWMを扱うには

mbedにはPwmOutというクラスが用意されているのでそれを使います。LPC1114でタイマー割り込みに対応しているのは1,2,18,24の4ピンなのでこれを出力に指定してオブジェクトを作り、クロック=1パルス波分の長さを指定します。

float smpl_frq  = 11025;         //11kHz
float smpl_rate = 256;           //8bit
float clk       = 1 / smpl_frq;  //clock 

PwmOut   snd_out (dp18);         //set dp18 pin -> sound out
snd_out.period_us (clk);

後はオブジェクトに対し、デューティ比またはオンとなる時間を指定すると、1クロック毎にパルス波を指定ピンからパルス波を出力します。

//デューティ比50%を指定
snd_out = 0.5; 
//オンの長さを0.001秒で指定
snd_out.pulsewidth(0.001); 

デューテ比は0.0から1.0の値で指定できるのですが、自分はどうも意図した結果が出せず、直接オンにする時間をマイクロ秒で指定する方法でやってみました。

結果と課題

今回はアナログ入力を使わず直接周波数を指定して、また、音の強弱自体もコントロールせずに発振器の波形変化だけに留めました。
波形としては、パルス波、ノコギリ波、三角波と、配列でPCMデータを用意する形でサイン波、4種類を関数にして、メイン関数から音程を変えて出すようにしてみました。
PCM音源の音程の変え方とか超いい加減な方法でやっていますが、一応ちゃんと4種類の音が出ます。サイン波がサイン波らしい音で聞こえた時は結構感動しました。
課題も色々。

  • パルス波がやっぱり音程が違う気がする。でもパルス波がノイズが一番少ない。
  • サイン波も音程が違う気がする。パルス波の逆で若干低い?。
  • 配列がRAMを食い過ぎる。256個の浮動小数点で1.4Kも使う。
  • 音が変わる時に酷いクリップノイズが出ることがある。
  • LPF(ローパスフィルター)がいい加減なのでエイリアスノイズが結構でかい。PCM式の時のエイリアスノイズとは全然違う音。
  • その他いろいろ。

でもロービットローファイな感じは悪くないので、今後LPFは改良するとして、プログラム的にはそろそろ次の課題に進んでも良い頃かも。

ソース

一応、ソースも貼り付け。
はてな記法を使ってなかったので面倒臭かったのですが、これだと楽チンですね。

/*
my pwm test
*/
#include "mbed.h"
//
float smpl_frq  = 11025;               //11kHz
float smpl_rate = 256;                 //8bit
float clk       = 1000000 / smpl_frq;  //micro_second > 1/ smpl_frq; 
float tone_frq;
float pcm_dat[256];

Ticker timer;
Serial pc(USBTX,USBRX);
PwmOut   snd_out (dp18);      //set dp18 pin -> sound out

// oscillator
//
float duty_pls(float t_clk)
{
    static int   cnt;
           float r_duty;
    cnt = cnt + 2;
    if (cnt > t_clk) {
        cnt = 0;
        }
    if (cnt < t_clk / 2) {
        r_duty = 1.0;
        } else {
        r_duty = 0.0;
        }
    return r_duty;
}

float duty_saw(float t_clk)
{
    static int   cnt;
           float r_duty;
    cnt = cnt + 1;
    if (cnt > t_clk) {
        cnt = 0;
        }
    r_duty = (t_clk - cnt) / t_clk;
    return r_duty;
}

float duty_tri(float t_clk)
{
    static int   cnt;
           float r_duty;
    cnt = cnt + 2;
    if (cnt > t_clk * 2) {
        cnt = 0;
        }
    if (cnt < t_clk) {
        r_duty = cnt / t_clk;
        } else {
        r_duty = ((t_clk * 2) - cnt) / t_clk;
        }
    return r_duty;
}

float duty_pcm(float t_clk)
{
    static int table_cnt;
           float r_duty;
    table_cnt = table_cnt + floor(smpl_rate / t_clk);
    if (table_cnt > smpl_rate) {
        table_cnt = table_cnt - floor(smpl_rate);
        }
    r_duty = pcm_dat[table_cnt] / smpl_rate;
    return r_duty;
}

// synthesize
//
float dco(int dco)
{
    float duty2clk;
    float tone2clk;
    tone2clk = smpl_frq / tone_frq ;
    switch (dco) {
        case 1 :
            duty2clk = duty_pls(tone2clk) * clk;
            break;
        case 2 :
            duty2clk = duty_saw(tone2clk) * clk;
            break;
        case 3 :
            duty2clk = duty_tri(tone2clk) * clk;
            break;
        case 4 :
            duty2clk = duty_pcm(tone2clk) * clk;
            break;
        default :
            duty2clk = 0.0;
    }
    return duty2clk;                 
}

int main() {
    printf("smpl_frq  =%fHz\n",smpl_frq);
    printf("smpl_rate =%f\n",smpl_rate);
    printf("1clk      =%fms\n",clk);
    snd_out.period_us (clk);
//
//  initialize
    printf("pcm_sin initialize...");
    for (int i = 0; i < smpl_rate; i++){
            pcm_dat[i] = ((sin(2 * 3.1415 * i / smpl_rate) / 2) + 0.5) * smpl_rate;
        }
    printf("finish.\n");
//
//  sound play
    printf("***** start! *****\n");
    while(1) {
//  pulse
            printf("pulse\n");
            for (int i = 0; i < 8; i++){
                tone_frq = i * 110 + 110;
                for (int j = 0; j < smpl_frq / 16; j++){
                    snd_out.pulsewidth_us(dco(1));            
                    wait_us(clk);
                }
            }
//  saw
            printf("saw\n");
            for (int i = 0; i < 8; i++){
                tone_frq = i * 110 + 110;
                for (int j = 0; j < smpl_frq / 16; j++){
                    snd_out.pulsewidth_us(dco(2));            
                    wait_us(clk);
                }
            }
//  triangle
            printf("triangle\n");
            for (int i = 0; i < 8; i++){
                tone_frq = i * 110 + 110;
                for (int j = 0; j < smpl_frq / 16; j++){
                    snd_out.pulsewidth_us(dco(3));            
                    wait_us(clk);
                }
            }
//  pcm_sin
//
            printf("pcm_sin\n");
            for (int i = 0; i < 8; i++){
                tone_frq = i * 110 + 110;
                for (int j = 0; j < smpl_frq / 16; j++){
                    snd_out.pulsewidth_us(dco(4));            
                    wait_us(clk);
                }
            }
    }
}