iOS 1.0.8: stem playlists, karaoke softness slider in lyrics
- Playlists tab splits into Plex and Stem. Plex playlists are unchanged and now show their custom poster when one is set. - Stem playlists live on the device; each song keeps its own mix. Add from any song or collection menu, or copy a whole Plex playlist. - Queue items can carry a mix, which wins over the song's saved mix. - Softness slider under the Vocals picker in lyrics when set to Soft. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
1 parent
6f634522e5
commit
e2cf62cb07
10 files changed
+374
-27
No files matched your search
@@ -46,6 +46,8 @@ enum SourceKind { case None, Stems, Original }
|
||||
struct QueueItem: Codable, Hashable, Identifiable {
|
||||
var uid: Int64
|
||||
var track: Track
|
||||
/// Set when the song came from a stem playlist; it plays with this mix instead of its saved one.
|
||||
var mix: [String: StemSetting]?
|
||||
var id: Int64 { uid }
|
||||
}
|
||||
|
||||
@@ -204,16 +206,17 @@ final class PlaybackManager: StemEngineListener, @unchecked Sendable {
|
||||
|
||||
// ---- queue --------------------------------------------------------------------------
|
||||
|
||||
private func items(_ tracks: [Track]) -> [QueueItem] {
|
||||
tracks.map { t in
|
||||
private func items(_ tracks: [Track], _ mixes: [[String: StemSetting]]? = nil) -> [QueueItem] {
|
||||
tracks.enumerated().map { i, t in
|
||||
defer { nextUid += 1 }
|
||||
return QueueItem(uid: nextUid, track: t)
|
||||
return QueueItem(uid: nextUid, track: t, mix: mixes.flatMap { i < $0.count ? $0[i] : nil })
|
||||
}
|
||||
}
|
||||
|
||||
func playTracks(_ tracks: [Track], start: Int = 0, shuffle doShuffle: Bool = false) {
|
||||
/// `mixes` lines up with `tracks`: each song plays with its own mix (stem playlists).
|
||||
func playTracks(_ tracks: [Track], mixes: [[String: StemSetting]]? = nil, start: Int = 0, shuffle doShuffle: Bool = false) {
|
||||
if tracks.isEmpty { return }
|
||||
let items = items(tracks)
|
||||
let items = items(tracks, mixes)
|
||||
if doShuffle {
|
||||
unshuffled = items
|
||||
let first = start > 0 && start < items.count ? items[start] : items.randomElement()!
|
||||
@@ -228,12 +231,12 @@ final class PlaybackManager: StemEngineListener, @unchecked Sendable {
|
||||
}
|
||||
}
|
||||
|
||||
func playNext(_ tracks: [Track]) { insert(index + 1, tracks) }
|
||||
func addToQueue(_ tracks: [Track]) { insert(queue.count, tracks) }
|
||||
func playNext(_ tracks: [Track], mixes: [[String: StemSetting]]? = nil) { insert(index + 1, tracks, mixes) }
|
||||
func addToQueue(_ tracks: [Track], mixes: [[String: StemSetting]]? = nil) { insert(queue.count, tracks, mixes) }
|
||||
|
||||
private func insert(_ at: Int, _ tracks: [Track]) {
|
||||
private func insert(_ at: Int, _ tracks: [Track], _ mixes: [[String: StemSetting]]? = nil) {
|
||||
if tracks.isEmpty { return }
|
||||
let items = items(tracks)
|
||||
let items = items(tracks, mixes)
|
||||
var q = queue
|
||||
q.insert(contentsOf: items, at: min(max(at, 0), q.count))
|
||||
queue = q
|
||||
@@ -415,10 +418,11 @@ final class PlaybackManager: StemEngineListener, @unchecked Sendable {
|
||||
pushMix()
|
||||
}
|
||||
|
||||
/// The mix a song starts with: karaoke if that's on, else the song's saved mix, else cleared (or kept, per settings).
|
||||
private func startMix(_ t: Track) -> [String: StemSetting] {
|
||||
/// The mix a song starts with: karaoke if that's on, else its stem playlist mix, else the song's saved mix,
|
||||
/// else cleared (or kept, per settings).
|
||||
private func startMix(_ item: QueueItem) -> [String: StemSetting] {
|
||||
if karaoke { return karaokeMix }
|
||||
return songMixes[t.ratingKey] ?? (settings.keepStemMix ? mix : [:])
|
||||
return item.mix ?? songMixes[item.track.ratingKey] ?? (settings.keepStemMix ? mix : [:])
|
||||
}
|
||||
|
||||
/// How much of the singer is left in karaoke mode. Soft is a guide under your own voice, as quiet as settings say.
|
||||
@@ -527,7 +531,7 @@ final class PlaybackManager: StemEngineListener, @unchecked Sendable {
|
||||
if play && !activateSession() { return }
|
||||
playWhenReady = play
|
||||
buffering = true
|
||||
mix = startMix(item.track)
|
||||
mix = startMix(item)
|
||||
changed()
|
||||
save()
|
||||
loadTask = Task { @MainActor [weak self] in
|
||||
@@ -700,7 +704,7 @@ final class PlaybackManager: StemEngineListener, @unchecked Sendable {
|
||||
guard let item = current else { return }
|
||||
durationMs = item.track.durationMs
|
||||
waveform = nil
|
||||
mix = startMix(item.track)
|
||||
mix = startMix(item)
|
||||
source = currentEntry != nil ? .Stems : .Original
|
||||
stemNames = Stems.sort(currentEntry?.stems.map(\.name) ?? ["mix"])
|
||||
pushMix()
|
||||
|
||||
Reference in new issue
Block a user