<?xml version="1.0" encoding="UTF-8"?><rss xmlns:dc="http://purl.org/dc/elements/1.1/" xmlns:content="http://purl.org/rss/1.0/modules/content/" xmlns:atom="http://www.w3.org/2005/Atom" version="2.0" xmlns:media="http://search.yahoo.com/mrss/"><channel><title><![CDATA[subalpine circuits]]></title><description><![CDATA[Making digital instruments in Vancouver, BC.]]></description><link>https://subalpinecircuits.com/</link><image><url>https://subalpinecircuits.com/favicon.png</url><title>subalpine circuits</title><link>https://subalpinecircuits.com/</link></image><generator>Ghost 5.81</generator><lastBuildDate>Mon, 23 Feb 2026 11:48:47 GMT</lastBuildDate><atom:link href="https://subalpinecircuits.com/rss/" rel="self" type="application/rss+xml"/><ttl>60</ttl><item><title><![CDATA[Feature complete firmware!]]></title><description><![CDATA[<p>Happy holidays and happy almost 2026!</p><p>It&apos;s been a while since my last update, but things are progressing. The firmware is now essentially feature complete - not <em>complete </em>complete, there are still bugs to be fixed and rough edges to sand down - but all of the major</p>]]></description><link>https://subalpinecircuits.com/feature-complete-firmware/</link><guid isPermaLink="false">6954768dbd2c12028a88ea93</guid><dc:creator><![CDATA[Drew]]></dc:creator><pubDate>Wed, 31 Dec 2025 15:51:43 GMT</pubDate><media:content url="https://subalpinecircuits.com/content/images/2025/12/DSCF4654.JPG" medium="image"/><content:encoded><![CDATA[<img src="https://subalpinecircuits.com/content/images/2025/12/DSCF4654.JPG" alt="Feature complete firmware!"><p>Happy holidays and happy almost 2026!</p><p>It&apos;s been a while since my last update, but things are progressing. The firmware is now essentially feature complete - not <em>complete </em>complete, there are still bugs to be fixed and rough edges to sand down - but all of the major pieces are there. I&apos;ve been adding a lot the past few months and think I&apos;ve got a pretty solid feature set for launch, with plenty of room for improvement and further feature development via firmware updates. Some recent additions:</p><ul><li>MIDI clock sync for the LFO. This one was quite a lot of work, and isn&apos;t perfect yet - I actually plan to do a post just on this topic, as it&apos;s pretty interesting. What I&apos;ve got working will be sufficient for release, but there&apos;s a lot more to be done in the future to really nail it down. I take back all of the bad words I&apos;ve said about other gear I have that has MIDI clock sync issues.</li><li>Every onboard oscillator is now available for use as an LFO, including the 8-step wavetable! This, combined with MIDI clock sync and either pitch modulation or filter modulation, opens up some new sound possibilities, essentially becoming a simple 8 step sequencer with a nice tactile interface. The LFO selector also has cute icons.</li><li>One shot LFO mode. The &quot;LFO reset&quot; button is now an &quot;LFO mode&quot; button that switches between several options:<ul><li>Continuous - all voice LFOs are synced and are free running</li><li>Reset - Each voice&apos;s LFO phase resets on note start</li><li>One shot full - Each voice&apos;s LFO starts on note start and runs for a single cycle and remains at the end of the cycle</li><li>One shot half - The same as &quot;one shot full&quot; but only half the waveform. Handy if you want to use the sawtooth waveform with LFO modulation as a second envelope but don&apos;t want the negative component of the wave (especially useful for pitch modulation).</li></ul></li><li>There&apos;s a new &quot;oscillator drift&quot; button, with &quot;low&quot; and &quot;high&quot; settings. This will add a random offset to the target frequency of every new note. </li><li>The oscillator envelope modulation knob is gone, and I added a new &quot;LFO waveform&quot; knob. I just wasn&apos;t using the oscillator env modulation much, and with the one shot LFO mode you can get something approximating env modulation pretty easily.</li><li>Instead of having a button to toggle polarity of the filter envelope modulation, the filter modulation knob is now bipolar, putting both positive and negative modulation amounts on the same control. There&apos;s enough room on the knob that it still feels responsive, and having that control on a button never felt right.</li><li>Filter keyboard tracking.</li><li>Output stage hard clipper has been replaced with a proper limiter. I went back and forth on this for a while, and am pretty happy with the sound of the limiter now. Playing all 6 voices and pushing the &quot;amp&quot; knob beyond about 60% will push the limiter, but if you&apos;re only playing 1 voice it&apos;s nice to be able to get more gain out. It&apos;s pretty transparent, I decided not to try to introduce any intentional additional distortion or saturation.</li><li>Parameter display values are all 0-255 (or -127 to 127 for bipolar controls). Behind the scenes there&apos;s a little more precision, 9-ish bits, but it&apos;s not necessary to display the full precision I think (it would be a little confusing, as I need to fudge the values a bit to handle settling, debouncing, etc). Plus, there&apos;s interpolation for the important knobs, so changing the filter cutoff from 254 to 255 has no audible &quot;stepping&quot; as the values are changes gradually over a few milliseconds.</li></ul><figure class="kg-card kg-video-card kg-width-regular kg-card-hascaption" data-kg-thumbnail="https://subalpinecircuits.com/content/media/2025/12/lfo_thumb.jpg" data-kg-custom-thumbnail>
            <div class="kg-video-container">
                <video src="https://subalpinecircuits.com/content/media/2025/12/lfo.mp4" poster="https://img.spacergif.org/v1/1920x1080/0a/spacer.png" width="1920" height="1080" playsinline preload="metadata" style="background: transparent url(&apos;https://subalpinecircuits.com/content/media/2025/12/lfo_thumb.jpg&apos;) 50% 50% / cover no-repeat;"></video>
                <div class="kg-video-overlay">
                    <button class="kg-video-large-play-icon" aria-label="Play video">
                        <svg xmlns="http://www.w3.org/2000/svg" viewbox="0 0 24 24">
                            <path d="M23.14 10.608 2.253.164A1.559 1.559 0 0 0 0 1.557v20.887a1.558 1.558 0 0 0 2.253 1.392L23.14 13.393a1.557 1.557 0 0 0 0-2.785Z"/>
                        </svg>
                    </button>
                </div>
                <div class="kg-video-player-container">
                    <div class="kg-video-player">
                        <button class="kg-video-play-icon" aria-label="Play video">
                            <svg xmlns="http://www.w3.org/2000/svg" viewbox="0 0 24 24">
                                <path d="M23.14 10.608 2.253.164A1.559 1.559 0 0 0 0 1.557v20.887a1.558 1.558 0 0 0 2.253 1.392L23.14 13.393a1.557 1.557 0 0 0 0-2.785Z"/>
                            </svg>
                        </button>
                        <button class="kg-video-pause-icon kg-video-hide" aria-label="Pause video">
                            <svg xmlns="http://www.w3.org/2000/svg" viewbox="0 0 24 24">
                                <rect x="3" y="1" width="7" height="22" rx="1.5" ry="1.5"/>
                                <rect x="14" y="1" width="7" height="22" rx="1.5" ry="1.5"/>
                            </svg>
                        </button>
                        <span class="kg-video-current-time">0:00</span>
                        <div class="kg-video-time">
                            /<span class="kg-video-duration">0:32</span>
                        </div>
                        <input type="range" class="kg-video-seek-slider" max="100" value="0">
                        <button class="kg-video-playback-rate" aria-label="Adjust playback speed">1&#xD7;</button>
                        <button class="kg-video-unmute-icon" aria-label="Unmute">
                            <svg xmlns="http://www.w3.org/2000/svg" viewbox="0 0 24 24">
                                <path d="M15.189 2.021a9.728 9.728 0 0 0-7.924 4.85.249.249 0 0 1-.221.133H5.25a3 3 0 0 0-3 3v2a3 3 0 0 0 3 3h1.794a.249.249 0 0 1 .221.133 9.73 9.73 0 0 0 7.924 4.85h.06a1 1 0 0 0 1-1V3.02a1 1 0 0 0-1.06-.998Z"/>
                            </svg>
                        </button>
                        <button class="kg-video-mute-icon kg-video-hide" aria-label="Mute">
                            <svg xmlns="http://www.w3.org/2000/svg" viewbox="0 0 24 24">
                                <path d="M16.177 4.3a.248.248 0 0 0 .073-.176v-1.1a1 1 0 0 0-1.061-1 9.728 9.728 0 0 0-7.924 4.85.249.249 0 0 1-.221.133H5.25a3 3 0 0 0-3 3v2a3 3 0 0 0 3 3h.114a.251.251 0 0 0 .177-.073ZM23.707 1.706A1 1 0 0 0 22.293.292l-22 22a1 1 0 0 0 0 1.414l.009.009a1 1 0 0 0 1.405-.009l6.63-6.631A.251.251 0 0 1 8.515 17a.245.245 0 0 1 .177.075 10.081 10.081 0 0 0 6.5 2.92 1 1 0 0 0 1.061-1V9.266a.247.247 0 0 1 .073-.176Z"/>
                            </svg>
                        </button>
                        <input type="range" class="kg-video-volume-slider" max="100" value="100">
                    </div>
                </div>
            </div>
            <figcaption><p><span style="white-space: pre-wrap;">LFO selector with icons!</span></p></figcaption>
        </figure><p>The remaining work is all in the physical domain:</p><ul><li>New production candidate PCB, addressing some audio issues on power loss and moving things around to fit into an enclosure.</li><li>Design the production panel, and determine whether the manufacturing process I&apos;d like to use is feasible (JLCPCB black aluminum panel).</li><li>Iterate on the wood enclosure idea, experiment with different wood options and finishes. I know so little about woodworking, I have a lot to learn here.</li></ul><p>There will be a lot more to do that I haven&apos;t discovered yet. But completing - by one definition - the firmware feels like a big step. On track for a 2026 release!</p>]]></content:encoded></item><item><title><![CDATA[Live waveform display (aka oscilloscope)]]></title><description><![CDATA[<p>One of the nice things about building a digital synthesizer is that everything is just code. My code is responsible for generating a series of integers that my DAC turns into audio, but I can do a lot of other things along the way as long as I can deliver</p>]]></description><link>https://subalpinecircuits.com/waveform-display-aka-oscilloscope/</link><guid isPermaLink="false">683e748341887b027c1acaff</guid><dc:creator><![CDATA[Drew]]></dc:creator><pubDate>Tue, 03 Jun 2025 05:35:16 GMT</pubDate><media:content url="https://subalpinecircuits.com/content/images/2025/06/DSCF0588.JPG" medium="image"/><content:encoded><![CDATA[<img src="https://subalpinecircuits.com/content/images/2025/06/DSCF0588.JPG" alt="Live waveform display (aka oscilloscope)"><p>One of the nice things about building a digital synthesizer is that everything is just code. My code is responsible for generating a series of integers that my DAC turns into audio, but I can do a lot of other things along the way as long as I can deliver those integers to the DAC in time (or a buffer underrun will occur, something that used to be a common occurrence making computer music).</p><p>For example, I wanted to generate a realtime oscilloscope view of the waveform being generated. The final result looks pretty good:</p><figure class="kg-card kg-video-card kg-width-regular kg-card-hascaption" data-kg-thumbnail="https://subalpinecircuits.com/content/media/2025/06/blogpostvideo_thumb.jpg" data-kg-custom-thumbnail>
            <div class="kg-video-container">
                <video src="https://subalpinecircuits.com/content/media/2025/06/blogpostvideo.mp4" poster="https://img.spacergif.org/v1/1920x1080/0a/spacer.png" width="1920" height="1080" loop autoplay muted playsinline preload="metadata" style="background: transparent url(&apos;https://subalpinecircuits.com/content/media/2025/06/blogpostvideo_thumb.jpg&apos;) 50% 50% / cover no-repeat;"></video>
                <div class="kg-video-overlay">
                    <button class="kg-video-large-play-icon" aria-label="Play video">
                        <svg xmlns="http://www.w3.org/2000/svg" viewbox="0 0 24 24">
                            <path d="M23.14 10.608 2.253.164A1.559 1.559 0 0 0 0 1.557v20.887a1.558 1.558 0 0 0 2.253 1.392L23.14 13.393a1.557 1.557 0 0 0 0-2.785Z"/>
                        </svg>
                    </button>
                </div>
                <div class="kg-video-player-container kg-video-hide">
                    <div class="kg-video-player">
                        <button class="kg-video-play-icon" aria-label="Play video">
                            <svg xmlns="http://www.w3.org/2000/svg" viewbox="0 0 24 24">
                                <path d="M23.14 10.608 2.253.164A1.559 1.559 0 0 0 0 1.557v20.887a1.558 1.558 0 0 0 2.253 1.392L23.14 13.393a1.557 1.557 0 0 0 0-2.785Z"/>
                            </svg>
                        </button>
                        <button class="kg-video-pause-icon kg-video-hide" aria-label="Pause video">
                            <svg xmlns="http://www.w3.org/2000/svg" viewbox="0 0 24 24">
                                <rect x="3" y="1" width="7" height="22" rx="1.5" ry="1.5"/>
                                <rect x="14" y="1" width="7" height="22" rx="1.5" ry="1.5"/>
                            </svg>
                        </button>
                        <span class="kg-video-current-time">0:00</span>
                        <div class="kg-video-time">
                            /<span class="kg-video-duration">0:03</span>
                        </div>
                        <input type="range" class="kg-video-seek-slider" max="100" value="0">
                        <button class="kg-video-playback-rate" aria-label="Adjust playback speed">1&#xD7;</button>
                        <button class="kg-video-unmute-icon" aria-label="Unmute">
                            <svg xmlns="http://www.w3.org/2000/svg" viewbox="0 0 24 24">
                                <path d="M15.189 2.021a9.728 9.728 0 0 0-7.924 4.85.249.249 0 0 1-.221.133H5.25a3 3 0 0 0-3 3v2a3 3 0 0 0 3 3h1.794a.249.249 0 0 1 .221.133 9.73 9.73 0 0 0 7.924 4.85h.06a1 1 0 0 0 1-1V3.02a1 1 0 0 0-1.06-.998Z"/>
                            </svg>
                        </button>
                        <button class="kg-video-mute-icon kg-video-hide" aria-label="Mute">
                            <svg xmlns="http://www.w3.org/2000/svg" viewbox="0 0 24 24">
                                <path d="M16.177 4.3a.248.248 0 0 0 .073-.176v-1.1a1 1 0 0 0-1.061-1 9.728 9.728 0 0 0-7.924 4.85.249.249 0 0 1-.221.133H5.25a3 3 0 0 0-3 3v2a3 3 0 0 0 3 3h.114a.251.251 0 0 0 .177-.073ZM23.707 1.706A1 1 0 0 0 22.293.292l-22 22a1 1 0 0 0 0 1.414l.009.009a1 1 0 0 0 1.405-.009l6.63-6.631A.251.251 0 0 1 8.515 17a.245.245 0 0 1 .177.075 10.081 10.081 0 0 0 6.5 2.92 1 1 0 0 0 1.061-1V9.266a.247.247 0 0 1 .073-.176Z"/>
                            </svg>
                        </button>
                        <input type="range" class="kg-video-volume-slider" max="100" value="100">
                    </div>
                </div>
            </div>
            <figcaption><p dir="ltr"><span style="white-space: pre-wrap;">Resonant filter makes for neat shapes</span></p></figcaption>
        </figure><p>Building a single cycle oscilloscope on its own would be a challenge - a general purpose scope has to be able to determine the frequency of an incoming signal in realtime, and if it contains many frequencies, which does it pick? If you&apos;ve ever had to fiddle with the tracking on a bench scope you&apos;ll know what I mean. In my case however, I&apos;m the one generating all of those samples. I know exactly when a cycle starts and ends, and even though it&apos;s a polyphonic synthesizer (e.g. can play 6 notes at once), I can decide which single frequency waveform to display. It&apos;s just data - numbers I&apos;m already doing the work of generating to make sound, so it&apos;s just a matter of picking the right numbers and getting them to the right place for displaying a screen (along with some scaling).</p><p>The code for doing this is only a handful of lines. It&apos;s a little sloppy - the FPU on the ESP32S3 is surprisingly capable, so I&apos;m doing more floating point operations than I should, but here&apos;s the idea. In <code>Voice.h</code>, some setup:</p><figure class="kg-card kg-code-card"><pre><code class="language-C++">  float prevPhase = 0.0f;
  std::array&lt;float, 128&gt; pendingBuffer = {0};
  std::array&lt;float, 128&gt; outputBuffer = {0};
  uint8_t lastPendingBufferIndex = 0;
  </code></pre><figcaption><p><span style="white-space: pre-wrap;">Voice.h </span></p></figcaption></figure><p>And in <code>Voice::Process()</code>, which generates a sample for a single voice:</p><figure class="kg-card kg-code-card"><pre><code class="language-C++">float newPhase = this-&gt;wavetableOscs[0].GetPhase();

