Skip to content
Open
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
105 changes: 71 additions & 34 deletions Sources/ViewModels/Audio/AudioStream.swift
Original file line number Diff line number Diff line change
@@ -1,13 +1,22 @@
//
// AudioStream.swift
// Lecture2Quiz
//
// Created by 바견규 on 4/27/25.
//

import AVFoundation


class AudioStreamer {
private let engine = AVAudioEngine()
private let inputNode: AVAudioInputNode
private var inputFormat: AVAudioFormat?
private var isPaused: Bool = false
private var audioWebSocket: AudioWebSocket?
private var converter: AVAudioConverter?

private var converter: AVAudioConverter?

// WhisperLive 설정에 맞춘 포맷
private var bufferSize: AVAudioFrameCount = 4096
private var sampleRate: Double = 16000
Expand All @@ -22,6 +31,7 @@ class AudioStreamer {
func configureAudioSession() {
let session = AVAudioSession.sharedInstance()
do {
// 🎧 블루투스 장치 포함하여 오디오 재생 및 녹음 설정
try session.setCategory(.playAndRecord, mode: .voiceChat, options: [.allowBluetooth, .defaultToSpeaker])
try session.setActive(true)

Expand All @@ -37,17 +47,11 @@ class AudioStreamer {
}

// ✅ 실제 하드웨어 포맷 가져오기
let inputSampleRate = session.sampleRate
let inputChannels = UInt32(session.inputNumberOfChannels)
print("🎙️ 설정된 샘플레이트: \(inputSampleRate)")
print("🎙️ 설정된 채널 수: \(inputChannels)")

// ✅ 샘플레이트를 16000으로 변환하도록 설정
let inputFormat = AVAudioFormat(commonFormat: .pcmFormatFloat32, sampleRate: inputSampleRate, channels: inputChannels, interleaved: false)!
let outputFormat = AVAudioFormat(commonFormat: .pcmFormatFloat32, sampleRate: 16000, channels: inputChannels, interleaved: false)!

converter = AVAudioConverter(from: inputFormat, to: outputFormat)

sampleRate = session.sampleRate
channels = UInt32(session.inputNumberOfChannels)
print("🎙️ 설정된 샘플레이트: \(sampleRate)")
print("🎙️ 설정된 채널 수: \(channels)")

} catch {
print("🔴 오디오 세션 설정 실패: \(error.localizedDescription)")
}
Expand All @@ -57,13 +61,30 @@ class AudioStreamer {
func startStreaming() {
configureAudioSession()

let format = inputNode.outputFormat(forBus: 0)
self.inputFormat = format
// ✅ 하드웨어 포맷에 맞춰 Tap 포맷 설정
let inputFormat = inputNode.inputFormat(forBus: 0)
print("🔍 Input Format: \(inputFormat)")

guard let outputFormat = AVAudioFormat(commonFormat: .pcmFormatInt16,
sampleRate: 16000,
channels: 1,
interleaved: true) else {
print("⚠️ AVAudioFormat 생성 실패")
return
}

// ✅ Converter 생성
guard let audioConverter = AVAudioConverter(from: inputFormat, to: outputFormat) else {
print("🔴 Converter 초기화 실패")
return
}

self.inputFormat = format

inputNode.installTap(onBus: 0, bufferSize: bufferSize, format: format) { [weak self] buffer, _ in
// 🔄 여기서 converter에 값을 할당해야 함
self.converter = audioConverter

print("✅ Converter 초기화 성공")

inputNode.installTap(onBus: 0, bufferSize: bufferSize, format: inputFormat) { [weak self] buffer, _ in
self?.processAudioBuffer(buffer)
}

Expand All @@ -75,6 +96,8 @@ class AudioStreamer {
}
}



// MARK: - 오디오 버퍼를 WebSocket으로 서버로 전송
func processAudioBuffer(_ buffer: AVAudioPCMBuffer) {
guard let converter = converter else { return }
Expand All @@ -94,32 +117,45 @@ class AudioStreamer {
return
}

if let audioData = convertBufferTo16BitPCM(outputBuffer) {
print("🔄 PCM 데이터 전송 중...")
audioWebSocket?.sendDataToServer(audioData)
// 먼저 16비트 PCM으로 변환 후 Float32로 다시 변환
if let pcmData = convertBufferTo16BitPCM(outputBuffer) {
// Float32로 변환하여 서버에 전송
let floatData = convertPCMToFloat32(pcmData)
print("🔄 Float32 데이터 전송 중...")
audioWebSocket?.sendDataToServer(floatData)
} else {
print("Error: Audio buffer 변환 실패")
}
}

// MARK: - 32bit float PCM -> 16bit int PCM 변환
func convertBufferTo16BitPCM(_ buffer: AVAudioPCMBuffer) -> Data? {
guard let floatChannelData = buffer.floatChannelData else {
print("floatChannelData is nil")
guard let channelData = buffer.int16ChannelData else {
print("int16ChannelData is nil")
return nil
}

let channelPointer = floatChannelData.pointee
let frameLength = Int(buffer.frameLength)
var pcmData = Data(capacity: frameLength * MemoryLayout<Int16>.size)

for i in 0..<frameLength {
let sample = max(-1.0, min(1.0, channelPointer[i])) // 클리핑 처리
var intSample = Int16(sample * Float(Int16.max))
pcmData.append(Data(bytes: &intSample, count: MemoryLayout<Int16>.size))
let channelPointer = channelData.pointee
let dataLength = Int(buffer.frameLength) * MemoryLayout<Int16>.size
return Data(bytes: channelPointer, count: dataLength)
}

// MARK: - 16bit int PCM -> Float32 변환 (새로 추가)
func convertPCMToFloat32(_ pcmData: Data) -> Data {
// 16비트 PCM 데이터를 float32로 변환
var floatArray = [Float32](repeating: 0, count: pcmData.count / 2)

pcmData.withUnsafeBytes { (bytes: UnsafeRawBufferPointer) -> Void in
if let baseAddress = bytes.baseAddress {
let int16Buffer = baseAddress.bindMemory(to: Int16.self, capacity: pcmData.count / 2)
for i in 0..<pcmData.count / 2 {
// -1.0에서 1.0 사이로 정규화 (Python 코드와 동일한 처리)
floatArray[i] = Float32(int16Buffer[i]) / 32768.0
}
}
}

return pcmData

// Float32 배열을 Data로 변환
return Data(bytes: floatArray, count: floatArray.count * MemoryLayout<Float32>.size)
}

// MARK: - 오디오 스트리밍 일시 정지
Expand Down Expand Up @@ -149,5 +185,6 @@ class AudioStreamer {
engine.stop()
print("🛑 AVAudioEngine 중지됨")
}


}