Audio Unit:音楽波形表示

いよいよ音楽ファイルの波形データを表示するプログラムです。
画面は以下の通りです。

苦労したのは先日書いたスクロール時座標軸も一緒にスクロールされてしまうことと、再生中に停止ボタンをクリックするとアプリがハングアップしてしまう不具合でした。

最初の不具合は先日書いたようにScroll Viewの設定で"Copy On Scroll"のチェックを外すことで解決しました。

次の不具合は調査に難儀しましたが、Audio Unitのコールバック関数からdelegateのメソッドを直接呼ぶのではなくperformSelectorOnMainThread:withObject:waitUntilDone:メソッドで完了を待ち合わせずに呼び出すことで解決しました。コールバック関数がどのスレッドで動作しているか調べて見たところメインスレッドとは別スレッドで動作していることがわかり、そこが臭いと思って件のメソッド経由でメインスレッドで画面の描画を行っているdelegateメソッドを動作させるようにしたらうまくいきました。また、直接呼び出していた時はデータの欠落が生じるのか所々でノイズが乗りましたがメインスレッドで動作させるようにしたらノイズは乗らなくなりました。

コールバック関数のコードを以下に示します。

//
// コールバック関数
//
OSStatus	RenderCallback(void	 *inRefCon,
						   AudioUnitRenderActionFlags *ioActionFlags,
						   const AudioTimeStamp *inTimeStamp,
						   UInt32				inBusNumber,
						   UInt32				inNumberFrames,
						   AudioBufferList		*ioData)
{
	//NSLog(@"inNumberFrames = %d",inNumberFrames);	
	NSAutoreleasePool *oARP = [[NSAutoreleasePool alloc] init];
	
	UCAudioData *oAudioData = inRefCon;

	float *outL = ioData->mBuffers[0].mData;
	float *outR = ioData->mBuffers[1].mData;
	
	int i, j;
	for (i=0; i< inNumberFrames; i++){
		if ( [oAudioData curFrame] >= [oAudioData frameCount] ) {
			break;
		}
		float wave = [oAudioData frameAtIndex:[oAudioData curFrame]];
		*outL++ = wave;
		*outR++ = wave;
		[oAudioData incCurFrame];
    }

	if ( i == 0 ) {
		// EOF
		for ( j = 0; j < inNumberFrames; j++ ) {
			*outL++ = 0.0;
			*outR++ = 0.0;
		}
		//if ( [[oAudioData delegate] respondsToSelector:@selector(musicDidEnd)] ) {
		//	[[oAudioData delegate] musicDidEnd];
		//}
		[[oAudioData delegate] performSelectorOnMainThread:@selector(musicDidEnd:)
												withObject:nil
											 waitUntilDone:NO
		 ];
		[oARP release];
		return noErr;
	}

	// Padding
	for ( j = i; j < inNumberFrames; j++ ) {
		*outL++ = 0.0;
		*outR++ = 0.0;
	}

	//if ( [[oAudioData delegate] respondsToSelector:@selector(increment:)] ) {
	//	[[oAudioData delegate] increment:i];
	//}
	[[oAudioData delegate] performSelectorOnMainThread:@selector(increment:)
											withObject:[NSNumber numberWithUnsignedInt:i]
										 waitUntilDone:NO
	 ];
	
	[oARP release];
	return noErr;
}

このプログラムはモデル・ビュー・コントローラー(MVC)から構成され、それぞれ「音声データクラス」、「波形表示ビュークラス」、「アプリケーションデリゲートクラス」が対応します。

●音声データクラス(UCAudioData)

指定された音楽ファイルをExtended Audio File Serviceを使ってメモリー上に読み込み、Audio Unitで再生する機能を提供します。
波形表示を1つにしたいのでモノラルに変換して読み込んでいます。

UCAudioData.h:音声データクラスインターフェースファイル
#import <Cocoa/Cocoa.h>
#import <AudioUnit/AudioUnit.h>
#import <AudioToolbox/ExtendedAudioFile.h>
#import "UCAudioDataDelegate.h"

