@umituz/react-native-sound 1.2.31 → 1.2.33
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/package.json +1 -1
- package/src/application/SoundCommands.ts +233 -0
- package/src/application/SoundEvents.ts +101 -0
- package/src/application/SoundService.ts +185 -0
- package/src/application/interfaces/IAudioService.ts +42 -0
- package/src/application/interfaces/ISoundCache.ts +19 -0
- package/src/domain/errors/SoundError.ts +44 -0
- package/src/domain/value-objects.ts +128 -0
- package/src/index.ts +45 -6
- package/src/infrastructure/AudioConfig.ts +24 -0
- package/src/infrastructure/AudioRepository.ts +216 -0
- package/src/infrastructure/Logger.ts +23 -0
- package/src/presentation/SoundStore.ts +156 -0
- package/src/presentation/useSound.ts +183 -0
- package/src/types.ts +5 -5
- package/src/utils.ts +173 -15
- package/src/AudioManager.ts +0 -338
- package/src/store.ts +0 -45
- package/src/useSound.ts +0 -107
package/package.json
CHANGED
|
@@ -0,0 +1,233 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Generic Command Processor - Eliminates Use Case Duplication
|
|
3
|
+
* Replaces 8 separate use case files with single command pattern
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
import type { SoundSourceValue } from '../domain/value-objects';
|
|
7
|
+
import type { IAudioService, PlaybackStatus } from './interfaces/IAudioService';
|
|
8
|
+
import type { ISoundCache } from './interfaces/ISoundCache';
|
|
9
|
+
import { SoundEvents } from './SoundEvents';
|
|
10
|
+
import { SoundError } from '../domain/errors/SoundError';
|
|
11
|
+
import { Logger } from '../infrastructure/Logger';
|
|
12
|
+
|
|
13
|
+
// ===== Command Definitions =====
|
|
14
|
+
export type SoundCommand =
|
|
15
|
+
| { type: 'PLAY'; payload: PlayCommand }
|
|
16
|
+
| { type: 'PAUSE'; payload: { sound: unknown } }
|
|
17
|
+
| { type: 'RESUME'; payload: { sound: unknown } }
|
|
18
|
+
| { type: 'STOP'; payload: { sound: unknown } }
|
|
19
|
+
| { type: 'SEEK'; payload: { sound: unknown; positionMillis: number } }
|
|
20
|
+
| { type: 'SET_VOLUME'; payload: { sound: unknown; volume: number } }
|
|
21
|
+
| { type: 'SET_RATE'; payload: { sound: unknown; rate: number } }
|
|
22
|
+
| { type: 'PRELOAD'; payload: PreloadCommand }
|
|
23
|
+
| { type: 'UNLOAD'; payload: void }
|
|
24
|
+
| { type: 'CLEAR_CACHE'; payload: void };
|
|
25
|
+
|
|
26
|
+
export interface PlayCommand {
|
|
27
|
+
id: string;
|
|
28
|
+
source: SoundSourceValue;
|
|
29
|
+
volume?: number;
|
|
30
|
+
rate?: number;
|
|
31
|
+
isLooping?: boolean;
|
|
32
|
+
positionMillis?: number;
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
export interface PreloadCommand {
|
|
36
|
+
id: string;
|
|
37
|
+
source: SoundSourceValue;
|
|
38
|
+
volume?: number;
|
|
39
|
+
rate?: number;
|
|
40
|
+
isLooping?: boolean;
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
// ===== Generic Command Processor =====
|
|
44
|
+
export class SoundCommandProcessor {
|
|
45
|
+
private currentSound: unknown | null = null;
|
|
46
|
+
private currentId: string | null = null;
|
|
47
|
+
private currentStatusUpdater: ((status: PlaybackStatus) => void) | null = null;
|
|
48
|
+
private isPlaying = false;
|
|
49
|
+
|
|
50
|
+
constructor(
|
|
51
|
+
private readonly audioService: IAudioService,
|
|
52
|
+
private readonly cache: ISoundCache,
|
|
53
|
+
private readonly events: SoundEvents
|
|
54
|
+
) {}
|
|
55
|
+
|
|
56
|
+
async execute(command: SoundCommand): Promise<void> {
|
|
57
|
+
try {
|
|
58
|
+
await this.handleCommand(command);
|
|
59
|
+
} catch (error) {
|
|
60
|
+
if (error instanceof SoundError) throw error;
|
|
61
|
+
throw SoundError.playbackFailed(error);
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
private async handleCommand(command: SoundCommand): Promise<void> {
|
|
66
|
+
switch (command.type) {
|
|
67
|
+
case 'PLAY':
|
|
68
|
+
await this.handlePlay(command.payload);
|
|
69
|
+
break;
|
|
70
|
+
case 'PAUSE':
|
|
71
|
+
await this.handlePause(command.payload);
|
|
72
|
+
break;
|
|
73
|
+
case 'RESUME':
|
|
74
|
+
await this.handleResume(command.payload);
|
|
75
|
+
break;
|
|
76
|
+
case 'STOP':
|
|
77
|
+
await this.handleStop(command.payload);
|
|
78
|
+
break;
|
|
79
|
+
case 'SEEK':
|
|
80
|
+
await this.handleSeek(command.payload);
|
|
81
|
+
break;
|
|
82
|
+
case 'SET_VOLUME':
|
|
83
|
+
await this.handleSetVolume(command.payload);
|
|
84
|
+
break;
|
|
85
|
+
case 'SET_RATE':
|
|
86
|
+
await this.handleSetRate(command.payload);
|
|
87
|
+
break;
|
|
88
|
+
case 'PRELOAD':
|
|
89
|
+
await this.handlePreload(command.payload);
|
|
90
|
+
break;
|
|
91
|
+
case 'UNLOAD':
|
|
92
|
+
await this.handleUnload();
|
|
93
|
+
break;
|
|
94
|
+
case 'CLEAR_CACHE':
|
|
95
|
+
await this.handleClearCache();
|
|
96
|
+
break;
|
|
97
|
+
}
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
// ===== Command Handlers =====
|
|
101
|
+
private async handlePlay(payload: PlayCommand): Promise<void> {
|
|
102
|
+
const { id, source, volume = 1.0, rate = 1.0, isLooping = false, positionMillis } = payload;
|
|
103
|
+
|
|
104
|
+
// Toggle if same sound
|
|
105
|
+
if (this.currentId === id && this.currentSound && this.isPlaying) {
|
|
106
|
+
await this.audioService.pause(this.currentSound);
|
|
107
|
+
this.isPlaying = false;
|
|
108
|
+
this.events.emit('PLAYBACK_STOPPED');
|
|
109
|
+
return;
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
// Cleanup current
|
|
113
|
+
await this.unloadCurrent();
|
|
114
|
+
|
|
115
|
+
// Try cache first
|
|
116
|
+
const cached = this.cache.get(id);
|
|
117
|
+
if (cached) {
|
|
118
|
+
this.currentSound = cached.sound;
|
|
119
|
+
this.cache.delete(id).catch((error) => Logger.warn('Failed to delete from cache', error));
|
|
120
|
+
} else {
|
|
121
|
+
const statusUpdater = (status: PlaybackStatus) => {
|
|
122
|
+
this.events.emit('STATUS_UPDATE', status);
|
|
123
|
+
this.isPlaying = status.isPlaying;
|
|
124
|
+
};
|
|
125
|
+
|
|
126
|
+
const { sound } = await this.audioService.createSound(
|
|
127
|
+
source,
|
|
128
|
+
{ shouldPlay: true, isLooping, volume, rate, positionMillis },
|
|
129
|
+
statusUpdater
|
|
130
|
+
);
|
|
131
|
+
|
|
132
|
+
this.currentSound = sound;
|
|
133
|
+
this.currentStatusUpdater = statusUpdater;
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
await this.audioService.play(this.currentSound);
|
|
137
|
+
this.currentId = id;
|
|
138
|
+
this.isPlaying = true;
|
|
139
|
+
|
|
140
|
+
this.events.emit('PLAYBACK_STARTED', {
|
|
141
|
+
id,
|
|
142
|
+
source,
|
|
143
|
+
volume,
|
|
144
|
+
rate,
|
|
145
|
+
isLooping,
|
|
146
|
+
});
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
private async handlePause(payload: { sound: unknown }): Promise<void> {
|
|
150
|
+
if (!payload.sound) return;
|
|
151
|
+
await this.audioService.pause(payload.sound);
|
|
152
|
+
this.isPlaying = false;
|
|
153
|
+
this.events.emit('PLAYBACK_STOPPED');
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
private async handleResume(payload: { sound: unknown }): Promise<void> {
|
|
157
|
+
if (!payload.sound) return;
|
|
158
|
+
await this.audioService.play(payload.sound);
|
|
159
|
+
this.isPlaying = true;
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
private async handleStop(payload: { sound: unknown }): Promise<void> {
|
|
163
|
+
if (!payload.sound) return;
|
|
164
|
+
await this.audioService.stop(payload.sound);
|
|
165
|
+
this.isPlaying = false;
|
|
166
|
+
this.events.emit('PLAYBACK_STOPPED');
|
|
167
|
+
this.events.emit('STATE_RESET');
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
private async handleSeek(payload: { sound: unknown; positionMillis: number }): Promise<void> {
|
|
171
|
+
if (!payload.sound) return;
|
|
172
|
+
await this.audioService.setPosition(payload.sound, payload.positionMillis);
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
private async handleSetVolume(payload: { sound: unknown; volume: number }): Promise<void> {
|
|
176
|
+
if (!payload.sound) return;
|
|
177
|
+
await this.audioService.setVolume(payload.sound, payload.volume);
|
|
178
|
+
this.events.emit('VOLUME_CHANGED', payload.volume);
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
private async handleSetRate(payload: { sound: unknown; rate: number }): Promise<void> {
|
|
182
|
+
if (!payload.sound) return;
|
|
183
|
+
await this.audioService.setRate(payload.sound, payload.rate);
|
|
184
|
+
this.events.emit('RATE_CHANGED', payload.rate);
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
private async handlePreload(payload: PreloadCommand): Promise<void> {
|
|
188
|
+
const { id, source, volume = 1.0, rate = 1.0, isLooping = false } = payload;
|
|
189
|
+
|
|
190
|
+
if (this.cache.has(id)) return;
|
|
191
|
+
|
|
192
|
+
const { sound } = await this.audioService.createSound(
|
|
193
|
+
source,
|
|
194
|
+
{ shouldPlay: false, isLooping, volume, rate },
|
|
195
|
+
() => {}
|
|
196
|
+
);
|
|
197
|
+
|
|
198
|
+
await this.cache.set(id, { sound, source, loadedAt: Date.now() });
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
private async handleUnload(): Promise<void> {
|
|
202
|
+
await this.unloadCurrent();
|
|
203
|
+
this.events.emit('STATE_RESET');
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
private async handleClearCache(): Promise<void> {
|
|
207
|
+
await this.cache.clear();
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
// ===== Private Helpers =====
|
|
211
|
+
private async unloadCurrent(): Promise<void> {
|
|
212
|
+
if (this.currentSound) {
|
|
213
|
+
try {
|
|
214
|
+
await this.audioService.unload(this.currentSound);
|
|
215
|
+
} catch (error) {
|
|
216
|
+
Logger.warn('Failed to unload sound', error);
|
|
217
|
+
}
|
|
218
|
+
}
|
|
219
|
+
this.currentStatusUpdater = null;
|
|
220
|
+
this.currentSound = null;
|
|
221
|
+
this.currentId = null;
|
|
222
|
+
this.isPlaying = false;
|
|
223
|
+
}
|
|
224
|
+
|
|
225
|
+
// ===== Public API =====
|
|
226
|
+
getCurrentSound(): unknown | null {
|
|
227
|
+
return this.currentSound;
|
|
228
|
+
}
|
|
229
|
+
|
|
230
|
+
getCurrentId(): string | null {
|
|
231
|
+
return this.currentId;
|
|
232
|
+
}
|
|
233
|
+
}
|
|
@@ -0,0 +1,101 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Event Bus - Eliminates Presenter Pattern
|
|
3
|
+
* Direct communication between commands and store
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
import type { PlaybackStatus } from './interfaces/IAudioService';
|
|
7
|
+
import type { SoundSourceValue } from '../domain/value-objects';
|
|
8
|
+
import { Logger } from '../infrastructure/Logger';
|
|
9
|
+
|
|
10
|
+
// ===== Event Types =====
|
|
11
|
+
export type SoundEvent =
|
|
12
|
+
| { type: 'PLAYBACK_STARTED'; payload: PlaybackStartedEvent }
|
|
13
|
+
| { type: 'PLAYBACK_STOPPED' }
|
|
14
|
+
| { type: 'STATUS_UPDATE'; payload: PlaybackStatus }
|
|
15
|
+
| { type: 'VOLUME_CHANGED'; payload: number }
|
|
16
|
+
| { type: 'RATE_CHANGED'; payload: number }
|
|
17
|
+
| { type: 'STATE_RESET' }
|
|
18
|
+
| { type: 'ERROR'; payload: string };
|
|
19
|
+
|
|
20
|
+
export interface PlaybackStartedEvent {
|
|
21
|
+
id: string;
|
|
22
|
+
source: SoundSourceValue;
|
|
23
|
+
volume: number;
|
|
24
|
+
rate: number;
|
|
25
|
+
isLooping: boolean;
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
// ===== Event Bus Implementation =====
|
|
29
|
+
type EventListener = (payload: unknown) => void;
|
|
30
|
+
|
|
31
|
+
export class SoundEvents {
|
|
32
|
+
private listeners = new Map<string, Set<EventListener>>();
|
|
33
|
+
|
|
34
|
+
on(eventType: string, listener: EventListener): () => void {
|
|
35
|
+
if (!this.listeners.has(eventType)) {
|
|
36
|
+
this.listeners.set(eventType, new Set());
|
|
37
|
+
}
|
|
38
|
+
const listeners = this.listeners.get(eventType);
|
|
39
|
+
if (listeners) {
|
|
40
|
+
listeners.add(listener);
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
// Return unsubscribe function
|
|
44
|
+
return () => {
|
|
45
|
+
this.off(eventType, listener);
|
|
46
|
+
};
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
off(eventType: string, listener: EventListener): void {
|
|
50
|
+
const eventListeners = this.listeners.get(eventType);
|
|
51
|
+
if (eventListeners) {
|
|
52
|
+
eventListeners.delete(listener);
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
emit<T = unknown>(eventType: string, payload?: T): void {
|
|
57
|
+
const eventListeners = this.listeners.get(eventType);
|
|
58
|
+
if (eventListeners) {
|
|
59
|
+
eventListeners.forEach((listener) => {
|
|
60
|
+
try {
|
|
61
|
+
listener(payload as T);
|
|
62
|
+
} catch (error) {
|
|
63
|
+
Logger.error(`Error in event listener for ${eventType}`, error);
|
|
64
|
+
}
|
|
65
|
+
});
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
clear(): void {
|
|
70
|
+
this.listeners.clear();
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
// ===== Convenience Methods =====
|
|
74
|
+
emitPlaybackStarted(event: PlaybackStartedEvent): void {
|
|
75
|
+
this.emit('PLAYBACK_STARTED', event);
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
emitPlaybackStopped(): void {
|
|
79
|
+
this.emit('PLAYBACK_STOPPED');
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
emitStatusUpdate(status: PlaybackStatus): void {
|
|
83
|
+
this.emit('STATUS_UPDATE', status);
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
emitVolumeChanged(volume: number): void {
|
|
87
|
+
this.emit('VOLUME_CHANGED', volume);
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
emitRateChanged(rate: number): void {
|
|
91
|
+
this.emit('RATE_CHANGED', rate);
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
emitStateReset(): void {
|
|
95
|
+
this.emit('STATE_RESET');
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
emitError(error: string): void {
|
|
99
|
+
this.emit('ERROR', error);
|
|
100
|
+
}
|
|
101
|
+
}
|
|
@@ -0,0 +1,185 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Unified Sound Service - Combines Facade + Repository Pattern
|
|
3
|
+
* Single entry point for all sound operations
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
import type { SoundSourceValue } from '../domain/value-objects';
|
|
7
|
+
import type { PlaybackOptions } from '../types';
|
|
8
|
+
import { AudioConfig } from '../infrastructure/AudioConfig';
|
|
9
|
+
import { AudioRepository } from '../infrastructure/AudioRepository';
|
|
10
|
+
import { SoundCommandProcessor, type SoundCommand, type PlayCommand, type PreloadCommand } from './SoundCommands';
|
|
11
|
+
import { SoundEvents } from './SoundEvents';
|
|
12
|
+
import { Logger } from '../infrastructure/Logger';
|
|
13
|
+
|
|
14
|
+
export class SoundService {
|
|
15
|
+
private static instance: SoundService | null = null;
|
|
16
|
+
private repository: AudioRepository;
|
|
17
|
+
private commandProcessor: SoundCommandProcessor;
|
|
18
|
+
private events: SoundEvents;
|
|
19
|
+
|
|
20
|
+
private constructor() {
|
|
21
|
+
this.events = new SoundEvents();
|
|
22
|
+
this.repository = new AudioRepository(this.events);
|
|
23
|
+
this.commandProcessor = new SoundCommandProcessor(
|
|
24
|
+
this.repository.audioService,
|
|
25
|
+
this.repository.cache,
|
|
26
|
+
this.events
|
|
27
|
+
);
|
|
28
|
+
|
|
29
|
+
// Initialize audio config
|
|
30
|
+
AudioConfig.configure().catch((error) => Logger.warn('Failed to configure audio', error));
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
static getInstance(): SoundService {
|
|
34
|
+
if (!this.instance) {
|
|
35
|
+
this.instance = new SoundService();
|
|
36
|
+
}
|
|
37
|
+
return this.instance;
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
// ===== Public API =====
|
|
41
|
+
|
|
42
|
+
/**
|
|
43
|
+
* Play a sound
|
|
44
|
+
*/
|
|
45
|
+
async play(id: string, source: SoundSourceValue, options?: PlaybackOptions): Promise<void> {
|
|
46
|
+
const command: SoundCommand = {
|
|
47
|
+
type: 'PLAY',
|
|
48
|
+
payload: {
|
|
49
|
+
id,
|
|
50
|
+
source,
|
|
51
|
+
volume: options?.volume,
|
|
52
|
+
rate: options?.rate,
|
|
53
|
+
isLooping: options?.isLooping,
|
|
54
|
+
positionMillis: options?.positionMillis,
|
|
55
|
+
},
|
|
56
|
+
};
|
|
57
|
+
await this.commandProcessor.execute(command);
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
/**
|
|
61
|
+
* Pause playback
|
|
62
|
+
*/
|
|
63
|
+
async pause(): Promise<void> {
|
|
64
|
+
const sound = this.commandProcessor.getCurrentSound();
|
|
65
|
+
await this.commandProcessor.execute({ type: 'PAUSE', payload: { sound } });
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
/**
|
|
69
|
+
* Resume playback
|
|
70
|
+
*/
|
|
71
|
+
async resume(): Promise<void> {
|
|
72
|
+
const sound = this.commandProcessor.getCurrentSound();
|
|
73
|
+
await this.commandProcessor.execute({ type: 'RESUME', payload: { sound } });
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
/**
|
|
77
|
+
* Stop playback
|
|
78
|
+
*/
|
|
79
|
+
async stop(): Promise<void> {
|
|
80
|
+
const sound = this.commandProcessor.getCurrentSound();
|
|
81
|
+
await this.commandProcessor.execute({ type: 'STOP', payload: { sound } });
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
/**
|
|
85
|
+
* Seek to position
|
|
86
|
+
*/
|
|
87
|
+
async seek(positionMillis: number): Promise<void> {
|
|
88
|
+
const sound = this.commandProcessor.getCurrentSound();
|
|
89
|
+
await this.commandProcessor.execute({
|
|
90
|
+
type: 'SEEK',
|
|
91
|
+
payload: { sound, positionMillis },
|
|
92
|
+
});
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
/**
|
|
96
|
+
* Set volume
|
|
97
|
+
*/
|
|
98
|
+
async setVolume(volume: number): Promise<void> {
|
|
99
|
+
const sound = this.commandProcessor.getCurrentSound();
|
|
100
|
+
await this.commandProcessor.execute({
|
|
101
|
+
type: 'SET_VOLUME',
|
|
102
|
+
payload: { sound, volume },
|
|
103
|
+
});
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
/**
|
|
107
|
+
* Set rate
|
|
108
|
+
*/
|
|
109
|
+
async setRate(rate: number): Promise<void> {
|
|
110
|
+
const sound = this.commandProcessor.getCurrentSound();
|
|
111
|
+
await this.commandProcessor.execute({
|
|
112
|
+
type: 'SET_RATE',
|
|
113
|
+
payload: { sound, rate },
|
|
114
|
+
});
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
/**
|
|
118
|
+
* Preload a sound
|
|
119
|
+
*/
|
|
120
|
+
async preload(id: string, source: SoundSourceValue, options?: PlaybackOptions): Promise<void> {
|
|
121
|
+
const command: SoundCommand = {
|
|
122
|
+
type: 'PRELOAD',
|
|
123
|
+
payload: {
|
|
124
|
+
id,
|
|
125
|
+
source,
|
|
126
|
+
volume: options?.volume,
|
|
127
|
+
rate: options?.rate,
|
|
128
|
+
isLooping: options?.isLooping,
|
|
129
|
+
},
|
|
130
|
+
};
|
|
131
|
+
await this.commandProcessor.execute(command);
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
/**
|
|
135
|
+
* Unload current sound
|
|
136
|
+
*/
|
|
137
|
+
async unload(): Promise<void> {
|
|
138
|
+
await this.commandProcessor.execute({ type: 'UNLOAD', payload: undefined });
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
/**
|
|
142
|
+
* Clear cache
|
|
143
|
+
*/
|
|
144
|
+
async clearCache(): Promise<void> {
|
|
145
|
+
await this.commandProcessor.execute({ type: 'CLEAR_CACHE', payload: undefined });
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
/**
|
|
149
|
+
* Check if sound is cached
|
|
150
|
+
*/
|
|
151
|
+
isCached(id: string): boolean {
|
|
152
|
+
return this.repository.cache.has(id);
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
/**
|
|
156
|
+
* Get current playing id
|
|
157
|
+
*/
|
|
158
|
+
getCurrentId(): string | null {
|
|
159
|
+
return this.commandProcessor.getCurrentId();
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
/**
|
|
163
|
+
* Subscribe to events
|
|
164
|
+
*/
|
|
165
|
+
on(eventType: string, listener: (payload: unknown) => void): () => void {
|
|
166
|
+
return this.events.on(eventType, listener);
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
/**
|
|
170
|
+
* Cleanup resources
|
|
171
|
+
*/
|
|
172
|
+
async dispose(): Promise<void> {
|
|
173
|
+
await this.unload();
|
|
174
|
+
await this.clearCache();
|
|
175
|
+
this.events.clear();
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
/**
|
|
179
|
+
* Get events instance (for internal use by useSound hook)
|
|
180
|
+
* @internal
|
|
181
|
+
*/
|
|
182
|
+
getEvents(): SoundEvents {
|
|
183
|
+
return this.events;
|
|
184
|
+
}
|
|
185
|
+
}
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Audio Service Interface
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
import type { AVPlaybackStatus } from 'expo-av';
|
|
6
|
+
import type { SoundSourceValue } from '../../domain/value-objects';
|
|
7
|
+
|
|
8
|
+
export interface PlaybackStatus {
|
|
9
|
+
isLoaded: boolean;
|
|
10
|
+
isPlaying: boolean;
|
|
11
|
+
isBuffering: boolean;
|
|
12
|
+
positionMillis: number;
|
|
13
|
+
durationMillis: number;
|
|
14
|
+
rate?: number;
|
|
15
|
+
isLooping: boolean;
|
|
16
|
+
didJustFinish: boolean;
|
|
17
|
+
error?: string;
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
export interface PlaybackOptions {
|
|
21
|
+
shouldPlay: boolean;
|
|
22
|
+
isLooping: boolean;
|
|
23
|
+
volume: number;
|
|
24
|
+
rate: number;
|
|
25
|
+
positionMillis?: number;
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
export interface IAudioService {
|
|
29
|
+
createSound(
|
|
30
|
+
source: SoundSourceValue,
|
|
31
|
+
options: PlaybackOptions,
|
|
32
|
+
onStatusUpdate: (status: PlaybackStatus) => void
|
|
33
|
+
): Promise<{ sound: unknown }>;
|
|
34
|
+
play(sound: unknown): Promise<void>;
|
|
35
|
+
pause(sound: unknown): Promise<void>;
|
|
36
|
+
stop(sound: unknown): Promise<void>;
|
|
37
|
+
unload(sound: unknown): Promise<void>;
|
|
38
|
+
setVolume(sound: unknown, volume: number): Promise<void>;
|
|
39
|
+
setRate(sound: unknown, rate: number): Promise<void>;
|
|
40
|
+
setPosition(sound: unknown, positionMillis: number): Promise<void>;
|
|
41
|
+
getStatus(sound: unknown): Promise<PlaybackStatus | null>;
|
|
42
|
+
}
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Sound Cache Interface
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
import type { SoundSourceValue } from '../../domain/value-objects';
|
|
6
|
+
|
|
7
|
+
export interface CachedSound {
|
|
8
|
+
sound: unknown;
|
|
9
|
+
source: SoundSourceValue;
|
|
10
|
+
loadedAt: number;
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
export interface ISoundCache {
|
|
14
|
+
get(id: string): CachedSound | undefined;
|
|
15
|
+
set(id: string, cached: CachedSound): Promise<void>;
|
|
16
|
+
delete(id: string): Promise<void>;
|
|
17
|
+
has(id: string): boolean;
|
|
18
|
+
clear(): Promise<void>;
|
|
19
|
+
}
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Domain Errors for Sound Operations
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
export enum SoundErrorCode {
|
|
6
|
+
INVALID_SOUND_ID = 'INVALID_SOUND_ID',
|
|
7
|
+
INVALID_SOUND_SOURCE = 'INVALID_SOUND_SOURCE',
|
|
8
|
+
PLAYBACK_FAILED = 'PLAYBACK_FAILED',
|
|
9
|
+
SOUND_NOT_LOADED = 'SOUND_NOT_LOADED',
|
|
10
|
+
INVALID_POSITION = 'INVALID_POSITION',
|
|
11
|
+
CACHE_ERROR = 'CACHE_ERROR',
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
export class SoundError extends Error {
|
|
15
|
+
constructor(
|
|
16
|
+
public readonly code: SoundErrorCode,
|
|
17
|
+
message: string,
|
|
18
|
+
public readonly originalError?: unknown
|
|
19
|
+
) {
|
|
20
|
+
super(message);
|
|
21
|
+
this.name = 'SoundError';
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
static invalidSoundId(): SoundError {
|
|
25
|
+
return new SoundError(SoundErrorCode.INVALID_SOUND_ID, 'Sound id must be a non-empty string');
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
static invalidSoundSource(): SoundError {
|
|
29
|
+
return new SoundError(SoundErrorCode.INVALID_SOUND_SOURCE, 'Sound source cannot be null or undefined');
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
static playbackFailed(error: unknown): SoundError {
|
|
33
|
+
const message = error instanceof Error ? error.message : 'Unknown playback error';
|
|
34
|
+
return new SoundError(SoundErrorCode.PLAYBACK_FAILED, message, error);
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
static soundNotLoaded(): SoundError {
|
|
38
|
+
return new SoundError(SoundErrorCode.SOUND_NOT_LOADED, 'Sound is not loaded');
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
static invalidPosition(): SoundError {
|
|
42
|
+
return new SoundError(SoundErrorCode.INVALID_POSITION, 'Position must be a finite number');
|
|
43
|
+
}
|
|
44
|
+
}
|