|
|
|
@@ -151,6 +151,28 @@ struct DJAudioDevice: Identifiable, Equatable {
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
@MainActor final class DJSystem: ObservableObject {
|
|
|
|
|
@Published private(set) var waveformJob: WaveformJobProgress?
|
|
|
|
|
@Published private(set) var generatingWaveforms = false
|
|
|
|
|
private var waveformTask: Task<Void,Never>?
|
|
|
|
|
func generateWaveforms(_ ids: Set<Int64>? = nil,force: Bool = false) {
|
|
|
|
|
guard !generatingWaveforms,!importing else { error = "Wait for the current import or waveform job to finish first.";return }
|
|
|
|
|
let tracks = songs.filter { ids == nil || ids!.contains($0.id) }
|
|
|
|
|
guard !tracks.isEmpty else { error = "Choose a DJ library track first.";return }
|
|
|
|
|
let cache = root.appendingPathComponent("Waveforms")
|
|
|
|
|
generatingWaveforms = true;waveformJob = .init(total:tracks.count)
|
|
|
|
|
waveformTask = Task.detached(priority:.utility) { [weak self] in
|
|
|
|
|
let result = WaveformBatch.run(songs:tracks,locations:[],cache:cache,force:force,cancelled:{Task.isCancelled}) { update in
|
|
|
|
|
Task { @MainActor [weak self] in self?.waveformJob = update }
|
|
|
|
|
}
|
|
|
|
|
await MainActor.run { [weak self] in
|
|
|
|
|
guard let self else { return }
|
|
|
|
|
self.waveformJob = result;self.generatingWaveforms = false;self.waveformTask = nil
|
|
|
|
|
for deck in [self.a,self.b] where tracks.contains(where:{$0.id == deck.song?.id}) { deck.player.refreshCachedWaveform() }
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
func cancelWaveforms() { waveformTask?.cancel();waveformJob?.current = "Cancelling…" }
|
|
|
|
|
func dismissWaveforms() { if !generatingWaveforms { waveformJob = nil } }
|
|
|
|
|
@Published private(set) var favourites = Set<Int64>()
|
|
|
|
|
let a: DJChannel
|
|
|
|
|
let b: DJChannel
|
|
|
|
@@ -230,7 +252,7 @@ struct DJAudioDevice: Identifiable, Equatable {
|
|
|
|
|
func addToPlaylist(_ playlist: Int64,song: Int64) { perform { try library?.addToPlaylist(playlist,song:song);try refresh() } }
|
|
|
|
|
func dragReference(_ song: Int64) -> SongDragReference { .init(song:song,library:dragSession,singer:nil,venue:nil,fromTonight:false) }
|
|
|
|
|
func importFiles(_ urls: [URL],loadInto: String? = nil,overridePlaying: Bool = false) {
|
|
|
|
|
guard !importing,!urls.isEmpty else { return }
|
|
|
|
|
guard !importing,!generatingWaveforms,!urls.isEmpty else { return }
|
|
|
|
|
importing = true;importProgress = .init();importLogVisible = true
|
|
|
|
|
let database = root.appendingPathComponent("Library.sqlite"),media = root.appendingPathComponent("Media"),cache = root.appendingPathComponent("Waveforms"),replacedSong = loadInto.flatMap{channel($0).song?.id}
|
|
|
|
|
importTask = Task.detached(priority:.utility) { [weak self] in
|
|
|
|
@@ -264,7 +286,7 @@ struct DJAudioDevice: Identifiable, Equatable {
|
|
|
|
|
requestFileLoad(next.urls,into:next.deck,override:next.override && channel(next.deck).song?.id == next.song)
|
|
|
|
|
}
|
|
|
|
|
func cancelReplacement() { pendingReplacement = nil;continueFileLoads() }
|
|
|
|
|
deinit { importTask?.cancel() }
|
|
|
|
|
deinit { importTask?.cancel();waveformTask?.cancel() }
|
|
|
|
|
|
|
|
|
|
func perform(_ action: () throws -> Void) { do { try action() } catch { self.error = error.localizedDescription } }
|
|
|
|
|
func applyMix() { let weights = DJMix.weights(crossfader,curve:settings.crossfaderCurve);let output = master*pow(10,-settings.headroomDB/20);a.master = output;b.master = output;a.crossWeight = routing.mode == "External" ? 1 : weights.0;b.crossWeight = routing.mode == "External" ? 1 : weights.1 }
|
|
|
|
@@ -724,7 +746,7 @@ private struct DJLibraryPageScroll: NSViewRepresentable {
|
|
|
|
|
private var observation: NSObjectProtocol?
|
|
|
|
|
private var endObservation: NSObjectProtocol?
|
|
|
|
|
private var advanced = false
|
|
|
|
|
private var lastY = 0.0
|
|
|
|
|
private var lastY: CGFloat = 0
|
|
|
|
|
override func viewDidMoveToWindow() { super.viewDidMoveToWindow();DispatchQueue.main.async { [weak self] in self?.attach() } }
|
|
|
|
|
func attach() {
|
|
|
|
|
guard window != nil,scroll == nil else { return }
|
|
|
|
@@ -772,6 +794,7 @@ private struct DJQueueButton: View {
|
|
|
|
|
|
|
|
|
|
struct DJConsole: View {
|
|
|
|
|
@ObservedObject var system: DJSystem
|
|
|
|
|
@State private var waveformDetails = false
|
|
|
|
|
@State private var favouritesOnly = false
|
|
|
|
|
@State private var queueVisible = false
|
|
|
|
|
@State private var query = ""
|
|
|
|
@@ -795,6 +818,9 @@ struct DJConsole: View {
|
|
|
|
|
header
|
|
|
|
|
DJContinuousConsole(system:system,loadA:{loadSelected("A")},loadB:{loadSelected("B")}).fixedSize(horizontal:false,vertical:true)
|
|
|
|
|
browser.frame(maxWidth:.infinity,maxHeight:.infinity)
|
|
|
|
|
if let progress = system.waveformJob {
|
|
|
|
|
WaveformProgressCard(progress:progress,running:system.generatingWaveforms,studio:"DJ",cancel:{system.cancelWaveforms()},details:{waveformDetails = true},dismiss:{system.dismissWaveforms()})
|
|
|
|
|
}
|
|
|
|
|
if let progress = system.importProgress {
|
|
|
|
|
HStack {
|
|
|
|
|
Text(progress.current).lineLimit(1);Text("\(progress.completed)/\(progress.total)").monospacedDigit()
|
|
|
|
@@ -818,6 +844,13 @@ struct DJConsole: View {
|
|
|
|
|
}
|
|
|
|
|
.sheet(isPresented:$system.cueEditing) { DJCueEditor(system:system) }
|
|
|
|
|
.sheet(isPresented:Binding(get:{system.loopEditor != nil},set:{if !$0 { system.loopEditor = nil }})) { let deck = system.channel(system.loopEditor ?? "A");DJLoopEditor(player:deck.player,letter:deck.letter) }
|
|
|
|
|
.sheet(isPresented:$waveformDetails) {
|
|
|
|
|
VStack(alignment:.leading,spacing:16) {
|
|
|
|
|
Text("DJ Waveform Details").font(.title2.bold())
|
|
|
|
|
ScrollView { Text(system.waveformJob?.messages.joined(separator:"\n\n") ?? "").textSelection(.enabled).frame(maxWidth:.infinity,alignment:.leading) }
|
|
|
|
|
HStack { Spacer();Button("Done") { waveformDetails = false }.keyboardShortcut(.defaultAction) }
|
|
|
|
|
}.padding(24).frame(width:620,height:400)
|
|
|
|
|
}
|
|
|
|
|
.sheet(isPresented:$queueVisible) {
|
|
|
|
|
VStack(spacing:16) {
|
|
|
|
|
DJDeckDropZone(system:system,letter:"Queue") { DJQueuePane(system:system,automix:system.automix) }.frame(maxWidth:.infinity,maxHeight:.infinity)
|
|
|
|
@@ -895,6 +928,14 @@ struct DJConsole: View {
|
|
|
|
|
Divider()
|
|
|
|
|
Button(system.favourites.contains(song.id) ? "Remove from Favourites" : "Add to Favourites") { system.toggleFavourite(song.id) }
|
|
|
|
|
Button("Add to DJ Queue") { system.automix.enqueue([song]) }
|
|
|
|
|
Menu("Waveforms") {
|
|
|
|
|
Button("Generate Waveform for This Track") { system.generateWaveforms([song.id]) }
|
|
|
|
|
Button("Generate Waveforms for Loaded Decks") { system.generateWaveforms(Set([system.a.song?.id,system.b.song?.id].compactMap{$0})) }.disabled(system.a.song == nil && system.b.song == nil)
|
|
|
|
|
Button("Generate Waveforms for All DJ Library Tracks") { system.generateWaveforms() }
|
|
|
|
|
Divider()
|
|
|
|
|
Button("Regenerate This Waveform") { system.generateWaveforms([song.id],force:true) }
|
|
|
|
|
Button("Regenerate All DJ Library Waveforms") { system.generateWaveforms(force:true) }
|
|
|
|
|
}.disabled(system.generatingWaveforms || system.importing)
|
|
|
|
|
Menu("Add to Playlist") { ForEach(system.playlists) { list in Button(list.name) { system.addToPlaylist(list.id,song:song.id);refreshPlaylist() } } }
|
|
|
|
|
if let playlist {
|
|
|
|
|
Button("Move Earlier") { system.perform { try system.library?.reorderPlaylist(playlist,song:song.id,by:-1) };refreshPlaylist() }
|
|
|
|
|