Skip to content

Commit

Permalink
Open source
Browse files Browse the repository at this point in the history
  • Loading branch information
tattn committed Oct 8, 2023
1 parent c0605f1 commit c6ed47b
Show file tree
Hide file tree
Showing 8 changed files with 503 additions and 2 deletions.
2 changes: 1 addition & 1 deletion app/xcode/Package.swift
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,7 @@ let package = Package(
.define("ENABLE_MOCOPI")
]),
.target(name: "VCamLocalization", resources: [.process("VCamResources")]),
.target(name: "VCamMedia", dependencies: ["VCamEntity", "VCamAppExtension"]),
.target(name: "VCamMedia", dependencies: ["VCamEntity", "VCamAppExtension", "VCamLogger"]),
.target(name: "VCamBridge", dependencies: ["VCamUIFoundation"]),
.target(name: "VCamCamera", dependencies: ["VCamData", "VCamLogger"]),
.target(name: "VCamTracking", dependencies: ["VCamData", "VCamLogger"]),
Expand Down
2 changes: 1 addition & 1 deletion app/xcode/Sources/VCamBridge/RenderTexture.swift
Original file line number Diff line number Diff line change
Expand Up @@ -35,7 +35,7 @@ public final class MainTexture {
}
}

func __bridge<T : AnyObject>(_ ptr: UnsafeRawPointer) -> T {
public func __bridge<T : AnyObject>(_ ptr: UnsafeRawPointer) -> T {
Unmanaged.fromOpaque(ptr).takeUnretainedValue()
}

Expand Down
164 changes: 164 additions & 0 deletions app/xcode/Sources/VCamMedia/AudioDevice+.swift
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,9 @@

import Foundation
import CoreAudio
import AVFAudio
import VCamEntity
import VCamLogger

extension AudioDevice {
public init(id: AudioDeviceID) {
Expand Down Expand Up @@ -149,3 +151,165 @@ public extension AudioDevice {
return buf
}
}

extension AudioDevice {
private static var cachedDevices: [AudioDevice] = [] {
didSet {
NotificationCenter.default.post(name: .deviceWasChanged, object: nil)
}
}

public static func configure() {
Self.cachedDevices = Self.loadDevices()

NotificationCenter.default.addObserver(forName: .AVCaptureDeviceWasConnected, object: nil, queue: .main) { _ in
Self.cachedDevices = Self.loadDevices()
}

NotificationCenter.default.addObserver(forName: .AVCaptureDeviceWasDisconnected, object: nil, queue: .main) { _ in
Self.cachedDevices = Self.loadDevices()
}
}

public static func devices() -> [AudioDevice] {
cachedDevices
}

private static func loadDevices() -> [AudioDevice] {
var propsize: UInt32 = 0

var address = AudioObjectPropertyAddress(
mSelector: kAudioHardwarePropertyDevices,
mScope: kAudioDevicePropertyScopeInput,
mElement: AudioObjectPropertyElement(kAudioObjectPropertyElementMain))

var result = AudioObjectGetPropertyDataSize(AudioObjectID(kAudioObjectSystemObject),
&address,
UInt32(MemoryLayout<AudioObjectPropertyAddress>.size),
nil, &propsize)

if result != 0 {
Logger.log("Error \(result) from AudioObjectGetPropertyDataSize")
return []
}

let deviceCount = Int(propsize / UInt32(MemoryLayout<AudioDeviceID>.size))

if deviceCount == 0 {
return []
}

var devids = [AudioDeviceID](repeating: 0, count: deviceCount)

result = 0
devids.withUnsafeMutableBufferPointer { bufferPointer in
if let pointer = bufferPointer.baseAddress {
result = AudioObjectGetPropertyData(AudioObjectID(kAudioObjectSystemObject),
&address,
0,
nil,
&propsize,
pointer)
}
}

if result != 0 {
Logger.log("Error \(result) from AudioObjectGetPropertyData")
return []
}

return devids.map {
AudioDevice(id: $0)
}
.filter {
let name = $0.name()
return !name.hasPrefix("CADefaultDevice") && !name.hasPrefix("vcam-audio-device")
}
.filter { $0.isMicrophone() }
}

public static func device(forUid uid: String) -> AudioDevice? {
devices().first { $0.uid == uid }
}

public func setAsDefaultDevice() {
Logger.log(name())

var outputID: AudioDeviceID = id
let propsize = UInt32(MemoryLayout<AudioDeviceID>.size)
let selector = isMicrophone() ? kAudioHardwarePropertyDefaultInputDevice : kAudioHardwarePropertyDefaultOutputDevice
var address = AudioObjectPropertyAddress(mSelector: selector, mScope: kAudioObjectPropertyScopeGlobal, mElement: kAudioObjectPropertyElementMain)
let error = AudioObjectSetPropertyData(AudioObjectID(kAudioObjectSystemObject),
&address,
0,
nil,
propsize,
&outputID)
if error != noErr {
Logger.log("defaultDevice error: \(error)")
}
}

private func isMicrophone() -> Bool {
// https://stackoverflow.com/questions/4575408/audioobjectgetpropertydata-to-get-a-list-of-input-devices
var streamConfigAddress = AudioObjectPropertyAddress(
mSelector: kAudioDevicePropertyStreamConfiguration,
mScope: kAudioDevicePropertyScopeInput,
mElement: 0)

var propertySize = UInt32(0)

var result = AudioObjectGetPropertyDataSize(id, &streamConfigAddress, 0, nil, &propertySize)
if result != 0 {
Logger.log("Error \(result) from AudioObjectGetPropertyDataSize")
return false
}

let audioBufferList = AudioBufferList.allocate(maximumBuffers: Int(propertySize))
defer {
free(audioBufferList.unsafeMutablePointer)
}
result = AudioObjectGetPropertyData(id, &streamConfigAddress, 0, nil, &propertySize, audioBufferList.unsafeMutablePointer)
if result != 0 {
Logger.log("Error \(result) from AudioObjectGetPropertyDataSize")
return false
}

var channelCount = 0
for i in 0 ..< Int(audioBufferList.unsafeMutablePointer.pointee.mNumberBuffers) {
channelCount = channelCount + Int(audioBufferList[i].mNumberChannels)
}

return channelCount > 0
}
}

extension AudioUnit {
public func getDeviceId() -> AudioDeviceID {
var outputID: AudioDeviceID = 0
var propsize = UInt32(MemoryLayout<AudioDeviceID>.size)
let error = AudioUnitGetProperty(self,
kAudioOutputUnitProperty_CurrentDevice,
kAudioUnitScope_Global,
0,
&outputID,
&propsize)
if error != noErr {
Logger.log("getDeviceID error: \(error)")
}
return outputID
}

public func set(_ device: AudioDevice) {
// https://www.hackingwithswift.com/forums/macos/how-do-you-specify-the-audio-output-device-on-a-mac-in-swift/13177
var inputDeviceID = device.id
let status = AudioUnitSetProperty(self,
kAudioOutputUnitProperty_CurrentDevice,
kAudioUnitScope_Global,
0,
&inputDeviceID,
UInt32(MemoryLayout<AudioDeviceID>.size))
print("AudioUnit.set", status)
}
}

66 changes: 66 additions & 0 deletions app/xcode/Sources/VCamUI/AudioManager.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,66 @@
//
// AudioManager.swift
//
//
// Created by Tatsuya Tanaka on 2022/03/06.
//

import AVFAudio
import VCamLogger

public final class AudioManager {
public init() {}

public var onUpdateAudioBuffer: ((AVAudioPCMBuffer, AVAudioTime, TimeInterval) -> Void) = { _, _, _ in }

public var isRunning: Bool {
audioEngine.isRunning
}

private var audioEngine = AVAudioEngine()

public func startRecording(onStart: @escaping (AVAudioFormat) -> Void) throws {
guard DeviceAuthorization.authorizationStatus(for: .mic) else {
Logger.log("requestAuthorization")
DeviceAuthorization.requestAuthorization(type: .mic) { [self] authorized in
guard authorized else { return }
DispatchQueue.main.async { [self] in
try? startRecording(onStart: onStart)
}
}
return
}

Task { @MainActor in
// After changing settings with CoreAudio, a delay is needed to prevent installTap failures
try? await Task.sleep(for: .milliseconds(500))

audioEngine = AVAudioEngine()
guard audioEngine.inputNode.inputFormat(forBus: 0).sampleRate != 0 else {
return
}

let inputNode = audioEngine.inputNode
let recordingFormat = inputNode.inputFormat(forBus: 0)

Logger.log("installTap")
inputNode.installTap(onBus: 0,
bufferSize: 1024,
format: recordingFormat) { [weak self] (buffer: AVAudioPCMBuffer, when: AVAudioTime) in
guard let self = self else { return }
// https://stackoverflow.com/questions/26115626/i-want-to-call-20-times-per-second-the-installtaponbusbuffersizeformatblock
// Matching the bufferSize prevents audio from intermittently cutting out during recording.
buffer.frameLength = 1024
self.onUpdateAudioBuffer(buffer, when, inputNode.presentationLatency)
}

try? audioEngine.start()
onStart(recordingFormat)
}
}

public func stopRecording() {
Logger.log("")
audioEngine.stop()
}
}
136 changes: 136 additions & 0 deletions app/xcode/Sources/VCamUI/AvatarAudioManager.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,136 @@
//
// AvatarAudioManager.swift
//
//
// Created by Tatsuya Tanaka on 2022/03/06.
//

import Foundation
import VCamData
import VCamMedia
import VCamEntity
import VCamBridge
import VCamLogger
import AVFAudio

public final class AvatarAudioManager: NSObject {
public static let shared = AvatarAudioManager()

public var videoRecorderRenderAudioFrame: (AVAudioPCMBuffer, AVAudioTime, TimeInterval, AudioDevice?) -> Void = { _, _, _, _ in }

private let audioManager = AudioManager()
private let audioExpressionEstimator = AudioExpressionEstimator()
private var usage = Usage()
private var isConfiguring = false

public var currentInputDevice: AudioDevice? {
guard let uid = UserDefaults.standard.value(for: .audioDeviceUid) else { return .defaultDevice() }
return AudioDevice.device(forUid: uid)
}

override init() {
super.init()

NotificationCenter.default.addObserver(
self, selector: #selector(onConfigurationChange), name: .AVAudioEngineConfigurationChange,
object: nil)

if let device = currentInputDevice {
setAudioDevice(device)
}
}

public func startIfNeeded() {
guard !UniBridge.shared.lipSyncWebCam.wrappedValue else { return }
AvatarAudioManager.shared.start(usage: .lipSync)
}

public func start(usage: Usage, isSystemSoundRecording: Bool = false) {
reconfigureIfNeeded()
do {
Logger.log("\(isConfiguring), \(audioManager.isRunning)")
if !isConfiguring, !audioManager.isRunning {
// There's a delay in AudioManager::startRecording, so don't allow consecutive calls (it causes a crash in installTap)
isConfiguring = true

if isSystemSoundRecording {
AudioDevice.device(forUid: "vcam-audio-device-001")?.setAsDefaultDevice()
} else {
currentInputDevice?.setAsDefaultDevice()
}
try audioManager.startRecording { [self] inputFormat in
audioExpressionEstimator.configure(format: inputFormat)
isConfiguring = false
}
}
self.usage.insert(usage)
} catch {
isConfiguring = false
Logger.error(error)
}
}

public func stop(usage: Usage) {
self.usage.remove(usage)
guard self.usage.isEmpty else { return }
audioManager.stopRecording()
audioExpressionEstimator.reset()
}

private func reconfigureIfNeeded() {
setEmotionEnabled(UserDefaults.standard.value(for: .useEmotion))
audioExpressionEstimator.onAudioLevelUpdate = { level in
Task { @MainActor in
UniBridge.shared.micAudioLevel.wrappedValue = CGFloat(level)
}
}
audioManager.onUpdateAudioBuffer = { buffer, time, latency in
if Self.shared.usage.contains(.lipSync) { // Ensure no malfunctions during recording
Self.shared.audioExpressionEstimator.analyze(buffer: buffer, time: time)
}
Self.shared.videoRecorderRenderAudioFrame(buffer, time, latency, Self.shared.currentInputDevice)
}
}

public func setEmotionEnabled(_ isEnabled: Bool) {
audioExpressionEstimator.onUpdate = isEnabled ? { emotion in
Task { @MainActor in
UniBridge.shared.facialExpression(emotion.rawValue)
}
} : nil
}

public func setAudioDevice(_ audioDevice: AudioDevice) {
Logger.log(audioDevice.name())
UserDefaults.standard.set(audioDevice.uid, for: .audioDeviceUid)

if audioManager.isRunning {
let usage = self.usage
stop(usage: usage)
Task { @MainActor in
try? await Task.sleep(for: .milliseconds(500))
start(usage: usage)
}
}
}

@objc private func onConfigurationChange(notification: Notification) {
// guard audioManager.isRunning else { return }
// stop()
// DispatchQueue.main.asyncAfter(deadline: .now() + 0.3) { [self] in
// start()
// }
}

public struct Usage: OptionSet {
public init(rawValue: UInt) {
self.rawValue = rawValue
}

public var rawValue: UInt = 0

public static let lipSync = Usage(rawValue: 0x1)
public static let record = Usage(rawValue: 0x2)
public static let all: Usage = [lipSync, record]
}
}
Loading

0 comments on commit c6ed47b

Please sign in to comment.