feat(mac,ios,playback,store): initial macos implementation
This commit is contained in:
1 parent
e5bb559d7f
commit
d2d7166cf0
72 files changed
+4452
-318
No files matched your search
@@ -6,8 +6,10 @@ import Testing
|
||||
actor RecordingEngine: OutputEngine {
|
||||
nonisolated let events: AsyncStream<OutputEvent>
|
||||
nonisolated let continuation: AsyncStream<OutputEvent>.Continuation
|
||||
private(set) var volumes: [Double] = []
|
||||
private(set) var loaded: [QueueItem.ID] = []
|
||||
private(set) var prepared: [QueueItem.ID?] = []
|
||||
private(set) var isPlaying = false
|
||||
|
||||
init() {
|
||||
(events, continuation) = AsyncStream<OutputEvent>.makeStream()
|
||||
@@ -21,8 +23,18 @@ actor RecordingEngine: OutputEngine {
|
||||
prepared.append(item?.id)
|
||||
}
|
||||
|
||||
func play() {}
|
||||
func pause() {}
|
||||
func setVolume(_ volume: Double) {
|
||||
volumes.append(volume)
|
||||
}
|
||||
|
||||
func play() {
|
||||
isPlaying = true
|
||||
}
|
||||
|
||||
func pause() {
|
||||
isPlaying = false
|
||||
}
|
||||
|
||||
func seek(to _: TimeInterval) {}
|
||||
func stop() {}
|
||||
}
|
||||
@@ -183,3 +195,37 @@ private func tracks(_ count: Int) -> [Track] {
|
||||
#expect(state.status == .paused && state.anchor.rate == 0)
|
||||
#expect(await engine.loaded.count == 2)
|
||||
}
|
||||
|
||||
@Test func volumeReachesEngineAndSurvivesQueueChanges() async {
|
||||
let engine = RecordingEngine()
|
||||
let session = PlaybackSession(engine: engine)
|
||||
await session.send(.setVolume(0.35))
|
||||
await session.send(.setQueue(tracks: tracks(2), startIndex: 0, shuffled: false))
|
||||
await session.send(.next)
|
||||
#expect(await session.snapshot.volume == 0.35)
|
||||
#expect(await engine.volumes == [0.35])
|
||||
await session.send(.setVolume(-1))
|
||||
await session.send(.setVolume(2))
|
||||
await session.send(.setVolume(.nan))
|
||||
#expect(await engine.volumes == [0.35, 0, 1])
|
||||
#expect(await session.snapshot.volume == 1)
|
||||
}
|
||||
|
||||
@Test func skippingPastTheEndStopsTheEngine() async {
|
||||
let engine = RecordingEngine()
|
||||
let session = PlaybackSession(engine: engine)
|
||||
await session.send(.setQueue(tracks: tracks(1), startIndex: 0, shuffled: false))
|
||||
#expect(await engine.isPlaying)
|
||||
await session.send(.next)
|
||||
#expect(await session.snapshot.status == .paused)
|
||||
#expect(await !engine.isPlaying)
|
||||
}
|
||||
|
||||
@Test func clearingHistoryKeepsCurrentAndUpcoming() async {
|
||||
let session = PlaybackSession(engine: RecordingEngine())
|
||||
await session.send(.setQueue(tracks: tracks(4), startIndex: 2, shuffled: false))
|
||||
await session.send(.clearHistory)
|
||||
let state = await session.snapshot
|
||||
#expect(state.history.isEmpty)
|
||||
#expect(state.current?.track.title == "Track 2" && state.upcoming.count == 1)
|
||||
}
|
||||
@@ -0,0 +1,34 @@
|
||||
import Domain
|
||||
import Foundation
|
||||
@testable import Playback
|
||||
import Testing
|
||||
|
||||
struct SampleRateMatcherTests {
|
||||
private let device = OutputDeviceInfo(id: 1, name: "Fake", nominalRate: 48000, availableRates: [44100, 48000, 96000])
|
||||
private let flac = AudioFormat(codec: .flac, sampleRate: 44100, bitDepth: 16)
|
||||
|
||||
@Test func settingOffLeavesDeviceAlone() {
|
||||
#expect(SampleRateMatcher(isEnabled: false).targetRate(for: flac, on: device) == nil)
|
||||
}
|
||||
|
||||
@Test func sameRateNeedsNoSwitch() {
|
||||
let format = AudioFormat(codec: .flac, sampleRate: 48000)
|
||||
#expect(SampleRateMatcher(isEnabled: true).targetRate(for: format, on: device) == nil)
|
||||
}
|
||||
|
||||
@Test func unavailableRateIsIgnored() {
|
||||
let format = AudioFormat(codec: .flac, sampleRate: 192_000)
|
||||
#expect(SampleRateMatcher(isEnabled: true).targetRate(for: format, on: device) == nil)
|
||||
}
|
||||
|
||||
@Test func unknownRateOrDeviceIsIgnored() {
|
||||
let matcher = SampleRateMatcher(isEnabled: true)
|
||||
#expect(matcher.targetRate(for: AudioFormat(codec: .mp3), on: device) == nil)
|
||||
#expect(matcher.targetRate(for: nil, on: device) == nil)
|
||||
#expect(matcher.targetRate(for: flac, on: nil) == nil)
|
||||
}
|
||||
|
||||
@Test func availableRateIsChosenExactly() {
|
||||
#expect(SampleRateMatcher(isEnabled: true).targetRate(for: flac, on: device) == 44100)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,37 @@
|
||||
import Domain
|
||||
import Store
|
||||
import Testing
|
||||
|
||||
@Test func searchRanksExactArtistFirstAndFindsTheirSongs() async {
|
||||
let source = SourceID()
|
||||
func id(_ remote: String) -> EntityID {
|
||||
EntityID(source: source, remote: remote)
|
||||
}
|
||||
func track(_ remote: String, _ title: String, by artist: String, _ artistID: EntityID?) -> Change {
|
||||
.upsert(.track(Track(
|
||||
id: id(remote),
|
||||
title: title,
|
||||
albumID: nil,
|
||||
albumTitle: "",
|
||||
artistID: artistID,
|
||||
artistName: artist,
|
||||
duration: 1
|
||||
)))
|
||||
}
|
||||
let library = MemoryLibrary()
|
||||
await library.apply([
|
||||
.upsert(.artist(Artist(id: id("ar-1"), name: "Kai Angel"))),
|
||||
.upsert(.artist(Artist(id: id("ar-2"), name: "Angel Olsen"))),
|
||||
.upsert(.album(Album(id: id("al-1"), title: "Shh...", artistID: id("ar-1"), artistName: "Kai Angel"))),
|
||||
track("t-1", "andy warhol", by: "Kai Angel", id("ar-1")),
|
||||
track("t-2", "Fallen Angel", by: "Other", nil),
|
||||
])
|
||||
|
||||
let exact = await library.search("kai angel")
|
||||
#expect(exact.top.map(\.id) == [id("ar-1"), id("t-1"), id("al-1")])
|
||||
#expect(exact.tracks.map(\.id) == [id("t-1")])
|
||||
|
||||
let broad = await library.search("angel")
|
||||
#expect(broad.artists.map(\.id) == [id("ar-2"), id("ar-1")])
|
||||
#expect(broad.tracks.map(\.id) == [id("t-2"), id("t-1")])
|
||||
}
|
||||
Reference in new issue
Block a user