波形の生成

波形の生成 #

Linuxでの波形生成 #

この章ではLinuxでOpenALを使うための基本的な手順について紹介します。使用する言語はC言語です。

コンピュータが音を再生するために #

コンピュータで音を再生するためには、最終的にスピーカーあるいはヘッドフォン端子に電気信号が送られなければなりません。それらは大抵サウンドカードと呼ばれる基板に繋がっています。しかしプログラムはハードウェアを直接操作することはできません。

サウンドドライバは音声の入出力するためにハードウェアを制御するソフトウェアです。オペレーティングシステムはこれを用いて音声データを読み書きします。そしてプログラマの使用できるAPIを提供しています。例えばWindowsはWin32マルチメディアAPIを、MacOSXはCore Audio Frameworksを、LinuxはALSAを、といった具合です。

それらのAPIはプログラマにとってサウンドプログラミングをするもっとも原始的なものであり、必要に応じてもっと抽象的なAPIが求められて備えられてきました。そのうちの1つがOpenALです。

OpenALとは #

OpenALは多彩なサウンド処理を行なうためのライブラリです。さまざまなオペレーティングシステムに対応しているため、同じコードを使って異なるハードウェアのサウンド機能を呼び出すことができます。コンピュータのほかに任天堂のWiiやソニーのPlayStation3といったゲーム機、iOSやAndroidといったモバイル機器でも採用されています。

OpenALは元々、WindowsのゲームをLinuxに移植しやすくするために開発されたものです。そのため3Dゲームで頻繁に使用されるOpenGLにとてもよく似ています。OpenALの開発は、開発元はコンピュータゲームの移植を行っていたLoki Softwareで、現在はクリエイティブテクノロジーが担っています。

OpenALの基本構成 #

OpenALプログラムを構成する3つの主要な要素がBuffer・Source・Listenerです。Bufferは音声データを管理します。SourceはBufferのデータを再生します。Listenerは音の検出点であり、Sourceと合わせて3D空間上に配置することができ、音の聞こえ方をシミュレートすることができます。

留意すべき点として、OpenALはMP3やAACなどの圧縮フォーマットに対応していません。それらのフォーマットを用いたい場合は、別途ライブラリを利用する必要があります。

OpenALの準備 #

OpenALをインストールする #

LinuxでOpenALを使ったプログラムを作るために、最初にOpenALとALUTをインストールする必要があります。ALUTはOpenALのライブラリで、簡単プログラムを作ったり学習したりなど、開発者を支援するものとして提供されています。

インストールするために、まず端末を起動します。それからUbuntuやLinuxMintなどDebian系のディストリビューションであれば、次のように入力してインストールを開始してください。

sudo apt-get install libopenal-dev libalut-dev

最初パスワードを求められるので、コンピュータのパスワードを入力してください。また途中で操作を続行するか問われるので、Y を入力してインストールを続行します。

これで、LinuxでOpenALを使用できる環境が整ったはずです。

プリプロセッサの書き方 #

LinuxとMacOSXではOpenALの場所が異なるので注意が必要です。Linuxでは通常 /usr/include/AL/al.h という風に AL 内にヘッダファイルが置かれますが、MacOSXの場合は OpenAL/al.h という風に OpenAL 内にファイルがあります。

これを踏まえつつ altest.c というファイルを作成します。プログラムの冒頭に次のプリプロセッサを挿入しましょう。

# include <AL/al.h>
# include <AL/alc.h>

コンパイルの仕方 #

ここではGCCを使います。最初に端末を起動しましょう。次にcdコマンドでコンパイルしたいプログラムのあるディレクトリに移動します。それから次のように入力します。

gcc altest.c -lalut -lopenal -o altest

OpenALの機能を使うために -lalut-lopenal の2つのライブラリを指定しています。コンパイルに成功すれば、altest という実行ファイルが生成されます。

OpenALを使ったプログラム #

OpenALの初期化 #

OpenALを使うプログラムを実際に書いていきましょう。まずは初期化処理を書く必要があります。最初にOpenALデバイスを開き、次にOpenALコンテキストを作成します。それから操作に使うコンテキストも選択しなければなりません。

ALCdevice *device;
ALCcontext *context;
device = alcOpenDevice(NULL);
context = alcCreateContext(device, NULL);

プログラムに出てくるALCはOpenALのコンテキスト管理のためのAPIです。この例では alcOpenDevice 関数を使ってプログラムがデバイスに接続しています。またalcCreateContext でコンテキストを生成しています。もし関数の実行でエラーがあった場合はNULLが返ります。

次いで alcMakeContextCurrent 関数を使って、操作に使うコンテキストを選択します。この関数の戻り値は ALC_TRUE の場合は成功で、ALC_FALSE の場合はエラーを意味します。

alcMakeContextCurrent(context);

これでOpenALの初期化が完了しました。次に波形データを生成するコードを書いていきましょう。

ホワイトノイズの波形データを生成する #

前回の記事では、基本的な波形を生成する方法を紹介しました。今回はそこで扱われなかったホワイトノイズを生成するプログラムを作成します。

ホワイトノイズは乱数を使って作ります。C言語には乱数を得るために rand 関数が用意されていますが、任意の範囲を指定することができません。それで以下のような関数を作ります。

int rnd(int min, int max) {
	return min + (int)(rand() * (max-min + 1.0) / (1.0 + RAND_MAX));
}

この rnd 関数は、第一引数に指定した数値から第二引数に指定した数値までの乱数を整数で取得します。

この関数を用いてホワイトノイズの波形データを生成するプログラムを書いてみましょう。OpenALではALshort型の配列を波形データとして使用します。ALshort型はC言語のshort型と同等です。

ALshort data[44100*3]; //44.1kHで3秒のデータ

//ホワイトノイズを生成する
for(i=0; i < 44100*3; i++)
	data[i] = rnd(-32767, 32767);

