<?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: zack</title>
    <description>The latest articles on DEV Community by zack (@zack_e36e64fd6fcd12ebbc13).</description>
    <link>https://dev.to/zack_e36e64fd6fcd12ebbc13</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%2F4047935%2F84015d3c-eb3a-4d69-9c4a-de26ba2311ca.png</url>
      <title>DEV Community: zack</title>
      <link>https://dev.to/zack_e36e64fd6fcd12ebbc13</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/zack_e36e64fd6fcd12ebbc13"/>
    <language>en</language>
    <item>
      <title>Can Sound Clean Dust From a Phone Speaker? The Boundary I Had to Design Around</title>
      <dc:creator>zack</dc:creator>
      <pubDate>Tue, 18 Aug 2026 00:52:51 +0000</pubDate>
      <link>https://dev.to/zack_e36e64fd6fcd12ebbc13/can-sound-clean-dust-from-a-phone-speaker-the-boundary-i-had-to-design-around-35nh</link>
      <guid>https://dev.to/zack_e36e64fd6fcd12ebbc13/can-sound-clean-dust-from-a-phone-speaker-the-boundary-i-had-to-design-around-35nh</guid>
      <description>&lt;p&gt;While building a browser-based speaker cleaner, I kept running into one deceptively simple product question: if vibration can move a water droplet, can it also clean dust?&lt;/p&gt;

&lt;p&gt;The tempting answer is yes. The useful answer is narrower. Water and dust can both make a phone speaker sound muffled, but they are different physical problems. Treating them as one problem would make the interface simpler at the cost of making the product less honest.&lt;/p&gt;

&lt;p&gt;That distinction ended up changing more than the help text. It affected the claims, the stop conditions, and even how I thought about the role of the tool.&lt;/p&gt;

&lt;h2&gt;
  
  
  Water moves. Dust accumulates.
&lt;/h2&gt;

&lt;p&gt;A small amount of water near a speaker grille can exist as droplets. Moving the speaker membrane moves air, and that motion may help a droplet shift toward the opening.&lt;/p&gt;

&lt;p&gt;"Dust" is a much less precise description. It might mean a few dry particles sitting on the surface. It might mean lint caught in the grille. It might be compacted material mixed with skin oil. Or it might be debris below the grille, where someone looking at the phone cannot see or safely reach it.&lt;/p&gt;

&lt;p&gt;Those cases may produce the same complaint—"my speaker sounds muffled"—without sharing the same solution.&lt;/p&gt;

&lt;p&gt;This is an awkward product-design problem because a browser cannot inspect the physical state of a phone. It can generate audio and explain what to do next. It cannot tell whether the obstruction is loose, sticky, internal, or accompanied by hardware damage.&lt;/p&gt;

&lt;h2&gt;
  
  
  The narrow case where sound may help
&lt;/h2&gt;

&lt;p&gt;There is one dust case where a sound cycle is at least a reasonable first step: light, loose, dry particles close to the external grille. Vibration may shift some of that material, especially when the speaker opening is pointed downward so gravity has somewhere useful to take it.&lt;/p&gt;

&lt;p&gt;The word &lt;strong&gt;may&lt;/strong&gt; matters here. A sound cycle is not a vacuum, and I do not have evidence that a particular tone reliably removes dust across different phones.&lt;/p&gt;

&lt;p&gt;The current tool runs a 90-second sweep between 150 and 190 Hz, moving up and down across that range every four seconds. Those numbers describe the implementation; they are not proof of a universal cleaning frequency. Phone speakers differ, obstructions differ, and a frequency value on its own says nothing about what is actually blocking the grille.&lt;/p&gt;

&lt;p&gt;That led to a deliberately modest instruction: try one short cycle for loose material, then listen again. If nothing changes, more playback is unlikely to turn the wrong method into the right one.&lt;/p&gt;

&lt;h2&gt;
  
  
  The cases sound does not solve
