<?xml version="1.0" encoding="UTF-8"?>
<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom" xmlns:dc="http://purl.org/dc/elements/1.1/">
  <channel>
    <title>DEV Community: NICK</title>
    <description>The latest articles on DEV Community by NICK (@devkit).</description>
    <link>https://dev.to/devkit</link>
    <image>
      <url>https://media2.dev.to/dynamic/image/width=90,height=90,fit=cover,gravity=auto,format=auto/https:%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Fuser%2Fprofile_image%2F4184977%2F80c7d530-b690-4bd6-aa08-f4c77c0ffb02.png</url>
      <title>DEV Community: NICK</title>
      <link>https://dev.to/devkit</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/devkit"/>
    <language>en</language>
    <item>
      <title>28 milliseconds: turning drum notation into a schedule</title>
      <dc:creator>NICK</dc:creator>
      <pubDate>Fri, 06 Nov 2026 02:15:00 +0000</pubDate>
      <link>https://dev.to/devkit/28-milliseconds-turning-drum-notation-into-a-schedule-1p9e</link>
      <guid>https://dev.to/devkit/28-milliseconds-turning-drum-notation-into-a-schedule-1p9e</guid>
      <description>&lt;h1&gt;
  
  
  28 milliseconds: turning drum notation into a schedule
&lt;/h1&gt;

&lt;p&gt;Drum notation looks like it should be trivial to turn into sound. It is a grid of hits. Put a click on each one.&lt;/p&gt;

&lt;p&gt;Then you try to encode a flam, and the whole model falls apart.&lt;/p&gt;

&lt;h2&gt;
  
  
  A flam is not two hits at the same time
&lt;/h2&gt;

&lt;p&gt;On the page, a flam is two notes drawn almost on top of each other: a small grace note, then the main note. A naive reading says "play both", and if you do that literally you get a doubled click — which is not a flam, it is a mistake.&lt;/p&gt;

&lt;p&gt;What a flam actually is: one hand plays a quiet grace note slightly before the main note, so close that the ear hears a single thicker attack. The spacing is tens of milliseconds, and the volume difference is most of the effect.&lt;/p&gt;

&lt;p&gt;In the trainer I build, the spacing is 28 ms and the grace note plays at 0.10 of full amplitude against 0.18 for an ordinary note and 0.30 for an accented one. Those three numbers are the whole difference between a flam, a tap, and a flam accent. Get any of them wrong and the rudiment stops sounding like the rudiment.&lt;/p&gt;

&lt;p&gt;That was the first lesson: &lt;strong&gt;the notation is not the schedule.&lt;/strong&gt; The schedule is &lt;code&gt;(time, hand, velocity)&lt;/code&gt;, and the notation is a compressed description that has to be expanded first.&lt;/p&gt;

&lt;h2&gt;
  
  
  Encoding 40 rudiments as strings
&lt;/h2&gt;

&lt;p&gt;The data for all 40 standard rudiments is a list of short strings:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nl"&gt;n&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;Single Paradiddle&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;s&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;RLRRLRLL&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;g&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;Paradiddle&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;n&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;Flam Accent&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;       &lt;span class="na"&gt;s&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;lR L R rL R L&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;g&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;Flam&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;n&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;Flam&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;              &lt;span class="na"&gt;s&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;lR&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;g&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;Flam&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Three rules do all the work:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;A space separates steps. A step is a sixteenth note, so the grid is musical rather than absolute.&lt;/li&gt;
&lt;li&gt;An uppercase letter is a main note; the letter is the hand (R or L).&lt;/li&gt;
&lt;li&gt;A lowercase letter is a grace note, played 28 ms early and quiet.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Parsing is one line per character:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="nx"&gt;out&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;push&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt; &lt;span class="na"&gt;right&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;lower&lt;/span&gt; &lt;span class="o"&gt;===&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;r&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;grace&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;ch&lt;/span&gt; &lt;span class="o"&gt;===&lt;/span&gt; &lt;span class="nx"&gt;lower&lt;/span&gt; &lt;span class="p"&gt;});&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;A single line covers a rudiment that would take a paragraph to describe in prose. And because the strings are readable, adding a rudiment is editing a string, not writing code.&lt;/p&gt;

&lt;p&gt;The trade-off is that this encoding cannot express everything. It has no dynamics beyond accent/grace, no flams with double grace notes written differently from single ones, and no subdivision inside a step. For the standard 40 that is enough — and where it is not enough, I would rather have a second field than a more expressive string language.&lt;/p&gt;

&lt;h2&gt;
  
  
  Time is musical, not milliseconds
&lt;/h2&gt;

&lt;p&gt;The tempo is in BPM, but the scheduler thinks in sixteenth notes:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;stepMs&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="mi"&gt;60000&lt;/span&gt; &lt;span class="o"&gt;/&lt;/span&gt; &lt;span class="nx"&gt;state&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;bpm&lt;/span&gt; &lt;span class="o"&gt;/&lt;/span&gt; &lt;span class="mi"&gt;4&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Everything derived from tempo goes through that one function. Change the BPM and the whole sequence rescales, because no duration is hard-coded anywhere else. If any part of the code had stored "a step is 187.5 ms", the trainer would only work at one tempo.&lt;/p&gt;

&lt;p&gt;This is the same reason the accent pattern is an array of step indices (&lt;code&gt;acc: [0, 2, 4, 6]&lt;/code&gt;) rather than a per-note flag: the pattern has to survive a tempo change and a subdivision change without being rewritten.&lt;/p&gt;

&lt;h2&gt;
  
  
  Hearing which hand played
&lt;/h2&gt;

&lt;p&gt;Both hands play the same voice — a triangle wave with a fast envelope — but at different frequencies: 1180 Hz for the right hand, 790 Hz for the left. Roughly a fifth apart, which is enough separation to hear two distinct hands and not so much that it sounds like two different instruments.&lt;/p&gt;

&lt;p&gt;That is a small decision with a large effect on whether the tool teaches anything. If both hands sound identical, a student can hear that &lt;em&gt;something&lt;/em&gt; played and cannot hear &lt;em&gt;which hand&lt;/em&gt; played it, which is precisely the information a rudiment trainer exists to convey. Pitching the hands apart turns the audio into a check on the sticking: you can hear a wrong hand.&lt;/p&gt;

&lt;p&gt;The envelope is four milliseconds of attack into an exponential decay. Without the fast attack the clicks smear; without the decay tail they sound like a test tone rather than a stroke.&lt;/p&gt;

&lt;h2&gt;
  
  
  The same index drives the screen
&lt;/h2&gt;

&lt;p&gt;The player keeps a single index — which step is sounding — and the renderer highlights the matching span. There is no separate animation timeline to drift out of sync with the audio, because both read the same state.&lt;/p&gt;

&lt;p&gt;This matters more than it sounds. Two timelines (one audio, one visual) always separate eventually, usually right when someone is watching closely. One source of truth cannot desynchronise from itself.&lt;/p&gt;