このプログラムでは サンプリング周波数 * 3 つまり3秒のデータを作成しています。rnd 関数を使って小さい振幅から大きい振幅の間で乱数を取得して、すべての配列にデータを書き込んでいます。

バッファとソースを作成する #

alGenBuffersを使って任意の数のバッファを要求します。この関数はいつでも呼び出し可能で、複数呼び出したときは複数のバッファの組を生成します。

alGenBuffers(1, &buffer);

バッファの内容、つまりサンプルデータを設定するには alBufferData 関数を使います。使用できるフォーマットは次のとおりです。

  • AL_FORMAT_MONO8
  • AL_FORMAT_MONO16
  • AL_FORMAT_STEREO8
  • AL_FORMAT_STEREO16

8ビットデータは0から255の符号なし値で表現されます。128が無音の出力となります。16ビットデータは -32768 から 32767 の符号あり値で表現されます。0が無音の出力です。

1チャネル以上のオーディオデータを含むバッファは3D空間化機能なしで再生されます。そのため、それらの形式はBGMに向いていると言えます。

void alBufferData(ALuint buffer,ALenum format,const ALvoid *data,ALsizei size,ALsizei freq);

それぞれ次のように設定します。

  • buffer – バッファ
  • format – データのフォーマット
  • data – サンプルデータ
  • size – データのサイズ
  • freq – 周波数
alBufferData(buffer, AL_FORMAT_MONO16, data, sizeof(data), 44100);

次にソースを作成します。ソースは、位置や速度、サンプルデータを伴うバッファのような属性を持ちます。 alGensources を使って任意の数のソースを要求します。

alGenSources(1, &source);

ソースの各種設定は alSourcei で行います。alSourcei(ソース、設定したい項目を表す定数、設定値); というように設定します。

alSourcei(source, AL_BUFFER, buffer);

ソースを再生する #

プログラムは、alGetSource とパラメータ AL_SOURCE_STAT を使用して、現在のソースのステートを取得することができます。現在のステートは AL_INITIALAL_PLAYINGAL_PAUSEDAL_STOPPED の4つのうちの1つでしょう。AL_PLAYINGAL_PAUSED であればソースは有効であり、 AL_STOPPEDAL_INITIAL のどちらかであればソースは無効であると考えられます。既定のステートは INITIAL です。

ソースを操作する以下のような関数があります。

  • void alSourcePlay (ALuint sName);
  • void alSourcePause (ALuint sName);
  • void alSourceStop (ALuint sName);
  • void alSourceRewind (ALuint sName);

ソースを再生するには以下のようにします。

alSourcePlay(source);

プログラムがすぐに終了してしまわないように、 sleep 関数を用いて指定の時間スリープさせます。

sleep(3);

その後、ソースに割り当てられているバッファを停止します。これは sourceAL_STOPED すなわち停止状態にします。

alSourceStop(source);

ソースの削除とバッファの解放 #

alDeleteSources を使って任意の数のソースの削除を要求できます。再生中のソースも削除可能です。ソースは自動的に停止され、そして削除されます。

alDeleteSources(1, &source);

alDeleteBuffers を使って任意の数のバッファの削除を要求できます。alIsBuffer(bname) はバッファの削除を証明するのに使う事ができます。ソースに割り当てられたバッファは削除不能です。

alDeleteBuffers(1, &buffer);

終了処理 #

コンテキストを破棄するためには、まずそのコンテキストを alcMakeCurrent 関数にNULLコンテキストを渡して解放する必要があります。そのようにしてコンテキストをカレントから解除しています。コンテキストに含まれる全てのソースはコンテキスト破棄時に自動的に削除されます。コンテキストの削除は alcDestroyContext 関数で行います。

alcMakeContextCurrent(NULL);
alcDestroyContext(context);

プログラムをデバイスから切断するには alcCloseDevice 関数を用います。成功すれば ALC_TRUE が、失敗したなら ALC_FALSE が返ります。一旦クローズするとコンテキストとソースは使えなくなります。

alcCloseDevice(device);

プログラム全文 #

これまでのコードをまとめると、次のようなプログラムになります。プログラムは前述したとおり、端末に gcc test.c -lalut -lopenal -o test と記入してコンパイルを実行します。

# include <AL/al.h>
# include <AL/alc.h>
# include <stdlib.h>

int rnd(int min, int max) {
	return min + (int)(rand() * (max  min + 1.0) / (1.0 + RAND_MAX));
}

int main() {
	int i;
	ALCdevice *device;
	ALCcontext *context;
	ALshort data[44100*3];
	ALuint buffer,source;
	device = alcOpenDevice(NULL);
	context = alcCreateContext(device, NULL);
	alcMakeContextCurrent(context);
	alGenBuffers(1, &buffer);
	for(i=0; i < 44100*3; i++)
	    data[i]=rnd(-32767, 32767);
	alBufferData(buffer, AL_FORMAT_MONO16, data, sizeof(data), 44100);
	alGenSources(1, &source);
	alSourcei(source, AL_BUFFER, buffer);
	alSourcePlay(source);
	sleep(3);
	alSourceStop(source);
	alDeleteSources(1, &source);
	alDeleteBuffers(1, &buffer);
	alcMakeContextCurrent(NULL);
	alcDestroyContext(context);
	alcCloseDevice(device);
	return 0;
}

おわりに #

この章ではLinuxでOpenALを使うための環境を整える手順と、OpenALの基本的な使い方について解説しました。

iOSでの波形生成 #

この章ではiOSでサウンドプログラミングをするために、OpenALを使用する方法を説明します。

準備 #

プロジェクトを作成する #

Xcodeを起動したら、メニューバーにある File > New > Project を選択します。次にテンプレートを選ぶよう促されるので Single View Application を選択して Next をクリックします。プロジェクトの情報を入力するように促す画面が出たら、Product Name: の欄にプロジェクト名を入力します。また、Language: は Swift を選択するようにします。

