<?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: Bryan Hamilton</title>
    <description>The latest articles on DEV Community by Bryan Hamilton (@bryanhamiltondev).</description>
    <link>https://dev.to/bryanhamiltondev</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%2F4169505%2Fcd835a1a-5b9d-44f8-b958-5b28e0732b4a.jpg</url>
      <title>DEV Community: Bryan Hamilton</title>
      <link>https://dev.to/bryanhamiltondev</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/bryanhamiltondev"/>
    <language>en</language>
    <item>
      <title>Building a Real Audio Hover Preview with Zero Dependencies</title>
      <dc:creator>Bryan Hamilton</dc:creator>
      <pubDate>Wed, 07 Oct 2026 17:15:41 +0000</pubDate>
      <link>https://dev.to/bryanhamiltondev/building-a-real-audio-hover-preview-with-zero-dependencies-2mgp</link>
      <guid>https://dev.to/bryanhamiltondev/building-a-real-audio-hover-preview-with-zero-dependencies-2mgp</guid>
      <description>&lt;p&gt;I run &lt;a href="https://thedjcalendar.com" rel="noopener noreferrer"&gt;The DJ Calendar&lt;/a&gt;, a real-time EDM tour date platform. The homepage has 99 artist cards. I wanted you to be able to hover any of them and hear their music instantly — without navigating away, without a page load, without a framework.&lt;/p&gt;

&lt;p&gt;Here's how I built it.&lt;/p&gt;

&lt;h2&gt;
  
  
  The Stack
&lt;/h2&gt;

&lt;p&gt;Zero. No React, no Vue, no jQuery, no audio player library. Just vanilla JavaScript and two browser APIs that ship with every modern browser:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;iTunes Search API&lt;/strong&gt; — free 30-second audio previews&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Web Audio API&lt;/strong&gt; — real frequency analysis for the equalizer&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Step 1: Getting the Audio
&lt;/h2&gt;

&lt;p&gt;Apple's iTunes Search API is a public endpoint that doesn't need authentication. A single request with &lt;code&gt;entity=musicTrack&amp;amp;limit=12&amp;amp;media=music&lt;/code&gt; returns preview URLs for any artist.&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;fetch&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;`https://itunes.apple.com/search?term=&lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nf"&gt;encodeURIComponent&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;name&lt;/span&gt;&lt;span class="p"&gt;)}&lt;/span&gt;&lt;span class="s2"&gt;&amp;amp;entity=musicTrack&amp;amp;limit=12&amp;amp;media=music`&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
  &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;then&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;r&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nx"&gt;r&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;json&lt;/span&gt;&lt;span class="p"&gt;())&lt;/span&gt;
  &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;then&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;data&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;result&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;data&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;results&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;find&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;r&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nx"&gt;r&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;previewUrl&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="nx"&gt;result&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
      &lt;span class="nx"&gt;card&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;dataset&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;previewUrl&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;result&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;previewUrl&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
      &lt;span class="nx"&gt;card&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;dataset&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;previewTitle&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;result&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;trackName&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;else&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
      &lt;span class="nx"&gt;card&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;dataset&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;previewMiss&lt;/span&gt; &lt;span class="o"&gt;=&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="p"&gt;});&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The preview URL gets cached on the card's &lt;code&gt;data-&lt;/code&gt; attribute. First hover does the lookup. Every hover after that is instant.&lt;/p&gt;

&lt;h2&gt;
  
  
  Step 2: The Analyser — Real Frequencies, Not a Fake
&lt;/h2&gt;

&lt;p&gt;This is the part I'm proudest of. The 8-bar equalizer isn't animated CSS pretending to be sound. It reads actual frequency data from the audio.&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;const&lt;/span&gt; &lt;span class="nx"&gt;audio&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;Audio&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
&lt;span class="nx"&gt;audio&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;crossOrigin&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;anonymous&lt;/span&gt;&lt;span class="dl"&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;ctx&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;src&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;createMediaElementSource&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;audio&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;analyser&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;createAnalyser&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
&lt;span class="nx"&gt;analyser&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;fftSize&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;64&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="nx"&gt;analyser&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;smoothingTimeConstant&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mf"&gt;0.8&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="nx"&gt;src&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;analyser&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="nx"&gt;analyser&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="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;bufferLength&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;analyser&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;frequencyBinCount&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;dataArray&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;Uint8Array&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;bufferLength&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;draw&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nx"&gt;analyser&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;getByteFrequencyData&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;dataArray&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="c1"&gt;// Clear canvas, draw 8 bars from dataArray[i*2]&lt;/span&gt;
  &lt;span class="nf"&gt;requestAnimationFrame&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;draw&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 &lt;code&gt;fftSize&lt;/code&gt; of 64 gives us 32 frequency bins. Reading every other one (&lt;code&gt;dataArray[i*2]&lt;/code&gt;) gives us exactly 8 bars. Each bar represents a real slice of the audio spectrum.&lt;/p&gt;

