ExporterTests.swift 8.1 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206
  1. import XCTest
  2. @testable import MixBoard
  3. /// Tests for all DAW exporters.
  4. final class ExporterTests: XCTestCase {
  5. private var playlist: Playlist!
  6. private var outputDir: URL!
  7. override func setUp() {
  8. super.setUp()
  9. outputDir = FileManager.default.temporaryDirectory
  10. .appendingPathComponent("MixBoardExportTests", isDirectory: true)
  11. try? FileManager.default.createDirectory(at: outputDir, withIntermediateDirectories: true)
  12. playlist = Playlist(name: "Test Mix")
  13. let t1 = Track(title: "Track One", artist: "Artist A", album: "Album X", filePath: "/tmp/t1.mp3", duration: 180, fileFormat: "MP3")
  14. let t2 = Track(title: "Track Two", artist: "Artist B", album: "Album Y", filePath: "/tmp/t2.wav", duration: 240, fileFormat: "WAV")
  15. let t3 = Track(title: "Track Three", artist: "Artist A", filePath: "/tmp/t3.flac", duration: 300, fileFormat: "FLAC")
  16. t1.bpm = 128
  17. t1.musicalKey = "Am"
  18. t2.bpm = 130
  19. playlist.addTrack(t1, crossfadeDuration: 0)
  20. playlist.addTrack(t2, crossfadeDuration: 2.0)
  21. playlist.addTrack(t3, crossfadeDuration: 1.5)
  22. }
  23. override func tearDown() {
  24. super.tearDown()
  25. try? FileManager.default.removeItem(at: outputDir)
  26. }
  27. // MARK: - Audition Exporter
  28. func testAuditionExport() throws {
  29. let url = outputDir.appendingPathComponent("test.sesx")
  30. var options = ExportOptions.default
  31. options.copyAudioFiles = false
  32. try AuditionExporter.export(playlist: playlist, to: url, options: options)
  33. let content = try String(contentsOf: url, encoding: .utf8)
  34. // Verify XML structure
  35. XCTAssertTrue(content.contains("<!DOCTYPE sesx>"))
  36. XCTAssertTrue(content.contains("<sesx version=\"1.9\">"))
  37. XCTAssertTrue(content.contains("<session"))
  38. XCTAssertTrue(content.contains("sampleRate="))
  39. XCTAssertTrue(content.contains("<audioTrack"))
  40. XCTAssertTrue(content.contains("<audioClip"))
  41. XCTAssertTrue(content.contains("Track One"))
  42. XCTAssertTrue(content.contains("Track Two"))
  43. XCTAssertTrue(content.contains("Track Three"))
  44. XCTAssertTrue(content.contains("<files>"))
  45. XCTAssertTrue(content.contains("absolutePath="))
  46. XCTAssertTrue(content.contains("</sesx>"))
  47. }
  48. func testAuditionExportFileReferences() throws {
  49. let url = outputDir.appendingPathComponent("test_refs.sesx")
  50. var options = ExportOptions.default
  51. options.copyAudioFiles = false
  52. try AuditionExporter.export(playlist: playlist, to: url, options: options)
  53. let content = try String(contentsOf: url, encoding: .utf8)
  54. // Should have file references with media handlers
  55. XCTAssertTrue(content.contains("mediaHandler=\"AmioMP3\""))
  56. XCTAssertTrue(content.contains("mediaHandler=\"AmioWav\""))
  57. XCTAssertTrue(content.contains("mediaHandler=\"AmioLSF\""))
  58. }
  59. func testAuditionExportMasterTrack() throws {
  60. let url = outputDir.appendingPathComponent("test_master.sesx")
  61. var options = ExportOptions.default
  62. options.copyAudioFiles = false
  63. try AuditionExporter.export(playlist: playlist, to: url, options: options)
  64. let content = try String(contentsOf: url, encoding: .utf8)
  65. XCTAssertTrue(content.contains("<masterTrack"))
  66. XCTAssertTrue(content.contains("Audition.Fader"))
  67. }
  68. // MARK: - CUE Sheet Exporter
  69. func testCueSheetExport() throws {
  70. let url = outputDir.appendingPathComponent("test.cue")
  71. var options = ExportOptions.default
  72. options.copyAudioFiles = false
  73. try CueSheetExporter.export(playlist: playlist, to: url, options: options)
  74. let content = try String(contentsOf: url, encoding: .utf8)
  75. XCTAssertTrue(content.contains("TITLE \"Test Mix\""))
  76. XCTAssertTrue(content.contains("TRACK 01 AUDIO"))
  77. XCTAssertTrue(content.contains("TRACK 02 AUDIO"))
  78. XCTAssertTrue(content.contains("TRACK 03 AUDIO"))
  79. XCTAssertTrue(content.contains("TITLE \"Track One\""))
  80. XCTAssertTrue(content.contains("PERFORMER \"Artist A\""))
  81. XCTAssertTrue(content.contains("INDEX 01"))
  82. }
  83. func testCueSheetBPMAndKey() throws {
  84. let url = outputDir.appendingPathComponent("test_bpm.cue")
  85. var options = ExportOptions.default
  86. options.copyAudioFiles = false
  87. try CueSheetExporter.export(playlist: playlist, to: url, options: options)
  88. let content = try String(contentsOf: url, encoding: .utf8)
  89. XCTAssertTrue(content.contains("REM BPM 128.0"))
  90. XCTAssertTrue(content.contains("REM KEY Am"))
  91. }
  92. // MARK: - EDL Exporter
  93. func testEDLExport() throws {
  94. let url = outputDir.appendingPathComponent("test.edl")
  95. var options = ExportOptions.default
  96. options.copyAudioFiles = false
  97. try EDLExporter.export(playlist: playlist, to: url, options: options)
  98. let content = try String(contentsOf: url, encoding: .utf8)
  99. XCTAssertTrue(content.contains("TITLE: Test Mix"))
  100. XCTAssertTrue(content.contains("FCM: NON-DROP FRAME"))
  101. XCTAssertTrue(content.contains("001"))
  102. XCTAssertTrue(content.contains("FROM CLIP NAME: Track One"))
  103. XCTAssertTrue(content.contains("ARTIST: Artist A"))
  104. }
  105. func testEDLTimecodes() throws {
  106. let url = outputDir.appendingPathComponent("test_tc.edl")
  107. var options = ExportOptions.default
  108. options.copyAudioFiles = false
  109. try EDLExporter.export(playlist: playlist, to: url, options: options)
  110. let content = try String(contentsOf: url, encoding: .utf8)
  111. // Should contain valid HH:MM:SS:FF timecodes
  112. let timecodePattern = #"\d{2}:\d{2}:\d{2}:\d{2}"#
  113. let regex = try NSRegularExpression(pattern: timecodePattern)
  114. let matches = regex.numberOfMatches(in: content, range: NSRange(content.startIndex..., in: content))
  115. XCTAssertGreaterThan(matches, 0, "Should contain SMPTE timecodes")
  116. }
  117. // MARK: - M3U Exporter
  118. func testM3UExport() throws {
  119. let url = outputDir.appendingPathComponent("test.m3u")
  120. var options = ExportOptions.default
  121. options.copyAudioFiles = false
  122. try M3UExporter.export(playlist: playlist, to: url, options: options)
  123. let content = try String(contentsOf: url, encoding: .utf8)
  124. XCTAssertTrue(content.contains("#EXTM3U"))
  125. XCTAssertTrue(content.contains("#PLAYLIST:Test Mix"))
  126. XCTAssertTrue(content.contains("#EXTINF:180,Artist A - Track One"))
  127. XCTAssertTrue(content.contains("#EXTINF:240,Artist B - Track Two"))
  128. }
  129. func testM3UBPMMetadata() throws {
  130. let url = outputDir.appendingPathComponent("test_meta.m3u")
  131. var options = ExportOptions.default
  132. options.copyAudioFiles = false
  133. try M3UExporter.export(playlist: playlist, to: url, options: options)
  134. let content = try String(contentsOf: url, encoding: .utf8)
  135. XCTAssertTrue(content.contains("#EXTBPM:128.0"))
  136. XCTAssertTrue(content.contains("#EXTKEY:Am"))
  137. }
  138. // MARK: - DAWproject Exporter
  139. func testDAWProjectExport() throws {
  140. let url = outputDir.appendingPathComponent("test.dawproject")
  141. var options = ExportOptions.default
  142. options.copyAudioFiles = false
  143. try DAWProjectExporter.export(playlist: playlist, to: url, options: options)
  144. // DAWProjectExporter writes to .dawproject.xml
  145. let xmlURL = url.deletingPathExtension().appendingPathExtension("dawproject.xml")
  146. let content = try String(contentsOf: xmlURL, encoding: .utf8)
  147. XCTAssertTrue(content.contains("<Project version=\"1.0\">"))
  148. XCTAssertTrue(content.contains("<Application name=\"MixBoard\""))
  149. XCTAssertTrue(content.contains("<Clip"))
  150. XCTAssertTrue(content.contains("Track One"))
  151. }
  152. // MARK: - MixExporter Dispatcher
  153. func testExportFormatProperties() {
  154. XCTAssertEqual(MixExporter.ExportFormat.allCases.count, 5)
  155. for format in MixExporter.ExportFormat.allCases {
  156. XCTAssertFalse(format.name.isEmpty)
  157. XCTAssertFalse(format.fileExtension.isEmpty)
  158. XCTAssertFalse(format.description.isEmpty)
  159. }
  160. }
  161. }