Skip to content

Commit

Permalink
Playback events support (#6)
Browse files Browse the repository at this point in the history
* feat: added sound chunk played event

* chore: bumped version
  • Loading branch information
demchuk-alex authored Jan 6, 2025
1 parent 3c182ad commit 9dd8fad
Show file tree
Hide file tree
Showing 6 changed files with 58 additions and 4 deletions.
10 changes: 8 additions & 2 deletions ios/ExpoPlayAudioStreamModule.swift
Original file line number Diff line number Diff line change
Expand Up @@ -3,8 +3,9 @@ import AVFoundation
import ExpoModulesCore

let audioDataEvent: String = "AudioData"
let soundIsPlayedEvent: String = "SoundChunkPlayed"

public class ExpoPlayAudioStreamModule: Module, AudioStreamManagerDelegate, MicrophoneDataDelegate {
public class ExpoPlayAudioStreamModule: Module, AudioStreamManagerDelegate, MicrophoneDataDelegate, SoundPlayerDelegate {
private let audioController = AudioController()
private let audioSessionManager = AudioSessionManager()
private lazy var microphone: Microphone = {
Expand All @@ -23,12 +24,13 @@ public class ExpoPlayAudioStreamModule: Module, AudioStreamManagerDelegate, Micr
Name("ExpoPlayAudioStream")

// Defines event names that the module can send to JavaScript.
Events([audioDataEvent])
Events([audioDataEvent, soundIsPlayedEvent])

OnCreate {
print("Setting up Audio Session Manager")
audioSessionManager.delegate = self
microphone.delegate = self
soundPlayer.delegate = self
}

/// Asynchronously starts audio recording with the given settings.
Expand Down Expand Up @@ -363,4 +365,8 @@ public class ExpoPlayAudioStreamModule: Module, AudioStreamManagerDelegate, Micr
// Emit the event to JavaScript
sendEvent(audioDataEvent, eventBody)
}

func onSoundChunkPlayed(_ isFinal: Bool) {
sendEvent(soundIsPlayedEvent, ["isFinal": isFinal])
}
}
9 changes: 9 additions & 0 deletions ios/SoundPlayer.swift
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ import AVFoundation
import ExpoModulesCore

class SoundPlayer {
weak var delegate: SoundPlayerDelegate?
private var audioEngine: AVAudioEngine!

private var inputNode: AVAudioInputNode!
Expand All @@ -13,6 +14,8 @@ class SoundPlayer {
private let bufferAccessQueue = DispatchQueue(label: "com.kinexpoaudiostream.bufferAccessQueue")

private var audioQueue: [(buffer: AVAudioPCMBuffer, promise: RCTPromiseResolveBlock, turnId: String)] = [] // Queue for audio segments
// needed to track segments in progress in order to send playbackevents properly
private var segmentsLeftToPlay: Int = 0
private var isPlaying: Bool = false // Tracks if audio is currently playing
private var isInterrupted: Bool = false
private var isAudioEngineIsSetup: Bool = false
Expand Down Expand Up @@ -58,6 +61,8 @@ class SoundPlayer {
self.audioPlayerNode.pause()
self.audioPlayerNode.stop()

self.segmentsLeftToPlay = 0

self.isPlaying = false
} else {
Logger.debug("Player is not playing")
Expand Down Expand Up @@ -102,6 +107,7 @@ class SoundPlayer {
}
let bufferTuple = (buffer: pcmBuffer, promise: resolver, turnId: strTurnId)
audioQueue.append(bufferTuple)
self.segmentsLeftToPlay += 1
print("New Chunk \(isPlaying)")
// If not already playing, start playback
playNextInQueue()
Expand Down Expand Up @@ -132,6 +138,9 @@ class SoundPlayer {
self.audioQueue.removeFirst()

self.audioPlayerNode.scheduleBuffer(buffer) {
self.segmentsLeftToPlay -= 1
let isFinalSegment = self.segmentsLeftToPlay == 0
self.delegate?.onSoundChunkPlayed(isFinalSegment)
promise(nil)


Expand Down
3 changes: 3 additions & 0 deletions ios/SoundPlayerDelegate.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
protocol SoundPlayerDelegate: AnyObject {
func onSoundChunkPlayed(_ isFinal: Bool)
}
2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "@mykin-ai/expo-audio-stream",
"version": "0.2.5",
"version": "0.2.6",
"description": "Expo Play Audio Stream module",
"main": "build/index.js",
"types": "build/index.d.ts",
Expand Down
10 changes: 10 additions & 0 deletions src/events.ts
Original file line number Diff line number Diff line change
Expand Up @@ -21,8 +21,18 @@ export interface AudioEventPayload {
streamUuid: string
}

export type SoundChunkPlayedEventPayload = {
isFinal: boolean
}

export function addAudioEventListener(
listener: (event: AudioEventPayload) => Promise<void>
): Subscription {
return emitter.addListener<AudioEventPayload>('AudioData', listener)
}

export function addSoundChunkPlayedListener(
listener: (event: SoundChunkPlayedEventPayload) => Promise<void>
): Subscription {
return emitter.addListener<SoundChunkPlayedEventPayload>('SoundChunkPlayed', listener)
}
28 changes: 27 additions & 1 deletion src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@ import {
StartRecordingResult,
} from "./types";

import { addAudioEventListener, AudioEventPayload } from "./events";
import { addAudioEventListener, addSoundChunkPlayedListener, AudioEventPayload, SoundChunkPlayedEventPayload } from "./events";

export class ExpoPlayAudioStream {
/**
Expand Down Expand Up @@ -262,6 +262,19 @@ export class ExpoPlayAudioStream {
}
}

/**
* Subscribes to audio events emitted during recording/streaming.
* @param onMicrophoneStream - Callback function that will be called when audio data is received.
* The callback receives an AudioDataEvent containing:
* - data: Base64 encoded audio data at original sample rate
* - data16kHz: Optional base64 encoded audio data resampled to 16kHz
* - position: Current position in the audio stream
* - fileUri: URI of the recording file
* - eventDataSize: Size of the current audio data chunk
* - totalSize: Total size of recorded audio so far
* @returns {Subscription} A subscription object that can be used to unsubscribe from the events
* @throws {Error} If encoded audio data is missing from the event
*/
static subscribeToAudioEvents(
onMicrophoneStream: (event: AudioDataEvent) => Promise<void>
): Subscription {
Expand All @@ -282,10 +295,23 @@ export class ExpoPlayAudioStream {
});
});
}

/**
* Subscribes to events emitted when a sound chunk has finished playing.
* @param onSoundChunkPlayed - Callback function that will be called when a sound chunk is played.
* The callback receives a SoundChunkPlayedEventPayload indicating if this was the final chunk.
* @returns {Subscription} A subscription object that can be used to unsubscribe from the events.
*/
static subscribeToSoundChunkPlayed(
onSoundChunkPlayed: (event: SoundChunkPlayedEventPayload) => Promise<void>
): Subscription {
return addSoundChunkPlayedListener(onSoundChunkPlayed);
}
}

export {
AudioDataEvent,
SoundChunkPlayedEventPayload,
AudioRecording,
RecordingConfig,
StartRecordingResult,
Expand Down

0 comments on commit 9dd8fad

Please sign in to comment.