OPM.js documentationOnline demos

Generated from Markdown source. Edit the source, not this HTML.

Voice v7 and expressive controls #

Voice format version 7 is independent of the npm package version. normalizeVoice accepts an omitted version as v7; explicit v1–v6 retain their original field restrictions. Normalized, prepared, imported and bank voices emit v7. Canonical bank entries require version, name, algorithm, feedback, modIndex, lfo, and exactly four ops; the exported JSON schema describes that canonical shape, including numeric LFO target tuples.

Inputs must use plain objects and dense own-data operator/target arrays. Unknown fields, accessors, malformed enums, nonfinite numbers and unsupported versions are rejected without evaluating getters. normalizeVoice rejects out-of-range numbers; the bank-oriented validateVoice/parseVoiceBank clamp finite numeric values to the documented bounds. Integer selectors and MIDI breakpoints must still be integers. LFO target tuples are detached and frozen even in normalized voices. prepareVoice and bank validation return detached, deeply frozen snapshots, including the optional pitch envelope. A cloned prepared patch must be validated again; a copied shape does not confer trusted identity.

Patch fields #

FieldBounds / meaning
algorithm, feedbackIntegers 0–7; algorithm selects the four-op graph. Feedback acts on operator zero.
name1–64 ASCII letters, digits, _, -; optional for single-voice input.
modIndex0–16 radians per unit modulator signal; single-input default 4.
Operator ratio0.125–32; required even with fixed Hz.
Operator level0–1 before envelope, key/velocity attenuation and live operator multiplier.
Operator detune−1200–1200 cents.
Operator adsrRequired {a,d,s,r}: attack/decay/release 0–10 seconds, sustain gain 0–1. Ramps are linear in dB; the −96 dB floor becomes silence.
Operator keyScaleOptional {breakpoint,leftDbPerOctave,rightDbPerOctave}: breakpoint integer MIDI 0–127, each attenuation 0–24 dB/octave.
Operator velocitySensitivityOptional 0–48 dB attenuation at velocity zero; default zero. Final note velocity gain remains independent.
Operator frequencyOptional 1–20000 Hz, replacing tuned note frequency × ratio. Detune, live pitch/glide, pitch envelope and LFO pitch modulation still apply. Changing MIDI note or tuning does not transpose fixed Hz, but optional key/rate scaling still responds to the played note.
Operator rateKeyScaleOptional 0–4; default zero. Each ADSR duration becomes min(10, seconds * 2 ** (-rateKeyScale * (note - 60) / 12)). Fractional notes interpolate continuously; zero durations stay zero. Durations are prepared at admission and do not follow live pitch bends or tuning changes.
Operator waveformOptional v7 sine (default), half, abs, quarter, alternating, camel, square, saw, noise.
Operator noiseRateOptional v7 20–20000 Hz, default 8000. Ignored unless waveform is noise.

Explicit v1 rejects key scaling and velocity sensitivity; v2 adds key scaling; v3 adds velocity sensitivity; v4 adds LFO waveform. All explicit v1–v4 reject frequency, rateKeyScale, pitchEnvelope, and LFO delay/sync/phase, rather than silently dropping them. V5 adds those expressive fields, represented by LegacyVoiceV5 and LegacyLFOV5. All explicit v1–v5 reject LFO amTargets/pmTargets. V6 adds those per-operator targets; leaving them absent preserves the legacy all-operator modulation behavior without adding default fields to snapshots.

Oscillator shapes and noise #

For θ = phase + modulation and fractional turns t = frac(θ / (2π)), the periodic shapes are:

ShapeFormula
sinesin(θ); the unchanged legacy fast path
halfmax(0, sin(θ))
absabs(sin(θ))
quarterabs(sin(θ)) when frac(θ/π) < 0.5, otherwise zero
alternatingsin(2θ) when t < 0.5, otherwise zero
camelabs(sin(2θ)) when t < 0.5, otherwise zero
square−1 for negative sine, +1 otherwise (including zero)
saw2 * frac(t + 0.5) - 1; rising, zero at θ = 0

Phase reduction uses fractional turns, including negative and very large finite arguments, never wrapping loops. Square, saw and held noise can alias: the existing per-voice oversampling/decimator reduces some products but does not make these oscillators alias-free.

