removed unwanted bar now with waveforms
This commit is contained in:
+20
-6
@@ -336,31 +336,45 @@ import CoreAudio
|
||||
var audibleScrubbing = false
|
||||
private var scrubPreviewTask: Task<Void,Never>?
|
||||
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 }
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user