| 1 | import AVFoundation |
| 2 | import Foundation |
| 3 | import LiveKitWebRTC |
| 4 | import os |
| 5 | |
| 6 | /// The level of one person's decoded audio, for the meter on their tile. |
| 7 | /// |
| 8 | /// A renderer on their remote audio track: WebRTC hands it every 10 ms |
| 9 | /// frame the moment it is decoded for playout, before the output volume |
| 10 | /// is applied (the track's sink sits ahead of the gain in the receive |
| 11 | /// channel). So it measures what arrived, whatever the app then does with |
| 12 | /// the volume — including the interop harness, which plays at zero. |
| 13 | /// |
| 14 | /// Chosen over inbound-rtp `audioLevel`, which is measured *after* that |
| 15 | /// gain and would read silence at volume zero, and which would take a |
| 16 | /// statistics walk per peer five times a second. The statistics are still |
| 17 | /// read, once a second, for the other half of "No sound from": whether |
| 18 | /// packets are arriving at all (MediaEngine.peerAudio). |
| 19 | /// |
| 20 | /// Called on WebRTC's audio thread: nothing here blocks or waits. |
| 21 | final class RemoteAudioTap: NSObject, RTCAudioRenderer, @unchecked Sendable { |
| 22 | private struct Reading { |
| 23 | var peak: Float = 0 |
| 24 | var buffers = 0 |
| 25 | } |
| 26 | private let lock = OSAllocatedUnfairLock(initialState: Reading()) |
| 27 | |
| 28 | func render(pcmBuffer: AVAudioPCMBuffer) { |
| 29 | let frames = Int(pcmBuffer.frameLength) |
| 30 | guard frames > 0 else { return } |
| 31 | var top: Float = 0 |
| 32 | if let int16 = pcmBuffer.int16ChannelData { |
| 33 | // The adapter's buffers are interleaved int16: one pointer, every |
| 34 | // channel's samples in it. |
| 35 | let channels = pcmBuffer.format.isInterleaved ? Int(pcmBuffer.format.channelCount) : 1 |
| 36 | let samples = int16[0] |
| 37 | var peak: Int32 = 0 |
| 38 | for i in 0..<(frames * channels) { peak = max(peak, abs(Int32(samples[i]))) } |
| 39 | top = Float(peak) / 32768 |
| 40 | } else if let float = pcmBuffer.floatChannelData { |
| 41 | let samples = float[0] |
| 42 | for i in 0..<frames { top = max(top, abs(samples[i])) } |
| 43 | } else { |
| 44 | return |
| 45 | } |
| 46 | let level = min(top, 1) |
| 47 | lock.withLock { r in |
| 48 | r.peak = max(r.peak, level) |
| 49 | r.buffers += 1 |
| 50 | } |
| 51 | } |
| 52 | |
| 53 | /// The loudest sample since the last call, and how many frames came. |
| 54 | func take() -> (peak: Float, buffers: Int) { |
| 55 | lock.withLock { r in |
| 56 | let out = (r.peak, r.buffers) |
| 57 | r = Reading() |
| 58 | return out |
| 59 | } |
| 60 | } |
| 61 | } |
| 62 | |