プログラミング」カテゴリーアーカイブ

離散フーリエ変換 その3

前回記事を書いてからだいぶ時間が経ってしまって5月にエントリがひとつもないのもさみしいので、書きかけだった記事をアップしておきます。iPadから書き込んでみたいというのもあったので…。

最近はiPhoneOS4.0にどっぷり使っていまして、あまり書けることがないんですよねぇ。ってことで、フーリエ変換の流れで今回は位相の話です。逆離散フーリエ変換とか行きたいところですが、またの機会にします。

位相とは

これまでの説明で何度か位相という言葉を使ってきたと思いますが、ちゃんとその定義を調べずに使っていたので、改めてwikipediaなどを見てみますと…

位相 (Wikipedia)

「ひとつの周期中の位置を示す無次元量」なんて書いてあって、無次元量って何だ?なんて思ってしまうわけですが、まぁ、DFTで使っているサイン波でいえば、サイン波の中のどの位置かということと思われます。さらに、周期のスタート位置の位相は「初期位相」というそうで、初期位相の事を単純に位相といわれたりすることもある、だそうです。「位相が○度ずれている」と言ったときには、2つの同じ周波数のサイン波を同じ時間軸にならべた状態で「初期位相が○度ずれている」という理解になるかと思います。

サイン関数を使ってサイン波を作る場合などは位相をラジアンで渡して値を取得しますが、0〜2π(角度で表すなら0°〜360°)が一周期で、2π以上や0以下の値を渡しても、返ってくるのは同じ値の繰り返しとなります。たとえば、0から始まるサイン波と、2πや4πや-2πから始まるサイン波というのは、全く同じ形となりますので、同じ位相といえます(たぶん)。DFTの周波数成分は、繰り返されているサイン波の1周期ですので、その位相はどこか2π分の範囲の中の位置だけを考えれば良い事になります。

DFT03_01.jpg

直線位相特性

よくデジタルフィルタの本をみていると、FIRフィルタだと位相のずれがない直線位相特性のフィルタが実現できるなんて書いてあったりします。下の図が直線位相特性のグラフなのですが…

DFT03_03.jpg

これを最初見た時は位相がずれないといってるのに、位相が周波数によってずれるというのが僕はよく理解できなかったのですが、いくつかのサイン波を並べて同じ時間遅らせて、遅らせたタイミングでも同じ波形になるようにしてみるとわかります。

DFT03_02.jpg

上の図では1Hzのサイン波を3/4周期遅らせて、同じ時間2Hzと3Hz遅らせて並べてみています。元のサイン波が緑色で、遅らせたサイン波が青色です。各周波数を同じ時間遅らせているので、遅らせたあとの波形はどの周波数も元の波形と同じ形に保たれます。

1Hzの3/4周期の遅れに対して、2Hzは3/2周期、3Hzは9/4周期、と位相がずれています。このような感じで周波数に比例して位相がずれるというのが直線位相特性です。直線位相特性を実現したフィルタを使用すれば、周波数によって位相のずれがないので、クオリティの高い処理ができるということのようです。

離散フーリエ変換 その2

離散フーリエ変換の第2回目という事で、実際にどういうことをやっているのかをコードで見ていきたいと思います。前回のDFTのコードの変換を行っているところが以下の部分です。

// iは抜き出す周波数
for (int i = 0; i < n; i++) {
    
    tmpReal[i] = 0.0;
    tmpImag[i] = 0.0;
    
    // 1サンプルの位相の差分
    double d = 2.0 * M_PI * i / n;
    
    for (int j = 0; j < n; j++) {
        
        double phase = d * j;
        
        // コサインをかけた結果を加算する
        tmpReal[i] += real[j] * cos(phase);
        // マイナスサインをかけた結果を加算する
        tmpImag[i] -= real[j] * sin(phase);
    }
}

変換するオーディオデータに対して、抜き出す周波数のcosをかけ算して全て足し合わせた値が実部、-sinをかけて足し合わせた値が虚部になります。具体的に説明はしませんが、このようにする事で、それぞれの周波数のcosやsinがオーディオデータにどれくらい含まれているかを得る事ができます。(ちゃんと理解したい方は、なにかしらフーリエ変換の本などを参考にしてください)

抜き出す周波数というのは、0Hzから始まって1Hz、2Hz...と続いて、変換するサンプル数-1Hzまでです。つまり、変換するサンプル数と同じ数の周波数成分に分けられます。DFTは、変換するオーディオデータが延々とループしていると仮定して周波数成分を取り出しますので、1Hz以上の周波数はぴったり整数の周波数になります。

DFT02ri.jpg

