カテゴリー
iOS Objective-C

CoreAudioを学ぶ(2)

パート2はGenerating Raw Audio Samplesを学んで行きます。方形波(square wave)、ノコギリ波(saw wave)、サイン波(sine wave)をプログラム的に生成し、ファイルを作成します。

#import <Foundation/Foundation.h>
#import <AudioToolbox/AudioToolbox.h>

// CD-qualiy sample rate
#define SAMPLE_RATE 44100
#define BITS_PER_SAMPLE 16
#define BYTE_SIZE_IN_BITS 8
#define BYTES_PER_SAMPLE BITS_PER_SAMPLE / BYTE_SIZE_IN_BITS
// We use LPCM so the encoding does not use packets. Hence,
// we are going to have 1 frame per packet.
#define FRAMES_PER_PACKET 1

// number of seconds we want to capture
#define DURATION 5.0
#define FILENAME_FORMAT @"%0.3f-%@.aif"

#define NUMBER_OF_CHANNELS 1

void buildFileURL(double hz, NSString *shape, NSURL** fileURL) {
    NSString* fileName = [NSString stringWithFormat:FILENAME_FORMAT, hz, shape];
    NSString* filePath = [[[NSFileManager defaultManager] currentDirectoryPath]
                          stringByAppendingPathComponent:fileName];
    *fileURL = [NSURL fileURLWithPath:filePath];
}

void buildAudioStreamBasicDescription(AudioStreamBasicDescription* audioStreamBasicDescription) {
    memset(audioStreamBasicDescription, 0, sizeof(AudioStreamBasicDescription)); // [5]
    
    audioStreamBasicDescription->mSampleRate = SAMPLE_RATE; // [6]
    audioStreamBasicDescription->mFormatID = kAudioFormatLinearPCM;
    audioStreamBasicDescription->mFormatFlags = kAudioFormatFlagIsBigEndian | kAudioFormatFlagIsSignedInteger | kAudioFormatFlagIsPacked;
    audioStreamBasicDescription->mBitsPerChannel = BITS_PER_SAMPLE;
    audioStreamBasicDescription->mChannelsPerFrame = NUMBER_OF_CHANNELS;
    audioStreamBasicDescription->mFramesPerPacket = FRAMES_PER_PACKET;
    audioStreamBasicDescription->mBytesPerFrame = BYTES_PER_SAMPLE * NUMBER_OF_CHANNELS;
    audioStreamBasicDescription->mBytesPerPacket = audioStreamBasicDescription->mFramesPerPacket * audioStreamBasicDescription->mBytesPerFrame;
} // buildAudioStreamBasicDescription

SInt16 generateSineShapeSample(int i, double waveLengthInSamples) {
    assert(i >= 1 && i <= waveLengthInSamples);
    
    return (SInt16)(SHRT_MAX * sin(2 * M_PI * (i - 1) / waveLengthInSamples));
}

SInt16 generateSquareShapeSample(int i, double waveLengthInSamples) {
    assert(i >= 1 && i <= waveLengthInSamples);
    
    if (i <= waveLengthInSamples / 2) {
        return SHRT_MAX;
    } else {
        return SHRT_MIN;
    }
}

SInt16 generateSawShapeSample(int i, double waveLengthInSamples) {
    assert(i >= 1 && i <= waveLengthInSamples);
    
    return (SInt16)(2 * SHRT_MAX / waveLengthInSamples * (i - 1) - SHRT_MAX);
}

NSString* correctShape(NSString *shape) { // [2]'
    if ([shape isEqualToString:@"square"] ||
        [shape isEqualToString:@"saw"] ||
        [shape isEqualToString:@"sine"])
    {
        return shape;
    } else {
        return @"square";
    }
}