プロジェクトが作成されたら、左側のプロジェクトナビゲーターから自動生成されているViewController.swiftを選択して開きます。すると次のようなコードが表示されるはずです。

import UIKit

class ViewController: UIViewController {
	override func viewDidLoad() {
	    super.viewDidLoad()
	    // Do any additional setup after loading the view, typically from a nib.
	    //
	}
	
	override func didReceiveMemoryWarning() {
	    super.didReceiveMemoryWarning()
	    // Dispose of any resources that can be recreated.
	}
}

フレームワークを追加する #

OpenALの機能を使うために、次の2つフレームワークをプロジェクトに追加しましょう。

  • AudioToolbox.framework
  • OpenAL.framework

フレームワークを追加するには、プロジェクトナビゲーターから現在のプロジェクトを選択してメイン画面のGneralメニューを表示します。その一番下にある Linked Frameworks and Libraries の項目の +(Add items)を選択して、表示されるフレームワークから必要なものを選び Add をクリックします。

import文を追加する #

ソースコードに次のimport文を挿入します。

import OpenAL.AL
import OpenAL.ALC
import AudioToolbox
import Foundation

このうちサウンドに直接関係するのものはOpenAL.AL、OpenAL.ALC、AudiToolboxの3つです。

OpenALメソッドの日本語化 #

今回はプログラムの意味を理解しやすくするために、使用するメソッドをすべて日本語に置き換えてます。次のようなコードを追加します。


func ALCデバイスを開く(デバイス:UnsafePointer<ALCchar>) -> COpaquePointer 
	return alcOpenDevice(デバイス)
}

func ALCコンテキストを作成する(device: COpaquePointer, attrlist: UnsafePointer<ALCint>) -> COpaquePointer 
	return alcCreateContext(device,attrlist)
}

func 使用するALCコンテキストを指定する(context: COpaquePointer) -> ALCboolean 
	return alcMakeContextCurrent(context)
}

func ALバッファを作成する(n: ALsizei, buffers: UnsafeMutablePointer<ALuint>) 
	alGenBuffers(n,buffers)
}

func 波形データをALバッファに格納する(bid: ALuint, format: ALenum, data: UnsafePointer<Void>, size: ALsizei, freq: ALsizei) 
	alBufferData(bid,format,data,size,freq)
}

func ALソースを作成する(n: ALsizei, sources: UnsafeMutablePointer<ALuint>) 
	alGenSources(n,sources)
}

func ALバッファをALソースに格納する(sid: ALuint, param: ALenum, value: ALint) {
	alSourcei(sid, param, value)
}

func ALソースを再生する(sid: ALuint) {
	alSourcePlay(sid)
}

func ALソースを停止する(sid: ALuint) {
	alSourceStop(sid)
}

func ALソースを削除する(n: ALsizei, sources: UnsafePointer<ALuint>) 
	alDeleteSources(n, sources)
}

func ALバッファを削除する(n: ALsizei, buffers: UnsafePointer<ALuint>) 
	alDeleteBuffers(n, buffers)
}

func ALCコンテキストを削除する(context: COpaquePointer) {
	alcDestroyContext(context)
}

func ALCデバイスを閉じる(device: COpaquePointer) -> ALCboolean 
	return alcCloseDevice(device)
}

func サイン波のデータを生成する(inout data:[ALshort]) 
	for var i = 0; i < 22050; ++i {
	    var a:Double = sin(Double(i) * 3.14159 * 2 * 440 / 22050) * 32767
	    var b:Int16 = Int16(a)
	    var c:ALshort = ALshort(b)
	    data[i] = c
	}
}

OpenALはポインタを利用します。C言語では * や & などを用いてポインタを表現できますが、Swiftでは COpaquePointer や UnsafePointer などを使用していることに注意しましょう。

また追加した最後のメソッドは波形データを生成するコードです。Swiftでは引数で指定した変数をメソッド内で直接操作するために inout を使用します。このコードの場合では、配列変数dataに直接波形データを代入しています。

生成した波形データをOpenALで使用できるようにするために、Double型のデータを一旦Int16型に変換し、それからOpenALで使用する ALshort型に変換してから返しています。

波形データを生成する #

^OpenALの機能を使って、波形データを再生するコード追加します。

var 波形データ:[ALshort] = [ALshort](count: 22050, repeatedValue: 0)
var ALバッファ:ALuint = 0
var ALソース:ALuint = 0
var ALCデバイス:COpaquePointer
var ALCコンテキスト:COpaquePointer
ALCデバイス = ALCデバイスを開く( UnsafePointer(bitPattern: 0) )
ALCコンテキスト = ALCコンテキストを作成する(ALCデバイス, UnsafePointer(bitPattern: 0))

使用するALCコンテキストを指定する( ALCコンテキスト )
サイン波のデータを生成する( &波形データ )

var s = 波形データ.count // * sizeof(ALsizei)
var size:ALsizei = ALsizei(s)
var freq:ALsizei = ALsizei(Int32(22050))
ALバッファを作成する(1, &ALバッファ)
波形データをALバッファに格納する(ALバッファ, AL_FORMAT_MONO16, 波形データ, size, freq)
ALソースを作成する(1, &ALソース);
ALバッファをALソースに格納する(ALソース, ALenum(AL_BUFFER), ALint(ALバッファ))
ALソースを再生する(ALソース)

sleep(1)
ALソースを停止する(ALソース)
ALソースを削除する(1, &ALソース)
ALバッファを削除する(1, &ALバッファ)
使用するALCコンテキストを指定する( ALCコンテキスト )
ALCコンテキストを削除する( ALCコンテキスト )
ALCデバイスを閉じる( ALCデバイス )

