VOGONS


First post, by bananaboy

User metadata
Rank Newbie
Rank
Newbie

I recently got interested in how the ADPCM worked on the Soundblaster, specifically the 4-bit version. I looked at the dosbox ADPCM code but it turns out that came from reverse engineering Blaster Master (old MS-DOS sound software) under the assumption that it used the Creative SDK and so the algorithm was correct and accurate. But the Creative SDK doesn't have any libs to decompress ADPCM so Gary Maddox author of Blaster Master must have figured it out himself.

But a few years ago TubeTime dumped an SB 2.02 DSP so we have the source of truth! Apologies if there has been previous discussion on this but I couldn't find anything.

It turns out the decoder looks something like this:

uint8_t DecodeADPCM4(uint8_t data, uint8_t& sample, uint8_t& accum)
{
assert(data < 0x10);

uint8_t halfAccum = accum >> 1;
uint8_t value = data & 7;
uint8_t delta = (value * accum) + halfAccum;
if (data & 8)
{
// data is negative
sample = (uint8_t)std::max((int16_t)sample - delta, 0);
}
else
{
// data is positive
sample = (uint8_t)std::min((int16_t)sample + delta, 0xff);
}

if (value == 0)
{
accum = halfAccum;
if (accum == 0)
accum = 1;
}
else if (value >= 5)
{
accum <<= 1;
if (accum == 0x10)
accum = 8;
}

return sample;
}

So it doesn't use tables, unlike dosbox. However, I compared this to dosbox and it seems to be binary identical! So great job Gary Maddox!

I found what looks like a bug in the decoding though (so a bug in the original SB - not the only one as TubeTime points out in the comments). Line 1692 loads register r3 with 4. That line is setting the number of samples to process when running the "no reference byte" DMA command. I'm fairly sure this should be 2 here though because in 4-bit each byte has two samples. In fact when the handler requests another data byte it does correctly set r3 to 2 (line 487). So it seems to me that when "no reference byte" mode is used, the first two samples will play twice.

There is also a little bug in the dosbox emulation - dosbox doesn't seem to send the first reference byte to the DAC whereas the SoundBlaster does.

Anyway I thought that was all pretty interesting.

Cheers,
Sam

Reply 1 of 9, by Wild-E

User metadata
Rank Newbie
Rank
Newbie

Hi,

EDIT: To anyone reading this later on - the algorithms produce identical results bit-for-bit if done correctly. See my last reply. As a programming exercise, spot my error in the code in this reply.

I have a project (and old game) with ADPCM samples. The port has inferior ADPCM decoding.

I've tried the algorithm from DOSBox (staging) and this one. Both are miles better. Sometimes it's difficult to hear the difference but sometimes it's much more clear

(to clafify: to the inferior one from MultimediaWiki - these two are indistinguishable at least by my ear, and even if substracting the waveforms, the difference is negligible but not 0).

However, the algorithm here and in DOSBox staging is not bit perfect, or I've made a mistake. It's certainly possible. I'm decoding in C, I may have made a mistake in either one - or maybe I'm having a slightly different lookup table 😀.

Anyways, here are the algorithm I'm using. Can you spot a mistake? The sound samples "sound" good and waveforms look a lot better than with the algorithm the port used (from MediaWiki). (In case you have ADPCM data, try decvogon and adpcm4decode below to see the difference yourself - it's small but it's there).

EDIT: It seems like out a total of 57 ADPCM samples, these algorithms produce an identical results, bit-for-bit, in 30 case, but differ in 27. It's weird - but there is a difference!

EDIT2: I changed the algorithms just so that they are initialized to 0 and also decode the first ADPCM byre. Now I get IDENTICAL 41 out of 57! But still 16 differ. They always differ at the first or the second byte. I still suspect maybe some byte is leaking outside the sample data in my code. I've updated the code below accordingly.

#include <stdlib.h>
#include <stdint.h>
#include <assert.h>
#include <stdio.h>

#include "adpcm_decode.h"

