diff --git a/cpp/AudioContext/AudioContextHostObject.h b/cpp/AudioContext/AudioContextHostObject.h index 58fcc230..15f9d761 100644 --- a/cpp/AudioContext/AudioContextHostObject.h +++ b/cpp/AudioContext/AudioContextHostObject.h @@ -20,15 +20,15 @@ namespace audiocontext std::shared_ptr wrapper_; public: - explicit AudioContextHostObject(const std::shared_ptr &wrapper) : wrapper_(wrapper) {} - - static void createAndInstallFromWrapper(const std::shared_ptr &wrapper, jlong jsContext) { + explicit AudioContextHostObject(std::shared_ptr wrapper) : wrapper_(wrapper) {} +#ifdef ANDROID + static void createAndInstallFromWrapper(const std::shared_ptr& wrapper, jlong jsContext) { auto runtime = reinterpret_cast(jsContext); auto hostObject = std::make_shared(wrapper); auto object = jsi::Object::createFromHostObject(*runtime, hostObject); runtime->global().setProperty(*runtime, "__AudioContext", std::move(object)); } - +#endif jsi::Value get(jsi::Runtime &runtime, const jsi::PropNameID &name) override; void set(jsi::Runtime &runtime, const jsi::PropNameID &name, const jsi::Value &value) override; std::vector getPropertyNames(jsi::Runtime &rt) override; diff --git a/cpp/AudioContext/AudioContextWrapper.h b/cpp/AudioContext/AudioContextWrapper.h index 6fb5210d..97f0b53c 100644 --- a/cpp/AudioContext/AudioContextWrapper.h +++ b/cpp/AudioContext/AudioContextWrapper.h @@ -8,6 +8,8 @@ #ifdef ANDROID #include "AudioContext.h" +#else +#include "OscillatorNodeHostObject.h" #endif namespace audiocontext { diff --git a/cpp/AudioContext/android/AudioContextWrapper.cpp b/cpp/AudioContext/android/AudioContextWrapper.cpp index 1cb43962..0932448e 100644 --- a/cpp/AudioContext/android/AudioContextWrapper.cpp +++ b/cpp/AudioContext/android/AudioContextWrapper.cpp @@ -1,3 +1,4 @@ +#ifdef ANDROID #include "AudioContextWrapper.h" namespace audiocontext { @@ -17,3 +18,4 @@ namespace audiocontext { return std::make_shared(gain); } } // namespace audiocontext +#endif \ No newline at end of file diff --git a/cpp/AudioContext/ios/.keep b/cpp/AudioContext/ios/.keep deleted file mode 100644 index e69de29b..00000000 diff --git a/cpp/AudioContext/ios/AudioContextWrapper.cpp b/cpp/AudioContext/ios/AudioContextWrapper.cpp new file mode 100644 index 00000000..3b9fe322 --- /dev/null +++ b/cpp/AudioContext/ios/AudioContextWrapper.cpp @@ -0,0 +1,14 @@ +#ifndef ANDROID +#include "AudioContextWrapper.h" + +namespace audiocontext { + + std::shared_ptr AudioContextWrapper::createOscillator() { + return std::make_shared(); + } + + std::shared_ptr AudioContextWrapper::getDestination() { + throw std::runtime_error("[AudioContextHostObject::getDestination] Not yet implemented!"); + } +} // namespace audiocontext +#endif diff --git a/cpp/AudioDestinationNode/AudioDestinationNodeWrapper.h b/cpp/AudioDestinationNode/AudioDestinationNodeWrapper.h index 333e4d87..b8cd2871 100644 --- a/cpp/AudioDestinationNode/AudioDestinationNodeWrapper.h +++ b/cpp/AudioDestinationNode/AudioDestinationNodeWrapper.h @@ -7,8 +7,9 @@ #include "AudioNodeWrapper.h" #endif +#include "AudioNodeWrapper.h" + namespace audiocontext { - using namespace facebook; #ifdef ANDROID class AudioDestinationNode; diff --git a/cpp/AudioNode/AudioNodeWrapper.h b/cpp/AudioNode/AudioNodeWrapper.h index 2ffa7375..4cbaaeec 100644 --- a/cpp/AudioNode/AudioNodeWrapper.h +++ b/cpp/AudioNode/AudioNodeWrapper.h @@ -7,8 +7,6 @@ #endif namespace audiocontext { - using namespace facebook; - class AudioNodeWrapper { #ifdef ANDROID protected: diff --git a/cpp/AudioNode/android/AudioNodeWrapper.cpp b/cpp/AudioNode/android/AudioNodeWrapper.cpp index ef4c7338..42aedf32 100644 --- a/cpp/AudioNode/android/AudioNodeWrapper.cpp +++ b/cpp/AudioNode/android/AudioNodeWrapper.cpp @@ -1,3 +1,4 @@ +#ifdef ANDROID #include "AudioNodeWrapper.h" namespace audiocontext { @@ -10,3 +11,5 @@ namespace audiocontext { node_->disconnect(node->node_); } } + +#endif diff --git a/cpp/AudioNode/ios/.keep b/cpp/AudioNode/ios/.keep deleted file mode 100644 index e69de29b..00000000 diff --git a/cpp/AudioNode/ios/AudioNodeWrapper.cpp b/cpp/AudioNode/ios/AudioNodeWrapper.cpp new file mode 100644 index 00000000..6436c8cb --- /dev/null +++ b/cpp/AudioNode/ios/AudioNodeWrapper.cpp @@ -0,0 +1,15 @@ +#ifndef ANDROID +#include "AudioNodeWrapper.h" +#include "iostream" + +namespace audiocontext { + + void AudioNodeWrapper::connect(const std::shared_ptr node) const { + throw std::runtime_error("[AudioNodeWrapper::connect] Not yet implemented!"); + } + + void AudioNodeWrapper::disconnect(const std::shared_ptr node) const { + throw std::runtime_error("[AudioNodeWrapper::disconnect] Not yet implemented!"); + } +} +#endif diff --git a/cpp/OscillatorNode/OscillatorNodeWrapper.h b/cpp/OscillatorNode/OscillatorNodeWrapper.h index 98efe10e..fefc2642 100644 --- a/cpp/OscillatorNode/OscillatorNodeWrapper.h +++ b/cpp/OscillatorNode/OscillatorNodeWrapper.h @@ -5,25 +5,27 @@ #ifdef ANDROID #include "OscillatorNode.h" +#else +#include "IOSOscillator.h" #endif namespace audiocontext { - using namespace facebook; - #ifdef ANDROID class OscillatorNode; #endif class OscillatorNodeWrapper: public AudioNodeWrapper { #ifdef ANDROID - private: std::shared_ptr oscillator_; +#else + std::shared_ptr oscillator_; +#endif + public: explicit OscillatorNodeWrapper(const std::shared_ptr &oscillator) : AudioNodeWrapper( oscillator), oscillator_(oscillator) {} #else - public: - explicit OscillatorNodeWrapper() {} + explicit OscillatorNodeWrapper() : oscillator_(std::make_shared()) {} #endif double getFrequency(); double getDetune(); diff --git a/cpp/OscillatorNode/android/OscillatorNodeWrapper.cpp b/cpp/OscillatorNode/android/OscillatorNodeWrapper.cpp index 2260e986..8a1950c9 100644 --- a/cpp/OscillatorNode/android/OscillatorNodeWrapper.cpp +++ b/cpp/OscillatorNode/android/OscillatorNodeWrapper.cpp @@ -1,3 +1,4 @@ +#ifdef ANDROID #include "OscillatorNodeWrapper.h" namespace audiocontext { @@ -34,3 +35,4 @@ namespace audiocontext { oscillator_->setWaveType(type); } } +#endif \ No newline at end of file diff --git a/cpp/OscillatorNode/ios/.keep b/cpp/OscillatorNode/ios/.keep deleted file mode 100644 index e69de29b..00000000 diff --git a/cpp/OscillatorNode/ios/OscillatorNodeWrapper.cpp b/cpp/OscillatorNode/ios/OscillatorNodeWrapper.cpp new file mode 100644 index 00000000..74e1427c --- /dev/null +++ b/cpp/OscillatorNode/ios/OscillatorNodeWrapper.cpp @@ -0,0 +1,38 @@ +#ifndef ANDROID +#include "OscillatorNodeWrapper.h" + +namespace audiocontext { + + double OscillatorNodeWrapper::getFrequency() { + return oscillator_->getFrequency(); + } + + double OscillatorNodeWrapper::getDetune() { + return oscillator_->getDetune(); + } + + std::string OscillatorNodeWrapper::getType() { + return oscillator_->getType(); + } + + void OscillatorNodeWrapper::start(double time) { + oscillator_->start(); + } + + void OscillatorNodeWrapper::stop(double time) { + oscillator_->stop(); + } + + void OscillatorNodeWrapper::setFrequency(double frequency) { + oscillator_->changeFrequency(frequency); + } + + void OscillatorNodeWrapper::setDetune(double detune) { + oscillator_->changeDetune(detune); + } + + void OscillatorNodeWrapper::setType(const std::string& type) { + oscillator_->setType(type); + } +} +#endif diff --git a/example/src/App.tsx b/example/src/App.tsx index e77ef58c..2490feb0 100644 --- a/example/src/App.tsx +++ b/example/src/App.tsx @@ -1,5 +1,5 @@ /* eslint-disable react/react-in-jsx-scope */ -import { Button, StyleSheet, Text, View } from 'react-native'; +import { Button, Platform, StyleSheet, Text, View } from 'react-native'; import { useRef, useState, useEffect } from 'react'; import { Slider } from '@miblanchard/react-native-slider'; @@ -9,12 +9,12 @@ import { type Gain, } from 'react-native-audio-context'; -const App = () => { +const App: React.FC = () => { const [isPlaying, setIsPlaying] = useState(false); const [gain, setGain] = useState(1.0); const [frequency, setFrequency] = useState(440); const [detune, setDetune] = useState(0); - + const audioContextRef = useRef(null); const oscillatorRef = useRef(null); const gainRef = useRef(null); @@ -29,10 +29,11 @@ const App = () => { gainRef.current = audioContextRef.current.createGain(); gainRef.current.gain = gain; - - const destination = audioContextRef.current.destination; - oscillatorRef.current.connect(gainRef.current); - gainRef.current.connect(destination); + + if (Platform.OS === 'android') { + const destination = audioContextRef.current.destination; + oscillatorRef.current.connect(destination!); + } }; const handleGainChange = (value: number[]) => { @@ -58,7 +59,7 @@ const App = () => { oscillatorRef.current.detune = newValue; } }; - + useEffect(() => { return () => { oscillatorRef.current?.stop(0); diff --git a/ios/AudioContextModule.h b/ios/AudioContextModule.h new file mode 100644 index 00000000..ebb123e4 --- /dev/null +++ b/ios/AudioContextModule.h @@ -0,0 +1,5 @@ +#import + +@interface AudioContextModule : NSObject + +@end diff --git a/ios/JSIExampleModule.mm b/ios/AudioContextModule.mm similarity index 61% rename from ios/JSIExampleModule.mm rename to ios/AudioContextModule.mm index 0e3fb779..8c05ef30 100644 --- a/ios/JSIExampleModule.mm +++ b/ios/AudioContextModule.mm @@ -1,14 +1,15 @@ -#import "JSIExampleModule.h" +#import "AudioContextModule.h" #import #import #import -#import "../cpp/JSIExampleHostObject.h" +#import "../cpp/AudioContext/AudioContextHostObject.h" +#import "../cpp/AudioContext/AudioContextWrapper.h" -@implementation JSIExampleModule +@implementation AudioContextModule -RCT_EXPORT_MODULE(JSIExample) +RCT_EXPORT_MODULE(AudioContextModule) RCT_EXPORT_BLOCKING_SYNCHRONOUS_METHOD(install) { @@ -27,9 +28,10 @@ @implementation JSIExampleModule } auto& runtime = *jsiRuntime; - auto hostObject = std::make_shared(); + auto wrapper = std::make_shared(); + auto hostObject = std::make_shared(wrapper); auto object = jsi::Object::createFromHostObject(runtime, hostObject); - runtime.global().setProperty(runtime, "__JSIExampleProxy", std::move(object)); + runtime.global().setProperty(runtime, "__AudioContext", std::move(object)); NSLog(@"Successfully installed JSI bindings for react-native-audio-context!"); return @true; diff --git a/ios/JSIExampleModule.h b/ios/JSIExampleModule.h deleted file mode 100644 index bcf1dd02..00000000 --- a/ios/JSIExampleModule.h +++ /dev/null @@ -1,5 +0,0 @@ -#import - -@interface JSIExampleModule : NSObject - -@end diff --git a/ios/nodes/AudioNode/AudioNode.h b/ios/nodes/AudioNode/AudioNode.h new file mode 100644 index 00000000..60a6c68c --- /dev/null +++ b/ios/nodes/AudioNode/AudioNode.h @@ -0,0 +1,14 @@ +#import +#import + +@interface AudioNode : NSObject + +@property (nonatomic, readonly) NSInteger numberOfInputs; +@property (nonatomic, readonly) NSInteger numberOfOutputs; +@property (nonatomic, strong) NSMutableArray *connectedNodes; + +- (void)connect:(AudioNode *)node; +- (void)disconnect; +- (void)process; + +@end diff --git a/ios/nodes/AudioNode/AudioNode.m b/ios/nodes/AudioNode/AudioNode.m new file mode 100644 index 00000000..e9791bf6 --- /dev/null +++ b/ios/nodes/AudioNode/AudioNode.m @@ -0,0 +1,29 @@ +#import "AudioNode.h" + +@implementation AudioNode + +- (instancetype)init { + if (self = [super init]) { + _connectedNodes = [NSMutableArray array]; + } + + return self; +} + +- (void)connect:(AudioNode *)node { + if (self.numberOfOutputs > 0) { + [self.connectedNodes addObject:node]; + } +} + +- (void)disconnect { + [self.connectedNodes removeAllObjects]; +} + +- (void)process { + for (AudioNode *node in self.connectedNodes) { + [node process]; + } +} + +@end diff --git a/ios/nodes/AudioScheduledSourceNode/AudioScheduledSourceNode.h b/ios/nodes/AudioScheduledSourceNode/AudioScheduledSourceNode.h new file mode 100644 index 00000000..caa81f39 --- /dev/null +++ b/ios/nodes/AudioScheduledSourceNode/AudioScheduledSourceNode.h @@ -0,0 +1,8 @@ +#import "AudioNode.h" + +@interface AudioScheduledSourceNode : AudioNode + +- (void)start; +- (void)stop; + +@end diff --git a/ios/nodes/AudioScheduledSourceNode/AudioScheduledSourceNode.m b/ios/nodes/AudioScheduledSourceNode/AudioScheduledSourceNode.m new file mode 100644 index 00000000..b2f5ce71 --- /dev/null +++ b/ios/nodes/AudioScheduledSourceNode/AudioScheduledSourceNode.m @@ -0,0 +1,20 @@ +#import "AudioScheduledSourceNode.h" + +@implementation AudioScheduledSourceNode + +- (void)start { + NSLog(@"Attempting to call `start` on a base class where it is not implemented. You must override `start` in a subclass."); + @throw [NSException exceptionWithName:NSInternalInconsistencyException + reason:[NSString stringWithFormat:@"You must override %@ in a subclass", NSStringFromSelector(_cmd)] + userInfo:nil]; +} + +- (void)stop { + NSLog(@"Attempting to call `stop` on a base class where it is not implemented. You must override `stop` in a subclass."); + @throw [NSException exceptionWithName:NSInternalInconsistencyException + reason:[NSString stringWithFormat:@"You must override %@ in a subclass", NSStringFromSelector(_cmd)] + userInfo:nil]; +} + +@end + diff --git a/ios/nodes/Oscillator/IOSOscillator.h b/ios/nodes/Oscillator/IOSOscillator.h new file mode 100644 index 00000000..fe592df1 --- /dev/null +++ b/ios/nodes/Oscillator/IOSOscillator.h @@ -0,0 +1,27 @@ +#pragma once + +#ifdef __OBJC__ // when compiled as Objective-C++ +#import +#else // when compiled as C++ +typedef struct objc_object OscillatorNode; +#endif // __OBJC__ + +#include + +namespace audiocontext { + class IOSOscillator { + public: + explicit IOSOscillator(); + void start() const; + void stop() const; + void changeFrequency(const float frequency) const; + float getFrequency() const; + void changeDetune(const float detune) const; + float getDetune() const; + void setType(const std::string &type) const; + std::string getType() const; + + protected: + OscillatorNode *OscillatorNode_; + }; +} // namespace audiocontext diff --git a/ios/nodes/Oscillator/IOSOscillator.mm b/ios/nodes/Oscillator/IOSOscillator.mm new file mode 100644 index 00000000..5786cec4 --- /dev/null +++ b/ios/nodes/Oscillator/IOSOscillator.mm @@ -0,0 +1,43 @@ +#include + +namespace audiocontext { + + IOSOscillator::IOSOscillator() { + OscillatorNode_ = [[OscillatorNode alloc] init]; + } + + void IOSOscillator::start() const { + [OscillatorNode_ start]; + } + + void IOSOscillator::stop() const { + [OscillatorNode_ stop]; + } + + void IOSOscillator::changeFrequency(const float frequency) const { + [OscillatorNode_ changeFrequency:frequency]; + } + + float IOSOscillator::getFrequency() const { + return [OscillatorNode_ getFrequency]; + } + + void IOSOscillator::changeDetune(const float detune) const { + [OscillatorNode_ changeDetune:detune]; + } + + float IOSOscillator::getDetune() const { + return [OscillatorNode_ getDetune]; + } + + void IOSOscillator::setType(const std::string &type) const { + NSString *nsType = [NSString stringWithUTF8String:type.c_str()]; + [OscillatorNode_ setType:nsType]; + } + + std::string IOSOscillator::getType() const { + NSString *nsType = [OscillatorNode_ getType]; + return std::string([nsType UTF8String]); + } + +} // namespace audiocontext diff --git a/ios/nodes/Oscillator/OscillatorNode.h b/ios/nodes/Oscillator/OscillatorNode.h new file mode 100644 index 00000000..fd88b6c4 --- /dev/null +++ b/ios/nodes/Oscillator/OscillatorNode.h @@ -0,0 +1,37 @@ +#import +#import +#import "WaveType.h" +#import "AudioScheduledSourceNode.h" + +static const double OCTAVE_IN_CENTS = 12 * 100; + +@interface OscillatorNode : AudioScheduledSourceNode + +@property (nonatomic, strong) AVAudioEngine *audioEngine; +@property (nonatomic, strong) AVAudioPlayerNode *playerNode; +@property (nonatomic, strong) AVAudioPCMBuffer *buffer; +@property (nonatomic, strong) AVAudioFormat *format; +@property (nonatomic, assign) WaveTypeEnum waveType; +@property (nonatomic, assign) float frequency; +@property (nonatomic, assign) float detune; +@property (nonatomic, assign) double sampleRate; + +- (instancetype)init; + +- (void)start; + +- (void)stop; + +- (void)changeFrequency:(float)frequency; + +- (float)getFrequency; + +- (void)changeDetune:(float)detune; + +- (float)getDetune; + +- (void)setType:(NSString *)type; + +- (NSString *)getType; + +@end diff --git a/ios/nodes/Oscillator/OscillatorNode.m b/ios/nodes/Oscillator/OscillatorNode.m new file mode 100644 index 00000000..9c183ad7 --- /dev/null +++ b/ios/nodes/Oscillator/OscillatorNode.m @@ -0,0 +1,92 @@ +#import + +@implementation OscillatorNode + +- (instancetype)init { + if (self != [super init]) { + return self; + } + + self.frequency = 440; + self.detune = 0; + self.sampleRate = 44100; + self.waveType = WaveTypeSine; + + self.audioEngine = [[AVAudioEngine alloc] init]; + self.playerNode = [[AVAudioPlayerNode alloc] init]; + + self.format = [[AVAudioFormat alloc] initStandardFormatWithSampleRate:self.sampleRate channels:1]; + AVAudioFrameCount bufferFrameCapacity = (AVAudioFrameCount)self.sampleRate; + self.buffer = [[AVAudioPCMBuffer alloc] initWithPCMFormat:self.format frameCapacity:bufferFrameCapacity]; + self.buffer.frameLength = bufferFrameCapacity; + + [self.audioEngine attachNode:self.playerNode]; + [self.audioEngine connect:self.playerNode to:self.audioEngine.mainMixerNode format: self.format]; + [self setBuffer]; + + NSError *error = nil; + if (![self.audioEngine startAndReturnError:&error]) { + NSLog(@"Error starting audio engine: %@", [error localizedDescription]); + } + + return self; +} + +- (void)start { + + [self.playerNode scheduleBuffer:self.buffer atTime:nil options:AVAudioPlayerNodeBufferLoops completionHandler:nil]; + [self.playerNode play]; +} + +- (void)stop { + [self.playerNode stop]; +} + +- (void)setBuffer { + AVAudioFrameCount bufferFrameCapacity = (AVAudioFrameCount)self.sampleRate; + + // Convert cents to HZ + double detuneRatio = pow(2.0, self.detune / OCTAVE_IN_CENTS); + double detunedFrequency = detuneRatio * self.frequency; + double phaseIncrement = FULL_CIRCLE_RADIANS * detunedFrequency / self.sampleRate; + double phase = 0.0; + float *audioBufferPointer = self.buffer.floatChannelData[0]; + + for (int frame = 0; frame < bufferFrameCapacity; frame++) { + audioBufferPointer[frame] = [WaveType valueForWaveType:self.waveType atPhase:phase]; + + phase += phaseIncrement; + if (phase > FULL_CIRCLE_RADIANS) { + phase -= FULL_CIRCLE_RADIANS; + } + } +} + +- (void)changeFrequency:(float)frequency { + self.frequency = frequency; + [self setBuffer]; +} + +- (float)getFrequency { + return self.frequency; +} + +- (void)changeDetune:(float)detune { + self.detune = detune; + [self setBuffer]; +} + +- (float)getDetune { + return self.detune; +} + +- (void)setType:(NSString *)type { + self.waveType = [WaveType waveTypeFromString:type]; + [self setBuffer]; // Update the buffer with the new wave type +} + +- (NSString *)getType { + return [WaveType stringFromWaveType:self.waveType]; +} + +@end diff --git a/ios/nodes/Oscillator/WaveType/WaveType.h b/ios/nodes/Oscillator/WaveType/WaveType.h new file mode 100644 index 00000000..6b1709fc --- /dev/null +++ b/ios/nodes/Oscillator/WaveType/WaveType.h @@ -0,0 +1,18 @@ +#import + +static const float FULL_CIRCLE_RADIANS = 2 * M_PI; + +typedef NS_ENUM(NSInteger, WaveTypeEnum) { + WaveTypeSine, + WaveTypeSquare, + WaveTypeSawtooth, + WaveTypeTriangle +}; + +@interface WaveType : NSObject + ++ (WaveTypeEnum)waveTypeFromString:(NSString *)type; ++ (NSString *)stringFromWaveType:(WaveTypeEnum)waveType; ++ (float)valueForWaveType:(WaveTypeEnum)waveType atPhase:(double)phase; + +@end diff --git a/ios/nodes/Oscillator/WaveType/WaveType.m b/ios/nodes/Oscillator/WaveType/WaveType.m new file mode 100644 index 00000000..2be34b1e --- /dev/null +++ b/ios/nodes/Oscillator/WaveType/WaveType.m @@ -0,0 +1,60 @@ +#import "WaveType.h" +#import + +@implementation WaveType + ++ (WaveTypeEnum)waveTypeFromString:(NSString *)type { + if ([type isEqualToString:@"sine"]) { + return WaveTypeSine; + } + + if ([type isEqualToString:@"square"]) { + return WaveTypeSquare; + } + + if ([type isEqualToString:@"sawtooth"]) { + return WaveTypeSawtooth; + } + + if ([type isEqualToString:@"triangle"]) { + return WaveTypeTriangle; + } + + @throw [NSException exceptionWithName:@"Invalid wave type" reason:@"You have to pick correct wave type: ['sine', 'square', 'sawtooth', 'trinagle']" userInfo:nil]; +} + ++ (NSString *)stringFromWaveType:(WaveTypeEnum)waveType { + switch (waveType) { + case WaveTypeSine: + return @"sine"; + case WaveTypeSquare: + return @"square"; + case WaveTypeSawtooth: + return @"sawtooth"; + case WaveTypeTriangle: + return @"triangle"; + default: + @throw [NSException exceptionWithName:@"Invalid wave type" reason:@"You have to pick correct wave type: ['sine', 'square', 'sawtooth', 'trinagle']" userInfo:nil]; + } + + return nil; +} + ++ (float)valueForWaveType:(WaveTypeEnum)waveType atPhase:(double)phase { + switch (waveType) { + case WaveTypeSine: + return (float)sin(phase); + case WaveTypeSquare: + return (float)(sin(phase) >= 0 ? 1 : -1); + case WaveTypeSawtooth: + return (float)(2 * (phase / FULL_CIRCLE_RADIANS - floor(phase / FULL_CIRCLE_RADIANS + 0.5))); + case WaveTypeTriangle: + return (float)(2 * fabs(2 * (phase / FULL_CIRCLE_RADIANS - floor(phase / FULL_CIRCLE_RADIANS + 0.5))) - 1); + default: + @throw [NSException exceptionWithName:@"Invalid wave type" reason:@"You have to pick correct wave type: ['sine', 'square', 'sawtooth', 'trinagle']" userInfo:nil]; + } + + return 0; +} + +@end diff --git a/src/AudioContext.ts b/src/AudioContext.ts new file mode 100644 index 00000000..7f36e857 --- /dev/null +++ b/src/AudioContext.ts @@ -0,0 +1,24 @@ +import { NativeModules, Platform } from 'react-native'; + +import type { + AudioDestinationNode, + BaseAudioContext, + Oscillator, +} from './types'; + +export class AudioContext implements BaseAudioContext { + destination: AudioDestinationNode | null; + + constructor() { + this.destination = null; + + if (Platform.OS === 'android') { + NativeModules.AudioContextModule.installAudioContext(); + this.destination = global.__AudioContext.destination; + } + } + + createOscillator(): Oscillator { + return global.__AudioContext.createOscillator(); + } +} diff --git a/src/index.ts b/src/index.ts index c6ddf66b..59f323be 100644 --- a/src/index.ts +++ b/src/index.ts @@ -6,11 +6,9 @@ import type { AudioDestinationNode, Gain, } from './types'; +import { installACModule } from './utils/install'; -declare global { - function nativeCallSyncHook(): unknown; - var __AudioContext: BaseAudioContext; -} +installACModule(); export class AudioContext implements BaseAudioContext { destination: AudioDestinationNode; diff --git a/src/modules/global.d.ts b/src/modules/global.d.ts new file mode 100644 index 00000000..b7dc178b --- /dev/null +++ b/src/modules/global.d.ts @@ -0,0 +1,11 @@ +import type { Oscillator } from '../types'; + +type AudioContext = { + createOscillator: () => Oscillator; + destination: AudioDestinationNode | null; +}; + +export declare global { + function nativeCallSyncHook(): unknown; + var __AudioContext: AudioContext; +} diff --git a/src/types.ts b/src/types.ts index 58790a68..c34ae0b9 100644 --- a/src/types.ts +++ b/src/types.ts @@ -1,5 +1,5 @@ export interface BaseAudioContext { - destination: AudioDestinationNode; + destination: AudioDestinationNode | null; createOscillator(): Oscillator; createGain(): Gain; } diff --git a/src/utils/install.ts b/src/utils/install.ts new file mode 100644 index 00000000..152d6af9 --- /dev/null +++ b/src/utils/install.ts @@ -0,0 +1,73 @@ +import { NativeModules, Platform } from 'react-native'; + +export function installACModule() { + if (Platform.OS !== 'ios') { + return; + } + + verifyExpoGo(); + + const AudioContextModule = getAudioContextModule(); + + verifyOnDevice(AudioContextModule); + installModule(AudioContextModule); + verifyInstallation(); +} + +function getAudioContextModule() { + const AudioContextModule = NativeModules.AudioContextModule; + if (AudioContextModule == null) { + let message = + 'Failed to install react-native-audio-context: The native `AudioContext` Module could not be found.'; + message += + '\n* Make sure react-native-audio-context is correctly autolinked (run `npx react-native config` to verify)'; + if (Platform.OS === 'ios' || Platform.OS === 'macos') { + message += '\n* Make sure you ran `pod install` in the ios/ directory.'; + } + if (Platform.OS === 'android') { + message += '\n* Make sure gradle is synced.'; + } + message += '\n* Make sure you rebuilt the app.'; + throw new Error(message); + } + return AudioContextModule; +} + +function verifyExpoGo() { + const ExpoConstants = + NativeModules.NativeUnimoduleProxy?.modulesConstants?.ExponentConstants; + if (ExpoConstants != null) { + if (ExpoConstants.appOwnership === 'expo') { + throw new Error( + 'react-native-audio-context is not supported in Expo Go! Use EAS (`expo prebuild`) or eject to a bare workflow instead.' + ); + } else { + throw new Error('\n* Make sure you ran `expo prebuild`.'); + } + } +} + +function verifyOnDevice(Module: any) { + if (global.nativeCallSyncHook == null || Module.install == null) { + throw new Error( + 'Failed to install react-native-audio-context: React Native is not running on-device. Audio Context can only be used when synchronous method invocations (JSI) are possible. If you are using a remote debugger (e.g. Chrome), switch to an on-device debugger (e.g. Flipper) instead.' + ); + } +} + +function installModule(Module: any) { + const result = Module.install(); + if (result !== true) { + throw new Error( + `Failed to install react-native-audio-context: The native Audio Context Module could not be installed! Looks like something went wrong when installing JSI bindings: ${result}` + ); + } +} + +function verifyInstallation() { + if (global.__AudioContext == null) { + throw new Error( + 'Failed to install react-native-audio-context, the native initializer private does not exist. Are you trying to use Audio Context from different JS Runtimes?' + ); + } +}