Emulating the YM2612: Part 7 - SSG-EG

This is the seventh and final post in a series on emulating the main Sega Genesis sound chip, the YM2612.

This post will cover the envelope generator’s SSG-EG mode.

Overview

The previous post on the envelope generators has a section that briefly explains what SSG-EG is.

As mentioned in that post, the YM2612 does not actually have a copy of the YM2149 SSG’s envelope generator hardware. Instead, SSG-EG emulates the SSG’s envelope shapes by modifying behavior of the FM synthesis envelope generators.

The SSG-EG hardware was seemingly designed to support the OPN’s SSG sound source, which was removed in the YM2612, but since SSG-EG is implemented using the FM envelope generators it can also be used with normal FM synthesis operation.

While the YM2612’s SSG-EG functionality was officially undocumented, a few games do use it, unlike CSM. SSG-EG is mentioned in Sega’s official Genesis documentation, but there’s no description of what it does - only a short blurb to the effect of “don’t write non-zero values to this register”.

SSG-EG is controlled entirely using four bits in registers $90-$9F:

  • Bit 3: SSG-EG enabled
  • Bit 2: Attack
  • Bit 1: Alternate
  • Bit 0: Hold

The lowest three bits have no effect unless bit 3 is set. Bit 3 enables SSG-EG mode for the operator, and the lowest three bits together select 1 of 8 envelope shapes.

SSG-EG EnvelopesSSG-EG envelope shapes, from YM2608 manual

Note that the Y-axis in the manual here is volume, not attenuation. Higher Y value = higher volume / lower attenuation.

SSG-EG mode causes envelope attenuation to oscillate between 0 and 0x200 / 512 (roughly 48 dB), at least when used as intended. Attenuation does not go all the way up to 0x3FF / 1023 except in specific cases where the envelope is holding at max attenuation (envelope numbers 1 and 7 above).

The YM2608 manual states that SSG-EG should only be used with attack rate set to 0x1F / 31 (max). This is because SSG-EG is designed around always skipping Attack phase at key on and only using the envelope generator’s Decay/Sustain/Release phases. Of course, the chip still does something if SSG-EG is used with lower attack rates, and a handful of games actually depend on emulating this correctly.

Very few commercial releases use SSG-EG, but there are some, including a few that exercise the quirkier parts of how it works.

How It’s Supposed to Work

Before describing how the chip actually implements SSG-EG, I think it’s helpful to describe how it works when it’s used as intended: attack rate is set to 31 so that Attack phase is always skipped, and you don’t modify any envelope generator parameters while the operator is keyed on.

Baseline

First, SSG-EG behavior when SSG-EG is enabled but the attack/alternate/hold bits are all clear:

SSG-EG BaselineSSG-EG attack/alternate/hold all clear

At key on, attenuation immediately drops to 0 because attack rate is 31, and thus (2R + Rks) is guaranteed to be 62 or 63. The envelope generator then immediately enters Decay phase and begins increasing attenuation, though at a faster rate than it would if SSG-EG was disabled. If attenuation crosses the sustain level then the envelope generator will automatically switch to Sustain phase, just as it would normally.

When attenuation reaches 0x200 / 512, something unusual happens: attenuation immediately drops back down to 0 and the envelope enters Decay phase again. With this particular envelope shape, SSG-EG also resets the phase generator’s 20-bit phase counter to 0 every time this happens, though it doesn’t do this for every envelope shape - it only does it here because the alternate and hold bits are both clear.

(What actually happens is that reaching attenuation 0x200 causes the chip to re-enter Attack phase, but since attack rate is 31, attenuation immediately drops to 0 and Attack phase gets skipped.)

Hold

The hold bit causes SSG-EG to hold at max attenuation instead of repeatedly dropping back down to 0 every time it reaches 0x200.

It does work a little differently than you might expect in that as soon as attenuation reaches 0x200, at the next sample it jumps up to max (0x3FF) and stays there.

SSG-EG HoldSSG-EG hold set, attack/alternate clear

If the hold bit is set, SSG-EG does not reset the phase generator’s internal counter once it reaches attenuation 0x200.

Alternate and Output Inversion

If the alternate bit is set, SSG-EG inverts its output every time it reaches attenuation 0x200. It accomplishes this using an internal output inversion flag that it toggles whenever internal attenuation is 0x200.

“Inverting its output” means that instead of the envelope generator always outputting its current attenuation A directly, it outputs (0x200 - A) while the output inversion flag is set.

When the hold bit is clear, this causes the envelope shape to be a triangle wave instead of a sawtooth wave. SSG-EG AlternateSSG-EG alternate set, attack/hold clear

Internally attenuation still repeatedly drops to 0 and then increases to 0x200, but output inversion causes the final envelope generator output to follow the above graph.

