#if os(macOS) import CoreAudio import XCTest @testable import CallsKit /// The Mac's input channel choice (spec ยง15.20), apart from any device: /// the remembered choice, the channel map it becomes, the channel count /// read from a stream configuration, and the routes' picker state. @MainActor final class InputChannelTests: XCTestCase { private var store: UserDefaults! private var storeName = "" override func setUp() { super.setUp() storeName = "calls-channel-tests-\(UUID().uuidString)" store = UserDefaults(suiteName: storeName) } override func tearDown() { store.removePersistentDomain(forName: storeName) super.tearDown() } func testInputOneIsWhereEveryDeviceStarts() { XCTAssertEqual(InputChannelPreference.load(uid: "scarlett", channels: 18, store), 0) } func testTheChoiceIsRememberedPerDevice() { InputChannelPreference.save(1, uid: "scarlett", store) XCTAssertEqual(InputChannelPreference.load(uid: "scarlett", channels: 18, store), 1) XCTAssertEqual(InputChannelPreference.load(uid: "motu", channels: 8, store), 0) InputChannelPreference.save(0, uid: "scarlett", store) XCTAssertEqual(InputChannelPreference.load(uid: "scarlett", channels: 18, store), 0) } func testAChoiceTheDeviceNoLongerHasFallsBackToInputOne() { InputChannelPreference.save(9, uid: "scarlett", store) XCTAssertEqual(InputChannelPreference.load(uid: "scarlett", channels: 18, store), 9) XCTAssertEqual(InputChannelPreference.load(uid: "scarlett", channels: 2, store), 0) } func testTheMapCarriesTheChosenInputOnEveryClientChannel() { // A mono client (the engine's mono connection): one entry. XCTAssertEqual(InputChannelMap.map(channel: 1, clientChannels: 1, mappedBefore: false), [1]) // The hardware's width: every channel is input 2, so any mixdown or // "first channel" of them is input 2. XCTAssertEqual( InputChannelMap.map(channel: 1, clientChannels: 4, mappedBefore: false), [1, 1, 1, 1]) // Input 1 on a unit never mapped: left alone, today's behaviour. XCTAssertNil(InputChannelMap.map(channel: 0, clientChannels: 18, mappedBefore: false)) // Back to input 1 after a choice: straight through. XCTAssertEqual( InputChannelMap.map(channel: 0, clientChannels: 3, mappedBefore: true), [0, 1, 2]) // A unit that has not said its width still gets a map. XCTAssertEqual(InputChannelMap.map(channel: 2, clientChannels: 0, mappedBefore: false), [2]) } /// 0.3.8: the map is strictly opt-in. Only an input other than the /// first, chosen on a microphone chosen by name that has more than /// one input; anything else leaves the unit as the module wired it, /// which is what 0.3.3 did for every microphone. func testTheMapIsOnlyForAnInputChosenOnAChosenInterface() { func channel(_ saved: Int, _ device: String, explicit: String?, inputs: Int) -> Int { InputChannelMap.channel(saved: saved, device: device, explicit: explicit, inputs: inputs) } XCTAssertEqual(channel(1, "scarlett", explicit: "scarlett", inputs: 18), 1, "chosen: input 2") XCTAssertEqual(channel(0, "scarlett", explicit: "scarlett", inputs: 18), 0) XCTAssertEqual(channel(1, "scarlett", explicit: nil, inputs: 18), 0, "on the system default: never mapped") XCTAssertEqual(channel(1, "scarlett", explicit: "usb", inputs: 18), 0, "another device chosen") XCTAssertEqual(channel(1, "airpods", explicit: "airpods", inputs: 1), 0, "a one-input microphone") XCTAssertEqual(channel(5, "solo", explicit: "solo", inputs: 2), 0, "a remembered input it no longer has") XCTAssertEqual(channel(1, "solo", explicit: "solo", inputs: 2), 1) XCTAssertNil( InputChannelMap.map(channel: channel(1, "scarlett", explicit: nil, inputs: 18), clientChannels: 18, mappedBefore: false), "so the unit is left alone") } func testChannelsAreCountedAcrossAStreamConfiguration() { let list = AudioBufferList.allocate(maximumBuffers: 3) defer { free(list.unsafeMutablePointer) } list[0] = AudioBuffer(mNumberChannels: 8, mDataByteSize: 0, mData: nil) list[1] = AudioBuffer(mNumberChannels: 8, mDataByteSize: 0, mData: nil) list[2] = AudioBuffer(mNumberChannels: 2, mDataByteSize: 0, mData: nil) XCTAssertEqual(InputChannelMap.count(list), 18) } func testChannelsAreLabelledByNumberAndName() { let device = InputDeviceChannels( uid: "scarlett", name: "Scarlett 18i8 USB", count: 3, names: ["Analogue 1", "", "3"]) XCTAssertEqual(device.label(0), "Input 1 (Analogue 1)") XCTAssertEqual(device.label(1), "Input 2") XCTAssertEqual(device.label(2), "Input 3", "a name that is only the number adds nothing") XCTAssertEqual(device.describe(channel: 0), "Scarlett 18i8 USB, input 1") XCTAssertEqual( InputDeviceChannels(uid: "mbp", name: "MacBook Pro Microphone", count: 1).describe(channel: 0), "MacBook Pro Microphone") } private func routes(channels: InputDeviceChannels?, rebuilt: @escaping () -> Void) -> AudioRoutes { let routes = AudioRoutes(defaults: store) routes.attach( AudioEngineControl( inventory: { AudioInventory( devices: [ SystemAudioDevice(uid: "mbp", name: "MacBook Pro Microphone", inputChannels: 1, outputChannels: 0), SystemAudioDevice(uid: "scarlett", name: "Scarlett 18i8 USB", inputChannels: 4, outputChannels: 0), ], defaultInput: "mbp", defaultOutput: nil) }, channels: { id in id == "scarlett" ? channels : nil }, rebuildInput: rebuilt)) return routes } func testThePickerAppearsForAMultiChannelMicrophoneOnly() throws { var rebuilds = 0 let scarlett = InputDeviceChannels(uid: "scarlett-uid", name: "Scarlett 18i8 USB", count: 4) let routes = routes(channels: scarlett) { rebuilds += 1 } XCTAssertEqual(routes.inputChannels, [], "on the default, which is not the interface here") try routes.use(input: "scarlett") XCTAssertEqual(routes.inputChannels, ["Input 1", "Input 2", "Input 3", "Input 4"]) XCTAssertEqual(routes.inputChannel, 0) XCTAssertEqual(routes.inputSource, "Scarlett 18i8 USB, input 1") routes.use(inputChannel: 1) XCTAssertEqual(routes.inputChannel, 1) XCTAssertEqual(routes.inputSource, "Scarlett 18i8 USB, input 2") XCTAssertEqual(rebuilds, 1, "the module's input is rebuilt so the map applies now") XCTAssertEqual(InputChannelPreference.load(uid: "scarlett-uid", channels: 4, store), 1) routes.use(inputChannel: 1) routes.use(inputChannel: 7) XCTAssertEqual(rebuilds, 1, "the same choice, or one the device lacks, does nothing") // Remembered: a new launch on the same defaults comes back on input 2. let again = self.routes(channels: scarlett) {} XCTAssertEqual(again.inputChannel, 1) } /// The interface as the system default, with an input remembered for /// it: no picker, and input 1 as the module wires it. func testOnTheSystemDefaultThereIsNoPickerAndNoMap() throws { InputChannelPreference.save(1, uid: "scarlett-uid", store) let scarlett = InputDeviceChannels(uid: "scarlett-uid", name: "Scarlett 18i8 USB", count: 4) let routes = AudioRoutes(defaults: store) routes.attach( AudioEngineControl( inventory: { AudioInventory( devices: [SystemAudioDevice(uid: "scarlett", name: "Scarlett 18i8 USB", inputChannels: 4, outputChannels: 0)], defaultInput: "scarlett", defaultOutput: nil) }, channels: { _ in scarlett })) XCTAssertEqual(routes.currentInput, AudioRouteRules.defaultID) XCTAssertEqual(routes.inputChannels, []) XCTAssertEqual(routes.inputChannel, 0) XCTAssertEqual(routes.inputSource, "Scarlett 18i8 USB, input 1") } func testAnOrdinaryMicrophoneHasNoPicker() throws { let routes = routes(channels: InputDeviceChannels(uid: "usb", name: "USB Mic", count: 1)) {} try routes.use(input: "scarlett") XCTAssertEqual(routes.inputChannels, []) XCTAssertEqual(routes.inputSource, "USB Mic") } } #endif