#define	CHANNEL_NUM		1

@interface UCAudioData : NSObject {
	AudioUnit		mOutUnit;		// Audio Unit(Default Output)
	Float64			mSamplingRate;	// サンプリングレート
	UInt32			mFrameCount;	// フレーム数
	UInt32			mCurFrame;		// カレントフレーム
	float			*mWaveDate;		// 波形データバッファ
	ExtAudioFileRef	mAudioFileRef;	// extended audio file object.
	float			mVolume;		// 音量
	id <UCAudioDataDelegate>	mDelegate;		// delegate
}

- (id)initWithContentsOfFile:(NSString *)strPath volume:(float)fVol delegate:(id <UCAudioDataDelegate>)delegate;
- (void)dealloc;
- (void)setVolume:(float)fVol;
- (Float64)samplingRate;
- (UInt32)frameCount;
- (UInt32)curFrame;
- (void)incCurFrame;
- (float)frameAtIndex:(UInt32)nIndex;
- (void)playAtIndex:(UInt32)nIndex;
- (void)stop;
- (id)delegate;

// 内部メソッド
- (BOOL)initAU;
- (BOOL)openAudioFile:(NSString *)strPath;
- (BOOL)readAudioFile;

@end

UCMyView.m:Custom Viewクラスインプリメントファイル
#import "UCAudioData.h"


@implementation UCAudioData
//
// コールバック関数
//
OSStatus	RenderCallback(void	 *inRefCon,
						   AudioUnitRenderActionFlags *ioActionFlags,
						   const AudioTimeStamp *inTimeStamp,
						   UInt32				inBusNumber,
						   UInt32				inNumberFrames,
						   AudioBufferList		*ioData)
{
	//NSLog(@"inNumberFrames = %d",inNumberFrames);	
	NSAutoreleasePool *oARP = [[NSAutoreleasePool alloc] init];
	
	UCAudioData *oAudioData = inRefCon;

	float *outL = ioData->mBuffers[0].mData;
	float *outR = ioData->mBuffers[1].mData;
	
	int i, j;
	for (i=0; i< inNumberFrames; i++){
		if ( [oAudioData curFrame] >= [oAudioData frameCount] ) {
			break;
		}
		float wave = [oAudioData frameAtIndex:[oAudioData curFrame]];
		*outL++ = wave;
		*outR++ = wave;
		[oAudioData incCurFrame];
    }

	if ( i == 0 ) {
		// EOF
		for ( j = 0; j < inNumberFrames; j++ ) {
			*outL++ = 0.0;
			*outR++ = 0.0;
		}
		//if ( [[oAudioData delegate] respondsToSelector:@selector(musicDidEnd)] ) {
		//	[[oAudioData delegate] musicDidEnd];
		//}
		[[oAudioData delegate] performSelectorOnMainThread:@selector(musicDidEnd:)
												withObject:nil
											 waitUntilDone:NO
		 ];
		[oARP release];
		return noErr;
	}

	// Padding
	for ( j = i; j < inNumberFrames; j++ ) {
		*outL++ = 0.0;
		*outR++ = 0.0;
	}

	//if ( [[oAudioData delegate] respondsToSelector:@selector(increment:)] ) {
	//	[[oAudioData delegate] increment:i];
	//}
	[[oAudioData delegate] performSelectorOnMainThread:@selector(increment:)
											withObject:[NSNumber numberWithUnsignedInt:i]
										 waitUntilDone:NO
	 ];
	
	[oARP release];
	return noErr;
}

//
// 初期化メソッド
//
- (id)initWithContentsOfFile:(NSString *)strPath volume:(float)fVol delegate:(id)delegate
{
	self = [super init];
	if ( self == nil ) {
		return nil;
	}
	
	// Audio Unitの初期化
	BOOL bRet = [self initAU];
	if ( bRet != YES ) {
		[self release];
		return nil;
	}
	
	// Audio Fileのオープン
	bRet = [self openAudioFile:strPath];
	if ( bRet != YES ) {
		[self release];
		return nil;
	}

	// AudioFileの読み込み
	bRet = [self readAudioFile];
	if ( bRet != YES ) {
		[self release];
		return nil;
	}

	// メンバー変数の初期化
	mCurFrame = 0;
	mVolume = fVol;
	[self setVolume:fVol];
	mDelegate = delegate;

	return self;
}