When the hold bit is set alongside the alternate bit, the output inversion takes effect before the hold. This causes the envelope generator’s output to hold at 0 instead of max attenuation.

The internal attenuation does not jump from 0x200 to 0x3FF at the hold - the chip only does this when output is not inverted. SSG-EG Alternate and HoldSSG-EG alternate/hold set, attack clear

Similar to the hold bit, setting the alternate bit causes SSG-EG to not reset the phase generator’s counter to 0 at internal attenuation 0x200. It only does this when both the alternate and hold bits are clear.

Attack

The attack bit simply applies an additional inversion to the output at all times when set. It’s functionally an XOR on the internal output inversion flag.

This causes the envelope generator’s output to begin at 0x200 (inverted 0) and gradually decrease to 0 (inverted 0x200).

If the attack bit is set and the alternate bit is clear, output is always inverted. SSG-EG AttackSSG-EG attack set, alternate/hold clear SSG-EG Attack and HoldSSG-EG attack/hold set, alternate clear

Note that when attack and hold are both set, internal attenuation does not jump to 0x3FF after it reaches 0x200, because output is inverted.

When the attack and alternate bits are both set, output inversion is always in the opposite state that it would be in if alternate was set and attack was clear. SSG-EG Attack and AlternateSSG-EG attack/alternate set, hold clear SSG-EG All SetSSG-EG attack/alternate/hold all set

When attack, alternate, and hold are all set, attenuation does jump up to max when the hold begins because output is not inverted.

Alright, now to describe how the chip actually implements SSG-EG to produce these 8 envelope shapes.

How It Works

Output Inversion

First, suppose there are 5 new bool fields, 4 for the SSG-EG control bits and 1 for the current output inversion state:

1
2
3
4
5
6
7
8
struct EnvelopeGenerator {
    ssg_enabled: bool,   // $90-$9F bit 3
    ssg_attack: bool,    // $90-$9F bit 2
    ssg_alternate: bool, // $90-$9F bit 1
    ssg_hold: bool,      // $90-$9F bit 0
    ssg_invert: bool,    // Current output inversion state
    ...
}

Output inversion is applied in two places: when the envelope generator’s output is read, and at key off. In both cases, inversion is applied based on the XOR of the attack bit and the inversion state bit.

When (ssg_attack ^ ssg_invert) is 1, inversion is applied like this:

1
(0x200 - level) & 0x3FF

This doesn’t work quite as intended if attenuation ever ends up higher than 0x200. This can happen when using attack rates lower than 31, or if software changes the decay/sustain rate after keying on but before attenuation reaches 0x200. However, performing the calculation this way is accurate to what actual hardware does.

SSG-EG Output Inversion

When the envelope generator’s output is read, SSG-EG applies output inversion whenever SSG-EG is enabled and the operator is not keyed off (i.e. the operator is not in Release phase):

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
impl EnvelopeGenerator {
    fn current_level(&self) -> u16 {
        // Only apply output inversion if SSG-EG is enabled and the operator is not keyed off
        let mut level = if self.ssg_enabled
            && self.phase != AdsrPhase::Release
            && (self.ssg_attack ^ self.ssg_invert)
        {
            // Invert output
            0x200_u16.wrapping_sub(self.level) & 0x3FF
        } else {
            // No inversion
            self.level
        };
        
        // Then apply total level and tremolo
    }
}

Keying off inverts the stored attenuation value in-place if output inversion is active, which is why output inversion on reads doesn’t apply to keyed-off operators:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
impl EnvelopeGenerator {
    fn set_key_on(&mut self, key_on: bool) {
        if self.key_on && !key_on {
            // Key on/off state changed to off; check for SSG-EG output inversion
            if self.ssg_enabled && (self.ssg_attack ^ self.ssg_invert) {
                // Invert stored attenuation in-place
                self.level = 0x200_u16.wrapping_sub(self.level) & 0x3FF;
            }
        }

        // Then other key on/off logic
    }
}

Keying off also holds the internal output inversion flag at 0 for as long as the operator is keyed off. Since this flag does nothing while an operator is keyed off, you can emulate this by clearing the flag at key on:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
impl EnvelopeGenerator {
    fn set_key_on(&mut self, key_on: bool) {
        if self.key_on && !key_on {
            ...
        } else if !self.key_on && key_on {
            // Key on/off state changed to on; clear output inversion state
            self.ssg_invert = false;
        }

        // Then other key on/off logic
    }
}

You could also emulate this by setting ssg_invert to false at key off and then only ever updating it while the operator is keyed on.

Envelope Update Logic

The normal envelope update logic works mostly the same way in SSG-EG mode. There’s only one big change: the way that envelope increment is applied in Decay, Sustain, and Release phases.

The normal logic is this:

A' = min(A + I, 0x3FF)

When SSG-EG is enabled, the logic changes to this:

