ここでは,より文字と音の扱いかたを,実例をとおして,解説していきます.下のリンクから ofSozai2.zip をダウンロードし,必要に応じて,プロジェクトごとの bin/data フォルダにコピーしてください.
- このページで利用するおもな素材:ofSozai2.zip
「oF 応用編(1)」の【画像の内部表現】のところで,ofDrawBitmapString() を使って文字列を表示する例をみました.右図のように,ドット状のフォントで,英数字を画面上に描画するというものでした.フォント(大きさが固定された等幅フォント)を変えられない点が不便であり,また見た目(表示の質)もイマイチでした.
スケーラブルなフォントを表示するには,ofTrueTypeFont クラスが便利です.英数字(いわゆる ASCII 文字)の表示であれば簡単です.TrueType フォントを,しかもアンチエイリアシング(ギザギザ感を低減させる処理)を加えて,画面上に描画することができます.
まず emptyExampe をコピーして,dojoEample13 をつくります.data/bin フォルダに利用したいフォント(ttf, ttc, otf などの拡張子をもつフォントファイル)を入れておきます.たとえば,Windows であれば,コンピュータ > ローカルディスク (C:) > Windows > Fonts の中から,Arial を dojoExample13/bin/data にコピーします.すると,arial.ttf などいくつかのフォントファイルが作られます.Mac OS であれば,Macintosh HD > Library > Fonts に Arial.ttf などがあります.
このフォントファイルを ofTrueTypeFont クラスで利用するには,つぎのようなプログラムを用意します.
class ofApp : public {
private:
ofTrueTypeFont font;
public:
(... 変更なし ...)
};
void ofApp::setup () { ofSetWindowShape(600, 400); ofSetFrameRate(30); ofSetBackgroundColor(0, 0, 0); // load font font.load("arial.ttf", 72); } void ofApp::update () { } void ofApp::draw () { ofSetColor(255, 255, 0); font.drawString("Koziken", 100, 200); }
font は ofTrueTypeFont クラスのインスタンスで,そのメソッド loadFont() を呼び出すことで,フォントファイルを読み込むことができます.これを ofApp::setup() の中で,最初に一度だけ実行するとよいでしょう.loadFont() の第2引数は,フォントの高さ(ピクセル)を指定します.たとえば,プログラムをつぎのように改変すると,そのフォントの高さがよくわかります.
void ofApp::setup () {
(... 変更なし ...)
}
void ofApp::update () {
}
void ofApp::draw () {
ofSetColor(255, 255, 0);
font.drawString("Koziken", 100, 200);
ofSetColor(255, 0, 0);
ofDrawLine(0, 200, 600, 200);
ofDrawLine(0, 200-72, 600, 200-72);
ofDrawLine(100, 0, 100, 600);
}
発展課題1: 大きさ(高さ)を変える場合は,あらかじめ ofTrueTypeFont のインスタンスを複数用意し,異なる高さを与えて loadFont() メソッドを呼び出すとよいでしょう.
class ofApp : public {
private:
ofTrueTypeFont font1, font2;
public:
(... 変更なし ...)
};
void ofApp::setup () { ofSetWindowShape(600, 400); ofSetFrameRate(30); ofSetBackgroundColor(0, 0, 0); // load font font1.load("arial.ttf", 72); font2.load("arial.ttf", 24); } void ofApp::update () { } void ofApp::draw () { ofSetColor(255, 255, 0); font1.drawString("Koziken", 100, 200); font2.drawString("Tohoku Univ 2018", 100, 250); }
発展課題2: 字体(ボールド体など)を変える場合も,あらかじめ ofTrueTypeFont のインスタンスを複数用意し,異なるフォントファイルを loadFont() メソッドで読み込みます.
void ofApp::setup () { ofSetWindowShape(600, 400); ofSetFrameRate(30); ofSetBackgroundColor(0, 0, 0); // load font font1.load("arialbd.ttf", 72); font2.load("arial.ttf", 72); } void ofApp::update () { } void ofApp::draw () { ofSetColor(255, 255, 0); font1.drawString("Koziken", 100, 200); font2.drawString("Tohoku 2018", 20, 300); }
発展課題3: アプリのタイトルなどは画面の中央にセンタリングして表示したいところです.stringWidth(), stringHeight() メソッドによって,文字列を画面に表示したときの大きさを調べることができます.つぎのプログラムを動かし,ウィンドウをリサイズして,その効果を確かめてください.
void ofApp::draw () {
ofSetColor(255, 255, 0);
float w = fontL.stringWidth("Koziken"),
h = fontL.stringHeight("Koziken");
float x = ofGetWidth() / 2 - w / 2,
y = ofGetHeight() / 2 + h / 2;
fontL.drawString("Koziken", x, y);
}
上にあげた ofTrueTypeFont クラスでは,ちょっと工夫をすることで日本語(非 ASCII 文字)を表示することができます.つぎのプログラムを参考にしてください.
void ofApp::setup () { ofSetWindowShape(600, 400); ofSetFrameRate(30); ofSetBackgroundColor(0, 0, 0); // for Japanese Text ofTrueTypeFontSettings settings("meiryob.ttc", 32); settings.addRanges(ofAlphabet::Latin); settings.addRanges(ofAlphabet::Japanese); font.load(settings); } void ofApp::update () { } void ofApp::draw () { ofSetColor(255, 255, 0); font.drawString(u8"こじ研といえば小嶋研究室", 50, 200); }
発展課題1: 文字の周りを縁取りすることで,明るい背景に描画しても読みやすい文字を実現できます.たとえば,つぎのような処理が考えられます.(注意:実行環境によっては,文字ザイズ(縦ピクセル数)を大きくすると実行時エラーとなるようです.32ピクセル程度を上限とするとよいでしょう.)
void ofApp::setup () { ofSetWindowShape(600, 400); ofSetFrameRate(30); ofSetBackgroundColor(255, 255, 255); // for Japanese Text ofTrueTypeFontSettings settings("meiryob.ttc", 32); settings.addRanges(ofAlphabet::Latin); settings.addRanges(ofAlphabet::Japanese); font.load(settings); } void ofApp::update () { } void ofApp::draw () { int posX = 50, posY = 200; ofSetColor(50, 50, 50); for (int y = -3; y < 3; y++) { for (int x = -3; x < 3; x++) { font.drawString(u8"こじ研といえば小嶋研究室", posX + x, posY + y ); } } ofSetColor(255, 255, 0); font.drawString(u8"こじ研といえば小嶋研究室", posX, posY); }
発展課題2: 中国語や韓国語の文字も表示することができます.つぎのプログラムを参考にしてください.
void ofApp::setup () { ofSetWindowShape(600, 400); ofSetFrameRate(30); ofSetBackgroundColor(0, 0, 0); // for Chinese Text ofTrueTypeFontSettings settings("msyh.ttc", 32); settings.addRanges(ofAlphabet::Latin); settings.addRanges(ofAlphabet::Chinese); font.load(settings); } void ofApp::update () { } void ofApp::draw () { ofSetColor(255, 255, 0); font.drawString(u8"简体字汉语", 100, 200); }
「oF 入門編(1)」の『マウスアプリに効果音を加える』のところで,ofSoundPlayer クラスを使って,効果音をアプリに加えてみました.しかし,ofSoundPlayer は比較的短い音を収録した音声ファイル(wav 形式など)の再生しかできません.リアルタイムに音を変化させたり,ゼロから音を生成したり,あるいはマイクから音を収録したりすることはできませんでした.
ここでは,音ストリームを柔軟に扱うことができる ofSoundStream クラスについて解説します.ofSoundStream クラスによって,音の入出力や,計算による音の変換や生成が可能となります.
つぎのプログラムは,880Hz (A5) の正弦波を計算によって生成し,スピーカから再生するものです.新しく dojoExample15 をつくって,その動作を確かめてください.
class ofApp : public ofBaseApp { private: float soundFreq; float phase; public: void audioRequested(float *buf, int bufSize, int nChan); (... 変更なし ...) };
void ofApp::setup () { ofSetWindowShape(600, 400); ofSetFrameRate(30); ofSetBackgroundColor(0, 0, 0); // [osc~ 880] soundFreq = 880; phase = 0; ofSoundStreamSetup(2, 0, this, 44100, 1024, 4); } void ofApp::audioRequested (float *buf, int bufSize, int nChan) { float phasePerSample = TWO_PI * soundFreq / 44100; for (int i = 0; i < bufSize; i++) { phase += phasePerSample; while (phase > TWO_PI) phase -= TWO_PI; float value = sin(phase); buf[i * nChan ] = value; buf[i * nChan + 1] = value; } } void ofApp::update () { } void ofApp::draw() { }
ofSoundStreamSetup(2, 0, this, 44100, 1024, 4) では,出力2チャネル,入力チャネルなし,コールバックは自分(this=ofApp),標本化周波数(サンプリングレート)は 44100Hz として,音出力機能をオンにしています.また,入出力バッファは 1024 サンプルのものを4つ用意しています.これ以降,システム側で音声出力の準備ができるたびに,ofApp::audioRequested(float *buf, int bufSize, int nChan) というメソッドが呼び出されますので,その中で,bufSize サンプル分(この場合は 1024 サンプル分)の波形データを,ofSoundStream に入力します.ofSoundStream は,波形データの再生を開始し,つぎの波形データの入力が可能になった時点で,再び ofApp::audioRequested() を呼び出します.
ofApp::audioRequested(float *buf, int bufSize, int nChan) の中では,現在の位相 phase から始めて,bufSize サンプル分だけの波形データを,buf に格納するようにします.buf に入れる波形データは,左・右の順に,bufSize サンプル(左・右の組の数)だけ連続した実数値 (float) です.呼び出しごとに phase が進むようにしています.
発展課題1: マウスをクリックして,音の高さ変えられるように改造してみます.以下の部分を書き足してください.
void ofApp::mousePressed (int x, int y, int button) { soundFreq = 500 + y; } void ofApp::mouseDragged (int x, int y, int button) { soundFreq = 500 + y; }
発展課題2: 音の周波数を表示するように改造してみます.以下の部分を書き足してください.
void ofApp::draw() {
ofSetColor(255, 255, 0);
ofDrawBitmapString(ofToString(soundFreq), 20, 20);
}
正弦波の出力装置をクラス Osc にまとめます.そのインスタンスを複数生成することで,音の重ね合わせが実現できます.クラスの作り方は「oF 入門編(1)」の後半にある『クラスとインスタンス』を参考にして,dojoExample16 として作成してください.(ヒント:プロジェクト > 新しい項目の追加 > C++ ファイル,名前は osc,場所は src として「追加」.同様にヘッダファイルも「追加」.)
#pragma once #include "ofMain.h" class Osc { private: float soundFreq, soundAmp; float phase, phasePerSample; public: void init(float freq, float amp); void change(float freq, float amp); void make(float *buf, int bufSize, bool overlay); };
#include "osc.h" void Osc::init (float freq, float amp) { phase = 0; change(freq, amp); } void Osc::change (float freq, float amp) { soundFreq = freq; soundAmp = amp; phasePerSample = TWO_PI * soundFreq / 44100; } void Osc::make (float *buf, int bufSize, bool overlay) { for (int i = 0; i < bufSize; i++) { phase += phasePerSample; while (phase > TWO_PI) phase -= TWO_PI; float value = soundAmp * sin(phase); if (overlay) { buf[i * 2 ] += value; buf[i * 2 + 1] += value; } else { buf[i * 2 ] = value; buf[i * 2 + 1] = value; } } }
Osc クラスは,周波数と振幅(0〜1)を与えて初期化する init() メソッド,周波数と振幅を変更する change メソッド,波形データを生成する make() メソッドを持ちます.make() メソッドは,overlay が false のときは新しい波形データを上書きし,true のときは現在の値に新しい波形データを加算(重ね合わせ)します.この Osc クラスを利用して,ド (C) とソ (G) を重ねた正弦波を生成・出力するには,ofApp.h と ofApp.cpp をつぎのように用意します.
#pragma once #include "ofMain.h" #include "osc.h" class ofApp : public ofBaseApp { private: Osc osc1, osc2; public: void audioRequested(float *buf, int bufSize, int nChan); (... 変更なし ...) };
void ofApp::setup () { ofSetWindowShape(600, 400); ofSetFrameRate(30); ofSetBackgroundColor(0, 0, 0); // [osc~ "C"] and [osc~ "G"] osc1.init(261.6, 0.5); osc2.init(391.9, 0.5); // sound stream ofSoundStreamSetup(2, 0, this, 44100, 1024, 4); } void ofApp::audioRequested (float *buf, int bufSize, int nChan) { osc1.make(buf, bufSize, false); osc2.make(buf, bufSize, true); } void ofApp::update () { } void ofApp::draw () { }
発展課題: 画面をタッチした位置に応じて,2つの正弦波の高さが変わるように改造してみます.以下の部分を追加してください.
void ofApp::mousePressed (int x, int y, int button) { osc1.change(x + 200, 0.5); osc2.change(y + 200, 0.5); } void ofApp::mouseDragged (int x, int y, int button) { osc1.change(x + 200, 0.5); osc2.change(y + 200, 0.5); }
マイクから音を入力し,その波形を配列変数に格納することができます.ここでは,その波形を画面に描画するプログラムをつくってみましょう.
#define BUFSIZE 512 class ofApp : public ofBaseApp { private: float buffer[BUFSIZE]; int bufferSize; public: void audioReceived(float *buf, int bufSize, int nChan); (... 変更なし ...) };
void ofApp::setup () { ofSetWindowShape(600, 400); ofSetFrameRate(30); ofSetBackgroundColor(0, 0, 0); // [adc~] bufferSize = 0; ofSoundStreamSetup(0, 1, this, 44100, BUFSIZE, 4); } void ofApp::audioReceived (float *buf, int bufSize, int nChan) { bufferSize = bufSize; for (int i = 0; i < bufferSize; i++) buffer[i] = buf[i]; } void ofApp::update () { } void ofApp::draw () { for (int i = 0; i < bufferSize; i++) { int dy = buffer[i] * 100; ofSetColor(255, 127, 127); ofDrawLine(i, 200, i, 200 - dy); } }
ofSoundStreamSetup(0, 1, this, 44100, 512, 4) では,出力チャネルなし,入力1チャネル(iPhone のマイクはモノラル...たぶん),コールバックは自分(this=ofApp)として,音入力機能をオンにしています.これ以降,最大 512 サンプルの波形データ(約 11.6ms 相当)が用意されるたびに,ofApp::audioReceived(float *buf, int bufSize, int nChan) というメソッドが呼び出されますので,その中で,bufSize サンプル(最大 512 サンプル)分の波形データを処理します.波形データは,−1〜1 の実数 (float) からなる配列です.
ofApp::audioReceived(float *buf, int bufSize, int nChan) の中では,bufSize サンプル分(最大 512 サンプル)分の波形データを,実数配列 buffer にコピーしています.その波形データは,ofApp::draw() によって画面に描画されるようにしています.
発展課題: ofApp::draw() をつぎのように改造して,クチパクするようにしてみてください.顔のデザインはご自由に.
void ofApp::draw(){ // the eyes ofSetColor(255, 255, 255); ofDrawEllipse(210, 130, 140, 50); ofDrawEllipse(390, 130, 140, 50); ofSetColor(0, 0, 0); ofDrawCircle( 210, 130, 30); ofDrawCircle( 390, 130, 30); // loudness float sum = 0; for (int i = 0; i < bufferSize; i++) sum += buffer[i] * buffer[i]; float rms = sqrt(sum / bufferSize); float mag = ofClamp(rms * 10, 0, 1); float height = mag * 120 + 80; // moving mouth ofSetColor(255, 127, 127); ofDrawEllipse(300, 280, 300, height); ofSetColor(0, 0, 0); ofDrawEllipse(300, 280, 240, height - 60); }
rms は 波形データの平均レベル(512 サンプルについての二乗平均平方根),mag はそれを 10倍に拡大して 0〜1 区間に制限したもの,そして height は口の開き(高さ)を表わしています.