//
// 後処理
//
- (void)dealloc
{
	AudioUnitUninitialize(mOutUnit);
	CloseComponent(mOutUnit);
	
	ExtAudioFileDispose( mAudioFileRef );

	if ( mWaveDate != NULL ) {
		free( mWaveDate );
	}
	
	[super dealloc];
}

//
// 音量の設定
//
- (void)setVolume:(float)fVol
{
	mVolume = fVol;
	OSStatus err = AudioUnitSetParameter( mOutUnit,
										 kHALOutputParam_Volume,
										 kAudioUnitScope_Global,
										 0,
										 mVolume,
										 0 );
	if ( err != noErr ) {
		NSLog( @"AudioUnitSetParameter(kHALOutputParam_Volume) failed. err=%d\n", err );
	}
}

//
// サンプリングレートの取得
//
- (Float64)samplingRate
{
	return mSamplingRate;
}

//
// 全フレーム数の取得
//
- (UInt32)frameCount
{
	return mFrameCount;
}

//
// カレントフレームの取得
//
- (UInt32)curFrame
{
	return mCurFrame;
}

//
// カレントフレームのインクリメント
//
- (void)incCurFrame
{
	mCurFrame++;
}

//
// 指定位置のフレームデータを取得する
//
- (float)frameAtIndex:(UInt32)nIndex
{
	if ( nIndex >= mFrameCount ) {
		return 0.0;
	} else {
		return mWaveDate[nIndex];
	}
}

//
// 再生の開始
//
- (void)playAtIndex:(UInt32)nIndex
{
	mCurFrame = nIndex;
	AudioOutputUnitStart(mOutUnit);
}

//
// 停止
//
- (void)stop
{
	AudioOutputUnitStop(mOutUnit);
}

//
// delegateオブジェクトの取得
//
- (id)delegate
{
	return mDelegate;
}

// 内部メソッド
//
// Audio Unitの初期化
//
- (BOOL)initAU
{
	// デフォルト出力のAudio Unitの取得
	ComponentDescription cd;
	cd.componentType = kAudioUnitType_Output;
	cd.componentSubType = kAudioUnitSubType_DefaultOutput;
	cd.componentManufacturer = kAudioUnitManufacturer_Apple;
	cd.componentFlags = 0;
	cd.componentFlagsMask = 0;
	
	Component comp = FindNextComponent(NULL, &cd);
	OpenAComponent(comp, &mOutUnit);
	
	// サンプリングレートの取得
	AudioStreamBasicDescription tASBD;
	UInt32 nSize = sizeof(tASBD);
	OSStatus err = AudioUnitGetProperty( mOutUnit, kAudioUnitProperty_StreamFormat, kAudioUnitScope_Output, 0, &tASBD, &nSize );
	if ( err != noErr ) {
		NSLog( @"AudioUnitGetProperty(kAudioUnitProperty_StreamFormat) failed. err=%d\n", err );
		return NO;
	}
	mSamplingRate = tASBD.mSampleRate;
	NSLog( @"mSamplingRate=%f\n", mSamplingRate );
	
	// コールバック関数の登録
    AURenderCallbackStruct input;
	input.inputProc = RenderCallback;
	input.inputProcRefCon = self;
	
	AudioUnitSetProperty (mOutUnit,
						  kAudioUnitProperty_SetRenderCallback,
						  kAudioUnitScope_Input,
						  0,
						  &input,
						  sizeof(input));
	
	// Audio Unitの初期化
	AudioUnitInitialize(mOutUnit);
	
	return YES;
}