if A < 0x200:
    A' = A + 4*I
else:
    A' = A

Attenuation increases at 4x the rate that it does normally, but only if it’s currently less than SSG-EG’s 0x200 magic number. If attenuation is at or above 0x200, it does not increase at all. This is important to ensure that the envelope generator’s output holds at 0 when SSG-EG is holding with inverted output, which happens when the SSG-EG hold bit is set and either the alternate bit or attack bit is set (but not both).

SSG-EG is meant to always skip Attack phase, but if it’s not skipped, it works exactly the same way as when SSG-EG is off: exponentially decreasing attenuation until it reaches 0, which triggers the transition to Decay phase.

There are many unintended behaviors possible when SSG-EG is combined with Attack phase, but these aren’t caused by any changes to how Attack phase itself works - they’re caused by how the additional SSG-EG logic interacts with Attack phase.

SSG-EG Logic

When SSG-EG is enabled, there is some additional logic that runs once per sample, 3x as frequently as the normal envelope update logic. This means that once attenuation hits 0x200, SSG-EG state changes take effect after only a single sample is generated with internal attenuation at 0x200. SSG-EG doesn’t wait for the next envelope update cycle.

On the sample cycles where SSG-EG logic and envelope updates both run, the SSG-EG logic runs before the normal envelope update logic, which is why a single sample is generated with attenuation 0x200 before SSG-EG state changes take effect.

All of this logic only runs if the envelope generator’s current internal attenuation is at or above 0x200.

These are the main pieces to the logic:

  1. If the alternate bit is set, toggle output inversion state
    • If the hold bit is also set, output inversion state is always set to 1 instead of toggled
  2. If the alternate and hold bits are both clear, reset the phase generator’s internal counter to 0
  3. If the operator is keyed on and the hold bit is clear, trigger a virtual key on event
  4. If not in Attack phase and the hold bit is set and output is not inverted, increase attenuation to max (0x3FF)
  5. If the operator is keyed off, increase attenuation to max (0x3FF)

It is important that the alternate bit logic (1) is applied before the hold max attenuation check (4), but otherwise the order of these does not matter.

The “virtual key on” event (3) is what causes attenuation to immediately drop from 0x200 to 0 when attack rate is set to 31. This virtual key on performs the same transition to Attack phase that the envelope generator performs when the key on/off state changes from keyed off to keyed on. This includes immediately dropping attenuation to 0 if (2R + Rks) is 62 or 63, which it will always be if attack rate is 31.

In terms of code, this function might look something like this:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
impl EnvelopeGenerator {
    fn ssg_update(&mut self, phase: &mut PhaseGenerator) {
        if !self.ssg_enabled || self.level < 0x200 {
            // Do nothing; none of this logic applies until attenuation reaches 0x200 while SSG-EG is on
            return;
        }

        // Apply alternate bit
        if self.ssg_alternate {
            // If hold bit is clear, toggle output inversion state
            // If hold bit is set, always set output inversion state to 1
            self.ssg_invert = !self.ssg_invert || self.ssg_hold;
        }

        // Reset phase generator counter if alternate and hold bits are both clear
        if !self.ssg_alternate && !self.ssg_hold {
            phase.counter = 0;
        }

        // Generate a virtual key on if operator is keyed on and not holding
        if !self.ssg_hold && self.phase != AdsrPhase::Release {
            // Exact same logic as when key state changes from keyed off to keyed on
            self.phase = AdsrPhase::Attack;

            let rks = phase.key_code() >> (3 - self.key_scale_level);
            if 2 * self.attack_rate + rks >= 62 {
                self.level = 0;
            }
        }

        // Check whether to increase attenuation to max
        // Case 1: Holding in a non-Attack phase and output is not inverted
        if self.ssg_hold
            && self.phase != AdsrPhase::Attack
            && !(self.ssg_attack ^ self.ssg_invert)
        {
            self.level = 0x3FF;
        }

        // Case 2: Operator is keyed off
        if self.phase == AdsrPhase::Release {
            self.level = 0x3FF;
        }
    }
}

And that is all you need for a complete SSG-EG implementation. There are a bunch of different ways you could structure this logic, but I think splitting everything out like this makes it more obvious what is happening.

There are a lot of possible weird behaviors if SSG-EG flags are modified while an operator is keyed on, but this should handle those cases roughly accurately, and regardless games don’t depend on handling these weird behaviors correctly as far as I know.

However, there are games that depend on correctly handling SSG-EG with Attack phase.

Attack Phase Weirdness

SSG-EG was designed around always skipping Attack phase, but what happens when it’s not skipped?

It turns out that not skipping Attack phase breaks a lot of assumptions made in SSG-EG’s implementation. It’s a wonder why the chip designers didn’t special case SSG-EG to always drop attenuation to 0 at key on regardless of attack rate.

