broken changes
This commit is contained in:
@@ -3,6 +3,7 @@ import SwiftUI
|
||||
import AVFoundation
|
||||
import AppKit
|
||||
import UniformTypeIdentifiers
|
||||
import CoreMIDI
|
||||
@testable import KJamsClone
|
||||
|
||||
final class KJamsCloneTests: XCTestCase {
|
||||
@@ -1342,7 +1343,7 @@ final class KJamsCloneTests: XCTestCase {
|
||||
@MainActor func testActualSingerDisclosurePressAndNavigationRetainExpansion() async throws {
|
||||
let root = FileManager.default.temporaryDirectory.appendingPathComponent(UUID().uuidString);defer { try? FileManager.default.removeItem(at:root) }
|
||||
let model = AppModel(storageURL:root);_ = try model.createEntity(.venue,name:"Hall");let singer = try model.createEntity(.singer,name:"Alex")
|
||||
let host = NSHostingView(rootView:LibraryView(model:model).frame(width:1320,height:900).preferredColorScheme(.dark))
|
||||
let host = NSHostingView(rootView:LibraryView(model:model,showDJ:false).frame(width:1320,height:900).preferredColorScheme(.dark))
|
||||
host.frame = NSRect(x:0,y:0,width:1320,height:900)
|
||||
let window = NSWindow(contentRect:host.frame,styleMask:[.borderless],backing:.buffered,defer:false);window.appearance = NSAppearance(named:.darkAqua);window.contentView = host
|
||||
let previous = NSApp.keyWindow
|
||||
@@ -1487,7 +1488,7 @@ final class KJamsCloneTests: XCTestCase {
|
||||
let a = try model.createSinger(name:"Alex",venue:venue,songs:[]),b = try model.createSinger(name:"Sam",venue:venue,songs:[])
|
||||
let audio = try XCTUnwrap(Bundle(for:Self.self).resourceURL?.appendingPathComponent("Fixtures/Demo.mp3"))
|
||||
let song = try XCTUnwrap(model.library).add(title:"Demo",audio:audio.path,cdg:nil);try model.refresh();model.activeSection = "Rotation";model.selectedSinger = a
|
||||
let host = NSHostingView(rootView:LibraryView(model:model).frame(width:1060,height:760).preferredColorScheme(.dark));host.frame = NSRect(x:0,y:0,width:1060,height:760)
|
||||
let host = NSHostingView(rootView:LibraryView(model:model,showDJ:false).frame(width:1060,height:760).preferredColorScheme(.dark));host.frame = NSRect(x:0,y:0,width:1060,height:760)
|
||||
let window = NSWindow(contentRect:host.frame,styleMask:[.borderless],backing:.buffered,defer:false);window.appearance = NSAppearance(named:.darkAqua);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() }
|
||||
@@ -1501,12 +1502,12 @@ final class KJamsCloneTests: XCTestCase {
|
||||
}
|
||||
try await Task.sleep(nanoseconds:120_000_000)
|
||||
}
|
||||
try await click(485,453);try capture("songs")
|
||||
try await click(990,516);try capture("added")
|
||||
try await click(485,502);try capture("songs")
|
||||
try await click(990,565);try capture("added")
|
||||
XCTAssertEqual(model.tonightSongIDs[a],[song],"Actual Add to Tonight button must enqueue the song")
|
||||
try await click(518,458);try capture("reordered")
|
||||
try await click(518,507);try capture("reordered")
|
||||
XCTAssertEqual(model.rotationSingers.map(\.id),[b,a],"Visible down arrow must move Alex after Sam")
|
||||
try await click(335,338)
|
||||
try await click(335,387)
|
||||
XCTAssertTrue(model.player.playing,"Actual Start Rotation button must play");XCTAssertTrue(model.rotationPlaybackActive);XCTAssertEqual(model.playbackSinger,a);XCTAssertNil(model.error)
|
||||
}
|
||||
|
||||
@@ -1626,3 +1627,175 @@ final class KJamsCloneTests: XCTestCase {
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
extension KJamsCloneTests {
|
||||
func testDJCrossfaderAndTempoMatch() {
|
||||
let left = DJMix.weights(0),right = DJMix.weights(1),centre = DJMix.weights(0.5)
|
||||
XCTAssertEqual(left.0,1,accuracy:0.0001);XCTAssertEqual(left.1,0,accuracy:0.0001)
|
||||
XCTAssertEqual(right.0,0,accuracy:0.0001);XCTAssertEqual(right.1,1,accuracy:0.0001)
|
||||
XCTAssertEqual(centre.0*centre.0+centre.1*centre.1,1,accuracy:0.0001)
|
||||
XCTAssertEqual(DJMix.weights(-20).0,1,accuracy:0.0001)
|
||||
XCTAssertEqual(DJMix.weights(.nan).0,centre.0,accuracy:0.0001)
|
||||
XCTAssertEqual(DJMix.syncRate(bpm:120,otherBPM:126,otherRate:1),1.05)
|
||||
XCTAssertNil(DJMix.syncRate(bpm:0,otherBPM:126,otherRate:1))
|
||||
XCTAssertNil(DJMix.syncRate(bpm:40,otherBPM:240,otherRate:1))
|
||||
}
|
||||
func testDJMIDIParserRunningStatusRealtimeSysExAndPacketSplits() {
|
||||
var decoder = DJMIDIDecoder()
|
||||
XCTAssertEqual(decoder.feed([0xB2,7]),[])
|
||||
XCTAssertEqual(decoder.feed([0xF8,127,8,0]),[.init(status:0xB2,data1:7,data2:127),.init(status:0xB2,data1:8,data2:0)])
|
||||
XCTAssertEqual(decoder.feed([0xF0,1,2,0xF8,3,0xF7,9,10]),[])
|
||||
XCTAssertEqual(decoder.feed([0x92,20,127,20,0,0x82,20,0]),[.init(status:0x92,data1:20,data2:127),.init(status:0x92,data1:20,data2:0),.init(status:0x82,data1:20,data2:0)])
|
||||
XCTAssertEqual(decoder.feed([0xC0,20,0xD0,1]),[])
|
||||
XCTAssertEqual(DJMIDIMapping.value(127,mode:"Relative signed"),-1)
|
||||
XCTAssertEqual(DJMIDIMapping.value(65,mode:"Relative offset"),1)
|
||||
}
|
||||
@MainActor func testDJMIDILearnPersistenceAndButtonReleaseFiltering() throws {
|
||||
let suite = "KJMix.DJ.Tests.\(UUID().uuidString)",defaults = try XCTUnwrap(UserDefaults(suiteName:suite))
|
||||
defer { defaults.removePersistentDomain(forName:suite) }
|
||||
let midi = DJMIDI(defaults:defaults,connect:false)
|
||||
var actions = [(String,Double)]();midi.onAction = { action,value,_ in actions.append((action,value)) }
|
||||
midi.learning = "A Play";midi.receive([0x90,60,127])
|
||||
XCTAssertNil(midi.learning);XCTAssertEqual(midi.mappings.count,1);XCTAssertTrue(actions.isEmpty)
|
||||
midi.receive([0x90,60,0,0x80,60,0]);XCTAssertTrue(actions.isEmpty)
|
||||
midi.receive([0x90,60,127]);XCTAssertEqual(actions.map{$0.0},["A Play"])
|
||||
midi.learning = "Crossfader";midi.receive([0xB0,3,64]);midi.receive([0xB0,3,0])
|
||||
XCTAssertEqual(actions.last?.0,"Crossfader");XCTAssertEqual(actions.last?.1,0)
|
||||
let restored = DJMIDI(defaults:defaults,connect:false)
|
||||
XCTAssertEqual(restored.mappings,midi.mappings)
|
||||
midi.remove(midi.mappings[0].id);XCTAssertEqual(midi.mappings.count,1)
|
||||
}
|
||||
@MainActor func testDJPausedLoadMixerIsolationTapAndBeatLoop() 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 song = Song(id:1,title:"DJ test",audio:fixture.path,cdg:nil,rating:0,plays:0)
|
||||
let karaoke = Player(waveformCacheDirectory:root.appendingPathComponent("Waveforms"))
|
||||
karaoke.volume = 0.42;karaoke.lowGain = 3
|
||||
let savedMix = try Data(contentsOf:root.appendingPathComponent("GlobalMixer.json"))
|
||||
let channel = DJChannel("A");defer { channel.player.stop();karaoke.stop() }
|
||||
try channel.load(song)
|
||||
XCTAssertFalse(channel.player.playing);XCTAssertFalse(channel.player.audioPlaybackStarted)
|
||||
XCTAssertEqual(channel.player.currentSongID,1);XCTAssertFalse(channel.bpmConfirmed)
|
||||
channel.tap(at:10);channel.tap(at:10.5);channel.tap(at:11)
|
||||
XCTAssertTrue(channel.bpmConfirmed);XCTAssertEqual(channel.bpm,120,accuracy:0.01)
|
||||
channel.level = 0.6;channel.gain = 6;channel.crossWeight = 0
|
||||
XCTAssertEqual(channel.player.volume,0,accuracy:0.0001)
|
||||
channel.crossWeight = 1;XCTAssertEqual(channel.player.volume,0.48,accuracy:0.0001);XCTAssertEqual(channel.player.inputGain,6)
|
||||
channel.player.lowGain = -5
|
||||
XCTAssertEqual(karaoke.volume,0.42);XCTAssertEqual(karaoke.lowGain,3)
|
||||
XCTAssertEqual(try Data(contentsOf:root.appendingPathComponent("GlobalMixer.json")),savedMix)
|
||||
channel.loopBeats = 1;channel.player.seek(0.5);channel.beatLoop()
|
||||
XCTAssertTrue(channel.player.loopEnabled);XCTAssertFalse(channel.player.playing)
|
||||
XCTAssertEqual(try XCTUnwrap(channel.player.loopIn),0.5,accuracy:0.01)
|
||||
XCTAssertEqual(try XCTUnwrap(channel.player.loopOut),1,accuracy:0.01)
|
||||
channel.beatLoop();XCTAssertFalse(channel.player.loopEnabled)
|
||||
}
|
||||
@MainActor func testDJDecksPlayIndependentlyAndSyncWithoutChangingKaraoke() async throws {
|
||||
let fixture = try XCTUnwrap(Bundle(for:Self.self).resourceURL?.appendingPathComponent("Fixtures/Demo.mp3"))
|
||||
let system = DJSystem(storageURL:FileManager.default.temporaryDirectory.appendingPathComponent(UUID().uuidString));defer { try? FileManager.default.removeItem(at:system.root) };defer { system.a.player.stop();system.b.player.stop();system.stopPreview() }
|
||||
let song = Song(id:1,title:"Two decks",audio:fixture.path,cdg:nil,rating:0,plays:0)
|
||||
system.master = 0 // Exercise real rendering silently.
|
||||
system.load(song,into:"A");system.load(song,into:"B")
|
||||
XCTAssertNil(system.error);XCTAssertFalse(system.a.player.playing);XCTAssertFalse(system.b.player.playing)
|
||||
system.a.player.toggle();system.b.player.seek(0.5);system.b.player.toggle()
|
||||
try await Task.sleep(nanoseconds:300_000_000)
|
||||
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.a.player.stopDeck();XCTAssertTrue(system.b.player.playing)
|
||||
system.startPreview(system.b);XCTAssertNotNil(system.error);XCTAssertNil(system.previewDeck);XCTAssertFalse(system.preview.playing)
|
||||
}
|
||||
}
|
||||
|
||||
extension KJamsCloneTests {
|
||||
@MainActor func testDJWorkstationNativeLayoutAndTransport() async throws {
|
||||
let root = FileManager.default.temporaryDirectory.appendingPathComponent(UUID().uuidString)
|
||||
defer { try? FileManager.default.removeItem(at:root) }
|
||||
let model = AppModel(storageURL:root)
|
||||
defer { model.player.stop();model.backgroundDeck.unload();model.dj.a.player.stop();model.dj.b.player.stop() }
|
||||
let fixture = try XCTUnwrap(Bundle(for:Self.self).resourceURL?.appendingPathComponent("Fixtures/Demo.mp3"))
|
||||
let id = try XCTUnwrap(model.library).add(title:"Midnight City · demo audio",audio:fixture.path,cdg:nil)
|
||||
try model.refresh()
|
||||
_ = try model.dj.library?.add(title:"DJ demo audio",audio:fixture.path,cdg:nil);try model.dj.refresh()
|
||||
model.dj.load(try XCTUnwrap(model.dj.songs.first),into:"A");model.dj.load(try XCTUnwrap(model.dj.songs.first),into:"B");model.dj.master = 0
|
||||
model.studio.automaticBackground = false
|
||||
try model.backgroundDeck.load(fixture,title:"Between singers · warm-up")
|
||||
model.dj.a.bpmConfirmed = true;model.dj.b.bpmConfirmed = true
|
||||
let host = NSHostingView(rootView:LibraryView(model:model).frame(width:1320,height:1100).preferredColorScheme(.dark))
|
||||
host.frame = NSRect(x:0,y:0,width:1320,height:1100)
|
||||
let window = NSWindow(contentRect:host.frame,styleMask:[.borderless],backing:.buffered,defer:false)
|
||||
window.appearance = NSAppearance(named:.darkAqua);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:350_000_000);host.layoutSubtreeIfNeeded()
|
||||
if let rep = host.bitmapImageRepForCachingDisplay(in:host.bounds) { host.cacheDisplay(in:host.bounds,to:rep);try XCTUnwrap(rep.representation(using:.png,properties:[:])).write(to:URL(fileURLWithPath:"/private/tmp/kjmix-dj-native.png")) }
|
||||
XCTAssertEqual(model.dj.a.player.currentSongID,id);XCTAssertEqual(model.dj.b.player.currentSongID,id)
|
||||
XCTAssertFalse(model.dj.a.player.playing);XCTAssertNil(model.player.currentSongID)
|
||||
}
|
||||
}
|
||||
|
||||
extension KJamsCloneTests {
|
||||
@MainActor func testDJReceivesActualCoreMIDIVirtualSourcePackets() async throws {
|
||||
var client = MIDIClientRef(0),source = MIDIEndpointRef(0)
|
||||
XCTAssertEqual(MIDIClientCreateWithBlock("KJMix MIDI test" as CFString,&client,nil),noErr)
|
||||
defer { if source != 0 { MIDIEndpointDispose(source) };MIDIClientDispose(client) }
|
||||
XCTAssertEqual(MIDISourceCreate(client,"KJMix virtual test controller" as CFString,&source),noErr)
|
||||
let suite = "KJMix.VirtualMIDI.\(UUID().uuidString)",defaults = try XCTUnwrap(UserDefaults(suiteName:suite))
|
||||
defer { defaults.removePersistentDomain(forName:suite) }
|
||||
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)
|
||||
var actions = [String]();midi.onAction = { action,_,_ in actions.append(action) }
|
||||
func send(_ packets: [[UInt8]]) throws {
|
||||
var list = MIDIPacketList()
|
||||
try withUnsafeMutablePointer(to:&list) { pointer in
|
||||
var packet = MIDIPacketListInit(pointer)
|
||||
for bytes in packets {
|
||||
packet = try bytes.withUnsafeBufferPointer { data in try XCTUnwrap(MIDIPacketListAdd(pointer,MemoryLayout<MIDIPacketList>.size,packet,0,data.count,data.baseAddress!)) }
|
||||
}
|
||||
XCTAssertEqual(MIDIReceived(source,pointer),noErr)
|
||||
}
|
||||
}
|
||||
midi.learning = "A Play";try send([[0x90,40,127]])
|
||||
try await Task.sleep(nanoseconds:80_000_000)
|
||||
XCTAssertEqual(midi.mappings.first?.action,"A Play");XCTAssertTrue(actions.isEmpty)
|
||||
try send([[0x90,40,0],[0x90,40,127],[0x80,40,0]])
|
||||
try await Task.sleep(nanoseconds:80_000_000)
|
||||
XCTAssertEqual(actions,["A Play"])
|
||||
midi.select(0)
|
||||
}
|
||||
}
|
||||
|
||||
extension KJamsCloneTests {
|
||||
@MainActor func testDJLibraryRecursiveImportPlaylistsAndPersistenceAreSeparateFromKaraoke() async throws {
|
||||
let root = FileManager.default.temporaryDirectory.appendingPathComponent(UUID().uuidString)
|
||||
defer { try? FileManager.default.removeItem(at:root) }
|
||||
let model = AppModel(storageURL:root)
|
||||
let fixtures = try XCTUnwrap(Bundle(for:Self.self).resourceURL?.appendingPathComponent("Fixtures"))
|
||||
let folder = root.appendingPathComponent("Input/Artist/Album")
|
||||
try FileManager.default.createDirectory(at:folder,withIntermediateDirectories:true)
|
||||
try FileManager.default.copyItem(at:fixtures.appendingPathComponent("Demo.mp3"),to:folder.appendingPathComponent("DJ Song.mp3"))
|
||||
try FileManager.default.copyItem(at:fixtures.appendingPathComponent("Demo.cdg"),to:folder.appendingPathComponent("DJ Song.cdg"))
|
||||
let karaoke = try XCTUnwrap(model.library),dj = try XCTUnwrap(model.dj.library)
|
||||
_ = try karaoke.add(title:"Karaoke only",audio:fixtures.appendingPathComponent("TestTrack.wav").path,cdg:nil)
|
||||
try model.refresh();XCTAssertTrue(model.dj.songs.isEmpty)
|
||||
model.dj.importFiles([root.appendingPathComponent("Input")])
|
||||
for _ in 0..<200 { if !model.dj.importing { break };try await Task.sleep(nanoseconds:25_000_000) }
|
||||
XCTAssertFalse(model.dj.importing);XCTAssertNil(model.dj.error)
|
||||
XCTAssertEqual(model.dj.songs.count,1);XCTAssertEqual(try karaoke.songs().map(\.title),["Karaoke only"])
|
||||
let track = try XCTUnwrap(model.dj.songs.first)
|
||||
XCTAssertTrue(track.audio.hasPrefix(root.appendingPathComponent("DJ/Media").path));XCTAssertNil(track.cdg)
|
||||
model.dj.createPlaylist("DJ set");let playlist = try XCTUnwrap(model.dj.playlists.first)
|
||||
model.dj.addToPlaylist(playlist.id,song:track.id)
|
||||
XCTAssertTrue(try karaoke.entities("playlists").isEmpty)
|
||||
XCTAssertEqual(try dj.playlistSongIDs(playlist.id),[track.id])
|
||||
model.dj.importFiles([root.appendingPathComponent("Input")])
|
||||
for _ in 0..<200 { if !model.dj.importing { break };try await Task.sleep(nanoseconds:25_000_000) }
|
||||
XCTAssertEqual(model.dj.songs.count,1)
|
||||
let reopened = DJSystem(storageURL:model.dj.root)
|
||||
XCTAssertEqual(reopened.songs.map(\.id),[track.id]);XCTAssertEqual(reopened.playlists.map(\.name),["DJ set"])
|
||||
XCTAssertNotEqual(model.dragReference(track.id).library,model.dj.dragReference(track.id).library)
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user