// if osc1 has reset, reset the output buffer
if (newPhase &lt; prevPhase) {
  outputBuffer = pendingBuffer;
  pendingBuffer = {0};
}

uint8_t index = static_cast&lt;uint8_t&gt;(newPhase * 128);

// interpolate in the case that a buffer has fewer than 128 samples
if (index - lastPendingBufferIndex &gt; 1) {
  for (uint8_t i = lastPendingBufferIndex; i &lt; index; i++) {
    pendingBuffer[i] = pendingBuffer[lastPendingBufferIndex];
  }
}

pendingBuffer[index] = processed;
lastPendingBufferIndex = index;

prevPhase = newPhase;</code></pre><figcaption><p><code spellcheck="false" style="white-space: pre-wrap;"><span>Voice::Process()</span></code></p></figcaption></figure><p>It&apos;s pretty straightforward - we initialize two buffers of size 128 (the OLED is 128 pixels wide) and a few variables for keeping track of where we are. Every time we generate a sample, we check the phase of the primary oscillator (I&apos;ve chosen to sync the oscilloscope to the primary oscillator, but it could be the secondary). If it has wrapped around, we know the pending buffer is complete - assign the output buffer and carry on. The rest of the code is just mapping the floating point phase value to the correct index in the pending buffer. This includes accounting for the case that there are fewer phase indices than the width of the display - as we go through the array, hang onto the previous sample, drop it in until we get to a new one. A possible improvement would be linear interpolation here, but a) I think it looks cool with the quantized waveform and b) I&apos;d have to rework this a bit to have dynamic length arrays - or deal with extra floating point math in the sample generation loop - and I don&apos;t really want to do either!</p><figure class="kg-card kg-video-card kg-width-regular kg-card-hascaption" data-kg-thumbnail="https://subalpinecircuits.com/content/media/2025/06/blogpostvideo2_thumb.jpg" data-kg-custom-thumbnail>
            <div class="kg-video-container">
                <video src="https://subalpinecircuits.com/content/media/2025/06/blogpostvideo2.mp4" poster="https://img.spacergif.org/v1/1920x1080/0a/spacer.png" width="1920" height="1080" loop autoplay muted playsinline preload="metadata" style="background: transparent url(&apos;https://subalpinecircuits.com/content/media/2025/06/blogpostvideo2_thumb.jpg&apos;) 50% 50% / cover no-repeat;"></video>
                <div class="kg-video-overlay">
                    <button class="kg-video-large-play-icon" aria-label="Play video">
                        <svg xmlns="http://www.w3.org/2000/svg" viewbox="0 0 24 24">
                            <path d="M23.14 10.608 2.253.164A1.559 1.559 0 0 0 0 1.557v20.887a1.558 1.558 0 0 0 2.253 1.392L23.14 13.393a1.557 1.557 0 0 0 0-2.785Z"/>
                        </svg>
                    </button>
                </div>
                <div class="kg-video-player-container kg-video-hide">
                    <div class="kg-video-player">
                        <button class="kg-video-play-icon" aria-label="Play video">
                            <svg xmlns="http://www.w3.org/2000/svg" viewbox="0 0 24 24">
                                <path d="M23.14 10.608 2.253.164A1.559 1.559 0 0 0 0 1.557v20.887a1.558 1.558 0 0 0 2.253 1.392L23.14 13.393a1.557 1.557 0 0 0 0-2.785Z"/>
                            </svg>
                        </button>
                        <button class="kg-video-pause-icon kg-video-hide" aria-label="Pause video">
                            <svg xmlns="http://www.w3.org/2000/svg" viewbox="0 0 24 24">
                                <rect x="3" y="1" width="7" height="22" rx="1.5" ry="1.5"/>
                                <rect x="14" y="1" width="7" height="22" rx="1.5" ry="1.5"/>
                            </svg>
                        </button>
                        <span class="kg-video-current-time">0:00</span>
                        <div class="kg-video-time">
                            /<span class="kg-video-duration">0:03</span>
                        </div>
                        <input type="range" class="kg-video-seek-slider" max="100" value="0">
                        <button class="kg-video-playback-rate" aria-label="Adjust playback speed">1&#xD7;</button>
                        <button class="kg-video-unmute-icon" aria-label="Unmute">
                            <svg xmlns="http://www.w3.org/2000/svg" viewbox="0 0 24 24">
                                <path d="M15.189 2.021a9.728 9.728 0 0 0-7.924 4.85.249.249 0 0 1-.221.133H5.25a3 3 0 0 0-3 3v2a3 3 0 0 0 3 3h1.794a.249.249 0 0 1 .221.133 9.73 9.73 0 0 0 7.924 4.85h.06a1 1 0 0 0 1-1V3.02a1 1 0 0 0-1.06-.998Z"/>
                            </svg>
                        </button>
                        <button class="kg-video-mute-icon kg-video-hide" aria-label="Mute">
                            <svg xmlns="http://www.w3.org/2000/svg" viewbox="0 0 24 24">
                                <path d="M16.177 4.3a.248.248 0 0 0 .073-.176v-1.1a1 1 0 0 0-1.061-1 9.728 9.728 0 0 0-7.924 4.85.249.249 0 0 1-.221.133H5.25a3 3 0 0 0-3 3v2a3 3 0 0 0 3 3h.114a.251.251 0 0 0 .177-.073ZM23.707 1.706A1 1 0 0 0 22.293.292l-22 22a1 1 0 0 0 0 1.414l.009.009a1 1 0 0 0 1.405-.009l6.63-6.631A.251.251 0 0 1 8.515 17a.245.245 0 0 1 .177.075 10.081 10.081 0 0 0 6.5 2.92 1 1 0 0 0 1.061-1V9.266a.247.247 0 0 1 .073-.176Z"/>
                            </svg>
                        </button>
                        <input type="range" class="kg-video-volume-slider" max="100" value="100">
                    </div>
                </div>
            </div>
            <figcaption><p><span style="white-space: pre-wrap;">Cool looking chunky waveform (fewer than 128 samples in a cycle)</span></p></figcaption>
        </figure><p>In the main audio loop, where we gather up and sum the samples from the voices and send samples to the DAC, we grab a pointer to the latest completed buffer from the last used voice and assign it to a variable in the <code>displayState</code> struct. This means that the scope will always be displaying the last played note, which feels correct to me (displaying a true summed output of all voices would be complicated and look less interesting).</p><figure class="kg-card kg-code-card"><pre><code class="language-C++">// update the display&apos;s scope view buffer to show the latest voice
