CoreAudioを使ってmacOSで音を再生する

しむどん 2026-01-09

CoreAudioを使って音声データを再生してみる事にした。

Swiftの使い方の確認

まずはSwiftについての基本的な事を確認する。

Hello world

print("Hello world!")

ビルドする。

swiftc hello.swift

Sleep 10

import Foundation

Thread.sleep(forTimeInterval: 10)

ビルドする。

swiftc sleep10.swift

シグナルハンドラ

import Foundation
import Darwin

func handleSignal(signal: Int32) {
    print("Received signal: \(signal)")
}

print("Start...")

signal(SIGINT, handleSignal)

Thread.sleep(forTimeInterval: 10)

ビルドする。

swiftc signal.swift

CoreAudioを使ってラの音を再生する

それではCoreAudioを使って、音声を再生してみよう。

事前準備と定数やコールバック用コンテキストの定義

モジュールのインポートや定数や後に必要となるコールバック用コンテキストを定義する。

import Foundation
import AudioToolbox

// オーディオ設定
let sampleRate: Double = 44100.0  // サンプリングレート(1秒間のサンプル数)
let channels: UInt32 = 2          // チャンネル数(2 = ステレオ)
let frequency: Double = 440.0     // 再生する周波数(440Hz = A4音/ラ)

// コールバック用コンテキスト
//   正弦波の位相を保持するためのクラス
//   コールバック関数はクロージャなので、外部の変数を直接変更できない
//   そのため、参照型のクラスを使って位相を保持する
class AudioContext {
    var phase: Double = 0.0  // 現在の位相(0 〜 2π)
}
let context = AudioContext()

HAL Output AudioUnitを作成

macOSでオーディオ出力を行うためのAudioComponentを探す。

// macOSでオーディオ出力を行うためのAudioComponentを探す
// HAL (Hardware Abstraction Layer) Outputは、macOSのデフォルト出力デバイスに音声を送る
var description = AudioComponentDescription(
    componentType: kAudioUnitType_Output,              // 出力用AudioUnit
    componentSubType: kAudioUnitSubType_HALOutput,     // macOS用HAL Output
    componentManufacturer: kAudioUnitManufacturer_Apple,
    componentFlags: 0,
    componentFlagsMask: 0
)

// 指定した条件に合致するAudioComponentを検索
guard let component = AudioComponentFindNext(nil, &description) else {
    fatalError("AudioComponentが見つかりません")
}

AudioComponentからAudioUnitのインスタンスを作成する。

var audioUnit: AudioUnit?
guard AudioComponentInstanceNew(component, &audioUnit) == noErr,
      let audioUnit = audioUnit else {
    fatalError("AudioUnitを作成できません")
}

ストリームフォーマットの設定

AudioUnitに渡すオーディオデータのフォーマットを定義する。

var format = AudioStreamBasicDescription(
    mSampleRate: sampleRate,                                               // サンプリングレート
    mFormatID: kAudioFormatLinearPCM,                                     // PCMフォーマット
    mFormatFlags: kAudioFormatFlagIsPacked | kAudioFormatFlagIsSignedInteger,  // パック形式、符号付き整数
    mBytesPerPacket: 4,      // 1パケットのバイト数(2ch × 16bit/8 = 4バイト)
    mFramesPerPacket: 1,     // 1パケットのフレーム数(PCMは常に1)
    mBytesPerFrame: 4,       // 1フレームのバイト数(2ch × 16bit/8 = 4バイト)
    mChannelsPerFrame: channels,  // チャンネル数
    mBitsPerChannel: 16,     // 1チャンネルあたりのビット数
    mReserved: 0
)

フォーマットをAudioUnitに設定する。

guard AudioUnitSetProperty(
    audioUnit,
    kAudioUnitProperty_StreamFormat,  // ストリームフォーマットプロパティ
    kAudioUnitScope_Input,            // Inputスコープ(アプリからAudioUnitへ)
    0,                                // バス番号
    &format,
    UInt32(MemoryLayout.size(ofValue: format))
) == noErr else {
    fatalError("ストリームフォーマットの設定に失敗")
}

レンダリングコールバックの設定

AudioUnitが音声データを要求するたびに呼ばれるコールバック関数と、それを保持す構造体を定義する。

