{"id":752,"date":"2018-09-22T12:26:52","date_gmt":"2018-09-22T19:26:52","guid":{"rendered":"http:\/\/www.earlevel.com\/main\/?p=752"},"modified":"2019-03-13T19:34:00","modified_gmt":"2019-03-14T02:34:00","slug":"wavetable-signal-to-noise-ratio","status":"publish","type":"post","link":"https:\/\/www.earlevel.com\/main\/2018\/09\/22\/wavetable-signal-to-noise-ratio\/","title":{"rendered":"Wavetable signal to noise ratio"},"content":{"rendered":"<p>In our wavetable series, we discussed what size our wavetables needed to be in order to give us an appropriate number of harmonics. But since we interpolated between adjacent table entries, the table size also dictates the signal to noise ratio of playback. A bigger (and therefore more oversampled) table will give lower interpolation error\u2014less noise. We use \u201csignal to noise ratio\u201d\u2014SNR for short\u2014as our metric for audio.<\/p>\n<p>SNR has a precise definition\u2014it\u2019s the RMS value of the signal divided by the RMS value of the noise, and we usually express the ratio in dB. We\u2019ll confine this article to sine tables, because they are useful and the ear is relatively sensitive to the purity of a sine wave.<\/p>\n<p>We could derive the relationship of table size and SNR analytically, but this article is about measurement, and it\u2019s easily extended to other types of waveforms and audio.<\/p>\n<h3>Calculating SNR<\/h3>\n<p>To calculate SNR, we need to know what part of the sampled audio is signal and what part is noise. Since we\u2019re generating the audio, it\u2019s pretty easy to know each. For example, if we interpolate our audio from a sine table, the signal part is the best precision sine calculations we can make, and the noise is that minus the wavetable-interpolated version. RMS is root-mean-squared, or taking the square roots of all the samples, producing the average, then squaring that value. We do that for both the signal and the noise, sample by sample, and divide the two\u2014and convert to dB. The greatest error between the samples will be somewhere in the middle. Picking halfway for the sine is a good guess, but we can easily take more measurements and see.<\/p>\n<p>It ends up that the table size can be relative small for an excellent SNR with linear interpolation. This shouldn\u2019t be surprising, since a sine wave is smooth and therefore the error of drawing a line between two points gets small quickly with table size. A 512 sample table is ample for direct use in audio. It yields a 97 dB SNR. While some might think that\u2019s fine for 16-bit audio but not so impressive for 24 bit, a closer look reveals just how good that SNR is.<\/p>\n<p>Keep in mind, this is a ratio of signal to noise. While the noise floor is -97 dB compared with the signal, that\u2019s not the same as saying we have a noise floor of -97 dB. The noise floor is -97 dB when the signal is 0 dB (actually, this is RMS, so a full-code sine wave is -3 dB and the noise is -100 dB). But people don\u2019t record and listen to sine waves at the loudest possible volume. When the signal is -30 dB, the noise floor is -127 dB. When the signal is disabled, the noise floor is non-existent.<\/p>\n<p>However, if that&#8217;s still not good enough for you, every doubling of the table size yields a 20 dB improvement.<\/p>\n<h3>Code<\/h3>\n<p>Here&#8217;s a simple C++ example that calculates the SNR of a sine table. Set the tableSize variable to check different table sizes (typically a power of 2, but not enforced). The span variable is the number of measurements from one table entry to the next. You can copy and paste, and execute, this code in an online compiler (search for &#8220;execute c++ online&#8221; for many options).<\/p>\n<pre lang=\"c\">#include &lt;iostream&gt;\r\n#include &lt;cmath&gt;\r\n#if !defined M_PI\r\nconst double M_PI = 3.14159265358979323846;\r\n#endif\r\nusing namespace std;\r\n\r\nint main(void) {\r\n    const long tableSize = 512;\r\n    const long span = 4;\r\n    const long len = tableSize * span;\r\n    double sigPower = 0;\r\n    double errPower = 0;\r\n    for (long idx = 0; idx &lt; len; idx++) {\t\t\r\n        long idxMod = fmod(idx, span);\r\n\r\n        double sig = sin((double)idx \/ len * 2 * M_PI);\r\n\r\n        double sin0, sin1;\r\n        if (!idxMod) {\r\n            sin0 = sig;\r\n            sin1 = sin((double)(idx + span) \/ len * 2 * M_PI);\r\n        }\r\n\r\n        double err = (sin1 - sin0) * idxMod \/ span + sin0 - sig;\r\n\r\n        sigPower += sig * sig;\r\n        errPower += err * err;\r\n    }\r\n    sigPower = sqrt(sigPower \/ len);\r\n    errPower = sqrt(errPower \/ len);\r\n\r\n    cout &lt;&lt; \"Table size: \" &lt;&lt; tableSize &lt;&lt; endl;\r\n    cout &lt;&lt; \"Signal: \" &lt;&lt; 20 * log10(sigPower) &lt;&lt; \" dB RMS\" &lt;&lt; endl;\r\n    cout &lt;&lt; \"Noise:  \" &lt;&lt; 20 * log10(errPower) &lt;&lt; \" dB RMS\" &lt;&lt; endl;\r\n    cout &lt;&lt; \"SNR:    \" &lt;&lt; 20 * log10(sigPower \/ errPower) &lt;&lt; \" dB RMS\" &lt;&lt; endl;\r\n}\r\n<\/pre>\n<h3>Quantifying the benefit of interpolation<\/h3>\n<p>This is a good opportunity to explore what linear interpolation buys us. Just change the error calculation line to \u201cdouble err = sin0 &#8211; sig;\u201d, and set span to a larger number, like 32, to get more readings between samples. Without linear interpolation, the SNR of a 512-sample table is about 43 dB, down from 97 dB, and we gain only 6 dB per table doubling.<\/p>\n<p>You can extend this comparison to other interpolation methods, but it&#8217;s clear that linear interpolation is sufficient for a sine table.<\/p>\n<h3>Extending to other waveforms<\/h3>\n<p>OK, how about other waveforms? A sawtooth wave is not as smooth as a sine, so as you might expect, it will take a larger table to yield high SNR number. Looking at it another way, the sawtooth is made up of a sine fundamental. The next harmonic is at half the amplitude, which alone would contribute half the signal and half the noise, but it\u2019s also double the frequency\u2014the equivalent of half the sine table size and therefore 20 dB worse than the fundamental is taken alone. It\u2019s a little more complicated than just summing up the errors of the component sines, though, because positive and negative errors can cancel.<\/p>\n<p>But the measurement technique is basically the same as with the sine example. The signal would be a high-resolution bandlimited sawtooth (not a naive sawtooth), and noise would be the difference between that and the interpolated values from your bandlimited sawtooth table. Left to you as an exercise, but you may be surprised at the poor numbers of a 2048 or 4096 sample table in the low octaves (where the is no oversampling). But again, the noise only occurs when you have signal, particularly when you have a bright waveform, and remains that far below it at any amplitude. It\u2019s still hard to hear the noise through the signal!<\/p>\n<p>Checking the SNR of wavetable generated by our wavetable oscillator code is a straightforward extension of the sine table code. For a wavetable of size 2048 and a given number of harmonics, for instance, create a table of size 2048 times span. Then subtract each entry of the wavetable, our &#8220;signal&#8221;, from the corresponding interpolated value for the &#8220;noise&#8221;. For instance, if tableSize is 2048 and span is 8, create a table of 16384 samples. For each signal sample <em>n<\/em>, from 0 to 16383, compare it with the linearly interpolated value between span points (compare samples 0-7 with the corresponding linear interpolation of samples 0 and 8, etc., using modulo or counters).<\/p>\n<p>It&#8217;s more code than I want to put up in this article, especially if I want to give a lot of options or waves and interpolations, but it&#8217;s easy. You might want to make a class that lets you specify a waveform, including number of harmonics and wavetable size, which creates the waveform. Create a function to do the linear interpolation (&#8220;lerp&#8221;) and possibly others (make it a class in that case); input the wavetable and span, output the computed signal and noise numbers. Then main simply makes the call to build the waveform, and another call to analyze it, and displays the results.<\/p>\n","protected":false},"excerpt":{"rendered":"<p>In our wavetable series, we discussed what size our wavetables needed to be in order to give us an appropriate number of harmonics. But since we interpolated between adjacent table entries, the table size also dictates the signal to noise &hellip; <a href=\"https:\/\/www.earlevel.com\/main\/2018\/09\/22\/wavetable-signal-to-noise-ratio\/\">Continue reading <span class=\"meta-nav\">&rarr;<\/span><\/a><\/p>\n","protected":false},"author":1,"featured_media":0,"comment_status":"open","ping_status":"open","sticky":false,"template":"","format":"standard","meta":{"footnotes":""},"categories":[4,20,24],"tags":[],"_links":{"self":[{"href":"https:\/\/www.earlevel.com\/main\/wp-json\/wp\/v2\/posts\/752"}],"collection":[{"href":"https:\/\/www.earlevel.com\/main\/wp-json\/wp\/v2\/posts"}],"about":[{"href":"https:\/\/www.earlevel.com\/main\/wp-json\/wp\/v2\/types\/post"}],"author":[{"embeddable":true,"href":"https:\/\/www.earlevel.com\/main\/wp-json\/wp\/v2\/users\/1"}],"replies":[{"embeddable":true,"href":"https:\/\/www.earlevel.com\/main\/wp-json\/wp\/v2\/comments?post=752"}],"version-history":[{"count":15,"href":"https:\/\/www.earlevel.com\/main\/wp-json\/wp\/v2\/posts\/752\/revisions"}],"predecessor-version":[{"id":864,"href":"https:\/\/www.earlevel.com\/main\/wp-json\/wp\/v2\/posts\/752\/revisions\/864"}],"wp:attachment":[{"href":"https:\/\/www.earlevel.com\/main\/wp-json\/wp\/v2\/media?parent=752"}],"wp:term":[{"taxonomy":"category","embeddable":true,"href":"https:\/\/www.earlevel.com\/main\/wp-json\/wp\/v2\/categories?post=752"},{"taxonomy":"post_tag","embeddable":true,"href":"https:\/\/www.earlevel.com\/main\/wp-json\/wp\/v2\/tags?post=752"}],"curies":[{"name":"wp","href":"https:\/\/api.w.org\/{rel}","templated":true}]}}