diff --git a/AudioStreaming/OggVorbis/VorbisFileDecoder.swift b/AudioStreaming/OggVorbis/VorbisFileDecoder.swift index 14988f4..2474078 100644 --- a/AudioStreaming/OggVorbis/VorbisFileDecoder.swift +++ b/AudioStreaming/OggVorbis/VorbisFileDecoder.swift @@ -120,6 +120,11 @@ final class VorbisFileDecoder { switch channels { case 1: layoutTag = kAudioChannelLayoutTag_Mono case 2: layoutTag = kAudioChannelLayoutTag_Stereo + // Vorbis uses FL, FC, FR, RL, RR, LFE channel order. The MPEG + // 5.1 layout instead expects FL, FR, FC, LFE, RL, RR, so tagging + // Vorbis PCM as MPEG 5.1 misidentifies four of the six channels. + // Ogg_5_1 describes the channel order emitted by the Vorbis decoder. + case 6: layoutTag = kAudioChannelLayoutTag_Ogg_5_1 default: layoutTag = kAudioChannelLayoutTag_Unknown | UInt32(channels) } diff --git a/AudioStreaming/Streaming/AudioPlayer/AudioPlayer.swift b/AudioStreaming/Streaming/AudioPlayer/AudioPlayer.swift index 13fba34..fed76bc 100644 --- a/AudioStreaming/Streaming/AudioPlayer/AudioPlayer.swift +++ b/AudioStreaming/Streaming/AudioPlayer/AudioPlayer.swift @@ -146,7 +146,15 @@ open class AudioPlayer { } /// An `AVAudioFormat` object for the canonical audio stream - private var outputAudioFormat: AVAudioFormat = .init(commonFormat: .pcmFormatFloat32, sampleRate: 44100.0, channels: 2, interleaved: true)! + private let outputAudioFormat: AVAudioFormat = { + let channelLayout = AVAudioChannelLayout(layoutTag: kAudioChannelLayoutTag_MPEG_5_1_A)! + return AVAudioFormat( + commonFormat: .pcmFormatFloat32, + sampleRate: 44100.0, + interleaved: true, + channelLayout: channelLayout + ) + }() /// Keeps track of the player's state before being paused. private var stateBeforePaused: InternalState = .initial diff --git a/AudioStreaming/Streaming/AudioPlayer/AudioRendererContext.swift b/AudioStreaming/Streaming/AudioPlayer/AudioRendererContext.swift index b7cbd60..07c2ca5 100644 --- a/AudioStreaming/Streaming/AudioPlayer/AudioRendererContext.swift +++ b/AudioStreaming/Streaming/AudioPlayer/AudioRendererContext.swift @@ -38,7 +38,10 @@ final class AudioRendererContext { framesRequiredForDataAfterSeekPlaying = Double(canonicalStream.mSampleRate) * Double(configuration.gracePeriodAfterSeekInSeconds) let dataByteSize = Int(canonicalStream.mSampleRate * configuration.bufferSizeInSeconds) * Int(canonicalStream.mBytesPerFrame) - inOutAudioBufferList = allocateBufferList(dataByteSize: dataByteSize) + inOutAudioBufferList = allocateBufferList( + dataByteSize: dataByteSize, + channelCount: canonicalStream.mChannelsPerFrame + ) audioBuffer = inOutAudioBufferList[0].mBuffers @@ -71,14 +74,17 @@ final class AudioRendererContext { /// /// - parameter dataByteSize: An `Int` value indicating the size that the buffer will hold /// - Returns: An `UnsafeMutablePointer` object -private func allocateBufferList(dataByteSize: Int) -> UnsafeMutablePointer { +private func allocateBufferList( + dataByteSize: Int, + channelCount: UInt32 +) -> UnsafeMutablePointer { let _bufferList = AudioBufferList.allocate(maximumBuffers: 1) _bufferList[0].mDataByteSize = UInt32(dataByteSize) let alignment = MemoryLayout.alignment let mData = UnsafeMutableRawPointer.allocate(byteCount: dataByteSize, alignment: alignment) _bufferList[0].mData = mData - _bufferList[0].mNumberChannels = 2 + _bufferList[0].mNumberChannels = channelCount return _bufferList.unsafeMutablePointer } diff --git a/AudioStreaming/Streaming/AudioPlayer/Processors/AudioFileStreamProcessor.swift b/AudioStreaming/Streaming/AudioPlayer/Processors/AudioFileStreamProcessor.swift index 77d560e..304ce22 100644 --- a/AudioStreaming/Streaming/AudioPlayer/Processors/AudioFileStreamProcessor.swift +++ b/AudioStreaming/Streaming/AudioPlayer/Processors/AudioFileStreamProcessor.swift @@ -244,10 +244,47 @@ final class AudioFileStreamProcessor { return } } + configureChannelLayouts(for: audioConverter, inputChannels: inputFormat.mChannelsPerFrame) self.inputFormat = inputFormat assignMagicCookieToConverterIfNeeded() } + private func configureChannelLayouts( + for converter: AudioConverterRef?, + inputChannels: UInt32 + ) { + guard let converter else { return } + + if let inputLayoutTag = channelLayoutTag(for: inputChannels), + let inputLayout = AVAudioChannelLayout(layoutTag: inputLayoutTag) { + AudioConverterSetProperty( + converter, + kAudioConverterInputChannelLayout, + UInt32(MemoryLayout.size), + inputLayout.layout + ) + } + + guard let outputLayout = AVAudioChannelLayout( + layoutTag: kAudioChannelLayoutTag_MPEG_5_1_A + ) else { return } + AudioConverterSetProperty( + converter, + kAudioConverterOutputChannelLayout, + UInt32(MemoryLayout.size), + outputLayout.layout + ) + } + + private func channelLayoutTag(for channelCount: UInt32) -> AudioChannelLayoutTag? { + return switch channelCount { + case 1: kAudioChannelLayoutTag_Mono + case 2: kAudioChannelLayoutTag_Stereo + case 6: kAudioChannelLayoutTag_MPEG_5_1_A + default: nil + } + } + private func assignMagicCookieToConverterIfNeeded() { // magic cookie info if let fileStream = audioFileStream { diff --git a/AudioStreaming/Streaming/AudioPlayer/Processors/AudioPlayerRenderProcessor.swift b/AudioStreaming/Streaming/AudioPlayer/Processors/AudioPlayerRenderProcessor.swift index 23f8ee4..4d0edcb 100644 --- a/AudioStreaming/Streaming/AudioPlayer/Processors/AudioPlayerRenderProcessor.swift +++ b/AudioStreaming/Streaming/AudioPlayer/Processors/AudioPlayerRenderProcessor.swift @@ -113,7 +113,7 @@ final class AudioPlayerRenderProcessor: NSObject { if used > 0 && !waitForBuffer && playingEntry != nil && state.contains(.running) && state != .paused { if end > start { let framesToCopy = min(inNumberFrames, used) - bufferList.mBuffers.mNumberChannels = 2 + bufferList.mBuffers.mNumberChannels = outputAudioFormat.mChannelsPerFrame bufferList.mBuffers.mDataByteSize = frameSizeInBytes * framesToCopy if isMuted { @@ -155,7 +155,7 @@ final class AudioPlayerRenderProcessor: NSObject { #endif frameToCopy = 0 } - bufferList.mBuffers.mNumberChannels = 2 + bufferList.mBuffers.mNumberChannels = outputAudioFormat.mChannelsPerFrame bufferList.mBuffers.mDataByteSize = frameSizeInBytes * frameToCopy if isMuted { @@ -172,7 +172,7 @@ final class AudioPlayerRenderProcessor: NSObject { let delta = inNumberFrames - frameToCopy if delta > 0 { moreFramesToCopy = min(delta, end) - bufferList.mBuffers.mNumberChannels = 2 + bufferList.mBuffers.mNumberChannels = outputAudioFormat.mChannelsPerFrame bufferList.mBuffers.mDataByteSize += frameSizeInBytes * moreFramesToCopy if let ioBufferData = bufferList.mBuffers.mData { if isMuted { diff --git a/AudioStreaming/Streaming/AudioPlayer/Processors/OggVorbisStreamProcessor.swift b/AudioStreaming/Streaming/AudioPlayer/Processors/OggVorbisStreamProcessor.swift index 884f467..012079d 100644 --- a/AudioStreaming/Streaming/AudioPlayer/Processors/OggVorbisStreamProcessor.swift +++ b/AudioStreaming/Streaming/AudioPlayer/Processors/OggVorbisStreamProcessor.swift @@ -291,12 +291,21 @@ final class OggVorbisStreamProcessor { private func createAudioConverter(from sourceFormat: AVAudioFormat, to destFormat: AudioStreamBasicDescription) { audioConverter = nil - var dest = destFormat + let dest = destFormat - guard let destAVFormat = AVAudioFormat(streamDescription: &dest) else { - Logger.error("Failed to create output AVAudioFormat", category: .audioRendering) + guard let channelLayout = AVAudioChannelLayout( + layoutTag: kAudioChannelLayoutTag_MPEG_5_1_A + ) else { + Logger.error("Failed to create 5.1 output channel layout", category: .audioRendering) return } + + let destAVFormat = AVAudioFormat( + commonFormat: .pcmFormatFloat32, + sampleRate: dest.mSampleRate, + interleaved: true, + channelLayout: channelLayout + ) guard let converter = AVAudioConverter(from: sourceFormat, to: destAVFormat) else { Logger.error("Failed to create AVAudioConverter", category: .audioRendering)