&lt;/h2&gt;

&lt;p&gt;Visible compacted dust needs physical exterior cleaning. Sticky residue does too. Material that has been pushed into an opening is even less likely to come back out because a web page plays another tone.&lt;/p&gt;

&lt;p&gt;Debris below the grille is outside the safe reach of a browser tool. The same is true when muffled audio is really a symptom of water damage, a torn component, a failing connection, or another hardware problem.&lt;/p&gt;

&lt;p&gt;This is where repetition can create false confidence. Running the cycle again feels like taking action, but it is not a repair. Persistent muffling, crackling, heat, silence, or debris below the grille is a reason to stop and check the device maker's guidance or use a qualified repair provider.&lt;/p&gt;

&lt;h2&gt;
  
  
  The exterior-cleaning checklist I would actually follow
&lt;/h2&gt;

&lt;p&gt;For dust that is visible on the outside, the useful procedure is pleasantly boring:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Power the phone off and unplug every cable.&lt;/li&gt;
&lt;li&gt;Remove the case and inspect the grille in good light.&lt;/li&gt;
&lt;li&gt;Hold the speaker opening downward so loosened particles can fall away.&lt;/li&gt;
&lt;li&gt;Move a clean, dry, small soft-bristled brush or soft lint-free cloth gently across the exterior surface.&lt;/li&gt;
&lt;li&gt;Stop if debris appears to be below the grille or the phone remains muffled, crackles, heats up, or goes silent.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;The important constraint is to clean &lt;em&gt;across&lt;/em&gt; the exterior, without pushing material into the openings.&lt;/p&gt;

&lt;p&gt;I would not insert a needle, spray liquid, press adhesive putty into the grille, use a hair dryer, or blast compressed air into an opening. Those shortcuts can push material farther in or damage the device.&lt;/p&gt;

&lt;p&gt;The exact care instructions still depend on the phone model. When the manufacturer gives model-specific guidance, that should take priority over a generic checklist.&lt;/p&gt;

&lt;h2&gt;
  
  
  The product copy changed because of this boundary
&lt;/h2&gt;

&lt;p&gt;The easy headline would be "clean dust with sound." It is short, searchable, and much broader than the thing the product can responsibly promise.&lt;/p&gt;

&lt;p&gt;I ended up separating the problem into three decisions instead.&lt;/p&gt;

&lt;p&gt;First, the interface has to name the kind and location of the material: &lt;strong&gt;light and loose near the grille&lt;/strong&gt; is different from &lt;strong&gt;visible and compacted&lt;/strong&gt;, which is different again from &lt;strong&gt;inside the phone&lt;/strong&gt;. That language is less magical, but it gives the user a useful decision point.&lt;/p&gt;

&lt;p&gt;Second, every action needs an exit. One sound cycle should lead to a listening check, not an endless repeat loop. Visible debris should lead to gentle exterior cleaning. Heat, crackling, silence, or an internal obstruction should end the browser workflow altogether.&lt;/p&gt;

&lt;p&gt;Third, the frequency range belongs in the implementation details, not in a promise. A precise number can look authoritative even when the physical situation is unknown. I would rather explain why the sweep exists than dress it up as a secret frequency that fixes every speaker.&lt;/p&gt;

&lt;p&gt;None of these changes make the audio code more sophisticated. They make the product around the code more accurate.&lt;/p&gt;

&lt;h2&gt;
  
  
  Where the browser tool fits
&lt;/h2&gt;

&lt;p&gt;A browser tool is useful when it offers a low-friction first step, keeps the device data private, and makes it easy to stop. It is less useful when a quick action is presented as a substitute for diagnosis.&lt;/p&gt;

&lt;p&gt;I kept that boundary visible in &lt;a href="https://speakercleaner.io/" rel="noopener noreferrer"&gt;the production version of Speaker Cleaner&lt;/a&gt;: try one short cycle for loose material near the grille, then switch to gentle exterior cleaning or qualified help when the symptom does not change.&lt;/p&gt;

