foresthillのブログ

ホームページ・CMS制作からアプリ開発、ゲーム製作となんでもござれ。

【iPhoneアプリ】【Swift】SolPlayer開発記(3)AVAudioEngineでリアルタイムでピッチを変更する

 

SolPlayer

SolPlayer

  • Naoya Morioka
  • ミュージック
  • 無料

 

すべての音源をソルフェジオに変える

SolPlayerの開発記3回目。

 

前回の記事はこちら↓

foresthill.hatenablog.com

 

 

今回は、このアプリのキモである

「ソルフェジオモード」の実装方法について解説します。

 

このソルフェジオモード、前回も紹介したとおり

一言で言えば「ピッチを特定の周波数に変換する」機能です。

 

 

 

課題は2つあります。

(1)技術的にどう実装するか

(2)ピッチをどれだけ変更すればよいのか

 

 

これを、一つ一つ具体的に解説していきます。

 

(1)技術的にどう実装するか

以下の3つの手順で実装します。

1.ピッチを変換する部品(クラス)をつくる

2.各部品(クラス)をつなぐ

3.再生する

 

前回紹介したサイトのコードを

dev.classmethod.jp

 

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)

b3a4s4s.web.fc2.com

 

b3a4s4s.web.fc2.com

 

ざっくりと説明すると、「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 }