Skip to content

Commit de8eeea

Browse files
committed
Allow setting rate on the fly
1 parent 5a123e2 commit de8eeea

File tree

8 files changed

+98
-19
lines changed

8 files changed

+98
-19
lines changed

ChunkedAudioPlayer.xcworkspace/xcshareddata/xcschemes/ChunkedAudioPlayer.xcscheme

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
<?xml version="1.0" encoding="UTF-8"?>
22
<Scheme
3-
LastUpgradeVersion = "1530"
3+
LastUpgradeVersion = "1600"
44
version = "1.7">
55
<BuildAction
66
parallelizeBuildables = "YES"

Example/Example.xcodeproj/project.pbxproj

Lines changed: 3 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -128,7 +128,7 @@
128128
attributes = {
129129
BuildIndependentTargetsInParallel = 1;
130130
LastSwiftUpdateCheck = 1520;
131-
LastUpgradeCheck = 1530;
131+
LastUpgradeCheck = 1600;
132132
TargetAttributes = {
133133
995EF5E82B815F17000B5E39 = {
134134
CreatedOnToolsVersion = 15.2;
@@ -306,7 +306,6 @@
306306
995EF5F92B815F1C000B5E39 /* Debug */ = {
307307
isa = XCBuildConfiguration;
308308
buildSettings = {
309-
ALWAYS_EMBED_SWIFT_STANDARD_LIBRARIES = YES;
310309
ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon;
311310
ASSETCATALOG_COMPILER_GLOBAL_ACCENT_COLOR_NAME = AccentColor;
312311
CODE_SIGN_STYLE = Automatic;
@@ -332,7 +331,7 @@
332331
"LD_RUNPATH_SEARCH_PATHS[sdk=macosx*]" = "@executable_path/../Frameworks";
333332
MACOSX_DEPLOYMENT_TARGET = 14.0;
334333
MARKETING_VERSION = 1.0;
335-
PRODUCT_BUNDLE_IDENTIFIER = "com.mseremet.Example-Chunked-Audio-Player";
334+
PRODUCT_BUNDLE_IDENTIFIER = "com.mseremet.chunked-audio-player-example";
336335
PRODUCT_NAME = "$(TARGET_NAME)";
337336
SDKROOT = auto;
338337
SUPPORTED_PLATFORMS = "appletvos appletvsimulator iphoneos iphonesimulator macosx xros xrsimulator";
@@ -350,7 +349,6 @@
350349
995EF5FA2B815F1C000B5E39 /* Release */ = {
351350
isa = XCBuildConfiguration;
352351
buildSettings = {
353-
ALWAYS_EMBED_SWIFT_STANDARD_LIBRARIES = YES;
354352
ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon;
355353
ASSETCATALOG_COMPILER_GLOBAL_ACCENT_COLOR_NAME = AccentColor;
356354
CODE_SIGN_STYLE = Automatic;
@@ -376,7 +374,7 @@
376374
"LD_RUNPATH_SEARCH_PATHS[sdk=macosx*]" = "@executable_path/../Frameworks";
377375
MACOSX_DEPLOYMENT_TARGET = 14.0;
378376
MARKETING_VERSION = 1.0;
379-
PRODUCT_BUNDLE_IDENTIFIER = "com.mseremet.Example-Chunked-Audio-Player";
377+
PRODUCT_BUNDLE_IDENTIFIER = "com.mseremet.chunked-audio-player-example";
380378
PRODUCT_NAME = "$(TARGET_NAME)";
381379
SDKROOT = auto;
382380
SUPPORTED_PLATFORMS = "appletvos appletvsimulator iphoneos iphonesimulator macosx xros xrsimulator";

Example/Example.xcodeproj/xcshareddata/xcschemes/Example.xcscheme

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
<?xml version="1.0" encoding="UTF-8"?>
22
<Scheme
3-
LastUpgradeVersion = "1530"
3+
LastUpgradeVersion = "1600"
44
version = "1.7">
55
<BuildAction
66
parallelizeBuildables = "YES"

Example/Example/LocalFileView.swift

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -22,11 +22,20 @@ struct LocalFileView: View {
2222
}
2323
}
2424

25+
private var rateBinding: Binding<Float> {
26+
Binding<Float> {
27+
player.rate
28+
} set: { rate in
29+
player.rate = rate
30+
}
31+
}
32+
2533
var body: some View {
2634
VStack(alignment: .center, spacing: 24) {
2735
pathLabel
2836
controlsView
2937
volumeView
38+
rateView
3039
}
3140
.padding()
3241
.frame(maxHeight: .infinity)
@@ -56,6 +65,9 @@ struct LocalFileView: View {
5665
.onChange(of: player.currentRate) { _, rate in
5766
print("Rate = \(rate)")
5867
}
68+
.onChange(of: player.currentState) { _, state in
69+
print("State = \(state)")
70+
}
5971
#if os(iOS) || os(visionOS)
6072
.navigationBarTitleDisplayMode(.inline)
6173
#endif
@@ -101,6 +113,15 @@ struct LocalFileView: View {
101113
.frame(maxWidth: 200)
102114
}
103115

116+
@ViewBuilder
117+
private var rateView: some View {
118+
VStack {
119+
Text("Rate: \(player.rate.formatted(.number.precision(.fractionLength(2))))")
120+
Slider(value: rateBinding, in: 0...1, step: 0.01)
121+
}
122+
.frame(maxWidth: 200)
123+
}
124+
104125
private func performConversion() {
105126
player.start(makeSampleStream(), type: kAudioFileMP3Type)
106127
}

Example/Example/Resources/Info.plist

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,5 +7,9 @@
77
<key>NSAllowsArbitraryLoads</key>
88
<true/>
99
</dict>
10+
<key>UIBackgroundModes</key>
11+
<array>
12+
<string>audio</string>
13+
</array>
1014
</dict>
1115
</plist>

Example/Example/TextToSpeechView.swift

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -37,11 +37,20 @@ struct TextToSpeechView: View {
3737
}
3838
}
3939

40+
private var rateBinding: Binding<Float> {
41+
Binding<Float> {
42+
player.rate
43+
} set: { rate in
44+
player.rate = rate
45+
}
46+
}
47+
4048
var body: some View {
4149
VStack(alignment: .center, spacing: 24) {
4250
inputTextField
4351
controlsView
4452
volumeView
53+
rateView
4554
}
4655
.padding()
4756
.frame(maxHeight: .infinity)
@@ -88,6 +97,9 @@ struct TextToSpeechView: View {
8897
.onChange(of: player.currentRate) { _, rate in
8998
print("Rate = \(rate)")
9099
}
100+
.onChange(of: player.currentState) { _, state in
101+
print("State = \(state)")
102+
}
91103
#if os(iOS) || os(visionOS)
92104
.navigationBarTitleDisplayMode(.inline)
93105
#endif
@@ -133,6 +145,15 @@ struct TextToSpeechView: View {
133145
.frame(maxWidth: 200)
134146
}
135147

148+
@ViewBuilder
149+
private var rateView: some View {
150+
VStack {
151+
Text("Rate: \(player.rate.formatted(.number.precision(.fractionLength(2))))")
152+
Slider(value: rateBinding, in: 0...1, step: 0.01)
153+
}
154+
.frame(maxWidth: 200)
155+
}
156+
136157
@ViewBuilder
137158
private var settingsMenu: some View {
138159
Menu {

README.md

Lines changed: 29 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -55,19 +55,23 @@ player.start(stream, type: kAudioFileMP3Type)
5555
* Listen for changes:
5656

5757
```swift
58-
player.$state.sink { state in
58+
player.$currentState.sink { state in
5959
// handle player state
6060
}.store(in: &bag)
6161

62-
player.$rate.sink { rate in
62+
player.$currentRate.sink { rate in
6363
// handle player rate
6464
}.store(in: &bag)
6565

66+
player.$currentDuration.sink { duration in
67+
// handle player duration
68+
}.store(in: &bag)
69+
6670
player.$currentTime.sink { time in
6771
// handle player time
6872
}.store(in: &bag)
6973

70-
player.$error.sink { error in
74+
player.$currentError.sink { error in
7175
if let error {
7276
// handle player error
7377
}
@@ -77,6 +81,15 @@ player.$error.sink { error in
7781
* Control playback:
7882

7983
```swift
84+
// Set stream volume
85+
player.volume = 0.5
86+
87+
// Set muted
88+
player.isMuted = true
89+
90+
// Set stream rate
91+
player.rate = 0.5
92+
8093
// Pause current stream
8194
player.pause()
8295

@@ -85,6 +98,15 @@ player.resume()
8598

8699
// Stop current stream
87100
player.stop()
101+
102+
// Rewind 5 seconds
103+
player.rewind(CMTime(seconds: 5.0, preferredTimescale: 1000))
104+
105+
// Forward 5 seconds
106+
player.forward(CMTime(seconds: 5.0, preferredTimescale: 1000))
107+
108+
// Seek to specific time
109+
player.seek(to: CMTime(seconds: 60, preferredTimescale: 1000))
88110
```
89111

90112
* SwiftUI Support
@@ -95,10 +117,11 @@ struct ContentView: View {
95117
@ObservedObject private var player = AudioPlayer()
96118

97119
var body: some View {
98-
Text("State \(player.state)")
99-
Text("Rate \(player.rate)")
120+
Text("State \(player.currentState)")
121+
Text("Rate \(player.currentRate)")
100122
Text("Time \(player.currentTime)")
101-
if let error = player.error {
123+
Text("Duration \(player.currentDuration)")
124+
if let error = player.currentError {
102125
Text("Error \(error)")
103126
}
104127
}

Sources/AudioSynchronizer.swift

Lines changed: 18 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -44,7 +44,15 @@ final class AudioSynchronizer {
4444
set { audioRenderer?.isMuted = newValue }
4545
}
4646

47-
var desiredRate: Float = 1.0
47+
var desiredRate: Float = 1.0 {
48+
didSet {
49+
if desiredRate == 0.0 {
50+
pause()
51+
} else {
52+
resume(at: desiredRate)
53+
}
54+
}
55+
}
4856

4957
init(
5058
timeUpdateInterval: CMTime,
@@ -91,10 +99,15 @@ final class AudioSynchronizer {
9199
onPaused()
92100
}
93101

94-
func resume() {
95-
guard let audioSynchronizer, audioSynchronizer.rate == 0.0 else { return }
96-
audioSynchronizer.rate = desiredRate
97-
onPlaying()
102+
func resume(at rate: Float? = nil) {
103+
guard let audioSynchronizer else { return }
104+
let oldRate = audioSynchronizer.rate
105+
let newRate = rate ?? desiredRate
106+
guard audioSynchronizer.rate != newRate else { return }
107+
audioSynchronizer.rate = newRate
108+
if oldRate == 0.0 && newRate > 0.0 {
109+
onPlaying()
110+
}
98111
}
99112

100113
func rewind(_ time: CMTime) {
@@ -155,7 +168,6 @@ final class AudioSynchronizer {
155168
private func onFileStreamDescriptionReceived(asbd: AudioStreamBasicDescription) {
156169
let renderer = AVSampleBufferAudioRenderer()
157170
let synchronizer = AVSampleBufferRenderSynchronizer()
158-
synchronizer.rate = desiredRate
159171
synchronizer.addRenderer(renderer)
160172
audioRenderer = renderer
161173
audioSynchronizer = synchronizer

0 commit comments

Comments
 (0)