DAWExporter.swift 5.0 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146
  1. import Foundation
  2. /// Protocol for all DAW export formats.
  3. protocol DAWExporter {
  4. /// Unique identifier for this export format.
  5. static var formatID: String { get }
  6. /// Display name for UI.
  7. static var formatName: String { get }
  8. /// File extension(s) for the export.
  9. static var fileExtension: String { get }
  10. /// Export a playlist to the specified URL.
  11. static func export(playlist: Playlist, to url: URL, options: ExportOptions) throws
  12. }
  13. /// Options for DAW export.
  14. struct ExportOptions {
  15. /// Whether to copy audio files alongside the session file.
  16. var copyAudioFiles: Bool = true
  17. /// Target sample rate for the session (nil = use original).
  18. var targetSampleRate: Double? = nil
  19. /// Target bit depth for the session.
  20. var targetBitDepth: Int = 24
  21. /// Include cue points as markers in the DAW session.
  22. var includeCuePoints: Bool = true
  23. /// Include crossfade information.
  24. var includeCrossfades: Bool = true
  25. /// Base directory for relative audio file paths.
  26. var audioFilesRelativePath: String = "Audio Files"
  27. /// Template for renaming copied files. Nil = keep original filenames.
  28. var fileNameTemplate: String? = nil
  29. static let `default` = ExportOptions()
  30. }
  31. /// Central exporter that dispatches to format-specific exporters.
  32. struct MixExporter {
  33. enum ExportFormat: String, CaseIterable, Identifiable {
  34. case audition = "audition"
  35. case cueSheet = "cue"
  36. case dawProject = "dawproject"
  37. case edl = "edl"
  38. case m3u = "m3u"
  39. var id: String { rawValue }
  40. var name: String {
  41. switch self {
  42. case .audition: return "Adobe Audition Session"
  43. case .cueSheet: return "Cue Sheet (.cue)"
  44. case .dawProject: return "DAWproject (Open Standard)"
  45. case .edl: return "Edit Decision List (EDL)"
  46. case .m3u: return "M3U Playlist"
  47. }
  48. }
  49. var fileExtension: String {
  50. switch self {
  51. case .audition: return "sesx"
  52. case .cueSheet: return "cue"
  53. case .dawProject: return "dawproject"
  54. case .edl: return "edl"
  55. case .m3u: return "m3u"
  56. }
  57. }
  58. var description: String {
  59. switch self {
  60. case .audition:
  61. return "Adobe Audition multitrack session with markers and crossfades"
  62. case .cueSheet:
  63. return "Standard cue sheet format, compatible with many audio tools"
  64. case .dawProject:
  65. return "Open DAW exchange format (Bitwig, PreSonus, REAPER)"
  66. case .edl:
  67. return "CMX 3600 Edit Decision List for professional DAWs"
  68. case .m3u:
  69. return "Simple playlist format for basic file list export"
  70. }
  71. }
  72. }
  73. /// Export a playlist in the specified format.
  74. static func export(
  75. playlist: Playlist,
  76. format: ExportFormat,
  77. to url: URL,
  78. options: ExportOptions = .default
  79. ) throws {
  80. // Copy audio files if requested
  81. if options.copyAudioFiles {
  82. let audioDir = url.deletingLastPathComponent()
  83. .appendingPathComponent(options.audioFilesRelativePath)
  84. let entries = playlist.sortedEntries
  85. try copyAudioFiles(entries: entries, to: audioDir, template: options.fileNameTemplate)
  86. }
  87. switch format {
  88. case .audition: try AuditionExporter.export(playlist: playlist, to: url, options: options)
  89. case .cueSheet: try CueSheetExporter.export(playlist: playlist, to: url, options: options)
  90. case .dawProject: try DAWProjectExporter.export(playlist: playlist, to: url, options: options)
  91. case .edl: try EDLExporter.export(playlist: playlist, to: url, options: options)
  92. case .m3u: try M3UExporter.export(playlist: playlist, to: url, options: options)
  93. }
  94. }
  95. private static func copyAudioFiles(entries: [PlaylistEntry], to directory: URL, template: String?) throws {
  96. let fm = FileManager.default
  97. try fm.createDirectory(at: directory, withIntermediateDirectories: true)
  98. let totalTracks = entries.count
  99. for (index, entry) in entries.enumerated() {
  100. guard let track = entry.track, track.hasLocalFile else { continue }
  101. let source = track.fileURL
  102. let ext = source.pathExtension
  103. let destName: String
  104. if let template {
  105. let baseName = FileNameTemplate.generate(
  106. template: template,
  107. track: track,
  108. playlistIndex: index,
  109. totalTracks: totalTracks
  110. )
  111. destName = "\(baseName).\(ext)"
  112. } else {
  113. destName = source.lastPathComponent
  114. }
  115. let dest = directory.appendingPathComponent(destName)
  116. if !fm.fileExists(atPath: dest.path) {
  117. try fm.copyItem(at: source, to: dest)
  118. }
  119. }
  120. }
  121. }