var callback = AURenderCallbackStruct(
    // inputProc: 音声データを生成するクロージャ
    // inRefCon: コンテキストへのポインタ(AudioContextを取得するために使用)
    // inNumberFrames: 生成する必要があるフレーム数
    // ioData: 音声データを書き込むバッファ
    inputProc: { (inRefCon, _, _, _, inNumberFrames, ioData) -> OSStatus in
        guard let ioData = ioData else { return noErr }

        // コンテキストを取得(Unmanagedで参照カウントを管理せずに取得)
        let ctx = Unmanaged<AudioContext>.fromOpaque(inRefCon).takeUnretainedValue()

        // 1サンプルあたりの位相の増分を計算
        // 位相増分 = 2π × 周波数 / サンプリングレート
        let phaseIncrement = 2.0 * Double.pi * frequency / sampleRate

        // AudioBufferListへのポインタを取得
        let buffers = UnsafeMutableAudioBufferListPointer(ioData)

        // 各バッファに対して処理(通常は1つ)
        for buffer in buffers {
            guard let data = buffer.mData else { continue }
            // Int16型(16bit)の配列としてアクセス
            let samples = data.assumingMemoryBound(to: Int16.self)

            // 要求されたフレーム数分のサンプルを生成
            for frame in 0..<Int(inNumberFrames) {
                // 正弦波を生成(sin関数で -1.0 〜 1.0 の値を生成)
                // 0.25を掛けて音量を25%に抑える
                // Int16.maxを掛けて16bit整数の範囲に変換
                let value = Int16(sin(ctx.phase) * 0.25 * Double(Int16.max))

                // ステレオなので左右チャンネルに同じ値を書き込む
                samples[frame * 2] = value      // Left channel
                samples[frame * 2 + 1] = value  // Right channel

                // 位相を進める
                ctx.phase += phaseIncrement
                // 位相が2πを超えたら0に戻す(周期性を保つ)
                if ctx.phase > 2.0 * Double.pi {
                    ctx.phase -= 2.0 * Double.pi
                }
            }
        }

        return noErr  // 成功を返す
    },
    // inputProcRefCon: コールバック関数に渡すコンテキストへのポインタ
    inputProcRefCon: Unmanaged.passUnretained(context).toOpaque()
)

コールバック構造体をAudioUnitに登録する。

guard AudioUnitSetProperty(
    audioUnit,
    kAudioUnitProperty_SetRenderCallback,  // レンダリングコールバックプロパティ
    kAudioUnitScope_Global,                // Globalスコープ
    0,                                     // バス番号
    &callback,
    UInt32(MemoryLayout.size(ofValue: callback))
) == noErr else {
    fatalError("コールバックの設定に失敗")
}

音声出力を開始

AudioUnitを初期化し開始する。コールバックが定期的に呼ばれるようになり、音声が出力される。

// 初期化
guard AudioUnitInitialize(audioUnit) == noErr else {
    fatalError("AudioUnitの初期化に失敗")
}

// 開始
guard AudioOutputUnitStart(audioUnit) == noErr else {
    fatalError("AudioUnitの開始に失敗")
}

コード全体

コード全体を掲載する。

// ============================================================
// AudioUnitを使って正弦波を再生するプログラム
// ============================================================
// macOSのCore Audio APIを使用して、440Hz(A4音/ラ)の正弦波を生成し、
// デフォルトのオーディオ出力デバイスに送信する
//
// コンパイル: swiftc play.swift
// 実行: ./play
// 停止: Ctrl+C
// ============================================================

import Foundation
import AudioToolbox

// ============================================================
// オーディオ設定
// ============================================================
let sampleRate: Double = 44100.0  // サンプリングレート(1秒間のサンプル数)
let channels: UInt32 = 2          // チャンネル数(2 = ステレオ)
let frequency: Double = 440.0     // 再生する周波数(440Hz = A4音/ラ)

// ============================================================
// コールバック用コンテキスト
// ============================================================
// 正弦波の位相を保持するためのクラス
// コールバック関数はクロージャなので、外部の変数を直接変更できない
// そのため、参照型のクラスを使って位相を保持する
class AudioContext {
    var phase: Double = 0.0  // 現在の位相(0 〜 2π)
}
let context = AudioContext()

// ============================================================
// HAL Output AudioUnitを作成
// ============================================================
// macOSでオーディオ出力を行うためのAudioComponentを探す
// HAL (Hardware Abstraction Layer) Outputは、macOSのデフォルト出力デバイスに音声を送る
var description = AudioComponentDescription(
    componentType: kAudioUnitType_Output,              // 出力用AudioUnit
    componentSubType: kAudioUnitSubType_HALOutput,     // macOS用HAL Output
    componentManufacturer: kAudioUnitManufacturer_Apple,
    componentFlags: 0,
    componentFlagsMask: 0
)

// 指定した条件に合致するAudioComponentを検索
guard let component = AudioComponentFindNext(nil, &description) else {
    fatalError("AudioComponentが見つかりません")
}

// AudioComponentからAudioUnitのインスタンスを作成
var audioUnit: AudioUnit?
guard AudioComponentInstanceNew(component, &audioUnit) == noErr,
      let audioUnit = audioUnit else {
    fatalError("AudioUnitを作成できません")
}

// ============================================================
// ストリームフォーマットを設定(16bit ステレオ PCM)
// ============================================================
// AudioUnitに渡すオーディオデータのフォーマットを定義
// Linear PCM = 非圧縮の生のオーディオデータ
var format = AudioStreamBasicDescription(
    mSampleRate: sampleRate,                                               // サンプリングレート
    mFormatID: kAudioFormatLinearPCM,                                     // PCMフォーマット
    mFormatFlags: kAudioFormatFlagIsPacked | kAudioFormatFlagIsSignedInteger,  // パック形式、符号付き整数
    mBytesPerPacket: 4,      // 1パケットのバイト数(2ch × 16bit/8 = 4バイト)
    mFramesPerPacket: 1,     // 1パケットのフレーム数(PCMは常に1)
    mBytesPerFrame: 4,       // 1フレームのバイト数(2ch × 16bit/8 = 4バイト)
    mChannelsPerFrame: channels,  // チャンネル数
    mBitsPerChannel: 16,     // 1チャンネルあたりのビット数
    mReserved: 0
)

