Skip to content

Commit a663621

Browse files
authored
feature(AudioPlayer): Adds loop mode (#123)
* Adds loop mode * Removes commented code
1 parent 45178a9 commit a663621

14 files changed

Lines changed: 347 additions & 19 deletions

File tree

AudioPlayer/AudioPlayer.xcodeproj/xcshareddata/xcschemes/AudioPlayer.xcscheme

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -51,6 +51,13 @@
5151
ReferencedContainer = "container:AudioPlayer.xcodeproj">
5252
</BuildableReference>
5353
</BuildableProductRunnable>
54+
<EnvironmentVariables>
55+
<EnvironmentVariable
56+
key = "OS_ACTIVITY_MODE"
57+
value = "disable"
58+
isEnabled = "NO">
59+
</EnvironmentVariable>
60+
</EnvironmentVariables>
5461
</LaunchAction>
5562
<ProfileAction
5663
buildConfiguration = "Release"

AudioPlayer/AudioPlayer/Content/AudioPlayer/AudioPlayerControlsView.swift

Lines changed: 99 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -37,6 +37,18 @@ struct AudioPlayerControls: View {
3737
}
3838
.buttonStyle(.plain)
3939
.padding(.leading, 8)
40+
Button(action: {
41+
model.cycleLoopMode()
42+
}) {
43+
Image(systemName: model.iconForLoopMode)
44+
.symbolVariant(model.isLoopActive ? .circle.fill : .none)
45+
.font(.title)
46+
.imageScale(.small)
47+
.foregroundStyle(model.isLoopActive ? .mint : .gray)
48+
}
49+
.buttonStyle(.plain)
50+
.padding(.leading, 8)
51+
.help(model.loopModeDescription)
4052
Spacer()
4153
HStack {
4254
Slider(value: $model.volume)
@@ -101,6 +113,31 @@ struct AudioPlayerControls: View {
101113
}
102114
.padding(.bottom, 8)
103115
.padding(.horizontal, 16)
116+
117+
if model.isLoopActive {
118+
Divider()
119+
VStack(alignment: .leading) {
120+
Text("Loop Times: \(model.loopTimes == 0 ? "" : "\(Int(model.loopTimes))")")
121+
.font(.subheadline)
122+
.fontWeight(.medium)
123+
.foregroundStyle(.black)
124+
HStack {
125+
Text("1")
126+
.font(.caption)
127+
Slider(value: $model.loopTimes, in: 0...10, step: 1)
128+
.onChange(of: model.loopTimes) { _, new in
129+
model.updateLoopTimes(new)
130+
}
131+
Text("")
132+
.font(.caption)
133+
}
134+
Text(model.loopTimes == 0 ? "Loop infinitely" : "Loop \(Int(model.loopTimes)) time\(Int(model.loopTimes) == 1 ? "" : "s")")
135+
.font(.caption2)
136+
.foregroundStyle(.secondary)
137+
}
138+
.padding(.bottom, 8)
139+
.padding(.horizontal, 16)
140+
}
104141
}
105142
.onChange(of: currentTrack) { oldValue, newValue in
106143
if let track = newValue {
@@ -132,6 +169,9 @@ extension AudioPlayerControls {
132169

133170
var isPlaying: Bool = false
134171
var isMuted: Bool = false
172+
173+
var loopMode: AudioPlayerLoopMode = .off
174+
var loopTimes: Double = 0 // 0 means infinite
135175

136176
var volume: Float = 0.5
137177

@@ -159,6 +199,52 @@ extension AudioPlayerControls {
159199
return "speaker.wave.3"
160200
}
161201
}
202+
203+
var iconForLoopMode: String {
204+
switch loopMode {
205+
case .off:
206+
return "repeat"
207+
case .single:
208+
return "repeat.1"
209+
case .all:
210+
return "repeat"
211+
}
212+
}
213+
214+
var loopModeDescription: String {
215+
switch loopMode {
216+
case .off:
217+
return "Loop: Off"
218+
case .single(let times):
219+
if let times = times, times > 0 {
220+
return "Loop: Single (\(times)x)"
221+
}
222+
return "Loop: Single (∞)"
223+
case .all(let times):
224+
if let times = times, times > 0 {
225+
return "Loop: All (\(times)x)"
226+
}
227+
return "Loop: All (∞)"
228+
}
229+
}
230+
231+
var isLoopActive: Bool {
232+
if case .off = loopMode {
233+
return false
234+
}
235+
return true
236+
}
237+
238+
var currentLoopTimes: Int? {
239+
switch loopMode {
240+
case .off:
241+
return nil
242+
case .single(let times):
243+
return times
244+
case .all(let times):
245+
return times
246+
}
247+
}
162248

163249
init(audioPlayerService: AudioPlayerService) {
164250
self.audioPlayerService = audioPlayerService
@@ -220,6 +306,19 @@ extension AudioPlayerControls {
220306
isMuted.toggle()
221307
audioPlayerService.toggleMute()
222308
}
309+
310+
func cycleLoopMode() {
311+
audioPlayerService.cycleLoopMode()
312+
loopMode = audioPlayerService.loopMode
313+
// Update loopTimes to match the current mode's times
314+
loopTimes = Double(currentLoopTimes ?? 0)
315+
}
316+
317+
func updateLoopTimes(_ times: Double) {
318+
let timesValue = times == 0 ? nil : Int(times)
319+
audioPlayerService.setLoopTimes(timesValue)
320+
loopMode = audioPlayerService.loopMode
321+
}
223322

224323
func playPause() {
225324
if audioPlayerService.state == .playing {

AudioPlayer/AudioPlayer/Dependencies/AudioPlayerService.swift

Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -39,6 +39,11 @@ final class AudioPlayerService {
3939
var state: AudioPlayerState {
4040
player.state
4141
}
42+
43+
var loopMode: AudioPlayerLoopMode {
44+
get { player.loopMode }
45+
set { player.loopMode = newValue }
46+
}
4247

4348
var statusChangedNotifier = Notifier<AudioPlayerState>()
4449
var metadataReceivedNotifier = Notifier<[String: String]>()
@@ -114,6 +119,32 @@ final class AudioPlayerService {
114119
func seek(at time: Double) {
115120
player.seek(to: time)
116121
}
122+
123+
func setLoopMode(_ mode: AudioPlayerLoopMode) {
124+
player.loopMode = mode
125+
}
126+
127+
func cycleLoopMode() {
128+
switch player.loopMode {
129+
case .off:
130+
player.loopMode = .single(times: nil)
131+
case .single:
132+
player.loopMode = .all(times: nil)
133+
case .all:
134+
player.loopMode = .off
135+
}
136+
}
137+
138+
func setLoopTimes(_ times: Int?) {
139+
switch player.loopMode {
140+
case .off:
141+
break
142+
case .single:
143+
player.loopMode = .single(times: times)
144+
case .all:
145+
player.loopMode = .all(times: times)
146+
}
147+
}
117148

118149
private func recreatePlayer() {
119150
player = audioPlayerProvider()

AudioStreaming/Core/Helpers/Logger.swift

Lines changed: 19 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -4,14 +4,14 @@
44
//
55

66
import Foundation
7-
import os
7+
import OSLog
88

99
private let loggingSubsystem = "audio.streaming.log"
1010

11-
enum Logger {
12-
private static let audioRendering = OSLog(subsystem: loggingSubsystem, category: "audio.rendering")
13-
private static let networking = OSLog(subsystem: loggingSubsystem, category: "audio.networking")
14-
private static let generic = OSLog(subsystem: loggingSubsystem, category: "audio.streaming.generic")
11+
extension Logger {
12+
public static let audioRendering = Logger(subsystem: loggingSubsystem, category: "audio.rendering")
13+
public static let networking = Logger(subsystem: loggingSubsystem, category: "audio.networking")
14+
public static let generic = Logger(subsystem: loggingSubsystem, category: "audio.streaming.generic")
1515

1616
/// Defines is the the logger displays any logs
1717
static var isEnabled = true
@@ -21,7 +21,7 @@ enum Logger {
2121
case networking
2222
case generic
2323

24-
func toOSLog() -> OSLog {
24+
func toOSLog() -> Logger {
2525
switch self {
2626
case .audioRendering: return Logger.audioRendering
2727
case .networking: return Logger.networking
@@ -30,24 +30,31 @@ enum Logger {
3030
}
3131
}
3232

33-
static func error(_ message: StaticString, category: Category, args: CVarArg...) {
33+
static func error(_ message: String, category: Category, args: CVarArg...) {
3434
process(message, category: category, type: .error, args: args)
3535
}
3636

37-
static func error(_ message: StaticString, category: Category) {
37+
static func error(_ message: String, category: Category) {
3838
error(message, category: category, args: [])
3939
}
4040

41-
static func debug(_ message: StaticString, category: Category, args: CVarArg...) {
41+
static func debug(_ message: String, category: Category, args: CVarArg...) {
4242
process(message, category: category, type: .debug, args: args)
4343
}
4444

45-
static func debug(_ message: StaticString, category: Category) {
45+
static func debug(_ message: String, category: Category) {
4646
debug(message, category: category, args: [])
4747
}
4848

49-
private static func process(_ message: StaticString, category: Category, type: OSLogType, args: CVarArg...) {
49+
private static func process(_ message: String, category: Category, type: OSLogType, args: CVarArg...) {
5050
guard isEnabled else { return }
51-
os_log(message, log: category.toOSLog(), type: type, args)
51+
switch type {
52+
case .debug:
53+
category.toOSLog().debug("\(message)")
54+
case .error:
55+
category.toOSLog().error("\(message)")
56+
default:
57+
category.toOSLog().info("\(message)")
58+
}
5259
}
5360
}

AudioStreaming/OggVorbis/VorbisFileDecoder.swift

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
import Foundation
22
import AudioCodecs
33
import AVFoundation
4+
import OSLog
45

56
/// A simple decoder for Ogg Vorbis files using libvorbisfile
67
final class VorbisFileDecoder {

AudioStreaming/Streaming/Audio Source/Mp4/Mp4Restructure.swift

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@
44
//
55

66
import Foundation
7+
import OSLog
78

89
struct MP4Atom: Equatable, CustomDebugStringConvertible {
910
let type: Int

AudioStreaming/Streaming/Audio Source/Mp4/RemoteMp4Restructure.swift

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@
44
//
55

66
import Foundation
7+
import OSLog
78

89
final class RemoteMp4Restructure {
910
struct RestructuredData {

0 commit comments

Comments
 (0)