@opentui/core 0.4.4 → 0.5.0
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/README.md +4 -4
- package/Renderable.d.ts +3 -0
- package/audio.d.ts +202 -1
- package/buffer.d.ts +3 -0
- package/chunk-bun-ctxxvhwz.js +17416 -0
- package/chunk-bun-ctxxvhwz.js.map +62 -0
- package/chunk-bun-v3e63tzw.js +10157 -0
- package/chunk-bun-v3e63tzw.js.map +32 -0
- package/{index-7z5n7k9m.js → chunk-node-1j69hr31.js} +147 -37
- package/chunk-node-1j69hr31.js.map +32 -0
- package/{index-za1krqsf.js → chunk-node-savhj5rp.js} +1594 -467
- package/chunk-node-savhj5rp.js.map +61 -0
- package/image.d.ts +105 -0
- package/index.bun.js +15128 -0
- package/index.bun.js.map +41 -0
- package/index.d.ts +2 -0
- package/{index.js → index.node.js} +2134 -90
- package/{index.js.map → index.node.js.map} +40 -5
- package/lib/env.d.ts +1 -0
- package/lib/stdin-parser.d.ts +5 -0
- package/lib/tree-sitter/default-parser-assets.bun.d.ts +1 -0
- package/lib/tree-sitter/default-parsers.d.ts +1 -0
- package/lib/tree-sitter/parsers-config.d.ts +2 -2
- package/lib/tree-sitter/types.d.ts +1 -0
- package/lib/tree-sitter/update-assets.js +124 -58
- package/lib/tree-sitter/update-assets.js.map +3 -3
- package/node-asset-target.d.ts +12 -0
- package/node-assets.d.ts +7 -0
- package/node-assets.js +264 -0
- package/node-assets.js.map +16 -0
- package/package.json +24 -14
- package/parser.worker.js +3353 -188
- package/parser.worker.js.map +18 -12
- package/platform/assets.d.ts +5 -0
- package/platform/ffi.d.ts +2 -0
- package/platform/runtime-assets.bun.d.ts +4 -0
- package/platform/runtime-assets.node.d.ts +4 -0
- package/platform/runtime.d.ts +5 -1
- package/renderables/Image.d.ts +44 -0
- package/renderables/index.d.ts +1 -0
- package/renderer.d.ts +14 -0
- package/runtime-plugin.js +2 -2
- package/runtime-plugin.js.map +1 -1
- package/testing.bun.js +1006 -0
- package/testing.bun.js.map +18 -0
- package/testing.js +4 -3
- package/testing.js.map +3 -3
- package/text-buffer-view.d.ts +2 -5
- package/types.d.ts +8 -0
- package/yoga.bun.js +194 -0
- package/yoga.bun.js.map +9 -0
- package/yoga.js +1 -1
- package/zig-structs.d.ts +45 -24
- package/zig.d.ts +70 -7
- package/index-7z5n7k9m.js.map +0 -32
- package/index-za1krqsf.js.map +0 -58
|
@@ -43,7 +43,7 @@ import {
|
|
|
43
43
|
mergeKeyAliases,
|
|
44
44
|
mergeKeyBindings,
|
|
45
45
|
wrapWithDelegates
|
|
46
|
-
} from "./
|
|
46
|
+
} from "./chunk-node-1j69hr31.js";
|
|
47
47
|
import {
|
|
48
48
|
ASCIIFontSelectionHelper,
|
|
49
49
|
ATTRIBUTE_BASE_BITS,
|
|
@@ -176,6 +176,7 @@ import {
|
|
|
176
176
|
red,
|
|
177
177
|
registerEnvVar,
|
|
178
178
|
renderFontToFrameBuffer,
|
|
179
|
+
resolveBundledFilePath,
|
|
179
180
|
resolveRenderLib,
|
|
180
181
|
reverse,
|
|
181
182
|
rgbToHex,
|
|
@@ -186,13 +187,14 @@ import {
|
|
|
186
187
|
stripAnsiSequences,
|
|
187
188
|
t,
|
|
188
189
|
terminalNamedSingleStrokeKeys,
|
|
190
|
+
toArrayBuffer,
|
|
189
191
|
treeSitterToStyledText,
|
|
190
192
|
treeSitterToTextChunks,
|
|
191
193
|
underline,
|
|
192
194
|
visualizeRenderableTree,
|
|
193
195
|
white,
|
|
194
196
|
yellow
|
|
195
|
-
} from "./
|
|
197
|
+
} from "./chunk-node-savhj5rp.js";
|
|
196
198
|
// src/post/effects.ts
|
|
197
199
|
function toU8(value) {
|
|
198
200
|
return Math.round(Math.max(0, Math.min(1, Number.isFinite(value) ? value : 0)) * 255);
|
|
@@ -2532,7 +2534,9 @@ class SlotRenderable extends Renderable {
|
|
|
2532
2534
|
}
|
|
2533
2535
|
// src/audio.ts
|
|
2534
2536
|
import { EventEmitter } from "events";
|
|
2535
|
-
import {
|
|
2537
|
+
import { randomBytes } from "node:crypto";
|
|
2538
|
+
import { open as openFile, readFile, rename, unlink } from "node:fs/promises";
|
|
2539
|
+
import { basename, dirname, join } from "node:path";
|
|
2536
2540
|
|
|
2537
2541
|
// src/audio-stream/icy/metadata.ts
|
|
2538
2542
|
function parseIcyMetadata(bytes, decoder) {
|
|
@@ -2693,6 +2697,28 @@ class AudioInitializationError extends Error {
|
|
|
2693
2697
|
this.cause = cause;
|
|
2694
2698
|
}
|
|
2695
2699
|
}
|
|
2700
|
+
|
|
2701
|
+
class AudioCaptureStreamError extends Error {
|
|
2702
|
+
context;
|
|
2703
|
+
constructor(message, context, cause) {
|
|
2704
|
+
super(message);
|
|
2705
|
+
this.name = "AudioCaptureStreamError";
|
|
2706
|
+
this.context = context;
|
|
2707
|
+
if (cause !== undefined)
|
|
2708
|
+
this.cause = cause;
|
|
2709
|
+
}
|
|
2710
|
+
}
|
|
2711
|
+
|
|
2712
|
+
class AudioRecorderError extends Error {
|
|
2713
|
+
context;
|
|
2714
|
+
constructor(message, context, cause) {
|
|
2715
|
+
super(message);
|
|
2716
|
+
this.name = "AudioRecorderError";
|
|
2717
|
+
this.context = context;
|
|
2718
|
+
if (cause !== undefined)
|
|
2719
|
+
this.cause = cause;
|
|
2720
|
+
}
|
|
2721
|
+
}
|
|
2696
2722
|
function statusToError(action, status) {
|
|
2697
2723
|
return new Error(`Audio ${action} failed: ${status}`);
|
|
2698
2724
|
}
|
|
@@ -2704,6 +2730,10 @@ var DEFAULT_STREAM_PROBE_BYTES = 1024 * 1024;
|
|
|
2704
2730
|
var STREAM_POLL_INTERVAL_MS = 5;
|
|
2705
2731
|
var MAX_TIMER_DELAY_MS = 2147483647;
|
|
2706
2732
|
var MAX_U32 = 4294967295;
|
|
2733
|
+
var DEFAULT_CAPTURE_CHUNK_FRAMES = 2048;
|
|
2734
|
+
var CAPTURE_DISCARD_BATCH_CHUNKS = 32;
|
|
2735
|
+
var WAV_HEADER_BYTES = 44;
|
|
2736
|
+
var MAX_WAV_DATA_BYTES = BigInt(MAX_U32 - 36);
|
|
2707
2737
|
var INVALID_STREAM_CHUNK_MESSAGE = "Audio stream chunks must be Uint8Array instances";
|
|
2708
2738
|
|
|
2709
2739
|
class AudioStreamError extends Error {
|
|
@@ -2739,6 +2769,32 @@ function resolvePositiveU32(value, fallback, name) {
|
|
|
2739
2769
|
throw new RangeError(`${name} exceeds the supported limit`);
|
|
2740
2770
|
return resolved;
|
|
2741
2771
|
}
|
|
2772
|
+
function resolveAudioCaptureStreamOptions(options, sampleRate) {
|
|
2773
|
+
const channels = resolvePositiveU32(options.channels, 1, "channels");
|
|
2774
|
+
const capacityFrames = resolvePositiveU32(options.capacityFrames, sampleRate, "capacityFrames");
|
|
2775
|
+
const chunkFrames = resolvePositiveU32(options.chunkFrames, DEFAULT_CAPTURE_CHUNK_FRAMES, "chunkFrames");
|
|
2776
|
+
if (chunkFrames > capacityFrames)
|
|
2777
|
+
throw new RangeError("chunkFrames must not exceed capacityFrames");
|
|
2778
|
+
if (chunkFrames > Math.floor(MAX_U32 / channels)) {
|
|
2779
|
+
throw new RangeError("chunkFrames * channels exceeds the supported limit");
|
|
2780
|
+
}
|
|
2781
|
+
return {
|
|
2782
|
+
sampleRate,
|
|
2783
|
+
channels,
|
|
2784
|
+
capacityFrames,
|
|
2785
|
+
chunkFrames,
|
|
2786
|
+
startOptions: options.startOptions,
|
|
2787
|
+
signal: options.signal
|
|
2788
|
+
};
|
|
2789
|
+
}
|
|
2790
|
+
function resolveU32Index(value, name) {
|
|
2791
|
+
if (!Number.isFinite(value) || !Number.isInteger(value) || value < 0) {
|
|
2792
|
+
throw new TypeError(`${name} must be a finite non-negative integer`);
|
|
2793
|
+
}
|
|
2794
|
+
if (value > MAX_U32)
|
|
2795
|
+
throw new RangeError(`${name} exceeds the supported limit`);
|
|
2796
|
+
return value;
|
|
2797
|
+
}
|
|
2742
2798
|
function resolveReconnectOptions(options) {
|
|
2743
2799
|
const maxRetries = options.maxRetries ?? Number.POSITIVE_INFINITY;
|
|
2744
2800
|
const initialDelayMs = options.initialDelayMs ?? 1000;
|
|
@@ -3093,18 +3149,21 @@ class AudioStream extends EventEmitter {
|
|
|
3093
3149
|
return true;
|
|
3094
3150
|
}
|
|
3095
3151
|
dispose() {
|
|
3096
|
-
if (this.disposed) {
|
|
3097
|
-
if (this.nativeStreamId != null && this.closeNativeStream(NativeAudioStreamCloseReason.Disposed) === 0)
|
|
3098
|
-
this.removeOwner();
|
|
3099
|
-
return;
|
|
3100
|
-
}
|
|
3101
|
-
this.disposed = true;
|
|
3102
3152
|
const wasExposed = this.exposed;
|
|
3103
|
-
this.
|
|
3153
|
+
if (!this.disposed) {
|
|
3154
|
+
this.disposed = true;
|
|
3155
|
+
this.lifecycleController.abort();
|
|
3156
|
+
this.setupReject(createAbortError());
|
|
3157
|
+
}
|
|
3104
3158
|
const cleanup = this.stopSource();
|
|
3105
|
-
this.
|
|
3106
|
-
if (
|
|
3107
|
-
|
|
3159
|
+
const closeStatus = this.closeNativeStream(NativeAudioStreamCloseReason.Disposed);
|
|
3160
|
+
if (closeStatus !== 0) {
|
|
3161
|
+
throw new AudioStreamError(`Audio stream destroy failed: ${closeStatus}`, {
|
|
3162
|
+
action: "destroy",
|
|
3163
|
+
status: closeStatus
|
|
3164
|
+
});
|
|
3165
|
+
}
|
|
3166
|
+
this.removeOwner();
|
|
3108
3167
|
cleanup.finally(() => {
|
|
3109
3168
|
if (wasExposed && !this.terminalEventScheduled)
|
|
3110
3169
|
this.emitTerminal("disposed");
|
|
@@ -3772,6 +3831,962 @@ class AudioStream extends EventEmitter {
|
|
|
3772
3831
|
this.removeFromOwner();
|
|
3773
3832
|
}
|
|
3774
3833
|
}
|
|
3834
|
+
var createAudioCaptureStream;
|
|
3835
|
+
var openAudioCaptureStream;
|
|
3836
|
+
var refreshAudioCaptureStreamFinalStats;
|
|
3837
|
+
|
|
3838
|
+
class AudioCaptureStream extends EventEmitter {
|
|
3839
|
+
readable;
|
|
3840
|
+
sampleRate;
|
|
3841
|
+
channels;
|
|
3842
|
+
chunkFrames;
|
|
3843
|
+
closed;
|
|
3844
|
+
init;
|
|
3845
|
+
lifecycleController = new AbortController;
|
|
3846
|
+
streamController = null;
|
|
3847
|
+
nativeStats;
|
|
3848
|
+
currentState = "initializing";
|
|
3849
|
+
pendingFrames = 0;
|
|
3850
|
+
pendingSamples;
|
|
3851
|
+
producerStopAttempted = false;
|
|
3852
|
+
producerStopped = false;
|
|
3853
|
+
producerMayBeRunning = false;
|
|
3854
|
+
ownerRemoved = false;
|
|
3855
|
+
exposed = false;
|
|
3856
|
+
terminal = false;
|
|
3857
|
+
discardRequested = false;
|
|
3858
|
+
discardDecisionScheduled = false;
|
|
3859
|
+
pumpPromise = null;
|
|
3860
|
+
producerCleanupPromise = null;
|
|
3861
|
+
lastCleanupFailure = null;
|
|
3862
|
+
terminalCompletionPromise = null;
|
|
3863
|
+
closedResolve;
|
|
3864
|
+
signalAbortListener = () => this.dispose();
|
|
3865
|
+
static {
|
|
3866
|
+
createAudioCaptureStream = (init) => new AudioCaptureStream(init);
|
|
3867
|
+
openAudioCaptureStream = (stream) => stream.open();
|
|
3868
|
+
refreshAudioCaptureStreamFinalStats = (stream) => stream.refreshFinalStats();
|
|
3869
|
+
}
|
|
3870
|
+
constructor(init) {
|
|
3871
|
+
super();
|
|
3872
|
+
this.init = init;
|
|
3873
|
+
this.sampleRate = init.options.sampleRate;
|
|
3874
|
+
this.channels = init.options.channels;
|
|
3875
|
+
this.chunkFrames = init.options.chunkFrames;
|
|
3876
|
+
this.pendingSamples = new Float32Array(this.chunkFrames * this.channels);
|
|
3877
|
+
this.nativeStats = {
|
|
3878
|
+
sampleRate: this.sampleRate,
|
|
3879
|
+
channels: this.channels,
|
|
3880
|
+
capacityFrames: init.options.capacityFrames,
|
|
3881
|
+
bufferedFrames: 0,
|
|
3882
|
+
framesReceived: 0n,
|
|
3883
|
+
framesRead: 0n,
|
|
3884
|
+
framesDropped: 0n
|
|
3885
|
+
};
|
|
3886
|
+
this.closed = new Promise((resolve) => this.closedResolve = resolve);
|
|
3887
|
+
this.readable = new ReadableStream({
|
|
3888
|
+
start: (controller) => {
|
|
3889
|
+
this.streamController = controller;
|
|
3890
|
+
},
|
|
3891
|
+
pull: () => this.pull(),
|
|
3892
|
+
cancel: () => this.disposeInternal(true)
|
|
3893
|
+
}, { highWaterMark: 0 });
|
|
3894
|
+
this.init.options.signal?.addEventListener("abort", this.signalAbortListener, { once: true });
|
|
3895
|
+
}
|
|
3896
|
+
get state() {
|
|
3897
|
+
return this.currentState;
|
|
3898
|
+
}
|
|
3899
|
+
async open() {
|
|
3900
|
+
if (this.init.options.signal?.aborted) {
|
|
3901
|
+
await this.disposeInternal(false);
|
|
3902
|
+
throw createAbortError();
|
|
3903
|
+
}
|
|
3904
|
+
let result;
|
|
3905
|
+
this.producerMayBeRunning = true;
|
|
3906
|
+
try {
|
|
3907
|
+
result = this.init.start();
|
|
3908
|
+
} catch (cause) {
|
|
3909
|
+
this.currentState = "errored";
|
|
3910
|
+
await this.cleanupProducer();
|
|
3911
|
+
this.closedResolve();
|
|
3912
|
+
throw new AudioCaptureStreamError("Audio capture stream start failed", { action: "start" }, cause);
|
|
3913
|
+
}
|
|
3914
|
+
if (result.status !== 0) {
|
|
3915
|
+
this.producerMayBeRunning = false;
|
|
3916
|
+
this.currentState = "errored";
|
|
3917
|
+
this.removeOwner();
|
|
3918
|
+
this.closedResolve();
|
|
3919
|
+
throw this.operationError("start", result);
|
|
3920
|
+
}
|
|
3921
|
+
if (this.terminal || this.init.options.signal?.aborted) {
|
|
3922
|
+
await (this.terminalCompletionPromise ?? this.disposeInternal(false));
|
|
3923
|
+
throw createAbortError();
|
|
3924
|
+
}
|
|
3925
|
+
let stats;
|
|
3926
|
+
try {
|
|
3927
|
+
stats = this.init.stats();
|
|
3928
|
+
} catch (cause) {
|
|
3929
|
+
stats = { status: -1, stats: null, cause };
|
|
3930
|
+
}
|
|
3931
|
+
if (stats.status !== 0 || stats.stats == null) {
|
|
3932
|
+
this.currentState = "errored";
|
|
3933
|
+
await this.cleanupProducer();
|
|
3934
|
+
this.closedResolve();
|
|
3935
|
+
throw this.operationError("stats", stats);
|
|
3936
|
+
}
|
|
3937
|
+
this.nativeStats = stats.stats;
|
|
3938
|
+
if (this.terminal || this.init.options.signal?.aborted) {
|
|
3939
|
+
await (this.terminalCompletionPromise ?? this.disposeInternal(false));
|
|
3940
|
+
throw createAbortError();
|
|
3941
|
+
}
|
|
3942
|
+
this.currentState = "capturing";
|
|
3943
|
+
this.exposed = true;
|
|
3944
|
+
}
|
|
3945
|
+
getStats() {
|
|
3946
|
+
if (!this.terminal) {
|
|
3947
|
+
const stats = this.refreshStats();
|
|
3948
|
+
if (stats != null && !this.observeProducer())
|
|
3949
|
+
this.scheduleDiscardIfIdle();
|
|
3950
|
+
}
|
|
3951
|
+
return this.publicStats();
|
|
3952
|
+
}
|
|
3953
|
+
stop() {
|
|
3954
|
+
if (this.terminal || this.currentState === "stopping")
|
|
3955
|
+
return;
|
|
3956
|
+
this.currentState = "stopping";
|
|
3957
|
+
const result = this.stopProducer();
|
|
3958
|
+
if (result != null && result.status !== 0)
|
|
3959
|
+
this.fail(this.operationError("stop", result));
|
|
3960
|
+
else
|
|
3961
|
+
this.scheduleDiscardIfIdle();
|
|
3962
|
+
}
|
|
3963
|
+
dispose() {
|
|
3964
|
+
this.disposeInternal(false);
|
|
3965
|
+
}
|
|
3966
|
+
disposeInternal(fromCancel) {
|
|
3967
|
+
if (this.terminal) {
|
|
3968
|
+
if (this.ownerRemoved)
|
|
3969
|
+
return this.terminalCompletionPromise ?? Promise.resolve();
|
|
3970
|
+
return this.retryTerminalCleanup();
|
|
3971
|
+
}
|
|
3972
|
+
this.terminal = true;
|
|
3973
|
+
this.currentState = "disposed";
|
|
3974
|
+
this.refreshFinalStats();
|
|
3975
|
+
this.lifecycleController.abort();
|
|
3976
|
+
const immediateCleanup = !this.producerMayBeRunning || this.producerStopped ? { status: 0 } : this.producerStopAttempted ? null : this.stopProducer();
|
|
3977
|
+
if (immediateCleanup?.status === 0)
|
|
3978
|
+
this.refreshFinalStats();
|
|
3979
|
+
this.terminalCompletionPromise = (async () => {
|
|
3980
|
+
const cleanup = immediateCleanup?.status === 0 ? immediateCleanup : await this.cleanupProducer();
|
|
3981
|
+
this.refreshFinalStats();
|
|
3982
|
+
if (cleanup.status === 0) {
|
|
3983
|
+
this.removeOwner();
|
|
3984
|
+
if (!fromCancel) {
|
|
3985
|
+
try {
|
|
3986
|
+
this.streamController?.close();
|
|
3987
|
+
} catch {}
|
|
3988
|
+
}
|
|
3989
|
+
if (this.exposed)
|
|
3990
|
+
this.emitTerminal("disposed");
|
|
3991
|
+
else
|
|
3992
|
+
this.closedResolve();
|
|
3993
|
+
return;
|
|
3994
|
+
}
|
|
3995
|
+
this.currentState = "errored";
|
|
3996
|
+
const error = this.operationError("destroy", cleanup);
|
|
3997
|
+
try {
|
|
3998
|
+
this.streamController?.error(error);
|
|
3999
|
+
} catch {}
|
|
4000
|
+
if (this.exposed)
|
|
4001
|
+
this.emitTerminal("error", error, error.context);
|
|
4002
|
+
else
|
|
4003
|
+
this.closedResolve();
|
|
4004
|
+
})();
|
|
4005
|
+
return this.terminalCompletionPromise;
|
|
4006
|
+
}
|
|
4007
|
+
pull() {
|
|
4008
|
+
return this.pump();
|
|
4009
|
+
}
|
|
4010
|
+
pump() {
|
|
4011
|
+
if (this.pumpPromise != null)
|
|
4012
|
+
return this.pumpPromise;
|
|
4013
|
+
const pump = Promise.resolve().then(async () => {
|
|
4014
|
+
do {
|
|
4015
|
+
await this.pumpSource();
|
|
4016
|
+
} while (this.discardRequested && !this.terminal);
|
|
4017
|
+
});
|
|
4018
|
+
this.pumpPromise = pump;
|
|
4019
|
+
const clear = () => {
|
|
4020
|
+
if (this.pumpPromise === pump)
|
|
4021
|
+
this.pumpPromise = null;
|
|
4022
|
+
};
|
|
4023
|
+
pump.then(clear, clear);
|
|
4024
|
+
return pump;
|
|
4025
|
+
}
|
|
4026
|
+
async pumpSource() {
|
|
4027
|
+
if (this.discardRequested) {
|
|
4028
|
+
await this.discardNativeRing();
|
|
4029
|
+
return;
|
|
4030
|
+
}
|
|
4031
|
+
const controller = this.streamController;
|
|
4032
|
+
while (!this.terminal) {
|
|
4033
|
+
if (this.discardRequested) {
|
|
4034
|
+
await this.discardNativeRing();
|
|
4035
|
+
return;
|
|
4036
|
+
}
|
|
4037
|
+
const stats = this.refreshStats();
|
|
4038
|
+
if (stats == null || this.terminal)
|
|
4039
|
+
return;
|
|
4040
|
+
const running = this.observeProducer();
|
|
4041
|
+
if (this.terminal)
|
|
4042
|
+
return;
|
|
4043
|
+
const neededFrames = this.chunkFrames - this.pendingFrames;
|
|
4044
|
+
const readableFrames = running ? stats.bufferedFrames >= neededFrames ? neededFrames : 0 : Math.min(neededFrames, stats.bufferedFrames);
|
|
4045
|
+
let framesRead = 0;
|
|
4046
|
+
if (readableFrames > 0) {
|
|
4047
|
+
let result;
|
|
4048
|
+
try {
|
|
4049
|
+
result = this.init.read(readableFrames);
|
|
4050
|
+
} catch (cause) {
|
|
4051
|
+
this.fail(new AudioCaptureStreamError("Audio capture stream read failed", { action: "read" }, cause));
|
|
4052
|
+
return;
|
|
4053
|
+
}
|
|
4054
|
+
if (result.status !== 0) {
|
|
4055
|
+
this.fail(this.operationError("read", result));
|
|
4056
|
+
return;
|
|
4057
|
+
}
|
|
4058
|
+
framesRead = Math.min(readableFrames, result.framesRead);
|
|
4059
|
+
if (framesRead > 0) {
|
|
4060
|
+
const sampleCount = framesRead * this.channels;
|
|
4061
|
+
this.pendingSamples.set(result.frames.subarray(0, sampleCount), this.pendingFrames * this.channels);
|
|
4062
|
+
this.pendingFrames += framesRead;
|
|
4063
|
+
if (this.pendingFrames === this.chunkFrames) {
|
|
4064
|
+
controller.enqueue(this.pendingSamples.slice());
|
|
4065
|
+
this.pendingFrames = 0;
|
|
4066
|
+
if (!running && stats.bufferedFrames <= framesRead)
|
|
4067
|
+
this.finishStopped();
|
|
4068
|
+
return;
|
|
4069
|
+
}
|
|
4070
|
+
}
|
|
4071
|
+
}
|
|
4072
|
+
if (!running && stats.bufferedFrames <= framesRead) {
|
|
4073
|
+
if (this.pendingFrames > 0) {
|
|
4074
|
+
controller.enqueue(this.pendingSamples.slice(0, this.pendingFrames * this.channels));
|
|
4075
|
+
this.pendingFrames = 0;
|
|
4076
|
+
}
|
|
4077
|
+
this.finishStopped();
|
|
4078
|
+
return;
|
|
4079
|
+
}
|
|
4080
|
+
if (!await waitForPoll(this.lifecycleController.signal))
|
|
4081
|
+
return;
|
|
4082
|
+
}
|
|
4083
|
+
}
|
|
4084
|
+
async discardNativeRing() {
|
|
4085
|
+
this.pendingFrames = 0;
|
|
4086
|
+
let chunksThisTurn = 0;
|
|
4087
|
+
while (!this.terminal) {
|
|
4088
|
+
const stats = this.refreshStats();
|
|
4089
|
+
if (stats == null || this.terminal)
|
|
4090
|
+
return;
|
|
4091
|
+
if (stats.bufferedFrames === 0) {
|
|
4092
|
+
this.finishStopped();
|
|
4093
|
+
return;
|
|
4094
|
+
}
|
|
4095
|
+
const frameCount = Math.min(this.chunkFrames, stats.bufferedFrames);
|
|
4096
|
+
let result;
|
|
4097
|
+
try {
|
|
4098
|
+
result = this.init.read(frameCount);
|
|
4099
|
+
} catch (cause) {
|
|
4100
|
+
this.fail(new AudioCaptureStreamError("Audio capture stream read failed", { action: "read" }, cause));
|
|
4101
|
+
return;
|
|
4102
|
+
}
|
|
4103
|
+
if (result.status !== 0) {
|
|
4104
|
+
this.fail(this.operationError("read", result));
|
|
4105
|
+
return;
|
|
4106
|
+
}
|
|
4107
|
+
chunksThisTurn += 1;
|
|
4108
|
+
if (result.framesRead === 0) {
|
|
4109
|
+
if (!await waitForPoll(this.lifecycleController.signal))
|
|
4110
|
+
return;
|
|
4111
|
+
} else if (chunksThisTurn >= CAPTURE_DISCARD_BATCH_CHUNKS) {
|
|
4112
|
+
chunksThisTurn = 0;
|
|
4113
|
+
await waitForDelay(0, this.lifecycleController.signal).catch(() => {
|
|
4114
|
+
return;
|
|
4115
|
+
});
|
|
4116
|
+
}
|
|
4117
|
+
}
|
|
4118
|
+
}
|
|
4119
|
+
requestDiscardDrain() {
|
|
4120
|
+
if (this.terminal || this.discardRequested)
|
|
4121
|
+
return;
|
|
4122
|
+
this.discardRequested = true;
|
|
4123
|
+
this.pump();
|
|
4124
|
+
}
|
|
4125
|
+
scheduleDiscardIfIdle() {
|
|
4126
|
+
if (this.terminal || this.discardRequested || this.discardDecisionScheduled)
|
|
4127
|
+
return;
|
|
4128
|
+
this.discardDecisionScheduled = true;
|
|
4129
|
+
setTimeout(() => {
|
|
4130
|
+
this.discardDecisionScheduled = false;
|
|
4131
|
+
if (this.terminal)
|
|
4132
|
+
return;
|
|
4133
|
+
if (this.readable.locked)
|
|
4134
|
+
this.scheduleDiscardIfIdle();
|
|
4135
|
+
else
|
|
4136
|
+
this.requestDiscardDrain();
|
|
4137
|
+
}, STREAM_POLL_INTERVAL_MS);
|
|
4138
|
+
}
|
|
4139
|
+
refreshStats() {
|
|
4140
|
+
let result;
|
|
4141
|
+
try {
|
|
4142
|
+
result = this.init.stats();
|
|
4143
|
+
} catch (cause) {
|
|
4144
|
+
this.fail(new AudioCaptureStreamError("Audio capture stream stats failed", { action: "stats" }, cause));
|
|
4145
|
+
return null;
|
|
4146
|
+
}
|
|
4147
|
+
if (result.status !== 0 || result.stats == null) {
|
|
4148
|
+
this.fail(this.operationError("stats", result));
|
|
4149
|
+
return null;
|
|
4150
|
+
}
|
|
4151
|
+
this.nativeStats = result.stats;
|
|
4152
|
+
return result.stats;
|
|
4153
|
+
}
|
|
4154
|
+
observeProducer() {
|
|
4155
|
+
if (this.producerStopAttempted)
|
|
4156
|
+
return false;
|
|
4157
|
+
let running;
|
|
4158
|
+
try {
|
|
4159
|
+
running = this.init.isRunning();
|
|
4160
|
+
} catch (cause) {
|
|
4161
|
+
this.fail(new AudioCaptureStreamError("Audio capture stream stats failed", { action: "stats" }, cause));
|
|
4162
|
+
return false;
|
|
4163
|
+
}
|
|
4164
|
+
if (this.terminal)
|
|
4165
|
+
return false;
|
|
4166
|
+
if (running)
|
|
4167
|
+
return true;
|
|
4168
|
+
this.currentState = "stopping";
|
|
4169
|
+
const result = this.stopProducer();
|
|
4170
|
+
if (result != null && result.status !== 0)
|
|
4171
|
+
this.fail(this.operationError("stop", result));
|
|
4172
|
+
return false;
|
|
4173
|
+
}
|
|
4174
|
+
stopProducer() {
|
|
4175
|
+
if (this.producerStopAttempted)
|
|
4176
|
+
return null;
|
|
4177
|
+
this.producerStopAttempted = true;
|
|
4178
|
+
try {
|
|
4179
|
+
const result = this.init.stop();
|
|
4180
|
+
if (result.status === 0) {
|
|
4181
|
+
this.producerStopped = true;
|
|
4182
|
+
this.producerMayBeRunning = false;
|
|
4183
|
+
}
|
|
4184
|
+
return result;
|
|
4185
|
+
} catch (cause) {
|
|
4186
|
+
return { status: -1, cause };
|
|
4187
|
+
}
|
|
4188
|
+
}
|
|
4189
|
+
finishStopped() {
|
|
4190
|
+
if (this.terminal)
|
|
4191
|
+
return;
|
|
4192
|
+
if (this.refreshStats() == null || this.terminal)
|
|
4193
|
+
return;
|
|
4194
|
+
this.terminal = true;
|
|
4195
|
+
this.currentState = "stopped";
|
|
4196
|
+
this.lifecycleController.abort();
|
|
4197
|
+
this.removeOwner();
|
|
4198
|
+
this.streamController?.close();
|
|
4199
|
+
this.emitTerminal("stopped");
|
|
4200
|
+
}
|
|
4201
|
+
fail(error) {
|
|
4202
|
+
if (this.terminal)
|
|
4203
|
+
return;
|
|
4204
|
+
this.terminal = true;
|
|
4205
|
+
this.currentState = "errored";
|
|
4206
|
+
this.lifecycleController.abort();
|
|
4207
|
+
try {
|
|
4208
|
+
this.streamController?.error(error);
|
|
4209
|
+
} catch {}
|
|
4210
|
+
this.terminalCompletionPromise = (async () => {
|
|
4211
|
+
if ((await this.cleanupProducer()).status === 0)
|
|
4212
|
+
this.removeOwner();
|
|
4213
|
+
this.refreshFinalStats();
|
|
4214
|
+
if (this.exposed)
|
|
4215
|
+
this.emitTerminal("error", error, error.context);
|
|
4216
|
+
else
|
|
4217
|
+
this.closedResolve();
|
|
4218
|
+
})();
|
|
4219
|
+
}
|
|
4220
|
+
operationError(action, result) {
|
|
4221
|
+
const context = { action };
|
|
4222
|
+
if (result.status !== 0)
|
|
4223
|
+
context.status = result.status;
|
|
4224
|
+
return new AudioCaptureStreamError(`Audio capture stream ${action} failed${result.status ? `: ${result.status}` : ""}`, context, result.cause);
|
|
4225
|
+
}
|
|
4226
|
+
cleanupProducer() {
|
|
4227
|
+
if (!this.producerMayBeRunning || this.producerStopped)
|
|
4228
|
+
return Promise.resolve({ status: 0 });
|
|
4229
|
+
if (this.producerCleanupPromise != null)
|
|
4230
|
+
return this.producerCleanupPromise;
|
|
4231
|
+
const cleanup = (async () => {
|
|
4232
|
+
let lastFailure = this.lastCleanupFailure ?? { status: -1 };
|
|
4233
|
+
for (let attempt = 0;attempt < 3; attempt += 1) {
|
|
4234
|
+
this.producerStopAttempted = false;
|
|
4235
|
+
const result = this.stopProducer();
|
|
4236
|
+
if (result?.status === 0) {
|
|
4237
|
+
this.lastCleanupFailure = null;
|
|
4238
|
+
return result;
|
|
4239
|
+
}
|
|
4240
|
+
if (result != null) {
|
|
4241
|
+
lastFailure = result;
|
|
4242
|
+
this.lastCleanupFailure = result;
|
|
4243
|
+
}
|
|
4244
|
+
if (attempt < 2)
|
|
4245
|
+
await new Promise((resolve) => setTimeout(resolve, STREAM_POLL_INTERVAL_MS));
|
|
4246
|
+
}
|
|
4247
|
+
return lastFailure;
|
|
4248
|
+
})();
|
|
4249
|
+
this.producerCleanupPromise = cleanup;
|
|
4250
|
+
cleanup.finally(() => {
|
|
4251
|
+
if (this.producerCleanupPromise === cleanup)
|
|
4252
|
+
this.producerCleanupPromise = null;
|
|
4253
|
+
});
|
|
4254
|
+
return cleanup;
|
|
4255
|
+
}
|
|
4256
|
+
retryTerminalCleanup() {
|
|
4257
|
+
return this.cleanupProducer().then((result) => {
|
|
4258
|
+
if (result.status === 0)
|
|
4259
|
+
this.removeOwner();
|
|
4260
|
+
});
|
|
4261
|
+
}
|
|
4262
|
+
publicStats() {
|
|
4263
|
+
return {
|
|
4264
|
+
...this.nativeStats,
|
|
4265
|
+
state: this.currentState,
|
|
4266
|
+
bufferedDurationMs: this.nativeStats.sampleRate === 0 ? 0 : this.nativeStats.bufferedFrames * 1000 / this.nativeStats.sampleRate
|
|
4267
|
+
};
|
|
4268
|
+
}
|
|
4269
|
+
refreshFinalStats() {
|
|
4270
|
+
try {
|
|
4271
|
+
const result = this.init.stats();
|
|
4272
|
+
if (result.status === 0 && result.stats != null)
|
|
4273
|
+
this.nativeStats = result.stats;
|
|
4274
|
+
} catch {}
|
|
4275
|
+
}
|
|
4276
|
+
removeOwner() {
|
|
4277
|
+
if (this.ownerRemoved)
|
|
4278
|
+
return;
|
|
4279
|
+
this.ownerRemoved = true;
|
|
4280
|
+
this.init.options.signal?.removeEventListener("abort", this.signalAbortListener);
|
|
4281
|
+
this.init.removeFromOwner();
|
|
4282
|
+
}
|
|
4283
|
+
emitTerminal(event, ...args) {
|
|
4284
|
+
setTimeout(() => {
|
|
4285
|
+
try {
|
|
4286
|
+
EventEmitter.prototype.emit.call(this, event, ...args);
|
|
4287
|
+
} finally {
|
|
4288
|
+
this.closedResolve();
|
|
4289
|
+
}
|
|
4290
|
+
}, 0);
|
|
4291
|
+
}
|
|
4292
|
+
}
|
|
4293
|
+
var createAudioRecorder;
|
|
4294
|
+
var openAudioRecorder;
|
|
4295
|
+
function createWavHeader(sampleRate, channels, dataBytes) {
|
|
4296
|
+
const header = new Uint8Array(WAV_HEADER_BYTES);
|
|
4297
|
+
const view = new DataView(header.buffer);
|
|
4298
|
+
header.set([82, 73, 70, 70], 0);
|
|
4299
|
+
view.setUint32(4, dataBytes + 36, true);
|
|
4300
|
+
header.set([87, 65, 86, 69], 8);
|
|
4301
|
+
header.set([102, 109, 116, 32], 12);
|
|
4302
|
+
view.setUint32(16, 16, true);
|
|
4303
|
+
view.setUint16(20, 1, true);
|
|
4304
|
+
view.setUint16(22, channels, true);
|
|
4305
|
+
view.setUint32(24, sampleRate, true);
|
|
4306
|
+
view.setUint32(28, sampleRate * channels * 2, true);
|
|
4307
|
+
view.setUint16(32, channels * 2, true);
|
|
4308
|
+
view.setUint16(34, 16, true);
|
|
4309
|
+
header.set([100, 97, 116, 97], 36);
|
|
4310
|
+
view.setUint32(40, dataBytes, true);
|
|
4311
|
+
return header;
|
|
4312
|
+
}
|
|
4313
|
+
|
|
4314
|
+
class AudioRecorder extends EventEmitter {
|
|
4315
|
+
static fileSystem = { open: openFile, rename, unlink };
|
|
4316
|
+
filePath;
|
|
4317
|
+
format = "wav";
|
|
4318
|
+
sampleRate;
|
|
4319
|
+
channels;
|
|
4320
|
+
closed;
|
|
4321
|
+
init;
|
|
4322
|
+
currentState = "initializing";
|
|
4323
|
+
capture = null;
|
|
4324
|
+
reader = null;
|
|
4325
|
+
fileHandle = null;
|
|
4326
|
+
tempPath = null;
|
|
4327
|
+
captureStats;
|
|
4328
|
+
framesWritten = 0n;
|
|
4329
|
+
dataBytesWritten = 0n;
|
|
4330
|
+
stopRequested = false;
|
|
4331
|
+
terminal = null;
|
|
4332
|
+
terminationRequest = null;
|
|
4333
|
+
publicationStarted = false;
|
|
4334
|
+
exposed = false;
|
|
4335
|
+
ownerRemoved = false;
|
|
4336
|
+
cleanupPromise = null;
|
|
4337
|
+
resourceCleanupPromise = null;
|
|
4338
|
+
retainedCleanupScheduled = false;
|
|
4339
|
+
lifecyclePromise = null;
|
|
4340
|
+
closedResolve;
|
|
4341
|
+
signalAbortListener = () => this.dispose();
|
|
4342
|
+
captureErrorListener = (error) => {
|
|
4343
|
+
this.fail(this.fromCaptureError(error));
|
|
4344
|
+
};
|
|
4345
|
+
static {
|
|
4346
|
+
createAudioRecorder = (init) => new AudioRecorder(init);
|
|
4347
|
+
openAudioRecorder = (recorder) => recorder.open();
|
|
4348
|
+
}
|
|
4349
|
+
constructor(init) {
|
|
4350
|
+
super();
|
|
4351
|
+
this.init = init;
|
|
4352
|
+
this.filePath = init.filePath;
|
|
4353
|
+
this.sampleRate = init.captureOptions.sampleRate;
|
|
4354
|
+
this.channels = init.captureOptions.channels;
|
|
4355
|
+
this.captureStats = {
|
|
4356
|
+
state: "initializing",
|
|
4357
|
+
sampleRate: this.sampleRate,
|
|
4358
|
+
channels: this.channels,
|
|
4359
|
+
capacityFrames: init.captureOptions.capacityFrames,
|
|
4360
|
+
bufferedFrames: 0,
|
|
4361
|
+
bufferedDurationMs: 0,
|
|
4362
|
+
framesReceived: 0n,
|
|
4363
|
+
framesRead: 0n,
|
|
4364
|
+
framesDropped: 0n
|
|
4365
|
+
};
|
|
4366
|
+
this.closed = new Promise((resolve) => this.closedResolve = resolve);
|
|
4367
|
+
this.init.signal?.addEventListener("abort", this.signalAbortListener, { once: true });
|
|
4368
|
+
}
|
|
4369
|
+
get state() {
|
|
4370
|
+
return this.currentState;
|
|
4371
|
+
}
|
|
4372
|
+
async open() {
|
|
4373
|
+
try {
|
|
4374
|
+
this.fileHandle = await this.openTemporaryFile();
|
|
4375
|
+
this.ensureOpening();
|
|
4376
|
+
await this.writeFully(new Uint8Array(WAV_HEADER_BYTES), 0, "write");
|
|
4377
|
+
this.ensureOpening();
|
|
4378
|
+
try {
|
|
4379
|
+
this.capture = await this.init.openCapture({
|
|
4380
|
+
channels: this.init.captureOptions.channels,
|
|
4381
|
+
capacityFrames: this.init.captureOptions.capacityFrames,
|
|
4382
|
+
chunkFrames: this.init.captureOptions.chunkFrames,
|
|
4383
|
+
startOptions: this.init.captureOptions.startOptions
|
|
4384
|
+
});
|
|
4385
|
+
this.capture.on("error", this.captureErrorListener);
|
|
4386
|
+
} catch (cause) {
|
|
4387
|
+
if (cause instanceof DOMException && cause.name === "AbortError")
|
|
4388
|
+
throw cause;
|
|
4389
|
+
throw new AudioRecorderError("Audio recorder capture start failed", { action: "start" }, cause);
|
|
4390
|
+
}
|
|
4391
|
+
this.ensureOpening();
|
|
4392
|
+
this.captureStats = this.capture.getStats();
|
|
4393
|
+
if (this.captureStats.framesDropped > 0n) {
|
|
4394
|
+
throw new AudioRecorderError("Audio recorder capture dropped frames", { action: "stats" });
|
|
4395
|
+
}
|
|
4396
|
+
this.currentState = "recording";
|
|
4397
|
+
this.exposed = true;
|
|
4398
|
+
this.reader = this.capture.readable.getReader();
|
|
4399
|
+
const lifecycle = this.consume();
|
|
4400
|
+
this.lifecyclePromise = lifecycle;
|
|
4401
|
+
lifecycle.finally(() => {
|
|
4402
|
+
if (this.lifecyclePromise === lifecycle)
|
|
4403
|
+
this.lifecyclePromise = null;
|
|
4404
|
+
const request = this.terminationRequest;
|
|
4405
|
+
if (request != null)
|
|
4406
|
+
this.finishCleanup(request.kind, request.error);
|
|
4407
|
+
});
|
|
4408
|
+
} catch (cause) {
|
|
4409
|
+
if (this.terminationRequest == null) {
|
|
4410
|
+
if (cause instanceof DOMException && cause.name === "AbortError")
|
|
4411
|
+
this.requestTermination("disposed");
|
|
4412
|
+
else {
|
|
4413
|
+
const error = cause instanceof AudioRecorderError ? cause : new AudioRecorderError("Audio recorder open failed", { action: "open" }, cause);
|
|
4414
|
+
this.requestTermination("error", error);
|
|
4415
|
+
}
|
|
4416
|
+
}
|
|
4417
|
+
const request = this.terminationRequest;
|
|
4418
|
+
const cleanupError = await this.finishCleanup(request.kind, request.error);
|
|
4419
|
+
if (cleanupError != null) {
|
|
4420
|
+
throw new AudioRecorderError("Audio recorder setup cleanup failed", { action: "destroy" }, new AggregateError([cause, cleanupError], "Audio recorder setup and cleanup failed"));
|
|
4421
|
+
}
|
|
4422
|
+
if (request.kind === "disposed")
|
|
4423
|
+
throw createAbortError();
|
|
4424
|
+
if (cause instanceof AudioRecorderError)
|
|
4425
|
+
throw cause;
|
|
4426
|
+
throw new AudioRecorderError("Audio recorder open failed", { action: "open" }, cause);
|
|
4427
|
+
}
|
|
4428
|
+
}
|
|
4429
|
+
getStats() {
|
|
4430
|
+
if (this.capture != null && this.terminal == null && this.terminationRequest == null) {
|
|
4431
|
+
this.captureStats = this.capture.getStats();
|
|
4432
|
+
if (this.captureStats.framesDropped > 0n) {
|
|
4433
|
+
this.fail(new AudioRecorderError("Audio recorder capture dropped frames", { action: "stats" }));
|
|
4434
|
+
}
|
|
4435
|
+
}
|
|
4436
|
+
return {
|
|
4437
|
+
sampleRate: this.captureStats.sampleRate,
|
|
4438
|
+
channels: this.captureStats.channels,
|
|
4439
|
+
capacityFrames: this.captureStats.capacityFrames,
|
|
4440
|
+
bufferedFrames: this.captureStats.bufferedFrames,
|
|
4441
|
+
bufferedDurationMs: this.captureStats.bufferedDurationMs,
|
|
4442
|
+
framesReceived: this.captureStats.framesReceived,
|
|
4443
|
+
framesRead: this.captureStats.framesRead,
|
|
4444
|
+
framesDropped: this.captureStats.framesDropped,
|
|
4445
|
+
state: this.currentState,
|
|
4446
|
+
framesWritten: this.framesWritten,
|
|
4447
|
+
dataBytesWritten: this.dataBytesWritten,
|
|
4448
|
+
durationMs: this.sampleRate === 0 ? 0 : Number(this.framesWritten) * 1000 / this.sampleRate
|
|
4449
|
+
};
|
|
4450
|
+
}
|
|
4451
|
+
stop() {
|
|
4452
|
+
if (this.terminal != null || this.terminationRequest != null || this.stopRequested)
|
|
4453
|
+
return;
|
|
4454
|
+
this.stopRequested = true;
|
|
4455
|
+
this.currentState = "stopping";
|
|
4456
|
+
try {
|
|
4457
|
+
this.capture?.stop();
|
|
4458
|
+
} catch (cause) {
|
|
4459
|
+
this.fail(new AudioRecorderError("Audio recorder capture stop failed", { action: "stop" }, cause));
|
|
4460
|
+
}
|
|
4461
|
+
}
|
|
4462
|
+
dispose() {
|
|
4463
|
+
if (this.terminal != null) {
|
|
4464
|
+
this.retryRetainedCleanup();
|
|
4465
|
+
return;
|
|
4466
|
+
}
|
|
4467
|
+
if (this.publicationStarted)
|
|
4468
|
+
return;
|
|
4469
|
+
this.requestTermination("disposed");
|
|
4470
|
+
}
|
|
4471
|
+
async consume() {
|
|
4472
|
+
const reader = this.reader;
|
|
4473
|
+
let pendingRead = this.readWithStats(reader);
|
|
4474
|
+
try {
|
|
4475
|
+
while (this.terminal == null && this.terminationRequest == null) {
|
|
4476
|
+
const result = await pendingRead;
|
|
4477
|
+
pendingRead = null;
|
|
4478
|
+
if (this.terminal != null || this.terminationRequest != null)
|
|
4479
|
+
return;
|
|
4480
|
+
if (result.done) {
|
|
4481
|
+
if (!this.stopRequested) {
|
|
4482
|
+
this.fail(new AudioRecorderError("Audio capture stopped unexpectedly", { action: "stop" }));
|
|
4483
|
+
} else {
|
|
4484
|
+
await this.complete();
|
|
4485
|
+
}
|
|
4486
|
+
return;
|
|
4487
|
+
}
|
|
4488
|
+
pendingRead = this.readWithStats(reader);
|
|
4489
|
+
this.captureStats = this.capture.getStats();
|
|
4490
|
+
if (this.captureStats.framesDropped > 0n) {
|
|
4491
|
+
this.fail(new AudioRecorderError("Audio recorder capture dropped frames", { action: "stats" }));
|
|
4492
|
+
return;
|
|
4493
|
+
}
|
|
4494
|
+
await this.writeSamples(result.value);
|
|
4495
|
+
}
|
|
4496
|
+
} catch (cause) {
|
|
4497
|
+
if (this.terminal == null && this.terminationRequest == null) {
|
|
4498
|
+
const error = cause instanceof AudioRecorderError ? cause : cause instanceof AudioCaptureStreamError ? this.fromCaptureError(cause) : new AudioRecorderError("Audio recorder read failed", { action: "read" }, cause);
|
|
4499
|
+
this.fail(error);
|
|
4500
|
+
}
|
|
4501
|
+
} finally {
|
|
4502
|
+
if (pendingRead != null)
|
|
4503
|
+
pendingRead.catch(() => {
|
|
4504
|
+
return;
|
|
4505
|
+
});
|
|
4506
|
+
try {
|
|
4507
|
+
reader.releaseLock();
|
|
4508
|
+
} catch {}
|
|
4509
|
+
}
|
|
4510
|
+
}
|
|
4511
|
+
async writeSamples(samples) {
|
|
4512
|
+
if (samples.length % this.channels !== 0) {
|
|
4513
|
+
throw new AudioRecorderError("Audio recorder received a partial frame", { action: "read" });
|
|
4514
|
+
}
|
|
4515
|
+
const bytes = new Uint8Array(samples.length * 2);
|
|
4516
|
+
const view = new DataView(bytes.buffer);
|
|
4517
|
+
for (let index = 0;index < samples.length; index += 1) {
|
|
4518
|
+
const sample = Math.max(-1, Math.min(1, samples[index]));
|
|
4519
|
+
view.setInt16(index * 2, Math.round(sample * 32767), true);
|
|
4520
|
+
}
|
|
4521
|
+
const nextDataBytes = this.dataBytesWritten + BigInt(bytes.byteLength);
|
|
4522
|
+
if (nextDataBytes > MAX_WAV_DATA_BYTES) {
|
|
4523
|
+
throw new AudioRecorderError("Audio recorder exceeded the classic RIFF size limit", { action: "write" });
|
|
4524
|
+
}
|
|
4525
|
+
await this.writeFully(bytes, WAV_HEADER_BYTES + Number(this.dataBytesWritten), "write");
|
|
4526
|
+
this.dataBytesWritten = nextDataBytes;
|
|
4527
|
+
this.framesWritten += BigInt(samples.length / this.channels);
|
|
4528
|
+
}
|
|
4529
|
+
readWithStats(reader) {
|
|
4530
|
+
return new Promise((resolve, reject) => {
|
|
4531
|
+
let settled = false;
|
|
4532
|
+
let timer;
|
|
4533
|
+
const finish = (callback) => {
|
|
4534
|
+
if (settled)
|
|
4535
|
+
return;
|
|
4536
|
+
settled = true;
|
|
4537
|
+
if (timer !== undefined)
|
|
4538
|
+
clearTimeout(timer);
|
|
4539
|
+
callback();
|
|
4540
|
+
};
|
|
4541
|
+
const poll = () => {
|
|
4542
|
+
if (settled)
|
|
4543
|
+
return;
|
|
4544
|
+
if (this.terminal != null || this.terminationRequest != null) {
|
|
4545
|
+
finish(() => resolve({ done: true, value: undefined }));
|
|
4546
|
+
return;
|
|
4547
|
+
}
|
|
4548
|
+
this.captureStats = this.capture.getStats();
|
|
4549
|
+
if (this.captureStats.framesDropped > 0n) {
|
|
4550
|
+
finish(() => reject(new AudioRecorderError("Audio recorder capture dropped frames", { action: "stats" })));
|
|
4551
|
+
return;
|
|
4552
|
+
}
|
|
4553
|
+
timer = setTimeout(poll, STREAM_POLL_INTERVAL_MS);
|
|
4554
|
+
};
|
|
4555
|
+
reader.read().then((result) => finish(() => resolve(result)), (cause) => finish(() => reject(cause)));
|
|
4556
|
+
timer = setTimeout(poll, STREAM_POLL_INTERVAL_MS);
|
|
4557
|
+
});
|
|
4558
|
+
}
|
|
4559
|
+
async complete() {
|
|
4560
|
+
if (this.terminal != null || this.terminationRequest != null)
|
|
4561
|
+
return;
|
|
4562
|
+
this.captureStats = this.capture.getStats();
|
|
4563
|
+
if (this.captureStats.framesDropped > 0n) {
|
|
4564
|
+
this.fail(new AudioRecorderError("Audio recorder capture dropped frames", { action: "stats" }));
|
|
4565
|
+
return;
|
|
4566
|
+
}
|
|
4567
|
+
try {
|
|
4568
|
+
await this.writeFully(createWavHeader(this.sampleRate, this.channels, Number(this.dataBytesWritten)), 0, "finalize");
|
|
4569
|
+
if (this.terminationRequest != null)
|
|
4570
|
+
return;
|
|
4571
|
+
await this.fileHandle.sync();
|
|
4572
|
+
if (this.terminationRequest != null)
|
|
4573
|
+
return;
|
|
4574
|
+
await this.fileHandle.close();
|
|
4575
|
+
this.fileHandle = null;
|
|
4576
|
+
} catch (cause) {
|
|
4577
|
+
if (this.terminationRequest == null) {
|
|
4578
|
+
this.fail(cause instanceof AudioRecorderError ? cause : new AudioRecorderError("Audio recorder finalize failed", { action: "finalize" }, cause));
|
|
4579
|
+
}
|
|
4580
|
+
return;
|
|
4581
|
+
}
|
|
4582
|
+
if (this.terminationRequest != null)
|
|
4583
|
+
return;
|
|
4584
|
+
this.publicationStarted = true;
|
|
4585
|
+
try {
|
|
4586
|
+
await this.publish();
|
|
4587
|
+
} catch (cause) {
|
|
4588
|
+
const error = new AudioRecorderError("Audio recorder publish failed", { action: "publish" }, cause);
|
|
4589
|
+
this.terminal = "error";
|
|
4590
|
+
this.currentState = "errored";
|
|
4591
|
+
await this.finishCleanup("error", error);
|
|
4592
|
+
return;
|
|
4593
|
+
}
|
|
4594
|
+
this.terminal = "stopped";
|
|
4595
|
+
this.currentState = "stopped";
|
|
4596
|
+
this.init.signal?.removeEventListener("abort", this.signalAbortListener);
|
|
4597
|
+
this.capture?.removeListener("error", this.captureErrorListener);
|
|
4598
|
+
if (this.hasRetainedResources())
|
|
4599
|
+
this.scheduleRetainedCleanup();
|
|
4600
|
+
else
|
|
4601
|
+
this.removeOwner();
|
|
4602
|
+
this.emitTerminal("stopped");
|
|
4603
|
+
}
|
|
4604
|
+
async publish() {
|
|
4605
|
+
const tempPath = this.tempPath;
|
|
4606
|
+
await AudioRecorder.fileSystem.rename(tempPath, this.filePath);
|
|
4607
|
+
this.tempPath = null;
|
|
4608
|
+
}
|
|
4609
|
+
fail(error) {
|
|
4610
|
+
if (this.terminal != null || this.publicationStarted)
|
|
4611
|
+
return;
|
|
4612
|
+
this.requestTermination("error", error);
|
|
4613
|
+
}
|
|
4614
|
+
requestTermination(kind, error) {
|
|
4615
|
+
if (this.terminal != null || this.publicationStarted || this.terminationRequest != null)
|
|
4616
|
+
return;
|
|
4617
|
+
this.terminationRequest = { kind, error };
|
|
4618
|
+
this.currentState = kind === "disposed" ? "disposed" : "errored";
|
|
4619
|
+
this.capture?.dispose();
|
|
4620
|
+
if (this.exposed && this.lifecyclePromise == null)
|
|
4621
|
+
this.finishCleanup(kind, error);
|
|
4622
|
+
}
|
|
4623
|
+
finishCleanup(kind, error) {
|
|
4624
|
+
if (this.cleanupPromise != null)
|
|
4625
|
+
return this.cleanupPromise;
|
|
4626
|
+
this.cleanupPromise = (async () => {
|
|
4627
|
+
this.init.signal?.removeEventListener("abort", this.signalAbortListener);
|
|
4628
|
+
const capture2 = this.capture;
|
|
4629
|
+
capture2?.dispose();
|
|
4630
|
+
if (capture2 != null) {
|
|
4631
|
+
await capture2.closed;
|
|
4632
|
+
this.captureStats = capture2.getStats();
|
|
4633
|
+
}
|
|
4634
|
+
capture2?.removeListener("error", this.captureErrorListener);
|
|
4635
|
+
const cleanupError = await this.cleanupOwnedResources();
|
|
4636
|
+
const captureCleanupError = capture2?.state === "errored" ? new AudioRecorderError("Audio recorder capture cleanup failed", { action: "destroy" }) : null;
|
|
4637
|
+
const terminalKind = cleanupError == null && captureCleanupError == null ? kind : "error";
|
|
4638
|
+
const terminalError = cleanupError ?? error ?? captureCleanupError;
|
|
4639
|
+
this.terminal = terminalKind;
|
|
4640
|
+
this.currentState = terminalKind === "disposed" ? "disposed" : "errored";
|
|
4641
|
+
if (this.hasRetainedResources())
|
|
4642
|
+
this.scheduleRetainedCleanup();
|
|
4643
|
+
else
|
|
4644
|
+
this.removeOwner();
|
|
4645
|
+
if (!this.exposed) {
|
|
4646
|
+
this.closedResolve();
|
|
4647
|
+
} else if (terminalKind === "error") {
|
|
4648
|
+
this.emitTerminal("error", terminalError, terminalError.context);
|
|
4649
|
+
} else {
|
|
4650
|
+
this.emitTerminal("disposed");
|
|
4651
|
+
}
|
|
4652
|
+
return cleanupError ?? captureCleanupError;
|
|
4653
|
+
})();
|
|
4654
|
+
return this.cleanupPromise;
|
|
4655
|
+
}
|
|
4656
|
+
cleanupOwnedResources() {
|
|
4657
|
+
if (!this.hasRetainedResources())
|
|
4658
|
+
return Promise.resolve(null);
|
|
4659
|
+
if (this.resourceCleanupPromise != null)
|
|
4660
|
+
return this.resourceCleanupPromise;
|
|
4661
|
+
const cleanup = (async () => {
|
|
4662
|
+
let lastError = null;
|
|
4663
|
+
for (let attempt = 0;attempt < 3; attempt += 1) {
|
|
4664
|
+
const handle = this.fileHandle;
|
|
4665
|
+
if (handle != null) {
|
|
4666
|
+
try {
|
|
4667
|
+
await handle.close();
|
|
4668
|
+
if (this.fileHandle === handle)
|
|
4669
|
+
this.fileHandle = null;
|
|
4670
|
+
} catch (cause) {
|
|
4671
|
+
lastError = new AudioRecorderError("Audio recorder file close failed", { action: "destroy" }, cause);
|
|
4672
|
+
}
|
|
4673
|
+
}
|
|
4674
|
+
if (this.fileHandle == null) {
|
|
4675
|
+
const tempPath = this.tempPath;
|
|
4676
|
+
if (tempPath != null) {
|
|
4677
|
+
try {
|
|
4678
|
+
await AudioRecorder.fileSystem.unlink(tempPath);
|
|
4679
|
+
if (this.tempPath === tempPath)
|
|
4680
|
+
this.tempPath = null;
|
|
4681
|
+
} catch (cause) {
|
|
4682
|
+
lastError = new AudioRecorderError("Audio recorder temp file cleanup failed", { action: "destroy" }, cause);
|
|
4683
|
+
}
|
|
4684
|
+
}
|
|
4685
|
+
}
|
|
4686
|
+
if (!this.hasRetainedResources())
|
|
4687
|
+
return null;
|
|
4688
|
+
if (attempt < 2)
|
|
4689
|
+
await new Promise((resolve) => setTimeout(resolve, STREAM_POLL_INTERVAL_MS));
|
|
4690
|
+
}
|
|
4691
|
+
return lastError ?? new AudioRecorderError("Audio recorder resource cleanup failed", { action: "destroy" });
|
|
4692
|
+
})();
|
|
4693
|
+
this.resourceCleanupPromise = cleanup;
|
|
4694
|
+
const clear = () => {
|
|
4695
|
+
if (this.resourceCleanupPromise === cleanup)
|
|
4696
|
+
this.resourceCleanupPromise = null;
|
|
4697
|
+
};
|
|
4698
|
+
cleanup.then(clear, clear);
|
|
4699
|
+
return cleanup;
|
|
4700
|
+
}
|
|
4701
|
+
retryRetainedCleanup() {
|
|
4702
|
+
this.capture?.dispose();
|
|
4703
|
+
if (!this.hasRetainedResources()) {
|
|
4704
|
+
this.removeOwner();
|
|
4705
|
+
return Promise.resolve();
|
|
4706
|
+
}
|
|
4707
|
+
return this.cleanupOwnedResources().then(() => {
|
|
4708
|
+
if (!this.hasRetainedResources())
|
|
4709
|
+
this.removeOwner();
|
|
4710
|
+
});
|
|
4711
|
+
}
|
|
4712
|
+
scheduleRetainedCleanup() {
|
|
4713
|
+
if (this.retainedCleanupScheduled || !this.hasRetainedResources())
|
|
4714
|
+
return;
|
|
4715
|
+
this.retainedCleanupScheduled = true;
|
|
4716
|
+
setTimeout(() => {
|
|
4717
|
+
this.retainedCleanupScheduled = false;
|
|
4718
|
+
this.retryRetainedCleanup();
|
|
4719
|
+
}, STREAM_POLL_INTERVAL_MS);
|
|
4720
|
+
}
|
|
4721
|
+
hasRetainedResources() {
|
|
4722
|
+
return this.fileHandle != null || this.tempPath != null;
|
|
4723
|
+
}
|
|
4724
|
+
async openTemporaryFile() {
|
|
4725
|
+
const directory = dirname(this.filePath);
|
|
4726
|
+
const destinationName = basename(this.filePath);
|
|
4727
|
+
const tempNameLength = Math.max(1, Math.min(24, destinationName.length));
|
|
4728
|
+
const singleCharacterNames = "0123456789abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ_-";
|
|
4729
|
+
for (let attempt = 0;attempt < singleCharacterNames.length; attempt += 1) {
|
|
4730
|
+
const tempName = tempNameLength === 1 ? singleCharacterNames[attempt] : randomBytes(16).toString("hex").slice(0, tempNameLength);
|
|
4731
|
+
if (tempName === destinationName)
|
|
4732
|
+
continue;
|
|
4733
|
+
const tempPath = join(directory, tempName);
|
|
4734
|
+
try {
|
|
4735
|
+
const handle = await AudioRecorder.fileSystem.open(tempPath, "wx");
|
|
4736
|
+
this.tempPath = tempPath;
|
|
4737
|
+
return handle;
|
|
4738
|
+
} catch (cause) {
|
|
4739
|
+
if (cause.code !== "EEXIST") {
|
|
4740
|
+
throw new AudioRecorderError("Audio recorder temp file open failed", { action: "open" }, cause);
|
|
4741
|
+
}
|
|
4742
|
+
}
|
|
4743
|
+
}
|
|
4744
|
+
throw new AudioRecorderError("Audio recorder could not allocate a temporary file", { action: "open" });
|
|
4745
|
+
}
|
|
4746
|
+
async writeFully(bytes, position, action) {
|
|
4747
|
+
let offset = 0;
|
|
4748
|
+
while (offset < bytes.byteLength) {
|
|
4749
|
+
let bytesWritten;
|
|
4750
|
+
try {
|
|
4751
|
+
({ bytesWritten } = await this.fileHandle.write(bytes, offset, bytes.byteLength - offset, position + offset));
|
|
4752
|
+
} catch (cause) {
|
|
4753
|
+
throw new AudioRecorderError(`Audio recorder ${action} write failed`, { action }, cause);
|
|
4754
|
+
}
|
|
4755
|
+
if (bytesWritten <= 0) {
|
|
4756
|
+
throw new AudioRecorderError(`Audio recorder ${action} write made no progress`, { action });
|
|
4757
|
+
}
|
|
4758
|
+
offset += Math.min(bytesWritten, bytes.byteLength - offset);
|
|
4759
|
+
}
|
|
4760
|
+
}
|
|
4761
|
+
ensureOpening() {
|
|
4762
|
+
if (this.terminationRequest?.kind === "disposed" || this.init.signal?.aborted)
|
|
4763
|
+
throw createAbortError();
|
|
4764
|
+
if (this.terminationRequest?.kind === "error")
|
|
4765
|
+
throw this.terminationRequest.error;
|
|
4766
|
+
}
|
|
4767
|
+
fromCaptureError(error) {
|
|
4768
|
+
const action = error.context.action === "stop" ? "stop" : error.context.action === "stats" ? "stats" : error.context.action === "destroy" ? "destroy" : "read";
|
|
4769
|
+
const context = { action };
|
|
4770
|
+
if (error.context.status !== undefined)
|
|
4771
|
+
context.status = error.context.status;
|
|
4772
|
+
return new AudioRecorderError(`Audio recorder capture ${action} failed`, context, error);
|
|
4773
|
+
}
|
|
4774
|
+
removeOwner() {
|
|
4775
|
+
if (this.ownerRemoved)
|
|
4776
|
+
return;
|
|
4777
|
+
this.ownerRemoved = true;
|
|
4778
|
+
this.init.removeFromOwner();
|
|
4779
|
+
}
|
|
4780
|
+
emitTerminal(event, ...args) {
|
|
4781
|
+
setTimeout(() => {
|
|
4782
|
+
try {
|
|
4783
|
+
EventEmitter.prototype.emit.call(this, event, ...args);
|
|
4784
|
+
} finally {
|
|
4785
|
+
this.closedResolve();
|
|
4786
|
+
}
|
|
4787
|
+
}, 0);
|
|
4788
|
+
}
|
|
4789
|
+
}
|
|
3775
4790
|
|
|
3776
4791
|
class Audio extends EventEmitter {
|
|
3777
4792
|
static create(options = {}) {
|
|
@@ -3791,6 +4806,13 @@ class Audio extends EventEmitter {
|
|
|
3791
4806
|
streams = new Set;
|
|
3792
4807
|
playbackStarted = false;
|
|
3793
4808
|
mixerStarted = false;
|
|
4809
|
+
captureStarted = false;
|
|
4810
|
+
captureDeviceOpen = false;
|
|
4811
|
+
captureBufferAvailable = false;
|
|
4812
|
+
captureChannels = 1;
|
|
4813
|
+
captureCapacityFrames = 0;
|
|
4814
|
+
captureOwner = null;
|
|
4815
|
+
captureStream = null;
|
|
3794
4816
|
disposing = false;
|
|
3795
4817
|
constructor(lib, options) {
|
|
3796
4818
|
super();
|
|
@@ -4060,127 +5082,444 @@ class Audio extends EventEmitter {
|
|
|
4060
5082
|
this.emitError("setGroupVolume", undefined, "Audio engine unavailable during setGroupVolume");
|
|
4061
5083
|
return false;
|
|
4062
5084
|
}
|
|
4063
|
-
const status = this.lib.audioSetGroupVolume(engine2, group, volume);
|
|
4064
|
-
if (status !== 0) {
|
|
4065
|
-
this.emitError("setGroupVolume", status);
|
|
5085
|
+
const status = this.lib.audioSetGroupVolume(engine2, group, volume);
|
|
5086
|
+
if (status !== 0) {
|
|
5087
|
+
this.emitError("setGroupVolume", status);
|
|
5088
|
+
return false;
|
|
5089
|
+
}
|
|
5090
|
+
return true;
|
|
5091
|
+
}
|
|
5092
|
+
setMasterVolume(volume) {
|
|
5093
|
+
const engine2 = this.engine;
|
|
5094
|
+
if (!engine2) {
|
|
5095
|
+
this.emitError("setMasterVolume", undefined, "Audio engine unavailable during setMasterVolume");
|
|
5096
|
+
return false;
|
|
5097
|
+
}
|
|
5098
|
+
const status = this.lib.audioSetMasterVolume(engine2, volume);
|
|
5099
|
+
if (status !== 0) {
|
|
5100
|
+
this.emitError("setMasterVolume", status);
|
|
5101
|
+
return false;
|
|
5102
|
+
}
|
|
5103
|
+
return true;
|
|
5104
|
+
}
|
|
5105
|
+
mixFrames(frameCount, channels = 2) {
|
|
5106
|
+
const engine2 = this.engine;
|
|
5107
|
+
if (!engine2) {
|
|
5108
|
+
this.emitError("mixFrames", undefined, "Audio engine unavailable during mixFrames");
|
|
5109
|
+
return null;
|
|
5110
|
+
}
|
|
5111
|
+
const output = new Float32Array(frameCount * channels);
|
|
5112
|
+
const status = this.lib.audioMixToBuffer(engine2, output, frameCount, channels);
|
|
5113
|
+
if (status !== 0) {
|
|
5114
|
+
this.emitError("mixFrames", status);
|
|
5115
|
+
return null;
|
|
5116
|
+
}
|
|
5117
|
+
return output;
|
|
5118
|
+
}
|
|
5119
|
+
enableTap(capacityFrames = 8192) {
|
|
5120
|
+
const engine2 = this.engine;
|
|
5121
|
+
if (!engine2) {
|
|
5122
|
+
this.emitError("enableTap", undefined, "Audio engine unavailable during enableTap");
|
|
5123
|
+
return false;
|
|
5124
|
+
}
|
|
5125
|
+
const status = this.lib.audioEnableTap(engine2, true, capacityFrames);
|
|
5126
|
+
if (status !== 0) {
|
|
5127
|
+
this.emitError("enableTap", status);
|
|
5128
|
+
return false;
|
|
5129
|
+
}
|
|
5130
|
+
return true;
|
|
5131
|
+
}
|
|
5132
|
+
disableTap() {
|
|
5133
|
+
const engine2 = this.engine;
|
|
5134
|
+
if (!engine2) {
|
|
5135
|
+
this.emitError("enableTap", undefined, "Audio engine unavailable during disableTap");
|
|
5136
|
+
return false;
|
|
5137
|
+
}
|
|
5138
|
+
const status = this.lib.audioEnableTap(engine2, false, 0);
|
|
5139
|
+
if (status !== 0) {
|
|
5140
|
+
this.emitError("enableTap", status);
|
|
5141
|
+
return false;
|
|
5142
|
+
}
|
|
5143
|
+
return true;
|
|
5144
|
+
}
|
|
5145
|
+
readTapFrames(frameCount, channels = 2) {
|
|
5146
|
+
const engine2 = this.engine;
|
|
5147
|
+
if (!engine2) {
|
|
5148
|
+
this.emitError("readTapFrames", undefined, "Audio engine unavailable during readTapFrames");
|
|
5149
|
+
return null;
|
|
5150
|
+
}
|
|
5151
|
+
const output = new Float32Array(frameCount * channels);
|
|
5152
|
+
const result = this.lib.audioReadTap(engine2, output, frameCount, channels);
|
|
5153
|
+
if (result.status !== 0) {
|
|
5154
|
+
this.emitError("readTapFrames", result.status);
|
|
5155
|
+
return null;
|
|
5156
|
+
}
|
|
5157
|
+
return { frames: output, framesRead: result.framesRead };
|
|
5158
|
+
}
|
|
5159
|
+
listPlaybackDevices() {
|
|
5160
|
+
const engine2 = this.engine;
|
|
5161
|
+
if (!engine2) {
|
|
5162
|
+
this.emitError("listPlaybackDevices", undefined, "Audio engine unavailable during listPlaybackDevices");
|
|
5163
|
+
return null;
|
|
5164
|
+
}
|
|
5165
|
+
const refreshStatus = this.lib.audioRefreshPlaybackDevices(engine2);
|
|
5166
|
+
if (refreshStatus !== 0) {
|
|
5167
|
+
this.emitError("listPlaybackDevices", refreshStatus);
|
|
5168
|
+
return null;
|
|
5169
|
+
}
|
|
5170
|
+
const count = this.lib.audioGetPlaybackDeviceCount(engine2);
|
|
5171
|
+
const devices = [];
|
|
5172
|
+
for (let index = 0;index < count; index += 1) {
|
|
5173
|
+
devices.push({
|
|
5174
|
+
index,
|
|
5175
|
+
name: this.lib.audioGetPlaybackDeviceName(engine2, index),
|
|
5176
|
+
isDefault: this.lib.audioIsPlaybackDeviceDefault(engine2, index)
|
|
5177
|
+
});
|
|
5178
|
+
}
|
|
5179
|
+
return devices;
|
|
5180
|
+
}
|
|
5181
|
+
selectPlaybackDevice(index) {
|
|
5182
|
+
const engine2 = this.engine;
|
|
5183
|
+
if (!engine2) {
|
|
5184
|
+
this.emitError("selectPlaybackDevice", undefined, "Audio engine unavailable during selectPlaybackDevice");
|
|
5185
|
+
return false;
|
|
5186
|
+
}
|
|
5187
|
+
const refreshStatus = this.lib.audioRefreshPlaybackDevices(engine2);
|
|
5188
|
+
if (refreshStatus !== 0) {
|
|
5189
|
+
this.emitError("selectPlaybackDevice", refreshStatus);
|
|
5190
|
+
return false;
|
|
5191
|
+
}
|
|
5192
|
+
const status = this.lib.audioSelectPlaybackDevice(engine2, index);
|
|
5193
|
+
if (status !== 0) {
|
|
5194
|
+
this.emitError("selectPlaybackDevice", status);
|
|
5195
|
+
return false;
|
|
5196
|
+
}
|
|
5197
|
+
return true;
|
|
5198
|
+
}
|
|
5199
|
+
clearPlaybackDeviceSelection() {
|
|
5200
|
+
const engine2 = this.engine;
|
|
5201
|
+
if (!engine2) {
|
|
5202
|
+
this.emitError("clearPlaybackDeviceSelection", undefined, "Audio engine unavailable during clearPlaybackDeviceSelection");
|
|
5203
|
+
return;
|
|
5204
|
+
}
|
|
5205
|
+
this.lib.audioClearPlaybackDeviceSelection(engine2);
|
|
5206
|
+
}
|
|
5207
|
+
async openCapture(options = {}) {
|
|
5208
|
+
const resolved = resolveAudioCaptureStreamOptions(options, this.sampleRate);
|
|
5209
|
+
if (resolved.signal?.aborted)
|
|
5210
|
+
throw createAbortError();
|
|
5211
|
+
if (!this.engine) {
|
|
5212
|
+
throw new AudioCaptureStreamError("Audio engine unavailable during capture stream start", { action: "start" });
|
|
5213
|
+
}
|
|
5214
|
+
if (this.captureOwner != null || this.isCapturing()) {
|
|
5215
|
+
throw new AudioCaptureStreamError("Audio capture ring is already in use", { action: "start" });
|
|
5216
|
+
}
|
|
5217
|
+
const owner = {};
|
|
5218
|
+
this.captureOwner = owner;
|
|
5219
|
+
let stream = null;
|
|
5220
|
+
try {
|
|
5221
|
+
stream = createAudioCaptureStream({
|
|
5222
|
+
options: resolved,
|
|
5223
|
+
start: () => this.startCaptureInternal(resolved, owner),
|
|
5224
|
+
read: (frameCount) => this.readCaptureInternal(frameCount, owner),
|
|
5225
|
+
stats: () => this.getCaptureStatsInternal(owner),
|
|
5226
|
+
stop: () => this.stopCaptureInternal(owner),
|
|
5227
|
+
isRunning: () => this.isCapturingInternal(owner),
|
|
5228
|
+
removeFromOwner: () => {
|
|
5229
|
+
if (this.captureOwner === owner)
|
|
5230
|
+
this.captureOwner = null;
|
|
5231
|
+
if (this.captureStream === stream)
|
|
5232
|
+
this.captureStream = null;
|
|
5233
|
+
if (stream != null)
|
|
5234
|
+
this.streams.delete(stream);
|
|
5235
|
+
}
|
|
5236
|
+
});
|
|
5237
|
+
this.captureStream = stream;
|
|
5238
|
+
this.streams.add(stream);
|
|
5239
|
+
await openAudioCaptureStream(stream);
|
|
5240
|
+
return stream;
|
|
5241
|
+
} catch (error) {
|
|
5242
|
+
if (stream == null) {
|
|
5243
|
+
if (this.captureOwner === owner)
|
|
5244
|
+
this.captureOwner = null;
|
|
5245
|
+
} else {
|
|
5246
|
+
stream.dispose();
|
|
5247
|
+
if (this.captureOwner !== owner)
|
|
5248
|
+
this.streams.delete(stream);
|
|
5249
|
+
}
|
|
5250
|
+
throw error;
|
|
5251
|
+
}
|
|
5252
|
+
}
|
|
5253
|
+
async recordToFile(filePath, options = {}) {
|
|
5254
|
+
if (typeof filePath !== "string" || filePath.length === 0)
|
|
5255
|
+
throw new TypeError("filePath must be a nonempty string");
|
|
5256
|
+
if (filePath.includes("\x00"))
|
|
5257
|
+
throw new TypeError("filePath must not contain NUL bytes");
|
|
5258
|
+
const resolved = resolveAudioCaptureStreamOptions(options, this.sampleRate);
|
|
5259
|
+
if (resolved.channels !== 1 && resolved.channels !== 2) {
|
|
5260
|
+
throw new RangeError("WAV recording supports only 1 or 2 channels");
|
|
5261
|
+
}
|
|
5262
|
+
if (this.sampleRate * resolved.channels * 2 > MAX_U32) {
|
|
5263
|
+
throw new RangeError("WAV byte rate exceeds the supported limit");
|
|
5264
|
+
}
|
|
5265
|
+
if (resolved.signal?.aborted)
|
|
5266
|
+
throw createAbortError();
|
|
5267
|
+
if (!this.engine)
|
|
5268
|
+
throw new AudioRecorderError("Audio engine unavailable during recorder open", { action: "open" });
|
|
5269
|
+
let recorder;
|
|
5270
|
+
recorder = createAudioRecorder({
|
|
5271
|
+
filePath,
|
|
5272
|
+
signal: resolved.signal,
|
|
5273
|
+
captureOptions: resolved,
|
|
5274
|
+
openCapture: (captureOptions) => this.openCapture(captureOptions),
|
|
5275
|
+
removeFromOwner: () => this.streams.delete(recorder)
|
|
5276
|
+
});
|
|
5277
|
+
this.streams.add(recorder);
|
|
5278
|
+
try {
|
|
5279
|
+
await openAudioRecorder(recorder);
|
|
5280
|
+
return recorder;
|
|
5281
|
+
} catch (error) {
|
|
5282
|
+
throw error;
|
|
5283
|
+
}
|
|
5284
|
+
}
|
|
5285
|
+
listCaptureDevices() {
|
|
5286
|
+
const engine2 = this.engine;
|
|
5287
|
+
if (!engine2) {
|
|
5288
|
+
this.emitError("listCaptureDevices", undefined, "Audio engine unavailable during listCaptureDevices");
|
|
5289
|
+
return null;
|
|
5290
|
+
}
|
|
5291
|
+
const refreshStatus = this.lib.audioRefreshCaptureDevices(engine2);
|
|
5292
|
+
if (refreshStatus !== 0) {
|
|
5293
|
+
this.emitError("listCaptureDevices", refreshStatus);
|
|
5294
|
+
return null;
|
|
5295
|
+
}
|
|
5296
|
+
const count = this.lib.audioGetCaptureDeviceCount(engine2);
|
|
5297
|
+
const devices = [];
|
|
5298
|
+
for (let index = 0;index < count; index += 1) {
|
|
5299
|
+
devices.push({
|
|
5300
|
+
index,
|
|
5301
|
+
name: this.lib.audioGetCaptureDeviceName(engine2, index),
|
|
5302
|
+
isDefault: this.lib.audioIsCaptureDeviceDefault(engine2, index)
|
|
5303
|
+
});
|
|
5304
|
+
}
|
|
5305
|
+
return devices;
|
|
5306
|
+
}
|
|
5307
|
+
selectCaptureDevice(index) {
|
|
5308
|
+
const resolvedIndex = resolveU32Index(index, "index");
|
|
5309
|
+
if (this.captureOwner != null) {
|
|
5310
|
+
this.emitCaptureOwnershipError("selectCaptureDevice");
|
|
4066
5311
|
return false;
|
|
4067
5312
|
}
|
|
4068
|
-
return true;
|
|
4069
|
-
}
|
|
4070
|
-
setMasterVolume(volume) {
|
|
4071
5313
|
const engine2 = this.engine;
|
|
4072
5314
|
if (!engine2) {
|
|
4073
|
-
this.emitError("
|
|
5315
|
+
this.emitError("selectCaptureDevice", undefined, "Audio engine unavailable during selectCaptureDevice");
|
|
4074
5316
|
return false;
|
|
4075
5317
|
}
|
|
4076
|
-
const status = this.lib.
|
|
5318
|
+
const status = this.lib.audioSelectCaptureDevice(engine2, resolvedIndex);
|
|
4077
5319
|
if (status !== 0) {
|
|
4078
|
-
this.emitError("
|
|
5320
|
+
this.emitError("selectCaptureDevice", status);
|
|
4079
5321
|
return false;
|
|
4080
5322
|
}
|
|
4081
5323
|
return true;
|
|
4082
5324
|
}
|
|
4083
|
-
|
|
5325
|
+
clearCaptureDeviceSelection() {
|
|
5326
|
+
if (this.captureOwner != null) {
|
|
5327
|
+
this.emitCaptureOwnershipError("clearCaptureDeviceSelection");
|
|
5328
|
+
return;
|
|
5329
|
+
}
|
|
4084
5330
|
const engine2 = this.engine;
|
|
4085
5331
|
if (!engine2) {
|
|
4086
|
-
this.emitError("
|
|
4087
|
-
return
|
|
4088
|
-
}
|
|
4089
|
-
const output = new Float32Array(frameCount * channels);
|
|
4090
|
-
const status = this.lib.audioMixToBuffer(engine2, output, frameCount, channels);
|
|
4091
|
-
if (status !== 0) {
|
|
4092
|
-
this.emitError("mixFrames", status);
|
|
4093
|
-
return null;
|
|
5332
|
+
this.emitError("clearCaptureDeviceSelection", undefined, "Audio engine unavailable during clearCaptureDeviceSelection");
|
|
5333
|
+
return;
|
|
4094
5334
|
}
|
|
4095
|
-
|
|
5335
|
+
this.lib.audioClearCaptureDeviceSelection(engine2);
|
|
4096
5336
|
}
|
|
4097
|
-
|
|
4098
|
-
|
|
4099
|
-
|
|
4100
|
-
|
|
5337
|
+
startCapture(options = {}) {
|
|
5338
|
+
if (this.disposing)
|
|
5339
|
+
return false;
|
|
5340
|
+
const channels = resolvePositiveU32(options.channels, 1, "channels");
|
|
5341
|
+
const capacityFrames = resolvePositiveU32(options.capacityFrames, this.sampleRate, "capacityFrames");
|
|
5342
|
+
if (this.captureOwner != null) {
|
|
5343
|
+
this.emitCaptureOwnershipError("startCapture");
|
|
4101
5344
|
return false;
|
|
4102
5345
|
}
|
|
4103
|
-
|
|
4104
|
-
|
|
4105
|
-
|
|
5346
|
+
if (this.isCapturing()) {
|
|
5347
|
+
const configurationMatches = (options.channels === undefined || channels === this.captureChannels) && (options.capacityFrames === undefined || capacityFrames === this.captureCapacityFrames) && options.startOptions === undefined;
|
|
5348
|
+
if (configurationMatches)
|
|
5349
|
+
return true;
|
|
5350
|
+
this.emitError("startCapture", undefined, "Audio capture is already running with a different configuration");
|
|
4106
5351
|
return false;
|
|
4107
5352
|
}
|
|
4108
|
-
return true;
|
|
4109
|
-
}
|
|
4110
|
-
disableTap() {
|
|
4111
5353
|
const engine2 = this.engine;
|
|
4112
5354
|
if (!engine2) {
|
|
4113
|
-
this.emitError("
|
|
5355
|
+
this.emitError("startCapture", undefined, "Audio engine unavailable during startCapture");
|
|
4114
5356
|
return false;
|
|
4115
5357
|
}
|
|
4116
|
-
const
|
|
4117
|
-
if (status !== 0) {
|
|
4118
|
-
this.emitError("
|
|
5358
|
+
const result = this.startCaptureInternal({ sampleRate: this.sampleRate, channels, capacityFrames, chunkFrames: 1, startOptions: options.startOptions }, null);
|
|
5359
|
+
if (result.status !== 0) {
|
|
5360
|
+
this.emitError("startCapture", result.status, undefined, result.cause);
|
|
4119
5361
|
return false;
|
|
4120
5362
|
}
|
|
4121
5363
|
return true;
|
|
4122
5364
|
}
|
|
4123
|
-
|
|
5365
|
+
isCapturing() {
|
|
5366
|
+
return this.isCapturingInternal(null);
|
|
5367
|
+
}
|
|
5368
|
+
isCapturingInternal(_owner) {
|
|
5369
|
+
if (!this.captureStarted)
|
|
5370
|
+
return false;
|
|
5371
|
+
const engine2 = this.engine;
|
|
5372
|
+
if (engine2 && this.lib.audioIsCaptureRunning(engine2))
|
|
5373
|
+
return true;
|
|
5374
|
+
this.captureStarted = false;
|
|
5375
|
+
this.emit("captureStopped");
|
|
5376
|
+
return this.captureStarted;
|
|
5377
|
+
}
|
|
5378
|
+
readCaptureFrames(frameCount) {
|
|
5379
|
+
const resolvedFrameCount = resolvePositiveU32(frameCount, frameCount, "frameCount");
|
|
5380
|
+
if (this.captureOwner != null) {
|
|
5381
|
+
this.emitCaptureOwnershipError("readCaptureFrames");
|
|
5382
|
+
return null;
|
|
5383
|
+
}
|
|
5384
|
+
if (!this.captureBufferAvailable) {
|
|
5385
|
+
this.emitError("readCaptureFrames", -4);
|
|
5386
|
+
return null;
|
|
5387
|
+
}
|
|
5388
|
+
if (resolvedFrameCount > this.captureCapacityFrames) {
|
|
5389
|
+
throw new RangeError("frameCount exceeds the capture buffer capacity");
|
|
5390
|
+
}
|
|
5391
|
+
if (resolvedFrameCount > Math.floor(MAX_U32 / this.captureChannels)) {
|
|
5392
|
+
throw new RangeError("frameCount * channels exceeds the supported limit");
|
|
5393
|
+
}
|
|
4124
5394
|
const engine2 = this.engine;
|
|
4125
5395
|
if (!engine2) {
|
|
4126
|
-
this.emitError("
|
|
5396
|
+
this.emitError("readCaptureFrames", undefined, "Audio engine unavailable during readCaptureFrames");
|
|
4127
5397
|
return null;
|
|
4128
5398
|
}
|
|
4129
|
-
const
|
|
4130
|
-
const result = this.lib.audioReadTap(engine2, output, frameCount, channels);
|
|
5399
|
+
const result = this.readCaptureInternal(resolvedFrameCount, null);
|
|
4131
5400
|
if (result.status !== 0) {
|
|
4132
|
-
this.emitError("
|
|
5401
|
+
this.emitError("readCaptureFrames", result.status, undefined, result.cause);
|
|
4133
5402
|
return null;
|
|
4134
5403
|
}
|
|
4135
|
-
return { frames:
|
|
5404
|
+
return { frames: result.frames, framesRead: result.framesRead };
|
|
4136
5405
|
}
|
|
4137
|
-
|
|
5406
|
+
getCaptureStats() {
|
|
4138
5407
|
const engine2 = this.engine;
|
|
4139
5408
|
if (!engine2) {
|
|
4140
|
-
this.emitError("
|
|
5409
|
+
this.emitError("getCaptureStats", undefined, "Audio engine unavailable during getCaptureStats");
|
|
4141
5410
|
return null;
|
|
4142
5411
|
}
|
|
4143
|
-
const
|
|
4144
|
-
if (
|
|
4145
|
-
this.emitError("
|
|
5412
|
+
const result = this.getCaptureStatsInternal(null);
|
|
5413
|
+
if (result.status !== 0 || result.stats == null) {
|
|
5414
|
+
this.emitError("getCaptureStats", result.status, undefined, result.cause);
|
|
4146
5415
|
return null;
|
|
4147
5416
|
}
|
|
4148
|
-
|
|
4149
|
-
const devices = [];
|
|
4150
|
-
for (let index = 0;index < count; index += 1) {
|
|
4151
|
-
devices.push({
|
|
4152
|
-
index,
|
|
4153
|
-
name: this.lib.audioGetPlaybackDeviceName(engine2, index),
|
|
4154
|
-
isDefault: this.lib.audioIsPlaybackDeviceDefault(engine2, index)
|
|
4155
|
-
});
|
|
4156
|
-
}
|
|
4157
|
-
return devices;
|
|
5417
|
+
return result.stats;
|
|
4158
5418
|
}
|
|
4159
|
-
|
|
4160
|
-
|
|
4161
|
-
|
|
4162
|
-
this.emitError("selectPlaybackDevice", undefined, "Audio engine unavailable during selectPlaybackDevice");
|
|
5419
|
+
stopCapture() {
|
|
5420
|
+
if (this.captureOwner != null) {
|
|
5421
|
+
this.emitCaptureOwnershipError("stopCapture");
|
|
4163
5422
|
return false;
|
|
4164
5423
|
}
|
|
4165
|
-
|
|
4166
|
-
|
|
4167
|
-
|
|
5424
|
+
if (!this.captureDeviceOpen)
|
|
5425
|
+
return true;
|
|
5426
|
+
const engine2 = this.engine;
|
|
5427
|
+
if (!engine2) {
|
|
5428
|
+
this.emitError("stopCapture", undefined, "Audio engine unavailable during stopCapture");
|
|
4168
5429
|
return false;
|
|
4169
5430
|
}
|
|
4170
|
-
const
|
|
4171
|
-
if (status !== 0) {
|
|
4172
|
-
this.emitError("
|
|
5431
|
+
const result = this.stopCaptureInternal(null);
|
|
5432
|
+
if (result.status !== 0) {
|
|
5433
|
+
this.emitError("stopCapture", result.status, undefined, result.cause);
|
|
4173
5434
|
return false;
|
|
4174
5435
|
}
|
|
4175
5436
|
return true;
|
|
4176
5437
|
}
|
|
4177
|
-
|
|
5438
|
+
emitCaptureOwnershipError(action) {
|
|
5439
|
+
this.emitError(action, undefined, `Audio ${action} failed: capture is owned by a stream`);
|
|
5440
|
+
}
|
|
5441
|
+
startCaptureInternal(options, owner) {
|
|
5442
|
+
if (this.captureOwner != null && this.captureOwner !== owner)
|
|
5443
|
+
return { status: -1 };
|
|
4178
5444
|
const engine2 = this.engine;
|
|
4179
|
-
if (!engine2)
|
|
4180
|
-
|
|
4181
|
-
|
|
5445
|
+
if (!engine2)
|
|
5446
|
+
return { status: -1 };
|
|
5447
|
+
let status;
|
|
5448
|
+
try {
|
|
5449
|
+
status = this.lib.audioStartCapture(engine2, options.startOptions, options.channels, options.capacityFrames);
|
|
5450
|
+
} catch (cause) {
|
|
5451
|
+
return { status: -1, cause };
|
|
5452
|
+
}
|
|
5453
|
+
if (status !== 0)
|
|
5454
|
+
return { status };
|
|
5455
|
+
this.captureChannels = options.channels;
|
|
5456
|
+
this.captureCapacityFrames = options.capacityFrames;
|
|
5457
|
+
this.captureBufferAvailable = true;
|
|
5458
|
+
this.captureDeviceOpen = true;
|
|
5459
|
+
this.captureStarted = true;
|
|
5460
|
+
this.emit("captureStarted");
|
|
5461
|
+
return { status: 0 };
|
|
5462
|
+
}
|
|
5463
|
+
readCaptureInternal(frameCount, owner) {
|
|
5464
|
+
const output = new Float32Array(frameCount * this.captureChannels);
|
|
5465
|
+
if (this.captureOwner != null && this.captureOwner !== owner)
|
|
5466
|
+
return { status: -1, frames: output, framesRead: 0 };
|
|
5467
|
+
const engine2 = this.engine;
|
|
5468
|
+
if (!engine2 || !this.captureBufferAvailable)
|
|
5469
|
+
return { status: -4, frames: output, framesRead: 0 };
|
|
5470
|
+
try {
|
|
5471
|
+
const result = this.lib.audioReadCapture(engine2, output, frameCount);
|
|
5472
|
+
return { status: result.status, frames: output, framesRead: Math.min(frameCount, result.framesRead) };
|
|
5473
|
+
} catch (cause) {
|
|
5474
|
+
return { status: -1, frames: output, framesRead: 0, cause };
|
|
5475
|
+
}
|
|
5476
|
+
}
|
|
5477
|
+
getCaptureStatsInternal(_owner) {
|
|
5478
|
+
const engine2 = this.engine;
|
|
5479
|
+
if (!engine2)
|
|
5480
|
+
return { status: -1, stats: null };
|
|
5481
|
+
try {
|
|
5482
|
+
const result = this.lib.audioGetCaptureStats(engine2);
|
|
5483
|
+
if (result.status !== 0 || result.stats == null)
|
|
5484
|
+
return { status: result.status, stats: null };
|
|
5485
|
+
return {
|
|
5486
|
+
status: 0,
|
|
5487
|
+
stats: {
|
|
5488
|
+
sampleRate: result.stats.sampleRate,
|
|
5489
|
+
channels: result.stats.channels,
|
|
5490
|
+
capacityFrames: result.stats.capacityFrames,
|
|
5491
|
+
bufferedFrames: result.stats.bufferedFrames,
|
|
5492
|
+
framesReceived: result.stats.framesReceived,
|
|
5493
|
+
framesRead: result.stats.framesRead,
|
|
5494
|
+
framesDropped: result.stats.framesDropped
|
|
5495
|
+
}
|
|
5496
|
+
};
|
|
5497
|
+
} catch (cause) {
|
|
5498
|
+
return { status: -1, stats: null, cause };
|
|
4182
5499
|
}
|
|
4183
|
-
|
|
5500
|
+
}
|
|
5501
|
+
stopCaptureInternal(owner) {
|
|
5502
|
+
if (this.captureOwner != null && this.captureOwner !== owner)
|
|
5503
|
+
return { status: -1 };
|
|
5504
|
+
if (!this.captureDeviceOpen)
|
|
5505
|
+
return { status: 0 };
|
|
5506
|
+
const engine2 = this.engine;
|
|
5507
|
+
if (!engine2)
|
|
5508
|
+
return { status: -1 };
|
|
5509
|
+
let status;
|
|
5510
|
+
try {
|
|
5511
|
+
status = this.lib.audioStopCapture(engine2);
|
|
5512
|
+
} catch (cause) {
|
|
5513
|
+
return { status: -1, cause };
|
|
5514
|
+
}
|
|
5515
|
+
if (status !== 0)
|
|
5516
|
+
return { status };
|
|
5517
|
+
const wasStarted = this.captureStarted;
|
|
5518
|
+
this.captureDeviceOpen = false;
|
|
5519
|
+
this.captureStarted = false;
|
|
5520
|
+
if (wasStarted)
|
|
5521
|
+
this.emit("captureStopped");
|
|
5522
|
+
return { status: 0 };
|
|
4184
5523
|
}
|
|
4185
5524
|
getStats() {
|
|
4186
5525
|
const engine2 = this.engine;
|
|
@@ -4198,24 +5537,535 @@ class Audio extends EventEmitter {
|
|
|
4198
5537
|
if (!this.engine || this.disposing)
|
|
4199
5538
|
return;
|
|
4200
5539
|
this.disposing = true;
|
|
5540
|
+
let firstError;
|
|
5541
|
+
let hasError = false;
|
|
5542
|
+
let childCleanupFailed = false;
|
|
5543
|
+
const runCleanup = (operation) => {
|
|
5544
|
+
try {
|
|
5545
|
+
operation();
|
|
5546
|
+
} catch (error) {
|
|
5547
|
+
if (!hasError) {
|
|
5548
|
+
firstError = error;
|
|
5549
|
+
hasError = true;
|
|
5550
|
+
}
|
|
5551
|
+
}
|
|
5552
|
+
};
|
|
4201
5553
|
try {
|
|
4202
|
-
for (const stream of [...this.streams])
|
|
4203
|
-
|
|
5554
|
+
for (const stream of [...this.streams]) {
|
|
5555
|
+
try {
|
|
5556
|
+
stream.dispose();
|
|
5557
|
+
} catch (error) {
|
|
5558
|
+
childCleanupFailed = true;
|
|
5559
|
+
if (!hasError) {
|
|
5560
|
+
firstError = error;
|
|
5561
|
+
hasError = true;
|
|
5562
|
+
}
|
|
5563
|
+
}
|
|
5564
|
+
}
|
|
5565
|
+
if (this.captureDeviceOpen) {
|
|
5566
|
+
let result;
|
|
5567
|
+
runCleanup(() => {
|
|
5568
|
+
result = this.stopCaptureInternal(this.captureOwner);
|
|
5569
|
+
});
|
|
5570
|
+
if (result && result.status !== 0) {
|
|
5571
|
+
const wasStarted = this.captureStarted;
|
|
5572
|
+
this.captureDeviceOpen = false;
|
|
5573
|
+
this.captureStarted = false;
|
|
5574
|
+
runCleanup(() => this.emitError("stopCapture", result.status, undefined, result.cause));
|
|
5575
|
+
if (wasStarted)
|
|
5576
|
+
runCleanup(() => void this.emit("captureStopped"));
|
|
5577
|
+
}
|
|
5578
|
+
}
|
|
5579
|
+
if (this.captureStream != null) {
|
|
5580
|
+
runCleanup(() => refreshAudioCaptureStreamFinalStats(this.captureStream));
|
|
5581
|
+
}
|
|
4204
5582
|
if (this.mixerStarted) {
|
|
4205
|
-
this.stop();
|
|
5583
|
+
runCleanup(() => void this.stop());
|
|
4206
5584
|
}
|
|
4207
5585
|
this.groups.clear();
|
|
4208
|
-
this.
|
|
4209
|
-
|
|
4210
|
-
|
|
5586
|
+
const engine2 = this.engine;
|
|
5587
|
+
let engineDestroyed = false;
|
|
5588
|
+
if (!childCleanupFailed) {
|
|
5589
|
+
runCleanup(() => {
|
|
5590
|
+
this.lib.destroyAudioEngine(engine2);
|
|
5591
|
+
engineDestroyed = true;
|
|
5592
|
+
});
|
|
5593
|
+
}
|
|
5594
|
+
if (engineDestroyed) {
|
|
5595
|
+
this.engine = null;
|
|
5596
|
+
this.captureStarted = false;
|
|
5597
|
+
this.captureDeviceOpen = false;
|
|
5598
|
+
this.captureBufferAvailable = false;
|
|
5599
|
+
this.captureCapacityFrames = 0;
|
|
5600
|
+
this.captureOwner = null;
|
|
5601
|
+
this.captureStream = null;
|
|
5602
|
+
runCleanup(() => void this.emit("disposed"));
|
|
5603
|
+
}
|
|
4211
5604
|
} finally {
|
|
4212
5605
|
this.disposing = false;
|
|
4213
5606
|
}
|
|
5607
|
+
if (hasError)
|
|
5608
|
+
throw firstError;
|
|
4214
5609
|
}
|
|
4215
5610
|
}
|
|
4216
5611
|
function setupAudio(options = {}) {
|
|
4217
5612
|
return Audio.create(options);
|
|
4218
5613
|
}
|
|
5614
|
+
// src/image.ts
|
|
5615
|
+
import { open, stat } from "node:fs/promises";
|
|
5616
|
+
class ImageLoadError extends Error {
|
|
5617
|
+
code;
|
|
5618
|
+
source;
|
|
5619
|
+
status;
|
|
5620
|
+
constructor(code, source, message, options) {
|
|
5621
|
+
super(message, { cause: options?.cause });
|
|
5622
|
+
this.name = "ImageLoadError";
|
|
5623
|
+
this.code = code;
|
|
5624
|
+
this.source = source;
|
|
5625
|
+
this.status = options?.status;
|
|
5626
|
+
}
|
|
5627
|
+
}
|
|
5628
|
+
|
|
5629
|
+
class OwnedRawImageImpl {
|
|
5630
|
+
data;
|
|
5631
|
+
width;
|
|
5632
|
+
height;
|
|
5633
|
+
stride;
|
|
5634
|
+
lib;
|
|
5635
|
+
handle;
|
|
5636
|
+
format = "rgba8";
|
|
5637
|
+
colorSpace = "srgb";
|
|
5638
|
+
alpha = "straight";
|
|
5639
|
+
constructor(data, width, height, stride, lib, handle) {
|
|
5640
|
+
this.data = data;
|
|
5641
|
+
this.width = width;
|
|
5642
|
+
this.height = height;
|
|
5643
|
+
this.stride = stride;
|
|
5644
|
+
this.lib = lib;
|
|
5645
|
+
this.handle = handle;
|
|
5646
|
+
}
|
|
5647
|
+
dispose() {
|
|
5648
|
+
if (!this.handle)
|
|
5649
|
+
return;
|
|
5650
|
+
this.lib.imageDestroy(this.handle);
|
|
5651
|
+
this.handle = null;
|
|
5652
|
+
}
|
|
5653
|
+
}
|
|
5654
|
+
var STATUS_MESSAGES = [
|
|
5655
|
+
"ok",
|
|
5656
|
+
"invalid image handle",
|
|
5657
|
+
"unsupported image format",
|
|
5658
|
+
"unsupported image color space",
|
|
5659
|
+
"malformed image data",
|
|
5660
|
+
"image dimensions exceed limits",
|
|
5661
|
+
"image memory limit exceeded",
|
|
5662
|
+
"invalid image argument",
|
|
5663
|
+
"out of memory",
|
|
5664
|
+
"image output buffer is too small",
|
|
5665
|
+
"internal image error",
|
|
5666
|
+
"unsupported image feature"
|
|
5667
|
+
];
|
|
5668
|
+
var STATUS_CODES = [
|
|
5669
|
+
"internal-error",
|
|
5670
|
+
"invalid-handle",
|
|
5671
|
+
"unsupported-format",
|
|
5672
|
+
"unsupported-color-space",
|
|
5673
|
+
"malformed-data",
|
|
5674
|
+
"dimension-limit",
|
|
5675
|
+
"memory-limit",
|
|
5676
|
+
"invalid-argument",
|
|
5677
|
+
"out-of-memory",
|
|
5678
|
+
"output-too-small",
|
|
5679
|
+
"internal-error",
|
|
5680
|
+
"unsupported-feature"
|
|
5681
|
+
];
|
|
5682
|
+
|
|
5683
|
+
class ImageError extends Error {
|
|
5684
|
+
code;
|
|
5685
|
+
status;
|
|
5686
|
+
constructor(status) {
|
|
5687
|
+
super(`Native image operation failed: ${STATUS_MESSAGES[status] ?? `unknown status ${status}`}`);
|
|
5688
|
+
this.name = "ImageError";
|
|
5689
|
+
this.status = status;
|
|
5690
|
+
this.code = STATUS_CODES[status] ?? "internal-error";
|
|
5691
|
+
}
|
|
5692
|
+
}
|
|
5693
|
+
var FILTER_IDS = {
|
|
5694
|
+
default: 0,
|
|
5695
|
+
area: 1,
|
|
5696
|
+
triangle: 2,
|
|
5697
|
+
"cubic-bspline": 3,
|
|
5698
|
+
"catmull-rom": 4,
|
|
5699
|
+
mitchell: 5,
|
|
5700
|
+
nearest: 6
|
|
5701
|
+
};
|
|
5702
|
+
var BLEND_IDS = {
|
|
5703
|
+
"source-over": 0,
|
|
5704
|
+
source: 1,
|
|
5705
|
+
"destination-over": 2
|
|
5706
|
+
};
|
|
5707
|
+
var PIXEL_FORMAT_BGRA = {
|
|
5708
|
+
rgba8: false,
|
|
5709
|
+
bgra8: true
|
|
5710
|
+
};
|
|
5711
|
+
var MAX_ENCODED_BYTES = 64 * 1024 * 1024;
|
|
5712
|
+
function imageError(status) {
|
|
5713
|
+
return new ImageError(status);
|
|
5714
|
+
}
|
|
5715
|
+
function checkStatus(status) {
|
|
5716
|
+
if (status !== 0)
|
|
5717
|
+
throw imageError(status);
|
|
5718
|
+
}
|
|
5719
|
+
function requireMappedOption(mapping, value, name) {
|
|
5720
|
+
if (!Object.prototype.hasOwnProperty.call(mapping, value))
|
|
5721
|
+
throw new TypeError(`Unsupported ${name}: ${String(value)}`);
|
|
5722
|
+
return mapping[value];
|
|
5723
|
+
}
|
|
5724
|
+
function requireU32(value, name, allowZero = false) {
|
|
5725
|
+
if (!Number.isSafeInteger(value) || value < (allowZero ? 0 : 1) || value > 4294967295) {
|
|
5726
|
+
throw new RangeError(`${name} must be ${allowZero ? "a non-negative" : "a positive"} u32 integer`);
|
|
5727
|
+
}
|
|
5728
|
+
return value;
|
|
5729
|
+
}
|
|
5730
|
+
function requireI32(value, name) {
|
|
5731
|
+
if (!Number.isSafeInteger(value) || value < -2147483648 || value > 2147483647) {
|
|
5732
|
+
throw new RangeError(`${name} must be an i32 integer`);
|
|
5733
|
+
}
|
|
5734
|
+
return value;
|
|
5735
|
+
}
|
|
5736
|
+
function requireByte(value, name) {
|
|
5737
|
+
if (!Number.isInteger(value) || value < 0 || value > 255)
|
|
5738
|
+
throw new RangeError(`${name} must be an integer from 0 to 255`);
|
|
5739
|
+
return value;
|
|
5740
|
+
}
|
|
5741
|
+
function unpackInfo(info) {
|
|
5742
|
+
const format = ["unknown", "png", "raw-rgba", "jpeg", "webp", "gif"][info.format];
|
|
5743
|
+
if (!format || format === "unknown")
|
|
5744
|
+
throw new Error(`Unknown native image format ${info.format}`);
|
|
5745
|
+
return {
|
|
5746
|
+
width: info.width,
|
|
5747
|
+
height: info.height,
|
|
5748
|
+
sourceWidth: info.sourceWidth,
|
|
5749
|
+
sourceHeight: info.sourceHeight,
|
|
5750
|
+
format,
|
|
5751
|
+
colorStatus: info.colorStatus === 1 ? "explicit-srgb" : "assumed-srgb",
|
|
5752
|
+
orientation: info.orientation,
|
|
5753
|
+
hasAlpha: info.hasAlpha !== 0
|
|
5754
|
+
};
|
|
5755
|
+
}
|
|
5756
|
+
function encodedBytes(data) {
|
|
5757
|
+
if (data instanceof Uint8Array)
|
|
5758
|
+
return data;
|
|
5759
|
+
if (data instanceof ArrayBuffer)
|
|
5760
|
+
return new Uint8Array(data);
|
|
5761
|
+
throw new TypeError("image data must be a Uint8Array or ArrayBuffer");
|
|
5762
|
+
}
|
|
5763
|
+
async function readResponseBytes(response, signal) {
|
|
5764
|
+
const contentLength = response.headers.get("content-length");
|
|
5765
|
+
if (contentLength !== null) {
|
|
5766
|
+
const declaredLength = Number(contentLength);
|
|
5767
|
+
if (Number.isFinite(declaredLength) && declaredLength > MAX_ENCODED_BYTES) {
|
|
5768
|
+
response.body?.cancel().catch(() => {});
|
|
5769
|
+
throw imageError(6);
|
|
5770
|
+
}
|
|
5771
|
+
}
|
|
5772
|
+
if (!response.body)
|
|
5773
|
+
return new Uint8Array;
|
|
5774
|
+
const reader = response.body.getReader();
|
|
5775
|
+
const abort = () => void reader.cancel(signal?.reason).catch(() => {});
|
|
5776
|
+
signal?.addEventListener("abort", abort, { once: true });
|
|
5777
|
+
let data = new Uint8Array;
|
|
5778
|
+
let total = 0;
|
|
5779
|
+
try {
|
|
5780
|
+
while (true) {
|
|
5781
|
+
signal?.throwIfAborted();
|
|
5782
|
+
const { done, value } = await reader.read();
|
|
5783
|
+
if (done)
|
|
5784
|
+
break;
|
|
5785
|
+
if (value.byteLength > MAX_ENCODED_BYTES - total) {
|
|
5786
|
+
throw imageError(6);
|
|
5787
|
+
}
|
|
5788
|
+
if (value.byteLength === 0)
|
|
5789
|
+
continue;
|
|
5790
|
+
const required = total + value.byteLength;
|
|
5791
|
+
if (required > data.byteLength) {
|
|
5792
|
+
const capacity = Math.min(MAX_ENCODED_BYTES, Math.max(required, data.byteLength * 2));
|
|
5793
|
+
const grown = new Uint8Array(capacity);
|
|
5794
|
+
grown.set(data.subarray(0, total));
|
|
5795
|
+
data = grown;
|
|
5796
|
+
}
|
|
5797
|
+
data.set(value, total);
|
|
5798
|
+
total = required;
|
|
5799
|
+
}
|
|
5800
|
+
} catch (error) {
|
|
5801
|
+
reader.cancel().catch(() => {});
|
|
5802
|
+
throw error;
|
|
5803
|
+
} finally {
|
|
5804
|
+
signal?.removeEventListener("abort", abort);
|
|
5805
|
+
reader.releaseLock();
|
|
5806
|
+
}
|
|
5807
|
+
return data.byteLength === total ? data : data.slice(0, total);
|
|
5808
|
+
}
|
|
5809
|
+
async function readFileBytes(path, signal) {
|
|
5810
|
+
signal?.throwIfAborted();
|
|
5811
|
+
if ((await stat(path)).size > MAX_ENCODED_BYTES)
|
|
5812
|
+
throw imageError(6);
|
|
5813
|
+
const file = await open(path, "r");
|
|
5814
|
+
const chunks = [];
|
|
5815
|
+
let total = 0;
|
|
5816
|
+
try {
|
|
5817
|
+
while (true) {
|
|
5818
|
+
signal?.throwIfAborted();
|
|
5819
|
+
const chunk = new Uint8Array(Math.min(64 * 1024, MAX_ENCODED_BYTES - total + 1));
|
|
5820
|
+
const { bytesRead } = await file.read(chunk, 0, chunk.byteLength, null);
|
|
5821
|
+
if (bytesRead === 0)
|
|
5822
|
+
break;
|
|
5823
|
+
total += bytesRead;
|
|
5824
|
+
if (total > MAX_ENCODED_BYTES)
|
|
5825
|
+
throw imageError(6);
|
|
5826
|
+
chunks.push(chunk.subarray(0, bytesRead));
|
|
5827
|
+
}
|
|
5828
|
+
} finally {
|
|
5829
|
+
await file.close();
|
|
5830
|
+
}
|
|
5831
|
+
const data = new Uint8Array(total);
|
|
5832
|
+
let offset = 0;
|
|
5833
|
+
for (const chunk of chunks) {
|
|
5834
|
+
data.set(chunk, offset);
|
|
5835
|
+
offset += chunk.byteLength;
|
|
5836
|
+
}
|
|
5837
|
+
return data;
|
|
5838
|
+
}
|
|
5839
|
+
async function loadResponseBytes(response, source, signal) {
|
|
5840
|
+
try {
|
|
5841
|
+
signal?.throwIfAborted();
|
|
5842
|
+
} catch (error) {
|
|
5843
|
+
response.body?.cancel().catch(() => {});
|
|
5844
|
+
throw error;
|
|
5845
|
+
}
|
|
5846
|
+
if (!response.ok) {
|
|
5847
|
+
response.body?.cancel().catch(() => {});
|
|
5848
|
+
throw new ImageLoadError("http-status", source, `Failed to fetch image: HTTP ${response.status}`, {
|
|
5849
|
+
status: response.status
|
|
5850
|
+
});
|
|
5851
|
+
}
|
|
5852
|
+
try {
|
|
5853
|
+
const data = await readResponseBytes(response, signal);
|
|
5854
|
+
signal?.throwIfAborted();
|
|
5855
|
+
return data;
|
|
5856
|
+
} catch (error) {
|
|
5857
|
+
if (signal?.aborted)
|
|
5858
|
+
throw signal.reason;
|
|
5859
|
+
if (error instanceof ImageError)
|
|
5860
|
+
throw error;
|
|
5861
|
+
throw new ImageLoadError("network", source, `Failed to read image response: ${source}`, { cause: error });
|
|
5862
|
+
}
|
|
5863
|
+
}
|
|
5864
|
+
function imageInfo(data) {
|
|
5865
|
+
const bytes = encodedBytes(data);
|
|
5866
|
+
if (bytes.byteLength === 0)
|
|
5867
|
+
throw new TypeError("image data must not be empty");
|
|
5868
|
+
const result = resolveRenderLib().imageInfo(bytes);
|
|
5869
|
+
checkStatus(result.status);
|
|
5870
|
+
return unpackInfo(result.info);
|
|
5871
|
+
}
|
|
5872
|
+
|
|
5873
|
+
class NativeImage {
|
|
5874
|
+
lib;
|
|
5875
|
+
handle;
|
|
5876
|
+
imageInfo;
|
|
5877
|
+
constructor(lib, handle, info) {
|
|
5878
|
+
this.lib = lib;
|
|
5879
|
+
this.handle = handle;
|
|
5880
|
+
this.imageInfo = info;
|
|
5881
|
+
}
|
|
5882
|
+
static decode(data) {
|
|
5883
|
+
const bytes = encodedBytes(data);
|
|
5884
|
+
if (bytes.byteLength === 0)
|
|
5885
|
+
throw new TypeError("image data must not be empty");
|
|
5886
|
+
const lib = resolveRenderLib();
|
|
5887
|
+
const result = lib.imageDecode(bytes);
|
|
5888
|
+
checkStatus(result.status);
|
|
5889
|
+
if (!result.handle)
|
|
5890
|
+
throw imageError(10);
|
|
5891
|
+
return NativeImage.fromHandle(lib, result.handle);
|
|
5892
|
+
}
|
|
5893
|
+
static async load(source, options = {}) {
|
|
5894
|
+
if (source instanceof Response) {
|
|
5895
|
+
return NativeImage.decode(await loadResponseBytes(source, source.url || "Response", options.signal));
|
|
5896
|
+
}
|
|
5897
|
+
options.signal?.throwIfAborted();
|
|
5898
|
+
if (source instanceof Uint8Array || source instanceof ArrayBuffer)
|
|
5899
|
+
return NativeImage.decode(source);
|
|
5900
|
+
if (source instanceof Blob) {
|
|
5901
|
+
if (source.size > MAX_ENCODED_BYTES)
|
|
5902
|
+
throw imageError(6);
|
|
5903
|
+
return NativeImage.decode(await loadResponseBytes(new Response(source), "Blob", options.signal));
|
|
5904
|
+
}
|
|
5905
|
+
const url = source instanceof URL ? source : (/^(?:https?|file|blob|data):/i.test(source) || /^[a-z][a-z0-9+.-]*:\/\//i.test(source)) && !/^[a-z]:[\\/]/i.test(source) ? new URL(source) : null;
|
|
5906
|
+
if (!url || url.protocol === "file:") {
|
|
5907
|
+
const path = url ?? source;
|
|
5908
|
+
let data;
|
|
5909
|
+
try {
|
|
5910
|
+
data = await readFileBytes(path, options.signal);
|
|
5911
|
+
} catch (error) {
|
|
5912
|
+
if (options.signal?.aborted)
|
|
5913
|
+
throw options.signal.reason;
|
|
5914
|
+
if (error instanceof ImageError)
|
|
5915
|
+
throw error;
|
|
5916
|
+
throw new ImageLoadError("file-read", String(source), `Failed to read image: ${String(source)}`, {
|
|
5917
|
+
cause: error
|
|
5918
|
+
});
|
|
5919
|
+
}
|
|
5920
|
+
options.signal?.throwIfAborted();
|
|
5921
|
+
return NativeImage.decode(data);
|
|
5922
|
+
}
|
|
5923
|
+
if (url.protocol !== "http:" && url.protocol !== "https:" && url.protocol !== "blob:" && url.protocol !== "data:") {
|
|
5924
|
+
throw new ImageLoadError("unsupported-url-scheme", url.href, `Unsupported image URL scheme: ${url.protocol}`);
|
|
5925
|
+
}
|
|
5926
|
+
let response;
|
|
5927
|
+
try {
|
|
5928
|
+
response = await (options.fetch ?? globalThis.fetch)(url, { signal: options.signal });
|
|
5929
|
+
} catch (error) {
|
|
5930
|
+
if (options.signal?.aborted)
|
|
5931
|
+
throw options.signal.reason;
|
|
5932
|
+
throw new ImageLoadError("network", url.href, `Failed to fetch image: ${url.href}`, { cause: error });
|
|
5933
|
+
}
|
|
5934
|
+
return NativeImage.decode(await loadResponseBytes(response, url.href, options.signal));
|
|
5935
|
+
}
|
|
5936
|
+
static fromRgba(pixels, width, height, stride = width * 4) {
|
|
5937
|
+
if (!(pixels instanceof Uint8Array))
|
|
5938
|
+
throw new TypeError("pixels must be a Uint8Array");
|
|
5939
|
+
requireU32(width, "width");
|
|
5940
|
+
requireU32(height, "height");
|
|
5941
|
+
requireU32(stride, "stride");
|
|
5942
|
+
const lib = resolveRenderLib();
|
|
5943
|
+
const result = lib.imageCreateFromRgba(pixels, width, height, stride);
|
|
5944
|
+
checkStatus(result.status);
|
|
5945
|
+
if (!result.handle)
|
|
5946
|
+
throw imageError(10);
|
|
5947
|
+
return NativeImage.fromHandle(lib, result.handle);
|
|
5948
|
+
}
|
|
5949
|
+
static fromHandle(lib, handle) {
|
|
5950
|
+
const result = lib.imageGetInfo(handle);
|
|
5951
|
+
if (result.status !== 0) {
|
|
5952
|
+
lib.imageDestroy(handle);
|
|
5953
|
+
throw imageError(result.status);
|
|
5954
|
+
}
|
|
5955
|
+
return new NativeImage(lib, handle, unpackInfo(result.info));
|
|
5956
|
+
}
|
|
5957
|
+
guard() {
|
|
5958
|
+
if (!this.handle)
|
|
5959
|
+
throw new Error("NativeImage is disposed");
|
|
5960
|
+
return this.handle;
|
|
5961
|
+
}
|
|
5962
|
+
get ptr() {
|
|
5963
|
+
return this.guard();
|
|
5964
|
+
}
|
|
5965
|
+
wrap(result) {
|
|
5966
|
+
checkStatus(result.status);
|
|
5967
|
+
if (!result.handle)
|
|
5968
|
+
throw imageError(10);
|
|
5969
|
+
return NativeImage.fromHandle(this.lib, result.handle);
|
|
5970
|
+
}
|
|
5971
|
+
info() {
|
|
5972
|
+
this.guard();
|
|
5973
|
+
return { ...this.imageInfo };
|
|
5974
|
+
}
|
|
5975
|
+
get width() {
|
|
5976
|
+
this.guard();
|
|
5977
|
+
return this.imageInfo.width;
|
|
5978
|
+
}
|
|
5979
|
+
get height() {
|
|
5980
|
+
this.guard();
|
|
5981
|
+
return this.imageInfo.height;
|
|
5982
|
+
}
|
|
5983
|
+
clone() {
|
|
5984
|
+
return this.wrap(this.lib.imageClone(this.guard()));
|
|
5985
|
+
}
|
|
5986
|
+
resize(options) {
|
|
5987
|
+
if (!options || options.width === undefined && options.height === undefined) {
|
|
5988
|
+
throw new TypeError("resize requires width, height, or both");
|
|
5989
|
+
}
|
|
5990
|
+
let width = options.width;
|
|
5991
|
+
let height = options.height;
|
|
5992
|
+
if (width !== undefined)
|
|
5993
|
+
requireU32(width, "width");
|
|
5994
|
+
if (height !== undefined)
|
|
5995
|
+
requireU32(height, "height");
|
|
5996
|
+
if (width === undefined)
|
|
5997
|
+
width = Math.max(1, Math.round(this.width * height / this.height));
|
|
5998
|
+
if (height === undefined)
|
|
5999
|
+
height = Math.max(1, Math.round(this.height * width / this.width));
|
|
6000
|
+
requireU32(width, "width");
|
|
6001
|
+
requireU32(height, "height");
|
|
6002
|
+
const filter = requireMappedOption(FILTER_IDS, options.kernel ?? "area", "resize kernel");
|
|
6003
|
+
return this.wrap(this.lib.imageResize(this.guard(), width, height, filter));
|
|
6004
|
+
}
|
|
6005
|
+
extract(options) {
|
|
6006
|
+
return this.wrap(this.lib.imageExtract(this.guard(), requireU32(options.left, "left", true), requireU32(options.top, "top", true), requireU32(options.width, "width"), requireU32(options.height, "height")));
|
|
6007
|
+
}
|
|
6008
|
+
extend(options = {}) {
|
|
6009
|
+
const background = options.background ?? [0, 0, 0, 0];
|
|
6010
|
+
if (background.length !== 4)
|
|
6011
|
+
throw new TypeError("background must contain four RGBA channels");
|
|
6012
|
+
const color = Uint8Array.from(background.map((value, index) => requireByte(value, `background[${index}]`)));
|
|
6013
|
+
return this.wrap(this.lib.imageExtend(this.guard(), requireU32(options.top ?? 0, "top", true), requireU32(options.right ?? 0, "right", true), requireU32(options.bottom ?? 0, "bottom", true), requireU32(options.left ?? 0, "left", true), color));
|
|
6014
|
+
}
|
|
6015
|
+
rotate(angle) {
|
|
6016
|
+
const operation = angle === 90 ? 0 : angle === 180 ? 1 : angle === 270 ? 2 : -1;
|
|
6017
|
+
if (operation < 0)
|
|
6018
|
+
throw new RangeError("angle must be 90, 180, or 270");
|
|
6019
|
+
return this.wrap(this.lib.imageTransform(this.guard(), operation));
|
|
6020
|
+
}
|
|
6021
|
+
flip() {
|
|
6022
|
+
return this.wrap(this.lib.imageTransform(this.guard(), 3));
|
|
6023
|
+
}
|
|
6024
|
+
flop() {
|
|
6025
|
+
return this.wrap(this.lib.imageTransform(this.guard(), 4));
|
|
6026
|
+
}
|
|
6027
|
+
composite(overlay, options = {}) {
|
|
6028
|
+
if (!(overlay instanceof NativeImage))
|
|
6029
|
+
throw new TypeError("overlay must be a NativeImage");
|
|
6030
|
+
const opacity = options.opacity ?? 1;
|
|
6031
|
+
if (!Number.isFinite(opacity) || opacity < 0 || opacity > 1)
|
|
6032
|
+
throw new RangeError("opacity must be between 0 and 1");
|
|
6033
|
+
return this.wrap(this.lib.imageComposite(this.guard(), overlay.guard(), requireI32(options.left ?? 0, "left"), requireI32(options.top ?? 0, "top"), requireMappedOption(BLEND_IDS, options.blend ?? "source-over", "blend mode"), Math.round(opacity * 255)));
|
|
6034
|
+
}
|
|
6035
|
+
raw(format = "rgba8") {
|
|
6036
|
+
const stride = this.width * 4;
|
|
6037
|
+
const data = new Uint8Array(stride * this.height);
|
|
6038
|
+
checkStatus(this.lib.imageCopyPixels(this.guard(), data, stride, requireMappedOption(PIXEL_FORMAT_BGRA, format, "pixel format")));
|
|
6039
|
+
return { data, width: this.width, height: this.height, stride, format, colorSpace: "srgb", alpha: "straight" };
|
|
6040
|
+
}
|
|
6041
|
+
takeRaw() {
|
|
6042
|
+
const handle = this.guard();
|
|
6043
|
+
const pointer = this.lib.imageGetPixelsPtr(handle);
|
|
6044
|
+
if (!pointer)
|
|
6045
|
+
throw new Error("Cannot transfer image pixels while native buffers retain the image");
|
|
6046
|
+
const width = this.imageInfo.width;
|
|
6047
|
+
const height = this.imageInfo.height;
|
|
6048
|
+
const stride = width * 4;
|
|
6049
|
+
const data = new Uint8Array(toArrayBuffer(pointer, 0, stride * height));
|
|
6050
|
+
const raw = new OwnedRawImageImpl(data, width, height, stride, this.lib, handle);
|
|
6051
|
+
this.handle = null;
|
|
6052
|
+
return raw;
|
|
6053
|
+
}
|
|
6054
|
+
copyTo(destination, options = {}) {
|
|
6055
|
+
if (!(destination instanceof Uint8Array))
|
|
6056
|
+
throw new TypeError("destination must be a Uint8Array");
|
|
6057
|
+
const stride = options.stride ?? this.width * 4;
|
|
6058
|
+
requireU32(stride, "stride");
|
|
6059
|
+
const bgra = requireMappedOption(PIXEL_FORMAT_BGRA, options.format ?? "rgba8", "pixel format");
|
|
6060
|
+
checkStatus(this.lib.imageCopyPixels(this.guard(), destination, stride, bgra));
|
|
6061
|
+
}
|
|
6062
|
+
dispose() {
|
|
6063
|
+
if (!this.handle)
|
|
6064
|
+
return;
|
|
6065
|
+
this.lib.imageDestroy(this.handle);
|
|
6066
|
+
this.handle = null;
|
|
6067
|
+
}
|
|
6068
|
+
}
|
|
4219
6069
|
// src/renderables/FrameBuffer.ts
|
|
4220
6070
|
class FrameBufferRenderable extends Renderable {
|
|
4221
6071
|
frameBuffer;
|
|
@@ -6843,6 +8693,189 @@ class InputRenderable extends TextareaRenderable {
|
|
|
6843
8693
|
}
|
|
6844
8694
|
set initialValue(value) {}
|
|
6845
8695
|
}
|
|
8696
|
+
// src/renderables/Image.ts
|
|
8697
|
+
var TRANSPARENT = RGBA.fromValues(0, 0, 0, 0);
|
|
8698
|
+
function resolveImageRenderProtocol(requested, capabilities, hasResolution) {
|
|
8699
|
+
if (requested !== "auto")
|
|
8700
|
+
return requested === "sixel" && !hasResolution ? "blocks" : requested;
|
|
8701
|
+
const configured = capabilities?.image_protocol ?? "auto";
|
|
8702
|
+
if (configured !== "auto")
|
|
8703
|
+
return configured === "sixel" && !hasResolution ? "blocks" : configured;
|
|
8704
|
+
if (!capabilities || capabilities.multiplexer === "tmux")
|
|
8705
|
+
return "blocks";
|
|
8706
|
+
if (capabilities.kitty_graphics)
|
|
8707
|
+
return "kitty";
|
|
8708
|
+
if (capabilities.sixel && hasResolution)
|
|
8709
|
+
return "sixel";
|
|
8710
|
+
return "blocks";
|
|
8711
|
+
}
|
|
8712
|
+
function pixelResolution(ctx) {
|
|
8713
|
+
const terminalWidth = ctx.terminalWidth ?? 0;
|
|
8714
|
+
const terminalHeight = ctx.terminalHeight ?? 0;
|
|
8715
|
+
const resolution = terminalWidth > 0 && terminalHeight > 0 ? ctx.resolution : null;
|
|
8716
|
+
return resolution && resolution.width > 0 && resolution.height > 0 ? resolution : null;
|
|
8717
|
+
}
|
|
8718
|
+
|
|
8719
|
+
class ImageRenderable extends Renderable {
|
|
8720
|
+
_source;
|
|
8721
|
+
_image = null;
|
|
8722
|
+
_loadError = null;
|
|
8723
|
+
_loadController = null;
|
|
8724
|
+
onLoad;
|
|
8725
|
+
onError;
|
|
8726
|
+
_fit;
|
|
8727
|
+
_protocol;
|
|
8728
|
+
loadPromise = null;
|
|
8729
|
+
constructor(ctx, options) {
|
|
8730
|
+
super(ctx, options);
|
|
8731
|
+
this._fit = options.fit ?? "fit";
|
|
8732
|
+
this._protocol = options.protocol ?? "auto";
|
|
8733
|
+
this.onLoad = options.onLoad;
|
|
8734
|
+
this.onError = options.onError;
|
|
8735
|
+
if (options.source !== undefined)
|
|
8736
|
+
this.source = options.source;
|
|
8737
|
+
}
|
|
8738
|
+
get source() {
|
|
8739
|
+
return this._source;
|
|
8740
|
+
}
|
|
8741
|
+
set source(source) {
|
|
8742
|
+
source ??= undefined;
|
|
8743
|
+
if (source === this._source)
|
|
8744
|
+
return;
|
|
8745
|
+
this._source = source;
|
|
8746
|
+
this._loadController?.abort();
|
|
8747
|
+
this._loadController = null;
|
|
8748
|
+
if (source === undefined) {
|
|
8749
|
+
this._loadError = null;
|
|
8750
|
+
this._image?.dispose();
|
|
8751
|
+
this._image = null;
|
|
8752
|
+
this.loadPromise = null;
|
|
8753
|
+
this.requestRender();
|
|
8754
|
+
return;
|
|
8755
|
+
}
|
|
8756
|
+
const controller = new AbortController;
|
|
8757
|
+
this._loadController = controller;
|
|
8758
|
+
this._loadError = null;
|
|
8759
|
+
this.loadPromise = this.load(source, controller);
|
|
8760
|
+
}
|
|
8761
|
+
get image() {
|
|
8762
|
+
return this._image;
|
|
8763
|
+
}
|
|
8764
|
+
get fit() {
|
|
8765
|
+
return this._fit;
|
|
8766
|
+
}
|
|
8767
|
+
set fit(value) {
|
|
8768
|
+
const next = value ?? "fit";
|
|
8769
|
+
if (this._fit === next)
|
|
8770
|
+
return;
|
|
8771
|
+
this._fit = next;
|
|
8772
|
+
this.requestRender();
|
|
8773
|
+
}
|
|
8774
|
+
get protocol() {
|
|
8775
|
+
return this._protocol;
|
|
8776
|
+
}
|
|
8777
|
+
set protocol(value) {
|
|
8778
|
+
const next = value ?? "auto";
|
|
8779
|
+
if (this._protocol === next)
|
|
8780
|
+
return;
|
|
8781
|
+
this._protocol = next;
|
|
8782
|
+
this.requestRender();
|
|
8783
|
+
}
|
|
8784
|
+
get effectiveProtocol() {
|
|
8785
|
+
return resolveImageRenderProtocol(this._protocol, this._ctx.capabilities, pixelResolution(this._ctx) !== null);
|
|
8786
|
+
}
|
|
8787
|
+
get cellAspectRatio() {
|
|
8788
|
+
const resolution = pixelResolution(this._ctx);
|
|
8789
|
+
if (!resolution)
|
|
8790
|
+
return 2;
|
|
8791
|
+
const cellWidth = resolution.width / this._ctx.terminalWidth;
|
|
8792
|
+
const cellHeight = resolution.height / this._ctx.terminalHeight;
|
|
8793
|
+
return cellWidth > 0 && cellHeight > 0 ? cellHeight / cellWidth : 2;
|
|
8794
|
+
}
|
|
8795
|
+
getFittedSize(targetWidth, targetHeight, cellAspectRatio = this.cellAspectRatio, sourceWidth = this._image?.width ?? 0, sourceHeight = this._image?.height ?? 0) {
|
|
8796
|
+
if (sourceWidth <= 0 || sourceHeight <= 0 || targetWidth <= 0 || targetHeight <= 0)
|
|
8797
|
+
return { width: 0, height: 0 };
|
|
8798
|
+
if (this._fit === "fill")
|
|
8799
|
+
return { width: targetWidth, height: targetHeight };
|
|
8800
|
+
const displayAspect = sourceWidth / sourceHeight * cellAspectRatio;
|
|
8801
|
+
const scale = this._fit === "fit" ? Math.min(targetWidth / displayAspect, targetHeight) : Math.max(targetWidth / displayAspect, targetHeight);
|
|
8802
|
+
return {
|
|
8803
|
+
width: Math.max(1, Math.round(displayAspect * scale)),
|
|
8804
|
+
height: Math.max(1, Math.round(scale))
|
|
8805
|
+
};
|
|
8806
|
+
}
|
|
8807
|
+
get loading() {
|
|
8808
|
+
return this._loadController !== null;
|
|
8809
|
+
}
|
|
8810
|
+
get loadError() {
|
|
8811
|
+
return this._loadError;
|
|
8812
|
+
}
|
|
8813
|
+
render(buffer, deltaTime) {
|
|
8814
|
+
if (this.buffered)
|
|
8815
|
+
this.frameBuffer?.clear(TRANSPARENT);
|
|
8816
|
+
super.render(buffer, deltaTime);
|
|
8817
|
+
}
|
|
8818
|
+
renderSelf(buffer) {
|
|
8819
|
+
if (!this._image || this.width <= 0 || this.height <= 0)
|
|
8820
|
+
return;
|
|
8821
|
+
const fitted = this._fit === "cover" ? { width: this.width, height: this.height } : this.getFittedSize(this.width, this.height);
|
|
8822
|
+
if (fitted.width <= 0 || fitted.height <= 0)
|
|
8823
|
+
return;
|
|
8824
|
+
const originX = this.buffered ? 0 : this._screenX;
|
|
8825
|
+
const originY = this.buffered ? 0 : this._screenY;
|
|
8826
|
+
const x = originX + Math.floor((this.width - fitted.width) / 2);
|
|
8827
|
+
const y = originY + Math.floor((this.height - fitted.height) / 2);
|
|
8828
|
+
const resolution = pixelResolution(this._ctx);
|
|
8829
|
+
const pixelWidth = resolution ? Math.max(1, Math.round(fitted.width * resolution.width / this._ctx.terminalWidth)) : 0;
|
|
8830
|
+
const pixelHeight = resolution ? Math.max(1, Math.round(fitted.height * resolution.height / this._ctx.terminalHeight)) : 0;
|
|
8831
|
+
let sourceX = 0;
|
|
8832
|
+
let sourceY = 0;
|
|
8833
|
+
let sourceWidth = this._image.width;
|
|
8834
|
+
let sourceHeight = this._image.height;
|
|
8835
|
+
if (this._fit === "cover") {
|
|
8836
|
+
const targetAspect = this.width / (this.height * this.cellAspectRatio);
|
|
8837
|
+
const sourceAspect = sourceWidth / sourceHeight;
|
|
8838
|
+
if (sourceAspect > targetAspect) {
|
|
8839
|
+
sourceWidth = Math.max(1, Math.round(sourceHeight * targetAspect));
|
|
8840
|
+
sourceX = Math.floor((this._image.width - sourceWidth) / 2);
|
|
8841
|
+
} else {
|
|
8842
|
+
sourceHeight = Math.max(1, Math.round(sourceWidth / targetAspect));
|
|
8843
|
+
sourceY = Math.floor((this._image.height - sourceHeight) / 2);
|
|
8844
|
+
}
|
|
8845
|
+
}
|
|
8846
|
+
buffer.drawImage(this._image, x, y, fitted.width, fitted.height, pixelWidth, pixelHeight, sourceX, sourceY, sourceWidth, sourceHeight, this._protocol);
|
|
8847
|
+
}
|
|
8848
|
+
async load(source, controller) {
|
|
8849
|
+
let image;
|
|
8850
|
+
try {
|
|
8851
|
+
image = await NativeImage.load(source, { signal: controller.signal });
|
|
8852
|
+
} catch (error) {
|
|
8853
|
+
if (controller.signal.aborted || this.isDestroyed || this._loadController !== controller)
|
|
8854
|
+
return;
|
|
8855
|
+
this._loadController = null;
|
|
8856
|
+
this._loadError = error;
|
|
8857
|
+
this.onError?.(error);
|
|
8858
|
+
return;
|
|
8859
|
+
}
|
|
8860
|
+
if (this.isDestroyed || this._loadController !== controller) {
|
|
8861
|
+
image.dispose();
|
|
8862
|
+
return;
|
|
8863
|
+
}
|
|
8864
|
+
const previous = this._image;
|
|
8865
|
+
this._image = image;
|
|
8866
|
+
this._loadController = null;
|
|
8867
|
+
previous?.dispose();
|
|
8868
|
+
this.requestRender();
|
|
8869
|
+
this.onLoad?.(image);
|
|
8870
|
+
}
|
|
8871
|
+
destroySelf() {
|
|
8872
|
+
this._loadController?.abort();
|
|
8873
|
+
this._loadController = null;
|
|
8874
|
+
this._image?.dispose();
|
|
8875
|
+
this._image = null;
|
|
8876
|
+
super.destroySelf();
|
|
8877
|
+
}
|
|
8878
|
+
}
|
|
6846
8879
|
// ../../node_modules/.bun/marked@17.0.1/node_modules/marked/lib/marked.esm.js
|
|
6847
8880
|
function L() {
|
|
6848
8881
|
return { async: false, breaks: false, extensions: null, gfm: true, hooks: null, pedantic: false, renderer: null, silent: false, tokenizer: null, walkTokens: null };
|
|
@@ -12839,7 +14872,9 @@ export {
|
|
|
12839
14872
|
rgbToHex,
|
|
12840
14873
|
reverse,
|
|
12841
14874
|
resolveRenderLib,
|
|
14875
|
+
resolveImageRenderProtocol,
|
|
12842
14876
|
resolveCoreSlot,
|
|
14877
|
+
resolveBundledFilePath,
|
|
12843
14878
|
renderFontToFrameBuffer,
|
|
12844
14879
|
registerEnvVar,
|
|
12845
14880
|
registerCorePlugin,
|
|
@@ -12881,6 +14916,7 @@ export {
|
|
|
12881
14916
|
isEditBufferRenderable,
|
|
12882
14917
|
instantiate,
|
|
12883
14918
|
infoStringToFiletype,
|
|
14919
|
+
imageInfo,
|
|
12884
14920
|
hsvToRgb,
|
|
12885
14921
|
hexToRgb,
|
|
12886
14922
|
hastToStyledText,
|
|
@@ -13013,6 +15049,7 @@ export {
|
|
|
13013
15049
|
OptimizedBuffer,
|
|
13014
15050
|
NativeSpanFeed,
|
|
13015
15051
|
NativeMeasureTargetKind,
|
|
15052
|
+
NativeImage,
|
|
13016
15053
|
NativeAudioStreamState2 as NativeAudioStreamState,
|
|
13017
15054
|
NativeAudioStreamFormat2 as NativeAudioStreamFormat,
|
|
13018
15055
|
NativeAudioStreamCloseReason2 as NativeAudioStreamCloseReason,
|
|
@@ -13031,6 +15068,9 @@ export {
|
|
|
13031
15068
|
InputRenderableEvents,
|
|
13032
15069
|
InputRenderable,
|
|
13033
15070
|
Input,
|
|
15071
|
+
ImageRenderable,
|
|
15072
|
+
ImageLoadError,
|
|
15073
|
+
ImageError,
|
|
13034
15074
|
INVERT_MATRIX,
|
|
13035
15075
|
Generic,
|
|
13036
15076
|
GREENSCALE_MATRIX,
|
|
@@ -13068,7 +15108,11 @@ export {
|
|
|
13068
15108
|
BaseRenderable,
|
|
13069
15109
|
AudioStreamError,
|
|
13070
15110
|
AudioStream,
|
|
15111
|
+
AudioRecorderError,
|
|
15112
|
+
AudioRecorder,
|
|
13071
15113
|
AudioInitializationError,
|
|
15114
|
+
AudioCaptureStreamError,
|
|
15115
|
+
AudioCaptureStream,
|
|
13072
15116
|
Audio,
|
|
13073
15117
|
ArrowRenderable,
|
|
13074
15118
|
ATTRIBUTE_BASE_MASK,
|
|
@@ -13079,5 +15123,5 @@ export {
|
|
|
13079
15123
|
ACHROMATOPSIA_MATRIX
|
|
13080
15124
|
};
|
|
13081
15125
|
|
|
13082
|
-
//# debugId=
|
|
13083
|
-
//# sourceMappingURL=index.js.map
|
|
15126
|
+
//# debugId=C687E1438C6F19C664756E2164756E21
|
|
15127
|
+
//# sourceMappingURL=index.node.js.map
|