//
// Audio Fileのオープン
//
- (BOOL)openAudioFile:(NSString *)strPath
{
	NSURL *oURL = [NSURL fileURLWithPath:strPath];
	OSStatus err = ExtAudioFileOpenURL( (CFURLRef)oURL, &mAudioFileRef );
	if ( err ) {
		NSLog( @"ExtAudioFileOpenURL failed.(err=%d)\n", err );
		return NO;
	}
	
	UInt32 nSize = sizeof(SInt64);
	err = ExtAudioFileGetProperty( mAudioFileRef,
								  kExtAudioFileProperty_FileLengthFrames,
								  &nSize,
								  &mFrameCount );
	if ( err ) {
		NSLog( @"ExtAudioFileGetProperty failed.(err=%d)\n", err );
		return NO;
	}
	NSLog( @"Frame Count = %d\n", mFrameCount );
	
	AudioStreamBasicDescription tFormat;
	
	tFormat.mChannelsPerFrame = CHANNEL_NUM;
	
	tFormat.mSampleRate = mSamplingRate;
	tFormat.mFormatFlags = kAudioFormatFlagsNativeFloatPacked;
	tFormat.mFormatID = kAudioFormatLinearPCM;
	tFormat.mBytesPerPacket = 4 * CHANNEL_NUM;
	tFormat.mFramesPerPacket = 1;
	tFormat.mBytesPerFrame = 4 * CHANNEL_NUM;
	tFormat.mBitsPerChannel = 32;
	
	nSize = sizeof(AudioStreamBasicDescription);
	err = ExtAudioFileSetProperty(mAudioFileRef,
								  kExtAudioFileProperty_ClientDataFormat, 
								  nSize,
								  &tFormat);
	if(err){
		NSLog( @"kExtAudioFileProperty_ClientDataFormat failed.(err=%d)\n", err );
		return NO;
	}
	
	return YES;
}

//
// 波形データの読み込み
//
- (BOOL)readAudioFile
{
	AudioBufferList audioBuffer;
	
    audioBuffer.mNumberBuffers = 1;
    audioBuffer.mBuffers[0].mNumberChannels = CHANNEL_NUM;
    audioBuffer.mBuffers[0].mData = malloc( mFrameCount * CHANNEL_NUM * sizeof(float) );
    audioBuffer.mBuffers[0].mDataByteSize = mFrameCount * CHANNEL_NUM * sizeof(float);

	OSStatus err = ExtAudioFileRead( mAudioFileRef, &mFrameCount, &audioBuffer );
	if ( err != noErr ) {
		NSLog( @"ExtAudioFileRead() failed. err=%d\n", err );
		free( audioBuffer.mBuffers[0].mData );
		return NO;
	}
	
	NSLog( @"mFrameCount=%d\n", mFrameCount );
	
	mWaveDate = audioBuffer.mBuffers[0].mData;
	
	return YES;
}

@end

●波形表示ビュークラス(UCWaveView)

音声データクラスのオブジェクトから波形データを取得し、対応する波形をビュー中に描画します。
波形の描画命令は全フレーム分作成し、Scroll Viewの機能を使ってスクロール表示させます。

UCWaveView.h:波形表示ビュークラスインターフェースファイル
#import <Cocoa/Cocoa.h>
#import "UCAudioData.h"

// 座標軸の分割数
#define DIV_CNT_X_AXIS		10
#define DIV_CNT_Y_AXIS		4

// 描画座標計算作業用構造体
typedef struct  {
	BOOL		bValid;				// データの有効/無効
	NSPoint		ptLower;			// Line描画開始位置(Y座標の小さい側)
	NSPoint		ptUpper;			// Line描画終了位置(Y座標の大きい側)
}   T_DRAW_LINE_INFO;

// 構造体初期化用マクロ
#define INIT_DRAW_LINE_INFO(t) {t.bValid = NO; t.ptLower.x = t.ptLower.y = -1; t.ptUpper.x = t.ptUpper.y = -1;}