上の図を見ると、cosもsinも0Hzの時は横一直線になっています。0HzはDC成分といわれたりしてちょっと特別です。cos0Hzでは全体に1がかけられますので、波形全体がプラスとマイナスのどちらにバランスが偏っているかという要素になります。sinは0をかけているので、元がどんな波形だろうと結果は0になります。

ちなみに、下の図の1.23Hzみたいな中途半端な周波数のサイン波などというのは途中で急激に途切れた状態で繰り返されることになりますので、DFTをすると1Hzを中心に高い周波数まで全体的に成分が含まれることがわかります。

DFT02s123.jpg

また、ちょっと説明が後回しになりましたが、なぜDFTでcosとsinを抜き出すかといえば、同じ周波数のcosとsinを足し合わせることで、360度どの位相のサイン波でも表現できるからです。

本当にcosとsinの足し算でサイン波が作れるのか、検証してみたのが以下のコードです。stRadを0〜2πの間で変えてみて実行してみてください。sinはDFTと同じようにマイナスにしています。もちろん計算の誤差はあると思いますので、近ければOKという基準で判断すれば、どの位相のサイン波でも、スタート位置がゼロのcosとsinの足し合わせで作り出せる事がわかると思います。(てきとうに書いたので、なんかおかしかったらすみません。0〜2πの範囲外では正確な結果が出るようになってません)いちおうDFTと同じように、sinはマイナスsinでやってます。

#include <stdio.h>
#include <math.h>
int main (int argc, const char * argv[]) {
    
    double stRad = 0.0; //sinの開始位置。0〜2πの間で!
    int n = 16;
    
    double xcos = 0;
    double xsin = 0;
    double co = fabs(sin(stRad));
    double si = sqrt(1.0 - co * co);
    
    if (stRad < M_PI_2) {
        //第一象限
        xcos = co;
        xsin = -si;
    } else if (stRad < M_PI) {
        //第二象限
        xcos = co;
        xsin = si;
    } else if (stRad < M_PI_2 * 3) {
        //第三象限
        xcos = -co;
        xsin = si;
    } else {
        //第四象限
        xcos = -co;
        xsin = -si;
    }
    
    for (int i = 0; i < n; i++) {
        
        double phase = (double)i / n * 2.0 * M_PI;
        double sinVal = sin(stRad + phase);
        double mixVal = cos(phase) * xcos - sin(phase) * xsin;
        
        printf("sin %03d %f / sin+cos %f\n", i, sinVal, mixVal);
        if (fabs(sinVal - mixVal) > 0.000001) {
            printf("error\n");
            return 0;
        }
    }
    
    printf("success\n");
    
    return 0;
}

離散フーリエ変換 その1

MacとかiPhoneとかあまり関係なく基礎を固めようと思っていろいろ勉強モードに入っていまして、すこしずつまとめていこうと思います。まずはフーリエ変換についてです。

あくまでプログラムで使う事を前提に書いていきますので、いろいろおかしかったりするかもしれませんがご了承ください。もし、明らかに変だったら突っ込んでいただけるとうれしいです。基本的に小難しそうな数式とかはできるだけ出さずにコード重視、ビジュアル重視で書いていくつもりです。

プログラミングでフーリエ変換というと、高速フーリエ変換(FFT)を使うという事になると思うのですが、FFTについては特に詳しく書きません。プログラム化されたコードを見た場合、FFTだと高速化されたアルゴリズムだけで本質的な部分がわからないので、高速化していないノーマルな離散フーリエ変換(DFT)のプログラムを見て、フーリエ変換の仕組みを調べていきます。

オーディオ信号に対してDFTを使うと何ができるかというと、時間単位で並んでいる時間領域のオーディオデータの一部分を切り出して、含まれている周波数ごとの成分に分けた周波数領域のデータに変換する事ができます。逆に、その周波数領域のデータを時間領域のオーディオデータに戻す事もでき、それが逆離散フーリエ変換(IDFT)になります。

DFT01.jpg

離散フーリエ変換というのは、デジタルでサンプリングされたオーディオ信号のような飛び飛びのデータに対して行うフーリエ変換です。いきなり「離散」と出てくると何の事かわからないかもしれませんが、「離散」=「デジタル」と考えればよいと思います。アナログの「連続」に対して、デジタルの「離散」です。そのDFTを高速化したのがFFTという関係になります。

まずは何より使ってみない事にはしょうがないということで、サンプルソースです。

