broken changes

This commit is contained in:
Mark Gregory
2026-11-05 16:02:42 +00:00
parent d565e40215
commit eb98f97ded
3 changed files with 402 additions and 63 deletions
+148 -2
View File
@@ -1703,7 +1703,7 @@ extension KJamsCloneTests {
XCTAssertTrue(system.a.player.audioPlaybackStarted);XCTAssertTrue(system.b.player.audioPlaybackStarted)
XCTAssertGreaterThan(system.a.player.time,0.05);XCTAssertGreaterThan(system.b.player.time,0.55)
system.a.bpm = 120;system.a.bpmConfirmed = true;system.b.bpm = 126;system.b.bpmConfirmed = true
system.sync(system.a);XCTAssertEqual(system.a.player.tempo,1.05,accuracy:0.001)
system.sync(system.a);XCTAssertTrue(system.syncActive);XCTAssertEqual(system.a.player.tempo,1,accuracy:0.001);XCTAssertEqual(system.b.player.tempo,120.0/126,accuracy:0.001)
system.a.player.stopDeck();XCTAssertTrue(system.b.player.playing)
system.startPreview(system.b);XCTAssertNotNil(system.error);XCTAssertNil(system.previewDeck);XCTAssertFalse(system.preview.playing)
}
@@ -1747,6 +1747,7 @@ extension KJamsCloneTests {
try await capturePreferences("DJ","General","/private/tmp/kjmix-dj-settings-general.png")
try await capturePreferences("DJ","Sound","/private/tmp/kjmix-dj-settings-sound.png")
try await capturePreferences("DJ","Appearance","/private/tmp/kjmix-dj-settings-appearance.png")
model.dj.sync(model.dj.a)
let console = NSHostingView(rootView:DJContinuousConsole(system:model.dj,loadA:{},loadB:{}).frame(width:1320,height:390).foregroundStyle(.white).preferredColorScheme(.dark))
console.frame = NSRect(x:0,y:0,width:1320,height:390)
let consoleWindow = NSWindow(contentRect:console.frame,styleMask:[.borderless],backing:.buffered,defer:false)
@@ -1769,6 +1770,11 @@ extension KJamsCloneTests {
let midi = DJMIDI(defaults:defaults);midi.refreshSources()
let input = try XCTUnwrap(midi.sources.first{$0.endpoint == source});midi.select(input.id)
XCTAssertEqual(midi.sourceID,input.id)
XCTAssertEqual(defaults.integer(forKey:"DJ.MIDISource.v1"),Int(input.id))
do {
let restored = DJMIDI(defaults:defaults)
XCTAssertEqual(restored.sourceID,input.id,"A fresh MIDI session restores the saved controller")
}
var actions = [String]();midi.onAction = { action,_,_ in actions.append(action) }
func send(_ packets: [[UInt8]]) throws {
var list = MIDIPacketList()
@@ -1787,6 +1793,8 @@ extension KJamsCloneTests {
try await Task.sleep(nanoseconds:80_000_000)
XCTAssertEqual(actions,["A Play"])
midi.select(0)
XCTAssertNil(defaults.object(forKey:"DJ.MIDISource.v1"))
XCTAssertEqual(DJMIDI(defaults:defaults).sourceID,0)
}
}
@@ -1910,7 +1918,7 @@ extension KJamsCloneTests {
let system = DJSystem(storageURL:root);system.master = 0
defer { system.a.player.stop();system.b.player.stop() }
system.load(first,into:"A");system.a.player.toggle()
system.load(second,into:"A");XCTAssertNotNil(system.error);XCTAssertEqual(system.a.song?.id,1)
system.load(second,into:"A");XCTAssertNotNil(system.pendingReplacement);XCTAssertEqual(system.a.song?.id,1);system.pendingReplacement = nil
system.a.player.stopDeck();system.error = nil
system.settings.autoPlayOnLoad = true;system.settings.resetEQOnLoad = true
system.settings.tempoRange = 8;system.settings.crossfaderCurve = "Linear";system.settings.headroomDB = 6
@@ -1923,3 +1931,141 @@ extension KJamsCloneTests {
XCTAssertEqual(restored.settings,system.settings)
}
}
extension KJamsCloneTests {
@MainActor func testLinkedSyncUsesFirstDeckTogglesBothAndRestoresOriginalTempos() async throws {
let root = FileManager.default.temporaryDirectory.appendingPathComponent(UUID().uuidString)
defer { try? FileManager.default.removeItem(at:root) }
let system = DJSystem(storageURL:root);system.master = 0
defer { system.a.unload();system.b.unload() }
let file = try XCTUnwrap(Bundle(for:Self.self).resourceURL?.appendingPathComponent("Fixtures/Demo.mp3"))
let song = Song(id:1,title:"Sync",audio:file.path,cdg:nil,rating:0,plays:0)
system.load(song,into:"A");system.load(song,into:"B")
system.a.bpm = 128;system.b.bpm = 120;system.a.bpmConfirmed = true;system.b.bpmConfirmed = true
system.a.player.tempo = 1.04;system.b.player.tempo = 0.96
system.sync(system.b)
XCTAssertTrue(system.syncActive);XCTAssertEqual(system.a.player.tempo,1.04,accuracy:0.001)
XCTAssertEqual(system.b.bpm*system.b.player.tempo,128*1.04,accuracy:0.001)
system.a.player.tempo = 1.02;try await Task.sleep(nanoseconds:40_000_000)
XCTAssertEqual(system.b.bpm*system.b.player.tempo,128*1.02,accuracy:0.001)
system.sync(system.a)
XCTAssertFalse(system.syncActive);XCTAssertEqual(system.a.player.tempo,1.04,accuracy:0.001);XCTAssertEqual(system.b.player.tempo,0.96,accuracy:0.001)
}
@MainActor func testDJLoopActuallyRepeatsAndExplicitCueDoesNotMovePlayback() async throws {
let deck = DJChannel("A");defer { deck.unload() };deck.master = 0
let file = try XCTUnwrap(Bundle(for:Self.self).resourceURL?.appendingPathComponent("Fixtures/Demo.mp3"))
try deck.load(.init(id:1,title:"Loop",audio:file.path,cdg:nil,rating:0,plays:0))
deck.bpm = 120;deck.bpmConfirmed = true;deck.loopBeats = 1
deck.player.seek(0.2);deck.beatLoop();deck.player.toggle()
try await Task.sleep(nanoseconds:900_000_000)
XCTAssertTrue(deck.player.playing);XCTAssertTrue(deck.player.loopEnabled);XCTAssertGreaterThanOrEqual(deck.player.time,0.19);XCTAssertLessThan(deck.player.time,0.8)
deck.beatLoop();XCTAssertFalse(deck.player.loopEnabled)
deck.player.stopDeck();deck.player.setCue(at:1.2345)
XCTAssertEqual(deck.player.cuePosition,1.2345,accuracy:0.0001);XCTAssertEqual(deck.player.time,0,accuracy:0.001)
deck.player.returnToCue();XCTAssertEqual(deck.player.time,1.2345,accuracy:0.001)
deck.player.defineLoop(start:deck.player.duration-0.2,end:deck.player.duration);deck.player.seek(deck.player.duration-0.2);deck.player.toggle()
try await Task.sleep(nanoseconds:500_000_000)
XCTAssertTrue(deck.player.playing,"Loop ending at EOF must restart instead of finishing")
}
func testBPMEstimationRecognisesPulseTrainAndRejectsSilence() throws {
let envelope = (0..<2400).map { $0 % 96 < 4 ? 1.0 : 0.02 }
XCTAssertEqual(try XCTUnwrap(DJBPM.estimate(envelope)),125,accuracy:0.5)
XCTAssertNil(DJBPM.estimate(Array(repeating:0,count:2400)))
XCTAssertNil(DJBPM.estimate(Array(repeating:0.5,count:2400)))
let root = FileManager.default.temporaryDirectory.appendingPathComponent(UUID().uuidString)
try FileManager.default.createDirectory(at:root,withIntermediateDirectories:true);defer { try? FileManager.default.removeItem(at:root) }
let format = try XCTUnwrap(AVAudioFormat(standardFormatWithSampleRate:8000,channels:1)),buffer = try XCTUnwrap(AVAudioPCMBuffer(pcmFormat:format,frameCapacity:96000))
buffer.frameLength = 96000;let samples = try XCTUnwrap(buffer.floatChannelData)[0]
for i in 0..<96000 { samples[i] = i % 3840 < 40 ? 0.8 : 0 }
let path = root.appendingPathComponent("125 BPM.wav")
do { let output = try AVAudioFile(forWriting:path,settings:format.settings);try output.write(from:buffer) }
XCTAssertEqual(try XCTUnwrap(DJBPM.analyse(path)),125,accuracy:1)
}
@MainActor func testFinderDropWarnsBeforeImportAndAllowsExplicitOverride() async throws {
let root = FileManager.default.temporaryDirectory.appendingPathComponent(UUID().uuidString)
defer { try? FileManager.default.removeItem(at:root) }
let system = DJSystem(storageURL:root);system.master = 0;defer { system.a.unload() }
let file = try XCTUnwrap(Bundle(for:Self.self).resourceURL?.appendingPathComponent("Fixtures/Demo.mp3"))
try system.a.load(.init(id:999,title:"Playing",audio:file.path,cdg:nil,rating:0,plays:0));system.a.player.toggle()
let provider = NSItemProvider()
provider.registerDataRepresentation(forTypeIdentifier:UTType.fileURL.identifier,visibility:.all) { completion in completion(Data(file.absoluteString.utf8),nil);return nil }
XCTAssertTrue(system.acceptDrop([provider],into:"A"))
for _ in 0..<100 { if system.pendingReplacement != nil { break };try await Task.sleep(nanoseconds:10_000_000) }
XCTAssertNotNil(system.pendingReplacement,system.error ?? system.status);XCTAssertTrue(system.a.player.playing);XCTAssertTrue(system.songs.isEmpty)
let captured = try XCTUnwrap(system.pendingReplacement);system.pendingReplacement = nil
system.confirmReplacement(captured)
for _ in 0..<300 { if !system.importing { break };try await Task.sleep(nanoseconds:10_000_000) }
XCTAssertNil(system.error);XCTAssertNil(system.pendingReplacement);XCTAssertEqual(system.songs.count,1)
XCTAssertNotEqual(system.a.song?.id,999);XCTAssertFalse(system.a.player.playing)
}
@MainActor func testArtworkSelectsTrackJPGAndLoadsFromOriginalFolder() async throws {
let root = FileManager.default.temporaryDirectory.appendingPathComponent(UUID().uuidString)
try FileManager.default.createDirectory(at:root,withIntermediateDirectories:true);defer { try? FileManager.default.removeItem(at:root) }
let image = NSImage(size:NSSize(width:8,height:8));image.lockFocus();NSColor.red.setFill();NSBezierPath(rect:NSRect(x:0,y:0,width:8,height:8)).fill();image.unlockFocus()
let bitmap = try XCTUnwrap(NSBitmapImageRep(data:try XCTUnwrap(image.tiffRepresentation)))
let jpg = try XCTUnwrap(bitmap.representation(using:.jpeg,properties:[:]))
try jpg.write(to:root.appendingPathComponent("Cover.jpg"));try jpg.write(to:root.appendingPathComponent("Song.JPG"))
let audio = try XCTUnwrap(Bundle(for:Self.self).resourceURL?.appendingPathComponent("Fixtures/Demo.mp3"))
var song = Song(id:1,title:"Art",audio:audio.path,cdg:nil,rating:0,plays:0);song.originalPath = root.appendingPathComponent("Song.mp3").path
XCTAssertEqual(DJTrackInfo.sidecar(song)?.lastPathComponent,"Song.JPG")
let artwork = await DJTrackInfo.artwork(song);XCTAssertNotNil(artwork)
}
func testMixtrackGoPresetUsesActualProtocol14BitTempoAndPadModes() {
var protocolState = MixtrackGoProtocol()
let preset = MixtrackGoProtocol.mappings(source:123)
XCTAssertTrue(preset.contains{$0.status == 0x91 && $0.number == 0 && $0.action == "B Play"})
XCTAssertTrue(preset.contains{$0.status == 0xBF && $0.number == 8 && $0.action == "Crossfader"})
XCTAssertEqual(protocolState.consume(.init(status:0xB0,data1:0x09,data2:64))?.count,0)
XCTAssertEqual(protocolState.consume(.init(status:0xB0,data1:0x29,data2:0))?.first?.value,0.5)
XCTAssertEqual(protocolState.consume(.init(status:0x94,data1:0x14,data2:127))?.first?.action,"A Hot cue 1")
_ = protocolState.consume(.init(status:0x95,data1:0,data2:127))
XCTAssertEqual(protocolState.consume(.init(status:0x95,data1:0x17,data2:127))?.first?.action,"B Loop 8")
XCTAssertEqual(protocolState.consume(.init(status:0x95,data1:0x1F,data2:127))?.first?.action,"B Loop off")
let leds = protocolState.feedback(aPlaying:true,bPlaying:false,sync:true,aLoaded:true,bLoaded:true,cue:nil)
XCTAssertEqual(Array(leds.prefix(9)),[0x90,0,127,0x90,1,1,0x90,2,127])
}
@MainActor func testEQAndFilterDoubleClickActuallyResetKnobs() async throws {
var values = [6.0,-6.0,0.75]
let host = NSHostingView(rootView:HStack(spacing:0) { ForEach(0..<3,id:\.self) { i in DJKnob(label:["HIGH","LOW","FILTER"][i],value:Binding(get:{values[i]},set:{values[i] = $0}),range:i == 2 ? -1...1 : -12...12,reset:0,color:.cyan).frame(width:80,height:80) } }.frame(width:240,height:80))
host.frame = NSRect(x:0,y:0,width:240,height:80)
let window = NSWindow(contentRect:host.frame,styleMask:[.borderless],backing:.buffered,defer:false);window.contentView = host
let previous = NSApp.keyWindow;window.setFrameOrigin(NSPoint(x:-10000,y:-10000));window.orderFront(nil);window.makeKey()
defer { window.orderOut(nil);window.contentView = nil;previous?.makeKey() }
try await Task.sleep(nanoseconds:150_000_000);host.layoutSubtreeIfNeeded()
for index in 0..<3 {
for click in 1...2 {
for type in [NSEvent.EventType.leftMouseDown,.leftMouseUp] {
let event = try XCTUnwrap(NSEvent.mouseEvent(with:type,location:NSPoint(x:40+index*80,y:46),modifierFlags:[],timestamp:ProcessInfo.processInfo.systemUptime,windowNumber:window.windowNumber,context:nil,eventNumber:0,clickCount:click,pressure:type == .leftMouseDown ? 1 : 0));NSApp.sendEvent(event)
}
}
try await Task.sleep(nanoseconds:100_000_000)
}
XCTAssertEqual(values,[0,0,0])
}
}
extension KJamsCloneTests {
@MainActor func testMixtrackGoInstalledPresetReceivesTempoPadsAndIgnoresButtonReleases() throws {
let suite = "KJMix.Mixtrack.Tests.\(UUID().uuidString)",defaults = try XCTUnwrap(UserDefaults(suiteName:suite))
defer { defaults.removePersistentDomain(forName:suite) }
let midi = DJMIDI(defaults:defaults,connect:false);midi.installMixtrackGo()
var events = [(String,Double)]()
midi.onAction = { name,value,_ in if name != "Controller feedback" { events.append((name,value)) } }
midi.receive([0x90,0,127,0x90,0,0,0x80,0,0])
midi.receive([0xB0,0x09,64,0xB0,0x29,0])
midi.receive([0x95,0,127,0x95,0,0,0x95,0x17,127])
XCTAssertEqual(events.map{$0.0},["A Play","A Tempo","B Loop 8"])
XCTAssertEqual(events[1].1,0.5,accuracy:0.0001)
XCTAssertTrue(DJMIDI(defaults:defaults,connect:false).mixtrackEnabled)
}
}
extension KJamsCloneTests {
func testDJWaveBeatGuidesUseSourceBPMAndLimitOverviewDensity() {
XCTAssertEqual(DJWaveMarkers.beats(start:0,span:2,bpm:120,width:800),[0,1,2,3,4])
XCTAssertEqual(DJWaveMarkers.beats(start:1.1,span:1,bpm:120,width:800),[3,4])
XCTAssertTrue(DJWaveMarkers.beats(start:0,span:8,bpm:nil,width:800).isEmpty)
XCTAssertTrue(DJWaveMarkers.beats(start:0,span:8,bpm:0,width:800).isEmpty)
XCTAssertLessThanOrEqual(DJWaveMarkers.beats(start:0,span:7200,bpm:180,width:500).count,101)
}
}