From 70a4dfd698996fda6fa32c6b4d94d5a0d30dedd6 Mon Sep 17 00:00:00 2001 From: dimitris-c Date: Thu, 18 Apr 2024 19:26:58 +0300 Subject: [PATCH] progress --- .../AudioPlayer.xcodeproj/project.pbxproj | 12 + AudioPlayer/AudioPlayer/App.swift | 23 ++ .../AudioPlayer/Common/AudioContent.swift | 26 +- .../AudioPlayer/Common/AudioTrack.swift | 1 + .../Content/AddNewAudioURLView.swift | 78 ++++++ .../Content/AudioPlayerModel.swift | 28 +- .../AudioPlayer/Content/AudioPlayerView.swift | 79 ++++-- .../AudioPlayer/Content/EqualizerView.swift | 239 ++++++++++++++++++ .../AudioPlayer/EqualizerService.swift | 48 ++++ .../AudioPlayer/Navigation/ContentView.swift | 4 +- .../AudioPlayer/Navigation/DetailView.swift | 4 +- 11 files changed, 512 insertions(+), 30 deletions(-) create mode 100644 AudioPlayer/AudioPlayer/Content/AddNewAudioURLView.swift create mode 100644 AudioPlayer/AudioPlayer/Content/EqualizerView.swift create mode 100644 AudioPlayer/AudioPlayer/EqualizerService.swift diff --git a/AudioPlayer/AudioPlayer.xcodeproj/project.pbxproj b/AudioPlayer/AudioPlayer.xcodeproj/project.pbxproj index a0a8efb..432320a 100644 --- a/AudioPlayer/AudioPlayer.xcodeproj/project.pbxproj +++ b/AudioPlayer/AudioPlayer.xcodeproj/project.pbxproj @@ -23,6 +23,9 @@ 9816A8B32BC8330C00AD1299 /* hipjazz.wav in Resources */ = {isa = PBXBuildFile; fileRef = 9816A8AF2BC832DC00AD1299 /* hipjazz.wav */; }; 9816A8BB2BC87BC200AD1299 /* AudioPlayerService.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9816A8BA2BC87BC200AD1299 /* AudioPlayerService.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 */; }; + 98BFB4232BCE78AB00E812C0 /* AddNewAudioURLView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 98BFB4222BCE78AB00E812C0 /* AddNewAudioURLView.swift */; }; 98E6119C2BC72C0E0036BC47 /* DetailView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 98E6119B2BC72C0E0036BC47 /* DetailView.swift */; }; /* End PBXBuildFile section */ @@ -58,6 +61,9 @@ 9816A8BA2BC87BC200AD1299 /* AudioPlayerService.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AudioPlayerService.swift; sourceTree = ""; }; 98BFB4192BC97AF800E812C0 /* DisplayLink.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DisplayLink.swift; sourceTree = ""; }; 98BFB41B2BCAAD8A00E812C0 /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist; path = Info.plist; sourceTree = ""; }; + 98BFB41C2BCD7BB800E812C0 /* EqualizerView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = EqualizerView.swift; sourceTree = ""; }; + 98BFB41E2BCD814000E812C0 /* EqualizerService.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = EqualizerService.swift; sourceTree = ""; }; + 98BFB4222BCE78AB00E812C0 /* AddNewAudioURLView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AddNewAudioURLView.swift; sourceTree = ""; }; 98E6119B2BC72C0E0036BC47 /* DetailView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DetailView.swift; sourceTree = ""; }; /* End PBXFileReference section */ @@ -94,6 +100,7 @@ isa = PBXGroup; children = ( 98BFB41B2BCAAD8A00E812C0 /* Info.plist */, + 98BFB41E2BCD814000E812C0 /* EqualizerService.swift */, 9816A8BA2BC87BC200AD1299 /* AudioPlayerService.swift */, 98BFB4192BC97AF800E812C0 /* DisplayLink.swift */, 9816A8A82BC7F4DE00AD1299 /* Common */, @@ -130,6 +137,8 @@ children = ( 9806E8302BC6927D00757370 /* AudioPlayerModel.swift */, 9806E8292BC68F8700757370 /* AudioPlayerView.swift */, + 98BFB41C2BCD7BB800E812C0 /* EqualizerView.swift */, + 98BFB4222BCE78AB00E812C0 /* AddNewAudioURLView.swift */, ); path = Content; sourceTree = ""; @@ -235,6 +244,8 @@ isa = PBXSourcesBuildPhase; buildActionMask = 2147483647; files = ( + 98BFB4232BCE78AB00E812C0 /* AddNewAudioURLView.swift in Sources */, + 98BFB41D2BCD7BB800E812C0 /* EqualizerView.swift in Sources */, 98BFB41A2BC97AF800E812C0 /* DisplayLink.swift in Sources */, 9806E81A2BC5D12500757370 /* ContentView.swift in Sources */, 98E6119C2BC72C0E0036BC47 /* DetailView.swift in Sources */, @@ -242,6 +253,7 @@ 9806E8262BC5D2A900757370 /* Sidebar.swift in Sources */, 9806E82A2BC68F8700757370 /* AudioPlayerView.swift in Sources */, 9806E8312BC6927D00757370 /* AudioPlayerModel.swift in Sources */, + 98BFB41F2BCD814000E812C0 /* EqualizerService.swift in Sources */, 9816A8AA2BC7F4F000AD1299 /* AudioTrack.swift in Sources */, 9816A8BB2BC87BC200AD1299 /* AudioPlayerService.swift in Sources */, 9806E8182BC5D12500757370 /* App.swift in Sources */, diff --git a/AudioPlayer/AudioPlayer/App.swift b/AudioPlayer/AudioPlayer/App.swift index 1f6ca3b..f5e5739 100644 --- a/AudioPlayer/AudioPlayer/App.swift +++ b/AudioPlayer/AudioPlayer/App.swift @@ -9,13 +9,36 @@ import SwiftUI @main struct AudioPlayerApp: App { + @State var model = AppModel() + var body: some Scene { WindowGroup { ContentView() + .environment(model) } } } +@Observable +class AppModel { + @ObservationIgnored + let audioPlayerService: AudioPlayerService + @ObservationIgnored + let equalizerService: EqualizerService + + init( + audioPlayerService: AudioPlayerService = provideAudioPlayerService(), + equalizerService: (AudioPlayerService) -> EqualizerService = provideEqualizerService + ) { + self.audioPlayerService = audioPlayerService + self.equalizerService = equalizerService(audioPlayerService) + } +} + +func provideEqualizerService(playerService: AudioPlayerService) -> EqualizerService { + EqualizerService(playerService: playerService) +} + func provideAudioPlayerService() -> AudioPlayerService { AudioPlayerService( audioPlayer: provideDefaultAudioPlayer() diff --git a/AudioPlayer/AudioPlayer/Common/AudioContent.swift b/AudioPlayer/AudioPlayer/Common/AudioContent.swift index 500b60d..1e365dd 100644 --- a/AudioPlayer/AudioPlayer/Common/AudioContent.swift +++ b/AudioPlayer/AudioPlayer/Common/AudioContent.swift @@ -7,11 +7,12 @@ import Foundation -enum AudioContent: Int, CaseIterable { +enum AudioContent { case offradio case enlefko case pepper966 case kosmos + case kosmosJazz case radiox case khruangbin case piano @@ -20,6 +21,7 @@ enum AudioContent: Int, CaseIterable { case remoteWave case local case localWave + case custom(String) var title: String { switch self { @@ -31,6 +33,8 @@ enum AudioContent: Int, CaseIterable { return "Pepper 96.6" case .kosmos: return "Kosmos 93.6" + case .kosmosJazz: + return "Kosmos Jazz" case .radiox: return "Radio X" case .khruangbin: @@ -47,21 +51,25 @@ enum AudioContent: Int, CaseIterable { return "Jazzy Frenchy" case .nonOptimized: return "Jazzy Frenchy" + case .custom(let url): + return url } } var subtitle: String? { switch self { case .offradio: - return "Stream" + return "Stream • offradio.gr" case .enlefko: - return "Stream" + return "Stream • enlefko.fm" case .pepper966: - return "Stream" + return "Stream • pepper966.gr" case .kosmos: - return "Stream" + return "Stream • ertecho.gr" + case .kosmosJazz: + return "Stream • ertecho.gr" case .radiox: - return "Stream" + return "Stream • globalplayer.com" case .khruangbin: return "Remote mp3" case .piano: @@ -76,6 +84,8 @@ enum AudioContent: Int, CaseIterable { return "Music by: bensound.com - m4a optimized" case .nonOptimized: return "Music by: bensound.com - m4a non-optimized" + case .custom: + return "" } } @@ -89,6 +99,8 @@ enum AudioContent: Int, CaseIterable { return URL(string: "https://n04.radiojar.com/pepper.m4a?1662039818=&rj-tok=AAABgvlUaioALhdOXDt0mgajoA&rj-ttl=5")! case .kosmos: return URL(string: "https://radiostreaming.ert.gr/ert-kosmos")! + case .kosmosJazz: + return URL(string: "https://radiostreaming.ert.gr/ert-webjazz")! case .radiox: return URL(string: "https://media-ssl.musicradio.com/RadioXLondon")! case .khruangbin: @@ -107,6 +119,8 @@ enum AudioContent: Int, CaseIterable { return URL(fileURLWithPath: path) case .remoteWave: return URL(string: "https://file-examples.com/wp-content/storage/2017/11/file_example_WAV_5MG.wav")! + case .custom(let url): + return URL(string: url)! } } } diff --git a/AudioPlayer/AudioPlayer/Common/AudioTrack.swift b/AudioPlayer/AudioPlayer/Common/AudioTrack.swift index 58d06d9..3a98c14 100644 --- a/AudioPlayer/AudioPlayer/Common/AudioTrack.swift +++ b/AudioPlayer/AudioPlayer/Common/AudioTrack.swift @@ -64,6 +64,7 @@ struct AudioTrackView: View { Text(track.title) .font(.headline) .fontWeight(.medium) + .foregroundStyle(.black) .padding(.top, 8) .padding(.bottom, track.subtitle == nil ? 8 : 0) if let subtitle = track.subtitle { diff --git a/AudioPlayer/AudioPlayer/Content/AddNewAudioURLView.swift b/AudioPlayer/AudioPlayer/Content/AddNewAudioURLView.swift new file mode 100644 index 0000000..23d3a0d --- /dev/null +++ b/AudioPlayer/AudioPlayer/Content/AddNewAudioURLView.swift @@ -0,0 +1,78 @@ +// +// AddNewAudioURLView.swift +// AudioPlayer +// +// Created by Dimitris Chatzieleftheriou on 16/04/2024. +// + +import SwiftUI + +struct AddNewAudioURLView: View { + @Environment(\.dismiss) var dismiss + + private let urlStyle = URL.FormatStyle(path: .omitWhen(.path, matches: ["/"]), query: .omitWhen(.query, matches: [""])) + + @State private var audioUrl: URL? + + var onAddNewUrl: (URL) -> Void + + init(onAddNewUrl: @escaping (URL) -> Void) { + self.onAddNewUrl = onAddNewUrl + } + + var body: some View { + NavigationStack { + VStack { + VStack(alignment: .leading) { + TextField(value: $audioUrl, format: urlStyle, prompt: nil, label: { + Text("Insert URL") + }) + .keyboardType(.URL) + .autocorrectionDisabled() + .textFieldStyle(RoundedBorderTextFieldStyle()) + .onSubmit { + if let url = audioUrl { + onAddNewUrl(url) + dismiss() + } + } + } + .padding(.horizontal, 16) + Button { + if let url = audioUrl { + onAddNewUrl(url) + dismiss() + } + } label: { + HStack { + Image(systemName: "plus") + Text("Add") + } + .foregroundStyle(Color.white) + } + .disabled(audioUrl == nil) + .opacity(audioUrl == nil ? 0.5 : 1.0) + .padding(.horizontal, 16) + .padding(.vertical, 8) + .background(.mint) + .clipShape(RoundedRectangle(cornerRadius: 16)) + } + .navigationTitle("Add Audio URL") + .navigationBarTitleDisplayMode(.inline) + .toolbar { + ToolbarItem(placement: .topBarTrailing) { + Button { + dismiss() + } label: { + Image(systemName: "xmark.circle.fill") + .foregroundStyle(Color.gray) + } + } + } + } + } +} + +#Preview { + AddNewAudioURLView(onAddNewUrl: { _ in }) +} diff --git a/AudioPlayer/AudioPlayer/Content/AudioPlayerModel.swift b/AudioPlayer/AudioPlayer/Content/AudioPlayerModel.swift index 151465d..ee5880f 100644 --- a/AudioPlayer/AudioPlayer/Content/AudioPlayerModel.swift +++ b/AudioPlayer/AudioPlayer/Content/AudioPlayerModel.swift @@ -7,10 +7,10 @@ import UIKit import Foundation import AudioStreaming -struct AudioPlaylist: Identifiable { +struct AudioPlaylist: Equatable, Identifiable { var id: String { title } let title: String - let tracks: [AudioTrack] + var tracks: [AudioTrack] } enum ScrubState: Equatable { @@ -22,7 +22,7 @@ enum ScrubState: Equatable { @Observable public class AudioPlayerModel { @ObservationIgnored - var audioPlayerService: AudioPlayerService + private(set) var audioPlayerService: AudioPlayerService @ObservationIgnored private var displayLink: DisplayLink? @@ -49,8 +49,8 @@ public class AudioPlayerModel { var currentTrack: AudioTrack? - init(audioTracksProvider: () -> [AudioPlaylist] = audioTracksProvider, audioPlayerService: () -> AudioPlayerService = provideAudioPlayerService) { - self.audioPlayerService = audioPlayerService() + init(audioTracksProvider: () -> [AudioPlaylist] = audioTracksProvider, audioPlayerService: AudioPlayerService) { + self.audioPlayerService = audioPlayerService self.audioTracks = audioTracksProvider() self.audioPlayerService.delegate = self @@ -62,6 +62,20 @@ public class AudioPlayerModel { displayLink = nil } + func addNewAudioTrack(url: URL) { + let customIndex = audioTracks.firstIndex(where: { $0.id == "Custom" }) + let audioTrack = AudioTrack(from: .custom(url.absoluteString), status: .idle) + let playlist = AudioPlaylist(title: "Custom", tracks: [audioTrack]) + if let customIndex { + let tracks = audioTracks[customIndex].tracks + if !tracks.contains(audioTrack) { + audioTracks[customIndex].tracks.append(audioTrack) + } + } else { + audioTracks.append(playlist) + } + } + func mute() { isMuted.toggle() audioPlayerService.toggleMute() @@ -187,7 +201,7 @@ extension AudioPlayerModel: AudioPlayerServiceDelegate { func metadataReceived(metadata: [String : String]) { guard !metadata.isEmpty else { return } if let title = metadata["StreamTitle"] { - liveAudioMetadata = title + liveAudioMetadata = title.isEmpty ? "-" : title } else { liveAudioMetadata = nil } @@ -195,7 +209,7 @@ extension AudioPlayerModel: AudioPlayerServiceDelegate { } func audioTracksProvider() -> [AudioPlaylist] { - let radioTracks: [AudioContent] = [.offradio, .enlefko, .pepper966, .kosmos, .radiox] + let radioTracks: [AudioContent] = [.offradio, .enlefko, .pepper966, .kosmos, .kosmosJazz, .radiox] let audioTracks: [AudioContent] = [.khruangbin, .piano, .optimized, .nonOptimized, .remoteWave, .local, .localWave] return [ diff --git a/AudioPlayer/AudioPlayer/Content/AudioPlayerView.swift b/AudioPlayer/AudioPlayer/Content/AudioPlayerView.swift index 57827e8..f48c82b 100644 --- a/AudioPlayer/AudioPlayer/Content/AudioPlayerView.swift +++ b/AudioPlayer/AudioPlayer/Content/AudioPlayerView.swift @@ -6,29 +6,78 @@ import SwiftUI struct AudioPlayerView: View { + @Environment(AppModel.self) var appModel - @State var model = AudioPlayerModel() + @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 { - List { - ForEach(model.audioTracks) { section in - Section { - ForEach(section.tracks) { track in - AudioTrackView(track: track) { - model.play(track) + ScrollViewReader { proxy in + List { + ForEach(model.audioTracks) { section in + Section { + ForEach(section.tracks) { track in + AudioTrackView(track: track) { + model.play(track) + } + .id(track.id) } + } header: { + Text(section.title) + } + } + } + .onChange(of: model.audioTracks) { _, newValue in + if let lastId = newValue.last?.tracks.last?.id { + withAnimation { + proxy.scrollTo(lastId, anchor: .bottom) } - } header: { - Text(section.title) } } } .safeAreaInset(edge: .bottom) { AudioPlayerControls(viewModel: model) - .background(Color.mint) + .background( + .ultraThinMaterial.shadow( + ShadowStyle.drop(color: .black.opacity(0.1), radius: 8, x: 0, y: -10) + ) + ) } + .ignoresSafeArea(.keyboard, edges: .bottom) .navigationTitle("Audio Player") .navigationBarTitleDisplayMode(.inline) + .toolbar { + ToolbarItemGroup(placement: .topBarTrailing) { + Button { + eqSheetIsShown.toggle() + } label: { + Image(systemName: "slider.horizontal.3") + } + Button { + addNewAudioIsShown.toggle() + } label: { + Image(systemName: "plus") + } + } + } + .sheet(isPresented: $eqSheetIsShown) { + EqualizerView(appModel: appModel) + .presentationDetents([.medium]) + } + .sheet(isPresented: $addNewAudioIsShown) { + AddNewAudioURLView( + onAddNewUrl: { url in + model.addNewAudioTrack(url: url) + } + ) + .presentationDetents([.height(150)]) + } } } @@ -59,12 +108,12 @@ struct AudioPlayerControls: View { .frame(width: 20, height: 20) .contentTransition(.symbolEffect(.replace)) } - .tint(.white) + .tint(.mint) .padding(16) if let audioMetadata = viewModel.liveAudioMetadata, viewModel.isLiveAudioStreaming { Text("Now Playing: \(audioMetadata)") .font(.caption) - .foregroundStyle(.white) + .foregroundStyle(.black) .padding(.horizontal, 16) } Divider() @@ -86,7 +135,7 @@ struct AudioPlayerControls: View { Spacer() Text(viewModel.formattedTotalTime ?? "") } - .foregroundStyle(.white) + .foregroundStyle(.black) .font(.caption) .fontWeight(.medium) } @@ -97,7 +146,7 @@ struct AudioPlayerControls: View { Text("Playback Rate: \(String(format: "%0.1f", viewModel.playbackRate))") .font(.subheadline) .fontWeight(.medium) - .foregroundStyle(.white) + .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)) @@ -110,5 +159,5 @@ struct AudioPlayerControls: View { } #Preview { - AudioPlayerView(model: AudioPlayerModel()) + AudioPlayerView(appModel: AppModel()) } diff --git a/AudioPlayer/AudioPlayer/Content/EqualizerView.swift b/AudioPlayer/AudioPlayer/Content/EqualizerView.swift new file mode 100644 index 0000000..997b62d --- /dev/null +++ b/AudioPlayer/AudioPlayer/Content/EqualizerView.swift @@ -0,0 +1,239 @@ +// +// Created by Dimitris Chatzieleftheriou on 15/04/2024. +// + +import SwiftUI + +@Observable +class EQBand: Identifiable { + var frequency: String + var min: Float + var max: Float + var value: Float + + @ObservationIgnored + let index: Int + + init(index: Int, frequency: String, min: Float, max: Float, value: Float) { + self.index = index + self.frequency = frequency + self.min = min + self.max = max + self.value = value + } +} + +struct EqualizerView: View { + @Environment(\.dismiss) var dismiss + + @Environment(AppModel.self) var appModel + + @State var model: Model + + init(appModel: AppModel) { + self._model = State(wrappedValue: Model(equalizerService: appModel.equalizerService)) + } + + var body: some View { + NavigationStack { + VStack { + Toggle(isOn: $model.isEnabled) { + Text("Enable") + } + .onChange(of: model.isEnabled) { _, _ in + model.enable() + } + .padding(.horizontal, 16) + VStack { + EQSliderView() + .frame(height: 180) + .padding(.horizontal, 16) + .environment(model) + Button { + withAnimation { + model.reset() + } + } label: { + HStack { + Text("Reset") + } + .foregroundStyle(Color.white) + } + .padding(.horizontal, 16) + .padding(.vertical, 8) + .background(.mint) + .clipShape(RoundedRectangle(cornerRadius: 16)) + } + } + .task { + Task { + model.generateBands() + } + } + .navigationTitle("Equalizer") + .navigationBarTitleDisplayMode(.inline) + .toolbar { + ToolbarItem(placement: .topBarTrailing) { + Button { + dismiss() + } label: { + Image(systemName: "xmark.circle.fill") + .foregroundStyle(Color.gray) + } + } + } + } + } +} + +struct EQSliderView: View { + @Environment(EqualizerView.Model.self) var eqModel + + @State private var dragPointYLocations: [CGFloat] = Array(repeating: .zero, count: 6) + + @State private var eqViewFrame: CGRect = .zero + + var body: some View { + VStack(spacing: 0) { + HStack(spacing: 0) { + // Draw labels for gain values + VStack { + Text("\(Int(eqModel.maxGain))db") + .font(.caption2) + .foregroundColor(.black) + Spacer() + Text("0dB") + .font(.caption2) + .foregroundColor(.black) + Spacer() + Text("\(Int(eqModel.minGain))db") + .font(.caption2) + .foregroundColor(.black) + } + GeometryReader { innerGeo in + ZStack { + Path { path in + path.move(to: CGPoint(x: innerGeo.size.width / 12, y: eqModel.dragPoints.first ?? 0)) + for index in 1.. CGFloat { + size.width / 12 * CGFloat(index * 2 + 1) + } + + func updateGainValue(at index: Int, in size: CGSize) { + let percentage = dragPointYLocations[index] / size.height + let gain = (1 - Float(percentage)) * (eqModel.maxGain - eqModel.minGain) + eqModel.minGain + eqModel.update(gain: gain, index: index) + } + + func gainToYPosition(at gain: Float, in size: CGSize) -> CGFloat { + let percentage = 1 - (gain - eqModel.minGain) / (eqModel.maxGain - eqModel.minGain) + return CGFloat(percentage) * size.height + } +} + +extension EqualizerView { + @Observable + class Model { + @ObservationIgnored + private let equalizerService: EqualizerService + + var isEnabled: Bool = false + + var bands: [EQBand] = [] + + var dragPoints: [CGFloat] = [] + + let minGain: Float = -12 + let maxGain: Float = 12 + + init(equalizerService: EqualizerService) { + self.equalizerService = equalizerService + dragPoints = [CGFloat].init(repeating: 0, count: equalizerService.bands.count) + } + + func generateBands() { + bands = equalizerService.bands.enumerated().map { index, item in + var measurement = item.frequency + var frequency = String(Int(measurement)) + if item.frequency >= 1000 { + measurement = item.frequency / 1000 + frequency = "\(String(Int(measurement)))K" + } + return EQBand(index: index, frequency: frequency, min: minGain, max: maxGain, value: item.gain) + } + + dragPoints = [CGFloat].init(repeating: 0, count: bands.count) + } + + func enable() { + if isEnabled { + equalizerService.activate() + } else { + equalizerService.deactivate() + } + } + + func update(gain: Float, index: Int) { + equalizerService.update(gain: gain, for: index) + } + + func reset() { + equalizerService.reset() + dragPoints = [CGFloat].init(repeating: 0, count: bands.count) + } + } +} + +#Preview { + EqualizerView(appModel: AppModel()) +} diff --git a/AudioPlayer/AudioPlayer/EqualizerService.swift b/AudioPlayer/AudioPlayer/EqualizerService.swift new file mode 100644 index 0000000..0398328 --- /dev/null +++ b/AudioPlayer/AudioPlayer/EqualizerService.swift @@ -0,0 +1,48 @@ +// +// Created by Dimitris Chatzieleftheriou on 15/04/2024. +// + +import AVFoundation + +final class EqualizerService { + private let playerService: AudioPlayerService + private let _freqs = [60, 150, 400, 1000, 2400, 15000] + private let eqUnit: AVAudioUnitEQ + + var bands: [AVAudioUnitEQFilterParameters] { + eqUnit.bands + } + + private(set) var isActivated: Bool = false + + init(playerService: AudioPlayerService) { + self.playerService = playerService + + eqUnit = AVAudioUnitEQ(numberOfBands: _freqs.count) + for i in 0 ..< _freqs.count { + eqUnit.bands[i].bypass = false + eqUnit.bands[i].filterType = .parametric + eqUnit.bands[i].frequency = Float(_freqs[i]) + eqUnit.bands[i].bandwidth = 0.5 + eqUnit.bands[i].gain = 0 + } + } + + func update(gain: Float, for index: Int) { + eqUnit.bands[index].gain = gain + } + + func reset() { + eqUnit.bands.forEach { $0.gain = 0 } + } + + func activate() { + isActivated = true + playerService.add(eqUnit) + } + + func deactivate() { + isActivated = false + playerService.remove(eqUnit) + } +} diff --git a/AudioPlayer/AudioPlayer/Navigation/ContentView.swift b/AudioPlayer/AudioPlayer/Navigation/ContentView.swift index e3a6df5..80269f8 100644 --- a/AudioPlayer/AudioPlayer/Navigation/ContentView.swift +++ b/AudioPlayer/AudioPlayer/Navigation/ContentView.swift @@ -6,7 +6,9 @@ import SwiftUI struct ContentView: View { - + + @Environment(AppModel.self) var appModel + @State private var selection: NavigationContent? var body: some View { diff --git a/AudioPlayer/AudioPlayer/Navigation/DetailView.swift b/AudioPlayer/AudioPlayer/Navigation/DetailView.swift index dd254cf..0cb4790 100644 --- a/AudioPlayer/AudioPlayer/Navigation/DetailView.swift +++ b/AudioPlayer/AudioPlayer/Navigation/DetailView.swift @@ -8,12 +8,14 @@ import SwiftUI struct DetailView: View { + @Environment(AppModel.self) var appModel + var selection: NavigationContent var body: some View { switch selection { case .audioPlayer: - AudioPlayerView() + AudioPlayerView(appModel: appModel) case .audioQueue: Text("Audio Queue") }