#include <stdio.h>
#include <math.h>
void DFT(int n, double *real, double *imag)
{
    double tmpReal[n], tmpImag[n];
    
    for (int i = 0; i < n; i++) {
        
        tmpReal[i] = 0.0;
        tmpImag[i] = 0.0;
        
        double d = 2.0 * M_PI * i / n;
        
        for (int j = 0; j < n; j++) {
            
            double phase = d * j;
            
            tmpReal[i] += real[j] * cos(phase);
            tmpImag[i] -= real[j] * sin(phase);
        }
    }
    
    for (int i = 0; i < n; i++) {
        real[i] = tmpReal[i];
        imag[i] = tmpImag[i];
    }
}
int main (int argc, const char * argv[]) {
    
    int n = 16;
    double real[n], imag[n];
    
    double d = 2.0 * M_PI / n;
    
    for (int i = 0; i < n; i++) {
        real[i] = sin(1.0 * i * d); //1Hzのサイン波
        real[i] += sin(3.0 * i * d + M_PI_4); //3Hzのサイン波(1/4πずらし)
        real[i] += sin(5.0 * i * d + M_PI_2); //5Hzのサイン波(1/2πずらし)
        imag[i] = 0.0;
    }
    
    //フーリエ変換
    DFT(n, real, imag);
    
    for (int i = 0; i < n; i++) {
        printf("%dHz %f\n", i, sqrt(real[i] * real[i] + imag[i] * imag[i]));
    }
    
    return 0;
}

Xcodeで実行する場合はCのCommand Line Toolでプロジェクトを作って、main.cを書き換えてください。

今回のコードでは、1Hzと3Hzと5Hzのサイン波をいろいろ位相を変えてミックスして元のデータを作ってから変換しています。結果は、

0Hz 0.000000
1Hz 8.000000
2Hz 0.000000
3Hz 8.000000
4Hz 0.000000
5Hz 8.000000
6Hz 0.000000
7Hz 0.000000
8Hz 0.000000
9Hz 0.000000
10Hz 0.000000
11Hz 8.000000
12Hz 0.000000
13Hz 8.000000
14Hz 0.000000
15Hz 8.000000

と表示されて、1Hzと3Hzと5Hzが含まれているということがわかります。上の周波数の11Hzと13Hzと15Hzにもデータが現れていますが、これは、ナイキスト周波数以上の周波数の音は、ナイキスト周波数を対称に折り返した低い周波数へ現れるということで出てきているものですので、単純に周波数特性を調べるだけなら無視してください(けっして無意味なデータというわけではありませんが...)。

コードの説明に入りまして、DFT関数がまさに離散フーリエ変換する関数になります。realに変換したいオーディオデータの配列、imagに同じサイズの空の配列、nに配列のサイズをそれぞれ渡すと、realとimagにフーリエ変換された値が返ってきます。

DFTをすると1つの周波数ごとにデータが実部と虚部という2つに分かれて返ってきます。今回のコードではrealが実部、imagが虚部のデータです。実部と虚部は2つでひとつの意味を持ったデータですので、どちらかだけを使うことはあまり無いと思います。main関数の中でやっていますが、周波数特性の振幅を得るという場合はrealとimagをそれぞれ2乗した値の平方根を求めます。

DFTして得られる周波数は、DFTに渡すサンプル数の長さを1Hzとして、0からサンプル数-1までの整数倍の周波数です。16サンプルの場合は、0・1・2・3・4・5・6・7・8・9・10・11・12・13・14・15Hzの16個です。ただこれが1
秒を1Hzと考えるときには、たとえば44.1kHzのオーディオの中の64サンプルを変換したという状況だと、0・689・1378...というふうにもっと広い間隔の周波数の並びになります。これはどこを基準にするかで違ってくるというだけの話ですので、このDFTの説明ページでは変換するサンプル数を1Hzとして表現します。

とりあえず、DFTができましたというところで1回目は終わりです。次へつづきます。

iPhone Core Audio プログラミング

ちょっと記事にするには乗り遅れた感がありますが、「iPhone Core Audio プログラミング」という、iPhoneのオーディオプログラミングに特化した本が発売されています。今はもうだいたいどこの本屋さんいっても置いてありますね。詳しい内容については筆者の永野さんのサイトのページ「iPhone Core Audioプログラミング」(書籍) 発売へ。

iPhoneのオーディオのAPIについては、これ一冊に日本語でほぼ全て書いてあります。日本語です!iPhone Dev Centerにも「Core Audio 概要」とか「Audio Session Programming Guide」とかの日本語訳はありますが、Apple純正という安心感はあるものの翻訳なので、ネイティブな日本人が書いた文章というのはやっぱり分かりやすいです。もう僕がiPhoneの開発ネタを書く事もないかなぁなんて思ったり。

基本的にはAPIの解説になるので、本の最後の章にアプリのサンプルはいくつかありますが、実際に自分好みのオーディオアプリを作り始めてみると壁にぶち当たると思います(主にパフォーマンスで)。が、まあそれはトライアンドエラーで頑張りましょうってことで。作るアプリの内容とか機種とかによっても引っかかる部分が違いますし。なぜか新しい機種の方が負荷に弱かったりするときもあるんですよねぇ。

