Sounds in Q-Bert remake

@spinal The sound effects are very nice in your game. Are you using playMusicStream()? What is the sample rate?

Iā€™ll just link this tweet here, so that other readers know what we are talking about.

nope, but iā€™m assuming it works roughly the same way.
This could probably do with a lot of tidying up, but itā€™s working OK for now.
Iā€™m updating the audio buffer in quarter chunks simply because I cant detect the playing position fast enough to fill the buffer when it loops, not without messing with the playing loop in HWSound and I donā€™t know what else might be using that.
This way seems to work well enough, there is very little crackling.

settings.h

#define PROJ_SOUND_BUFFERED     1
#define PROJ_ENABLE_SOUND 1
#define PROJ_AUD_FREQ 11025
#include "HWSound.h"

bool playSample = 0;
int soundPoint;
const uint8_t *currentSound;
int currentSoundSize=0;
int oldQuart;

// Clear the buffer, might not be required as it should self-clear when updating
void emptyBuffer(){
    for(int t=0; t<SBUFSIZE;){
        soundbuf[++t]=0;
    }
}

// start a sound playing
void playSound(const unsigned char *sound, uint16_t soundSize){

    soundPoint=0;
    currentSound = sound;
    currentSoundSize = soundSize;
    playSample = 1;

}


// update teh sound buffer, call as often as possible
void updateSample(){

 int quart = soundbufindex / 512;
 int sndOffset[]={512,1024,1536,0};

    if(oldQuart != quart){
        oldQuart = quart;

            for(int t=0; t<SBUFSIZE/4;){
                if(playSample==0){
                    soundbuf[t+sndOffset[quart]]=0;
                }else{
                    soundbuf[t+sndOffset[quart]]=currentSound[soundPoint];
                    if(++soundPoint >= currentSoundSize){
                        playSample=0;
                    }
                }
                t++;
            }

    }

}
// in main function before loop
pokPlayStream(); // start pokitto sound function
emptyBuffer(); // clear the sound buffer in case of junk data

not sure if its necessary to update outside of the display function, but it cant hurt.

