{"id":892,"date":"2019-04-28T23:40:50","date_gmt":"2019-04-29T06:40:50","guid":{"rendered":"https:\/\/www.earlevel.com\/main\/?p=892"},"modified":"2020-10-01T20:54:25","modified_gmt":"2020-10-02T03:54:25","slug":"wavetableosc-optimized","status":"publish","type":"post","link":"https:\/\/www.earlevel.com\/main\/2019\/04\/28\/wavetableosc-optimized\/","title":{"rendered":"WaveTableOsc optimized"},"content":{"rendered":"<p>The wave table oscillator developed <a href=\"\/main\/2012\/05\/25\/a-wavetable-oscillator\u2014the-code\/\">here<\/a> in 2012 is pretty lightweight, but I never took a close look at optimization at the time. An efficient design is the number one optimization, and it already had that. I was curious how much minor code tweaks would help.<\/p>\n<h3>Free wrap<\/h3>\n<p>For instance, to avoid added complexity in the original article, I didn\u2019t employ a common trick of extending the wave table an extra sample in order to avoid the need for a wrap-around test in the linear interpolation. A rewrite was a good opportunity to add it, and check on the improvement. The code change was trivial\u2014add one to the allocation size for each table, and duplicate the first sample to the last when filling them, in AddWaveTable. Then remove the bounds check in the linear interpolation, saving a test and branch for every sample generated.<\/p>\n<pre>\/\/ old\nfloat samp0 = waveTable-&gt;waveTable[intPart];\nif (++intPart &gt;= waveTable-&gt;waveTableLen)\n    intPart = 0;\nfloat samp1 = waveTable-&gt;waveTable[intPart];\n\n\/\/ new\nfloat samp0 = waveTable-&gt;waveTable[intPart];\nfloat samp1 = waveTable-&gt;waveTable[intPart + 1];\n<\/pre>\n<h3>Factor<\/h3>\n<p>And, I spotted another optimization opportunity. The oscillator needs to select the appropriate wavetable each time it calculates the next sample (GetOutput), but the choice of wavetable only changes when the frequency changes. So, the selection should be moved to SetFrequency. In general use, it should make no difference\u2014an analog-like synth would usually have continuous modulation of the oscillator, for instance. But moving the calculation to SetFrequency would be a substantial win for steady tones.<\/p>\n<p>I\u2019ll go through the changes and their contributions, but a little explanation first. Unlike most of my DSP classes that provide a new sample on each Process call, the oscillator requires two calls\u2014GetOutput and UpdatePhase\u2014every time. It was just a design choice, I could have also provided a Process call that does both. In addition, SetFrequency might also be called every time, if the modulation is constant. For a steady tone, we\u2019d call SetFrequency once, then the other two calls repeatedly for each sample. For a sweep, all three would be called for each sample. Of course I could trivially add a Process call that couples the existing UpdatePhase and GetOutput.<\/p>\n<p>The wave table selection code iterates from lowest to highest, until it finds the right table, so it\u2019s slower for higher frequencies. So, we\u2019d expect an improvement in moving that code to SetFrequency for the steady tone case, and that the amount of improvement would be greater for high tone settings. We\u2019d expect the case of a sweep to give no change in performance, since it still requires the wave table selection on each sample.<\/p>\n<pre>\/\/ old\ninline void WaveTableOsc::setFrequency(double inc) {\n  phaseInc = inc;\n}\n\n\/\/ new: the added code was simply repositioned from GetOutput\nvoid SetFrequency(double inc) {\n  mPhaseInc = inc;\n\n  \/\/ update the current wave table selector\n  int curWaveTable = 0;\n  while ((mPhaseInc &gt;= mWaveTables[curWaveTable].topFreq) &amp;&amp; (curWaveTable &lt; (mNumWaveTables - 1))) {\n    ++curWaveTable;\n  }\n  mCurWaveTable = curWaveTable;\n}\n<\/pre>\n<p>After moving the table selection from GetOutput, implementing the \u201cone extra sample\u201d optimization gave about a 20% improvement to the steady-tone case (SetFrequency not included) on its own. Unexpectedly, changing the variable temp from double to float gave a similar individual improvement. For steady tones, these changes together yielded a 30% improvement to the loop (SetFrequency not included), and 60% for high tones.<\/p>\n<p>At first test, the sweep case did not see the improvement, as the biggest change simply moved the calculation between the two routines called on each sample. I was disappointed, because I expected at least a small improvement from the other changes, but instruction pipelines can be fickle. The minor tweak of using a temporary variable to calculate mCurWaveTable changed that\u2014yielding a a 24% improvement. This is in a loop that sweeps a sawtooth exponentially through the audible range (SetFrequency, GetOutput, UpdatePhase, calculate the next frequency with a multiply, loop overhead). Basically, all the same code is executed, but apparently it worked out better for the processor. So, a great improvement overall.<\/p>\n<p>Another goal of the change was to put everything in a single header file\u2014because I like that. GetOutput was formerly not inline, so there was a small improvement moving it. I also changed the case of the function names (setFrequency to SetFrequency) to align with the way I currently write code, so it\u2019s not a drop-in replacement for the old version.<\/p>\n<p>I tried one other improvement. The wave table selection &#8220;while&#8221; loop takes longer, of course, as frequency moves to higher tables. I tried to special-case octave tables by using clever manipulation of a floating point number, essentially getting a cheap log conversion. The advantage would be a single calculation for any frequency, instead of an iterative search. It ended up not being worth it for the case of octave tables, the iterative solution was more efficient that I expected, and more importantly it&#8217;s far more general.<\/p>\n<p>Using picobench to benchmark, the new code is about 25% faster for the case of a 20-20kHz sawtooth sweep. Not a huge change, because the code was already very efficient. But as part of single-header-file rewrite without increasing complexity, that&#8217;s a pretty good bonus.<\/p>\n<p><a href=\"https:\/\/www.earlevel.com\/main\/wp-content\/uploads\/2019\/04\/WaveTableOsc.h.zip\">WaveTableOsc.h<\/a><\/p>\n","protected":false},"excerpt":{"rendered":"<p>The wave table oscillator developed here in 2012 is pretty lightweight, but I never took a close look at optimization at the time. An efficient design is the number one optimization, and it already had that. I was curious how &hellip; <a href=\"https:\/\/www.earlevel.com\/main\/2019\/04\/28\/wavetableosc-optimized\/\">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":[26,24],"tags":[],"_links":{"self":[{"href":"https:\/\/www.earlevel.com\/main\/wp-json\/wp\/v2\/posts\/892"}],"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=892"}],"version-history":[{"count":13,"href":"https:\/\/www.earlevel.com\/main\/wp-json\/wp\/v2\/posts\/892\/revisions"}],"predecessor-version":[{"id":1098,"href":"https:\/\/www.earlevel.com\/main\/wp-json\/wp\/v2\/posts\/892\/revisions\/1098"}],"wp:attachment":[{"href":"https:\/\/www.earlevel.com\/main\/wp-json\/wp\/v2\/media?parent=892"}],"wp:term":[{"taxonomy":"category","embeddable":true,"href":"https:\/\/www.earlevel.com\/main\/wp-json\/wp\/v2\/categories?post=892"},{"taxonomy":"post_tag","embeddable":true,"href":"https:\/\/www.earlevel.com\/main\/wp-json\/wp\/v2\/tags?post=892"}],"curies":[{"name":"wp","href":"https:\/\/api.w.org\/{rel}","templated":true}]}}