&lt;h2&gt;
  
  
  What this design costs
&lt;/h2&gt;

&lt;p&gt;The audio is synthesised, not sampled from a real snare. That is a real limitation: a student practising to a sine click is learning the grid, not the sound. Recorded samples would sound better and would also multiply the page weight, the licensing questions, and the number of browsers that fail to decode them.&lt;/p&gt;

&lt;p&gt;The encoding is deliberately narrow, and the tool is a reference rather than a teacher — it will play a rudiment at your tempo with the hands separated, and it will not tell you that your grip is collapsing.&lt;/p&gt;

&lt;p&gt;Those are the boundaries I chose. The interesting part was not the audio code; it was discovering that most of the work sits between the notation and the schedule, in 28 milliseconds and three velocity levels.&lt;/p&gt;




&lt;p&gt;The trainer described here runs at &lt;a href="https://drumdesk.net/tools/rudiment-trainer/" rel="noopener noreferrer"&gt;drumdesk.net/tools/rudiment-trainer&lt;/a&gt; — all 40 rudiments, 40–220 BPM, hands separated, no account.&lt;/p&gt;

</description>
      <category>drum</category>
      <category>drums</category>
      <category>drumming</category>
      <category>drumtuner</category>
    </item>
    <item>
      <title>A repair manual that has to know what is broken</title>
      <dc:creator>NICK</dc:creator>
      <pubDate>Fri, 06 Nov 2026 02:07:28 +0000</pubDate>
      <link>https://dev.to/devkit/a-repair-manual-that-has-to-know-what-is-broken-39cg</link>
      <guid>https://dev.to/devkit/a-repair-manual-that-has-to-know-what-is-broken-39cg</guid>
      <description>&lt;h1&gt;
  
  
  A repair manual that has to know what is broken
&lt;/h1&gt;

&lt;p&gt;Most "how to fix X" content is a list of steps. It assumes you already know what the fault is. That assumption is where most home repairs actually fail: someone watches a video about replacing a fill valve when the real problem is the flapper, and buys the wrong part.&lt;/p&gt;

&lt;p&gt;So the design question I started from was not "how do I write instructions" but "how do I get from symptom to part without a technician". That changes the shape of everything.&lt;/p&gt;

&lt;h2&gt;
  
  
  The unit is a machine, not an article
&lt;/h2&gt;

&lt;p&gt;Each guide opens with an animated diagram of the thing the user already owns — a tap, a toilet, a drain. The parts move because the mechanism is the explanation. A static drawing of a toilet tells you nothing about why it keeps running; a diagram where the flapper lifts and the fill valve opens shows you the failure before you read a word.&lt;/p&gt;

&lt;p&gt;The diagram is not a video. Video is heavy, unsearchable, and impossible to pause on the exact frame that matters; it also cannot be re-rendered when the interface language changes. In the walkthrough the parts move as you go, so the explanation and the step are the same object rather than a picture sitting beside a paragraph.&lt;/p&gt;

&lt;h2&gt;
  
  
  Diagnosis is a tree, not a form
&lt;/h2&gt;

&lt;p&gt;The diagnosis is two or three questions, and the answers are not a form submission — they are local state that narrows a decision tree. The tree is data rather than code, roughly:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight json"&gt;&lt;code&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"id"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"toilet-running"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"question"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"Does the water stop if you lift the float?"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"yes"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"goto"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"fill-valve"&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;},&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"no"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt;  &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"goto"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"flapper-seat"&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Two properties matter here. First, the tree has to be finite and small — if a symptom needs five questions, it is usually two different faults wearing one description, and the guide should be split. Second, every leaf must point at a part the user can actually see and name, or the diagnosis is useless in practice.&lt;/p&gt;

&lt;p&gt;Everything runs client-side. There is no server deciding what to show, which means the whole thing works offline once loaded, and there is no analytics pipeline recording what broke in someone's bathroom.&lt;/p&gt;

&lt;h2&gt;
  
  
  The practice round is the part people skip
&lt;/h2&gt;

&lt;p&gt;Before the walkthrough, there is a practice round: pick the right action without hints, wrong picks cost a mark. This came from watching how people actually use repair guides — they read the steps, nod, then open the thing and immediately lose track of which screw they were on.&lt;/p&gt;

&lt;p&gt;The practice round is cheap to build (it is the same step data with the hints removed and a scoring wrapper) and it changes behaviour more than any amount of explanatory text. It is also the only part of the site that resembles a game, and I resisted making it more game-like on purpose: a repair guide that feels like a mobile game gets dismissed by the person who is standing in front of a leaking toilet at 11pm.&lt;/p&gt;

&lt;h2&gt;
  
  
  Four languages, not one language machine-translated
&lt;/h2&gt;

&lt;p&gt;The site ships in English, Chinese, French and Spanish. The guides are written per language rather than translated by pipeline — the parts have different names in different markets, and a literal translation of "flapper" produces something no French hardware shop will recognise.&lt;/p&gt;

&lt;p&gt;That is expensive, and it is the main reason the coverage is a set of common faults rather than an encyclopedia. The alternative — machine translation of 400 guides — would produce a site that ranks in four languages and helps in none.&lt;/p&gt;

&lt;h2&gt;
  
  
  What is not there
&lt;/h2&gt;

&lt;p&gt;No account, no saved history, no "your garage" dashboard. The guides run in the browser and store nothing. That is a real limitation for anyone maintaining several properties, and it is the price of not having a login wall in front of a dripping tap.&lt;/p&gt;

&lt;p&gt;The site is new, and it covers a small set of faults. It is not a substitute for a professional on gas, mains electrical, or anything structural — the site says so rather than hiding it in a footer.&lt;/p&gt;




&lt;p&gt;If you want to see how the diagnosis and the animation behave together, the tap guide is the shortest path through all of it: &lt;a href="https://supperman.net/faucet/" rel="noopener noreferrer"&gt;supperman.net/faucet&lt;/a&gt;.&lt;/p&gt;

</description>
      <category>homerepair</category>
      <category>diy</category>
      <category>plumbing</category>
      <category>appliancerepair</category>
    </item>
    <item>
      <title>Three numbers that looked right and were wrong</title>
      <dc:creator>NICK</dc:creator>
      <pubDate>Wed, 04 Nov 2026 07:54:47 +0000</pubDate>
      <link>https://dev.to/devkit/three-numbers-that-looked-right-and-were-wrong-34il</link>
      <guid>https://dev.to/devkit/three-numbers-that-looked-right-and-were-wrong-34il</guid>
      <description>&lt;h1&gt;
  
  
  Three numbers that looked right and were wrong
&lt;/h1&gt;

&lt;p&gt;Every pronunciation guide says the same thing: /θ/ is a voiceless dental fricative, put your tongue between your teeth, don't voice it. That sentence is not wrong. It is also not checkable — you cannot look at it and decide whether the advice worked, and neither can a student.&lt;/p&gt;

