a22c238dc4
A complete audio-reactive visualizer for psytrance music featuring: Audio Analysis (DSPEngine): - FFT spectrum analysis via Accelerate/vDSP - 64-band Mel spectrogram - Sub-bass energy extraction (<100Hz) - Automatic sidechain pump detection - Harmonic-to-Noise ratio (HNR) calculation - Peak/transient detection 8 Visualization Modes (Metal Shaders): 1. FFT Classic - Frequency spectrum bars with glow 2. Mel Spectrogram - Waterfall display 3. Sub-Bass - Pulsating rings 4. Sidechain Pump - Breathing zoom effect 5. Harmonic/Noise - Geometric vs chaotic particles 6. Mandelbrot - Audio-reactive fractal zoom 7. Tunnel Warp - Infinite tunnel with distortion 8. DMT Geometry - Sacred geometry patterns Features: - Selectable audio input device (BlackHole support) - Configurable buffer size (512/1024) - Reactivity slider for visual intensity - Auto-hiding control panel - Fullscreen support with keyboard shortcuts (1-8, F, ESC) - Persistent settings via UserDefaults - Psytrance-inspired neon/UV color palette
358 lines
10 KiB
Swift
358 lines
10 KiB
Swift
//
|
|
// AudioInputManager.swift
|
|
// PsytranceVisualizer
|
|
//
|
|
// Manages audio input devices and captures audio buffers
|
|
//
|
|
|
|
import AVFoundation
|
|
import CoreAudio
|
|
import Combine
|
|
|
|
/// Represents an audio input device
|
|
struct AudioDevice: Identifiable, Hashable {
|
|
let id: AudioDeviceID
|
|
let uid: String
|
|
let name: String
|
|
let manufacturer: String
|
|
let isInput: Bool
|
|
|
|
func hash(into hasher: inout Hasher) {
|
|
hasher.combine(uid)
|
|
}
|
|
|
|
static func == (lhs: AudioDevice, rhs: AudioDevice) -> Bool {
|
|
lhs.uid == rhs.uid
|
|
}
|
|
}
|
|
|
|
/// Manages audio input capture using AVAudioEngine
|
|
final class AudioInputManager: ObservableObject {
|
|
// MARK: - Published Properties
|
|
|
|
@Published private(set) var availableDevices: [AudioDevice] = []
|
|
@Published private(set) var selectedDevice: AudioDevice?
|
|
@Published private(set) var isRunning = false
|
|
@Published private(set) var currentBufferSize: Int = 1024
|
|
|
|
// MARK: - Audio Properties
|
|
|
|
private var audioEngine: AVAudioEngine?
|
|
private var inputNode: AVAudioInputNode?
|
|
private let sampleRate: Double = 44100.0
|
|
|
|
// MARK: - Callbacks
|
|
|
|
var onAudioBuffer: ((AVAudioPCMBuffer) -> Void)?
|
|
|
|
// MARK: - Private Properties
|
|
|
|
private var deviceListenerBlock: AudioObjectPropertyListenerBlock?
|
|
private let processingQueue = DispatchQueue(label: "com.psytrance.audio", qos: .userInteractive)
|
|
|
|
// MARK: - Initialization
|
|
|
|
init() {
|
|
refreshDeviceList()
|
|
setupDeviceChangeListener()
|
|
}
|
|
|
|
deinit {
|
|
stop()
|
|
removeDeviceChangeListener()
|
|
}
|
|
|
|
// MARK: - Public Methods
|
|
|
|
/// Returns list of available audio input devices
|
|
func getAvailableInputDevices() -> [AudioDevice] {
|
|
return availableDevices
|
|
}
|
|
|
|
/// Refreshes the list of available audio input devices
|
|
func refreshDeviceList() {
|
|
var propertyAddress = AudioObjectPropertyAddress(
|
|
mSelector: kAudioHardwarePropertyDevices,
|
|
mScope: kAudioObjectPropertyScopeGlobal,
|
|
mElement: kAudioObjectPropertyElementMain
|
|
)
|
|
|
|
var dataSize: UInt32 = 0
|
|
var status = AudioObjectGetPropertyDataSize(
|
|
AudioObjectID(kAudioObjectSystemObject),
|
|
&propertyAddress,
|
|
0,
|
|
nil,
|
|
&dataSize
|
|
)
|
|
|
|
guard status == noErr else {
|
|
print("[AudioInputManager] Failed to get device list size: \(status)")
|
|
return
|
|
}
|
|
|
|
let deviceCount = Int(dataSize) / MemoryLayout<AudioDeviceID>.size
|
|
var deviceIDs = [AudioDeviceID](repeating: 0, count: deviceCount)
|
|
|
|
status = AudioObjectGetPropertyData(
|
|
AudioObjectID(kAudioObjectSystemObject),
|
|
&propertyAddress,
|
|
0,
|
|
nil,
|
|
&dataSize,
|
|
&deviceIDs
|
|
)
|
|
|
|
guard status == noErr else {
|
|
print("[AudioInputManager] Failed to get device list: \(status)")
|
|
return
|
|
}
|
|
|
|
var devices: [AudioDevice] = []
|
|
|
|
for deviceID in deviceIDs {
|
|
if let device = getDeviceInfo(deviceID: deviceID), device.isInput {
|
|
devices.append(device)
|
|
}
|
|
}
|
|
|
|
DispatchQueue.main.async {
|
|
self.availableDevices = devices
|
|
print("[AudioInputManager] Found \(devices.count) input devices")
|
|
}
|
|
}
|
|
|
|
/// Selects an audio input device by UID
|
|
func selectDevice(uid: String) {
|
|
guard let device = availableDevices.first(where: { $0.uid == uid }) else {
|
|
print("[AudioInputManager] Device not found: \(uid)")
|
|
return
|
|
}
|
|
|
|
let wasRunning = isRunning
|
|
if wasRunning {
|
|
stop()
|
|
}
|
|
|
|
selectedDevice = device
|
|
setSystemInputDevice(deviceID: device.id)
|
|
|
|
if wasRunning {
|
|
start()
|
|
}
|
|
|
|
print("[AudioInputManager] Selected device: \(device.name)")
|
|
}
|
|
|
|
/// Sets the buffer size (512 or 1024)
|
|
func setBufferSize(_ size: Int) {
|
|
guard [512, 1024].contains(size) else {
|
|
print("[AudioInputManager] Invalid buffer size: \(size)")
|
|
return
|
|
}
|
|
|
|
let wasRunning = isRunning
|
|
if wasRunning {
|
|
stop()
|
|
}
|
|
|
|
currentBufferSize = size
|
|
|
|
if wasRunning {
|
|
start()
|
|
}
|
|
|
|
print("[AudioInputManager] Buffer size set to: \(size)")
|
|
}
|
|
|
|
/// Starts audio capture
|
|
func start() {
|
|
guard !isRunning else { return }
|
|
|
|
do {
|
|
// Create new audio engine
|
|
audioEngine = AVAudioEngine()
|
|
guard let engine = audioEngine else { return }
|
|
|
|
inputNode = engine.inputNode
|
|
|
|
guard let inputNode = inputNode else {
|
|
print("[AudioInputManager] No input node available")
|
|
return
|
|
}
|
|
|
|
// Get the input format
|
|
let inputFormat = inputNode.outputFormat(forBus: 0)
|
|
|
|
print("[AudioInputManager] Input format: \(inputFormat)")
|
|
|
|
// Install tap on input node
|
|
let bufferSize = AVAudioFrameCount(currentBufferSize)
|
|
|
|
inputNode.installTap(onBus: 0, bufferSize: bufferSize, format: inputFormat) { [weak self] buffer, _ in
|
|
self?.processingQueue.async {
|
|
self?.onAudioBuffer?(buffer)
|
|
}
|
|
}
|
|
|
|
// Prepare and start the engine
|
|
engine.prepare()
|
|
try engine.start()
|
|
|
|
DispatchQueue.main.async {
|
|
self.isRunning = true
|
|
}
|
|
|
|
print("[AudioInputManager] Audio capture started")
|
|
|
|
} catch {
|
|
print("[AudioInputManager] Failed to start audio capture: \(error)")
|
|
}
|
|
}
|
|
|
|
/// Stops audio capture
|
|
func stop() {
|
|
guard isRunning else { return }
|
|
|
|
inputNode?.removeTap(onBus: 0)
|
|
audioEngine?.stop()
|
|
audioEngine = nil
|
|
inputNode = nil
|
|
|
|
DispatchQueue.main.async {
|
|
self.isRunning = false
|
|
}
|
|
|
|
print("[AudioInputManager] Audio capture stopped")
|
|
}
|
|
|
|
// MARK: - Private Methods
|
|
|
|
/// Gets device info for a specific device ID
|
|
private func getDeviceInfo(deviceID: AudioDeviceID) -> AudioDevice? {
|
|
// Check if device has input channels
|
|
var propertyAddress = AudioObjectPropertyAddress(
|
|
mSelector: kAudioDevicePropertyStreamConfiguration,
|
|
mScope: kAudioDevicePropertyScopeInput,
|
|
mElement: kAudioObjectPropertyElementMain
|
|
)
|
|
|
|
var dataSize: UInt32 = 0
|
|
var status = AudioObjectGetPropertyDataSize(deviceID, &propertyAddress, 0, nil, &dataSize)
|
|
|
|
guard status == noErr, dataSize > 0 else { return nil }
|
|
|
|
let bufferListPointer = UnsafeMutablePointer<AudioBufferList>.allocate(capacity: Int(dataSize))
|
|
defer { bufferListPointer.deallocate() }
|
|
|
|
status = AudioObjectGetPropertyData(deviceID, &propertyAddress, 0, nil, &dataSize, bufferListPointer)
|
|
|
|
guard status == noErr else { return nil }
|
|
|
|
let bufferList = UnsafeMutableAudioBufferListPointer(bufferListPointer)
|
|
var inputChannelCount: UInt32 = 0
|
|
for buffer in bufferList {
|
|
inputChannelCount += buffer.mNumberChannels
|
|
}
|
|
|
|
guard inputChannelCount > 0 else { return nil }
|
|
|
|
// Get device UID
|
|
var uid: CFString = "" as CFString
|
|
var uidSize = UInt32(MemoryLayout<CFString>.size)
|
|
propertyAddress.mSelector = kAudioDevicePropertyDeviceUID
|
|
propertyAddress.mScope = kAudioObjectPropertyScopeGlobal
|
|
|
|
status = AudioObjectGetPropertyData(deviceID, &propertyAddress, 0, nil, &uidSize, &uid)
|
|
guard status == noErr else { return nil }
|
|
|
|
// Get device name
|
|
var name: CFString = "" as CFString
|
|
var nameSize = UInt32(MemoryLayout<CFString>.size)
|
|
propertyAddress.mSelector = kAudioDevicePropertyDeviceNameCFString
|
|
|
|
status = AudioObjectGetPropertyData(deviceID, &propertyAddress, 0, nil, &nameSize, &name)
|
|
guard status == noErr else { return nil }
|
|
|
|
// Get manufacturer
|
|
var manufacturer: CFString = "" as CFString
|
|
var manufacturerSize = UInt32(MemoryLayout<CFString>.size)
|
|
propertyAddress.mSelector = kAudioDevicePropertyDeviceManufacturerCFString
|
|
|
|
AudioObjectGetPropertyData(deviceID, &propertyAddress, 0, nil, &manufacturerSize, &manufacturer)
|
|
|
|
return AudioDevice(
|
|
id: deviceID,
|
|
uid: uid as String,
|
|
name: name as String,
|
|
manufacturer: manufacturer as String,
|
|
isInput: true
|
|
)
|
|
}
|
|
|
|
/// Sets the system default input device
|
|
private func setSystemInputDevice(deviceID: AudioDeviceID) {
|
|
var deviceIDCopy = deviceID
|
|
var propertyAddress = AudioObjectPropertyAddress(
|
|
mSelector: kAudioHardwarePropertyDefaultInputDevice,
|
|
mScope: kAudioObjectPropertyScopeGlobal,
|
|
mElement: kAudioObjectPropertyElementMain
|
|
)
|
|
|
|
let status = AudioObjectSetPropertyData(
|
|
AudioObjectID(kAudioObjectSystemObject),
|
|
&propertyAddress,
|
|
0,
|
|
nil,
|
|
UInt32(MemoryLayout<AudioDeviceID>.size),
|
|
&deviceIDCopy
|
|
)
|
|
|
|
if status != noErr {
|
|
print("[AudioInputManager] Failed to set input device: \(status)")
|
|
}
|
|
}
|
|
|
|
/// Sets up listener for device changes
|
|
private func setupDeviceChangeListener() {
|
|
var propertyAddress = AudioObjectPropertyAddress(
|
|
mSelector: kAudioHardwarePropertyDevices,
|
|
mScope: kAudioObjectPropertyScopeGlobal,
|
|
mElement: kAudioObjectPropertyElementMain
|
|
)
|
|
|
|
deviceListenerBlock = { [weak self] _, _ in
|
|
DispatchQueue.main.async {
|
|
self?.refreshDeviceList()
|
|
}
|
|
}
|
|
|
|
if let block = deviceListenerBlock {
|
|
AudioObjectAddPropertyListenerBlock(
|
|
AudioObjectID(kAudioObjectSystemObject),
|
|
&propertyAddress,
|
|
DispatchQueue.main,
|
|
block
|
|
)
|
|
}
|
|
}
|
|
|
|
/// Removes device change listener
|
|
private func removeDeviceChangeListener() {
|
|
guard let block = deviceListenerBlock else { return }
|
|
|
|
var propertyAddress = AudioObjectPropertyAddress(
|
|
mSelector: kAudioHardwarePropertyDevices,
|
|
mScope: kAudioObjectPropertyScopeGlobal,
|
|
mElement: kAudioObjectPropertyElementMain
|
|
)
|
|
|
|
AudioObjectRemovePropertyListenerBlock(
|
|
AudioObjectID(kAudioObjectSystemObject),
|
|
&propertyAddress,
|
|
DispatchQueue.main,
|
|
block
|
|
)
|
|
}
|
|
}
|