これでiOSとOpenALを使用して音を再生するプログラムが完成したことになります。このプログラムでは440Hzのサイン波を1秒間再生します。

macOSでの波形生成 #

この記事ではSwiftとOpenALを使ったMacOSXで動作するプログラムを作成します。プログラムを実行すると、サイン波(正弦波)を合成して1秒間再生します。

プログラムを作成する #

MacOSXにはOpenALが最初から組み込まれています。また音のテストをするだけならフレームワークやライブラリを指定する必要もありません。ですから、MacOSX上でOpenALを使用するのは比較的容易であると言えるかもしれません。

プロジェクトを作成する #

Xcodeを起動しましょう。メニューバーにある File > New > Project を選択します。次にテンプレートを選ぶよう促されるので OSX の Application 項目にある Command Line Tool を選択して Next をクリックします。プロジェクトの情報を入力するように促す画面が出たら、Product Name: の欄にプロジェクト名を入力します。また、Language: は Swift を選択してください。

プロジェクトが作成されたら、左側のプロジェクトナビゲーターから自動生成されている main.swiftを選択して開きます。すると次のようなコードが表示されるはずです。

import Foundation

println("Hello, World!")

ここにコードを追加していく形でプログラムを作成していきます。

import文を追加する #

ソースコードに次のimport文を挿入しましょう。

import OpenAL.AL
import OpenAL.ALC
import AudioToolbox

日本語化したOpenALメソッドを追加する #

次に日本語化したメソッドを挿入します。(メソッドの日本語化は必須ではありません)。

//関数定義
func ALCデバイスを開く(デバイス:UnsafePointer<ALCchar>) -> COpaquePointer 
	return alcOpenDevice(デバイス)
}
func ALCコンテキストを作成する(device: COpaquePointer, attrlist: UnsafePointer<ALCint>) -> COpaquePointer 
	return alcCreateContext(device,attrlist)
}
func 使用するALCコンテキストを指定する(context: COpaquePointer) -> ALCboolean 
	return alcMakeContextCurrent(context)
}
func ALバッファを作成する(n: ALsizei, buffers: UnsafeMutablePointer<ALuint>) 
	alGenBuffers(n,buffers)
}
func 波形データをALバッファに格納する(bid: ALuint, format: ALenum, data: UnsafePointer<Void>, size: ALsizei, freq: ALsizei) 
	alBufferData(bid,format,data,size,freq)
}
func ALソースを作成する(n: ALsizei, sources: UnsafeMutablePointer<ALuint>) 
	alGenSources(n,sources)
}
func ALバッファをALソースに格納する(sid: ALuint, param: ALenum, value: ALint) {
	alSourcei(sid, param, value)
}
func ALソースを再生する(sid: ALuint) {
	alSourcePlay(sid)
}
func ALソースを停止する(sid: ALuint) {
	alSourceStop(sid)
}
func ALソースを削除する(n: ALsizei, sources: UnsafePointer<ALuint>) 
	alDeleteSources(n, sources)
}
func ALバッファを削除する(n: ALsizei, buffers: UnsafePointer<ALuint>) 
	alDeleteBuffers(n, buffers)
}
func ALCコンテキストを削除する(context: COpaquePointer) {
	alcDestroyContext(context)
}
func ALCデバイスを閉じる(device: COpaquePointer) -> ALCboolean 
	return alcCloseDevice(device)
}
func サイン波のデータを生成する(inout data:[ALshort]) 
	for var i = 0; i < 22050; ++i {
	    var a:Double = sin(Double(i) * 3.14159 * 2 * 440 / 22050) * 32767
	    var b:Int16 = Int16(a)
	    var c:ALshort = ALshort(b)
	    data[i] = c
	}
}

メイン処理を追加する #

最後にOpenALを使用したメイン処理を挿入します。

//メイン処理
var 波形データ:[ALshort] = [ALshort](count: 22050, repeatedValue: 0)
var ALバッファ:ALuint = 0
var ALソース:ALuint = 0
var ALCデバイス:COpaquePointer
var ALCコンテキスト:COpaquePointer
ALCデバイス = ALCデバイスを開く( UnsafePointer(bitPattern: 0) )
ALCコンテキスト = ALCコンテキストを作成する(ALCデバイス, UnsafePointer(bitPattern: 0))
使用するALCコンテキストを指定する( ALCコンテキスト )
サイン波のデータを生成する( &波形データ )
var s = 波形データ.count
var size:ALsizei = ALsizei(s)
var freq:ALsizei = ALsizei(Int32(22050))
ALバッファを作成する(1, &ALバッファ)
波形データをALバッファに格納する(ALバッファ, AL_FORMAT_MONO16, 波形データ, size, freq)
ALソースを作成する(1, &ALソース);
ALバッファをALソースに格納する(ALソース, ALenum(AL_BUFFER), ALint(ALバッファ))
ALソースを再生する(ALソース)
sleep(1)
ALソースを停止する(ALソース)
ALソースを削除する(1, &ALソース)
ALバッファを削除する(1, &ALバッファ)
使用するALCコンテキストを指定する( ALCコンテキスト )
ALCコンテキストを削除する( ALCコンテキスト )
ALCデバイスを閉じる( ALCデバイス )

これでMacOSXでOpenALを使用するプログラムが完成しました。

プログラムを実行する #

Xcodeのメニューバーにある Product > Run を実行してみましょう。1秒間サイン波が再生されたら成功です。

基本的な波形の生成 #

iOSとOpenALを使って基本的なサウンド波形を合成する方法を紹介します。サイン波・三角波・ノコギリ波・矩形波・パルス波の5つの波形を生成します。使用する言語はSwiftです。この記事では、すでにXcodeでOpenALを利用できる環境があることを想定しています。iOSでOpenALを使用するための方法については以前の記事を参照してください。

準備 #

プログラムで使用する関数 #

