Audio Unit + vDSP:ソノグラフ表示

今回はAccelerateフレーム内のvDSP関数群を使ってソノグラフ表示を行います。
画面は以下の通りです。

ソノグラフ表示は以下の手順からなります。
・波形データの取得(サンプリング)
・ウィンドウ関数の適用
・高速フーリエ変換(FFT)の適用
・パワースペクトルの算出

このうちFFTとウィンドウ関数の処理にvDSPのAPIを使用します。

具体的にはvDSP関数群のうち以下の関数を使用しました。

vDSP_create_fftsetupD 倍精度のFFT用の構造体の生成
vDSP_destroy_fftsetupD 倍精度のFFT用の構造体の破棄
vDSP_fft_zipD 倍精度のFFTの実行
vDSP_blkman_windowD Blackmanウィンドウの生成
vDSP_hamm_windowD Hammingウィンドウの生成
vDSP_hann_windowD Hanningウィンドウの生成
vDSP_vmulD 倍精度ベクターデータの乗算

このプログラムはモデル・ビュー・コントローラー(MVC)から構成され、それぞれ「音声データクラス」、「ソノグラフビュークラス」、「アプリケーションデリゲートクラス」が対応します。音声データクラスは波形表示と全く同じなので説明は省略します。

●ソノグラフビュークラス(UCSonographView)

【メンバー変数】

主なメンバー変数を以下に示します。

	NSImage			*mImg;			// 画像イメージ
	FFTSetupD		mFFT;			// FFTハンドル
	DSPDoubleSplitComplex	mCmp;	// 複素数
	E_WINDOW		mWindowType;	// ウィンドウ関数種別
	double			*mWindow;		// ウィンドウデータ
	UCAudioData		*mAudioData;	// 音響データクラス
	double			*mOrgData;		// オリジナル波形データ
	double			*mPower;		// パワースペクトル
	Float64			mSamplingRate;	// サンプリングレート
	Float64			mDeltaT;		// Δt=1/mSamplingRate
	UInt32			mSampleCntInLog2n;	// サンプリング数(log2n)
	UInt32			mSampleCnt;		// サンプリング数
	Float32			mDeltaF;		// 周波数分解能
	Float32			mScale;			// スケール
	UInt32			mMaxFreq;		// 表示最大周波数
	UInt32			mFrameCount;	// 全フレーム数
	UInt32			mCurFrame;		// カレントフレームインデックス
	double			mAmpFactor;		// 増幅係数
	double			mNormFactor;	// 正規化係数

【初期化】

初期化処理を以下に示します。

- (id)initWithFrame:(NSRect)frame {
    self = [super initWithFrame:frame];
    if (self) {
        // Initialization code here.
		[self initColorTbl];
		mImg = [[NSImage alloc] initWithSize:frame.size];
		[mImg lockFocus];
		[[g_arrColorTbl objectAtIndex:0] set];
		NSRectFill( [self bounds] );
		[mImg unlockFocus];
		mAudioData = nil;
		mCmp.realp = (double *)malloc( sizeof(double) * MAX_SAMPLES );
		mCmp.imagp = (double *)malloc( sizeof(double) * MAX_SAMPLES );
		mOrgData = (double *)malloc( sizeof(double) * MAX_SAMPLES );
		mWindow = (double *)malloc( sizeof(double) * MAX_SAMPLES );
		mPower = (double *)malloc( sizeof(double) * (MAX_SAMPLES / 2.0) );
		mFFT = NULL;
		mFrameCount = 0;
		mCurFrame = 0;
		mAmpFactor = 1.0;
		mNormFactor = 255 / 51.587060580590524;	// 最大値が255になるための係数
	}
    return self;
}

【ウィンドウデータの生成】

ウィンドウデータの生成処理を以下に示します。

- (void)createWindow:(E_WINDOW)eWindow
{
	switch ( eWindow ) {
		case eBlkManWin:
			vDSP_blkman_windowD( mWindow, mSampleCnt, 0 );
			break;
		case eHammWin:
			vDSP_hamm_windowD( mWindow, mSampleCnt, 0 );
			break;
		case eHannDenormWin:
			vDSP_hann_windowD( mWindow, mSampleCnt, vDSP_HANN_DENORM );
			break;
		case eHannNormWin:
			vDSP_hann_windowD( mWindow, mSampleCnt, vDSP_HANN_NORM );
			break;
	}
}

