Reading the NTSC encoder page at http://wiki.nesdev.com/w/index.php/NTSC_video , I decided to create my own palette synthesizer based on the description. I first looked upon Blargg's nes_ntsc, but it seems to use a sinewave rather than the described squarewave for the color synthesis.
Here is my code, in C++11:
Outcome:
To quote someone, I'd say it "is looking pretty good"... OLD TEXT: Though I am not sure whether the effect of combining multiple de-emphasis bits is correct. But at least the code for doing so is neater than in nes_ntsc :)
EDIT: Not to accuse nes_ntsc of anything. I did not compare the results, and it may very well do the very same thing through precalculated mathematics; I just thought it'd be good idea to actually replicate the process in detail. Also, this function is not meant to be called for each pixel. Ideally, you would precache the colors or calculate&cache them as needed.
EDIT: Replaced the YIQ conversion matrix with the FCC one, added guard for NaNs and removed the extended-range hack. This addresses problems
pointed out in beannaich's post(s), below.
EDIT: Replaced the attenuation code with single-attenuator and updated the screenshot. This addresses the problem with whether the combination of de-emphasis bits works properly or not.
Here is my code, in C++11:
Code:
unsigned MakeRGBcolor(unsigned pixel)
{
// The input value is a NES color index (with de-emphasis bits).
// We need RGB values. Convert the index into RGB.
// For most part, this process is described at:
// http://wiki.nesdev.com/w/index.php/NTSC_video
// Decode the color index
int color = (pixel & 0x0F), level = color<0xE ? (pixel>>4) & 3 : 1;
// Voltage levels, relative to synch voltage
static const float black=.518f, white=1.962f, attenuation=.746f,
levels[8] = {.350f, .518f, .962f,1.550f, // Signal low
1.094f,1.506f,1.962f,1.962f}; // Signal high
float lo_and_hi[2] = { levels[level + 4 * (color == 0x0)],
levels[level + 4 * (color < 0xD)] };
// Calculate the luma and chroma by emulating the relevant circuits:
float y=0.f, i=0.f, q=0.f, gamma=1.8f;
auto wave = [](int p, int color) { return (color+p+8)%12 < 6; };
for(int p=0; p<12; ++p) // 12 clock cycles per pixel.
{
// NES NTSC modulator (square wave between two voltage levels):
float spot = lo_and_hi[wave(p,color)];
// De-emphasis bits attenuate a part of the signal:
if(((pixel & 0x40) && wave(p,12))
|| ((pixel & 0x80) && wave(p, 4))
|| ((pixel &0x100) && wave(p, 8))) spot *= attenuation;
// Normalize:
float v = (spot - black) / (white-black) / 12.f;
// Ideal TV NTSC demodulator:
y += v;
i += v * std::cos(3.141592653 * p / 6);
q += v * std::sin(3.141592653 * p / 6); // Or cos(... p-3 ... )
// Note: Integrating cos() and sin() for p-0.5 .. p+0.5 range gives
// the exactly same result, scaled by a factor of 2*cos(pi/12).
}
// Convert YIQ into RGB according to FCC-sanctioned conversion matrix.
auto gammafix = [=](float f) { return f < 0.f ? 0.f : std::pow(f, 2.2f / gamma); };
auto clamp = [](int v) { return v<0 ? 0 : v>255 ? 255 : v; };
unsigned rgb = 0x10000*clamp(255 * gammafix(y + 0.946882f*i + 0.623557f*q))
+ 0x00100*clamp(255 * gammafix(y + -0.274788f*i + -0.635691f*q))
+ 0x00001*clamp(255 * gammafix(y + -1.108545f*i + 1.709007f*q));
return rgb;
}
{
// The input value is a NES color index (with de-emphasis bits).
// We need RGB values. Convert the index into RGB.
// For most part, this process is described at:
// http://wiki.nesdev.com/w/index.php/NTSC_video
// Decode the color index
int color = (pixel & 0x0F), level = color<0xE ? (pixel>>4) & 3 : 1;
// Voltage levels, relative to synch voltage
static const float black=.518f, white=1.962f, attenuation=.746f,
levels[8] = {.350f, .518f, .962f,1.550f, // Signal low
1.094f,1.506f,1.962f,1.962f}; // Signal high
float lo_and_hi[2] = { levels[level + 4 * (color == 0x0)],
levels[level + 4 * (color < 0xD)] };
// Calculate the luma and chroma by emulating the relevant circuits:
float y=0.f, i=0.f, q=0.f, gamma=1.8f;
auto wave = [](int p, int color) { return (color+p+8)%12 < 6; };
for(int p=0; p<12; ++p) // 12 clock cycles per pixel.
{
// NES NTSC modulator (square wave between two voltage levels):
float spot = lo_and_hi[wave(p,color)];
// De-emphasis bits attenuate a part of the signal:
if(((pixel & 0x40) && wave(p,12))
|| ((pixel & 0x80) && wave(p, 4))
|| ((pixel &0x100) && wave(p, 8))) spot *= attenuation;
// Normalize:
float v = (spot - black) / (white-black) / 12.f;
// Ideal TV NTSC demodulator:
y += v;
i += v * std::cos(3.141592653 * p / 6);
q += v * std::sin(3.141592653 * p / 6); // Or cos(... p-3 ... )
// Note: Integrating cos() and sin() for p-0.5 .. p+0.5 range gives
// the exactly same result, scaled by a factor of 2*cos(pi/12).
}
// Convert YIQ into RGB according to FCC-sanctioned conversion matrix.
auto gammafix = [=](float f) { return f < 0.f ? 0.f : std::pow(f, 2.2f / gamma); };
auto clamp = [](int v) { return v<0 ? 0 : v>255 ? 255 : v; };
unsigned rgb = 0x10000*clamp(255 * gammafix(y + 0.946882f*i + 0.623557f*q))
+ 0x00100*clamp(255 * gammafix(y + -0.274788f*i + -0.635691f*q))
+ 0x00001*clamp(255 * gammafix(y + -1.108545f*i + 1.709007f*q));
return rgb;
}
Outcome:
To quote someone, I'd say it "is looking pretty good"... OLD TEXT: Though I am not sure whether the effect of combining multiple de-emphasis bits is correct. But at least the code for doing so is neater than in nes_ntsc :)
EDIT: Not to accuse nes_ntsc of anything. I did not compare the results, and it may very well do the very same thing through precalculated mathematics; I just thought it'd be good idea to actually replicate the process in detail. Also, this function is not meant to be called for each pixel. Ideally, you would precache the colors or calculate&cache them as needed.
EDIT: Replaced the YIQ conversion matrix with the FCC one, added guard for NaNs and removed the extended-range hack. This addresses problems
pointed out in beannaich's post(s), below.
EDIT: Replaced the attenuation code with single-attenuator and updated the screenshot. This addresses the problem with whether the combination of de-emphasis bits works properly or not.