&lt;p&gt;So I built a reference where every number is measured instead of asserted. English phoneme symbols each get a page, a forced aligner runs over synthesised speech, and the page reports what the audio actually did: duration in milliseconds, pitch, harmonics-to-noise ratio, and for connected speech, the energy drop in decibels at a word boundary. /θ/, for instance, comes out at 77 ms mean across 12 measured tokens, with a 50–120 ms range and 13.0 dB mean HNR.&lt;/p&gt;

&lt;p&gt;The interesting part of that project was not the pipeline. It was the three places where a correct-looking number turned out to be worse than no number at all.&lt;/p&gt;

&lt;h2&gt;
  
  
  The pipeline, briefly
&lt;/h2&gt;

&lt;p&gt;Twelve English sentences, synthesised by a TTS engine in four American voices — two male, two female — 22.05 kHz mono at 64 kbps. Each sentence goes through MMS_FA for forced alignment, which gives a start and end timestamp per phone. From each aligned segment we compute duration, F0 and HNR. Everything is recomputed at build time from the audio; nothing is typed in by hand, and there is no database of "known" values to drift away from the files.&lt;/p&gt;

&lt;p&gt;That last property matters more than it sounds. If the numbers live in a table that is edited by hand, the audio files and the table can drift apart, and nothing in the build tells you which one is stale. Deriving them from the audio every time removes that failure mode instead of monitoring it.&lt;/p&gt;

&lt;h2&gt;
  
  
  Number one: the /s/ centroid that follows the vowel
&lt;/h2&gt;

&lt;p&gt;The easiest way to separate /s/ from /ʃ/ is brightness. /s/ concentrates energy higher. So compute the spectral centroid, sort, done.&lt;/p&gt;

&lt;p&gt;Except the centroid moves with the next vowel. The same /s/ measured 8466 Hz in &lt;em&gt;sun&lt;/em&gt; and 5293 Hz in &lt;em&gt;sock&lt;/em&gt;. Once you sort six minimal pairs by centroid, &lt;em&gt;shun&lt;/em&gt; at 5887 Hz comes out above &lt;em&gt;sock&lt;/em&gt; at 5293 Hz — one crossing, and the neat story collapses.&lt;/p&gt;

&lt;p&gt;The fix was to stop using a summary statistic that the following vowel can drag around. Instead the page reports the share of energy above 6 kHz. On that measure the lowest /s/ is 49%, the highest /ʃ/ is 44%, and across all six pairs there is no overlap at all. The same six pairs, the same audio, two metrics — one produces a ranking with a hole in it, the other produces a clean separation.&lt;/p&gt;

&lt;p&gt;Duration told a similar story: across all six pairs /ʃ/ is longer, with /s/ running 40–60 ms and /ʃ/ running 70–91 ms. That is a real cue, but it is not the one people teach first.&lt;/p&gt;

&lt;p&gt;The point is not "use spectral share". It is that a metric which is easy to compute and easy to explain can still be the wrong metric, and you only find out by checking whether the ranking holds across every pair rather than the pair you happened to plot.&lt;/p&gt;

&lt;h2&gt;
  
  
  Number two: the 2 ms difference we refused to print
&lt;/h2&gt;

&lt;p&gt;Elision guides say that &lt;em&gt;-ing&lt;/em&gt; becomes &lt;em&gt;-in'&lt;/em&gt; in casual speech. Fine. So measure it: 12 sentences, 6 pairs, full spelling against the apostrophe version, same four voices.&lt;/p&gt;

&lt;p&gt;Target word length came out at 334 ms mean, range 262–383 ms. And the pair that motivated the whole page — &lt;em&gt;something&lt;/em&gt; against &lt;em&gt;somethin'&lt;/em&gt; — measured 383 ms against 381 ms. Two milliseconds.&lt;/p&gt;

&lt;p&gt;Printing that would be worse than printing nothing. A reader sees "383 vs 381" and concludes there is no difference, which is false: the difference is in the transcription, not in the duration. So that cell is empty on the page, with a note explaining exactly this, and the measurement is still in the build output for anyone who wants it.&lt;/p&gt;

&lt;p&gt;There was a second trap in the same experiment. When the target word sat at the end of the sentence, the aligner stretched its end time to the end of the clip — &lt;em&gt;fishin'&lt;/em&gt; came out at 1252 ms, which is nonsense. Target words now sit mid-sentence. This is the kind of bug that never announces itself: the number is present, plausible-looking, and off by a factor of four.&lt;/p&gt;

&lt;p&gt;You can see how both decisions are rendered on the &lt;a href="https://phonemetrick.net/en/english/elision" rel="noopener noreferrer"&gt;elision page&lt;/a&gt; — the empty cells are marked as empty rather than filled with something adjacent.&lt;/p&gt;

&lt;h2&gt;
  
  
  Number three: formants on audio that cannot support them
&lt;/h2&gt;

&lt;p&gt;Weak forms are the most obviously measurable thing in connected speech: a function word gets compressed, so its duration ratio against its strong form should drop. Measured ratios came out at 0.3–0.5, often 60–80 ms — which is three or four analysis frames at a 20 ms window. Tight, but usable.&lt;/p&gt;

&lt;p&gt;Then I tried to add formants, because "the vowel centralises towards schwa" is the standard explanation and F1/F2 would show it. On 16 kHz conversational audio, twelve vowels measured in Praat, five came out clearly wrong: &lt;em&gt;tell&lt;/em&gt; returned F1 1031 Hz and F2 1235 Hz where roughly 600 and 1800 Hz were expected. At a 20 ms frame there is not enough pitch information for a reliable formant estimate.&lt;/p&gt;

&lt;p&gt;The honest conclusion was to drop the formant column for that audio rather than ship numbers that would look authoritative and be wrong a third of the time. The lesson generalises: when the recording cannot support a measurement, no amount of post-processing makes it support one. Changing the analysis method does not add information that was never captured.&lt;/p&gt;

&lt;h2&gt;
  
  
  What "measured" means for connected speech
&lt;/h2&gt;

&lt;p&gt;Linking is where most guides switch from description to judgement: "these words are linked". That is not falsifiable either. What is measurable is the energy dip at the join.&lt;/p&gt;

&lt;p&gt;For every word boundary we take the energy envelope in 5 ms windows with 2.5 ms steps, find the quietest point within ±60 ms of the boundary, and subtract it from the median energy inside the window. The result is a number in dB. A few from the &lt;a href="https://phonemetrick.net/en/english/linking" rel="noopener noreferrer"&gt;linking page&lt;/a&gt;: -2.4 dB, -1.8 dB, -0.5 dB, and one at +0.7 dB where the "join" is louder than the surroundings — sentence medians run from 12.6 to 22.0 dB.&lt;/p&gt;

&lt;p&gt;That single change — from a label to a measurement — makes the claim checkable. You can disagree with the threshold, but you can see what the audio did.&lt;/p&gt;

&lt;h2&gt;
  
  
  The part that is boring and decides whether it works
&lt;/h2&gt;

