6

I wrote a program in c++ to generate a .wav file for an 800Hz sine wave (1 channel, 8-bit, 16000Hz sampling, 32000 samples so 2 seconds long), but when I play it or examine its spectrogram in Audacity, it has overtones.

I think the problem is with the algorithm converting the sine wave to PCM; I'm not sure where to put 'zero' displacement, at 127, or 127.5, or 128 etc.

char data[32000];
for (int j = 0; j < 32000; ++j)
{
    data[j] = (char) (round(127 + 60 * (sin(2.0 * 3.14159265358979323846264338327950 * j / 20.0))));
}

and the file produced is this: output.wav

If necessary, here's the cpp file: wavwriter.cpp

Thanks!

EDIT 2: I have changed the char to an uint8_t

uint8_t data[32000];
for (int j = 0; j < 32000; ++j)
{ 
    data[j] = round(127 + 60 * (sin(2.0 * 3.14159265358979323846264338327950 * j / 20.0)));


}
outfile.write((char *)&data[0], sizeof data);


outfile.close();
return true;

to avoid undefined behaviour. The same problem still applies.

Rob No
  • 61
  • 4
  • Is `char` signed or unsigned on your platform? – Oliver Charlesworth Apr 23 '16 at 18:10
  • What do you mean? I'm using Windows and c++. – Rob No Apr 23 '16 at 18:12
  • How much do you think `(char)(127 + 60)` is? – Ivan Aksamentov - Drop Apr 23 '16 at 18:21
  • You can't write unsigned char to ofstream. When I said 127, I meant 0x79 and 128 meant 0x80. To answer Drop, it would be -69 for a char and 187 for an unsigned char, but both would be written in the same way in hexadecimal as 0xBB. – Rob No Apr 23 '16 at 18:30
  • @user1320881 I meant the case when sin is 1 or close to this – Ivan Aksamentov - Drop Apr 23 '16 at 18:37
  • @RobNo signed overflow is UB, not `-69` – Ivan Aksamentov - Drop Apr 23 '16 at 18:37
  • So then it would be round(127+60 * 1) which gives round(127+60) which should be 187. This should be converted to BB – Rob No Apr 23 '16 at 18:38
  • It should be converted to pink elephants that poo with colorful butterflies or to any other unpredictable result, as `(char)(187)` triggers [undefined behavior](http://stackoverflow.com/questions/18195715/why-is-unsigned-integer-overflow-defined-behavior-but-signed-integer-overflow-is). In practice, on Windows x86 and x64 it might be `0xBB`, but you should not rely on that. Anyway, changing `char` to `uint8_t` everywhere along the serialization code and testing again should tell whether signedness is a problem or is it something else. – Ivan Aksamentov - Drop Apr 23 '16 at 18:43
  • But I somehow have to convert that to char, or I can't write it to ofstream; according to this https://stackoverflow.com/questions/5040920/converting-from-signed-char-to-unsigned-char-and-back-again#5042335 I can just use static_cast. – Rob No Apr 23 '16 at 18:46
  • 1
    Nothing has changed in your new code, UB just moved one line down. See [Writing unsigned chars to a binary file using write()](http://stackoverflow.com/questions/6995971/writing-unsigned-chars-to-a-binary-file-using-write) for the recipe you need. To simplify: make array to be `uint8_t` and remove `static_cast`. Apply `reinterpret_cast` to data. – Ivan Aksamentov - Drop Apr 23 '16 at 18:52
  • 1
    Just throwing out a random idea, is it possible that you get this because 8 bits are not enough for a good approximation? Do you get less overtones with 16 bits? – Asik Apr 23 '16 at 18:56
  • That's a good idea, I'll find out. – Rob No Apr 23 '16 at 18:57
  • " I can't write it to ofstream" -- yes you can, in binary mode. – 2785528 Apr 23 '16 at 19:10
  • 1
    The waveform is perfect for 8-bit PCM -- you won't be able to eliminate those smaller, higher frequency peaks, because they come from quantization noise. Of course, moving to 16 bit samples would improve this. – Christopher Oicles Apr 23 '16 at 20:30
  • Thanks, this makes sense. Just to confirm though, where should I have centred the sine wave? 127, 127.5 or 128? – Rob No Apr 23 '16 at 20:42

1 Answers1

3

You've added rounding noise and deterministic quantization noise. Your sinewave repeats in an exact integer number of samples; thus the rounding error (or difference between the float value and the UInt_8 value of your sinewave) repeats exactly periodically, which produces audible harmonics.

You can reduce this noise by using dithering and noise filtered rounding when converting your floating point sinewave into UInt_8 values.

Dithering (or adding fractional random values to every sample before rounding) removes the deterministic noise. The resulting noise will be whiter instead of made out of overtones.

Noise filtering doesn't just throw away the fractional portion from rounding, but integrates this fractional remainder with an IIR filter to move the quantization noise to where it might be less audible and less likely to create a DC offset.

hotpaw2
  • 70,107
  • 14
  • 90
  • 153