diff --git a/HaishinKit.xcodeproj/project.pbxproj b/HaishinKit.xcodeproj/project.pbxproj index b5d6c97fe..a711f952e 100644 --- a/HaishinKit.xcodeproj/project.pbxproj +++ b/HaishinKit.xcodeproj/project.pbxproj @@ -258,7 +258,6 @@ BCFB355524FA27EA00DC5108 /* PlaybackViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = BCFB355324FA275600DC5108 /* PlaybackViewController.swift */; }; BCFB355A24FA40DD00DC5108 /* PlaybackContainerViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = BCFB355924FA40DD00DC5108 /* PlaybackContainerViewController.swift */; }; BCFC51FE2AAB420700014428 /* IOAudioResampler.swift in Sources */ = {isa = PBXBuildFile; fileRef = BCFC51FD2AAB420700014428 /* IOAudioResampler.swift */; }; - BCFC9BE02AB43A3A00378E56 /* AudioCodecSettingsTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = BCFC9BDF2AB43A3A00378E56 /* AudioCodecSettingsTests.swift */; }; BCFF640B29C0C44B004EFF2F /* SampleVideo_360x240_5mb_2ch.ts in Resources */ = {isa = PBXBuildFile; fileRef = BCFF640A29C0C44B004EFF2F /* SampleVideo_360x240_5mb_2ch.ts */; }; /* End PBXBuildFile section */ @@ -645,7 +644,6 @@ BCFB355324FA275600DC5108 /* PlaybackViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PlaybackViewController.swift; sourceTree = ""; }; BCFB355924FA40DD00DC5108 /* PlaybackContainerViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PlaybackContainerViewController.swift; sourceTree = ""; }; BCFC51FD2AAB420700014428 /* IOAudioResampler.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = IOAudioResampler.swift; sourceTree = ""; }; - BCFC9BDF2AB43A3A00378E56 /* AudioCodecSettingsTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AudioCodecSettingsTests.swift; sourceTree = ""; }; BCFF640A29C0C44B004EFF2F /* SampleVideo_360x240_5mb_2ch.ts */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.typescript; path = SampleVideo_360x240_5mb_2ch.ts; sourceTree = ""; }; /* End PBXFileReference section */ @@ -896,7 +894,6 @@ isa = PBXGroup; children = ( 2950181F1FFA1BD700358E10 /* AudioCodecTests.swift */, - BCFC9BDF2AB43A3A00378E56 /* AudioCodecSettingsTests.swift */, ); path = Codec; sourceTree = ""; @@ -1844,7 +1841,6 @@ 290EA8AA1DFB61E700053022 /* CRC32Tests.swift in Sources */, 035AFA042263868E009DD0BB /* RTMPStreamTests.swift in Sources */, 290686031DFDB7A7008EB7ED /* RTMPConnectionTests.swift in Sources */, - BCFC9BE02AB43A3A00378E56 /* AudioCodecSettingsTests.swift in Sources */, BCC9E9092636FF7400948774 /* DataBufferTests.swift in Sources */, ); runOnlyForDeploymentPostprocessing = 0; diff --git a/Sources/Codec/AudioCodec.swift b/Sources/Codec/AudioCodec.swift index 26ae4ccac..38d37691b 100644 --- a/Sources/Codec/AudioCodec.swift +++ b/Sources/Codec/AudioCodec.swift @@ -42,6 +42,7 @@ public class AudioCodec { guard var inSourceFormat, inSourceFormat != oldValue else { return } + cursor = 0 inputBuffers.removeAll() outputBuffers.removeAll() audioConverter = makeAudioConverter(&inSourceFormat) diff --git a/Sources/Codec/AudioCodecSettings.swift b/Sources/Codec/AudioCodecSettings.swift index 3462aa4d1..061d938c5 100644 --- a/Sources/Codec/AudioCodecSettings.swift +++ b/Sources/Codec/AudioCodecSettings.swift @@ -5,11 +5,10 @@ import Foundation public struct AudioCodecSettings: Codable { /// The default value. public static let `default` = AudioCodecSettings() - /// Maximum number of channels supported by the system public static let maximumNumberOfChannels: UInt32 = 2 /// Maximum sampleRate supported by the system - public static let mamimumSampleRate: Float64 = 48000 + public static let mamimumSampleRate: Float64 = 48000.0 /// The type of the AudioCodec supports format. enum Format: Codable { @@ -133,33 +132,34 @@ public struct AudioCodecSettings: Codable { /// Specifies the sampleRate of audio output. public var sampleRate: Float64 /// Specifies the channels of audio output. - public var channels: Int - /// Map of the output to input channels. - public var channelMap: [Int: Int] + public var channels: UInt32 + /// Specifies the mixes the channels or not. Currently, it supports input sources with 4, 5, 6, and 8 channels. + public var downmix: Bool + /// Specifies the map of the output to input channels. + /// ## Example code: + /// ``` + /// // If you want to use the 3rd and 4th channels from a 4-channel input source for a 2-channel output, you would specify it like this. + /// channelMap = [2, 3] + /// ``` + public var channelMap: [Int]? /// Specifies the output format. var format: AudioCodecSettings.Format = .aac - /// Create an new AudioCodecSettings instance. + /// Create an new AudioCodecSettings instance. A value of 0 will use the same value as the input source. public init( bitRate: Int = 64 * 1000, sampleRate: Float64 = 0, - channels: Int = 0, - channelMap: [Int: Int] = [0: 0, 1: 1] + channels: UInt32 = 0, + downmix: Bool = false, + channelMap: [Int]? = nil ) { self.bitRate = bitRate self.sampleRate = sampleRate self.channels = channels + self.downmix = downmix self.channelMap = channelMap } - func invalidateConverter(_ oldValue: AudioCodecSettings) -> Bool { - return !( - sampleRate == oldValue.sampleRate && - channels == oldValue.channels && - channelMap == oldValue.channelMap - ) - } - func apply(_ converter: AVAudioConverter?, oldValue: AudioCodecSettings?) { guard let converter else { return @@ -175,37 +175,12 @@ public struct AudioCodecSettings: Codable { } } - func makeOutputChannels(_ inChannels: Int) -> Int { - return min(channels == 0 ? inChannels : channels, Int(Self.maximumNumberOfChannels)) - } - - func makeChannelMap(_ inChannels: Int) -> [NSNumber] { - let outChannels = makeOutputChannels(inChannels) - var result = Array(repeating: -1, count: outChannels) - for inputIndex in 0.. AVAudioFormat? { - guard let inputFormat else { - return nil - } - let numberOfChannels = makeOutputChannels(Int(inputFormat.channelCount)) - guard let channelLayout = AVAudioChannelLayout(layoutTag: kAudioChannelLayoutTag_DiscreteInOrder | UInt32(numberOfChannels)) else { - return nil - } + func makeAudioResamplerSettings() -> IOAudioResamplerSettings { return .init( - commonFormat: inputFormat.commonFormat, - sampleRate: min(sampleRate == 0 ? inputFormat.sampleRate : sampleRate, Self.mamimumSampleRate), - interleaved: inputFormat.isInterleaved, - channelLayout: channelLayout + sampleRate: sampleRate, + channels: channels, + downmix: downmix, + channelMap: channelMap?.map { NSNumber(value: $0) } ) } } diff --git a/Sources/Media/IOAudioResampler.swift b/Sources/Media/IOAudioResampler.swift index b645dd2e5..11ad13ba3 100644 --- a/Sources/Media/IOAudioResampler.swift +++ b/Sources/Media/IOAudioResampler.swift @@ -10,13 +10,68 @@ protocol IOAudioResamplerDelegate: AnyObject { func resampler(_ resampler: IOAudioResampler, errorOccurred error: AudioCodec.Error) } +struct IOAudioResamplerSettings { + let sampleRate: Float64 + let channels: UInt32 + let downmix: Bool + let channelMap: [NSNumber]? + + init(sampleRate: Float64 = 0, channels: UInt32 = 0, downmix: Bool = false, channelMap: [NSNumber]? = nil) { + self.sampleRate = sampleRate + self.channels = channels + self.downmix = downmix + self.channelMap = channelMap + } + + func invalidate(_ oldValue: IOAudioResamplerSettings!) -> Bool { + return (sampleRate != oldValue.sampleRate && + channels != oldValue.channels) + } + + func apply(_ converter: AVAudioConverter?, oldValue: IOAudioResamplerSettings?) { + guard let converter else { + return + } + if converter.downmix != downmix { + converter.downmix = downmix + } + if let channelMap { + converter.channelMap = channelMap + } else { + switch converter.outputFormat.channelCount { + case 1: + converter.channelMap = [0] + case 2: + converter.channelMap = (converter.inputFormat.channelCount == 1) ? [0, 0] : [0, 1] + default: + logger.error("channelCount must be 2 or less.") + } + } + } + + func makeOutputFormat(_ inputFormat: AVAudioFormat?) -> AVAudioFormat? { + guard let inputFormat else { + return nil + } + return .init( + commonFormat: inputFormat.commonFormat, + sampleRate: min(sampleRate == 0 ? inputFormat.sampleRate : sampleRate, AudioCodecSettings.mamimumSampleRate), + channels: min(channels == 0 ? inputFormat.channelCount : channels, AudioCodecSettings.maximumNumberOfChannels), + interleaved: inputFormat.isInterleaved + ) + } +} + final class IOAudioResampler { - var settings: AudioCodecSettings = .default { + var settings: IOAudioResamplerSettings = .init() { didSet { - guard var inSourceFormat, settings.invalidateConverter(oldValue) else { - return + if settings.invalidate(oldValue) { + if var inSourceFormat { + setUp(&inSourceFormat) + } + } else { + settings.apply(audioConverter, oldValue: oldValue) } - setUp(&inSourceFormat) } } weak var delegate: T? @@ -42,7 +97,7 @@ final class IOAudioResampler { guard let audioConverter else { return } - audioConverter.channelMap = settings.makeChannelMap(Int(audioConverter.inputFormat.channelCount)) + settings.apply(audioConverter, oldValue: nil) audioConverter.primeMethod = .normal delegate?.resampler(self, didOutput: audioConverter.outputFormat) } diff --git a/Sources/Media/IOAudioUnit.swift b/Sources/Media/IOAudioUnit.swift index b6bee163b..1fa27ceaf 100644 --- a/Sources/Media/IOAudioUnit.swift +++ b/Sources/Media/IOAudioUnit.swift @@ -31,7 +31,7 @@ final class IOAudioUnit: NSObject, IOUnit { var settings: AudioCodecSettings = .default { didSet { codec.settings = settings - resampler.settings = settings + resampler.settings = settings.makeAudioResamplerSettings() } } diff --git a/Sources/Util/AVAudioFormatFactory.swift b/Sources/Util/AVAudioFormatFactory.swift index 18041f833..9c434f79d 100644 --- a/Sources/Util/AVAudioFormatFactory.swift +++ b/Sources/Util/AVAudioFormatFactory.swift @@ -8,10 +8,21 @@ enum AVAudioFormatFactory { guard inSourceFormat.mBitsPerChannel == 16 else { return nil } - if let layout = Self.makeChannelLayout(inSourceFormat.mChannelsPerFrame) { - return .init(commonFormat: .pcmFormatInt16, sampleRate: inSourceFormat.mSampleRate, interleaved: true, channelLayout: layout) + let interleaved = !((inSourceFormat.mFormatFlags & kLinearPCMFormatFlagIsNonInterleaved) == kLinearPCMFormatFlagIsNonInterleaved) + if let channelLayout = Self.makeChannelLayout(inSourceFormat.mChannelsPerFrame) { + return .init( + commonFormat: .pcmFormatInt16, + sampleRate: inSourceFormat.mSampleRate, + interleaved: interleaved, + channelLayout: channelLayout + ) } - return AVAudioFormat(commonFormat: .pcmFormatInt16, sampleRate: inSourceFormat.mSampleRate, channels: inSourceFormat.mChannelsPerFrame, interleaved: true) + return .init( + commonFormat: .pcmFormatInt16, + sampleRate: inSourceFormat.mSampleRate, + channels: inSourceFormat.mChannelsPerFrame, + interleaved: interleaved + ) } if let layout = Self.makeChannelLayout(inSourceFormat.mChannelsPerFrame) { return .init(streamDescription: &inSourceFormat, channelLayout: layout) @@ -23,6 +34,17 @@ enum AVAudioFormatFactory { guard 2 < numberOfChannels else { return nil } - return AVAudioChannelLayout(layoutTag: kAudioChannelLayoutTag_DiscreteInOrder | numberOfChannels) + switch numberOfChannels { + case 4: + return AVAudioChannelLayout(layoutTag: kAudioChannelLayoutTag_AudioUnit_4) + case 5: + return AVAudioChannelLayout(layoutTag: kAudioChannelLayoutTag_AudioUnit_5) + case 6: + return AVAudioChannelLayout(layoutTag: kAudioChannelLayoutTag_AudioUnit_6) + case 8: + return AVAudioChannelLayout(layoutTag: kAudioChannelLayoutTag_AudioUnit_8) + default: + return AVAudioChannelLayout(layoutTag: kAudioChannelLayoutTag_DiscreteInOrder | numberOfChannels) + } } } diff --git a/Tests/Codec/AudioCodecSettingsTests.swift b/Tests/Codec/AudioCodecSettingsTests.swift deleted file mode 100644 index 185018ba3..000000000 --- a/Tests/Codec/AudioCodecSettingsTests.swift +++ /dev/null @@ -1,39 +0,0 @@ -import Foundation -import XCTest -import AVFoundation - -@testable import HaishinKit - -final class AudioCodecSettingsTests: XCTestCase { - func testChannelMaps() { - XCTAssertEqual(AudioCodecSettings(bitRate: 0, sampleRate: 0, channels: 1, channelMap: [:]).makeChannelMap(1), [0]) - XCTAssertEqual(AudioCodecSettings(bitRate: 0, sampleRate: 0, channels: 1, channelMap: [0: 0]).makeChannelMap(1), [0]) - XCTAssertEqual(AudioCodecSettings(bitRate: 0, sampleRate: 0, channels: 1, channelMap: [0: 0, 1: 1]).makeChannelMap(1), [0]) - XCTAssertEqual(AudioCodecSettings(bitRate: 0, sampleRate: 0, channels: 1, channelMap: [0: -1]).makeChannelMap(1), [-1]) - XCTAssertEqual(AudioCodecSettings(bitRate: 0, sampleRate: 0, channels: 1, channelMap: [Int.max: Int.max]).makeChannelMap(1), [0]) - - XCTAssertEqual(AudioCodecSettings(bitRate: 0, sampleRate: 0, channels: 2, channelMap: [:]).makeChannelMap(1), [0, -1]) - XCTAssertEqual(AudioCodecSettings(bitRate: 0, sampleRate: 0, channels: 2, channelMap: [0: 0]).makeChannelMap(1), [0, -1]) - XCTAssertEqual(AudioCodecSettings(bitRate: 0, sampleRate: 0, channels: 2, channelMap: [0: 1]).makeChannelMap(1), [0, -1]) - XCTAssertEqual(AudioCodecSettings(bitRate: 0, sampleRate: 0, channels: 2, channelMap: [0: -1, 1: -1]).makeChannelMap(1), [-1, -1]) - XCTAssertEqual(AudioCodecSettings(bitRate: 0, sampleRate: 0, channels: 2, channelMap: [0: 1, 1: Int.max]).makeChannelMap(1), [0, -1]) - - XCTAssertEqual(AudioCodecSettings(bitRate: 0, sampleRate: 0, channels: 1, channelMap: [:]).makeChannelMap(2), [0]) - XCTAssertEqual(AudioCodecSettings(bitRate: 0, sampleRate: 0, channels: 1, channelMap: [0: 0]).makeChannelMap(2), [0]) - XCTAssertEqual(AudioCodecSettings(bitRate: 0, sampleRate: 0, channels: 1, channelMap: [0: 1]).makeChannelMap(2), [1]) - XCTAssertEqual(AudioCodecSettings(bitRate: 0, sampleRate: 0, channels: 1, channelMap: [0: -1]).makeChannelMap(1), [-1]) - XCTAssertEqual(AudioCodecSettings(bitRate: 0, sampleRate: 0, channels: 1, channelMap: [Int.max: 0]).makeChannelMap(2), [0]) - - XCTAssertEqual(AudioCodecSettings(bitRate: 0, sampleRate: 0, channels: 2, channelMap: [:]).makeChannelMap(2), [0, 1]) - XCTAssertEqual(AudioCodecSettings(bitRate: 0, sampleRate: 0, channels: 2, channelMap: [0: 0]).makeChannelMap(2), [0, 1]) - XCTAssertEqual(AudioCodecSettings(bitRate: 0, sampleRate: 0, channels: 2, channelMap: [0: 0, 1: 1]).makeChannelMap(2), [0, 1]) - XCTAssertEqual(AudioCodecSettings(bitRate: 0, sampleRate: 0, channels: 2, channelMap: [0: -1, 1: -1]).makeChannelMap(2), [-1, -1]) - XCTAssertEqual(AudioCodecSettings(bitRate: 0, sampleRate: 0, channels: 2, channelMap: [0: -1, 1: 1]).makeChannelMap(2), [-1, 1]) - XCTAssertEqual(AudioCodecSettings(bitRate: 0, sampleRate: 0, channels: 2, channelMap: [0: 0, 1: 1, Int.max: Int.max]).makeChannelMap(2), [0, 1]) - XCTAssertEqual(AudioCodecSettings(bitRate: 0, sampleRate: 0, channels: 2, channelMap: [0: 0, 1: Int.max]).makeChannelMap(2), [0, 1]) - - XCTAssertEqual(AudioCodecSettings(bitRate: 0, sampleRate: 0, channels: 2, channelMap: [:]).makeChannelMap(12), [0, 1]) - XCTAssertEqual(AudioCodecSettings(bitRate: 0, sampleRate: 0, channels: 2, channelMap: [0: -1, 1: 11]).makeChannelMap(12), [-1, 11]) - } -} - diff --git a/Tests/Media/IOAudioResamplerTests.swift b/Tests/Media/IOAudioResamplerTests.swift index 452d66c35..0ea9f36b4 100644 --- a/Tests/Media/IOAudioResamplerTests.swift +++ b/Tests/Media/IOAudioResamplerTests.swift @@ -20,7 +20,7 @@ final class IOAudioResamplerTests: XCTestCase { func testpKeep16000() { let resampler = IOAudioResampler() - resampler.settings = .init(bitRate: 0, sampleRate: 16000, channels: 1) + resampler.settings = .init(sampleRate: 16000, channels: 1) resampler.delegate = nullIOAudioResamplerDelegate resampler.appendSampleBuffer(CMAudioSampleBufferFactory.makeSinWave(48000, numSamples: 1024, channels: 1)!) XCTAssertEqual(resampler.outputFormat?.sampleRate, 16000) @@ -30,7 +30,7 @@ final class IOAudioResamplerTests: XCTestCase { func testpKeep44100() { let resampler = IOAudioResampler() - resampler.settings = .init(bitRate: 0, sampleRate: 44100, channels: 1) + resampler.settings = .init(sampleRate: 44100, channels: 1) resampler.delegate = nullIOAudioResamplerDelegate resampler.appendSampleBuffer(CMAudioSampleBufferFactory.makeSinWave(48000, numSamples: 1024, channels: 1)!) XCTAssertEqual(resampler.outputFormat?.sampleRate, 44100) @@ -44,7 +44,7 @@ final class IOAudioResamplerTests: XCTestCase { func testpKeep48000() { let resampler = IOAudioResampler() - resampler.settings = .init(bitRate: 0, sampleRate: 48000, channels: 1) + resampler.settings = .init(sampleRate: 48000, channels: 1) resampler.delegate = nullIOAudioResamplerDelegate resampler.appendSampleBuffer(CMAudioSampleBufferFactory.makeSinWave(48000, numSamples: 1024, channels: 1)!) XCTAssertEqual(resampler.outputFormat?.sampleRate, 48000) @@ -54,7 +54,7 @@ final class IOAudioResamplerTests: XCTestCase { func testpPassthrough48000_44100() { let resampler = IOAudioResampler() - resampler.settings = .init(bitRate: 0, sampleRate: 0, channels: 1) + resampler.settings = .init(sampleRate: 0, channels: 1) resampler.delegate = nullIOAudioResamplerDelegate resampler.appendSampleBuffer(CMAudioSampleBufferFactory.makeSinWave(44000, numSamples: 1024, channels: 1)!) XCTAssertEqual(resampler.outputFormat?.sampleRate, 44000) @@ -64,7 +64,7 @@ final class IOAudioResamplerTests: XCTestCase { func testpPassthrough44100_48000() { let resampler = IOAudioResampler() - resampler.settings = .init(bitRate: 0, sampleRate: 0, channels: 1) + resampler.settings = .init(sampleRate: 0, channels: 1) resampler.delegate = nullIOAudioResamplerDelegate resampler.appendSampleBuffer(CMAudioSampleBufferFactory.makeSinWave(48000, numSamples: 1024, channels: 1)!) XCTAssertEqual(resampler.outputFormat?.sampleRate, 48000) @@ -74,7 +74,7 @@ final class IOAudioResamplerTests: XCTestCase { func testpPassthrough16000_48000() { let resampler = IOAudioResampler() - resampler.settings = .init(bitRate: 0, sampleRate: 0, channels: 1) + resampler.settings = .init(sampleRate: 0, channels: 1) resampler.delegate = nullIOAudioResamplerDelegate resampler.appendSampleBuffer(CMAudioSampleBufferFactory.makeSinWave(16000, numSamples: 1024, channels: 1)!) XCTAssertEqual(resampler.outputFormat?.sampleRate, 16000)