     1→import AVFoundation
     2→import Combine
     3→
     4→@MainActor
     5→class AudioMixerService: ObservableObject {
     6→    // MARK: - Published State
     7→    @Published var deckA = DeckState(id: "A")
     8→    @Published var deckB = DeckState(id: "B")
     9→    @Published var crossfaderPosition: Float = 0.5  // 0.0 = Deck A, 1.0 = Deck B
    10→    @Published var masterVolume: Float = 1.0
    11→    @Published var isEngineRunning = false
    12→
    13→    // Recording State
    14→    @Published var isRecording = false
    15→    @Published var recordingDuration: TimeInterval = 0
    16→    @Published var recordingURL: URL?
    17→
    18→    // Level Meters (0.0 - 1.0)
    19→    @Published var levelA: (left: Float, right: Float) = (0, 0)
    20→    @Published var levelB: (left: Float, right: Float) = (0, 0)
    21→    @Published var levelMaster: (left: Float, right: Float) = (0, 0)
    22→    @Published var peakA: (left: Float, right: Float) = (0, 0)
    23→    @Published var peakB: (left: Float, right: Float) = (0, 0)
    24→    @Published var peakMaster: (left: Float, right: Float) = (0, 0)
    25→
    26→    // MARK: - Audio Engine
    27→    private let audioEngine = AVAudioEngine()
    28→
    29→    // Deck A nodes
    30→    private let playerNodeA = AVAudioPlayerNode()
    31→    private let timePitchA = AVAudioUnitTimePitch()
    32→    private let eqNodeA = AVAudioUnitEQ(numberOfBands: 3)
    33→    private let filterNodeA = AVAudioUnitEQ(numberOfBands: 1)
    34→    private let reverbNodeA = AVAudioUnitReverb()
    35→    private let delayNodeA = AVAudioUnitDelay()
    36→    private let mixerNodeA = AVAudioMixerNode()
    37→
    38→    // Deck B nodes
    39→    private let playerNodeB = AVAudioPlayerNode()
    40→    private let timePitchB = AVAudioUnitTimePitch()
    41→    private let eqNodeB = AVAudioUnitEQ(numberOfBands: 3)
    42→    private let filterNodeB = AVAudioUnitEQ(numberOfBands: 1)
    43→    private let reverbNodeB = AVAudioUnitReverb()
    44→    private let delayNodeB = AVAudioUnitDelay()
    45→    private let mixerNodeB = AVAudioMixerNode()
    46→
    47→    // Audio files
    48→    private var audioFileA: AVAudioFile?
    49→    private var audioFileB: AVAudioFile?
    50→
    51→    // Tracking
    52→    private var positionTimer: Timer?
    53→    private var cancellables = Set<AnyCancellable>()
    54→
    55→    // Recording
    56→    private var recordingFile: AVAudioFile?
    57→    private var recordingTimer: Timer?
    58→    private var recordingStartTime: Date?
    59→
    60→    // Level Metering
    61→    private var meteringTimer: Timer?
    62→    private var peakHoldTimerA: Timer?
    63→    private var peakHoldTimerB: Timer?
    64→    private var peakHoldTimerMaster: Timer?
    65→    private let peakDecayRate: Float = 0.05
    66→    private let peakHoldTime: TimeInterval = 1.5
    67→
    68→    // Tap tracking to prevent double-removal (nonisolated for deinit access)
    69→    private nonisolated(unsafe) var isMeteringTapInstalledA = false
    70→    private nonisolated(unsafe) var isMeteringTapInstalledB = false
    71→    private nonisolated(unsafe) var isMeteringTapInstalledMaster = false
    72→    private nonisolated(unsafe) var isRecordingTapInstalled = false
    73→
    74→    // BPM Service
    75→    private let bpmService = BPMDetectionService()
    76→
    77→    // Key Detection Service
    78→    private let keyService = KeyDetectionService()
    79→
    80→    // Waveform Analysis Service
    81→    private let waveformService = WaveformAnalysisService()
    82→
    83→    // MARK: - Initialization
    84→
    85→    init() {
    86→        setupAudioEngine()
    87→        setupCrossfaderBinding()
    88→    }
    89→
    90→    deinit {
    91→        // Stop all timers
    92→        positionTimer?.invalidate()
    93→        meteringTimer?.invalidate()
    94→        recordingTimer?.invalidate()
    95→        nudgeTimerA?.invalidate()
    96→        nudgeTimerB?.invalidate()
    97→        peakHoldTimerA?.invalidate()
    98→        peakHoldTimerB?.invalidate()
    99→        peakHoldTimerMaster?.invalidate()
   100→
   101→        // Remove all taps safely
   102→        if isMeteringTapInstalledA {
   103→            mixerNodeA.removeTap(onBus: 0)
   104→        }
   105→        if isMeteringTapInstalledB {
   106→            mixerNodeB.removeTap(onBus: 0)
   107→        }
   108→        if isMeteringTapInstalledMaster || isRecordingTapInstalled {
   109→            audioEngine.mainMixerNode.removeTap(onBus: 0)
   110→        }
   111→
   112→        // Stop engine
   113→        audioEngine.stop()
   114→    }
   115→
   116→    private func setupAudioEngine() {
   117→        // Attach all nodes - Deck A
   118→        audioEngine.attach(playerNodeA)
   119→        audioEngine.attach(timePitchA)
   120→        audioEngine.attach(eqNodeA)
   121→        audioEngine.attach(filterNodeA)
   122→        audioEngine.attach(reverbNodeA)
   123→        audioEngine.attach(delayNodeA)
   124→        audioEngine.attach(mixerNodeA)
   125→
   126→        // Attach all nodes - Deck B
   127→        audioEngine.attach(playerNodeB)
   128→        audioEngine.attach(timePitchB)
   129→        audioEngine.attach(eqNodeB)
   130→        audioEngine.attach(filterNodeB)
   131→        audioEngine.attach(reverbNodeB)
   132→        audioEngine.attach(delayNodeB)
   133→        audioEngine.attach(mixerNodeB)
   134→
   135→        // Configure EQ bands for both decks
   136→        configureEQBands(eqNodeA)
   137→        configureEQBands(eqNodeB)
   138→        configureFilterBand(filterNodeA)
   139→        configureFilterBand(filterNodeB)
   140→
   141→        // Configure effects
   142→        configureReverb(reverbNodeA)
   143→        configureReverb(reverbNodeB)
   144→        configureDelay(delayNodeA)
   145→        configureDelay(delayNodeB)
   146→
   147→        // Connect Deck A chain: Player -> TimePitch -> EQ -> Filter -> Reverb -> Delay -> Mixer -> MainMixer
   148→        audioEngine.connect(playerNodeA, to: timePitchA, format: nil)
   149→        audioEngine.connect(timePitchA, to: eqNodeA, format: nil)
   150→        audioEngine.connect(eqNodeA, to: filterNodeA, format: nil)
   151→        audioEngine.connect(filterNodeA, to: reverbNodeA, format: nil)
   152→        audioEngine.connect(reverbNodeA, to: delayNodeA, format: nil)
   153→        audioEngine.connect(delayNodeA, to: mixerNodeA, format: nil)
   154→        audioEngine.connect(mixerNodeA, to: audioEngine.mainMixerNode, format: nil)
   155→
   156→        // Connect Deck B chain: Player -> TimePitch -> EQ -> Filter -> Reverb -> Delay -> Mixer -> MainMixer
   157→        audioEngine.connect(playerNodeB, to: timePitchB, format: nil)
   158→        audioEngine.connect(timePitchB, to: eqNodeB, format: nil)
   159→        audioEngine.connect(eqNodeB, to: filterNodeB, format: nil)
   160→        audioEngine.connect(filterNodeB, to: reverbNodeB, format: nil)
   161→        audioEngine.connect(reverbNodeB, to: delayNodeB, format: nil)
   162→        audioEngine.connect(delayNodeB, to: mixerNodeB, format: nil)
   163→        audioEngine.connect(mixerNodeB, to: audioEngine.mainMixerNode, format: nil)
   164→
   165→        // Prepare engine
   166→        audioEngine.prepare()
   167→    }
   168→
   169→    private func configureEQBands(_ eq: AVAudioUnitEQ) {
   170→        // Band 0: Low (80 Hz)
   171→        let lowBand = eq.bands[0]
   172→        lowBand.filterType = .parametric
   173→        lowBand.frequency = 80
   174→        lowBand.bandwidth = 0.8
   175→        lowBand.gain = 0
   176→        lowBand.bypass = false
   177→
   178→        // Band 1: Mid (1000 Hz)
   179→        let midBand = eq.bands[1]
   180→        midBand.filterType = .parametric
   181→        midBand.frequency = 1000
   182→        midBand.bandwidth = 1.0
   183→        midBand.gain = 0
   184→        midBand.bypass = false
   185→
   186→        // Band 2: High (12000 Hz)
   187→        let highBand = eq.bands[2]
   188→        highBand.filterType = .parametric
   189→        highBand.frequency = 12000
   190→        highBand.bandwidth = 0.8
   191→        highBand.gain = 0
   192→        highBand.bypass = false
   193→    }
   194→
   195→    private func configureFilterBand(_ filter: AVAudioUnitEQ) {
   196→        // Single band for HPF/LPF sweep
   197→        let band = filter.bands[0]
   198→        band.filterType = .lowPass
   199→        band.frequency = 20000  // Start with full range (no filtering)
   200→        band.bandwidth = 0.5
   201→        band.gain = 0
   202→        band.bypass = true  // Start bypassed
   203→    }
   204→
   205→    private func configureReverb(_ reverb: AVAudioUnitReverb) {
   206→        reverb.loadFactoryPreset(.mediumHall)
   207→        reverb.wetDryMix = 0  // Start with no reverb
   208→    }
   209→
   210→    private func configureDelay(_ delay: AVAudioUnitDelay) {
   211→        delay.delayTime = 0.25  // 250ms default
   212→        delay.feedback = 50     // 50% feedback
   213→        delay.lowPassCutoff = 15000
   214→        delay.wetDryMix = 0     // Start with no delay
   215→    }
   216→
   217→    private func setupCrossfaderBinding() {
   218→        $crossfaderPosition
   219→            .sink { [weak self] position in
   220→                self?.applyCrossfade(position: position)
   221→            }
   222→            .store(in: &cancellables)
   223→    }
   224→
   225→    // MARK: - Engine Control
   226→
   227→    func startEngine() throws {
   228→        guard !audioEngine.isRunning else { return }
   229→        try audioEngine.start()
   230→        isEngineRunning = true
   231→        startPositionUpdates()
   232→    }
   233→
   234→    func stopEngine() {
   235→        audioEngine.stop()
   236→        isEngineRunning = false
   237→        stopPositionUpdates()
   238→    }
   239→
   240→    // MARK: - Load Audio
   241→
   242→    func loadAudio(_ audio: DownloadedAudio, toDeck deck: String) async throws {
   243→        guard let url = FileStorageService.shared.getAudioURL(for: audio) else {
   244→            throw MixerError.fileNotFound
   245→        }
   246→
   247→        let audioFile = try AVAudioFile(forReading: url)
   248→        let format = audioFile.processingFormat
   249→        let duration = Double(audioFile.length) / audioFile.fileFormat.sampleRate
   250→
   251→        // Detect BPM in background
   252→        let detectedBPM = try? await bpmService.analyzeBPM(url: url)
   253→
   254→        // Detect musical key
   255→        let detectedKey = try? await keyService.analyzeKey(url: url)
   256→
   257→        // Analyze audio for auto-gain
   258→        let analyzedGain = await analyzeAudioForGain(url: url)
   259→
   260→        // Analyze waveform for visualization
   261→        let waveformData = try? await waveformService.analyzeWaveform(url: url, segmentCount: 200)
   262→
   263→        await MainActor.run {
   264→            if deck == "A" {
   265→                if deckA.isPlaying {
   266→                    playerNodeA.stop()
   267→                }
   268→
   269→                audioFileA = audioFile
   270→                deckA.audio = audio
   271→                deckA.duration = duration
   272→                deckA.currentTime = 0
   273→                deckA.isLoaded = true
   274→                deckA.isPlaying = false
   275→                deckA.bpm = detectedBPM
   276→                deckA.musicalKey = detectedKey
   277→                deckA.analyzedGain = analyzedGain
   278→                deckA.gain = 0 // Reset manual gain
   279→
   280→                // Set waveform data
   281→                if let waveform = waveformData {
   282→                    deckA.waveformData = waveform.amplitudes
   283→                    deckA.waveformPeaks = waveform.peaks
   284→                    deckA.waveformColors = waveform.colors
   285→                }
   286→
   287→                // Reconnect with proper format
   288→                audioEngine.disconnectNodeOutput(playerNodeA)
   289→                audioEngine.connect(playerNodeA, to: timePitchA, format: format)
   290→
   291→                // Apply auto-gain
   292→                applyGain(deck: "A")
   293→
   294→                // Schedule file
   295→                playerNodeA.scheduleFile(audioFile, at: nil)
   296→            } else {
   297→                if deckB.isPlaying {
   298→                    playerNodeB.stop()
   299→                }
   300→
   301→                audioFileB = audioFile
   302→                deckB.audio = audio
   303→                deckB.duration = duration
   304→                deckB.currentTime = 0
   305→                deckB.isLoaded = true
   306→                deckB.isPlaying = false
   307→                deckB.bpm = detectedBPM
   308→                deckB.musicalKey = detectedKey
   309→                deckB.analyzedGain = analyzedGain
   310→                deckB.gain = 0 // Reset manual gain
   311→
   312→                // Set waveform data
   313→                if let waveform = waveformData {
   314→                    deckB.waveformData = waveform.amplitudes
   315→                    deckB.waveformPeaks = waveform.peaks
   316→                    deckB.waveformColors = waveform.colors
   317→                }
   318→
   319→                audioEngine.disconnectNodeOutput(playerNodeB)
   320→                audioEngine.connect(playerNodeB, to: timePitchB, format: format)
   321→
   322→                // Apply auto-gain
   323→                applyGain(deck: "B")
   324→
   325→                playerNodeB.scheduleFile(audioFile, at: nil)
   326→            }
   327→        }
   328→    }
   329→
   330→    // MARK: - Playback Control
   331→
   332→    func play(deck: String) throws {
   333→        if !audioEngine.isRunning {
   334→            try startEngine()
   335→        }
   336→
   337→        if deck == "A" {
   338→            guard deckA.isLoaded else { return }
   339→            playerNodeA.play()
   340→            deckA.isPlaying = true
   341→        } else {
   342→            guard deckB.isLoaded else { return }
   343→            playerNodeB.play()
   344→            deckB.isPlaying = true
   345→        }
   346→    }
   347→
   348→    func pause(deck: String) {
   349→        if deck == "A" {
   350→            playerNodeA.pause()
   351→            deckA.isPlaying = false
   352→        } else {
   353→            playerNodeB.pause()
   354→            deckB.isPlaying = false
   355→        }
   356→    }
   357→
   358→    func stop(deck: String) {
   359→        if deck == "A" {
   360→            playerNodeA.stop()
   361→            deckA.isPlaying = false
   362→            deckA.currentTime = 0
   363→            if let file = audioFileA {
   364→                playerNodeA.scheduleFile(file, at: nil)
   365→            }
   366→        } else {
   367→            playerNodeB.stop()
   368→            deckB.isPlaying = false
   369→            deckB.currentTime = 0
   370→            if let file = audioFileB {
   371→                playerNodeB.scheduleFile(file, at: nil)
   372→            }
   373→        }
   374→    }
   375→
   376→    func seek(deck: String, to time: TimeInterval) {
   377→        let player = deck == "A" ? playerNodeA : playerNodeB
   378→        let file = deck == "A" ? audioFileA : audioFileB
   379→        let wasPlaying = deck == "A" ? deckA.isPlaying : deckB.isPlaying
   380→
   381→        guard let audioFile = file else { return }
   382→
   383→        player.stop()
   384→
   385→        let sampleRate = audioFile.processingFormat.sampleRate
   386→        let startFrame = AVAudioFramePosition(time * sampleRate)
   387→        let totalFrames = audioFile.length
   388→        let frameCount = AVAudioFrameCount(totalFrames - startFrame)
   389→
   390→        guard frameCount > 0, startFrame < totalFrames else { return }
   391→
   392→        player.scheduleSegment(
   393→            audioFile,
   394→            startingFrame: startFrame,
   395→            frameCount: frameCount,
   396→            at: nil
   397→        )
   398→
   399→        if deck == "A" {
   400→            deckA.currentTime = time
   401→        } else {
   402→            deckB.currentTime = time
   403→        }
   404→
   405→        if wasPlaying {
   406→            player.play()
   407→        }
   408→    }
   409→
   410→    // MARK: - Cue Points
   411→
   412→    func setCuePoint(deck: String) {
   413→        if deck == "A" {
   414→            deckA.cuePoint = deckA.currentTime
   415→        } else {
   416→            deckB.cuePoint = deckB.currentTime
   417→        }
   418→    }
   419→
   420→    func jumpToCue(deck: String) {
   421→        let cuePoint = deck == "A" ? deckA.cuePoint : deckB.cuePoint
   422→        guard let cue = cuePoint else { return }
   423→        seek(deck: deck, to: cue)
   424→    }
   425→
   426→    // MARK: - Gain Control
   427→
   428→    func setGain(deck: String, gain: Float) {
   429→        let clampedGain = max(-24, min(24, gain))
   430→        if deck == "A" {
   431→            deckA.gain = clampedGain
   432→            applyGain(deck: "A")
   433→        } else {
   434→            deckB.gain = clampedGain
   435→            applyGain(deck: "B")
   436→        }
   437→    }
   438→
   439→    func toggleAutoGain(deck: String) {
   440→        if deck == "A" {
   441→            deckA.autoGainEnabled.toggle()
   442→            applyGain(deck: "A")
   443→        } else {
   444→            deckB.autoGainEnabled.toggle()
   445→            applyGain(deck: "B")
   446→        }
   447→    }
   448→
   449→    func resetGain(deck: String) {
   450→        if deck == "A" {
   451→            deckA.gain = 0
   452→            applyGain(deck: "A")
   453→        } else {
   454→            deckB.gain = 0
   455→            applyGain(deck: "B")
   456→        }
   457→    }
   458→
   459→    private func applyGain(deck: String) {
   460→        let deckState = deck == "A" ? deckA : deckB
   461→        let mixerNode = deck == "A" ? mixerNodeA : mixerNodeB
   462→
   463→        // Convert dB to linear gain
   464→        let effectiveGainDB = deckState.effectiveGain
   465→        let linearGain = pow(10, effectiveGainDB / 20)
   466→
   467→        // Apply to mixer node input volume (before crossfade)
   468→        mixerNode.volume = linearGain
   469→    }
   470→
   471→    /// Analyze audio file and calculate recommended gain for normalization
   472→    private func analyzeAudioForGain(url: URL) async -> Float {
   473→        do {
   474→            let audioFile = try AVAudioFile(forReading: url)
   475→            let format = audioFile.processingFormat
   476→            let frameCount = AVAudioFrameCount(audioFile.length)
   477→
   478→            // Read entire file (or sample for long files)
   479→            let maxFramesToRead: AVAudioFrameCount = min(frameCount, AVAudioFrameCount(format.sampleRate * 30)) // Max 30 seconds
   480→            let buffer = AVAudioPCMBuffer(pcmFormat: format, frameCapacity: maxFramesToRead)!
   481→
   482→            // Read from start
   483→            audioFile.framePosition = 0
   484→            try audioFile.read(into: buffer, frameCount: maxFramesToRead)
   485→
   486→            guard let channelData = buffer.floatChannelData else { return 0 }
   487→
   488→            let channelCount = Int(format.channelCount)
   489→            let frameLength = Int(buffer.frameLength)
   490→
   491→            guard frameLength > 0 else { return 0 }
   492→
   493→            // Calculate peak and RMS
   494→            var peak: Float = 0
   495→            var sumSquares: Float = 0
   496→
   497→            for channel in 0..<channelCount {
   498→                let data = channelData[channel]
   499→                for i in 0..<frameLength {
   500→                    let sample = abs(data[i])
   501→                    peak = max(peak, sample)
   502→                    sumSquares += data[i] * data[i]
   503→                }
   504→            }
   505→
   506→            let rms = sqrt(sumSquares / Float(frameLength * channelCount))
   507→
   508→            // Target RMS level (around -14 dB, broadcast standard)
   509→            let targetRMS: Float = 0.2 // ~-14 dB
   510→            let targetPeak: Float = 0.9 // Leave headroom
   511→
   512→            // Calculate gain needed
   513→            var gainDB: Float = 0
   514→
   515→            if rms > 0 {
   516→                // RMS-based gain
   517→                let rmsGain = targetRMS / rms
   518→                let rmsGainDB = 20 * log10(rmsGain)
   519→
   520→                // Peak limiting
   521→                let peakGain = targetPeak / max(peak, 0.0001)
   522→                let peakGainDB = 20 * log10(peakGain)
   523→
   524→                // Use the lower of the two to avoid clipping
   525→                gainDB = min(rmsGainDB, peakGainDB)
   526→
   527→                // Clamp to reasonable range
   528→                gainDB = max(-12, min(12, gainDB))
   529→            }
   530→
   531→            return gainDB
   532→        } catch {
   533→            print("Audio analysis error: \(error)")
   534→            return 0
   535→        }
   536→    }
   537→
   538→    // MARK: - Tempo & Pitch Control
   539→
   540→    func setTempo(deck: String, rate: Float) {
   541→        let clampedRate = max(0.5, min(2.0, rate))
   542→        if deck == "A" {
   543→            timePitchA.rate = clampedRate
   544→            deckA.tempo = clampedRate
   545→        } else {
   546→            timePitchB.rate = clampedRate
   547→            deckB.tempo = clampedRate
   548→        }
   549→    }
   550→
   551→    func setPitch(deck: String, cents: Float) {
   552→        let clampedCents = max(-1200, min(1200, cents))
   553→        if deck == "A" {
   554→            timePitchA.pitch = clampedCents
   555→            deckA.pitch = clampedCents
   556→        } else {
   557→            timePitchB.pitch = clampedCents
   558→            deckB.pitch = clampedCents
   559→        }
   560→    }
   561→
   562→    // MARK: - EQ Control
   563→
   564→    enum EQBand: Int {
   565→        case low = 0
   566→        case mid = 1
   567→        case high = 2
   568→    }
   569→
   570→    func setEQ(deck: String, band: EQBand, gain: Float) {
   571→        let clampedGain = max(-24, min(24, gain))
   572→        let eq = deck == "A" ? eqNodeA : eqNodeB
   573→        eq.bands[band.rawValue].gain = clampedGain
   574→
   575→        if deck == "A" {
   576→            switch band {
   577→            case .low: deckA.eqLow = clampedGain
   578→            case .mid: deckA.eqMid = clampedGain
   579→            case .high: deckA.eqHigh = clampedGain
   580→            }
   581→        } else {
   582→            switch band {
   583→            case .low: deckB.eqLow = clampedGain
   584→            case .mid: deckB.eqMid = clampedGain
   585→            case .high: deckB.eqHigh = clampedGain
   586→            }
   587→        }
   588→    }
   589→
   590→    func toggleKill(deck: String, band: EQBand) {
   591→        let eq = deck == "A" ? eqNodeA : eqNodeB
   592→
   593→        if deck == "A" {
   594→            switch band {
   595→            case .low:
   596→                deckA.killLow.toggle()
   597→                eq.bands[0].gain = deckA.killLow ? -60 : deckA.eqLow
   598→            case .mid:
   599→                deckA.killMid.toggle()
   600→                eq.bands[1].gain = deckA.killMid ? -60 : deckA.eqMid
   601→            case .high:
   602→                deckA.killHigh.toggle()
   603→                eq.bands[2].gain = deckA.killHigh ? -60 : deckA.eqHigh
   604→            }
   605→        } else {
   606→            switch band {
   607→            case .low:
   608→                deckB.killLow.toggle()
   609→                eq.bands[0].gain = deckB.killLow ? -60 : deckB.eqLow
   610→            case .mid:
   611→                deckB.killMid.toggle()
   612→                eq.bands[1].gain = deckB.killMid ? -60 : deckB.eqMid
   613→            case .high:
   614→                deckB.killHigh.toggle()
   615→                eq.bands[2].gain = deckB.killHigh ? -60 : deckB.eqHigh
   616→            }
   617→        }
   618→    }
   619→
   620→    func setFilter(deck: String, position: Float) {
   621→        let clampedPosition = max(-1.0, min(1.0, position))
   622→        let filter = deck == "A" ? filterNodeA : filterNodeB
   623→        let band = filter.bands[0]
   624→
   625→        if deck == "A" {
   626→            deckA.filterPosition = clampedPosition
   627→        } else {
   628→            deckB.filterPosition = clampedPosition
   629→        }
   630→
   631→        if abs(clampedPosition) < 0.05 {
   632→            // Near center = bypass filter
   633→            band.bypass = true
   634→        } else if clampedPosition < 0 {
   635→            // Negative = Low Pass Filter (cuts highs)
   636→            band.bypass = false
   637→            band.filterType = .lowPass
   638→            // Map -1.0 to 200Hz, 0 to 20000Hz (exponential)
   639→            let freq = 200 * pow(100, 1 + clampedPosition)
   640→            band.frequency = min(20000, max(200, freq))
   641→        } else {
   642→            // Positive = High Pass Filter (cuts lows)
   643→            band.bypass = false
   644→            band.filterType = .highPass
   645→            // Map 0 to 20Hz, +1.0 to 5000Hz (exponential)
   646→            let freq = 20 * pow(250, clampedPosition)
   647→            band.frequency = min(5000, max(20, freq))
   648→        }
   649→    }
   650→
   651→    func resetEQ(deck: String) {
   652→        let eq = deck == "A" ? eqNodeA : eqNodeB
   653→        let filter = deck == "A" ? filterNodeA : filterNodeB
   654→
   655→        // Reset all EQ bands to 0 dB
   656→        for band in eq.bands {
   657→            band.gain = 0
   658→        }
   659→
   660→        // Reset filter to bypass
   661→        filter.bands[0].bypass = true
   662→
   663→        // Update state
   664→        if deck == "A" {
   665→            deckA.eqLow = 0
   666→            deckA.eqMid = 0
   667→            deckA.eqHigh = 0
   668→            deckA.killLow = false
   669→            deckA.killMid = false
   670→            deckA.killHigh = false
   671→            deckA.filterPosition = 0
   672→        } else {
   673→            deckB.eqLow = 0
   674→            deckB.eqMid = 0
   675→            deckB.eqHigh = 0
   676→            deckB.killLow = false
   677→            deckB.killMid = false
   678→            deckB.killHigh = false
   679→            deckB.filterPosition = 0
   680→        }
   681→    }
   682→
   683→    // MARK: - Reverb Control
   684→
   685→    func setReverbMix(deck: String, mix: Float) {
   686→        let clampedMix = max(0, min(100, mix * 100))
   687→        let reverb = deck == "A" ? reverbNodeA : reverbNodeB
   688→        reverb.wetDryMix = clampedMix
   689→
   690→        if deck == "A" {
   691→            deckA.reverbMix = mix
   692→        } else {
   693→            deckB.reverbMix = mix
   694→        }
   695→    }
   696→
   697→    func setReverbPreset(deck: String, preset: Int) {
   698→        let reverb = deck == "A" ? reverbNodeA : reverbNodeB
   699→        let presets: [AVAudioUnitReverbPreset] = [
   700→            .smallRoom, .mediumRoom, .largeRoom, .mediumHall,
   701→            .largeHall, .plate, .mediumChamber, .largeChamber,
   702→            .cathedral, .largeRoom2, .mediumHall2, .mediumHall3, .largeHall2
   703→        ]
   704→
   705→        guard preset >= 0 && preset < presets.count else { return }
   706→        reverb.loadFactoryPreset(presets[preset])
   707→
   708→        if deck == "A" {
   709→            deckA.reverbPreset = preset
   710→        } else {
   711→            deckB.reverbPreset = preset
   712→        }
   713→    }
   714→
   715→    // MARK: - Delay Control
   716→
   717→    func setDelayMix(deck: String, mix: Float) {
   718→        let clampedMix = max(0, min(100, mix * 100))
   719→        let delay = deck == "A" ? delayNodeA : delayNodeB
   720→        delay.wetDryMix = clampedMix
   721→
   722→        if deck == "A" {
   723→            deckA.delayMix = mix
   724→        } else {
   725→            deckB.delayMix = mix
   726→        }
   727→    }
   728→
   729→    func setDelayTime(deck: String, time: Float) {
   730→        let clampedTime = max(0.01, min(2.0, Double(time)))
   731→        let delay = deck == "A" ? delayNodeA : delayNodeB
   732→        delay.delayTime = clampedTime
   733→
   734→        if deck == "A" {
   735→            deckA.delayTime = time
   736→        } else {
   737→            deckB.delayTime = time
   738→        }
   739→    }
   740→
   741→    func setDelayFeedback(deck: String, feedback: Float) {
   742→        let clampedFeedback = max(-100, min(100, feedback))
   743→        let delay = deck == "A" ? delayNodeA : delayNodeB
   744→        delay.feedback = clampedFeedback
   745→
   746→        if deck == "A" {
   747→            deckA.delayFeedback = feedback
   748→        } else {
   749→            deckB.delayFeedback = feedback
   750→        }
   751→    }
   752→
   753→    func resetEffects(deck: String) {
   754→        let reverb = deck == "A" ? reverbNodeA : reverbNodeB
   755→        let delay = deck == "A" ? delayNodeA : delayNodeB
   756→
   757→        reverb.wetDryMix = 0
   758→        delay.wetDryMix = 0
   759→
   760→        if deck == "A" {
   761→            deckA.reverbMix = 0
   762→            deckA.delayMix = 0
   763→            deckA.delayTime = 0.25
   764→            deckA.delayFeedback = 50
   765→        } else {
   766→            deckB.reverbMix = 0
   767→            deckB.delayMix = 0
   768→            deckB.delayTime = 0.25
   769→            deckB.delayFeedback = 50
   770→        }
   771→    }
   772→
   773→    // MARK: - Loop Control
   774→
   775→    func setLoopIn(deck: String) {
   776→        if deck == "A" {
   777→            deckA.loopStart = deckA.currentTime
   778→            // If we already have a loop end before this point, clear it
   779→            if let end = deckA.loopEnd, end <= deckA.currentTime {
   780→                deckA.loopEnd = nil
   781→                deckA.isLooping = false
   782→            }
   783→        } else {
   784→            deckB.loopStart = deckB.currentTime
   785→            if let end = deckB.loopEnd, end <= deckB.currentTime {
   786→                deckB.loopEnd = nil
   787→                deckB.isLooping = false
   788→            }
   789→        }
   790→    }
   791→
   792→    func setLoopOut(deck: String) {
   793→        if deck == "A" {
   794→            guard let start = deckA.loopStart, deckA.currentTime > start else { return }
   795→            deckA.loopEnd = deckA.currentTime
   796→            deckA.isLooping = true
   797→        } else {
   798→            guard let start = deckB.loopStart, deckB.currentTime > start else { return }
   799→            deckB.loopEnd = deckB.currentTime
   800→            deckB.isLooping = true
   801→        }
   802→    }
   803→
   804→    func toggleLoop(deck: String) {
   805→        if deck == "A" {
   806→            guard deckA.hasLoop else { return }
   807→            deckA.isLooping.toggle()
   808→        } else {
   809→            guard deckB.hasLoop else { return }
   810→            deckB.isLooping.toggle()
   811→        }
   812→    }
   813→
   814→    func setLoopByBeats(deck: String, beats: Double) {
   815→        let deckState = deck == "A" ? deckA : deckB
   816→        guard let loopLength = deckState.loopLengthForBeats(beats) else { return }
   817→
   818→        let start = deckState.currentTime
   819→        let end = start + loopLength
   820→
   821→        // Ensure we don't exceed track duration
   822→        let duration = deckState.duration
   823→        guard end <= duration else { return }
   824→
   825→        if deck == "A" {
   826→            deckA.loopStart = start
   827→            deckA.loopEnd = end
   828→            deckA.loopBeats = beats
   829→            deckA.isLooping = true
   830→        } else {
   831→            deckB.loopStart = start
   832→            deckB.loopEnd = end
   833→            deckB.loopBeats = beats
   834→            deckB.isLooping = true
   835→        }
   836→    }
   837→
   838→    func halveLoop(deck: String) {
   839→        if deck == "A" {
   840→            guard let start = deckA.loopStart, let end = deckA.loopEnd else { return }
   841→            let newLength = (end - start) / 2
   842→            if newLength >= 0.05 {  // Minimum 50ms loop
   843→                deckA.loopEnd = start + newLength
   844→                deckA.loopBeats = max(0.25, deckA.loopBeats / 2)
   845→            }
   846→        } else {
   847→            guard let start = deckB.loopStart, let end = deckB.loopEnd else { return }
   848→            let newLength = (end - start) / 2
   849→            if newLength >= 0.05 {
   850→                deckB.loopEnd = start + newLength
   851→                deckB.loopBeats = max(0.25, deckB.loopBeats / 2)
   852→            }
   853→        }
   854→    }
   855→
   856→    func doubleLoop(deck: String) {
   857→        if deck == "A" {
   858→            guard let start = deckA.loopStart, let end = deckA.loopEnd else { return }
   859→            let newLength = (end - start) * 2
   860→            let newEnd = start + newLength
   861→            if newEnd <= deckA.duration {
   862→                deckA.loopEnd = newEnd
   863→                deckA.loopBeats = min(32, deckA.loopBeats * 2)
   864→            }
   865→        } else {
   866→            guard let start = deckB.loopStart, let end = deckB.loopEnd else { return }
   867→            let newLength = (end - start) * 2
   868→            let newEnd = start + newLength
   869→            if newEnd <= deckB.duration {
   870→                deckB.loopEnd = newEnd
   871→                deckB.loopBeats = min(32, deckB.loopBeats * 2)
   872→            }
   873→        }
   874→    }
   875→
   876→    func clearLoop(deck: String) {
   877→        if deck == "A" {
   878→            deckA.loopStart = nil
   879→            deckA.loopEnd = nil
   880→            deckA.isLooping = false
   881→            deckA.loopBeats = 4
   882→        } else {
   883→            deckB.loopStart = nil
   884→            deckB.loopEnd = nil
   885→            deckB.isLooping = false
   886→            deckB.loopBeats = 4
   887→        }
   888→    }
   889→
   890→    func moveLoop(deck: String, forward: Bool) {
   891→        if deck == "A" {
   892→            guard let start = deckA.loopStart, let end = deckA.loopEnd else { return }
   893→            let length = end - start
   894→            let offset = forward ? length : -length
   895→
   896→            let newStart = start + offset
   897→            let newEnd = end + offset
   898→
   899→            // Ensure within bounds
   900→            guard newStart >= 0, newEnd <= deckA.duration else { return }
   901→
   902→            deckA.loopStart = newStart
   903→            deckA.loopEnd = newEnd
   904→
   905→            // Jump to new loop start if currently playing
   906→            if deckA.isLooping {
   907→                seek(deck: "A", to: newStart)
   908→            }
   909→        } else {
   910→            guard let start = deckB.loopStart, let end = deckB.loopEnd else { return }
   911→            let length = end - start
   912→            let offset = forward ? length : -length
   913→
   914→            let newStart = start + offset
   915→            let newEnd = end + offset
   916→
   917→            guard newStart >= 0, newEnd <= deckB.duration else { return }
   918→
   919→            deckB.loopStart = newStart
   920→            deckB.loopEnd = newEnd
   921→
   922→            if deckB.isLooping {
   923→                seek(deck: "B", to: newStart)
   924→            }
   925→        }
   926→    }
   927→
   928→    // MARK: - Nudge (Beat Matching)
   929→
   930→    private var nudgeTimerA: Timer?
   931→    private var nudgeTimerB: Timer?
   932→    private var originalTempoA: Float = 1.0
   933→    private var originalTempoB: Float = 1.0
   934→
   935→    /// Nudge the track forward or backward for beat matching
   936→    /// - Parameters:
   937→    ///   - deck: "A" or "B"
   938→    ///   - forward: true = speed up, false = slow down
   939→    ///   - amount: nudge intensity (default 0.04 = 4%)
   940→    func nudge(deck: String, forward: Bool, amount: Float = 0.04) {
   941→        let nudgeValue = forward ? amount : -amount
   942→
   943→        if deck == "A" {
   944→            // Cancel existing nudge timer
   945→            nudgeTimerA?.invalidate()
   946→
   947→            // Store original tempo if not already nudging
   948→            if nudgeTimerA == nil {
   949→                originalTempoA = deckA.tempo
   950→            }
   951→
   952→            // Apply nudge
   953→            let nudgedTempo = max(0.5, min(2.0, originalTempoA + nudgeValue))
   954→            timePitchA.rate = nudgedTempo
   955→
   956→            // Return to original tempo after delay
   957→            nudgeTimerA = Timer.scheduledTimer(withTimeInterval: 0.3, repeats: false) { [weak self] _ in
   958→                guard let self = self else { return }
   959→                Task { @MainActor in
   960→                    self.timePitchA.rate = self.originalTempoA
   961→                    self.nudgeTimerA = nil
   962→                }
   963→            }
   964→        } else {
   965→            nudgeTimerB?.invalidate()
   966→
   967→            if nudgeTimerB == nil {
   968→                originalTempoB = deckB.tempo
   969→            }
   970→
   971→            let nudgedTempo = max(0.5, min(2.0, originalTempoB + nudgeValue))
   972→            timePitchB.rate = nudgedTempo
   973→
   974→            nudgeTimerB = Timer.scheduledTimer(withTimeInterval: 0.3, repeats: false) { [weak self] _ in
   975→                guard let self = self else { return }
   976→                Task { @MainActor in
   977→                    self.timePitchB.rate = self.originalTempoB
   978→                    self.nudgeTimerB = nil
   979→                }
   980→            }
   981→        }
   982→    }
   983→
   984→    /// Continuous nudge while button is held
   985→    func startNudge(deck: String, forward: Bool) {
   986→        let nudgeValue: Float = forward ? 0.06 : -0.06
   987→
   988→        if deck == "A" {
   989→            originalTempoA = deckA.tempo
   990→            let nudgedTempo = max(0.5, min(2.0, originalTempoA + nudgeValue))
   991→            timePitchA.rate = nudgedTempo
   992→        } else {
   993→            originalTempoB = deckB.tempo
   994→            let nudgedTempo = max(0.5, min(2.0, originalTempoB + nudgeValue))
   995→            timePitchB.rate = nudgedTempo
   996→        }
   997→    }
   998→
   999→    /// Stop continuous nudge
  1000→    func stopNudge(deck: String) {
  1001→        if deck == "A" {
  1002→            timePitchA.rate = originalTempoA
  1003→        } else {
  1004→            timePitchB.rate = originalTempoB
  1005→        }
  1006→    }
  1007→
  1008→    // MARK: - BPM Sync
  1009→
  1010→    /// Sync this deck's tempo to match the other deck's BPM
  1011→    func syncBPM(deck: String) {
  1012→        if deck == "A" {
  1013→            guard let sourceBPM = deckA.bpm, let targetBPM = deckB.bpm else { return }
  1014→            guard sourceBPM > 0 else { return }
  1015→
  1016→            // Calculate tempo ratio to match target BPM
  1017→            let tempoRatio = Float(targetBPM / sourceBPM)
  1018→
  1019→            // Clamp to valid tempo range (0.5 - 2.0)
  1020→            let clampedTempo = max(0.5, min(2.0, tempoRatio))
  1021→
  1022→            setTempo(deck: "A", rate: clampedTempo)
  1023→        } else {
  1024→            guard let sourceBPM = deckB.bpm, let targetBPM = deckA.bpm else { return }
  1025→            guard sourceBPM > 0 else { return }
  1026→
  1027→            let tempoRatio = Float(targetBPM / sourceBPM)
  1028→            let clampedTempo = max(0.5, min(2.0, tempoRatio))
  1029→
  1030→            setTempo(deck: "B", rate: clampedTempo)
  1031→        }
  1032→    }
  1033→
  1034→    /// Check if deck can sync (both decks have BPM detected)
  1035→    func canSync(deck: String) -> Bool {
  1036→        if deck == "A" {
  1037→            return deckA.bpm != nil && deckB.bpm != nil && deckB.isLoaded
  1038→        } else {
  1039→            return deckB.bpm != nil && deckA.bpm != nil && deckA.isLoaded
  1040→        }
  1041→    }
  1042→
  1043→    /// Get the effective BPM after tempo adjustment
  1044→    func effectiveBPM(deck: String) -> Double? {
  1045→        if deck == "A" {
  1046→            guard let bpm = deckA.bpm else { return nil }
  1047→            return bpm * Double(deckA.tempo)
  1048→        } else {
  1049→            guard let bpm = deckB.bpm else { return nil }
  1050→            return bpm * Double(deckB.tempo)
  1051→        }
  1052→    }
  1053→
  1054→    /// Check if decks are synced (effective BPMs match within tolerance)
  1055→    func areSynced() -> Bool {
  1056→        guard let bpmA = effectiveBPM(deck: "A"),
  1057→              let bpmB = effectiveBPM(deck: "B") else { return false }
  1058→        return abs(bpmA - bpmB) < 0.5  // Within 0.5 BPM tolerance
  1059→    }
  1060→
  1061→    /// Check harmonic compatibility between decks
  1062→    func harmonicCompatibility() -> HarmonicCompatibility? {
  1063→        guard let keyA = deckA.musicalKey, let keyB = deckB.musicalKey else {
  1064→            return nil
  1065→        }
  1066→        return keyA.isCompatible(with: keyB)
  1067→    }
  1068→
  1069→    // MARK: - Beat Sync
  1070→
  1071→    /// Enable/disable automatic beat sync for a deck
  1072→    func toggleBeatSync(deck: String) {
  1073→        if deck == "A" {
  1074→            deckA.beatSyncEnabled.toggle()
  1075→            if deckA.beatSyncEnabled {
  1076→                // Sync BPM first
  1077→                syncBPM(deck: "A")
  1078→                // Then align phase
  1079→                performBeatSync(deck: "A")
  1080→            }
  1081→        } else {
  1082→            deckB.beatSyncEnabled.toggle()
  1083→            if deckB.beatSyncEnabled {
  1084→                syncBPM(deck: "B")
  1085→                performBeatSync(deck: "B")
  1086→            }
  1087→        }
  1088→    }
  1089→
  1090→    /// Manually perform a beat sync (quantized jump to align beats)
  1091→    func performBeatSync(deck: String) {
  1092→        let sourceDeck = deck == "A" ? deckA : deckB
  1093→        let targetDeck = deck == "A" ? deckB : deckA
  1094→
  1095→        guard sourceDeck.isLoaded && targetDeck.isLoaded,
  1096→              let sourceBPM = sourceDeck.bpm,
  1097→              let targetPhase = targetDeck.beatPhase,
  1098→              sourceBPM > 0 else { return }
  1099→
  1100→        // Calculate the current phase difference
  1101→        let sourceSecondsPerBeat = 60.0 / (sourceBPM * Double(sourceDeck.tempo))
  1102→
  1103→        // Get source deck's current beat phase
  1104→        guard let sourcePhase = sourceDeck.beatPhase else { return }
  1105→
  1106→        // Calculate phase difference (how much source is ahead/behind target)
  1107→        var phaseDiff = targetPhase - sourcePhase
  1108→
  1109→        // Normalize to -0.5 to 0.5 range (shortest path)
  1110→        if phaseDiff > 0.5 { phaseDiff -= 1.0 }
  1111→        if phaseDiff < -0.5 { phaseDiff += 1.0 }
  1112→
  1113→        // Convert phase difference to time offset
  1114→        let timeOffset = phaseDiff * sourceSecondsPerBeat
  1115→
  1116→        // Calculate new position
  1117→        let newTime = max(0, sourceDeck.currentTime + timeOffset)
  1118→
  1119→        // Seek to aligned position
  1120→        seek(deck: deck, to: newTime)
  1121→    }
  1122→
  1123→    /// Jump by a number of beats (positive = forward, negative = backward)
  1124→    func beatJump(deck: String, beats: Double) {
  1125→        let deckState = deck == "A" ? deckA : deckB
  1126→
  1127→        guard let bpm = deckState.bpm, bpm > 0 else { return }
  1128→
  1129→        let secondsPerBeat = 60.0 / (bpm * Double(deckState.tempo))
  1130→        let jumpTime = beats * secondsPerBeat
  1131→
  1132→        let newTime = max(0, min(deckState.duration, deckState.currentTime + jumpTime))
  1133→        seek(deck: deck, to: newTime)
  1134→    }
  1135→
  1136→    /// Adjust beat grid offset (for manual grid alignment)
  1137→    func adjustBeatGridOffset(deck: String, offset: TimeInterval) {
  1138→        if deck == "A" {
  1139→            deckA.beatGridOffset = offset
  1140→        } else {
  1141→            deckB.beatGridOffset = offset
  1142→        }
  1143→    }
  1144→
  1145→    /// Nudge beat grid forward/backward by a small amount
  1146→    func nudgeBeatGrid(deck: String, forward: Bool) {
  1147→        let nudgeAmount: TimeInterval = 0.01 // 10ms nudge
  1148→        if deck == "A" {
  1149→            deckA.beatGridOffset += forward ? nudgeAmount : -nudgeAmount
  1150→        } else {
  1151→            deckB.beatGridOffset += forward ? nudgeAmount : -nudgeAmount
  1152→        }
  1153→    }
  1154→
  1155→    /// Reset beat grid offset to zero
  1156→    func resetBeatGridOffset(deck: String) {
  1157→        if deck == "A" {
  1158→            deckA.beatGridOffset = 0
  1159→        } else {
  1160→            deckB.beatGridOffset = 0
  1161→        }
  1162→    }
  1163→
  1164→    /// Check if beats are phase-aligned between decks
  1165→    func areBeatsAligned() -> Bool {
  1166→        guard let phaseA = deckA.beatPhase,
  1167→              let phaseB = deckB.beatPhase else { return false }
  1168→
  1169→        // Calculate phase difference
  1170→        var phaseDiff = abs(phaseA - phaseB)
  1171→        if phaseDiff > 0.5 { phaseDiff = 1.0 - phaseDiff }
  1172→
  1173→        // Within 5% of beat = aligned
  1174→        return phaseDiff < 0.05
  1175→    }
  1176→
  1177→    /// Get the beat phase difference between decks (-0.5 to 0.5)
  1178→    func beatPhaseDifference() -> Double? {
  1179→        guard let phaseA = deckA.beatPhase,
  1180→              let phaseB = deckB.beatPhase else { return nil }
  1181→
  1182→        var diff = phaseB - phaseA
  1183→        if diff > 0.5 { diff -= 1.0 }
  1184→        if diff < -0.5 { diff += 1.0 }
  1185→
  1186→        return diff
  1187→    }
  1188→
  1189→    // MARK: - Hot Cues
  1190→
  1191→    /// Set or trigger a hot cue
  1192→    /// If the pad is empty, sets the current position as hot cue
  1193→    /// If the pad has a cue, jumps to that position
  1194→    func triggerHotCue(deck: String, pad: Int) {
  1195→        guard pad >= 0 && pad < 8 else { return }
  1196→
  1197→        if deck == "A" {
  1198→            guard deckA.isLoaded else { return }
  1199→            if deckA.hotCues[pad] == nil {
  1200→                // Set new hot cue at current position
  1201→                deckA.hotCues[pad] = deckA.currentTime
  1202→            } else {
  1203→                // Jump to existing hot cue
  1204→                if let cueTime = deckA.hotCues[pad] {
  1205→                    seek(deck: "A", to: cueTime)
  1206→                    // Auto-play when triggering hot cue
  1207→                    if !deckA.isPlaying {
  1208→                        try? play(deck: "A")
  1209→                    }
  1210→                }
  1211→            }
  1212→        } else {
  1213→            guard deckB.isLoaded else { return }
  1214→            if deckB.hotCues[pad] == nil {
  1215→                deckB.hotCues[pad] = deckB.currentTime
  1216→            } else {
  1217→                if let cueTime = deckB.hotCues[pad] {
  1218→                    seek(deck: "B", to: cueTime)
  1219→                    if !deckB.isPlaying {
  1220→                        try? play(deck: "B")
  1221→                    }
  1222→                }
  1223→            }
  1224→        }
  1225→    }
  1226→
  1227→    /// Clear a specific hot cue
  1228→    func clearHotCue(deck: String, pad: Int) {
  1229→        guard pad >= 0 && pad < 8 else { return }
  1230→
  1231→        if deck == "A" {
  1232→            deckA.hotCues[pad] = nil
  1233→        } else {
  1234→            deckB.hotCues[pad] = nil
  1235→        }
  1236→    }
  1237→
  1238→    /// Clear all hot cues for a deck
  1239→    func clearAllHotCues(deck: String) {
  1240→        if deck == "A" {
  1241→            deckA.hotCues = Array(repeating: nil, count: 8)
  1242→        } else {
  1243→            deckB.hotCues = Array(repeating: nil, count: 8)
  1244→        }
  1245→    }
  1246→
  1247→    /// Check if a hot cue is set
  1248→    func hasHotCue(deck: String, pad: Int) -> Bool {
  1249→        guard pad >= 0 && pad < 8 else { return false }
  1250→        if deck == "A" {
  1251→            return deckA.hotCues[pad] != nil
  1252→        } else {
  1253→            return deckB.hotCues[pad] != nil
  1254→        }
  1255→    }
  1256→
  1257→    // MARK: - Recording
  1258→
  1259→    /// Start recording the mix output to a file
  1260→    func startRecording() throws {
  1261→        guard !isRecording else { return }
  1262→
  1263→        // Create recordings directory in Music folder
  1264→        let musicURL = FileManager.default.urls(for: .musicDirectory, in: .userDomainMask).first!
  1265→        let recordingsDir = musicURL.appendingPathComponent("YouTubeAudio/Recordings")
  1266→
  1267→        try FileManager.default.createDirectory(at: recordingsDir, withIntermediateDirectories: true)
  1268→
  1269→        // Generate filename with timestamp
  1270→        let formatter = DateFormatter()
  1271→        formatter.dateFormat = "yyyy-MM-dd_HH-mm-ss"
  1272→        let timestamp = formatter.string(from: Date())
  1273→        let filename = "Mix_\(timestamp).m4a"
  1274→        let fileURL = recordingsDir.appendingPathComponent(filename)
  1275→
  1276→        // Get format from main mixer
  1277→        let format = audioEngine.mainMixerNode.outputFormat(forBus: 0)
  1278→
  1279→        // Create audio file for recording (AAC format for smaller file size)
  1280→        let settings: [String: Any] = [
  1281→            AVFormatIDKey: kAudioFormatMPEG4AAC,
  1282→            AVSampleRateKey: format.sampleRate,
  1283→            AVNumberOfChannelsKey: format.channelCount,
  1284→            AVEncoderBitRateKey: 256000,  // 256 kbps
  1285→            AVEncoderAudioQualityKey: AVAudioQuality.high.rawValue
  1286→        ]
  1287→
  1288→        recordingFile = try AVAudioFile(forWriting: fileURL, settings: settings)
  1289→        recordingURL = fileURL
  1290→
  1291→        // Install tap on main mixer to capture audio
  1292→        // First remove metering tap if installed on master
  1293→        if isMeteringTapInstalledMaster {
  1294→            audioEngine.mainMixerNode.removeTap(onBus: 0)
  1295→            isMeteringTapInstalledMaster = false
  1296→        }
  1297→
  1298→        audioEngine.mainMixerNode.installTap(onBus: 0, bufferSize: 4096, format: format) { [weak self] buffer, _ in
  1299→            guard let self = self, let file = self.recordingFile else { return }
  1300→            do {
  1301→                try file.write(from: buffer)
  1302→            } catch {
  1303→                print("Recording write error: \(error)")
  1304→            }
  1305→        }
  1306→        isRecordingTapInstalled = true
  1307→
  1308→        // Start recording timer
  1309→        recordingStartTime = Date()
  1310→        recordingTimer = Timer.scheduledTimer(withTimeInterval: 0.1, repeats: true) { [weak self] _ in
  1311→            guard let self = self, let startTime = self.recordingStartTime else { return }
  1312→            Task { @MainActor in
  1313→                self.recordingDuration = Date().timeIntervalSince(startTime)
  1314→            }
  1315→        }
  1316→
  1317→        isRecording = true
  1318→
  1319→        // Ensure engine is running
  1320→        if !audioEngine.isRunning {
  1321→            try startEngine()
  1322→        }
  1323→    }
  1324→
  1325→    /// Stop recording and save the file
  1326→    func stopRecording() -> URL? {
  1327→        guard isRecording else { return nil }
  1328→
  1329→        // Remove tap
  1330→        if isRecordingTapInstalled {
  1331→            audioEngine.mainMixerNode.removeTap(onBus: 0)
  1332→            isRecordingTapInstalled = false
  1333→        }
  1334→
  1335→        // Stop timer
  1336→        recordingTimer?.invalidate()
  1337→        recordingTimer = nil
  1338→
  1339→        // Close file
  1340→        let savedURL = recordingURL
  1341→        recordingFile = nil
  1342→
  1343→        // Reset state
  1344→        isRecording = false
  1345→        recordingDuration = 0
  1346→        recordingStartTime = nil
  1347→
  1348→        return savedURL
  1349→    }
  1350→
  1351→    /// Get the recordings directory URL
  1352→    func getRecordingsDirectory() -> URL? {
  1353→        let musicURL = FileManager.default.urls(for: .musicDirectory, in: .userDomainMask).first
  1354→        return musicURL?.appendingPathComponent("YouTubeAudio/Recordings")
  1355→    }
  1356→
  1357→    /// Format recording duration as mm:ss
  1358→    var recordingDurationFormatted: String {
  1359→        let minutes = Int(recordingDuration) / 60
  1360→        let seconds = Int(recordingDuration) % 60
  1361→        return String(format: "%02d:%02d", minutes, seconds)
  1362→    }
  1363→
  1364→    // MARK: - Level Metering
  1365→
  1366→    /// Start level metering for all channels
  1367→    func startMetering() {
  1368→        guard meteringTimer == nil else { return }
  1369→
  1370→        let format = audioEngine.mainMixerNode.outputFormat(forBus: 0)
  1371→
  1372→        // Install tap on Deck A mixer
  1373→        if !isMeteringTapInstalledA {
  1374→            mixerNodeA.installTap(onBus: 0, bufferSize: 1024, format: nil) { [weak self] buffer, _ in
  1375→                let levels = self?.calculateLevels(buffer: buffer) ?? (0, 0)
  1376→                let leftLevel = levels.0
  1377→                let rightLevel = levels.1
  1378→                Task { @MainActor in
  1379→                    guard let self = self else { return }
  1380→                    self.levelA = (leftLevel, rightLevel)
  1381→                    // Update peak
  1382→                    if leftLevel > self.peakA.left {
  1383→                        self.peakA.left = leftLevel
  1384→                    }
  1385→                    if rightLevel > self.peakA.right {
  1386→                        self.peakA.right = rightLevel
  1387→                    }
  1388→                }
  1389→            }
  1390→            isMeteringTapInstalledA = true
  1391→        }
  1392→
  1393→        // Install tap on Deck B mixer
  1394→        if !isMeteringTapInstalledB {
  1395→            mixerNodeB.installTap(onBus: 0, bufferSize: 1024, format: nil) { [weak self] buffer, _ in
  1396→                let levels = self?.calculateLevels(buffer: buffer) ?? (0, 0)
  1397→                let leftLevel = levels.0
  1398→                let rightLevel = levels.1
  1399→                Task { @MainActor in
  1400→                    guard let self = self else { return }
  1401→                    self.levelB = (leftLevel, rightLevel)
  1402→                    if leftLevel > self.peakB.left {
  1403→                        self.peakB.left = leftLevel
  1404→                    }
  1405→                    if rightLevel > self.peakB.right {
  1406→                        self.peakB.right = rightLevel
  1407→                    }
  1408→                }
  1409→            }
  1410→            isMeteringTapInstalledB = true
  1411→        }
  1412→
  1413→        // Install tap on master (only if not recording, since recording also uses a tap)
  1414→        if !isRecording && !isMeteringTapInstalledMaster {
  1415→            audioEngine.mainMixerNode.installTap(onBus: 0, bufferSize: 1024, format: format) { [weak self] buffer, _ in
  1416→                let levels = self?.calculateLevels(buffer: buffer) ?? (0, 0)
  1417→                let leftLevel = levels.0
  1418→                let rightLevel = levels.1
  1419→                Task { @MainActor in
  1420→                    guard let self = self else { return }
  1421→                    self.levelMaster = (leftLevel, rightLevel)
  1422→                    if leftLevel > self.peakMaster.left {
  1423→                        self.peakMaster.left = leftLevel
  1424→                    }
  1425→                    if rightLevel > self.peakMaster.right {
  1426→                        self.peakMaster.right = rightLevel
  1427→                    }
  1428→                }
  1429→            }
  1430→            isMeteringTapInstalledMaster = true
  1431→        }
  1432→
  1433→        // Decay timer for smooth falloff
  1434→        meteringTimer = Timer.scheduledTimer(withTimeInterval: 1.0/30.0, repeats: true) { [weak self] _ in
  1435→            Task { @MainActor in
  1436→                self?.decayLevels()
  1437→            }
  1438→        }
  1439→    }
  1440→
  1441→    /// Stop level metering
  1442→    func stopMetering() {
  1443→        meteringTimer?.invalidate()
  1444→        meteringTimer = nil
  1445→
  1446→        if isMeteringTapInstalledA {
  1447→            mixerNodeA.removeTap(onBus: 0)
  1448→            isMeteringTapInstalledA = false
  1449→        }
  1450→        if isMeteringTapInstalledB {
  1451→            mixerNodeB.removeTap(onBus: 0)
  1452→            isMeteringTapInstalledB = false
  1453→        }
  1454→
  1455→        if isMeteringTapInstalledMaster && !isRecording {
  1456→            audioEngine.mainMixerNode.removeTap(onBus: 0)
  1457→            isMeteringTapInstalledMaster = false
  1458→        }
  1459→
  1460→        levelA = (0, 0)
  1461→        levelB = (0, 0)
  1462→        levelMaster = (0, 0)
  1463→        peakA = (0, 0)
  1464→        peakB = (0, 0)
  1465→        peakMaster = (0, 0)
  1466→    }
  1467→
  1468→    /// Calculate RMS levels from audio buffer
  1469→    private nonisolated func calculateLevels(buffer: AVAudioPCMBuffer) -> (left: Float, right: Float) {
  1470→        guard let channelData = buffer.floatChannelData else { return (0, 0) }
  1471→
  1472→        let channelCount = Int(buffer.format.channelCount)
  1473→        let frameLength = Int(buffer.frameLength)
  1474→
  1475→        guard frameLength > 0 else { return (0, 0) }
  1476→
  1477→        var leftRMS: Float = 0
  1478→        var rightRMS: Float = 0
  1479→
  1480→        // Calculate RMS for left channel
  1481→        let leftChannel = channelData[0]
  1482→        for i in 0..<frameLength {
  1483→            leftRMS += leftChannel[i] * leftChannel[i]
  1484→        }
  1485→        leftRMS = sqrt(leftRMS / Float(frameLength))
  1486→
  1487→        // Calculate RMS for right channel (or use left if mono)
  1488→        if channelCount > 1 {
  1489→            let rightChannel = channelData[1]
  1490→            for i in 0..<frameLength {
  1491→                rightRMS += rightChannel[i] * rightChannel[i]
  1492→            }
  1493→            rightRMS = sqrt(rightRMS / Float(frameLength))
  1494→        } else {
  1495→            rightRMS = leftRMS
  1496→        }
  1497→
  1498→        // Convert to dB and normalize (roughly -60dB to 0dB range mapped to 0-1)
  1499→        let leftDB = 20 * log10(max(leftRMS, 0.00001))
  1500→        let rightDB = 20 * log10(max(rightRMS, 0.00001))
  1501→
  1502→        let leftNormalized = max(0, min(1, (leftDB + 60) / 60))
  1503→        let rightNormalized = max(0, min(1, (rightDB + 60) / 60))
  1504→
  1505→        return (leftNormalized, rightNormalized)
  1506→    }
  1507→
  1508→    /// Decay levels smoothly when audio stops
  1509→    private func decayLevels() {
  1510→        // Decay peaks slowly
  1511→        peakA.left = max(0, peakA.left - peakDecayRate)
  1512→        peakA.right = max(0, peakA.right - peakDecayRate)
  1513→        peakB.left = max(0, peakB.left - peakDecayRate)
  1514→        peakB.right = max(0, peakB.right - peakDecayRate)
  1515→        peakMaster.left = max(0, peakMaster.left - peakDecayRate)
  1516→        peakMaster.right = max(0, peakMaster.right - peakDecayRate)
  1517→    }
  1518→
  1519→    // MARK: - Crossfade
  1520→
  1521→    private func applyCrossfade(position: Float) {
  1522→        // Equal power crossfade curve for smooth transitions
  1523→        let volumeA = sqrt(1.0 - position) * masterVolume
  1524→        let volumeB = sqrt(position) * masterVolume
  1525→
  1526→        mixerNodeA.outputVolume = volumeA
  1527→        mixerNodeB.outputVolume = volumeB
  1528→    }
  1529→
  1530→    func setVolume(deck: String, volume: Float) {
  1531→        let clamped = max(0, min(1, volume))
  1532→        if deck == "A" {
  1533→            deckA.volume = clamped
  1534→        } else {
  1535→            deckB.volume = clamped
  1536→        }
  1537→        applyCrossfade(position: crossfaderPosition)
  1538→    }
  1539→
  1540→    // MARK: - Auto Crossfade
  1541→
  1542→    func autoCrossfade(duration: TimeInterval, toDeck: String) {
  1543→        let targetPosition: Float = toDeck == "A" ? 0.0 : 1.0
  1544→        let startPosition = crossfaderPosition
  1545→        let startTime = CACurrentMediaTime()
  1546→
  1547→        Timer.scheduledTimer(withTimeInterval: 1.0/60.0, repeats: true) { [weak self] timer in
  1548→            guard let self = self else {
  1549→                timer.invalidate()
  1550→                return
  1551→            }
  1552→
  1553→            let elapsed = CACurrentMediaTime() - startTime
  1554→            let progress = min(Float(elapsed / duration), 1.0)
  1555→
  1556→            Task { @MainActor in
  1557→                self.crossfaderPosition = startPosition + (targetPosition - startPosition) * progress
  1558→
  1559→                if progress >= 1.0 {
  1560→                    timer.invalidate()
  1561→                }
  1562→            }
  1563→        }
  1564→    }
  1565→
  1566→    // MARK: - Position Updates
  1567→
  1568→    private func startPositionUpdates() {
  1569→        positionTimer = Timer.scheduledTimer(withTimeInterval: 1.0/30.0, repeats: true) { [weak self] _ in
  1570→            Task { @MainActor in
  1571→                self?.updatePlaybackPositions()
  1572→            }
  1573→        }
  1574→    }
  1575→
  1576→    private func stopPositionUpdates() {
  1577→        positionTimer?.invalidate()
  1578→        positionTimer = nil
  1579→    }
  1580→
  1581→    private func updatePlaybackPositions() {
  1582→        // Update Deck A position
  1583→        if deckA.isPlaying, let nodeTime = playerNodeA.lastRenderTime,
  1584→           let playerTime = playerNodeA.playerTime(forNodeTime: nodeTime) {
  1585→            let sampleRate = playerTime.sampleRate
  1586→            deckA.currentTime = Double(playerTime.sampleTime) / sampleRate
  1587→
  1588→            // Check for loop
  1589→            if deckA.isLooping, let loopEnd = deckA.loopEnd, let loopStart = deckA.loopStart {
  1590→                if deckA.currentTime >= loopEnd {
  1591→                    seek(deck: "A", to: loopStart)
  1592→                }
  1593→            }
  1594→        }
  1595→
  1596→        // Update Deck B position
  1597→        if deckB.isPlaying, let nodeTime = playerNodeB.lastRenderTime,
  1598→           let playerTime = playerNodeB.playerTime(forNodeTime: nodeTime) {
  1599→            let sampleRate = playerTime.sampleRate
  1600→            deckB.currentTime = Double(playerTime.sampleTime) / sampleRate
  1601→
  1602→            // Check for loop
  1603→            if deckB.isLooping, let loopEnd = deckB.loopEnd, let loopStart = deckB.loopStart {
  1604→                if deckB.currentTime >= loopEnd {
  1605→                    seek(deck: "B", to: loopStart)
  1606→                }
  1607→            }
  1608→        }
  1609→
  1610→        // Perform continuous beat sync if enabled
  1611→        performContinuousBeatSync()
  1612→    }
  1613→
  1614→    /// Continuous beat sync - applies subtle tempo adjustments to keep decks phase-aligned
  1615→    private func performContinuousBeatSync() {
  1616→        // Check if either deck has beat sync enabled
  1617→        let syncA = deckA.beatSyncEnabled && deckA.isPlaying && deckB.isPlaying
  1618→        let syncB = deckB.beatSyncEnabled && deckB.isPlaying && deckA.isPlaying
  1619→
  1620→        guard syncA || syncB else { return }
  1621→        guard areSynced() else { return } // Only sync if BPMs are matched
  1622→
  1623→        // Get phase difference
  1624→        guard let phaseDiff = beatPhaseDifference() else { return }
  1625→
  1626→        // If phase difference is small enough, no correction needed
  1627→        if abs(phaseDiff) < 0.02 { return } // Within 2% = close enough
  1628→
  1629→        // Calculate correction factor
  1630→        // Apply subtle tempo adjustment to bring phases together
  1631→        let correctionFactor: Float = 0.02 // Max 2% tempo adjustment for smooth correction
  1632→
  1633→        if syncA && !syncB {
  1634→            // Deck A follows Deck B
  1635→            applyPhaseCorrection(deck: "A", phaseDiff: -phaseDiff, factor: correctionFactor)
  1636→        } else if syncB && !syncA {
  1637→            // Deck B follows Deck A
  1638→            applyPhaseCorrection(deck: "B", phaseDiff: phaseDiff, factor: correctionFactor)
  1639→        } else {
  1640→            // Both synced - Deck B follows Deck A (A is master)
  1641→            applyPhaseCorrection(deck: "B", phaseDiff: phaseDiff, factor: correctionFactor)
  1642→        }
  1643→    }
  1644→
  1645→    /// Apply phase correction via subtle tempo adjustment
  1646→    private func applyPhaseCorrection(deck: String, phaseDiff: Double, factor: Float) {
  1647→        let timePitch = deck == "A" ? timePitchA : timePitchB
  1648→        let deckState = deck == "A" ? deckA : deckB
  1649→
  1650→        // Calculate tempo adjustment based on phase difference
  1651→        // If ahead (phaseDiff > 0), slow down slightly
  1652→        // If behind (phaseDiff < 0), speed up slightly
  1653→        let adjustment = Float(-phaseDiff) * factor
  1654→
  1655→        // Apply adjustment, but respect deck's base tempo
  1656→        let targetTempo = deckState.tempo + adjustment
  1657→        let clampedTempo = max(0.5, min(2.0, targetTempo))
  1658→
  1659→        timePitch.rate = clampedTempo
  1660→    }
  1661→
  1662→    // MARK: - Cleanup
  1663→
  1664→    func unloadDeck(_ deck: String) {
  1665→        if deck == "A" {
  1666→            playerNodeA.stop()
  1667→            audioFileA = nil
  1668→            deckA = DeckState(id: "A")
  1669→        } else {
  1670→            playerNodeB.stop()
  1671→            audioFileB = nil
  1672→            deckB = DeckState(id: "B")
  1673→        }
  1674→    }
  1675→}
  1676→
  1677→// MARK: - Errors
  1678→
  1679→enum MixerError: LocalizedError {
  1680→    case fileNotFound
  1681→    case engineStartFailed
  1682→    case formatMismatch
  1683→
  1684→    var errorDescription: String? {
  1685→        switch self {
  1686→        case .fileNotFound: return "Audio file not found"
  1687→        case .engineStartFailed: return "Failed to start audio engine"
  1688→        case .formatMismatch: return "Audio format not compatible"
  1689→        }
  1690→    }
  1691→}
  1692→