@interface UCWaveView : NSView {
	UCAudioData		*mAudioData;	
	Float64			mSamplingRate;	// サンプリングレート
	UInt32			mFrameCount;
	UInt32			mCurFrame;
	Float64			mDeltaT;
	CGFloat			mScale;
	NSSize			mPrevSize;
	NSBezierPath	*mWaveLine;
	NSBezierPath	*mAxisLine;
	NSBezierPath	*mCursorLine;
}

- (id)initWithFrame:(NSRect)frame;
- (void)dealloc;
- (void)drawRect:(NSRect)dirtyRect;

// 公開メソッド
- (void)setAudioData:(UCAudioData *)oData withScale:(float)fScale;
- (void)setScale:(float)fScale;
- (void)incrementFrame:(UInt32)nFrames;
- (void)onScroller:(id)sender;
- (UInt32)curFrame;
- (NSString *)totalTime;
- (NSString *)curTime;

// 内部メソッド
- (void)createWaveBezierPath;
- (void)createAxisBezierPath;
- (void)createCursorBezierPath;
- (NSString *)timeString:(Float64)dTime;

@end
UCWaveView.m:波形表示ビュークラスインプリメントファイル
#import "UCWaveView.h"


@implementation UCWaveView

//
// 初期化
//
- (id)initWithFrame:(NSRect)frame {
    self = [super initWithFrame:frame];
    if (self) {
        // Initialization code here.
		mPrevSize = NSMakeSize(0.0, 0.0);
		mWaveLine = nil;
		mAxisLine = nil;
		mCursorLine = nil;
    }
    return self;
}

//
// 後処理
//
- (void)dealloc
{
	if ( mWaveLine != nil ) {
		[mWaveLine release];
	}
	if ( mAxisLine != nil ) {
		[mAxisLine release];
	}
	if ( mCursorLine != nil ) {
		[mCursorLine release];
	}
	
	[super dealloc];
}

//
// 描画処理
//
- (void)drawRect:(NSRect)dirtyRect {
    // Drawing code here.
	NSRect rect = [self bounds];

	// 背景描画
	[[NSColor blackColor] set];
	[NSBezierPath fillRect:rect]; 

	// 座標軸表示
	[self createAxisBezierPath];
	if ( mAxisLine != nil ) {
		[[NSColor greenColor] set];
		[mAxisLine stroke];
	}

	// 波形表示
	NSRect rectVisible = [(NSClipView *)[self superview] documentVisibleRect];
	if ( NSEqualSizes( mPrevSize, rectVisible.size ) != YES ) {
		mPrevSize = rectVisible.size;
		[self createWaveBezierPath];
	}
	if ( mWaveLine != nil ) {
		[[NSColor whiteColor] set];
		[mWaveLine stroke];
	}

	// カーソル表示
	[self createCursorBezierPath];
	if ( mCursorLine != nil ) {
		[[NSColor yellowColor] set];
		[mCursorLine stroke];
	}
}

// 公開メソッド
//
// 音声データの設定
//
- (void)setAudioData:(UCAudioData *)oData withScale:(float)fScale
{
	// パラメータの保持
	if ( mAudioData != nil ) {
		[mAudioData release];
	}
	mAudioData = [oData retain];
	mSamplingRate = [mAudioData samplingRate];
	mDeltaT = 1.0 / mSamplingRate;
	mFrameCount = [mAudioData frameCount];
	mCurFrame = 0;

	// 波形の生成
	[self setScale:fScale];
}

//
// スケールの設定
//
- (void)setScale:(float)fScale
{
	mScale = fScale;
	
	// ビューサイズ算出と設定
	NSRect rectVisible = [(NSClipView *)[self superview] documentVisibleRect];
	CGFloat fX = rectVisible.size.width / (mScale / mDeltaT) * (CGFloat)mFrameCount;
	NSRect rectFrame = [self frame];
	rectFrame.size.width = fX + rectVisible.size.width;
	NSLog( @"fX=%f\n", fX );
	
	[self setFrameSize:rectFrame.size];
	
	// 波形の生成
	[self createWaveBezierPath];
	[self setNeedsDisplay:YES];
}