オーディオのプログラミングっていうと、オーディオの再生とか録音とかシンセサイズとかってところに興味がいってしまいがちだと思いますが、iPhoneに関していえば最も重要なのはAudio Sessionです!ちょっとでもiPhoneでオーディオ鳴らすなら、Chapter4と7は熟読してください!知っていてあえて使わないのと、何となくめんどくさいから読まないで知らずに使えないのとでは大きな差があります。まぁ、App Storeに出さずに自分のデバイスだけで動かせればいいっていうなら話は別ですけどね。他のChapterは気になるところだけピックアップして読めば良いと思います。

値段は4,200円と、他の入門書と比べてやや高いですが、それだけの価値はあると思います。僕はオーディオメインでプログラミングを勉強してますが、やっぱりその中でも興味があるところに偏って調べていたりするので、読んでみるといろいろ知らないところがたくさんあります。今後iPhoneのオーディオで、忘れていたり、何か新たに調べたくなったときに、役立ってくれる事は確実です。

Grand Central Dispatch その7 おまけ

さらに今更ながら、ASCII.jpさんの「マルチコア時代の新機軸! Snow LeopardのGCD」に、わりと一般向けの説明から始まって、コードを使用したプログラマ向けの説明までしているページがありました。とりあえずここらへんで基本知識を得てから、いろいろ試した方が良かったのかもしれません。

その記事を見ていて改めて発見したのは、dispatch_get_main_queueで、メインスレッドで実行されるキューが取得できるという事です。なるほど、これがあればperformSelectorOnMainThreadを呼ばなくてもディスパッチで呼び出しが完結できます。前回のタイマーとかはメインキュー使う方が良い気がします。

でも、こういった解説記事の中でディスパッチソースにまで触れられているものってあんまり見かけないですね。ディスパッチさえあればアプリの基本的な仕組みが作れてしまうみたいな。ということで、ソース系でもうひとつ調べてみました。

やってみたかったのは、ディスパッチでバッチ処理的な事をさせておいている途中で、キャンセルしたいって言う事です。すくなくとも僕の認識では、dispatch_asyncでブロックをキューに渡してしまうとキャンセルできない様なので、これをやりたいとおもって見つけたのがディスパッチソースの中のDISPATCH_SOURCE_TYPE_DATA_ADDというやつです。

それを使って書いてみたソースがこちらです。今回は自分でキャンセルしてみたいので、Cocoa Applicationでプロジェクトを作ってAppDelegateを以下のように変更し、Interface Builderでウィンドウにボタンをひとつ作ってAppDelegateのcancelにつなげてください。

//
// AppDelegate.h
//
#import <Cocoa/Cocoa.h>
@interface AppDelegate : NSObject <nsapplicationDelegate> {
    NSWindow *window;
    dispatch_source_t _source;
}
@property (assign) IBOutlet NSWindow *window;
- (IBAction)cancel:(id)sender;
@end
//
// AppDelegate.m
//
#import "AppDelegate.h"
@implementation AppDelegate
@synthesize window;
- (void)applicationDidFinishLaunching:(NSNotification *)aNotification {
    dispatch_queue_t queue = dispatch_queue_create("test", NULL);
    _source = dispatch_source_create(DISPATCH_SOURCE_TYPE_DATA_ADD, 0, 0, queue);
    
    __block int count = 0;
    
    dispatch_source_set_event_handler(_source, ^{
        count++;
        printf("call %d\n", count);
        usleep(100000);
        dispatch_source_merge_data(_source, 1);
    });
    
    dispatch_source_set_cancel_handler(_source, ^{
        printf("end\n");
        dispatch_release(_source);
        _source = NULL;
        dispatch_release(queue);
        exit(0);
    });
    
    dispatch_source_merge_data(_source, 1);
    dispatch_resume(_source);
}
- (IBAction)cancel:(id)sender
{
    dispatch_source_cancel(_source);
}
@end

実行すると、event handlerのブロックが繰り返し呼ばれてログにカウントが表示されます。キャンセルボタンを押すと終了します。

DATA_ADDの使い方としては、dispatch_source_merge_data()の2つ目の引数で1以上の値を与えて呼ぶと、dispatch_source_set_event_handlerで登録したブロックが呼び出されるという感じになります。このサンプルでは、event_handlerのブロックの中から直接merge_dataを呼び出して処理を繰り返すようにしています。

