| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165 |
- 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)
- }
- }
- }
- }
|