プログラム例では次の関数を使用しています。これは合成した波形の型を変換してOpenALで使えるようにするためのものです。

func Double型をALshortに変換する(入力値:Double) -> ALshort 
	return ALshort(Int16(入力値))
}

数式の記号とプログラムの変数 #

数式中で使用している記号は、主に次の6つです。

  • pi:円周率
  • f:信号の周波数
  • f_s:サンプリング周波数
  • A:信号の最大振幅
  • N:総サンプル数
  • n:離散時間

それぞれ次のような意味を持っています。

  • pi は円周率を表します。
  • f で表されている信号の周波数は生成する音の高さを指します。
  • f_s はサンプリング周波数であり、1秒間のサンプル数を表します。
  • n は1サンプルを構成する時間を表す離散時間です。
  • A は最大振幅、つまり最大の音の大きさを表します。アンプのAです。
  • N は総サンプル数を表します。音の長さは fracNf_s秒 となります。

これらの記号をプログラムで使用できるように、それぞれ変数に置き換えていきます。次のように表すことができます。

var 円周率:Double = 3.14159 //pi
var 信号の周波数:Double = 440 //f
var サンプリング周波数:Double = 22050 //fs
var 信号の最大振幅:Double = 10000 //amp
var 総サンプル数:Int = 22050 //N
var 離散時間:Int = 0 //n

var サンプルデータ:[ALshort] = [ALshort](count: 総サンプル数, repeatedValue: 0)

最後の行のサンプルデータは数式には登場しませんが、数式での関数 y(n) に対応するものと考えることができます。これは波形データそのものを表す、総サンプル数個の配列を持った変数です。このデータをOpenALに渡すことにより音声データを再生することができます。

基本的な5つの波形を生成する #

1.サイン波(正弦波)を生成する #

最初にサイン波を合成してみましょう。サイン波は次のような数式で表せます。

beginequation y(n)= beginarrayl Asin left( 2pi f fracnf_s right) endarray endequation tag1

右辺で表されている数式を次のようなプログラムに置き換えることができます。

sin(2.0 * 円周率 * 信号の周波数 * Double(i) / サンプリング周波数) * 信号の最大振幅

ここで登場するiは現在のサンプル位置を表します。これを使って波形生成の関数を作成しましょう。次のように書くことができます。

var 円周率:Double = 3.14159 //pi
var 信号の周波数:Double = 440 //f
var サンプリング周波数:Double = 22050 //fs
var 信号の最大振幅:Double = 10000 //amp
var 総サンプル数:Int = 22050 //N
var 離散時間:Int = 0 //n
var サンプルデータ:[ALshort] = [ALshort](count: 総サンプル数, repeatedValue: 0)

func サイン波のデータを生成する() {
	for var i = 0; i < 総サンプル数; ++i {
	    サンプルデータ[i] = Double型をALshortに変換する(
	        sin(2.0 * π * 信号の周波数 * Double(i) / サンプリング周波数) * 信号の最大振幅
	    )
	}
}

プログラム中でサイン波のデータを生成する()を実行することで、440Hzのデータが生成され、サンプルデータ[n]に格納されます。

2.三角波を生成する #

5つの波形のうち、最も計算が複雑なのが三角波の生成です。三角波は次の数式で表せます。