【ソナグラフの描画】

ソナグラフの描画処理を以下に示します。波形の時は線のデータだったのでNSBezierPathで描画命令を生成していましたが、ソナグラフの場合、面を塗りつぶす処理となるのでNSImageで描画データを生成・保持するようにしました。

// ソノグラフの描画
- (void)drawSonograph
{
	// vDSP初期化
	[self setupFFT];

	// イメージサイズの算出
	NSRect rectVisible = [(NSClipView *)[self superview] documentVisibleRect];
	CGFloat fX = rectVisible.size.width / (mScale / mDeltaT) * (CGFloat)mFrameCount;
	NSSize size = [mImg size];
	size.width = fX + rectVisible.size.width;
	[mImg setSize:size];
	[self setFrameSize:size];

	UInt32	nCntX, nCntY;
	nCntX = (mFrameCount + mSampleCnt - 1) / mSampleCnt;
	nCntY = (UInt32)((Float32)mMaxFreq / mDeltaF);

	CGFloat	fDeltaX, fDeltaY;
	fDeltaX = fX / ((CGFloat)mFrameCount / (CGFloat)mSampleCnt);
	fDeltaY = rectVisible.size.height / (CGFloat)nCntY;

	NSLog( @"nCntX=%d, nCntY=%d, fDeltaX=%f fDeltaY=%f\n", nCntX, nCntY, fDeltaX, fDeltaY );

	NSRect rect = NSMakeRect(rectVisible.size.width / 2, 0, fDeltaX, fDeltaY);

	[mImg lockFocus];
	
	// 背景描画
	NSRect rectBounds = [self bounds];
	[[g_arrColorTbl objectAtIndex:0] set];
	NSRectFill( rectBounds );

	UInt32	i, j;
	for ( i = 0; i < mFrameCount; i += mSampleCnt ) {
		double *real, *img;
		real = mOrgData;
		img = mCmp.imagp;
		
		// データのセットアップ
		for ( j = 0; j < mSampleCnt; j++ ) {
			*real++ = [mAudioData frameAtIndex:i + j];
			*img++ = 0.0;
		}

		// 窓関数の適用
		vDSP_vmulD( mOrgData, 1, mWindow, 1, mCmp.realp, 1, mSampleCnt );
		
		// FFTの実行
		vDSP_fft_zipD( mFFT, &mCmp, 1, mSampleCntInLog2n, kFFTDirection_Forward );
		
		// パワースペクトルの算出
		real = mCmp.realp;
		img = mCmp.imagp;
		for ( j = 0; j < mSampleCnt / 2; j++, real++, img++ ) {
			double dWk = sqrt( *real * *real + *img * *img );
			mPower[j] = mAmpFactor * mNormFactor * 20 * log10( dWk + 1.0 );
			if ( mPower[j] > 255 ) {
				mPower[j] = 255;
			}
		}
		
		rect.origin.y = 0;
		for ( j = 0; j < nCntY; j++ ) {
			[[g_arrColorTbl objectAtIndex:(NSUInteger)mPower[j]] set];
			NSRectFill( rect );
			rect.origin.y += fDeltaY;
		}
	
		rect.origin.x += fDeltaX;
	}
	
	[mImg unlockFocus];

	[self disposeFFT];
	mFFT = NULL;
}

●アプリケーションデリゲートクラス(SonographViewAppDelegate)

UIとモデル、ビュー間の仲立ちをしてアプリケーション全体の動きを制御します。

SonographViewAppDelegate.h:アプリケーションデリゲートクラスインターフェースファイル
#import <Cocoa/Cocoa.h>
#import "UCSonographView.h"
#import "UCAudioData.h"
#import "UCAudioDataDelegate.h"

@interface SonographViewAppDelegate : NSObject <NSApplicationDelegate, UCAudioDataDelegate> {
    NSWindow *window;
	UCSonographView *outView;
	NSTextField *outTxtLabel1;
	NSTextField *outTxtLabel2;
	NSTextField *outTxtLabel3;
	NSTextField *outTxtLabel4;
	NSTextField *outTxtFileName;
	NSButton *outBtnPlay;
	NSButton *outBtnChoose;
	NSComboBox *outCmbSamplingCnt;
	NSComboBox *outCmbWindow;
	NSComboBox *outCmbMaxFreq;
	NSSlider *outSldVol;
	NSSlider *outSldScale;
	NSSlider *outSldAmp;
	NSProgressIndicator *outProgInd;

@private
	NSString	*mAudioFilePath;
	UCAudioData *mAudioData;
	BOOL		mPlaying;
}

