ADPCM codecs

From NeoGeo Development Wiki
Jump to: navigation, search

ADPCM codecs are used to encode samples to be used by the YM2610.

All the code in this page process 16bit monoral, little endian raw sound data data (".snd" files, Goldwave can output this), make sure to have your samples at the proper frequency. C# implementation.

Remember to size/pad your sample so it's a 256 bytes multiple, this is not done in the following code.

ADPCM-A

This codec accepts 18.5kHz samples, playback rate is fixed.

Inspired by Mame and MVStracker. This codec implementation was done with some trial & error, might not be 100% correct. It however fixes the issue with long samples degenerating.

static short[] step_size = { 
			   16, 17, 19, 21, 23, 25, 28, 31, 34, 37,
			   41, 45, 50, 55, 60, 66, 73, 80, 88, 97,
			   107, 118, 130, 143, 157, 173, 190, 209, 230, 253,
			   279, 307, 337, 371, 408, 449, 494, 544, 598, 658,
			   724, 796, 876, 963, 1060, 1166, 1282, 1411, 1552
			   }; //49 items
static int[] step_adj = { -1, -1, -1, -1, 2, 5, 7, 9, -1, -1, -1, -1, 2, 5, 7, 9 };

//buffers
private byte[] buffer;	//input buffer, load your sound file into this
private short[] inBuffer;	//temp work buffer, used correct byte order and downsample
private byte[] outBuffer;	//output buffer, this is your PCM file, save it

//decode stuff
private int[] jedi_table;
int acc = 0; //ADPCM accumulator, initial condition must be 0
int decstep = 0; //ADPCM decoding step, initial condition must be 0

//encode stuff
int diff;
int step;
int predsample;
int index;
int prevsample = 0; // previous sample, initial condition must be 0
int previndex = 0; //previous index, initial condition must be 0

//jedi table is used speed up decoding, run this to init the table before encoding. Mame copy-pasta.
private void jedi_table_init()
{
	int step, nib;

	jedi_table = new int[16 * 49];
	for (step = 0; step < 49; step++)
	{
		for (nib = 0; nib < 16; nib++)
		{
			int value = (2 * (nib & 0x07) + 1) * step_size[step] / 8;
			jedi_table[step * 16 + nib] = ((nib & 0x08) != 0) ? -value : value;
		}
	}
}

//decode sub, returns decoded 12bit data
private short YM2610_ADPCM_A_Decode(byte code)
{
	acc += jedi_table[decstep + code];
	if ((acc & ~0x7ff) != 0) // acc is > 2047
		acc |= ~0xfff;
	else acc &= 0xfff;
	decstep += step_adj[code & 7] * 16;
	if (decstep < 0) decstep = 0;
	if (decstep > 48 * 16) decstep = 48 * 16;
	return (short)acc;
}

// our encoding sub, returns ADPCM nibble
private byte YM2610_ADPCM_A_Encode(short sample)
{
	int tempstep;
	byte code;

	predsample = prevsample;
	index = previndex;
	step = step_size[index];

	diff = sample - predsample;
	if (diff >= 0)
		code = 0;
	else
	{
		code = 8;
		diff = -diff;
	}

	tempstep = step;
	if (diff >= tempstep)
	{
		code |= 4;
		diff -= tempstep;
	}
	tempstep >>= 1;
	if (diff >= tempstep)
	{
		code |= 2;
		diff -= tempstep;
	}
	tempstep >>= 1;
	if (diff >= tempstep) code |= 1;

	predsample = YM2610_ADPCM_A_Decode(code);

	index += step_adj[code];
	if (index < 0) index = 0;
	if (index > 48) index = 48;

	prevsample = predsample;
	previndex = index;

	return code;
}

//our main sub, init buffers and runs the encode process
//enter this with your sound file loaded into buffer
private void YM_encode()
{
	int i;

	//reset to initial conditions
	acc = 0;
	decstep = 0;
	prevsample = 0;
	previndex = 0;

	//watch out for odd data count & allocate buffers
	if ((buffer.Length / 2) % 2 != 0)
	{
		inBuffer = new short[(buffer.Length / 2) + 1];
		inBuffer[inBuffer.Length - 1] = 0x00;
	}
	else inBuffer = new short[buffer.Length / 2];
	outBuffer = new byte[inBuffer.Length / 2];

	//fix byte order and downscale data to 12 bits
	for (i = 0; i < buffer.Length; i += 2)
	{
		inBuffer[i / 2] = (short)((buffer[i]) | (buffer[i + 1] << 8));
		inBuffer[i / 2] >>= 4;
	}
	
	//actual encoding
	for (i = 0; i < inBuffer.Length; i += 2)
	{
		outBuffer[i / 2] = (byte)((YM2610_ADPCM_A_Encode(inBuffer[i]) << 4) | YM2610_ADPCM_A_Encode(inBuffer[i + 1]));
	}
}

Encoding example: File:Gold Rush 16b.snd.zip

ADPCM-B

This codec accepts 1.8kHz ~ 55.5kHz samples, playback rate has to be specified at runtime.

Official codec from the YM2610 datasheeet, works fine.

static long[] stepsizeTable = { 57, 57, 57, 57, 77, 102, 128, 153, 57, 57, 57, 57, 77, 102, 128, 153 };
private byte[] buffer;	//our input buffer, load your sample file into this before encoding
private byte[] outBuffer; //our output buffer, this is your PCM file, save it after encoding. Its size has to be allocated to buffer.length / 4 (16 bits per sample to 4 bits per sample)
private int outBufferIndex = 0; //reset to 0 before each encoding

private void YM2610_ADPCM_B_Encode()
{
	int lpc, flag;
	long i, dn, xn, stepSize;
	byte adpcm;
	byte adpcmPack = 0;
	short src;

	xn = 0;
	stepSize = 127;
	flag = 0;

	for (lpc = 0; lpc < (buffer.Length / 2); lpc++)
	{
		src = (short)((buffer[lpc * 2]) | (buffer[lpc * 2 + 1] << 8)); //16 bit samples, + fixing byte order
		dn = src - xn;
		i = (Math.Abs(dn) << 16) / (stepSize << 14);
		if (i > 7) i = 7;
		adpcm = (byte)i;
		i = (adpcm * 2 + 1) * stepSize / 8;
		if (dn < 0)
		{
			adpcm |= 0x8;
			xn -= i;
		}
		else
		{
			xn += i;
		}
		stepSize = (stepsizeTable[adpcm] * stepSize) / 64;
		if (stepSize < 127)
			stepSize = 127;
		else if (stepSize > 24576)
			stepSize = 24576;
		if (flag == 0)
		{
			adpcmPack = (byte)(adpcm << 4);
			flag = 1;
		}
		else
		{
			adpcmPack |= adpcm;
			outBuffer[outBufferIndex++] = adpcmPack;
			flag = 0;
		}
	}
}

Decoding tool

DAT interface

Decoding ROM files / samples can be done using the YM2610 ADPCM decoder tool. It decodes A and B type samples to a WAV file.

Works under modern windows OS, unlike previous tools that are deprecated.

File:YM2610 Decoder.zip

ABOUT NEO GEO CD

Actual events leading to this discovery.

Fun fact: Neo Geo CD does not support ADPCM-B.