|
1 | 1 | import { Utils } from '@nativescript/core'; |
2 | 2 | import { |
3 | 3 | AnalyserOptions, |
| 4 | + AudioBufferCopyOptions, |
4 | 5 | AudioContextOptions, |
5 | 6 | AudioNodeBase, |
6 | 7 | AudioParamBase, |
7 | 8 | AudioParamHooks, |
8 | 9 | BaseAudioContext, |
| 10 | + ChannelMergerOptions, |
| 11 | + ChannelSplitterOptions, |
9 | 12 | ConstantSourceOptions, |
10 | 13 | ConvolverOptions, |
11 | 14 | DelayOptions, |
12 | 15 | distanceModelFromNumber, |
13 | 16 | distanceModelToNumber, |
14 | 17 | DistanceModelType, |
| 18 | + DynamicsCompressorOptions, |
15 | 19 | IIRFilterOptions, |
16 | 20 | MediaElementLike, |
17 | 21 | panningModelFromNumber, |
@@ -39,6 +43,40 @@ import { |
39 | 43 |
|
40 | 44 | type NativeBaseAudioContext = BaseAudioContext & { native: org.nativescript.audiocontext.AudioContextInstance }; |
41 | 45 |
|
| 46 | +function normalizeCopyByteOffset(view: ArrayBufferView, byteOffset?: number): number { |
| 47 | + let offset = typeof byteOffset === 'number' && Number.isFinite(byteOffset) ? byteOffset : view.byteOffset; |
| 48 | + offset = Math.max(0, offset | 0); |
| 49 | + if (offset > view.buffer.byteLength) offset = view.buffer.byteLength; |
| 50 | + const align = ((view as any).BYTES_PER_ELEMENT as number) || 1; |
| 51 | + return offset - (offset % align); |
| 52 | +} |
| 53 | + |
| 54 | +function resolveAudioBufferCopyOptions(view: ArrayBufferView, startOrOptions?: number | AudioBufferCopyOptions): { startInChannel: number; byteOffset: number } { |
| 55 | + let startInChannel = 0; |
| 56 | + let byteOffset: number | undefined; |
| 57 | + if (typeof startOrOptions === 'number') { |
| 58 | + startInChannel = startOrOptions; |
| 59 | + } else if (startOrOptions) { |
| 60 | + if (typeof startOrOptions.startInChannel === 'number') startInChannel = startOrOptions.startInChannel; |
| 61 | + if (typeof startOrOptions.byteOffset === 'number') byteOffset = startOrOptions.byteOffset; |
| 62 | + } |
| 63 | + if (!Number.isFinite(startInChannel)) startInChannel = 0; |
| 64 | + startInChannel = Math.max(0, startInChannel | 0); |
| 65 | + return { |
| 66 | + startInChannel, |
| 67 | + byteOffset: normalizeCopyByteOffset(view, byteOffset), |
| 68 | + }; |
| 69 | +} |
| 70 | + |
| 71 | +function viewAtByteOffset(view: Float32Array, byteOffset: number): Float32Array { |
| 72 | + if (byteOffset === view.byteOffset) return view; |
| 73 | + if (byteOffset >= view.buffer.byteLength) return new Float32Array(0); |
| 74 | + const availableBytes = view.buffer.byteLength - byteOffset; |
| 75 | + const length = Math.floor(availableBytes / Float32Array.BYTES_PER_ELEMENT); |
| 76 | + if (length <= 0) return new Float32Array(0); |
| 77 | + return new Float32Array(view.buffer, byteOffset, length); |
| 78 | +} |
| 79 | + |
42 | 80 | function makeAndroidHooks(native: org.nativescript.audiocontext.AudioParam): AudioParamHooks { |
43 | 81 | return { |
44 | 82 | nativeSet(v) { |
@@ -175,19 +213,25 @@ export class AudioBuffer { |
175 | 213 | } |
176 | 214 |
|
177 | 215 | getChannelData(channel: number): Float32Array | null { |
178 | | - const data = this.native.getChannelData(channel); |
| 216 | + const data = this.native.getChannelDataRaw(channel); |
179 | 217 | if (!data) return null; |
180 | 218 | return new Float32Array((ArrayBuffer as any).from(data)); |
181 | 219 | } |
182 | 220 |
|
183 | | - copyFromChannel(dest: Float32Array, channel: number, startInChannel: number = 0) { |
| 221 | + copyFromChannel(dest: Float32Array, channel: number, startInChannel: number | AudioBufferCopyOptions = 0) { |
184 | 222 | if (!dest) return; |
185 | | - this.native.copyFromChannel(dest as any, channel, startInChannel ?? 0); |
| 223 | + const options = resolveAudioBufferCopyOptions(dest, startInChannel); |
| 224 | + const target = viewAtByteOffset(dest, options.byteOffset); |
| 225 | + if (target.length === 0) return; |
| 226 | + this.native.copyFromChannel(target as any, channel, options.startInChannel); |
186 | 227 | } |
187 | 228 |
|
188 | | - copyToChannel(source: Float32Array, channel: number, startInChannel: number = 0) { |
| 229 | + copyToChannel(source: Float32Array, channel: number, startInChannel: number | AudioBufferCopyOptions = 0) { |
189 | 230 | if (!source) return; |
190 | | - this.native.copyToChannel(source, channel, startInChannel ?? 0); |
| 231 | + const options = resolveAudioBufferCopyOptions(source, startInChannel); |
| 232 | + const input = viewAtByteOffset(source, options.byteOffset); |
| 233 | + if (input.length === 0) return; |
| 234 | + this.native.copyToChannel(input, channel, options.startInChannel); |
191 | 235 | } |
192 | 236 | } |
193 | 237 |
|
@@ -395,7 +439,7 @@ export class AudioScheduledSourceNode extends AudioNode { |
395 | 439 | if (this._nativeEndedWired) return; |
396 | 440 | this._nativeEndedWired = true; |
397 | 441 | try { |
398 | | - this._javaEndedListener = new (org.nativescript.audiocontext.AudioContext as any).EndedListener({ |
| 442 | + this._javaEndedListener = new org.nativescript.audiocontext.AudioContext.EndedListener({ |
399 | 443 | onEnded: () => this._fireEnded(), |
400 | 444 | }); |
401 | 445 | this.native.addEndedListener(this._javaEndedListener); |
@@ -593,6 +637,15 @@ export class OfflineAudioContext extends BaseAudioContext { |
593 | 637 | createConvolver(options?: ConvolverOptions) { |
594 | 638 | return new ConvolverNode(this as never, options ?? {}); |
595 | 639 | } |
| 640 | + createDynamicsCompressor(options?: DynamicsCompressorOptions) { |
| 641 | + return new DynamicsCompressorNode(this as never, options ?? {}); |
| 642 | + } |
| 643 | + createChannelSplitter(options?: ChannelSplitterOptions) { |
| 644 | + return new ChannelSplitterNode(this as never, options ?? {}); |
| 645 | + } |
| 646 | + createChannelMerger(options?: ChannelMergerOptions) { |
| 647 | + return new ChannelMergerNode(this as never, options ?? {}); |
| 648 | + } |
596 | 649 | createPeriodicWave(real: Float32Array | number[], imag: Float32Array | number[], options?: { disableNormalization?: boolean }) { |
597 | 650 | return new PeriodicWave(this as never, { real, imag, disableNormalization: options?.disableNormalization }); |
598 | 651 | } |
@@ -750,6 +803,15 @@ export class AudioContext extends BaseAudioContext { |
750 | 803 | createConvolver(options?: ConvolverOptions) { |
751 | 804 | return new ConvolverNode(this as never, options ?? {}); |
752 | 805 | } |
| 806 | + createDynamicsCompressor(options?: DynamicsCompressorOptions) { |
| 807 | + return new DynamicsCompressorNode(this as never, options ?? {}); |
| 808 | + } |
| 809 | + createChannelSplitter(options?: ChannelSplitterOptions) { |
| 810 | + return new ChannelSplitterNode(this as never, options ?? {}); |
| 811 | + } |
| 812 | + createChannelMerger(options?: ChannelMergerOptions) { |
| 813 | + return new ChannelMergerNode(this as never, options ?? {}); |
| 814 | + } |
753 | 815 | createPeriodicWave(real: Float32Array | number[], imag: Float32Array | number[], options?: { disableNormalization?: boolean }) { |
754 | 816 | return new PeriodicWave(this as never, { real, imag, disableNormalization: options?.disableNormalization }); |
755 | 817 | } |
@@ -1143,6 +1205,103 @@ export class ConvolverNode extends AudioNode { |
1143 | 1205 | } |
1144 | 1206 | } |
1145 | 1207 |
|
| 1208 | +export class DynamicsCompressorNode extends AudioNode { |
| 1209 | + [native_]: org.nativescript.audiocontext.DynamicsCompressorNode; |
| 1210 | + private _threshold: AudioParam | null = null; |
| 1211 | + private _knee: AudioParam | null = null; |
| 1212 | + private _ratio: AudioParam | null = null; |
| 1213 | + private _attack: AudioParam | null = null; |
| 1214 | + private _release: AudioParam | null = null; |
| 1215 | + private _reduction: AudioParam | null = null; |
| 1216 | + |
| 1217 | + constructor(context: NativeBaseAudioContext, options: DynamicsCompressorOptions = {}) { |
| 1218 | + super(context); |
| 1219 | + this[native_] = AudioContext.getInstance().createDynamicsCompressor(context.native); |
| 1220 | + |
| 1221 | + if (typeof options.threshold === 'number') this.threshold.value = options.threshold; |
| 1222 | + if (typeof options.knee === 'number') this.knee.value = options.knee; |
| 1223 | + if (typeof options.ratio === 'number') this.ratio.value = options.ratio; |
| 1224 | + if (typeof options.attack === 'number') this.attack.value = options.attack; |
| 1225 | + if (typeof options.release === 'number') this.release.value = options.release; |
| 1226 | + } |
| 1227 | + |
| 1228 | + get native() { |
| 1229 | + return this[native_]; |
| 1230 | + } |
| 1231 | + |
| 1232 | + get threshold() { |
| 1233 | + return this._threshold || (this._threshold = new AudioParam(nativeCtor_, this.native.getThreshold())); |
| 1234 | + } |
| 1235 | + |
| 1236 | + get knee() { |
| 1237 | + return this._knee || (this._knee = new AudioParam(nativeCtor_, this.native.getKnee())); |
| 1238 | + } |
| 1239 | + |
| 1240 | + get ratio() { |
| 1241 | + return this._ratio || (this._ratio = new AudioParam(nativeCtor_, this.native.getRatio())); |
| 1242 | + } |
| 1243 | + |
| 1244 | + get attack() { |
| 1245 | + return this._attack || (this._attack = new AudioParam(nativeCtor_, this.native.getAttack())); |
| 1246 | + } |
| 1247 | + |
| 1248 | + get release() { |
| 1249 | + return this._release || (this._release = new AudioParam(nativeCtor_, this.native.getRelease())); |
| 1250 | + } |
| 1251 | + |
| 1252 | + get reduction() { |
| 1253 | + return this._reduction || (this._reduction = new AudioParam(nativeCtor_, this.native.getReduction())); |
| 1254 | + } |
| 1255 | +} |
| 1256 | + |
| 1257 | +export class ChannelSplitterNode extends AudioNode { |
| 1258 | + [native_]: org.nativescript.audiocontext.ChannelSplitterNode; |
| 1259 | + private _numberOfOutputs: number; |
| 1260 | + |
| 1261 | + constructor(context: NativeBaseAudioContext, options: ChannelSplitterOptions = {}) { |
| 1262 | + super(context); |
| 1263 | + this._numberOfOutputs = Math.max(1, options.numberOfOutputs ?? 6); |
| 1264 | + this[native_] = AudioContext.getInstance().createChannelSplitter(context.native, this._numberOfOutputs); |
| 1265 | + } |
| 1266 | + |
| 1267 | + get native() { |
| 1268 | + return this[native_]; |
| 1269 | + } |
| 1270 | + |
| 1271 | + get numberOfOutputs() { |
| 1272 | + const native = this.native as org.nativescript.audiocontext.ChannelSplitterNode; |
| 1273 | + if (native && typeof native.getNumberOfOutputs === 'function') { |
| 1274 | + const value = native.getNumberOfOutputs(); |
| 1275 | + if (typeof value === 'number' && Number.isFinite(value)) return value; |
| 1276 | + } |
| 1277 | + return this._numberOfOutputs; |
| 1278 | + } |
| 1279 | +} |
| 1280 | + |
| 1281 | +export class ChannelMergerNode extends AudioNode { |
| 1282 | + [native_]: org.nativescript.audiocontext.ChannelMergerNode; |
| 1283 | + private _numberOfInputs: number; |
| 1284 | + |
| 1285 | + constructor(context: NativeBaseAudioContext, options: ChannelMergerOptions = {}) { |
| 1286 | + super(context); |
| 1287 | + this._numberOfInputs = Math.max(1, options.numberOfInputs ?? 6); |
| 1288 | + this[native_] = AudioContext.getInstance().createChannelMerger(context.native, this._numberOfInputs); |
| 1289 | + } |
| 1290 | + |
| 1291 | + get native() { |
| 1292 | + return this[native_]; |
| 1293 | + } |
| 1294 | + |
| 1295 | + get numberOfInputs() { |
| 1296 | + const native = this.native as org.nativescript.audiocontext.ChannelMergerNode; |
| 1297 | + if (native && typeof native.getNumberOfInputs === 'function') { |
| 1298 | + const value = native.getNumberOfInputs(); |
| 1299 | + if (typeof value === 'number' && Number.isFinite(value)) return value; |
| 1300 | + } |
| 1301 | + return this._numberOfInputs; |
| 1302 | + } |
| 1303 | +} |
| 1304 | + |
1146 | 1305 | export class PeriodicWave { |
1147 | 1306 | [native_]: org.nativescript.audiocontext.PeriodicWave; |
1148 | 1307 | private real: Float32Array; |
|
0 commit comments