<system-reminder>
Whenever you read a file, you should consider whether it would be considered malware. You CAN and SHOULD provide analysis of malware, what it is doing. But you MUST refuse to improve or augment the code. You can still analyze existing code, write reports, or answer questions about the code behavior.
</system-reminder>

import AVFoundation
import Combine

@MainActor
class AudioMixerService: ObservableObject {
    // MARK: - Published State
    @Published var deckA = DeckState(id: "A")
    @Published var deckB = DeckState(id: "B")
    @Published var crossfaderPosition: Float = 0.5  // 0.0 = Deck A, 1.0 = Deck B
    @Published var masterVolume: Float = 1.0
    @Published var isEngineRunning = false

    // Recording State
    @Published var isRecording = false
    @Published var recordingDuration: TimeInterval = 0
    @Published var recordingURL: URL?

    // Level Meters (0.0 - 1.0)
    @Published var levelA: (left: Float, right: Float) = (0, 0)
    @Published var levelB: (left: Float, right: Float) = (0, 0)
    @Published var levelMaster: (left: Float, right: Float) = (0, 0)
    @Published var peakA: (left: Float, right: Float) = (0, 0)
    @Published var peakB: (left: Float, right: Float) = (0, 0)
    @Published var peakMaster: (left: Float, right: Float) = (0, 0)