&lt;p&gt;Every figure is recomputed on each build. Where a value cannot be measured, the cell stays empty and the page says why. The interface language and the target language are separate choices, so an English speaker reading about Spanish /θ/ and a Chinese speaker reading about it get different pages, not a machine translation of one page.&lt;/p&gt;

&lt;p&gt;None of that is novel engineering. It is the ordinary discipline of not printing a number you cannot defend — which, for a reference site, turns out to be the whole product.&lt;/p&gt;

&lt;p&gt;If you want to see the metric comparison that started this, the &lt;a href="https://phonemetrick.net/en/english/sib" rel="noopener noreferrer"&gt;/s/ and /ʃ/ page&lt;/a&gt; shows both the centroid and the above-6 kHz share side by side, including the pair where the centroid ranks them the wrong way round.&lt;/p&gt;




&lt;p&gt;&lt;em&gt;All figures above are from the current build of &lt;a href="https://phonemetrick.net/en/english/" rel="noopener noreferrer"&gt;Phoneme·Trick&lt;/a&gt;, a free pronunciation reference. Audio is synthesised, not recorded from learners — which limits what the numbers can claim, and the site says so wherever the numbers are documented.&lt;/em&gt;&lt;/p&gt;

</description>
      <category>pronunciation</category>
      <category>phonetics</category>
      <category>ipa</category>
      <category>phonemes</category>
    </item>
    <item>
      <title>Three conversions that quietly break in production: cron, SQL dialects, and time zones</title>
      <dc:creator>NICK</dc:creator>
      <pubDate>Wed, 04 Nov 2026 00:57:54 +0000</pubDate>
      <link>https://dev.to/devkit/three-conversions-that-quietly-break-in-production-cron-sql-dialects-and-time-zones-3c6k</link>
      <guid>https://dev.to/devkit/three-conversions-that-quietly-break-in-production-cron-sql-dialects-and-time-zones-3c6k</guid>
      <description>&lt;h1&gt;
  
  
  Three conversions that quietly break in production: cron, SQL dialects, and time zones
&lt;/h1&gt;

&lt;p&gt;Every team has a small set of conversions that nobody owns. They get pasted into a scratch file, run once, and forgotten — until the one month a year when they are wrong.&lt;/p&gt;

&lt;p&gt;I collected three of them into browser tools because I kept re-deriving them, and each one has a failure mode that only shows up at a specific time of year or on a specific engine. The failure modes are more interesting than the tools, so here they are.&lt;/p&gt;

&lt;h2&gt;
  
  
  1. Cron: the expression is right and the job still doesn't run
&lt;/h2&gt;

&lt;p&gt;&lt;code&gt;0 2 * * *&lt;/code&gt; means "2am every day". In a timezone with daylight saving, it means "2am every day except the day the clocks jump", because &lt;strong&gt;02:00 does not exist on that date&lt;/strong&gt;. Many schedulers skip the run entirely; some run it twice on the way back.&lt;/p&gt;

&lt;p&gt;The mistake is treating a cron expression as a time. It is a &lt;strong&gt;local calendar rule&lt;/strong&gt;, and local calendars have holes and duplicates:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Spring forward: 02:00–03:00 never happens. A job pinned to 02:30 has no valid instant.&lt;/li&gt;
&lt;li&gt;Fall back: 01:00–02:00 happens twice. A job pinned to 01:30 has two valid instants.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;So a useful cron explainer has to do two things beyond translation:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Resolve the expression against a real timezone&lt;/strong&gt;, not against UTC arithmetic. "Next 5 runs" is only useful in the timezone the operator lives in, with DST rules applied to each occurrence independently.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Show the hole.&lt;/strong&gt; If the next occurrence lands in a spring-forward gap, say so, rather than silently skipping to the day after — that is the difference between a bug report and a mystery.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;The other thing worth surfacing is the &lt;em&gt;count&lt;/em&gt; of runs: &lt;code&gt;*/15&lt;/code&gt; in the minute field is 96 runs a day, and &lt;code&gt;0 0 1 * *&lt;/code&gt; is 12 a year. Half of "why is our queue flooded" tickets are somebody reading a cron as "every hour".&lt;/p&gt;

&lt;p&gt;I built a cron explainer that prints the next runs in your own zone with DST applied, and flags the DST edge case explicitly: &lt;a href="https://1024toolkit.com/tools/cron" rel="noopener noreferrer"&gt;Cron Expression Explainer&lt;/a&gt;.&lt;/p&gt;

&lt;h2&gt;
  
  
  2. SQL dialects: silent disagreement is worse than a syntax error
&lt;/h2&gt;

&lt;p&gt;Every ORM promises dialect independence, and every project with more than one database eventually hits the seams. The tricky ones are not the syntax errors — those you find in CI. The tricky ones are the constructs that are &lt;strong&gt;valid in both dialects and mean different things&lt;/strong&gt;:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Construct&lt;/th&gt;
&lt;th&gt;MySQL&lt;/th&gt;
&lt;th&gt;PostgreSQL&lt;/th&gt;
&lt;th&gt;SQLite&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;"double quotes"&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;string literal (unless &lt;code&gt;ANSI_QUOTES&lt;/code&gt;)&lt;/td&gt;
&lt;td&gt;identifier&lt;/td&gt;
&lt;td&gt;identifier&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;LIMIT 10, 20&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;offset 10, count 20&lt;/td&gt;
&lt;td&gt;syntax error&lt;/td&gt;
&lt;td&gt;offset-comma supported&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;
&lt;code&gt;TRUE&lt;/code&gt; / &lt;code&gt;FALSE&lt;/code&gt;
&lt;/td&gt;
&lt;td&gt;tinyint 1/0&lt;/td&gt;
&lt;td&gt;real boolean&lt;/td&gt;
&lt;td&gt;1/0 stored as integer&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;INSERT ... ON DUPLICATE KEY&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;native upsert&lt;/td&gt;
&lt;td&gt;needs &lt;code&gt;ON CONFLICT&lt;/code&gt;
&lt;/td&gt;
&lt;td&gt;
&lt;code&gt;ON CONFLICT&lt;/code&gt; or &lt;code&gt;INSERT OR REPLACE&lt;/code&gt;
&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Implicit string→number cast&lt;/td&gt;
&lt;td&gt;permissive&lt;/td&gt;
&lt;td&gt;strict, errors&lt;/td&gt;
&lt;td&gt;permissive&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;The &lt;code&gt;"double quotes"&lt;/code&gt; row is the one that gets people. A query written against MySQL that quotes a string with double quotes will, after a migration, be interpreted as an identifier — and depending on the driver's quoting behaviour, it can fail loudly or return something plausible. &lt;strong&gt;Plausible is the dangerous outcome.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;That is why a converter that guesses is worse than one that refuses. When I built the &lt;a href="https://1024toolkit.com/tools/sql" rel="noopener noreferrer"&gt;SQL formatter/dialect converter&lt;/a&gt;, the rule I settled on was: translate what is provably equivalent, list every change made, and &lt;strong&gt;flag anything that cannot be translated safely instead of approximating it&lt;/strong&gt;. A converter that hands you SQL and says "3 constructs were rewritten, 1 could not be translated" is usable in a migration review. One that silently produces something that runs is a trap.&lt;/p&gt;

