refactor
This commit is contained in:
@@ -22,6 +22,7 @@
|
||||
9816A8B22BC8330C00AD1299 /* bensound-jazzyfrenchy.m4a in Resources */ = {isa = PBXBuildFile; fileRef = 9816A8AE2BC832DB00AD1299 /* bensound-jazzyfrenchy.m4a */; };
|
||||
9816A8B32BC8330C00AD1299 /* hipjazz.wav in Resources */ = {isa = PBXBuildFile; fileRef = 9816A8AF2BC832DC00AD1299 /* hipjazz.wav */; };
|
||||
9816A8BB2BC87BC200AD1299 /* AudioPlayerService.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9816A8BA2BC87BC200AD1299 /* AudioPlayerService.swift */; };
|
||||
984DE9552BDAE59C004B427A /* Notifier.swift in Sources */ = {isa = PBXBuildFile; fileRef = 984DE9542BDAE59C004B427A /* Notifier.swift */; };
|
||||
98BFB41A2BC97AF800E812C0 /* DisplayLink.swift in Sources */ = {isa = PBXBuildFile; fileRef = 98BFB4192BC97AF800E812C0 /* DisplayLink.swift */; };
|
||||
98BFB41D2BCD7BB800E812C0 /* EqualizerView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 98BFB41C2BCD7BB800E812C0 /* EqualizerView.swift */; };
|
||||
98BFB41F2BCD814000E812C0 /* EqualizerService.swift in Sources */ = {isa = PBXBuildFile; fileRef = 98BFB41E2BCD814000E812C0 /* EqualizerService.swift */; };
|
||||
@@ -61,6 +62,7 @@
|
||||
9816A8AE2BC832DB00AD1299 /* bensound-jazzyfrenchy.m4a */ = {isa = PBXFileReference; lastKnownFileType = file; path = "bensound-jazzyfrenchy.m4a"; sourceTree = "<group>"; };
|
||||
9816A8AF2BC832DC00AD1299 /* hipjazz.wav */ = {isa = PBXFileReference; lastKnownFileType = audio.wav; path = hipjazz.wav; sourceTree = "<group>"; };
|
||||
9816A8BA2BC87BC200AD1299 /* AudioPlayerService.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AudioPlayerService.swift; sourceTree = "<group>"; };
|
||||
984DE9542BDAE59C004B427A /* Notifier.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Notifier.swift; sourceTree = "<group>"; };
|
||||
98BFB4192BC97AF800E812C0 /* DisplayLink.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DisplayLink.swift; sourceTree = "<group>"; };
|
||||
98BFB41B2BCAAD8A00E812C0 /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist; path = Info.plist; sourceTree = "<group>"; };
|
||||
98BFB41C2BCD7BB800E812C0 /* EqualizerView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = EqualizerView.swift; sourceTree = "<group>"; };
|
||||
@@ -103,10 +105,9 @@
|
||||
9806E8162BC5D12500757370 /* AudioPlayer */ = {
|
||||
isa = PBXGroup;
|
||||
children = (
|
||||
984DE9532BDAE57F004B427A /* Dependencies */,
|
||||
98BFB41B2BCAAD8A00E812C0 /* Info.plist */,
|
||||
98BFB41E2BCD814000E812C0 /* EqualizerService.swift */,
|
||||
9816A8BA2BC87BC200AD1299 /* AudioPlayerService.swift */,
|
||||
98BFB4192BC97AF800E812C0 /* DisplayLink.swift */,
|
||||
984DE9522BDAE571004B427A /* Helpers */,
|
||||
9816A8A82BC7F4DE00AD1299 /* Common */,
|
||||
9806E8282BC68F7300757370 /* Content */,
|
||||
9806E8272BC68F6600757370 /* Navigation */,
|
||||
@@ -173,6 +174,24 @@
|
||||
path = Resources;
|
||||
sourceTree = "<group>";
|
||||
};
|
||||
984DE9522BDAE571004B427A /* Helpers */ = {
|
||||
isa = PBXGroup;
|
||||
children = (
|
||||
98BFB4192BC97AF800E812C0 /* DisplayLink.swift */,
|
||||
984DE9542BDAE59C004B427A /* Notifier.swift */,
|
||||
);
|
||||
path = Helpers;
|
||||
sourceTree = "<group>";
|
||||
};
|
||||
984DE9532BDAE57F004B427A /* Dependencies */ = {
|
||||
isa = PBXGroup;
|
||||
children = (
|
||||
98BFB41E2BCD814000E812C0 /* EqualizerService.swift */,
|
||||
9816A8BA2BC87BC200AD1299 /* AudioPlayerService.swift */,
|
||||
);
|
||||
path = Dependencies;
|
||||
sourceTree = "<group>";
|
||||
};
|
||||
98E3921B2BD845D200B586E9 /* AudioQueue */ = {
|
||||
isa = PBXGroup;
|
||||
children = (
|
||||
@@ -275,6 +294,7 @@
|
||||
98E392202BD8461D00B586E9 /* AudioQueueModel.swift in Sources */,
|
||||
9816A8AC2BC820DF00AD1299 /* AudioContent.swift in Sources */,
|
||||
9806E8262BC5D2A900757370 /* Sidebar.swift in Sources */,
|
||||
984DE9552BDAE59C004B427A /* Notifier.swift in Sources */,
|
||||
9806E82A2BC68F8700757370 /* AudioPlayerView.swift in Sources */,
|
||||
9806E8312BC6927D00757370 /* AudioPlayerModel.swift in Sources */,
|
||||
98BFB41F2BCD814000E812C0 /* EqualizerService.swift in Sources */,
|
||||
|
||||
@@ -23,43 +23,18 @@ enum ScrubState: Equatable {
|
||||
public class AudioPlayerModel {
|
||||
@ObservationIgnored
|
||||
private(set) var audioPlayerService: AudioPlayerService
|
||||
@ObservationIgnored
|
||||
private var displayLink: DisplayLink?
|
||||
|
||||
var audioTracks: [AudioPlaylist] = []
|
||||
|
||||
var isLiveAudioStreaming: Bool {
|
||||
totalTime == 0
|
||||
}
|
||||
|
||||
var liveAudioMetadata: String?
|
||||
|
||||
var isPlaying: Bool = false
|
||||
var isMuted: Bool = false
|
||||
|
||||
var playbackRate: Double = 0.0
|
||||
|
||||
var currentTime: Double = 0
|
||||
var totalTime: Double?
|
||||
|
||||
var scrubState: ScrubState = .idle
|
||||
|
||||
var formattedCurrentTime: String?
|
||||
var formattedTotalTime: String?
|
||||
|
||||
var currentTrack: AudioTrack?
|
||||
|
||||
init(audioTracksProvider: () -> [AudioPlaylist] = audioTracksProvider, audioPlayerService: AudioPlayerService) {
|
||||
self.audioPlayerService = audioPlayerService
|
||||
self.audioTracks = audioTracksProvider()
|
||||
|
||||
self.audioPlayerService.delegate = self
|
||||
}
|
||||
|
||||
deinit {
|
||||
audioPlayerService.stop()
|
||||
displayLink?.deactivate()
|
||||
displayLink = nil
|
||||
}
|
||||
|
||||
func addNewAudioTrack(url: URL) {
|
||||
@@ -76,136 +51,11 @@ public class AudioPlayerModel {
|
||||
}
|
||||
}
|
||||
|
||||
func mute() {
|
||||
isMuted.toggle()
|
||||
audioPlayerService.toggleMute()
|
||||
}
|
||||
|
||||
func playPause() {
|
||||
if audioPlayerService.state == .playing {
|
||||
audioPlayerService.pause()
|
||||
} else {
|
||||
audioPlayerService.resume()
|
||||
}
|
||||
}
|
||||
|
||||
func update(rate: Float) {
|
||||
let rate = round(rate / 0.2) * 0.2
|
||||
audioPlayerService.update(rate: rate)
|
||||
}
|
||||
|
||||
|
||||
func stop() {
|
||||
isPlaying = false
|
||||
audioPlayerService.stop()
|
||||
currentTrack?.status = .idle
|
||||
currentTrack = nil
|
||||
}
|
||||
|
||||
func play(_ track: AudioTrack) {
|
||||
if track != currentTrack {
|
||||
currentTrack?.status = .idle
|
||||
audioPlayerService.play(url: track.url)
|
||||
currentTrack = track
|
||||
}
|
||||
}
|
||||
|
||||
func onTick() {
|
||||
let duration = audioPlayerService.duration
|
||||
let progress = audioPlayerService.progress
|
||||
if duration > 0 {
|
||||
let elapsed = Int(progress)
|
||||
let remaining = Int(duration - progress)
|
||||
totalTime = duration
|
||||
switch scrubState {
|
||||
case .idle:
|
||||
currentTime = progress
|
||||
case .started:
|
||||
break
|
||||
case .ended(let seekTime):
|
||||
currentTime = seekTime
|
||||
if audioPlayerService.duration > 0 {
|
||||
audioPlayerService.seek(at: seekTime)
|
||||
}
|
||||
scrubState = .idle
|
||||
}
|
||||
formattedCurrentTime = timeFrom(seconds: Int(elapsed))
|
||||
formattedTotalTime = timeFrom(seconds: remaining)
|
||||
} else {
|
||||
let elapsed = Int(progress)
|
||||
formattedCurrentTime = timeFrom(seconds: Int(elapsed))
|
||||
if formattedTotalTime != nil {
|
||||
formattedTotalTime = nil
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func resetLabels() {
|
||||
currentTime = 0
|
||||
totalTime = 0
|
||||
formattedCurrentTime = nil
|
||||
formattedTotalTime = nil
|
||||
}
|
||||
|
||||
private func timeFrom(seconds: Int) -> String {
|
||||
let correctSeconds = seconds % 60
|
||||
let minutes = (seconds / 60) % 60
|
||||
let hours = seconds / 3600
|
||||
|
||||
if hours > 0 {
|
||||
return String(format: "%02d:%02d:%02d", hours, minutes, correctSeconds)
|
||||
}
|
||||
return String(format: "%02d:%02d", minutes, correctSeconds)
|
||||
}
|
||||
}
|
||||
|
||||
extension AudioPlayerModel: AudioPlayerServiceDelegate {
|
||||
func didStartPlaying(id: AudioEntryId) {
|
||||
self.displayLink = DisplayLink(onTick: { [weak self] _ in
|
||||
self?.onTick()
|
||||
})
|
||||
displayLink?.activate()
|
||||
}
|
||||
|
||||
func didStopPlaying(id: AudioEntryId, reason: AudioPlayerStopReason) {
|
||||
resetLabels()
|
||||
liveAudioMetadata = nil
|
||||
playbackRate = 1.0
|
||||
displayLink?.deactivate()
|
||||
}
|
||||
|
||||
func statusChanged(status: AudioStreaming.AudioPlayerState) {
|
||||
isPlaying = status == .playing
|
||||
displayLink?.isPaused = !isPlaying
|
||||
switch status {
|
||||
case .bufferring:
|
||||
currentTrack?.status = .buffering
|
||||
case .error:
|
||||
currentTrack?.status = .error
|
||||
currentTrack = nil
|
||||
case .playing:
|
||||
currentTrack?.status = .playing
|
||||
case .paused:
|
||||
currentTrack?.status = .paused
|
||||
case .stopped:
|
||||
currentTrack?.status = .idle
|
||||
default:
|
||||
currentTrack?.status = .idle
|
||||
}
|
||||
}
|
||||
|
||||
func errorOccurred(error: AudioStreaming.AudioPlayerError) {
|
||||
|
||||
}
|
||||
|
||||
func metadataReceived(metadata: [String : String]) {
|
||||
guard !metadata.isEmpty else { return }
|
||||
if let title = metadata["StreamTitle"] {
|
||||
liveAudioMetadata = title.isEmpty ? "-" : title
|
||||
} else {
|
||||
liveAudioMetadata = nil
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func audioTracksProvider() -> [AudioPlaylist] {
|
||||
|
||||
@@ -42,7 +42,7 @@ struct AudioPlayerView: View {
|
||||
}
|
||||
}
|
||||
.safeAreaInset(edge: .bottom) {
|
||||
AudioPlayerControls(viewModel: model)
|
||||
AudioPlayerControls(appModel: appModel, currentTrack: $model.currentTrack)
|
||||
.background(
|
||||
.ultraThinMaterial.shadow(
|
||||
ShadowStyle.drop(color: .black.opacity(0.1), radius: 8, x: 0, y: -10)
|
||||
@@ -82,26 +82,35 @@ struct AudioPlayerView: View {
|
||||
}
|
||||
|
||||
struct AudioPlayerControls: View {
|
||||
@Bindable var viewModel: AudioPlayerModel
|
||||
@State var model: Model
|
||||
@Binding var currentTrack: AudioTrack?
|
||||
|
||||
init(appModel: AppModel, currentTrack: Binding<AudioTrack?>) {
|
||||
self._model = State(wrappedValue: Model(audioPlayerService: appModel.audioPlayerService))
|
||||
self._currentTrack = currentTrack
|
||||
}
|
||||
|
||||
var body: some View {
|
||||
VStack(alignment: .leading) {
|
||||
HStack {
|
||||
Button(action: { viewModel.playPause() }) {
|
||||
Image(systemName: viewModel.isPlaying ? "pause.fill" : "play.fill")
|
||||
Button(action: { model.playPause() }) {
|
||||
Image(systemName: model.isPlaying ? "pause.fill" : "play.fill")
|
||||
.font(.title)
|
||||
.imageScale(.small)
|
||||
}
|
||||
.contentTransition(.symbolEffect(.replace))
|
||||
Button(action: { viewModel.stop() }) {
|
||||
Button(action: {
|
||||
model.stop()
|
||||
currentTrack = nil
|
||||
}) {
|
||||
Image(systemName: "stop.fill")
|
||||
.font(.title)
|
||||
.imageScale(.small)
|
||||
}
|
||||
.padding(.leading, 8)
|
||||
Spacer()
|
||||
Button(action: { viewModel.mute() }) {
|
||||
Image(systemName: viewModel.isMuted ? "speaker.slash.fill" : "speaker.fill")
|
||||
Button(action: { model.mute() }) {
|
||||
Image(systemName: model.isMuted ? "speaker.slash.fill" : "speaker.fill")
|
||||
.font(.title)
|
||||
.imageScale(.small)
|
||||
}
|
||||
@@ -110,7 +119,7 @@ struct AudioPlayerControls: View {
|
||||
}
|
||||
.tint(.mint)
|
||||
.padding(16)
|
||||
if let audioMetadata = viewModel.liveAudioMetadata, viewModel.isLiveAudioStreaming {
|
||||
if let audioMetadata = model.liveAudioMetadata, model.isLiveAudioStreaming {
|
||||
Text("Now Playing: \(audioMetadata)")
|
||||
.font(.caption)
|
||||
.foregroundStyle(.black)
|
||||
@@ -119,21 +128,21 @@ struct AudioPlayerControls: View {
|
||||
Divider()
|
||||
VStack {
|
||||
Slider(
|
||||
value: $viewModel.currentTime,
|
||||
in: 0...(viewModel.totalTime ?? 1.0),
|
||||
value: $model.currentTime,
|
||||
in: 0...(model.totalTime ?? 1.0),
|
||||
onEditingChanged: { scrubStarted in
|
||||
if scrubStarted {
|
||||
viewModel.scrubState = .started
|
||||
model.scrubState = .started
|
||||
} else {
|
||||
viewModel.scrubState = .ended(viewModel.currentTime)
|
||||
model.scrubState = .ended(model.currentTime)
|
||||
}
|
||||
}
|
||||
)
|
||||
.disabled(viewModel.totalTime == nil)
|
||||
.disabled(model.totalTime == nil)
|
||||
HStack {
|
||||
Text(viewModel.formattedCurrentTime ?? "--:--")
|
||||
Text(model.formattedCurrentTime ?? "--:--")
|
||||
Spacer()
|
||||
Text(viewModel.formattedTotalTime ?? "")
|
||||
Text(model.formattedTotalTime ?? "")
|
||||
}
|
||||
.foregroundStyle(.black)
|
||||
.font(.caption)
|
||||
@@ -143,21 +152,209 @@ struct AudioPlayerControls: View {
|
||||
.padding(.horizontal, 16)
|
||||
Divider()
|
||||
VStack(alignment: .leading) {
|
||||
Text("Playback Rate: \(String(format: "%0.1f", viewModel.playbackRate))")
|
||||
Text("Playback Rate: \(String(format: "%0.1f", model.playbackRate))")
|
||||
.font(.subheadline)
|
||||
.fontWeight(.medium)
|
||||
.foregroundStyle(.black)
|
||||
Slider(value: $viewModel.playbackRate, in: 1.0...4.0, step: 0.2)
|
||||
.onChange(of: viewModel.playbackRate) { _, new in
|
||||
viewModel.update(rate: Float(new))
|
||||
Slider(value: $model.playbackRate, in: 1.0...4.0, step: 0.2)
|
||||
.onChange(of: model.playbackRate) { _, new in
|
||||
model.update(rate: Float(new))
|
||||
}
|
||||
}
|
||||
.padding(.bottom, 8)
|
||||
.padding(.horizontal, 16)
|
||||
}
|
||||
.onChange(of: currentTrack) { oldValue, newValue in
|
||||
if let track = newValue {
|
||||
model.play(track)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
extension AudioPlayerControls {
|
||||
@Observable
|
||||
final class Model {
|
||||
@ObservationIgnored
|
||||
private(set) var audioPlayerService: AudioPlayerService
|
||||
@ObservationIgnored
|
||||
private var displayLink: DisplayLink?
|
||||
|
||||
var isLiveAudioStreaming: Bool {
|
||||
totalTime == 0
|
||||
}
|
||||
|
||||
var liveAudioMetadata: String?
|
||||
|
||||
var isPlaying: Bool = false
|
||||
var isMuted: Bool = false
|
||||
|
||||
var playbackRate: Double = 0.0
|
||||
|
||||
var currentTime: Double = 0
|
||||
var totalTime: Double?
|
||||
|
||||
var scrubState: ScrubState = .idle
|
||||
|
||||
var formattedCurrentTime: String?
|
||||
var formattedTotalTime: String?
|
||||
|
||||
var currentTrack: AudioTrack?
|
||||
|
||||
init(audioPlayerService: AudioPlayerService) {
|
||||
self.audioPlayerService = audioPlayerService
|
||||
|
||||
registerObservations()
|
||||
}
|
||||
|
||||
deinit {
|
||||
displayLink?.deactivate()
|
||||
displayLink = nil
|
||||
}
|
||||
|
||||
func registerObservations() {
|
||||
Task { @MainActor in
|
||||
for await status in await audioPlayerService.statusChangedNotifier.values() {
|
||||
isPlaying = status == .playing
|
||||
displayLink?.isPaused = !isPlaying
|
||||
switch status {
|
||||
case .bufferring:
|
||||
currentTrack?.status = .buffering
|
||||
case .error:
|
||||
currentTrack?.status = .error
|
||||
currentTrack = nil
|
||||
case .playing:
|
||||
currentTrack?.status = .playing
|
||||
case .paused:
|
||||
currentTrack?.status = .paused
|
||||
case .stopped:
|
||||
currentTrack?.status = .idle
|
||||
default:
|
||||
currentTrack?.status = .idle
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Task { @MainActor in
|
||||
for await metadata in await audioPlayerService.metadataReceivedNotifier.values() {
|
||||
guard !metadata.isEmpty else { break }
|
||||
if let title = metadata["StreamTitle"] {
|
||||
liveAudioMetadata = title.isEmpty ? "-" : title
|
||||
} else {
|
||||
liveAudioMetadata = nil
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Task { @MainActor in
|
||||
for await startStopped in await audioPlayerService.playingStartedStopped.values() {
|
||||
if startStopped.started {
|
||||
self.didStartPlaying()
|
||||
} else {
|
||||
self.didStopPlaying()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func mute() {
|
||||
isMuted.toggle()
|
||||
audioPlayerService.toggleMute()
|
||||
}
|
||||
|
||||
func playPause() {
|
||||
if audioPlayerService.state == .playing {
|
||||
audioPlayerService.pause()
|
||||
} else {
|
||||
audioPlayerService.resume()
|
||||
}
|
||||
}
|
||||
|
||||
func update(rate: Float) {
|
||||
let rate = round(rate / 0.2) * 0.2
|
||||
audioPlayerService.update(rate: rate)
|
||||
}
|
||||
|
||||
func stop() {
|
||||
isPlaying = false
|
||||
audioPlayerService.stop()
|
||||
currentTrack?.status = .idle
|
||||
currentTrack = nil
|
||||
}
|
||||
|
||||
func play(_ track: AudioTrack) {
|
||||
if track != currentTrack {
|
||||
currentTrack?.status = .idle
|
||||
audioPlayerService.play(url: track.url)
|
||||
currentTrack = track
|
||||
}
|
||||
}
|
||||
|
||||
func onTick() {
|
||||
let duration = audioPlayerService.duration
|
||||
let progress = audioPlayerService.progress
|
||||
if duration > 0 {
|
||||
let elapsed = Int(progress)
|
||||
let remaining = Int(duration - progress)
|
||||
totalTime = duration
|
||||
switch scrubState {
|
||||
case .idle:
|
||||
currentTime = progress
|
||||
case .started:
|
||||
break
|
||||
case .ended(let seekTime):
|
||||
currentTime = seekTime
|
||||
if audioPlayerService.duration > 0 {
|
||||
audioPlayerService.seek(at: seekTime)
|
||||
}
|
||||
scrubState = .idle
|
||||
}
|
||||
formattedCurrentTime = timeFrom(seconds: Int(elapsed))
|
||||
formattedTotalTime = timeFrom(seconds: remaining)
|
||||
} else {
|
||||
let elapsed = Int(progress)
|
||||
formattedCurrentTime = timeFrom(seconds: Int(elapsed))
|
||||
if formattedTotalTime != nil {
|
||||
formattedTotalTime = nil
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func resetLabels() {
|
||||
currentTime = 0
|
||||
totalTime = 0
|
||||
formattedCurrentTime = nil
|
||||
formattedTotalTime = nil
|
||||
}
|
||||
|
||||
private func timeFrom(seconds: Int) -> String {
|
||||
let correctSeconds = seconds % 60
|
||||
let minutes = (seconds / 60) % 60
|
||||
let hours = seconds / 3600
|
||||
|
||||
if hours > 0 {
|
||||
return String(format: "%02d:%02d:%02d", hours, minutes, correctSeconds)
|
||||
}
|
||||
return String(format: "%02d:%02d", minutes, correctSeconds)
|
||||
}
|
||||
|
||||
private func didStartPlaying() {
|
||||
self.displayLink = DisplayLink(onTick: { [weak self] _ in
|
||||
self?.onTick()
|
||||
})
|
||||
displayLink?.activate()
|
||||
}
|
||||
|
||||
private func didStopPlaying() {
|
||||
resetLabels()
|
||||
liveAudioMetadata = nil
|
||||
playbackRate = 1.0
|
||||
displayLink?.deactivate()
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
#Preview {
|
||||
AudioPlayerView(appModel: AppModel())
|
||||
}
|
||||
|
||||
@@ -127,7 +127,6 @@ struct EQSliderView: View {
|
||||
}
|
||||
GeometryReader { innerGeo in
|
||||
ZStack {
|
||||
|
||||
LineShape(values: eqModel.shouldReset ? resetPoints : dragPointYLocations.map { Double($0) })
|
||||
.stroke(Color.mint, lineWidth: 2)
|
||||
.animation(.easeInOut(duration: 0.2), value: eqModel.shouldReset)
|
||||
|
||||
@@ -4,3 +4,16 @@
|
||||
//
|
||||
|
||||
import Foundation
|
||||
|
||||
@Observable
|
||||
class AudioQueueModel {
|
||||
@ObservationIgnored
|
||||
private(set) var audioPlayerService: AudioPlayerService
|
||||
@ObservationIgnored
|
||||
private var displayLink: DisplayLink?
|
||||
|
||||
|
||||
init(audioPlayerService: AudioPlayerService) {
|
||||
self.audioPlayerService = audioPlayerService
|
||||
}
|
||||
}
|
||||
|
||||
@@ -6,11 +6,22 @@
|
||||
import SwiftUI
|
||||
|
||||
struct AudioQueueView: View {
|
||||
@Environment(AppModel.self) var appModel
|
||||
|
||||
@State var model: AudioPlayerModel
|
||||
|
||||
@State var eqSheetIsShown: Bool = false
|
||||
@State var addNewAudioIsShown: Bool = false
|
||||
|
||||
init(appModel: AppModel) {
|
||||
self._model = State(wrappedValue: AudioPlayerModel(audioPlayerService: appModel.audioPlayerService))
|
||||
}
|
||||
|
||||
var body: some View {
|
||||
Text(/*@START_MENU_TOKEN@*/"Hello, World!"/*@END_MENU_TOKEN@*/)
|
||||
return Text("")
|
||||
}
|
||||
}
|
||||
|
||||
#Preview {
|
||||
AudioQueueView()
|
||||
AudioQueueView(appModel: AppModel())
|
||||
}
|
||||
|
||||
+8
@@ -40,6 +40,10 @@ final class AudioPlayerService {
|
||||
player.state
|
||||
}
|
||||
|
||||
var statusChangedNotifier = Notifier<AudioPlayerState>()
|
||||
var metadataReceivedNotifier = Notifier<[String: String]>()
|
||||
var playingStartedStopped = Notifier<(started: Bool, AudioEntryId, AudioPlayerStopReason?)>()
|
||||
|
||||
init(audioPlayer: AudioPlayer) {
|
||||
player = audioPlayer
|
||||
player.delegate = self
|
||||
@@ -150,12 +154,14 @@ extension AudioPlayerService: AudioPlayerDelegate {
|
||||
func audioPlayerDidStartPlaying(player _: AudioPlayer, with id: AudioEntryId) {
|
||||
print("audioPlayerDidStartPlaying entryId: \(id)")
|
||||
delegate?.didStartPlaying(id: id)
|
||||
Task { await playingStartedStopped.send((true, id, nil)) }
|
||||
}
|
||||
|
||||
func audioPlayerDidFinishBuffering(player _: AudioPlayer, with _: AudioEntryId) {}
|
||||
|
||||
func audioPlayerStateChanged(player _: AudioPlayer, with newState: AudioPlayerState, previous _: AudioPlayerState) {
|
||||
print("audioPlayerDidStartPlaying newState: \(newState)")
|
||||
Task { await statusChangedNotifier.send(newState) }
|
||||
delegate?.statusChanged(status: newState)
|
||||
}
|
||||
|
||||
@@ -166,6 +172,7 @@ extension AudioPlayerService: AudioPlayerDelegate {
|
||||
duration _: Double)
|
||||
{
|
||||
print("audioPlayerDidFinishPlaying entryId: \(id), reason: \(reason)")
|
||||
Task { await playingStartedStopped.send((false, id, reason)) }
|
||||
delegate?.didStopPlaying(id: id, reason: reason)
|
||||
}
|
||||
|
||||
@@ -176,6 +183,7 @@ extension AudioPlayerService: AudioPlayerDelegate {
|
||||
func audioPlayerDidCancel(player _: AudioPlayer, queuedItems _: [AudioEntryId]) {}
|
||||
|
||||
func audioPlayerDidReadMetadata(player _: AudioPlayer, metadata: [String: String]) {
|
||||
Task { await metadataReceivedNotifier.send(metadata) }
|
||||
delegate?.metadataReceived(metadata: metadata)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,29 @@
|
||||
//
|
||||
// Created by Dimitris Chatzieleftheriou on 25/04/2024.
|
||||
//
|
||||
|
||||
import Foundation
|
||||
|
||||
actor Notifier<Output> {
|
||||
private var continuations: [UUID: AsyncStream<Output>.Continuation] = [:]
|
||||
|
||||
func values(bufferingPolicy limit: AsyncStream<Output>.Continuation.BufferingPolicy = .bufferingNewest(1)) -> AsyncStream<Output> {
|
||||
AsyncStream<Output>(bufferingPolicy: limit) { continuation in
|
||||
let id = UUID()
|
||||
continuations[id] = continuation
|
||||
continuation.onTermination = { _ in
|
||||
Task { await self.cancel(id) }
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func send(_ value: Output) {
|
||||
for continuation in continuations.values {
|
||||
continuation.yield(value)
|
||||
}
|
||||
}
|
||||
|
||||
private func cancel(_ id: UUID) {
|
||||
continuations[id] = nil
|
||||
}
|
||||
}
|
||||
@@ -16,7 +16,7 @@ struct DetailView: View {
|
||||
case .audioPlayer:
|
||||
AudioPlayerView(appModel: appModel)
|
||||
case .audioQueue:
|
||||
Text("Audio Queue")
|
||||
AudioQueueView(appModel: appModel)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user