const signed char sndsb_adpcm_4bit_scalemap[64] = {
0, 1, 2, 3, 4, 5, 6, 7, 0, -1, -2, -3, -4, -5, -6, -7,
1, 3, 5, 7, 9, 11, 13, 15, -1, -3, -5, -7, -9, -11, -13, -15,
2, 6, 10, 14, 18, 22, 26, 30, -2, -6, -10, -14, -18, -22, -26, -30,
4, 12, 20, 28, 36, 44, 52, 60, -4, -12, -20, -28, -36, -44, -52, -60
};

const signed char sndsb_adpcm_4bit_adjustmap[64] = {
0, 0, 0, 0, 0, 16, 16, 16,
0, 0, 0, 0, 0, 16, 16, 16,
240, 0, 0, 0, 0, 16, 16, 16,
240, 0, 0, 0, 0, 16, 16, 16,
240, 0, 0, 0, 0, 16, 16, 16,
240, 0, 0, 0, 0, 16, 16, 16,
240, 0, 0, 0, 0, 0, 0, 0,
240, 0, 0, 0, 0, 0, 0, 0
};


// /*********************************************************************************
// * UPDATED to adapt information from Dosbox Staging source -> */
// * https://github.com/dosbox-staging/dosbox-staging/blob/main/src/hardware/audio/soundblaster.cpp */
// *********************************************************************************/
unsigned char *adpcm4decode(unsigned char *data, int len) {

unsigned char *new_sample = malloc (2 * (len ));
int byte;
unsigned char value;
int i;
int map_i = 31;
int step; //, shift; // limit , sign;
step = 0;


byte = 0; // data[0];
new_sample[0] = 0;

printf("Pointer before: %p\n", new_sample);

for (int i = 0; i < len; i++) {
for ( int j = 1; j >= 0 ; j-- ) { // low<>high bits
// higher or lower 4 bits
value = j == 0 ? data[i] & 0x0f : ( data[i] & 0xf0 ) >> 4;
map_i = value + step;
// clamp to max_i = 63 (len-1)
map_i = map_i < 0 ? 0 : map_i > 63 ? 63 : map_i;
step = ( step + sndsb_adpcm_4bit_adjustmap[map_i]) & 0xff; // why this AND? drop >8 bits?
byte = ( byte + sndsb_adpcm_4bit_scalemap[map_i] );
// clamp to 0 ... 255
byte = byte < 0 ? 0 : byte > 255 ? 255 : byte;
new_sample[0] = byte;
new_sample++;
}
Show last 74 lines
    }
new_sample-=2 * ( len - 0 ) ;
printf("Pointer after: %p\n", new_sample);
return new_sample;

}

unsigned char *decvogon(unsigned char *data, int len) {
unsigned char *new_sample = malloc (2 * (len ));
int byte;
unsigned char value;
int i;

int step; //, shift; // limit , sign;
printf("Pointer before: %p\n", new_sample);
new_sample[0]=0; // data[0];
int lastsample = 0; // just for the first byte needed
for (i = 0; i < len; i++) {
for (int j=1; j>=0; j--) {
value = j == 0 ? data[i] & 0x0f : ( data[i] & 0xf0 ) >> 4;
new_sample[0] = DecodeADPCM4(value , lastsample);
lastsample=new_sample[0];
new_sample++;
}
}
new_sample-=2*(len-0);
printf("Pointer after: %p\n", new_sample);
return new_sample;
}

// /*********************************************************************************
// * Decoder taken from:
// * https://www.vogons.org/viewtopic.php?t=93707
// * by bananaboy
// *********************************************************************************/
uint8_t DecodeADPCM4(uint8_t data, int sample)
{
assert(data < 0x10);

static int accum;
uint8_t halfAccum = (int) accum >> 1;
// printf("%i", halfAccum);
uint8_t value = data & 7;
uint8_t delta = (value * accum) + halfAccum;

// int sample;
if (data & 8)
{
// data is negative
sample = sample - delta;
sample = sample < 0 ? 0 : sample;
} else {
// data is positive
sample = sample + delta;
sample = sample > 255 ? 255 : sample;
}

if (value == 0)
{
accum = halfAccum;
if (accum == 0)
accum = 1;
}
else if (value >= 5)
{
accum <<= 1;
if (accum == 0x10)
accum = 8;
}

return sample;
}

(It's The Last Eichhof I'm working on)

Last edited by Wild-E on 2026-05-07, 10:56. Edited 1 time in total.