&lt;h2&gt;
  
  
  Step 3: The Race Condition Guard
&lt;/h2&gt;

&lt;p&gt;Sweep your cursor across a row of cards fast and three things race: the audio load, the canvas draw loop, and your mouse leaving. Here's the fix:&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;card&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;_djcPreviewGen&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;card&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;_djcPreviewGen&lt;/span&gt; &lt;span class="o"&gt;||&lt;/span&gt; &lt;span class="mi"&gt;0&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="p"&gt;;&lt;/span&gt;
&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;gen&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;card&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;_djcPreviewGen&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="c1"&gt;// Inside every async callback:&lt;/span&gt;
&lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;gen&lt;/span&gt; &lt;span class="o"&gt;!==&lt;/span&gt; &lt;span class="nx"&gt;card&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;_djcPreviewGen&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="c1"&gt;// stale, abort&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;New hover bumps the counter. Every previous hover's callbacks check before touching the DOM. The orphaned callbacks just die silently.&lt;/p&gt;

&lt;h2&gt;
  
  
  Step 4: Consent-First Design
&lt;/h2&gt;

&lt;p&gt;Most hover previews break on the devices where hover doesn't exist. This one treats every "no" as a first-class answer.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Touch devices&lt;/strong&gt; (&lt;code&gt;hover: none, pointer: coarse&lt;/code&gt;): hover never fires. A tap on the photo toggles play/stop. The tap also satisfies the autoplay policy — no extra click needed.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Reduced motion&lt;/strong&gt; (&lt;code&gt;prefers-reduced-motion: reduce&lt;/code&gt;): no hover preview at all. Motion-reactive feedback is exactly what these visitors asked the OS to skip.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;No preview found&lt;/strong&gt;: the card gets &lt;code&gt;data-preview-miss&lt;/code&gt; and is silently skipped forever. No repeated failing lookups on every hover.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight css"&gt;&lt;code&gt;&lt;span class="k"&gt;@media&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;hover&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;none&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="n"&gt;and&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nb"&gt;pointer&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;coarse&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nc"&gt;.dj-card&lt;/span&gt; &lt;span class="nc"&gt;.card-image-overlay&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nl"&gt;display&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;none&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="k"&gt;@media&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;prefers-reduced-motion&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;reduce&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nc"&gt;.dj-card&lt;/span&gt; &lt;span class="nc"&gt;.card-image-overlay&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nl"&gt;display&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;none&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;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  Step 5: The Extras
&lt;/h2&gt;

&lt;p&gt;A few things that came for free once the audio pipeline was in place:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Document title swap&lt;/strong&gt;: while a preview plays, the tab reads "David Guetta - Titanium (feat. Sia)". Restores on stop.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Media Session API&lt;/strong&gt;: the track shows up in OS media controls with artwork.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Cross-origin audio&lt;/strong&gt;: &lt;code&gt;audio.crossOrigin = 'anonymous'&lt;/code&gt; is what makes the analyser legal on Apple's CDN. Without it, the analyser returns all zeros.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  The Result
&lt;/h2&gt;

&lt;p&gt;A hover preview that works on every device, respects every accessibility setting, handles race conditions, and drives a real audio-reactive equalizer — all in vanilla JS, zero dependencies.&lt;br&gt;
[Try it Live] &lt;a href="https://bryanhamiltondev.github.io/dj-card-preview/" rel="noopener noreferrer"&gt;https://bryanhamiltondev.github.io/dj-card-preview/&lt;/a&gt; — click the page once, then hover the card.&lt;/p&gt;

&lt;p&gt;The full source is on GitHub: &lt;a href="https://github.com/bryanhamiltondev/dj-card-preview" rel="noopener noreferrer"&gt;bryanhamiltondev/dj-card-preview&lt;/a&gt;&lt;/p&gt;