merge_dataに0を渡してもブロックが呼ばれない事からも分かるように、たぶん本来はこれで処理自体をするものではなく、こちらのページにのっているように、dispatch_applyとかでやっている処理の進捗の量をmerge_dataの引数で受け取ってインジケータに表示するような用途に使うものだと思われます。

Grand Central Dispatch その6 timer

いまさらながら、GCDのサンプルソースはMac Dev Centerにもいろいろありますので参考に。中でもDispatch_Samplesなんかはシンプルなサンプルがたくさん集まってますので、最初に見るには良いと思います。

今回からはディスパッチの中でもソースというものを見ていこうと思います。前回のエントリにキューとソースは別なんて一度書いてしまいましたが、間違いでソースでもキューは使います。

と書いてて、ディスパッチのソースと、ソースコードの区別がわけ分からなくなりそうなので、ディスパッチのソースは「ディスパッチソース」と呼ぶ事にします。

ディスパッチソースの種類にはいろいろありますが、基本的には、何かしら外部からイベントを受け取ったらキューにブロックを渡すというもののようです。以下のようなものがあります。

DISPATCH_SOURCE_TYPE_DATA_ADD:
DISPATCH_SOURCE_TYPE_DATA_OR:
DISPATCH_SOURCE_TYPE_MACH_SEND
DISPATCH_SOURCE_TYPE_MACH_RECV
DISPATCH_SOURCE_TYPE_PROC
DISPATCH_SOURCE_TYPE_READ
DISPATCH_SOURCE_TYPE_SIGNAL
DISPATCH_SOURCE_TYPE_TIMER
DISPATCH_SOURCE_TYPE_VNODE
DISPATCH_SOURCE_TYPE_WRITE

まあ、ほとんどは自分じゃ使わないなぁと思って調べてなかったりするんで、簡単なそうなタイマーを見ていきます。まずはサンプルソースから。

#include <stdio.h>
#include <stdlib.h>
#include <dispatch/dispatch.h>
int main (int argc, const char * argv[]) {
    dispatch_queue_t queue = dispatch_queue_create("timerQueue", 0);
    //タイマーのソースを作成
    dispatch_source_t timer = dispatch_source_create(DISPATCH_SOURCE_TYPE_TIMER, 0, 0, queue);
    __block int i = 0;
    //イベントを受け取った時に実行されるブロックをソースに設定する
    dispatch_source_set_event_handler(timer, ^{
        printf("%d\n", i);
        i++;
        if (i == 10) dispatch_source_cancel(timer);
    });
    //ソースがキャンセルされてときに実行されるブロックをソースに設定する
    dispatch_source_set_cancel_handler(timer, ^{
        dispatch_release(timer);
        dispatch_release(queue);
        printf("end\n");
        exit(0);
    });
    dispatch_time_t start = dispatch_time(DISPATCH_TIME_NOW, NSEC_PER_SEC); //今から1秒後
    uint64_t interval = NSEC_PER_SEC / 5; //0.2秒
    //タイマーを設定する
    dispatch_source_set_timer(timer, start, interval, 0);
    printf("start\n");
    dispatch_resume(timer);
    dispatch_main();
    return 0;
}

実行すると、ログに0から10まで数字が0.2秒おきに出力されて、アプリが終了します。

では、コードを上から見ていきます。まずは、タイマーのディスパッチソースを作成しています。ディスパッチソースの作成はdispatch_source_createです。

dispatch_source_t
dispatch_source_create(dispatch_source_type_t type,
uintptr_t handle,
unsigned long mask,
dispatch_queue_t queue);

ひとつめの引数にはディスパッチソースの種類を渡します。タイマーはDISPATCH_SOURCE_TYPE_TIMERです。2つめの引数のhandleは、ディスパッチソースが動作する元になるものを渡しますが、タイマーは必要ないので0で良いです。3つ目もタイマーでは必要ありませんので、これも0です。最後のqueueには、ディスパッチソースからブロックを渡すキューを設定します。

ちなみに、キューはグローバルでもシリアルでも使えますが、グローバルキューだと他の色んなブロックが入っていたら後回しになって影響を受けて実行されるのが遅れやすくなりそうなので、シリアルキューにして完全にセパレートされている方が良いのかなぁと思います。あくまで予測ですけど。

dispatch_source_set_event_handlerは、イベントを受け取ったときに呼ばれるブロックを設定します。今回はタイマーですので、タイマーが定期的にイベントを投げてきて、ここのブロックが呼ばれる事になります。

dispatch_source_set_cancel_handlerはディスパッチソースがキャンセルされたときに呼ばれるブロックを設定します。基本、タイマーのディスパッチソースやキューを解放します。