state.displayState.outputBuffer = state.voices[state.lastVoiceIndex].GetOutputBuffer();
state.displayState.outputMultiplier = state.activePreset-&gt;amp;
</code></pre><figcaption><p><span style="white-space: pre-wrap;">Audio loop code, assigning output buffer for display</span></p></figcaption></figure><p>And finally in the display code - iterate through each horizontal pixel, clamp (in the case of a clipping sample, the filter is quite resonant) &amp; normalize the vertical pixel, then either draw it to the framebuffer directly or connect it to the previous vertical pixel with a line.</p><pre><code class="language-C++">ssd1306_clear_screen(display, 0);

uint8_t prevY;
auto outputBuffer = *state.outputBuffer;

for (int i = 0; i &lt; 128; i++) {
  // fclamp to hard clip signal
  float normalized = fclamp(((-outputBuffer[i] * state.outputMultiplier) + 1.0f) / 2.0f, 0.0f, 1.0f) * 64.f;
  uint8_t y = static_cast&lt;uint8_t&gt;(normalized);
  if (i == 0) {
    ssd1306_fill_point(display, i, y, 1);
  } else {
    if (abs(prevY - y) &gt; 1) {
      ssd1306_draw_line(display, i - 1, prevY, i, y);
    } else {
      ssd1306_fill_point(display, i, y, 1);
    }
  }
  prevY = y;
}