&lt;p&gt;&lt;em&gt;This is part of a series extracting production patterns from The DJ Calendar. Also check out the &lt;a href="https://github.com/bryanhamiltondev/eq-visualizer" rel="noopener noreferrer"&gt;eq-visualizer&lt;/a&gt; (pure-CSS decorative EQ) and &lt;a href="https://github.com/bryanhamiltondev/next-show-radar" rel="noopener noreferrer"&gt;next-show-radar&lt;/a&gt; (consent-first tour map).&lt;/em&gt;&lt;/p&gt;

</description>
      <category>javascript</category>
      <category>webdev</category>
      <category>opensource</category>
      <category>webaudio</category>
    </item>
    <item>
      <title>Extracting a Production Hover Preview: Audio, Canvas EQ, and Consent-First Design</title>
      <dc:creator>Bryan Hamilton</dc:creator>
      <pubDate>Wed, 07 Oct 2026 17:00:58 +0000</pubDate>
      <link>https://dev.to/bryanhamiltondev/extracting-a-production-hover-preview-audio-canvas-eq-and-consent-first-design-2lac</link>
      <guid>https://dev.to/bryanhamiltondev/extracting-a-production-hover-preview-audio-canvas-eq-and-consent-first-design-2lac</guid>
      <description>&lt;p&gt;&lt;strong&gt;1. The Problem&lt;/strong&gt;&lt;br&gt;
99 artist cards on The DJ Calendar homepage&lt;br&gt;
Users needed a way to preview music without clicking through to each artist page&lt;br&gt;
Requirements: must work on hover AND touch, must be zero-dependency, must handle browser autoplay policy&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;2. The iTunes Search API — Free Audio&lt;/strong&gt;&lt;br&gt;
No audio hosting needed — Apple's 30-second previews are publicly accessible&lt;br&gt;
entity=musicTrack&amp;amp;limit=12&amp;amp;media=music — grab the first result with a preview URL&lt;br&gt;
Cache it on the card's data-preview-url so it's a one-time lookup per card&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;3. Web Audio API Analyser — Real EQ, Not a Fake&lt;/strong&gt;&lt;br&gt;
createMediaElementSource feeds real audio into an analyserNode&lt;br&gt;
fftSize: 64, smoothing: 0.8 — 8 bars reading their own frequency bins&lt;br&gt;
Canvas draws each frame from dataArray[i*2]&lt;br&gt;
This is the key line: "This is not an animation pretending to be sound"&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;4. The Race Condition Guard&lt;/strong&gt;&lt;br&gt;
Sweep across cards fast and audio load, canvas draw, and mouseleave all race&lt;br&gt;
Each card has a _djcPreviewGen counter&lt;br&gt;
New hover bumps the counter, previous async callbacks check before touching DOM&lt;br&gt;
Simple pattern, saves a ton of edge-case bugs&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;5. Consent-First Design — The "No" States&lt;/strong&gt;&lt;br&gt;
Touch devices (hover: none, pointer: coarse): tap toggles play/stop&lt;br&gt;
Reduced motion (prefers-reduced-motion: reduce): no hover preview&lt;br&gt;
Conflicting audio: card previews pause the favorites player, resume it&lt;br&gt;
No preview found: data-preview-miss, silently skipped forever&lt;br&gt;
Most hover widgets break on these — this one treats them as first-class&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;6. Extras That Come Free&lt;/strong&gt;&lt;br&gt;
document.title swaps to "Artist - Track" while playing&lt;br&gt;
navigator.mediaSession for OS media controls&lt;br&gt;
crossOrigin: anonymous makes the analyser legal on cross-origin audio&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;7. The Extraction — From Production to Repo&lt;/strong&gt;&lt;br&gt;
This code was running on thedjcalendar.com for months&lt;br&gt;
Extracted verbatim into a standalone repo with a GitHub Pages demo&lt;br&gt;
Same code, no build step, no framework — just a script tag and some HTML&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;8. Live Demo&lt;/strong&gt;&lt;br&gt;
bryanhamiltondev.github.io/dj-card-preview&lt;br&gt;
David Guetta — Titanium — click once, hover the card, hear it play&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;9. Sibling Repos&lt;/strong&gt;&lt;br&gt;
eq-visualizer — the decorative pure-CSS EQ&lt;br&gt;
next-show-radar — the consent-first tour map&lt;/p&gt;

</description>
      <category>javascript</category>
      <category>webdev</category>
      <category>opensource</category>
      <category>webaudio</category>
    </item>
  </channel>
</rss>
