import Foundation /// Protocol for all DAW export formats. protocol DAWExporter { /// Unique identifier for this export format. static var formatID: String { get } /// Display name for UI. static var formatName: String { get } /// File extension(s) for the export. static var fileExtension: String { get } /// Export a playlist to the specified URL. static func export(playlist: Playlist, to url: URL, options: ExportOptions) throws } /// Options for DAW export. struct ExportOptions { /// Whether to copy audio files alongside the session file. var copyAudioFiles: Bool = true /// Target sample rate for the session (nil = use original). var targetSampleRate: Double? = nil /// Target bit depth for the session. var targetBitDepth: Int = 24 /// Include cue points as markers in the DAW session. var includeCuePoints: Bool = true /// Include crossfade information. var includeCrossfades: Bool = true /// Base directory for relative audio file paths. var audioFilesRelativePath: String = "Audio Files" /// Template for renaming copied files. Nil = keep original filenames. var fileNameTemplate: String? = nil /// Downloaded cloud track files, mapped by Track.id to local temp URL. /// Used by the export pipeline to include cloud tracks that were downloaded before export. var downloadedFiles: [UUID: URL] = [:] static let `default` = ExportOptions() /// Returns the effective file URL for a track, considering downloaded cloud files. func effectiveFileURL(for track: Track) -> URL? { if let downloaded = downloadedFiles[track.id] { return downloaded } guard track.hasLocalFile else { return nil } return track.fileURL } } /// Central exporter that dispatches to format-specific exporters. struct MixExporter { enum ExportFormat: String, CaseIterable, Identifiable { case audition = "audition" case cueSheet = "cue" case dawProject = "dawproject" case edl = "edl" case m3u = "m3u" var id: String { rawValue } var name: String { switch self { case .audition: return "Adobe Audition Session" case .cueSheet: return "Cue Sheet (.cue)" case .dawProject: return "DAWproject (Open Standard)" case .edl: return "Edit Decision List (EDL)" case .m3u: return "M3U Playlist" } } var fileExtension: String { switch self { case .audition: return "sesx" case .cueSheet: return "cue" case .dawProject: return "dawproject" case .edl: return "edl" case .m3u: return "m3u" } } var description: String { switch self { case .audition: return "Adobe Audition multitrack session with markers and crossfades" case .cueSheet: return "Standard cue sheet format, compatible with many audio tools" case .dawProject: return "Open DAW exchange format (Bitwig, PreSonus, REAPER)" case .edl: return "CMX 3600 Edit Decision List for professional DAWs" case .m3u: return "Simple playlist format for basic file list export" } } } /// Export a playlist in the specified format. static func export( playlist: Playlist, format: ExportFormat, to url: URL, options: ExportOptions = .default ) throws { // Wrap output in a parent folder named after the session file let baseName = url.deletingPathExtension().lastPathComponent let wrapperDir = url.deletingLastPathComponent().appendingPathComponent(baseName, isDirectory: true) try FileManager.default.createDirectory(at: wrapperDir, withIntermediateDirectories: true) let wrappedURL = wrapperDir.appendingPathComponent(url.lastPathComponent) // Copy audio files if requested if options.copyAudioFiles { let audioDir = wrapperDir .appendingPathComponent(options.audioFilesRelativePath) let entries = playlist.sortedEntries try copyAudioFiles(entries: entries, to: audioDir, options: options) } switch format { case .audition: try AuditionExporter.export(playlist: playlist, to: wrappedURL, options: options) case .cueSheet: try CueSheetExporter.export(playlist: playlist, to: wrappedURL, options: options) case .dawProject: try DAWProjectExporter.export(playlist: playlist, to: wrappedURL, options: options) case .edl: try EDLExporter.export(playlist: playlist, to: wrappedURL, options: options) case .m3u: try M3UExporter.export(playlist: playlist, to: wrappedURL, options: options) } } private static func copyAudioFiles(entries: [PlaylistEntry], to directory: URL, options: ExportOptions) throws { let fm = FileManager.default try fm.createDirectory(at: directory, withIntermediateDirectories: true) let totalTracks = entries.count for (index, entry) in entries.enumerated() { guard let track = entry.track, let source = options.effectiveFileURL(for: track) else { continue } let ext = source.pathExtension let destName: String if let template = options.fileNameTemplate { let baseName = FileNameTemplate.generate( template: template, track: track, playlistIndex: index, totalTracks: totalTracks ) destName = "\(baseName).\(ext)" } else { destName = source.lastPathComponent } let dest = directory.appendingPathComponent(destName) if !fm.fileExists(atPath: dest.path) { try fm.copyItem(at: source, to: dest) } } } }