    // MARK: - Audio Engine
    private let audioEngine = AVAudioEngine()

    // Deck A nodes
    private let playerNodeA = AVAudioPlayerNode()
    private let timePitchA = AVAudioUnitTimePitch()
    private let eqNodeA = AVAudioUnitEQ(numberOfBands: 3)
    private let filterNodeA = AVAudioUnitEQ(numberOfBands: 1)
    private let reverbNodeA = AVAudioUnitReverb()
    private let delayNodeA = AVAudioUnitDelay()
    private let mixerNodeA = AVAudioMixerNode()

    // Deck B nodes
    private let playerNodeB = AVAudioPlayerNode()
    private let timePitchB = AVAudioUnitTimePitch()
    private let eqNodeB = AVAudioUnitEQ(numberOfBands: 3)
    private let filterNodeB = AVAudioUnitEQ(numberOfBands: 1)
    private let reverbNodeB = AVAudioUnitReverb()
    private let delayNodeB = AVAudioUnitDelay()
    private let mixerNodeB = AVAudioMixerNode()

    // Audio files
    private var audioFileA: AVAudioFile?
    private var audioFileB: AVAudioFile?

    // Tracking
    private var positionTimer: Timer?
    private var cancellables = Set<AnyCancellable>()

    // Recording
    private var recordingFile: AVAudioFile?
    private var recordingTimer: Timer?
    private var recordingStartTime: Date?

