removed unwanted bar now with waveforms, major updates
This commit is contained in:
@@ -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
@@ -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
|
||||
}
|
||||
|
||||
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user