CoreAudioを使ってmacOSで音を再生する
しむどん
:
2026-01-09
CoreAudioを使って音声データを再生してみる事にした。
Swiftの使い方の確認
まずはSwiftについての基本的な事を確認する。
Hello world
print("Hello world!")ビルドする。
swiftc hello.swiftSleep 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.swiftCoreAudioを使ってラの音を再生する
それでは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