-
Notifications
You must be signed in to change notification settings - Fork 0
Open
Description
概要
現在のAVAudioEngineベースの実装から、長時間録音に最適化されたAVAudioRecorderベースの新しいオーディオレコーダーを実装する。
現在の実装の問題点
AVAudioEngineベースの課題
- メモリ使用量: 長時間録音でメモリが蓄積される
- CPU負荷: リアルタイム処理による継続的なCPU使用
- バッテリー消費: 音声認識処理による電力消費
- 安定性: 長時間録音での予期しない停止
- 録音時間初期化: 前回の録音時間が残る問題
AVAudioRecorderベースの利点
長時間録音への最適化
- 低メモリ使用量: ファイルに直接書き込み、メモリ蓄積なし
- 低CPU負荷: シンプルな録音処理のみ
- 省電力: 不要な音声認識処理を排除
- 高い安定性: Appleの最適化されたレコーダー実装
- 自動フォーマット変換: 指定フォーマットへの自動変換
新しい実装設計
ファイル構成
VoiLog/Recording/
├── LongRecordingAudioClient.swift # 新しいクライアントインターフェース
├── LongRecordingAudioRecorder.swift # AVAudioRecorderベースの実装
├── RecordingConfiguration.swift # 録音設定管理
└── RecordingState.swift # 状態管理
インターフェース設計
struct LongRecordingAudioClient {
var currentTime: @Sendable () async -> TimeInterval
var requestRecordPermission: @Sendable () async -> Bool
var startRecording: @Sendable (URL, RecordingConfiguration) async throws -> Bool
var stopRecording: @Sendable () async -> Void
var pauseRecording: @Sendable () async -> Void
var resumeRecording: @Sendable () async -> Void
var audioLevel: @Sendable () async -> Float
var recordingState: @Sendable () async -> RecordingState
}状態管理
enum RecordingState: Equatable {
case idle
case preparing
case recording(startTime: Date)
case paused(startTime: Date, pausedTime: Date, duration: TimeInterval)
case completed(duration: TimeInterval)
case error(RecordingError)
}
enum RecordingError: Error, Equatable {
case permissionDenied
case fileCreationFailed
case audioSessionFailed
case recordingFailed(String)
case diskSpaceInsufficient
}録音設定
struct RecordingConfiguration: Equatable {
let fileFormat: AudioFileFormat
let quality: AudioQuality
let sampleRate: Double
let numberOfChannels: Int
static let `default` = RecordingConfiguration(
fileFormat: .m4a,
quality: .high,
sampleRate: 44100,
numberOfChannels: 1
)
enum AudioFileFormat: String, CaseIterable {
case m4a = "m4a"
case wav = "wav"
case aiff = "aiff"
case caf = "caf"
var settings: [String: Any] {
switch self {
case .m4a:
return [
AVFormatIDKey: kAudioFormatMPEG4AAC,
AVEncoderAudioQualityKey: AVAudioQuality.high.rawValue
]
case .wav:
return [
AVFormatIDKey: kAudioFormatLinearPCM,
AVLinearPCMBitDepthKey: 16,
AVLinearPCMIsFloatKey: false
]
case .aiff:
return [
AVFormatIDKey: kAudioFormatLinearPCM,
AVLinearPCMBitDepthKey: 16,
AVLinearPCMIsFloatKey: false
]
case .caf:
return [
AVFormatIDKey: kAudioFormatAppleLossless
]
}
}
}
enum AudioQuality: String, CaseIterable {
case low, medium, high, max
var avQuality: AVAudioQuality {
switch self {
case .low: return .low
case .medium: return .medium
case .high: return .high
case .max: return .max
}
}
}
}実装詳細
AVAudioRecorderベースの実装
private actor LongRecordingAudioRecorder {
private var audioRecorder: AVAudioRecorder?
private var recordingTimer: Timer?
private var currentTime: TimeInterval = 0
private var startTime: Date?
private var pausedDuration: TimeInterval = 0
private var state: RecordingState = .idle
// 長時間録音最適化設定
func setupAudioSession() async throws {
let session = AVAudioSession.sharedInstance()
try session.setCategory(
.playAndRecord,
mode: .default,
options: [
.defaultToSpeaker,
.allowBluetooth,
.allowBluetoothA2DP,
.mixWithOthers // 他のアプリとの共存
]
)
// 長時間録音に最適化された設定
try session.setPreferredIOBufferDuration(0.005) // 5ms バッファ
try session.setActive(true)
}
func startRecording(url: URL, configuration: RecordingConfiguration) async throws -> Bool {
// 前回の状態をリセット
currentTime = 0
pausedDuration = 0
startTime = Date()
state = .preparing
try await setupAudioSession()
// AVAudioRecorderの設定
var settings = configuration.fileFormat.settings
settings[AVSampleRateKey] = configuration.sampleRate
settings[AVNumberOfChannelsKey] = configuration.numberOfChannels
settings[AVEncoderAudioQualityKey] = configuration.quality.avQuality.rawValue
audioRecorder = try AVAudioRecorder(url: url, settings: settings)
audioRecorder?.delegate = self
audioRecorder?.isMeteringEnabled = true
// 録音開始
guard audioRecorder?.record() == true else {
state = .error(.recordingFailed("Failed to start recording"))
return false
}
state = .recording(startTime: Date())
startTimer()
return true
}
func stopRecording() async {
audioRecorder?.stop()
stopTimer()
let finalDuration = currentTime
state = .completed(duration: finalDuration)
// リソースクリーンアップ
audioRecorder = nil
try? AVAudioSession.sharedInstance().setActive(false)
}
func pauseRecording() async {
guard case .recording(let startTime) = state else { return }
audioRecorder?.pause()
stopTimer()
let pauseTime = Date()
state = .paused(startTime: startTime, pausedTime: pauseTime, duration: currentTime)
}
func resumeRecording() async {
guard case .paused(let startTime, _, let duration) = state else { return }
pausedDuration += duration
audioRecorder?.record()
startTimer()
state = .recording(startTime: startTime)
}
private func startTimer() {
recordingTimer = Timer.scheduledTimer(withTimeInterval: 0.1, repeats: true) { [weak self] _ in
Task { await self?.updateCurrentTime() }
}
}
private func stopTimer() {
recordingTimer?.invalidate()
recordingTimer = nil
}
private func updateCurrentTime() {
guard let recorder = audioRecorder, recorder.isRecording else { return }
currentTime = recorder.currentTime + pausedDuration
}
func getCurrentTime() -> TimeInterval {
return currentTime
}
func getAudioLevel() -> Float {
guard let recorder = audioRecorder, recorder.isRecording else { return 0.0 }
recorder.updateMeters()
return recorder.averagePower(forChannel: 0)
}
func getCurrentState() -> RecordingState {
return state
}
}
// AVAudioRecorderDelegate実装
extension LongRecordingAudioRecorder: AVAudioRecorderDelegate {
func audioRecorderDidFinishRecording(_ recorder: AVAudioRecorder, successfully flag: Bool) {
if flag {
state = .completed(duration: currentTime)
} else {
state = .error(.recordingFailed("Recording failed unexpectedly"))
}
}
func audioRecorderEncodeErrorDidOccur(_ recorder: AVAudioRecorder, error: Error?) {
let errorMessage = error?.localizedDescription ?? "Unknown encoding error"
state = .error(.recordingFailed(errorMessage))
}
}長時間録音最適化
メモリ管理
- 直接ファイル書き込み: メモリ蓄積を防止
- 適切なバッファサイズ: 5msの小さなバッファで安定性確保
- リソースクリーンアップ: 録音終了時の確実なリソース解放
電力効率
- 音声認識処理の除去: 不要な処理を排除
- タイマー最適化: 0.1秒間隔での効率的な時間更新
- セッション管理: 適切なAudioSession設定
安定性向上
- デリゲートベースのエラーハンドリング
- 状態管理の明確化
- 中断・復帰処理の改善
タスク
Phase 1: 基本実装 (Week 1)
- LongRecordingAudioClient構造体の作成
- 基本的な録音/停止機能の実装
- RecordingConfiguration設計
- RecordingState列挙型の実装
Phase 2: 高度な機能 (Week 2)
- 一時停止/再開機能の実装
- リアルタイム音量測定
- エラーハンドリングの充実
- バックグラウンド録音対応
Phase 3: 最適化 (Week 3)
- 長時間録音のメモリ最適化
- バッテリー使用量の最適化
- 中断処理の改善
- パフォーマンステスト
Phase 4: 統合 (Week 4)
- RecordingFeatureとの統合
- 既存実装からの移行
- テスト作成
- ドキュメント作成
受け入れ基準
機能要件
- 1時間以上の連続録音が可能
- 録音時間が正しく初期化される(0から開始)
- メモリ使用量が一定範囲内に収まる
- 音声品質が劣化しない
- 一時停止/再開が正確に動作する
非機能要件
- CPU使用率が現在実装の50%以下
- メモリ使用量が現在実装の30%以下
- バッテリー消費が現在実装の40%以下
- 連続3時間録音でクラッシュしない
パフォーマンステスト
- 30分連続録音テスト
- 1時間連続録音テスト
- 3時間連続録音テスト
- バックグラウンド録音テスト
- 通話割り込みテスト
実装上の注意点
- 状態初期化: 録音開始時に前回の時間を確実にリセット
- メモリ管理: AVAudioRecorderインスタンスの適切な管理
- エラー処理: デリゲートメソッドでの包括的エラーハンドリング
- TCA統合: 既存のRecordingFeatureとの互換性維持
マイルストーン
Milestone 1: 基本録音機能 (2週間)
基本的な録音/停止/一時停止機能の完成
Milestone 2: 長時間録音対応 (1週間)
1時間以上の安定した録音機能の実現
Milestone 3: 統合完了 (1週間)
既存システムとの統合とテスト完了
関連Issue
- 録音時間初期化問題 (Issue Feature/add review #17)
- UI/UX改善全般
- 長時間録音の安定性改善
Definition of Done
- 3時間の連続録音が安定して動作する
- メモリリークが発生しない
- 録音時間が正しく初期化される
- 既存機能との互換性が保たれている
- 包括的なテストが完了している
Reactions are currently unavailable
Metadata
Metadata
Assignees
Labels
No labels