beginequation y(n)= left { beginarrayl 4Acdot fracff_scdot x'-A,hphantom-sqrt{-} & x'<fracf_s2f -4Acdot fracff_scdot x'+3A, & x’geq fracf_s2f endarray right. endequation tag2

ここで新たに x' 記号が登場しています。これはここではmod (n,fracf_sf)を表わします。modはプログラムではfmod(x,y)のように書くことができ、これは引数Xを引数Yで割った余剰を返すという意味です。数式の x' は時間を周期で割ったときの余りを表しています。

それでは数式をプログラムに置き換え、三角波を生成する関数を作ってみましょう。次のように書くことができます。

var 円周率:Double = 3.14159 //pi
var 信号の周波数:Double = 440 //f
var サンプリング周波数:Double = 22050 //fs
var 信号の最大振幅:Double = 10000 //amp
var 総サンプル数:Int = 22050 //N
var 離散時間:Int = 0 //n
var サンプルデータ:[ALshort] = [ALshort](count: 総サンプル数, repeatedValue: 0)

func 三角波のデータを生成する() {
	for var i = 0; i < 総サンプル数; ++i {
	    var tmp:Double = サンプリング周波数 / 信号の周波数
	    if fmod(Double(i), tmp) < サンプリング周波数 / (2 * 信号の周波数) {
	        サンプルデータ[i] = Double型をALshortに変換する(
	            4 * 信号の最大振幅 * (信号の周波数 / サンプリング周波数) * fmod(Double(i), tmp) - 信号の最大振幅
	        )
	    }
	    if fmod(Double(i), tmp) >= サンプリング周波数 / (2 * 信号の周波数) {
	        サンプルデータ[i] = Double型をALshortに変換する(
	            -4 * 信号の最大振幅 * (信号の周波数 / サンプリング周波数) * fmod(Double(i), tmp) + 3 * 信号の最大振幅
	        )
	    }
	}
}

三角波のデータを生成する()を実行するとサンプルデータ[n]に三角波のデータが格納されます。音そのものはサイン波と矩形波の中間ぐらいに聴こえます。

3.ノコギリ波(鋸波)を生成する #

ノコギリ波も x' を用いますが、三角波に比べると簡単な数式で表せます。

beginequation y(n)= beginarrayl 2Acdot fracff_scdot x'-A endarray endequation tag3

この数式には x' を使った分岐がありません。ノコギリ波形は、振幅が時間が経過するごとに斜めに増大し最大値に達したらゼロに戻る、という周期を繰り返します。この数式を参考にノコギリ波を生成するプログラムを作成してみましょう。次のように書くことができます。

var 円周率:Double = 3.14159 //pi
var 信号の周波数:Double = 440 //f
var サンプリング周波数:Double = 22050 //fs
var 信号の最大振幅:Double = 10000 //amp
var 総サンプル数:Int = 22050 //N
var 離散時間:Int = 0 //n
var サンプルデータ:[ALshort] = [ALshort](count: 総サンプル数, repeatedValue: 0)

func ノコギリ波のデータを生成する() {
	for var i = 0; i < 総サンプル数; ++i {
	    var tmp:Double = サンプリング周波数 / 信号の周波数
	    サンプルデータ[i] = Double型をALshortに変換する(
	        2 * 信号の最大振幅 * (信号の周波数 / サンプリング周波数) * fmod(Double(i), tmp) - 信号の最大振幅
	    )
	}
}

ノコギリ波のデータを生成する()を実行すると、ノコギリ波形のデータがサンプルデータ[n]に格納されます。ノコギリ波はブザー音のような音がします。

4.矩形波(方形波)を生成する #

矩形波は次のような数式で表すことができます。

beginequation y(n)= left { beginarrayl A,hphantom-sqrt{-} & x'<fracf_s2f -A, & x’geq fracf_s2f endarray right. endequation tag4

矩形波は後述するパルス波の一種と捉えることができます。出力される振幅が信号の最大振幅-信号の最大振幅かに固定されているのが特長です。この数式をプログラムに置き換えてみましょう。次のように書くことができます。

var 円周率:Double = 3.14159 //pi
var 信号の周波数:Double = 440 //f
var サンプリング周波数:Double = 22050 //fs
var 信号の最大振幅:Double = 10000 //amp
var 総サンプル数:Int = 22050 //N
var 離散時間:Int = 0 //n
var サンプルデータ:[ALshort] = [ALshort](count: 総サンプル数, repeatedValue: 0)

func 矩形波のデータを生成する() {
	for var i = 0; i < 総サンプル数; ++i {
	    var tmp:Double = サンプリング周波数 / 信号の周波数
	    if fmod(Double(i), tmp) < サンプリング周波数 / (2 * 信号の周波数) {
	        サンプルデータ[i] = Double型をALshortに変換する(
	            信号の最大振幅
	        )
	    }
	    if fmod(Double(i), tmp) >= サンプリング周波数 / (2 * 信号の周波数) {
	        サンプルデータ[i] = Double型をALshortに変換する(
	            信号の最大振幅 * -1
	        )
	    }
	}
}

矩形波のデータを生成する()を実行すると、PSG音源でおなじみのまっすぐな音色を聴くことができます。

5.パルス波を生成する #

パルス波はさらにシンプルな数式で表すことができます。

beginequation y(n)= left { beginarrayl A,hphantom-sqrt{-} & x'<1 0, & x’geq 1 endarray right. endequation tag5

パルス波は最大振幅と最小振幅によって構成される波形であり、この数式では最大出力と無音状態で表されています。この数式をプログラムに置き換えてみましょう。次のように書くことができます。

var 円周率:Double = 3.14159 //pi
var 信号の周波数:Double = 440 //f
var サンプリング周波数:Double = 22050 //fs
var 信号の最大振幅:Double = 10000 //amp
var 総サンプル数:Int = 22050 //N
var 離散時間:Int = 0 //n
var サンプルデータ:[ALshort] = [ALshort](count: 総サンプル数, repeatedValue: 0)

func パルス波のデータを生成する() {
	for var i = 0; i < 22050; ++i {
	    var tmp:Double = サンプリング周波数 / 信号の周波数
	    if fmod(Double(i), tmp) < 1 {
	        サンプルデータ[i] = Double型をALshortに変換する(
	            信号の最大振幅
	        )
	    }
	    if fmod(Double(i), tmp) >= 1 {
	        サンプルデータ[i] = Double型をALshortに変換する(
	            0
	        )
	    }
	}
}

パルス波のデータを生成する()を実行することでパルス波を生成することができます。パルス波は矩形波に比べると細い、メロディラインに向いた音色です。

まとめ #

この記事で紹介した数式はサウンドエフェクトのプログラミング Cによる音の加工と音源合成から引用しました。この本ではC言語による波形合成について詳しく取り上げられています。

これまで5つの基本的な波形を生成する方法を考えてきました。実際にこれらのコードを使って音を再生するには、OpenALなどの準備が必要です。iOSでOpenALを使う方法については以前の記事で解説しています。

Rosenberg波の生成 #

この章ではSwiftとOpenALを使ってMac上でRosenberg波を合成し、再生するプログラムを作成します。

プログラムの作成 #

ここで作成するプログラムは以前の記事で作ったものを雛形にしています。

プロジェクトを作成する #

Xcodeを起動しましょう。メニューバーにある File > New > Project を選択します。次にテンプレートを選ぶよう促されるので OSX の Application 項目にある Command Line Tool を選択して Next をクリックします。プロジェクトの情報を入力するように促す画面が出たら、Product Name: の欄にプロジェクト名を入力します。また、Language: は Swift を選択してください。

プロジェクトが作成されたら、左側のプロジェクトナビゲーターから自動生成されている main.swiftを選択して開きます。

Rosenberg波を再生するプログラムを書く #

次にmain.swift内を次のプログラムに置き換えます。

import Foundation
import OpenAL.AL
import OpenAL.ALC
import AudioToolbox

//関数定義
func ALCデバイスを開く(デバイス:UnsafePointer<ALCchar>) -> COpaquePointer 
	return alcOpenDevice(デバイス)
}
func ALCコンテキストを作成する(device: COpaquePointer, attrlist: UnsafePointer<ALCint>) -> COpaquePointer 
	return alcCreateContext(device,attrlist)
}
func 使用するALCコンテキストを指定する(context: COpaquePointer) -> ALCboolean 
	return alcMakeContextCurrent(context)
}
func ALバッファを作成する(n: ALsizei, buffers: UnsafeMutablePointer<ALuint>) 
	alGenBuffers(n,buffers)
}
func 波形データをALバッファに格納する(bid: ALuint, format: ALenum, data: UnsafePointer<Void>, size: ALsizei, freq: ALsizei) 
	alBufferData(bid,format,data,size,freq)
}
func ALソースを作成する(n: ALsizei, sources: UnsafeMutablePointer<ALuint>) 
	alGenSources(n,sources)
}
func ALバッファをALソースに格納する(sid: ALuint, param: ALenum, value: ALint) {
	alSourcei(sid, param, value)
}
func ALソースを再生する(sid: ALuint) {
	alSourcePlay(sid)
}
func ALソースを停止する(sid: ALuint) {
	alSourceStop(sid)
}
func ALソースを削除する(n: ALsizei, sources: UnsafePointer<ALuint>) 
	alDeleteSources(n, sources)
}
func ALバッファを削除する(n: ALsizei, buffers: UnsafePointer<ALuint>) 
	alDeleteBuffers(n, buffers)
}
func ALCコンテキストを削除する(context: COpaquePointer) {
	alcDestroyContext(context)
}
func ALCデバイスを閉じる(device: COpaquePointer) -> ALCboolean 
	return alcCloseDevice(device)
}
func Rosenberg波のデータを生成する(inout data:[ALshort]) 
	var サンプリング周波数:Double = 22050
	var 信号の周波数:Double = 220
	var 波形の増幅:Double = 20000
	
	var t:Double = 0
	var τ1:Double = 0.40
	var τ2:Double = 0.16
	
	var tmp:Double = 0
	
	for var i:Int = 0; i < 22050; ++i {
	    t += 信号の周波数 / サンプリング周波数
	    t -= floor(t)
	
	    if t <= τ1 {
	        tmp = 波形の増幅 * (3.0 * pow(t/τ1,2.0) - 2.0 * pow(t/τ1,3.0))
	    } else if (t < τ1+τ2) {
	        tmp = 波形の増幅 * (1.0 - pow((t-τ1)/τ2,2.0))
	    }
	
	    data[i] = ALshort(Int16( tmp ))
	}
}