dispatch_source_set_timerでは、タイマーの開始時間とインターバルを指定します。スタートの時間は、dispatch_timeを使うと今から何秒後みたいな感じで絶対時間のdispatch_time_tが取得できます。インターバルは、1秒がNSEC_PER_SECで定義されていますから、そこから求めるのが良いと思います。

で、忘れていけないのは、dispatch_resumeです。ディスパッチソースは作った段階ではsuspend状態ですので、resumeで発動させなければいけません。

Grand Central Dispatch その5 semaphore

ディスパッチはいままでの並列処理の方法と比べるとシンプルで簡単だ、なんて書かれていたりしますけど、どうやらセマフォは欠かせないようで、ディスパッチにもあります。

とりあえずサンプルコードから。

#include <stdio.h>
#include <unistd.h>
#include <dispatch/dispatch.h>
int main (int argc, const char * argv[]) {
    
    dispatch_queue_t queue = dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0);
    dispatch_group_t group = dispatch_group_create();
    dispatch_semaphore_t semaphore = dispatch_semaphore_create(1);
    
    for (int i = 0; i < 10; i++) {
        dispatch_group_async(group, queue, ^{
            dispatch_semaphore_wait(semaphore, DISPATCH_TIME_FOREVER);
            //クリティカルセクション開始
            printf("start %d\n", (int)i);
            usleep(10000);
            printf("end %d\n", (int)i);
            //クリティカルセクション終了
            dispatch_semaphore_signal(semaphore);
        });
    }
    
    dispatch_group_wait(group, DISPATCH_TIME_FOREVER);
    
    dispatch_release(semaphore);
    dispatch_release(group);
    
    return 0;
}

ディスパッチのセマフォの使い方としてはまず、dispatch_semaphore_createでセマフォのオブジェクトdispatch_semaphore_tを作ります。引数にはセマフォの旗の数を指定できます。0だと一回も進入できなくなってしまいますので1以上を指定します。

そして、クリティカルセクションをdispatch_semaphore_wait〜dispatch_semaphore_signalで囲みます。それぞれ引数にセマフォオブジェクトを渡します。waitでは進入できない時に待つ時間を指定できて、DISPATCH_TIME_FOREVERなら永久に待ちます。

サンプルコードを実行すると、クリティカルセクション開始〜終了としているところは同時に実行されず、printfでstartとendがセットで0から9まで順番に出力されます。もし、セマフォ関連の関数を排除したら、startのあとにスリープさせていますので、startが10個出力された後に、endが10個出力されるはずです。

という感じで、ディスパッチのキューに直接ブロックを渡す関係は以上で終わりです。ちなみに、キューにブロックを渡してしまうと後からキャンセルが出来なかったりしますのでご注意を。

その処理のキャンセルとかも含めて、ディスパッチにはソースという仕組みも用意されていますので、次回以降見ていこうと思います。

CoreAudio その3〜5改 AudioObjectでのプロパティ取得・設定

Mac OS X 10.6 Snow Leopardから、AudioHardware〜とかAudioDevice〜とかAudioStream〜とかのGet・Set系がことごとくDEPRECATEDになってしまいました。

僕の認識では、もともとAudioObject〜があって、そのコンビニエンスメソッドとしてAudioDeviceとかの関数があると思っていたのですけど、どうやら歴史的には逆だったようです。ヘッダをよく見てみたら、AudioObject系はわりと最近の10.4から増えてたんですね。

あと、AudioUnitがiPhoneと同じようにAudioComponentにいきなり変わっててびっくりしました。iPhoneで慣れ親しんでいたとはいえ、もちょっと緩やかに移行してくれてもいいんじゃないかなと思います。

プロパティの定数系も結構Deprecatedしてます。これはXcodeで警告を出してくれないので注意が必要です。ヘッダのコメントに”Some Day Be Deprecated”なんてこっそりかかれてます。Device系だとkAudioDevicePropertyDeviceNameから始まるところですね。10.7になっていきなりエラー出まくりなんて事のないようにしっかりチェックしておきましょう。

ということで、取得・設定系をAudioObjectを使ってやってみたいと思います。

AudioObjectでオーディオ情報の取得・設定をするにはAudioObjectGetPropertyDataやAudioObjectSetPropertyDataという関数を使います。これはいままでAudioHardware〜やAudioDevice〜など別々の関数でやっていたオーディオ情報の取得や設定を、ひとつでまかなえるものです。プロパティのサイズを調べるのにも、AudioObjectGetPropertyDataSizeという関数で行う事になります。

たとえば、プロパティの取得する関数はこんな感じで定義されています。

extern OSStatus
AudioObjectGetPropertyData(AudioObjectID inObjectID,
                           const AudioObjectPropertyAddress* inAddress,
                           UInt32 inQualifierDataSize,
                           const void* inQualifierData,
                           UInt32* ioDataSize,
                           void* outData)