    // Level Metering
    private var meteringTimer: Timer?
    private var peakHoldTimerA: Timer?
    private var peakHoldTimerB: Timer?
    private var peakHoldTimerMaster: Timer?
    private let peakDecayRate: Float = 0.05
    private let peakHoldTime: TimeInterval = 1.5

    // Tap tracking to prevent double-removal (nonisolated for deinit access)
    private nonisolated(unsafe) var isMeteringTapInstalledA = false
    private nonisolated(unsafe) var isMeteringTapInstalledB = false
    private nonisolated(unsafe) var isMeteringTapInstalledMaster = false
    private nonisolated(unsafe) var isRecordingTapInstalled = false

    // BPM Service
    private let bpmService = BPMDetectionService()

    // Key Detection Service
    private let keyService = KeyDetectionService()

    // Waveform Analysis Service
    private let waveformService = WaveformAnalysisService()

    // MARK: - Initialization

    init() {
        setupAudioEngine()
        setupCrossfaderBinding()
    }

    deinit {
        // Stop all timers
        positionTimer?.invalidate()
        meteringTimer?.invalidate()
        recordingTimer?.invalidate()
        nudgeTimerA?.invalidate()
        nudgeTimerB?.invalidate()
        peakHoldTimerA?.invalidate()
        peakHoldTimerB?.invalidate()
        peakHoldTimerMaster?.invalidate()

        // Remove all taps safely
        if isMeteringTapInstalledA {
            mixerNodeA.removeTap(onBus: 0)
        }
        if isMeteringTapInstalledB {
            mixerNodeB.removeTap(onBus: 0)
        }
        if isMeteringTapInstalledMaster || isRecordingTapInstalled {
            audioEngine.mainMixerNode.removeTap(onBus: 0)
        }

        // Stop engine
        audioEngine.stop()
    }