ssd1306_refresh_gram(display);</code></pre><p>The <a href="https://github.com/subalpinecircuits/esp-bsp-ssd1306">display libary</a> can push the framebuffer out at about 45 hz over I2C, so this looks really cool.</p><p>Here&apos;s a more interesting demo, running MIDI to the device for the bassline from my 2012 track <a href="https://grmnygrmny.bandcamp.com/track/with-you-ft-kotomi">&quot;With You ft. Kotomi&quot;</a>:</p><figure class="kg-card kg-video-card kg-width-regular kg-card-hascaption" data-kg-thumbnail="https://subalpinecircuits.com/content/media/2025/06/withu2_thumb.jpg" data-kg-custom-thumbnail>
            <div class="kg-video-container">
                <video src="https://subalpinecircuits.com/content/media/2025/06/withu2.mp4" poster="https://img.spacergif.org/v1/1920x1080/0a/spacer.png" width="1920" height="1080" playsinline preload="metadata" style="background: transparent url(&apos;https://subalpinecircuits.com/content/media/2025/06/withu2_thumb.jpg&apos;) 50% 50% / cover no-repeat;"></video>
                <div class="kg-video-overlay">
                    <button class="kg-video-large-play-icon" aria-label="Play video">
                        <svg xmlns="http://www.w3.org/2000/svg" viewbox="0 0 24 24">
                            <path d="M23.14 10.608 2.253.164A1.559 1.559 0 0 0 0 1.557v20.887a1.558 1.558 0 0 0 2.253 1.392L23.14 13.393a1.557 1.557 0 0 0 0-2.785Z"/>
                        </svg>
                    </button>
                </div>
                <div class="kg-video-player-container">
                    <div class="kg-video-player">
                        <button class="kg-video-play-icon" aria-label="Play video">
                            <svg xmlns="http://www.w3.org/2000/svg" viewbox="0 0 24 24">
                                <path d="M23.14 10.608 2.253.164A1.559 1.559 0 0 0 0 1.557v20.887a1.558 1.558 0 0 0 2.253 1.392L23.14 13.393a1.557 1.557 0 0 0 0-2.785Z"/>
                            </svg>
                        </button>
                        <button class="kg-video-pause-icon kg-video-hide" aria-label="Pause video">
                            <svg xmlns="http://www.w3.org/2000/svg" viewbox="0 0 24 24">
                                <rect x="3" y="1" width="7" height="22" rx="1.5" ry="1.5"/>
                                <rect x="14" y="1" width="7" height="22" rx="1.5" ry="1.5"/>
                            </svg>
                        </button>
                        <span class="kg-video-current-time">0:00</span>
                        <div class="kg-video-time">
                            /<span class="kg-video-duration">1:05</span>
                        </div>
                        <input type="range" class="kg-video-seek-slider" max="100" value="0">
                        <button class="kg-video-playback-rate" aria-label="Adjust playback speed">1&#xD7;</button>
                        <button class="kg-video-unmute-icon" aria-label="Unmute">
                            <svg xmlns="http://www.w3.org/2000/svg" viewbox="0 0 24 24">
                                <path d="M15.189 2.021a9.728 9.728 0 0 0-7.924 4.85.249.249 0 0 1-.221.133H5.25a3 3 0 0 0-3 3v2a3 3 0 0 0 3 3h1.794a.249.249 0 0 1 .221.133 9.73 9.73 0 0 0 7.924 4.85h.06a1 1 0 0 0 1-1V3.02a1 1 0 0 0-1.06-.998Z"/>
                            </svg>
                        </button>
                        <button class="kg-video-mute-icon kg-video-hide" aria-label="Mute">
                            <svg xmlns="http://www.w3.org/2000/svg" viewbox="0 0 24 24">
                                <path d="M16.177 4.3a.248.248 0 0 0 .073-.176v-1.1a1 1 0 0 0-1.061-1 9.728 9.728 0 0 0-7.924 4.85.249.249 0 0 1-.221.133H5.25a3 3 0 0 0-3 3v2a3 3 0 0 0 3 3h.114a.251.251 0 0 0 .177-.073ZM23.707 1.706A1 1 0 0 0 22.293.292l-22 22a1 1 0 0 0 0 1.414l.009.009a1 1 0 0 0 1.405-.009l6.63-6.631A.251.251 0 0 1 8.515 17a.245.245 0 0 1 .177.075 10.081 10.081 0 0 0 6.5 2.92 1 1 0 0 0 1.061-1V9.266a.247.247 0 0 1 .073-.176Z"/>
                            </svg>
                        </button>
                        <input type="range" class="kg-video-volume-slider" max="100" value="100">
                    </div>
                </div>
            </div>
            <figcaption><p dir="ltr"><span style="white-space: pre-wrap;">Demo with actual music!</span></p></figcaption>
        </figure>]]></content:encoded></item><item><title><![CDATA[SSD1306 display drivers and font rendering]]></title><description><![CDATA[<p>When I first started implementing the SSD1306 OLED on my prototype, I grabbed the quickest and easiest to implement driver I could find - a <a href="https://github.com/espressif/esp-bsp/tree/1452b261c778453c32a98fe897b571561db95bb5/components/ssd1306">driver Espressif shipped as part of ESP-BSP</a> that has since been removed. It worked great, updated the screen at about 40 hz, and was very</p>]]></description><link>https://subalpinecircuits.com/ssd1306-and-font-rendering/</link><guid isPermaLink="false">67fd157741887b027c1ac98b</guid><dc:creator><![CDATA[Drew]]></dc:creator><pubDate>Mon, 14 Apr 2025 15:24:41 GMT</pubDate><media:content url="https://subalpinecircuits.com/content/images/2025/04/DSCF0261.JPG" medium="image"/><content:encoded><![CDATA[<img src="https://subalpinecircuits.com/content/images/2025/04/DSCF0261.JPG" alt="SSD1306 display drivers and font rendering"><p>When I first started implementing the SSD1306 OLED on my prototype, I grabbed the quickest and easiest to implement driver I could find - a <a href="https://github.com/espressif/esp-bsp/tree/1452b261c778453c32a98fe897b571561db95bb5/components/ssd1306">driver Espressif shipped as part of ESP-BSP</a> that has since been removed. It worked great, updated the screen at about 40 hz, and was very light on resources. However, it only supports a single font, and isn&apos;t set up easily to support adding others without doing a fair amount of work to get each glyph into a specific format of C array. I wanted to add a different font, so I started looking around at other options.</p><p>That driver is no longer supported, and Espressif has replaced it with a lower level driver that doesn&apos;t actually support any fonts, just direct bitmap drawing. The recommendation is now to use <a href="https://lvgl.io">LVGL</a>, a fully featured graphics stack that has widgets, buttons, all sorts of things. So I went about implementing LVGL, which - sure enough - has good support for adding your own fonts, but can&apos;t hit more than about 18 - 20 hz on the ESP32 (running at full speed I2C, 400khz). It&apos;s also set up with its own timer and draw loop, and after fiddling around for quite a while I wasn&apos;t able to get the display to update faster. In addition, the draw loop persistently uses about 5% of a core on the ESP32 regardless of how much work there is to do - it just didn&apos;t feel like the right thing for me. Additionally, and I didn&apos;t look too much into this, when I use LVGL an audible whine comes out of my SSD1306 display. Maybe that&apos;s something I could debug, but I had already planned to move on at this point.</p><figure class="kg-card kg-image-card kg-card-hascaption"><img src="https://subalpinecircuits.com/content/images/2025/04/Screenshot-2025-04-14-at-8.18.46-AM.png" class="kg-image" alt="SSD1306 display drivers and font rendering" loading="lazy" width="1179" height="604" srcset="https://subalpinecircuits.com/content/images/size/w600/2025/04/Screenshot-2025-04-14-at-8.18.46-AM.png 600w, https://subalpinecircuits.com/content/images/size/w1000/2025/04/Screenshot-2025-04-14-at-8.18.46-AM.png 1000w, https://subalpinecircuits.com/content/images/2025/04/Screenshot-2025-04-14-at-8.18.46-AM.png 1179w" sizes="(min-width: 720px) 720px"><figcaption><span style="white-space: pre-wrap;">LVGL is... a bit much for my use case</span></figcaption></figure><p><a href="https://github.com/olikraus/u8g2">U8G2</a> is a popular library that supports dozens of small displays, including the SSD1306. I had used it before, so figured I&apos;d give it a shot again. It&apos;s going a ton of fonts built in and has a system for importing additional fonts into its internal format. It doesn&apos;t natively support ESP-IDF, but there&apos;s a <a href="https://github.com/mkfrey/u8g2-hal-esp-idf">wrapper</a> that works pretty well. However, after getting this implemented, again I hit the issue of slow update speed - the fastest I could get is around 18 hz (at 400khz I2C). After doing some research, others had noticed the same thing, but consensus seemed to be that that was good enough. Not good enough for me!</p><p>I found <a href="https://github.com/nopnop2002/esp-idf-ssd1306/tree/master">another promising SSD1306</a> driver that, when running a simple text test in isolation, could hit 30+ hz. It also had an example supporting BDF (a popular font format for vintage fonts) fonts, so seemed pretty promising. However, that example was quite slow for a reason I was <a href="https://github.com/nopnop2002/esp-idf-ssd1306/issues/46">able to fix</a> but left me a little confused. Additionally, the kerning wasn&apos;t quite right with the BDF example, and I wasn&apos;t keen on trying to fix that. And finally, when I included the driver in my full synthesizer project, it was using more resources than I expected, and wasn&apos;t drawing fast enough.</p><figure class="kg-card kg-image-card kg-card-hascaption"><img src="https://subalpinecircuits.com/content/images/2025/04/IMG_4669.jpg" class="kg-image" alt="SSD1306 display drivers and font rendering" loading="lazy" width="1949" height="1445" srcset="https://subalpinecircuits.com/content/images/size/w600/2025/04/IMG_4669.jpg 600w, https://subalpinecircuits.com/content/images/size/w1000/2025/04/IMG_4669.jpg 1000w, https://subalpinecircuits.com/content/images/size/w1600/2025/04/IMG_4669.jpg 1600w, https://subalpinecircuits.com/content/images/2025/04/IMG_4669.jpg 1949w" sizes="(min-width: 720px) 720px"><figcaption><span style="white-space: pre-wrap;">Kerning is important</span></figcaption></figure><p>At this point I was close to despair. I know AdafruitGFX is a popular graphics library, but it doesn&apos;t support ESP-IDF (only Arduino) natively. To implement it in my project I&apos;d have to bring in an Arduino compatibility layer, and even then, I don&apos;t know anything about the performance or resource usage, so it felt like a risk that might not pay off.</p><p>I decided to back to the one driver I knew worked really well - the ESP-BSP driver that has been deprecated. I had since upgraded my ESP-IDF version to 5.4.x, and could actually no longer use the driver as written, as it only supports what is now called the &quot;legacy&quot; I2C driver. So I forked the code into my own repo and replaced updated all of the I2c API calls (something I had already done with my ES8388 driver), which worked. And it was still fast! Faster than any other driver I had used. Why is it faster? My hunch is because of how it pushes framebuffer data to the I2C bus in a single transaction, whereas U8G2 pushes bytes to the display in chunks, I&apos;m guessing because of its support for many different variants of displays (I&apos;m not sure about LVGL or the others, but might be similar).</p><figure class="kg-card kg-code-card"><pre><code class="language-C">static esp_err_t ssd1306_write_data(ssd1306_handle_t dev, const uint8_t *const data, const uint16_t data_len)
{
    ssd1306_dev_t *device = (ssd1306_dev_t *) dev;
    esp_err_t ret;

    i2c_cmd_handle_t cmd = i2c_cmd_link_create();
    ret = i2c_master_start(cmd);
    assert(ESP_OK == ret);
    ret = i2c_master_write_byte(cmd, device-&gt;dev_addr | I2C_MASTER_WRITE, true);
    assert(ESP_OK == ret);
    ret = i2c_master_write_byte(cmd, SSD1306_WRITE_DAT, true);
    assert(ESP_OK == ret);
    ret = i2c_master_write(cmd, data, data_len, true);
    assert(ESP_OK == ret);
    ret = i2c_master_stop(cmd);
    assert(ESP_OK == ret);
    ret = i2c_master_cmd_begin(device-&gt;bus, cmd, 1000 / portTICK_PERIOD_MS);
    i2c_cmd_link_delete(cmd);

    return ret;
}