&lt;h2&gt;
  
  
  3. Time zones: offsets are not time zones
&lt;/h2&gt;

&lt;p&gt;The classic bug: someone stores a timestamp with an offset, or worse, converts to a fixed offset once and stores that. Then a date six months later is off by an hour, because the offset was a property of one moment, not of the zone.&lt;/p&gt;

&lt;p&gt;Two facts that make this concrete:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;An offset is a snapshot; a zone is a rule set.&lt;/strong&gt; &lt;code&gt;2026-03-29T01:30+01:00&lt;/code&gt; tells you one instant. &lt;code&gt;Europe/London&lt;/code&gt; tells you how to interpret every wall-clock time in that zone — differently before and after the transition.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Wall-clock times can be missing or duplicated.&lt;/strong&gt; Same holes as cron. If a user types &lt;code&gt;2026-03-29 01:30&lt;/code&gt; in London, that time does not exist; a naive parser will happily produce an instant an hour off, and the confirmation email will say the wrong thing.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The practical rules I keep re-learning:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Store UTC instants plus the &lt;strong&gt;IANA zone name&lt;/strong&gt; if you need the local wall clock later. Never store a derived offset as the source of truth.&lt;/li&gt;
&lt;li&gt;When parsing user input, treat "this time does not exist" and "this time is ambiguous" as &lt;strong&gt;validation errors you must decide about&lt;/strong&gt; — not as parser edge cases. Pick a policy (shift forward / pick the first occurrence / reject), and put it in the code where a reviewer can see it.&lt;/li&gt;
&lt;li&gt;When displaying, convert at the edge. Formatting in the database is where zone rules go to die.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;The &lt;a href="https://1024toolkit.com/tools/timezone" rel="noopener noreferrer"&gt;time zone converter&lt;/a&gt; exists because I wanted to see the failure rather than have it explained: enter a wall-clock time and a zone, and if that time falls in a gap or a duplicate, it says so instead of picking silently.&lt;/p&gt;

&lt;h2&gt;
  
  
  Why these live in the browser
&lt;/h2&gt;

&lt;p&gt;All three are pure functions: text in, text out. There is no reason for a server to be involved, and a real cost if one is — anything pasted into a "quick converter" is often a production fragment, a customer list, or a connection string.&lt;/p&gt;

&lt;p&gt;So the constraint I set was: &lt;strong&gt;the input never leaves the page&lt;/strong&gt;. No upload, no account, no analytics on the content. It simplifies the security story to nothing and removes the temptation to log request bodies "for debugging".&lt;/p&gt;

&lt;p&gt;The trade-offs are honest ones: no server means all the parsing happens in JavaScript on the user's machine, so heavy inputs are limited by their browser, and there is no API for scripting. For a pasted cron expression or a 200-line SQL file, that is the right trade.&lt;/p&gt;

&lt;p&gt;The other two conversions in the same family — &lt;a href="https://1024toolkit.com/tools/dataconvert" rel="noopener noreferrer"&gt;JSON/YAML/TOML&lt;/a&gt; with errors pinned to line and column, and &lt;a href="https://1024toolkit.com/tools/subnet" rel="noopener noreferrer"&gt;IP subnet&lt;/a&gt; with IPv6 — follow the same rule: show the failure precisely, do not paper over it.&lt;/p&gt;

&lt;p&gt;If you have a fourth conversion in this category — one where being silently plausible is worse than erroring — I would like to hear which one, because that is the list I am building next.&lt;/p&gt;

</description>
      <category>calculator</category>
      <category>converter</category>
      <category>onlinetools</category>
      <category>productivity</category>
    </item>
    <item>
      <title>setInterval can't keep time: building a metronome that doesn't drift</title>
      <dc:creator>NICK</dc:creator>
      <pubDate>Mon, 26 Oct 2026 00:46:29 +0000</pubDate>
      <link>https://dev.to/devkit/setinterval-cant-keep-time-building-a-metronome-that-doesnt-drift-372a</link>
      <guid>https://dev.to/devkit/setinterval-cant-keep-time-building-a-metronome-that-doesnt-drift-372a</guid>
      <description>&lt;p&gt;Open a metronome built with &lt;code&gt;setInterval&lt;/code&gt;, set it to 120 BPM, leave it running for two minutes, then play a click track next to it. They will not still be together.&lt;/p&gt;

&lt;p&gt;This is not a bug in the metronome. It is &lt;code&gt;setInterval&lt;/code&gt; doing exactly what it promises, which is "run this callback roughly every N milliseconds, if you can." A metronome needs something much stronger: "produce a sound at this exact point in time, no matter what else the page is doing."&lt;/p&gt;

&lt;p&gt;I ran into this when building &lt;a href="https://drumdesk.net" rel="noopener noreferrer"&gt;DrumDesk&lt;/a&gt;, a couple of browser-based practice tools. The fix is older than most of the frameworks I use, and it is worth writing down because the wrong version sounds &lt;em&gt;almost&lt;/em&gt; right — which is worse than sounding obviously broken.&lt;/p&gt;

&lt;h2&gt;
  
  
  The three ways &lt;code&gt;setInterval&lt;/code&gt; loses
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;1. The main thread owns it.&lt;/strong&gt; &lt;code&gt;setInterval&lt;/code&gt; callbacks go into the same queue as your event handlers, layout, garbage collection, and third-party scripts. If the browser spends 180ms doing layout, your click is 180ms late. It does not get rescheduled to catch up — it just arrives late, and then the next one arrives on the new, slightly wrong schedule.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;2. Background tabs get throttled.&lt;/strong&gt; Browsers clamp timers in inactive tabs — historically to 1 second, and still to at least a few hundred milliseconds in most cases. A user who switches to their email to look up a tempo marking comes back to a metronome that is no longer a metronome.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;3. Errors accumulate.&lt;/strong&gt; &lt;code&gt;setInterval(fn, 500)&lt;/code&gt; does not mean "call &lt;code&gt;fn&lt;/code&gt; every 500ms of real time." It means "wait at least 500ms between calls." Every delay is added to the timeline permanently. Small delays are the norm, so the drift is monotonic — it only ever goes one way.&lt;/p&gt;

&lt;p&gt;The third one is what makes it insidious. A metronome that is 8ms fast sounds fine. A metronome that gets 8ms slower every bar is a different instrument after a minute.&lt;/p&gt;

&lt;h2&gt;
  
  
  Two clocks, and only one of them can be trusted
&lt;/h2&gt;

