I just wrote an article about the SC600, including a tour of SSINIT. I presume subchannel 25 corresponds to the “Mixer/Effects” radio buttons, which list as options:
None
Pseudo Stereo (Mono Source)
Spatial (Stereo Source)
EDIT: there is very few info about the spatial audio feature. This post provides some basics.
Last edited by Bodo on 2026-09-03, 17:34. Edited 1 time in total.
Very interesting. I have one of these cards and i've been using the default S2000 software just fine, but now i saw the GUI and there's options i've never seen before. I better start using the original Reveal software.
Bodowrote on 2026-09-02, 21:13:I just wrote an article about the SC600, including a tour of SSINIT. I presume subchannel 25 corresponds to the “Mixer/Effects” […] Show full quote
I just wrote an article about the SC600, including a tour of SSINIT. I presume subchannel 25 corresponds to the “Mixer/Effects” radio buttons, which list as options:
None
Pseudo Stereo (Mono Source)
Spatial (Stereo Source)
Indeed, it is exactly that. SSINIT encodes it as "0 = None, 1 = Pseudo, 2 = Spatial", and sends it as is to the firmware. The firmware handles that value in the function at address 0x4f50, which dispatches to 0x4F5C for 0 (no effect), to 0x4F62 for 1 (pseudo stereo) and to 0x4F68 for 2 (spatial stereo). Those labels chose the value to write into register 8 ("switch functions") of the TDA8421 chip. The muting bit is taken from mixer "channel" 26, while bits 0..4 are 0x0E in mode 0 ("none"), 0x1E in mode 1 ("pseudo") and 0x016 in mode 2 ("spatial"). The affected bits thus are 3 and 4, which are called "STL" and "EFL" in the TDA8421 datasheet. Mode 0 translates to STL=1, EFL=0, which is called "stereo" according to the data sheet. Mode 1 translates to STL=1, EFL=1, which is called "spatial" according to the data sheet, and finally Mode 2 translates to STL=0, EFL=1, which is called "pseudo" according to the data sheet. So my post you cite contains a mistake, mapping 2 to "reserved configuration" (which would be STL=0, EFL=0), but it seems there is a firmware bug or SSINIT bug in the firmware dated "Mon Jan 24 1994 17:56", because it confuses modes 1 and 2.
The basic idea is that "spatial (stereo source)" is meant to "make the stereo stronger" so the sound is more vivid, while "pseudo (mono source)" is meant to fake stereo from a mono source by applying different filters to the left and right input (which should be identical for a mono source). The datasheet shows the filters in the block diagram: "pseudo stereo" adds some phase shift to the left channel, but not to the right channel, so the signals are no longer identical, and thus sound "more stereo". Spatial shows a mixing matrix in which a part of the left channel goes into the right channel and a part of the right channel goes into the left channel. As there are are capacitors connected to the "spatial" element, it is very likely that some kind of filtering is applied to the "foreign" channels before they get mixed into the respective output. As the goal of that section is to enhance stereo, you typically subtract a the foreign signal to increase the stereo difference.
Applying "spatial stereo" to a mono source is supposed to have no effect (if it has some effect, it likely means that the components in the spatializer do not exactly match, which can easily happen if the capacitors at pin 6 and 23, which have a suggested value of 3.9nF, have 20% tolerance. On the other hand, applying "pseudo stereo" to a mono source will transform the blunt dry mono sound into something spacey, hard to localize stereo sound. If you have the hardware at hand, you can test whether the modes are actually swapped (as my reverse engineering suggestes), or whether either the reverse engineering efforts or the data sheet is wrong.
Interesting article. Yet I am not convinced that your theory that the SC600 and the S-2000 were independent parallel developments is true. Especially as the inital S-2000 firmware is extremely similar to the SC600 firmware, I still consider my theory that Packard Bell/Reveal had that card designed first and then Ensoniq built on the Packard Bell design. Whoever made the SC600 firmware (either Packard Bell or some team at Ensoniq cooperating with Packard Bell) must have had access to the S-1000 firmware source code, so it is unlikely that Packard Bell designed the SC600 without assistance by Ensoniq.
As the Sound Blaster 16 included a bass/trebble control, I assume that Packard Bell/Reveal required feature parity in that regard, so they added the TDA8421. As Packard Bell had to market machines that had an SC600 instead of an SB16 installed, it makes sense that they demanded feature parity.
One thing that is clearly wrong in your otherwise great write-up is the claim that the AD1848/CS4248 has been added to "improve SoundBlaster compatiblity". This is not the case. The MC68000 processor, which runs the SoundBlaster emulation code, and which processes the writes to ports 0x220..0x22F does not have any access to the AD1848. That chip is a completely sound source which is mixed on the analog part of the sound card to the Soundscape output. The reason to add the AD1848/CS4248 is likely the requirement for "CD-quality audio playback and recording", which the S-1000 design does not deliver in default configuration. They run the OTTO at the maximum count of 32 "voices", which results in a sample rate of 1MHz / 32, which is 31250Hz, so obviously below 44100Hz. While you can play back sounds sampled at 44100Hz using the S-1000 wave functionality, that sound would get resampled by the OTTO chip to 31250Hz before going to the AD converter. According to the ODIE data sheet, CD quality recording was provided. They also claim PCM playback up to 44100 on the S-1000 design, but "accidentally" fail to mention that this would require to reduce the polyphony. The OTTO datasheet suggests 19 voices at a slightly increased core clock to reach 44100 Hz, but the S-1000/S-2000 designs do not provide a 16.9MHz base clock to the OTTO (just the standard 16.0MHz), and the firmware versions that were delivered to the customer have no way of selecting a 24-voice polyphony mode.
Sound Blaster compatiblity has been enhanced by "the new firmware" (named .CO0 and so on instead of .COD) instead.
Regarding the tables in the firmware, the actual functions are:
Base address 0x2DE: 146 16-bit usigned values Mapping from the logarithmic volume used inside the firmware to the floating-point volume values. Volume code 0 is "-inf dB" (muted), volume code 1 is "-72dB", and then the volume increases in steps of 0.5dB until volume code 145 hits "0dB". You can generate it using this Python script:
1def vol_entry(v): 2 exponent = v//12 + 4 3 mantissa = int((2**((v%12)/12) - 1) * 4096) 4 if mantissa == 0 and exponent >= 15: mantissa = -1 5 return (exponent << 12) + mantissa 6 7list(map(hex, [0] + [vol_entry(v) for v in range(145)]))
Base address 0x402: Ramp step size table: 128 16-bit values to be treated as unsigned fix-point integers with 2 integer and 14 fractional bits. This table maps the ramp duration code to the fraction of the ramp hight performed in 4ms. Ramp duration code 0 is undefined, ramp duration code 1 represents 4ms, and each increase of the ramp duration code by 10 makes the ramp to take twice as long.
1[0] + [int(16384 / 2**(x/10)) for x in range(127)]
Base address 0x502: Ramp step count table: 128 16-bit values to be treated as integers indicating the count of whole 4ms intervals the ramp requires. A final fractional 4ms interval is not included.
1[0] + [int(2**(x/10)) for x in range(127)]
Base address 0x602: Pitch scale factor table. The pitch of some instrument layers is influenced by the velocity or some LFO output ("vibrato"). The intensity of pitch modulation is given as an 8-bit value, which is translated using this table. The firmware handles pitch in a logarithmic way in which a difference of 2048 means an octave (and thus 1024 means a tritone). The modulation parameters are generally 8-bit signed values, so they range from -128 to +127. At this point -127 is considered -1.0 and +127 is considered +1.0, so -128 is slightly more intense modulation than "-1.0". The elements of this table are multiplied with the modulation input (that is -127..+127) and it yields the pitch difference with 8 extra fractional bits. This table has 255 entries and is intentionally not just linear. Note that the index to this table is a constant scale factor taken from the instrument definition (which also ranges between -127 and +127), not a dynamic input value (like an envelope output, LFO output or the velocity). If the table index is between -64 and +64, the full-range modulation is a fraction of +/- 1 semitone. -64 maps a modulation input of -127 to +1 semitone and +127 to -1 semitone, and +64 maps -127 to -1 semitone while +127 is mapped to +1 semitone. +32 maps -127 to -0.5 semitones and +127 to +0.5 semitones. Table inputs outside of the range -64 to +64 indicate a whole number of semitones as available range, fitting continously to the fine-tune range in the center, thus +65 maps to -/+2 semitones, +66 maps to -/+3 semitones and so on until +127 maps to -/+64 semitones. Array indices are shifted, so the first entry refers to "-127" and the 255th one to "+127".
1# scale factor that scales -127..+127 to -1semitone..+1semitone 2# The factor has 8 fractional bits, and the pitch is on a binary logarithmic scale with 11 fractional bits. 3full_range_to_semitone_8x8 = 256 * 2048/12 / 127 4 5# range -64..64 is +/- one semitone for full modulation depth. 6# range -127..-65; +65..+127 is +/- 2 to 64 semitones for full modulation depth 7def expand_factor(x): return x-(-63) if x < -64 else x - 63 if x > 64 else x/64 8 9[int(full_range_to_semitone_8x8 * expand_factor(x-127)) for x in range(255)]
Base address 0x800: 128 16-bit values. LFO frequency table. The mapping from LFO frequency code (0..127) to the actual frequency. The LFO frequency is stored in units of 250Hz/4096, and as the LFO is sampled at 250Hz, the highest sensible value is half the sample rate, i.e. 125Hz, which is 2048 of those LFO frequency units. Generally, using a lorarithmic encoding for frequencies makes sense (for example MIDI note numbers instead of frequencies in Hz), so that's the basic idea used for LFO frequency codes. A simple logarithmic encoding could be defined by mapping the numeric value 0.0 means the minimum frequency of 250Hz/4096 and the numeric value 1.0 to the maximum frequency of 2048 * (250Hz/4096). If that numeric value is stored as fixed-point number with 7 fractional bits, you would get the exponential expansion function 2048**(code/128) * f0, with the base frequency f0 = 250Hz/4096. This function is at the core of this table, but using it as-is has several issues, which are solved by tweaking the function. For one, you can't get a frequency of 0 (LFO not running) using any exponential law, so code 0 will be treated specially as 0 instead of meaning exactly f0. This will lose access to exactly f0, but the next issue will show that this is not an issue: The table contains integer multiples of f0, so at low frequencies, rounding errors are very significant. Most importantly, code 6 would translate to 1.43*f0, which is still rounded to 1*f0, and codes 7 to 15 would all map to 2*f0 after rounding, which is a waste of codes. Losing the code value 0 for f0 thus is not an issue at all, because code 1 to 6 also translate to f0 after rounding. Let's postpone how to deal with this issue until after looking at the next issue: The maximum frequency is encoded as 128, but the code space is just 0 to 127. To solve this issue, we subtract one from the theoretical code values. This will lose another code at the low end, which is not an issue, because f0 is still available at codes 1 to 5 if the adjusted encoding is used. So, only the "wasting codes at the low end" issue remains. The formula with this remaining issue is
1[0 if x == 0 else round(2048**((x+1)/128)) for x in range(128)]
Now, to make better use of the code space, and actually provide 127 different non-zero frequencies, Ensoniq decided to start with a linear encoding at the low end, until the exponential curve intersects the linear mapping. This means code 1 maps to f0, code 2 maps to 2*f0 and so on, until at code 71 the exponential function takes over providing 73*f0. The final function to calculate this table is
1[max(x,0 if x == 0 else round(2048**((x+1)/128))) for x in range(128)]
Base address 0x900: 128 16-bit values. MIDI note number to relative logarithmic frequency. As already mentioned, the primary representation of frequencies is a logarithmic fixed-point representation in which an increase by 2048 means a factor of 2 (an octave), which can also be interpreted as an exponent to the base 2 with 11 fractional bits. This table maps MIDI note numbers to the amount of pitch this note is higher than MIDI note 0. This table thus generates the tuning, which is nearly equal temperament. I'm unsure whether the deviation is an intended tuning choice or an artifact of how the table was generated. In equal temperament, a interval of 3 semitones is the 4th root of 2, which is representable as 2**0.25, so the exponent has an exact binary representation, and this is perfectly represented in this table. But the two intermediate semitones do not exactly hit the spot. If you would represent 1/12 and 2/12 as fixed-point integers with 11 fractional bits, you would get 0xAB and 0x155 (if you use round to nearest), but the table uses 0xAC and 0x154 instead. The table with the slight deviation can be obtained with this code:
1[round(x*2048/12) + ((x+1)%3)-1 for x in range(128)]
Base address 0xA00: 128 unsigned 16-bit values: Low-pass filter coefficitent table. This table maps a frequency value, given in semitones (like a MIDI note number) to the value to be programmed into the OTTO IIR filter coefficient register for low-pass filtering. I was unable to determine the exact equation used to derive the table, and one of my many "postponed projects" is trying to identify it. The table is quite close to mapping MIDI note frequencies to a specific damping on the low-pass filter curve, but it doesn't fit exactly, so I suspect some approximation was used generating this table.
Base address 0xB00: 128 unsigned 16-bit values: High-pass filter coefficitent table. Just like the low-pass table, it maps a frequency representation given in semitones to the value for the OTTO IIR filter coefficient. I do not yet fully understand how the table is generated. The high-pass is generally more difficult to understand, because as I understand the OTTO datasheet, the filter uses a topology in which passband gain of the high-pass filter is frequency dependent. Furthermore, the coefficient is somehow offset by 0.5 (or 32786 in fixed-point representation).
Base address 0xC00: 128 signed 8-bit values used for panning. The index to the table is the closeness of the panning position to -63 for the left channel, or the closeness of the panning position to +63 for the right channel. "closeness" is defined as the "negative distance" and ranges from 0 (exact match) to -126 (fully opposite, i.e. panning position -63 when calculating volume of the right channel). The "closeness" is shifted by 127 to yield an array indices of 1 for "fully opposite" and 127 for "exact match". A centered voice has a closeness to -63 to either end of the panning spectrum, and thus uses array index 64 for both the left and the right channel. The value is a volume offset in units of .5dB. It seems that the sum of the amplitudes (not the logarithmic volumes in dB) is meant to stay constant over the whole panning range, and that's why you get a value of -12 (which represents -6dB, which is an attenuation factor of 2) for both the left and the right channel for a centered voice.
Base address 0xC80: Pitch conversion table: 1024 32-bit intergers. This table is used to convert from fixed-point logarithmic pitch to 32-bit fixed-point linear pitch as used by the OTTO chip. The integer part of the loarithmic fixed-point representation can easily be converted by shifting the final value, but the 11 fractional bits need to be linearized. The lowest fractional bit is ignored, and the remaining 10 bits are used as index into this table. The value of this table is a fixed point value with 17 fractional bits in the range between 1 and (nearly) 2 representing the factor resulting from the fractional logarithmic bits.
1[round(2**17 * 2**(idx/1024)) for idx in range(1024)]
Base address 0x1CB0: OTTO sample rates: 12 16-bit values: Sample rate generated by the OTTO chip depending on voice count (21..32 voices) if the OTTO chip is clocked at 16 MHz.
1def srate(voicecount): return 1_000_000/voicecount 2 3[round(srate(x)) for x in range(21,33)]
Base address 0x1C98: 12 16-bit values: pitch difference between the OTTO sample rate and 44100 Hz, given in firmware pitch units (logarithmic fix-point with 11 fractional bits)
1import math 2def tune_shift_fine(voicecount): return round(math.log(44100/srate(voicecount))/math.log(2)*2048) 3 4[tune_shift_fine(x) for x in range(21,33)]
Base address 0x1C80: 12 16-bit values: pitch difference between the OTTO sample rate and 44100 Hz, given in semitones (rounded)
1[round(tune_shift_fine(x)*12/2048) for x in range(21,33)]
Base address 0x1CC8: MIDI velocity/volume translation table. MIDI specifies a square law between (velocity + volume) and the wave amplitude. This table translates a value in the range 0..127 which is mapped to 0..1.0 to a firmware volume value (expressed in units of 0.5dB). For that to happen, the integer index is divided by 127 to get from the integer representation to the floating point value, then squared (to obey the curve shape specified by General MIDI), then the base-10 logarithm and the result multiplied by 20 to convert an amplitude fraction into dB, and again multiplied by 2 to go to units of 0.5dB. Finally, we want 127 for maximum volume, not 0, so 127 is added. The values generated by this code are sometimes off-by-one, presumably due to different rounding.
1# a log variant that includes 0 in its domain 2def safelog(x,base=math.e): return -math.inf if x==0 else math.log(x,base) 3def clamplow(x,limit): return limit if x < limit else x 4 5[round(clamplow(safelog((idx/127)**2, 10)*40+127,limit = 0)) for idx in range(128)]
Base 0x1D5C: LFO shape "sine": 256 signed 8-bit values defining a sine wave between -127 and +127, starting at the rising zero crossing
1[int(math.sin(x/256*2*math.pi)*127) for x in range(256)]
Base 0x1E5C: LFO shape "positive sine": 256 8-bit values defining a (co)sine wave between 0 and 126, starting at the minimum (i.e. 0)
1[int(-math.cos(x/256*2*math.pi)*63+63) for x in range(256)]
Base 0x1F5C: LFO shape "triangle": 256 signed 8-bit values defining a triangle wave between -127 and +127, stating at the rising zero crossing.
1def triangle(x): return x/64 if x < 64 else 2-x/64 if x < 192 else x/64-4 2 3[int(triangle(x)*127) for x in range(256)]
Base 0x205C: LFO shape "positive triangle": 256 8-bit values defining a triangle wave between 0 and 128, starting at the minimum
Base 0x215C: LFO shape "square": 256 signed 8-bit values defining a square wave between -127 and +127, starting at the falling zero crossing
Base 0x225C: LFO shape "positive square": 256 8-bit values defining a square wave between 0 and +127, starting at the falling edge
Base 0x235C: LFO shape "sawtooth": 256 signed 8-bit values defining a rising sawtooth between -127 and +127, starting at the (rising) zero crossing.
Base 0x235C: LFO shape "positive sawtooth": 256 8-bit values defining a rising sawtooth between 0 and +127, starting at the minimum (zero).
Base 0x255C: LFO shape "trine" (word invented by me, meaning a superposition of a TRIangle and a siNE): 256 signed 8-bit values defining a trine wave between -127 and +127, starting at the rising zero crossing
1[int((triangle(x)+math.sin(x/128*math.pi))/2*127) for x in range(256)]
Base 0x255C: LFO shape "positive trine": 256 8-bit values defining a trine wave between 0 and +127, starting at the minimum (i.e. zero)
Can you provide a zipped folder of your SC600 installation? I haven’t been able to find suitable drivers out there to get the software version of SSINIT you show in your SC600 article
Can you provide a zipped folder of your SC600 installation? I haven’t been able to find suitable drivers out there to get the software version of SSINIT you show in your SC600 article
Interesting article. Yet I am not convinced that your theory that the SC600 and the S-2000 were independent parallel developments is true.
This thesis, stated like this, is somewhat stronger than what I wanted to claim; probably the language in my article was unclear. I just wanted to state that the SC600 is not a clone of the S-2000, as both cards contain their own unique features. Apart for the elements for which we have documents, I am agnostic about which ideas went from Ensoniq to Reveal, which from Reveal to Ensoniq, and which were devised independently by both teams. I also wonder whether the companies were OK with the two cards ending up being so similar.
One thing that is clearly wrong in your otherwise great write-up is the claim that the AD1848/CS4248 has been added to "improve SoundBlaster compatiblity".
Thanks; now fixed (with credits). The phrase came from me misreading a detail in your “Classic Soundscapes” essay.
Can you provide a zipped folder of your SC600 installation? I haven’t been able to find suitable drivers out there to get the software version of SSINIT you show in your SC600 article
If you have the hardware at hand, you can test whether the [pseudo-stereo and spatial] modes are actually swapped (as my reverse engineering suggestes), or whether either the reverse engineering efforts or the data sheet is wrong.
I don’t have my card with me (I had to hack SSINIT to record my video “playthrough”), but if @carlostex has the opportunity, this would be an interesting experiment.
FWIW, Boxpressed had also uploaded an alternative version of the SC600 drivers, slightly newer than mine (1.09.05 of Apr 1994 vs 1.09.02 of Jan 1994).
Since LLMs make everything cheap, I spent some tokens to diff its SNDSCAPE.COD with my version. Almost no difference:
* some reordering of the subfunction handlers, which makes the firmware even more similar to the Soundscape’s one
* variable 0xEE0E is initialized to 0x0 instead of 0x7F. My agent says that such bit is mapped into ODIE’s 0xA0001A bit7, but I did not look its deeper meaning (nor checked whether the LLM’s analysis is correct)
* for the good and the bad, no change in the tone control code. If my version has a bug in the stereo effect mapping, it’s also there in Boxpressed’s version.
Last edited by Bodo on 2026-09-07, 18:11. Edited 2 times in total.
variable 0xEE0E is initialized to 0x0 instead of 0x7F. My agent says that such bit is mapped into ODIE’s 0xA0001A bit7, but I did not look its deeper meaning (nor checked whether the LLM’s analysis is correct)
0xEE0E (actually 0xFFEE0E in the 24-bit 68000 address space, an 0xFFFFEE0E if you load that file in IDA) is the third mixer control which is supposed to select between stereo/line in input or mono/microphone input. The LLM is correct that this variable is applied to ODIE port 0x1A, bit 7. In the old firmware this is performed by the mixer update function at 0x4E70, which is referred in the third slot of the mixer update function table at 0x4E10.
Bit 7 of 0xA0001A sets the level GPO7.
As recording on the SC600 is performed using the WSS codec, I'm not sure whether this bit still serves any function. As far as I know, SSINIT initializes all mixer channels anyway, so the change of the default value of mixer channel 2 does not seem to have a significant effect on normal card operation.