Skip to content

Repository files navigation





PxtoneJS
Play Pxtone Collage "pxtone" files in the browser.





npm downloads npm version jsr version
dependencies license

Demo

PxtoneJS v4 Demo

Install

npm

npm install pxtone

JSR (Deno)

deno add jsr:@petamoriken/pxtone

Direct download

Bundled scripts are available on GitHub Releases:

  • Pxtone.js — IIFE bundle (exposes a Pxtone namespace as a global, e.g. Pxtone.Pxtone, Pxtone.PxtoneError)
  • Pxtone.mjs — ES Modules bundle

Usage

Playing a .ptcop / .pttune file

Pxtone holds a native WebAssembly resource. While it will be automatically released eventually, it is recommended to call close() when the instance is no longer needed to ensure the resource is released promptly. If Symbol.dispose is available in your environment, you can also use the using declaration (Explicit Resource Management).

stream() returns a ReadableStream<AudioData> (format "f32-planar"). To play back with the Web Audio API using AudioBufferSourceNode, schedule each chunk ahead of time:

const response = await fetch("song.ptcop");
const fileBytes = await response.arrayBuffer();

const BUFFER_AHEAD = 0.5; // seconds

const ctx = new AudioContext();
const pxtone = new Pxtone({ sampleRate: ctx.sampleRate });
pxtone.read(fileBytes);

const stream = pxtone.stream({ loop: true });
const reader = stream.getReader();
let nextStartTime = ctx.currentTime + 0.1;

async function scheduleMore() {
  while (nextStartTime < ctx.currentTime + BUFFER_AHEAD) {
    const { done, value: audioData } = await reader.read();
    if (done) return;

    const buffer = new AudioBuffer({
      numberOfChannels: audioData.numberOfChannels,
      length: audioData.numberOfFrames,
      sampleRate: audioData.sampleRate,
    });
    for (let ch = 0; ch < audioData.numberOfChannels; ch++) {
      audioData.copyTo(buffer.getChannelData(ch), { planeIndex: ch });
    }
    audioData.close();

    if (nextStartTime < ctx.currentTime) nextStartTime = ctx.currentTime + 0.05;
    const source = new AudioBufferSourceNode(ctx, { buffer });
    source.connect(ctx.destination);
    source.start(nextStartTime);
    nextStartTime += buffer.duration;
  }
}

await scheduleMore();
setInterval(scheduleMore, 100);

// When stopping playback:
// pxtone.close();

Each AudioData chunk can also be forwarded to an AudioWorkletNode, MediaStreamTrackGenerator, or other consumers:

const response = await fetch("song.ptcop");
const fileBytes = await response.arrayBuffer();

const pxtone = new Pxtone();
pxtone.read(fileBytes);

console.log(pxtone.name);
console.log(pxtone.duration); // total length in seconds

const stream = pxtone.stream({ loop: true });
const reader = stream.getReader();

while (true) {
  const { done, value: audioData } = await reader.read();
  if (done) break;
  // pass audioData to an AudioWorklet, MediaStreamTrackGenerator, etc.
}
pxtone.close();

Decoding a .ptnoise file

decodeNoiseData() returns an AudioData with format "f32-planar". To play it back with the Web Audio API, copy each channel plane into an AudioBuffer:

const response = await fetch("drum.ptnoise");
const fileBytes = await response.arrayBuffer();

const ctx = new AudioContext();
const pxtone = new Pxtone({ sampleRate: ctx.sampleRate });
const audioData = await pxtone.decodeNoiseData(fileBytes);
pxtone.close();

const buffer = new AudioBuffer({
  numberOfChannels: audioData.numberOfChannels,
  length: audioData.numberOfFrames,
  sampleRate: audioData.sampleRate,
});
for (let ch = 0; ch < audioData.numberOfChannels; ch++) {
  audioData.copyTo(buffer.getChannelData(ch), { planeIndex: ch });
}
audioData.close();

const source = new AudioBufferSourceNode(ctx, { buffer });
source.connect(ctx.destination);
source.start();

API

new Pxtone(options?: PxtoneOptions)

