【iPhoneアプリ】【Swift】SolPlayer開発記(3)AVAudioEngineでリアルタイムでピッチを変更する
すべての音源をソルフェジオに変える
SolPlayerの開発記3回目。
前回の記事はこちら↓
今回は、このアプリのキモである
「ソルフェジオモード」の実装方法について解説します。
このソルフェジオモード、前回も紹介したとおり
一言で言えば「ピッチを特定の周波数に変換する」機能です。
課題は2つあります。
(1)技術的にどう実装するか
(2)ピッチをどれだけ変更すればよいのか
これを、一つ一つ具体的に解説していきます。
(1)技術的にどう実装するか
以下の3つの手順で実装します。
1.ピッチを変換する部品(クラス)をつくる
2.各部品(クラス)をつなぐ
3.再生する
前回紹介したサイトのコードを
1.ピッチ変換するメソッドを作る
まず最初に、ピッチ変換をするためのメソッドを作ります。
このメソッドは、簡略に言えば「画面上のスイッチを押された時にピッチを変更する」という処理を行うものです。
※今回の説明では、画面側の処理とかはすべて省略します。
具体的には、AVAudioUnitTimePitchというクラスの変数(timePitch)を作ります。
そして、timePitchのフィールド「.pitch」に、ピッチ変更する値を指定します。
var timePitch: AVAudioUnitTimePitch! = AVAudioUnitTimePitch() (中略) /** ソルフェジオモードon/off(ピッチ変更)処理 */ func pitchChange(solSwitch: Bool){ if(solSwitch){ switch userConfigManager.solMode { case 1: timePitch.pitch = 15.66738339053706 //17:440Hz→444.34Hz 16:440Hz→444.09Hz break case 2: timePitch.pitch = -31.76665363343202 //-32:440Hz→431.941776Hz break default: timePitch.pitch = 0 } } else { timePitch.pitch = 0 } }
指定している値は「15.66738339053706」「-31.76665363343202」「0」の3つ。
「0」はなんとなく想像がつくかと思いますが(デフォルト値)、 前述の二つは何の数字なのか?なぜこの値なのか?という疑問が出てくると思います。 これについては後述します。
2.つなぐ
AudioEngineで音を出力するために、各部品(クラス)を「つなぐ」という作業が必要です。
「つなぐ」ことで、読み込んだ音源にエフェクトを加えて(今回はピッチを変えて)出力することができます。
実際のコードですが、前回紹介したサイトでは以下のような記述がありました。これは、エフェクトを入れずにつないだ例です。
Player Node → Mixer Node → Output Node
// [2] AVAudioPlayerNode オブジェクトを準備する self.audioPlayerNode = [AVAudioPlayerNode new]; [self.engine attachNode:self.audioPlayerNode]; // [3] Node 同士を繋ぐ AVAudioMixerNode *mixerNode = [self.engine mainMixerNode]; [self.engine connect:self.audioPlayerNode to:mixerNode format:self.audioFile.processingFormat];
これをSwiftで書き換えるとこうなります。
//AVKit var audioEngine: AVAudioEngine! var audioPlayerNode: AVAudioPlayerNode! var audioFile: AVAudioFile! (中略) // [2] AVAudioPlayerNode オブジェクトを準備する self.audioPlayerNode = AVAudioPlayerNode() self.audioEngine.attachNode(audioPlayerNode) // [3] Node 同士を繋ぐ let mixerNode: AVAudioMixerNode = self.audioEngine.mainMixerNode self.audioEngine.connect(audioPlayerNode, to:audioEngine.mainMixerNode, format:self.audioFile.processingFormat)
注)上記の例ではAudioEngineの変数をengine → audioEngineに変えています
これで音源を再生することができますが、今回はソルフェジオ変換(ピッチ変換)処理を加えたいので、ここにpitch変化するための処理を挟みこみます。
Player Node → ピッチ変換(AVAudioUnitTimePitch) →Mixer Node → Output Node
//AVKit var audioEngine: AVAudioEngine! var audioPlayerNode: AVAudioPlayerNode! var audioFile: AVAudioFile! var timePitch: AVAudioUnitTimePitch! //「1.ピッチ変換」の変数 (中略) // [2] AVAudioPlayerNode オブジェクトを準備する self.audioPlayerNode = AVAudioPlayerNode() self.audioEngine.attachNode(audioPlayerNode) // [3] Node 同士を繋ぐ let mixerNode: AVAudioMixerNode = self.audioEngine.mainMixerNode self.audioEngine.connect(audioPlayerNode, to:timePitch, format:self.audioFile.processingFormat) self.audioEngine.connect(timePitch, to:audioEngine.mainMixerNode, format:self.audioFile.processingFormat)
こうすることで、エフェクトがかかった(ピッチ変換された)音が出力されるようになります。
以下、上記と同じ処理を若干汎用性を高めた記述を紹介します(もっとキレイな書き方があれば教えて下さい)
//AVKit var audioEngine: AVAudioEngine! var audioPlayerNode: AVAudioPlayerNode! = AVAudioPlayerNode() var audioFile: AVAudioFile! (中略) audioPlayerNode = AVAudioPlayerNode() //アタッチリスト var attachList:Array= [audioPlayerNode, reverbEffect, timePitch] //AVAudioEngineにアタッチ for i in 0 ... attachList.count-1 { audioEngine.attachNode(attachList[i]) if(i >= 1){ audioEngine.connect(attachList[i-1], to:attachList[i], format:audioFile.processingFormat) } } //ミキサー出力 audioEngine.connect(attachList.last!, to:audioEngine.mainMixerNode, format:audioFile.processingFormat)
アタッチリストにエフェクト(他にもリバーブなどがあります)を加えていけば、AudioPlayerNodeで出力される際に付加するエフェクトを追加できます。
var attachList:Array= [audioPlayerNode, reverbEffect, timePitch]
3.再生
上記が完了したら、audioEngineを起動した後(audioEngine.start)、audioPlayerNodeを再生(audioPlayerNode.play)します。
(中略)
//AVAudioEngineの開始
audioEngine.prepare()
do {
try audioEngine.start()
audioPlayerNode.play()
} catch {
}
これで、ピッチ変換された音が再生できます。
(2)ピッチをどれだけ変更すればよいのか
さて、それでは上記の記事内ででてきたピッチ変換の値「15.66738339053706」「-31.76665363343202」について説明します。
一般に、音声編集ソフト(Audacity等)を使ったピッチ変換では、「変換前の周波数(440Hz)」と「変換後の周波数(444Hz)」をヘルツで指定するだけで、ピッチを変換することができます。
が、今回ピッチ変換に使用したAVAudioUnitTimePitchというクラスでは、「ヘルツ(Hz)」ではなく、「セント」という単位を使います。
この「セント」が曲者で、けっこう計算式が複雑なんですね。
詳細は以下記事をご覧ください。
何セント変えればソルフェジオ周波数になるのか?(セント/ヘルス変換)
100セントで半音、
1200セントで1オクターブとのこと。
セント・ヘルツ 換算表 (cent/Hz)
ざっくりと説明すると、「1200セントで1オクターブ変わる」んですが。。
1セントは「1/1200」ではなく、「log(1200)」(1200乗すると2倍になる)んです。
(計算式が複雑になるため、詳細は割愛します汗)
で、上記のようなサイトを参考に計算した結果、
・440→444Hzに変換するためには「15.66738339053706」セント
・440→432Hzに変換するためには「-31.76665363343202」セント
ピッチをずらす必要があることが判明しました。
これが、(1)で出てきた謎の値の種明かしです。
※432Hzについては、今後どこかで解説するかもしれません
…とりあえず、今回の記事はこんな感じです。
記載が至らない箇所はいくつもあるかと思いますが、
不明点などあればお気軽にコメントいただければ幸いです。
次回は、、どうしましょう?汗 未定です。。
とりあえず、引き続きAVAudioEngine等を使った開発に関する記事を掲載します。
それでは、また。
追記(2016/09/24)
ちなみに、ピッチではなく再生速度を変更する際も、全く同じクラス(AVAudioUnitTimePitch)でできます。
具体的には、AVAudioUnitTimePitchのフィールド「.rate」に、ピッチ変更する値を指定します。
(実装例)
var timePitch: AVAudioUnitTimePitch! = AVAudioUnitTimePitch()
/** 再生スピード変更処理 - parameter speedSliderValue(画面の再生速度スライダーから) */ func speedChange(speedSliderValue: Float){ timePitch.rate = speedSliderValue }