&lt;p&gt;The thing that changed my approach: &lt;strong&gt;Web Audio has its own clock.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;&lt;code&gt;AudioContext.currentTime&lt;/code&gt; is driven by the audio hardware, not by the main thread. It advances in real time even when your JavaScript is blocked, because the audio pipeline runs on a separate real-time-priority thread. If the main thread stalls for 200ms, &lt;code&gt;currentTime&lt;/code&gt; keeps moving and the sounds you already scheduled still fire exactly when you scheduled them.&lt;/p&gt;

&lt;p&gt;So the architecture becomes:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;A slow, sloppy timer&lt;/strong&gt; that periodically wakes up and asks "do I need to schedule more sounds yet?" It is allowed to be late. Nobody hears it.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;The audio clock&lt;/strong&gt; that decides exactly &lt;em&gt;when&lt;/em&gt; each sound plays. It is never late.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;This is the pattern Chris Wilson described as "A Tale of Two Clocks" in 2013, and it is still the answer.&lt;/p&gt;

&lt;h2&gt;
  
  
  The scheduler
&lt;/h2&gt;

&lt;p&gt;The trick is a lookahead queue. Every 25ms, look 150ms into the future and schedule everything that falls inside that window. Then go back to sleep.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="kd"&gt;var&lt;/span&gt; &lt;span class="nx"&gt;nextTime&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;ctx&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;currentTime&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="mf"&gt;0.05&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;   &lt;span class="c1"&gt;// when the next note should sound&lt;/span&gt;
&lt;span class="kd"&gt;var&lt;/span&gt; &lt;span class="nx"&gt;LOOKAHEAD&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mf"&gt;0.15&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;                    &lt;span class="c1"&gt;// seconds of future to pre-schedule&lt;/span&gt;
&lt;span class="kd"&gt;var&lt;/span&gt; &lt;span class="nx"&gt;INTERVAL&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;25&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;                       &lt;span class="c1"&gt;// ms between wake-ups&lt;/span&gt;

&lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;schedule&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="k"&gt;while &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;nextTime&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;&lt;/span&gt; &lt;span class="nx"&gt;ctx&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;currentTime&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="nx"&gt;LOOKAHEAD&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="nf"&gt;click&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;nextTime&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="cm"&gt;/* isRightHand */&lt;/span&gt; &lt;span class="kc"&gt;true&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mf"&gt;0.35&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="kc"&gt;false&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="nx"&gt;nextTime&lt;/span&gt; &lt;span class="o"&gt;+=&lt;/span&gt; &lt;span class="nf"&gt;stepSeconds&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;           &lt;span class="c1"&gt;// one subdivision&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;
  &lt;span class="nx"&gt;timer&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;setTimeout&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;schedule&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;INTERVAL&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Two numbers, two different jobs:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;code&gt;INTERVAL = 25&lt;/code&gt; can drift, jitter, or arrive 60ms late. It does not matter, because its only job is to keep the buffer full.&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;LOOKAHEAD = 0.15&lt;/code&gt; is the safety margin. Any main-thread stall shorter than 150ms is completely absorbed. A stall longer than that will produce a gap — but that is a genuinely broken page, not a busy one.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The &lt;code&gt;while&lt;/code&gt; loop is the important part. If the timer fires late, the loop runs several times in one go and catches up. Compare that to &lt;code&gt;setInterval&lt;/code&gt;, which can only ever fire once per tick and therefore falls further behind forever.&lt;/p&gt;

&lt;h2&gt;
  
  
  Making a click that doesn't sound like a click
&lt;/h2&gt;

&lt;p&gt;Scheduling a sample-accurate start time is half of it. The other half is making the sound not be a pop.&lt;/p&gt;

&lt;p&gt;A bare oscillator started and stopped abruptly produces a discontinuity in the waveform, which you hear as a click — an ugly one, distinct from the click you actually wanted. The fix is a fast envelope:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;click&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;at&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;isRight&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;vol&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;long&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="kd"&gt;var&lt;/span&gt; &lt;span class="nx"&gt;dur&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;long&lt;/span&gt; &lt;span class="p"&gt;?&lt;/span&gt; &lt;span class="mf"&gt;0.16&lt;/span&gt; &lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mf"&gt;0.07&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="kd"&gt;var&lt;/span&gt; &lt;span class="nx"&gt;osc&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;ctx&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;createOscillator&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
  &lt;span class="kd"&gt;var&lt;/span&gt; &lt;span class="nx"&gt;g&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;ctx&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;createGain&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;

  &lt;span class="nx"&gt;osc&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;type&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;triangle&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="nx"&gt;osc&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;frequency&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;setValueAtTime&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;isRight&lt;/span&gt; &lt;span class="p"&gt;?&lt;/span&gt; &lt;span class="mi"&gt;1180&lt;/span&gt; &lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;790&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;at&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

  &lt;span class="nx"&gt;g&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;gain&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;setValueAtTime&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mf"&gt;0.0001&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;at&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;                       &lt;span class="c1"&gt;// start near zero&lt;/span&gt;
  &lt;span class="nx"&gt;g&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;gain&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;exponentialRampToValueAtTime&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;vol&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;at&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="mf"&gt;0.004&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;    &lt;span class="c1"&gt;// 4ms attack&lt;/span&gt;
  &lt;span class="nx"&gt;g&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;gain&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;exponentialRampToValueAtTime&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mf"&gt;0.0001&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;at&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="nx"&gt;dur&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;   &lt;span class="c1"&gt;// decay to silence&lt;/span&gt;

  &lt;span class="nx"&gt;osc&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;connect&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;g&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="nx"&gt;g&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;connect&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;ctx&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;destination&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="nx"&gt;osc&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;start&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;at&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="nx"&gt;osc&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;stop&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;at&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="nx"&gt;dur&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="mf"&gt;0.01&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;                               &lt;span class="c1"&gt;// small tail&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Three details in there are worth stealing:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;It starts at &lt;code&gt;0.0001&lt;/code&gt;, not &lt;code&gt;0&lt;/code&gt;.&lt;/strong&gt; &lt;code&gt;exponentialRampToValueAtTime&lt;/code&gt; cannot ramp to or from zero — a zero target throws, and a zero start makes the ramp mathematically meaningless. &lt;code&gt;0.0001&lt;/code&gt; is the usual idiom. You will find this out the hard way otherwise.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;The oscillator is stopped slightly after the envelope ends.&lt;/strong&gt; &lt;code&gt;dur + 0.01&lt;/code&gt; gives the gain node time to finish ramping before the source dies. Without it you can get a truncated tail.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Right and left hand get different pitches.&lt;/strong&gt; 1180 Hz against 790 Hz is roughly seven semitones apart — far enough to hear which hand is which, close enough that it still reads as one instrument. For rudiment practice this matters more than any visual notation: you hear a wrong sticking before you see it.&lt;/p&gt;

&lt;h2&gt;
  
  
  The visual can lag. It can't disagree.
&lt;/h2&gt;

&lt;p&gt;Once the audio is sample-accurate, the UI becomes the problem. If you highlight the current beat inside the scheduler loop, the highlight happens ~150ms &lt;em&gt;before&lt;/em&gt; the sound — because you scheduled it, you did not play it.&lt;/p&gt;

