removed unwanted bar now with waveforms, major updates and enhancements
This commit is contained in:
@@ -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 { }
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user