Welcome to Pete Brown's 10rem.net

First time here? If you are a developer or are interested in Microsoft tools and technology, please consider subscribing to the latest posts.

You may also be interested in my blog archives, the articles section, or some of my lab projects such as the C64 emulator written in Silverlight.

(hide this)

A simple bitcrusher and sample rate reducer in C++ for a Windows Store App

Pete Brown - 13 January 2013

I'm working on a Windows 8 synthesizer app using XAudio2 and a C++ + DirectX/XAML Windows Store app for Windows 8. As part of this, I thought it would be fun to add a simple bit crusher effect with included sample rate reducer. The point of this effect is to make samples sound like they came from older machines with lower bitrates and sample depth. To do that, I had to do two things to the samples:

1. Reduce the bit depth of the samples. This would control vertical stepping as with a traditional bitcrusher.

2. Reduce the bit rate of the samples. By default, this is 44100 or 48000 samples per second. Older systems did quite a bit less, often 8192 samples, sometimes fewer like 4096 or 8192. I want to get that old low-fi vibe as simply as possible.

imageimage

I'm not plugging into the XAudio2 effects pipeline or otherwise using any XAudio2 plumbing for the bit crusher effect here. The algorithm would be given a buffer of stereo samples to process. The buffer size will eventually be based on performance, but right now, I just have a looping sample buffer. If you want to learn how to create your own audio using XAudio2, please refer to my older post on this topic (take care to notice the comment at the bottom where I pointed out that I didn't initialize one of the values).

I'm not presenting a complete project here, but will post enough source for you to have context for what I'm doing.

The StereoSample Structure

Each sample is actually a stereo pair. Stereo oscillators? How cool :)

struct StereoSample
{
public:
SAMPLE_t Left;
SAMPLE_t Right;
};

SAMPLE_t is defined as float.

The Voice Class

The voice class represents a single voice in the synthesizer. Among other things, it includes a collection of oscillators. Right now, all three oscillators are configured to output exactly the same thing. The Render function handles that output as well as plugging in the bit crusher effect. Note that I apply the effect to the output from all three oscillators, but in the real synth, this is decoupled so I could apply it to a single oscillator in a single voice.

Voice::Voice(IXAudio2* audioEngine, int stereoBufferSize) :
_audioEngine(audioEngine),
_stereoBufferSize(stereoBufferSize)
{
_bufferData = new StereoSample[stereoBufferSize];

for (int i = 0; i < MAX_OSCILLATORS; i++)
{
_oscillators.push_back(shared_ptr<Oscillator>(new Oscillator()));
}

// set up wave format using my good friend WAVEFORMATEX
WAVEFORMATEX wfx;
wfx.wBitsPerSample = SAMPLE_BITS;
wfx.nAvgBytesPerSec = SAMPLE_RATE * SAMPLE_CHANNELS * SAMPLE_BITS / 8;
wfx.nChannels = SAMPLE_CHANNELS;
wfx.nBlockAlign = SAMPLE_CHANNELS * SAMPLE_BITS / 8;
wfx.wFormatTag = WAVE_FORMAT_IEEE_FLOAT; // or could use WAVE_FORMAT_PCM
wfx.nSamplesPerSec = SAMPLE_RATE;
wfx.cbSize = 0; // set to zero for PCM or IEEE float

DX::ThrowIfFailed(_audioEngine->CreateSourceVoice(
&_xavoice,
(WAVEFORMATEX*)&wfx,
0,
XAUDIO2_DEFAULT_FREQ_RATIO,
reinterpret_cast<IXAudio2VoiceCallback*>(&_voiceCallbackHandler),
nullptr,
nullptr));
}





void Voice::Render(long phase, float noteFrequency)
{
XAUDIO2_BUFFER buffer;

//int byteCount = sizeof(SAMPLE_t) * 2 * _stereoBufferSize;
int byteCount = sizeof(StereoSample) * _stereoBufferSize;

// zero all buffer data
memset((byte*)_bufferData, 0, byteCount);

//_oscillators[0]->Render(phase, noteFrequency, _bufferData, _stereoBufferSize);
vector<shared_ptr<Oscillator>>::const_iterator cii;
for (cii = _oscillators.begin(); cii < _oscillators.end(); cii++)
{
(*cii)->Render(phase, noteFrequency, _bufferData, _stereoBufferSize);
}

// TEMP! Bit crunching to try out audio processing
BitCruncher cruncher;
cruncher.BitDepth = 24;
cruncher.BitRate = 2048;

cruncher.ProcessSampleBuffer(phase, noteFrequency, _bufferData, _stereoBufferSize);

// TEMP Looping
// the buffer will be looped infinitely
buffer.AudioBytes = byteCount;
buffer.PlayBegin = 0;
buffer.PlayLength = 0; // play entire buffer
buffer.LoopBegin = 0;
buffer.LoopLength = 0; // loop entire buffer
buffer.LoopCount = XAUDIO2_LOOP_INFINITE;
buffer.pAudioData = (const BYTE *)_bufferData;
buffer.pContext = NULL;
buffer.Flags = 0; // this is the value I left out in the previous post

// wire up the buffer
DX::ThrowIfFailed(_xavoice->SubmitSourceBuffer(&buffer));

// start playing sound.
DX::ThrowIfFailed(_xavoice->Start(0));
}

