ํฐ์คํ ๋ฆฌ ๋ทฐ
audio streaming - 100ms ์ ๋ ฅ ๋ฒํผ๋ฅผ 30ms ํจํท์ผ๋ก ๋๋๊ธฐ
ggasoon2 2025. 9. 29. 17:09
์ค์๊ฐ ์์ฑ ์คํธ๋ฆฌ๋ฐ ๊ตฌํ์ค ์ง๋ฉดํ ๋ฌธ์ ์ ๋๋ค.
์ฐ์ ์๋ฒ๋ ํด๋ผ์ด์ธํธ๊ฐ
16kHz, Int16, 30ms ๋จ์๋ก buffer๋ฅผ ์ฃผ๊ณ ๋ฐ์ผ๋ ค๊ณ ํ์์ต๋๋ค.
let sampleRate: Double = 16000
let frameDuration: Double = 0.03 // 100ms
lazy var audioFrameCount = Int(sampleRate * frameDuration)
์คํธ๋ฆฌ๋ฐ ๋ณด๋ผ๋ ๊ท๊ฒฉ์ ๋ง์ถฐ์ ๋ฆฌ์ํ๋งํ ๋ฐ์ดํฐ๋ฅผ ์ ์กํด์ฃผ์๋๋ฐ
// ๋ณํ ํ ๋ฐ์ดํฐ ์ ์ฒด๋ฅผ ํ ๋ฒ์ ์ ์ก
let pcmBuf = AVAudioPCMBuffer(pcmFormat: mixerFormat, frameCapacity: frameCount)!
audioInputConverter?.convert(to: pcmBuf, error: nil) { _, outStatus in
outStatus.pointee = .haveData
return buffer
}
if let channelData = pcmBuf.int16ChannelData?.pointee {
let array = Array(UnsafeBufferPointer(start: channelData, count: Int(pcmBuf.frameLength)))
let data = Data(buffer: UnsafeBufferPointer(start: array, count: array.count))
sendVoiceDataRelay.send(StreamRequest(..., data: data, ...))
}
๋ฐ๋์ชฝ์์ ์์ฑ buffer๊ฐ ์์ฒญ ๋๊ฒจ์ ๋ค๋ฆฌ๋ ๋ฌธ์ ๊ฐ ์์์ต๋๋ค
ํ์ธํด๋ณด๋
iOS์์ ๋ง์ดํฌ ์ ๋ ฅ์ ๋ฐ์ ๋,
audio engine์ ์ ๋ ฅ์ฒ๋ฆฌ ๋ถ๋ถ์์ 100ms ๊ฐ๊ฒฉ์ผ๋ก streaming ์ ์ก๋๊ณ ์์์ต๋๋ค.
inputNode?.installTap(onBus: 0, bufferSize: tapFrames, format: audioInputFormat) {
[weak self] (buffer, when) in
// ์ฌ๊ธฐ์ ์ฐ์ด๋ณด๋ 100ms๊ฐ๊ฒฉ
}
audioFrameCount๋ฅผ 480(16000 * 0.03)๋ก ์ค์ ํด๋๋๋ฐ๋ 30ms๋ง๋ค ๋ณด๋ด์ผํ๋๋ฐ
100ms ๊ฐ๊ฒฉ์ผ๋ก ์ ์กํ์ฌ ๋ฐ๋์ชฝ์์ ์์ฑ์ด ๋๊ฒจ์ ๋ค๋ ธ์์ต๋๋ค.
์ ๊ท๊ฒฉ๋๋ก ๋ง์ถฐ์ค ์์ ์ ์ก๊ฐ๊ฒฉ 30ms๊ฐ ์๋ 100ms์ธ์ง ํ์ธํด๋ณด๋,
audio engine์ ์ ๋ ฅ ์ฒ๋ฆฌ ์ต์ ๊ฐ๊ฒฉ์ด 100ms ์ ๋์๊ณ ์ด๊ฑธ ๋ ๋ฎ์ถ๋๊ฒ ๋ถ๊ฐ๋ฅํ์ต๋๋ค.
์ด๋ฅผ ํด๊ฒฐํ๊ธฐ ์ํด์
1. installTap ๋ด๋ถ์์ 100ms ๋ฐํ๋๋ ๋ฐ์ดํฐ๋ฅผ ํ์ ๋ด์์ฃผ๊ณ (*๋ฒํผ ํ)
2. 30ms ๋งํผ ๋ฒํผ ํ์์ ๊บผ๋ด์ ๋ฐํํ์์ต๋๋ค. (*์ฒญํฌ ๋ถํ )
3. ๊ทธ๋ฆฌ๊ณ 30ms ์ค๋์ค ์ฌ์ด์ฆ (480 frame)๋งํผ ์๋ฆฐ ๋ฐ์ดํฐ๋ฅผ 30ms ์๊ฐ ๊ฐ๊ฒฉ์ผ๋ก streaming ์ ์ก ํด์ฃผ์์ต๋๋ค. (*pacer)
-> installTap์์๋ ์ต์ ๊ฐ๊ฒฉ์ด 100ms ์ด๋ฏ๋ก, 30m *3๊ฐ 10ms * 1๊ฐ ์ด๋ ๊ฒ ์ ์ก์ด ์ด๋ฃจ์ด์ง๊ฒ ๋ฉ๋๋ค.
-> 30ms ์๊ฐ๊ฐ๊ฒฉ์ผ๋ก streaming ํด์ฃผ๊ธฐ ์ํด pacer๊ฐ ํ์
์ด๋ ๊ฒ 3๊ฐ์ง ๊ฐ๋ ์ (queue, chunk, pacer) ์ ์ฉํ์ฌ ํด๊ฒฐํ์์ต๋๋ค
์กฐ๊ธ ๋ํ ์ผํ๊ฒ ์ดํด๋ณด๋ฉด
private let txQueue = DispatchQueue(label: "com.readysay.txPacer")
private var txAccum = [Int16]()
inputNode?.installTap(onBus: 0, bufferSize: tapFrames, format: audioInputFormat) {
[weak self] (buffer, when) in
// ๋ฆฌ์ํ๋ง ๋ณํ AVAudioConverter๋ก 48kHz → 16kHz ๋ณํ
// ... ์๋ต
// ์ดํ ๋ฒํผํ์ ์ค๋์ค ์ํ๋ง ๋ฐ์ดํฐ ๋์
self.txQueue.async { [weak self] in
guard let self = self else { return }
self.txAccum += Array(UnsafeBufferPointer(start: chPtr, count: outFrames))
}
}
installTap์ ๋ฐํ ๋ฐ์ดํฐ์ธ ๋ฆฌ์ํ๋ง ๋ฐ์ดํฐ๋ฅผ txAccum ๋ฒํผ ํ์ ๊ณ์ ๋์ ํด์ฃผ์์ต๋๋ค.
์ดํ
let framesPerPacket = 480 // 30ms @16k
...
let pkt = Array(self.txAccum.prefix(framesPerPacket))
self.txAccum.removeFirst(framesPerPacket)
480 frame ๋งํผ ์๋ผ๋ด์ด data๋ก ๋ง๋ค์ด์ ๋๊ฒจ์ฃผ๋ฉฐ
480 frame์ด ์๋ ๊ฒฝ์ฐ ๋ฒํผ ํ์ ๋จ๊ฒจ์ ๋ค์ tick๋ ๋๊ฒจ์ง๋๋ก ํด๋์์ต๋๋ค.
guard self.txAccum.count >= framesPerPacket else { return }
์๋๋ ๋ํ ์ผ ํ ์ฝ๋์ ๋๋ค
private func startTxPacer(displayName: String) {
guard txTimer == nil else { return }
let framesPerPacket = 480 // 30ms @ 16k
var started = false
let minStart = framesPerPacket * 2 // ~60ms ๋งํผ ์์ธ ๋ค ์์ (์ด๊ธฐ ๋๊น ๋ฐฉ์ง)
let timer = DispatchSource.makeTimerSource(queue: txQueue)
timer.schedule(deadline: .now() + .milliseconds(30), repeating: .milliseconds(30), leeway: .milliseconds(3))
timer.setEventHandler { [weak self] in
guard let self = self else { return }
if !started {
if self.txAccum.count < minStart { return }
started = true
}
guard self.txAccum.count >= framesPerPacket else { return }
// 30ms ํจํท ๋ง๋ค๊ธฐ
let pkt = Array(self.txAccum.prefix(framesPerPacket))
self.txAccum.removeFirst(framesPerPacket)
var data = Data()
pkt.withUnsafeBytes { data.append($0.bindMemory(to: UInt8.self)) }
self.txSeq &+= 1
// print("โฑ๏ธ TX tick → #\(self.txSeq) bytes=\(data.count)")
// ๋ฐ์ดํฐ ์คํธ๋ฆฌ๋ฐ ์ ์ก ๋ถ๋ถ
let sourceLang = LoginUserHashCache.checkSourceTranslateLnCodeHashCache()
let targetLang = LoginUserHashCache.checkTargetTranslateLnCodeHashCache()
let req = StreamRequest(userId: self.userId,
channelId: self.channel.id,
data: data,
sourceLang: sourceLang,
translateLang: targetLang,
displayName: displayName)
inputRelays.sendVoiceDataRelay.accept(req)
}
txTimer = timer
timer.resume()
}
๊ทธ๋์ 100ms ๋ง๋ค ๋ฐํ๋ฐ์ ์ค๋์ค ๋ฐ์ดํฐ๋ฅผ
30ms ์ฌ์ด์ฆ(480 frame) ๋งํผ ์ฒญํฌ๋ฅผ ๋ถํ ํ๊ณ
30ms ์ฃผ๊ธฐ๋ก ์คํธ๋ฆฌ๋ฐ์ ๋ด์ ๋ณด๋ด์ฃผ๋๋ก ํด์ฃผ์์ต๋๋ค
์ด๋ ๊ฒ ์ฒญํฌ ๋ถํ ์ ์ ์ฉํ์ฌ
100ms ๋จ์ ์ ๋ ฅ → 30ms ๋จ์ ์ถ๋ ฅ์ผ๋ก ์์ ํด์ฃผ์์ต๋๋ค.
30ms,30ms,30ms,10ms ํ๋ฒ์ ์ ์ก๋์ง์๊ณ
์ผ์ ํ๊ฒ 30ms ์ฃผ๊ธฐ๋ก ์ ์ก๋๋๋ก pacer๋ฅผ ์ ์ฉํ์ฌ ๋ฌธ์ ๋ฅผ ํด๊ฒฐํด์ฃผ์์ต๋๋ค
- Total
- Today
- Yesterday
- rag ai
- ๋น๋๊ธฐ ํ์ด์ง swift
- rag ๊ธฐ๋ฐ llm
- rag ์์ฑํ ai
- llm csv
- swift get excel
- rag llm pdf
- swift ์์ ๊ฐ์ ธ์ค๊ธฐ
- filemanager excel read
- ๋น๋๊ธฐ ๋ฆฌ์คํธ swift
- swift ์์ ์ฝ๊ธฐ
- swift excel read
- rag๊ธฐ๋ฐ ai
- ios gitignore
- rag ์ฑ๋ด
- ๊ณต๋ถ ํ์ด๋จธ ์ดํ
- rag๊ธฐ๋ฐ ์ฑ๋ด
- concurrency pagination
- ๋ ๋์ธ์ด ์ดํ
- ๋ ๋์ธ์ด
- swift filemanager get excel
- swift filemanager excel
- llm pdf rag
- chatgpt rag llm
- readysay
- rag ์์
- swift ์๊ฐ
- ์๋์ํํธ ๋ ์ด์ธ์ด
- swift git ignore
- llm rag
| ์ผ | ์ | ํ | ์ | ๋ชฉ | ๊ธ | ํ |
|---|---|---|---|---|---|---|
| 1 | 2 | 3 | 4 | 5 | 6 | 7 |
| 8 | 9 | 10 | 11 | 12 | 13 | 14 |
| 15 | 16 | 17 | 18 | 19 | 20 | 21 |
| 22 | 23 | 24 | 25 | 26 | 27 | 28 |