...

static esp_err_t ssd1306_write_data(ssd1306_handle_t dev, const uint8_t *const data, const uint16_t data_len) {
  ssd1306_dev_t *device = (ssd1306_dev_t *)dev;
  esp_err_t ret;

  uint8_t *out_buf = (uint8_t *)calloc(data_len + 1, sizeof(uint8_t));
  out_buf[0] = SSD1306_WRITE_DAT;
  memcpy(out_buf + 1, data, data_len);
  ret = i2c_master_transmit(device-&gt;i2c_dev_handle, out_buf, data_len + 1, 1000);
  free(out_buf);

  return ret;
}</code></pre><figcaption><p><span style="white-space: pre-wrap;">Legacy I2C API vs the new I2C API</span></p></figcaption></figure><p>But I was basically back to square one. I have a display driver that works great and is fast, but it only supports one font.</p><p>At this point I started thinking more about handling the font drawing myself - can I add a library to render a font to a bitmap, and then draw the bitmap directly using my driver? After some research I came across <a href="http://unhaut.epizy.com/nvbdflib/">nvbdflib</a>, which actually parses BDF fonts directly and allows you to provide your own drawing function! This seemed pretty promising - maybe I can include this library, give it a BDF font and a function that draws directly to my framebuffer, skipping the intermediate bitmap altogether.</p><figure class="kg-card kg-code-card"><pre><code class="language-C">void bdf_drawing_function(int x, int y, int c, void *ctx) {
  ssd1306_dev_t *device = (ssd1306_dev_t *)ctx;
  ssd1306_fill_point(device, x, y, c);
}