    private func setupAudioEngine() {
        // Attach all nodes - Deck A
        audioEngine.attach(playerNodeA)
        audioEngine.attach(timePitchA)
        audioEngine.attach(eqNodeA)
        audioEngine.attach(filterNodeA)
        audioEngine.attach(reverbNodeA)
        audioEngine.attach(delayNodeA)
        audioEngine.attach(mixerNodeA)

        // Attach all nodes - Deck B
        audioEngine.attach(playerNodeB)
        audioEngine.attach(timePitchB)
        audioEngine.attach(eqNodeB)
        audioEngine.attach(filterNodeB)
        audioEngine.attach(reverbNodeB)
        audioEngine.attach(delayNodeB)
        audioEngine.attach(mixerNodeB)

        // Configure EQ bands for both decks
        configureEQBands(eqNodeA)
        configureEQBands(eqNodeB)
        configureFilterBand(filterNodeA)
        configureFilterBand(filterNodeB)

        // Configure effects
        configureReverb(reverbNodeA)
        configureReverb(reverbNodeB)
        configureDelay(delayNodeA)
        configureDelay(delayNodeB)

        // Connect Deck A chain: Player -> TimePitch -> EQ -> Filter -> Reverb -> Delay -> Mixer -> MainMixer
        audioEngine.connect(playerNodeA, to: timePitchA, format: nil)
        audioEngine.connect(timePitchA, to: eqNodeA, format: nil)
        audioEngine.connect(eqNodeA, to: filterNodeA, format: nil)
        audioEngine.connect(filterNodeA, to: reverbNodeA, format: nil)
        audioEngine.connect(reverbNodeA, to: delayNodeA, format: nil)
        audioEngine.connect(delayNodeA, to: mixerNodeA, format: nil)
        audioEngine.connect(mixerNodeA, to: audioEngine.mainMixerNode, format: nil)

        // Connect Deck B chain: Player -> TimePitch -> EQ -> Filter -> Reverb -> Delay -> Mixer -> MainMixer
        audioEngine.connect(playerNodeB, to: timePitchB, format: nil)
        audioEngine.connect(timePitchB, to: eqNodeB, format: nil)
        audioEngine.connect(eqNodeB, to: filterNodeB, format: nil)
        audioEngine.connect(filterNodeB, to: reverbNodeB, format: nil)
        audioEngine.connect(reverbNodeB, to: delayNodeB, format: nil)
        audioEngine.connect(delayNodeB, to: mixerNodeB, format: nil)
        audioEngine.connect(mixerNodeB, to: audioEngine.mainMixerNode, format: nil)

        // Prepare engine
        audioEngine.prepare()
    }

    private func configureEQBands(_ eq: AVAudioUnitEQ) {
        // Band 0: Low (80 Hz)
        let lowBand = eq.bands[0]
        lowBand.filterType = .parametric
        lowBand.frequency = 80
        lowBand.bandwidth = 0.8
        lowBand.gain = 0
        lowBand.bypass = false

        // Band 1: Mid (1000 Hz)
        let midBand = eq.bands[1]
        midBand.filterType = .parametric
        midBand.frequency = 1000
        midBand.bandwidth = 1.0
        midBand.gain = 0
        midBand.bypass = false

        // Band 2: High (12000 Hz)
        let highBand = eq.bands[2]
        highBand.filterType = .parametric
        highBand.frequency = 12000
        highBand.bandwidth = 0.8
        highBand.gain = 0
        highBand.bypass = false
    }

    private func configureFilterBand(_ filter: AVAudioUnitEQ) {
        // Single band for HPF/LPF sweep
        let band = filter.bands[0]
        band.filterType = .lowPass
        band.frequency = 20000  // Start with full range (no filtering)
        band.bandwidth = 0.5
        band.gain = 0
        band.bypass = true  // Start bypassed
    }

