From dbaa74576d7152d23f2fc75018d237607906b1c9 Mon Sep 17 00:00:00 2001 From: Mark Gregory Date: Fri, 6 Nov 2026 10:06:12 +0000 Subject: [PATCH] removed unwanted bar now with waveforms --- KJamsClone/Player.swift | 26 ++++++++++++++++++++------ KJamsCloneTests/KJamsCloneTests.swift | 24 ++++++++++++++++++++++-- 2 files changed, 42 insertions(+), 8 deletions(-) diff --git a/KJamsClone/Player.swift b/KJamsClone/Player.swift index 77f5e8e..24183aa 100644 --- a/KJamsClone/Player.swift +++ b/KJamsClone/Player.swift @@ -336,31 +336,45 @@ import CoreAudio var audibleScrubbing = false private var scrubPreviewTask: Task? private var scrubPreviewActive = false + private var lastScrubAudio = -Double.infinity + private var scrubTarget: Double? + var scrubOutputLevel: Float { let levels = meter.snapshot();return max(levels.0,levels.1) } /// Preview short grains through the deck's normal EQ, fader and output route. /// Keep transport paused so scrubbing cannot start Automix or change sync state. func scrub(to value: Double) { - guard audibleScrubbing,!playing,let file,value.isFinite else { seek(value);return } + guard audibleScrubbing,let file,value.isFinite else { seek(value);return } + let target = min(duration,max(0,value)),now = ProcessInfo.processInfo.systemUptime + scrubTarget = target + // Do not starve HAL by stopping/restarting the node on every mouse event. + if now-lastScrubAudio < 0.12 { + if !playing { time = target;render() } + return + } + lastScrubAudio = now + if playing { seek(target);return } scrubPreviewTask?.cancel() - seek(value) + seek(target) guard time < duration,engine.isRunning else { return } generation = UUID();let token = generation node.stop() let rate = file.processingFormat.sampleRate let start = AVAudioFramePosition(time*rate) - let count = AVAudioFrameCount(min(file.length-start,Int64(rate*0.1))) + let count = AVAudioFrameCount(min(file.length-start,Int64(rate*0.25))) guard count > 0 else { return } node.scheduleSegment(file,startingFrame:start,frameCount:count,at:nil) scrubPreviewActive = true;node.play() scrubPreviewTask = Task { @MainActor [weak self] in - do { try await Task.sleep(nanoseconds:100_000_000) } catch { return } + do { try await Task.sleep(nanoseconds:250_000_000) } catch { return } guard let self,self.generation == token else { return } self.endScrubbing() } } func endScrubbing() { scrubPreviewTask?.cancel();scrubPreviewTask = nil - guard scrubPreviewActive else { return };scrubPreviewActive = false - if !playing { schedule(at:time,play:false) } + let target = scrubTarget;scrubTarget = nil;lastScrubAudio = -Double.infinity + let previewed = scrubPreviewActive;scrubPreviewActive = false + if playing,let target { seek(target) } + else if previewed { schedule(at:target ?? time,play:false) } } func seek(_ value: Double) { guard value.isFinite else { return } diff --git a/KJamsCloneTests/KJamsCloneTests.swift b/KJamsCloneTests/KJamsCloneTests.swift index c72b2e2..97dacc9 100644 --- a/KJamsCloneTests/KJamsCloneTests.swift +++ b/KJamsCloneTests/KJamsCloneTests.swift @@ -1193,7 +1193,7 @@ final class KJamsCloneTests: XCTestCase { XCTAssertLessThan(player.outputOpacity,0.2);XCTAssertEqual(player.globalMix,mix) player.fadeSeconds = 0.12;player.fadeOutAndStop();try await Task.sleep(nanoseconds:220_000_000) XCTAssertNil(player.currentSongID);XCTAssertEqual(player.globalMix,mix) - try player.load(song);player.fadeOutAndStop();try player.load(song);try await Task.sleep(nanoseconds:180_000_000) + try player.load(song);player.fadeOutAndStop();try player.load(song);try await Task.sleep(nanoseconds:330_000_000) XCTAssertEqual(player.currentSongID,1);XCTAssertFalse(player.fading);player.stop() } func testMP3FileTagWritingBacksUpPreservesAudioAndRejectsBadTags() async throws { @@ -2655,7 +2655,7 @@ extension KJamsCloneTests { XCTAssertFalse(player.audioPlaybackStarted);XCTAssertFalse(player.playing) XCTAssertEqual(player.time,0.2,accuracy:0.001) player.scrub(to:0.4) - try await Task.sleep(nanoseconds:180_000_000) + try await Task.sleep(nanoseconds:330_000_000) XCTAssertFalse(player.audioPlaybackStarted,"Preview must stop if dragging stops") player.toggle() player.scrub(to:0.6) @@ -2722,4 +2722,24 @@ extension KJamsCloneTests { XCTAssertTrue(system.a.player.playing) } + @MainActor func testRapidDJScrubbingProducesAudioAtOutputTap() async throws { + let root = FileManager.default.temporaryDirectory.appendingPathComponent(UUID().uuidString) + defer { try? FileManager.default.removeItem(at:root) } + let fixture = try XCTUnwrap(Bundle(for:Self.self).resourceURL?.appendingPathComponent("Fixtures/Demo.mp3")) + let player = Player(waveformCacheDirectory:root,persistsMix:false) + defer { player.stop() } + player.audibleScrubbing = true;player.autoFade = false;player.volume = 0.8 + try player.load(Song(id:1,title:"Demo",audio:fixture.path,cdg:nil,rating:0,plays:0),startPlaying:false) + var peak: Float = 0 + for i in 0..<40 { + player.scrub(to:0.3+Double(i)*0.01) + try await Task.sleep(nanoseconds:10_000_000) + peak = max(peak,player.scrubOutputLevel) + } + XCTAssertGreaterThan(peak,0.0001,"Rapid mouse-like scrub updates must produce decoded signal at the audio output") + XCTAssertFalse(player.playing) + player.endScrubbing();XCTAssertFalse(player.audioPlaybackStarted) + XCTAssertEqual(player.time,0.69,accuracy:0.001) + } + }