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 static let `default` = ExportOptions() } /// 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 { // Copy audio files if requested if options.copyAudioFiles { let audioDir = url.deletingLastPathComponent() .appendingPathComponent(options.audioFilesRelativePath) let entries = playlist.sortedEntries try copyAudioFiles(entries: entries, to: audioDir, template: options.fileNameTemplate) } switch format { case .audition: try AuditionExporter.export(playlist: playlist, to: url, options: options) case .cueSheet: try CueSheetExporter.export(playlist: playlist, to: url, options: options) case .dawProject: try DAWProjectExporter.export(playlist: playlist, to: url, options: options) case .edl: try EDLExporter.export(playlist: playlist, to: url, options: options) case .m3u: try M3UExporter.export(playlist: playlist, to: url, options: options) } } private static func copyAudioFiles(entries: [PlaylistEntry], to directory: URL, template: String?) 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, track.hasLocalFile else { continue } let source = track.fileURL let ext = source.pathExtension let destName: String if let template { 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) } } } }