@property (assign) IBOutlet NSWindow *window;
@property (assign) IBOutlet UCSonographView *outView;
@property (assign) IBOutlet NSTextField *outTxtLabel1;
@property (assign) IBOutlet NSTextField *outTxtLabel2;
@property (assign) IBOutlet NSTextField *outTxtLabel3;
@property (assign) IBOutlet NSTextField *outTxtLabel4;
@property (assign) IBOutlet NSTextField *outTxtFileName;
@property (assign) IBOutlet NSButton *outBtnPlay;
@property (assign) IBOutlet NSButton *outBtnChoose;
@property (assign) IBOutlet NSComboBox *outCmbSamplingCnt;
@property (assign) IBOutlet NSComboBox *outCmbWindow;
@property (assign) IBOutlet NSComboBox *outCmbMaxFreq;
@property (assign) IBOutlet NSSlider *outSldVol;
@property (assign) IBOutlet NSSlider *outSldScale;
@property (assign) IBOutlet NSSlider *outSldAmp;
@property (assign) IBOutlet NSProgressIndicator *outProgInd;

//
// NSApplication delegate methods
//
- (void)applicationDidFinishLaunching:(NSNotification *)aNotification;
- (BOOL)applicationShouldTerminateAfterLastWindowClosed:(NSApplication *) theApplication;
- (NSApplicationTerminateReply)applicationShouldTerminate:(NSApplication *) sender;

//
// イベントハンドラ
//
- (IBAction)onBtnChoose:(id)sender;
- (IBAction)onBtnPlay:(id)sender;
- (IBAction)onCmbSamplingCnt:(id)sender;
- (IBAction)onCmbWindow:(id)sender;
- (IBAction)onCmbMaxFreq:(id)sender;
- (IBAction)onSldScale:(id)sender;
- (IBAction)onSldVolume:(id)sender;
- (IBAction)onSldAmp:(id)sender;
- (IBAction)onScroller:(id)sender;

//
// UCAudioData Delegate
//
- (void)increment:(id)oFrames;
- (void)musicDidEnd:(id)obj;

// 内部メソッド
- (UInt32)selectedSamplingCount;
- (E_WINDOW)selectedWindow;
- (UInt32)selectedMaxFrequency;

// プログレスバー表示
- (void)showProgressIndicator;

// プログレスバー非表示
- (void)hideProgressIndicator;

@end

AudioWaveViewAppDelegate.m:アプリケーションデリゲートクラスインプリメントファイル
#import "SonographViewAppDelegate.h"

@implementation SonographViewAppDelegate

@synthesize window;
@synthesize outView;
@synthesize outTxtLabel1;
@synthesize outTxtLabel2;
@synthesize outTxtLabel3;
@synthesize outTxtLabel4;
@synthesize outTxtFileName;
@synthesize outBtnPlay;
@synthesize outBtnChoose;
@synthesize outCmbSamplingCnt;
@synthesize outCmbWindow;
@synthesize outCmbMaxFreq;
@synthesize outSldVol;
@synthesize outSldScale;
@synthesize outSldAmp;
@synthesize outProgInd;

//
// アプリケーション起動時の処理
//
- (void)applicationDidFinishLaunching:(NSNotification *)aNotification {
	// Insert code here to initialize your application 
	mAudioData = nil;
	mAudioFilePath = nil;
	mPlaying = NO;
	
	// コンボボックスの初期値設定
	[outCmbSamplingCnt selectItemAtIndex:1];
	[outCmbWindow selectItemAtIndex:0];
	[outCmbMaxFreq selectItemAtIndex:3];
	
	// 周波数ラベルの設定
	[outTxtLabel1 setStringValue:@"10k"];
	[outTxtLabel2 setStringValue:@"7.5k"];
	[outTxtLabel3 setStringValue:@"5k"];
	[outTxtLabel4 setStringValue:@"2.5k"];
}