&lt;p&gt;That is a smaller promise than "remove dust from any phone speaker." It is also a promise the interface can support without pretending that JavaScript knows what is happening behind the grille.&lt;/p&gt;

&lt;h2&gt;
  
  
  A limitation can be part of the feature
&lt;/h2&gt;

&lt;p&gt;When software reaches into a physical problem, the limitation should not live in tiny text at the bottom of the page. It should shape the primary flow: what the tool can attempt, what the user should observe, and when they should stop.&lt;/p&gt;

&lt;p&gt;The browser can generate a controlled vibration and offer a safe next step. It cannot inspect the speaker, remove compacted debris, or repair hardware. Making that boundary obvious is not a concession. In this case, it is part of the feature.&lt;/p&gt;

</description>
      <category>webdev</category>
      <category>productivity</category>
      <category>ux</category>
      <category>showdev</category>
    </item>
    <item>
      <title>Building a Small, Local-First Web Audio Tool (and the Edge Cases I Didn't Expect)</title>
      <dc:creator>zack</dc:creator>
      <pubDate>Mon, 27 Jul 2026 13:21:30 +0000</pubDate>
      <link>https://dev.to/zack_e36e64fd6fcd12ebbc13/building-a-small-local-first-web-audio-tool-and-the-edge-cases-i-didnt-expect-o1f</link>
      <guid>https://dev.to/zack_e36e64fd6fcd12ebbc13/building-a-small-local-first-web-audio-tool-and-the-edge-cases-i-didnt-expect-o1f</guid>
      <description>&lt;p&gt;I set out to build a very small browser tool: press a button, play a short low-frequency sweep, stop after 30 seconds.&lt;/p&gt;

&lt;p&gt;The first prototype was maybe twenty lines of Web Audio code. The version I shipped is still small, but the hard part turned out not to be generating a tone. The hard part was making the tone behave like a decent citizen on a phone: start only when someone asks for it, avoid an abrupt click, stop when the tab disappears, and leave nothing running behind the UI.&lt;/p&gt;

&lt;p&gt;That made it a useful little case study in the difference between “the API works” and “this feels safe to put in front of people.”&lt;/p&gt;

&lt;h2&gt;
  
  
  The audio graph I actually needed
&lt;/h2&gt;

&lt;p&gt;The sound path is deliberately boring:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;OscillatorNode → GainNode → AudioDestinationNode
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;An oscillator gives me a deterministic sine wave. The gain node is there to shape the beginning and end of playback. I could have connected the oscillator straight to &lt;code&gt;context.destination&lt;/code&gt;, but that is how I got the first unpleasant lesson: an oscillator that starts or stops at a non-zero point in the waveform can produce a click.&lt;/p&gt;

&lt;p&gt;This is the core setup, stripped down to the pieces that matter:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;context&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;AudioContext&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;oscillator&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;context&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;const&lt;/span&gt; &lt;span class="nx"&gt;gain&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;context&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="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;context&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="nx"&gt;oscillator&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="kd"&gt;type&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;sine&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="nx"&gt;oscillator&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;gain&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;connect&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;context&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;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;There is no microphone input, file picker, upload pipeline, or audio recording in this path. The browser generates the tone on the device and sends it to the device's normal audio destination.&lt;/p&gt;

&lt;p&gt;That constraint was useful. It kept the product small, but it also removed a surprising number of privacy and failure modes from the design.&lt;/p&gt;

&lt;h2&gt;
  
  
  Audio begins with a user gesture
&lt;/h2&gt;

&lt;p&gt;Autoplay rules are not a corner case for a tool like this; they are the normal case. Creating an audio context on page load and hoping it will make sound later is an invitation to confusing behavior, especially on mobile browsers.&lt;/p&gt;

&lt;p&gt;I create and resume the context inside the button handler. I also keep the WebKit constructor fallback because the experience is intended for phones, not only a desktop development machine.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="kd"&gt;type&lt;/span&gt; &lt;span class="nx"&gt;WebkitWindow&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;Window&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&lt;/span&gt;
  &lt;span class="k"&gt;typeof&lt;/span&gt; &lt;span class="nx"&gt;globalThis&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="nx"&gt;webkitAudioContext&lt;/span&gt;&lt;span class="p"&gt;?:&lt;/span&gt; &lt;span class="k"&gt;typeof&lt;/span&gt; &lt;span class="nx"&gt;AudioContext&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="p"&gt;};&lt;/span&gt;

