removed unwanted bar now with waveforms, major updates

This commit is contained in:
Mark Gregory
2026-11-06 10:46:32 +00:00
parent f947308421
commit 7b45184dff
3 changed files with 34 additions and 11 deletions
+2 -5
View File
@@ -649,10 +649,7 @@ struct StudioSettingsDialog: View {
HStack {
VStack(alignment:.leading,spacing:4) { Text("Settings").font(.title2.bold());Text(category).font(.callout).foregroundStyle(.secondary) }
Spacer(minLength:24)
VStack(alignment:.leading,spacing:6) {
Text("Workspace").font(.caption).foregroundStyle(.secondary).fixedSize()
Picker("Workspace",selection:$scope) { Text("Karaoke Studio").tag("Karaoke");Text("DJ Studio").tag("DJ") }.labelsHidden().pickerStyle(.segmented).frame(width:340)
}.fixedSize(horizontal:true,vertical:true)
Picker("Studio",selection:$scope) { Text("Karaoke Studio").tag("Karaoke");Text("DJ Studio").tag("DJ") }.labelsHidden().pickerStyle(.segmented).frame(width:340).fixedSize(horizontal:true,vertical:true)
}.padding(.horizontal,24).padding(.top,18).padding(.bottom,10)
HStack(spacing:8) {
ForEach(categories,id:\.self) { name in
@@ -667,7 +664,7 @@ struct StudioSettingsDialog: View {
else if category == "Devices" { KaraokeDevicesPreferences(model:model) }
else { StudioSettingsPane(showHeading:false,section:category,model:model).id(category) }
Divider()
HStack { Text(scope == "DJ" && category == "Devices" ? "DJ routing · Changes apply immediately" : scope+" settings · Changes save automatically").font(.caption).foregroundStyle(.secondary);Spacer();Button("Done") { dismiss() }.keyboardShortcut(.defaultAction) }.padding(.horizontal,24).padding(.vertical,16)
HStack { Text(scope == "DJ" && category == "Devices" ? "DJ routing · Changes apply immediately" : (scope == "DJ" ? "DJ Studio" : "Karaoke Studio")+" settings · Changes save automatically").font(.caption).foregroundStyle(.secondary);Spacer();Button("Done") { dismiss() }.keyboardShortcut(.defaultAction) }.padding(.horizontal,24).padding(.vertical,16)
}.frame(width:850,height:700).background(Color(nsColor:.windowBackgroundColor))
.onChange(of:scope) { _ in model.dj.midi.learning = nil;if !categories.contains(category) { category = "General" } }
.onChange(of:category) { _ in model.dj.midi.learning = nil }
+27 -4
View File
@@ -30,9 +30,21 @@ import CoreAudio
private var loopBufferTask: Task<Void,Never>?
private var loopBuffer: AVAudioPCMBuffer?
private var preparedLoop: ClosedRange<Double>?
private var scheduledLoop: ClosedRange<Double>?
private var pendingLoopSwitch: (at: Double,range: ClosedRange<Double>)?
private(set) var loopAudioRestartCount = 0
private var rawAudioPosition: Double {
guard let file,let render = node.lastRenderTime,let current = node.playerTime(forNodeTime:render) else { return time }
return anchor+Double(current.sampleTime)/file.processingFormat.sampleRate
}
private(set) var nativeLoopActive = false
private func loopPosition(_ position: Double) -> Double {
guard nativeLoopActive,let range = preparedLoop,position >= range.upperBound else { return position }
guard nativeLoopActive else { return position }
if let pendingLoopSwitch,position >= pendingLoopSwitch.at {
let range = pendingLoopSwitch.range
return range.lowerBound+(position-pendingLoopSwitch.at).truncatingRemainder(dividingBy:range.upperBound-range.lowerBound)
}
guard let range = scheduledLoop,position >= range.upperBound else { return position }
return range.lowerBound+(position-range.upperBound).truncatingRemainder(dividingBy:range.upperBound-range.lowerBound)
}
private func prepareNativeLoop() {
@@ -54,7 +66,18 @@ import CoreAudio
guard !Task.isCancelled,let self,self.currentSongID == songID,self.loopEnabled,self.loopIn == start,self.loopOut == end else { return }
let position = self.displayTime,resume = self.playing
self.loopBuffer = buffer;self.preparedLoop = start...end
self.schedule(at:position < end ? position : start,play:resume,notifyPlayback:false)
if self.nativeLoopActive,resume {
let raw = self.rawAudioPosition
let activeRange: ClosedRange<Double>
if let pending = self.pendingLoopSwitch,raw >= pending.at { activeRange = pending.range }
else { activeRange = self.scheduledLoop ?? start...end }
let remaining = max(0,activeRange.upperBound-self.loopPosition(raw))
self.node.scheduleBuffer(buffer,at:nil,options:[.loops,.interruptsAtLoop])
self.pendingLoopSwitch = (raw+remaining,start...end)
} else {
self.loopAudioRestartCount += 1
self.schedule(at:position < end ? position : start,play:resume,notifyPlayback:false)
}
} catch { }
}
}
@@ -346,13 +369,13 @@ import CoreAudio
let start = min(file.length,max(0,AVAudioFramePosition(seconds*rate)))
anchor = Double(start)/rate; time = anchor
guard start < file.length else { playing = false; render(); return }
nativeLoopActive = false
nativeLoopActive = false;pendingLoopSwitch = nil;scheduledLoop = nil
if loopEnabled,let range = preparedLoop,range.lowerBound == loopIn,range.upperBound == loopOut,let loopBuffer,seconds < range.upperBound {
let endFrame = AVAudioFramePosition(range.upperBound*rate)
let remainder = AVAudioFrameCount(max(1,endFrame-start))
node.scheduleSegment(file,startingFrame:start,frameCount:remainder,at:nil)
node.scheduleBuffer(loopBuffer,at:nil,options:.loops)
nativeLoopActive = true
nativeLoopActive = true;scheduledLoop = range
if play { if notifyPlayback { startNodePlayback() } else { node.play() } }
playing = play;render();return
}
+5 -2
View File
@@ -1345,7 +1345,8 @@ final class KJamsCloneTests: XCTestCase {
let model = AppModel(storageURL:root);_ = try model.createEntity(.venue,name:"Hall");let singer = try model.createEntity(.singer,name:"Alex")
let host = NSHostingView(rootView:LibraryView(model:model).frame(width:1320,height:900).preferredColorScheme(.dark))
host.frame = NSRect(x:0,y:0,width:1320,height:900)
let window = NSWindow(contentRect:host.frame,styleMask:[.borderless],backing:.buffered,defer:false);window.appearance = NSAppearance(named:.darkAqua);window.contentView = host
final class SidebarTestWindow: NSWindow { override var canBecomeKey: Bool { true } }
let window = SidebarTestWindow(contentRect:host.frame,styleMask:[.borderless],backing:.buffered,defer:false);window.appearance = NSAppearance(named:.darkAqua);window.contentView = host
let previous = NSApp.keyWindow
window.setFrameOrigin(NSPoint(x:-10000,y:-10000));window.orderFront(nil);window.makeKey()
defer { window.orderOut(nil);window.contentView = nil;previous?.makeKey() }
@@ -2861,8 +2862,10 @@ extension KJamsCloneTests {
XCTAssertTrue(player.audioPlaybackStarted);XCTAssertTrue(player.nativeLoopActive)
XCTAssertGreaterThanOrEqual(player.displayTime,0.2);XCTAssertLessThan(player.displayTime,0.401)
}
let restarts = player.loopAudioRestartCount
player.defineLoop(start:0.2,end:0.3)
try await Task.sleep(nanoseconds:100_000_000)
try await Task.sleep(nanoseconds:300_000_000)
XCTAssertEqual(player.loopAudioRestartCount,restarts,"Resizing an active loop must not restart the audio node")
XCTAssertTrue(player.nativeLoopActive);XCTAssertLessThan(player.displayTime,0.301)
let exit = player.displayTime
player.loopEnabled = false