//
// 最後のウィンドウクローズ時の指示
//
- (BOOL)applicationShouldTerminateAfterLastWindowClosed:(NSApplication *) theApplication
{
	return YES;
}

//
// アプリケーション終了時の処理
//
- (NSApplicationTerminateReply)applicationShouldTerminate:(NSApplication *) sender
{
	if ( mPlaying == YES ) {
		[mAudioData stop];
	}
	if ( mAudioData != nil ) {
		[mAudioData release];
	}
	if ( mAudioFilePath != nil ) {
		[mAudioFilePath release];
	}
	
	return NSTerminateNow;
}

//
// イベントハンドラ
//
// ファイル選択ボタンハンドラ
- (IBAction)onBtnChoose:(id)sender
{
	NSOpenPanel *oOpnPnl = [NSOpenPanel openPanel];
	int nResult = [oOpnPnl runModalForTypes:nil];
	if ( nResult == NSFileHandlingPanelOKButton ) {
		NSString *strPath = [[oOpnPnl filenames] objectAtIndex:0];
		NSFileManager *oFM = [NSFileManager defaultManager];
		if ( [oFM fileExistsAtPath:strPath] != YES ) {
			NSBeep();
			return;
		}
		[outTxtFileName setStringValue:[strPath lastPathComponent]];

		[self showProgressIndicator];

		if ( mAudioFilePath != nil ) {
			[mAudioFilePath release];
		}
		mAudioFilePath = [strPath retain];
		
		if ( mAudioData != nil ) {
			[mAudioData release];
		}
		mAudioData = [[UCAudioData alloc] initWithContentsOfFile:mAudioFilePath volume:[outSldVol floatValue] delegate:self];
		if ( mAudioData == nil ) {
			NSBeep();
			return;
		}
		[outBtnPlay setEnabled:YES];
		
		float fScale = [outSldScale floatValue];
		UInt32 nSamplingCnt = [self selectedSamplingCount];
		E_WINDOW eWin = [self selectedWindow];
		UInt32 nMaxFreq = [self selectedMaxFrequency];
		double dAmp = [outSldAmp doubleValue];
		[outView setAudioData:mAudioData withScale:fScale samples:nSamplingCnt window:eWin maxFreq:nMaxFreq ampFactor:dAmp];
		
		[self hideProgressIndicator];

		[outView scrollPoint:NSMakePoint( 0, 0 )];
	}
}

// 再生・停止ボタンハンドラ
- (IBAction)onBtnPlay:(id)sender
{
	if ( mPlaying == YES ) {
		[mAudioData stop];
		[outBtnPlay setTitle:@"Play"];
		[outBtnChoose setEnabled:YES];
		mPlaying = NO;
	} else {
		[mAudioData playAtIndex:[outView curFrame]];
		[outBtnPlay setTitle:@"Stop"];
		[outBtnChoose setEnabled:NO];
		mPlaying = YES;
	}
}

// サンプリング数コンボボックスハンドラ
- (IBAction)onCmbSamplingCnt:(id)sender
{
	[self showProgressIndicator];
	[outView setSampleCount:[self selectedSamplingCount]];
	[self hideProgressIndicator];
}

// ウィンドウコンボボックスハンドラ
- (IBAction)onCmbWindow:(id)sender
{
	[self showProgressIndicator];
	[outView selectWindow:[self selectedWindow]];
	[self hideProgressIndicator];
}