Noise is an original deterministic 17-bit maximal-length LFSR (x^17+x^3+1), seeded with 0x1ffff at every note admission, including pooled slot reuse. The low bit produces bipolar ±1 and advances on the noiseRate time-domain hold clock, independent of output sample rate. Identical admissions reproduce the sequence. Ratio, fixed frequency, detune, tuning, pitch envelopes, live pitch and modulator PM do not pitch noise; existing operatorRatios/operatorFrequencies controls remain valid but have no audible pitch effect on it. Envelope, key/rate scaling, velocity, level, AM and graph routing still apply; noise can be either a modulator or carrier.

This OPM-NE-inspired musical extension allows noise on any operator of any voice, unlike hardware restricted to the last operator of channel 8. It is not a Yamaha/YM2151 fidelity claim. Explicit v1–v6 reject operator waveform or noiseRate with a version-7 requirement; LegacyVoiceV6 preserves the prior shape. Prepared snapshots, bank JSON, content-keyed registration and worklet validation retain both new fields.

Pitch envelope #

Optional pitchEnvelope requires all seven own-data fields:

pitchEnvelope: {
  a: 0.02, d: 0.15, r: 0.1,
  initial: -300, peak: 100, sustain: 0, final: -100
}

a, d, r are 0–10 seconds. initial, peak, sustain, final are −4800–4800 cents. Held notes move linearly in cents initial → peak → sustain. A zero attack skips directly to peak; a zero decay skips directly to sustain. Release moves from the current cents value to final, even during attack/decay; zero release immediately selects final. Final pitch persists while operator releases remain audible. The pitch envelope does not prolong otherwise silent operators or filter tails.

Pitch cents combine multiplicatively with original operator Hz, detune, live semitone pitch/glide and LFO PM. Pitch-envelope and glide evaluation use deterministic synthesis substeps for the selected quality profile: standard 4× (default), eco 2×, high 8×. Splitting render into different buffer sizes does not change the timeline. Operator phase increments are limited to 45% of the internal sample rate before/after LFO PM, preserving the engine's existing high-frequency safety cap.

LFO #

lfo requires rate (0–20 Hz), amDepth (0–1) and pmDepth (0–1200 cents); single-input omission disables modulation. waveform defaults to sine, with triangle, rising saw, and square also supported.

LFO values are evaluated once per output frame. Sine and triangle start at zero rising, saw at −1, square at +1. For operator i, AM multiplies gain by 1 - amDepth * amTargets[i] * (0.5 + 0.5 * waveformValue); PM multiplies pitch by 2 ** (pmDepth * pmTargets[i] * waveformValue / 1200). Missing targets use 1 in these formulas. Delay, sync and phase remain shared by all four operators.

Live operator levels #

Synth.updateNote, host updateNote, scheduled controls, and sequence automation accept:

{ operatorLevels: [1, 0.4, 1.5, 0], ramp: 0.1 }

The readonly tuple must contain exactly four finite own-data numbers in 0–2. Each number multiplies its patch operator level after key/velocity scaling and before FM routing/feedback. All default to 1. A multiplier cannot revive a patch operator whose level is zero. Modulator levels change timbre; carrier levels change that carrier's contribution. Final expression/velocity and equal-power pan remain separate controls.

ramp is 0–10 seconds, default zero. The four multipliers ramp linearly and independently from their current values using the same supplied duration. An interrupted ramp starts from its current value, not the old target. Unrelated expression, pan, modulation or pitch updates do not cancel operator ramps. Validation detaches and freezes the tuple and its outer control record before scheduling. Recycled/stealing voice slots preserve current tails and fully reset controls for new admissions; render processing allocates no arrays or objects.

Live timbre and envelope controls #

The same note-control APIs accept these optional fields independently:

ControlBounds / meaning
feedbackFinite 0–7 continuous feedback selector. Between 0 and 1, gain interpolates from zero to the classic selector-1 gain; 1–7 follows the original exponential gain mapping. Patch feedback remains an integer.
lfoRate, amDepth, pmDepth0–20 Hz, 0–1, 0–1200 cents respectively; update the admitted note's LFO independently.
operatorRatiosReadonly four-element tuple of finite 0.125–32 ratios. Fixed-Hz operators retain these ratios but ignore them until restored to ratio mode.
operatorFrequenciesReadonly four-element tuple, each entry finite 1–20000 Hz or null to restore tuned note × ratio. Fixed Hz ignores MIDI/tuning transposition; detune, live pitch, pitch envelope and LFO PM still apply.
operatorADSRReadonly four-element tuple of complete {a,d,s,r} objects using the patch ADSR bounds. Parameters are replaced, not interpolated by ramp.