//
// 音声データクラスのdelegateメソッド
//
- (void)incrementFrame:(UInt32)nFrames
{
	mCurFrame += nFrames;
	
	NSRect rectVisible = [(NSClipView *)[self superview] documentVisibleRect];
	NSRect rectBounds = [self bounds];
	CGFloat x = (rectBounds.size.width - rectVisible.size.width) * (CGFloat)mCurFrame / (CGFloat)mFrameCount;

	[self scrollPoint:NSMakePoint(x, 0)];
}

//
// スクロールバーイベントハンドラ
//
- (void)onScroller:(id)sender
{
	NSScrollerPart nPart = [(NSScroller *)sender hitPart];
	NSRect rectVisible = [(NSClipView *)[self superview] documentVisibleRect];
	NSRect rectBounds = [self bounds];
	
	//NSLog( @"hitPart=%d, x=%f", nPart, rectVisible.origin.x );
	
	switch ( nPart ) {
		case NSScrollerIncrementLine:
			rectVisible.origin.x += 10;
			if ( rectVisible.origin.x + rectVisible.size.width >= rectBounds.size.width ) {
				rectVisible.origin.x = rectBounds.size.width - rectVisible.size.width;
			}
			break;
		case NSScrollerDecrementLine:
			rectVisible.origin.x -= 10;
			if ( rectVisible.origin.x < 0 ) {
				rectVisible.origin.x = 0;
			}
			break;
		case NSScrollerIncrementPage:
			rectVisible.origin.x += 100;
			if ( rectVisible.origin.x + rectVisible.size.width >= rectBounds.size.width ) {
				rectVisible.origin.x = rectBounds.size.width - rectVisible.size.width;
			}
			break;
		case NSScrollerDecrementPage:
			rectVisible.origin.x -= 100;
			if ( rectVisible.origin.x < 0 ) {
				rectVisible.origin.x = 0;
			}
			break;
		case NSScrollerKnob:
		case NSScrollerKnobSlot:
		{
			CGFloat fPos = [(NSScroller *)sender floatValue];
			rectVisible.origin.x = (rectBounds.size.width - rectVisible.size.width) * fPos;
		}
			break;
	}
	//NSLog( @"onScroller: x=%f, y=%f\n", rectVisible.origin.x, rectVisible.origin.y );
	[self scrollPoint:rectVisible.origin];

	mCurFrame = (UInt32)(rectVisible.origin.x / (rectBounds.size.width - rectVisible.size.width) * (CGFloat)mFrameCount);
}

//
// カレントフレームの取得
//
- (UInt32)curFrame
{
	return mCurFrame;
}

//
// トータル時間の取得
//
- (NSString *)totalTime
{
	return [self timeString:mDeltaT * (Float64)mFrameCount];
}

//
// カレント時間の取得
//
- (NSString *)curTime
{
	return [self timeString:mDeltaT * (Float64)mCurFrame];
}