&lt;p&gt;The fix is to keep the visual on a separate timer that fires when the note is actually due:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="nf"&gt;setTimeout&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nf"&gt;function &lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nf"&gt;renderSticking&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;i&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="p"&gt;},&lt;/span&gt; &lt;span class="nb"&gt;Math&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;max&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;at&lt;/span&gt; &lt;span class="o"&gt;-&lt;/span&gt; &lt;span class="nx"&gt;ctx&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;currentTime&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="mi"&gt;1000&lt;/span&gt;&lt;span class="p"&gt;));&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;A visual that is 20ms behind the audio is invisible. A visual that is 150ms ahead makes the whole tool feel broken, even though the audio is perfect. Humans forgive latency; they do not forgive contradiction.&lt;/p&gt;

&lt;h2&gt;
  
  
  One more thing: the context starts suspended
&lt;/h2&gt;

&lt;p&gt;Browsers will not let you make noise before a user gesture. &lt;code&gt;new AudioContext()&lt;/code&gt; creates a context in &lt;code&gt;suspended&lt;/code&gt; state, and every &lt;code&gt;start()&lt;/code&gt; on a suspended context is silently dropped.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;ctx&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;state&lt;/span&gt; &lt;span class="o"&gt;===&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;suspended&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;ctx&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;resume&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Call it on every play action rather than once at load — the context can be suspended again after a tab has been backgrounded, and "the metronome works until I switch tabs" is a confusing bug report to receive.&lt;/p&gt;

&lt;h2&gt;
  
  
  Why bother with the browser at all
&lt;/h2&gt;

&lt;p&gt;Because the alternative is asking someone to install an app to hear a click.&lt;/p&gt;

&lt;p&gt;The whole premise of these tools is that a drummer sits down for ten minutes, wants a metronome and a subdivision grid, and should not have to create an account, download 40MB, or watch a splash screen first. Browser audio was "not good enough" for long enough that the assumption stuck around. With a lookahead scheduler it is sample-accurate, and the latency budget belongs to the audio thread rather than to whatever else the page is doing.&lt;/p&gt;

&lt;p&gt;The tuner next to it uses the same clock for a different job: &lt;code&gt;f = base × (A4 / 440) × 2^(cents / 1200)&lt;/code&gt;, which is boring, correct, and instantly verifiable against a reference tone.&lt;/p&gt;

&lt;p&gt;If you want to hear the result rather than read about it, &lt;a href="https://drumdesk.net/tools/rudiment-trainer/" rel="noopener noreferrer"&gt;the rudiment trainer is here&lt;/a&gt; — 40 rudiments, left and right hand at different pitches, 40–220 BPM, subdivisions, and a random mode. No account, no install, nothing stored. It exists because the &lt;code&gt;setInterval&lt;/code&gt; version I wrote first was, to put it kindly, not a metronome.&lt;/p&gt;

</description>
      <category>javascript</category>
      <category>webdev</category>
      <category>audio</category>
      <category>performance</category>
    </item>
    <item>
      <title>The 20-second rule: when a browser tool beats an app</title>
      <dc:creator>NICK</dc:creator>
      <pubDate>Thu, 22 Oct 2026 03:27:06 +0000</pubDate>
      <link>https://dev.to/devkit/the-20-second-rule-when-a-browser-tool-beats-an-app-2ing</link>
      <guid>https://dev.to/devkit/the-20-second-rule-when-a-browser-tool-beats-an-app-2ing</guid>
      <description>&lt;p&gt;There is a threshold I stopped arguing with: if a job takes less than twenty seconds, I refuse to install anything for it.&lt;/p&gt;

&lt;p&gt;Not because installers are evil. Because the overhead is invisible until you count it. An installer is a download, a EULA, a restart, an update notification six weeks later, and one more icon competing for your attention. For a task I do twice a month, the tax is bigger than the task.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;What actually crosses the line&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;The tasks below are all under ten seconds of work once the page is open, which is why they live in a browser tab instead of a menu:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;a percentage change between two numbers I read somewhere&lt;/li&gt;
&lt;li&gt;a timestamp someone pasted in a Slack thread&lt;/li&gt;
&lt;li&gt;a subnet mask I want to sanity-check before touching a config&lt;/li&gt;
&lt;li&gt;the length of a string I just typed into a validation rule&lt;/li&gt;
&lt;li&gt;a currency or unit conversion, usually while reading documentation&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;None of these deserve an app. All of them are one search away from a site that wants my email.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;The rule, stated properly&lt;/strong&gt;&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;If the tool does not need my data, does not need to run in the background, and does not need a history, it should not be installed.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;That last clause is the interesting one. History is the only real reason to install a desktop tool: a timestamp converter that remembers my last ten inputs is genuinely better than one that does not. Everything without state should be a page.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Where this gets uncomfortable&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Browser tools have a bad reputation because most of them are pageviews with a text box. You can usually tell within five seconds: if the page asks you to sign up before it shows a result, it is not a tool, it is a funnel. If it works with JavaScript disabled, it is probably a real calculator.&lt;/p&gt;

&lt;p&gt;The collection I keep on a pinned tab is &lt;a href="https://1024toolkit.com" rel="noopener noreferrer"&gt;1024Toolkit&lt;/a&gt; — 45 single-purpose pages (converters, calculators, encoders, generators), no account, nothing to install. I pin it because the alternative is a search results page full of ads pretending to be tools.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;The part I did not expect&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Treating small jobs as browser jobs changed what I install, not how much I work. My machine has fewer programs on it than it did a year ago, and I have not missed one. The twenty-second rule quietly deleted a whole category of software from my life.&lt;/p&gt;

&lt;p&gt;If you want to test the rule, pick the smallest thing you did today — the one you would be embarrassed to install an app for — and see whether a page does it. It usually does.&lt;/p&gt;

</description>
      <category>calculator</category>
      <category>converter</category>
      <category>onlinetools</category>
      <category>productivity</category>
    </item>
    <item>
      <title>45 free browser tools that answer one question well</title>
      <dc:creator>NICK</dc:creator>
      <pubDate>Wed, 21 Oct 2026 13:04:26 +0000</pubDate>
      <link>https://dev.to/devkit/45-free-browser-tools-that-answer-one-question-well-2hd7</link>
      <guid>https://dev.to/devkit/45-free-browser-tools-that-answer-one-question-well-2hd7</guid>
      <description>&lt;p&gt;Most calculator sites have a business model that works against the answer. You get four fields and a paywall, or a page that needs an account before it shows a number, or a result that arrives wrapped in three ad blocks. The question underneath never changed: &lt;em&gt;how much, how many, how long?&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;1024Toolkit&lt;/strong&gt; is my attempt at the opposite. It is a free collection of &lt;a href="https://1024toolkit.com" rel="noopener noreferrer"&gt;45 browser tools&lt;/a&gt; for finance, health, engineering and development. Each tool answers one question well and runs entirely in your browser — no sign-up, no account, nothing uploaded, four languages.&lt;/p&gt;

