Help us understand the problem. What is going on with this article?

SwiftとAudioUnitで音を鳴らす

More than 3 years have passed since last update.

※こっちの記事のほうが新しいです

http://qiita.com/naokitomita/items/85160e1b354f1281309c

はじめに

iPhoneで録音した音声ファイルをまったく同時に再生したいと思ったのですが、AVAudioPlayerではうまくいきませんでした。
どうすればいいか調べたのですが、どうやらAudioUnitを使わないと完全に同時に再生することはできないようです。
なので、まずはAudioUnitを使って音を鳴らすところからはじめることにします。

Swiftや音声処理については全くの素人なので、間違っているところやわからなかったところが多々あります。ご存知の方にツッコミを入れていただきたく思い、公開させていただきました。
よろしくお願いします。

音を出す

Swift初心者なもので、どうやっていいか、さっぱりわかりませんでしたが、とりあえず動くものができました。

とりあえず動くもの

下記コードをどっかのファイルにコピーして、SoundPlayer#Play()とやればラの音(440Hz)の音が出るはずです。
iOSシミュレータではノイズが乗りまくりました。
実機ではうまくいくんですが・・・。

func RenderCallback (
    inRefCon: UnsafeMutablePointer<Void>,
    ioActionFlags: UnsafeMutablePointer<AudioUnitRenderActionFlags>,
    inTimeStamp: UnsafePointer<AudioTimeStamp>,
    inBusNumber: UInt32,
    inNumberFrames: UInt32,
    ioData: UnsafeMutablePointer<AudioBufferList>) -> (OSStatus)
{
    let tmp: UnsafeMutablePointer<SoundPlayerData> = UnsafeMutablePointer<SoundPlayerData>(inRefCon);
    let data: SoundPlayerData = tmp.memory;
    let buf: AudioBufferList = ioData.memory;
    var datas: UnsafeMutablePointer<Float> = UnsafeMutablePointer<Float>(buf.mBuffers.mData);

    let sineWaveFreq: Float = 440.0;
    let samplingRate: Float = 44100.0;
    let freq: Float = sineWaveFreq * 2.0 * Float(M_PI) / samplingRate;

    for(var i: UInt32 = 0; i < inNumberFrames; i++)
    {
        var tmpVal: Float = sin(data.time);
        memcpy(datas, &tmpVal, sizeof(Float));
        datas++;
        data.time += freq;
    }

    return noErr;
}

class SoundPlayerData
{
    var time: Float = 0.0;
}

class SoundPlayer
{
    var audioUnit: AudioUnit?;
    var acd: AudioComponentDescription?;
    var ac: AudioComponent?;
    var data: SoundPlayerData = SoundPlayerData();
    var isPlaying: Bool = false;

    func play()
    {   
        // AudioUnitの準備
        audioUnit = AudioUnit();

        // AudioComponentDescriptionの準備
        acd = AudioComponentDescription();
        acd?.componentType = kAudioUnitType_Output;
        acd?.componentSubType = kAudioUnitSubType_RemoteIO;
        acd?.componentManufacturer = kAudioUnitManufacturer_Apple;
        acd?.componentFlags = 0;
        acd?.componentFlagsMask = 0;

        // AudioComponentの生成
        ac = AudioComponentFindNext(nil, &acd!);

        // AudioUnitとAudioComponentとの関連づけ
        AudioComponentInstanceNew(ac!, &audioUnit!);

        // 音声処理のコールバックメソッドを準備
        // メソッドとメソッドに渡すデータを用意しておきます。
        var input = AURenderCallbackStruct(inputProc: RenderCallback, inputProcRefCon: &data);

        // AudioUnitとコールバックメソッドの関連づけ
        AudioUnitSetProperty(audioUnit!, kAudioUnitProperty_SetRenderCallback, kAudioUnitScope_Input, 0, &input, UInt32(sizeofValue(input)));

        // AudioUnitの初期化
        AudioUnitInitialize(audioUnit!);

        // 音声再生実行開始
        AudioOutputUnitStart(audioUnit!);
        isPlaying = true;
    }

    func stop()
    {
        if(audioUnit == nil || isPlaying == false)
        {
            return;
        }

        AudioOutputUnitStop(audioUnit!);
        audioUnit = nil;
    }
}

ソースコードの解説

音がどうやって出ているかについては参考の第三回あたりを読むと良いでしょう。
音は空気の振動であり、44.1kHzでサンプリングされた音は、1秒間に44100回の振動データを保存しているということになります。
逆に再生する時には44100回、スピーカーに波の信号を送っているということになります。