&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;browserWindow&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nb"&gt;window&lt;/span&gt; &lt;span class="k"&gt;as&lt;/span&gt; &lt;span class="nx"&gt;WebkitWindow&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;AudioContextConstructor&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt;
  &lt;span class="nx"&gt;browserWindow&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;AudioContext&lt;/span&gt; &lt;span class="o"&gt;??&lt;/span&gt; &lt;span class="nx"&gt;browserWindow&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;webkitAudioContext&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;!&lt;/span&gt;&lt;span class="nx"&gt;AudioContextConstructor&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nf"&gt;setStatus&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;error&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="k"&gt;return&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;context&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;AudioContextConstructor&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
&lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;context&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;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The button is doing more than starting sound. It is the point where the browser receives an explicit intent to play audio. Treating that as part of the interface, rather than an implementation detail, made the failure state much easier to explain.&lt;/p&gt;

&lt;h2&gt;
  
  
  The click that made the first version feel broken
&lt;/h2&gt;

&lt;p&gt;The initial version used &lt;code&gt;oscillator.start()&lt;/code&gt; and &lt;code&gt;oscillator.stop()&lt;/code&gt; with no envelope. It technically worked. It also sounded a little like a fault when it began or ended on some speakers.&lt;/p&gt;

&lt;p&gt;The fix was a small gain envelope. I ramp from silence to a modest gain over 250 ms, hold it during the cycle, then return to silence before the scheduled stop. The live tool runs for 30 seconds.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;CLEANING_CYCLE_MS&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;30&lt;/span&gt;&lt;span class="nx"&gt;_000&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;startedAt&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;context&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="nx"&gt;gain&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="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;startedAt&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="nx"&gt;gain&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;linearRampToValueAtTime&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mf"&gt;0.12&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;startedAt&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="mf"&gt;0.25&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="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.12&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="nx"&gt;startedAt&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;CLEANING_CYCLE_MS&lt;/span&gt; &lt;span class="o"&gt;-&lt;/span&gt; &lt;span class="mi"&gt;350&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;/&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="nx"&gt;_000&lt;/span&gt;&lt;span class="p"&gt;,&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="nx"&gt;gain&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;linearRampToValueAtTime&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="nx"&gt;startedAt&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="nx"&gt;CLEANING_CYCLE_MS&lt;/span&gt; &lt;span class="o"&gt;/&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="nx"&gt;_000&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
&lt;span class="p"&gt;);&lt;/span&gt;

&lt;span class="nx"&gt;oscillator&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;startedAt&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="nx"&gt;oscillator&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;startedAt&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="nx"&gt;CLEANING_CYCLE_MS&lt;/span&gt; &lt;span class="o"&gt;/&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="nx"&gt;_000&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="mf"&gt;0.02&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The exact gain value is not especially interesting. The envelope is. It gave the control a predictable beginning and end, and it made the stop button feel intentional instead of abrupt.&lt;/p&gt;

&lt;p&gt;If I were building a music tool, I would probably expose more of this as a parameter. For a single-purpose control, a fixed, conservative envelope is a better trade-off.&lt;/p&gt;

&lt;h2&gt;
  
  
  I used a range, not a universal frequency
&lt;/h2&gt;

