モジュラー形式でFM

「正弦波の周波数を正弦波で変調してみる」こういうのモジュラー形式とかいうそうで。
願望としてtestApp.cppのsetup()に以下のように書きたい。

SinOsc* mod = new SinOsc(2.0f, 22.0f); //コントロールする側
SinOsc* osc = new SinOsc(440.0f, 1.0f, mod);  //音を出す側

考えなおして「正弦波の周波数を変調する正弦波の周波数を変調する正弦波」とかわけわからない文章でも

SinOsc* hoge = new SinOsc(22.0f, 2.0f); //コントロールする側をコントロールする側
SinOsc* mod = new SinOsc(hoge, 22.0f); //コントロールする側
SinOsc* osc = new SinOsc(440.0f, 1.0f, mod);  //音を出す側

3行です。
昨日色々書いていたtestApp::audioRequested()もスッキリします。もちろん願望です。

void testApp::audioRequested(float* output, int bufferSize, int nChannels){
  for(int i=0; i<bufferSize; i++){
    output[i] = osc->updata();
  }
}

それでは実現させましょう。
main.cppは昨日と同じで省略。最初はtestApp.hから。昨日と同じところはザックリ省略してます。

#include "ofMain.h"
#include "sinOsc.h"

class testApp : public ofBaseApp{
  public:
    void setup();
    void update();
    void draw();

    //昨日と同じなので省略

    void exit(); //動的メモリを解放するつもりで追加   

    void audioRequested(float* input, int bufferSize, int nChannels);

    SinOsc* mod; //コントロールするオシレータ
    SinOsc* osc; //周波数がコントロールされる方のオシレータ
};

次にtestApp.cppです。

#include "testApp.h"

void testApp::setup(){
  ofBackground(255,255,255);

  mod = new SinOsc(1.0f, 220.0f); //イメージ通り
  osc = new SinOsc(440.0f, 0.0f, mod);  //イメージ通り

  ofSoundStreamSetup(1,0,this,44100,256,4);

  ofSetFrameRate(60);
}
void testApp::update(){}
void testApp::draw(){}

//省略

void testApp::exit(){
  delete mod, osc; //動的なメモリを開放します
}

//省略

void testApp::mouseDragged(int x, int y, int button){
  int width = ofGetWidth();
  mod->setfreq( 30.0f * (x/(float)width) + 1);  //コントロールする周波数を変更
}
void testApp::mousePressed(int x, int y, int button){
  osc->setamp( 1.0f );  //オシレータのする振幅を変更
}
void testApp::mouseReleased(int x, int y, int button){
  osc->setamp( 0.0f );
}

//省略

void testApp::audioRequested(float* output, int bufferSize, int nChannels){
  for(int i=0; i<bufferSize; i++){
    output[i] = osc->updata(); //イメージ通り
  }
}

ここからは、この通りにいくようなクラスをデザイン。

  1. Sndクラスはサンプリングレートなどの変数を持たせる。また一定の値を出力するオシレータ機能も。
  2. Genクラスは音を生成するクラスのひな形。これを正弦波や矩形波などが継承。
  3. SinOscクラスは正弦波を生成する。sinOsc::updataに処理を書く。
Snd <- Gen <- SinOsc //こんな感じに継承

それではsnd.hから。読んでの通りサンプリングレートや円周率の2倍のメンバ変数がある。さらにコンストラクタがオーバーロードになっていて、引数が与えられた場合はその値を出力するオシレータ、なければゼロを出力する。これは周波数の値に正弦波そのものか数値を与えるのかデザインを変えずに時々で選べるようにしている。

#ifndef __SND_H__
#define __SND_H__
class Snd{
  protected:
    float sr; //サンプリングレート
    float TWOPI; //2*PI。組み込み定数を使うべきだがいまはこのまま。
    float number; //出力する値

  public:
    Snd();
    Snd(float n); //数値を与えられた場合はその定数を吐き出すオシレータになる。
    ~Snd();
    virtual float updata();
};
#endif  // __SND_H__

