Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 3 additions & 3 deletions docs/bindings-roadmap.adoc
Original file line number Diff line number Diff line change
Expand Up @@ -55,9 +55,9 @@ no further significant ReScript → AffineScript work is tractable.

|2
|*@pixi/sound* (`Sound.from`, play, stop, volume, setVolume, loop)
|`○`
|`affinescript-pixijs` sub-module, or `affinescript-pixi-sound`
|idaptik `src/bindings/PixiSound.res`; BGM + SFX.
|`◐` scaffold (from / play / stop / pause / resume / setVolume / setLoop landed)
|`stdlib/PixiSound.affine` (eventual home: `affinescript-pixijs` sub-module, or `affinescript-pixi-sound`)
|idaptik `src/bindings/PixiSound.res`; BGM + SFX. Initial surface (`pixiSoundFrom` / `pixiSoundPlay` / `pixiSoundStop` / `pixiSoundPause` / `pixiSoundResume` / `pixiSoundSetVolume` / `pixiSoundSetLoop`) lands in `stdlib/` parallel to Motion / Http / Sqlite / Crypto; consumer provides `globalThis.__as_pixi_sound` (the `Sound` named export from `@pixi/sound`) at module-init time. Test fixture: `tests/codegen-deno/pixisound_smoke.{affine,harness.mjs}`. Follow-ups: `Sound.add` (multi-source registry), sprite atlases, async load (await + onload), `pauseAll` / `stopAll` / `resumeAll`, the `sound` singleton's filter pipeline.

