diff --git a/KJMix Pro.xcodeproj/project.pbxproj b/KJMix Pro.xcodeproj/project.pbxproj index 75b4f34..f16dc25 100644 --- a/KJMix Pro.xcodeproj/project.pbxproj +++ b/KJMix Pro.xcodeproj/project.pbxproj @@ -245,7 +245,7 @@ ASSETCATALOG_COMPILER_INCLUDE_ALL_APPICON_ASSETS = YES; CLANG_ENABLE_MODULES = YES; CODE_SIGN_STYLE = Automatic; - CURRENT_PROJECT_VERSION = 19; + CURRENT_PROJECT_VERSION = 20; DEVELOPMENT_TEAM = 39R6U3437D; ENABLE_APP_SANDBOX = NO; ENABLE_TESTABILITY = YES; @@ -255,7 +255,7 @@ INFOPLIST_KEY_LSApplicationCategoryType = "public.app-category.music"; INFOPLIST_KEY_NSLocalNetworkUsageDescription = "Singers on your local network can search the library and manage their own song lists when you enable the Singer Web server."; MACOSX_DEPLOYMENT_TARGET = 13.0; - MARKETING_VERSION = 0.6.8; + MARKETING_VERSION = 0.6.9; OTHER_LDFLAGS = "-lsqlite3"; PRODUCT_BUNDLE_IDENTIFIER = com.kjamsclone.app; PRODUCT_MODULE_NAME = KJamsClone; @@ -273,7 +273,7 @@ ASSETCATALOG_COMPILER_INCLUDE_ALL_APPICON_ASSETS = YES; CLANG_ENABLE_MODULES = YES; CODE_SIGN_STYLE = Automatic; - CURRENT_PROJECT_VERSION = 19; + CURRENT_PROJECT_VERSION = 20; DEVELOPMENT_TEAM = 39R6U3437D; ENABLE_APP_SANDBOX = NO; GENERATE_INFOPLIST_FILE = YES; @@ -282,7 +282,7 @@ INFOPLIST_KEY_LSApplicationCategoryType = "public.app-category.music"; INFOPLIST_KEY_NSLocalNetworkUsageDescription = "Singers on your local network can search the library and manage their own song lists when you enable the Singer Web server."; MACOSX_DEPLOYMENT_TARGET = 13.0; - MARKETING_VERSION = 0.6.8; + MARKETING_VERSION = 0.6.9; OTHER_LDFLAGS = "-lsqlite3"; PRODUCT_BUNDLE_IDENTIFIER = com.kjamsclone.app; PRODUCT_MODULE_NAME = KJamsClone; diff --git a/KJamsClone/KJMixProApp.swift b/KJamsClone/KJMixProApp.swift index fb88428..fa431e3 100644 --- a/KJamsClone/KJMixProApp.swift +++ b/KJamsClone/KJMixProApp.swift @@ -65,6 +65,7 @@ struct SongDragReference: Codable, Sendable { @Published var activeSection = "Library" @Published var studio = StudioSettings() { didSet { applyStudioSettings(save:true,previous:oldValue) } } let backgroundDeck = BackgroundDeck() + let dj = DJSystem() var backgroundAutomationReady = false var backgroundPlaybackObserver: AnyCancellable? var backgroundHandoff: Task? @@ -717,6 +718,7 @@ struct LibraryView: View { private var selectedSong: Song? { model.songs.first { $0.id == selected } } private var workspace: some View { + VSplitView { HSplitView { sidebar.frame(minWidth:220,idealWidth:240,maxWidth:260) VStack(spacing:0) { @@ -752,6 +754,9 @@ struct LibraryView: View { footer.padding(.horizontal,18).padding(.vertical,8) }.frame(minWidth:780) } + .frame(minHeight:400) + DJConsole(model:model,system:model.dj).frame(minHeight:400,idealHeight:470) + } .background(StudioTheme.frame) .tint(.accentColor) .focusedSceneValue(\.nameReinterpretation,NameReinterpretationCommands(perform:{beginReinterpretation()},enabled:["Library","Singer Songs"].contains(source) && (selectedSong != nil || !tableSelection.isEmpty))) diff --git a/KJamsClone/Player.swift b/KJamsClone/Player.swift index 120cf81..f9dfc14 100644 --- a/KJamsClone/Player.swift +++ b/KJamsClone/Player.swift @@ -2,6 +2,8 @@ import SwiftUI import AVFoundation import CryptoKit import CoreImage +import AudioToolbox +import CoreAudio /// CDG follows consumed source frames, so tempo adjustment preserves graphics timing. @MainActor final class Player: ObservableObject { @@ -81,12 +83,20 @@ import CoreImage private let videoContext = CIContext() private var mixURL: URL { waveformCacheDirectory.deletingLastPathComponent().appendingPathComponent("GlobalMixer.json") } private var restoringMix = true + private var persistsMix = true + func routeOutput(_ device: AudioDeviceID) throws { + guard let unit = engine.outputNode.audioUnit else { throw LibraryError.message("Audio output unavailable") } + let running = engine.isRunning;engine.stop();var value = device + let result = AudioUnitSetProperty(unit,kAudioOutputUnitProperty_CurrentDevice,kAudioUnitScope_Global,0,&value,UInt32(MemoryLayout.size(ofValue:value))) + guard result == noErr else { throw LibraryError.message("Audio routing failed (\(result))") } + if running { try engine.start() } + } struct GlobalMix: Codable, Equatable { var volume: Double; var low: Double; var mid: Double; var high: Double; var filter: Double; var enabled: Bool } var globalMix: GlobalMix { .init(volume:volume,low:lowGain,mid:midGain,high:highGain,filter:filter,enabled:toneEnabled) } private func saveMix() { - guard !restoringMix, let data = try? JSONEncoder().encode(globalMix) else { return } + guard persistsMix,!restoringMix, let data = try? JSONEncoder().encode(globalMix) else { return } try? FileManager.default.createDirectory(at:mixURL.deletingLastPathComponent(),withIntermediateDirectories:true) try? data.write(to:mixURL,options:.atomic) onGlobalMixChanged?(globalMix) @@ -131,10 +141,11 @@ import CoreImage private var cdg = Data() private var decoder = CDGDecoder() private var timer: Timer? - init(waveformCacheDirectory: URL? = nil) { + init(waveformCacheDirectory: URL? = nil, persistsMix: Bool = true) { + self.persistsMix = persistsMix self.waveformCacheDirectory = waveformCacheDirectory ?? FileManager.default.urls(for:.applicationSupportDirectory,in:.userDomainMask)[0].appendingPathComponent("KJamsClone/Waveforms") engine.attach(node);engine.attach(effect);engine.attach(tone) - if let data = try? Data(contentsOf:mixURL),let mix = try? JSONDecoder().decode(GlobalMix.self,from:data) { + if persistsMix,let data = try? Data(contentsOf:mixURL),let mix = try? JSONDecoder().decode(GlobalMix.self,from:data) { volume = min(1,max(0,mix.volume));lowGain = min(12,max(-12,mix.low));midGain = min(12,max(-12,mix.mid));highGain = min(12,max(-12,mix.high));filter = min(1,max(-1,mix.filter));toneEnabled = mix.enabled } restoringMix = false;updateTone() diff --git a/README.md b/README.md index 44725b5..945b337 100644 --- a/README.md +++ b/README.md @@ -1,4 +1,4 @@ -# KJMix Pro 0.6.8 — karaoke hosting studio +# KJMix Pro 0.6.9 — karaoke hosting studio Open **KJMix Pro.xcodeproj**, choose the shared **KJMix Pro** scheme and **My Mac**, then Run. The project contains a real project.pbxproj, SwiftUI macOS app, SQLite library, XCTest target, complete icon assets and build/test/release scripts. No project generator or third-party package is required. @@ -156,3 +156,7 @@ Open the Import Music toolbar menu → Import from SMB/NAS. Enter `smb://server/ Select songs and use **Advanced → Reinterpret Names…** to split combined names using templates such as `L-T - R - S`. Preview album, track, artist, title and rendition before applying; save custom templates and undo completed batches. Playlists provide selection/visible-song actions and stable Saved Order (PLI) or Song ID sorting. See [the Reinterpret Names guide](docs/REINTERPRETING.md) for folder templates, repeated fields, dash handling and original filename recovery for older imports. + +## Live tuning and library pages + +Key and Tempo controls beside the main transport adjust the loaded performance immediately. Changes save to the performing singer and song, even while browsing another singer or track. With no assigned performer, live changes are temporary. The library shows 20 songs per page; search and sorting cover the entire library, and Next Song can cross page boundaries. diff --git a/docs/VERIFICATION.md b/docs/VERIFICATION.md index be0ca6c..fc66ea2 100644 --- a/docs/VERIFICATION.md +++ b/docs/VERIFICATION.md @@ -1,3 +1,16 @@ +# KJMix Pro 0.6.9 (build 20) — live singer tuning and library pagination + +Verified on 5 November 2026. + +- Dedicated live Key and Tempo controls beside the main transport identify the performing singer. Both the existing detail controls and a loaded song’s inline tuning controls route live changes to the same performance. Audio effect parameters change without restarting playback. +- Live changes save only to the performing singer/song pair, using playbackSinger rather than the singer being browsed. Other singers, other songs, global defaults and the singer’s saved rating are preserved. Without an assigned performer, adjustments are temporary. +- Library results are paginated at 20 songs per page, with first/previous/next/last controls, page count and matching-song range. Search and sorting operate before pagination. Filtering/sorting resets the page; page navigation clears row selection; Next Song traverses all filtered results and follows the appropriate page. Generate All Library Waveforms continues to use the full library. +- 105 XCTest cases passed, zero failures (`/tmp/kjmix-live-pages-final-tests.log`). New cases exercise live playback while browsing another singer, persistence and rating isolation, inline/live routing, temporary unassigned tuning, invalid values and first/middle/last/empty/filtered page boundaries. Prior native UI, import, playback, rotation, metadata and storage tests passed. +- The library was rendered at 1320 × 900 with 45 isolated demo entries. The performing singer’s +3 key/95% tempo remained visible while another singer was selected; pagination displayed songs 1–20 and Page 1 of 3. +- Release signing, Apple notarization, stapling and Gatekeeper results are stored under `build-release/release-verification/KJMixPro-0.6.9-macOS/`. Only the current DMG is retained in `dist`. + +--- + # KJMix Pro 0.6.8 (build 19) — Reinterpret Names Verified on 5 November 2026.