Pi.js v2.3.0 rewrites the sound plugin on a mixed bus graph, introduces the optional sound-advanced plugin, and gives the keyboard, pointer, and gamepad plugins one shared set of input conventions. This release renames the input commands to camelCase without aliases and changes the sound() envelope arguments, so most applications will need code changes before upgrading.
INTRODUCTION
Pi.js v2.3.0 is a feature release for applications upgrading from any Pi.js v2.x version. Unlike v2.1 and v2.2, it is not a drop-in update: the sound, keyboard, pointer, and gamepad plugins are now version 2.0.0, and their renamed commands have no aliases. An old name is not registered, so code that uses it fails at its first call.
Applications upgrading directly from Pi.js v1.2.x should read the v2.0, v2.1, and v2.2 upgrade guides first because the architectural and API changes introduced in those releases still apply.
UPGRADE AT A GLANCE
-
EXISTING V2.X APPLICATIONS
- Drawing, text, image, shader, view, and screen code remains compatible
- Input commands must be renamed to their camelCase forms
- Sound code that uses envelopes, audio pools, or PLAY articulation needs review
- Applications that use neither input nor sound commands should behave as they did in v2.2, apart from stricter validation
-
NEW CAPABILITIES
- Control individual playing samples with instance IDs, and stream long audio files
- Mix sound on
sfx,music,audio, andmasterbuses through an output limiter - Add synthesis, bus effects, presets, instruments, and recording with the
sound-advancedplugin - Read mouse-wheel input, name gamepad buttons and axes, and rumble gamepads
- Detect cancelled input with a
cancelledfield on input data - Share an API between plugins with
provideService()andgetService()
-
BEHAVIOR TO REVIEW
- Input commands are camelCase with no aliases:
inkeyis nowinKey,onmouseisonMouse, andingamepadisinGamepad sound()uses an ADSR envelope, which changes the meaning of its seventh positional argumentloadAudio()no longer acceptspoolSize- Touch handler modes are
"down","move", and"up" - Registering the same input handler twice now runs it once
set()throws for options it does not recognize$.clearEvents()clears pointer handlers on every screen- Loading a standalone plugin that the full build already includes now throws
- Input commands are camelCase with no aliases:
NEW FEATURES
-
AUDIO INSTANCES AND STREAMING
playAudio()now returns an instance ID for the sample it startedpauseAudio(),resumeAudio(),setAudio(), andstopAudio()accept that instance IDsetAudio()changes the volume, playback rate, and pan of a playing instanceloadAudio()accepts astreamflag that plays the file through a media element instead of decoding it- Streaming suits long music tracks; decoded audio suits short, frequently played effects
Audio instance example:
const music = $.loadAudio( "media/theme.mp3", "theme", true ); $.ready( () => { const instance = $.playAudio( { "audioId": music, "loop": true } ); $.setAudio( instance, 0.5 ); $.pauseAudio( instance ); $.resumeAudio( instance ); } ); -
SOUND BUSES AND OUTPUT LIMITER
- Sound is mixed on three buses before the master volume:
sfx,music, andaudio sound()plays onsfx,play()onmusic, andplayAudio()onaudiosetBusVolume()sets the volume of one bus, or ofmastersetSoundLimiter()turns the output limiter on or off; it is on by default- This makes separate music and effects volume settings a single call each
Bus volume example:
$.setBusVolume( "music", 0.4 ); $.setBusVolume( "sfx", 1 ); - Sound is mixed on three buses before the master volume:
-
EXTENDED PLAY STRINGS
MHsets the envelope sustain level andMRsets the releaseMPsets the stereo panWNandWPselect white and pink noise waveforms@nselects an instrument defined with thesound-advancedplugin
-
THE SOUND-ADVANCED PLUGIN
sound-advanced1.0.0 is a new plugin- It is not part of either bundle; load it separately after them
- Requires either the full version of pi.js or the lite version with the sound plugin
synth()plays a synthesized voice with pulse waves, a filter and filter envelope, vibrato, tremolo, arpeggio, and periodic noisesynth()withholdsustains untilreleaseSound(), andsetSynth()changes it while it playssfx()plays a preset,definePreset()adds one, andgenerateSfx()builds a repeatable sound effect from a seeddefineInstrument()defines a PLAY instrument, synthesized or from a loaded audio filesetBusEffect()adds reverb, delay, filter, distortion, bitcrush, or chorus to a bus, alone or as a chaingetSoundLevels()reports the levels, spectrum, and waveform of a bus or of the outputstartRecording(),stopRecording(),getRecordingState(), andsaveRecording()record a bus or the output to a WAV fileonPlay()andoffPlay()run handlers at each note’s audible time, for visuals synced to music
Bus effect example:
$.setBusEffect( "music", "reverb", { "time": 2.5, "mix": 0.4 } ); $.sfx( "zap", 0.3 ); -
MOUSE-WHEEL INPUT
onWheel()andoffWheel()register and remove mouse-wheel handlers- Handlers receive
{ x, y, deltaX, deltaY }in CSS pixels - While a screen has wheel handlers, the wheel does not scroll the page over its canvas
clearEvents( "wheel" )removes every wheel handler
Mouse-wheel example:
let zoom = 1; $.onWheel( ( wheelData ) => { zoom = Math.min( Math.max( zoom - wheelData.deltaY / 500, 0.25 ), 4 ); } ); -
GAMEPAD NAMES AND VIBRATION
- Gamepad helpers accept standard-mapping names as well as indices
- Button names include
south,east,west,north,leftShoulder,rightShoulder,leftTrigger,rightTrigger,select,start,leftStick,rightStick,dpadUp,dpadDown,dpadLeft,dpadRight, andhome - Axis names are
leftX,leftY,rightX, andrightY vibrateGamepad()plays a dual-rumble effect for a duration in milliseconds- It returns
falsewhere the browser or pad cannot vibrate
Gamepad example:
const gamepad = $.inGamepad( 0 ); if( gamepad && gamepad.getButtonJustPressed( "south" ) ) { $.vibrateGamepad( 0, 200, 1, 0.5 ); } -
CANCELLED INPUT
- Key, mouse, touch, press, and click data have a new
cancelledfield - It is set when a release comes from blur, a hidden page, a stop command, or the browser cancelling the input
- Handlers can use it to tell a real release from one the user did not make
- Key, mouse, touch, press, and click data have a new
-
PLUGIN SERVICES
provideService()lets a plugin publish an API during initializationgetService()returns the service of a plugin it depends on- The
soundplugin provides a sound service thatsound-advanceduses getPlugins()now reports each plugin’sstate
BEHAVIORAL CHANGES
-
INPUT COMMANDS RENAMED
- Keyboard:
inkey,onkey, andoffkeyare nowinKey,onKey, andoffKey - Mouse:
inmouse,onmouse, andoffmouseare nowinMouse,onMouse, andoffMouse - Touch:
intouch,ontouch, andofftouchare nowinTouch,onTouch, andoffTouch - Press:
inpress,onpress, andoffpressare nowinPress,onPress, andoffPress - Click:
onclickandoffclickare nowonClickandoffClick - Context menu:
setEnableContextMenuand theenableContextMenusetting are nowsetContextMenuandcontextMenu - Gamepad:
ingamepadis nowinGamepad - Gamepad connections:
onGamepadConnected( fn )andonGamepadDisconnected( fn )are nowonGamepad( "connect", fn )andonGamepad( "disconnect", fn ), removed withoffGamepad() - Gamepad dead zone:
setGamepadSensitivityand thegamepadSensitivitysetting are nowsetGamepadDeadZoneandgamepadDeadZone
Before v2.3:
$.onkey( "Enter", "down", start ); $.onmouse( "down", shoot ); const pads = $.ingamepad();With v2.3:
$.onKey( "Enter", "down", start ); $.onMouse( "down", shoot ); const pads = $.inGamepad(); - Keyboard:
-
SOUND USES ADSR ENVELOPES
sound()replacesattackanddecaywithattackTime,decayTime,sustainLevel, andreleaseTime- The positional order is now
frequency,duration,volume,oType,delay,attackTime,decayTime,sustainLevel,releaseTime,pan, andfrequencyEnd - The seventh argument used to set the tail of the sound; it is now
decayTime, and the tail is the 0.1 second default release - Frequency is no longer rounded, and the default envelope and a short de-click ramp change the sound slightly
- A sweep with an endpoint at or below 0 throws
INVALID_FREQUENCY
Before v2.3:
$.sound( 440, 1, 1, "square", 0, 0.01, 0.5 );With v2.3:
$.sound( { "frequency": 440, "duration": 1, "oType": "square", "attackTime": 0.01, "releaseTime": 0.5 } ); -
AUDIO POOLS REMOVED
loadAudio()no longer takespoolSize; its third argument is now thestreamboolean- There is no per-file limit on overlapping playback, only the shared voice limits
- The
stopAudio()object form takesidinstead ofaudioId, and stopping fades out playbackRateis limited to 0.25–4 for streamed audio and 0.0625–16 for decoded audio; values outside the range throwINVALID_PLAYBACK_RATE- Default audio IDs are
audio_Ninstead ofaudioPool_N - Audio needs a page served over HTTP or HTTPS;
file://pages throwUNSUPPORTED_PROTOCOL
-
SHARED VOICE BUDGET AND TIMING
- Samples, synthesized sounds, and PLAY notes share one voice budget
- Looping samples count toward it but are never stolen automatically
- When no voice can be replaced, a new sound is rejected; the call still returns an ID that is already completed
- Delayed
sound()andplayAudio()requests are limited to 1024 pending; beyond that the call throwsTOO_MANY_PENDING_SOUNDS stopSound(),stopPlay(), andstopAudio()finish about 15 ms after the call with a short fade, instead of at oncesetVolume()sets one master gain for samples and synthesized sound alike, before the limiter- With the limiter on, output above 0.9 is soft-clipped, so mixes that clipped hard now saturate smoothly
-
PLAY STRING CHANGES
- The PLAY envelope is ADSR:
MAandMDset attack and decay times,MHthe sustain level, andMRthe release, each as a percentage MTand the undocumentedMU,MX,MY,MZ, andMKare ignored with a warningMS,MN, andMLshorten the sounding part of each note and keep the beat, so songs that usedMSorMNplay at their written tempo, slower than in v2.2- A dot after a length is honored:
C4.is a dotted quarter C-andB#cross the octave boundary, andN116–N119play a semitone lower than before
- The PLAY envelope is ADSR:
-
SHARED INPUT CONVENTIONS
- A handler is identified by its mode and function; registering the same function for the same mode again does nothing
- State is updated before handlers run, and a handler that throws is reported with
console.errorwhile the other handlers still run - A handler registered during an event first runs for the next one, and one removed during an event does not run later in it
- Input data objects and lists are frozen or updated in place; copy a value to keep it
- Validation throws
TypeErrororRangeErrorwith a code per parameter instead ofINVALID_PARAMETERS - Non-boolean flags such as
oncethrow instead of being coerced
-
KEYBOARD CHANGES
- The keyboard plugin starts on first use rather than when it loads; call
startKeyboard()to track keys from an earlier point offKey()matches on key, mode, and function, andoffKey( key )alone throws- Blur, a hidden page,
stopKeyboard(), typing into a text field, and aninput()prompt release held keys through"up"handlers withcancelled: true - Keys typed into an
input()prompt, including the Enter that ends it, no longer reachonKey()handlers orinKey() setActionKeys()replaces the action keys instead of adding to theminKey( "" )andinKey( 0 )throw instead of returning the list
- The keyboard plugin starts on first use rather than when it loads; call
-
MOUSE, TOUCH, PRESS, AND CLICK CHANGES
- Touch handlers use the modes
"down","move", and"up"instead of"start","move", and"end"; the old modes throwINVALID_MODE - Touch handlers run once per touch, with the touch that changed
- Mouse, touch, press, and click data share one shape:
x,y,lastX,lastY,buttons,action,type,id, andcancelled inMouse()andinPress()returnnullbefore the first event and after their input stopsclearEvents( "press" )no longer clears click handlers; use"click"setPinchZoom()is now a screen command that setstouch-actionon that screen’s canvas and no longer changes<body>- A click needs the same pointer’s press and release inside its hit box; right and middle buttons no longer click
- A drag that leaves the canvas keeps reporting moves and reports its release
- A hit box that is not an object, or that has a negative size, throws instead of being ignored
Before v2.3:
$.ontouch( "start", onTouchStart ); $.ontouch( "end", onTouchEnd );With v2.3:
$.onTouch( "down", onTouchStart ); $.onTouch( "up", onTouchEnd ); - Touch handlers use the modes
-
GAMEPAD CHANGES
inGamepad()always returns an array, empty while polling is stopped;inGamepad( i )returnsnullfor a missing pad- Sticks on standard-mapping pads use a radial dead zone, so a full diagonal reads a distance of 1 and near-axis movement no longer snaps to the axis
- The dead zone must be under 1:
setGamepadDeadZone( 1 )throws - “Just pressed”, “just released”, and “axis changed” report what happened since the previous read, so slower loops see every press
- A connect handler registered later receives the pads already connected
- A helper index must be an integer or a standard name:
getButton( "0" )now throws stopGamepad()holds even with handlers registered, untilstartGamepad()
-
STRICTER CORE VALIDATION
set()throwsINVALID_OPTIONfor an option it does not recognize, including options from plugins that are not loaded- Every option name is checked first, so a call that throws applies no setting
$.clearEvents()clears mouse, touch, press, and click handlers on every screen and cancels aninput()prompt on any screen; callclearEvents()on a screen to clear only that screenarc(),loadFont(),loadSpritesheet(), andsetPrintSize()reject values they used to accept, such as non-finite angles and negative margins$.removeScreen()throwsINVALID_SCREEN_IDfor a missing, unknown, or already removed screen- An argument passed as
undefinedcounts as omitted and takes the default - Loading a standalone plugin that the full build already includes throws
DUPLICATE_PLUGIN - Registering a plugin after initialization throws
REGISTRATION_CLOSED
FIXES AND RESOURCE MANAGEMENT
-
KEYBOARD AND INPUT PROMPTS
- A key is released by its code, so a letter pressed with Shift no longer stays held
input()reads its own keys, so it works afterstopKeyboard()- Space no longer scrolls the page and Tab no longer moves focus during a prompt; pasted text is inserted
- Numeric prompts resolve with a number, and the prompt keeps to one line
startKeyboard()no longer blurs the focused element
-
POINTER HANDLING
- Removing a handler no longer disables the other handlers of its mode
- Each touch in
inTouch()keeps its own action - A release for a button or touch that is not held is ignored
- A press that starts on the canvas border or padding is ignored, and hit boxes accept fractional values
-
GAMEPAD RELIABILITY
- A button press that happens between reads is no longer lost
- A throwing connection handler no longer stops the others or leaves a pad tracked after it disconnects
-
DRAWING, FONTS, AND SCREENS
setChar()works on every font, including the default font, and leaves the source image passed toloadFont()unchangedgetImage()of an offscreen screen returns a canvas copy of its pixels- Offscreen screens draw again after a WebGL context loss that happens while no offscreen screen exists
blitImage()andblitSprite()accept every color formdrawImage()accepts, and their object forms apply the documented defaultspolygon()fills shapes with coordinates far off the screen correctlygetPal( false )andgetDefaultPal( false )exclude index 0- An image loaded or captured without a name gets one no other image uses
-
PLUGIN LIFECYCLE AND BUILDS
- A plugin whose initialization fails leaves nothing installed, and its name can be registered again
- The circle geometry cache is bounded
- Builds are identical on every platform
API COMPATIBILITY
Pi.js v2.3.0 renames the input commands of the keyboard, pointer, and gamepad plugins and changes the arguments of several sound commands. No aliases or compatibility shims are provided. Core drawing, text, image, shader, and screen commands are not renamed.
NEW COMMANDS
pauseAudio(),resumeAudio(), andsetAudio()setBusVolume()andsetSoundLimiter()onWheel()andoffWheel()onGamepad(),offGamepad(), andvibrateGamepad()
EXTENDED COMMANDS
playAudio()returns an instance IDloadAudio()acceptsstreamand no longer acceptspoolSizestopAudio()accepts instance IDssound()acceptsattackTime,decayTime,sustainLevel, andreleaseTimein place ofattackanddecayplay()acceptsMH,MR,MP,WN,WP, and@ngetPlugins()reports each plugin’sstate
RENAMED ERROR CODES
AUDIO_POOL_NOT_FOUNDis nowAUDIO_NOT_FOUNDEMPTY_POOLis nowAUDIO_NOT_LOADEDINVALID_POOL_SIZEis removed withpoolSize
PLUGINS
- The
sound,keyboard,pointer, andgamepadplugins are now version 2.0.0 - The
sound-advancedplugin is new and is loaded separately with either build - The unreleased
onscreen-keyboard,pi-vision,print-table, andpensplugins were removed from the source; they were never part of the release package
TYPESCRIPT
- Projects using
nodenextmodule resolution get the package’s types - Lite projects that load a plugin get that plugin’s command, screen command, and setting types
- The global
piand$are declared by the full declarations only; lite projects use the module’s exports
MIGRATION GUIDE
-
UPDATE THE LIBRARY
- Replace all Pi.js v2.x bundle files with matching v2.3 bundle files
- Keep full, lite, ESM, and IIFE variants consistent with the variant already in use
- Update separately loaded plugin bundles at the same time when applicable
- Do not mix v2.2 core files and v2.3-generated type definitions
- If you use the full build, remove any standalone script for a plugin it already includes
-
RENAME INPUT COMMANDS
- Search for the lowercase names:
inkey,onkey,offkey,inmouse,onmouse,offmouse,intouch,ontouch,offtouch,inpress,onpress,offpress,onclick,offclick, andingamepad - Replace each with its camelCase form
- Replace
setEnableContextMenuwithsetContextMenuandsetGamepadSensitivitywithsetGamepadDeadZone, along with theirset()option names - Replace
onGamepadConnected( fn )andonGamepadDisconnected( fn )withonGamepad( "connect", fn )andonGamepad( "disconnect", fn ) - Take care not to rename DOM properties such as
element.onclickthat belong to the browser, not Pi.js
- Search for the lowercase names:
-
UPDATE TOUCH MODES AND INPUT READS
- Change
onTouch( "start" )to"down"andonTouch( "end" )to"up" - Review touch handlers that expected every touch in one call; they now run once per touch
- Handle
nullfrominMouse()andinPress()before the first event - Replace
if( pads )checks oninGamepad()with a length check, and=== undefinedchecks on a single pad with=== null - Copy values out of input data objects instead of storing or modifying the objects
Before v2.3:
const pads = $.ingamepad(); if( pads ) { updatePlayers( pads ); }With v2.3:
const pads = $.inGamepad(); if( pads.length > 0 ) { updatePlayers( pads ); } - Change
-
UPDATE SOUND ENVELOPES
- Search for
sound()calls that pass six or more positional arguments, or theattackanddecayoptions - Rewrite them with
attackTime,decayTime,sustainLevel, andreleaseTime; the object form is the clearest - Move a tail length that was the seventh argument to
releaseTime - Rewrite PLAY envelopes from
MAa MTt MDdtoMAa MDd MHh MRrwith new percentages - Listen to songs that use
MSorMN; they now play at their written tempo, so raise the tempo if the v2.2 speed was intended
- Search for
-
UPDATE AUDIO LOADING AND PLAYBACK
- Remove
poolSizefrom everyloadAudio()call; the third argument is now thestreamboolean - Change the
stopAudio()object form fromaudioIdtoid - Update error handling that checks
AUDIO_POOL_NOT_FOUND,EMPTY_POOL, orINVALID_POOL_SIZE - Serve the page over HTTP or HTTPS; audio no longer loads from
file://pages - Keep the instance ID from
playAudio()when a sample needs to be paused, adjusted, or stopped on its own
Before v2.3:
const bomb = $.loadAudio( "media/bomb.wav", "bomb", 4 );With v2.3:
const bomb = $.loadAudio( "media/bomb.wav", "bomb" ); - Remove
-
REVIEW SETTINGS AND EVENT CLEANUP
- Check every
set()call for misspelled options or options from plugins that are not loaded - Pass every action key in one
setActionKeys()call, since it now replaces the list - Replace
$.clearEvents()with a screen’sclearEvents()where only one screen should be cleared - Replace
clearEvents( "press" )with"click"where click handlers should be removed - Call
setPinchZoom()on a screen, and remove any reliance on it changing<body>
- Check every
-
VERIFY EXISTING CODE
- Run the application and watch the console for errors from renamed commands and stricter validation
- Check handlers that were registered more than once and relied on running more than once
- Check code that watched for Enter with
onKey()to detect the end of aninput()prompt - Check right-click and middle-click behavior, since those buttons no longer produce clicks
- Test gamepad stick movement and tune the dead zone under the new radial model
- Listen to the overall mix with the limiter on, and adjust bus volumes if needed
-
ADOPT NEW FEATURES
- Use
setBusVolume()for separate music and effects volume controls - Stream long music files with the
streamflag onloadAudio() - Use gamepad button and axis names in place of numeric indices
- Add
onWheel()handlers for zooming and scrolling interfaces - Load the
sound-advancedplugin when you need synthesis, bus effects, presets, or recording
- Use
TECHNICAL DETAILS
SOUND MIXING MODEL
sound()plays on thesfxbus,play()onmusic, andplayAudio()onaudiomasteris the mix of all three buses, aftersetVolume()and before the limiter- The limiter soft-clips output above 0.9 and is on by default
- On iOS, the mute switch silences audio files as well as synthesized sound, and game audio mixes with audio from other apps
VOICE AND TIMING MODEL
- Samples, synthesized sounds, and PLAY notes draw from one voice budget
- Looping samples are never stolen automatically; a rejected sound returns a completed ID
- After the main thread stalls, a sound late by up to 25 ms starts at its place in the timeline with a short fade in
- Later PLAY notes and delayed one-shots are skipped, and a looping sample moves to where it would be
- Suspending the audio context keeps a song’s position
INPUT HANDLER MODEL
- Handlers are identified by mode and function across the keyboard, pointer, and gamepad plugins
- State is updated before handlers run, and a throwing handler does not stop the others
- Data objects are live: frozen or updated in place until the next event
- Releases caused by blur, a hidden page, or a stop command arrive through
"up"handlers withcancelled: true
PLUGIN MODEL
- Standalone plugin entry points are for the lite build; the full build already includes its plugins
- Plugins can publish a service with
provideService()and consume one withgetService() - Failed plugin initialization leaves nothing installed
sound-advanceddepends on thesoundplugin and shares its audio context, master volume, limiter, and voice limits
UPGRADE CHECKLIST
- ☐ Replace v2.x bundles, plugin bundles, and type definitions with v2.3 versions
- ☐ Remove standalone plugin scripts that duplicate plugins in the full build
- ☐ Rename keyboard, mouse, touch, press, click, and gamepad commands to camelCase
- ☐ Rename
setEnableContextMenu,setGamepadSensitivity, and the gamepad connection handlers - ☐ Change touch modes from
"start"and"end"to"down"and"up" - ☐ Rewrite
sound()envelopes withattackTime,decayTime,sustainLevel, andreleaseTime - ☐ Rewrite PLAY envelopes and review songs that use
MSorMN - ☐ Remove
poolSizefromloadAudio()and updatestopAudio()object forms - ☐ Update checks for renamed audio error codes
- ☐ Handle
nullfrominMouse()andinPress(), and the array frominGamepad() - ☐ Review handlers registered more than once
- ☐ Fix or remove unrecognized
set()options - ☐ Review
$.clearEvents(),setActionKeys(), andsetPinchZoom()usage - ☐ Serve the application over HTTP or HTTPS when it loads audio
- ☐ Test gamepad dead zones, click behavior, and the overall sound mix
SUMMARY
- ✅ Audio instance control with pause, resume, and live volume, rate, and pan changes
- ✅ Streamed audio loading for long music files
- ✅ Sound buses with per-bus volume and an output limiter
- ✅ ADSR envelopes for
sound()and PLAY - ✅ New
sound-advancedplugin for synthesis, effects, presets, instruments, and recording - ✅ Mouse-wheel input, gamepad button and axis names, and gamepad vibration
- ✅ One set of conventions across the keyboard, pointer, and gamepad plugins
- ✅ Plugin services for sharing an API between plugins
- ✅ Stricter validation with specific error types and codes
- ✅ Fixes for stuck keys, lost gamepad presses, pointer handler removal, and offscreen screens
Most v2.2 applications need code changes for this release. Rename the input commands, update sound() envelopes and loadAudio() calls, and review touch modes, set() options, and event cleanup before upgrading.
For detailed API documentation, visit: https://pijs.org/api