play()メソッド

play()メソッドが音を鳴らすための準備と再生開始のメソッドとなっています。
流れとしては、次のようになります。

  1. AudioUnitを生成
  2. AudioComponentDescriptionを生成
  3. AudioComponentDescriptionからAudioComponentを生成
  4. AudioComponentとAudioUnitを関連づける
  5. 音を鳴らすための計算を行うコールバック関数を設定する
    • コールバック関数が呼ばれた時に渡されるデータにselfをセットすることはできませんでした。Mutableなポインタにできないとか言われた気がしますが、なんででしょうか。
  6. AudioUnitの初期化
  7. 再生開始(コールバックメソッドが呼び出されるようになります。)

RenderCallback()関数

play()を実行すると、RenderCallback()メソッドが1秒間に86回(だったかな)呼び出されるようになります。
第一引数のinRefConplay()メソッド内で設定したコールバック関数に渡されるデータのポインタが入っています。
第五引数のinNumberFramesは第六引数であるバッファの箱の数が入っています。バッファは,1/86秒分の音声を保持することができます。
第六引数のioDataは音声出力用バッファです。ここに-1.0 ~ 1.0までのデータを連続で入力してあげることで、1/86秒分の音声を出力することができます。

関数の先頭で、引数に渡されたUnsafeMutablePointerの値をSwiftで扱えるようにキャストしたりしています。やり方がさっぱりわからなかったため、強引にやっています。正しいやり方を教えてください・・・。

次に、440Hzの音声を出力するために必要な定数を計算しています。ここでいちいち計算する必要はないので、無駄な処理ですね。

最後にループ内で、inNumberFrames個分のSin波を計算してバッファに詰めています。

がんばったところ

  • ポインタのキャスト
    • Swiftで扱えるような形にポインタをキャストするのに苦労しました。正しいやり方とは思えませんが・・・。
  • 出力用のバッファに値を書き込むところ
    • バッファに値がどうしてもうまく書き込めなかったので、memcpyを使っています。たぶん、もっとスマートな方法があると思います。教えてください。

わからないところ

本来であれば、AudioBufferListは次のような構造体だそうです。

struct AudioBufferList
{
    UInt32      mNumberBuffers;
    AudioBuffer mBuffers[kVariableLengthArray];
};
typedef struct AudioBufferList  AudioBufferList;

struct AudioBuffer
{
    UInt32  mNumberChannels;
    UInt32  mDataByteSize;
    void*    mData;
};
typedef struct AudioBuffer  AudioBuffer;

しかし、Swiftでは次のように定義されていました。

public struct AudioBufferList {
    public var mNumberBuffers: UInt32
    public var mBuffers: (AudioBuffer) // this is a variable length array of mNumberBuffers elements
    public init()
    public init(mNumberBuffers: UInt32, mBuffers: (AudioBuffer))
}

public struct AudioBuffer {
    public var mNumberChannels: UInt32
    public var mDataByteSize: UInt32
    public var mData: UnsafeMutablePointer<Void>
    public init()
    public init(mNumberChannels: UInt32, mDataByteSize: UInt32, mData: UnsafeMutablePointer<Void>)
}

AudioBufferList#mBuffersは配列であり、ステレオ出力の場合にはmNumberBuffersが2となります。Obj-Cのサンプルでは

AudioBufferList#mBuffers[0]
AudioBufferList#mBuffers[1]

というふうにアクセスして左右の音声を出力していたのですが、Swiftは配列ではないように見えます。

public var mBuffers: (AudioBuffer) // []で括ってないから配列ではないのでは?

コメントには、

this is a variable length array of mNumberBuffers elements
これはmNumberBuffers個の可変長の変数です。

とあるので、配列なんだと思うのですが、アクセス方法がわかりませんでした。
どなたか、詳しい方、教えてください。

参考

Getting Started With AudioUnit / My Codex Leicester

Why not register and get more from Qiita?
  1. We will deliver articles that match you
    By following users and tags, you can catch up information on technical fields that you are interested in as a whole
  2. you can read useful information later efficiently
    By "stocking" the articles you like, you can search right away
Comments
Sign up for free and join this conversation.
If you already have a Qiita account
Why do not you register as a user and use Qiita more conveniently?
You need to log in to use this function. Qiita can be used more conveniently after logging in.
You seem to be reading articles frequently this month. Qiita can be used more conveniently after logging in.
  1. We will deliver articles that match you
    By following users and tags, you can catch up information on technical fields that you are interested in as a whole
  2. you can read useful information later efficiently
    By "stocking" the articles you like, you can search right away