The alternate bit implementation is the most egregious example. When alternate is set and the hold bit is clear, output inversion state toggles every sample while attenuation is at or above 0x200. If an operator is at max attenuation at key on and attack rate is somewhat low, this causes chaos during the initial Attack phase while internal attenuation is in the 0x200-0x3FF range: SSG-EG Alternate with Low Attack RateSSG-EG alternate, initial Attack phase

Even after the initial Attack, there is weirdness every time internal attenuation reaches 0x200. When attack rate is low, the envelope generator’s internal attenuation could stay at 0x200 for a decent number of samples before Attack phase decreases it below 0x200, and during this time the envelope generator’s output will repeatedly alternate between 0 and 0x200 due to output inversion.

Setting both the alternate and hold bits also causes weird behavior, though not to the same degree. Since output is inverted the entire time, during the initial Attack, envelope generator output gradually climbs from 0x201 to 0x3FF before dropping to 0: SSG-EG Alternate + Hold with Attack PhaseSSG-EG alternate/hold set with Attack phase

There’s another odd behavior when the alternate and hold bits are both clear, caused by SSG-EG holding the phase generator counter at 0 whenever internal attenuation is at least 0x200: The operator is effectively muted during the initial Attack phase until attenuation drops below 0x200. Additionally, every time attenuation increases to 0x200 as part of an SSG-EG repetition, the operator is muted until Attack phase drops attenuation below 0x200, which can take a while if attack rate is low.

This is not a complete description of every weird behavior that can happen when SSG-EG is used with a non-max attack rate, but suffice it to say, this is not an intended usage of the chip.

The fun part is that SSG-EG with Attack phase isn’t just an edge case that nothing depends on - there are games that trigger this behavior!

Examples

Alisia Dragoon is probably the most well-known game that uses SSG-EG, at least that I know of. However, as far as I can tell it only uses SSG-EG for specific percussion instruments, and I find the difference practically unnoticeable compared to not emulating SSG-EG at all.

Here’s a recording of part of the Stage 1 music that has only the channel that’s using SSG-EG, with ladder effect emulation disabled so the sound is clearer:

No SSG-EG emulation
With SSG-EG emulation

Out of the games that I’ve seen mentioned as using SSG-EG, Ghostbusters is one where SSG-EG emulation makes a huge difference. Not emulating SSG-EG makes some of its instruments sound completely wrong, for example:

No SSG-EG emulation
With SSG-EG emulation

In this song it has SSG-EG enabled for Channel 6’s operators 1, 3, and 4 while using algorithm 2. Operator 1 has only the SSG-EG attack bit set (inverted sawtooth envelope), the other two have the attack/alternate/hold bits all clear (normal sawtooth envelope). All three of these operators have attack rate set to max, using SSG-EG as intended.

Olympic Gold and Beavis & Butthead have several sound effects that use SSG-EG with attack rates other than 31, so Attack phase does not get skipped like it’s supposed to. Beavis & Butthead only ever seems to use SSG-EG with attack/alternate/hold all clear, but Olympic Gold uses multiple SSG-EG envelope shapes, including the chaotic envelope created by setting only the SSG-EG alternate bit and using a very low attack rate.

Here’s an example sound effect from Olympic Gold’s diving game which was apparently not emulated completely correctly in any Genesis emulator until the late 2000s or so, when Nemesis reverse engineered how SSG-EG works in great detail.

First, with no SSG-EG emulation at all:

No SSG-EG emulation

Next, with SSG-EG emulation, but ignoring the attack rate and always skipping Attack phase:

With SSG-EG emulation, skipping Attack phase

Finally, with proper SSG-EG emulation including Attack phase:

With SSG-EG emulation, with Attack phase

I don’t believe that any of these is exactly what it sounded like in earlier emulators, but this does demonstrate that the diving board and splash sound effects both depend on correctly emulating SSG-EG with Attack phase.

The splash sound effect is produced using a combination of channels 5 and 6.

Channel 5 uses algorithm 2 with SSG-EG enabled for all four operators, with the following SSG-EG settings:

  • Operator 1: Alternate set, attack/hold clear, attack rate 1 (chaos envelope)
  • Operator 2: Hold set, attack/alternate clear, attack rate 14
  • Operator 3: Hold set, attack/alternate clear, attack rate 21
  • Operator 4: Attack/alternate/hold all set, attack rate 28

Channel 6 uses algorithm 4, with SSG-EG enabled only for Operator 2. It has the SSG-EG attack/alternate/hold bits all set and attack rate set to 20.

The End

With that, I believe I’ve covered all parts of the hardware except for the undocumented test registers, which nothing uses and are not well-supported in most emulators aside from Nuked-OPN2 (which of course emulates all of them).

So, that’s the end of this series!

updatedupdated2025-04-142025-04-14