Reply 2 of 9, by MagefromAntares

User metadata
Rank Newbie
Rank
Newbie

Hi,

On line 100 in the DecodeADPCM4 function the static variable is uninitialized even after your second edit, have you updated the code block? If not then depending on the compiler and flags used that variable might be initialized to a random value found in memory.

"A process cannot be understood by stopping it. Understanding must move with the flow of the process, must join it and flow with it." - Dune

Reply 3 of 9, by Wild-E

User metadata
Rank Newbie
Rank
Newbie

Hmm, that is true, I somehow missed that. I don't know what it should be initialised, maybe to 1, judging from the rest of the code? The OP is not clear on the matter. It is better to initialise it to *something* than let the compiler lottery 🤣

Reply 4 of 9, by jmarsh

User metadata
Rank Oldbie
Rank
Oldbie

static variables are always initialized to zero by default, as part of the language specification. It should not vary by compiler.

Reply 5 of 9, by Wild-E

User metadata
Rank Newbie
Rank
Newbie

Hmm it may still be possible accum is supposed to be 1 (rather than 0) at the beginning, since it seems to be clamped (well, floored) at one later. Also, one sample will be bit-perfectly same as with the algorithm based on lookup tables with clamp initialized to 1.

Reply 6 of 9, by MagefromAntares

User metadata
Rank Newbie
Rank
Newbie
jmarsh wrote on 2026-05-07, 00:24:

static variables are always initialized to zero by default, as part of the language specification. It should not vary by compiler.

Yes you are right, for some reason I assumed that a 90s era compiler is being used by hearing the name SoundBlaster 😁 Newer compilers do initialize static arithmetic types to 0 according to the C99 standard, sorry for giving no longer correct information 🙁

"A process cannot be understood by stopping it. Understanding must move with the flow of the process, must join it and flow with it." - Dune

Reply 7 of 9, by Wild-E

User metadata
Rank Newbie
Rank
Newbie

Ok, I noticed my error. It was the accumulator. And a few others.

Depending on how the decoding is initialised (wrongly!), the first few bytes might have been identical just by accident. I.e. at certain values, despite the accum being initialised wrong, the output might be identical (i.e. another error cancels out another). Some implementation online incorrectly take the first byte of the ADPCM (encoded) sample as the first (decoded) sample. This is wrong (I think). The output should not be initialised to anything - Now I just initialise the decoders first value and decode every byte. The output lengths is 2 x length (of input) (not 2xlen -1 or anything like that as I've seen elsewhere).

The question is, what is the first reference value? We can not really know this. Any value could work. I just take 127, since other values tend to cause a click at the beginning. Main thing is - whatever you decide to initialise, these algorithms should be identical (when initialised at the same value).

Now, as a sidenote, sleeping is a funny thing. I went to sleep after my last reply. The first thing I thought, when I woke up, still lying in my bed, was that the accum variable is still wrong.

It needs to be initialised (to what? 1? EDIT: looking at the code, it's somewhat obvious it should be 1, otherwise first nibble would be discarded) before every decoding. In my implementation (1st reply), it used to remember what it was left when the last decoding finished. The OP has pointer to the accum, instead of handling it internally (it would work the way I've done it without pointers if everything was done in the same function and nibbles not decoded in a separate one, as then I could re-initialise it - I still do but at the outer function).

Anyways, I've updated my algorithm to initialise accum and can confirm both algorithms produce 100% identical results (at least with the samples from The Last Eichhof), if done correctly. Updated code block here (I'll leave the first one as a funny programming exercise / for reference for future readers.)

This is all C (EDIT: Cleaned up a bit unnecessary comments, more concise):

const signed char sndsb_adpcm_4bit_scalemap[64] = {
0, 1, 2, 3, 4, 5, 6, 7, 0, -1, -2, -3, -4, -5, -6, -7,
1, 3, 5, 7, 9, 11, 13, 15, -1, -3, -5, -7, -9, -11, -13, -15,
2, 6, 10, 14, 18, 22, 26, 30, -2, -6, -10, -14, -18, -22, -26, -30,
4, 12, 20, 28, 36, 44, 52, 60, -4, -12, -20, -28, -36, -44, -52, -60
};