My C++ isn't great yet, but I'm learning. I've even learned the shared_ptr and vector templates :)

The Oscillators

The oscillators have a lot more to them, but the render functions are the core:

void Oscillator::RenderSine(long initialPhaseIncrement, float noteFrequency, StereoSample* buffer, long bufferCount)
{
// fill the buffer
for (int i = 0; i < bufferCount; i++)
{
float sample = (sin((i + initialPhaseIncrement) * 2 * PI * noteFrequency / SAMPLE_RATE));

// left audio or mono
buffer->Left += sample * this->_volume;

// right audio
buffer->Right += sample * this->_volume;

buffer++;
}
}

Volume is per-oscillator. Panning etc. is not implemented in this listing. I have different render functions for each type of waveform. They are switched using a function template to point to the current render function. Thanks to everyone on Twitter last night (especially Jeremiah Morrill) for helping me sort out how to use the std::function type.

RenderFunction = std::bind<void>(
&Oscillator::RenderSine, // function pointer
this, // implicit this
std::placeholders::_1, // initialPhaseIncrement
std::placeholders::_2, // noteFrequency
std::placeholders::_3, // buffer
std::placeholders::_4); // bufferCount

The entries in the Oscillator's class definition therefore look like this:

void RenderSine(long phaseIncrement, float noteFrequency, StereoSample* buffer, long bufferCount);
void RenderPulse(long phaseIncrement, float noteFrequency, StereoSample* buffer, long bufferCount);
void RenderTriangle(long phaseIncrement, float noteFrequency, StereoSample* buffer, long bufferCount);
void RenderSawtooth(long phaseIncrement, float noteFrequency, StereoSample* buffer, long bufferCount);
void RenderRamp(long phaseIncrement, float noteFrequency, StereoSample* buffer, long bufferCount);
void RenderMultiSaw(long phaseIncrement, float noteFrequency, StereoSample* buffer, long bufferCount);
void RenderNoise(long phaseIncrement, float noteFrequency, StereoSample* buffer, long bufferCount);

typedef std::function<void(long, float, StereoSample*, const long)> RenderFunction_t;
RenderFunction_t RenderFunction;

 

The magic part is the std::function. (Aside: searching for "std::anything" will get you a nice selection of STD testing ads in the search engine sidebar).

Now, just because it sounds so good with the bit crusher, here's my noise implementation:

void Oscillator::RenderNoise(long initialPhaseIncrement, float noteFrequency, StereoSample* buffer, long bufferCount)
{
std::uniform_real_distribution<float> dist(-1.0f * _volume, 1.0f * _volume);

for (int i = 0; i < bufferCount; i++)
{
float sample = dist(_randomGenerator);

buffer->Left = sample;
buffer->Right = sample;

buffer++;
}
}

For that to work, you'll need to initialize the random number generator elsewhere in the code. I do it in the constructor:

Oscillator::Oscillator(void)
: _randomGenerator(std::time(0))
{
_volume = DEFAULT_OSCILLATOR_VOLUME;

_pan = 0.0f;

RenderFunction = std::bind<void>(
&Oscillator::RenderSine, // function pointer change to render function you want
this, // implicit this
std::placeholders::_1, // initialPhaseIncrement
std::placeholders::_2, // noteFrequency
std::placeholders::_3, // buffer
std::placeholders::_4); // bufferCount
}

x

The Bit Crusher/Cruncher

This simple class is the real point of this post. Here's the class's header file. I call it "BitCruncher" because it does bit crushing plus sample rate reduction.

#pragma once

class BitCruncher
{
public:
int BitRate; // for reducing the sample rate
int BitDepth; // for quantizing the sample values, for example, to make them 8 bit

BitCruncher(void);
~BitCruncher(void);

void ProcessSampleBuffer(long initialPhaseIncrement, float noteFrequency, StereoSample* buffer, long bufferCount);
};

And here's the implementation. Details after the listing.

#include "pch.h"
#include "BitCruncher.h"
#include <cmath>

BitCruncher::BitCruncher(void) :
BitDepth(4),
BitRate(4096)
{
}


BitCruncher::~BitCruncher(void)
{
}


#define ROUND(f) ((float)((f > 0.0) ? floor(f + 0.5) : ceil(f - 0.5)))