while (game.isRunning()) {
    
	updateSample(); // call outside of display loop for more speed
    
	if(game.update()){
// to play a sound
playSound(snd_death, sizeof(snd_death));
where snd_sample is a const char array of raw data. this can be saved as raw from a sound editor, or just remove the first 44 bytes from a .wav file, then simply copy the data using a hex editor or something.

const unsigned char snd_death[20964] = {
	0x80, 0x80, 0x7F, 0x80, 0x80, 0x7F, 0x80, 0x80, 0x80, 0x7F, 0x80, 0x80,
	0x81, 0x80, 0x7F, 0x80, 0x81, 0x81, 0x7F, 0x80, 0x80, 0x80, 0x80, 0x81,
	0x80, 0x80, 0x80, 0x80, 0x80, 0x80, 0x80, 0x80, 0x82, 0x86, 0x89, 0x8D,
	0x90, 0x94, 0x96, 0x9A, 0x80, 0x78, 0x7A, 0x78, 0x79, 0x78, 0x78, 0x7B,

	... etc.
2 Likes

Thanks, looks a very simple and clean implementation.

I was contemplating trying to play a couple of sounds at once, not 100% sure the effort would be worth it, or even noticeable.

So in case you want to change the pitch of the sounds: the playback speed of the audio interrupt is not fixed. For the purpose of playing musical notes, several interrupt frequency tables are provided. But in your case you could play back at any frequency you wish. Just do the same calls as are done in the audio init routine and replace with another frequency. Then push out the sound

2 Likes

Yes, unless there will be a background music also.

@spinal Quick and dirty frequency change on the fly. Replace POK_AUD_FRQ with a variable

        /* Initialize 32-bit timer 0 clock */
	Chip_TIMER_Init(LPC_TIMER32_0);

        /* Timer rate is system clock rate */
	timerFreq = Chip_Clock_GetSystemClockRate();

	/* Timer setup for match and interrupt at TICKRATE_HZ */
	Chip_TIMER_Reset(LPC_TIMER32_0);

	/* Enable both timers to generate interrupts when time matches */
	Chip_TIMER_MatchEnableInt(LPC_TIMER32_0, 1);

        /* Setup 32-bit timer's duration (32-bit match time) */
	Chip_TIMER_SetMatch(LPC_TIMER32_0, 1, (timerFreq / POK_AUD_FREQ));

	/* Setup both timers to restart when match occurs */
	Chip_TIMER_ResetOnMatchEnable(LPC_TIMER32_0, 1);

	/* Start both timers */
	Chip_TIMER_Enable(LPC_TIMER32_0);

	/* Clear both timers of any pending interrupts */
    #define TIMER_32_0_IRQn 18
	NVIC_ClearPendingIRQ((IRQn_Type)TIMER_32_0_IRQn);

    /* Redirect IRQ vector - Jonne*/
    NVIC_SetVector((IRQn_Type)TIMER_32_0_IRQn, (uint32_t)&TIMER32_0_IRQHandler);

	/* Enable both timer interrupts */
	NVIC_EnableIRQ((IRQn_Type)TIMER_32_0_IRQn);

EDIT: I will make an additional soundInit version that allows a free frequency setting in the next push to pokittolib

1 Like

Iā€™m going to have a try at mixing 2 samples at a time but Iā€™m not at home. Would I be correct in thinking something like this
Sample = ( sample1 - 128) + ( sample2 - 128) /2 + 128;
For 8bit unsigned pcm?

- 128 is probably going to cause underflow, is that intended?

Dunno, depends what underflow is.

Assuming from ā€œ8 bit unsigned pcmā€ that sample1 is std::uint8_t then underflow is what happens when you do 64 - 128 and end up with 192.

Basically if thereā€™s only 8 bits you can only represent so many numbers, so when you try to subtract a large integer from a small integer you get ā€˜underflowā€™.

1 Like

OK, I follow.

so I would have to recast the variable to a signed format first, then perform add themā€¦?

It seems that I was over thinking it. The sample data doesnā€™t seem to be -127 to 127, but rather 0 to 255.

int mix_sample(int a, int b) {
    int sample = a+b;
    if(sample>255)sample=255;
    return sample;
}

That should probably be:

std::uint8_t mix(std::uint8_t sample0, std::uint8_t sample1)
{
	constexpr std::uint8_t maximum = 0xFF;
	const unsigned int result = ((static_cast<unsigned int>(sample0) + static_cast<unsigned int>(sample1)) / 2);
	return static_cast<std::uint8_t>((result < maximum) ? result : maximum);
}

But I donā€™t know, I havenā€™t really looked into PCM.
All I know is that 128 is supposed to be the ā€˜centrepointā€™ (whatever that is).

Thatā€™s the theory I was working with, however, I did get a little confused remembering that zeroing out the sound buffer resulted in silence. Together with saving the audio as 8bit unsigned pcm seemed to play correctly.

So at this point I am not 100% sure how to audio format works, it doesnā€™t seem to be -127 to 127 as I expected.

Iā€™ve got a vague idea of how PCM works now, but more importantly I found this:

http://www.vttoth.com/CMS/index.php/technical-notes/68

Which suggests the following mathematical function is better for mixing:

CodeCogsEqn

And hereā€™s a Pharap-approved C++ implementation of it:

std::uint8_t mixToth(std::uint8_t sample0, std::uint8_t sample1)
{
	constexpr std::uint8_t maximum = 0xFF;

	const unsigned int s0 = static_cast<unsigned int>(sample0);
	const unsigned int s1 = static_cast<unsigned int>(sample1);
	
	const unsigned int result = (s0 + s1) - ((s0 * s1) / maximum);
	
	return static_cast<std::uint8_t>((result < maximum) ? result : maximum);
}

(I considered using std::max but Iā€™m not actually sure if references can bind to constexpr expressions or integer literals, Iā€™ll have to look it up.)

(This could be made constexpr with some effort, but Iā€™m not sure youā€™d benefit from it since youā€™re unlikely to be mixing at compile time.)

2 Likes

0ā€¦255 unsigned. 127 is ā€œsilenceā€ and midpointā€¦ but so is also any number x if the signal does not change. Sound, as you know is variations in air pressure

1 Like

Since 127 is the midpoint I think you might need the second part of that link you provided ( the mixing function that consists of 2 parts)

1 Like

I found that link yesterday and came up with the following alteration to allow up to 10 (although I doubt Iā€™d use that many samples at once)

uint8_t mixSound()
{
	int temp = 0;
    int multi = 0;
    int sampleCount=0;
	for(int t=0; t<10; t++){
        if(snd[t].playSample!=0){
            temp += (snd[t].currentSound[snd[t].soundPoint]*snd[t].volume)/100;
            multi *= (snd[t].currentSound[snd[t].soundPoint]*snd[t].volume)/100;
            sampleCount++;
        }
	}
    temp -= (multi/256);


	return static_cast<uint8_t>((temp < 0xFF) ? temp : 0xFF);
}
1 Like

2 things:

  1. thou shalt not divide in a tight routine
  2. thou especially shalt not use 10 factorials to do so

Thats tens of instructions because DIV is a software operation on a device without an FPU

To handle volume control fast: multiply by volume (0-255) (mult=1 instruction) then shift right by 8 bits (1 instruction) to get a ratio of 1/256

3 Likes