ramp gives feedback, LFO and frequency/ratio updates independent linear timelines. It must accompany a rampable scalar or tuple control; operatorADSR alone with ramp rejects. Interrupted updates start at the current value; omitted controls keep their existing timelines. Live lfoRate integrates phase without resetting the current phase, including interrupted rate ramps. Tuples and nested ADSRs are strict own-data snapshots detached and frozen before scheduling.

Updating a held operator's ADSR restarts attack from its current dB level, then decays to the new sustain. With both attack and decay zero, a one-frame bridge preserves continuity. An update during release starts from the current dB level and completes the new key-scaled release, bounded to ten seconds; zero release silences immediately. Envelope durations provide continuity rather than interpolating ADSR parameters with the control ramp.

DX7 import: retained data versus approximations #

importDX7 remains a lossy six-to-four-operator conversion, not hardware emulation. Source format and behavior were checked against the Yamaha DX7 manual, printed pp. 13–17 and 30–31 and the DX7II packed VMEM layout, Add-11.

Operator selection, envelope/output response, topology, feedback placement, velocity and level scaling remain approximations. Oscillator phase carry, source transpose, positive keyboard scaling, per-operator AM and nonzero final amplitude-envelope levels are not preserved. describeDX7 reports these losses separately from the strict usable voice shape.

OPM text import #

importOPM(source: string | Uint8Array): Voice[] and describeOPM(source: string | Uint8Array): OPMImportDescription[] live in opm.js/voices/opm.js. They convert decimal VOPM/MXDRV-family text patches into complete v7 voices through normal normalization. This is approximate conversion, not register-level YM2151 emulation, with no hardware-fidelity claim.

Format references: the published MiOPM text-layout header, VOPM unofficial manual and OPM application manual, especially figures 2.5–2.16. Only format/parameter documentation was used; no emulator or converter implementation was copied. The VOPM GUI multiplier description is doubled relative to hardware MUL: this importer conservatively uses raw hardware values. The published text example contains an out-of-range KS value; hardware KS 0–3 is enforced rather than guessing its meaning. No physical or listening comparison has been performed.

Each patch starts with @:<program 0..127> <name> and must contain exactly one each of LFO:, CH:, M1:, C1:, M2:, C2:. Blank lines and lines starting with // after whitespace are ignored; CRLF is accepted. Section order is flexible; fields are decimal integers only:

LFO: LFRQ AMD PMD WF NFRQ
CH: PAN FL CON AMS PMS SLOT NE
M1: AR D1R D2R RR D1L TL KS MUL DT1 DT2 AMS-EN
C1: (same eleven fields)
M2: (same eleven fields)
C2: (same eleven fields)

Ranges: LFRQ 0–255; AMD/PMD 0–127; WF 0–3; NFRQ 0–31; PAN byte 0–255 (ignored); FL/CON/PMS 0–7; AMS 0–3; NE/AMS-EN 0–1. SLOT is the raw key-on mask: only bits 3/4/5/6 (8/16/32/64) enable M1/C1/M2/C2, so all enabled is 120, not 15. Disabled operators get zero level with warnings. AR/D1R/D2R 0–31; RR/D1L/MUL 0–15; TL 0–127; KS/DT2 0–3; DT1 0–7. Duplicate/missing/unknown sections, malformed/out-of-range fields and NUL reject with RangeError and a line number. Empty files reject. Limits are 262144 input bytes, 128 patches and 512 bytes per line. Byte input is decoded explicitly as Latin-1, not UTF-8 or Windows-1252; strings use UTF-8 byte counts for bounds. Names become [a-zA-Z0-9_-]{1,64}, falling back to OPM, with deterministic _2, _3, … suffixes and truncation.

Conversion formulas #

describeOPM returns {name,program,algorithm,operators,warnings}. operators lists source labels in destination order. Warnings are deterministic, de-duplicated and bounded by the fixed four-operator conversion, including every approximation/substitution/clamp above. Program numbers are metadata only; wire imported names into a MIDI program map explicitly. Patches, not file programs, are de-duplicated by name.