Compare commits
2
Commits
ios-v1.0.5
...
ios-v1.0.6
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
41b8171df2 | ||
|
|
8e5ebb70b1 |
No files matched your search
@@ -185,6 +185,7 @@ final class Settings: @unchecked Sendable {
|
|||||||
var downloadsWifiOnly: Bool { get { read(\.downloadsWifiOnly, "downloadsWifiOnly", true) } set { write(\.downloadsWifiOnly, "downloadsWifiOnly", newValue, true) } }
|
var downloadsWifiOnly: Bool { get { read(\.downloadsWifiOnly, "downloadsWifiOnly", true) } set { write(\.downloadsWifiOnly, "downloadsWifiOnly", newValue, true) } }
|
||||||
/// Downloads page order: 0 download order, 1 artist, 2 release date.
|
/// Downloads page order: 0 download order, 1 artist, 2 release date.
|
||||||
var downloadSort: Int { get { read(\.downloadSort, "downloadSort", 0) } set { write(\.downloadSort, "downloadSort", newValue, 0) } }
|
var downloadSort: Int { get { read(\.downloadSort, "downloadSort", 0) } set { write(\.downloadSort, "downloadSort", newValue, 0) } }
|
||||||
|
var karaokeVocals: Int { get { read(\.karaokeVocals, "karaokeVocals", KaraokeVocals.Off.rawValue) } set { write(\.karaokeVocals, "karaokeVocals", newValue, KaraokeVocals.Off.rawValue) } }
|
||||||
var keepStemMix: Bool { get { read(\.keepStemMix, "keepStemMix", true) } set { write(\.keepStemMix, "keepStemMix", newValue, true) } }
|
var keepStemMix: Bool { get { read(\.keepStemMix, "keepStemMix", true) } set { write(\.keepStemMix, "keepStemMix", newValue, true) } }
|
||||||
var masterVolume: Float {
|
var masterVolume: Float {
|
||||||
get { access(keyPath: \.masterVolume); lock.lock(); defer { lock.unlock() }; return (store["masterVolume"] as? NSNumber)?.floatValue ?? 0.85 }
|
get { access(keyPath: \.masterVolume); lock.lock(); defer { lock.unlock() }; return (store["masterVolume"] as? NSNumber)?.floatValue ?? 0.85 }
|
||||||
|
|||||||
@@ -19,6 +19,8 @@ enum Stems {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
enum KaraokeVocals: Int, CaseIterable { case On, Soft, Off }
|
||||||
|
|
||||||
/// One stem's fader. [level] is the slider position; 0.8 is unity gain.
|
/// One stem's fader. [level] is the slider position; 0.8 is unity gain.
|
||||||
struct StemSetting: Codable, Hashable {
|
struct StemSetting: Codable, Hashable {
|
||||||
static let unity: Float = 0.8
|
static let unity: Float = 0.8
|
||||||
@@ -124,7 +126,7 @@ final class PlaybackManager: StemEngineListener, @unchecked Sendable {
|
|||||||
private(set) var message: AppMessage?
|
private(set) var message: AppMessage?
|
||||||
/// Mixes saved for single songs (by rating key), put back whenever that song starts.
|
/// Mixes saved for single songs (by rating key), put back whenever that song starts.
|
||||||
private(set) var songMixes: [String: [String: StemSetting]] = [:]
|
private(set) var songMixes: [String: [String: StemSetting]] = [:]
|
||||||
/// Karaoke mode: vocals out on every song, lyrics on Now Playing.
|
/// Karaoke mode: vocals on, soft or out (per settings) on every song, lyrics on Now Playing.
|
||||||
private(set) var karaoke = false
|
private(set) var karaoke = false
|
||||||
@ObservationIgnored private var preKaraokeMix: [String: StemSetting]?
|
@ObservationIgnored private var preKaraokeMix: [String: StemSetting]?
|
||||||
|
|
||||||
@@ -415,11 +417,25 @@ final class PlaybackManager: StemEngineListener, @unchecked Sendable {
|
|||||||
|
|
||||||
/// The mix a song starts with: karaoke if that's on, else the song's saved mix, else cleared (or kept, per settings).
|
/// 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] {
|
private func startMix(_ t: Track) -> [String: StemSetting] {
|
||||||
if karaoke { return Self.karaokeMix }
|
if karaoke { return karaokeMix }
|
||||||
return songMixes[t.ratingKey] ?? (settings.keepStemMix ? mix : [:])
|
return songMixes[t.ratingKey] ?? (settings.keepStemMix ? mix : [:])
|
||||||
}
|
}
|
||||||
|
|
||||||
private static let karaokeMix = ["vocals": StemSetting(muted: true)]
|
/// How much of the singer is left in karaoke mode. Soft is about -12 dB, a guide under your own voice.
|
||||||
|
private var karaokeMix: [String: StemSetting] {
|
||||||
|
switch karaokeVocals {
|
||||||
|
case .On: return [:]
|
||||||
|
case .Soft: return ["vocals": StemSetting(level: 0.4)]
|
||||||
|
case .Off: return ["vocals": StemSetting(muted: true)]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
var karaokeVocals: KaraokeVocals { KaraokeVocals(rawValue: settings.karaokeVocals) ?? .Off }
|
||||||
|
|
||||||
|
func setKaraokeVocals(_ v: KaraokeVocals) {
|
||||||
|
settings.karaokeVocals = v.rawValue
|
||||||
|
if karaoke { applyMix(karaokeMix) }
|
||||||
|
}
|
||||||
|
|
||||||
/// Turning it off puts back the song's saved mix, or the one from before, unless the mix was changed meanwhile.
|
/// Turning it off puts back the song's saved mix, or the one from before, unless the mix was changed meanwhile.
|
||||||
func setKaraoke(_ on: Bool) {
|
func setKaraoke(_ on: Bool) {
|
||||||
@@ -427,9 +443,9 @@ final class PlaybackManager: StemEngineListener, @unchecked Sendable {
|
|||||||
karaoke = on
|
karaoke = on
|
||||||
if on {
|
if on {
|
||||||
preKaraokeMix = mix
|
preKaraokeMix = mix
|
||||||
applyMix(Self.karaokeMix)
|
applyMix(karaokeMix)
|
||||||
} else {
|
} else {
|
||||||
if MixPresets.normalized(mix) == Self.karaokeMix { applyMix(songMix ?? preKaraokeMix ?? [:]) }
|
if MixPresets.normalized(mix) == karaokeMix { applyMix(songMix ?? preKaraokeMix ?? [:]) }
|
||||||
preKaraokeMix = nil
|
preKaraokeMix = nil
|
||||||
changed()
|
changed()
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -183,7 +183,7 @@ struct NowPlayingScreen: View {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/// Opens a panel, or goes back to the art if it's already open.
|
/// Opens a panel, or goes back to the art if it's already open.
|
||||||
/// Karaoke: vocals out and lyrics up. If it's on but another panel is showing, brings the lyrics back first.
|
/// Karaoke: vocals down and lyrics up. If it's on but another panel is showing, brings the lyrics back first.
|
||||||
private func karaoke() {
|
private func karaoke() {
|
||||||
let pm = g.playback
|
let pm = g.playback
|
||||||
withAnimation(.spring(duration: 0.38, bounce: 0.12)) {
|
withAnimation(.spring(duration: 0.38, bounce: 0.12)) {
|
||||||
@@ -858,6 +858,22 @@ private struct LyricsPanel: View {
|
|||||||
private var g: AppGraph { .shared }
|
private var g: AppGraph { .shared }
|
||||||
|
|
||||||
var body: some View {
|
var body: some View {
|
||||||
|
VStack(spacing: 0) {
|
||||||
|
// Soft leaves the singer in quietly as a guide.
|
||||||
|
HStack(spacing: 12) {
|
||||||
|
Text("Vocals").font(.subheadline.weight(.semibold)).foregroundStyle(p.label2)
|
||||||
|
Picker("Vocals", selection: Binding(get: { g.playback.karaokeVocals }, set: { g.playback.setKaraokeVocals($0) })) {
|
||||||
|
ForEach(KaraokeVocals.allCases, id: \.self) { Text(String(describing: $0)).tag($0) }
|
||||||
|
}
|
||||||
|
.pickerStyle(.segmented)
|
||||||
|
}
|
||||||
|
.padding(.horizontal, 28).padding(.top, 8)
|
||||||
|
content
|
||||||
|
}
|
||||||
|
.task(id: track.ratingKey) { await follow() }
|
||||||
|
}
|
||||||
|
|
||||||
|
@ViewBuilder private var content: some View {
|
||||||
Group {
|
Group {
|
||||||
if loading {
|
if loading {
|
||||||
ProgressView()
|
ProgressView()
|
||||||
@@ -869,7 +885,9 @@ private struct LyricsPanel: View {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
.frame(maxWidth: .infinity, maxHeight: .infinity)
|
.frame(maxWidth: .infinity, maxHeight: .infinity)
|
||||||
.task(id: track.ratingKey) {
|
}
|
||||||
|
|
||||||
|
private func follow() async {
|
||||||
loading = true
|
loading = true
|
||||||
at = -1
|
at = -1
|
||||||
lyrics = await LyricsSource.shared.lyrics(track)
|
lyrics = await LyricsSource.shared.lyrics(track)
|
||||||
@@ -883,7 +901,6 @@ private struct LyricsPanel: View {
|
|||||||
try? await Task.sleep(nanoseconds: 150_000_000)
|
try? await Task.sleep(nanoseconds: 150_000_000)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
private func lines(_ l: Lyrics) -> some View {
|
private func lines(_ l: Lyrics) -> some View {
|
||||||
ScrollViewReader { proxy in
|
ScrollViewReader { proxy in
|
||||||
|
|||||||
+2
-2
@@ -6,8 +6,8 @@ options:
|
|||||||
settings:
|
settings:
|
||||||
base:
|
base:
|
||||||
SWIFT_VERSION: "5.0"
|
SWIFT_VERSION: "5.0"
|
||||||
MARKETING_VERSION: "1.0.5"
|
MARKETING_VERSION: "1.0.6"
|
||||||
CURRENT_PROJECT_VERSION: "6"
|
CURRENT_PROJECT_VERSION: "7"
|
||||||
targets:
|
targets:
|
||||||
Stemify:
|
Stemify:
|
||||||
type: application
|
type: application
|
||||||
|
|||||||
@@ -15,6 +15,15 @@
|
|||||||
"tintColor": "#FA2D48",
|
"tintColor": "#FA2D48",
|
||||||
"category": "entertainment",
|
"category": "entertainment",
|
||||||
"versions": [
|
"versions": [
|
||||||
|
{
|
||||||
|
"version": "1.0.5",
|
||||||
|
"buildVersion": "6",
|
||||||
|
"date": "2026-10-20T09:23:39-07:00",
|
||||||
|
"localizedDescription": "- Sort menu on Artists, Albums, Songs and Playlists (Name, Date Added, Last Played, Plays, Rating, Year, Duration and more). Tap the current field again to flip direction (A to Z / Z to A, newest / oldest).\n- Each screen remembers its sort.\n\nUnsigned IPA: sideload with AltStore, SideStore or Feather.",
|
||||||
|
"downloadURL": "https://git.iamchrisama.com/cdnutter/stemify/releases/download/ios-v1.0.5/stemify-1.0.5.ipa",
|
||||||
|
"size": 1370686,
|
||||||
|
"minOSVersion": "17.0"
|
||||||
|
},
|
||||||
{
|
{
|
||||||
"version": "1.0.4",
|
"version": "1.0.4",
|
||||||
"buildVersion": "5",
|
"buildVersion": "5",
|
||||||
|
|||||||
Reference in new issue
Block a user