続いてsnd.cpp。Snd::updataをみれば定数をずっと出力するジェネレータだとわかるでしょう。

#include "snd.h"

Snd::Snd(){
    sr = 44100.0f;
    TWOPI = 2*3.1415926535897;
    number = 0.0f;
}
Snd::Snd(float n){
    sr = 44100.0f;
    TWOPI = 2*3.1415926535897;
    number = n;
}
Snd::~Snd(){
}
float Snd::updata(){
    return number; //コンストラクタに引数を指定しないとゼロを返す。
}

さてgen.hです。ココらへんの説明はいらないでしょう。

#ifndef __GEN_H__
#define __GEN_H__

#include "snd.h"
class Gen: public Snd{
  protected:        
    float phase;
    float freq;
    float amp;
    Snd* addfreq;
    Snd* addamp;
        
  public:        
    Gen(float f, float a);
    Gen(float f, float a, Snd* addf);
    Gen(float f, float a, Snd* addf, Snd* adda);
    Gen(float f, float a, float addf, Snd* adda);
    ~Gen();
    void setfreq(float f);
    void setamp(float a);
    float getfreq();
    float getamp();
    float updata();
};
#endif  // __GEN_H__

急いでgen.cpp。ここがポイントです。引数が2つしかないコンストラクタに注目して、メンバ変数addfreqやaddampはSndクラスのインスタンス変数が生成されそれらに代入。これは0を出力する。引数が3つある場合はメンバ変数addfreqはSndのポインタが代入されてます。一方でaddampはインスタンス変数が代入。

#include "gen.h"
#include <assert.h>

Gen::Gen(float f, float a){
    phase = 0.0f;
    freq = f;
    amp = a;
    addfreq = new Snd(0.0f);
    addamp = new Snd(0.0f);
}
Gen::Gen(float f, float a, Snd* addf){
    phase = 0.0f;
    freq = f;
    amp = a;
    addfreq = addf;
    addamp = new Snd(0.0f);
}
Gen::Gen(float f, float a, Snd* addf, Snd* adda){
    phase = 0.0f;
    freq = f;
    amp = a;
    addfreq = addf;
    addamp = adda;
}
Gen::Gen(float f, float a, float addf, Snd* adda){
    phase = 0.0f;
    freq = f;
    amp = a;
    addfreq = new Snd(addf);
    addamp = adda;
}
Gen::~Gen(){
    delete addfreq;
    delete addamp;
}
void Gen::setfreq(float f){freq=f;}
void Gen::setamp(float a){amp=a;}
float Gen::getfreq(){
    return (freq + addfreq->updata());
}
float Gen::getamp(){
    return (amp + addamp->updata());
}
float Gen::updata(){
    return 0;
}

最後にsinOsc.h

#ifndef __SINOSC_H__
#define __SINOSC_H__

#include "gen.h"

class SinOsc: public Gen{
    public:
    SinOsc(float f, float a);
    SinOsc(float f, float a, Snd* addf);
    SinOsc(float f, float a, Snd* addf, Snd* adda);
    SinOsc(float f, float a, float addf, Snd* adda);
    ~SinOsc();
    float updata();
};
#endif  // __SINOSC_H__

本当の最後にsinOsc.cpp

#include "sinOsc.h"
#include <assert.h>
#include <math.h>

SinOsc::SinOsc(float f, float a):Gen(f, a){}
SinOsc::SinOsc(float f, float a, Snd* addf):Gen(f, a, addf){}
SinOsc::SinOsc(float f, float a, Snd* addf, Snd* adda):Gen(f, a, addf, adda){}
SinOsc::SinOsc(float f, float a, float addf, Snd* adda):Gen(f, a, addf, adda){}
SinOsc::~SinOsc(){}
float SinOsc::updata(){
  phase += getfreq()/sr;
  if(phase > 1.0){
    phase = 0.0;
  }
  return sin(TWOPI*phase)*getamp();
}

冗長なところあるでしょうがこのへんで。