@gjsify/webaudio 0.3.21 → 0.4.3

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.
@@ -1,109 +0,0 @@
1
- // GStreamer decode pipeline: ArrayBuffer (MP3/WAV/OGG) → AudioBuffer (PCM Float32)
2
- //
3
- // Pipeline: appsrc → decodebin → audioconvert → audioresample → capsfilter(F32LE) → appsink
4
- // Uses try_pull_sample() for synchronous decoding (avoids GJS thread-safety issues).
5
- //
6
- // Reference: GStreamer 1.0 via gi://Gst, GstApp via gi://GstApp
7
-
8
- import { ensureGstInit, Gst } from './gst-init.js';
9
- import { AudioBuffer } from './audio-buffer.js';
10
-
11
- // Force GstApp typelib load so get_by_name() resolves AppSrc/AppSink types
12
- import GstApp from 'gi://GstApp?version=1.0';
13
- void GstApp;
14
-
15
- const PIPELINE_DESC =
16
- 'appsrc name=src ! decodebin ! audioconvert ! audioresample ! ' +
17
- 'capsfilter caps=audio/x-raw,format=F32LE,layout=interleaved ! ' +
18
- 'appsink name=sink sync=false';
19
-
20
- /**
21
- * Decode encoded audio data (MP3, WAV, OGG, FLAC, etc.) into an AudioBuffer
22
- * containing PCM Float32 channel data.
23
- *
24
- * This is a synchronous operation that blocks until decoding completes.
25
- * It must be called from the main thread (GJS requirement).
26
- */
27
- export function decodeAudioDataSync(arrayBuffer: ArrayBuffer): AudioBuffer {
28
- ensureGstInit();
29
-
30
- // Reject non-ArrayBuffer / empty input before touching GStreamer —
31
- // gst_memory_new_wrapped() asserts data != NULL and empty TypedArrays
32
- // marshal to a NULL pointer through GI.
33
- if (!(arrayBuffer instanceof ArrayBuffer) || arrayBuffer.byteLength === 0) {
34
- throw new DOMException('Unable to decode audio data', 'EncodingError');
35
- }
36
-
37
- const pipeline = Gst.parse_launch(PIPELINE_DESC) as Gst.Bin;
38
- const appsrc = pipeline.get_by_name('src')!;
39
- const appsink = pipeline.get_by_name('sink')!;
40
-
41
- pipeline.set_state(Gst.State.PLAYING);
42
-
43
- // Push encoded data into the pipeline
44
- const data = new Uint8Array(arrayBuffer);
45
- (appsrc as any).push_buffer(Gst.Buffer.new_wrapped(data));
46
- (appsrc as any).end_of_stream();
47
-
48
- // Pull decoded PCM samples
49
- const chunks: Uint8Array[] = [];
50
- let sampleRate = 0;
51
- let channels = 0;
52
-
53
- while (true) {
54
- const sample = (appsink as any).try_pull_sample(2 * Number(Gst.SECOND));
55
- if (!sample) break;
56
-
57
- // Read format from the first sample's negotiated caps
58
- if (sampleRate === 0) {
59
- const caps = sample.get_caps();
60
- if (caps) {
61
- const struct = caps.get_structure(0);
62
- [, sampleRate] = struct.get_int('rate');
63
- [, channels] = struct.get_int('channels');
64
- }
65
- }
66
-
67
- const buffer = sample.get_buffer();
68
- if (!buffer) continue;
69
-
70
- const [ok, mapInfo] = buffer.map(Gst.MapFlags.READ);
71
- if (ok) {
72
- // Copy data — mapInfo.data is only valid until unmap
73
- chunks.push(new Uint8Array(mapInfo.data));
74
- buffer.unmap(mapInfo);
75
- }
76
- }
77
-
78
- pipeline.set_state(Gst.State.NULL);
79
-
80
- if (sampleRate === 0 || channels === 0) {
81
- throw new DOMException('Unable to decode audio data', 'EncodingError');
82
- }
83
-
84
- // Concatenate chunks into a single interleaved Float32 buffer
85
- let totalBytes = 0;
86
- for (const c of chunks) totalBytes += c.length;
87
- const totalFrames = totalBytes / (4 * channels);
88
-
89
- const audioBuffer = new AudioBuffer({
90
- numberOfChannels: channels,
91
- length: totalFrames,
92
- sampleRate,
93
- });
94
-
95
- // De-interleave into per-channel Float32Arrays
96
- let offset = 0;
97
- for (const chunk of chunks) {
98
- const f32 = new Float32Array(chunk.buffer, chunk.byteOffset, chunk.length / 4);
99
- const framesInChunk = f32.length / channels;
100
- for (let frame = 0; frame < framesInChunk; frame++) {
101
- for (let ch = 0; ch < channels; ch++) {
102
- audioBuffer._channelData[ch][offset + frame] = f32[frame * channels + ch];
103
- }
104
- }
105
- offset += framesInChunk;
106
- }
107
-
108
- return audioBuffer;
109
- }
package/src/gst-init.ts DELETED
@@ -1,15 +0,0 @@
1
- // Lazy GStreamer initialization — call ensureGstInit() before any Gst API usage.
2
- // Reference: GStreamer 1.0 via gi://Gst
3
-
4
- import Gst from 'gi://Gst?version=1.0';
5
-
6
- let initialized = false;
7
-
8
- export function ensureGstInit(): void {
9
- if (!initialized) {
10
- Gst.init(null);
11
- initialized = true;
12
- }
13
- }
14
-
15
- export { Gst };
package/src/gst-player.ts DELETED
@@ -1,185 +0,0 @@
1
- // GStreamer playback pipeline: AudioBuffer PCM → audio output
2
- //
3
- // Pipeline: appsrc(F32LE) → audioconvert → volume → autoaudiosink
4
- // Each GstPlayer instance is single-use (matches W3C AudioBufferSourceNode).
5
- //
6
- // Reference: GStreamer 1.0 via gi://Gst
7
-
8
- import GLib from 'gi://GLib?version=2.0';
9
- import { ensureGstInit, Gst } from './gst-init.js';
10
- import type { AudioBuffer } from './audio-buffer.js';
11
-
12
- // Force GstApp typelib load
13
- import GstApp from 'gi://GstApp?version=1.0';
14
- void GstApp;
15
-
16
- export interface GstPlayerOptions {
17
- audioBuffer: AudioBuffer;
18
- volume: number;
19
- loop: boolean;
20
- offset: number; // start offset in seconds
21
- duration?: number; // play duration in seconds (undefined = full)
22
- playbackRate: number;
23
- onEnded: () => void;
24
- }
25
-
26
- /**
27
- * Manages a single GStreamer playback pipeline for one AudioBufferSourceNode.start() call.
28
- */
29
- export class GstPlayer {
30
- private _pipeline: any = null;
31
- private _volumeElement: any = null;
32
- private _busWatchId: number | null = null;
33
- private _ended = false;
34
- private _loop: boolean;
35
- private _onEnded: () => void;
36
- private _audioBuffer: AudioBuffer;
37
-
38
- constructor(options: GstPlayerOptions) {
39
- ensureGstInit();
40
- this._loop = options.loop;
41
- this._onEnded = options.onEnded;
42
- this._audioBuffer = options.audioBuffer;
43
-
44
- const { audioBuffer, volume, offset, duration, playbackRate } = options;
45
- const sr = audioBuffer.sampleRate;
46
- const ch = audioBuffer.numberOfChannels;
47
-
48
- // Build interleaved PCM data
49
- const pcmData = this._interleave(audioBuffer, offset, duration);
50
- if (pcmData.length === 0) {
51
- // Empty buffer — fire ended immediately
52
- this._fireEnded();
53
- return;
54
- }
55
-
56
- // Build pipeline — format=3 (TIME) ensures downstream gets TIME-based
57
- // segments, preventing gst_segment_to_stream_time assertion failures.
58
- const capsStr = `audio/x-raw,format=F32LE,rate=${sr},channels=${ch},layout=interleaved`;
59
- const desc = `appsrc name=src caps="${capsStr}" format=3 ! audioconvert ! volume name=vol ! autoaudiosink`;
60
- this._pipeline = Gst.parse_launch(desc);
61
- this._volumeElement = this._pipeline.get_by_name('vol');
62
- const appsrc = this._pipeline.get_by_name('src')!;
63
-
64
- // Set volume
65
- this._volumeElement.set_property('volume', Math.max(0, Math.min(volume, 10)));
66
-
67
- // Set up bus watch for EOS/ERROR
68
- const bus = this._pipeline.get_bus();
69
- this._busWatchId = bus.add_watch(0, (_bus: any, msg: any) => {
70
- if (msg.type === Gst.MessageType.EOS) {
71
- if (this._loop && !this._ended) {
72
- // Restart: push data again
73
- this._restartPlayback(appsrc, pcmData);
74
- } else {
75
- this._fireEnded();
76
- }
77
- } else if (msg.type === Gst.MessageType.ERROR) {
78
- this._fireEnded();
79
- }
80
- return true; // keep watching
81
- });
82
-
83
- // Push PCM data with proper timestamps for TIME-format segments
84
- const gstBuf = Gst.Buffer.new_wrapped(pcmData);
85
- const totalFrames = pcmData.length / (4 * ch);
86
- gstBuf.pts = 0;
87
- gstBuf.duration = Math.floor((totalFrames / sr) * Number(Gst.SECOND));
88
- appsrc.push_buffer(gstBuf);
89
- appsrc.end_of_stream();
90
-
91
- // Apply playback rate if not 1.0
92
- if (playbackRate !== 1.0) {
93
- this._pipeline.set_state(Gst.State.PAUSED);
94
- // Seek with rate change
95
- this._pipeline.seek(
96
- playbackRate,
97
- Gst.Format.TIME,
98
- Gst.SeekFlags.FLUSH | Gst.SeekFlags.ACCURATE,
99
- Gst.SeekType.SET, 0,
100
- Gst.SeekType.NONE, -1
101
- );
102
- }
103
-
104
- this._pipeline.set_state(Gst.State.PLAYING);
105
- }
106
-
107
- /** Update volume on a running pipeline */
108
- setVolume(value: number): void {
109
- if (this._volumeElement && !this._ended) {
110
- this._volumeElement.set_property('volume', Math.max(0, Math.min(value, 10)));
111
- }
112
- }
113
-
114
- /** Update loop flag */
115
- setLoop(value: boolean): void {
116
- this._loop = value;
117
- }
118
-
119
- /** Stop playback and clean up */
120
- stop(): void {
121
- if (this._ended) return;
122
- this._fireEnded();
123
- }
124
-
125
- /** Whether playback has ended */
126
- get ended(): boolean {
127
- return this._ended;
128
- }
129
-
130
- private _restartPlayback(appsrc: any, pcmData: Uint8Array): void {
131
- // For looping: seek pipeline to start
132
- if (this._pipeline) {
133
- this._pipeline.seek_simple(Gst.Format.TIME, Gst.SeekFlags.FLUSH, 0);
134
- }
135
- }
136
-
137
- private _fireEnded(): void {
138
- if (this._ended) return;
139
- this._ended = true;
140
- this._cleanup();
141
- this._onEnded();
142
- }
143
-
144
- private _cleanup(): void {
145
- const pipeline = this._pipeline;
146
- this._pipeline = null;
147
- this._volumeElement = null;
148
- this._busWatchId = null;
149
-
150
- if (pipeline) {
151
- // Defer NULL-state transition to a low-priority idle so this method does not
152
- // block the GLib main loop when called from within the bus-watch callback (EOS/ERROR).
153
- // autoaudiosink teardown flushes the audio device, which can take several ms and
154
- // causes frame drops when SFX fire frequently during gameplay.
155
- GLib.idle_add(GLib.PRIORITY_LOW, () => {
156
- pipeline.set_state(Gst.State.NULL);
157
- return GLib.SOURCE_REMOVE;
158
- });
159
- }
160
- }
161
-
162
- /**
163
- * Interleave AudioBuffer's per-channel Float32Arrays into a single Uint8Array.
164
- * Applies offset (seconds) and optional duration (seconds).
165
- */
166
- private _interleave(buf: AudioBuffer, offsetSec: number, durationSec?: number): Uint8Array {
167
- const ch = buf.numberOfChannels;
168
- const startFrame = Math.min(Math.floor(offsetSec * buf.sampleRate), buf.length);
169
- const maxFrames = buf.length - startFrame;
170
- const frames = durationSec !== undefined
171
- ? Math.min(Math.floor(durationSec * buf.sampleRate), maxFrames)
172
- : maxFrames;
173
-
174
- if (frames <= 0) return new Uint8Array(0);
175
-
176
- const interleaved = new Float32Array(frames * ch);
177
- for (let frame = 0; frame < frames; frame++) {
178
- for (let c = 0; c < ch; c++) {
179
- interleaved[frame * ch + c] = buf._channelData[c][startFrame + frame];
180
- }
181
- }
182
-
183
- return new Uint8Array(interleaved.buffer);
184
- }
185
- }
@@ -1,76 +0,0 @@
1
- // HTMLAudioElement — format detection + basic playback via GStreamer playbin.
2
- // Used by Excalibur.js for canPlayType() format sniffing.
3
- //
4
- // Reference: https://developer.mozilla.org/en-US/docs/Web/API/HTMLAudioElement
5
-
6
- import { ensureGstInit, Gst } from './gst-init.js';
7
-
8
- // GStreamer-supported MIME types (common on GNOME systems)
9
- const SUPPORTED_TYPES = new Set([
10
- 'audio/mpeg',
11
- 'audio/mp3',
12
- 'audio/wav',
13
- 'audio/x-wav',
14
- 'audio/ogg',
15
- 'audio/webm',
16
- 'audio/flac',
17
- 'audio/x-flac',
18
- 'audio/aac',
19
- 'audio/mp4',
20
- ]);
21
-
22
- export class HTMLAudioElement {
23
- src = '';
24
- volume = 1;
25
- loop = false;
26
- paused = true;
27
- currentTime = 0;
28
- duration = 0;
29
- readyState = 0;
30
-
31
- private _pipeline: any = null;
32
-
33
- canPlayType(type: string): CanPlayTypeResult {
34
- // Strip codecs parameter: "audio/ogg; codecs=vorbis" → "audio/ogg"
35
- const mime = type.split(';')[0].trim().toLowerCase();
36
- return SUPPORTED_TYPES.has(mime) ? 'maybe' : '';
37
- }
38
-
39
- play(): Promise<void> {
40
- if (!this.src) return Promise.resolve();
41
-
42
- ensureGstInit();
43
- this._cleanup();
44
-
45
- this._pipeline = Gst.ElementFactory.make('playbin', 'player');
46
- if (!this._pipeline) return Promise.resolve();
47
-
48
- this._pipeline.set_property('uri', this.src);
49
- this._pipeline.set_property('volume', this.volume);
50
- this._pipeline.set_state(Gst.State.PLAYING);
51
- this.paused = false;
52
-
53
- return Promise.resolve();
54
- }
55
-
56
- pause(): void {
57
- if (this._pipeline) {
58
- this._pipeline.set_state(Gst.State.PAUSED);
59
- this.paused = true;
60
- }
61
- }
62
-
63
- load(): void {
64
- this._cleanup();
65
- }
66
-
67
- addEventListener(_type: string, _listener: any): void {}
68
- removeEventListener(_type: string, _listener: any): void {}
69
-
70
- private _cleanup(): void {
71
- if (this._pipeline) {
72
- this._pipeline.set_state(Gst.State.NULL);
73
- this._pipeline = null;
74
- }
75
- }
76
- }
package/src/index.ts DELETED
@@ -1,14 +0,0 @@
1
- // Web Audio API for GJS — backed by GStreamer 1.0.
2
- //
3
- // This module has no side effects. Importing @gjsify/webaudio gives
4
- // named access to Web Audio classes but does NOT register globals.
5
- // Use @gjsify/webaudio/register to set globalThis.AudioContext, etc.
6
-
7
- export { AudioContext } from './audio-context.js';
8
- export { AudioBuffer } from './audio-buffer.js';
9
- export { AudioNode } from './audio-node.js';
10
- export { AudioDestinationNode } from './audio-destination-node.js';
11
- export { AudioBufferSourceNode } from './audio-buffer-source-node.js';
12
- export { GainNode } from './gain-node.js';
13
- export { AudioParam } from './audio-param.js';
14
- export { HTMLAudioElement } from './html-audio-element.js';
package/src/register.ts DELETED
@@ -1,17 +0,0 @@
1
- // Side-effect module: registers Web Audio API globals on GJS.
2
- // On Node.js the alias layer routes this to @gjsify/empty.
3
-
4
- import { AudioContext, HTMLAudioElement } from './index.js';
5
-
6
- if (typeof (globalThis as any).AudioContext === 'undefined') {
7
- (globalThis as any).AudioContext = AudioContext;
8
- }
9
- if (typeof (globalThis as any).webkitAudioContext === 'undefined') {
10
- (globalThis as any).webkitAudioContext = AudioContext;
11
- }
12
- if (typeof (globalThis as any).Audio === 'undefined') {
13
- (globalThis as any).Audio = HTMLAudioElement;
14
- }
15
- if (typeof (globalThis as any).HTMLAudioElement === 'undefined') {
16
- (globalThis as any).HTMLAudioElement = HTMLAudioElement;
17
- }
package/src/test.mts DELETED
@@ -1,5 +0,0 @@
1
- import webaudioSpec from './webaudio.spec.js';
2
-
3
- const results = {
4
- webaudio: await webaudioSpec(),
5
- };