// 内部メソッド
//
// 波形データの生成
//
- (void)createWaveBezierPath
{
	NSRect rectVisible = [(NSClipView *)[self superview] documentVisibleRect];
	NSRect rectBounds = [self bounds];
	CGFloat fCenterY = rectBounds.size.height / 2.0;

	// 波形データ計算用作業領域の獲得
	T_DRAW_LINE_INFO *ptDrawInfo = (T_DRAW_LINE_INFO *)malloc( sizeof(T_DRAW_LINE_INFO) * (size_t)rectBounds.size.width );
	UInt32 i;
	for ( i = 0; i < (UInt32)rectBounds.size.width; i++ ) {
		INIT_DRAW_LINE_INFO( ptDrawInfo[i] );
	}

	// 波形データ計算
	int offset = rectVisible.size.width / 2;
	for ( i = 0; i < mFrameCount; i++ ) {
		float wave = [mAudioData frameAtIndex:i];
		int x = (int)((rectBounds.size.width - rectVisible.size.width) / (CGFloat)mFrameCount * (CGFloat)i);
		if ( ptDrawInfo[x+offset].bValid == NO ) {
			ptDrawInfo[x+offset].bValid = YES;
			ptDrawInfo[x+offset].ptLower = NSMakePoint((CGFloat)(x+offset), wave * fCenterY);
			ptDrawInfo[x+offset].ptUpper = NSMakePoint((CGFloat)(x+offset), wave * fCenterY + 1.0);
		} else {
			if ( wave * fCenterY > ptDrawInfo[x+offset].ptUpper.y ) {
				ptDrawInfo[x+offset].ptUpper.y = wave * fCenterY;
			} else if ( wave * fCenterY < ptDrawInfo[x+offset].ptLower.y ) {
				ptDrawInfo[x+offset].ptLower.y = wave * fCenterY;
			}
		}
	}

	// 波形描画パスの生成
	NSPoint			pt;
	NSBezierPath	*oBP = [NSBezierPath bezierPath];
	for ( i = 0; i < rectBounds.size.width; i++ ) {
		if ( ptDrawInfo[i].bValid == YES ) {
			pt = NSMakePoint(ptDrawInfo[i].ptLower.x, ptDrawInfo[i].ptLower.y + fCenterY);
			[oBP moveToPoint:pt];
			pt = NSMakePoint(ptDrawInfo[i].ptUpper.x, ptDrawInfo[i].ptUpper.y + fCenterY);
			[oBP lineToPoint:pt];
		}
	}
	free( ptDrawInfo );
	
	if ( mWaveLine != nil ) {
		[mWaveLine release];
	}
	mWaveLine = [oBP retain];
	
}

//
// 座標軸の生成
//
- (void)createAxisBezierPath
{
	int				i;
	NSPoint			pt;
	NSBezierPath	*oBP = [NSBezierPath bezierPath];
	NSRect			rectVisible = [(NSClipView *)[self superview] documentVisibleRect]; 

	//NSLog( @"createAxisBezierPath: x=%f, y=%f\n", rectVisible.origin.x, rectVisible.origin.y );

	// X軸
	for ( i = 1; i < DIV_CNT_X_AXIS; i++ ) {
		float   x = (rectVisible.size.width / (float)DIV_CNT_X_AXIS) * i + rectVisible.origin.x;
		pt = NSMakePoint( x, 0 );
		[oBP moveToPoint:pt];
		pt = NSMakePoint( x, rectVisible.size.height );
		[oBP lineToPoint:pt];
	}

	// Y軸
	for ( i = 1; i < DIV_CNT_Y_AXIS; i++ ) {
		float   y = (rectVisible.size.height / (float)DIV_CNT_Y_AXIS) * i;
		pt = NSMakePoint( rectVisible.origin.x, y );
		[oBP moveToPoint:pt];
		pt = NSMakePoint( rectVisible.origin.x + rectVisible.size.width, y );
		[oBP lineToPoint:pt];
	}

	if ( mAxisLine != nil ) {
		[mAxisLine release];
	}
	mAxisLine = [oBP retain];
}

//
// カーソルの生成
//
- (void)createCursorBezierPath
{
	NSPoint			pt;
	NSBezierPath	*oBP = [NSBezierPath bezierPath];
	NSRect			rectVisible = [(NSClipView *)[self superview] documentVisibleRect]; 
	CGFloat x = rectVisible.origin.x + rectVisible.size.width / 2.0;
	[oBP setLineWidth:2.0];
	pt = NSMakePoint(x, 0);
	[oBP moveToPoint:pt];
	pt = NSMakePoint( x, rectVisible.size.height );
	[oBP lineToPoint:pt];

	if ( mCursorLine != nil ) {
		[mCursorLine release];
	}
	mCursorLine = [oBP retain];
}

//
// 時間文字列の生成
//
- (NSString *)timeString:(Float64)dTime
{
	UInt32	nHour, nMin, nSec, nMSec, nTime;

	nTime = (UInt32)dTime;
	nMSec = (UInt32)((dTime - (Float64)nTime) * 1000.0);
	nHour = nTime / 3600;
	nTime %= 3600;
	nMin = nTime / 60;
	nTime %= 60;
	nSec = nTime;

	NSString * strTime = [NSString stringWithFormat:@"%d:%02d:%02d.%03d", nHour, nMin, nSec, nMSec];
	return strTime;
}