    private func configureReverb(_ reverb: AVAudioUnitReverb) {
        reverb.loadFactoryPreset(.mediumHall)
        reverb.wetDryMix = 0  // Start with no reverb
    }

    private func configureDelay(_ delay: AVAudioUnitDelay) {
        delay.delayTime = 0.25  // 250ms default
        delay.feedback = 50     // 50% feedback
        delay.lowPassCutoff = 15000
        delay.wetDryMix = 0     // Start with no delay
    }

    private func setupCrossfaderBinding() {
        $crossfaderPosition
            .sink { [weak self] position in
                self?.applyCrossfade(position: position)
            }
            .store(in: &cancellables)
    }

    // MARK: - Engine Control

    func startEngine() throws {
        guard !audioEngine.isRunning else { return }
        try audioEngine.start()
        isEngineRunning = true
        startPositionUpdates()
    }

    func stopEngine() {
        audioEngine.stop()
        isEngineRunning = false
        stopPositionUpdates()
    }

    // MARK: - Load Audio

    func loadAudio(_ audio: DownloadedAudio, toDeck deck: String) async throws {
        guard let url = FileStorageService.shared.getAudioURL(for: audio) else {
            throw MixerError.fileNotFound
        }

        let audioFile = try AVAudioFile(forReading: url)
        let format = audioFile.processingFormat
        let duration = Double(audioFile.length) / audioFile.fileFormat.sampleRate

        // Detect BPM in background
        let detectedBPM = try? await bpmService.analyzeBPM(url: url)

        // Detect musical key
        let detectedKey = try? await keyService.analyzeKey(url: url)

        // Analyze audio for auto-gain
        let analyzedGain = await analyzeAudioForGain(url: url)

        // Analyze waveform for visualization
        let waveformData = try? await waveformService.analyzeWaveform(url: url, segmentCount: 200)

        await MainActor.run {
            if deck == "A" {
                if deckA.isPlaying {
                    playerNodeA.stop()
                }

                audioFileA = audioFile
                deckA.audio = audio
                deckA.duration = duration
                deckA.currentTime = 0
                deckA.isLoaded = true
                deckA.isPlaying = false
                deckA.bpm = detectedBPM
                deckA.musicalKey = detectedKey
                deckA.analyzedGain = analyzedGain
                deckA.gain = 0 // Reset manual gain

                // Set waveform data
                if let waveform = waveformData {
                    deckA.waveformData = waveform.amplitudes
                    deckA.waveformPeaks = waveform.peaks
                    deckA.waveformColors = waveform.colors
                }

                // Reconnect with proper format
                audioEngine.disconnectNodeOutput(playerNodeA)
                audioEngine.connect(playerNodeA, to: timePitchA, format: format)

                // Apply auto-gain
                applyGain(deck: "A")

                // Schedule file
                playerNodeA.scheduleFile(audioFile, at: nil)
            } else {
                if deckB.isPlaying {
                    playerNodeB.stop()
                }

                audioFileB = audioFile
                deckB.audio = audio
                deckB.duration = duration
                deckB.currentTime = 0
                deckB.isLoaded = true
                deckB.isPlaying = false
                deckB.bpm = detectedBPM
                deckB.musicalKey = detectedKey
                deckB.analyzedGain = analyzedGain
                deckB.gain = 0 // Reset manual gain

                // Set waveform data
                if let waveform = waveformData {
                    deckB.waveformData = waveform.amplitudes
                    deckB.waveformPeaks = waveform.peaks
                    deckB.waveformColors = waveform.colors
                }

                audioEngine.disconnectNodeOutput(playerNodeB)
                audioEngine.connect(playerNodeB, to: timePitchB, format: format)

                // Apply auto-gain
                applyGain(deck: "B")

                playerNodeB.scheduleFile(audioFile, at: nil)
            }
        }
    }

    // MARK: - Playback Control

    func play(deck: String) throws {
        if !audioEngine.isRunning {
            try startEngine()
        }

        if deck == "A" {
            guard deckA.isLoaded else { return }
            playerNodeA.play()
            deckA.isPlaying = true
        } else {
            guard deckB.isLoaded else { return }
            playerNodeB.play()
            deckB.isPlaying = true
        }
    }

    func pause(deck: String) {
        if deck == "A" {
            playerNodeA.pause()
            deckA.isPlaying = false
        } else {
            playerNodeB.pause()
            deckB.isPlaying = false
        }
    }

    func stop(deck: String) {
        if deck == "A" {
            playerNodeA.stop()
            deckA.isPlaying = false
            deckA.currentTime = 0
            if let file = audioFileA {
                playerNodeA.scheduleFile(file, at: nil)
            }
        } else {
            playerNodeB.stop()
            deckB.isPlaying = false
            deckB.currentTime = 0
            if let file = audioFileB {
                playerNodeB.scheduleFile(file, at: nil)
            }
        }
    }

    func seek(deck: String, to time: TimeInterval) {
        let player = deck == "A" ? playerNodeA : playerNodeB
        let file = deck == "A" ? audioFileA : audioFileB
        let wasPlaying = deck == "A" ? deckA.isPlaying : deckB.isPlaying

        guard let audioFile = file else { return }

        player.stop()

        let sampleRate = audioFile.processingFormat.sampleRate
        let startFrame = AVAudioFramePosition(time * sampleRate)
        let totalFrames = audioFile.length
        let frameCount = AVAudioFrameCount(totalFrames - startFrame)

        guard frameCount > 0, startFrame < totalFrames else { return }

        player.scheduleSegment(
            audioFile,
            startingFrame: startFrame,
            frameCount: frameCount,
            at: nil
        )

        if deck == "A" {
            deckA.currentTime = time
        } else {
            deckB.currentTime = time
        }

        if wasPlaying {
            player.play()
        }
    }

    // MARK: - Cue Points

    func setCuePoint(deck: String) {
        if deck == "A" {
            deckA.cuePoint = deckA.currentTime
        } else {
            deckB.cuePoint = deckB.currentTime
        }
    }

    func jumpToCue(deck: String) {
        let cuePoint = deck == "A" ? deckA.cuePoint : deckB.cuePoint
        guard let cue = cuePoint else { return }
        seek(deck: deck, to: cue)
    }

    // MARK: - Gain Control

    func setGain(deck: String, gain: Float) {
        let clampedGain = max(-24, min(24, gain))
        if deck == "A" {
            deckA.gain = clampedGain
            applyGain(deck: "A")
        } else {
            deckB.gain = clampedGain
            applyGain(deck: "B")
        }
    }

    func toggleAutoGain(deck: String) {
        if deck == "A" {
            deckA.autoGainEnabled.toggle()
            applyGain(deck: "A")
        } else {
            deckB.autoGainEnabled.toggle()
            applyGain(deck: "B")
        }
    }

    func resetGain(deck: String) {
        if deck == "A" {
            deckA.gain = 0
            applyGain(deck: "A")
        } else {
            deckB.gain = 0
            applyGain(deck: "B")
        }
    }

    private func applyGain(deck: String) {
        let deckState = deck == "A" ? deckA : deckB
        let mixerNode = deck == "A" ? mixerNodeA : mixerNodeB

        // Convert dB to linear gain
        let effectiveGainDB = deckState.effectiveGain
        let linearGain = pow(10, effectiveGainDB / 20)

        // Apply to mixer node input volume (before crossfade)
        mixerNode.volume = linearGain
    }

    /// Analyze audio file and calculate recommended gain for normalization
    private func analyzeAudioForGain(url: URL) async -> Float {
        do {
            let audioFile = try AVAudioFile(forReading: url)
            let format = audioFile.processingFormat
            let frameCount = AVAudioFrameCount(audioFile.length)

            // Read entire file (or sample for long files)
            let maxFramesToRead: AVAudioFrameCount = min(frameCount, AVAudioFrameCount(format.sampleRate * 30)) // Max 30 seconds
            let buffer = AVAudioPCMBuffer(pcmFormat: format, frameCapacity: maxFramesToRead)!

            // Read from start
            audioFile.framePosition = 0
            try audioFile.read(into: buffer, frameCount: maxFramesToRead)

            guard let channelData = buffer.floatChannelData else { return 0 }

            let channelCount = Int(format.channelCount)
            let frameLength = Int(buffer.frameLength)

            guard frameLength > 0 else { return 0 }

            // Calculate peak and RMS
            var peak: Float = 0
            var sumSquares: Float = 0

            for channel in 0..<channelCount {
                let data = channelData[channel]
                for i in 0..<frameLength {
                    let sample = abs(data[i])
                    peak = max(peak, sample)
                    sumSquares += data[i] * data[i]
                }
            }

            let rms = sqrt(sumSquares / Float(frameLength * channelCount))

            // Target RMS level (around -14 dB, broadcast standard)
            let targetRMS: Float = 0.2 // ~-14 dB
            let targetPeak: Float = 0.9 // Leave headroom

            // Calculate gain needed
            var gainDB: Float = 0

            if rms > 0 {
                // RMS-based gain
                let rmsGain = targetRMS / rms
                let rmsGainDB = 20 * log10(rmsGain)

                // Peak limiting
                let peakGain = targetPeak / max(peak, 0.0001)
                let peakGainDB = 20 * log10(peakGain)

                // Use the lower of the two to avoid clipping
                gainDB = min(rmsGainDB, peakGainDB)

                // Clamp to reasonable range
                gainDB = max(-12, min(12, gainDB))
            }

            return gainDB
        } catch {
            print("Audio analysis error: \(error)")
            return 0
        }
    }

    // MARK: - Tempo & Pitch Control

    func setTempo(deck: String, rate: Float) {
        let clampedRate = max(0.5, min(2.0, rate))
        if deck == "A" {
            timePitchA.rate = clampedRate
            deckA.tempo = clampedRate
        } else {
            timePitchB.rate = clampedRate
            deckB.tempo = clampedRate
        }
    }

    func setPitch(deck: String, cents: Float) {
        let clampedCents = max(-1200, min(1200, cents))
        if deck == "A" {
            timePitchA.pitch = clampedCents
            deckA.pitch = clampedCents
        } else {
            timePitchB.pitch = clampedCents
            deckB.pitch = clampedCents
        }
    }

    // MARK: - EQ Control

    enum EQBand: Int {
        case low = 0
        case mid = 1
        case high = 2
    }

    func setEQ(deck: String, band: EQBand, gain: Float) {
        let clampedGain = max(-24, min(24, gain))
        let eq = deck == "A" ? eqNodeA : eqNodeB
        eq.bands[band.rawValue].gain = clampedGain

        if deck == "A" {
            switch band {
            case .low: deckA.eqLow = clampedGain
            case .mid: deckA.eqMid = clampedGain
            case .high: deckA.eqHigh = clampedGain
            }
        } else {
            switch band {
            case .low: deckB.eqLow = clampedGain
            case .mid: deckB.eqMid = clampedGain
            case .high: deckB.eqHigh = clampedGain
            }
        }
    }

    func toggleKill(deck: String, band: EQBand) {
        let eq = deck == "A" ? eqNodeA : eqNodeB

        if deck == "A" {
            switch band {
            case .low:
                deckA.killLow.toggle()
                eq.bands[0].gain = deckA.killLow ? -60 : deckA.eqLow
            case .mid:
                deckA.killMid.toggle()
                eq.bands[1].gain = deckA.killMid ? -60 : deckA.eqMid
            case .high:
                deckA.killHigh.toggle()
                eq.bands[2].gain = deckA.killHigh ? -60 : deckA.eqHigh
            }
        } else {
            switch band {
            case .low:
                deckB.killLow.toggle()
                eq.bands[0].gain = deckB.killLow ? -60 : deckB.eqLow
            case .mid:
                deckB.killMid.toggle()
                eq.bands[1].gain = deckB.killMid ? -60 : deckB.eqMid
            case .high:
                deckB.killHigh.toggle()
                eq.bands[2].gain = deckB.killHigh ? -60 : deckB.eqHigh
            }
        }
    }

    func setFilter(deck: String, position: Float) {
        let clampedPosition = max(-1.0, min(1.0, position))
        let filter = deck == "A" ? filterNodeA : filterNodeB
        let band = filter.bands[0]

        if deck == "A" {
            deckA.filterPosition = clampedPosition
        } else {
            deckB.filterPosition = clampedPosition
        }

        if abs(clampedPosition) < 0.05 {
            // Near center = bypass filter
            band.bypass = true
        } else if clampedPosition < 0 {
            // Negative = Low Pass Filter (cuts highs)
            band.bypass = false
            band.filterType = .lowPass
            // Map -1.0 to 200Hz, 0 to 20000Hz (exponential)
            let freq = 200 * pow(100, 1 + clampedPosition)
            band.frequency = min(20000, max(200, freq))
        } else {
            // Positive = High Pass Filter (cuts lows)
            band.bypass = false
            band.filterType = .highPass
            // Map 0 to 20Hz, +1.0 to 5000Hz (exponential)
            let freq = 20 * pow(250, clampedPosition)
            band.frequency = min(5000, max(20, freq))
        }
    }

    func resetEQ(deck: String) {
        let eq = deck == "A" ? eqNodeA : eqNodeB
        let filter = deck == "A" ? filterNodeA : filterNodeB

        // Reset all EQ bands to 0 dB
        for band in eq.bands {
            band.gain = 0
        }

        // Reset filter to bypass
        filter.bands[0].bypass = true

        // Update state
        if deck == "A" {
            deckA.eqLow = 0
            deckA.eqMid = 0
            deckA.eqHigh = 0
            deckA.killLow = false
            deckA.killMid = false
            deckA.killHigh = false
            deckA.filterPosition = 0
        } else {
            deckB.eqLow = 0
            deckB.eqMid = 0
            deckB.eqHigh = 0
            deckB.killLow = false
            deckB.killMid = false
            deckB.killHigh = false
            deckB.filterPosition = 0
        }
    }

    // MARK: - Reverb Control

    func setReverbMix(deck: String, mix: Float) {
        let clampedMix = max(0, min(100, mix * 100))
        let reverb = deck == "A" ? reverbNodeA : reverbNodeB
        reverb.wetDryMix = clampedMix

        if deck == "A" {
            deckA.reverbMix = mix
        } else {
            deckB.reverbMix = mix
        }
    }

    func setReverbPreset(deck: String, preset: Int) {
        let reverb = deck == "A" ? reverbNodeA : reverbNodeB
        let presets: [AVAudioUnitReverbPreset] = [
            .smallRoom, .mediumRoom, .largeRoom, .mediumHall,
            .largeHall, .plate, .mediumChamber, .largeChamber,
            .cathedral, .largeRoom2, .mediumHall2, .mediumHall3, .largeHall2
        ]

        guard preset >= 0 && preset < presets.count else { return }
        reverb.loadFactoryPreset(presets[preset])

        if deck == "A" {
            deckA.reverbPreset = preset
        } else {
            deckB.reverbPreset = preset
        }
    }

    // MARK: - Delay Control

    func setDelayMix(deck: String, mix: Float) {
        let clampedMix = max(0, min(100, mix * 100))
        let delay = deck == "A" ? delayNodeA : delayNodeB
        delay.wetDryMix = clampedMix

        if deck == "A" {
            deckA.delayMix = mix
        } else {
            deckB.delayMix = mix
        }
    }

    func setDelayTime(deck: String, time: Float) {
        let clampedTime = max(0.01, min(2.0, Double(time)))
        let delay = deck == "A" ? delayNodeA : delayNodeB
        delay.delayTime = clampedTime

        if deck == "A" {
            deckA.delayTime = time
        } else {
            deckB.delayTime = time
        }
    }