Creates an instance backed by a WebAssembly service.

export interface PxtoneOptions {
  /**
   * Number of output channels. Must be `1` (mono) or `2` (stereo).
   * @default 2
   */
  numberOfChannels?: 1 | 2;
  /**
   * Output sample rate in Hz.
   * @default 44100
   */
  sampleRate?: number;
}

Properties

Audio output

Property Type Description
numberOfChannels 1 | 2 Output channel count
sampleRate number Output sample rate in Hz

Metadata (available after read())

Property Type Description
name string | null Song title (Shift-JIS decoded)
comment string | null Song comment (Shift-JIS decoded)

Master (available after read())

Property Type Description
ticksPerBeat number | null Ticks per beat
beatsPerMeasure number | null Beats per measure
beatTempo number | null Tempo in BPM
numberOfMeasures number | null Total number of measures
numberOfTicks number | null Total length in ticks
duration number | null Total duration in seconds

Loop (available after read())

Property Type Description
loopStartMeasure number | null Loop start position in measures
loopEndMeasure number | null Loop end position in measures
loopStart number | null Loop start position in seconds
loopEnd number | null Loop end position in seconds

Playback

Property Type Description
currentTick number Current playback position in ticks
currentTime number Current playback position in seconds

Song data

Property Type Description
units readonly PxtoneUnit[] Instrument tracks
events readonly PxtoneEvent[] Events ordered by tick and pxtone priority
notes readonly PxtoneNote[] Notes ordered by tick, then by unit

Methods

static Pxtone.validate(buffer: ArrayBuffer | Uint8Array): boolean

Checks whether buffer is a valid .ptcop or .pttune file without loading it. Returns true if valid, false otherwise.

static Pxtone.validateNoiseData(buffer: ArrayBuffer | Uint8Array): boolean

Checks whether buffer is a valid .ptnoise file without loading it. Returns true if valid, false otherwise.

read(buffer: ArrayBuffer | Uint8Array): void

Loads a .ptcop or .pttune file and prepares it for playback. Throws a PxtoneError if the file is invalid or a stream is currently active.

stream(options?: StreamOptions): ReadableStream<AudioData>

Returns a ReadableStream that yields PCM chunks as AudioData objects (format "f32-planar"). Each chunk has at most numberOfFrames frames; the final chunk may be shorter. Only one stream may be active at a time. Throws a PxtoneError if no data has been loaded or a stream is already active.

export interface StreamOptions {
  /**
   * Playback start position in seconds.
   * @default 0
   */
  startTime?: number;
  /**
   * Units whose `played` flag is false are silenced.
   * @default false
   */
  unitMute?: boolean;
  /**
   * Loop playback from the song's repeat point.
   * @default false
   */
  loop?: boolean;
  /**
   * Maximum number of frames per channel per chunk. The final chunk may be shorter.
   * @default 1024
   */
  numberOfFrames?: number;
  /**
   * Backpressure threshold for the underlying `ReadableStream`.
   * @default 1
   */
  highWaterMark?: number;
  /** AbortSignal to cancel the stream early. */
  signal?: AbortSignal;
}

close(): void

Releases the underlying Wasm resource. Safe to call multiple times; subsequent calls are no-ops. If Symbol.dispose is available in the runtime environment, it is aliased to close() so the using declaration also works.

clear(): void

Resets the instance to its initial idle state, discarding all loaded song data.

decodeNoiseData(buffer: ArrayBuffer | Uint8Array): Promise<AudioData>

Decodes a .ptnoise file and returns an AudioData with format "f32-planar". Throws a PxtoneError if the data is invalid.

PxtoneError

Thrown by Pxtone methods on operation failures. Extends Error with an optional code property for programmatic error handling.