int main(int argc, const char * argv[]) {
    if (argc < 2) {
        printf("Usage: WriteRawAudioSamples n <shape>\nwhere n is tone in Hz, shape is one of 'square' (default), 'saw', 'sine'\n");
        return -1;
    }
    
    @autoreleasepool {
        double hz = atof(argv[1]); // [1]
        assert(hz > 0);
        
        NSString *shape;
        if (argc == 2) {
            shape = @"square"; // [2]
        } else {
            shape = [NSString stringWithFormat:@"%s", argv[2]];
            shape = correctShape(shape);
        }
        
        NSLog(@"generating %f hz tone with shape %@...", hz, shape);
        
        NSURL *fileURL = NULL;
        buildFileURL(hz, shape, &fileURL);
        
        // Prepare the format
        AudioStreamBasicDescription audioStreamBasicDescription; // [3]
        buildAudioStreamBasicDescription(&audioStreamBasicDescription); // [4]
        
        // Set up the file
        AudioFileID audioFile; // [7]
        OSStatus error = noErr;
        
        error = AudioFileCreateWithURL((__bridge CFURLRef)fileURL,
                                       kAudioFileAIFFType,
                                       &audioStreamBasicDescription,
                                       kAudioFileFlags_EraseFile,
                                       &audioFile);
        assert(error == noErr);
        
        // Start writing samples;
        long maxSampleCount = SAMPLE_RATE * DURATION; // [8]
        
        long sampleCount = 1;
        UInt32 bytesToWrite = BYTES_PER_SAMPLE; // [9]
        double waveLengthInSamples = SAMPLE_RATE / hz; // [10]
        NSLog(@"wave (or cycle) length in samples: %.4f\n", waveLengthInSamples);
        
        while (sampleCount <= maxSampleCount) { // [11]
            for(int i = 1; i <= waveLengthInSamples; i++) {
                SInt16 sample = 0;
                
                if ([shape isEqualToString:@"square"]) {
                    sample = generateSquareShapeSample(i, waveLengthInSamples);
                }
                else if ([shape isEqualToString:@"saw"]) {
                    sample = generateSawShapeSample(i, waveLengthInSamples);
                } else if ([shape isEqualToString:@"sine"]) {
                    sample = generateSineShapeSample(i, waveLengthInSamples);
                }
                sample = CFSwapInt16HostToBig(sample);
                
                SInt64 offset = sampleCount * bytesToWrite;
                error = AudioFileWriteBytes(audioFile, false, offset, &bytesToWrite, &sample);
                assert(error == noErr);
                
                sampleCount++;
            }
        }
        error = AudioFileClose(audioFile);
        assert(error == noErr);
        NSLog(@"wrote %ld samples", sampleCount);
    }
    return 0;
}

[1] atof()というC言語の文字列をDouble型へ変換する関数を使って、周波数を代入しています。

[2] 実行ファイル実行時の引数が2の場合はデフォルトの”square”(方形波)が選ばれ、それ以外の場合は引数に応じて”saw”(ノコギリ波)、”sine”(サイン波)が選択されます。”square”, “saw”, “sine”以外の文字列が引数として渡された場合は”square”が選択されます[2]’。

[3] オーディオデータをファイルに書き込むにはAudioStreamBasicDescriptionのインスタンスを用いて、CoreAudio APIにどのようなフォーマットのデータを書き込みたいかを伝える必要があります。

[4] [5] まず、memset( )を使い構造体AudioStreamBasicDescriptionのサイズ分のメモリを確保し、そのメモリの全てのバイトに0(ゼロ)を書き込みます。これはセットしないパラメータに意図しないランダムな値が代入されていないようにするグッドプラクティスだそうです。

[6] 必要なパラメータを設定していきます。

[7] AudioFileCreateWithURL( )を使い、オーディオファイルを作成します。

[8] SAMPLE_RATE(一秒間に何箇所数値化するか) x DURATION(秒数)で、総サンプル数を計算します。この例では 44,100 x 5 = 220,500サンプルです。

[9] これは単純に、ビット深度(bits per sample, またはbit depth)が16の場合、byteで表すとなにか?を設定しています。1 byte == 8 bitですから、bytesToWriteは2です。

[10] サンプルレートを音の高さ(周波数、hz)で割ると、1サイクルにいくつサンプルがあるのかを計算できます。A4(440hz)の場合、44,100 / 440 = 100.2272…です。

[11] 波形タイプに応じて、サンプル毎の数値を計算し、メモリのoffsetの位置を sampleCount (何サンプル目) x bytesToWrite(サンプル毎のデータの大きさ)としてそこに書き込んでいきます。