//メイン処理
var 波形データ:[ALshort] = [ALshort](count: 22050, repeatedValue: 0)
var ALバッファ:ALuint = 0
var ALソース:ALuint = 0
var ALCデバイス:COpaquePointer
var ALCコンテキスト:COpaquePointer
ALCデバイス = ALCデバイスを開く( UnsafePointer(bitPattern: 0) )
ALCコンテキスト = ALCコンテキストを作成する(ALCデバイス, UnsafePointer(bitPattern: 0))
使用するALCコンテキストを指定する( ALCコンテキスト )

Rosenberg波のデータを生成する( &波形データ )

var s = 波形データ.count
var size:ALsizei = ALsizei(s)
var freq:ALsizei = ALsizei(Int32(22050))
ALバッファを作成する(1, &ALバッファ)
波形データをALバッファに格納する(ALバッファ, AL_FORMAT_MONO16, 波形データ, size, freq)
ALソースを作成する(1, &ALソース);
ALバッファをALソースに格納する(ALソース, ALenum(AL_BUFFER), ALint(ALバッファ))
ALソースを再生する(ALソース)
sleep(1)
ALソースを停止する(ALソース)
ALソースを削除する(1, &ALソース)
ALバッファを削除する(1, &ALバッファ)
使用するALCコンテキストを指定する( ALCコンテキスト )
ALCコンテキストを削除する( ALCコンテキスト )
ALCデバイスを閉じる( ALCデバイス )

これでRosenberg波を再生する準備が整いました。

プログラムを実行する #

Xcodeのメニューバーにある Product > Run を実行してみましょう。1秒間ブザーのような音が再生されたら成功です。

最後に合成した波形を見てみましょう。ここではAudacityで録音したものを掲載します。拡大して見ると、波形がなだらかな三角波であることを確認することができます。

キューイング #

この記事ではキューイングを用いてサイン波をループ再生するプログラムを作成します。開発言語はSwiftを、音の再生にはOpenALを使用します。iOSでの動作を想定しています。

プロジェクトの準備 #

プロジェクトを作成する #

Xcodeを起動したら、メニューバーにある File > New > Project を選択します。次にテンプレートを選ぶよう促されるので Single View Application を選択して Next をクリックします。プロジェクトの情報を入力するように促す画面が出たら、Product Name: の欄にプロジェクト名を入力します。また、Language: は Swift を選択するようにします。

フレームワークを追加する #

OpenALの機能を使うために、次の2つフレームワークをプロジェクトに追加しましょう。

  • AudioToolbox.framework
  • OpenAL.framework

フレームワークを追加するには、プロジェクトナビゲーターから現在のプロジェクトを選択してメイン画面のGneralメニューを表示します。その一番下にある Linked Frameworks and Libraries の項目の +(Add items)を選択して、表示されるフレームワークから必要なものを選び Add をクリックします。

新しいファイルを作成する #

Xcodeのメニューバーにある File > New > File を選択します。次にテンプレートを選ぶように促されるので Swift File をクリックして選び、次いで Next をクリックします。ファイルの情報を入力するように促されるので、 Save As: の欄にファイル名を入力します。この記事では OpenALSound.swift というファイル名で作成します。すると次のような内容が自動的に表示されます。

//
//  OpenALSound.swift。\n\n「
//  プロジェクト名
//
//  Created by dolphilia on 2015/08/15.
//  Copyright (c) 2015年 dolphilia. All rights reserved.
//

import Foundation

プログラムの作成と実行 #

コードを追加する #

OpenALSound.swift の末尾にコードを追加して、次のように書き換えます。