code Static constant Description
"DISPOSED" PxtoneError.CODE_DISPOSED The instance has already been disposed.
"STREAMING_ACTIVE" PxtoneError.CODE_STREAMING_ACTIVE Operation not allowed while a stream is active.
"NOT_READY" PxtoneError.CODE_NOT_READY read() has not been called yet.
"READ_FAILED" PxtoneError.CODE_READ_FAILED Failed to load the pxtone data.
"TONES_READY_FAILED" PxtoneError.CODE_TONES_READY_FAILED Failed to initialize audio tones.
"PLAYBACK_PREPARATION_FAILED" PxtoneError.CODE_PLAYBACK_PREPARATION_FAILED Failed to prepare audio playback.
"RENDER_NOISE_FAILED" PxtoneError.CODE_RENDER_NOISE_FAILED Failed to render noise data.
try {
  pxtone.read(buffer);
} catch (e) {
  if (e instanceof PxtoneError && e.code === PxtoneError.CODE_READ_FAILED) {
    console.error("Invalid pxtone file");
  }
}

PxtoneUnit

Property Type Description
index number Position in Pxtone#units
name string Display name
played boolean Whether the unit is active (not muted)

togglePlayed(force?: boolean): void

Toggles the played flag for this unit. If force is provided, the flag is set explicitly rather than toggled.

PxtoneEvent

Property Type Description
tick number Tick position
unit PxtoneUnit Target unit in the loaded song
kind PxtoneEventKind Event type (see PxtoneEvent.KIND_* constants)
value number Event payload

PxtoneNote

Each note corresponds to one note-on event. Pitch, volume, and stereo-pan changes during the note are represented by segments. Notes belonging to the same unit never overlap: if the next note-on arrives before the current note is over, the current note is cut short at that tick.

Property Type Description
unit PxtoneUnit Instrument track
startTick number Start position in ticks
endTick number End position in ticks
startTime number Start position in seconds
endTime number End position in seconds
velocity number Attack strength (0–128)
pitchSegments readonly PxtonePitchSegment[] Pitch movement over the note
volumeSegments readonly PxtoneVolumeSegment[] Volume over the note
panVolumeSegments readonly PxtonePanVolumeSegment[] Stereo pan position over the note

velocity is the value written alongside the note-on, which is how pxtone stores the velocity of a note. A note with no velocity event of its own keeps whichever value was still in effect, starting from the default of 104.

PxtonePitchSegment

Property Type Description
startTick number Segment start in ticks
endTick number Segment end in ticks
startTime number Segment start in seconds
endTime number Segment end in seconds
startKey number Start key in pxtone units (256 per semitone)
endKey number End key in pxtone units
targetKey number Key the segment is heading for, in pxtone units
startPitch number Start pitch in semitones
endPitch number End pitch in semitones
targetPitch number targetKey in semitones
interpolation "hold" | "linear" Constant pitch or linear portamento

A note always has at least one pitch segment, and its segments are chronological and cover the note from startTick to endTick without gaps. Portamento is exposed as a continuous linear model for visualization; tuning events and sample-level integer rounding are not included.

targetKey equals endKey except when the note ends, or another key event arrives, before the portamento completes; then endKey is the interpolated pitch reached so far while targetKey stays the key that was written. Renderers that snap a note to a single key row want targetKey; renderers that draw the glide want endKey.

PxtoneVolumeSegment

Property Type Description
startTick number Segment start in ticks
endTick number Segment end in ticks
startTime number Segment start in seconds
endTime number Segment end in seconds
value number Native pxtone volume
gain number Gain multiplier, normally 0–1 (value / 128)

PxtonePanVolumeSegment

Property Type Description
startTick number Segment start in ticks
endTick number Segment end in ticks
startTime number Segment start in seconds
endTime number Segment end in seconds
value number Native pxtone pan volume
pan number Pan position (-1 left, 0 center, 1 right)

Volume and pan-volume events change their value immediately rather than interpolating. Each array is chronological and covers the note without gaps. Values set before a note are carried into its first segment. Volume defaults to 104; pan volume defaults to 64 and ranges from 0 (left) to 64 (center) to 128 (right).

WebAssembly

src/pxtone.wasm is built from petamoriken/pxtone-rs, a Rust port of the Pxtone Collage library.

License

MIT

See NOTICE.md for third-party license notices.

About

Play Pxtone Collage "pxtone" files in the browser.

Topics

Resources

Stars

33 stars

Watchers

0 watching

Forks

Releases

Used by

Contributors

Languages