removed unwanted bar now with waveforms, major updates and enhancements

This commit is contained in:
Mark Gregory
2026-11-06 14:47:53 +00:00
parent f9655503df
commit c39c2bd757
6 changed files with 649 additions and 89 deletions
+108 -4
View File
@@ -2867,7 +2867,7 @@ extension KJamsCloneTests {
XCTAssertTrue(player.nativeLoopActive);XCTAssertLessThan(player.displayTime,0.301)
let exit = player.displayTime,restartsOnExit = player.audioScheduleRestartCount
player.loopEnabled = false
XCTAssertEqual(player.audioScheduleRestartCount,restartsOnExit,"Loop Off must not stop and reschedule the running audio node")
XCTAssertEqual(player.audioScheduleRestartCount,restartsOnExit+1,"Loop Off prepares the idle node before retiring the audible node")
XCTAssertFalse(player.nativeLoopActive);XCTAssertTrue(player.playing)
XCTAssertEqual(player.time,exit,accuracy:0.03)
try await Task.sleep(nanoseconds:180_000_000)
@@ -3070,7 +3070,7 @@ extension KJamsCloneTests {
try await Task.sleep(nanoseconds:400_000_000)
XCTAssertGreaterThanOrEqual(player.displayTime,start);XCTAssertLessThan(player.displayTime,start+0.301)
player.loopEnabled = false
XCTAssertEqual(player.audioScheduleRestartCount,restarts+1,"Repeating and cancelling must not replace the node commands again")
XCTAssertEqual(player.audioScheduleRestartCount,restarts+2,"Cancellation prepares the idle node without stopping audible output")
XCTAssertTrue(player.audioPlaybackStarted)
}
@@ -3094,7 +3094,7 @@ extension KJamsCloneTests {
var finished = false;player.onFinished = { finished = true }
let restarts = player.audioScheduleRestartCount
player.loopEnabled = false
XCTAssertEqual(player.audioScheduleRestartCount,restarts)
XCTAssertEqual(player.audioScheduleRestartCount,restarts+1)
for _ in 0..<200 where !finished { try await Task.sleep(nanoseconds:5_000_000) }
XCTAssertTrue(finished);XCTAssertFalse(player.playing)
}
@@ -3140,7 +3140,111 @@ extension KJamsCloneTests {
player.loopEnabled = false
try await Task.sleep(nanoseconds:800_000_000)
XCTAssertGreaterThan(player.scrubOutputLevel,0.4,"Loop Off must resume the actual file beyond the loop")
XCTAssertEqual(player.audioScheduleRestartCount,handoffs,"Resizing and cancellation must keep the node running")
XCTAssertEqual(player.audioScheduleRestartCount,handoffs+1,"Cancellation prepares the idle node; resizing reuses the active loop")
}
@MainActor func testLoopHandoffsKeepContinuousAudioAcrossRepeatedStartAndStop() async throws {
let root = FileManager.default.temporaryDirectory.appendingPathComponent(UUID().uuidString)
defer { try? FileManager.default.removeItem(at:root) }
try FileManager.default.createDirectory(at:root,withIntermediateDirectories:true)
let format = try XCTUnwrap(AVAudioFormat(standardFormatWithSampleRate:44100,channels:2))
let buffer = try XCTUnwrap(AVAudioPCMBuffer(pcmFormat:format,frameCapacity:441000));buffer.frameLength = 441000
let channels = try XCTUnwrap(buffer.floatChannelData)
for i in 0..<441000 {
let sample = Float(sin(Double(i)*2 * .pi*440/44100))*0.25
channels[0][i] = sample;channels[1][i] = sample
}
let url = root.appendingPathComponent("Continuous.wav")
do { let file = try AVAudioFile(forWriting:url,settings:format.settings);try file.write(from:buffer) }
let player = Player(waveformCacheDirectory:root.appendingPathComponent("Cache"),persistsMix:false)
defer { player.stop() }
player.autoFade = false;player.volume = 0.8
try player.load(Song(id:1,title:"Continuous",audio:url.path,cdg:nil,rating:0,plays:0))
let other = Player(waveformCacheDirectory:root.appendingPathComponent("OtherCache"),persistsMix:false)
defer { other.stop() };other.autoFade = false;other.volume = 0.3
try other.load(Song(id:2,title:"Other deck",audio:url.path,cdg:nil,rating:0,plays:0))
try await Task.sleep(nanoseconds:200_000_000)
player.beginAudioContinuityCheck()
for iteration in 0..<4 {
if iteration == 2 { player.tempo = 1.12;other.tempo = 0.95 }
let start = player.displayTime
player.defineLoop(start:start,end:start+0.307)
for _ in 0..<200 where !player.nativeLoopActive { try await Task.sleep(nanoseconds:5_000_000) }
XCTAssertTrue(player.nativeLoopActive)
try await Task.sleep(nanoseconds:50_000_000)
XCTAssertLessThan(player.audioContinuity.silentFrames,128);XCTAssertLessThan(player.audioContinuity.largestStep,0.08)
player.beginAudioContinuityCheck()
try await Task.sleep(nanoseconds:400_000_000)
XCTAssertLessThan(player.audioContinuity.silentFrames,128);XCTAssertLessThan(player.audioContinuity.largestStep,0.08)
player.beginAudioContinuityCheck()
player.loopEnabled = false
try await Task.sleep(nanoseconds:100_000_000)
XCTAssertLessThan(player.audioContinuity.silentFrames,128);XCTAssertLessThan(player.audioContinuity.largestStep,0.08)
XCTAssertTrue(player.playing);XCTAssertTrue(player.audioPlaybackStarted)
XCTAssertGreaterThan(player.scrubOutputLevel,0.05)
}
XCTAssertLessThan(player.audioContinuity.silentFrames,128,"Loop handoffs must not insert a silent audio block")
XCTAssertLessThan(player.audioContinuity.largestStep,0.08,"Loop handoffs must not introduce a hard sample discontinuity")
}
@MainActor func testAppleMusicAuthorizationAndSubscriptionGuardPlayback() async throws {
guard #available(macOS 14.0, *) else { throw XCTSkip("MusicKit playback requires macOS 14") }
let client = TestAppleMusicClient(),session = AppleMusicSession(client:client)
XCTAssertEqual(client.authorizationRequests,0)
client.granted = .denied;await session.connect()
XCTAssertEqual(session.access,.denied);XCTAssertNotNil(session.error);XCTAssertEqual(client.browseRequests,0)
client.granted = .authorized;client.subscription = false;await session.connect()
for _ in 0..<100 where session.busy { try await Task.sleep(nanoseconds:1_000_000) }
let track = try XCTUnwrap(session.tracks.first)
await session.play(track);XCTAssertTrue(client.played.isEmpty);XCTAssertNotNil(session.error)
client.subscription = true;await session.connect()
for _ in 0..<100 where session.busy { try await Task.sleep(nanoseconds:1_000_000) }
await session.play(track);XCTAssertEqual(client.played,[track.id]);XCTAssertTrue(session.playback.playing)
session.seek(1000);XCTAssertEqual(client.playback.position,track.duration)
session.stop();XCTAssertFalse(session.playback.playing)
}
@MainActor func testAppleMusicSearchRejectsStaleResultsAndPaginatesWithoutDuplicates() async throws {
guard #available(macOS 14.0, *) else { throw XCTSkip("MusicKit playback requires macOS 14") }
let client = TestAppleMusicClient(),session = AppleMusicSession(client:client)
session.query = "slow";session.reload();try await Task.sleep(nanoseconds:5_000_000)
session.query = "latest";session.reload();try await Task.sleep(nanoseconds:150_000_000)
XCTAssertEqual(session.tracks.map(\.id),["latest"])
session.more()
for _ in 0..<100 where session.loadingMore { try await Task.sleep(nanoseconds:1_000_000) }
XCTAssertEqual(session.tracks.map(\.id),["latest","next"]);XCTAssertFalse(session.hasMore);XCTAssertTrue(client.played.isEmpty)
let view = NSHostingView(rootView:AppleMusicBrowser(session:session,hasPlayingDeck:{false},stopDecks:{}).frame(width:1100,height:420))
view.frame = NSRect(x:0,y:0,width:1100,height:420);view.layoutSubtreeIfNeeded()
let bitmap = try XCTUnwrap(view.bitmapImageRepForCachingDisplay(in:view.bounds));view.cacheDisplay(in:view.bounds,to:bitmap)
let png = try XCTUnwrap(bitmap.representation(using:.png,properties:[:]))
XCTContext.runActivity(named:"Apple Music browser layout") { activity in
let attachment = XCTAttachment(data:png,uniformTypeIdentifier:"public.png");attachment.lifetime = .keepAlways;activity.add(attachment)
}
}
}
@available(macOS 14.0, *) @MainActor private final class TestAppleMusicClient: AppleMusicClient {
var access: AppleMusicAccess = .authorized
var granted: AppleMusicAccess = .authorized
var subscription = true
var authorizationRequests = 0,browseRequests = 0
var played = [String]()
var playback = AppleMusicPlayback()
func authorize() async -> AppleMusicAccess { authorizationRequests += 1;access = granted;return granted }
func subscribed() async throws -> Bool { subscription }
func playlists() async throws -> [AppleMusicPlaylist] { [.init(id:"playlist",name:"My playlist")] }
func browse(query: String,playlist: String?,offset: Int) async throws -> AppleMusicPage {
browseRequests += 1
if query == "slow" { try? await Task.sleep(nanoseconds:100_000_000) }
let id = query.isEmpty ? "library" : query
let track = AppleMusicTrack(id:id,title:id,artist:"Artist",album:"Album",duration:180,artwork:nil)
return .init(tracks:offset == 0 ? [track] : [track,.init(id:"next",title:"Next",artist:"Artist",album:"Album",duration:180,artwork:nil)],hasMore:offset == 0)
}
func play(_ id: String) async throws { played.append(id);playback = .init(title:id,artist:"Artist",playing:true,position:0,duration:180) }
func enqueue(_ id: String) async throws { }
func resume() async throws { playback.playing = true }
func pause() { playback.playing = false }
func stop() { playback.playing = false;playback.position = 0 }
func seek(_ seconds: Double) { playback.position = seconds }
func next() async throws { }
}