@opentui/core 0.4.4 → 0.5.0

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