@opentui/core 0.4.5 → 0.5.1

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