From Sound Chip to Beep

Control the Sega Genesis / Mega Drive YM2612 with JavaScript, one register at a time.

This is the browser companion to the chip basics and C++ beep tutorial. We will use one channel and one audible operator. You can edit and run the complete example below in Playground.

1. What does the chip do?

The YM2612 makes sound from settings written by a program. It has six FM channels, each with four operators. An operator produces a sine-based signal and controls its loudness over time with an envelope. Operators can become audible output or modulate other operators.

For our first beep, we only need a steady tone: one audible operator, no modulation, and no feedback.

JavaScript → register settings → YM2612 emulator → audio → speakers

On the original hardware, the CPU sends bytes through the chip's data bus and a clock drives the chip. Here, Playground runs the emulator and sends its generated samples to browser audio. We only need to supply the settings and the timing.

2. A register is a small setting

A register has an address and holds a byte. The address chooses what to change; the byte supplies the setting. JavaScript's 0x prefix means hexadecimal: 0x07 is 7 and 0x7f is 127.

write(0xb0, 0x07); // CH1: algorithm 7, feedback 0

This write(address, value) helper is provided by Playground. It combines the two bus writes used in the C++ tutorial: select a register, then send its value.

// C++ / chip bus interface:
chip.write(0, 0xb0); // Select a register in bank 0.
chip.write(1, 0x07); // Send its value.

// JavaScript / Playground:
write(0xb0, 0x07);

Most channel registers are divided into two banks: bank 0 for CH1–CH3 and bank 1 for CH4–CH6. Playground calls this bank argument port. You can make it explicit with write(0, 0xb0, 0x07). The two-argument form defaults to bank 0.

The C++ bus offsets are different: 0 and 1 select/write bank 0; 2 and 3 select/write bank 1. For this beep, all writes use bank 0.

3. Choose a simple voice

Algorithm 7 connects all four operators directly to the output, in parallel. We make OP1–OP3 very quiet and leave OP4 audible.

ALG 7: OP1 + OP2 + OP3 + OP4 → OUT
OP1–OP3 quiet · OP4 audible · FB off

The four hardware slot offsets are 0, 4, 8, 12. Adding an offset to a register group's base address selects a slot. The last offset, 12 (0x0c), selects OP4; for example, its total level is at 0x4c.

These are hardware address offsets, not Playground's OP1–OP4 constants. The two middle slots have a different logical order; they receive identical settings here.

CH1 register groupSettingOur beep
0x30 + offsetDT / MULTI: detune and frequency multiplier0x01: no detune, ×1
0x40 + offsetTL: attenuation0x7f for quiet slots; 0x00 for OP4. Higher means quieter.
0x50 + offsetAR: attack rate0x1f: fastest attack, rate scaling off
0x80 + offsetSL / RR: sustain level threshold and release rate0x0f: SL 0, fastest release
0xb0Algorithm / feedback0x07: algorithm 7, feedback off
0xb4Stereo output / LFO sensitivity0xc0: left and right enabled, sensitivity 0

We begin with fm.reset(). This leaves the two decay rates, D1R and D2R, at zero, so the tone stays steady until key off. Release then makes it fade quickly.

4. Set the pitch, then press the key

The chip stores pitch as a range called BLOCK and an 11-bit number called FNUM. The C++ beep uses BLOCK 4 and FNUM 0x269. They occupy two registers:

write(0xa4, 0x22); // High: (BLOCK 4 << 3) | upper FNUM bits 2
write(0xa0, 0x69); // Low: remaining eight FNUM bits

Write the high part first, then the low part. The low write commits the combined pitch. These values reproduce the tutorial's pitch; they are not a frequency in Hz.

Finally, register 0x28 controls key on and key off. In 0xf0, the upper four bits turn on all four operators and the lower bits select CH1. Key on starts the envelopes; it does not choose the voice or pitch.

write(0x28, 0xf0); // CH1: key on
await sleep(3);    // Let it sound for three seconds.
write(0x28, 0x00); // CH1: key off

5. Play the beep

Press Run in Playground. This is the complete JavaScript version of the register setup in ex03_beep.cpp, followed by a timed key off. Playground provides fm, write, and sleep.

Open in full Playground →

Change one thing and listen

6. From register bytes to musical controls

Once the register version makes sense, Playground's named methods let you express the same channel settings more directly:

fm.setAlgo(CH1, 7, 0);          // write(0xb0, 0x07)
fm.setPan(CH1, true, true);    // write(0xb4, 0xc0)
fm.setFrequency(CH1, 4, 0x269); // writes 0xa4 and 0xa0
fm.keyOn(CH1);                // write(0x28, 0xf0)
await sleep(3);
fm.keyOff(CH1);               // write(0x28, 0x00)

This fragment replaces the channel and playback lines; it still needs the operator setup above. The sound comes from the same sequence: configure the voice, set the pitch, key on, wait, key off.

Next, explore operators and FM sound design, or use one tone to explore tuning and musical intervals.