最初のAudioObjectIDというのはAudioDeviceIDやAudioStreamIDをそのまま渡します。前の記事のその3でも説明しましたが、AudioDeviceIDもAudioStreamIDもAudioObjectIDをtypedefしているだけのもので、つまり全てAudioObjectIDです(リファレンス的にAudioDeviceなどはAudioObjectのサブクラスってことらしい)。AudioHardware〜系の関数をつかっていたプロパティのときはkAudioObjectSystemObjectを渡します。

ちょっとひとつ飛ばしまして、inQualifierDataSizeとinQualifierDataは、サンプルで使っているものが無かったのでよくわかりません。基本0とNULLで大丈夫なようです。もしかしたらまだ機能していないかもしれません。ioDataSizeとoutDataは取得するプロパティのサイズと受け取るメモリ領域で、前と同じです。

戻ってAudioObjectPropertyAddressは、

struct  AudioObjectPropertyAddress
{
    AudioObjectPropertySelector mSelector;
    AudioObjectPropertyScope    mScope;
    AudioObjectPropertyElement  mElement;
};
typedef struct AudioObjectPropertyAddress   AudioObjectPropertyAddress;

と定義されてます。デバイスとかストリームとか関係なくプロパティを特定するのに必要な情報ですね。

Selectorは〜PropertyIDの事です。Scopeは、ほとんどの場合はkAudioObjectPropertyScopeGlobalで、Deviceなどの入出力部分のときはInputとかOutputで指定したりします。Elementは基本kAudioObjectPropertyElementMaster(= 0)でOKだと思います(これ以外指定するパターンがすぐに見つけられませんでした。Channel的なところで0以外を使うのではないかと思います)。

最後に、サンプルソースです。Xcodeで新規プロジェクト→Command Line Tool→Foundationでプロジェクトを作成して、CoreAudio.Frameworkを追加して、main.mを以下のソースに差し替えてください。走らせると、デフォルトになっているオーディオデバイスの名前を表示して、サンプルレートを設定可能な中で変更します。

#import <Foundation/Foundation.h>
#import <CoreAudio/CoreAudio.h>
int main (int argc, const char * argv[]) {
    
    AudioDeviceID devID;
    UInt32 size;
    AudioObjectPropertyAddress address;
    AudioValueRange *sampleRates;
    CFStringRef deviceName = NULL;
    //デフォルトのアウトプットに設定されているオーディオデバイスを取得する
    address.mSelector = kAudioHardwarePropertyDefaultOutputDevice;
    address.mScope = kAudioObjectPropertyScopeGlobal;
    address.mElement = kAudioObjectPropertyElementMaster;
    size = sizeof(devID);
    
    AudioObjectGetPropertyData(kAudioObjectSystemObject, &address, 0, NULL, &size, &devID);
    
    //アウトプットの名前を取得する
    address.mSelector = kAudioObjectPropertyName;
    size = sizeof(deviceName);
    
    AudioObjectGetPropertyData(devID, &address, 0, NULL, &size, &deviceName);
    
    //アウトプットのデバイスが対応しているサンプルレートを取得する
    address.mSelector = kAudioDevicePropertyAvailableNominalSampleRates;
    AudioObjectGetPropertyDataSize(devID, &address, 0, NULL, &size);
    
    UInt32 numOfSampleRates = size / sizeof(AudioValueRange);
    sampleRates = calloc(numOfSampleRates, sizeof(AudioValueRange));
    
    AudioObjectGetPropertyData(devID, &address, 0, NULL, &size, sampleRates);
    
    //現在のサンプルレートを取得する
    Float64 currentSampleRate;
    address.mSelector = kAudioDevicePropertyNominalSampleRate;
    size = sizeof(Float64);
    
    AudioObjectGetPropertyData(devID, &address, 0, NULL, &size, ¤tSampleRate);
    
    //サンプルレートを別のにする
    int currentIndex = 0;
    for (int i = 0; i < numOfSampleRates; i++) {
        if (sampleRates[i].mMinimum == currentSampleRate) {
            currentIndex = i;
            break;
        }
    }
    
    int newIndex = currentIndex + 1;
    if (newIndex >= numOfSampleRates) newIndex = 0;
    
    //サンプルレートを設定する
    Float64 newSampleRate = sampleRates[newIndex].mMinimum;
    size = sizeof(Float64);
    
    AudioObjectSetPropertyData(devID, &address, 0, NULL, size, &newSampleRate);
    
    NSLog(@"device name = %@", deviceName);
    NSLog(@"new samplerate = %f", newSampleRate);
    
    free(sampleRates);
    CFRelease(deviceName);
    
    return 0;
}