// フォーマットをAudioUnitに設定
// HAL OutputではInputスコープに設定する(アプリ→AudioUnitへのデータ流れ)
guard AudioUnitSetProperty(
    audioUnit,
    kAudioUnitProperty_StreamFormat,  // ストリームフォーマットプロパティ
    kAudioUnitScope_Input,            // Inputスコープ(アプリからAudioUnitへ)
    0,                                // バス番号
    &format,
    UInt32(MemoryLayout.size(ofValue: format))
) == noErr else {
    fatalError("ストリームフォーマットの設定に失敗")
}

// ============================================================
// レンダリングコールバックを設定
// ============================================================
// AudioUnitが音声データを要求するたびに呼ばれる関数を定義
// このコールバックで実際の音声データ(正弦波)を生成する
var callback = AURenderCallbackStruct(
    // inputProc: 音声データを生成するクロージャ
    // inRefCon: コンテキストへのポインタ(AudioContextを取得するために使用)
    // inNumberFrames: 生成する必要があるフレーム数
    // ioData: 音声データを書き込むバッファ
    inputProc: { (inRefCon, _, _, _, inNumberFrames, ioData) -> OSStatus in
        guard let ioData = ioData else { return noErr }

        // コンテキストを取得(Unmanagedで参照カウントを管理せずに取得)
        let ctx = Unmanaged<AudioContext>.fromOpaque(inRefCon).takeUnretainedValue()

        // 1サンプルあたりの位相の増分を計算
        // 位相増分 = 2π × 周波数 / サンプリングレート
        let phaseIncrement = 2.0 * Double.pi * frequency / sampleRate

        // AudioBufferListへのポインタを取得
        let buffers = UnsafeMutableAudioBufferListPointer(ioData)

        // 各バッファに対して処理(通常は1つ)
        for buffer in buffers {
            guard let data = buffer.mData else { continue }
            // Int16型(16bit)の配列としてアクセス
            let samples = data.assumingMemoryBound(to: Int16.self)

            // 要求されたフレーム数分のサンプルを生成
            for frame in 0..<Int(inNumberFrames) {
                // 正弦波を生成(sin関数で -1.0 〜 1.0 の値を生成)
                // 0.25を掛けて音量を25%に抑える
                // Int16.maxを掛けて16bit整数の範囲に変換
                let value = Int16(sin(ctx.phase) * 0.25 * Double(Int16.max))

                // ステレオなので左右チャンネルに同じ値を書き込む
                samples[frame * 2] = value      // Left channel
                samples[frame * 2 + 1] = value  // Right channel

                // 位相を進める
                ctx.phase += phaseIncrement
                // 位相が2πを超えたら0に戻す(周期性を保つ)
                if ctx.phase > 2.0 * Double.pi {
                    ctx.phase -= 2.0 * Double.pi
                }
            }
        }

        return noErr  // 成功を返す
    },
    // inputProcRefCon: コールバック関数に渡すコンテキストへのポインタ
    inputProcRefCon: Unmanaged.passUnretained(context).toOpaque()
)

// コールバック構造体をAudioUnitに登録
// Globalスコープに設定(AudioUnit全体に適用)
guard AudioUnitSetProperty(
    audioUnit,
    kAudioUnitProperty_SetRenderCallback,  // レンダリングコールバックプロパティ
    kAudioUnitScope_Global,                // Globalスコープ
    0,                                     // バス番号
    &callback,
    UInt32(MemoryLayout.size(ofValue: callback))
) == noErr else {
    fatalError("コールバックの設定に失敗")
}

// ============================================================
// AudioUnitを初期化して開始
// ============================================================
// AudioUnitを初期化(内部リソースを確保)
guard AudioUnitInitialize(audioUnit) == noErr else {
    fatalError("AudioUnitの初期化に失敗")
}

// AudioUnitを開始(音声出力を開始)
// この時点からコールバックが定期的に呼ばれるようになる
guard AudioOutputUnitStart(audioUnit) == noErr else {
    fatalError("AudioUnitの開始に失敗")
}

// ============================================================
// プログラムを実行し続ける
// ============================================================
print("440Hzの正弦波を再生中... (Ctrl+Cで停止)")

// RunLoopを実行してプログラムを終了させない
// コールバックはバックグラウンドスレッドで呼ばれるため、
// メインスレッドが終了しないようにする必要がある
RunLoop.current.run()

ビルドと実行

swiftcでビルドする。

swiftc play.swift

実行するとラの音(440Hz、A4音)が再生される。

./play