&lt;p&gt;I did not want to present one number as the answer for every phone speaker. The tool moves smoothly between 150 Hz and 190 Hz, then back again, with a four-second triangle pattern.&lt;/p&gt;

&lt;p&gt;Here is the pure function behind that behavior:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;SWEEP_MIN_HZ&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;150&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;SWEEP_MAX_HZ&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;190&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;SWEEP_PERIOD_MS&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;4&lt;/span&gt;&lt;span class="nx"&gt;_000&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;getSweepFrequency&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;elapsedMs&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kr"&gt;number&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;phase&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt;
    &lt;span class="p"&gt;(((&lt;/span&gt;&lt;span class="nx"&gt;elapsedMs&lt;/span&gt; &lt;span class="o"&gt;%&lt;/span&gt; &lt;span class="nx"&gt;SWEEP_PERIOD_MS&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="nx"&gt;SWEEP_PERIOD_MS&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;%&lt;/span&gt; &lt;span class="nx"&gt;SWEEP_PERIOD_MS&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;/&lt;/span&gt;
    &lt;span class="nx"&gt;SWEEP_PERIOD_MS&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;triangle&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;phase&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;=&lt;/span&gt; &lt;span class="mf"&gt;0.5&lt;/span&gt; &lt;span class="p"&gt;?&lt;/span&gt; &lt;span class="nx"&gt;phase&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="mi"&gt;2&lt;/span&gt; &lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt; &lt;span class="o"&gt;-&lt;/span&gt; &lt;span class="nx"&gt;phase&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="mi"&gt;2&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

  &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nx"&gt;SWEEP_MIN_HZ&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;SWEEP_MAX_HZ&lt;/span&gt; &lt;span class="o"&gt;-&lt;/span&gt; &lt;span class="nx"&gt;SWEEP_MIN_HZ&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="nx"&gt;triangle&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;The function is easy to test because it has no dependency on React or the audio context. In the component, I call it from &lt;code&gt;requestAnimationFrame&lt;/code&gt;, update the frequency at the context's current time, and use the same snapshot to drive the visible countdown and frequency readout.&lt;/p&gt;

&lt;p&gt;That last part mattered more than I expected. A fixed duration without feedback feels arbitrary. A running timer gives someone a clear answer to “is it still doing anything?” and makes stopping early a normal action rather than a failure.&lt;/p&gt;

&lt;h2&gt;
  
  
  Stopping is part of the feature
&lt;/h2&gt;

&lt;p&gt;Starting an oscillator is easy. Cleaning up after one requires a little more discipline.&lt;/p&gt;

&lt;p&gt;I keep refs to the audio context, oscillator, gain node, and animation-frame ID. When a cycle is stopped early, I cancel the frame, ramp gain down briefly, schedule a stop, disconnect the nodes, and close the context. The code is defensive because an oscillator may already have reached its scheduled stop by the time cleanup runs.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;now&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;context&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="nx"&gt;gain&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;cancelScheduledValues&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;now&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="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="nx"&gt;gain&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="nx"&gt;value&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;now&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="nx"&gt;gain&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;linearRampToValueAtTime&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="nx"&gt;now&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="mf"&gt;0.08&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

&lt;span class="nx"&gt;oscillator&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;now&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="mf"&gt;0.09&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

&lt;span class="nb"&gt;window&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;setTimeout&lt;/span&gt;&lt;span class="p"&gt;(()&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nx"&gt;oscillator&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;disconnect&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;disconnect&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
  &lt;span class="k"&gt;void&lt;/span&gt; &lt;span class="nx"&gt;context&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;close&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
&lt;span class="p"&gt;},&lt;/span&gt; &lt;span class="mi"&gt;120&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The other lifecycle rule is simple: if the document becomes hidden while sound is playing, stop the cycle.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="nf"&gt;useEffect&lt;/span&gt;&lt;span class="p"&gt;(()&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;stopWhenHidden&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nb"&gt;document&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;hidden&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; &lt;span class="nx"&gt;isRunningRef&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;current&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
      &lt;span class="nf"&gt;stopCycle&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;
  &lt;span class="p"&gt;};&lt;/span&gt;

  &lt;span class="nb"&gt;document&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;addEventListener&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;visibilitychange&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;stopWhenHidden&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="k"&gt;return &lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="nb"&gt;document&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;removeEventListener&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;visibilitychange&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;stopWhenHidden&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="nf"&gt;releaseAudio&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="p"&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;releaseAudio&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;stopCycle&lt;/span&gt;&lt;span class="p"&gt;]);&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This avoids a weird class of “I switched apps and my phone was still playing a tone” reports. It also gives unmounting a clear cleanup path.&lt;/p&gt;

&lt;h2&gt;
  
  
  Local-only was an implementation choice
&lt;/h2&gt;

&lt;p&gt;“Runs locally” can sound like a slogan, so I tried to make it concrete in the interface.&lt;/p&gt;

&lt;p&gt;The audio feature does not request microphone permission, inspect files, or send audio anywhere. The UI shows a local-audio label, a 30-second countdown, the current frequency, and a live status message for screen readers. Those pieces are not decoration. They make the state of an otherwise invisible process observable.&lt;/p&gt;

&lt;p&gt;The site has optional analytics behind a consent choice, but that is separate from the audio path. Someone can use the tool without granting analytics consent; the sound generation itself still happens in the browser.&lt;/p&gt;

&lt;h2&gt;
  
  
  A boundary worth stating plainly
&lt;/h2&gt;

&lt;p&gt;I built this around a narrow claim: moving a speaker diaphragm with a low tone may help shift a small droplet sitting near an external grille. It is not a repair method. It cannot dry internal components, reverse liquid damage, or replace a device maker's guidance.&lt;/p&gt;

&lt;p&gt;That boundary belongs in the product, not in a footnote. If the phone has a liquid warning, charging trouble, heat, severe distortion, or broader hardware symptoms, sound is the wrong next step. I keep &lt;a href="https://speakercleaner.io/blog/phone-speaker-muffled-after-water" rel="noopener noreferrer"&gt;the safety notes I publish alongside the tool&lt;/a&gt; separate from the implementation because the two jobs are different: one explains the engineering, the other helps people decide when not to use it.&lt;/p&gt;

&lt;h2&gt;
  
  
  What I would like to hear from other Web Audio builders
&lt;/h2&gt;

&lt;p&gt;This was a small project, but it made me pay more attention to the lifecycle around a browser API than the API itself. The oscillator was the easy bit. The useful work was deciding how it starts, how it stops, and what the person using it can understand while it is running.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://speakercleaner.io/" rel="noopener noreferrer"&gt;Try the live demo: Speaker Cleaner&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;If you have shipped Web Audio on mobile, I would be curious how you handle cleanup when a page is backgrounded. Do you stop immediately, suspend the context, or take a different approach?&lt;/p&gt;




&lt;h2&gt;
  
  
  Pre-publish notes (do not paste into the article)
&lt;/h2&gt;

&lt;ol&gt;
&lt;li&gt;Upload the cover image and replace its automatically inserted alt text with the supplied description.&lt;/li&gt;
&lt;li&gt;Keep the two Speaker Cleaner links exactly as written; do not add a homepage link in the opening or code sections.&lt;/li&gt;
&lt;li&gt;In DEV preview, confirm the TypeScript code fences display correctly and that the final question remains the last paragraph.&lt;/li&gt;
&lt;li&gt;Publish manually after previewing the mobile layout. Record the resulting DEV URL in the backlink ledger as &lt;code&gt;live&lt;/code&gt;; wait for separate evidence before classifying it as indexed, Google-discovered, or referral traffic.&lt;/li&gt;
&lt;/ol&gt;

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