Files
hearbit-ai-app/src-tauri/resources/create_hearbit_audio.swift
michael.borak a06e473e85 Release 1.1.0: Add Import Audio Files feature
- New Import tab with drag-and-drop support for audio files
- Support for 8 formats: MP3, MP4, WAV, M4A, FLAC, OGG, AAC, WMA
- File metadata display (duration, size, format)
- Editable meeting titles
- Progress tracking with visual indicators
- Smart template selection
- Auto-navigation to Transcription view
- Updated README with BlackHole requirement and Teams config
- Added get_audio_metadata Rust command
- Version bump to 1.1.0
2026-01-21 09:08:56 +01:00

192 lines
6.8 KiB
Swift

#!/usr/bin/env swift
import Foundation
import CoreAudio
// Extensions and Helpers
extension Int32 {
var fourCC: String {
let utf16 = [
UInt16((self >> 24) & 0xFF),
UInt16((self >> 16) & 0xFF),
UInt16((self >> 8) & 0xFF),
UInt16(self & 0xFF)
]
return String(utf16CodeUnits: utf16, count: 4)
}
}
// Safer Property Getter
func getPropertyData<T>(objectID: AudioObjectID, selector: AudioObjectPropertySelector, initialValue: T) -> T? {
var address = AudioObjectPropertyAddress(
mSelector: selector,
mScope: kAudioObjectPropertyScopeGlobal,
mElement: kAudioObjectPropertyElementMain
)
var size = UInt32(MemoryLayout<T>.size)
var value = initialValue
let status = AudioObjectGetPropertyData(objectID, &address, 0, nil, &size, &value)
if status == noErr {
return value
}
return nil
}
// CFString Helper
func getStringProperty(objectID: AudioObjectID, selector: AudioObjectPropertySelector) -> String? {
var address = AudioObjectPropertyAddress(
mSelector: selector,
mScope: kAudioObjectPropertyScopeGlobal,
mElement: kAudioObjectPropertyElementMain
)
// CFStringRef is just a pointer, so size of Optional<Unmanaged<CFString>> is pointer size
var size = UInt32(MemoryLayout<Unmanaged<CFString>?>.size)
var value: Unmanaged<CFString>?
let status = AudioObjectGetPropertyData(objectID, &address, 0, nil, &size, &value)
if status == noErr, let existingValue = value {
return existingValue.takeRetainedValue() as String
}
return nil
}
func findDeviceByName(_ name: String) -> AudioObjectID? {
// System Object is 1
let systemID = AudioObjectID(kAudioObjectSystemObject)
// Get all devices
var address = AudioObjectPropertyAddress(
mSelector: kAudioHardwarePropertyDevices,
mScope: kAudioObjectPropertyScopeGlobal,
mElement: kAudioObjectPropertyElementMain
)
var size: UInt32 = 0
guard AudioObjectGetPropertyDataSize(systemID, &address, 0, nil, &size) == noErr else { return nil }
let count = Int(size) / MemoryLayout<AudioObjectID>.size
var deviceIDs = [AudioObjectID](repeating: 0, count: count)
guard AudioObjectGetPropertyData(systemID, &address, 0, nil, &size, &deviceIDs) == noErr else { return nil }
for id in deviceIDs {
if let devName = getStringProperty(objectID: id, selector: kAudioDevicePropertyDeviceNameCFString) {
if devName == name {
return id
}
}
}
return nil
}
func findDeviceByUID(_ uid: String) -> AudioObjectID? {
let systemID = AudioObjectID(kAudioObjectSystemObject)
var address = AudioObjectPropertyAddress(
mSelector: kAudioHardwarePropertyDevices,
mScope: kAudioObjectPropertyScopeGlobal,
mElement: kAudioObjectPropertyElementMain
)
var size: UInt32 = 0
guard AudioObjectGetPropertyDataSize(systemID, &address, 0, nil, &size) == noErr else { return nil }
let count = Int(size) / MemoryLayout<AudioObjectID>.size
var deviceIDs = [AudioObjectID](repeating: 0, count: count)
guard AudioObjectGetPropertyData(systemID, &address, 0, nil, &size, &deviceIDs) == noErr else { return nil }
for id in deviceIDs {
if let devUID = getStringProperty(objectID: id, selector: kAudioDevicePropertyDeviceUID) {
if devUID == uid {
return id
}
}
}
return nil
}
func createAggregateDevice() {
print("Searching for devices...")
guard let blackHoleID = findDeviceByName("BlackHole 2ch") else {
print("Error: BlackHole 2ch not found. Please install it first.")
exit(1)
}
print("Found BlackHole 2ch (ID: \(blackHoleID))")
// --- PART 1: Hearbit Audio (Input: Mic + BlackHole) ---
print("\n--- Creating 'Hearbit Audio' (Input) ---")
// Default Input
var defaultInputID: AudioObjectID = 0
var size = UInt32(MemoryLayout<AudioObjectID>.size)
var address = AudioObjectPropertyAddress(
mSelector: kAudioHardwarePropertyDefaultInputDevice,
mScope: kAudioObjectPropertyScopeGlobal,
mElement: kAudioObjectPropertyElementMain
)
if AudioObjectGetPropertyData(AudioObjectID(kAudioObjectSystemObject), &address, 0, nil, &size, &defaultInputID) != noErr {
print("Error: Could not find default input.")
exit(1)
}
print("Found Default Input (ID: \(defaultInputID))")
// Check for existing "Hearbit Audio"
let inputUID = "hearbit_audio_aggregate_v1"
if let existingID = findDeviceByUID(inputUID) {
print("Found existing Hearbit Audio (ID: \(existingID)). Destroying...")
AudioHardwareDestroyAggregateDevice(existingID)
Thread.sleep(forTimeInterval: 0.5)
}
guard let bhUID = getStringProperty(objectID: blackHoleID, selector: kAudioDevicePropertyDeviceUID) else {
print("Error: Could not get BlackHole UID.")
exit(1)
}
guard let micUID = getStringProperty(objectID: defaultInputID, selector: kAudioDevicePropertyDeviceUID) else {
print("Error: Could not get Default Input UID.")
exit(1)
}
var subDevicesUIDs = [bhUID]
if micUID != bhUID {
subDevicesUIDs.append(micUID)
}
let subDevicesArray = subDevicesUIDs.map { [kAudioSubDeviceUIDKey: $0] }
let inputDesc: [String: Any] = [
kAudioAggregateDeviceNameKey: "Hearbit Audio",
kAudioAggregateDeviceUIDKey: inputUID,
kAudioAggregateDeviceIsPrivateKey: Int(0),
kAudioAggregateDeviceIsStackedKey: Int(0),
kAudioAggregateDeviceSubDeviceListKey: subDevicesArray
]
var outInputID: AudioObjectID = 0
let errIn = AudioHardwareCreateAggregateDevice(inputDesc as CFDictionary, &outInputID)
if errIn == noErr {
print("Success! Created 'Hearbit Audio' with ID: \(outInputID)")
} else {
print("Failed to create 'Hearbit Audio'. Error: \(errIn)")
}
// --- PART 2: Cleanup Unstable "Hearbit Speakers" ---
// The previous "Hearbit Speakers" device caused MS Teams to crash.
// We strictly remove it here to restore stability.
print("\n--- Cleaning up Unstable Devices ---")
let stopOutputUID = "hearbit_speakers_aggregate_v1"
if let existingOutID = findDeviceByUID(stopOutputUID) {
print("Found unstable 'Hearbit Speakers' (ID: \(existingOutID)). Removing to fix Teams crash...")
let errDist = AudioHardwareDestroyAggregateDevice(existingOutID)
if errDist == noErr {
print("Successfully removed unstable device.")
} else {
print("Warning: Failed to remove device. Error: \(errDist)")
}
} else {
print("No unstable 'Hearbit Speakers' found. System is clean.")
}
exit(0)
}
createAggregateDevice()