| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081 |
- import Foundation
- /// Exports a playlist as a standard CUE sheet (.cue).
- /// CUE sheets are widely supported by CD burning software, virtual CD drives,
- /// and many audio tools including Audacity, foobar2000, and various DAWs.
- struct CueSheetExporter: DAWExporter {
- static let formatID = "cue"
- static let formatName = "Cue Sheet"
- static let fileExtension = "cue"
- static func export(playlist: Playlist, to url: URL, options: ExportOptions) throws {
- let entries = playlist.sortedEntries
- var lines: [String] = []
- lines.append("REM Generated by MixBoard")
- lines.append("REM Date: \(ISO8601DateFormatter().string(from: Date()))")
- lines.append("TITLE \"\(playlist.name)\"")
- var timelinePosition: TimeInterval = 0
- for (index, entry) in entries.enumerated() {
- guard let track = entry.track,
- let fileURL = options.effectiveFileURL(for: track) else { continue }
- let trackNumber = String(format: "%02d", index + 1)
- let fileName = fileURL.lastPathComponent
- let relativePath = "\(options.audioFilesRelativePath)/\(fileName)"
- lines.append("FILE \"\(relativePath)\" WAVE")
- lines.append(" TRACK \(trackNumber) AUDIO")
- lines.append(" TITLE \"\(track.title)\"")
- if !track.artist.isEmpty {
- lines.append(" PERFORMER \"\(track.artist)\"")
- }
- // INDEX 01 is the start time in MM:SS:FF format (frames = 1/75 second)
- let indexTime = formatCueTime(timelinePosition)
- lines.append(" INDEX 01 \(indexTime)")
- // Add cue points as INDEX entries
- if options.includeCuePoints {
- for (cpIndex, cuePoint) in track.cuePoints.sorted().enumerated() {
- // INDEX 02+ for additional cue points
- let cpTime = formatCueTime(timelinePosition + cuePoint.timestamp - entry.startOffset)
- lines.append(" REM CUE \(cuePoint.name.isEmpty ? cuePoint.type.rawValue : cuePoint.name)")
- if cpIndex < 98 { // CUE format supports INDEX 00-99
- lines.append(" INDEX \(String(format: "%02d", cpIndex + 2)) \(cpTime)")
- }
- }
- }
- // Add BPM as a remark
- if let bpm = track.bpm {
- lines.append(" REM BPM \(String(format: "%.1f", bpm))")
- }
- if let key = track.musicalKey {
- lines.append(" REM KEY \(key)")
- }
- timelinePosition += entry.effectiveDuration
- if index + 1 < entries.count {
- timelinePosition -= entries[index + 1].crossfadeDuration
- }
- }
- let content = lines.joined(separator: "\n") + "\n"
- try content.write(to: url, atomically: true, encoding: .utf8)
- }
- // MARK: - CUE Time Format
- /// Convert seconds to CUE time format MM:SS:FF where FF = frames (1/75 second).
- private static func formatCueTime(_ seconds: TimeInterval) -> String {
- let totalSeconds = max(0, seconds)
- let minutes = Int(totalSeconds) / 60
- let secs = Int(totalSeconds) % 60
- let frames = Int((totalSeconds - Double(Int(totalSeconds))) * 75)
- return String(format: "%02d:%02d:%02d", minutes, secs, frames)
- }
- }
|