...

bdfSetDrawingFunction(bdf_drawing_function, (void *)device);
</code></pre><figcaption><p><span style="white-space: pre-wrap;">nvbdflib allows you to pass in a drawing function directly</span></p></figcaption></figure><p>It took a little fiddling - I don&apos;t have a filesystem on my device yet, so to load a BDF file into a buffer I used <a href="https://notisrac.github.io/FileToCArray/">this</a> (alternatively could compile in as an object file, but wasn&apos;t sure how to do that with ESP-IDF). It worked! It does load the entire font into memory, but the BDF format is just plain text, so I edited and trimmed it down to the 94 characters I needed. The trimmed down font I loaded in, the 8x16 IBM VGA font from 1987, was about 10 kb. I&apos;m not memory constrained in my project - CPU is my primary constraint - so this was a perfectly fine compromise for being able to drop in another font very easily without a compilation step into an intermediate format (an improvement to memory usage would be to add that compilation step, but it&apos;s not important for my use case). After some tweaks to nvbdflib to pass along the consumer&apos;s context into the provided drawing function, I had the library <a href="https://github.com/subalpinecircuits/esp-bsp-ssd1306/blob/92190097391d34518588660f6adc62b2b84c7708/ssd1306.c#L165">working nicely inside of the display driver</a>. </p><figure class="kg-card kg-code-card"><pre><code>STARTFONT 2.1
FONT -IBM-VGA-Normal-R-Normal--16-120-96-96-C-80-ISO10646-1
SIZE 12 96 96
FONTBOUNDINGBOX 8 16 0 -4
STARTPROPERTIES 34
FOUNDRY &quot;IBM&quot;
FAMILY_NAME &quot;VGA 8x16&quot;
WEIGHT_NAME &quot;Normal&quot;
SLANT &quot;R&quot;
SETWIDTH_NAME &quot;Normal&quot;
ADD_STYLE_NAME &quot;&quot;
PIXEL_SIZE 16
POINT_SIZE 120
RESOLUTION_X 96
RESOLUTION_Y 96</code></pre><figcaption><p><span style="white-space: pre-wrap;">BDF fonts are just plain text, which makes editing them easy</span></p></figcaption></figure><p>So that&apos;s where I am now - I have a SSD1306 display driver capable of hitting full speed (40 hz), but also supporting any font in BDF format! I&apos;m going to keep refining the driver and added things I need - calculate bounding box for a string, for example - but I feel good about where it is already. No big dependencies, no compatibility layers, and using modern I2C APIs. Neat!</p><figure class="kg-card kg-video-card kg-width-wide kg-card-hascaption" data-kg-thumbnail="https://subalpinecircuits.com/content/media/2025/04/Timeline-2-2_thumb.jpg" data-kg-custom-thumbnail>
            <div class="kg-video-container">
                <video src="https://subalpinecircuits.com/content/media/2025/04/Timeline-2-2.mp4" poster="https://img.spacergif.org/v1/1920x1080/0a/spacer.png" width="1920" height="1080" playsinline preload="metadata" style="background: transparent url(&apos;https://subalpinecircuits.com/content/media/2025/04/Timeline-2-2_thumb.jpg&apos;) 50% 50% / cover no-repeat;"></video>
                <div class="kg-video-overlay">
                    <button class="kg-video-large-play-icon" aria-label="Play video">
                        <svg xmlns="http://www.w3.org/2000/svg" viewbox="0 0 24 24">
                            <path d="M23.14 10.608 2.253.164A1.559 1.559 0 0 0 0 1.557v20.887a1.558 1.558 0 0 0 2.253 1.392L23.14 13.393a1.557 1.557 0 0 0 0-2.785Z"/>
                        </svg>
                    </button>
                </div>
                <div class="kg-video-player-container">
                    <div class="kg-video-player">
                        <button class="kg-video-play-icon" aria-label="Play video">
                            <svg xmlns="http://www.w3.org/2000/svg" viewbox="0 0 24 24">
                                <path d="M23.14 10.608 2.253.164A1.559 1.559 0 0 0 0 1.557v20.887a1.558 1.558 0 0 0 2.253 1.392L23.14 13.393a1.557 1.557 0 0 0 0-2.785Z"/>
                            </svg>
                        </button>
                        <button class="kg-video-pause-icon kg-video-hide" aria-label="Pause video">
                            <svg xmlns="http://www.w3.org/2000/svg" viewbox="0 0 24 24">
                                <rect x="3" y="1" width="7" height="22" rx="1.5" ry="1.5"/>
                                <rect x="14" y="1" width="7" height="22" rx="1.5" ry="1.5"/>
                            </svg>
                        </button>
                        <span class="kg-video-current-time">0:00</span>
                        <div class="kg-video-time">
                            /<span class="kg-video-duration">0:13</span>
                        </div>
                        <input type="range" class="kg-video-seek-slider" max="100" value="0">
                        <button class="kg-video-playback-rate" aria-label="Adjust playback speed">1&#xD7;</button>
                        <button class="kg-video-unmute-icon" aria-label="Unmute">
                            <svg xmlns="http://www.w3.org/2000/svg" viewbox="0 0 24 24">
                                <path d="M15.189 2.021a9.728 9.728 0 0 0-7.924 4.85.249.249 0 0 1-.221.133H5.25a3 3 0 0 0-3 3v2a3 3 0 0 0 3 3h1.794a.249.249 0 0 1 .221.133 9.73 9.73 0 0 0 7.924 4.85h.06a1 1 0 0 0 1-1V3.02a1 1 0 0 0-1.06-.998Z"/>
                            </svg>
                        </button>
                        <button class="kg-video-mute-icon kg-video-hide" aria-label="Mute">
                            <svg xmlns="http://www.w3.org/2000/svg" viewbox="0 0 24 24">
                                <path d="M16.177 4.3a.248.248 0 0 0 .073-.176v-1.1a1 1 0 0 0-1.061-1 9.728 9.728 0 0 0-7.924 4.85.249.249 0 0 1-.221.133H5.25a3 3 0 0 0-3 3v2a3 3 0 0 0 3 3h.114a.251.251 0 0 0 .177-.073ZM23.707 1.706A1 1 0 0 0 22.293.292l-22 22a1 1 0 0 0 0 1.414l.009.009a1 1 0 0 0 1.405-.009l6.63-6.631A.251.251 0 0 1 8.515 17a.245.245 0 0 1 .177.075 10.081 10.081 0 0 0 6.5 2.92 1 1 0 0 0 1.061-1V9.266a.247.247 0 0 1 .073-.176Z"/>
                            </svg>
                        </button>
                        <input type="range" class="kg-video-volume-slider" max="100" value="100">
                    </div>
                </div>
            </div>
            <figcaption><p dir="ltr"><span style="white-space: pre-wrap;">New code on the left, old on the right.</span></p></figcaption>
        </figure>]]></content:encoded></item><item><title><![CDATA[What am I making?]]></title><description><![CDATA[<p>For a little over a year now, I&apos;ve been building a digital synthesizer. I&apos;m a <a href="https://grmnygrmny.bandcamp.com" rel="noreferrer">musician &amp; artist</a> so I&apos;ve spent a lot of time with synthesizers but never tried building one of my own. I find the history of synthesis fascinating and spent</p>]]></description><link>https://subalpinecircuits.com/what-am-i-making/</link><guid isPermaLink="false">6789f5c03c58b6027d100177</guid><dc:creator><![CDATA[Drew]]></dc:creator><pubDate>Fri, 17 Jan 2025 07:11:34 GMT</pubDate><media:content url="https://subalpinecircuits.com/content/images/2025/01/DSCF0065.jpg" medium="image"/><content:encoded><![CDATA[<img src="https://subalpinecircuits.com/content/images/2025/01/DSCF0065.jpg" alt="What am I making?"><p>For a little over a year now, I&apos;ve been building a digital synthesizer. I&apos;m a <a href="https://grmnygrmny.bandcamp.com" rel="noreferrer">musician &amp; artist</a> so I&apos;ve spent a lot of time with synthesizers but never tried building one of my own. I find the history of synthesis fascinating and spent a lot of time reading about the origins of digital synthesis (I highly recommend <a href="http://sites.music.columbia.edu/cmc/courses/g6610/fall2016/week8/Musical_Applications_of_Microprocessors-Charmberlin.pdf">Musical Applications of Microprocessors</a>).</p><p>Wavetable synthesis is very simple - generate samples by continually stepping through a waveform at a speed relative to the frequency of the note being played. Modern wavetable synthesizers allow you to draw enormous, complex waveforms built of thousands of samples, computed via a variety of methods, but I was curious what it would sound like to build something using a very small wavetable. Typically you&apos;d interpolate between the values in a wavetable to smooth over the discontinuities in the resulting waveform, but I was curious what it would sound like if you didn&apos;t do that.  So, for fun, I started writing an implementation of this idea using a <a href="https://electro-smith.com/products/daisy-seed">prototyping device</a> I had sitting in a drawer.</p><p>The waveform that this creates is essentially an 8-segment step function, and there&apos;s a lot of harmonic content in each of those steps. After experimenting with a few values in code, I threw together a quick protoboard prototype using 8 slide potentiometers and mapped those sliders to values in my step function. It worked, and it was fun! It didn&apos;t really sound like what I thought it would, but I found it interesting nonetheless.</p><figure class="kg-card kg-video-card kg-width-regular kg-card-hascaption" data-kg-thumbnail="https://subalpinecircuits.com/content/media/2025/01/img_0142_thumb.jpg" data-kg-custom-thumbnail>
            <div class="kg-video-container">
                <video src="https://subalpinecircuits.com/content/media/2025/01/img_0142.mp4" poster="https://img.spacergif.org/v1/360x640/0a/spacer.png" width="360" height="640" playsinline preload="metadata" style="background: transparent url(&apos;https://subalpinecircuits.com/content/media/2025/01/img_0142_thumb.jpg&apos;) 50% 50% / cover no-repeat;"></video>
                <div class="kg-video-overlay">
                    <button class="kg-video-large-play-icon" aria-label="Play video">
                        <svg xmlns="http://www.w3.org/2000/svg" viewbox="0 0 24 24">
                            <path d="M23.14 10.608 2.253.164A1.559 1.559 0 0 0 0 1.557v20.887a1.558 1.558 0 0 0 2.253 1.392L23.14 13.393a1.557 1.557 0 0 0 0-2.785Z"/>
                        </svg>
                    </button>
                </div>
                <div class="kg-video-player-container">
                    <div class="kg-video-player">
                        <button class="kg-video-play-icon" aria-label="Play video">
                            <svg xmlns="http://www.w3.org/2000/svg" viewbox="0 0 24 24">
                                <path d="M23.14 10.608 2.253.164A1.559 1.559 0 0 0 0 1.557v20.887a1.558 1.558 0 0 0 2.253 1.392L23.14 13.393a1.557 1.557 0 0 0 0-2.785Z"/>
                            </svg>
                        </button>
                        <button class="kg-video-pause-icon kg-video-hide" aria-label="Pause video">
                            <svg xmlns="http://www.w3.org/2000/svg" viewbox="0 0 24 24">
                                <rect x="3" y="1" width="7" height="22" rx="1.5" ry="1.5"/>
                                <rect x="14" y="1" width="7" height="22" rx="1.5" ry="1.5"/>
                            </svg>
                        </button>
                        <span class="kg-video-current-time">0:00</span>
                        <div class="kg-video-time">
                            /<span class="kg-video-duration">0:29</span>
                        </div>
                        <input type="range" class="kg-video-seek-slider" max="100" value="0">
                        <button class="kg-video-playback-rate" aria-label="Adjust playback speed">1&#xD7;</button>
                        <button class="kg-video-unmute-icon" aria-label="Unmute">
                            <svg xmlns="http://www.w3.org/2000/svg" viewbox="0 0 24 24">
                                <path d="M15.189 2.021a9.728 9.728 0 0 0-7.924 4.85.249.249 0 0 1-.221.133H5.25a3 3 0 0 0-3 3v2a3 3 0 0 0 3 3h1.794a.249.249 0 0 1 .221.133 9.73 9.73 0 0 0 7.924 4.85h.06a1 1 0 0 0 1-1V3.02a1 1 0 0 0-1.06-.998Z"/>
                            </svg>
                        </button>
                        <button class="kg-video-mute-icon kg-video-hide" aria-label="Mute">
                            <svg xmlns="http://www.w3.org/2000/svg" viewbox="0 0 24 24">
                                <path d="M16.177 4.3a.248.248 0 0 0 .073-.176v-1.1a1 1 0 0 0-1.061-1 9.728 9.728 0 0 0-7.924 4.85.249.249 0 0 1-.221.133H5.25a3 3 0 0 0-3 3v2a3 3 0 0 0 3 3h.114a.251.251 0 0 0 .177-.073ZM23.707 1.706A1 1 0 0 0 22.293.292l-22 22a1 1 0 0 0 0 1.414l.009.009a1 1 0 0 0 1.405-.009l6.63-6.631A.251.251 0 0 1 8.515 17a.245.245 0 0 1 .177.075 10.081 10.081 0 0 0 6.5 2.92 1 1 0 0 0 1.061-1V9.266a.247.247 0 0 1 .073-.176Z"/>
                            </svg>
                        </button>
                        <input type="range" class="kg-video-volume-slider" max="100" value="100">
                    </div>
                </div>
            </div>
            <figcaption><p><span style="white-space: pre-wrap;">The first prototype</span></p></figcaption>
        </figure><p>From there I started thinking about how to build this into a larger device. I&apos;m a fan of polyphonic synthesizers, so I added multiple voices. I wanted a resonant filter, so I spent a lot of time experimenting with different virtual analog filter simulations (eventually landing on the <a href="http://www.cytomic.com/files/dsp/SvfLinearTrapOptimised.pdf">Cytomic SVF</a>, which sounds good and is computationally inexpensive). The Daisy platform is nice for prototyping, but I wanted to go lower level and understand both the software and hardware more. I&apos;ve built a few small projects with the ESP32 platform before, so I ported my prototype to the ESP32 and started migrating everything from C to C++20 (I&apos;m a web developer by trade, so C++ is a fun challenge). This resulted in a fair amount of my own reimplementation for things like MIDI input, but I enjoyed getting closer to the metal and losing unnecessary abstractions. I&apos;ll write another post about the software architecture, but it&apos;s all built on the ESP-IDF framework and makes a lot of use of FreeRTOS for scheduling, multitasking, etc.</p><p>It&apos;s now a year and a bit later than that and I&apos;m still just iterating on the same idea - a very simple wavetable mapped to 8 sliders, no interpolation, only anti-aliasing (a story for another post). Digital synthesis - inspired by a time when memory was greatly limited - combined with modern emulations of analog technology to make an instrument that sounds good and is fun to play and tweak. Still have a long way to go until I&apos;ve got a production ready device but it&apos;s already quite usable!</p><figure class="kg-card kg-embed-card"><iframe width="200" height="113" src="https://www.youtube.com/embed/iMsWZ0fjPzg?feature=oembed" frameborder="0" allow="accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture; web-share" referrerpolicy="strict-origin-when-cross-origin" allowfullscreen title="12/4/2024 demo"></iframe></figure>]]></content:encoded></item><item><title><![CDATA[Switching USB controller at boot on ESP32-S3]]></title><description><![CDATA[<p>I&apos;m building a digital synthesizer based on the ESP32-S3 MCU (using the ESP-IDF framework) and have been meaning to start a dev blog for a while. I couldn&apos;t figure out a good first post so I figured why not just start somewhere - let&apos;s</p>]]></description><link>https://subalpinecircuits.com/usb-controller-esp32s3/</link><guid isPermaLink="false">661aec5968dbf8014277e9ed</guid><category><![CDATA[dev]]></category><dc:creator><![CDATA[Drew]]></dc:creator><pubDate>Sat, 13 Apr 2024 20:34:33 GMT</pubDate><content:encoded><![CDATA[<p>I&apos;m building a digital synthesizer based on the ESP32-S3 MCU (using the ESP-IDF framework) and have been meaning to start a dev blog for a while. I couldn&apos;t figure out a good first post so I figured why not just start somewhere - let&apos;s talk about USB!</p><p>I&apos;ve been pondering connectivity on my synth for a while. It&apos;s got hardware MIDI, but I also figured I&apos;d take advantage of the fact that the ESP32-S3 has native USB to add USB-MIDI support. The downside of this is that I&apos;m also using this USB port for programming &amp; debugging (and plan to ship the device with the ability for users to upload firmware updates). By default the ESP32-S3 USB port exposes a <a href="https://docs.espressif.com/projects/esp-idf/en/stable/esp32s3/api-guides/usb-serial-jtag-console.html">CDC-ACM device</a> that allows you to upload &amp; debug without a USB-UART device. It&apos;s very handy - if you&apos;re designing your own board, all you need to do is run two data pins (and power &amp; ground) from the internal PHY pins to a USB connector for direct upload (including automatic reset, etc). I don&apos;t want to lose this functionality, but I want to add a default mode where my synth appears as a USB-MIDI device. What to do?</p><p>The answer turns out is pretty straightward. If you follow the <a href="https://docs.espressif.com/projects/esp-idf/en/latest/esp32s3/api-reference/peripherals/usb_device.html">USB stack docs</a>, or better yet the <a href="https://github.com/espressif/esp-idf/tree/d7ca8b94/examples/peripherals/usb/device/tusb_midi">USB-MIDI example</a> which I&apos;ve been adapting, you&apos;ll see that there&apos;s a process in your <code>app_main</code> function to initialize &amp; install the TinyUSB driver (<code>tinyusb_driver_install</code>). Calling this switches the internal USB PHY port from the CDC-ACM serial controller to the USB-OTG controller, which is run by the TinyUSB stack. So, that&apos;s handy - that means that at runtime it&apos;s possible to switch the functionality of the USB PHY.</p><p>My first attempt was then to just conditionalize calling <code>tinyusb_driver_install</code> - if you boot up holding the &quot;debug&quot; button, skip installing the TinyUSB driver, and huzzah - we have the USB Serial/JTAG controller connected to the PHY, if you boot without holding the button, we install the driver. This works, but unfortunately since the default behaviour in the ROM is to connect the serial/JTAG controller to the PHY, computers will first enumerate the device as a serial device, then again as a USB-MIDI device. This is not great for M* Macs which pop up a dialog confirming that the user wants to connect a USB device (that&apos;ll happen twice).</p><p>The solution is pretty well documented in the <a href="https://www.espressif.com/sites/default/files/documentation/esp32-s3_technical_reference_manual_en.pdf">Technical Reference Manual</a>. The important bit:</p><blockquote>After the user program is running, it can modify the initial configuration by setting registers. Specifically,&#xA0;RTC_CNTL_SW_HW_USB_PHY_SEL&#xA0;can be used to have software override the effect of&#xA0;EFUSE_USB_PHY_SEL: if this bit is set, the USB PHY selection logic will use the value of the&#xA0;RTC_CNTL_SW_USB_PHY_SEL&#xA0;bit in place of that of&#xA0;EFUSE_USB_PHY_SEL.</blockquote><p>So, sure enough, by first burning the <code>USB_PHY_SEL</code> eFuse to have the ROM connect the internal USB PHY to the USB-OTG controller, we can then either boot normally and call <code>tinyusb_driver_install</code> to run as a USB-MIDI device (with no startup enumeration as a serial device), or we can decide at startup to boot into USB Serial/JTAG controller mode by running the following:</p><pre><code class="language-C++">#include &quot;soc/rtc_cntl_reg.h&quot;

...

uint32_t new_val = REG_READ(RTC_CNTL_USB_CONF_REG) | RTC_CNTL_SW_HW_USB_PHY_SEL;
REG_WRITE(RTC_CNTL_USB_CONF_REG, new_val);</code></pre><p>I&apos;m planning on persisting the mode via an <a href="https://docs.espressif.com/projects/esp-idf/en/stable/esp32/api-reference/storage/nvs_flash.html">NVS</a> variable so I can easily stay in debugging mode. A future improvement would be to allow a user to toggle between the two modes at runtime, not just at boot - it seems this has been added to the <code>arduino-esp32</code> package <a href="https://github.com/espressif/arduino-esp32/blob/def319add8392d82892386a340f61580e708eca1/cores/esp32/esp32-hal-tinyusb.c#L480C1-L481C18">here</a> so it seems possible but requires some fiddling/restarting of peripherals, none of which is necessary for my use case. Additionally, when I ship a final product, I probably won&apos;t expose the low level USB Serial/JTAG controller in &quot;upload&quot; mode and might implement something based on DFU mode for the actual user update process.</p><p>Hope someone else finds this useful!</p>]]></content:encoded></item></channel></rss>