//
//  OpenALSound.swift
//  プロジェクト名
//
//  Created by dolphilia on 2015/08/15.
//  Copyright (c) 2015年 dolphilia. All rights reserved.
//

import Foundation
import OpenAL.AL
import OpenAL.ALC
import AudioToolbox

func makeSineWave(inout sample:[ALshort])  //サイン波の波形を生成する
	var pi:Double = 3.14159 //円周率
	var f:Double = 440 //信号の周波数
	var fs:Double = 22050 //サンプリング周波数
	var amp:Double = 32767 //波形の最大整数値
	for var i = 0; i < 22050; ++i {
	    sample[i] = ALshort(Int16( sin(2.0 * pi * f * Double(i) / fs) * amp ))
	}
}

class OpenALSound:NSObject {
	var device:COpaquePointer
	var context:COpaquePointer
	var buffer:ALuint
	var source:ALuint
	var sample:[ALshort]
	var size:ALsizei
	var freq:ALsizei
	override init() {
	    device = alcOpenDevice( UnsafePointer<ALCchar>(bitPattern: 0) ) //ALCデバイスを開く
	    context = alcCreateContext(device, UnsafePointer<ALCint>(bitPattern: 0)) //ALCコンテキストを作成する
	    buffer = 0
	    source = 0
	    sample = [ALshort](count: 22050, repeatedValue: 0)
	    freq = ALsizei(Int32(22050)) //波形の周波数を設定する
	    size = ALsizei(0)
	    super.init()
	    alcMakeContextCurrent( context ) //使用するALCコンテキストを指定する
	    makeSineWave( &sample ) //サイン波の波形を生成する
	    size = ALsizei(sample.count) //波形のサイズを取得する
	    alGenBuffers(1, &buffer) //ALバッファを作成する
	    alBufferData(buffer, AL_FORMAT_MONO16, sample, size, freq) //波形データをALバッファに格納する
	    alGenSources(1, &source) //ALソースを作成する
	    for var i=0; i<5; i++ { //再生前にキューにバッファを5個追加しておく
	        alSourceQueueBuffers(source, 1, &buffer) //キューにバッファを追加する
	    }
	    alSourcePlay(source) //ALソースを再生する
	    var timer = NSTimer.scheduledTimerWithTimeInterval( // タイマーを生成する
	        0.1, target: self, selector: "onTimer:", userInfo: nil, repeats: true)
	}
	deinit {
	    alSourceStop(source) //ALソースを停止する
	    alDeleteSources(1, &source) //ALソースを削除する
	    alDeleteBuffers(1, &buffer) //ALバッファを削除する
	    alcMakeContextCurrent( context ) //使用するALCコンテキストを指定する
	    alcDestroyContext( context ) //ALCコンテキストを削除する
	    alcCloseDevice( device ) //ALCデバイスを閉じる
	}
	func onTimer(timer:NSTimer?) {
	    var count:ALint=0
	    var state:ALint=0
	    alGetSourcei(source, AL_BUFFERS_PROCESSED, &count); //再生終了したキューがあるかチェックする
	    if count != 0 {
	        for var i:ALint=0; i<count; i++ {
	            alSourceUnqueueBuffers(source, 1, &buffer)
	        }
	    }
	    alGetSourcei(source, AL_BUFFERS_QUEUED, &count); //未使用のキューの数をチェック
	    if count < 5 {
	        alSourceQueueBuffers(source, 1, &buffer) //キューにバッファを追加する
	        alGetSourcei(source, AL_SOURCE_STATE, &state)
	        if state != AL_PLAYING {
	            alSourcePlay(source)
	        }
	    }
	}
}

OpenALでのリアルタイム合成で重要な関数はalSourceQueueBuffersです。これはバッファをソースに格納するalSourceiと基本的に同じ役割をしていますが、キューにバッファを追加するという点で大きな違いがあります。コードを見てみましょう。

for var i=0; i<5; i++  //再生前にキューにバッファを5個追加しておく
	alSourceQueueBuffers(source, 1, &buffer) //キューにバッファを追加する
}

このコードは単純にalSourceQueueBuffersを5回繰り返すというものです。もしバッファに格納されているのが1秒間のサンプルだったら、ソースには5秒間のキューが追加されたことになります。ソースを再生する関数はalSourcePlay(source)です。

キューイングによるループ再生を実現するためにタイマーを使用します。タイマーイベントが発生するごとにソースの状態をチェックして、必要に応じてキューの追加をするようにします。ソースの状態をチェックする関数がalGetSourceiです。

コードのインスタンスを生成する(ViewController.swift) #

次に左側のプロジェクトナビゲーターからViewController.swiftを選択して開きます。すると次のようなコードが表示されるはずです。

...略...
import UIKit

class ViewController: UIViewController {

	override func viewDidLoad() {
	    super.viewDidLoad()
	    // Do any additional setup after loading the view, typically from a nib.
	}
...略...
}

このViewDidLoad関数の末尾に次のコードを追加します。

var sound:OpenALSound = OpenALSound()

次のようなコードになります。

//
//  ViewController.swift
//  プロジェクト名
//
//  Created by dolphilia on 2015/07/07.
//  Copyright (c) 2015年 dolphilia. All rights reserved.
//

import UIKit

class ViewController: UIViewController {

	override func viewDidLoad() {
	    super.viewDidLoad()
	    // Do any additional setup after loading the view, typically from a nib.
	    var sound:OpenALSound = OpenALSound()
	}
	
	override func didReceiveMemoryWarning() {
	    super.didReceiveMemoryWarning()
	    // Dispose of any resources that can be recreated.
	}
}

プログラムを実行する #

Xcodeのメニューバーにある Product > Run をクリックします。プログラムが起動するとサイン波がプログラムが終了するまで再生され続けます。再生を停止するには、Xcodeのタイトルバーにある停止ボタンを押してください。