172 lines
5.4 KiB
Swift
172 lines
5.4 KiB
Swift
import Foundation
|
|
|
|
public struct SongGenerationContext: Codable, Equatable, Sendable {
|
|
public static let empty = SongGenerationContext(selectedInstruments: [])
|
|
|
|
public var selectedInstruments: [SelectedInstrumentContext]
|
|
|
|
public init(selectedInstruments: [SelectedInstrumentContext]) {
|
|
self.selectedInstruments = selectedInstruments
|
|
}
|
|
|
|
public init(project: SongProject) {
|
|
let sectionsByID = Dictionary(uniqueKeysWithValues: project.sections.map { ($0.id, $0) })
|
|
selectedInstruments = project.selectedInstrumentTracks.map { track in
|
|
SelectedInstrumentContext(
|
|
instrumentId: track.instrumentId,
|
|
displayName: Self.displayName(for: track),
|
|
playingStyle: Self.normalized(track.playingStyle),
|
|
role: Self.normalized(track.role),
|
|
autoArrangementEnabled: track.autoArrangementEnabled,
|
|
placements: track.placements.map { placement in
|
|
SelectedInstrumentPlacementContext(
|
|
sectionId: placement.sectionId,
|
|
sectionTitle: placement.sectionId.flatMap { sectionsByID[$0]?.title },
|
|
sectionType: placement.sectionId.flatMap { sectionsByID[$0]?.type },
|
|
startTime: placement.startTime,
|
|
endTime: placement.endTime,
|
|
direction: Self.normalized(placement.direction)
|
|
)
|
|
}
|
|
)
|
|
}
|
|
}
|
|
|
|
public var hasSelectedInstruments: Bool {
|
|
!selectedInstruments.isEmpty
|
|
}
|
|
|
|
public var sunoStyleInstrumentPhrase: String {
|
|
selectedInstruments
|
|
.map(\.stylePromptPhrase)
|
|
.joined(separator: ", ")
|
|
}
|
|
|
|
private static func displayName(for track: InstrumentTrack) -> String {
|
|
normalized(track.variant) ?? track.instrumentId
|
|
}
|
|
|
|
private static func normalized(_ value: String?) -> String? {
|
|
let trimmedValue = value?.trimmingCharacters(in: .whitespacesAndNewlines)
|
|
return trimmedValue?.isEmpty == false ? trimmedValue : nil
|
|
}
|
|
}
|
|
|
|
public struct SelectedInstrumentContext: Codable, Equatable, Sendable {
|
|
public var instrumentId: String
|
|
public var displayName: String
|
|
public var playingStyle: String?
|
|
public var role: String?
|
|
public var autoArrangementEnabled: Bool
|
|
public var placements: [SelectedInstrumentPlacementContext]
|
|
|
|
public init(
|
|
instrumentId: String,
|
|
displayName: String,
|
|
playingStyle: String? = nil,
|
|
role: String? = nil,
|
|
autoArrangementEnabled: Bool = true,
|
|
placements: [SelectedInstrumentPlacementContext] = []
|
|
) {
|
|
self.instrumentId = instrumentId
|
|
self.displayName = displayName
|
|
self.playingStyle = playingStyle
|
|
self.role = role
|
|
self.autoArrangementEnabled = autoArrangementEnabled
|
|
self.placements = placements
|
|
}
|
|
|
|
public var stylePromptPhrase: String {
|
|
var details: [String] = []
|
|
|
|
if let role {
|
|
details.append(role)
|
|
}
|
|
|
|
if let playingStyle {
|
|
details.append(playingStyle)
|
|
}
|
|
|
|
let timingPhrase = placements
|
|
.map(\.stylePromptPhrase)
|
|
.filter { !$0.isEmpty }
|
|
.joined(separator: "; ")
|
|
|
|
if !timingPhrase.isEmpty {
|
|
details.append(timingPhrase)
|
|
}
|
|
|
|
guard !details.isEmpty else {
|
|
return displayName
|
|
}
|
|
|
|
return "\(displayName) (\(details.joined(separator: ", ")))"
|
|
}
|
|
}
|
|
|
|
public struct SelectedInstrumentPlacementContext: Codable, Equatable, Sendable {
|
|
public var sectionId: String?
|
|
public var sectionTitle: String?
|
|
public var sectionType: SongSectionType?
|
|
public var startTime: TimeInterval?
|
|
public var endTime: TimeInterval?
|
|
public var direction: String?
|
|
|
|
public init(
|
|
sectionId: String? = nil,
|
|
sectionTitle: String? = nil,
|
|
sectionType: SongSectionType? = nil,
|
|
startTime: TimeInterval? = nil,
|
|
endTime: TimeInterval? = nil,
|
|
direction: String? = nil
|
|
) {
|
|
self.sectionId = sectionId
|
|
self.sectionTitle = sectionTitle
|
|
self.sectionType = sectionType
|
|
self.startTime = startTime
|
|
self.endTime = endTime
|
|
self.direction = direction
|
|
}
|
|
|
|
public var stylePromptPhrase: String {
|
|
var parts: [String] = []
|
|
|
|
if let sectionTitle, !sectionTitle.isEmpty {
|
|
parts.append(sectionTitle)
|
|
} else if let sectionType {
|
|
parts.append(sectionType.rawValue)
|
|
}
|
|
|
|
if let timing = timingPhrase {
|
|
parts.append(timing)
|
|
}
|
|
|
|
if let direction, !direction.isEmpty {
|
|
parts.append(direction)
|
|
}
|
|
|
|
return parts.joined(separator: " ")
|
|
}
|
|
|
|
private var timingPhrase: String? {
|
|
switch (startTime, endTime) {
|
|
case let (start?, end?):
|
|
return "\(Self.formattedTime(start))-\(Self.formattedTime(end))"
|
|
case let (start?, nil):
|
|
return "from \(Self.formattedTime(start))"
|
|
case let (nil, end?):
|
|
return "until \(Self.formattedTime(end))"
|
|
case (nil, nil):
|
|
return nil
|
|
}
|
|
}
|
|
|
|
private static func formattedTime(_ time: TimeInterval) -> String {
|
|
let roundedTime = time.rounded()
|
|
if roundedTime == time {
|
|
return "\(Int(roundedTime))s"
|
|
}
|
|
return String(format: "%.1fs", time)
|
|
}
|
|
}
|