@end

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

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

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

@interface AudioWaveViewAppDelegate : NSObject <NSApplicationDelegate, UCAudioDataDelegate> {
    NSWindow *window;
	UCWaveView *outView;
	NSTextField *outTxtFileName;
	NSTextField *outTxtTime;
	NSButton *outBtnChoose;
	NSButton *outBtnPlay;
	NSSlider *outSldVol;
	NSSlider *outSldScale;

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

@property (assign) IBOutlet NSWindow *window;
@property (assign) IBOutlet UCWaveView *outView;
@property (assign) IBOutlet NSTextField *outTxtFileName;
@property (assign) IBOutlet NSTextField *outTxtTime;
@property (assign) IBOutlet NSButton *outBtnChoose;
@property (assign) IBOutlet NSButton *outBtnPlay;
@property (assign) IBOutlet NSSlider *outSldVol;
@property (assign) IBOutlet NSSlider *outSldScale;

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

//
// イベントハンドラ
//
- (IBAction)onBtnChoose:(id)sender;
- (IBAction)onBtnPlay:(id)sender;
- (IBAction)onSldVol:(id)sender;
- (IBAction)onSldScale:(id)sender;
- (IBAction)onScroller:(id)sender;

//
// UCAudioData delegateメソッド
//
- (void)increment:(id)oFrames;
- (void)musicDidEnd:(id)obj;

@end

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

@implementation AudioWaveViewAppDelegate

@synthesize window;
@synthesize outView;
@synthesize outTxtFileName;
@synthesize outTxtTime;
@synthesize outBtnChoose;
@synthesize outBtnPlay;
@synthesize outSldVol;
@synthesize outSldScale;

//
// アプリケーション起動時の処理
//
- (void)applicationDidFinishLaunching:(NSNotification *)aNotification {
	// Insert code here to initialize your application 
	mAudioData = nil;
	mAudioFilePath = nil;
	mPlaying = NO;
	mTotalTime = nil;
}

//
// 最後のウィンドウクローズ時の指示
//
- (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];
	}
	if ( mTotalTime != nil ) {
		[mTotalTime 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]];

		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];
		
		[outView setAudioData:mAudioData withScale:[outSldScale floatValue]];
		[mAudioData release];
		
		if ( mTotalTime != nil ) {
			[mTotalTime release];
		}
		mTotalTime = [[outView totalTime] retain];
		NSString * strCurTime = [outView curTime];
		[outTxtTime setStringValue:[NSString stringWithFormat:@"%@ / %@", strCurTime, mTotalTime]];

		[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)onSldVol:(id)sender
{
	[mAudioData setVolume:[outSldVol floatValue]];
}

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

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

	if ( mPlaying ) {
		[mAudioData playAtIndex:[outView curFrame]];
	}

	NSString * strCurTime = [outView curTime];
	[outTxtTime setStringValue:[NSString stringWithFormat:@"%@ / %@", strCurTime, mTotalTime]];
}

//
// UCAudioData delegateメソッド
//
- (void)increment:(id)oFrames
{
	UInt32 nFrames = [(NSNumber *)oFrames unsignedIntValue];
	[outView incrementFrame:nFrames];
	NSString * strCurTime = [outView curTime];
	[outTxtTime setStringValue:[NSString stringWithFormat:@"%@ / %@", strCurTime, mTotalTime]];
}

- (void)musicDidEnd:(id)obj
{
	[mAudioData stop];
	[outBtnPlay setTitle:@"Play"];
	[outBtnChoose setEnabled:YES];
	mPlaying = NO;
	NSString * strCurTime = [outView curTime];
	[outTxtTime setStringValue:[NSString stringWithFormat:@"%@ / %@", strCurTime, mTotalTime]];
}

@end

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

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

一覧に戻る