removed unwanted bar now with waveforms, major updates
This commit is contained in:
@@ -195,6 +195,7 @@ struct DJAudioDevice: Identifiable, Equatable {
|
||||
guard !ids.isEmpty else { return }
|
||||
guard ![a.player,b.player,preview].contains(where:{$0.playing && ($0.currentSongID.map{ids.contains($0)} ?? false)}) else { error = "Stop the affected deck before removing its track.";return }
|
||||
guard !movingStorage,!importing,!generatingWaveforms,!namesBusy else { error = "Finish the current library job first.";return }
|
||||
automaticWaveforms.cancel()
|
||||
storageInSettings = fromSettings
|
||||
automix.stop("Library update")
|
||||
for deck in [a,b] where deck.song.map({ids.contains($0.id)}) == true { deck.unload() }
|
||||
@@ -267,6 +268,7 @@ struct DJAudioDevice: Identifiable, Equatable {
|
||||
func moveStorage(to destination: URL) {
|
||||
guard !movingStorage,!importing,!generatingWaveforms,!namesBusy,!a.player.playing,!b.player.playing,!preview.playing else { error = "Stop playback and finish the current library job before moving DJ files.";return }
|
||||
let loadedA = a.song?.id,loadedB = b.song?.id,database = root.appendingPathComponent("Library.sqlite"),localRoot = root
|
||||
automaticWaveforms.cancel()
|
||||
a.unload();b.unload();stopPreview();storageInSettings = true;storageAction = nil;movingStorage = true;storageProgress = .init(current:"Preparing DJ storage move…")
|
||||
storageTask = Task.detached(priority:.utility) { [weak self] in
|
||||
let access = destination.startAccessingSecurityScopedResource();defer { if access { destination.stopAccessingSecurityScopedResource() } }
|
||||
@@ -302,6 +304,14 @@ struct DJAudioDevice: Identifiable, Equatable {
|
||||
guard let gains = DJGraphicEQSettings.presets.first(where:{$0.0 == name})?.1 else { return }
|
||||
graphicEQ = .init(enabled:true,preset:name,gains:gains)
|
||||
}
|
||||
@Published var automaticWaveformProgress: WaveformJobProgress?
|
||||
let automaticWaveforms = PostImportWaveformQueue()
|
||||
private func queueImportedWaveforms(_ ids: Set<Int64>) {
|
||||
automaticWaveforms.enqueue(songs.filter{ids.contains($0.id)},locations:referenceLocations,cache:waveformDirectory,update:{ [weak self] in self?.automaticWaveformProgress = $0 },completed:{ [weak self] tracks in
|
||||
guard let self else { return }
|
||||
for deck in [self.a,self.b] where tracks.contains(where:{$0.id == deck.song?.id}) { deck.player.refreshCachedWaveform() }
|
||||
})
|
||||
}
|
||||
@Published private(set) var waveformJob: WaveformJobProgress?
|
||||
@Published private(set) var generatingWaveforms = false
|
||||
private var waveformTask: Task<Void,Never>?
|
||||
@@ -408,7 +418,7 @@ struct DJAudioDevice: Identifiable, Equatable {
|
||||
func importFiles(_ urls: [URL],loadInto: String? = nil,overridePlaying: Bool = false) {
|
||||
guard !importing,!generatingWaveforms,!movingStorage,!urls.isEmpty else { return }
|
||||
importing = true;importProgress = .init();importLogVisible = true
|
||||
let database = root.appendingPathComponent("Library.sqlite"),media = mediaDirectory,cache = waveformDirectory,copyMedia = copyImportedMedia,replacedSong = loadInto.flatMap{channel($0).song?.id}
|
||||
let database = root.appendingPathComponent("Library.sqlite"),media = mediaDirectory,beforeIDs = Set(songs.map(\.id)),copyMedia = copyImportedMedia,replacedSong = loadInto.flatMap{channel($0).song?.id}
|
||||
importTask = Task.detached(priority:.utility) { [weak self] in
|
||||
let access = urls.filter{$0.startAccessingSecurityScopedResource()}
|
||||
defer { for url in access { url.stopAccessingSecurityScopedResource() } }
|
||||
@@ -423,7 +433,7 @@ struct DJAudioDevice: Identifiable, Equatable {
|
||||
_ = try db.saveMediaLocation(name:url.lastPathComponent,url:url,bookmark:bookmark,external:true)
|
||||
}
|
||||
}
|
||||
importedIDs = try Importer(media:media,audioExtensions:["mp3","wav","m4a","mp4","m4v","mov","aiff","aif","caf","flac","aac"],includeGraphics:false,waveformCache:cache).run(urls,library:db,copyMedia:copyMedia,cancelled:{Task.isCancelled}) { value in
|
||||
importedIDs = try Importer(media:media,audioExtensions:["mp3","wav","m4a","mp4","m4v","mov","aiff","aif","caf","flac","aac"],includeGraphics:false).run(urls,library:db,copyMedia:copyMedia,cancelled:{Task.isCancelled}) { value in
|
||||
latest = value
|
||||
Task { @MainActor [weak self] in if self?.importing == true { self?.importProgress = value } }
|
||||
}
|
||||
@@ -436,6 +446,7 @@ struct DJAudioDevice: Identifiable, Equatable {
|
||||
self.perform { try self.refresh() }
|
||||
if let failure = result.failure { if loadInto != nil,!self.importLogVisible { self.error = failure } }
|
||||
else if let loadInto,let song = self.songs.first(where:{loadedIDs.contains($0.id)}) { self.load(song,into:loadInto,overridePlaying:overridePlaying && self.channel(loadInto).song?.id == replacedSong) }
|
||||
if !result.cancelled { self.queueImportedWaveforms(Set(loadedIDs).union(self.songs.filter{!beforeIDs.contains($0.id)}.map(\.id))) }
|
||||
self.continueFileLoads()
|
||||
}
|
||||
}
|
||||
@@ -1042,6 +1053,9 @@ struct DJConsole: View {
|
||||
header
|
||||
DJContinuousConsole(system:system,loadA:{loadSelected("A")},loadB:{loadSelected("B")}).fixedSize(horizontal:false,vertical:true)
|
||||
DJLibrarySnapshot(revision:libraryRevision,selection:selection,selected:selected,page:page,busy:system.generatingWaveforms || system.importing,font:system.settings.libraryFont,height:system.settings.libraryRowHeight) { browser }.equatable().frame(maxWidth:.infinity,maxHeight:.infinity)
|
||||
if let progress = system.automaticWaveformProgress {
|
||||
BackgroundWaveformStatus(progress:progress,cancel:{system.automaticWaveforms.cancel()},dismiss:{system.automaticWaveformProgress = nil})
|
||||
}
|
||||
if let progress = system.importProgress {
|
||||
HStack {
|
||||
Text(progress.current).lineLimit(1);Text("\(progress.completed)/\(progress.total)").monospacedDigit()
|
||||
|
||||
@@ -87,6 +87,13 @@ struct SongDragReference: Codable, Sendable {
|
||||
var playingIntermission = false
|
||||
@Published var error: String?
|
||||
@Published var creating: EntityKind?
|
||||
@Published var automaticWaveformProgress: WaveformJobProgress?
|
||||
let automaticWaveforms = PostImportWaveformQueue()
|
||||
private func queueImportedWaveforms(_ ids: Set<Int64>) {
|
||||
automaticWaveforms.enqueue(songs.filter{ids.contains($0.id)},locations:mediaLocations,cache:root.appendingPathComponent("Waveforms"),update:{ [weak self] in self?.automaticWaveformProgress = $0 },completed:{ [weak self] tracks in
|
||||
guard let self,let id = self.player.currentSongID,tracks.contains(where:{$0.id == id}) else { return };self.player.refreshCachedWaveform()
|
||||
})
|
||||
}
|
||||
@Published private(set) var waveformJob: WaveformJobProgress?
|
||||
@Published private(set) var generatingWaveforms = false
|
||||
private var waveformJobTask: Task<Void,Never>?
|
||||
@@ -476,15 +483,15 @@ struct SongDragReference: Codable, Sendable {
|
||||
func importFiles(_ urls: [URL]) {
|
||||
guard !isImporting,scanningSource == nil,!generatingWaveforms,!movingMedia,!namesBusy else { error = "Wait for the current import, scan or waveform job to finish first.";return }
|
||||
guard !urls.isEmpty else { return }
|
||||
let token = UUID(),database = root.appendingPathComponent("Library.sqlite"),media = managedMediaDirectory,cache = root.appendingPathComponent("Waveforms")
|
||||
let token = UUID(),database = root.appendingPathComponent("Library.sqlite"),media = managedMediaDirectory,beforeIDs = Set(songs.map(\.id))
|
||||
importToken = token;isImporting = true;importProgress = ImportProgress()
|
||||
importTask = Task.detached(priority:.utility) { [weak self] in
|
||||
let accessed = urls.filter { $0.startAccessingSecurityScopedResource() }
|
||||
defer { for url in accessed { url.stopAccessingSecurityScopedResource() } }
|
||||
var latest = ImportProgress()
|
||||
var latest = ImportProgress(),importedIDs = [Int64]()
|
||||
do {
|
||||
let database = try Library(url:database)
|
||||
try Importer(media:media,waveformCache:cache).run(urls,library:database,cancelled:{Task.isCancelled},progress:{ update in
|
||||
importedIDs = try Importer(media:media).run(urls,library:database,cancelled:{Task.isCancelled},progress:{ update in
|
||||
latest = update
|
||||
Task { @MainActor [weak self] in
|
||||
guard let self,self.importToken == token else { return };self.importProgress = update
|
||||
@@ -493,13 +500,14 @@ struct SongDragReference: Codable, Sendable {
|
||||
} catch is CancellationError { latest.cancelled = true;latest.current = "Import cancelled" }
|
||||
catch { latest.failure = error.localizedDescription;latest.current = "Import stopped" }
|
||||
latest.finished = true
|
||||
let result = latest
|
||||
let result = latest,ids = Set(importedIDs)
|
||||
await MainActor.run { [weak self] in
|
||||
guard let self,self.importToken == token else { return }
|
||||
self.importToken = nil;self.importTask = nil;self.isImporting = false
|
||||
self.perform { try self.refresh() }
|
||||
self.importProgress = result
|
||||
if let failure = result.failure { self.error = failure }
|
||||
if !result.cancelled { self.queueImportedWaveforms(ids.union(self.songs.filter{!beforeIDs.contains($0.id)}.map(\.id))) }
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -652,6 +660,23 @@ struct StudioSearchField: View {
|
||||
.overlay(RoundedRectangle(cornerRadius:7).strokeBorder(.primary.opacity(0.12)))
|
||||
}
|
||||
}
|
||||
struct BackgroundWaveformStatus: View {
|
||||
let progress: WaveformJobProgress
|
||||
let cancel: () -> Void
|
||||
let dismiss: () -> Void
|
||||
var body: some View {
|
||||
HStack(spacing:10) {
|
||||
Image(systemName:progress.finished ? "checkmark.circle.fill" : "waveform").foregroundStyle(progress.cancelled || progress.issues > 0 ? Color.orange : progress.finished ? .green : .cyan)
|
||||
VStack(alignment:.leading,spacing:2) {
|
||||
Text(progress.finished ? progress.cancelled ? "Background waveforms cancelled" : "Background waveforms complete" : "Preparing waveforms in the background").font(.caption.weight(.semibold))
|
||||
Text("\(progress.processed) / \(progress.total) · \(progress.generated) generated · \(progress.cached) reused · \(progress.issues) issues").font(.caption2).foregroundStyle(.secondary)
|
||||
}
|
||||
Spacer()
|
||||
if progress.finished { Button(action:dismiss) { Image(systemName:"xmark") }.buttonStyle(.plain).help("Dismiss waveform results") }
|
||||
else { ProgressView(value:Double(progress.processed),total:Double(max(1,progress.total))).tint(.cyan).frame(width:120);Button("Cancel",action:cancel).controlSize(.small) }
|
||||
}.padding(10).background(Color.cyan.opacity(0.05),in:RoundedRectangle(cornerRadius:8))
|
||||
}
|
||||
}
|
||||
struct WaveformProgressCard: View {
|
||||
let progress: WaveformJobProgress
|
||||
let running: Bool
|
||||
@@ -961,6 +986,9 @@ struct LibraryView: View {
|
||||
}.frame(width:920).background(Color(nsColor:.windowBackgroundColor))
|
||||
}
|
||||
sourceContent.frame(maxWidth:.infinity,maxHeight:.infinity)
|
||||
if let progress = model.automaticWaveformProgress {
|
||||
BackgroundWaveformStatus(progress:progress,cancel:{model.automaticWaveforms.cancel()},dismiss:{model.automaticWaveformProgress = nil}).padding(.horizontal,18).padding(.vertical,8)
|
||||
}
|
||||
if let progress = model.waveformJob {
|
||||
WaveformProgressCard(progress:progress,running:model.generatingWaveforms,cancel:{model.cancelWaveformGeneration()},details:{waveformDetails = true},dismiss:{model.dismissWaveformProgress()})
|
||||
.padding(.horizontal,18).padding(.vertical,10)
|
||||
|
||||
@@ -188,3 +188,41 @@ extension WaveformBatch {
|
||||
progress(state);return state
|
||||
}
|
||||
}
|
||||
|
||||
/// Serial batches of background analysis, independent of import completion.
|
||||
@MainActor final class PostImportWaveformQueue {
|
||||
private var pending = [Int64:Song]()
|
||||
private var locations = [MediaLocation]()
|
||||
private var cache: URL?
|
||||
private var task: Task<Void,Never>?
|
||||
private var token = UUID()
|
||||
private var update: ((WaveformJobProgress) -> Void)?
|
||||
private var completed: (([Song]) -> Void)?
|
||||
private var latest: WaveformJobProgress?
|
||||
var isRunning: Bool { task != nil }
|
||||
func enqueue(_ songs: [Song],locations: [MediaLocation],cache: URL,update: @escaping (WaveformJobProgress) -> Void,completed: @escaping ([Song]) -> Void) {
|
||||
for song in songs { pending[song.id] = song }
|
||||
self.locations = locations;self.cache = cache;self.update = update;self.completed = completed
|
||||
if task == nil { startNext() }
|
||||
}
|
||||
private func startNext() {
|
||||
guard !pending.isEmpty,let cache else { return }
|
||||
let songs = pending.values.sorted{$0.id < $1.id},locations = self.locations,id = UUID()
|
||||
pending.removeAll();token = id;latest = .init(total:songs.count);update?(latest!)
|
||||
task = Task.detached(priority:.utility) { [weak self] in
|
||||
let result = await WaveformBatch.runConcurrent(songs:songs,locations:locations,cache:cache) { value in
|
||||
Task { @MainActor [weak self] in guard let self,self.token == id,self.task != nil else { return };self.latest = value;self.update?(value) }
|
||||
}
|
||||
await MainActor.run { [weak self] in
|
||||
guard let self,self.token == id else { return }
|
||||
self.task = nil;self.latest = result;self.update?(result);self.completed?(songs)
|
||||
self.startNext()
|
||||
}
|
||||
}
|
||||
}
|
||||
func cancel() {
|
||||
token = UUID();pending.removeAll();task?.cancel();task = nil
|
||||
if var value = latest { value.finished = true;value.cancelled = true;value.current = "Background waveform job cancelled";latest = value;update?(value) }
|
||||
}
|
||||
deinit { task?.cancel() }
|
||||
}
|
||||
|
||||
+49
-11
@@ -23,16 +23,20 @@ import CoreAudio
|
||||
@Published var loopEnabled = false { didSet {
|
||||
if loopEnabled { prepareNativeLoop() }
|
||||
else {
|
||||
loopBufferTask?.cancel();loopBufferTask = nil;loopBuffer = nil
|
||||
if nativeLoopActive { let position = displayTime;nativeLoopActive = false;schedule(at:position,play:playing) }
|
||||
loopBufferTask?.cancel();loopBufferTask = nil
|
||||
if nativeLoopActive,!exitNativeLoopWithoutRestart() { let position = displayTime;nativeLoopActive = false;schedule(at:position,play:playing) }
|
||||
loopBuffer = nil;preparedRawLoop = nil
|
||||
}
|
||||
} }
|
||||
private var loopBufferTask: Task<Void,Never>?
|
||||
private var loopBuffer: AVAudioPCMBuffer?
|
||||
private var preparedRawLoop: AVAudioPCMBuffer?
|
||||
private var scheduledRawLoop: AVAudioPCMBuffer?
|
||||
private var preparedLoop: ClosedRange<Double>?
|
||||
private var scheduledLoop: ClosedRange<Double>?
|
||||
private var pendingLoopSwitch: (at: Double,range: ClosedRange<Double>)?
|
||||
private var pendingLoopSwitch: (at: Double,range: ClosedRange<Double>,raw: AVAudioPCMBuffer)?
|
||||
private(set) var loopAudioRestartCount = 0
|
||||
private(set) var audioScheduleRestartCount = 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
|
||||
@@ -47,19 +51,53 @@ import CoreAudio
|
||||
guard let range = scheduledLoop,position >= range.upperBound else { return position }
|
||||
return range.lowerBound+(position-range.upperBound).truncatingRemainder(dividingBy:range.upperBound-range.lowerBound)
|
||||
}
|
||||
private func exitNativeLoopWithoutRestart() -> Bool {
|
||||
guard playing,let file,let render = node.lastRenderTime,let clock = node.playerTime(forNodeTime:render) else { return false }
|
||||
let sourceTime = rawAudioPosition,pending = pendingLoopSwitch
|
||||
let range = pending.map{sourceTime >= $0.at ? $0.range : scheduledLoop} ?? scheduledLoop
|
||||
let raw = pending.map{sourceTime >= $0.at ? $0.raw : scheduledRawLoop} ?? scheduledRawLoop
|
||||
guard let range,let raw,let source = raw.floatChannelData,!raw.format.isInterleaved else { return false }
|
||||
let position = displayTime,rate = file.processingFormat.sampleRate
|
||||
let offset = max(0,min(Int(raw.frameLength)-1,Int((position-range.lowerBound)*rate)))
|
||||
let count = min(2048,Int(raw.frameLength)-offset)
|
||||
guard count > 0,let bridge = AVAudioPCMBuffer(pcmFormat:raw.format,frameCapacity:AVAudioFrameCount(count)),let output = bridge.floatChannelData else { return false }
|
||||
bridge.frameLength = AVAudioFrameCount(count)
|
||||
for channel in 0..<Int(raw.format.channelCount) { memcpy(output[channel],source[channel].advanced(by:offset),count*MemoryLayout<Float>.size) }
|
||||
let now = mach_absolute_time(),elapsed = render.isHostTimeValid && now >= render.hostTime ? min(0.1,AVAudioTime.seconds(forHostTime:now-render.hostTime))*tempo : 0
|
||||
anchor = position-Double(clock.sampleTime)/rate-elapsed;time = position
|
||||
generation = UUID();let token = generation
|
||||
nativeLoopActive = false;pendingLoopSwitch = nil;scheduledLoop = nil;scheduledRawLoop = nil
|
||||
// Interrupt only the scheduled loop, leaving the running node and its clock intact.
|
||||
node.scheduleBuffer(bridge,at:nil,options:.interrupts,completionHandler:nil)
|
||||
let next = min(file.length,AVAudioFramePosition(position*rate)+AVAudioFramePosition(count))
|
||||
if next < file.length {
|
||||
node.scheduleSegment(file,startingFrame:next,frameCount:AVAudioFrameCount(min(file.length-next,Int64(UInt32.max))),at:nil,completionCallbackType:.dataPlayedBack) { [weak self] _ in
|
||||
Task { @MainActor in
|
||||
guard let self,self.generation == token else { return }
|
||||
if self.repeating { self.schedule(at:0,play:true) }
|
||||
else { self.playing = false;self.time = self.duration;self.timer?.invalidate();self.timer = nil;self.render();self.onFinished?() }
|
||||
}
|
||||
}
|
||||
}
|
||||
startTimer();self.render();return true
|
||||
}
|
||||
private func prepareNativeLoop() {
|
||||
loopBufferTask?.cancel()
|
||||
guard loopEnabled,let start = loopIn,let end = loopOut,end > start,let url = audioURL else { return }
|
||||
// Bound memory for long manual loops; common beat loops stay sample-timed.
|
||||
guard let file,(end-start)*file.processingFormat.sampleRate*Double(file.processingFormat.channelCount)*4 <= 64*1024*1024 else { return }
|
||||
guard let file,(end-start)*file.processingFormat.sampleRate*Double(file.processingFormat.channelCount)*8 <= 64*1024*1024 else { return }
|
||||
let songID = currentSongID
|
||||
loopBufferTask = Task { @MainActor [weak self] in
|
||||
let worker = Task.detached(priority:.userInitiated) { () throws -> AVAudioPCMBuffer in
|
||||
let worker = Task.detached(priority:.userInitiated) { () throws -> (AVAudioPCMBuffer,AVAudioPCMBuffer) in
|
||||
let source = try AVAudioFile(forReading:url)
|
||||
let first = AVAudioFramePosition(start*source.processingFormat.sampleRate)
|
||||
let count = AVAudioFrameCount(max(1,min(source.length-first,AVAudioFramePosition((end-start)*source.processingFormat.sampleRate))))
|
||||
guard let buffer = AVAudioPCMBuffer(pcmFormat:source.processingFormat,frameCapacity:count) else { throw LibraryError.message("Cannot prepare loop audio") }
|
||||
source.framePosition = first;try source.read(into:buffer,frameCount:count);try Task.checkCancellation()
|
||||
guard let raw = AVAudioPCMBuffer(pcmFormat:buffer.format,frameCapacity:buffer.frameLength) else { throw LibraryError.message("Cannot prepare loop continuation") }
|
||||
raw.frameLength = buffer.frameLength
|
||||
let sourceBuffers = UnsafeMutableAudioBufferListPointer(buffer.mutableAudioBufferList),rawBuffers = UnsafeMutableAudioBufferListPointer(raw.mutableAudioBufferList)
|
||||
for i in sourceBuffers.indices { if let source = sourceBuffers[i].mData,let target = rawBuffers[i].mData { memcpy(target,source,Int(sourceBuffers[i].mDataByteSize)) } }
|
||||
// Meet the first sample at the loop seam without changing its beat length.
|
||||
if let data = buffer.floatChannelData {
|
||||
let frames = Int(buffer.frameLength),stride = Int(buffer.stride)
|
||||
@@ -74,13 +112,13 @@ import CoreAudio
|
||||
}
|
||||
}
|
||||
}
|
||||
return buffer
|
||||
return (buffer,raw)
|
||||
}
|
||||
do {
|
||||
let buffer = try await withTaskCancellationHandler(operation:{try await worker.value},onCancel:{worker.cancel()})
|
||||
let (buffer,raw) = try await withTaskCancellationHandler(operation:{try await worker.value},onCancel:{worker.cancel()})
|
||||
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.loopBuffer = buffer;self.preparedRawLoop = raw;self.preparedLoop = start...end
|
||||
if self.nativeLoopActive,resume {
|
||||
let raw = self.rawAudioPosition
|
||||
let activeRange: ClosedRange<Double>
|
||||
@@ -88,7 +126,7 @@ import CoreAudio
|
||||
else { activeRange = self.scheduledLoop ?? start...end }
|
||||
let remaining = max(0,activeRange.upperBound-self.loopPosition(raw))
|
||||
self.node.scheduleBuffer(buffer,at:nil,options:[.loops,.interruptsAtLoop],completionHandler:nil)
|
||||
self.pendingLoopSwitch = (raw+remaining,start...end)
|
||||
self.pendingLoopSwitch = (raw+remaining,start...end,self.preparedRawLoop!)
|
||||
} else {
|
||||
self.loopAudioRestartCount += 1
|
||||
self.schedule(at:position < end ? position : start,play:resume,notifyPlayback:false)
|
||||
@@ -382,7 +420,7 @@ import CoreAudio
|
||||
private func schedule(at seconds: Double, play: Bool,notifyPlayback: Bool = true) {
|
||||
guard let file else { return }
|
||||
cancelPendingPlayback();generation = UUID(); let token = generation
|
||||
node.stop();if !fading { setFade(1) }
|
||||
node.stop();audioScheduleRestartCount += 1;if !fading { setFade(1) }
|
||||
let rate = file.processingFormat.sampleRate
|
||||
let start = min(file.length,max(0,AVAudioFramePosition(seconds*rate)))
|
||||
anchor = Double(start)/rate; time = anchor
|
||||
@@ -393,7 +431,7 @@ import CoreAudio
|
||||
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;scheduledLoop = range
|
||||
nativeLoopActive = true;scheduledLoop = range;scheduledRawLoop = preparedRawLoop
|
||||
if play { if notifyPlayback { startNodePlayback() } else { node.play() } }
|
||||
playing = play;render();return
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user