Grand Central Dispatch その4 group

前回紹介したdispatch_applyはブロックの処理が終わるまで待ってくれていましたが、dispatch_asyncは別スレッドで処理されてしまうので、どの順番で処理されるのかも分からない状態でブロックを処理させっぱなしになってしまいます。

グループというのを使えば、asyncで処理する複数のブロックをひとつのグループにまとめて、それらのブロック全部が終わったタイミングを受け取る事が出来ます。

まず、グループを作る関数がこれです。

dispatch_group_t dispatch_group_create(void);

次に、グループで処理させたいブロックを渡すのがdispatch_group_asyncです。グループはasyncオンリーでsyncはありません。

void
dispatch_group_async(dispatch_group_t group,
dispatch_queue_t queue,
dispatch_block_t block);

そして、グループの処理全部が終わったら呼ばれるブロックを設定するのがdispatch_group_notifyです。基本的に、グループ内共通で使ったメモリ領域などを解放するのに使うようです。どこかに通知するのにも使えると思います。

void
dispatch_group_notify(dispatch_group_t group,
dispatch_queue_t queue,
dispatch_block_t block);

カレントスレッドで処理の終わりを待つ必要があれば、待たせることもできます。dispatch_group_waitです。どれだけ待つのかを指定できます。DISPATCH_TIME_FOREVERという定数をtimeoutに渡せば、いつまでも待ちます。

long
dispatch_group_wait(dispatch_group_t group, dispatch_time_t timeout);

なお、waitで待っても待たなくても、notifyで設定したブロックが呼ばれるのは別スレッドのようです。

サンプルソースです。

#include <stdio.h>
#include <dispatch/dispatch.h>
int main (int argc, const char * argv[]) {
    
    dispatch_queue_t queue = dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0);
    dispatch_group_t group = dispatch_group_create();
    
    __block int sum = 0;
    
    for (int i = 0; i < 10; i++) {
        dispatch_group_async(group, queue, ^{
            printf("+%d\n", i);
            sum += i;
        });
    }
    
    dispatch_group_notify(group, queue, ^{
        printf("finish");
        //<strike>dispatch_release(group);</strike>
    });
    
    dispatch_group_wait(group, DISPATCH_TIME_FOREVER);
    
    printf("sum = %d\n", sum);
    dispatch_release(group);
    
    return 0;
}

前回のapplyと同じような処理をさせています。waitが無ければ、おそらくsumは0で終わってしまうと思いますが、waitで待っている事でちゃんと45と答えが出ます。

waitの代わりにdispatch_main()にしておいてnotifyのブロックの中でsumを見れば、そこでも45となっていて、ちゃんとasyncで渡したブロックが全て実行された後に呼ばれる事が分かると思います。

ちなみに、notifyのブロックが呼ばれる前にasyncで新たなブロックを同じグループに入れると、その新しいブロックも終わってからnotifyのブロックが呼ばれます。が、、notifyのタイミングが新たなブロックの前か後かどちらでくるか分からなくなってしまうので、やらない方が良いと思います。

Grand Central Dispatch その3 apply

前回のコードのように同じ処理をforループでキューに渡すようなとき、それ専用の関数がディスパッチには既に用意されています。dispatch_applyです。

void
dispatch_apply(size_t iterations, dispatch_queue_t queue, void (^block)(size_t));

iterationsの数だけブロックをキューに渡して処理させます。ブロックはsize_tの引数を一つだけ持ち、よくあるforループと同じように0から1ずつ増やした値が渡されます。

まずは、サンプルコードです。

#include <stdio.h>
#include <dispatch/dispatch.h>
int main (int argc, const char * argv[]) {
    
    dispatch_queue_t queue = dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0);
    
    __block int sum = 0;
    
    dispatch_apply(10, queue, ^(size_t i){
        sum += i;
    });
    
    printf("sum = %d\n", sum); // sumは45になる
    
    return 0;
}

これを実行すると、0から9までがsumに足されて45になります。

dispatch_applyはグローバルキューに渡していても、ちゃんと全てのブロックが処理されるのを待ちますので、dispatch_main()が無くてもアプリが終了する前に全て足された答えが出ます。ですが、sync的に順番に処理されている訳ではなく、並列に処理はされています。ただ、asyncと同じという訳でもなく、applyでは、呼び出したスレッドと別スレッドを組み合わせて並列にブロックが実行されているようです(asyncは別スレッドだけ)。

ブロックの途中でスリープさせたりとかして試してみると、applyでは同時に2つまでしか処理されません(試したのがMacBook Airでコアが2つだから?)。asyncだといくらでも同時に実行されてしまいます。このあたりは、使い道の違いって感じですかね。