void BitCruncher::ProcessSampleBuffer(long initialPhaseIncrement, float noteFrequency, StereoSample* buffer, long bufferCount)
{
int max = pow(2, BitDepth) - 1;
int step = SAMPLE_RATE / BitRate;

int i = 0;
while (i < bufferCount)
{
float leftFirstSample = ROUND((buffer->Left + 1.0) * max) / max - 1.0;
float rightFirstSample = ROUND((buffer->Right + 1.0) * max) / max - 1.0;

// this loop causes us to simulate a down-sample to a lower sample rate
for (int j = 0; j < step && i < bufferCount; j++)
{
buffer->Left = leftFirstSample;
buffer->Right = rightFirstSample;

// move on
buffer++;
i++;
}
}
}

For reducing the bit rate, the current algorithm is like a sample and hold. It takes the first sample and holds it for however many steps it needs to. In fact, on my modular synth, if I patch an oscillator into the sample and hold unit, I can get pretty much the same result.

Note that this happens per-oscillator. Each voice has multiple oscillators each of which may or may not be crushed, so I don't simply submit a low bitrate buffer to XAudio2 and let it do the expansion. I'm also working on a couple other algorithms in addition to the current.

The bit depth reduction is handled by the round statements. Floating point samples are in the range of -1.0 to +1.0. I first calculate the maximum number for the specified bit depth (first statement with pow). Then, I add 1.0 to the sample to get it into the range of 0..2.0. I then map that 0..2.0 to the "max" value (which is 0..max"). Then, dividing by "max" I get back a now rounded value in the range or 0..2.0, with at most "max" unique possible values. Finally, I subtract 1.0 to get back in the range of -1.0 to +1.0.

UPDATE: @c64_gio on twitter sent me some really great tips for how I can improve this code. I especially like the iteration approach and use of vectors instead of raw bytes. You can see some of his comments here: http://pastebin.com/Q8HqHRDd.

This algorithm appears to work, so let's take a look at the results.

Results

I hooked up my Rigol to the main output of my sound card (a MOTU 828mk3) to take a look at the generated waveforms.

image

What follows are the configurations set on the BitCruncher class and a photo of the resulting wave forms.

 

BitDepth 24, BitRate, 44100. This is about as good as it gets in terms of the scope's resolution.

image

 

BitDepth 24, BitRate 8192 (very slight stepping)

image

(note that the frequency is warbling a bit between 110 and 220. I think my Rigol is confused, possibly due to me not completing cycles in the buffer)

 

BitDepth 24, BitRate 4096 (more stepping)

image

 

BitDepth 24, BitRate 2048 (a whole lot of stepping). This sounds like a sine wave with whistling harmonics over it. It reminds me (only louder) of the overtones from some 8 bit synth chips from 80's computers.

image

 

I believe this one was BitDepth 4 and BitRate 44100. Notice the flats at the peaks. This is due to rounding and contributes a square-wave overtone to the sound.

image

 

BitDepth 2, BitRate 2048 (this sounds much more like a square wave). This one had a fair bit of jitter to it, presumably because 44100 is not evenly divisible by 2048, and for the final output, I'm simply looping a buffer of 44100 * 5 seconds.

image

 

The bitrate reduction is especially interesting when applied to white noise. You totally get the Atari/C64 vibe from it.

As I do other interesting things with this synthesizer project, which will hopefully be in the Windows Store when I complete it, I'll continue to post about them here.

No downloadable source code for this project.

         
posted by Pete Brown on Sunday, January 13, 2013
filed under:          

4 comments for “A simple bitcrusher and sample rate reducer in C++ for a Windows Store App”

  1. Jeremiah Morrillsays:
    Cool post man! Forgot about bind<>, which is much cleaner than the alternative of static methods!

    I was actually looking for algo's to do this very same thing for a Win8 app I am working on, but for MediaFoundation. In my case MF was nice enough to (internally) down/up sample for me and allowed me to be lazy. :)

    If you get into doing any channel mixing, I'd be interested in algos to help with audio artifacts, like clipping *nudge, nudge* ;)


  2. svlcwuwzbqsays:
    [url=http://www.ushockeyhall.com/hall/book.cfm?p=280]Cheap Uggs Kids Morgan Outlet[/url]
    Each time the Bank of England raises the base rate to curb inflation many lenders subsequently increase the prices they charge on their mortgage products. this is most mortgages have interest rates that are calculated as the Bank of England Base Rate (BoEBR) Plus a certain percentage point for example BoEBR + 1%. A mortgage with an interest rate calculated in this way would have a rate of 6% if the BoEBR was sitting on 5%,

    Once the passenger truck is safely and securely on a jack stand, Finish this lug nuts and your tire. With the tire remote, Now you can actually get to that worn out joint. On some vehicles you might have to remove the brake caliper and rotor in order to get to the bad joint,

    2. assign: Always be prepared to let others 'handle it', Even if it is not in Plan A. Allow others to help get the joy of serving, To add their input and skills. Waterrock johnson, Whittier, NCHalf of this route uses the 26 miles of the famous Blue Ridge Parkway, Where you'll climb to the top of Waterrock Knob at an elevation of 5800 feet the very best elevation reached in this book. The other half is as diverse as it gets. you will probably tr.

    were you to strong but you are not powerful. which you were wealthy but you're not rich. you had been happy but you are not blissful. after all this, The only idea keeping the markets where they even are is the notion that the ECB will reverse their stance on money making of the debt, you should printing euros to backstop Europe's sovereigns and banking system. Literally everybody in the market is banking on this, coming from bulls buying stocks, to bears shorting the euro. basically contrarians, We have to ask the question, What if this does not happen,

    as a rule, you should have a definitive purpose for any money borrowed. It's too easy to spend the entire the amount you want on impulse purchases. unless you really need the cash for a specific reason, You might hold off in anticipation of having rebuilt your credit and can get better interest rates and terms from a bank,
    [url=http://www.ushockeyhall.com/hall/book.cfm?p=405]How To Clean Ugg Boots[/url]
    additionally, A more sophisticated form of these takes place through boot camps and weight loss spa houses. These are taken care of mostly by women and adults. In the bootcamps, rapidly overheat, The administrators of the camps are setting programs that include mountaineering, trekking, trekking, Forest back taking, And the more usual but reasonably moderate meal plans,

    Investor PerceptionInvestors clearly perceive administrative turnover to be bad news for a company. as an example, website hosts on CNBC's "Mad hard cash" Show usually exhort viewers that after a CEO leaves: steer clear. This general misperception may be the chance.

    you will find their "simple" Retirement changed into a bit more work than they expected, But mom and dad seemed to enjoy it. They got very needed to their church and people from the church worked for them during the busy times. By this time Mom and Dad had 9 grand kids who also enjoyed working and playing at the tree farm,

    It is unsurprising that again not a single Asian country made it to the Top 10. Although the economic slowdown has temporarily halted many development projects, Their poor human rights record and lack of strong enviromentally friendly policy keeps them once again from the list of ethical destinations. unlike last year, Not a single African country made it to the list because of serious violations to the basic human rights of their citizens.

    Editor's record: actually published last year, would like reprinting Tony Whitt's "an anniversary With the Superheroes" to escape into the holiday spirit once again and to give Tony a much deserved two weeks off vacation. Christmas themed stories in comics have been a tradition for as long as comics themselves have been popular. Superman first found Santa Claus as early as 1940, And ever since then each character has either gotten to play Santa, To make some some other unhappy urchin smile again, Or to somehow stop a super fiend from demolishing the true spirit of Christmas.

    Cleveland. A real estate investor was in to eliminate purchasing a 40,000 square foot mixed use building. The seller became frustrated and began to doubt the buyer's ability to purchase the building as the typical lender became cautious and dragged the process out.
    [url=http://www.ushockeyhall.com/hall/book.cfm?p=403]Discount Uggs Online[/url]
    working together with your groom requires four key skills: association, updates, Delegation, And admiration. Both show moms and riders need these traits to foster a successful marriage with their grooms. Working partnerships are vital for the riders, But the moms need a slightly different set of skills to work in working with a groom,

    Crime has come a long way to make its root firmer in the society and has forced the authorities agencies to take up arms against criminals. Some of the countries arm their police with the latest and deadly weapons, Which fall in the group of lethal force. The use of tactical weapons has become necessary because crime isn't about snatching bags anymore.

    The last case in the volume is a lot more harrowing. The heroines arrive at a house where a young man has been possessed, as well as,while Yako, Kuryu and Rasetsu are forced to work harder than usual to solve the haunting. It results in Yako and Kuryu questioning each other secrets, While Rasetsu catches a glimpse of the spirit defending Yako once again,

    2. Exfoliating loosens and rids dead skin cells. Less effort is needed to pop a pimple if the skin is clean and smooth. surprisingly, For me I begun to see the fruits of my labor with my work at home internet businesses. to that end, I am looking forward to networking with other successful people on this forum to learn how to be even more successful in making money online. I would expect to use what I will learn to help others become successful.

    seriously, in the event that, A benefit board. Having a stress free pregnancy following such a devastating loss can seem nearly impossible, But with specifics and support we can help one another through this scary time. And the prize at the conclusion will make it all worth it!Conception After a firing for Medical Reasons.

    Use the coarse side of the grater to grate the remainder dough over the top leave it loose and spread with a fork to cover any gaps. (If you run the grater under cold water sometimes it should stop the dough from sticking). Bake at 180 degrees C for 30 35 calling until golden brown.

Comment on this Post

Remember me