/**
* DX7 Voice (patch) parser and manipulator for Yamaha DX7 SYX files
*
* Handles both the 128-byte packed format (DX7 internal format) and
* 169-byte unpacked format (full parameter set with all operators).
*
* Features:
* - Parse native DX7 voices from SYX banks or single voice files
* - Convert between packed (128-byte) and unpacked (169-byte) formats
* - Import/export JSON representation with human-readable parameters
* - Translate special DX7 characters for compatibility
* - Calculate checksums for SysEx validation
* - Support for algorithm configurations (1-32)
* - Per-operator parameter access (EG, scaling, frequency, output)
* - LFO and pitch envelope parameters
* - Global voice parameters (algorithm, feedback, transpose, etc.)
*
* Based on Dexed implementation by Pascal Gauthier
* @see https://github.com/asb2m10/dexed
*/
import { DX7ParseError, DX7ValidationError } from "../../core/errors.js"
import { DX7Bank } from "./DX7Bank.js"
/**
* JSON representation of a DX7 operator (human-readable)
* @typedef {Object} DX7OperatorJSON
* @memberof DX7Voice
* @property {number} id - Operator number (1-6)
* @property {Object} osc - Oscillator parameters (frequency, detune)
* @property {Object} eg - Envelope generator (rates and levels)
* @property {Object} key - Key scaling parameters (break point, velocity)
* @property {Object} output - Output parameters (level, amp mod sensitivity)
* @property {Object} scale - Keyboard scaling (left/right curves and depths)
*/
/**
* JSON representation of a DX7 voice (patch)
* @typedef {Object} DX7VoiceJSON
* @memberof DX7Voice
* @property {string} name - Voice/patch name (max 10 characters)
* @property {DX7OperatorJSON[]} operators - Array of 6 operators
* @property {Object} pitchEG - Pitch envelope generator (rates and levels)
* @property {Object} lfo - Low frequency oscillator parameters
* @property {Object} global - Global voice parameters (algorithm, feedback, transpose, etc.)
*/
/**
* JSON representation of a DX7 voice with bank index
* @typedef {Object} DX7VoiceIndexJSON
* @memberof DX7Voice
* @property {number} index - Voice index in bank (1-32)
* @property {string} name - Voice/patch name
* @property {DX7OperatorJSON[]} operators - Array of 6 operators
* @property {Object} pitchEG - Pitch envelope parameters
* @property {Object} lfo - LFO parameters
* @property {Object} global - Global voice parameters
*/
/**
* @typedef {Object} DX7BankJSON - JSON representation of a DX7 bank
* @memberof DX7Voice
* @property {string} version - Version string (e.g., "1.0")
* @property {string} name - Bank name (e.g., filename)
* @property {DX7VoiceIndexJSON[]} voices - Array of 32 voices
*/
/**
* DX7 Voice (patch) structure
* Represents a single DX7 voice/patch with 6 operators, algorithm selection,
* LFO, pitch envelope, and global parameters.
*
* Supports two formats:
* - **Packed format (128 bytes)**: DX7 internal format with bit-packed parameters
* - **Unpacked format (169 bytes)**: Decompressed format with all parameters expanded
*
* Each voice contains:
* - 6 FM operators with individual EGs, frequency, output level, and scaling
* - Algorithm selection (1-32) for operator routing
* - Feedback loop (0-7) for algorithm feedback
* - LFO parameters (speed, delay, depth, wave shape)
* - Pitch envelope generator (4 rates, 4 levels)
* - Global parameters (transpose, pitch mod sensitivity, etc.)
* - 10-character voice name
*
* @example
* // Create a voice from packed DX7 bank data
* const bankData = new Uint8Array(...); // 4096 bytes for 32 voices
* const voiceIndex = 5; // 6th voice in bank
* const voiceData = bankData.subarray(voiceIndex * 128, (voiceIndex + 1) * 128);
* const voice = new DX7Voice(voiceData, voiceIndex);
* console.log(voice.name); // e.g., "BASS 1"
*
* @example
* // Load a voice from a single-voice SYX file
* const file = document.getElementById("voice-file").files[0];
* const voice = await DX7Voice.fromFile(file);
* console.log(voice.toJSON());
*
* @example
* // Create from JSON and export to SysEx
* const json = loadVoiceData();
* const voice = DX7Voice.fromJSON(json);
* const syxData = voice.toSysEx(); // For single voice synths like Volca FM
* const packed = voice.data; // 128 bytes for DX7 bank
*
* @example
* // Modify operator parameters
* const voice = await DX7Voice.fromFile(file);
* const unpacked = voice.unpack();
* unpacked[DX7Voice.UNPACKED_OP_OUTPUT_LEVEL] = 80; // Change OP1 output level
* unpacked[DX7Voice.UNPACKED_ALGORITHM] = 5; // Change to algorithm 6
* const modifiedVoice = DX7Voice.fromUnpacked(unpacked);
*/
export class DX7Voice {
// Packed format (128 bytes)
// See: DX7 Service Manual, Voice Memory Format
static PACKED_SIZE = 128
static PACKED_OP_SIZE = 17 // 17 bytes per operator in packed format
static NUM_OPERATORS = 6
// Packed operator parameter offsets (within each 17-byte operator block)
static PACKED_OP_EG_RATE_1 = 0
static PACKED_OP_EG_RATE_2 = 1
static PACKED_OP_EG_RATE_3 = 2
static PACKED_OP_EG_RATE_4 = 3
static PACKED_OP_EG_LEVEL_1 = 4
static PACKED_OP_EG_LEVEL_2 = 5
static PACKED_OP_EG_LEVEL_3 = 6
static PACKED_OP_EG_LEVEL_4 = 7
static PACKED_OP_BREAK_POINT = 8
static PACKED_OP_L_SCALE_DEPTH = 9
static PACKED_OP_R_SCALE_DEPTH = 10
static PACKED_OP_CURVES = 11 // LC and RC packed
static PACKED_OP_RATE_SCALING = 12 // RS and DET packed
static PACKED_OP_MOD_SENS = 13 // AMS and KVS packed
static PACKED_OP_OUTPUT_LEVEL = 14
static PACKED_OP_MODE_FREQ = 15 // Mode and Freq Coarse packed
static PACKED_OP_DETUNE_FINE = 16 // OSC Detune and Freq Fine packed
// Packed voice offsets (after 6 operators = bytes 102+)
static PACKED_PITCH_EG_RATE_1 = 102
static PACKED_PITCH_EG_RATE_2 = 103
static PACKED_PITCH_EG_RATE_3 = 104
static PACKED_PITCH_EG_RATE_4 = 105
static PACKED_PITCH_EG_LEVEL_1 = 106
static PACKED_PITCH_EG_LEVEL_2 = 107
static PACKED_PITCH_EG_LEVEL_3 = 108
static PACKED_PITCH_EG_LEVEL_4 = 109
static OFFSET_ALGORITHM = 110
static OFFSET_FEEDBACK = 111 // Also contains OSC Sync
static OFFSET_LFO_SPEED = 112
static OFFSET_LFO_DELAY = 113
static OFFSET_LFO_PM_DEPTH = 114
static OFFSET_LFO_AM_DEPTH = 115
static OFFSET_LFO_SYNC_WAVE = 116 // LFO sync, wave, and PM sensitivity packed
static OFFSET_TRANSPOSE = 117
static OFFSET_AMP_MOD_SENS = 118
static OFFSET_EG_BIAS_SENS = 119
// Voice name (bytes 118-127)
// IMPORTANT: Byte 118 serves dual-purpose in DX7 hardware:
// 1. First character of voice name (as ASCII)
// 2. Amp Mod Sensitivity parameter (as numeric value 0-127)
// Both interpretations are used when converting to unpacked format
static PACKED_NAME_START = 118
static NAME_LENGTH = 10
// Unpacked format (169 bytes)
static UNPACKED_SIZE = 169 // Total unpacked size (159 params + 10 name)
static UNPACKED_OP_SIZE = 23 // 23 bytes per operator in unpacked format
// Unpacked operator parameter offsets (within each 23-byte operator block)
static UNPACKED_OP_EG_RATE_1 = 0
static UNPACKED_OP_EG_RATE_2 = 1
static UNPACKED_OP_EG_RATE_3 = 2
static UNPACKED_OP_EG_RATE_4 = 3
static UNPACKED_OP_EG_LEVEL_1 = 4
static UNPACKED_OP_EG_LEVEL_2 = 5
static UNPACKED_OP_EG_LEVEL_3 = 6
static UNPACKED_OP_EG_LEVEL_4 = 7
static UNPACKED_OP_BREAK_POINT = 8
static UNPACKED_OP_L_SCALE_DEPTH = 9
static UNPACKED_OP_R_SCALE_DEPTH = 10
static UNPACKED_OP_L_CURVE = 11
static UNPACKED_OP_R_CURVE = 12
static UNPACKED_OP_RATE_SCALING = 13
static UNPACKED_OP_DETUNE = 14
static UNPACKED_OP_AMP_MOD_SENS = 15
static UNPACKED_OP_OUTPUT_LEVEL = 16
static UNPACKED_OP_MODE = 17 // Mode (0=ratio, 1=fixed)
static UNPACKED_OP_KEY_VEL_SENS = 18
static UNPACKED_OP_FREQ_COARSE = 19
static UNPACKED_OP_OSC_DETUNE = 20
static UNPACKED_OP_FREQ_FINE = 21
// Unpacked pitch EG offsets (after 6 operators = index 138+)
static UNPACKED_PITCH_EG_RATE_1 = 138
static UNPACKED_PITCH_EG_RATE_2 = 139
static UNPACKED_PITCH_EG_RATE_3 = 140
static UNPACKED_PITCH_EG_RATE_4 = 141
static UNPACKED_PITCH_EG_LEVEL_1 = 142
static UNPACKED_PITCH_EG_LEVEL_2 = 143
static UNPACKED_PITCH_EG_LEVEL_3 = 144
static UNPACKED_PITCH_EG_LEVEL_4 = 145
// Unpacked global parameters (after pitch EG = index 146+)
static UNPACKED_ALGORITHM = 146
static UNPACKED_FEEDBACK = 147
static UNPACKED_OSC_SYNC = 148
static UNPACKED_LFO_SPEED = 149
static UNPACKED_LFO_DELAY = 150
static UNPACKED_LFO_PM_DEPTH = 151
static UNPACKED_LFO_AM_DEPTH = 152
static UNPACKED_LFO_KEY_SYNC = 153
static UNPACKED_LFO_WAVE = 154
static UNPACKED_LFO_PM_SENS = 155
static UNPACKED_AMP_MOD_SENS = 156
static UNPACKED_TRANSPOSE = 157
static UNPACKED_EG_BIAS_SENS = 158
static UNPACKED_NAME_START = 159
// VCED (single voice SysEx) format - for DX7 single patch dumps
static VCED_SIZE = 163 // Total VCED sysex size (6 header + 155 data + 1 checksum + 1 end)
static VCED_HEADER_SIZE = 6
static VCED_DATA_SIZE = 155 // Voice data bytes (6 operators × 21 bytes + 8 pitch EG + 11 global + 10 name)
// VCED header bytes - DX7 single voice dump format
static VCED_SYSEX_START = 0xf0 // SysEx Message Start
static VCED_YAMAHA_ID = 0x43 // Yamaha manufacturer ID
static VCED_SUB_STATUS = 0x00
static VCED_FORMAT_SINGLE = 0x00 // Single voice format identifier
static VCED_BYTE_COUNT_MSB = 0x01 // High byte of data length (1)
static VCED_BYTE_COUNT_LSB = 0x1b // Low byte of data length (27 in decimal = 155 bytes)
static VCED_SYSEX_END = 0xf7 // SysEx Message End
// Bit masks
static MASK_7BIT = 0x7f // Standard 7-bit MIDI data mask
static MASK_2BIT = 0x03 // For 2-bit values (curves)
static MASK_3BIT = 0x07 // For 3-bit values (RS, detune)
static MASK_4BIT = 0x0f // For 4-bit values (detune, fine freq)
static MASK_5BIT = 0x1f // For 5-bit values (algorithm, freq coarse)
static MASK_1BIT = 0x01 // For 1-bit values (mode, sync)
// Parameter value ranges
static TRANSPOSE_CENTER = 24 // MIDI note 24 = C0 (center of DX7 transpose range: -24 to +24 semitones)
// Special character mappings - for Japanese DX7 character set compatibility
static CHAR_YEN = 92 // Japanese Yen symbol (¥) maps to ASCII backslash
static CHAR_ARROW_RIGHT = 126 // Right arrow (→) maps to ASCII tilde
static CHAR_ARROW_LEFT = 127 // Left arrow (←) maps to ASCII DEL
static CHAR_REPLACEMENT_Y = 89 // Replace Yen symbol with "Y"
static CHAR_REPLACEMENT_GT = 62 // Right arrow with ">"
static CHAR_REPLACEMENT_LT = 60 // Left arrow with "<"
static CHAR_SPACE = 32 // Standard space character
static CHAR_MIN_PRINTABLE = 32 // Minimum ASCII printable character
static CHAR_MAX_PRINTABLE = 126 // Maximum ASCII printable character
// Default voice values
static DEFAULT_EG_RATE = 99
static DEFAULT_EG_LEVEL_MAX = 99
static DEFAULT_EG_LEVEL_MIN = 0
static DEFAULT_BREAK_POINT = 60 // MIDI note 60 = C3
static DEFAULT_OUTPUT_LEVEL = 99
static DEFAULT_PITCH_EG_LEVEL = 50
static DEFAULT_LFO_SPEED = 35
static DEFAULT_LFO_PM_SENS = 3
static DEFAULT_ALGORITHM = 0
static DEFAULT_FEEDBACK = 0
// MIDI notes
static MIDI_OCTAVE_OFFSET = -2 // For displaying MIDI notes (MIDI 0 = C-2)
static MIDI_BREAK_POINT_OFFSET = 21 // Offset for breakpoint display
/**
* Create a DX7Voice from raw 128-byte data
* @param {Array<number>|Uint8Array} data - 128 bytes of voice data
* @param {number} index - Voice index (0-31)
* @throws {DX7ValidationError} If data length is not exactly 128 bytes
*/
constructor(data, index = 0) {
if (data.length !== DX7Voice.PACKED_SIZE) {
throw new DX7ValidationError(
`Invalid voice data length: expected ${DX7Voice.PACKED_SIZE} bytes, got ${data.length}`,
"length",
data.length,
)
}
this.index = index
this.data = new Uint8Array(data)
this.name = this._extractName()
this._unpackedCache = null
}
/**
* Extract the voice name from the data (10 characters at offset 118)
* @private
*/
_extractName() {
const nameBytes = this.data.subarray(DX7Voice.PACKED_NAME_START, DX7Voice.PACKED_NAME_START + DX7Voice.NAME_LENGTH)
const normalized = Array.from(nameBytes).map((byte) => {
let c = byte & DX7Voice.MASK_7BIT
// Dexed special character mappings
if (c === DX7Voice.CHAR_YEN) c = DX7Voice.CHAR_REPLACEMENT_Y
if (c === DX7Voice.CHAR_ARROW_RIGHT) c = DX7Voice.CHAR_REPLACEMENT_GT
if (c === DX7Voice.CHAR_ARROW_LEFT) c = DX7Voice.CHAR_REPLACEMENT_LT
if (c < DX7Voice.CHAR_MIN_PRINTABLE || c > DX7Voice.CHAR_MAX_PRINTABLE) c = DX7Voice.CHAR_SPACE
return String.fromCharCode(c)
})
return normalized.join("").trim()
}
/**
* Get a raw parameter value from the packed data
* @param {number} offset - Byte offset in the voice data (0-127)
* @returns {number} Parameter value (0-127)
* @throws {DX7ValidationError} If offset is out of range
*/
getParameter(offset) {
if (offset < 0 || offset >= DX7Voice.PACKED_SIZE) {
throw new DX7ValidationError(
`Parameter offset out of range: ${offset} (must be 0-${DX7Voice.PACKED_SIZE - 1})`,
"offset",
offset,
)
}
return this.data[offset] & DX7Voice.MASK_7BIT
}
/**
* Get a parameter value from the unpacked 169-byte format
* @param {number} offset - Byte offset in the unpacked data (0-168)
* @returns {number} Parameter value (0-127)
* @throws {DX7ValidationError} If offset is out of range
*/
getUnpackedParameter(offset) {
if (offset < 0 || offset >= DX7Voice.UNPACKED_SIZE) {
throw new DX7ValidationError(
`Unpacked parameter offset out of range: ${offset} (must be 0-${DX7Voice.UNPACKED_SIZE - 1})`,
"offset",
offset,
)
}
if (!this._unpackedCache) {
this._unpackedCache = this.unpack()
}
return this._unpackedCache[offset] & DX7Voice.MASK_7BIT
}
/**
* Set a raw parameter value in the packed data
* @param {number} offset - Byte offset in the voice data
* @param {number} value - Parameter value (0-127)
*/
setParameter(offset, value) {
if (offset < 0 || offset >= DX7Voice.PACKED_SIZE) {
throw new DX7ValidationError(
`Parameter offset out of range: ${offset} (must be 0-${DX7Voice.PACKED_SIZE - 1})`,
"offset",
offset,
)
}
this.data[offset] = value & DX7Voice.MASK_7BIT
this._unpackedCache = null
// Update name if name bytes changed
if (offset >= DX7Voice.PACKED_NAME_START && offset < DX7Voice.PACKED_NAME_START + DX7Voice.NAME_LENGTH) {
this.name = this._extractName()
}
}
/**
* Unpack the voice data to 169-byte unpacked format
* This converts the packed 128-byte format to the full DX7 parameter set
* @returns {Uint8Array} 169 bytes of unpacked voice data (138 operator + 8 pitch EG + 13 global + 10 name = 169 bytes)
*/
unpack() {
const packed = this.data
const unpacked = new Uint8Array(DX7Voice.UNPACKED_SIZE)
this._unpackOperators(packed, unpacked)
this._unpackPitchEG(packed, unpacked)
this._unpackGlobalParams(packed, unpacked)
this._unpackName(packed, unpacked)
return unpacked
}
/**
* Unpack all 6 operators from packed to unpacked format
* @private
*/
_unpackOperators(packed, unpacked) {
for (let op = 0; op < DX7Voice.NUM_OPERATORS; op++) {
// DX7 stores operators in reverse: OP1 at end, OP6 at start
const src = (DX7Voice.NUM_OPERATORS - 1 - op) * DX7Voice.PACKED_OP_SIZE
const dst = op * DX7Voice.UNPACKED_OP_SIZE
this._unpackOperator(packed, unpacked, src, dst)
}
}
/**
* Unpack a single operator's parameters
* @private
*/
_unpackOperator(packed, unpacked, src, dst) {
this._unpackOperatorEG(packed, unpacked, src, dst)
this._unpackOperatorScaling(packed, unpacked, src, dst)
this._unpackOperatorPackedParams(packed, unpacked, src, dst)
this._unpackOperatorFrequency(packed, unpacked, src, dst)
}
/**
* Unpack operator EG rates and levels
* @private
*/
_unpackOperatorEG(packed, unpacked, src, dst) {
// EG rates (4 bytes)
unpacked[dst + DX7Voice.UNPACKED_OP_EG_RATE_1] = packed[src + DX7Voice.PACKED_OP_EG_RATE_1] & DX7Voice.MASK_7BIT
unpacked[dst + DX7Voice.UNPACKED_OP_EG_RATE_2] = packed[src + DX7Voice.PACKED_OP_EG_RATE_2] & DX7Voice.MASK_7BIT
unpacked[dst + DX7Voice.UNPACKED_OP_EG_RATE_3] = packed[src + DX7Voice.PACKED_OP_EG_RATE_3] & DX7Voice.MASK_7BIT
unpacked[dst + DX7Voice.UNPACKED_OP_EG_RATE_4] = packed[src + DX7Voice.PACKED_OP_EG_RATE_4] & DX7Voice.MASK_7BIT
// EG levels (4 bytes)
unpacked[dst + DX7Voice.UNPACKED_OP_EG_LEVEL_1] = packed[src + DX7Voice.PACKED_OP_EG_LEVEL_1] & DX7Voice.MASK_7BIT
unpacked[dst + DX7Voice.UNPACKED_OP_EG_LEVEL_2] = packed[src + DX7Voice.PACKED_OP_EG_LEVEL_2] & DX7Voice.MASK_7BIT
unpacked[dst + DX7Voice.UNPACKED_OP_EG_LEVEL_3] = packed[src + DX7Voice.PACKED_OP_EG_LEVEL_3] & DX7Voice.MASK_7BIT
unpacked[dst + DX7Voice.UNPACKED_OP_EG_LEVEL_4] = packed[src + DX7Voice.PACKED_OP_EG_LEVEL_4] & DX7Voice.MASK_7BIT
}
/**
* Unpack operator keyboard scaling parameters
* @private
*/
_unpackOperatorScaling(packed, unpacked, src, dst) {
unpacked[dst + DX7Voice.UNPACKED_OP_BREAK_POINT] = packed[src + DX7Voice.PACKED_OP_BREAK_POINT] & DX7Voice.MASK_7BIT
unpacked[dst + DX7Voice.UNPACKED_OP_L_SCALE_DEPTH] =
packed[src + DX7Voice.PACKED_OP_L_SCALE_DEPTH] & DX7Voice.MASK_7BIT
unpacked[dst + DX7Voice.UNPACKED_OP_R_SCALE_DEPTH] =
packed[src + DX7Voice.PACKED_OP_R_SCALE_DEPTH] & DX7Voice.MASK_7BIT
}
/**
* Unpack operator bit-packed parameters (curves, rate scaling, mod sens)
* @private
*/
_unpackOperatorPackedParams(packed, unpacked, src, dst) {
// Key scales (bits 0-1 = LC, bits 2-3 = RC)
const curves = packed[src + DX7Voice.PACKED_OP_CURVES] & DX7Voice.MASK_7BIT
unpacked[dst + DX7Voice.UNPACKED_OP_L_CURVE] = curves & DX7Voice.MASK_2BIT
unpacked[dst + DX7Voice.UNPACKED_OP_R_CURVE] = (curves >> 2) & DX7Voice.MASK_2BIT
// Rate scaling and detune (bits 0-2 = RS, bits 3-6 = DET)
const rateScaling = packed[src + DX7Voice.PACKED_OP_RATE_SCALING] & DX7Voice.MASK_7BIT
unpacked[dst + DX7Voice.UNPACKED_OP_RATE_SCALING] = rateScaling & DX7Voice.MASK_3BIT
unpacked[dst + DX7Voice.UNPACKED_OP_DETUNE] = (rateScaling >> 3) & DX7Voice.MASK_4BIT
// Amp mod sensitivity and key velocity sensitivity (bits 0-1 = AMS, bits 2-4 = KVS)
const modSens = packed[src + DX7Voice.PACKED_OP_MOD_SENS] & DX7Voice.MASK_7BIT
unpacked[dst + DX7Voice.UNPACKED_OP_AMP_MOD_SENS] = modSens & DX7Voice.MASK_2BIT
unpacked[dst + DX7Voice.UNPACKED_OP_KEY_VEL_SENS] = (modSens >> 2) & DX7Voice.MASK_3BIT
// Output level
unpacked[dst + DX7Voice.UNPACKED_OP_OUTPUT_LEVEL] =
packed[src + DX7Voice.PACKED_OP_OUTPUT_LEVEL] & DX7Voice.MASK_7BIT
}
/**
* Unpack operator frequency parameters
* @private
*/
_unpackOperatorFrequency(packed, unpacked, src, dst) {
// Mode and frequency (bits 0 = MODE, bits 1-5 = FREQ)
const modeFreq = packed[src + DX7Voice.PACKED_OP_MODE_FREQ] & DX7Voice.MASK_7BIT
unpacked[dst + DX7Voice.UNPACKED_OP_MODE] = modeFreq & DX7Voice.MASK_1BIT
unpacked[dst + DX7Voice.UNPACKED_OP_FREQ_COARSE] = (modeFreq >> 1) & DX7Voice.MASK_5BIT
// OSC detune and frequency fine (bits 0-2 = OSC DET, bits 3-6 = FREQ FINE)
const detuneFine = packed[src + DX7Voice.PACKED_OP_DETUNE_FINE] & DX7Voice.MASK_7BIT
unpacked[dst + DX7Voice.UNPACKED_OP_OSC_DETUNE] = detuneFine & DX7Voice.MASK_3BIT
unpacked[dst + DX7Voice.UNPACKED_OP_FREQ_FINE] = (detuneFine >> 3) & DX7Voice.MASK_4BIT
}
/**
* Unpack pitch envelope generator parameters
* @private
*/
_unpackPitchEG(packed, unpacked) {
unpacked[DX7Voice.UNPACKED_PITCH_EG_RATE_1] = packed[DX7Voice.PACKED_PITCH_EG_RATE_1] & DX7Voice.MASK_7BIT
unpacked[DX7Voice.UNPACKED_PITCH_EG_RATE_2] = packed[DX7Voice.PACKED_PITCH_EG_RATE_2] & DX7Voice.MASK_7BIT
unpacked[DX7Voice.UNPACKED_PITCH_EG_RATE_3] = packed[DX7Voice.PACKED_PITCH_EG_RATE_3] & DX7Voice.MASK_7BIT
unpacked[DX7Voice.UNPACKED_PITCH_EG_RATE_4] = packed[DX7Voice.PACKED_PITCH_EG_RATE_4] & DX7Voice.MASK_7BIT
unpacked[DX7Voice.UNPACKED_PITCH_EG_LEVEL_1] = packed[DX7Voice.PACKED_PITCH_EG_LEVEL_1] & DX7Voice.MASK_7BIT
unpacked[DX7Voice.UNPACKED_PITCH_EG_LEVEL_2] = packed[DX7Voice.PACKED_PITCH_EG_LEVEL_2] & DX7Voice.MASK_7BIT
unpacked[DX7Voice.UNPACKED_PITCH_EG_LEVEL_3] = packed[DX7Voice.PACKED_PITCH_EG_LEVEL_3] & DX7Voice.MASK_7BIT
unpacked[DX7Voice.UNPACKED_PITCH_EG_LEVEL_4] = packed[DX7Voice.PACKED_PITCH_EG_LEVEL_4] & DX7Voice.MASK_7BIT
}
/**
* Unpack global voice parameters (algorithm, feedback, LFO, etc.)
* @private
*/
_unpackGlobalParams(packed, unpacked) {
unpacked[DX7Voice.UNPACKED_ALGORITHM] = packed[DX7Voice.OFFSET_ALGORITHM] & DX7Voice.MASK_5BIT
// Feedback and OSC Sync combined (bits 0-2 = Feedback, bit 3 = OSC Sync)
const feedbackOscSync = packed[DX7Voice.OFFSET_FEEDBACK] & DX7Voice.MASK_7BIT
unpacked[DX7Voice.UNPACKED_FEEDBACK] = feedbackOscSync & DX7Voice.MASK_3BIT
unpacked[DX7Voice.UNPACKED_OSC_SYNC] = (feedbackOscSync >> 3) & DX7Voice.MASK_1BIT
unpacked[DX7Voice.UNPACKED_LFO_SPEED] = packed[DX7Voice.OFFSET_LFO_SPEED] & DX7Voice.MASK_7BIT
unpacked[DX7Voice.UNPACKED_LFO_DELAY] = packed[DX7Voice.OFFSET_LFO_DELAY] & DX7Voice.MASK_7BIT
unpacked[DX7Voice.UNPACKED_LFO_PM_DEPTH] = packed[DX7Voice.OFFSET_LFO_PM_DEPTH] & DX7Voice.MASK_7BIT
unpacked[DX7Voice.UNPACKED_LFO_AM_DEPTH] = packed[DX7Voice.OFFSET_LFO_AM_DEPTH] & DX7Voice.MASK_7BIT
// LFO Key Sync, Wave, Pitch Mod Sensitivity packed
const lfoParams = packed[DX7Voice.OFFSET_LFO_SYNC_WAVE] & DX7Voice.MASK_7BIT
unpacked[DX7Voice.UNPACKED_LFO_KEY_SYNC] = lfoParams & DX7Voice.MASK_1BIT
unpacked[DX7Voice.UNPACKED_LFO_WAVE] = (lfoParams >> 1) & DX7Voice.MASK_3BIT
unpacked[DX7Voice.UNPACKED_LFO_PM_SENS] = (lfoParams >> 4) & DX7Voice.MASK_3BIT
unpacked[DX7Voice.UNPACKED_AMP_MOD_SENS] = packed[DX7Voice.OFFSET_AMP_MOD_SENS] & DX7Voice.MASK_7BIT
unpacked[DX7Voice.UNPACKED_TRANSPOSE] = packed[DX7Voice.OFFSET_TRANSPOSE] & DX7Voice.MASK_7BIT
unpacked[DX7Voice.UNPACKED_EG_BIAS_SENS] = packed[DX7Voice.OFFSET_EG_BIAS_SENS] & DX7Voice.MASK_7BIT
}
/**
* Copy voice name from packed to unpacked format
* @private
*/
_unpackName(packed, unpacked) {
for (let i = 0; i < DX7Voice.NAME_LENGTH; i++) {
unpacked[DX7Voice.UNPACKED_NAME_START + i] = packed[DX7Voice.PACKED_NAME_START + i] & DX7Voice.MASK_7BIT
}
}
/**
* Pack 169-byte unpacked data to 128-byte format
* @param {Array<number>|Uint8Array} unpacked - 169 bytes of unpacked data (159 parameters + 10 name bytes)
* @returns {Uint8Array} 128 bytes of packed data
*/
static pack(unpacked) {
if (unpacked.length !== DX7Voice.UNPACKED_SIZE) {
throw new DX7ValidationError(
`Invalid unpacked data length: expected ${DX7Voice.UNPACKED_SIZE} bytes, got ${unpacked.length}`,
"length",
unpacked.length,
)
}
const packed = new Uint8Array(DX7Voice.PACKED_SIZE)
DX7Voice._packOperators(unpacked, packed)
DX7Voice._packPitchEG(unpacked, packed)
DX7Voice._packGlobalParams(unpacked, packed)
DX7Voice._packName(unpacked, packed)
return packed
}
/**
* Pack all 6 operators from unpacked to packed format
* @private
*/
static _packOperators(unpacked, packed) {
for (let op = 0; op < DX7Voice.NUM_OPERATORS; op++) {
// DX7 stores operators in reverse: OP1 goes to packed[85-101], OP6 to packed[0-16]
const src = op * DX7Voice.UNPACKED_OP_SIZE
const dst = (DX7Voice.NUM_OPERATORS - 1 - op) * DX7Voice.PACKED_OP_SIZE
DX7Voice._packOperator(unpacked, packed, src, dst)
}
}
/**
* Pack a single operator's parameters
* @private
*/
static _packOperator(unpacked, packed, src, dst) {
DX7Voice._packOperatorEG(unpacked, packed, src, dst)
DX7Voice._packOperatorScaling(unpacked, packed, src, dst)
DX7Voice._packOperatorPackedParams(unpacked, packed, src, dst)
DX7Voice._packOperatorFrequency(unpacked, packed, src, dst)
}
/**
* Pack operator EG rates and levels
* @private
*/
static _packOperatorEG(unpacked, packed, src, dst) {
// EG rates (4 bytes)
packed[dst + DX7Voice.PACKED_OP_EG_RATE_1] = unpacked[src + DX7Voice.UNPACKED_OP_EG_RATE_1]
packed[dst + DX7Voice.PACKED_OP_EG_RATE_2] = unpacked[src + DX7Voice.UNPACKED_OP_EG_RATE_2]
packed[dst + DX7Voice.PACKED_OP_EG_RATE_3] = unpacked[src + DX7Voice.UNPACKED_OP_EG_RATE_3]
packed[dst + DX7Voice.PACKED_OP_EG_RATE_4] = unpacked[src + DX7Voice.UNPACKED_OP_EG_RATE_4]
// EG levels (4 bytes)
packed[dst + DX7Voice.PACKED_OP_EG_LEVEL_1] = unpacked[src + DX7Voice.UNPACKED_OP_EG_LEVEL_1]
packed[dst + DX7Voice.PACKED_OP_EG_LEVEL_2] = unpacked[src + DX7Voice.UNPACKED_OP_EG_LEVEL_2]
packed[dst + DX7Voice.PACKED_OP_EG_LEVEL_3] = unpacked[src + DX7Voice.UNPACKED_OP_EG_LEVEL_3]
packed[dst + DX7Voice.PACKED_OP_EG_LEVEL_4] = unpacked[src + DX7Voice.UNPACKED_OP_EG_LEVEL_4]
}
/**
* Pack operator keyboard scaling parameters
* @private
*/
static _packOperatorScaling(unpacked, packed, src, dst) {
packed[dst + DX7Voice.PACKED_OP_BREAK_POINT] = unpacked[src + DX7Voice.UNPACKED_OP_BREAK_POINT]
packed[dst + DX7Voice.PACKED_OP_L_SCALE_DEPTH] = unpacked[src + DX7Voice.UNPACKED_OP_L_SCALE_DEPTH]
packed[dst + DX7Voice.PACKED_OP_R_SCALE_DEPTH] = unpacked[src + DX7Voice.UNPACKED_OP_R_SCALE_DEPTH]
}
/**
* Pack operator bit-packed parameters
* @private
*/
static _packOperatorPackedParams(unpacked, packed, src, dst) {
// Combine key scales (LC and RC)
const lc = unpacked[src + DX7Voice.UNPACKED_OP_L_CURVE] & DX7Voice.MASK_2BIT
const rc = unpacked[src + DX7Voice.UNPACKED_OP_R_CURVE] & DX7Voice.MASK_2BIT
packed[dst + DX7Voice.PACKED_OP_CURVES] = lc | (rc << 2)
// Combine rate scaling and detune
const rs = unpacked[src + DX7Voice.UNPACKED_OP_RATE_SCALING] & DX7Voice.MASK_3BIT
const det = unpacked[src + DX7Voice.UNPACKED_OP_DETUNE] & DX7Voice.MASK_4BIT
packed[dst + DX7Voice.PACKED_OP_RATE_SCALING] = rs | (det << 3)
// Combine amp mod sensitivity and key velocity sensitivity
const ams = unpacked[src + DX7Voice.UNPACKED_OP_AMP_MOD_SENS] & DX7Voice.MASK_2BIT
const kvs = unpacked[src + DX7Voice.UNPACKED_OP_KEY_VEL_SENS] & DX7Voice.MASK_3BIT
packed[dst + DX7Voice.PACKED_OP_MOD_SENS] = ams | (kvs << 2)
// Output level
packed[dst + DX7Voice.PACKED_OP_OUTPUT_LEVEL] = unpacked[src + DX7Voice.UNPACKED_OP_OUTPUT_LEVEL]
}
/**
* Pack operator frequency parameters
* @private
*/
static _packOperatorFrequency(unpacked, packed, src, dst) {
// Combine mode and frequency
const mode = unpacked[src + DX7Voice.UNPACKED_OP_MODE] & DX7Voice.MASK_1BIT
const freq = unpacked[src + DX7Voice.UNPACKED_OP_FREQ_COARSE] & DX7Voice.MASK_5BIT
packed[dst + DX7Voice.PACKED_OP_MODE_FREQ] = mode | (freq << 1)
// Combine OSC detune and frequency fine
const oscDetune = unpacked[src + DX7Voice.UNPACKED_OP_OSC_DETUNE] & DX7Voice.MASK_3BIT
const freqFine = unpacked[src + DX7Voice.UNPACKED_OP_FREQ_FINE] & DX7Voice.MASK_4BIT
packed[dst + DX7Voice.PACKED_OP_DETUNE_FINE] = oscDetune | (freqFine << 3)
}
/**
* Pack pitch envelope generator parameters
* @private
*/
static _packPitchEG(unpacked, packed) {
packed[DX7Voice.PACKED_PITCH_EG_RATE_1] = unpacked[DX7Voice.UNPACKED_PITCH_EG_RATE_1]
packed[DX7Voice.PACKED_PITCH_EG_RATE_2] = unpacked[DX7Voice.UNPACKED_PITCH_EG_RATE_2]
packed[DX7Voice.PACKED_PITCH_EG_RATE_3] = unpacked[DX7Voice.UNPACKED_PITCH_EG_RATE_3]
packed[DX7Voice.PACKED_PITCH_EG_RATE_4] = unpacked[DX7Voice.UNPACKED_PITCH_EG_RATE_4]
packed[DX7Voice.PACKED_PITCH_EG_LEVEL_1] = unpacked[DX7Voice.UNPACKED_PITCH_EG_LEVEL_1]
packed[DX7Voice.PACKED_PITCH_EG_LEVEL_2] = unpacked[DX7Voice.UNPACKED_PITCH_EG_LEVEL_2]
packed[DX7Voice.PACKED_PITCH_EG_LEVEL_3] = unpacked[DX7Voice.UNPACKED_PITCH_EG_LEVEL_3]
packed[DX7Voice.PACKED_PITCH_EG_LEVEL_4] = unpacked[DX7Voice.UNPACKED_PITCH_EG_LEVEL_4]
}
/**
* Pack global voice parameters (algorithm, feedback, LFO, etc.)
* @private
*/
static _packGlobalParams(unpacked, packed) {
packed[DX7Voice.OFFSET_ALGORITHM] = unpacked[DX7Voice.UNPACKED_ALGORITHM]
// Combine feedback and OSC Sync
const feedback = unpacked[DX7Voice.UNPACKED_FEEDBACK] & DX7Voice.MASK_3BIT
const oscSync = unpacked[DX7Voice.UNPACKED_OSC_SYNC] & DX7Voice.MASK_1BIT
packed[DX7Voice.OFFSET_FEEDBACK] = feedback | (oscSync << 3)
packed[DX7Voice.OFFSET_LFO_SPEED] = unpacked[DX7Voice.UNPACKED_LFO_SPEED]
packed[DX7Voice.OFFSET_LFO_DELAY] = unpacked[DX7Voice.UNPACKED_LFO_DELAY]
packed[DX7Voice.OFFSET_LFO_PM_DEPTH] = unpacked[DX7Voice.UNPACKED_LFO_PM_DEPTH]
packed[DX7Voice.OFFSET_LFO_AM_DEPTH] = unpacked[DX7Voice.UNPACKED_LFO_AM_DEPTH]
// Combine LFO Key Sync, Wave, Pitch Mod Sensitivity
const lfoKeySync = unpacked[DX7Voice.UNPACKED_LFO_KEY_SYNC] & DX7Voice.MASK_1BIT
const lfoWave = unpacked[DX7Voice.UNPACKED_LFO_WAVE] & DX7Voice.MASK_3BIT
const lfoPitchSens = unpacked[DX7Voice.UNPACKED_LFO_PM_SENS] & DX7Voice.MASK_3BIT
packed[DX7Voice.OFFSET_LFO_SYNC_WAVE] = lfoKeySync | (lfoWave << 1) | (lfoPitchSens << 4)
packed[DX7Voice.OFFSET_AMP_MOD_SENS] = unpacked[DX7Voice.UNPACKED_AMP_MOD_SENS]
packed[DX7Voice.OFFSET_TRANSPOSE] = unpacked[DX7Voice.UNPACKED_TRANSPOSE]
packed[DX7Voice.OFFSET_EG_BIAS_SENS] = unpacked[DX7Voice.UNPACKED_EG_BIAS_SENS]
}
/**
* Copy voice name from unpacked to packed format
* @private
*/
static _packName(unpacked, packed) {
for (let i = 0; i < DX7Voice.NAME_LENGTH; i++) {
packed[DX7Voice.PACKED_NAME_START + i] = unpacked[DX7Voice.UNPACKED_NAME_START + i]
}
}
/**
* Create a default/empty voice
* @param {number} index - Voice index
* @returns {DX7Voice}
*/
static createDefault(index = 0) {
const unpacked = new Uint8Array(DX7Voice.UNPACKED_SIZE)
for (let op = 0; op < DX7Voice.NUM_OPERATORS; op++) {
const opOffset = op * DX7Voice.UNPACKED_OP_SIZE
// EG rates
unpacked[opOffset + DX7Voice.UNPACKED_OP_EG_RATE_1] = DX7Voice.DEFAULT_EG_RATE
unpacked[opOffset + DX7Voice.UNPACKED_OP_EG_RATE_2] = DX7Voice.DEFAULT_EG_RATE
unpacked[opOffset + DX7Voice.UNPACKED_OP_EG_RATE_3] = DX7Voice.DEFAULT_EG_RATE
unpacked[opOffset + DX7Voice.UNPACKED_OP_EG_RATE_4] = DX7Voice.DEFAULT_EG_RATE
// EG levels
unpacked[opOffset + DX7Voice.UNPACKED_OP_EG_LEVEL_1] = DX7Voice.DEFAULT_EG_LEVEL_MAX
unpacked[opOffset + DX7Voice.UNPACKED_OP_EG_LEVEL_2] = DX7Voice.DEFAULT_EG_LEVEL_MAX
unpacked[opOffset + DX7Voice.UNPACKED_OP_EG_LEVEL_3] = DX7Voice.DEFAULT_EG_LEVEL_MAX
unpacked[opOffset + DX7Voice.UNPACKED_OP_EG_LEVEL_4] = DX7Voice.DEFAULT_EG_LEVEL_MIN
// Break point, scaling, curves
unpacked[opOffset + DX7Voice.UNPACKED_OP_BREAK_POINT] = DX7Voice.DEFAULT_BREAK_POINT
unpacked[opOffset + DX7Voice.UNPACKED_OP_L_SCALE_DEPTH] = 0
unpacked[opOffset + DX7Voice.UNPACKED_OP_R_SCALE_DEPTH] = 0
unpacked[opOffset + DX7Voice.UNPACKED_OP_L_CURVE] = 0
unpacked[opOffset + DX7Voice.UNPACKED_OP_R_CURVE] = 0
// Rate scaling, detune, sensitivities
unpacked[opOffset + DX7Voice.UNPACKED_OP_RATE_SCALING] = 0
unpacked[opOffset + DX7Voice.UNPACKED_OP_DETUNE] = 7 // Detune center (actual detune = value - 7)
unpacked[opOffset + DX7Voice.UNPACKED_OP_AMP_MOD_SENS] = 0
unpacked[opOffset + DX7Voice.UNPACKED_OP_KEY_VEL_SENS] = 0
unpacked[opOffset + DX7Voice.UNPACKED_OP_OUTPUT_LEVEL] = DX7Voice.DEFAULT_OUTPUT_LEVEL
// Oscillator parameters
unpacked[opOffset + DX7Voice.UNPACKED_OP_MODE] = 0 // Ratio mode
unpacked[opOffset + DX7Voice.UNPACKED_OP_FREQ_COARSE] = 0
unpacked[opOffset + DX7Voice.UNPACKED_OP_OSC_DETUNE] = 0
unpacked[opOffset + DX7Voice.UNPACKED_OP_FREQ_FINE] = 0
}
// Pitch EG rates
unpacked[DX7Voice.UNPACKED_PITCH_EG_RATE_1] = DX7Voice.DEFAULT_EG_RATE
unpacked[DX7Voice.UNPACKED_PITCH_EG_RATE_2] = DX7Voice.DEFAULT_EG_RATE
unpacked[DX7Voice.UNPACKED_PITCH_EG_RATE_3] = DX7Voice.DEFAULT_EG_RATE
unpacked[DX7Voice.UNPACKED_PITCH_EG_RATE_4] = DX7Voice.DEFAULT_EG_RATE
// Pitch EG levels
unpacked[DX7Voice.UNPACKED_PITCH_EG_LEVEL_1] = DX7Voice.DEFAULT_PITCH_EG_LEVEL
unpacked[DX7Voice.UNPACKED_PITCH_EG_LEVEL_2] = DX7Voice.DEFAULT_PITCH_EG_LEVEL
unpacked[DX7Voice.UNPACKED_PITCH_EG_LEVEL_3] = DX7Voice.DEFAULT_PITCH_EG_LEVEL
unpacked[DX7Voice.UNPACKED_PITCH_EG_LEVEL_4] = DX7Voice.DEFAULT_PITCH_EG_LEVEL
// Global params
unpacked[DX7Voice.UNPACKED_ALGORITHM] = DX7Voice.DEFAULT_ALGORITHM
unpacked[DX7Voice.UNPACKED_FEEDBACK] = DX7Voice.DEFAULT_FEEDBACK
unpacked[DX7Voice.UNPACKED_OSC_SYNC] = 0
unpacked[DX7Voice.UNPACKED_LFO_SPEED] = DX7Voice.DEFAULT_LFO_SPEED
unpacked[DX7Voice.UNPACKED_LFO_DELAY] = 0
unpacked[DX7Voice.UNPACKED_LFO_PM_DEPTH] = 0
unpacked[DX7Voice.UNPACKED_LFO_AM_DEPTH] = 0
unpacked[DX7Voice.UNPACKED_LFO_KEY_SYNC] = 0
unpacked[DX7Voice.UNPACKED_LFO_WAVE] = 0
unpacked[DX7Voice.UNPACKED_LFO_PM_SENS] = DX7Voice.DEFAULT_LFO_PM_SENS
unpacked[DX7Voice.UNPACKED_AMP_MOD_SENS] = 0
unpacked[DX7Voice.UNPACKED_TRANSPOSE] = DX7Voice.TRANSPOSE_CENTER
unpacked[DX7Voice.UNPACKED_EG_BIAS_SENS] = 0
const name = "Init Voice"
for (let i = 0; i < DX7Voice.NAME_LENGTH; i++) {
unpacked[DX7Voice.UNPACKED_NAME_START + i] = i < name.length ? name.charCodeAt(i) : DX7Voice.CHAR_SPACE
}
const packed = DX7Voice.pack(unpacked)
return new DX7Voice(packed, index)
}
/**
* Create a voice from unpacked 169-byte data
* @param {Array<number>|Uint8Array} unpacked - 169 bytes of unpacked data (159 parameters + 10 name bytes)
* @param {number} index - Voice index
* @returns {DX7Voice}
*/
static fromUnpacked(unpacked, index = 0) {
const packed = DX7Voice.pack(unpacked)
return new DX7Voice(packed, index)
}
/**
* Load a DX7 voice from a single voice SYX file
* @param {File|Blob} file - SYX file (single voice in VCED format)
* @returns {Promise<DX7Voice>}
* @throws {DX7ParseError} If file has invalid VCED header
* @throws {Error} If file cannot be read (FileReader error)
*/
static async fromFile(file) {
return new Promise((resolve, reject) => {
const reader = new FileReader()
reader.onload = (e) => {
try {
const bytes = new Uint8Array(e.target.result)
if (
bytes[0] !== DX7Voice.VCED_SYSEX_START ||
bytes[1] !== DX7Voice.VCED_YAMAHA_ID ||
bytes[2] !== DX7Voice.VCED_SUB_STATUS ||
bytes[3] !== DX7Voice.VCED_FORMAT_SINGLE ||
bytes[4] !== DX7Voice.VCED_BYTE_COUNT_MSB ||
bytes[5] !== DX7Voice.VCED_BYTE_COUNT_LSB
) {
throw new DX7ParseError("Invalid VCED header", "header", 0)
}
// Extract the 155 bytes of voice data
const voiceData = bytes.subarray(
DX7Voice.VCED_HEADER_SIZE,
DX7Voice.VCED_HEADER_SIZE + DX7Voice.VCED_DATA_SIZE,
)
const checksum = bytes[DX7Voice.VCED_HEADER_SIZE + DX7Voice.VCED_DATA_SIZE]
const calculatedChecksum = DX7Bank._calculateChecksum(voiceData, DX7Voice.VCED_DATA_SIZE)
if (checksum !== calculatedChecksum) {
console.warn(
`DX7 VCED checksum mismatch (expected ${calculatedChecksum.toString(16)}, got ${checksum.toString(16)}). This is common with vintage SysEx files.`,
)
}
// Convert VCED data to unpacked format (169 bytes)
const unpacked = new Uint8Array(DX7Voice.UNPACKED_SIZE)
let offset = 0
// Operators: 6 × 21 bytes = 126 bytes
// VCED stores operators in reverse order: OP6, OP5, OP4, OP3, OP2, OP1
for (let op = 0; op < DX7Voice.NUM_OPERATORS; op++) {
const dst = (DX7Voice.NUM_OPERATORS - 1 - op) * DX7Voice.UNPACKED_OP_SIZE
// Copy operator parameters
unpacked[dst + DX7Voice.UNPACKED_OP_EG_RATE_1] = voiceData[offset++]
unpacked[dst + DX7Voice.UNPACKED_OP_EG_RATE_2] = voiceData[offset++]
unpacked[dst + DX7Voice.UNPACKED_OP_EG_RATE_3] = voiceData[offset++]
unpacked[dst + DX7Voice.UNPACKED_OP_EG_RATE_4] = voiceData[offset++]
unpacked[dst + DX7Voice.UNPACKED_OP_EG_LEVEL_1] = voiceData[offset++]
unpacked[dst + DX7Voice.UNPACKED_OP_EG_LEVEL_2] = voiceData[offset++]
unpacked[dst + DX7Voice.UNPACKED_OP_EG_LEVEL_3] = voiceData[offset++]
unpacked[dst + DX7Voice.UNPACKED_OP_EG_LEVEL_4] = voiceData[offset++]
unpacked[dst + DX7Voice.UNPACKED_OP_BREAK_POINT] = voiceData[offset++]
unpacked[dst + DX7Voice.UNPACKED_OP_L_SCALE_DEPTH] = voiceData[offset++]
unpacked[dst + DX7Voice.UNPACKED_OP_R_SCALE_DEPTH] = voiceData[offset++]
unpacked[dst + DX7Voice.UNPACKED_OP_L_CURVE] = voiceData[offset++]
unpacked[dst + DX7Voice.UNPACKED_OP_R_CURVE] = voiceData[offset++]
unpacked[dst + DX7Voice.UNPACKED_OP_RATE_SCALING] = voiceData[offset++]
unpacked[dst + DX7Voice.UNPACKED_OP_DETUNE] = voiceData[offset++]
// Amp mod sensitivity and key velocity sensitivity are packed in VCED
const modSens = voiceData[offset++]
unpacked[dst + DX7Voice.UNPACKED_OP_AMP_MOD_SENS] = modSens & DX7Voice.MASK_2BIT
unpacked[dst + DX7Voice.UNPACKED_OP_KEY_VEL_SENS] = (modSens >> 2) & DX7Voice.MASK_3BIT
unpacked[dst + DX7Voice.UNPACKED_OP_OUTPUT_LEVEL] = voiceData[offset++]
unpacked[dst + DX7Voice.UNPACKED_OP_MODE] = voiceData[offset++]
unpacked[dst + DX7Voice.UNPACKED_OP_FREQ_COARSE] = voiceData[offset++]
// FREQ_FINE and OSC_DETUNE order swapped from unpacked format
unpacked[dst + DX7Voice.UNPACKED_OP_FREQ_FINE] = voiceData[offset++]
unpacked[dst + DX7Voice.UNPACKED_OP_OSC_DETUNE] = voiceData[offset++]
}
// Pitch EG: 8 bytes
unpacked[DX7Voice.UNPACKED_PITCH_EG_RATE_1] = voiceData[offset++]
unpacked[DX7Voice.UNPACKED_PITCH_EG_RATE_2] = voiceData[offset++]
unpacked[DX7Voice.UNPACKED_PITCH_EG_RATE_3] = voiceData[offset++]
unpacked[DX7Voice.UNPACKED_PITCH_EG_RATE_4] = voiceData[offset++]
unpacked[DX7Voice.UNPACKED_PITCH_EG_LEVEL_1] = voiceData[offset++]
unpacked[DX7Voice.UNPACKED_PITCH_EG_LEVEL_2] = voiceData[offset++]
unpacked[DX7Voice.UNPACKED_PITCH_EG_LEVEL_3] = voiceData[offset++]
unpacked[DX7Voice.UNPACKED_PITCH_EG_LEVEL_4] = voiceData[offset++]
// Algorithm and global parameters: 11 bytes
unpacked[DX7Voice.UNPACKED_ALGORITHM] = voiceData[offset++]
unpacked[DX7Voice.UNPACKED_FEEDBACK] = voiceData[offset++]
unpacked[DX7Voice.UNPACKED_OSC_SYNC] = voiceData[offset++]
unpacked[DX7Voice.UNPACKED_LFO_SPEED] = voiceData[offset++]
unpacked[DX7Voice.UNPACKED_LFO_DELAY] = voiceData[offset++]
unpacked[DX7Voice.UNPACKED_LFO_PM_DEPTH] = voiceData[offset++]
unpacked[DX7Voice.UNPACKED_LFO_AM_DEPTH] = voiceData[offset++]
unpacked[DX7Voice.UNPACKED_LFO_KEY_SYNC] = voiceData[offset++]
unpacked[DX7Voice.UNPACKED_LFO_WAVE] = voiceData[offset++]
unpacked[DX7Voice.UNPACKED_LFO_PM_SENS] = voiceData[offset++]
unpacked[DX7Voice.UNPACKED_TRANSPOSE] = voiceData[offset++]
// Voice name: 10 bytes
for (let i = 0; i < DX7Voice.NAME_LENGTH; i++) {
unpacked[DX7Voice.UNPACKED_NAME_START + i] = voiceData[offset++]
}
// Pack to 128-byte format
const packed = DX7Voice.pack(unpacked)
resolve(new DX7Voice(packed, 0))
} catch (err) {
reject(err)
}
}
reader.onerror = () => reject(new Error("Failed to read file"))
reader.readAsArrayBuffer(file)
})
}
/**
* Create a DX7Voice from SysEx data
* @param {Array<number>|ArrayBuffer|Uint8Array} data - Voice data (128 bytes of packed voice data) or VCED SysEx data (163 bytes with header/footer)
* @param {number} index - Voice index (optional, defaults to 0)
* @returns {DX7Voice}
* @throws {DX7ParseError} If VCED header is invalid
* @throws {DX7ValidationError} If data length is invalid
*/
static fromSysEx(data, index = 0) {
const bytes = data instanceof Uint8Array ? data : new Uint8Array(data)
// Check if we have raw voice data or full VCED SysEx
let voiceData
// Remove VCED wrapper if present
if (bytes[0] === DX7Voice.VCED_SYSEX_START) {
// Validate VCED header
if (
bytes[0] !== DX7Voice.VCED_SYSEX_START ||
bytes[1] !== DX7Voice.VCED_YAMAHA_ID ||
bytes[2] !== DX7Voice.VCED_SUB_STATUS ||
bytes[3] !== DX7Voice.VCED_FORMAT_SINGLE ||
bytes[4] !== DX7Voice.VCED_BYTE_COUNT_MSB ||
bytes[5] !== DX7Voice.VCED_BYTE_COUNT_LSB
) {
throw new DX7ParseError("Invalid VCED header", "header", 0)
}
// Extract voice data (skip header and footer)
voiceData = bytes.subarray(DX7Voice.VCED_HEADER_SIZE, DX7Voice.VCED_HEADER_SIZE + DX7Voice.VCED_DATA_SIZE)
} else if (bytes.length === DX7Voice.PACKED_SIZE) {
// Raw voice data, no SysEx wrapper
voiceData = bytes
} else {
throw new DX7ValidationError(
`Invalid data length: expected ${DX7Voice.PACKED_SIZE} or ${DX7Voice.VCED_SIZE} bytes, got ${bytes.length}`,
"length",
bytes.length,
)
}
// Validate voice data length
if (voiceData.length !== DX7Voice.VCED_DATA_SIZE && voiceData.length !== DX7Voice.PACKED_SIZE) {
throw new DX7ValidationError(
`Invalid voice data length: expected ${DX7Voice.VCED_DATA_SIZE} or ${DX7Voice.PACKED_SIZE} bytes, got ${voiceData.length}`,
"length",
voiceData.length,
)
}
// If we have VCED data (155 bytes), convert it to packed format
if (voiceData.length === DX7Voice.VCED_DATA_SIZE) {
// Convert VCED to unpacked format first
const unpacked = new Uint8Array(DX7Voice.UNPACKED_SIZE)
let offset = 0
// Operators: 6 × 21 bytes = 126 bytes
// VCED stores operators in reverse order: OP6, OP5, OP4, OP3, OP2, OP1
for (let op = 0; op < DX7Voice.NUM_OPERATORS; op++) {
const dst = (DX7Voice.NUM_OPERATORS - 1 - op) * DX7Voice.UNPACKED_OP_SIZE
// Copy operator parameters
unpacked[dst + DX7Voice.UNPACKED_OP_EG_RATE_1] = voiceData[offset++]
unpacked[dst + DX7Voice.UNPACKED_OP_EG_RATE_2] = voiceData[offset++]
unpacked[dst + DX7Voice.UNPACKED_OP_EG_RATE_3] = voiceData[offset++]
unpacked[dst + DX7Voice.UNPACKED_OP_EG_RATE_4] = voiceData[offset++]
unpacked[dst + DX7Voice.UNPACKED_OP_EG_LEVEL_1] = voiceData[offset++]
unpacked[dst + DX7Voice.UNPACKED_OP_EG_LEVEL_2] = voiceData[offset++]
unpacked[dst + DX7Voice.UNPACKED_OP_EG_LEVEL_3] = voiceData[offset++]
unpacked[dst + DX7Voice.UNPACKED_OP_EG_LEVEL_4] = voiceData[offset++]
unpacked[dst + DX7Voice.UNPACKED_OP_BREAK_POINT] = voiceData[offset++]
unpacked[dst + DX7Voice.UNPACKED_OP_L_SCALE_DEPTH] = voiceData[offset++]
unpacked[dst + DX7Voice.UNPACKED_OP_R_SCALE_DEPTH] = voiceData[offset++]
unpacked[dst + DX7Voice.UNPACKED_OP_L_CURVE] = voiceData[offset++]
unpacked[dst + DX7Voice.UNPACKED_OP_R_CURVE] = voiceData[offset++]
unpacked[dst + DX7Voice.UNPACKED_OP_RATE_SCALING] = voiceData[offset++]
unpacked[dst + DX7Voice.UNPACKED_OP_DETUNE] = voiceData[offset++]
// Amp mod sensitivity and key velocity sensitivity are packed in VCED
const modSens = voiceData[offset++]
unpacked[dst + DX7Voice.UNPACKED_OP_AMP_MOD_SENS] = modSens & DX7Voice.MASK_2BIT
unpacked[dst + DX7Voice.UNPACKED_OP_KEY_VEL_SENS] = (modSens >> 2) & DX7Voice.MASK_3BIT
unpacked[dst + DX7Voice.UNPACKED_OP_OUTPUT_LEVEL] = voiceData[offset++]
unpacked[dst + DX7Voice.UNPACKED_OP_MODE] = voiceData[offset++]
unpacked[dst + DX7Voice.UNPACKED_OP_FREQ_COARSE] = voiceData[offset++]
// FREQ_FINE and OSC_DETUNE order swapped from unpacked format
unpacked[dst + DX7Voice.UNPACKED_OP_FREQ_FINE] = voiceData[offset++]
unpacked[dst + DX7Voice.UNPACKED_OP_OSC_DETUNE] = voiceData[offset++]
}
// Pitch EG: 8 bytes
unpacked[DX7Voice.UNPACKED_PITCH_EG_RATE_1] = voiceData[offset++]
unpacked[DX7Voice.UNPACKED_PITCH_EG_RATE_2] = voiceData[offset++]
unpacked[DX7Voice.UNPACKED_PITCH_EG_RATE_3] = voiceData[offset++]
unpacked[DX7Voice.UNPACKED_PITCH_EG_RATE_4] = voiceData[offset++]
unpacked[DX7Voice.UNPACKED_PITCH_EG_LEVEL_1] = voiceData[offset++]
unpacked[DX7Voice.UNPACKED_PITCH_EG_LEVEL_2] = voiceData[offset++]
unpacked[DX7Voice.UNPACKED_PITCH_EG_LEVEL_3] = voiceData[offset++]
unpacked[DX7Voice.UNPACKED_PITCH_EG_LEVEL_4] = voiceData[offset++]
// Algorithm and global parameters: 11 bytes
unpacked[DX7Voice.UNPACKED_ALGORITHM] = voiceData[offset++]
unpacked[DX7Voice.UNPACKED_FEEDBACK] = voiceData[offset++]
unpacked[DX7Voice.UNPACKED_OSC_SYNC] = voiceData[offset++]
unpacked[DX7Voice.UNPACKED_LFO_SPEED] = voiceData[offset++]
unpacked[DX7Voice.UNPACKED_LFO_DELAY] = voiceData[offset++]
unpacked[DX7Voice.UNPACKED_LFO_PM_DEPTH] = voiceData[offset++]
unpacked[DX7Voice.UNPACKED_LFO_AM_DEPTH] = voiceData[offset++]
unpacked[DX7Voice.UNPACKED_LFO_KEY_SYNC] = voiceData[offset++]
unpacked[DX7Voice.UNPACKED_LFO_WAVE] = voiceData[offset++]
unpacked[DX7Voice.UNPACKED_LFO_PM_SENS] = voiceData[offset++]
unpacked[DX7Voice.UNPACKED_TRANSPOSE] = voiceData[offset++]
// Voice name: 10 bytes
for (let i = 0; i < DX7Voice.NAME_LENGTH; i++) {
unpacked[DX7Voice.UNPACKED_NAME_START + i] = voiceData[offset++]
}
// Pack to 128-byte format
const packed = DX7Voice.pack(unpacked)
return new DX7Voice(packed, index)
}
// We already have packed data
return new DX7Voice(voiceData, index)
}
/**
* Create a DX7Voice from a JSON object
* @param {DX7VoiceJSON} json - JSON representation of a DX7 voice
* @param {number} index - Voice index (optional, defaults to 0)
* @returns {DX7Voice}
* @throws {DX7ValidationError} If JSON structure is invalid
*/
static fromJSON(json, index = 0) {
if (!json || typeof json !== "object") {
throw new DX7ValidationError("Invalid JSON: expected object", "json", json)
}
const unpacked = new Uint8Array(DX7Voice.UNPACKED_SIZE)
const setParam = (offset, value, name, min = 0, max = 127) => {
if (value === undefined || value === null) {
throw new DX7ValidationError(`Missing required parameter: ${name}`, name, value)
}
const numValue = Number(value)
if (Number.isNaN(numValue)) {
throw new DX7ValidationError(`Invalid parameter value for ${name}: ${value}`, name, value)
}
if (numValue < min || numValue > max) {
throw new DX7ValidationError(
`Parameter ${name} out of range: ${numValue} (must be ${min}-${max})`,
name,
numValue,
)
}
unpacked[offset] = Math.floor(numValue)
}
const getCurveValue = (curveName) => {
const curves = { "-LN": 0, "-EX": 1, "+EX": 2, "+LN": 3 }
return curves[curveName] !== undefined ? curves[curveName] : 0
}
const getWaveValue = (waveName) => {
const waves = {
TRIANGLE: 0,
"SAW DOWN": 1,
"SAW UP": 2,
SQUARE: 3,
SINE: 4,
"SAMPLE & HOLD": 5,
}
return waves[waveName] !== undefined ? waves[waveName] : 0
}
const parseNoteName = (noteName) => {
if (!noteName || typeof noteName !== "string") return 60 // Default to C3
const match = noteName.trim().match(/^([A-G]#?)(-?\d+)$/)
if (!match) return 60
const [, note, octaveStr] = match
const octave = parseInt(octaveStr, 10)
const noteMap = { C: 0, "C#": 1, D: 2, "D#": 3, E: 4, F: 5, "F#": 6, G: 7, "G#": 8, A: 9, "A#": 10, B: 11 }
const noteValue = noteMap[note.toUpperCase()]
if (noteValue === undefined) return 60
return (octave - DX7Voice.MIDI_OCTAVE_OFFSET) * 12 + noteValue
}
if (!Array.isArray(json.operators)) {
throw new DX7ValidationError("Invalid operators array: expected array", "operators", json.operators)
}
// Validate each operator's data structure first (before checking length)
// This ensures we get specific validation errors (e.g., "Invalid EG rates")
// rather than generic length errors when data is malformed
for (let op = 0; op < json.operators.length; op++) {
const opData = json.operators[op]
if (!opData || typeof opData !== "object") {
throw new DX7ValidationError(`Invalid operator data at index ${op}`, `operators[${op}]`, opData)
}
// EG rates (0-99)
if (!opData.eg || !Array.isArray(opData.eg.rates) || opData.eg.rates.length !== 4) {
throw new DX7ValidationError(
`Invalid EG rates for operator ${op}`,
`operators[${op}].eg.rates`,
opData.eg?.rates,
)
}
// EG levels (0-99)
if (!opData.eg || !Array.isArray(opData.eg.levels) || opData.eg.levels.length !== 4) {
throw new DX7ValidationError(
`Invalid EG levels for operator ${op}`,
`operators[${op}].eg.levels`,
opData.eg?.levels,
)
}
}
if (json.operators.length !== DX7Voice.NUM_OPERATORS) {
throw new DX7ValidationError(
`Invalid operators array: expected ${DX7Voice.NUM_OPERATORS} operators`,
"operators",
json.operators,
)
}
for (let op = 0; op < DX7Voice.NUM_OPERATORS; op++) {
const opData = json.operators[op]
const opOffset = op * DX7Voice.UNPACKED_OP_SIZE
// EG rates (0-99)
if (!opData.eg || !Array.isArray(opData.eg.rates) || opData.eg.rates.length !== 4) {
throw new DX7ValidationError(
`Invalid EG rates for operator ${op}`,
`operators[${op}].eg.rates`,
opData.eg?.rates,
)
}
setParam(opOffset + DX7Voice.UNPACKED_OP_EG_RATE_1, opData.eg.rates[0], `operators[${op}].eg.rates[0]`, 0, 99)
setParam(opOffset + DX7Voice.UNPACKED_OP_EG_RATE_2, opData.eg.rates[1], `operators[${op}].eg.rates[1]`, 0, 99)
setParam(opOffset + DX7Voice.UNPACKED_OP_EG_RATE_3, opData.eg.rates[2], `operators[${op}].eg.rates[2]`, 0, 99)
setParam(opOffset + DX7Voice.UNPACKED_OP_EG_RATE_4, opData.eg.rates[3], `operators[${op}].eg.rates[3]`, 0, 99)
// EG levels (0-99)
if (!opData.eg || !Array.isArray(opData.eg.levels) || opData.eg.levels.length !== 4) {
throw new DX7ValidationError(
`Invalid EG levels for operator ${op}`,
`operators[${op}].eg.levels`,
opData.eg?.levels,
)
}
setParam(opOffset + DX7Voice.UNPACKED_OP_EG_LEVEL_1, opData.eg.levels[0], `operators[${op}].eg.levels[0]`, 0, 99)
setParam(opOffset + DX7Voice.UNPACKED_OP_EG_LEVEL_2, opData.eg.levels[1], `operators[${op}].eg.levels[1]`, 0, 99)
setParam(opOffset + DX7Voice.UNPACKED_OP_EG_LEVEL_3, opData.eg.levels[2], `operators[${op}].eg.levels[2]`, 0, 99)
setParam(opOffset + DX7Voice.UNPACKED_OP_EG_LEVEL_4, opData.eg.levels[3], `operators[${op}].eg.levels[3]`, 0, 99)
// Break point (MIDI note number)
const breakPoint = parseNoteName(opData.key?.breakPoint) - DX7Voice.MIDI_BREAK_POINT_OFFSET
setParam(opOffset + DX7Voice.UNPACKED_OP_BREAK_POINT, breakPoint, `operators[${op}].key.breakPoint`, 0, 127)
// Scale depths (0-99)
setParam(
opOffset + DX7Voice.UNPACKED_OP_L_SCALE_DEPTH,
opData.scale?.left?.depth || 0,
`operators[${op}].scale.left.depth`,
0,
99,
)
setParam(
opOffset + DX7Voice.UNPACKED_OP_R_SCALE_DEPTH,
opData.scale?.right?.depth || 0,
`operators[${op}].scale.right.depth`,
0,
99,
)
// Curves
unpacked[opOffset + DX7Voice.UNPACKED_OP_L_CURVE] = getCurveValue(opData.scale?.left?.curve || "-LN")
unpacked[opOffset + DX7Voice.UNPACKED_OP_R_CURVE] = getCurveValue(opData.scale?.right?.curve || "-LN")
// Rate scaling (0-7)
setParam(
opOffset + DX7Voice.UNPACKED_OP_RATE_SCALING,
opData.key?.scaling || 0,
`operators[${op}].key.scaling`,
0,
7,
)
// Detune (-7 to +7, stored as 0-14)
const detune = Number(opData.osc?.detune) || 0
setParam(opOffset + DX7Voice.UNPACKED_OP_DETUNE, detune + 7, `operators[${op}].osc.detune`, 0, 14)
// Amp mod sensitivity (0-3)
setParam(
opOffset + DX7Voice.UNPACKED_OP_AMP_MOD_SENS,
opData.output?.ampModSens || 0,
`operators[${op}].output.ampModSens`,
0,
3,
)
// Output level (0-99)
setParam(
opOffset + DX7Voice.UNPACKED_OP_OUTPUT_LEVEL,
opData.output?.level || 0,
`operators[${op}].output.level`,
0,
99,
)
// Mode and frequency
const mode = opData.osc?.freq?.mode?.toUpperCase() === "FIXED" ? 1 : 0
const freqCoarse = Number(opData.osc?.freq?.coarse) || 0
const freqFine = Number(opData.osc?.freq?.fine) || 0
unpacked[opOffset + DX7Voice.UNPACKED_OP_MODE] = mode
setParam(opOffset + DX7Voice.UNPACKED_OP_FREQ_COARSE, freqCoarse, `operators[${op}].osc.freq.coarse`, 0, 31)
setParam(opOffset + DX7Voice.UNPACKED_OP_FREQ_FINE, freqFine, `operators[${op}].osc.freq.fine`, 0, 15)
// Key velocity sensitivity (0-7)
setParam(
opOffset + DX7Voice.UNPACKED_OP_KEY_VEL_SENS,
opData.key?.velocity || 0,
`operators[${op}].key.velocity`,
0,
7,
)
}
// Parse pitch EG
if (!json.pitchEG || !Array.isArray(json.pitchEG.rates) || json.pitchEG.rates.length !== 4) {
throw new DX7ValidationError("Invalid pitch EG rates", "pitchEG.rates", json.pitchEG?.rates)
}
if (!json.pitchEG || !Array.isArray(json.pitchEG.levels) || json.pitchEG.levels.length !== 4) {
throw new DX7ValidationError("Invalid pitch EG levels", "pitchEG.levels", json.pitchEG?.levels)
}
setParam(DX7Voice.UNPACKED_PITCH_EG_RATE_1, json.pitchEG.rates[0], "pitchEG.rates[0]", 0, 99)
setParam(DX7Voice.UNPACKED_PITCH_EG_RATE_2, json.pitchEG.rates[1], "pitchEG.rates[1]", 0, 99)
setParam(DX7Voice.UNPACKED_PITCH_EG_RATE_3, json.pitchEG.rates[2], "pitchEG.rates[2]", 0, 99)
setParam(DX7Voice.UNPACKED_PITCH_EG_RATE_4, json.pitchEG.rates[3], "pitchEG.rates[3]", 0, 99)
setParam(DX7Voice.UNPACKED_PITCH_EG_LEVEL_1, json.pitchEG.levels[0], "pitchEG.levels[0]", 0, 99)
setParam(DX7Voice.UNPACKED_PITCH_EG_LEVEL_2, json.pitchEG.levels[1], "pitchEG.levels[1]", 0, 99)
setParam(DX7Voice.UNPACKED_PITCH_EG_LEVEL_3, json.pitchEG.levels[2], "pitchEG.levels[2]", 0, 99)
setParam(DX7Voice.UNPACKED_PITCH_EG_LEVEL_4, json.pitchEG.levels[3], "pitchEG.levels[3]", 0, 99)
// Parse LFO
if (!json.lfo || typeof json.lfo !== "object") {
throw new DX7ValidationError("Invalid LFO data", "lfo", json.lfo)
}
setParam(DX7Voice.UNPACKED_LFO_SPEED, json.lfo.speed, "lfo.speed", 0, 99)
setParam(DX7Voice.UNPACKED_LFO_DELAY, json.lfo.delay, "lfo.delay", 0, 99)
setParam(DX7Voice.UNPACKED_LFO_PM_DEPTH, json.lfo.pmDepth, "lfo.pmDepth", 0, 99)
setParam(DX7Voice.UNPACKED_LFO_AM_DEPTH, json.lfo.amDepth, "lfo.amDepth", 0, 99)
unpacked[DX7Voice.UNPACKED_LFO_KEY_SYNC] = json.lfo.keySync ? 1 : 0
unpacked[DX7Voice.UNPACKED_LFO_WAVE] = getWaveValue(json.lfo.wave)
// Parse global parameters
if (!json.global || typeof json.global !== "object") {
throw new DX7ValidationError("Invalid global data", "global", json.global)
}
// Algorithm (0-31 in unpacked, 1-32 in JSON)
const algorithm = Number(json.global.algorithm) || 1
setParam(DX7Voice.UNPACKED_ALGORITHM, algorithm - 1, "global.algorithm", 0, 31)
// Feedback (0-7)
setParam(DX7Voice.UNPACKED_FEEDBACK, json.global.feedback, "global.feedback", 0, 7)
// OSC Sync
unpacked[DX7Voice.UNPACKED_OSC_SYNC] = json.global.oscKeySync ? 1 : 0
// Pitch Mod Sensitivity
setParam(DX7Voice.UNPACKED_LFO_PM_SENS, json.global.pitchModSens, "global.pitchModSens", 0, 7)
// Transpose (-24 to +24 in JSON, 0-127 in unpacked where 24 = C0)
const transpose = Number(json.global.transpose) || 0
setParam(DX7Voice.UNPACKED_TRANSPOSE, transpose + DX7Voice.TRANSPOSE_CENTER, "global.transpose", 0, 127)
// Amp Mod Sensitivity (optional, defaults to 0)
setParam(DX7Voice.UNPACKED_AMP_MOD_SENS, json.global.ampModSens || 0, "global.ampModSens", 0, 3)
// EG Bias Sensitivity (default to 0 if not provided)
setParam(DX7Voice.UNPACKED_EG_BIAS_SENS, json.global.egBiasSens || 0, "global.egBiasSens", 0, 7)
// Set voice name
const name = json.name || ""
for (let i = 0; i < DX7Voice.NAME_LENGTH; i++) {
unpacked[DX7Voice.UNPACKED_NAME_START + i] = i < name.length ? name.charCodeAt(i) : DX7Voice.CHAR_SPACE
}
return DX7Voice.fromUnpacked(unpacked, index)
}
/**
* Export voice to DX7 single voice SysEx format (VCED format)
* This is useful for synths that only support single voice dumps (e.g., KORG Volca FM)
* Converts from 169-byte unpacked format to 155-byte VCED format
* @returns {Uint8Array} Single voice SysEx data (163 bytes)
*/
toSysEx() {
const unpacked = this.unpack()
const result = new Uint8Array(DX7Voice.VCED_SIZE)
let offset = 0
// DX7 single voice dump header
result[offset++] = DX7Voice.VCED_SYSEX_START
result[offset++] = DX7Voice.VCED_YAMAHA_ID
result[offset++] = DX7Voice.VCED_SUB_STATUS
result[offset++] = DX7Voice.VCED_FORMAT_SINGLE
result[offset++] = DX7Voice.VCED_BYTE_COUNT_MSB
result[offset++] = DX7Voice.VCED_BYTE_COUNT_LSB
// Convert operators: 6 × 21 bytes = 126 bytes
// VCED expects operators in reverse order: OP6, OP5, OP4, OP3, OP2, OP1
for (let op = DX7Voice.NUM_OPERATORS - 1; op >= 0; op--) {
const src = op * DX7Voice.UNPACKED_OP_SIZE
// Copy 21 bytes per operator, skipping bytes 18 and 22 in our format
result[offset++] = unpacked[src + DX7Voice.UNPACKED_OP_EG_RATE_1]
result[offset++] = unpacked[src + DX7Voice.UNPACKED_OP_EG_RATE_2]
result[offset++] = unpacked[src + DX7Voice.UNPACKED_OP_EG_RATE_3]
result[offset++] = unpacked[src + DX7Voice.UNPACKED_OP_EG_RATE_4]
result[offset++] = unpacked[src + DX7Voice.UNPACKED_OP_EG_LEVEL_1]
result[offset++] = unpacked[src + DX7Voice.UNPACKED_OP_EG_LEVEL_2]
result[offset++] = unpacked[src + DX7Voice.UNPACKED_OP_EG_LEVEL_3]
result[offset++] = unpacked[src + DX7Voice.UNPACKED_OP_EG_LEVEL_4]
result[offset++] = unpacked[src + DX7Voice.UNPACKED_OP_BREAK_POINT]
result[offset++] = unpacked[src + DX7Voice.UNPACKED_OP_L_SCALE_DEPTH]
result[offset++] = unpacked[src + DX7Voice.UNPACKED_OP_R_SCALE_DEPTH]
result[offset++] = unpacked[src + DX7Voice.UNPACKED_OP_L_CURVE]
result[offset++] = unpacked[src + DX7Voice.UNPACKED_OP_R_CURVE]
result[offset++] = unpacked[src + DX7Voice.UNPACKED_OP_RATE_SCALING]
result[offset++] = unpacked[src + DX7Voice.UNPACKED_OP_DETUNE]
// Pack amp mod sensitivity and key velocity sensitivity for VCED
const ams = unpacked[src + DX7Voice.UNPACKED_OP_AMP_MOD_SENS] & DX7Voice.MASK_2BIT
const kvs = unpacked[src + DX7Voice.UNPACKED_OP_KEY_VEL_SENS] & DX7Voice.MASK_3BIT
result[offset++] = ams | (kvs << 2)
result[offset++] = unpacked[src + DX7Voice.UNPACKED_OP_OUTPUT_LEVEL]
result[offset++] = unpacked[src + DX7Voice.UNPACKED_OP_MODE]
result[offset++] = unpacked[src + DX7Voice.UNPACKED_OP_FREQ_COARSE]
// VCED has OSC_DETUNE before FREQ_FINE (opposite of unpacked format)
result[offset++] = unpacked[src + DX7Voice.UNPACKED_OP_OSC_DETUNE]
result[offset++] = unpacked[src + DX7Voice.UNPACKED_OP_FREQ_FINE]
}
// Pitch EG: 8 bytes (Rates 1-4, Levels 1-4)
result[offset++] = unpacked[DX7Voice.UNPACKED_PITCH_EG_RATE_1]
result[offset++] = unpacked[DX7Voice.UNPACKED_PITCH_EG_RATE_2]
result[offset++] = unpacked[DX7Voice.UNPACKED_PITCH_EG_RATE_3]
result[offset++] = unpacked[DX7Voice.UNPACKED_PITCH_EG_RATE_4]
result[offset++] = unpacked[DX7Voice.UNPACKED_PITCH_EG_LEVEL_1]
result[offset++] = unpacked[DX7Voice.UNPACKED_PITCH_EG_LEVEL_2]
result[offset++] = unpacked[DX7Voice.UNPACKED_PITCH_EG_LEVEL_3]
result[offset++] = unpacked[DX7Voice.UNPACKED_PITCH_EG_LEVEL_4]
// Algorithm and global parameters: 11 bytes
result[offset++] = unpacked[DX7Voice.UNPACKED_ALGORITHM]
result[offset++] = unpacked[DX7Voice.UNPACKED_FEEDBACK]
result[offset++] = unpacked[DX7Voice.UNPACKED_OSC_SYNC]
result[offset++] = unpacked[DX7Voice.UNPACKED_LFO_SPEED]
result[offset++] = unpacked[DX7Voice.UNPACKED_LFO_DELAY]
result[offset++] = unpacked[DX7Voice.UNPACKED_LFO_PM_DEPTH]
result[offset++] = unpacked[DX7Voice.UNPACKED_LFO_AM_DEPTH]
result[offset++] = unpacked[DX7Voice.UNPACKED_LFO_KEY_SYNC]
result[offset++] = unpacked[DX7Voice.UNPACKED_LFO_WAVE]
result[offset++] = unpacked[DX7Voice.UNPACKED_LFO_PM_SENS]
result[offset++] = unpacked[DX7Voice.UNPACKED_TRANSPOSE]
// Voice name: 10 bytes
for (let i = 0; i < DX7Voice.NAME_LENGTH; i++) {
result[offset++] = unpacked[DX7Voice.UNPACKED_NAME_START + i]
}
// Calculate checksum on 155 bytes of data
const dataForChecksum = result.subarray(
DX7Voice.VCED_HEADER_SIZE,
DX7Voice.VCED_HEADER_SIZE + DX7Voice.VCED_DATA_SIZE,
)
result[offset++] = DX7Bank._calculateChecksum(dataForChecksum, DX7Voice.VCED_DATA_SIZE)
result[offset++] = DX7Voice.VCED_SYSEX_END
return result
}
/**
* Convert voice to JSON format
* @returns {object} Voice data in JSON format
*/
toJSON() {
const unpacked = this.unpack()
const operators = []
const getKeyScaleCurve = (value) => {
const curves = ["-LN", "-EX", "+EX", "+LN"]
return curves[value] || "UNKNOWN"
}
const getLFOWave = (value) => {
const waves = ["TRIANGLE", "SAW DOWN", "SAW UP", "SQUARE", "SINE", "SAMPLE & HOLD"]
return waves[value] || "UNKNOWN"
}
const getNoteName = (midiNote) => {
const notes = ["C", "C#", "D", "D#", "E", "F", "F#", "G", "G#", "A", "A#", "B"]
const octave = Math.floor(midiNote / 12) + DX7Voice.MIDI_OCTAVE_OFFSET
const note = notes[midiNote % 12]
return `${note}${octave}`
}
for (let op = 0; op < DX7Voice.NUM_OPERATORS; op++) {
const opOffset = op * DX7Voice.UNPACKED_OP_SIZE
const mode = unpacked[opOffset + DX7Voice.UNPACKED_OP_MODE] === 0 ? "RATIO" : "FIXED"
operators.push({
id: op + 1,
osc: {
detune: unpacked[opOffset + DX7Voice.UNPACKED_OP_OSC_DETUNE],
freq: {
coarse: unpacked[opOffset + DX7Voice.UNPACKED_OP_FREQ_COARSE],
fine: unpacked[opOffset + DX7Voice.UNPACKED_OP_FREQ_FINE],
mode: mode,
},
},
eg: {
rates: [
unpacked[opOffset + DX7Voice.UNPACKED_OP_EG_RATE_1],
unpacked[opOffset + DX7Voice.UNPACKED_OP_EG_RATE_2],
unpacked[opOffset + DX7Voice.UNPACKED_OP_EG_RATE_3],
unpacked[opOffset + DX7Voice.UNPACKED_OP_EG_RATE_4],
],
levels: [
unpacked[opOffset + DX7Voice.UNPACKED_OP_EG_LEVEL_1],
unpacked[opOffset + DX7Voice.UNPACKED_OP_EG_LEVEL_2],
unpacked[opOffset + DX7Voice.UNPACKED_OP_EG_LEVEL_3],
unpacked[opOffset + DX7Voice.UNPACKED_OP_EG_LEVEL_4],
],
},
key: {
velocity: unpacked[opOffset + DX7Voice.UNPACKED_OP_KEY_VEL_SENS],
scaling: unpacked[opOffset + DX7Voice.UNPACKED_OP_RATE_SCALING],
breakPoint: getNoteName(
unpacked[opOffset + DX7Voice.UNPACKED_OP_BREAK_POINT] + DX7Voice.MIDI_BREAK_POINT_OFFSET,
),
},
output: {
level: unpacked[opOffset + DX7Voice.UNPACKED_OP_OUTPUT_LEVEL],
ampModSens: unpacked[opOffset + DX7Voice.UNPACKED_OP_AMP_MOD_SENS],
},
scale: {
left: {
depth: unpacked[opOffset + DX7Voice.UNPACKED_OP_L_SCALE_DEPTH],
curve: getKeyScaleCurve(unpacked[opOffset + DX7Voice.UNPACKED_OP_L_CURVE]),
},
right: {
depth: unpacked[opOffset + DX7Voice.UNPACKED_OP_R_SCALE_DEPTH],
curve: getKeyScaleCurve(unpacked[opOffset + DX7Voice.UNPACKED_OP_R_CURVE]),
},
},
})
}
return {
name: this.name || "(Empty)",
operators: operators,
pitchEG: {
rates: [
unpacked[DX7Voice.UNPACKED_PITCH_EG_RATE_1],
unpacked[DX7Voice.UNPACKED_PITCH_EG_RATE_2],
unpacked[DX7Voice.UNPACKED_PITCH_EG_RATE_3],
unpacked[DX7Voice.UNPACKED_PITCH_EG_RATE_4],
],
levels: [
unpacked[DX7Voice.UNPACKED_PITCH_EG_LEVEL_1],
unpacked[DX7Voice.UNPACKED_PITCH_EG_LEVEL_2],
unpacked[DX7Voice.UNPACKED_PITCH_EG_LEVEL_3],
unpacked[DX7Voice.UNPACKED_PITCH_EG_LEVEL_4],
],
},
lfo: {
speed: unpacked[DX7Voice.UNPACKED_LFO_SPEED],
delay: unpacked[DX7Voice.UNPACKED_LFO_DELAY],
pmDepth: unpacked[DX7Voice.UNPACKED_LFO_PM_DEPTH],
amDepth: unpacked[DX7Voice.UNPACKED_LFO_AM_DEPTH],
keySync: unpacked[DX7Voice.UNPACKED_LFO_KEY_SYNC] === 1,
wave: getLFOWave(unpacked[DX7Voice.UNPACKED_LFO_WAVE]),
},
global: {
algorithm: unpacked[DX7Voice.UNPACKED_ALGORITHM] + 1,
feedback: unpacked[DX7Voice.UNPACKED_FEEDBACK],
oscKeySync: unpacked[DX7Voice.UNPACKED_OSC_SYNC] === 1,
pitchModSens: unpacked[DX7Voice.UNPACKED_LFO_PM_SENS],
transpose: unpacked[DX7Voice.UNPACKED_TRANSPOSE] - DX7Voice.TRANSPOSE_CENTER,
},
}
}
}