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