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 { HStack {
VStack(alignment:.leading,spacing:4) { Text("Settings").font(.title2.bold());Text(category).font(.callout).foregroundStyle(.secondary) } VStack(alignment:.leading,spacing:4) { Text("Settings").font(.title2.bold());Text(category).font(.callout).foregroundStyle(.secondary) }
Spacer(minLength:24) Spacer(minLength:24)
VStack(alignment:.leading,spacing:6) { 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)
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)
}.padding(.horizontal,24).padding(.top,18).padding(.bottom,10) }.padding(.horizontal,24).padding(.top,18).padding(.bottom,10)
HStack(spacing:8) { HStack(spacing:8) {
ForEach(categories,id:\.self) { name in ForEach(categories,id:\.self) { name in
@@ -667,7 +664,7 @@ struct StudioSettingsDialog: View {
else if category == "Devices" { KaraokeDevicesPreferences(model:model) } else if category == "Devices" { KaraokeDevicesPreferences(model:model) }
else { StudioSettingsPane(showHeading:false,section:category,model:model).id(category) } else { StudioSettingsPane(showHeading:false,section:category,model:model).id(category) }
Divider() 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)) }.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:scope) { _ in model.dj.midi.learning = nil;if !categories.contains(category) { category = "General" } }
.onChange(of:category) { _ in model.dj.midi.learning = nil } .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 loopBufferTask: Task<Void,Never>?
private var loopBuffer: AVAudioPCMBuffer? private var loopBuffer: AVAudioPCMBuffer?
private var preparedLoop: ClosedRange<Double>? 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(set) var nativeLoopActive = false
private func loopPosition(_ position: Double) -> Double { 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) return range.lowerBound+(position-range.upperBound).truncatingRemainder(dividingBy:range.upperBound-range.lowerBound)
} }
private func prepareNativeLoop() { 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 } 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 let position = self.displayTime,resume = self.playing
self.loopBuffer = buffer;self.preparedLoop = start...end 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 { } } catch { }
} }
} }
@@ -346,13 +369,13 @@ import CoreAudio
let start = min(file.length,max(0,AVAudioFramePosition(seconds*rate))) let start = min(file.length,max(0,AVAudioFramePosition(seconds*rate)))
anchor = Double(start)/rate; time = anchor anchor = Double(start)/rate; time = anchor
guard start < file.length else { playing = false; render(); return } 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 { if loopEnabled,let range = preparedLoop,range.lowerBound == loopIn,range.upperBound == loopOut,let loopBuffer,seconds < range.upperBound {
let endFrame = AVAudioFramePosition(range.upperBound*rate) let endFrame = AVAudioFramePosition(range.upperBound*rate)
let remainder = AVAudioFrameCount(max(1,endFrame-start)) let remainder = AVAudioFrameCount(max(1,endFrame-start))
node.scheduleSegment(file,startingFrame:start,frameCount:remainder,at:nil) node.scheduleSegment(file,startingFrame:start,frameCount:remainder,at:nil)
node.scheduleBuffer(loopBuffer,at:nil,options:.loops) node.scheduleBuffer(loopBuffer,at:nil,options:.loops)
nativeLoopActive = true nativeLoopActive = true;scheduledLoop = range
if play { if notifyPlayback { startNodePlayback() } else { node.play() } } if play { if notifyPlayback { startNodePlayback() } else { node.play() } }
playing = play;render();return 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 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)) 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) 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 let previous = NSApp.keyWindow
window.setFrameOrigin(NSPoint(x:-10000,y:-10000));window.orderFront(nil);window.makeKey() window.setFrameOrigin(NSPoint(x:-10000,y:-10000));window.orderFront(nil);window.makeKey()
defer { window.orderOut(nil);window.contentView = nil;previous?.makeKey() } defer { window.orderOut(nil);window.contentView = nil;previous?.makeKey() }
@@ -2861,8 +2862,10 @@ extension KJamsCloneTests {
XCTAssertTrue(player.audioPlaybackStarted);XCTAssertTrue(player.nativeLoopActive) XCTAssertTrue(player.audioPlaybackStarted);XCTAssertTrue(player.nativeLoopActive)
XCTAssertGreaterThanOrEqual(player.displayTime,0.2);XCTAssertLessThan(player.displayTime,0.401) XCTAssertGreaterThanOrEqual(player.displayTime,0.2);XCTAssertLessThan(player.displayTime,0.401)
} }
let restarts = player.loopAudioRestartCount
player.defineLoop(start:0.2,end:0.3) 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) XCTAssertTrue(player.nativeLoopActive);XCTAssertLessThan(player.displayTime,0.301)
let exit = player.displayTime let exit = player.displayTime
player.loopEnabled = false player.loopEnabled = false