// 周波数レンジコンボボックスハンドラ
- (IBAction)onCmbMaxFreq:(id)sender
{
	NSComboBox *oCmb = (NSComboBox *)sender;
	NSInteger nSel = [oCmb indexOfSelectedItem];
	UInt32 nFreq;
	switch ( nSel ) {
		case 0:
			nFreq = 1000;
			[outTxtLabel1 setStringValue:@"1000"];
			[outTxtLabel2 setStringValue:@"750"];
			[outTxtLabel3 setStringValue:@"500"];
			[outTxtLabel4 setStringValue:@"250"];
			break;
		case 1:
			nFreq = 2000;
			[outTxtLabel1 setStringValue:@"2000"];
			[outTxtLabel2 setStringValue:@"1500"];
			[outTxtLabel3 setStringValue:@"1000"];
			[outTxtLabel4 setStringValue:@"500"];
			break;
		case 2:
			nFreq = 5000;
			[outTxtLabel1 setStringValue:@"5000"];
			[outTxtLabel2 setStringValue:@"3750"];
			[outTxtLabel3 setStringValue:@"2500"];
			[outTxtLabel4 setStringValue:@"1250"];			
			break;
		case 3:
			nFreq = 10000;
			[outTxtLabel1 setStringValue:@"10k"];
			[outTxtLabel2 setStringValue:@"7.5k"];
			[outTxtLabel3 setStringValue:@"5k"];
			[outTxtLabel4 setStringValue:@"2.5k"];
			break;
		case 4:
			nFreq = 20000;
			[outTxtLabel1 setStringValue:@"20k"];
			[outTxtLabel2 setStringValue:@"15k"];
			[outTxtLabel3 setStringValue:@"10k"];
			[outTxtLabel4 setStringValue:@"5k"];
			break;
	}
	[self showProgressIndicator];
	[outView setMaxFrequency:nFreq];
	[self hideProgressIndicator];
}

// スケールスライダーハンドラ
- (IBAction)onSldScale:(id)sender
{
	[self showProgressIndicator];
	[outView setScale:[outSldScale floatValue]];
	[self hideProgressIndicator];
}

// 音量スライダーハンドラ
- (IBAction)onSldVolume:(id)sender
{
	[mAudioData setVolume:[outSldVol floatValue]];
}

// 増幅率スライダーハンドラ
- (IBAction)onSldAmp:(id)sender
{
	[self showProgressIndicator];
	[outView setAmplificationFactor:[outSldAmp doubleValue]];
	[self hideProgressIndicator];
}

// スクロールバーハンドラ
- (IBAction)onScroller:(id)sender
{
	if ( mPlaying ) {
		[mAudioData stop];
	}
	
	[outView onScroller:sender];
	
	if ( mPlaying ) {
		[mAudioData playAtIndex:[outView curFrame]];
	}
}

//
// UCAudioData Delegate
//
- (void)increment:(id)oFrames
{
	UInt32 nFrames = [(NSNumber *)oFrames unsignedIntValue];
	[outView incrementFrame:nFrames];
}

- (void)musicDidEnd:(id)obj
{
	[mAudioData stop];
	[outBtnPlay setTitle:@"Play"];
	[outBtnChoose setEnabled:YES];
	mPlaying = NO;
}

// 内部メソッド
- (UInt32)selectedSamplingCount
{
	NSInteger nSel = [outCmbSamplingCnt indexOfSelectedItem];
	switch ( nSel ) {
		case 0:
			return 9;		// 512
			break;
		case 1:
			return 10;		// 1024
			break;
		case 2:
			return 11;		// 2048
			break;
		case 3:
			return 12;		// 4096
			break;
		case 4:
			return 13;		// 8192
			break;
		default:
			return 10;		// 1024
			break;
	}
}

- (E_WINDOW)selectedWindow
{
	NSInteger nSel = [outCmbWindow indexOfSelectedItem];
	switch ( nSel ) {
		case 0:
			return eBlkManWin;
			break;
		case 1:
			return eHammWin;
			break;
		case 2:
			return eHannDenormWin;
			break;
		case 3:
			return eHannNormWin;
			break;
		default:
			return eBlkManWin;
			break;
	}
}

- (UInt32)selectedMaxFrequency
{
	NSInteger nSel = [outCmbMaxFreq indexOfSelectedItem];
	switch ( nSel ) {
		case 0:
			return 1000;
			break;
		case 1:
			return 2000;
			break;
		case 2:
			return 5000;
			break;
		case 3:
			return 10000;
			break;
		case 4:
			return 20000;
			break;
		default:
			return 10000;
			break;
	}
}

// プログレスバー表示スレッド
- (void)showProgressIndicator
{
	[outProgInd setHidden:NO];
	[outProgInd startAnimation:self];
}

// プログレスバー非表示
- (void)hideProgressIndicator
{
	[outProgInd stopAnimation:self];
	[outProgInd setHidden:YES];
}


@end

サンプルプログラムのダウンロード(49KB)

【作成・確認環境】
MacOS X v10.6.4
Xcode v3.2.3

一覧に戻る