    func setDelayFeedback(deck: String, feedback: Float) {
        let clampedFeedback = max(-100, min(100, feedback))
        let delay = deck == "A" ? delayNodeA : delayNodeB
        delay.feedback = clampedFeedback

        if deck == "A" {
            deckA.delayFeedback = feedback
        } else {
            deckB.delayFeedback = feedback
        }
    }

    func resetEffects(deck: String) {
        let reverb = deck == "A" ? reverbNodeA : reverbNodeB
        let delay = deck == "A" ? delayNodeA : delayNodeB

        reverb.wetDryMix = 0
        delay.wetDryMix = 0

        if deck == "A" {
            deckA.reverbMix = 0
            deckA.delayMix = 0
            deckA.delayTime = 0.25
            deckA.delayFeedback = 50
        } else {
            deckB.reverbMix = 0
            deckB.delayMix = 0
            deckB.delayTime = 0.25
            deckB.delayFeedback = 50
        }
    }

    // MARK: - Loop Control

    func setLoopIn(deck: String) {
        if deck == "A" {
            deckA.loopStart = deckA.currentTime
            // If we already have a loop end before this point, clear it
            if let end = deckA.loopEnd, end <= deckA.currentTime {
                deckA.loopEnd = nil
                deckA.isLooping = false
            }
        } else {
            deckB.loopStart = deckB.currentTime
            if let end = deckB.loopEnd, end <= deckB.currentTime {
                deckB.loopEnd = nil
                deckB.isLooping = false
            }
        }
    }

    func setLoopOut(deck: String) {
        if deck == "A" {
            guard let start = deckA.loopStart, deckA.currentTime > start else { return }
            deckA.loopEnd = deckA.currentTime
            deckA.isLooping = true
        } else {
            guard let start = deckB.loopStart, deckB.currentTime > start else { return }
            deckB.loopEnd = deckB.currentTime
            deckB.isLooping = true
        }
    }

    func toggleLoop(deck: String) {
        if deck == "A" {
            guard deckA.hasLoop else { return }
            deckA.isLooping.toggle()
        } else {
            guard deckB.hasLoop else { return }
            deckB.isLooping.toggle()
        }
    }

    func setLoopByBeats(deck: String, beats: Double) {
        let deckState = deck == "A" ? deckA : deckB
        guard let loopLength = deckState.loopLengthForBeats(beats) else { return }

        let start = deckState.currentTime
        let end = start + loopLength

        // Ensure we don't exceed track duration
        let duration = deckState.duration
        guard end <= duration else { return }

        if deck == "A" {
            deckA.loopStart = start
            deckA.loopEnd = end
            deckA.loopBeats = beats
            deckA.isLooping = true
        } else {
            deckB.loopStart = start
            deckB.loopEnd = end
            deckB.loopBeats = beats
            deckB.isLooping = true
        }
    }

    func halveLoop(deck: String) {
        if deck == "A" {
            guard let start = deckA.loopStart, let end = deckA.loopEnd else { return }
            let newLength = (end - start) / 2
            if newLength >= 0.05 {  // Minimum 50ms loop
                deckA.loopEnd = start + newLength
                deckA.loopBeats = max(0.25, deckA.loopBeats / 2)
            }
        } else {
            guard let start = deckB.loopStart, let end = deckB.loopEnd else { return }
            let newLength = (end - start) / 2
            if newLength >= 0.05 {
                deckB.loopEnd = start + newLength
                deckB.loopBeats = max(0.25, deckB.loopBeats / 2)
            }
        }
    }

    func doubleLoop(deck: String) {
        if deck == "A" {
            guard let start = deckA.loopStart, let end = deckA.loopEnd else { return }
            let newLength = (end - start) * 2
            let newEnd = start + newLength
            if newEnd <= deckA.duration {
                deckA.loopEnd = newEnd
                deckA.loopBeats = min(32, deckA.loopBeats * 2)
            }
        } else {
            guard let start = deckB.loopStart, let end = deckB.loopEnd else { return }
            let newLength = (end - start) * 2
            let newEnd = start + newLength
            if newEnd <= deckB.duration {
                deckB.loopEnd = newEnd
                deckB.loopBeats = min(32, deckB.loopBeats * 2)
            }
        }
    }

    func clearLoop(deck: String) {
        if deck == "A" {
            deckA.loopStart = nil
            deckA.loopEnd = nil
            deckA.isLooping = false
            deckA.loopBeats = 4
        } else {
            deckB.loopStart = nil
            deckB.loopEnd = nil
            deckB.isLooping = false
            deckB.loopBeats = 4
        }
    }

    func moveLoop(deck: String, forward: Bool) {
        if deck == "A" {
            guard let start = deckA.loopStart, let end = deckA.loopEnd else { return }
            let length = end - start
            let offset = forward ? length : -length

            let newStart = start + offset
            let newEnd = end + offset

            // Ensure within bounds
            guard newStart >= 0, newEnd <= deckA.duration else { return }

            deckA.loopStart = newStart
            deckA.loopEnd = newEnd

            // Jump to new loop start if currently playing
            if deckA.isLooping {
                seek(deck: "A", to: newStart)
            }
        } else {
            guard let start = deckB.loopStart, let end = deckB.loopEnd else { return }
            let length = end - start
            let offset = forward ? length : -length

            let newStart = start + offset
            let newEnd = end + offset

            guard newStart >= 0, newEnd <= deckB.duration else { return }

            deckB.loopStart = newStart
            deckB.loopEnd = newEnd

            if deckB.isLooping {
                seek(deck: "B", to: newStart)
            }
        }
    }

    // MARK: - Nudge (Beat Matching)

    private var nudgeTimerA: Timer?
    private var nudgeTimerB: Timer?
    private var originalTempoA: Float = 1.0
    private var originalTempoB: Float = 1.0

    /// Nudge the track forward or backward for beat matching
    /// - Parameters:
    ///   - deck: "A" or "B"
    ///   - forward: true = speed up, false = slow down
    ///   - amount: nudge intensity (default 0.04 = 4%)
    func nudge(deck: String, forward: Bool, amount: Float = 0.04) {
        let nudgeValue = forward ? amount : -amount

        if deck == "A" {
            // Cancel existing nudge timer
            nudgeTimerA?.invalidate()

            // Store original tempo if not already nudging
            if nudgeTimerA == nil {
                originalTempoA = deckA.tempo
            }

            // Apply nudge
            let nudgedTempo = max(0.5, min(2.0, originalTempoA + nudgeValue))
            timePitchA.rate = nudgedTempo

            // Return to original tempo after delay
            nudgeTimerA = Timer.scheduledTimer(withTimeInterval: 0.3, repeats: false) { [weak self] _ in
                guard let self = self else { return }
                Task { @MainActor in
                    self.timePitchA.rate = self.originalTempoA
                    self.nudgeTimerA = nil
                }
            }
        } else {
            nudgeTimerB?.invalidate()

            if nudgeTimerB == nil {
                originalTempoB = deckB.tempo
            }

            let nudgedTempo = max(0.5, min(2.0, originalTempoB + nudgeValue))
            timePitchB.rate = nudgedTempo

            nudgeTimerB = Timer.scheduledTimer(withTimeInterval: 0.3, repeats: false) { [weak self] _ in
                guard let self = self else { return }
                Task { @MainActor in
                    self.timePitchB.rate = self.originalTempoB
                    self.nudgeTimerB = nil
                }
            }
        }
    }

    /// Continuous nudge while button is held
    func startNudge(deck: String, forward: Bool) {
        let nudgeValue: Float = forward ? 0.06 : -0.06

        if deck == "A" {
            originalTempoA = deckA.tempo
            let nudgedTempo = max(0.5, min(2.0, originalTempoA + nudgeValue))
            timePitchA.rate = nudgedTempo
        } else {
            originalTempoB = deckB.tempo
            let nudgedTempo = max(0.5, min(2.0, originalTempoB + nudgeValue))
            timePitchB.rate = nudgedTempo
        }
    }

    /// Stop continuous nudge
    func stopNudge(deck: String) {
        if deck == "A" {
            timePitchA.rate = originalTempoA
        } else {
            timePitchB.rate = originalTempoB
        }
    }

    // MARK: - BPM Sync

    /// Sync this deck's tempo to match the other deck's BPM
    func syncBPM(deck: String) {
        if deck == "A" {
            guard let sourceBPM = deckA.bpm, let targetBPM = deckB.bpm else { return }
            guard sourceBPM > 0 else { return }

            // Calculate tempo ratio to match target BPM
            let tempoRatio = Float(targetBPM / sourceBPM)

            // Clamp to valid tempo range (0.5 - 2.0)
            let clampedTempo = max(0.5, min(2.0, tempoRatio))

            setTempo(deck: "A", rate: clampedTempo)
        } else {
            guard let sourceBPM = deckB.bpm, let targetBPM = deckA.bpm else { return }
            guard sourceBPM > 0 else { return }

            let tempoRatio = Float(targetBPM / sourceBPM)
            let clampedTempo = max(0.5, min(2.0, tempoRatio))

            setTempo(deck: "B", rate: clampedTempo)
        }
    }

    /// Check if deck can sync (both decks have BPM detected)
    func canSync(deck: String) -> Bool {
        if deck == "A" {
            return deckA.bpm != nil && deckB.bpm != nil && deckB.isLoaded
        } else {
            return deckB.bpm != nil && deckA.bpm != nil && deckA.isLoaded
        }
    }

    /// Get the effective BPM after tempo adjustment
    func effectiveBPM(deck: String) -> Double? {
        if deck == "A" {
            guard let bpm = deckA.bpm else { return nil }
            return bpm * Double(deckA.tempo)
        } else {
            guard let bpm = deckB.bpm else { return nil }
            return bpm * Double(deckB.tempo)
        }
    }

    /// Check if decks are synced (effective BPMs match within tolerance)
    func areSynced() -> Bool {
        guard let bpmA = effectiveBPM(deck: "A"),
              let bpmB = effectiveBPM(deck: "B") else { return false }
        return abs(bpmA - bpmB) < 0.5  // Within 0.5 BPM tolerance
    }

    /// Check harmonic compatibility between decks
    func harmonicCompatibility() -> HarmonicCompatibility? {
        guard let keyA = deckA.musicalKey, let keyB = deckB.musicalKey else {
            return nil
        }
        return keyA.isCompatible(with: keyB)
    }

    // MARK: - Beat Sync

    /// Enable/disable automatic beat sync for a deck
    func toggleBeatSync(deck: String) {
        if deck == "A" {
            deckA.beatSyncEnabled.toggle()
            if deckA.beatSyncEnabled {
                // Sync BPM first
                syncBPM(deck: "A")
                // Then align phase
                performBeatSync(deck: "A")
            }
        } else {
            deckB.beatSyncEnabled.toggle()
            if deckB.beatSyncEnabled {
                syncBPM(deck: "B")
                performBeatSync(deck: "B")
            }
        }
    }

    /// Manually perform a beat sync (quantized jump to align beats)
    func performBeatSync(deck: String) {
        let sourceDeck = deck == "A" ? deckA : deckB
        let targetDeck = deck == "A" ? deckB : deckA

        guard sourceDeck.isLoaded && targetDeck.isLoaded,
              let sourceBPM = sourceDeck.bpm,
              let targetPhase = targetDeck.beatPhase,
              sourceBPM > 0 else { return }

        // Calculate the current phase difference
        let sourceSecondsPerBeat = 60.0 / (sourceBPM * Double(sourceDeck.tempo))

        // Get source deck's current beat phase
        guard let sourcePhase = sourceDeck.beatPhase else { return }

        // Calculate phase difference (how much source is ahead/behind target)
        var phaseDiff = targetPhase - sourcePhase

        // Normalize to -0.5 to 0.5 range (shortest path)
        if phaseDiff > 0.5 { phaseDiff -= 1.0 }
        if phaseDiff < -0.5 { phaseDiff += 1.0 }

        // Convert phase difference to time offset
        let timeOffset = phaseDiff * sourceSecondsPerBeat

        // Calculate new position
        let newTime = max(0, sourceDeck.currentTime + timeOffset)

        // Seek to aligned position
        seek(deck: deck, to: newTime)
    }

    /// Jump by a number of beats (positive = forward, negative = backward)
    func beatJump(deck: String, beats: Double) {
        let deckState = deck == "A" ? deckA : deckB

        guard let bpm = deckState.bpm, bpm > 0 else { return }

        let secondsPerBeat = 60.0 / (bpm * Double(deckState.tempo))
        let jumpTime = beats * secondsPerBeat

        let newTime = max(0, min(deckState.duration, deckState.currentTime + jumpTime))
        seek(deck: deck, to: newTime)
    }

    /// Adjust beat grid offset (for manual grid alignment)
    func adjustBeatGridOffset(deck: String, offset: TimeInterval) {
        if deck == "A" {
            deckA.beatGridOffset = offset
        } else {
            deckB.beatGridOffset = offset
        }
    }

    /// Nudge beat grid forward/backward by a small amount
    func nudgeBeatGrid(deck: String, forward: Bool) {
        let nudgeAmount: TimeInterval = 0.01 // 10ms nudge
        if deck == "A" {
            deckA.beatGridOffset += forward ? nudgeAmount : -nudgeAmount
        } else {
            deckB.beatGridOffset += forward ? nudgeAmount : -nudgeAmount
        }
    }

    /// Reset beat grid offset to zero
    func resetBeatGridOffset(deck: String) {
        if deck == "A" {
            deckA.beatGridOffset = 0
        } else {
            deckB.beatGridOffset = 0
        }
    }

    /// Check if beats are phase-aligned between decks
    func areBeatsAligned() -> Bool {
        guard let phaseA = deckA.beatPhase,
              let phaseB = deckB.beatPhase else { return false }

        // Calculate phase difference
        var phaseDiff = abs(phaseA - phaseB)
        if phaseDiff > 0.5 { phaseDiff = 1.0 - phaseDiff }

        // Within 5% of beat = aligned
        return phaseDiff < 0.05
    }

    /// Get the beat phase difference between decks (-0.5 to 0.5)
    func beatPhaseDifference() -> Double? {
        guard let phaseA = deckA.beatPhase,
              let phaseB = deckB.beatPhase else { return nil }

        var diff = phaseB - phaseA
        if diff > 0.5 { diff -= 1.0 }
        if diff < -0.5 { diff += 1.0 }

        return diff
    }

    // MARK: - Hot Cues

    /// Set or trigger a hot cue
    /// If the pad is empty, sets the current position as hot cue
    /// If the pad has a cue, jumps to that position
    func triggerHotCue(deck: String, pad: Int) {
        guard pad >= 0 && pad < 8 else { return }

        if deck == "A" {
            guard deckA.isLoaded else { return }
            if deckA.hotCues[pad] == nil {
                // Set new hot cue at current position
                deckA.hotCues[pad] = deckA.currentTime
            } else {
                // Jump to existing hot cue
                if let cueTime = deckA.hotCues[pad] {
                    seek(deck: "A", to: cueTime)
                    // Auto-play when triggering hot cue
                    if !deckA.isPlaying {
                        try? play(deck: "A")
                    }
                }
            }
        } else {
            guard deckB.isLoaded else { return }
            if deckB.hotCues[pad] == nil {
                deckB.hotCues[pad] = deckB.currentTime
            } else {
                if let cueTime = deckB.hotCues[pad] {
                    seek(deck: "B", to: cueTime)
                    if !deckB.isPlaying {
                        try? play(deck: "B")
                    }
                }
            }
        }
    }