|3
|*@pixi/ui* (Button, FancyButton, Switch, Slider, Input, ScrollBox)
Expand Down
23 changes: 23 additions & 0 deletions lib/codegen_deno.ml
Original file line number Diff line number Diff line change
Expand Up @@ -242,6 +242,21 @@ const __as_pixiUiSliderAsContainer = (s) => s;
const __as_pixiUiSwitchNew = (options) => new globalThis.__as_pixi_ui.Switch(options);
const __as_pixiUiSwitchOnChange = (sw, cb) => { sw.onChange.connect(cb); return 0; };
const __as_pixiUiSwitchAsContainer = (sw) => sw;
// ---- @pixi/sound (bindings #2): consumer-provided import ----
// Host JS environment exposes globalThis.__as_pixi_sound (the `Sound`
// named export from `@pixi/sound`). Tests set it in the harness before
// importing the generated module; production consumers typically do
// `import { Sound } from "@pixi/sound"; globalThis.__as_pixi_sound = Sound;`
// once at module-init time. The AffineScript-side externs
// (stdlib/PixiSound.affine) don't see this indirection — they call
// __as_pixiSound* helpers directly.
const __as_pixiSoundFrom = (url) => globalThis.__as_pixi_sound.from(url);
const __as_pixiSoundPlay = (s) => { s.play(); return 0; };
const __as_pixiSoundStop = (s) => { s.stop(); return 0; };
const __as_pixiSoundPause = (s) => { s.pause(); return 0; };
const __as_pixiSoundResume = (s) => { s.resume(); return 0; };
const __as_pixiSoundSetVolume = (s, vol) => { s.volume = vol; return 0; };
const __as_pixiSoundSetLoop = (s, loop) => { s.loop = loop; return 0; };
// `++` is overloaded (string concat / array concat); `a + b` would
// stringify arrays. Dispatch on shape so stdlib/string.affine's
// `result ++ [x]` and `a ++ b` are both correct.
Expand Down Expand Up @@ -434,6 +449,14 @@ let () =
b "motionTween" (fun a -> Printf.sprintf "__as_motionTween(%s, %s, %s, %s)" (arg 0 a) (arg 1 a) (arg 2 a) (arg 3 a));
b "motionSpring" (fun a -> Printf.sprintf "__as_motionSpring(%s, %s, %s)" (arg 0 a) (arg 1 a) (arg 2 a));
b "motionEase" (fun a -> Printf.sprintf "__as_motionEase(%s)" (arg 0 a));
(* ---- @pixi/sound (bindings #2) ---- *)
b "pixiSoundFrom" (fun a -> Printf.sprintf "__as_pixiSoundFrom(%s)" (arg 0 a));
b "pixiSoundPlay" (fun a -> Printf.sprintf "__as_pixiSoundPlay(%s)" (arg 0 a));
b "pixiSoundStop" (fun a -> Printf.sprintf "__as_pixiSoundStop(%s)" (arg 0 a));
b "pixiSoundPause" (fun a -> Printf.sprintf "__as_pixiSoundPause(%s)" (arg 0 a));
b "pixiSoundResume" (fun a -> Printf.sprintf "__as_pixiSoundResume(%s)" (arg 0 a));
b "pixiSoundSetVolume" (fun a -> Printf.sprintf "__as_pixiSoundSetVolume(%s, %s)" (arg 0 a) (arg 1 a));
b "pixiSoundSetLoop" (fun a -> Printf.sprintf "__as_pixiSoundSetLoop(%s, %s)" (arg 0 a) (arg 1 a));
(* Generic JS array push helper (returns the array, fluent). *)
b "arrayPush" (fun a -> Printf.sprintf "(%s.push(%s), %s)" (arg 0 a) (arg 1 a) (arg 0 a));
(* ---- honest string/number primitives underpinning the
Expand Down
73 changes: 73 additions & 0 deletions stdlib/PixiSound.affine
Original file line number Diff line number Diff line change
@@ -0,0 +1,73 @@
// SPDX-License-Identifier: MPL-2.0
// SPDX-FileCopyrightText: 2026 hyperpolymath
//
// PixiSound.affine — bindings for the `@pixi/sound` npm library
// (bindings #2 in docs/bindings-roadmap.adoc).
//
// Provides a typed surface over `@pixi/sound`'s `Sound.from()` factory
// plus the basic transport operations (play / stop / pause / resume,
// volume + loop control). Targets the Deno-ESM backend; the consumer
// (or its host wrapper) is responsible for putting the `Sound` class
// from `@pixi/sound` at `globalThis.__as_pixi_sound` before any
// generated module that uses these externs runs. The test harness
// pattern is in `tests/codegen-deno/pixisound_smoke.harness.mjs`.
//
// This file lives in `stdlib/` for parity with Motion / Http / Sqlite
// / Crypto. The dedicated `affinescript-pixijs` sub-module / standalone
// `affinescript-pixi-sound` package home flagged in the bindings
// roadmap is the long-term destination; the migration from here to
// there is additive and source-compatible.
//
// Surface coverage in this version: `Sound.from`, play, stop, pause,
// resume, setVolume, setLoop — the same surface idaptik's
// `src/bindings/PixiSound.res` consumes.
// Follow-ups (deferred): `Sound.add` (multi-source registry), sprite
// atlases (multi-segment audio with named ranges), async load
// (`await Sound.from(...)` with onload Promise), `pauseAll` /
// `stopAll` / `resumeAll`, the `sound` singleton's filter pipeline.
// Status row in `docs/bindings-roadmap.adoc` (#2) updates with each
// coverage tranche.

module PixiSound;

// Opaque handle to a `@pixi/sound` `Sound` instance. The underlying
// value is the `Sound` object produced by `Sound.from(url)`; treated
// opaquely at the AS boundary.
pub extern type Sound;

/// `Sound.from(url) -> Sound`. The argument is a URL or path; the
/// host library is responsible for resolving + decoding it. Returns
/// the constructed `Sound` handle synchronously; the underlying load
/// may be in-flight (a `.play()` queued before the asset is ready is
/// a documented `@pixi/sound` use-case — it autoplays on completion).
pub extern fn pixiSoundFrom(url: String) -> Sound;

/// `sound.play()` — start (or resume) playback from the beginning.
/// Returns 0. `@pixi/sound`'s real `play` accepts an options object
/// (volume / loop / sprite / completion callback); this minimal
/// surface ignores it. Use `pixiSoundSetVolume` / `pixiSoundSetLoop`
/// beforehand for the common cases.
pub extern fn pixiSoundPlay(s: Sound) -> Int;

/// `sound.stop()` — stop playback and reset the play head.
/// Returns 0.
pub extern fn pixiSoundStop(s: Sound) -> Int;

/// `sound.pause()` — pause playback at the current position.
/// Returns 0. Use `pixiSoundResume` to continue from the same point.
pub extern fn pixiSoundPause(s: Sound) -> Int;

/// `sound.resume()` — resume playback from a paused position.
/// Returns 0. A `resume` on a non-paused sound is a no-op.
pub extern fn pixiSoundResume(s: Sound) -> Int;

/// `sound.volume = vol` — set playback volume in the conventional
/// `[0.0, 1.0]` range (1.0 = full, 0.0 = silent). Values outside the
/// range are accepted by `@pixi/sound` (the underlying Web Audio API
/// gain node will clip), but consumers should treat that as out-of-
/// contract. Returns 0.
pub extern fn pixiSoundSetVolume(s: Sound, vol: Float) -> Int;

/// `sound.loop = loop` — enable or disable looping. When `true`,
/// playback restarts from the beginning on completion. Returns 0.
pub extern fn pixiSoundSetLoop(s: Sound, loop: Bool) -> Int;
29 changes: 29 additions & 0 deletions tests/codegen-deno/pixisound_smoke.affine
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
// SPDX-License-Identifier: MPL-2.0
// bindings #2 — @pixi/sound smoke test.
//
// The harness installs a mock at globalThis.__as_pixi_sound before
// importing the generated module; we re-export thin wrappers so the
// harness can drive each extern via stable names.

use PixiSound::{Sound, pixiSoundFrom, pixiSoundPlay, pixiSoundStop, pixiSoundPause, pixiSoundResume, pixiSoundSetVolume, pixiSoundSetLoop};

pub fn smokeFrom(url: String) -> Sound =
pixiSoundFrom(url);

pub fn smokePlay(s: Sound) -> Int =
pixiSoundPlay(s);

pub fn smokeStop(s: Sound) -> Int =
pixiSoundStop(s);

pub fn smokePause(s: Sound) -> Int =
pixiSoundPause(s);

pub fn smokeResume(s: Sound) -> Int =
pixiSoundResume(s);

pub fn smokeSetVolume(s: Sound, vol: Float) -> Int =
pixiSoundSetVolume(s, vol);

pub fn smokeSetLoop(s: Sound, loop: Bool) -> Int =
pixiSoundSetLoop(s, loop);
67 changes: 67 additions & 0 deletions tests/codegen-deno/pixisound_smoke.harness.mjs
Original file line number Diff line number Diff line change
@@ -0,0 +1,67 @@
// SPDX-License-Identifier: MPL-2.0
// bindings #2 — Node ESM harness for the @pixi/sound binding.
//
// Installs a globalThis.__as_pixi_sound mock before importing the
// generated module, drives each smoke* wrapper, and asserts the side
// effects (URL forwarded, transport calls counted, volume/loop set)
// were observed on the underlying mock sound instance.

import assert from "node:assert/strict";

class MockSound {
constructor(url) {
this.url = url;
this.playCount = 0;
this.stopCount = 0;
this.pauseCount = 0;
this.resumeCount = 0;
this.volume = 1.0;
this.loop = false;
}
play() { this.playCount += 1; }
stop() { this.stopCount += 1; }
pause() { this.pauseCount += 1; }
resume() { this.resumeCount += 1; }
}

let lastFromUrl = null;

globalThis.__as_pixi_sound = {
from(url) {
lastFromUrl = url;
return new MockSound(url);
},
};

const {
smokeFrom, smokePlay, smokeStop, smokePause, smokeResume,
smokeSetVolume, smokeSetLoop,
} = await import("./pixisound_smoke.deno.js");

const s = smokeFrom("assets/bgm.mp3");
assert.equal(lastFromUrl, "assets/bgm.mp3", "from URL reaches host");
assert.equal(s.url, "assets/bgm.mp3", "constructed sound carries url");

assert.equal(smokePlay(s), 0, "play returns 0");
assert.equal(smokePlay(s), 0, "play returns 0 (twice)");
assert.equal(s.playCount, 2, "play invoked twice");

assert.equal(smokeStop(s), 0, "stop returns 0");
assert.equal(s.stopCount, 1, "stop invoked once");

assert.equal(smokePause(s), 0, "pause returns 0");
assert.equal(s.pauseCount, 1, "pause invoked once");

assert.equal(smokeResume(s), 0, "resume returns 0");
assert.equal(s.resumeCount, 1, "resume invoked once");

assert.equal(smokeSetVolume(s, 0.5), 0, "setVolume returns 0");
assert.equal(s.volume, 0.5, "volume field updated");

assert.equal(smokeSetLoop(s, true), 0, "setLoop returns 0");
assert.equal(s.loop, true, "loop field updated");

assert.equal(smokeSetLoop(s, false), 0, "setLoop(false) returns 0");
assert.equal(s.loop, false, "loop field cleared");

console.log("pixisound_smoke.harness.mjs OK");
Loading