Instruments Reference
Instrument type= and fields are chip-specific. This page focuses on Game Boy (chip gameboy). For other targets see:
- Game Boy chip guide
- NES
- SMS / Game Gear
- Spectrum 128 / CPC
- Instrument macros (
pitch_env,vol_env,subpat, …) - Game Boy tutorial
Pulse (Duty)
The Game Boy has two pulse channels: Pulse 1 (type=pulse1) and Pulse 2 (type=pulse2). Pulse 1 additionally supports frequency sweep.
Duty options
- 12.5% — thin, cutting (good for arpeggios and trebly leads)
- 25% — classic square-like timbre
- 50% — balanced, full-sounding
- 75% — darker/thicker tone
Envelope & sweep
env=gb:<initial>,<up|down>,<period>—initial0–15,period0 means constant volume. Useperiodto control envelope speed.sweep(Pulse 1 only) applies frequency shifts over time to create slides; use moderate parameters to avoid abrupt pitch jumps.
Tips & pitfalls
- Short envelope periods (1–2) create plucky staccato notes; long periods create pads.
period=0preserves initial volume. - For low notes use wider duty or add subtle detune on the pulse pair to fill the spectrum.
Wave
The wave channel (type=wave) plays back a custom 32-nibble wavetable stored in Game Boy Wave RAM, giving it a distinct, richer timbre compared to the pulse channels.
Format & sizing
- The Game Boy Wave RAM holds 32 4-bit nibbles (values 0–15); BeatBax uses 32 as the native wavetable size. The
parseWaveTableengine function always produces a 32-entry array. - 32-value arrays are used directly (preferred — full control over each nibble).
- 16-value arrays are also accepted as a convenient shorthand; the engine automatically tiles them to fill all 32 slots (entries 0–15 are repeated as entries 16–31).
- Hex string format: a 32-character hex string (e.g.
"0478ABBB986202467776420146777631") is supported and is the native hUGETracker format. Each character is one nibble. - Input may be a JS array, a JSON-like string (
"[0,1,2,...]"), or a 32-nibble hex string. Values are clamped to 0..15.
Normalization & headroom
- Ensure your wavetable reaches near the top (max ≈ 15) for good perceived loudness. Very-low peaks result in quiet output even with
volume=100. - Avoid strong DC bias; if your table is mostly >8 or <8, rescale or center the waveform to prevent bias-related artifacts.
Design tips & examples
- Bass: smooth symmetric shapes (triangle-like) with limited high harmonics.
- Pads: midharmonic-rich shapes, consider gentle asymmetry for warmth.
- Metallic: asymmetric, high-harmonic shapes (may alias at high pitches).
Example wavetables:
# Smooth bass (32 values — preferred)
wave = [0,2,4,6,8,10,12,14,15,14,12,10,8,6,4,2,0,2,4,6,8,10,12,14,15,14,12,10,8,6,4,2]
# Smooth bass (16-value shorthand — tiled to 32 automatically)
wave = [0,2,4,6,8,10,12,14,15,14,12,10,8,6,4,2]
# Metallic bell (32 values)
wave = [15,0,12,3,9,6,6,9,3,12,0,15,0,12,3,9,15,0,12,3,9,6,6,9,3,12,0,15,0,12,3,9]
# Hex string (32-nibble native UGE format)
wave = "02468ACDFFEDCA862468ACDF0FDCA864"
Aliasing considerations
- High-harmonic tables may alias when played at high pitches; reduce high-frequency content for very high notes or lower the octave for those parts.
Wave Channel Volume 🔊
The Game Boy wave channel has a global volume control separate from the wavetable data. BeatBax supports specifying this per-wave instrument using volume= (or vol= with a percent suffix).
Valid values:
volume=0orvol=0%— Mute (0%)volume=25orvol=25%— Quiet (25%)volume=50orvol=50%— Medium (50%)volume=100orvol=100%— Loud (100%) — default
Quick reference (BeatBax → hUGE → NR32)
BeatBax volume= | hUGE stored value | NR32 (hex) |
|---|---|---|
100 | 1 | 0x20 |
50 | 2 | 0x40 |
25 | 3 | 0x60 |
0 | 0 | 0x00 |
Note: hUGE stores the raw selector value (0..3); hUGEDriver writes NR32 = (value << 5). This is an output-level selector — not a per-note envelope.
Examples:
inst bass type=wave wave=[0,4,8,12,15,12,8,4,0,4,8,12,15,12,8,4] volume=100
inst pad type=wave wave=[8,11,13,14,15,14,13,11,8,4,2,1,0,1,2,4] vol=50%
Interoperability / round-trip note:
- When importing a
.ugefile, the rawvolume(0..3) should be mapped back to BeatBaxvolume=percentages (1→100, 2→50, 3→25, 0→0) so round-trips remain human-readable. Keep in mind this is a selector only — editing it in an existing song will not affect already-sounding notes until they're retriggered.
Best practices:
- Use
volume=100for leads and bass to sit well with pulse channels. - Use
volume=50for background pads or textures. - Avoid
volume=25unless intentionally very quiet. volume=0is useful for temporarily muting a wave instrument without removing it.
Important: volume= is an output-level selector (stored as 0..3 in UGE). Changes to this value only take effect when the note is retriggered or the instrument is changed — they do not immediately alter already-sounding notes.
FAQ
-
Q: Can I change
volume=mid-note and expect an instantaneous level change?- A: No — the Game Boy hardware (and hUGEDriver) only applies the output-level when the note is triggered or the instrument changes; mid-note changes do not affect the sounding voice.
-
Q: Why does my wave sound quiet even at
volume=100?- A: Check your wavetable peak values — if the maximum sample < 15 the waveform may be quieter than expected. Also verify panning and the channel mix.
Mixing tips
- Use
volume=100for melodic leads and bass patches to match pulse channel perceived loudness. - Use
volume=50for background pads or textures to avoid masking leads. - Reserve
volume=25for very quiet textures or layered effects.
Testing & validation
- Export to UGE (
npm run cli -- export uge <file> out.uge) and inspect using the UGE reader ordxtools to verify theoutput_levelfield for wave instruments. Unit tests in the engine includepackages/engine/tests/uge-wave-volume.test.tsto assert raw storage semantics.
Implementation note
- The PCM renderer applies a simple multiplier mapping (0 → 0.0, 25 → 0.25, 50 → 0.5, 100 → 1.0) to wave samples during rendering to emulate the output-level locally; the UGE writer stores the raw selector (0..3) for hUGE. For consistent results, prefer using
volume=100for key melodic parts if you rely on round-trip UGE exports.
Noise (LFSR)
The noise channel (type=noise) uses a linear-feedback shift register to produce percussion and sound effects.
Modes & parameters
width=7(7-bit) — shorter LFSR; metallic / pitched “crack” (classic snares, toms, kicks)width=15(15-bit) — longer LFSR; broader white noise (hi-hats, shakers, cymbals, ambience)divisorandshiftcontrol the LFSR update rate: highershift→ lower pitched noise. Use combinations to sculpt brightness/time.
Percussion & envelopes
- Snares usually combine
width=7,uge_note=C-7, a fast envelope, and often a shortpitch_env“pop” (e.g.[0,7,0]). - Hi-hats usually use
width=15with short, quiet envelopes.
Default Note Parameter (note=)
New in instrument note mapping feature
All instrument types (pulse, wave, noise) can specify a default note value using the note= parameter. When you use the instrument name as a pattern token (e.g., snare or kick), this note is automatically used:
inst kick type=pulse1 duty=12.5 env=15,down note=C2
inst snare type=noise gb:width=7 env=13,down note=C6
inst hihat_cl type=noise gb:width=15 env=6,down note=C6
pat drums = kick . snare . kick . hihat_cl . # Uses default notes automatically
Behavior
- Pulse/Wave instruments: The specified note is the actual pitch played when using the instrument name as a token.
- Noise instruments: Use
note=for named-token defaults. Preferuge_note=(hUGE display notation) so BeatBax playback and UGE export share the same LFSR clock. - Override per-note: Explicit notes still win:
inst(snare) D6. - No
note=specified: Defaults to C5 for backward compatibility in exports.
Recommended Values
For Game Boy percussion (follows hUGETracker conventions):
- Kicks (pulse channels):
note=C2(deep bass) - Kicks (noise):
uge_note=C-6(often withpitch_env) - Snares (7-bit noise):
uge_note=C-7,note=C6 - Closed hi-hats (15-bit noise):
uge_note=C-7toD-7 - Open hi-hats (15-bit noise):
uge_note=D-7toE-7 - Toms (7-bit noise):
uge_note=C-6toE-6
Important: hUGETracker displays notes ONE OCTAVE HIGHER than BeatBax MIDI-style note= values. Prefer writing tracker pitches with uge_note=.
See Instrument note mapping and Instrument macros.
Cheat-sheet (at-a-glance)
| Instrument | Key params | Typical defaults |
|---|---|---|
| Pulse 1 / 2 | duty, env, sweep (Pulse 1 only) | duty 50, env=gb:15,down,1 |
| Wave | wave=[32] (or [16] shorthand), volume= | 32 nibbles 0..15, volume default 100 |
| Noise | gb:width, uge_note, env, optional divisor/shift | width=7 or 15, uge_note=C-7 for snares |
Tests & examples
- See
songs/instrument_demo.baxand the added tutorial example for quick demos. - Test suggestions: validate
parseWaveTable()clamps values, that 16-value input tiles correctly to 32, that 32-value input is used as-is, that 32-nibble hex strings are parsed correctly, envelope parsing edge cases, and noise frequency mapping under unit tests.
Note — rendering path discrepancy: The PCM renderer (
pcmRenderer.ts) contains a separate localparseWaveTablethat does not tile 16-value inputs to 32; it returns the values as-is and uses a 16-value fallback. The WebAudio path (chips/gameboy/wave.ts) and UGE exporter (ugeWriter.ts) both use 32 as the native size. If you rely on PCM/WAV export, providing all 32 values explicitly gives identical results across both paths.
For other instrument details, consult the respective sections (pulse, noise) in this document or the individual feature pages in /docs/features/.