Skip to content

Commit

Permalink
feat: allow specifying the Audio Session Category for iOS and Usage f…
Browse files Browse the repository at this point in the history
…or Android (#28)

* feat: allow ios to specify the audio session category when setting up the stream

* feat: allow specifying use in android

* chore: update docs
  • Loading branch information
itsramiel authored Feb 23, 2025
1 parent 191a9b0 commit 7eff1a9
Show file tree
Hide file tree
Showing 17 changed files with 201 additions and 41 deletions.
21 changes: 17 additions & 4 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -24,14 +24,15 @@ For iOS, run `pod install` in the `ios` directory.

## Usage

1. Setup an Audio Stream using the singleton `AudioManager`'s `shared` static property and calling its `setupAudioStream(sampleRate: number, channelCount: number): void`
1. Setup an Audio Stream using the singleton `AudioManager`'s `shared` static property and calling its `setupAudioStream`.
`setupAudioStream` takes in an optional options argument where you can pass in the `sampleRate` and `channelCount` of your audio files, or do not specify them and it will default to `44100` and `2` respectively.

[How do I know what `sampleRate` and `channelCount` I need to pass?](#sample-rates-and-channel-counts)

```ts
import { AudioManager } from 'react-native-audio-playback';

AudioManager.shared.setupAudioStream(44100, 2);
AudioManager.shared.setupAudioStream({ sampleRate: 44100, channelCount: 2 });
```

2. Load in your audio sounds as such:
Expand Down Expand Up @@ -96,8 +97,20 @@ AudioManager.shared.<some-method>

#### Methods:

- `setupAudioStream(sampleRate: number = 44100, channelCount: number = 2): void`: sets up the Audio Stream to allow it later be opened.
Note: You shouldn't setup multiple streams simultaneously because you only need one stream. Trying to setup another one will simply fails because there is already one setup.
- `setupAudioStream(options?: {
sampleRate?: number;
channelCount?: number;
ios?: {
audioSessionCategory?: IosAudioSessionCategory;
};
android?: {
usage?: AndroidAudioStreamUsage;
};
}): void`: sets up the Audio Stream to allow it later be opened.
Notes:
1. You shouldn't setup multiple streams simultaneously because you only need one stream. Trying to setup another one will simply fails because there is already one setup.
2. You can change the ios audio session category using the `audioSessionCategory` option in the `ios` object. Check [apple docs](https://developer.apple.com/documentation/avfaudio/avaudiosession/category-swift.struct#Getting-Standard-Categories) for more info on the different audio session categories.
3. You can change the android usage using the `usage` option in the `android` object. Check [here](https://github.com/google/oboe/blob/11afdfcd3e1c46dc2ea4b86c83519ebc2d44a1d4/include/oboe/Definitions.h#L316-L377) for the list of options.
- `openAudioStream(): void`: Opens the audio stream to allow audio to be played
Note: You should have called `setupAudioStream` before calling this method. You can't open a stream that hasn't been setup
- `pauseAudioStream(): void`: Pauses the audio stream (An example of when to use this is when user puts app to background)
Expand Down
24 changes: 23 additions & 1 deletion android/src/main/cpp/AudioEngine.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,10 @@

#include "audio/AAssetDataSource.h"

SetupAudioStreamResult AudioEngine::setupAudioStream(double sampleRate, double channelCount) {
SetupAudioStreamResult AudioEngine::setupAudioStream(
double sampleRate,
double channelCount,
int usage) {
if(mAudioStream) {
return { .error = "Setting up an audio stream while one is already available"};
}
Expand All @@ -18,6 +21,7 @@ SetupAudioStreamResult AudioEngine::setupAudioStream(double sampleRate, double c

oboe::AudioStreamBuilder builder {};

builder.setUsage(getUsageFromInt(usage));
builder.setFormat(oboe::AudioFormat::Float);
builder.setFormatConversionAllowed(true);
builder.setPerformanceMode(oboe::PerformanceMode::LowLatency);
Expand Down Expand Up @@ -185,3 +189,21 @@ void AudioEngine::unloadSounds(const std::optional<std::vector<std::string>> &id
mPlayers.clear();
}
}

oboe::Usage AudioEngine::getUsageFromInt(int usage) {
switch(usage) {
case 0: return oboe::Usage::Media;
case 1: return oboe::Usage::VoiceCommunication;
case 2: return oboe::Usage::VoiceCommunicationSignalling;
case 3: return oboe::Usage::Alarm;
case 4: return oboe::Usage::Notification;
case 5: return oboe::Usage::NotificationRingtone;
case 6: return oboe::Usage::NotificationEvent;
case 7: return oboe::Usage::AssistanceAccessibility;
case 8: return oboe::Usage::AssistanceNavigationGuidance;
case 9: return oboe::Usage::AssistanceSonification;
case 10: return oboe::Usage::Game;
case 11: return oboe::Usage::Assistant;
default: return oboe::Usage::Media;
}
}
4 changes: 3 additions & 1 deletion android/src/main/cpp/AudioEngine.h
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,7 @@

class AudioEngine : public oboe::AudioStreamDataCallback{
public:
SetupAudioStreamResult setupAudioStream(double sampleRate, double channelCount);
SetupAudioStreamResult setupAudioStream(double sampleRate, double channelCount, int usage);
OpenAudioStreamResult openAudioStream();
PauseAudioStreamResult pauseAudioStream();
CloseAudioStreamResult closeAudioStream();
Expand All @@ -34,6 +34,8 @@ class AudioEngine : public oboe::AudioStreamDataCallback{
std::map<std::string, std::unique_ptr<Player>> mPlayers;
int32_t mDesiredSampleRate{};
int mDesiredChannelCount{};

oboe::Usage getUsageFromInt(int usage);
};

#endif //AUDIOPLAYBACK_AUDIOENGINE_H
9 changes: 7 additions & 2 deletions android/src/main/cpp/native-lib.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -83,8 +83,13 @@ std::vector<std::string> jniStringArrayToStringVector(JNIEnv* env, jobjectArray

extern "C" {
JNIEXPORT jobject JNICALL
Java_com_audioplayback_AudioPlaybackModule_setupAudioStreamNative(JNIEnv *env, jobject thiz, jdouble sample_rate, jdouble channel_count) {
auto result = audioEngine->setupAudioStream(sample_rate, channel_count);
Java_com_audioplayback_AudioPlaybackModule_setupAudioStreamNative(
JNIEnv *env,
jobject thiz,
jdouble sample_rate,
jdouble channel_count,
jint usage) {
auto result = audioEngine->setupAudioStream(sample_rate, channel_count, usage);

jclass structClass = env->FindClass("com/audioplayback/models/SetupAudioStreamResult");
jmethodID constructor = env->GetMethodID(structClass, "<init>", "(Ljava/lang/String;)V");
Expand Down
11 changes: 8 additions & 3 deletions android/src/main/java/com/audioplayback/AudioPlaybackModule.kt
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ import com.audioplayback.models.OpenAudioStreamResult
import com.audioplayback.models.PauseAudioStreamResult
import com.audioplayback.models.SetupAudioStreamResult
import com.facebook.react.bridge.Arguments
import com.facebook.react.bridge.ReadableMap
import com.facebook.react.bridge.WritableMap
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers
Expand All @@ -29,8 +30,12 @@ class AudioPlaybackModule internal constructor(context: ReactApplicationContext)
}

@ReactMethod(isBlockingSynchronousMethod = true)
override fun setupAudioStream(sampleRate: Double, channelCount: Double): WritableMap {
val result = setupAudioStreamNative(sampleRate, channelCount)
override fun setupAudioStream(options: ReadableMap): WritableMap {
val sampleRate = options.getDouble("sampleRate")
val channelCount = options.getDouble("channelCount")
val usage = options.getMap("android")!!.getInt("usage")

val result = setupAudioStreamNative(sampleRate, channelCount, usage)
val map = Arguments.createMap()
result.error?.let { map.putString("error", it) } ?: map.putNull("error")
return map
Expand Down Expand Up @@ -185,7 +190,7 @@ class AudioPlaybackModule internal constructor(context: ReactApplicationContext)
unloadSoundsNative(null)
}

private external fun setupAudioStreamNative(sampleRate: Double, channelCount: Double): SetupAudioStreamResult
private external fun setupAudioStreamNative(sampleRate: Double, channelCount: Double, usage: Int): SetupAudioStreamResult
private external fun openAudioStreamNative(): OpenAudioStreamResult
private external fun pauseAudioStreamNative(): PauseAudioStreamResult
private external fun closeAudioStreamNative(): CloseAudioStreamResult
Expand Down
3 changes: 2 additions & 1 deletion example/Gemfile.lock
Original file line number Diff line number Diff line change
Expand Up @@ -66,7 +66,7 @@ GEM
netrc (~> 0.11)
cocoapods-try (1.2.0)
colored2 (3.1.2)
concurrent-ruby (1.3.4)
concurrent-ruby (1.3.3)
connection_pool (2.4.1)
drb (2.2.1)
escape (0.0.4)
Expand Down Expand Up @@ -110,6 +110,7 @@ PLATFORMS
DEPENDENCIES
activesupport (>= 6.1.7.5, != 7.1.0)
cocoapods (>= 1.13, != 1.15.1, != 1.15.0)
concurrent-ruby (< 1.3.4)
xcodeproj (< 1.26.0)

RUBY VERSION
Expand Down
2 changes: 1 addition & 1 deletion example/android/gradle.properties
Original file line number Diff line number Diff line change
Expand Up @@ -32,7 +32,7 @@ reactNativeArchitectures=armeabi-v7a,arm64-v8a,x86,x86_64
# your application. You should enable this flag either if you want
# to write custom TurboModules/Fabric components OR use libraries that
# are providing them.
newArchEnabled=false
newArchEnabled=true

# Use this property to enable or disable the Hermes JS engine.
# If set to false, you will be using JSC instead.
Expand Down
6 changes: 3 additions & 3 deletions example/ios/Podfile.lock
Original file line number Diff line number Diff line change
Expand Up @@ -1210,7 +1210,7 @@ PODS:
- React-jsiexecutor
- React-RCTFBReactNativeSpec
- ReactCommon/turbomodule/core
- react-native-audio-playback (1.0.5):
- react-native-audio-playback (1.0.6):
- DoubleConversion
- glog
- hermes-engine
Expand Down Expand Up @@ -1818,7 +1818,7 @@ SPEC CHECKSUMS:
React-logger: e7eeebaed32b88dcc29b10901aa8c5822dc397c4
React-Mapbuffer: 73dd1210c4ecf0dfb4e2d4e06f2a13f824a801a9
React-microtasksnativemodule: d03753688e2abf135edcd4160ab3ce7526da8b0d
react-native-audio-playback: 6b828a31071736a3beee00128fd445c6fb3016ef
react-native-audio-playback: e3b40982c3b6dff7dc0ed0680d7481d43a8f54dc
react-native-slider: 54e7f67e9e4c92c0edac77bbf58abfaf1f60055f
React-nativeconfig: cb207ebba7cafce30657c7ad9f1587a8f32e4564
React-NativeModulesApple: 8411d548b1ad9d2b3e597beb9348e715c8020e0c
Expand Down Expand Up @@ -1854,4 +1854,4 @@ SPEC CHECKSUMS:

PODFILE CHECKSUM: 0de3c0d5ec96ef8679c10cf5c8e600381b2b0ac8

COCOAPODS: 1.14.3
COCOAPODS: 1.15.2
11 changes: 9 additions & 2 deletions example/src/components/StreamControl.tsx
Original file line number Diff line number Diff line change
@@ -1,4 +1,7 @@
import { AudioManager } from 'react-native-audio-playback';
import {
AndroidAudioStreamUsage,
AudioManager,
} from 'react-native-audio-playback';

import { Button } from './Button';
import { Section } from './Section';
Expand All @@ -9,7 +12,11 @@ interface StreamControlProps {

export function StreamControl({ onLoadSounds }: StreamControlProps) {
function onSetupStream() {
AudioManager.shared.setupAudioStream();
AudioManager.shared.setupAudioStream({
android: {
usage: AndroidAudioStreamUsage.Alarm,
},
});
}

function onOpenStream() {
Expand Down
15 changes: 10 additions & 5 deletions ios/AudioEngine.swift
Original file line number Diff line number Diff line change
Expand Up @@ -59,10 +59,6 @@ class AudioEngine {
private var interruptionState: InterruptionState?

init() {
// configure Audio Session
let audioSession = AVAudioSession.sharedInstance()
try? audioSession.setCategory(.playback)
try? audioSession.setActive(true)

NotificationCenter.default.addObserver(
self,
Expand Down Expand Up @@ -101,10 +97,19 @@ class AudioEngine {
}
}

public func setupAudioStream(sampleRate: Double, channelCount: Int) throws {
public func setupAudioStream(
sampleRate: Double,
channelCount: Int,
audioSessionCategory: AVAudioSession.Category
) throws {
guard audioStreamState != .initialized else {
throw AudioEngineError.audioStreamAlreadyInitialized
}

// configure Audio Session
let audioSession = AVAudioSession.sharedInstance()
try? audioSession.setCategory(audioSessionCategory)
try? audioSession.setActive(true)

self.desiredSampleRate = sampleRate
self.desiredChannelCount = channelCount
Expand Down
9 changes: 7 additions & 2 deletions ios/AudioPlayback.mm
Original file line number Diff line number Diff line change
Expand Up @@ -20,8 +20,13 @@ - (instancetype) init {

RCT_EXPORT_MODULE()

RCT_EXPORT_SYNCHRONOUS_TYPED_METHOD(NSDictionary *, setupAudioStream:(double)sampleRate channelCount:(double)channelCount) {
NSString *error = [moduleImpl setupAudioStreamWithSampleRate:sampleRate channelCount:channelCount];
RCT_EXPORT_SYNCHRONOUS_TYPED_METHOD(NSDictionary *, setupAudioStream:(JS::NativeAudioPlayback::SpecSetupAudioStreamOptions &)options) {
double sampleRate = options.sampleRate();
double channelCount = options.channelCount();
double audioSessionCategory = options.ios().audioSessionCategory();


NSString *error = [moduleImpl setupAudioStreamWithSampleRate:sampleRate channelCount:channelCount audioSessionCategory: audioSessionCategory];

return @{@"error":error?: [NSNull null]};
}
Expand Down
28 changes: 26 additions & 2 deletions ios/AudioPlaybackImpl.swift
Original file line number Diff line number Diff line change
Expand Up @@ -6,9 +6,21 @@ import AudioToolbox
private static let unknownError: String = "An unknown error occurred while loading the audio file. Please create an issue with a reproducible"
let audioEngine = AudioEngine()

@objc public func setupAudioStream(sampleRate: Double, channelCount: Double) -> String? {
@objc public func setupAudioStream(
sampleRate: Double,
channelCount: Double,
audioSessionCategory: Double
) -> String? {
guard let audioSessionCategoryEnum = getAudoSessionCategoryEnumMemeberFromRawValue(audioSessionCategory) else {
return "Invalid audio session category"
}

do {
try audioEngine.setupAudioStream(sampleRate: sampleRate, channelCount: Int(channelCount))
try audioEngine.setupAudioStream(
sampleRate: sampleRate,
channelCount: Int(channelCount),
audioSessionCategory: audioSessionCategoryEnum
)
return nil
} catch let error as AudioEngineError {
return error.localizedDescription
Expand Down Expand Up @@ -151,5 +163,17 @@ import AudioToolbox
}
return result
}

private func getAudoSessionCategoryEnumMemeberFromRawValue(_ rawValue: Double) -> AVAudioSession.Category? {
switch Int(rawValue) {
case 0: return .ambient
case 1: return .multiRoute
case 2: return .playAndRecord
case 3: return .playback
case 4: return .record
case 5: return .soloAmbient
default: return nil
}
}
}

14 changes: 10 additions & 4 deletions src/NativeAudioPlayback.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,10 +2,16 @@ import type { TurboModule } from 'react-native';
import { TurboModuleRegistry } from 'react-native';

export interface Spec extends TurboModule {
setupAudioStream: (
sampleRate: number,
channelCount: number
) => { error: string | null };
setupAudioStream: (options: {
sampleRate: number;
channelCount: number;
ios: {
audioSessionCategory: number;
};
android: {
usage: number;
};
}) => { error: string | null };
openAudioStream: () => { error: string | null };
pauseAudioStream: () => { error: string | null };
closeAudioStream: () => { error: string | null };
Expand Down
1 change: 1 addition & 0 deletions src/index.tsx
Original file line number Diff line number Diff line change
@@ -1 +1,2 @@
export { AudioManager, Player } from './models';
export { IosAudioSessionCategory, AndroidAudioStreamUsage } from './types';
34 changes: 29 additions & 5 deletions src/models/AudioManager.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,18 +9,42 @@ import {
setSoundsVolume,
setupAudioStream,
} from '../module';

import { AndroidAudioStreamUsage, IosAudioSessionCategory } from '../types';
import { Player } from './Player';

export class AudioManager {
public static shared = new AudioManager();

private constructor() {}

public setupAudioStream(
sampleRate: number = 44100,
channelCount: number = 2
) {
setupAudioStream(sampleRate, channelCount);
public setupAudioStream(options?: {
sampleRate?: number;
channelCount?: number;
ios?: {
audioSessionCategory?: IosAudioSessionCategory;
};
android?: {
usage?: AndroidAudioStreamUsage;
};
}) {
const sampleRate = options?.sampleRate ?? 44100;
const channelCount = options?.channelCount ?? 2;
const iosAudioSessionCategory =
options?.ios?.audioSessionCategory ?? IosAudioSessionCategory.Playback;
const androidUsage =
options?.android?.usage ?? AndroidAudioStreamUsage.Media;

setupAudioStream({
channelCount,
sampleRate,
ios: {
audioSessionCategory: iosAudioSessionCategory,
},
android: {
usage: androidUsage,
},
});
}

public openAudioStream(): void {
Expand Down
Loading

0 comments on commit 7eff1a9

Please sign in to comment.