removed unwanted bar now with waveforms, major updates
This commit is contained in:
@@ -172,6 +172,16 @@ struct DJAudioDevice: Identifiable, Equatable {
|
||||
}
|
||||
|
||||
@MainActor final class DJSystem: ObservableObject {
|
||||
@Published var copyImportedMedia = false { didSet { perform { try library?.setSetting("DJ.CopyImportedMedia.v1",copyImportedMedia ? "1" : "0") } } }
|
||||
private var referenceAccess = [String:URL]()
|
||||
private var referenceLocations = [MediaLocation]()
|
||||
private func ensureReferenceAccess(_ path: String) {
|
||||
for location in referenceLocations where path == location.path || path.hasPrefix(location.path+"/") {
|
||||
guard referenceAccess[location.path] == nil,let bookmark = location.bookmark else { continue }
|
||||
var stale = false
|
||||
if let url = try? URL(resolvingBookmarkData:bookmark,options:[.withSecurityScope,.withoutUI],relativeTo:nil,bookmarkDataIsStale:&stale),url.startAccessingSecurityScopedResource() { referenceAccess[location.path] = url }
|
||||
}
|
||||
}
|
||||
@Published private(set) var mediaDirectory: URL
|
||||
@Published private(set) var waveformDirectory: URL
|
||||
@Published private(set) var movingStorage = false
|
||||
@@ -199,12 +209,12 @@ struct DJAudioDevice: Identifiable, Equatable {
|
||||
func moveStorage(to destination: URL) {
|
||||
guard !movingStorage,!importing,!generatingWaveforms,!namesBusy,!a.player.playing,!b.player.playing,!preview.playing else { error = "Stop playback and finish the current library job before moving DJ files.";return }
|
||||
let loadedA = a.song?.id,loadedB = b.song?.id,database = root.appendingPathComponent("Library.sqlite"),localRoot = root
|
||||
a.unload();b.unload();stopPreview();movingStorage = true;storageProgress = .init()
|
||||
a.unload();b.unload();stopPreview();movingStorage = true;storageProgress = .init(current:"Preparing DJ storage move…")
|
||||
storageTask = Task.detached(priority:.utility) { [weak self] in
|
||||
let access = destination.startAccessingSecurityScopedResource();defer { if access { destination.stopAccessingSecurityScopedResource() } }
|
||||
var latest = ImportProgress()
|
||||
do { latest = try DJStorageMove.run(database:database,defaultRoot:localRoot,destination:destination,cancelled:{Task.isCancelled}) { value in
|
||||
latest = value;Task { @MainActor [weak self] in self?.storageProgress = value }
|
||||
latest = value;Task { @MainActor [weak self] in guard let self,self.movingStorage,self.storageTask?.isCancelled != true else { return };self.storageProgress = value }
|
||||
} } catch is CancellationError { latest.cancelled = true;latest.current = "Move cancelled; verified files kept" }
|
||||
catch { latest.failure = error.localizedDescription;latest.current = "Move needs attention" }
|
||||
latest.finished = true;let result = latest
|
||||
@@ -216,7 +226,7 @@ struct DJAudioDevice: Identifiable, Equatable {
|
||||
}
|
||||
}
|
||||
}
|
||||
func cancelStorageMove() { storageTask?.cancel() }
|
||||
func cancelStorageMove() { storageTask?.cancel();storageProgress?.current = "Cancelling… Originals and verified copies will be kept." }
|
||||
@Published var namesBusy = false
|
||||
private var graphicSaveTask: Task<Void,Never>?
|
||||
@Published var graphicEQ = DJGraphicEQSettings() { didSet {
|
||||
@@ -241,10 +251,10 @@ struct DJAudioDevice: Identifiable, Equatable {
|
||||
guard !generatingWaveforms,!importing,!movingStorage 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 = waveformDirectory
|
||||
let cache = waveformDirectory,locations = referenceLocations
|
||||
generatingWaveforms = true;waveformJob = .init(total:tracks.count)
|
||||
waveformTask = Task.detached(priority:.utility) { [weak self] in
|
||||
let result = await WaveformBatch.runConcurrent(songs:tracks,locations:[],cache:cache,force:force) { update in
|
||||
let result = await WaveformBatch.runConcurrent(songs:tracks,locations:locations,cache:cache,force:force) { update in
|
||||
Task { @MainActor [weak self] in self?.waveformJob = update }
|
||||
}
|
||||
await MainActor.run { [weak self] in
|
||||
@@ -312,7 +322,7 @@ struct DJAudioDevice: Identifiable, Equatable {
|
||||
}.store(in:&syncObservers)
|
||||
perform {
|
||||
try FileManager.default.createDirectory(at:root,withIntermediateDirectories:true)
|
||||
library = try Library(url:root.appendingPathComponent("Library.sqlite"));try loadStorage();try refresh()
|
||||
library = try Library(url:root.appendingPathComponent("Library.sqlite"));copyImportedMedia = try library?.setting("DJ.CopyImportedMedia.v1") == "1";try loadStorage();try refresh()
|
||||
if let data = try? Data(contentsOf:root.appendingPathComponent("DJGraphicEQ.json")),let saved = try? JSONDecoder().decode(DJGraphicEQSettings.self,from:data) { graphicEQ = saved.sanitized }
|
||||
applyGraphicEQ()
|
||||
favourites = Set((try library?.setting("DJ.Favourites.v1") ?? "").split(separator:",").compactMap{Int64($0)})
|
||||
@@ -323,7 +333,7 @@ struct DJAudioDevice: Identifiable, Equatable {
|
||||
audioDeviceMonitor = DJAudioDeviceMonitor { [weak self] in self?.refreshDevices() }
|
||||
}
|
||||
private func saveSettings() { perform { let data = try JSONEncoder().encode(settings.sanitized);try data.write(to:root.appendingPathComponent("DJSettings.json"),options:.atomic) } }
|
||||
func refresh() throws { songs = try library?.songs() ?? [];playlists = try library?.entities("playlists") ?? [] }
|
||||
func refresh() throws { songs = try library?.songs() ?? [];playlists = try library?.entities("playlists") ?? [];referenceLocations = try library?.mediaLocations() ?? [] }
|
||||
func toggleFavourite(_ song: Int64) {
|
||||
var updated = favourites
|
||||
if !updated.insert(song).inserted { updated.remove(song) }
|
||||
@@ -340,7 +350,7 @@ struct DJAudioDevice: Identifiable, Equatable {
|
||||
func importFiles(_ urls: [URL],loadInto: String? = nil,overridePlaying: Bool = false) {
|
||||
guard !importing,!generatingWaveforms,!movingStorage,!urls.isEmpty else { return }
|
||||
importing = true;importProgress = .init();importLogVisible = true
|
||||
let database = root.appendingPathComponent("Library.sqlite"),media = mediaDirectory,cache = waveformDirectory,replacedSong = loadInto.flatMap{channel($0).song?.id}
|
||||
let database = root.appendingPathComponent("Library.sqlite"),media = mediaDirectory,cache = waveformDirectory,copyMedia = copyImportedMedia,replacedSong = loadInto.flatMap{channel($0).song?.id}
|
||||
importTask = Task.detached(priority:.utility) { [weak self] in
|
||||
let access = urls.filter{$0.startAccessingSecurityScopedResource()}
|
||||
defer { for url in access { url.stopAccessingSecurityScopedResource() } }
|
||||
@@ -348,7 +358,14 @@ struct DJAudioDevice: Identifiable, Equatable {
|
||||
var importedIDs = [Int64]()
|
||||
do {
|
||||
let db = try Library(url:database)
|
||||
importedIDs = try Importer(media:media,audioExtensions:["mp3","wav","m4a","mp4","m4v","mov","aiff","aif","caf","flac","aac"],includeGraphics:false,waveformCache:cache).run(urls,library:db,cancelled:{Task.isCancelled}) { value in
|
||||
if !copyMedia {
|
||||
for url in urls {
|
||||
if Task.isCancelled { throw CancellationError() }
|
||||
let bookmark = (try? url.bookmarkData(options:.withSecurityScope,includingResourceValuesForKeys:nil,relativeTo:nil)) ?? (try? url.bookmarkData(options:.minimalBookmark,includingResourceValuesForKeys:nil,relativeTo:nil))
|
||||
_ = try db.saveMediaLocation(name:url.lastPathComponent,url:url,bookmark:bookmark,external:true)
|
||||
}
|
||||
}
|
||||
importedIDs = try Importer(media:media,audioExtensions:["mp3","wav","m4a","mp4","m4v","mov","aiff","aif","caf","flac","aac"],includeGraphics:false,waveformCache:cache).run(urls,library:db,copyMedia:copyMedia,cancelled:{Task.isCancelled}) { value in
|
||||
latest = value
|
||||
Task { @MainActor [weak self] in if self?.importing == true { self?.importProgress = value } }
|
||||
}
|
||||
@@ -372,7 +389,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();waveformTask?.cancel();storageTask?.cancel();storageAccess?.stopAccessingSecurityScopedResource() }
|
||||
deinit { importTask?.cancel();waveformTask?.cancel();storageTask?.cancel();storageAccess?.stopAccessingSecurityScopedResource();for url in referenceAccess.values { url.stopAccessingSecurityScopedResource() } }
|
||||
|
||||
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 }
|
||||
@@ -389,6 +406,7 @@ struct DJAudioDevice: Identifiable, Equatable {
|
||||
func load(_ song: Song,into letter: String,overridePlaying: Bool = false,automated: Bool = false) {
|
||||
guard !movingStorage else { error = "Wait for the DJ storage move to finish.";return }
|
||||
perform {
|
||||
ensureReferenceAccess(song.audio)
|
||||
let deck = channel(letter)
|
||||
if settings.protectPlayingDeck,deck.player.playing,!overridePlaying { pendingReplacement = .init(deck:letter,song:song,urls:[]);return }
|
||||
if !automated { automix.manualTakeover() }
|
||||
@@ -441,6 +459,7 @@ struct DJAudioDevice: Identifiable, Equatable {
|
||||
try config.validate(devices:devices,defaultID:DJAudioDevice.defaultOutput())
|
||||
}
|
||||
func previewLibrary(_ song: Song) {
|
||||
ensureReferenceAccess(song.audio)
|
||||
perform {
|
||||
guard !cueUID.isEmpty else { throw LibraryError.message("Choose a headphone output in DJ Devices first.") }
|
||||
let output = try device(cueUID)
|
||||
@@ -1617,7 +1636,11 @@ struct DJPreferencesPane: View {
|
||||
}.padding(26).frame(maxWidth:.infinity,alignment:.leading)
|
||||
}
|
||||
}
|
||||
}.alert("DJ settings",isPresented:Binding(get:{system.error != nil},set:{if !$0 { system.error = nil }})) { Button("OK") { system.error = nil } } message: { Text(system.error ?? "") }
|
||||
}
|
||||
.sheet(isPresented:Binding(get:{system.storageProgress != nil},set:{if !$0,!system.movingStorage { system.storageProgress = nil }})) {
|
||||
if let progress = system.storageProgress { ImportProgressPanel(progress:progress,studio:"DJ Studio Storage",cancel:{system.cancelStorageMove()},close:{system.storageProgress = nil},isMove:true).interactiveDismissDisabled(system.movingStorage) }
|
||||
}
|
||||
.alert("DJ settings",isPresented:Binding(get:{system.error != nil},set:{if !$0 { system.error = nil }})) { Button("OK") { system.error = nil } } message: { Text(system.error ?? "") }
|
||||
}
|
||||
private var general: some View {
|
||||
VStack(alignment:.leading,spacing:26) {
|
||||
@@ -1656,6 +1679,12 @@ struct DJPreferencesPane: View {
|
||||
}
|
||||
private var library: some View {
|
||||
VStack(alignment:.leading,spacing:26) {
|
||||
PreferencesSection("Import Mode") {
|
||||
VStack(alignment:.leading,spacing:12) {
|
||||
Picker("Media files",selection:$system.copyImportedMedia) { Text("Reference files in their original location").tag(false);Text("Copy files into DJ storage").tag(true) }.pickerStyle(.radioGroup)
|
||||
Text("Referencing avoids duplicate media. Keep the source drive connected. Saved waveforms stay in your selected waveform folder. Audio inside ZIP archives is extracted into DJ storage.").font(.caption).foregroundStyle(.secondary)
|
||||
}.padding(12)
|
||||
}
|
||||
PreferencesSection("Media and Waveform Storage") {
|
||||
VStack(alignment:.leading,spacing:14) {
|
||||
LabeledContent("Media") { Text(system.mediaDirectory.path).textSelection(.enabled) }
|
||||
|
||||
Reference in New Issue
Block a user