sezo-audio-engine 0.0.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +21 -0
- package/README.md +51 -0
- package/android/build.gradle +44 -0
- package/android/cpp/CMakeLists.txt +23 -0
- package/android/cpp/NativeBridge.cpp +6 -0
- package/android/settings.gradle +12 -0
- package/android/src/main/java/expo/modules/audioengine/ExpoAudioEngineModule.kt +549 -0
- package/dist/AudioEngineModule.d.ts +2 -0
- package/dist/AudioEngineModule.js +46 -0
- package/dist/AudioEngineModule.types.d.ts +108 -0
- package/dist/AudioEngineModule.types.js +1 -0
- package/dist/index.d.ts +2 -0
- package/dist/index.js +2 -0
- package/expo-module.config.json +9 -0
- package/ios/AudioEngine/AudioEngineConfig.swift +13 -0
- package/ios/AudioEngine/AudioSessionManager.swift +61 -0
- package/ios/AudioEngine/AudioTrack.swift +72 -0
- package/ios/AudioEngine/NativeAudioEngine.swift +1180 -0
- package/ios/ExpoAudioEngine.podspec +26 -0
- package/ios/ExpoAudioEngineModule.swift +156 -0
- package/package.json +60 -0
|
@@ -0,0 +1,108 @@
|
|
|
1
|
+
export type PlaybackState = 'stopped' | 'playing' | 'paused' | 'recording';
|
|
2
|
+
export interface AudioEngineConfig {
|
|
3
|
+
sampleRate?: number;
|
|
4
|
+
bufferSize?: number;
|
|
5
|
+
maxTracks?: number;
|
|
6
|
+
enableProcessing?: boolean;
|
|
7
|
+
}
|
|
8
|
+
export interface AudioTrack {
|
|
9
|
+
id: string;
|
|
10
|
+
uri: string;
|
|
11
|
+
type?: 'local' | 'remote';
|
|
12
|
+
volume?: number;
|
|
13
|
+
pan?: number;
|
|
14
|
+
muted?: boolean;
|
|
15
|
+
startTimeMs?: number;
|
|
16
|
+
}
|
|
17
|
+
export interface RecordingConfig {
|
|
18
|
+
sampleRate?: number;
|
|
19
|
+
channels?: number;
|
|
20
|
+
format?: 'aac' | 'mp3' | 'wav';
|
|
21
|
+
bitrate?: number;
|
|
22
|
+
quality?: 'low' | 'medium' | 'high';
|
|
23
|
+
enableNoiseGate?: boolean;
|
|
24
|
+
enableNormalization?: boolean;
|
|
25
|
+
}
|
|
26
|
+
export interface RecordingResult {
|
|
27
|
+
uri: string;
|
|
28
|
+
duration: number;
|
|
29
|
+
startTimeMs: number;
|
|
30
|
+
startTimeSamples?: number;
|
|
31
|
+
sampleRate: number;
|
|
32
|
+
channels: number;
|
|
33
|
+
format: 'aac' | 'mp3' | 'wav';
|
|
34
|
+
bitrate?: number;
|
|
35
|
+
fileSize: number;
|
|
36
|
+
}
|
|
37
|
+
export interface ExtractionConfig {
|
|
38
|
+
format?: 'aac' | 'mp3' | 'wav';
|
|
39
|
+
bitrate?: number;
|
|
40
|
+
includeEffects?: boolean;
|
|
41
|
+
outputDir?: string;
|
|
42
|
+
}
|
|
43
|
+
export interface ExtractionResult {
|
|
44
|
+
trackId?: string;
|
|
45
|
+
uri: string;
|
|
46
|
+
duration: number;
|
|
47
|
+
format: 'aac' | 'mp3' | 'wav';
|
|
48
|
+
bitrate?: number;
|
|
49
|
+
fileSize: number;
|
|
50
|
+
}
|
|
51
|
+
export interface MediaMetadata {
|
|
52
|
+
title: string;
|
|
53
|
+
artist?: string;
|
|
54
|
+
album?: string;
|
|
55
|
+
artwork?: string;
|
|
56
|
+
}
|
|
57
|
+
export type AudioEngineEvent = 'playbackStateChange' | 'positionUpdate' | 'playbackComplete' | 'trackLoaded' | 'trackUnloaded' | 'recordingStarted' | 'recordingStopped' | 'extractionProgress' | 'extractionComplete' | 'error';
|
|
58
|
+
export interface AudioEngineError {
|
|
59
|
+
code: string;
|
|
60
|
+
message: string;
|
|
61
|
+
details?: unknown;
|
|
62
|
+
}
|
|
63
|
+
export interface AudioEngine {
|
|
64
|
+
initialize(config: AudioEngineConfig): Promise<void>;
|
|
65
|
+
release(): Promise<void>;
|
|
66
|
+
loadTracks(tracks: AudioTrack[]): Promise<void>;
|
|
67
|
+
unloadTrack(trackId: string): Promise<void>;
|
|
68
|
+
unloadAllTracks(): Promise<void>;
|
|
69
|
+
getLoadedTracks(): AudioTrack[];
|
|
70
|
+
play(): void;
|
|
71
|
+
pause(): void;
|
|
72
|
+
stop(): void;
|
|
73
|
+
seek(positionMs: number): void;
|
|
74
|
+
isPlaying(): boolean;
|
|
75
|
+
getCurrentPosition(): number;
|
|
76
|
+
getDuration(): number;
|
|
77
|
+
setTrackVolume(trackId: string, volume: number): void;
|
|
78
|
+
setTrackMuted(trackId: string, muted: boolean): void;
|
|
79
|
+
setTrackSolo(trackId: string, solo: boolean): void;
|
|
80
|
+
setTrackPan(trackId: string, pan: number): void;
|
|
81
|
+
setTrackPitch(trackId: string, semitones: number): void;
|
|
82
|
+
getTrackPitch(trackId: string): number;
|
|
83
|
+
setTrackSpeed(trackId: string, rate: number): void;
|
|
84
|
+
getTrackSpeed(trackId: string): number;
|
|
85
|
+
setMasterVolume(volume: number): void;
|
|
86
|
+
getMasterVolume(): number;
|
|
87
|
+
setPitch(semitones: number): void;
|
|
88
|
+
getPitch(): number;
|
|
89
|
+
setSpeed(rate: number): void;
|
|
90
|
+
getSpeed(): number;
|
|
91
|
+
setTempoAndPitch(tempo: number, pitch: number): void;
|
|
92
|
+
startRecording(config?: RecordingConfig): Promise<void>;
|
|
93
|
+
stopRecording(): Promise<RecordingResult>;
|
|
94
|
+
isRecording(): boolean;
|
|
95
|
+
setRecordingVolume(volume: number): void;
|
|
96
|
+
extractTrack(trackId: string, config?: ExtractionConfig): Promise<ExtractionResult>;
|
|
97
|
+
extractAllTracks(config?: ExtractionConfig): Promise<ExtractionResult[]>;
|
|
98
|
+
cancelExtraction(jobId?: number): boolean;
|
|
99
|
+
getInputLevel(): number;
|
|
100
|
+
getOutputLevel(): number;
|
|
101
|
+
getTrackLevel(trackId: string): number;
|
|
102
|
+
enableBackgroundPlayback(metadata: MediaMetadata): Promise<void>;
|
|
103
|
+
updateNowPlayingInfo(metadata: Partial<MediaMetadata>): void;
|
|
104
|
+
disableBackgroundPlayback(): Promise<void>;
|
|
105
|
+
addListener(event: AudioEngineEvent, callback: Function): {
|
|
106
|
+
remove: () => void;
|
|
107
|
+
};
|
|
108
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
package/dist/index.d.ts
ADDED
package/dist/index.js
ADDED
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
struct AudioEngineConfig {
|
|
2
|
+
let sampleRate: Double?
|
|
3
|
+
let bufferSize: Double?
|
|
4
|
+
let maxTracks: Int?
|
|
5
|
+
let enableProcessing: Bool?
|
|
6
|
+
|
|
7
|
+
init(dictionary: [String: Any]) {
|
|
8
|
+
sampleRate = dictionary["sampleRate"] as? Double
|
|
9
|
+
bufferSize = dictionary["bufferSize"] as? Double
|
|
10
|
+
maxTracks = dictionary["maxTracks"] as? Int
|
|
11
|
+
enableProcessing = dictionary["enableProcessing"] as? Bool
|
|
12
|
+
}
|
|
13
|
+
}
|
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
import AVFoundation
|
|
2
|
+
|
|
3
|
+
final class AudioSessionManager {
|
|
4
|
+
private let session = AVAudioSession.sharedInstance()
|
|
5
|
+
|
|
6
|
+
func configure(with config: AudioEngineConfig) {
|
|
7
|
+
do {
|
|
8
|
+
try session.setCategory(
|
|
9
|
+
.playAndRecord,
|
|
10
|
+
mode: .default,
|
|
11
|
+
options: [.defaultToSpeaker, .allowBluetooth]
|
|
12
|
+
)
|
|
13
|
+
} catch {
|
|
14
|
+
return
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
if let sampleRate = config.sampleRate {
|
|
18
|
+
try? session.setPreferredSampleRate(sampleRate)
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
if let bufferSize = config.bufferSize {
|
|
22
|
+
let sampleRate = config.sampleRate ?? session.sampleRate
|
|
23
|
+
if sampleRate > 0 {
|
|
24
|
+
let duration = bufferSize / sampleRate
|
|
25
|
+
try? session.setPreferredIOBufferDuration(duration)
|
|
26
|
+
}
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
try? session.setActive(true)
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
func enableBackgroundPlayback(with config: AudioEngineConfig) {
|
|
33
|
+
do {
|
|
34
|
+
try session.setCategory(
|
|
35
|
+
.playback,
|
|
36
|
+
mode: .default,
|
|
37
|
+
options: [.allowBluetooth, .allowAirPlay]
|
|
38
|
+
)
|
|
39
|
+
} catch {
|
|
40
|
+
return
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
if let sampleRate = config.sampleRate {
|
|
44
|
+
try? session.setPreferredSampleRate(sampleRate)
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
if let bufferSize = config.bufferSize {
|
|
48
|
+
let sampleRate = config.sampleRate ?? session.sampleRate
|
|
49
|
+
if sampleRate > 0 {
|
|
50
|
+
let duration = bufferSize / sampleRate
|
|
51
|
+
try? session.setPreferredIOBufferDuration(duration)
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
try? session.setActive(true)
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
func deactivate() {
|
|
59
|
+
try? session.setActive(false, options: .notifyOthersOnDeactivation)
|
|
60
|
+
}
|
|
61
|
+
}
|
|
@@ -0,0 +1,72 @@
|
|
|
1
|
+
import AVFoundation
|
|
2
|
+
|
|
3
|
+
final class AudioTrack {
|
|
4
|
+
let id: String
|
|
5
|
+
let uri: String
|
|
6
|
+
let url: URL
|
|
7
|
+
let file: AVAudioFile
|
|
8
|
+
let playerNode: AVAudioPlayerNode
|
|
9
|
+
let timePitch: AVAudioUnitTimePitch
|
|
10
|
+
let durationMs: Double
|
|
11
|
+
var startTimeMs: Double
|
|
12
|
+
var volume: Double
|
|
13
|
+
var pan: Double
|
|
14
|
+
var muted: Bool
|
|
15
|
+
var solo: Bool
|
|
16
|
+
var pitch: Double
|
|
17
|
+
var speed: Double
|
|
18
|
+
var isAttached: Bool
|
|
19
|
+
|
|
20
|
+
init?(input: [String: Any]) {
|
|
21
|
+
guard let id = input["id"] as? String,
|
|
22
|
+
let uri = input["uri"] as? String,
|
|
23
|
+
let url = AudioTrack.resolveURL(uri: uri) else {
|
|
24
|
+
return nil
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
do {
|
|
28
|
+
let file = try AVAudioFile(forReading: url)
|
|
29
|
+
let sampleRate = file.processingFormat.sampleRate
|
|
30
|
+
let durationMs = sampleRate > 0 ? (Double(file.length) / sampleRate) * 1000.0 : 0.0
|
|
31
|
+
|
|
32
|
+
self.id = id
|
|
33
|
+
self.uri = uri
|
|
34
|
+
self.url = url
|
|
35
|
+
self.file = file
|
|
36
|
+
self.durationMs = durationMs
|
|
37
|
+
self.startTimeMs = input["startTimeMs"] as? Double ?? 0.0
|
|
38
|
+
self.volume = input["volume"] as? Double ?? 1.0
|
|
39
|
+
self.pan = input["pan"] as? Double ?? 0.0
|
|
40
|
+
self.muted = input["muted"] as? Bool ?? false
|
|
41
|
+
self.solo = input["solo"] as? Bool ?? false
|
|
42
|
+
self.pitch = 0.0
|
|
43
|
+
self.speed = 1.0
|
|
44
|
+
self.playerNode = AVAudioPlayerNode()
|
|
45
|
+
self.timePitch = AVAudioUnitTimePitch()
|
|
46
|
+
self.isAttached = false
|
|
47
|
+
} catch {
|
|
48
|
+
return nil
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
func asDictionary() -> [String: Any] {
|
|
53
|
+
return [
|
|
54
|
+
"id": id,
|
|
55
|
+
"uri": uri,
|
|
56
|
+
"volume": volume,
|
|
57
|
+
"pan": pan,
|
|
58
|
+
"muted": muted,
|
|
59
|
+
"startTimeMs": startTimeMs
|
|
60
|
+
]
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
private static func resolveURL(uri: String) -> URL? {
|
|
64
|
+
if uri.hasPrefix("file://") {
|
|
65
|
+
return URL(string: uri)
|
|
66
|
+
}
|
|
67
|
+
if uri.hasPrefix("/") {
|
|
68
|
+
return URL(fileURLWithPath: uri)
|
|
69
|
+
}
|
|
70
|
+
return URL(string: uri)
|
|
71
|
+
}
|
|
72
|
+
}
|