const signed char sndsb_adpcm_4bit_adjustmap[64] = {
0, 0, 0, 0, 0, 16, 16, 16,
0, 0, 0, 0, 0, 16, 16, 16,
240, 0, 0, 0, 0, 16, 16, 16,
240, 0, 0, 0, 0, 16, 16, 16,
240, 0, 0, 0, 0, 16, 16, 16,
240, 0, 0, 0, 0, 16, 16, 16,
240, 0, 0, 0, 0, 0, 0, 0,
240, 0, 0, 0, 0, 0, 0, 0
};

#define ADPCM_INIT 0x7f // value to init to. Could be anything
// I've noticed 127 produces least clicks
// ADPCM samples tend to have end/start clicks anyways

/* This is something called 8 to 4 bit ADPCM */
/* Originally, the decoding algorithm was taken from */
/* http://wiki.multimedia.cx/index.php?title=Creative_8_bits_ADPCM */

// /*********************************************************************************
// * UPDATED to adapt information from Dosbox Staging source -> */
// * https://github.com/dosbox-staging/dosbox-staging/blob/main/src/hardware/audio/soundblaster.cpp */
// *********************************************************************************/
unsigned char *adpcm4decode_dbs(unsigned char *data, int len) {

unsigned char *new_sample = malloc (2 * (len ));
int byte = ADPCM_INIT;
unsigned char value;
int map_i;
int step = 0;

for (int i = 0; i < len; i++) {
for ( int j = 1; j >= 0 ; j-- ) { // low<>high bits
// higher or lower 4 bits
value = j == 0 ? data[i] & 0x0f : ( data[i] & 0xf0 ) >> 4;
map_i = value + step;
// clamp to max_i = 63 (len-1)
map_i = map_i < 0 ? 0 : map_i > 63 ? 63 : map_i;
step = ( step + sndsb_adpcm_4bit_adjustmap[map_i]) & 0xff; // why this AND? drop >8 bits?
byte = ( byte + sndsb_adpcm_4bit_scalemap[map_i] );
// clamp to 0 ... 255
byte = byte < 0 ? 0 : byte > 255 ? 255 : byte;
new_sample[0] = byte;
new_sample++;
}
}
new_sample-=2 * ( len - 0 ) ;
return new_sample;
}

unsigned char *adpcm4decode_ban(unsigned char *data, int len) {

unsigned char *new_sample = malloc (2*len);
Show last 50 lines
  int accum = 1;
int lastsample = ADPCM_INIT;

for (int i = 0; i < len; i++) {
for (int j=1; j>=0; j--) {
unsigned char value = j == 0 ? data[i] & 0x0f : ( data[i] & 0xf0 ) >> 4;
new_sample[0] = DecodeADPCM4(value , lastsample, &accum);
lastsample=new_sample[0];
new_sample++;
}
}
new_sample-=2*len;
return new_sample;
}

// /*********************************************************************************
// * Decoder adapted from:
// * https://www.vogons.org/viewtopic.php?t=93707
// * by bananaboy
// *********************************************************************************/
uint8_t DecodeADPCM4(uint8_t data, int sample, int *accum) {

uint8_t halfAccum = (int) *accum >> 1;
uint8_t value = data & 7;
uint8_t delta = (value * (*accum)) + halfAccum;

if (data & 8) {
// data is negative
sample = sample - delta;
} else {
// data is positive
sample = sample + delta;
}

// Clamp to 0...255
sample = sample < 0 ? 0 : sample > 255 ? 255 : sample;

if (value == 0) {
*accum = halfAccum;
if (*accum == 0) *accum = 1;
}

else if (value >= 5) {
*accum <<= 1;
if (*accum == 0x10) *accum = 8;
}

return sample;
}

Reply 8 of 9, by bananaboy

User metadata
Rank Newbie
Rank
Newbie

Hey nice I'm glad you found this useful. Sorry for my original post causing so much confusion! I checked my notes and implementation and you're right, `accum` should be initialised to 1. Great to hear you've solved your issue!

Cheers,
Sam

Reply 9 of 9, by Wild-E

User metadata
Rank Newbie
Rank
Newbie

Hi Sam,

No problems!

This decoding algorithm is in-use in my fork of the Allegro4 port of The Last Eichoff, which can be found in gitea.com.