@@ -3,7 +3,86 @@ import { knownFolders, Observable } from '@nativescript/core';
33export const native_ = Symbol ( '[[native]]' ) ;
44export const context_ = Symbol ( '[[context]]' ) ;
55export const nativeCtor_ = Symbol ( '[[nativeConstructor]]' ) ;
6- export class BaseAudioContext extends Observable { }
6+
7+ export type AudioContextState = 'suspended' | 'running' | 'closed' ;
8+
9+ export class AudioEventTarget extends Observable {
10+ addEventListener ( type : string , callback : any , thisArg ?: any ) {
11+ if ( typeof thisArg === 'boolean' ) thisArg = { capture : thisArg } ;
12+ let listener : any = callback ;
13+ if ( callback && typeof callback === 'object' ) {
14+ listener = callback [ `on${ type } ` ] ;
15+ }
16+ if ( typeof listener !== 'function' ) return ;
17+ const wasListening = this . hasListeners ( type ) ;
18+ super . addEventListener ( type , listener , thisArg ) ;
19+ if ( ! wasListening ) this . _onFirstListenerAdded ?.( type ) ;
20+ }
21+
22+ removeEventListener ( type : string , callback ?: any , thisArg ?: any ) {
23+ if ( typeof thisArg === 'boolean' ) thisArg = { capture : thisArg } ;
24+ let listener : any = callback ;
25+ if ( callback && typeof callback === 'object' ) {
26+ listener = callback [ `on${ type } ` ] ;
27+ }
28+ if ( typeof listener === 'function' ) {
29+ super . removeEventListener ( type , listener , thisArg ) ;
30+ } else {
31+ super . removeEventListener ( type ) ;
32+ }
33+ if ( ! this . hasListeners ( type ) ) this . _onLastListenerRemoved ?.( type ) ;
34+ }
35+
36+ dispatchEvent ( event : { type : string ; [ k : string ] : any } ) : boolean {
37+ this . notify ( { ...event , eventName : event . type , object : this } ) ;
38+ return true ;
39+ }
40+
41+ protected _onFirstListenerAdded ?( type : string ) : void ;
42+
43+ protected _onLastListenerRemoved ?( type : string ) : void ;
44+ }
45+
46+ export class BaseAudioContext extends AudioEventTarget {
47+ protected _state : AudioContextState = 'running' ;
48+ private _onstatechange : ( ( ev : { type : 'statechange' } ) => void ) | null = null ;
49+
50+ get state ( ) : AudioContextState {
51+ return this . _state ;
52+ }
53+
54+ get onstatechange ( ) : ( ( ev : { type : 'statechange' } ) => void ) | null {
55+ return this . _onstatechange ;
56+ }
57+
58+ set onstatechange ( cb : ( ( ev : { type : 'statechange' } ) => void ) | null ) {
59+ if ( this . _onstatechange ) super . removeEventListener ( 'statechange' , this . _onstatechange ) ;
60+ this . _onstatechange = typeof cb === 'function' ? cb : null ;
61+ if ( this . _onstatechange ) super . addEventListener ( 'statechange' , this . _onstatechange ) ;
62+ }
63+
64+ protected _setState ( next : AudioContextState ) {
65+ if ( this . _state === next ) return ;
66+ this . _state = next ;
67+ this . dispatchEvent ( { type : 'statechange' , target : this } ) ;
68+ }
69+ }
70+
71+ export class AudioNodeBase extends AudioEventTarget {
72+ protected [ context_ ] : BaseAudioContext ;
73+
74+ constructor ( context : BaseAudioContext ) {
75+ super ( ) ;
76+ if ( ! ( context instanceof BaseAudioContext ) ) {
77+ throw new TypeError ( `${ this . constructor . name } constructor: Argument 1 does not implement interface BaseAudioContext.` ) ;
78+ }
79+ this [ context_ ] = context ;
80+ }
81+
82+ get context ( ) : BaseAudioContext {
83+ return this [ context_ ] ;
84+ }
85+ }
786
887export function normalizeSourcePath ( source : string ) {
988 let filePath = source ;
@@ -20,13 +99,6 @@ export function looksLikePath(source: string): boolean {
2099 return source . indexOf ( '/' ) >= 0 || source . indexOf ( 'file:' ) === 0 || source . indexOf ( '~' ) === 0 ;
21100}
22101
23- export function toUint8Array ( source : ArrayBuffer | ArrayBufferView ) {
24- if ( ArrayBuffer . isView ( source ) ) {
25- return new Uint8Array ( source . buffer , source . byteOffset , source . byteLength ) ;
26- }
27- return new Uint8Array ( source ) ;
28- }
29-
30102export type LatencyHint = 'interactive' | 'balanced' | 'playback' | number ;
31103
32104export interface AudioContextOptions {
@@ -351,3 +423,204 @@ export class AudioListenerBase {
351423 this . upZ . value = uz ;
352424 }
353425}
426+
427+ export type DistanceModelType = 'linear' | 'inverse' | 'exponential' ;
428+ export type PanningModelType = 'equalpower' | 'HRTF' ;
429+
430+ const DISTANCE_MODEL_NAMES : DistanceModelType [ ] = [ 'linear' , 'inverse' , 'exponential' ] ;
431+ const PANNING_MODEL_NAMES : PanningModelType [ ] = [ 'equalpower' , 'HRTF' ] ;
432+
433+ export function distanceModelToNumber ( value : DistanceModelType | number | null | undefined ) : number {
434+ if ( typeof value === 'number' ) return value ;
435+ if ( typeof value !== 'string' ) return 0 ;
436+ const i = DISTANCE_MODEL_NAMES . indexOf ( value as DistanceModelType ) ;
437+ return i === - 1 ? 0 : i ;
438+ }
439+
440+ export function distanceModelFromNumber ( value : number ) : DistanceModelType {
441+ return DISTANCE_MODEL_NAMES [ value | 0 ] ?? 'linear' ;
442+ }
443+
444+ export function panningModelToNumber ( value : PanningModelType | number | null | undefined ) : number {
445+ if ( typeof value === 'number' ) return value ;
446+ if ( typeof value !== 'string' ) return 0 ;
447+ const i = PANNING_MODEL_NAMES . indexOf ( value as PanningModelType ) ;
448+ return i === - 1 ? 0 : i ;
449+ }
450+
451+ export function panningModelFromNumber ( value : number ) : PanningModelType {
452+ return PANNING_MODEL_NAMES [ value | 0 ] ?? 'equalpower' ;
453+ }
454+
455+ export interface MediaElementTapProvider {
456+ attachAudioContextTap ?( contextNative : any ) : any ;
457+ detachAudioContextTap ?( ) : void ;
458+ }
459+
460+ export interface MediaElementLike extends MediaElementTapProvider {
461+ src ?: string ;
462+ currentTime ?: number ;
463+ loop ?: boolean ;
464+ volume ?: number ;
465+ play ?( ) : Promise < void > | void ;
466+ pause ?( ) : void ;
467+ addEventListener ?( type : string , cb : Function , opts ?: any ) : void ;
468+ removeEventListener ?( type : string , cb : Function , opts ?: any ) : void ;
469+ [ k : string ] : any ;
470+ }
471+
472+ const PLAYER_REF_ = Symbol ( '[[playerRef]]' ) ;
473+
474+ export abstract class MediaElementAudioSourceBase {
475+ protected [ context_ ] : BaseAudioContext ;
476+ protected [ PLAYER_REF_ ] : MediaElementLike ;
477+ protected _internalGain : any = null ;
478+ protected _activeSource : any = null ;
479+ protected _decoded : any = null ;
480+ protected _decodingFor : string | null = null ;
481+ protected _wasMuted = false ;
482+ protected _originalVolume = 1 ;
483+ protected _onPlayBound : ( ) => void ;
484+ protected _onPauseBound : ( ) => void ;
485+ protected _onEndedBound : ( ) => void ;
486+ protected _tapNode : any = null ;
487+ protected _usingTap = false ;
488+
489+ constructor ( context : BaseAudioContext , mediaElement : MediaElementLike ) {
490+ if ( ! ( context instanceof BaseAudioContext ) ) throw new TypeError ( 'MediaElementAudioSourceNode: invalid context' ) ;
491+ if ( ! mediaElement || ( typeof mediaElement . play !== 'function' && typeof mediaElement . src !== 'string' ) ) {
492+ throw new TypeError ( 'MediaElementAudioSourceNode: requires a media element with `src` and `play()`' ) ;
493+ }
494+ this [ context_ ] = context ;
495+ this [ PLAYER_REF_ ] = mediaElement ;
496+ this . _internalGain = this . _createGainNode ( ) ;
497+
498+ if ( typeof mediaElement . attachAudioContextTap === 'function' ) {
499+ try {
500+ const tapNative = mediaElement . attachAudioContextTap ( ( this . context as any ) . native ) ;
501+ if ( tapNative ) {
502+ this . _tapNode = this . _wrapTapNative ( tapNative ) ;
503+ if ( this . _tapNode ) {
504+ this . _tapNode . connect ( this . _internalGain ) ;
505+ this . _usingTap = true ;
506+ }
507+ }
508+ } catch ( e ) { }
509+ }
510+
511+ this . _onPlayBound = ( ) => this . _handlePlay ( ) ;
512+ this . _onPauseBound = ( ) => this . _handlePause ( ) ;
513+ this . _onEndedBound = ( ) => this . _handleEnded ( ) ;
514+ mediaElement . addEventListener ?.( 'play' , this . _onPlayBound ) ;
515+ mediaElement . addEventListener ?.( 'pause' , this . _onPauseBound ) ;
516+ mediaElement . addEventListener ?.( 'ended' , this . _onEndedBound ) ;
517+ }
518+ protected abstract _wrapTapNative ( tapNative : any ) : { connect ( t : any ) : void ; disconnect ( t ?: any ) : void ; stop ?( ) : void } | null ;
519+
520+ get mediaElement ( ) : MediaElementLike {
521+ return this [ PLAYER_REF_ ] ;
522+ }
523+
524+ get context ( ) : BaseAudioContext {
525+ return this [ context_ ] ;
526+ }
527+
528+ get _outputNode ( ) {
529+ return this . _internalGain ;
530+ }
531+
532+ connect ( target : any , output ?: number , input ?: number ) {
533+ return this . _internalGain . connect ( target , output , input ) ;
534+ }
535+ disconnect ( target ?: any , output ?: number , input ?: number ) {
536+ return this . _internalGain . disconnect ( target , output , input ) ;
537+ }
538+
539+ disposeMediaElementSource ( ) {
540+ const el = this [ PLAYER_REF_ ] ;
541+ try {
542+ el . removeEventListener ?.( 'play' , this . _onPlayBound ) ;
543+ el . removeEventListener ?.( 'pause' , this . _onPauseBound ) ;
544+ el . removeEventListener ?.( 'ended' , this . _onEndedBound ) ;
545+ } catch ( e ) { }
546+ if ( this . _usingTap ) {
547+ try {
548+ el . detachAudioContextTap ?.( ) ;
549+ } catch ( e ) { }
550+ try {
551+ this . _tapNode ?. disconnect ?.( ) ;
552+ } catch ( e ) { }
553+ this . _tapNode = null ;
554+ this . _usingTap = false ;
555+ }
556+ this . _stopActive ( ) ;
557+ this . _restoreVolume ( ) ;
558+ }
559+
560+ protected abstract _createGainNode ( ) : any ;
561+ protected abstract _createBufferSource ( buffer : any ) : any ;
562+ protected abstract _decodeFromUrl ( src : string ) : Promise < any > ;
563+
564+ protected async _handlePlay ( ) {
565+ if ( this . _usingTap ) return ;
566+ const el = this [ PLAYER_REF_ ] ;
567+ const src = el . src ?? '' ;
568+ if ( ! src ) return ;
569+ try {
570+ if ( this . _decoded == null || this . _decodingFor !== src ) {
571+ this . _decodingFor = src ;
572+ this . _decoded = await this . _decodeFromUrl ( src ) ;
573+ }
574+ this . _muteOriginal ( ) ;
575+ this . _stopActive ( ) ;
576+ const source = this . _createBufferSource ( this . _decoded ) ;
577+ source . loop = ! ! el . loop ;
578+ source . connect ( this . _internalGain ) ;
579+ source . start ( ) ;
580+ this . _activeSource = source ;
581+ } catch ( e ) { }
582+ }
583+
584+ protected _handlePause ( ) {
585+ if ( this . _usingTap ) return ;
586+ this . _stopActive ( ) ;
587+ this . _restoreVolume ( ) ;
588+ }
589+
590+ protected _handleEnded ( ) {
591+ if ( this . _usingTap ) return ;
592+ this . _stopActive ( ) ;
593+ this . _restoreVolume ( ) ;
594+ }
595+
596+ protected _stopActive ( ) {
597+ const s = this . _activeSource ;
598+ this . _activeSource = null ;
599+ if ( ! s ) return ;
600+ try {
601+ s . stop ?.( ) ;
602+ s . disconnect ?.( ) ;
603+ } catch ( e ) { }
604+ }
605+
606+ protected _muteOriginal ( ) {
607+ if ( this . _wasMuted ) return ;
608+ const el = this [ PLAYER_REF_ ] ;
609+ if ( typeof el . volume === 'number' ) {
610+ this . _originalVolume = el . volume ;
611+ try {
612+ el . volume = 0 ;
613+ this . _wasMuted = true ;
614+ } catch ( e ) { }
615+ }
616+ }
617+
618+ protected _restoreVolume ( ) {
619+ if ( ! this . _wasMuted ) return ;
620+ const el = this [ PLAYER_REF_ ] ;
621+ try {
622+ el . volume = this . _originalVolume ;
623+ } catch ( e ) { }
624+ this . _wasMuted = false ;
625+ }
626+ }
0 commit comments