&lt;h2&gt;
  
  
  What "one question well" means in practice
&lt;/h2&gt;

&lt;p&gt;Take compound interest. The question is not "can you model monthly contributions with variable rates and a tax wrapper?" It is: &lt;strong&gt;what does £150 a month become at 6% over fifteen years?&lt;/strong&gt; One number, one curve, two fields. Anything more belongs in a spreadsheet.&lt;/p&gt;

&lt;p&gt;The same discipline shaped the rest:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Position sizing&lt;/strong&gt; — what should this trade be, given my account and my risk per trade?&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;BMI&lt;/strong&gt; — one weight, one height, one classification.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Concrete volume&lt;/strong&gt; — how many bags for 2.4 m³, and how much does that cost?&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Cron expressions&lt;/strong&gt; — what does &lt;code&gt;0 3 * * 1&lt;/code&gt; actually run, and when is the next run?&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Subnet maths&lt;/strong&gt; — what does a /27 leave me with?&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Why everything stays client-side
&lt;/h2&gt;

&lt;p&gt;Every calculation runs in JavaScript in your own tab. There is no server to send numbers to, which means there is also no database to breach and no "free tier" to upgrade from. It also means the tools keep working when a train tunnel eats your signal — as long as the page is open, the maths works.&lt;/p&gt;

&lt;h2&gt;
  
  
  Who it is for
&lt;/h2&gt;

&lt;p&gt;Honestly: anyone who re-googles the same conversion more than twice a week. Developers get cron, subnet and timestamp tools; investors get compound interest and position sizing; DIY gets concrete and paint coverage; the gym gets BMI and plate math.&lt;/p&gt;

&lt;p&gt;The full set lives at &lt;a href="https://1024toolkit.com" rel="noopener noreferrer"&gt;1024toolkit.com&lt;/a&gt; — one tab, 45 answers, no account.&lt;/p&gt;

</description>
      <category>calculator</category>
      <category>converter</category>
      <category>onlinetools</category>
      <category>productivity</category>
    </item>
    <item>
      <title>45 tiny tools, zero hassle: what I learned shipping one-page utilities</title>
      <dc:creator>NICK</dc:creator>
      <pubDate>Wed, 14 Oct 2026 15:36:41 +0000</pubDate>
      <link>https://dev.to/devkit/45-tiny-tools-zero-hassle-what-i-learned-shipping-one-page-utilities-40om</link>
      <guid>https://dev.to/devkit/45-tiny-tools-zero-hassle-what-i-learned-shipping-one-page-utilities-40om</guid>
      <description>&lt;p&gt;Every few weeks I hit the same small wall.&lt;/p&gt;

&lt;p&gt;I need to know what &lt;code&gt;0 3 * * 1&lt;/code&gt; actually means. Or how many bags of concrete 2.4 m³ is. Or what&lt;br&gt;
£150 a month turns into at 6% over fifteen years. Nothing hard — just a question that should take&lt;br&gt;
ten seconds to answer.&lt;/p&gt;

&lt;p&gt;And every time, the same routine: land on a site that wants an account, or pushes my numbers to a&lt;br&gt;
server, or is more ad than page.&lt;/p&gt;

&lt;p&gt;So I started collecting the answers I kept needing as single static pages. That collection is now&lt;br&gt;
&lt;strong&gt;45 tools&lt;/strong&gt;, and it lives here: &lt;strong&gt;&lt;a href="https://1024toolkit.com" rel="noopener noreferrer"&gt;https://1024toolkit.com&lt;/a&gt;&lt;/strong&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  The constraint I set myself
&lt;/h2&gt;

&lt;p&gt;One rule shaped everything else: &lt;strong&gt;a tool must answer one question, and it must work without me.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;That meant:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;No account.&lt;/strong&gt; Nothing to sign up for, nothing to remember, nothing to reset.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Nothing you type is stored.&lt;/strong&gt; Whatever you enter stays in the tab. (A couple of tools need live
data — the stock lookup, for instance — and fetch it through a thin proxy. Nothing you type is
saved there either.)&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;One question per page.&lt;/strong&gt; If a tool needs a manual, it is the wrong tool.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The discipline of "one question per page" sounds limiting. In practice it is the opposite — it&lt;br&gt;
makes every addition obvious. Either the tool collapses into a single input and a single result,&lt;br&gt;
or it does not belong.&lt;/p&gt;

&lt;h2&gt;
  
  
  What's in there
&lt;/h2&gt;

&lt;p&gt;45 tools, in four loose clusters plus a long tail of everyday ones:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Finance&lt;/strong&gt; — compound interest, savings-rate, discount, options payoff, position sizing&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Engineering&lt;/strong&gt; — concrete volume, cut lists, triangle geometry, great-circle distance&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Health&lt;/strong&gt; — BMI, BMR, TDEE, heart-rate zones, A1C&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Developer utilities&lt;/strong&gt; — cron, regex, timestamps, time zones, subnets, SQL formatting, mock data&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The developer ones are the reason I started. I got tired of opening a heavyweight site to decode a&lt;br&gt;
cron string I was staring at anyway.&lt;/p&gt;

&lt;p&gt;The whole site is also available in four languages (English, German, Japanese and Simplified&lt;br&gt;
Chinese), which turned out to be far less work than expected once the tools were small enough.&lt;/p&gt;

&lt;h2&gt;
  
  
  What I would tell someone starting the same thing
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;1. Ship the boring version first.&lt;/strong&gt; A page with an input, a label and a result is a legitimate&lt;br&gt;
tool. I have talked myself out of more tools by planning a "proper" version than by any other means.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;2. Name the tool the way people search for it.&lt;/strong&gt; "Cron expression parser" beats "TimeKit" every&lt;br&gt;
single time. The name is the feature.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;3. Almost no backend is a feature, not a shortcut.&lt;/strong&gt; Every server you do not run is a data breach&lt;br&gt;
you cannot have, a bill you do not pay, and a page that keeps working when something upstream is&lt;br&gt;
down. The handful of endpoints I do run exist only to keep an API key off the client — never to hold&lt;br&gt;
your data.&lt;/p&gt;

&lt;h2&gt;
  
  
  The honest part
&lt;/h2&gt;

&lt;p&gt;These are small tools. None of them is clever. The value is that they are &lt;em&gt;there&lt;/em&gt;, they load&lt;br&gt;
instantly, and they never ask for anything.&lt;/p&gt;

&lt;p&gt;If you use one and it is missing an input you need, or the answer is wrong, I would genuinely like&lt;br&gt;
to know — that is the most useful feedback I can get.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;&lt;a href="https://1024toolkit.com" rel="noopener noreferrer"&gt;https://1024toolkit.com&lt;/a&gt;&lt;/strong&gt;&lt;/p&gt;

</description>
      <category>webdev</category>
      <category>showdev</category>
      <category>javascript</category>
      <category>productivity</category>
    </item>
  </channel>
</rss>
