Skip to content

Commit

Permalink
Gapless playback + other fixes
Browse files Browse the repository at this point in the history
  • Loading branch information
kartik-venugopal committed Dec 19, 2024
1 parent be8eb30 commit 2dacf05
Show file tree
Hide file tree
Showing 13 changed files with 214 additions and 61 deletions.
12 changes: 12 additions & 0 deletions Aural.xcodeproj/project.pbxproj
Original file line number Diff line number Diff line change
Expand Up @@ -562,6 +562,7 @@
3E5701962C6EB16A007B8611 /* ReplayGain.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3E5701952C6EB16A007B8611 /* ReplayGain.swift */; };
3E5864A02B76DD0000BDB5A6 /* PlayerViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3E58649F2B76DD0000BDB5A6 /* PlayerViewController.swift */; };
3E5864A22B76EC8700BDB5A6 /* MenuBarPlayerViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3E5864A12B76EC8700BDB5A6 /* MenuBarPlayerViewController.swift */; };
3E58A9252D1421CC00591DEA /* GaplessPlaybackProgress.xib in Resources */ = {isa = PBXBuildFile; fileRef = 3E58A9242D1421CC00591DEA /* GaplessPlaybackProgress.xib */; };
3E58DEEE2C8D7EC8007ED247 /* ImageCache.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3E58DEED2C8D7EC8007ED247 /* ImageCache.swift */; };
3E58DEF02C8D816D007ED247 /* DataExtensions.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3E58DEEF2C8D816D007ED247 /* DataExtensions.swift */; };
3E59656B283BF91C00AF43FF /* ControlsContainerView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3E59656A283BF91C00AF43FF /* ControlsContainerView.swift */; };
Expand Down Expand Up @@ -1650,6 +1651,7 @@
3E5896082C6E6D6E00BB9EBC /* NewREADME.md */ = {isa = PBXFileReference; lastKnownFileType = net.daringfireball.markdown; path = NewREADME.md; sourceTree = "<group>"; };
3E5896092C6E6D6E00BB9EBC /* README-v4.md */ = {isa = PBXFileReference; lastKnownFileType = net.daringfireball.markdown; path = "README-v4.md"; sourceTree = "<group>"; };
3E58960A2C6E6D6E00BB9EBC /* Release Notes.md */ = {isa = PBXFileReference; lastKnownFileType = net.daringfireball.markdown; path = "Release Notes.md"; sourceTree = "<group>"; };
3E58A9242D1421CC00591DEA /* GaplessPlaybackProgress.xib */ = {isa = PBXFileReference; lastKnownFileType = file.xib; path = GaplessPlaybackProgress.xib; sourceTree = "<group>"; };
3E58DEED2C8D7EC8007ED247 /* ImageCache.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ImageCache.swift; sourceTree = "<group>"; };
3E58DEEF2C8D816D007ED247 /* DataExtensions.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DataExtensions.swift; sourceTree = "<group>"; };
3E59656A283BF91C00AF43FF /* ControlsContainerView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ControlsContainerView.swift; sourceTree = "<group>"; };
Expand Down Expand Up @@ -3804,6 +3806,14 @@
path = Documentation;
sourceTree = "<group>";
};
3E58A9232D1421B900591DEA /* GaplessPlaybackProgress */ = {
isa = PBXGroup;
children = (
3E58A9242D1421CC00591DEA /* GaplessPlaybackProgress.xib */,
);
path = GaplessPlaybackProgress;
sourceTree = "<group>";
};
3E58DEEC2C8D7EBD007ED247 /* ImageCache */ = {
isa = PBXGroup;
children = (
Expand Down Expand Up @@ -4545,6 +4555,7 @@
3E6C101A25CEB98600BF0D07 /* Player */ = {
isa = PBXGroup;
children = (
3E58A9232D1421B900591DEA /* GaplessPlaybackProgress */,
3E58649F2B76DD0000BDB5A6 /* PlayerViewController.swift */,
3ED65A522C45BA3000859677 /* PlayerViewController+Actions.swift */,
3ED65A542C45BAD000859677 /* PlayerViewController+Theming.swift */,
Expand Down Expand Up @@ -5705,6 +5716,7 @@
3E25B65627F7875700D10A5F /* PlayQueueSimpleView.xib in Resources */,
3EC079472B78549F008CB68C /* MenuBarSettings.xib in Resources */,
3ED373EB2C70E4F500836511 /* ReplayGainUnit.xib in Resources */,
3E58A9252D1421CC00591DEA /* GaplessPlaybackProgress.xib in Resources */,
3E6C12ED25CEBE8100BF0D07 /* DelayUnit.xib in Resources */,
3E6668B82ADB651500BBD119 /* ThemeSetup.xib in Resources */,
3E51F346262AFDCA0048452F /* ThemesManager.xib in Resources */,
Expand Down
Binary file not shown.
74 changes: 43 additions & 31 deletions Source/Core/PlayQueue/PlayQueue.swift
Original file line number Diff line number Diff line change
Expand Up @@ -29,8 +29,8 @@ class PlayQueue: TrackList, PlayQueueProtocol {

// Contains a pre-computed shuffle sequence, when shuffleMode is .on
lazy var shuffleSequence: ShuffleSequence = ShuffleSequence()
private lazy var messenger = Messenger(for: self)

let gaplessPrepQueue: OperationQueue = .init(opCount: System.numberOfActiveCores, qos: .userInteractive)

// MARK: Accessor functions

Expand Down Expand Up @@ -218,46 +218,49 @@ class PlayQueue: TrackList, PlayQueueProtocol {
}

func prepareForGaplessPlayback() throws {

var audioFormatsSet: Set<AVAudioFormat> = Set()
let audioFormatsSet: ConcurrentSet<AVAudioFormat> = ConcurrentSet()
var errorMsg: String? = nil

for track in self.tracks {

do {
if audioFormatsSet.count > 1 {

errorMsg = "The tracks in the Play Queue do not all have the same audio format."
break

try trackReader.prepareForPlayback(track: track, immediate: false)
} else if errorMsg != nil {
break
}

gaplessPrepQueue.addOperation {

if let audioFormat = track.playbackContext?.audioFormat {
do {

audioFormatsSet.insert(audioFormat)
try trackReader.prepareForGaplessPlayback(track: track)

if audioFormatsSet.count > 1 {
if let audioFormat = track.playbackContext?.audioFormat {
audioFormatsSet.insert(audioFormat)

errorMsg = "The tracks in the Play Queue do not all have the same audio format."
break
} else {
errorMsg = "Unable to prepare for gapless playback: No audio context for track: \(track)."
}

} else {

errorMsg = "Unable to prepare for gapless playback: No audio context for track: \(track)."
break
} catch {
errorMsg = "Unable to prepare track \(track) for gapless playback: \(error)"
}

} catch {

errorMsg = "Unable to prepare track \(track) for gapless playback: \(error)"
break
}
}

if let theErrorMsg = errorMsg {
throw GaplessPlaybackNotPossibleError(theErrorMsg)
}
print("\(gaplessPrepQueue.operationCount) ops running ... ")

gaplessPrepQueue.waitUntilAllOperationsAreFinished()

let success = audioFormatsSet.count == 1
if let errorMsg {
throw GaplessPlaybackNotPossibleError(errorMsg)
}

guard success else {
guard audioFormatsSet.count == 1 else {
throw GaplessPlaybackNotPossibleError("The tracks in the Play Queue do not all have the same audio format.")
}

Expand All @@ -271,7 +274,7 @@ class PlayQueue: TrackList, PlayQueueProtocol {
}

override func preTrackLoad() {
messenger.publish(.PlayQueue.startedAddingTracks)
Messenger.publish(.PlayQueue.startedAddingTracks)
}

override func firstBatchLoaded(atIndices indices: IndexSet) {
Expand All @@ -291,26 +294,33 @@ class PlayQueue: TrackList, PlayQueueProtocol {
}

override func postBatchLoad(indices: IndexSet) {
messenger.publish(PlayQueueTracksAddedNotification(trackIndices: indices))
Messenger.publish(PlayQueueTracksAddedNotification(trackIndices: indices))
}

override func postTrackLoad() {

if markLoadedItemsForHistory.value {
messenger.publish(HistoryItemsAddedNotification(itemURLs: session.urls))
Messenger.publish(HistoryItemsAddedNotification(itemURLs: session.urls))
}

if preferences.metadataPreferences.cacheTrackMetadata.value {
metadataRegistry.persistCoverArt()
}

messenger.publish(.PlayQueue.doneAddingTracks)
Messenger.publish(.PlayQueue.doneAddingTracks)

defer {

firstTrackLoad = false
autoplayResumeSequence.setFalse()
}

if autoplayResumeSequence.value, shuffleMode == .on,
let hist = appPersistentState.playQueue?.history
if hist?.shuffleSequence == nil {
print("No SS !!!")
}

if firstTrackLoad, shuffleMode == .on,
let pQPersistentState = appPersistentState.playQueue,
let persistentTracks = pQPersistentState.tracks,
let historyPersistentState = pQPersistentState.history,
Expand Down Expand Up @@ -340,7 +350,7 @@ class PlayQueue: TrackList, PlayQueueProtocol {
shuffleSequence.initialize(with: sequenceTracks,
playedTracks: playedTracks)

if let track = sequenceTracks.first,
if autoplayResumeSequence.value, let track = sequenceTracks.first,
let playbackPosition = historyPersistentState.lastPlaybackPosition {

playbackDelegate.resumeShuffleSequence(with: track,
Expand All @@ -359,3 +369,5 @@ class PlayQueue: TrackList, PlayQueueProtocol {
}
}
}

fileprivate var firstTrackLoad: Bool = true
7 changes: 6 additions & 1 deletion Source/Core/Playback/Delegates/PlaybackDelegate.swift
Original file line number Diff line number Diff line change
Expand Up @@ -118,12 +118,16 @@ class PlaybackDelegate: PlaybackDelegateProtocol {

private func doBeginGaplessPlayback() {

guard let firstTrack = playQueueDelegate.tracks.first else {return}

_ = playQueueDelegate.start()

player.playGapless(tracks: playQueueDelegate.tracks)
trackReader.loadArtAsync(for: firstTrack, immediate: true)

// Inform observers of the track change/transition.
messenger.publish(TrackTransitionNotification(beginTrack: nil, beginState: .stopped,
endTrack: playQueueDelegate.tracks.first, endState: player.state))
endTrack: firstTrack, endState: player.state))
}

func previousTrack() {
Expand Down Expand Up @@ -497,6 +501,7 @@ class PlaybackDelegate: PlaybackDelegateProtocol {
}

session.track = subsequentTrack
trackReader.loadArtAsync(for: subsequentTrack, immediate: true)

} else {

Expand Down
43 changes: 43 additions & 0 deletions Source/Core/TrackIO/TrackReader.swift
Original file line number Diff line number Diff line change
Expand Up @@ -170,6 +170,49 @@ class TrackReader {
}
}

///
/// Loads all metadata and resources that are required for track playback.
///
func prepareForGaplessPlayback(track: Track) throws {

// Make sure track is valid before trying to prep it for playback.
if let prepError = track.metadata.preparationError {
throw prepError

} else if let validationError = track.metadata.validationError {

track.metadata.preparationError = validationError
throw validationError
}

do {

// If a playback context has been previously computed, just open it.
if let theContext = track.playbackContext {
try theContext.open()

} else {

// No playback context was previously computed, so compute it and open it.

try computePlaybackContext(for: track)
try track.playbackContext?.open()
}

} catch {

NSLog("Unable to prepare track \(track.displayName) for playback. Error: \(error)")

track.metadata.preparationFailed = true

if let prepError = error as? DisplayableError {
track.metadata.preparationError = prepError
}

throw error
}
}

///
/// Loads cover art for a track, asynchronously. This is useful when
/// cover art is not required immediately, and a short delay is acceptable.
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,12 @@ class ConcurrentSet<T: Hashable> {
private var _set: Set<T> = Set<T>()
var set: Set<T> {_set}

var count: Int {_set.count}
var count: Int {

lock.produceValueAfterWait {
_set.count
}
}

func contains(_ value: T) -> Bool {

Expand Down
8 changes: 4 additions & 4 deletions Source/UI/CompactPlayer/CompactPlayerWindow.xib
Original file line number Diff line number Diff line change
Expand Up @@ -26,7 +26,7 @@
<windowCollectionBehavior key="collectionBehavior" fullScreenNone="YES"/>
<windowPositionMask key="initialPositionMask" leftStrut="YES" topStrut="YES"/>
<rect key="contentRect" x="335" y="100" width="300" height="430"/>
<rect key="screenRect" x="0.0" y="0.0" width="2560" height="1415"/>
<rect key="screenRect" x="0.0" y="0.0" width="3440" height="1415"/>
<value key="minSize" type="size" width="300" height="430"/>
<value key="maxSize" type="size" width="300" height="430"/>
<view key="contentView" id="R1o-nt-GJA" userLabel="Player View">
Expand All @@ -40,7 +40,7 @@
<autoresizingMask key="autoresizingMask" widthSizable="YES" heightSizable="YES"/>
<subviews>
<button toolTip="Minimize" wantsLayer="YES" focusRingType="none" fixedFrame="YES" translatesAutoresizingMaskIntoConstraints="NO" id="OWA-YG-E9q" userLabel="Minimize Button" customClass="TintedImageButton" customModule="Aural" customModuleProvider="target">
<rect key="frame" x="41" y="407" width="14.5" height="10"/>
<rect key="frame" x="40" y="407" width="12.5" height="10"/>
<autoresizingMask key="autoresizingMask" flexibleMaxX="YES" flexibleMinY="YES"/>
<buttonCell key="cell" type="square" bezelStyle="shadowlessSquare" image="minus" catalog="system" imagePosition="only" alignment="center" refusesFirstResponder="YES" focusRingType="none" imageScaling="axesIndependently" inset="2" id="0g1-om-Ujd">
<behavior key="behavior" pushIn="YES" lightByBackground="YES" lightByGray="YES"/>
Expand All @@ -51,7 +51,7 @@
</connections>
</button>
<button toolTip="Quit Aural" wantsLayer="YES" focusRingType="none" fixedFrame="YES" translatesAutoresizingMaskIntoConstraints="NO" id="ZT2-MQ-uY4" userLabel="Quit Button" customClass="TintedImageButton" customModule="Aural" customModuleProvider="target">
<rect key="frame" x="10" y="404.5" width="11.5" height="14"/>
<rect key="frame" x="10" y="406.5" width="11.5" height="10"/>
<autoresizingMask key="autoresizingMask" flexibleMaxX="YES" flexibleMinY="YES"/>
<buttonCell key="cell" type="square" bezelStyle="shadowlessSquare" image="xmark" catalog="system" imagePosition="only" alignment="center" refusesFirstResponder="YES" focusRingType="none" imageScaling="proportionallyUpOrDown" inset="2" id="vcA-DL-aSl">
<behavior key="behavior" pushIn="YES" lightByBackground="YES" lightByGray="YES"/>
Expand Down Expand Up @@ -79,7 +79,7 @@
<imageCell key="cell" refusesFirstResponder="YES" focusRingType="none" alignment="left" imageScaling="proportionallyDown" image="AppTitle" id="2Hm-AK-QNc"/>
</imageView>
<popUpButton toolTip="Change presentation mode" wantsLayer="YES" focusRingType="none" horizontalHuggingPriority="750" verticalHuggingPriority="750" fixedFrame="YES" translatesAutoresizingMaskIntoConstraints="NO" id="wA6-bg-jfE" userLabel="Presentation Mode Menu">
<rect key="frame" x="22" y="402" width="18" height="18"/>
<rect key="frame" x="22" y="403" width="16" height="16"/>
<autoresizingMask key="autoresizingMask" flexibleMaxX="YES" flexibleMinY="YES"/>
<popUpButtonCell key="cell" type="disclosure" bezelStyle="roundedDisclosure" alignment="center" lineBreakMode="truncatingTail" refusesFirstResponder="YES" focusRingType="none" imageScaling="proportionallyUpOrDown" inset="2" pullsDown="YES" arrowPosition="noArrow" preferredEdge="maxX" autoenablesItems="NO" altersStateOfSelectedItem="NO" id="9A6-gF-XEj">
<behavior key="behavior" pushIn="YES" lightByBackground="YES" lightByGray="YES"/>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -263,7 +263,7 @@ class CompactPlayerWindowController: NSWindowController {

private func updateMainMenuState() {

appDelegate.playbackMenuRootItem.enableIf(compactPlayerUIState.displayedView == .player)
// appDelegate.playbackMenuRootItem.enableIf(compactPlayerUIState.displayedView == .player)
appDelegate.soundMenuRootItem.enableIf(compactPlayerUIState.displayedView.equalsOneOf(.player, .effects))
appDelegate.playQueueMenuRootItem.enableIf(compactPlayerUIState.displayedView == .playQueue)
}
Expand Down
4 changes: 2 additions & 2 deletions Source/UI/CompactPlayer/PlayQueue/CompactPlayQueue.xib
Original file line number Diff line number Diff line change
Expand Up @@ -485,7 +485,7 @@
</constraints>
</popUpButton>
<textField horizontalHuggingPriority="251" verticalHuggingPriority="750" translatesAutoresizingMaskIntoConstraints="NO" id="ILg-fo-rF1" userLabel="Play Queue Caption Label" customClass="CenterTextLabel" customModule="Aural" customModuleProvider="target">
<rect key="frame" x="23" y="360" width="214" height="25"/>
<rect key="frame" x="8" y="360" width="214" height="25"/>
<constraints>
<constraint firstAttribute="height" constant="25" id="76d-fr-gp4"/>
<constraint firstAttribute="width" constant="210" id="dTN-oT-aj0"/>
Expand All @@ -509,7 +509,7 @@
<constraint firstItem="rGi-Br-hY8" firstAttribute="centerY" secondItem="EEM-hK-rk8" secondAttribute="centerY" id="GlG-h3-FHh"/>
<constraint firstItem="pcR-HY-5FP" firstAttribute="leading" secondItem="8Vi-ax-FQk" secondAttribute="trailing" constant="15" id="IRK-bo-zz9"/>
<constraint firstAttribute="bottom" secondItem="xTF-ma-4qy" secondAttribute="bottom" constant="10" id="JjW-Lc-7JF"/>
<constraint firstItem="ILg-fo-rF1" firstAttribute="leading" secondItem="el0-VE-Ib7" secondAttribute="leading" constant="25" id="Uiv-0W-eZk"/>
<constraint firstItem="ILg-fo-rF1" firstAttribute="leading" secondItem="el0-VE-Ib7" secondAttribute="leading" constant="10" id="Uiv-0W-eZk"/>
<constraint firstItem="M4Y-BC-T8J" firstAttribute="centerY" secondItem="sb4-QZ-Nqd" secondAttribute="centerY" id="Zba-h8-1u4"/>
<constraint firstItem="aBL-E4-FyY" firstAttribute="leading" secondItem="M1d-v2-GBv" secondAttribute="trailing" constant="5" id="buk-BK-wnV"/>
<constraint firstItem="EEM-hK-rk8" firstAttribute="centerY" secondItem="RsD-Tk-fTw" secondAttribute="centerY" id="cOX-KA-Z9f"/>
Expand Down
Loading

0 comments on commit 2dacf05

Please sign in to comment.