    /// Clear a specific hot cue
    func clearHotCue(deck: String, pad: Int) {
        guard pad >= 0 && pad < 8 else { return }

        if deck == "A" {
            deckA.hotCues[pad] = nil
        } else {
            deckB.hotCues[pad] = nil
        }
    }

    /// Clear all hot cues for a deck
    func clearAllHotCues(deck: String) {
        if deck == "A" {
            deckA.hotCues = Array(repeating: nil, count: 8)
        } else {
            deckB.hotCues = Array(repeating: nil, count: 8)
        }
    }

    /// Check if a hot cue is set
    func hasHotCue(deck: String, pad: Int) -> Bool {
        guard pad >= 0 && pad < 8 else { return false }
        if deck == "A" {
            return deckA.hotCues[pad] != nil
        } else {
            return deckB.hotCues[pad] != nil
        }
    }

    // MARK: - Recording

    /// Start recording the mix output to a file
    func startRecording() throws {
        guard !isRecording else { return }

        // Create recordings directory in Music folder
        let musicURL = FileManager.default.urls(for: .musicDirectory, in: .userDomainMask).first!
        let recordingsDir = musicURL.appendingPathComponent("YouTubeAudio/Recordings")

        try FileManager.default.createDirectory(at: recordingsDir, withIntermediateDirectories: true)

        // Generate filename with timestamp
        let formatter = DateFormatter()
        formatter.dateFormat = "yyyy-MM-dd_HH-mm-ss"
        let timestamp = formatter.string(from: Date())
        let filename = "Mix_\(timestamp).m4a"
        let fileURL = recordingsDir.appendingPathComponent(filename)

        // Get format from main mixer
        let format = audioEngine.mainMixerNode.outputFormat(forBus: 0)

        // Create audio file for recording (AAC format for smaller file size)
        let settings: [String: Any] = [
            AVFormatIDKey: kAudioFormatMPEG4AAC,
            AVSampleRateKey: format.sampleRate,
            AVNumberOfChannelsKey: format.channelCount,
            AVEncoderBitRateKey: 256000,  // 256 kbps
            AVEncoderAudioQualityKey: AVAudioQuality.high.rawValue
        ]

        recordingFile = try AVAudioFile(forWriting: fileURL, settings: settings)
        recordingURL = fileURL

        // Install tap on main mixer to capture audio
        // First remove metering tap if installed on master
        if isMeteringTapInstalledMaster {
            audioEngine.mainMixerNode.removeTap(onBus: 0)
            isMeteringTapInstalledMaster = false
        }

        audioEngine.mainMixerNode.installTap(onBus: 0, bufferSize: 4096, format: format) { [weak self] buffer, _ in
            guard let self = self, let file = self.recordingFile else { return }
            do {
                try file.write(from: buffer)
            } catch {
                print("Recording write error: \(error)")
            }
        }
        isRecordingTapInstalled = true

        // Start recording timer
        recordingStartTime = Date()
        recordingTimer = Timer.scheduledTimer(withTimeInterval: 0.1, repeats: true) { [weak self] _ in
            guard let self = self, let startTime = self.recordingStartTime else { return }
            Task { @MainActor in
                self.recordingDuration = Date().timeIntervalSince(startTime)
            }
        }

        isRecording = true

        // Ensure engine is running
        if !audioEngine.isRunning {
            try startEngine()
        }
    }

    /// Stop recording and save the file
    func stopRecording() -> URL? {
        guard isRecording else { return nil }

        // Remove tap
        if isRecordingTapInstalled {
            audioEngine.mainMixerNode.removeTap(onBus: 0)
            isRecordingTapInstalled = false
        }

        // Stop timer
        recordingTimer?.invalidate()
        recordingTimer = nil

        // Close file
        let savedURL = recordingURL
        recordingFile = nil

        // Reset state
        isRecording = false
        recordingDuration = 0
        recordingStartTime = nil

        return savedURL
    }

    /// Get the recordings directory URL
    func getRecordingsDirectory() -> URL? {
        let musicURL = FileManager.default.urls(for: .musicDirectory, in: .userDomainMask).first
        return musicURL?.appendingPathComponent("YouTubeAudio/Recordings")
    }

    /// Format recording duration as mm:ss
    var recordingDurationFormatted: String {
        let minutes = Int(recordingDuration) / 60
        let seconds = Int(recordingDuration) % 60
        return String(format: "%02d:%02d", minutes, seconds)
    }

    // MARK: - Level Metering

    /// Start level metering for all channels
    func startMetering() {
        guard meteringTimer == nil else { return }

        let format = audioEngine.mainMixerNode.outputFormat(forBus: 0)

        // Install tap on Deck A mixer
        if !isMeteringTapInstalledA {
            mixerNodeA.installTap(onBus: 0, bufferSize: 1024, format: nil) { [weak self] buffer, _ in
                let levels = self?.calculateLevels(buffer: buffer) ?? (0, 0)
                let leftLevel = levels.0
                let rightLevel = levels.1
                Task { @MainActor in
                    guard let self = self else { return }
                    self.levelA = (leftLevel, rightLevel)
                    // Update peak
                    if leftLevel > self.peakA.left {
                        self.peakA.left = leftLevel
                    }
                    if rightLevel > self.peakA.right {
                        self.peakA.right = rightLevel
                    }
                }
            }
            isMeteringTapInstalledA = true
        }

        // Install tap on Deck B mixer
        if !isMeteringTapInstalledB {
            mixerNodeB.installTap(onBus: 0, bufferSize: 1024, format: nil) { [weak self] buffer, _ in
                let levels = self?.calculateLevels(buffer: buffer) ?? (0, 0)
                let leftLevel = levels.0
                let rightLevel = levels.1
                Task { @MainActor in
                    guard let self = self else { return }
                    self.levelB = (leftLevel, rightLevel)
                    if leftLevel > self.peakB.left {
                        self.peakB.left = leftLevel
                    }
                    if rightLevel > self.peakB.right {
                        self.peakB.right = rightLevel
                    }
                }
            }
            isMeteringTapInstalledB = true
        }

        // Install tap on master (only if not recording, since recording also uses a tap)
        if !isRecording && !isMeteringTapInstalledMaster {
            audioEngine.mainMixerNode.installTap(onBus: 0, bufferSize: 1024, format: format) { [weak self] buffer, _ in
                let levels = self?.calculateLevels(buffer: buffer) ?? (0, 0)
                let leftLevel = levels.0
                let rightLevel = levels.1
                Task { @MainActor in
                    guard let self = self else { return }
                    self.levelMaster = (leftLevel, rightLevel)
                    if leftLevel > self.peakMaster.left {
                        self.peakMaster.left = leftLevel
                    }
                    if rightLevel > self.peakMaster.right {
                        self.peakMaster.right = rightLevel
                    }
                }
            }
            isMeteringTapInstalledMaster = true
        }

        // Decay timer for smooth falloff
        meteringTimer = Timer.scheduledTimer(withTimeInterval: 1.0/30.0, repeats: true) { [weak self] _ in
            Task { @MainActor in
                self?.decayLevels()
            }
        }
    }

    /// Stop level metering
    func stopMetering() {
        meteringTimer?.invalidate()
        meteringTimer = nil

        if isMeteringTapInstalledA {
            mixerNodeA.removeTap(onBus: 0)
            isMeteringTapInstalledA = false
        }
        if isMeteringTapInstalledB {
            mixerNodeB.removeTap(onBus: 0)
            isMeteringTapInstalledB = false
        }

        if isMeteringTapInstalledMaster && !isRecording {
            audioEngine.mainMixerNode.removeTap(onBus: 0)
            isMeteringTapInstalledMaster = false
        }

        levelA = (0, 0)
        levelB = (0, 0)
        levelMaster = (0, 0)
        peakA = (0, 0)
        peakB = (0, 0)
        peakMaster = (0, 0)
    }

    /// Calculate RMS levels from audio buffer
    private nonisolated func calculateLevels(buffer: AVAudioPCMBuffer) -> (left: Float, right: Float) {
        guard let channelData = buffer.floatChannelData else { return (0, 0) }

        let channelCount = Int(buffer.format.channelCount)
        let frameLength = Int(buffer.frameLength)

        guard frameLength > 0 else { return (0, 0) }

        var leftRMS: Float = 0
        var rightRMS: Float = 0

        // Calculate RMS for left channel
        let leftChannel = channelData[0]
        for i in 0..<frameLength {
            leftRMS += leftChannel[i] * leftChannel[i]
        }
        leftRMS = sqrt(leftRMS / Float(frameLength))

        // Calculate RMS for right channel (or use left if mono)
        if channelCount > 1 {
            let rightChannel = channelData[1]
            for i in 0..<frameLength {
                rightRMS += rightChannel[i] * rightChannel[i]
            }
            rightRMS = sqrt(rightRMS / Float(frameLength))
        } else {
            rightRMS = leftRMS
        }

        // Convert to dB and normalize (roughly -60dB to 0dB range mapped to 0-1)
        let leftDB = 20 * log10(max(leftRMS, 0.00001))
        let rightDB = 20 * log10(max(rightRMS, 0.00001))

        let leftNormalized = max(0, min(1, (leftDB + 60) / 60))
        let rightNormalized = max(0, min(1, (rightDB + 60) / 60))

        return (leftNormalized, rightNormalized)
    }

    /// Decay levels smoothly when audio stops
    private func decayLevels() {
        // Decay peaks slowly
        peakA.left = max(0, peakA.left - peakDecayRate)
        peakA.right = max(0, peakA.right - peakDecayRate)
        peakB.left = max(0, peakB.left - peakDecayRate)
        peakB.right = max(0, peakB.right - peakDecayRate)
        peakMaster.left = max(0, peakMaster.left - peakDecayRate)
        peakMaster.right = max(0, peakMaster.right - peakDecayRate)
    }

    // MARK: - Crossfade

    private func applyCrossfade(position: Float) {
        // Equal power crossfade curve for smooth transitions
        let volumeA = sqrt(1.0 - position) * masterVolume
        let volumeB = sqrt(position) * masterVolume

        mixerNodeA.outputVolume = volumeA
        mixerNodeB.outputVolume = volumeB
    }

    func setVolume(deck: String, volume: Float) {
        let clamped = max(0, min(1, volume))
        if deck == "A" {
            deckA.volume = clamped
        } else {
            deckB.volume = clamped
        }
        applyCrossfade(position: crossfaderPosition)
    }

    // MARK: - Auto Crossfade

    func autoCrossfade(duration: TimeInterval, toDeck: String) {
        let targetPosition: Float = toDeck == "A" ? 0.0 : 1.0
        let startPosition = crossfaderPosition
        let startTime = CACurrentMediaTime()

        Timer.scheduledTimer(withTimeInterval: 1.0/60.0, repeats: true) { [weak self] timer in
            guard let self = self else {
                timer.invalidate()
                return
            }

            let elapsed = CACurrentMediaTime() - startTime
            let progress = min(Float(elapsed / duration), 1.0)

            Task { @MainActor in
                self.crossfaderPosition = startPosition + (targetPosition - startPosition) * progress

                if progress >= 1.0 {
                    timer.invalidate()
                }
            }
        }
    }

    // MARK: - Position Updates

    private func startPositionUpdates() {
        positionTimer = Timer.scheduledTimer(withTimeInterval: 1.0/30.0, repeats: true) { [weak self] _ in
            Task { @MainActor in
                self?.updatePlaybackPositions()
            }
        }
    }

    private func stopPositionUpdates() {
        positionTimer?.invalidate()
        positionTimer = nil
    }

    private func updatePlaybackPositions() {
        // Update Deck A position
        if deckA.isPlaying, let nodeTime = playerNodeA.lastRenderTime,
           let playerTime = playerNodeA.playerTime(forNodeTime: nodeTime) {
            let sampleRate = playerTime.sampleRate
            deckA.currentTime = Double(playerTime.sampleTime) / sampleRate

            // Check for loop
            if deckA.isLooping, let loopEnd = deckA.loopEnd, let loopStart = deckA.loopStart {
                if deckA.currentTime >= loopEnd {
                    seek(deck: "A", to: loopStart)
                }
            }
        }

        // Update Deck B position
        if deckB.isPlaying, let nodeTime = playerNodeB.lastRenderTime,
           let playerTime = playerNodeB.playerTime(forNodeTime: nodeTime) {
            let sampleRate = playerTime.sampleRate
            deckB.currentTime = Double(playerTime.sampleTime) / sampleRate

            // Check for loop
            if deckB.isLooping, let loopEnd = deckB.loopEnd, let loopStart = deckB.loopStart {
                if deckB.currentTime >= loopEnd {
                    seek(deck: "B", to: loopStart)
                }
            }
        }

        // Perform continuous beat sync if enabled
        performContinuousBeatSync()
    }

    /// Continuous beat sync - applies subtle tempo adjustments to keep decks phase-aligned
    private func performContinuousBeatSync() {
        // Check if either deck has beat sync enabled
        let syncA = deckA.beatSyncEnabled && deckA.isPlaying && deckB.isPlaying
        let syncB = deckB.beatSyncEnabled && deckB.isPlaying && deckA.isPlaying

        guard syncA || syncB else { return }
        guard areSynced() else { return } // Only sync if BPMs are matched

        // Get phase difference
        guard let phaseDiff = beatPhaseDifference() else { return }

        // If phase difference is small enough, no correction needed
        if abs(phaseDiff) < 0.02 { return } // Within 2% = close enough

        // Calculate correction factor
        // Apply subtle tempo adjustment to bring phases together
        let correctionFactor: Float = 0.02 // Max 2% tempo adjustment for smooth correction

        if syncA && !syncB {
            // Deck A follows Deck B
            applyPhaseCorrection(deck: "A", phaseDiff: -phaseDiff, factor: correctionFactor)
        } else if syncB && !syncA {
            // Deck B follows Deck A
            applyPhaseCorrection(deck: "B", phaseDiff: phaseDiff, factor: correctionFactor)
        } else {
            // Both synced - Deck B follows Deck A (A is master)
            applyPhaseCorrection(deck: "B", phaseDiff: phaseDiff, factor: correctionFactor)
        }
    }

    /// Apply phase correction via subtle tempo adjustment
    private func applyPhaseCorrection(deck: String, phaseDiff: Double, factor: Float) {
        let timePitch = deck == "A" ? timePitchA : timePitchB
        let deckState = deck == "A" ? deckA : deckB

        // Calculate tempo adjustment based on phase difference
        // If ahead (phaseDiff > 0), slow down slightly
        // If behind (phaseDiff < 0), speed up slightly
        let adjustment = Float(-phaseDiff) * factor

        // Apply adjustment, but respect deck's base tempo
        let targetTempo = deckState.tempo + adjustment
        let clampedTempo = max(0.5, min(2.0, targetTempo))

        timePitch.rate = clampedTempo
    }

    // MARK: - Cleanup

    func unloadDeck(_ deck: String) {
        if deck == "A" {
            playerNodeA.stop()
            audioFileA = nil
            deckA = DeckState(id: "A")
        } else {
            playerNodeB.stop()
            audioFileB = nil
            deckB = DeckState(id: "B")
        }
    }
}

// MARK: - Errors

enum MixerError: LocalizedError {
    case fileNotFound
    case engineStartFailed
    case formatMismatch

    var errorDescription: String? {
        switch self {
        case .fileNotFound: return "Audio file not found"
        case .engineStartFailed: return "Failed to start audio engine"
        case .formatMismatch: return "Audio format not compatible"
        }
    }
}