<?xml version="1.0" encoding="UTF-8"?>
<rss xmlns:dc="http://purl.org/dc/elements/1.1/" xmlns:content="http://purl.org/rss/1.0/modules/content/"
    xmlns:atom="http://www.w3.org/2005/Atom" xmlns:media="http://search.yahoo.com/mrss/" version="2.0">
    <channel>
        
        <title>
            <![CDATA[ audio - freeCodeCamp.org ]]>
        </title>
        <description>
            <![CDATA[ Browse thousands of programming tutorials written by experts. Learn Web Development, Data Science, DevOps, Security, and get developer career advice. ]]>
        </description>
        <link>https://www.freecodecamp.org/news/</link>
        <image>
            <url>https://cdn.freecodecamp.org/universal/favicons/favicon.png</url>
            <title>
                <![CDATA[ audio - freeCodeCamp.org ]]>
            </title>
            <link>https://www.freecodecamp.org/news/</link>
        </image>
        <generator>Eleventy</generator>
        <lastBuildDate>Tue, 26 May 2026 16:23:06 +0000</lastBuildDate>
        <atom:link href="https://www.freecodecamp.org/news/tag/audio/rss.xml" rel="self" type="application/rss+xml" />
        <ttl>60</ttl>
        
            <item>
                <title>
                    <![CDATA[ The Bluetooth LE Audio Handbook: From "Why Does My Call Sound Like a Tin Can?" to AOSP Implementation ]]>
                </title>
                <description>
                    <![CDATA[ Since the early 2000s, Bluetooth has been the dominant way we listen to wireless audio, powering everything from the first mono headsets to today's true wireless earbuds. But the underlying technology ]]>
                </description>
                <link>https://www.freecodecamp.org/news/the-bluetooth-le-audio-handbook/</link>
                <guid isPermaLink="false">69d6805e707c1ce76855752b</guid>
                
                    <category>
                        <![CDATA[ LEAudio ]]>
                    </category>
                
                    <category>
                        <![CDATA[ bluetooth ]]>
                    </category>
                
                    <category>
                        <![CDATA[ Bluetooth Low Energy ]]>
                    </category>
                
                    <category>
                        <![CDATA[ audio ]]>
                    </category>
                
                    <category>
                        <![CDATA[ handbook ]]>
                    </category>
                
                <dc:creator>
                    <![CDATA[ Nikheel Vishwas Savant ]]>
                </dc:creator>
                <pubDate>Wed, 08 Apr 2026 16:20:46 +0000</pubDate>
                <media:content url="https://cdn.hashnode.com/uploads/covers/5e1e335a7a1d3fcc59028c64/4c5a3b97-9a23-40cd-8999-333927f58e6c.png" medium="image" />
                <content:encoded>
                    <![CDATA[ <p>Since the early 2000s, Bluetooth has been the dominant way we listen to wireless audio, powering everything from the first mono headsets to today's true wireless earbuds.</p>
<p>But the underlying technology hasn't kept pace with how we actually use it. True wireless earbuds, all-day hearing aids, shared audio experiences – none of these were anticipated when the original Bluetooth audio stack was designed.</p>
<p>LE Audio, introduced by the Bluetooth SIG and finalized in 2022, is a ground-up redesign that replaces the Classic Bluetooth audio stack with an entirely new architecture built on Bluetooth Low Energy. It introduces a new codec (LC3), new transport primitives (isochronous channels), new profiles for unified audio streaming, and an entirely new broadcast capability called Auracast.</p>
<p>Together, these changes address long-standing limitations around audio quality, power consumption, multi-device streaming, and accessibility.</p>
<p>This handbook is a comprehensive technical deep dive into LE Audio: what it is, why it exists, how it works at every layer of the stack, and how it's implemented in Android (AOSP). We'll start with the history and motivation, build up an intuitive understanding of the core concepts, and then go deep into the architecture and code.</p>
<p>Here's what you'll learn:</p>
<ul>
<li><p>Why Classic Bluetooth audio hit its limits, the relay problem, the two-profile split, power constraints, and the lack of broadcast or hearing aid support</p>
</li>
<li><p>How the LC3 codec works, and why it delivers better audio at roughly half the bitrate of SBC</p>
</li>
<li><p>What isochronous channels are, the new transport primitive that replaces SCO and ACL for audio, in both unicast (CIS) and broadcast (BIS) forms</p>
</li>
<li><p>How the LE Audio profile stack is organized, from foundational services like BAP and PACS up through use-case profiles like TMAP and HAP</p>
</li>
<li><p>How multi-stream audio eliminates the earbud relay hack, with native synchronized streams to each earbud</p>
</li>
<li><p>What Auracast enables, one-to-many broadcast audio and the infrastructure that supports it</p>
</li>
<li><p>How all of this is implemented in Android (AOSP), a full walkthrough of the architecture from framework APIs through the native C++ stack to the Bluetooth controller, including the state machines, codec negotiation, and data flow</p>
</li>
</ul>
<p>Whether you're a Bluetooth engineer, an embedded developer, an Android platform engineer, or just someone curious about how your devices actually work, this guide aims to make one of the most complex parts of modern wireless systems feel approachable.</p>
<p>If you've ever wondered why your earbuds sound great for music but terrible on calls, why one earbud always dies first, or why you can't easily share audio with people around you, read on. The answers are all here.</p>
<h2 id="heading-table-of-contents">Table of Contents</h2>
<ol>
<li><p><a href="#heading-once-upon-a-time-in-bluetooth-land">Once Upon a Time in Bluetooth Land</a></p>
</li>
<li><p><a href="#heading-2-the-problems-with-classic-bluetooth-audio">The Problems With Classic Bluetooth Audio</a></p>
</li>
<li><p><a href="#heading-3-enter-le-audio-the-hero-we-needed">Enter LE Audio: The Hero We Needed</a></p>
</li>
<li><p><a href="#heading-4-the-lc3-codec-better-sound-less-power-more-magic">The LC3 Codec: Better Sound, Less Power, More Magic</a></p>
</li>
<li><p><a href="#heading-5-isochronous-channels-the-new-plumbing">Isochronous Channels: The New Plumbing</a></p>
</li>
<li><p><a href="#heading-6-the-le-audio-profile-stack-a-layer-cake-of-specifications">The LE Audio Profile Stack: A Layer Cake of Specifications</a></p>
</li>
<li><p><a href="#heading-7-multi-stream-audio-no-more-left-earbud-relay">Multi-Stream Audio: No More Left Earbud Relay</a></p>
</li>
<li><p><a href="#heading-8-auracast-broadcast-audio-for-the-masses">Auracast: Broadcast Audio for the Masses</a></p>
</li>
<li><p><a href="#heading-9-le-audio-in-androidaosp-the-implementation">LE Audio in Android/AOSP: The Implementation</a></p>
</li>
<li><p><a href="#heading-10-the-aosp-architecture-from-app-to-antenna">The AOSP Architecture: From App to Antenna</a></p>
</li>
<li><p><a href="#heading-11-server-side-source-implementation">Server-Side (Source) Implementation</a></p>
</li>
<li><p><a href="#heading-12-client-side-sink-implementation">Client-Side (Sink) Implementation</a></p>
</li>
<li><p><a href="#heading-13-the-state-machine-that-runs-it-all">The State Machine That Runs It All</a></p>
</li>
<li><p><a href="#heading-14-putting-it-all-together-a-day-in-the-life-of-an-le-audio-packet">Putting It All Together: A Day in the Life of an LE Audio Packet</a></p>
</li>
<li><p><a href="#heading-wrapping-up">Wrapping Up</a></p>
</li>
</ol>
<h2 id="heading-1-once-upon-a-time-in-bluetooth-land">1. Once Upon a Time in Bluetooth Land</h2>
<p>Picture this: it's 2003. Flip phones are cool. The first Bluetooth headsets hit the market, and suddenly you can walk around looking like a cyborg while taking calls.</p>
<p>That mono, telephone-quality audio? Powered by a little thing called <strong>HFP</strong> (Hands-Free Profile) using the <strong>CVSD</strong> codec at a whopping 64 kbps. It sounded like your caller was speaking from inside a submarine, but hey, no wires!</p>
<p>Fast forward a few years. We got <strong>A2DP</strong> (Advanced Audio Distribution Profile) for streaming music, bringing us <strong>SBC</strong> (Sub-Band Codec), the audio codec equivalent of a Honda Civic. Not flashy, not terrible, gets the job done. A2DP gave us stereo music streaming, and life was good.</p>
<p>For a while.</p>
<p>The Bluetooth SIG (Special Interest Group), the consortium of thousands of companies that governs Bluetooth, kept iterating on the classic Bluetooth audio stack. We got better codecs like <strong>aptX</strong>, <strong>AAC</strong>, and <strong>LDAC</strong>. But here's the thing: all of these were built on top of the same ancient plumbing. It's like renovating your kitchen while the house's foundation is slowly cracking.</p>
<p>The Bluetooth audio stack was built on <strong>BR/EDR</strong> (Basic Rate/Enhanced Data Rate), the "Classic Bluetooth" radio. This is the same radio technology from the early 2000s, designed when streaming audio from a phone to a single headset was the pinnacle of innovation. Nobody imagined true wireless earbuds, hearing aids that stream directly from your phone, or broadcasting audio to an entire airport terminal.</p>
<p>By the late 2010s, Bluetooth audio was showing its age. Badly.</p>
<h2 id="heading-2-the-problems-with-classic-bluetooth-audio">2. The Problems With Classic Bluetooth Audio</h2>
<p>Let's catalogue the issues of Classic Bluetooth Audio, because they're educational:</p>
<h3 id="heading-issue-1-the-two-profile-personality-disorder">Issue #1: The Two-Profile Personality Disorder</h3>
<p>Classic Bluetooth had a split personality. Want to listen to music? Use A2DP with SBC/AAC at nice quality. Want to make a phone call? Switch to HFP, which uses a completely different codec (CVSD or mSBC) at dramatically lower quality.</p>
<p>Ever noticed how your wireless earbuds sound amazing playing Spotify, but the moment you jump on a Zoom call, it sounds like you're talking through a paper towel tube? That's the A2DP-to-HFP switchover. Different profiles, different codecs, different audio paths. The switch isn't even graceful, there's often an audible glitch.</p>
<img src="https://cdn.hashnode.com/uploads/covers/68a51326db25241b7cb0c047/fd614824-0684-4fb3-87a8-8c97052721b6.png" alt="Bluetooth audio quality diagram" style="display:block;margin:0 auto" width="960" height="1310" loading="lazy">

<p>The above diagram shows the audio quality drop when switching from A2DP (music streaming with SBC/AAC at high quality) to HFP (voice call with CVSD/mSBC at low quality). The switch causes an audible glitch and dramatic reduction in audio fidelity.</p>
<h3 id="heading-issue-2-the-relay-problem-true-wireless-earbuds">Issue #2: The Relay Problem (True Wireless Earbuds)</h3>
<p>When you have true wireless earbuds (left and right earbuds with no wire between them), Classic Bluetooth has a dirty little secret: <strong>A2DP can only stream to one device at a time.</strong></p>
<p>So what actually happens with your fancy earbuds?</p>
<ol>
<li><p>Your phone sends the stereo audio stream to the <strong>primary earbud</strong> (usually the right one)</p>
</li>
<li><p>The primary earbud receives both left and right channels</p>
</li>
<li><p>It then <strong>relays</strong> the other channel to the secondary earbud via a separate Bluetooth link</p>
</li>
</ol>
<p>This relay architecture has a few important consequences. First, you have double the battery drain on the primary earbud (it dies first, you've noticed this). You also get higher latency to the secondary earbud</p>
<p>There are also potential synchronization issues between left and right channels. And if the primary earbud runs out of battery or loses connection, both earbuds go silent.</p>
<h3 id="heading-issue-3-power-hungry">Issue #3: Power Hungry</h3>
<p>BR/EDR was designed in an era when "low power" meant "runs on AA batteries." Streaming audio over Classic Bluetooth is relatively power-hungry. The radio has to maintain a constant, high-bandwidth connection. For devices like hearing aids that need to run all day on tiny batteries, this was a dealbreaker.</p>
<h3 id="heading-issue-4-one-to-one-only">Issue #4: One-to-One Only</h3>
<p>Classic Bluetooth audio is fundamentally <strong>point-to-point</strong>. One source, one sink (or at best, a very hacky "dual audio" implementation where the phone maintains two separate A2DP connections). There's no way to broadcast audio to multiple listeners simultaneously without establishing individual connections to each one.</p>
<p>Imagine you're at an airport gate and want to stream the boarding announcements to everyone's earbuds. With Classic Bluetooth, you'd need to pair with every single person's device individually. Good luck with that at Gate B47.</p>
<h3 id="heading-issue-5-no-standard-for-hearing-aids">Issue #5: No Standard for Hearing Aids</h3>
<p>Before LE Audio, there was no official Bluetooth standard for hearing aids. Apple created its own proprietary MFi (Made for iPhone) hearing aid protocol. Google created ASHA (Audio Streaming for Hearing Aid) as a semi-proprietary BLE-based solution for Android. Neither was an official Bluetooth standard, and interoperability was... let's call it "aspirational."</p>
<h2 id="heading-3-enter-le-audio-the-hero-we-needed">3. Enter LE Audio: The Hero We Needed</h2>
<p>In January 2020, at CES, the Bluetooth SIG unveiled <strong>LE Audio</strong>, a complete reimagining of Bluetooth audio built on top of Bluetooth Low Energy (BLE) instead of Classic BR/EDR.</p>
<p>The core transport features (isochronous channels, EATT, LE Power Control) shipped in the Bluetooth Core Specification v5.2 in late 2019/early 2020. But the full suite of LE Audio profiles and services wasn't completed until July 12, 2022, when the Bluetooth SIG officially announced that all LE Audio specifications had been adopted.</p>
<p>The effort involved over 25 working groups, thousands of engineers from hundreds of companies, and took approximately 7 years from initial concept to completion. This wasn't a minor spec update. It was a ground-up redesign.</p>
<p>Here's what LE Audio brings to the table:</p>
<table>
<thead>
<tr>
<th>Feature</th>
<th>Classic Audio</th>
<th>LE Audio</th>
</tr>
</thead>
<tbody><tr>
<td>Radio</td>
<td>BR/EDR (Classic)</td>
<td>BLE (Low Energy)</td>
</tr>
<tr>
<td>Mandatory Codec</td>
<td>SBC</td>
<td>LC3</td>
</tr>
<tr>
<td>Audio Quality at Same Bitrate</td>
<td>Good</td>
<td>Better (LC3 wins)</td>
</tr>
<tr>
<td>Power Consumption</td>
<td>Higher</td>
<td>Lower</td>
</tr>
<tr>
<td>Multi-Stream</td>
<td>No (relay hack)</td>
<td>Yes (native)</td>
</tr>
<tr>
<td>Broadcast Audio</td>
<td>No</td>
<td>Yes (Auracast)</td>
</tr>
<tr>
<td>Hearing Aid Support</td>
<td>No standard (MFi/ASHA)</td>
<td>Yes (HAP)</td>
</tr>
<tr>
<td>Bidirectional Audio</td>
<td>Separate profiles (A2DP + HFP)</td>
<td>Unified (BAP)</td>
</tr>
<tr>
<td>Audio Sharing</td>
<td>Very limited</td>
<td>Built-in</td>
</tr>
</tbody></table>
<p>Think of it this way: Classic Bluetooth Audio is like a landline telephone system: reliable, well-understood, but fundamentally limited.</p>
<p>LE Audio is like the transition to VoIP and streaming: same goal (getting audio from A to B), but entirely new infrastructure that unlocks capabilities the old system could never support.</p>
<h2 id="heading-4-the-lc3-codec-better-sound-less-power-more-magic">4. The LC3 Codec: Better Sound, Less Power, More Magic</h2>
<p>At the heart of LE Audio is a new mandatory codec called <strong>LC3</strong>: Low Complexity Communication Codec. If SBC is the Honda Civic, LC3 is a Tesla Model 3. It's more efficient, more capable, and designed from the ground up for the modern era.</p>
<h3 id="heading-what-even-is-a-codec">What Even Is a Codec?</h3>
<p>For the uninitiated: a codec (<strong>co</strong>der-<strong>dec</strong>oder) is an algorithm that compresses audio so it can be transmitted over a limited-bandwidth wireless link, and then decompresses it on the other side. The better the codec, the better the audio sounds at a given bitrate, and the less battery it eats doing the math.</p>
<h3 id="heading-lc3-technical-specs">LC3 Technical Specs</h3>
<p>LC3 was developed by Fraunhofer IIS (the same folks who brought us MP3 and AAC, they know a thing or two about audio coding) and Ericsson.</p>
<p>Here are the key specs:</p>
<ul>
<li><p><strong>Sample rates</strong>: 8, 16, 24, 32, 44.1, and 48 kHz</p>
</li>
<li><p><strong>Bit depth</strong>: 16, 24, or 32 bits</p>
</li>
<li><p><strong>Frame durations</strong>: 7.5 ms and 10 ms</p>
</li>
<li><p><strong>Bitrate range</strong>: 16 to 320 kbps per channel</p>
</li>
<li><p><strong>Algorithmic latency</strong>: 7.5 ms (for 7.5 ms frames) or 10 ms (for 10 ms frames)</p>
</li>
<li><p><strong>Channels</strong>: Mono or stereo</p>
</li>
</ul>
<h3 id="heading-why-lc3-is-better-than-sbc">Why LC3 Is Better Than SBC</h3>
<p>The big headline: LC3 delivers equivalent or better audio quality at roughly half the bitrate of SBC.</p>
<p>In listening tests conducted by Fraunhofer, participants rated LC3 at 160 kbps as equivalent to or better than SBC at 345 kbps. That's not a marginal improvement, it's nearly a 2x efficiency gain.</p>
<img src="https://cdn.hashnode.com/uploads/covers/68a51326db25241b7cb0c047/293ea94c-3a03-4462-8361-89617e07329f.png" alt="SBC vs LC3 bar chart comparing audio quality" style="display:block;margin:0 auto" width="960" height="932" loading="lazy">

<p>The above bar chart compares subjective audio quality ratings of LC3 and SBC at various bitrates. LC3 at 160 kbps is rated equivalent to or better than SBC at 345 kbps, demonstrating roughly 2x efficiency improvement.</p>
<p>This efficiency gain translates directly into one of two things (or a combination of both):</p>
<ol>
<li><p><strong>Better audio quality at the same power</strong>, more bits for quality, less wasted</p>
</li>
<li><p><strong>Same audio quality at lower power</strong>, the device runs longer on a charge</p>
</li>
</ol>
<h3 id="heading-how-lc3-actually-works-the-simplified-version">How LC3 Actually Works (The Simplified Version)</h3>
<p>LC3 uses a <strong>modified discrete cosine transform (MDCT)</strong>, a mathematical technique that converts audio from the time domain (a waveform) to the frequency domain (which frequencies are present). This is similar to what AAC and other modern codecs do, but LC3's transform is optimized for low computational complexity.</p>
<p>Here's the encoding pipeline, simplified:</p>
<img src="https://cdn.hashnode.com/uploads/covers/68a51326db25241b7cb0c047/f3961a0a-42af-443a-96b4-67f340a55944.png" alt="flowchart of the LC3 encoding pipeline" style="display:block;margin:0 auto" width="2556" height="1475" loading="lazy">

<p>This is a flowchart of the LC3 encoding pipeline. PCM audio input passes through an MDCT (Modified Discrete Cosine Transform) to convert from time domain to frequency domain. Then spectral noise shaping applies a psychoacoustic model to hide quantization noise in inaudible frequency regions, followed by quantization and entropy coding to produce the compressed LC3 bitstream.</p>
<p>The key insight is <strong>spectral noise shaping</strong>: LC3 uses a psychoacoustic model (a model of how humans perceive sound) to ensure that the quantization noise (the artifacts introduced by compression) is shaped to fall in frequency regions where it's least audible. Your ears literally can't hear the distortion. Clever, right?</p>
<h3 id="heading-lc3-vs-lc3plus">LC3 vs. LC3plus</h3>
<p>You might also hear about <strong>LC3plus</strong>, an enhanced version that adds:</p>
<ul>
<li><p>Super-wideband and fullband modes (up to 48 kHz audio bandwidth)</p>
</li>
<li><p>Additional frame sizes (2.5 ms, 5 ms) for ultra-low-latency applications</p>
</li>
<li><p>Higher quality at very low bitrates</p>
</li>
</ul>
<p>LC3plus is not part of the base LE Audio spec but is used in some implementations (like DECT NR+ for cordless phones).</p>
<h2 id="heading-5-isochronous-channels-the-new-plumbing">5. Isochronous Channels: The New Plumbing</h2>
<p>Here's where things get architecturally interesting. Classic Bluetooth audio used <strong>SCO</strong> (Synchronous Connection-Oriented) links for voice and <strong>L2CAP</strong> over <strong>ACL</strong> (Asynchronous Connection-Less) links for A2DP streaming. These were okay, but they're like using garden hoses for different purposes, functional but not optimized for audio.</p>
<p>LE Audio introduces a brand-new transport mechanism at the link layer: <strong>Isochronous Channels</strong>. These are purpose-built pipes for time-sensitive data like audio.</p>
<h3 id="heading-what-isochronous-means">What "Isochronous" Means</h3>
<p>"Isochronous" (from Greek: <em>iso</em> = equal, <em>chronos</em> = time) means "occurring at regular time intervals." An isochronous channel guarantees that data arrives at a predictable, regular cadence, exactly what you need for audio.</p>
<p>Think of it this way:</p>
<ul>
<li><p><strong>Asynchronous</strong> (ACL): "Here's some data. It'll get there when it gets there." (Great for file transfers, bad for audio)</p>
</li>
<li><p><strong>Synchronous</strong> (SCO): "Here's data that MUST arrive on time, and if it doesn't, too bad." (Old voice links, no retransmissions)</p>
</li>
<li><p><strong>Isochronous</strong>: "Here's data that should arrive on time, and we'll try our best to make that happen with some smart retransmission." (Best of both worlds)</p>
</li>
</ul>
<img src="https://cdn.hashnode.com/uploads/covers/68a51326db25241b7cb0c047/51324579-2e10-4b26-bc09-482fa6ade853.png" alt="Comparison of Bluetooth transport types: asynchronous, synchronous, and isosynchronous" style="display:block;margin:0 auto" width="2217" height="939" loading="lazy">

<p>This above chart is a comparison of three Bluetooth transport types: Asynchronous (ACL) delivers data without timing guarantees, Synchronous (SCO) delivers data on a fixed schedule with no retransmission, and Isochronous delivers data on a regular schedule with smart retransmission, combining the reliability of ACL with the timing guarantees of SCO.</p>
<h3 id="heading-two-flavors-cis-and-bis">Two Flavors: CIS and BIS</h3>
<p>Isochronous channels come in two flavors, and this is where the magic happens:</p>
<h4 id="heading-cis-connected-isochronous-stream">CIS — Connected Isochronous Stream</h4>
<p>CIS is for <strong>point-to-point</strong> audio (unicast). It's what your phone uses to stream music to your earbuds.</p>
<img src="https://cdn.hashnode.com/uploads/covers/68a51326db25241b7cb0c047/2b44fe9e-0e5d-44ee-877c-b26e147b63a1.png" alt="Diagram of a Connected Isochronous Stream (CIS) setup" style="display:block;margin:0 auto" width="1362" height="796" loading="lazy">

<p>The aboe is a diagram of a Connected Isochronous Stream (CIS) setup: a phone (Unicast Client) sends two synchronized CIS streams within a single CIG (Connected Isochronous Group), one to the left earbud and one to the right earbud. Arrows show bidirectional audio flow, with music going to the earbuds and microphone audio returning to the phone.</p>
<p>Key features of CIS:</p>
<ul>
<li><p><strong>Bidirectional</strong>: Audio can flow in both directions simultaneously (unicast to earbuds AND microphone audio back)</p>
</li>
<li><p><strong>Acknowledged</strong>: The receiver sends acknowledgments, enabling retransmissions of lost packets</p>
</li>
<li><p><strong>Grouped into CIGs</strong>: Multiple CIS streams are grouped into a <strong>CIG</strong> (Connected Isochronous Group), ensuring they're synchronized</p>
</li>
</ul>
<p>That last point is crucial. A CIG ensures the left and right earbud receive their audio packets with tight synchronization, no more "my left ear is 50ms ahead of my right ear" issues.</p>
<h4 id="heading-bis-broadcast-isochronous-stream">BIS — Broadcast Isochronous Stream</h4>
<p>BIS is for <strong>one-to-many</strong> audio (broadcast). It's the foundation of Auracast.</p>
<img src="https://cdn.hashnode.com/uploads/covers/68a51326db25241b7cb0c047/22beaaf6-ace8-4110-a33c-d7cf370f93d3.png" alt="Diagram of a Broadcast Isochronous Stream (BIS) setup" style="display:block;margin:0 auto" width="2361" height="1281" loading="lazy">

<p>The above is a diagram of a Broadcast Isochronous Stream (BIS) setup: a single broadcast source transmits audio via a BIG (Broadcast Isochronous Group) containing multiple BIS streams. Multiple receivers (broadcast sinks) independently receive the same audio without any connection to the source, similar to FM radio.</p>
<p>Key features of BIS:</p>
<ul>
<li><p><strong>Unidirectional</strong>: One-way only (source to listeners), makes sense, you can't have a million people talking back</p>
</li>
<li><p><strong>Unacknowledged</strong>: No acks from listeners (the source doesn't even know who's listening)</p>
</li>
<li><p><strong>Grouped into BIGs</strong>: Multiple BIS streams form a <strong>BIG</strong> (Broadcast Isochronous Group)</p>
</li>
<li><p><strong>Scalable</strong>: No upper limit on listeners, it's actual radio broadcasting</p>
</li>
</ul>
<h3 id="heading-the-iso-data-path">The ISO Data Path</h3>
<p>Under the hood, isochronous data follows a specific path through the controller:</p>
<img src="https://cdn.hashnode.com/uploads/covers/68a51326db25241b7cb0c047/8bfd7893-b93a-4f07-af09-17cbd737fcbb.png" alt="Diagram of the isochronous data path through the Bluetooth controller" style="display:block;margin:0 auto" width="1655" height="1835" loading="lazy">

<p>The above is a diagram of the isochronous data path through the Bluetooth controller. Audio frames from the host pass through HCI, then through the ISO Adaptation Layer (ISO-AL) which handles segmentation, timestamping, and flush timeout management, before reaching the Link Layer for transmission over the air.</p>
<p>The key innovation is the <strong>ISO-AL</strong> (Isochronous Adaptation Layer), which sits between HCI and the Link Layer. It handles:</p>
<ul>
<li><p><strong>Segmentation</strong>: Breaking audio frames into link-layer-sized pieces</p>
</li>
<li><p><strong>Time-stamping</strong>: Each audio frame gets a timestamp so the receiver knows exactly when to play it</p>
</li>
<li><p><strong>Flush timeout</strong>: If a frame can't be delivered in time, it's flushed (better to skip a frame than play it late)</p>
</li>
</ul>
<h2 id="heading-6-the-le-audio-profile-stack-a-layer-cake-of-specifications">6. The LE Audio Profile Stack: A Layer Cake of Specifications</h2>
<p>If you've ever looked at the list of LE Audio specifications and felt your eyes glaze over, you're not alone. There are a LOT of them. But they're organized in a logical hierarchy, and once you understand the structure, it all makes sense.</p>
<h3 id="heading-visual-the-profile-stack">Visual: The Profile Stack</h3>
<p>Here's a three-tier diagram of the LE Audio profile stack:</p>
<img src="https://cdn.hashnode.com/uploads/covers/68a51326db25241b7cb0c047/e4968717-72bc-43c5-b72c-057a65534bb1.png" alt="Three-tier diagram of the LE Audio profile stack" style="display:block;margin:0 auto" width="2268" height="1907" loading="lazy">

<p>Tier 1 (foundation) contains BAP, VCP, MCP, CCP, MICP, CSIP, and BASS. Tier 2 (grouping layer) contains CAP, which coordinates the Tier 1 profiles. Tier 3 (use-case profiles) contains TMAP for telephony and media, HAP for hearing aids, and PBP for public broadcasts. Each tier builds on the one below it.</p>
<p>Think of it as a wedding cake with three tiers:</p>
<h3 id="heading-tier-1-the-foundation-core-services-and-profiles">Tier 1: The Foundation (Core Services and Profiles)</h3>
<p>These are the building blocks everything else is built on:</p>
<h4 id="heading-bap-basic-audio-profile">BAP — Basic Audio Profile</h4>
<p>The big kahuna. BAP defines the fundamental procedures for discovering, configuring, and establishing LE Audio streams. It defines two roles:</p>
<ul>
<li><p><strong>Unicast Client</strong>: The device that initiates and controls audio streams (typically your phone)</p>
</li>
<li><p><strong>Unicast Server</strong>: The device that renders or captures audio (typically your earbuds)</p>
</li>
</ul>
<p>BAP relies on several GATT services:</p>
<ul>
<li><p><strong>PACS</strong> (Published Audio Capabilities Service): "Hey, here's what audio formats I support"</p>
</li>
<li><p><strong>ASCS</strong> (Audio Stream Control Service): "Let's set up and manage audio streams"</p>
</li>
</ul>
<h4 id="heading-vcp-volume-control-profile">VCP — Volume Control Profile</h4>
<p>Handles remote volume control. Your phone can control the volume on your earbuds (and vice versa) using the <strong>VCS</strong> (Volume Control Service).</p>
<h4 id="heading-mcp-media-control-profile">MCP — Media Control Profile</h4>
<p>Allows remote control of media playback. Pause, play, skip, and so on, through the <strong>MCS</strong> (Media Control Service). Like AVRCP for LE Audio.</p>
<h4 id="heading-ccp-call-control-profile">CCP — Call Control Profile</h4>
<p>Manages phone call state. Answer, reject, hold calls via the <strong>TBS</strong> (Telephone Bearer Service). This replaces HFP's call control functionality.</p>
<h4 id="heading-micp-microphone-control-profile">MICP — Microphone Control Profile</h4>
<p>Handles remote mute/unmute of a device's microphone. Simple but essential, ever been on a call where you couldn't figure out how to mute? MICP standardizes it.</p>
<h4 id="heading-csip-coordinated-set-identification-profile">CSIP — Coordinated Set Identification Profile</h4>
<p>This is the "these two earbuds belong together" profile. It uses the <strong>CSIS</strong> (Coordinated Set Identification Service) to tell the phone: "Hey, I'm the left earbud, and my buddy over there is the right earbud. We're a set."</p>
<p>Without CSIP, your phone would treat each earbud as a completely independent device. CSIP is what enables seamless "coordinated set" behavior.</p>
<h4 id="heading-bass-broadcast-audio-scan-service">BASS — Broadcast Audio Scan Service</h4>
<p>Handles the discovery of broadcast audio sources. A device with BASS can scan for nearby broadcasts and help another device (like hearing aids) tune into them.</p>
<h3 id="heading-tier-2-the-grouping-layer">Tier 2: The Grouping Layer</h3>
<h4 id="heading-cap-common-audio-profile">CAP — Common Audio Profile</h4>
<p>CAP sits on top of the Tier 1 profiles and provides common procedures that higher-level profiles use. It handles things like:</p>
<ul>
<li><p>Discovering a coordinated set of devices (using CSIP)</p>
</li>
<li><p>Setting up unicast audio streams to a coordinated set (using BAP)</p>
</li>
<li><p>Initiating broadcast audio streams</p>
</li>
</ul>
<p>Think of CAP as the "orchestrator" that coordinates all the Tier 1 profiles to work together.</p>
<h3 id="heading-tier-3-the-use-case-profiles">Tier 3: The Use-Case Profiles</h3>
<p>These are the profiles that map to actual user scenarios:</p>
<h4 id="heading-tmap-telephony-and-media-audio-profile">TMAP — Telephony and Media Audio Profile</h4>
<p>The "all-in-one" profile for typical audio use cases. TMAP defines roles like:</p>
<ul>
<li><p><strong>Call Terminal (CT)</strong>: Can make and receive calls</p>
</li>
<li><p><strong>Unicast Media Sender (UMS)</strong>: Can send media audio (your phone)</p>
</li>
<li><p><strong>Unicast Media Receiver (UMR)</strong>: Can receive media audio (your earbuds)</p>
</li>
<li><p><strong>Broadcast Media Sender (BMS)</strong>: Can broadcast media audio</p>
</li>
<li><p><strong>Broadcast Media Receiver (BMR)</strong>: Can receive broadcast media audio</p>
</li>
</ul>
<p>If you're building a typical phone + earbuds experience, TMAP is your profile.</p>
<h4 id="heading-hap-hearing-access-profile">HAP — Hearing Access Profile</h4>
<p>The standardized profile for hearing aids. This replaces the proprietary MFi and ASHA solutions with an official Bluetooth standard. HAP defines procedures for:</p>
<ul>
<li><p>Streaming audio to hearing aids</p>
</li>
<li><p>Adjusting hearing aid presets</p>
</li>
<li><p>Controlling volume on hearing aids</p>
</li>
</ul>
<p>This is a huge deal. For the first time, hearing aids can interoperate across all Bluetooth devices using a standard protocol.</p>
<h4 id="heading-pbp-public-broadcast-profile">PBP — Public Broadcast Profile</h4>
<p>Defines how to set up and discover public broadcasts (Auracast). This is what enables "broadcast audio in the airport terminal" scenarios.</p>
<h2 id="heading-7-multi-stream-audio-no-more-left-earbud-relay">7. Multi-Stream Audio: No More Left Earbud Relay</h2>
<p>Remember the relay problem with Classic Bluetooth? LE Audio eliminates it entirely with <strong>multi-stream audio</strong>.</p>
<p>With LE Audio, the source device (your phone) can send independent, synchronized audio streams directly to each earbud:</p>
<img src="https://cdn.hashnode.com/uploads/covers/68a51326db25241b7cb0c047/45b7d1d7-f9ba-4857-a296-64bc0dfdd346.png" alt="Diagram comparing Classic Bluetooth relay architecture with LE Audio multi-stream architecture" style="display:block;margin:0 auto" width="2858" height="1018" loading="lazy">

<p>This diagram compares Classic Bluetooth relay architecture (phone sends stereo to primary earbud, which relays to secondary) with LE Audio multi-stream architecture (phone sends independent synchronized streams directly to each earbud via separate CIS channels within a CIG). The LE Audio approach provides balanced battery drain and lower latency.</p>
<h3 id="heading-how-it-works">How It Works</h3>
<ol>
<li><p>Both earbuds connect to the phone independently via BLE</p>
</li>
<li><p>The phone identifies them as a coordinated set using CSIP</p>
</li>
<li><p>The phone establishes a <strong>CIG</strong> (Connected Isochronous Group) with two <strong>CIS</strong> streams, one per earbud</p>
</li>
<li><p>The phone sends the left channel on CIS #1 and the right channel on CIS #2</p>
</li>
<li><p>The CIG ensures both streams are synchronized, the earbuds play their respective channels at exactly the same time</p>
</li>
</ol>
<p>Benefits:</p>
<ul>
<li><p><strong>Balanced battery drain</strong>: Both earbuds do equal work</p>
</li>
<li><p><strong>Lower latency</strong>: No relay hop means fewer delays</p>
</li>
<li><p><strong>Better reliability</strong>: If one earbud loses connection, the other keeps playing</p>
</li>
<li><p><strong>True stereo</strong>: Each earbud gets its own independent stream, no need to decode and split</p>
</li>
</ul>
<h2 id="heading-8-auracast-broadcast-audio-for-the-masses">8. Auracast: Broadcast Audio for the Masses</h2>
<p><strong>Auracast</strong> is LE Audio's broadcast feature, and it's arguably the most revolutionary part. It's like FM radio for Bluetooth: one source, unlimited listeners.</p>
<h3 id="heading-how-auracast-works">How Auracast Works</h3>
<ol>
<li><p>A Broadcast Source creates a BIG (Broadcast Isochronous Group) containing one or more BIS streams</p>
</li>
<li><p>The source advertises the broadcast using Extended Advertising with metadata (stream name, language, codec config)</p>
</li>
<li><p>A Broadcast Sink discovers the advertisement, syncs to the Periodic Advertising train to get stream parameters</p>
</li>
<li><p>The sink joins the BIG and starts receiving audio</p>
</li>
</ol>
<img src="https://cdn.hashnode.com/uploads/covers/68a51326db25241b7cb0c047/d00d5d24-e7c1-44ae-9052-b61ae049b2ba.png" alt="Diagram of the Auracast broadcast flow" style="display:block;margin:0 auto" width="2676" height="1540" loading="lazy">

<p>The above diagram shows the Auracast broadcast flow: a broadcast source advertises via Extended Advertising, broadcast sinks discover the advertisement and sync to Periodic Advertising to receive stream parameters, then join the BIG to receive audio. There is no limit on the number of sinks.</p>
<h3 id="heading-auracast-use-cases">Auracast Use Cases</h3>
<p>The use cases are actually compelling:</p>
<ul>
<li><p><strong>Airports/Train Stations</strong>: Broadcast gate announcements directly to travelers' earbuds (in multiple languages!)</p>
</li>
<li><p><strong>Gyms</strong>: Every TV on the wall can broadcast its own audio, pick which one to listen to</p>
</li>
<li><p><strong>Museums</strong>: Audio guides streamed to visitors' own earbuds</p>
</li>
<li><p><strong>Bars/Sports Events</strong>: Watch the game on the big screen with commentary in your earbuds, without blasting everyone</p>
</li>
<li><p><strong>Conferences</strong>: Live translation channels broadcast to attendees</p>
</li>
<li><p><strong>Silent Discos</strong>: Obviously</p>
</li>
</ul>
<h3 id="heading-the-bass-role-broadcast-assistants">The BASS Role: Broadcast Assistants</h3>
<p>There's a neat supporting concept called a <strong>Broadcast Assistant</strong>. This is a device (typically your phone) that helps another device (typically your earbuds) discover and tune into broadcasts.</p>
<p>Why? Because tiny earbuds might not have the processing power or UI to scan for and select broadcasts themselves. So your phone does the scanning, shows you available broadcasts, and tells your earbuds which one to tune into via the <strong>BASS</strong> (Broadcast Audio Scan Service).</p>
<img src="https://cdn.hashnode.com/uploads/covers/68a51326db25241b7cb0c047/dda3bf79-028f-4624-bfa1-7616bbb40a25.png" alt="Diagram showing the Broadcast Assistant role" style="display:block;margin:0 auto" width="3120" height="2568" loading="lazy">

<p>The above diagram showes the Broadcast Assistant role: a phone scans for available Auracast broadcasts and displays them to the user. When the user selects a broadcast, the phone (acting as Broadcast Assistant) instructs the user's earbuds to tune into the selected broadcast via BASS (Broadcast Audio Scan Service), since the earbuds may lack the UI or processing power to scan on their own.</p>
<h2 id="heading-9-le-audio-in-androidaosp-the-implementation">9. LE Audio in Android/AOSP: The Implementation</h2>
<p>Now let's get into the code. This is where the rubber meets the road.</p>
<h3 id="heading-timeline-of-android-le-audio-support">Timeline of Android LE Audio Support</h3>
<ul>
<li><p><strong>Android 12 (2021)</strong>: Initial LE Audio APIs introduced (developer preview quality)</p>
</li>
<li><p><strong>Android 13 (2022)</strong>: Full LE Audio support, including unicast client/server, broadcast source/sink</p>
</li>
<li><p><strong>Android 14 (2023)</strong>: Improved stability, broadcast audio enhancements, LE Audio source role support</p>
</li>
<li><p><strong>Android 15 (2024)</strong>: Auracast Broadcast Sink support, Broadcast Assistant role, improved audio context switching</p>
</li>
<li><p><strong>Android 16 (2025)</strong>: Native Auracast UI in Quick Settings/Bluetooth settings, enhanced audio sharing experience</p>
</li>
</ul>
<p>The LE Audio implementation in AOSP lives primarily in the <strong>Bluetooth module</strong> (<code>packages/modules/Bluetooth</code>), which is a <strong>Mainline module</strong>, meaning it can be updated via Google Play System Updates independent of full Android OS updates.</p>
<h3 id="heading-key-aosp-source-locations">Key AOSP Source Locations</h3>
<p>If you want to dive into the code yourself, here's your treasure map:</p>
<table>
<thead>
<tr>
<th>Component</th>
<th>Path</th>
</tr>
</thead>
<tbody><tr>
<td>LE Audio Java Service</td>
<td><code>packages/modules/Bluetooth/android/app/src/com/android/bluetooth/le_audio/LeAudioService.java</code></td>
</tr>
<tr>
<td>JNI Bridge</td>
<td><code>packages/modules/Bluetooth/android/app/src/com/android/bluetooth/le_audio/LeAudioNativeInterface.java</code></td>
</tr>
<tr>
<td>Native LE Audio Client</td>
<td><code>packages/modules/Bluetooth/system/bta/le_audio/le_audio_client.cc</code></td>
</tr>
<tr>
<td>Codec Manager</td>
<td><code>packages/modules/Bluetooth/system/bta/le_audio/codec_manager.cc</code></td>
</tr>
<tr>
<td>State Machine</td>
<td><code>packages/modules/Bluetooth/system/bta/le_audio/state_machine.cc</code></td>
</tr>
<tr>
<td>LC3 Codec Library</td>
<td><code>external/liblc3/</code></td>
</tr>
<tr>
<td>Framework API</td>
<td><code>frameworks/base/core/java/android/bluetooth/BluetoothLeAudio.java</code></td>
</tr>
<tr>
<td>Broadcast API</td>
<td><code>frameworks/base/core/java/android/bluetooth/BluetoothLeBroadcast.java</code></td>
</tr>
</tbody></table>
<h3 id="heading-high-level-architecture">High-Level Architecture</h3>
<p>The AOSP Bluetooth stack for LE Audio follows Android's classic layered architecture:</p>
<img src="https://cdn.hashnode.com/uploads/covers/68a51326db25241b7cb0c047/1f4a9658-a26a-4388-917d-92794f0f407a.png" alt="Layered architecture diagram of the AOSP Bluetooth LE Audio stack" style="display:block;margin:0 auto" width="1335" height="444" loading="lazy">

<p>In this layered architecture diagram of the AOSP Bluetooth LE Audio stack, here's what's shown from top to bottom: Application layer, Framework APIs (BluetoothLeAudio, BluetoothLeBroadcast), LeAudioService (Java), JNI Bridge, Native C++ stack (le_audio_client, codec_manager, state_machine, iso_manager), HCI layer, and Bluetooth Controller hardware.</p>
<h2 id="heading-10-the-aosp-architecture-from-app-to-antenna">10. The AOSP Architecture: From App to Antenna</h2>
<p>Let's walk through each layer in detail.</p>
<h3 id="heading-layer-1-the-framework-apis">Layer 1: The Framework APIs</h3>
<p>Android exposes LE Audio functionality through several public API classes in <code>android.bluetooth</code>:</p>
<h4 id="heading-bluetoothleaudio"><code>BluetoothLeAudio</code></h4>
<p>The main API for unicast LE Audio. Apps use this to:</p>
<ul>
<li><p>Connect to LE Audio devices</p>
</li>
<li><p>Set active device for audio playback/capture</p>
</li>
<li><p>Query group information (coordinated sets)</p>
</li>
<li><p>Select codec configuration</p>
</li>
</ul>
<pre><code class="language-java">// Example: Connect to an LE Audio device
BluetoothLeAudio leAudio = bluetoothAdapter.getProfileProxy(
    context, listener, BluetoothProfile.LE_AUDIO);

// Set the LE Audio device as active for media playback
leAudio.setActiveDevice(leAudioDevice);
</code></pre>
<h4 id="heading-bluetoothlebroadcast"><code>BluetoothLeBroadcast</code></h4>
<p>API for broadcast audio (Auracast). Apps use this to:</p>
<ul>
<li><p>Start/stop broadcast audio</p>
</li>
<li><p>Set broadcast metadata (name, language)</p>
</li>
<li><p>Configure broadcast code (encryption password)</p>
</li>
</ul>
<pre><code class="language-java">// Start a broadcast
BluetoothLeBroadcast broadcast = bluetoothAdapter.getProfileProxy(
    context, listener, BluetoothProfile.LE_AUDIO_BROADCAST);

broadcast.startBroadcast(contentMetadata, audioConfig, broadcastCode);
</code></pre>
<h4 id="heading-bluetoothlebroadcastassistant"><code>BluetoothLeBroadcastAssistant</code></h4>
<p>API for the broadcast assistant role, helping another device tune into a broadcast.</p>
<h4 id="heading-bluetoothvolumecontrol"><code>BluetoothVolumeControl</code></h4>
<p>API for remote volume control via VCP.</p>
<h4 id="heading-bluetoothhapclient"><code>BluetoothHapClient</code></h4>
<p>API for the Hearing Access Profile, controlling hearing aid presets and streaming.</p>
<h3 id="heading-layer-2-leaudioservice-the-brain">Layer 2: LeAudioService (The Brain)</h3>
<p>The <code>LeAudioService</code> is the central service within the Bluetooth app that orchestrates all LE Audio functionality. This is where the magic happens.</p>
<p>Key responsibilities:</p>
<ul>
<li><p><strong>Device Management</strong>: Tracking connected LE Audio devices and their capabilities</p>
</li>
<li><p><strong>Group Management</strong>: Managing coordinated sets (which devices belong together)</p>
</li>
<li><p><strong>Audio Routing</strong>: Deciding which device(s) should be active for playback/capture</p>
</li>
<li><p><strong>State Machine Management</strong>: Handling the lifecycle of audio connections</p>
</li>
<li><p><strong>Profile Coordination</strong>: Coordinating BAP, VCP, MCP, CCP, and CSIP</p>
</li>
</ul>
<p>Here's a simplified view of how <code>LeAudioService</code> is structured:</p>
<pre><code class="language-java">public class LeAudioService extends ProfileService {
    
    // Map of device address -&gt; state machine
    private Map&lt;BluetoothDevice, LeAudioStateMachine&gt; mStateMachines;
    
    // Map of group ID -&gt; group information
    private Map&lt;Integer, LeAudioGroupDescriptor&gt; mGroupDescriptors;
    
    // Native interface bridge
    private LeAudioNativeInterface mNativeInterface;
    
    // Active device tracking
    private BluetoothDevice mActiveAudioOutDevice;
    private BluetoothDevice mActiveAudioInDevice;
    
    // Codec configuration
    private BluetoothLeAudioCodecConfig mInputLocalCodecConfig;
    private BluetoothLeAudioCodecConfig mOutputLocalCodecConfig;
    
    public void connect(BluetoothDevice device) {
        // 1. Check if device supports LE Audio (PACS)
        // 2. Create state machine for device
        // 3. Initiate connection via native stack
        // 4. Discover GATT services (PACS, ASCS, VCS, etc.)
        // 5. Read audio capabilities
    }
    
    public void setActiveDevice(BluetoothDevice device) {
        // 1. Look up device's group
        // 2. Find all devices in the coordinated set
        // 3. Configure audio streams via BAP
        // 4. Set up isochronous channels
        // 5. Start audio routing
    }
}
</code></pre>
<h3 id="heading-layer-3-the-native-stack-c">Layer 3: The Native Stack (C++)</h3>
<p>Below the Java layer, the heavy lifting happens in C++. The native LE Audio implementation lives in the Bluetooth stack (historically called "Fluoride," with newer components in "Gabeldorsche").</p>
<p>Key native components:</p>
<h4 id="heading-leaudioclientcc-leaudioclientimpl"><code>le_audio_client.cc</code> / <code>le_audio_client_impl</code></h4>
<p>The main C++ implementation of the LE Audio client. This handles:</p>
<ul>
<li><p>GATT client operations (discovering services, reading characteristics)</p>
</li>
<li><p>ASE (Audio Stream Endpoint) state machine management</p>
</li>
<li><p>Codec negotiation with remote devices</p>
</li>
<li><p>CIS/BIS creation and management</p>
</li>
</ul>
<h4 id="heading-statemachinecc"><code>state_machine.cc</code></h4>
<p>Manages the connection state machine for each LE Audio device:</p>
<img src="https://cdn.hashnode.com/uploads/covers/68a51326db25241b7cb0c047/408b1944-6522-403f-84d1-639362e0b5df.png" alt="State diagram of the native LE Audio connection state machine with states: Disconnected, Connecting, Connected, and Disconnecting. " style="display:block;margin:0 auto" width="2562" height="656" loading="lazy">

<p>The above is a state diagram of the native LE Audio connection state machine with states: Disconnected, Connecting, Connected, and Disconnecting. The state machine is managed per-device in the native C++ layer and drives GATT connection setup, service discovery, and characteristic reads before transitioning to Connected.</p>
<h4 id="heading-codecmanagercc"><code>codec_manager.cc</code></h4>
<p>Handles codec configuration:</p>
<ul>
<li><p>Enumerates supported codec capabilities</p>
</li>
<li><p>Selects optimal codec configuration based on device capabilities and use case</p>
</li>
<li><p>Interfaces with the LC3 encoder/decoder</p>
</li>
</ul>
<h4 id="heading-isomanagercc"><code>iso_manager.cc</code></h4>
<p>Manages isochronous channels:</p>
<ul>
<li><p>Creates and tears down CIG/CIS for unicast</p>
</li>
<li><p>Creates and tears down BIG/BIS for broadcast</p>
</li>
<li><p>Handles the HCI interface for isochronous data</p>
</li>
</ul>
<h4 id="heading-audiohalclientcc"><code>audio_hal_client.cc</code></h4>
<p>Bridges the Bluetooth stack with the Android audio HAL:</p>
<ul>
<li><p>Receives PCM audio from the Android audio framework</p>
</li>
<li><p>Passes it to the LC3 encoder</p>
</li>
<li><p>Sends encoded audio over isochronous channels</p>
</li>
</ul>
<h3 id="heading-layer-4-the-controller-hardware">Layer 4: The Controller (Hardware)</h3>
<p>The Bluetooth controller handles the low-level radio operations:</p>
<ul>
<li><p>Link layer scheduling of isochronous events</p>
</li>
<li><p>PHY layer (1M, 2M, or Coded PHY)</p>
</li>
<li><p>Packet formatting and CRC</p>
</li>
<li><p>Retransmission of lost isochronous PDUs</p>
</li>
</ul>
<p>The host (Android) communicates with the controller via <strong>HCI</strong> (Host Controller Interface), using specific HCI commands for isochronous channels:</p>
<ul>
<li><p><code>HCI_LE_Set_CIG_Parameters</code>: Configure a Connected Isochronous Group</p>
</li>
<li><p><code>HCI_LE_Create_CIS</code>: Create Connected Isochronous Streams</p>
</li>
<li><p><code>HCI_LE_Create_BIG</code>: Create a Broadcast Isochronous Group</p>
</li>
<li><p><code>HCI_LE_Setup_ISO_Data_Path</code>: Set up the path for ISO data (HCI vs. vendor-specific)</p>
</li>
<li><p><code>HCI_LE_BIG_Create_Sync</code>: Synchronize to a BIG (for broadcast receivers)</p>
</li>
</ul>
<h2 id="heading-11-server-side-source-implementation">11. Server-Side (Source) Implementation</h2>
<p>The "server side" in LE Audio terminology is actually the <strong>Unicast Server</strong>, the device that renders audio (your earbuds). Yes, it's confusing that the receiver is called the "server." Think of it as a GATT server: it hosts the GATT services that the client connects to.</p>
<h3 id="heading-what-the-unicast-server-does">What the Unicast Server Does</h3>
<p>The Unicast Server (earbud) hosts several GATT services:</p>
<img src="https://cdn.hashnode.com/uploads/covers/68a51326db25241b7cb0c047/a6ce4211-1720-46b3-8965-0f5346c413fb.png" alt="GATT services hosted by a Unicast Server (earbud)" style="display:block;margin:0 auto" width="860" height="600" loading="lazy">

<p>The above diagram shows the GATT services hosted by a Unicast Server (earbud). The server exposes four key services:</p>
<ul>
<li><p>PACS (Published Audio Capabilities Service), which advertises the device's supported codecs, sample rates, frame durations, and audio contexts</p>
</li>
<li><p>ASCS (Audio Stream Control Service), which contains one or more ASE (Audio Stream Endpoint) characteristics that the client writes to in order to configure and control audio streams</p>
</li>
<li><p>VCS (Volume Control Service), which allows the client to read and set the device's volume level</p>
</li>
<li><p>and CSIS (Coordinated Set Identification Service), which identifies this device as part of a coordinated set (for example, "I am the left earbud, and my partner is the right earbud").</p>
</li>
</ul>
<p>The Unicast Client (phone) connects to these services via GATT to discover capabilities, configure streams, and control playback.</p>
<h3 id="heading-the-ase-state-machine-server-side">The ASE State Machine (Server Side)</h3>
<p>Each <strong>ASE</strong> (Audio Stream Endpoint) on the server has a state machine. This is the heart of audio stream management:</p>
<img src="https://cdn.hashnode.com/uploads/covers/68a51326db25241b7cb0c047/e47ff774-1f97-4704-9ca7-83ba31ab17b1.png" alt="State diagram of the ASE (Audio Stream Endpoint) state machine on the Unicast Server" style="display:block;margin:0 auto" width="745" height="2157" loading="lazy">

<p>The above is a state diagram of the ASE (Audio Stream Endpoint) state machine on the Unicast Server. States: Idle, Codec Configured, QoS Configured, Enabling, Streaming, Disabling, and Releasing. The client drives transitions by writing operations (Config Codec, Config QoS, Enable, Disable, Release) to the ASE Control Point characteristic.</p>
<p>State transitions:</p>
<ol>
<li><p><strong>IDLE → CODEC_CONFIGURED</strong>: The client writes a <code>Config Codec</code> operation to the ASE Control Point, specifying codec type (LC3), sample rate, frame duration, and so on.</p>
</li>
<li><p><strong>CODEC_CONFIGURED → QoS_CONFIGURED</strong>: The client writes a <code>Config QoS</code> operation, specifying:</p>
<ul>
<li><p>SDU interval (how often audio frames are sent)</p>
</li>
<li><p>Framing (framed or unframed)</p>
</li>
<li><p>Max SDU size</p>
</li>
<li><p>Retransmission number</p>
</li>
<li><p>Max transport latency</p>
</li>
<li><p>Presentation delay</p>
</li>
</ul>
</li>
<li><p><strong>QoS_CONFIGURED → ENABLING</strong>: The client writes an <code>Enable</code> operation. The server prepares to receive audio.</p>
</li>
<li><p><strong>ENABLING → STREAMING</strong>: The CIS is established and audio data starts flowing. This transition happens after the client creates the CIS and both sides are ready.</p>
</li>
<li><p><strong>STREAMING → DISABLING</strong>: The client writes a <code>Disable</code> operation, or the connection is being torn down.</p>
</li>
<li><p><strong>Any state → IDLE</strong>: The client writes a <code>Release</code> operation, tearing down the stream configuration.</p>
</li>
</ol>
<h3 id="heading-standard-codec-configurations">Standard Codec Configurations</h3>
<p>BAP defines a set of named codec configurations that map to specific LC3 parameters. These are the "presets" that devices negotiate:</p>
<table>
<thead>
<tr>
<th>Config</th>
<th>Sample Rate</th>
<th>Frame Duration</th>
<th>Octets/Frame</th>
<th>Bitrate</th>
<th>Typical Use</th>
</tr>
</thead>
<tbody><tr>
<td>8_1</td>
<td>8 kHz</td>
<td>7.5 ms</td>
<td>26</td>
<td>~27.7 kbps</td>
<td>Low-bandwidth voice</td>
</tr>
<tr>
<td>8_2</td>
<td>8 kHz</td>
<td>10 ms</td>
<td>30</td>
<td>24 kbps</td>
<td>Low-bandwidth voice</td>
</tr>
<tr>
<td>16_1</td>
<td>16 kHz</td>
<td>7.5 ms</td>
<td>30</td>
<td>32 kbps</td>
<td>Telephony (low latency)</td>
</tr>
<tr>
<td>16_2</td>
<td>16 kHz</td>
<td>10 ms</td>
<td>40</td>
<td>32 kbps</td>
<td>Telephony (standard)</td>
</tr>
<tr>
<td>24_2</td>
<td>24 kHz</td>
<td>10 ms</td>
<td>60</td>
<td>48 kbps</td>
<td>Wideband voice</td>
</tr>
<tr>
<td>32_1</td>
<td>32 kHz</td>
<td>7.5 ms</td>
<td>60</td>
<td>64 kbps</td>
<td>Super-wideband voice</td>
</tr>
<tr>
<td>32_2</td>
<td>32 kHz</td>
<td>10 ms</td>
<td>80</td>
<td>64 kbps</td>
<td>Super-wideband voice</td>
</tr>
<tr>
<td>48_1</td>
<td>48 kHz</td>
<td>7.5 ms</td>
<td>75</td>
<td>80 kbps</td>
<td>Music (low latency)</td>
</tr>
<tr>
<td>48_2</td>
<td>48 kHz</td>
<td>10 ms</td>
<td>100</td>
<td>80 kbps</td>
<td>Music (balanced)</td>
</tr>
<tr>
<td>48_4</td>
<td>48 kHz</td>
<td>10 ms</td>
<td>120</td>
<td>96 kbps</td>
<td>Music (high quality)</td>
</tr>
<tr>
<td>48_6</td>
<td>48 kHz</td>
<td>10 ms</td>
<td>155</td>
<td>124 kbps</td>
<td>Music (highest quality)</td>
</tr>
</tbody></table>
<p>For most consumer earbuds, you'll see <strong>48_4</strong> (96 kbps at 48 kHz) for media and <strong>16_2</strong> (32 kbps at 16 kHz) for phone calls. That single LC3 codec handles both use cases – no more switching between SBC and mSBC!</p>
<h3 id="heading-audio-context-types">Audio Context Types</h3>
<p>LE Audio defines <strong>Audio Context Types</strong>, metadata that tells the receiving device <em>what kind</em> of audio is being streamed. This allows the device to optimize its behavior (for example, enabling noise cancellation for calls or boosting bass for music):</p>
<table>
<thead>
<tr>
<th>Context</th>
<th>Bit</th>
<th>When It's Used</th>
</tr>
</thead>
<tbody><tr>
<td>Unspecified</td>
<td>0x0001</td>
<td>Generic audio, no specific optimization</td>
</tr>
<tr>
<td>Conversational</td>
<td>0x0002</td>
<td>Phone calls, VoIP, bidirectional, low-latency</td>
</tr>
<tr>
<td>Media</td>
<td>0x0004</td>
<td>Music, podcasts, video, high quality</td>
</tr>
<tr>
<td>Game</td>
<td>0x0008</td>
<td>Gaming, ultra-low latency priority</td>
</tr>
<tr>
<td>Instructional</td>
<td>0x0010</td>
<td>Navigation prompts, announcements</td>
</tr>
<tr>
<td>Voice Assistants</td>
<td>0x0020</td>
<td>"Hey Google" / "Hey Siri"</td>
</tr>
<tr>
<td>Live</td>
<td>0x0040</td>
<td>Live audio (concerts, broadcasts)</td>
</tr>
<tr>
<td>Sound Effects</td>
<td>0x0080</td>
<td>UI clicks, keyboard sounds</td>
</tr>
<tr>
<td>Notifications</td>
<td>0x0100</td>
<td>Message alerts, app notifications</td>
</tr>
<tr>
<td>Ringtone</td>
<td>0x0200</td>
<td>Incoming call ringtone</td>
</tr>
<tr>
<td>Alerts</td>
<td>0x0400</td>
<td>Alarms, timer alerts</td>
</tr>
<tr>
<td>Emergency Alarm</td>
<td>0x0800</td>
<td>Emergency broadcast alerts</td>
</tr>
</tbody></table>
<p>This is way more granular than Classic Audio, which basically only knew two states: "you're playing music" (A2DP) or "you're on a call" (HFP). With LE Audio, the device can make intelligent decisions, like "this is a game, use 7.5ms frames for minimum latency" or "this is a notification, mix it in without interrupting the music stream."</p>
<h3 id="heading-aosp-unicast-server-implementation">AOSP Unicast Server Implementation</h3>
<p>In AOSP, the Unicast Server functionality is implemented primarily for cases where the Android device acts as a receiver (for example, an Android-powered hearing aid or a Chromebook receiving audio).</p>
<p>Key classes:</p>
<ul>
<li><p><code>LeAudioService.java</code>: Handles server-side operations when the device is in sink role</p>
</li>
<li><p>In native code: <code>le_audio_server.cc</code> manages the GATT server hosting PACS, ASCS, and so on.</p>
</li>
</ul>
<h3 id="heading-broadcast-source-implementation">Broadcast Source Implementation</h3>
<p>For broadcast audio (Auracast), the source side in AOSP involves:</p>
<pre><code class="language-java">// In LeAudioService.java / BroadcastService
public void startBroadcast(BluetoothLeBroadcastSettings settings) {
    // 1. Configure LC3 encoder with broadcast parameters
    // 2. Set up Extended Advertising with broadcast metadata
    // 3. Set up Periodic Advertising for stream parameters
    // 4. Create BIG via HCI
    // 5. Start sending ISO data on BIS streams
}
</code></pre>
<p>The native implementation:</p>
<ul>
<li><p><code>broadcaster.cc</code> / <code>broadcaster_impl</code>: Manages broadcast lifecycle</p>
</li>
<li><p>Configures <strong>Extended Advertising</strong> with the broadcast name and metadata</p>
</li>
<li><p>Configures <strong>Periodic Advertising</strong> to carry the BASE (Broadcast Audio Source Endpoint) data structure</p>
</li>
<li><p>Creates a <strong>BIG</strong> with the appropriate number of BIS streams</p>
</li>
<li><p>Routes encoded audio to the BIS data path</p>
</li>
</ul>
<h2 id="heading-12-client-side-sink-implementation">12. Client-Side (Sink) Implementation</h2>
<p>The "client side" is the <strong>Unicast Client</strong>, typically your phone. It discovers, connects to, and controls LE Audio devices.</p>
<h3 id="heading-connection-flow">Connection Flow</h3>
<p>Here's what happens when you connect to LE Audio earbuds, step by step:</p>
<img src="https://cdn.hashnode.com/uploads/covers/68a51326db25241b7cb0c047/391fca1a-897e-4e4c-b8ea-d0daad76bb38.png" alt="Sequence diagram of the LE Audio connection flow between a phone (Unicast Client) and earbuds (Unicast Server). " style="display:block;margin:0 auto" width="2574" height="3653" loading="lazy">

<p>Steps: BLE scan and discovery, GATT connection, service discovery (finding PACS, ASCS, CSIP, VCS), reading PAC records to learn audio capabilities, reading CSIS to identify coordinated set membership, then ASE configuration (Config Codec, Config QoS, Enable) followed by CIS creation and audio streaming.</p>
<h3 id="heading-aosp-client-implementation-in-detail">AOSP Client Implementation in Detail</h3>
<h4 id="heading-step-1-3-discovery-and-connection">Step 1-3: Discovery and Connection</h4>
<pre><code class="language-java">// LeAudioService.java
public void connect(BluetoothDevice device) {
    // Creates a new LeAudioStateMachine for this device
    LeAudioStateMachine sm = getOrCreateStateMachine(device);
    sm.sendMessage(LeAudioStateMachine.CONNECT);
    
    // The state machine handles:
    // - GATT connection
    // - Service discovery
    // - Characteristic reads
}
</code></pre>
<p>The <code>LeAudioStateMachine</code> manages the connection lifecycle:</p>
<pre><code class="language-java">// LeAudioStateMachine.java (simplified)
class LeAudioStateMachine extends StateMachine {
    
    class Disconnected extends State {
        void processMessage(Message msg) {
            if (msg.what == CONNECT) {
                // Initiate GATT connection via native
                mNativeInterface.connectLeAudio(mDevice);
                transitionTo(mConnecting);
            }
        }
    }
    
    class Connecting extends State {
        void processMessage(Message msg) {
            if (msg.what == CONNECTION_STATE_CHANGED) {
                if (newState == CONNECTED) {
                    transitionTo(mConnected);
                }
            }
        }
    }
    
    class Connected extends State {
        void enter() {
            // GATT services have been discovered
            // Audio capabilities have been read
            // Device is ready for streaming
            broadcastConnectionState(BluetoothProfile.STATE_CONNECTED);
        }
    }
}
</code></pre>
<h4 id="heading-step-4-6-capability-discovery">Step 4-6: Capability Discovery</h4>
<p>The native layer reads PACS to understand what the remote device supports:</p>
<pre><code class="language-cpp">// In native le_audio_client_impl (C++)
void OnGattServiceDiscovery(BluetoothDevice device) {
    // Read PAC records from PACS
    ReadPacsCharacteristics(device);
    
    // Read CSIS for coordinated set info
    ReadCsisCharacteristics(device);
    
    // Read ASCS for ASE count and state
    ReadAscsCharacteristics(device);
}

void OnPacsRead(BluetoothDevice device, PacRecord sink_pac) {
    // sink_pac contains:
    //   codec_id: LC3
    //   sampling_frequencies: 48000, 44100, 32000, 24000, 16000, 8000
    //   frame_durations: 10ms, 7.5ms
    //   channel_counts: 1
    //   octets_per_frame: 40-155  (maps to bitrate range)
    //   supported_contexts: MEDIA, CONVERSATIONAL, GAME
    
    // Store capabilities for later codec negotiation
    device_info.sink_capabilities = sink_pac;
}
</code></pre>
<h4 id="heading-step-7-12-stream-setup">Step 7-12: Stream Setup</h4>
<p>When audio playback begins, the client configures and enables streams:</p>
<pre><code class="language-cpp">// In native codec_manager (C++)
CodecConfig SelectCodecConfiguration(
    PacRecord remote_capabilities,
    AudioContext context  // MEDIA, CONVERSATIONAL, etc.
) {
    // For media playback, prefer high quality:
    //   48 kHz, 10ms frames, 96 kbps per channel
    
    // For voice calls, optimize for latency:
    //   16 kHz, 7.5ms frames, 32 kbps per channel
    
    // Negotiate: intersect local and remote capabilities
    // Select the best configuration both sides support
}

// In native le_audio_client_impl
void GroupStreamStart(int group_id, AudioContext context) {
    auto group = GetGroup(group_id);
    auto codec_config = SelectCodecConfiguration(
        group-&gt;GetRemoteCapabilities(), context);
    
    // For each device in the group:
    for (auto&amp; device : group-&gt;GetDevices()) {
        // For each ASE on the device:
        for (auto&amp; ase : device-&gt;GetAses()) {
            // Step 8: Config Codec
            WriteAseControlPoint(device, OPCODE_CONFIG_CODEC, {
                .ase_id = ase-&gt;id,
                .codec_id = LC3,
                .codec_specific = {
                    .sampling_freq = 48000,
                    .frame_duration = 10ms,
                    .channel_allocation = LEFT,  // or RIGHT
                    .octets_per_frame = 120
                }
            });
        }
    }
    // After codec configured notification:
    //   Step 9: Config QoS → Step 10: Enable → Step 11: Create CIS
}
</code></pre>
<h4 id="heading-step-13-audio-data-flow">Step 13: Audio Data Flow</h4>
<p>Once streaming, here's how audio data flows through the AOSP stack:</p>
<img src="https://cdn.hashnode.com/uploads/covers/68a51326db25241b7cb0c047/b42f698d-2a24-4376-8121-bffe101fa7f5.png" alt="Diagram showing audio data flow during LE Audio streaming" style="display:block;margin:0 auto" width="900" height="704" loading="lazy">

<p>The above diagram shows audio data flow during LE Audio streaming: PCM audio from the Android audio framework reaches the Bluetooth Audio HAL, is encoded by the LC3 encoder, packetized into ISO SDUs with timestamps, sent over HCI to the controller, transmitted over the air via CIS, received by the earbud's controller, decoded by the earbud's LC3 decoder, and rendered as audio.</p>
<h3 id="heading-broadcast-sink-implementation">Broadcast Sink Implementation</h3>
<p>For receiving broadcast audio (Auracast), AOSP implements:</p>
<pre><code class="language-cpp">// Broadcast sink flow (native)
void OnBroadcastSourceFound(AdvertisingReport report) {
    // Parse Extended Advertising for broadcast metadata
    BroadcastMetadata metadata = ParseBroadcastMetadata(report);
    
    // Display: "Airport Gate B47 - English"
    NotifyBroadcastSourceFound(metadata);
}

void SyncToBroadcast(BroadcastMetadata metadata) {
    // 1. Sync to Periodic Advertising
    HCI_LE_Periodic_Advertising_Create_Sync(metadata.sync_info);
    
    // 2. On PA sync established, parse BASE
    BASE base = ParseBASE(periodic_adv_data);
    
    // 3. Select subgroup and BIS streams
    // 4. Sync to BIG
    HCI_LE_BIG_Create_Sync(base.big_params, selected_bis);
    
    // 5. Set up ISO data path
    HCI_LE_Setup_ISO_Data_Path(bis_handle, HCI_DATA_PATH);
    
    // 6. Start receiving and decoding audio
}
</code></pre>
<h2 id="heading-13-the-state-machine-that-runs-it-all">13. The State Machine That Runs It All</h2>
<p>The AOSP LE Audio implementation uses several interconnected state machines:</p>
<h3 id="heading-connection-state-machine">Connection State Machine</h3>
<p>Manages the overall connection lifecycle for each device:</p>
<img src="https://cdn.hashnode.com/uploads/covers/68a51326db25241b7cb0c047/f534c8b5-5874-4af8-824c-da1c15e3188e.png" alt="State diagram showing the LE Audio connection state machine with four states: Disconnected, Connecting, Connected, and Disconnecting." style="display:block;margin:0 auto" width="2562" height="656" loading="lazy">

<p>This state diagram shows the LE Audio connection state machine with four states: Disconnected, Connecting, Connected, and Disconnecting.</p>
<p>Transitions: CONNECT event moves from Disconnected to Connecting, successful connection moves to Connected, DISCONNECT event moves to Disconnecting, and completion returns to Disconnected. Timeout or failure from Connecting also returns to Disconnected.</p>
<h3 id="heading-group-audio-state-machine">Group Audio State Machine</h3>
<p>Manages the audio state for a <em>group</em> of devices (coordinated set):</p>
<img src="https://cdn.hashnode.com/uploads/covers/68a51326db25241b7cb0c047/54c9bb32-d6ad-4f1d-8238-7fd8c09c19d4.png" alt="State diagram of the group audio state machine with states: Idle, Codec Configured, QoS Configured, Enabling, Streaming, and Disabling. " style="display:block;margin:0 auto" width="1410" height="2586" loading="lazy">

<p>This is a state diagram showing the group audio state machine with states: Idle, Codec Configured, QoS Configured, Enabling, Streaming, and Disabling. The forward path proceeds through each state in order as audio streams are set up. The Release operation returns any state to Idle.</p>
<h3 id="heading-how-the-pieces-fit-together-code-walkthrough">How the Pieces Fit Together (Code Walkthrough)</h3>
<p>Here's a simplified walkthrough of what happens when you press "play" on your music app with LE Audio earbuds connected:</p>
<img src="https://cdn.hashnode.com/uploads/covers/68a51326db25241b7cb0c047/8086fa96-68c6-4d28-9670-76b3264d9031.png" alt="Diagram that traces the sequence of events when a user presses &quot;play&quot; in a music app with LE Audio earbuds connected" style="display:block;margin:0 auto" width="2800" height="3866" loading="lazy">

<p>The above diagram traces the sequence of events when a user presses "play" in a music app with LE Audio earbuds connected.</p>
<p>The flow is:</p>
<ol>
<li><p>The music app writes PCM audio to an AudioTrack.</p>
</li>
<li><p>The Android AudioFlinger routes the audio to the Bluetooth Audio HAL.</p>
</li>
<li><p>The HAL notifies LeAudioService that audio is starting.</p>
</li>
<li><p>LeAudioService looks up the active group and triggers GroupStreamStart in the native stack.</p>
</li>
<li><p>The native stack configures ASEs on both earbuds (Config Codec → Config QoS → Enable) by writing to the ASCS control point on each device.</p>
</li>
<li><p>The native stack creates a CIG with two CIS channels via HCI.</p>
</li>
<li><p>Both CIS channels are established to the earbuds.</p>
</li>
<li><p>The ISO data path is set up.</p>
</li>
<li><p>PCM audio flows from the HAL to the LC3 encoder, which produces compressed frames</p>
</li>
<li><p>The compressed frames are sent as ISO SDUs over HCI to the controller</p>
</li>
<li><p>The controller transmits the frames over the air on the scheduled CIS intervals</p>
</li>
<li><p>The earbuds receive, decode, and render the audio at the agreed presentation delay.</p>
</li>
</ol>
<h2 id="heading-14-putting-it-all-together-a-day-in-the-life-of-an-le-audio-packet">14. Putting It All Together: A Day in the Life of an LE Audio Packet</h2>
<p>Let's follow a single audio packet from your music app to your earbud:</p>
<img src="https://cdn.hashnode.com/uploads/covers/68a51326db25241b7cb0c047/e4e634cc-04db-4413-a197-ddbd1169c16a.png" alt="Diagram following a single audio packet through every stage of the LE Audio pipeline" style="display:block;margin:0 auto" width="1120" height="1334" loading="lazy">

<p>The above diagram follows a single audio packet through every stage of the LE Audio pipeline.</p>
<p>Starting at the top: the music app generates PCM audio, which passes through Android's AudioFlinger to the Bluetooth Audio HAL. The HAL feeds 10ms of PCM samples (480 samples at 48 kHz) to the LC3 encoder, which compresses them into a ~120-byte frame.</p>
<p>This frame is wrapped in an ISO SDU with a timestamp and sequence number, then passed over HCI to the Bluetooth controller. The controller segments the SDU into link-layer PDUs, schedules them on the next CIS event, and transmits them over the air using the negotiated PHY (for example, 2M PHY).</p>
<p>On the earbud side, the controller receives the PDUs, reassembles the ISO SDU, and passes the LC3 frame to the earbud's decoder. The decoder reconstructs 480 PCM samples, which are buffered until the presentation delay timestamp is reached, then rendered to the speaker driver.</p>
<p><strong>Total latency</strong>: ~40ms from phone to earbud (with 10ms frame + transport + presentation delay). Compare this to Classic Bluetooth A2DP which typically runs at 100-200ms!</p>
<h3 id="heading-the-presentation-delay-the-synchronization-secret">The Presentation Delay: The Synchronization Secret</h3>
<p>The <strong>presentation delay</strong> is a crucial LE Audio concept. It's a fixed delay that both sides agree upon during stream setup. All audio must be rendered (played) at exactly:</p>
<pre><code class="language-plaintext">rendering_time = reference_anchor_point + presentation_delay
</code></pre>
<p>This ensures:</p>
<ul>
<li><p>Left and right earbuds play audio at the exact same instant</p>
</li>
<li><p>Even if transport latency varies between the two CIS channels</p>
</li>
<li><p>The presentation delay provides a "buffer" for the receiver to absorb jitter</p>
</li>
</ul>
<p>Think of it like a choir director: "Everyone sing at the count of 3. Not before, not after. Exactly at 3."</p>
<h2 id="heading-15-wrapping-up">15. Wrapping Up</h2>
<p>Bluetooth LE Audio is the most significant upgrade to Bluetooth audio since... well, since Bluetooth audio was invented. Let's recap:</p>
<h3 id="heading-what-it-solves">What It Solves</h3>
<ul>
<li><p><strong>Better codec</strong> (LC3) — equivalent quality at half the bitrate, or better quality at the same bitrate</p>
</li>
<li><p><strong>Multi-stream</strong> — no more relay earbud architecture, balanced battery life</p>
</li>
<li><p><strong>Broadcast audio</strong> (Auracast) — one-to-many streaming, opening up entirely new use cases</p>
</li>
<li><p><strong>Hearing aid support</strong> (HAP) — finally a standard, interoperable solution</p>
</li>
<li><p><strong>Unified audio</strong> (BAP) — one profile for both music and calls, no more A2DP/HFP switching</p>
</li>
</ul>
<h3 id="heading-the-aosp-stack">The AOSP Stack</h3>
<ul>
<li><p><strong>Framework layer</strong>: <code>BluetoothLeAudio</code>, <code>BluetoothLeBroadcast</code> APIs</p>
</li>
<li><p><strong>Service layer</strong>: <code>LeAudioService</code> orchestrates everything</p>
</li>
<li><p><strong>Native layer</strong>: C++ <code>le_audio_client_impl</code> handles GATT, ASE state machines, codec negotiation</p>
</li>
<li><p><strong>Controller layer</strong>: CIS/BIS isochronous channels managed via HCI</p>
</li>
</ul>
<h3 id="heading-whats-next">What's Next?</h3>
<p>LE Audio is still maturing. Key areas of development:</p>
<ul>
<li><p><strong>Better interoperability</strong> across devices from different manufacturers</p>
</li>
<li><p><strong>Auracast infrastructure</strong> — venues need to install broadcast transmitters</p>
</li>
<li><p><strong>Dual-mode support</strong> — many devices will support both Classic and LE Audio during the transition period</p>
</li>
<li><p><strong>Higher quality</strong> — as Bluetooth bandwidth improves, LC3 can scale to even higher bitrates</p>
</li>
<li><p><strong>Gaming</strong> — ultra-low-latency configurations (7.5ms frames, minimal presentation delay)</p>
</li>
</ul>
<p>The transition from Classic Audio to LE Audio won't happen overnight. It's more like the transition from IPv4 to IPv6 – gradual, sometimes painful, but ultimately necessary. The good news is that both can coexist, and the AOSP implementation supports fallback to Classic Audio for devices that don't support LE Audio.</p>
<p>So the next time you connect your earbuds and marvel at the audio quality (or lack thereof), you'll know exactly which parts of this massive protocol stack are working (or failing) to get those sound waves from your phone to your ears.</p>
<p>Happy coding, and may your packets always be isochronous!</p>
<h3 id="heading-references">References</h3>
<ol>
<li><p>Bluetooth SIG — <a href="https://www.bluetooth.com/learn-about-bluetooth/feature-enhancements/le-audio/le-audio-specifications/">LE Audio Specifications</a></p>
</li>
<li><p>Bluetooth SIG — <a href="https://www.bluetooth.com/blog/a-technical-overview-of-lc3/">A Technical Overview of LC3</a></p>
</li>
<li><p>AOSP Bluetooth Module — <a href="https://android.googlesource.com/platform/packages/modules/Bluetooth/">packages/modules/Bluetooth</a></p>
</li>
<li><p>Zephyr Project — <a href="https://docs.zephyrproject.org/latest/connectivity/bluetooth/api/audio/bluetooth-le-audio-arch.html">LE Audio Stack Documentation</a></p>
</li>
<li><p>Fraunhofer IIS — <a href="https://www.iis.fraunhofer.de/en/ff/amm/communication/lc3.html">LC3 Codec</a></p>
</li>
</ol>
 ]]>
                </content:encoded>
            </item>
        
            <item>
                <title>
                    <![CDATA[ How to Build a Podcast Player with Transcriptions using Vue and Supabase ]]>
                </title>
                <description>
                    <![CDATA[ In this post we will walk through setting up a Podcast Player app using Supabase and Vue 3, including getting transcriptions for the podcasts.  This is a continuation of my previous post on setting up Authentication using Supabase. If you aren't fami... ]]>
                </description>
                <link>https://www.freecodecamp.org/news/build-a-podcast-player-with-transcriptions-using-vue-supabase/</link>
                <guid isPermaLink="false">66bb92ddd2bda3e4315491ed</guid>
                
                    <category>
                        <![CDATA[ audio ]]>
                    </category>
                
                    <category>
                        <![CDATA[ projects ]]>
                    </category>
                
                    <category>
                        <![CDATA[ supabase ]]>
                    </category>
                
                    <category>
                        <![CDATA[ vue ]]>
                    </category>
                
                <dc:creator>
                    <![CDATA[ Brian Barrow ]]>
                </dc:creator>
                <pubDate>Mon, 28 Feb 2022 23:05:43 +0000</pubDate>
                <media:content url="https://www.freecodecamp.org/news/content/images/2022/02/Build-Podcast-Player-app-w-transcriptions-using-Vue-Supabase@2x.jpg" medium="image" />
                <content:encoded>
                    <![CDATA[ <p>In this post we will walk through setting up a Podcast Player app using Supabase and Vue 3, including getting transcriptions for the podcasts. </p>
<p>This is a continuation of my previous post on <a target="_blank" href="https://www.freecodecamp.org/news/add-supabase-authentication-to-vue/">setting up Authentication using Supabase</a>. If you aren't familiar with getting Supabase set up in your project, I highly recommend going through that post. </p>
<h2 id="heading-the-starting-code-repo">The Starting Code Repo</h2>
<p>Here is the repo from my previous post that will get you to where this post will be starting. You'll just need to set up Supabase and add your credentials/API key to a <code>.env.local</code> file to get up and running. This repo also has styling applied to it that was not included in the previous post.</p>
<div class="embed-wrapper"><div class="embed-loading"><div class="loadingRow"></div><div class="loadingRow"></div></div><a class="embed-card" href="https://github.com/briancbarrow/vue-supabase-auth">https://github.com/briancbarrow/vue-supabase-auth</a></div>
<h2 id="heading-prerequisites">Prerequisites</h2>
<p>You should be familiar with JavaScript, have had some experience with Vue 3, and you should have Node.js and NPM installed on your machine. </p>
<p>If you've gone through the previous post about Supabase Authentication or this other post on <a target="_blank" href="https://developers.deepgram.com/blog/2021/11/getting-started-with-supabase/">Getting Started with Supabase</a> you'll be good to go.</p>
<p>You will also need a <a target="_blank" href="https://console.deepgram.com/signup">free API key from Deepgram</a> for when we get to the transcription section. </p>
<h2 id="heading-getting-started">Getting Started</h2>
<p>Once you have downloaded the <a target="_blank" href="https://github.com/briancbarrow/vue-supabase-auth">repo from above</a> run <code>npm install</code> to get the packages installed for the project. </p>
<p>Add your <code>VITE_SUPABASE_URL</code> and <code>VITE_SUPABASE_ANON_KEY</code> environment variables from your the dashboard of your own Supabase project.</p>
<p>Run <code>npm run dev</code> to get the local dev server started.</p>
<p>Sign in to the app using either the Sign In form or the Magic Link form. Once you get signed in, you should see the HelloWorld component/page with a sign out button at the top. </p>
<p><img src="https://www.freecodecamp.org/news/content/images/2022/02/Screen-Shot-2022-02-18-at-2.20.26-PM.png" alt="Image" width="600" height="400" loading="lazy">
<em>Hello World component</em></p>
<h2 id="heading-how-to-fetch-a-podcast-rss-feed">How to Fetch a Podcast RSS Feed</h2>
<p>The first thing we need to do is add functionality to get a podcast feed into our app. Create a new component in the components folder called <code>PodcastFeed.vue</code>. </p>
<p>Most podcasts have a public RSS feed that we can use to get the information we need with a simple fetch request. </p>
<p>Inside the of the <code>PodcastFeed.vue</code> component create the following form that takes in a RSS feed URL, and hooks up to a button that triggers the fetch request.</p>
<p>Note: I've tried to add comments in the code to help you understand what each part is doing.</p>
<pre><code class="lang-js">&lt;template&gt;
  <span class="xml"><span class="hljs-tag">&lt;<span class="hljs-name">div</span> <span class="hljs-attr">class</span>=<span class="hljs-string">"podcast-input-feed"</span>&gt;</span>
    <span class="hljs-tag">&lt;<span class="hljs-name">label</span> <span class="hljs-attr">for</span>=<span class="hljs-string">"email"</span>&gt;</span>Podcast RSS Feed URL<span class="hljs-tag">&lt;/<span class="hljs-name">label</span>&gt;</span>
    <span class="hljs-tag">&lt;<span class="hljs-name">div</span> <span class="hljs-attr">class</span>=<span class="hljs-string">""</span>&gt;</span>
      <span class="hljs-comment">&lt;!-- binding the url input field to the 'url' data property --&gt;</span>
      <span class="hljs-tag">&lt;<span class="hljs-name">input</span>
        <span class="hljs-attr">type</span>=<span class="hljs-string">"url"</span>
        <span class="hljs-attr">name</span>=<span class="hljs-string">"url"</span>
        <span class="hljs-attr">id</span>=<span class="hljs-string">"url"</span>
        <span class="hljs-attr">v-model</span>=<span class="hljs-string">"url"</span>
        <span class="hljs-attr">placeholder</span>=<span class="hljs-string">"https://rss.your-org.org/feed/"</span>
        <span class="hljs-attr">aria-describedby</span>=<span class="hljs-string">"rss-url"</span>
      /&gt;</span>
    <span class="hljs-tag">&lt;/<span class="hljs-name">div</span>&gt;</span>
    <span class="hljs-comment">&lt;!-- hooking the button click to the 'getRssFeed' method --&gt;</span>
    <span class="hljs-tag">&lt;<span class="hljs-name">button</span> @<span class="hljs-attr">click</span>=<span class="hljs-string">"getRssFeed()"</span> <span class="hljs-attr">type</span>=<span class="hljs-string">"button"</span> <span class="hljs-attr">class</span>=<span class="hljs-string">""</span>&gt;</span>Get Feed<span class="hljs-tag">&lt;/<span class="hljs-name">button</span>&gt;</span>
  <span class="hljs-tag">&lt;/<span class="hljs-name">div</span>&gt;</span></span>
&lt;/template&gt;

<span class="xml"><span class="hljs-tag">&lt;<span class="hljs-name">script</span>&gt;</span><span class="javascript">
<span class="hljs-keyword">import</span> { ref } <span class="hljs-keyword">from</span> <span class="hljs-string">"vue"</span>;
<span class="hljs-keyword">import</span> { supabase } <span class="hljs-keyword">from</span> <span class="hljs-string">"../supabase"</span>;
<span class="hljs-keyword">import</span> { store } <span class="hljs-keyword">from</span> <span class="hljs-string">"../store"</span>;
<span class="hljs-keyword">export</span> <span class="hljs-keyword">default</span> {
  setup() {
    <span class="hljs-comment">// I am initializing the url to a url I know works, so that I don't need to keep inputing a url as I'm developing.</span>
    <span class="hljs-comment">// feel free to change this to a url of your own choosing</span>
    <span class="hljs-keyword">const</span> url = ref(<span class="hljs-string">"https://anchor.fm/s/3e9db190/podcast/rss"</span>);
    <span class="hljs-comment">// initializing the podcast state to an empty object</span>
    <span class="hljs-keyword">const</span> podcast = ref({});

    <span class="hljs-function"><span class="hljs-keyword">function</span> <span class="hljs-title">getRssFeed</span>(<span class="hljs-params"></span>) </span>{
      <span class="hljs-keyword">const</span> feedUrl = url.value;
      <span class="hljs-keyword">return</span> (
        fetch(feedUrl)
          <span class="hljs-comment">// this returns a promise so we need to convert it to a string</span>
          .then(<span class="hljs-function">(<span class="hljs-params">response</span>) =&gt;</span> response.text())
          <span class="hljs-comment">// this next line is to parse the xml response</span>
          .then(<span class="hljs-function">(<span class="hljs-params">str</span>) =&gt;</span>
            <span class="hljs-keyword">new</span> <span class="hljs-built_in">window</span>.DOMParser().parseFromString(str, <span class="hljs-string">"text/xml"</span>)
          )
          <span class="hljs-comment">// parsing the data from the xml response and setting it into the podcast state</span>
          .then(<span class="hljs-function">(<span class="hljs-params">data</span>) =&gt;</span> {
            <span class="hljs-built_in">console</span>.log(<span class="hljs-string">"Data: "</span>, data);
            podcast.value.image_url = data
              .querySelector(<span class="hljs-string">"image"</span>)
              .querySelector(<span class="hljs-string">"url"</span>).innerHTML;
            podcast.value.title = data.querySelector(<span class="hljs-string">"title"</span>).textContent;
            podcast.value.description =
              data.querySelector(<span class="hljs-string">"description"</span>).textContent;
            podcast.value.rss_url = feedUrl;
          })
          .catch(<span class="hljs-function">(<span class="hljs-params">err</span>) =&gt;</span> {
            <span class="hljs-built_in">console</span>.log(<span class="hljs-string">"ERROR: "</span>, err);
          })
      );
    }
    <span class="hljs-keyword">return</span> {
      url,
      podcast,
      store,

      getRssFeed,
    };
  },
};
</span><span class="hljs-tag">&lt;/<span class="hljs-name">script</span>&gt;</span></span>
</code></pre>
<p>With that set up, replace the HelloWorld component in the <code>App.vue</code> file with this new <code>PodcastFeed.vue</code> component:</p>
<pre><code class="lang-js">&lt;template&gt;
  <span class="xml"><span class="hljs-tag">&lt;<span class="hljs-name">button</span> <span class="hljs-attr">v-if</span>=<span class="hljs-string">"store.state.user"</span> <span class="hljs-attr">class</span>=<span class="hljs-string">"signout-button"</span> @<span class="hljs-attr">click</span>=<span class="hljs-string">"signOut"</span>&gt;</span>Sign Out<span class="hljs-tag">&lt;/<span class="hljs-name">button</span>&gt;</span></span>
  &lt;!-- Check <span class="hljs-keyword">if</span> user is available <span class="hljs-keyword">in</span> the store, <span class="hljs-keyword">if</span> not show auth compoenent --&gt;
  <span class="xml"><span class="hljs-tag">&lt;<span class="hljs-name">Auth</span> <span class="hljs-attr">v-if</span>=<span class="hljs-string">"!store.state.user"</span> /&gt;</span></span>
  &lt;!-- If user is available, show the app --&gt;
  <span class="xml"><span class="hljs-tag">&lt;<span class="hljs-name">div</span> <span class="hljs-attr">v-else</span> <span class="hljs-attr">class</span>=<span class="hljs-string">"app"</span>&gt;</span>
    <span class="hljs-tag">&lt;<span class="hljs-name">PodcastFeed</span> /&gt;</span>
  <span class="hljs-tag">&lt;/<span class="hljs-name">div</span>&gt;</span></span>
&lt;/template&gt;

<span class="xml"><span class="hljs-tag">&lt;<span class="hljs-name">script</span>&gt;</span><span class="javascript">
<span class="hljs-keyword">import</span> Auth <span class="hljs-keyword">from</span> <span class="hljs-string">"./components/Auth.vue"</span>;
<span class="hljs-keyword">import</span> PodcastFeed <span class="hljs-keyword">from</span> <span class="hljs-string">"./components/PodcastFeed.vue"</span>;

<span class="hljs-keyword">import</span> { store } <span class="hljs-keyword">from</span> <span class="hljs-string">"./store"</span>;
<span class="hljs-keyword">import</span> { supabase } <span class="hljs-keyword">from</span> <span class="hljs-string">"./supabase"</span>;

<span class="hljs-keyword">export</span> <span class="hljs-keyword">default</span> {
  <span class="hljs-attr">components</span>: {
    PodcastFeed,
    Auth,
  },
  setup() {
    <span class="hljs-comment">// we initially verify if a user is logged in with Supabase</span>
    store.state.user = supabase.auth.user();
    <span class="hljs-comment">// we then set up a listener to update the store when the user changes either by logging in or out</span>
    supabase.auth.onAuthStateChange(<span class="hljs-function">(<span class="hljs-params">event, session</span>) =&gt;</span> {
      <span class="hljs-keyword">if</span> (event == <span class="hljs-string">"SIGNED_OUT"</span>) {
        store.state.user = <span class="hljs-literal">null</span>;
      } <span class="hljs-keyword">else</span> {
        store.state.user = session.user;
      }
    });

    <span class="hljs-keyword">async</span> <span class="hljs-function"><span class="hljs-keyword">function</span> <span class="hljs-title">signOut</span>(<span class="hljs-params"></span>) </span>{
      <span class="hljs-keyword">const</span> { error } = <span class="hljs-keyword">await</span> supabase.auth.signOut();
    }

    <span class="hljs-keyword">return</span> {
      store,

      signOut,
    };
  },
};
</span><span class="hljs-tag">&lt;/<span class="hljs-name">script</span>&gt;</span></span>

<span class="xml"><span class="hljs-tag">&lt;<span class="hljs-name">style</span>&gt;</span><span class="hljs-tag">&lt;/<span class="hljs-name">style</span>&gt;</span></span>
</code></pre>
<p>So now the app should look like this:</p>
<p><img src="https://www.freecodecamp.org/news/content/images/2022/02/Screen-Shot-2022-02-18-at-2.38.40-PM.png" alt="Image" width="600" height="400" loading="lazy">
<em>App after adding PodcastFeed.vue</em></p>
<p>When you click the button, the data that comes back from the fetch request will show up in the console. </p>
<p><img src="https://www.freecodecamp.org/news/content/images/2022/02/xml-data-from-rss.png" alt="Image" width="600" height="400" loading="lazy">
<em>Parsed XML Data</em></p>
<p>In the <code>getRssFeed</code> method we parse that data and then take the information we need and add it to the <code>podcast</code> state data. We need to display that data so that the user can know the request was successful. We also want to add better error messaging in case the request fails. </p>
<p>Create a new component called <code>PodcastInfo.vue</code> and add the following code:</p>
<pre><code class="lang-js">&lt;template&gt;
  <span class="xml"><span class="hljs-tag">&lt;<span class="hljs-name">div</span> <span class="hljs-attr">class</span>=<span class="hljs-string">"podcast-info"</span>&gt;</span>
    <span class="hljs-tag">&lt;<span class="hljs-name">div</span> <span class="hljs-attr">class</span>=<span class="hljs-string">"image-container"</span>&gt;</span>
      <span class="hljs-tag">&lt;<span class="hljs-name">img</span> <span class="hljs-attr">:src</span>=<span class="hljs-string">"podcast.image_url"</span> <span class="hljs-attr">alt</span>=<span class="hljs-string">""</span> <span class="hljs-attr">class</span>=<span class="hljs-string">""</span> /&gt;</span>
    <span class="hljs-tag">&lt;/<span class="hljs-name">div</span>&gt;</span>
    <span class="hljs-tag">&lt;<span class="hljs-name">div</span> <span class="hljs-attr">class</span>=<span class="hljs-string">"podcast-text"</span>&gt;</span>
      <span class="hljs-tag">&lt;<span class="hljs-name">div</span> <span class="hljs-attr">class</span>=<span class="hljs-string">"title-desc"</span>&gt;</span>
        <span class="hljs-tag">&lt;<span class="hljs-name">p</span> <span class="hljs-attr">class</span>=<span class="hljs-string">"title"</span>&gt;</span>
          {{ podcast.title }}
        <span class="hljs-tag">&lt;/<span class="hljs-name">p</span>&gt;</span>
        <span class="hljs-tag">&lt;<span class="hljs-name">p</span> <span class="hljs-attr">class</span>=<span class="hljs-string">"desc"</span>&gt;</span>
          {{ podcast.description }}
        <span class="hljs-tag">&lt;/<span class="hljs-name">p</span>&gt;</span>
      <span class="hljs-tag">&lt;/<span class="hljs-name">div</span>&gt;</span>
    <span class="hljs-tag">&lt;/<span class="hljs-name">div</span>&gt;</span>
  <span class="hljs-tag">&lt;/<span class="hljs-name">div</span>&gt;</span></span>
&lt;/template&gt;

<span class="xml"><span class="hljs-tag">&lt;<span class="hljs-name">script</span>&gt;</span><span class="javascript">
<span class="hljs-keyword">import</span> { store } <span class="hljs-keyword">from</span> <span class="hljs-string">"../store"</span>;
<span class="hljs-keyword">import</span> { supabase } <span class="hljs-keyword">from</span> <span class="hljs-string">"../supabase"</span>;

<span class="hljs-keyword">export</span> <span class="hljs-keyword">default</span> {
  <span class="hljs-attr">props</span>: {
    <span class="hljs-attr">podcast</span>: {
      <span class="hljs-attr">type</span>: <span class="hljs-built_in">Object</span>,
      <span class="hljs-attr">required</span>: <span class="hljs-literal">true</span>,
    },
  },
  <span class="hljs-attr">computed</span>: {},
  <span class="hljs-attr">methods</span>: {},
  setup() {},
};
</span><span class="hljs-tag">&lt;/<span class="hljs-name">script</span>&gt;</span></span>

<span class="xml"><span class="hljs-tag">&lt;<span class="hljs-name">style</span> <span class="hljs-attr">scoped</span>&gt;</span><span class="hljs-tag">&lt;/<span class="hljs-name">style</span>&gt;</span></span>
</code></pre>
<pre><code class="lang-js">&lt;template&gt;
  <span class="xml"><span class="hljs-tag">&lt;<span class="hljs-name">div</span> <span class="hljs-attr">class</span>=<span class="hljs-string">"info-error"</span>&gt;</span>
    <span class="hljs-tag">&lt;<span class="hljs-name">h3</span> <span class="hljs-attr">class</span>=<span class="hljs-string">""</span>&gt;</span>There was an error with your request<span class="hljs-tag">&lt;/<span class="hljs-name">h3</span>&gt;</span>
    <span class="hljs-tag">&lt;<span class="hljs-name">p</span> <span class="hljs-attr">class</span>=<span class="hljs-string">""</span>&gt;</span>Check your RSS feed URL and try again.<span class="hljs-tag">&lt;/<span class="hljs-name">p</span>&gt;</span>
  <span class="hljs-tag">&lt;/<span class="hljs-name">div</span>&gt;</span></span>
&lt;/template&gt;

<span class="xml"><span class="hljs-tag">&lt;<span class="hljs-name">script</span>&gt;</span><span class="javascript">
<span class="hljs-keyword">export</span> <span class="hljs-keyword">default</span> {};
</span><span class="hljs-tag">&lt;/<span class="hljs-name">script</span>&gt;</span></span>
</code></pre>
<p>Then update the  <code>PodcastFeed.vue</code> to the following in order to bring in the component:</p>
<pre><code class="lang-js">&lt;template&gt;
  <span class="xml"><span class="hljs-tag">&lt;<span class="hljs-name">div</span> <span class="hljs-attr">class</span>=<span class="hljs-string">"podcast-input-feed"</span>&gt;</span>
    <span class="hljs-tag">&lt;<span class="hljs-name">label</span> <span class="hljs-attr">for</span>=<span class="hljs-string">"email"</span>&gt;</span>Podcast RSS Feed URL<span class="hljs-tag">&lt;/<span class="hljs-name">label</span>&gt;</span>
    <span class="hljs-tag">&lt;<span class="hljs-name">div</span> <span class="hljs-attr">class</span>=<span class="hljs-string">""</span>&gt;</span>
      <span class="hljs-comment">&lt;!-- binding the url input field to the 'url' data property --&gt;</span>
      <span class="hljs-tag">&lt;<span class="hljs-name">input</span>
        <span class="hljs-attr">type</span>=<span class="hljs-string">"url"</span>
        <span class="hljs-attr">name</span>=<span class="hljs-string">"url"</span>
        <span class="hljs-attr">id</span>=<span class="hljs-string">"url"</span>
        <span class="hljs-attr">v-model</span>=<span class="hljs-string">"url"</span>
        <span class="hljs-attr">placeholder</span>=<span class="hljs-string">"https://rss.your-org.org/feed/"</span>
        <span class="hljs-attr">aria-describedby</span>=<span class="hljs-string">"rss-url"</span>
      /&gt;</span>
    <span class="hljs-tag">&lt;/<span class="hljs-name">div</span>&gt;</span>
    <span class="hljs-comment">&lt;!-- hooking the button click to the 'getRssFeed' method --&gt;</span>
    <span class="hljs-tag">&lt;<span class="hljs-name">button</span> @<span class="hljs-attr">click</span>=<span class="hljs-string">"getRssFeed()"</span> <span class="hljs-attr">type</span>=<span class="hljs-string">"button"</span> <span class="hljs-attr">class</span>=<span class="hljs-string">""</span>&gt;</span>Get Feed<span class="hljs-tag">&lt;/<span class="hljs-name">button</span>&gt;</span>
    <span class="hljs-comment">&lt;!-- Adding in these two new components --&gt;</span>
    <span class="hljs-tag">&lt;<span class="hljs-name">podcast-info</span> <span class="hljs-attr">v-if</span>=<span class="hljs-string">"podcast.title &amp;&amp; !requestError"</span> <span class="hljs-attr">:podcast</span>=<span class="hljs-string">"podcast"</span> /&gt;</span>
  <span class="hljs-tag">&lt;/<span class="hljs-name">div</span>&gt;</span></span>
&lt;/template&gt;

<span class="xml"><span class="hljs-tag">&lt;<span class="hljs-name">script</span>&gt;</span><span class="javascript">
<span class="hljs-keyword">import</span> { ref } <span class="hljs-keyword">from</span> <span class="hljs-string">"vue"</span>;
<span class="hljs-keyword">import</span> { supabase } <span class="hljs-keyword">from</span> <span class="hljs-string">"../supabase"</span>;
<span class="hljs-keyword">import</span> { store } <span class="hljs-keyword">from</span> <span class="hljs-string">"../store"</span>;

<span class="hljs-keyword">import</span> PodcastInfo <span class="hljs-keyword">from</span> <span class="hljs-string">"./PodcastInfo.vue"</span>;

<span class="hljs-keyword">export</span> <span class="hljs-keyword">default</span> {
  <span class="hljs-attr">components</span>: {
    PodcastInfo,
  },

  setup() {
    <span class="hljs-comment">// I am initializing the url to a url I know works, so that I don't need to keep inputing a url as I'm developing.</span>
    <span class="hljs-comment">// feel free to change this to a url of your own choosing</span>
    <span class="hljs-keyword">const</span> url = ref(<span class="hljs-string">"https://anchor.fm/s/3e9db190/podcast/rss"</span>);
    <span class="hljs-comment">// initializing the podcast state to an empty object</span>
    <span class="hljs-keyword">const</span> podcast = ref({});
    <span class="hljs-keyword">const</span> requestError = ref(<span class="hljs-literal">false</span>);

    <span class="hljs-function"><span class="hljs-keyword">function</span> <span class="hljs-title">getRssFeed</span>(<span class="hljs-params"></span>) </span>{
      <span class="hljs-keyword">const</span> feedUrl = url.value;
      <span class="hljs-keyword">return</span> (
        fetch(feedUrl)
          <span class="hljs-comment">// this returns a promise so we need to convert it to a string</span>
          .then(<span class="hljs-function">(<span class="hljs-params">response</span>) =&gt;</span> response.text())
          <span class="hljs-comment">// this next line is to parse the xml response</span>
          .then(<span class="hljs-function">(<span class="hljs-params">str</span>) =&gt;</span>
            <span class="hljs-keyword">new</span> <span class="hljs-built_in">window</span>.DOMParser().parseFromString(str, <span class="hljs-string">"text/xml"</span>)
          )
          <span class="hljs-comment">// parsing the data from the xml response and setting it into the podcast state</span>
          .then(<span class="hljs-function">(<span class="hljs-params">data</span>) =&gt;</span> {
            <span class="hljs-built_in">console</span>.log(<span class="hljs-string">"Data: "</span>, data);
            podcast.value.image_url = data
              .querySelector(<span class="hljs-string">"image"</span>)
              .querySelector(<span class="hljs-string">"url"</span>).innerHTML;
            podcast.value.title = data.querySelector(<span class="hljs-string">"title"</span>).textContent;
            podcast.value.description =
              data.querySelector(<span class="hljs-string">"description"</span>).textContent;
            podcast.value.rss_url = feedUrl;
          })
          .catch(<span class="hljs-function">(<span class="hljs-params">err</span>) =&gt;</span> {
            requestError.value = <span class="hljs-literal">true</span>;
          })
      );
    }
    <span class="hljs-keyword">return</span> {
      url,
      podcast,
      store,
      requestError,

      getRssFeed,
    };
  },
};
</span><span class="hljs-tag">&lt;/<span class="hljs-name">script</span>&gt;</span></span>
</code></pre>
<p>Now when you click the "Get Feed" button, you should see the following:</p>
<p><img src="https://www.freecodecamp.org/news/content/images/2022/02/Screen-Shot-2022-02-18-at-3.05.15-PM.png" alt="Image" width="600" height="400" loading="lazy"></p>
<p>Now that we can get the info displayed, we can wire up the app to save the info to Supabase. </p>
<h2 id="heading-how-to-add-a-table-to-the-supabase-db">How to Add a Table to the Supabase DB</h2>
<p>The first thing we need to do is add a table to our Supabase DB. In the Dashboard for your project on Supabase, select the Table Editor and click the <code>New table</code> button. I name the new table <code>podcasts</code>. Enable the Row Level Security (this makes our DB more secure) and add the following columns:</p>
<ul>
<li>id (this column should be filled in for you when you create a new table)</li>
<li>created_at</li>
<li>name</li>
<li>image_url</li>
<li>description</li>
<li>rss_url</li>
<li>user_id (for this one, we want to link it via foreign key to our users table created by the Auth service. Click the chain-link icon to get that set up and link it to the <code>users</code> table on the <code>id</code> column.)</li>
</ul>
<p><img src="https://www.freecodecamp.org/news/content/images/2022/02/Screen-Shot-2022-01-19-at-9.08.50-AM.png" alt="Image" width="600" height="400" loading="lazy">
<em>podcasts table setup</em></p>
<p>Because we enabled the Row Level Security, the table won't let anything get inserted until we update the policies for it. </p>
<p>Under the Authentication tab there is a section called 'Policies'. There you should see the <code>podcasts</code> table and a button to create a new policy. When you click on it, it will give you the option to create a policy from a template. Choose the template called 'Enable insert access for authenticated users only'. Now, only users that are authenticated have access to insert anything into the table. </p>
<p>When Supabase runs the <code>insert</code> command, it will automatically run a <code>select</code> command and return the newly inserted row. Because of this, we also have to add a policy to the table allowing the user <code>SELECT</code> access. </p>
<p>Create a new policy with the name <code>Enable select based on userid</code> and then in the <code>USING expression</code> section put <code>(uid() = user_id)</code>. That will prevent users from reading other users' information, while still giving them access to their own podcasts in the table.</p>
<p><img src="https://www.freecodecamp.org/news/content/images/2022/02/select-user-policy.png" alt="Image" width="600" height="400" loading="lazy">
<em>Select based on userid policy</em></p>
<h2 id="heading-how-to-link-up-the-ui-to-the-db-so-the-user-can-save-podcasts">How to Link Up the UI to the DB So the User Can Save Podcasts</h2>
<p>To add a podcast to our DB, we will first add a button to the <code>PodcastInfo</code> component. Add this code to the bottom of the <code>&lt;div class="podcast-info"&gt;</code>:</p>
<pre><code class="lang-html"><span class="hljs-tag">&lt;<span class="hljs-name">button</span> @<span class="hljs-attr">click</span>=<span class="hljs-string">"addPodcast"</span>&gt;</span>Add to My Podcasts<span class="hljs-tag">&lt;/<span class="hljs-name">button</span>&gt;</span>
</code></pre>
<p>Now add a method called <code>addPodcast</code> to the setup function of the component like this. Don't forget to add the <code>props</code> to the argument of the setup function.</p>
<pre><code class="lang-js">setup(props) {
    <span class="hljs-function"><span class="hljs-keyword">function</span> <span class="hljs-title">addPodcast</span>(<span class="hljs-params"></span>) </span>{
      <span class="hljs-comment">// Setting up the podcast object to send to supabase</span>
      <span class="hljs-keyword">const</span> podcast = {
        <span class="hljs-attr">name</span>: props.podcast.title,
        <span class="hljs-attr">image_url</span>: props.podcast.image_url,
        <span class="hljs-attr">description</span>: props.podcast.description,
        <span class="hljs-attr">rss_url</span>: props.podcast.rss_url,
        <span class="hljs-attr">user_id</span>: store.state.user.id,
      };
      <span class="hljs-comment">// calling supabase method to insert into the db</span>
      supabase
        .from(<span class="hljs-string">"podcasts"</span>)
        .insert(podcast)
        .then(<span class="hljs-function">(<span class="hljs-params">{ body }</span>) =&gt;</span> {
          store.addPodcastToStore(body[<span class="hljs-number">0</span>]);
        })
        .catch(<span class="hljs-function">(<span class="hljs-params">err</span>) =&gt;</span> {
          <span class="hljs-built_in">console</span>.log(err);
        });
    }

    <span class="hljs-keyword">return</span> {
      addPodcast,
    };
  },
</code></pre>
<p>You can see in the <code>.then</code> statement we call a method from the global store. Update the <code>store.js</code> file to the following:</p>
<pre><code class="lang-js"><span class="hljs-keyword">import</span> { reactive } <span class="hljs-keyword">from</span> <span class="hljs-string">"vue"</span>;

<span class="hljs-keyword">export</span> <span class="hljs-keyword">const</span> store = {
  <span class="hljs-attr">state</span>: reactive({
    <span class="hljs-attr">user</span>: {},
    <span class="hljs-comment">// adding podcasts array to global store</span>
    <span class="hljs-attr">podcasts</span>: [],
  }),

  <span class="hljs-comment">// adding addPodcastToStore method to store object</span>
  addPodcastToStore(podcast) {
    <span class="hljs-built_in">this</span>.state.podcasts.push(podcast);
  },
};
</code></pre>
<p>Now when we click the "Add To My Podcasts" button, the app makes a call to Supabase and then takes the result of that call and adds it to the podcasts array in the global store. (If you are getting a 403 error, make sure you set up the policies correctly. Maybe try restarting the dev server too.)</p>
<p>If a podcast is already in a user's list of podcasts, we don't want to let them click the add button again. To prevent this we need to first call Supabase to get all of the user's podcasts, and then check if the podcast they are looking at is in that list.</p>
<p>This method will not be specific to any one component so we want to create it inside of the global store. That way, any component has access to it. Add this method to the <code>store.js</code> file underneath the <code>addPodcastToStore</code> method:</p>
<pre><code class="lang-js">getPodcastsFromDB() {
    supabase
        .from(<span class="hljs-string">"podcasts"</span>)
        .select(<span class="hljs-string">"*"</span>)
        .then(<span class="hljs-function">(<span class="hljs-params">{ body }</span>) =&gt;</span> {
            <span class="hljs-built_in">this</span>.state.podcasts = body;
        });
},
</code></pre>
<p>Then we want to update the method to be called whenever a user signs in. Inside of <code>App.vue</code> change the <code>onAuthStateChange</code> handler to this:</p>
<pre><code class="lang-js">supabase.auth.onAuthStateChange(<span class="hljs-function">(<span class="hljs-params">event, session</span>) =&gt;</span> {
    <span class="hljs-keyword">if</span> (event == <span class="hljs-string">"SIGNED_OUT"</span>) {
        store.state.user = <span class="hljs-literal">null</span>;
    } <span class="hljs-keyword">else</span> {
        <span class="hljs-comment">// make call to supabase to get Podcasts for the user</span>
        store.getPodcastsFromDB();
        store.state.user = session.user;
    }
});
</code></pre>
<p>Now update the <code>PodcastInfo.vue</code> file to this in order to display to the user if a podcast is already in their library.</p>
<pre><code class="lang-js">&lt;template&gt;
  <span class="xml"><span class="hljs-tag">&lt;<span class="hljs-name">div</span> <span class="hljs-attr">class</span>=<span class="hljs-string">"podcast-info"</span>&gt;</span>
    <span class="hljs-tag">&lt;<span class="hljs-name">div</span> <span class="hljs-attr">class</span>=<span class="hljs-string">"image-container"</span>&gt;</span>
      <span class="hljs-tag">&lt;<span class="hljs-name">img</span> <span class="hljs-attr">:src</span>=<span class="hljs-string">"podcast.image_url"</span> <span class="hljs-attr">alt</span>=<span class="hljs-string">""</span> <span class="hljs-attr">class</span>=<span class="hljs-string">""</span> /&gt;</span>
    <span class="hljs-tag">&lt;/<span class="hljs-name">div</span>&gt;</span>
    <span class="hljs-tag">&lt;<span class="hljs-name">div</span> <span class="hljs-attr">class</span>=<span class="hljs-string">"podcast-text"</span>&gt;</span>
      <span class="hljs-tag">&lt;<span class="hljs-name">div</span> <span class="hljs-attr">class</span>=<span class="hljs-string">"title-desc"</span>&gt;</span>
        <span class="hljs-tag">&lt;<span class="hljs-name">p</span> <span class="hljs-attr">class</span>=<span class="hljs-string">"title"</span>&gt;</span>
          {{ podcast.title }}
        <span class="hljs-tag">&lt;/<span class="hljs-name">p</span>&gt;</span>
        <span class="hljs-tag">&lt;<span class="hljs-name">p</span> <span class="hljs-attr">class</span>=<span class="hljs-string">"desc"</span>&gt;</span>
          {{ podcast.description }}
        <span class="hljs-tag">&lt;/<span class="hljs-name">p</span>&gt;</span>
      <span class="hljs-tag">&lt;/<span class="hljs-name">div</span>&gt;</span>
    <span class="hljs-tag">&lt;/<span class="hljs-name">div</span>&gt;</span>
    <span class="hljs-comment">&lt;!-- Add check in markup to remove the button if the podcast already exists in the user's list --&gt;</span>
    <span class="hljs-tag">&lt;<span class="hljs-name">div</span> <span class="hljs-attr">v-if</span>=<span class="hljs-string">"isInUserPodcasts"</span> <span class="hljs-attr">class</span>=<span class="hljs-string">"in-podcasts"</span>&gt;</span>In Your Podcasts<span class="hljs-tag">&lt;/<span class="hljs-name">div</span>&gt;</span>
    <span class="hljs-tag">&lt;<span class="hljs-name">button</span> <span class="hljs-attr">v-else</span> <span class="hljs-attr">class</span>=<span class="hljs-string">""</span> @<span class="hljs-attr">click</span>=<span class="hljs-string">"addPodcast"</span>&gt;</span>Add to My Podcasts<span class="hljs-tag">&lt;/<span class="hljs-name">button</span>&gt;</span>
  <span class="hljs-tag">&lt;/<span class="hljs-name">div</span>&gt;</span></span>
&lt;/template&gt;

<span class="xml"><span class="hljs-tag">&lt;<span class="hljs-name">script</span>&gt;</span><span class="javascript">
<span class="hljs-comment">// importing computed</span>
<span class="hljs-keyword">import</span> { ref, computed } <span class="hljs-keyword">from</span> <span class="hljs-string">"vue"</span>;
<span class="hljs-keyword">import</span> { store } <span class="hljs-keyword">from</span> <span class="hljs-string">"../store"</span>;
<span class="hljs-keyword">import</span> { supabase } <span class="hljs-keyword">from</span> <span class="hljs-string">"../supabase"</span>;

<span class="hljs-keyword">export</span> <span class="hljs-keyword">default</span> {
  <span class="hljs-attr">props</span>: {
    <span class="hljs-attr">podcast</span>: {
      <span class="hljs-attr">type</span>: <span class="hljs-built_in">Object</span>,
      <span class="hljs-attr">required</span>: <span class="hljs-literal">true</span>,
    },
  },
  setup(props) {
    <span class="hljs-comment">// add computed property checking if podcast is in user's podcasts</span>
    <span class="hljs-keyword">const</span> isInUserPodcasts = computed(<span class="hljs-function">() =&gt;</span> {
      <span class="hljs-keyword">return</span> store.state.podcasts.some(
        <span class="hljs-function">(<span class="hljs-params">podcast</span>) =&gt;</span> podcast.rss_url === props.podcast.rss_url
      );
    });

    <span class="hljs-function"><span class="hljs-keyword">function</span> <span class="hljs-title">addPodcast</span>(<span class="hljs-params"></span>) </span>{
      <span class="hljs-comment">// check if podcast is already in user's podcasts</span>
      <span class="hljs-keyword">if</span> (isInUserPodcasts.value) {
        alert(<span class="hljs-string">"You already have this podcast in your list!"</span>);
      } <span class="hljs-keyword">else</span> {
        <span class="hljs-keyword">const</span> podcast = {
          <span class="hljs-attr">name</span>: props.podcast.title,
          <span class="hljs-attr">image_url</span>: props.podcast.image_url,
          <span class="hljs-attr">description</span>: props.podcast.description,
          <span class="hljs-attr">rss_url</span>: props.podcast.rss_url,
          <span class="hljs-attr">user_id</span>: store.state.user.id,
        };
        supabase
          .from(<span class="hljs-string">"podcasts"</span>)
          .insert(podcast)
          .then(<span class="hljs-function">(<span class="hljs-params">{ body }</span>) =&gt;</span> {
            store.addPodcastToStore(body[<span class="hljs-number">0</span>]);
          })
          .catch(<span class="hljs-function">(<span class="hljs-params">err</span>) =&gt;</span> {
            <span class="hljs-built_in">console</span>.log(err);
          });
      }
    }

    <span class="hljs-keyword">return</span> {
      <span class="hljs-comment">// exposing the isInUserPodcasts computed property</span>
      isInUserPodcasts,
      addPodcast,
    };
  },
};
</span><span class="hljs-tag">&lt;/<span class="hljs-name">script</span>&gt;</span></span>

<span class="xml"><span class="hljs-tag">&lt;<span class="hljs-name">style</span> <span class="hljs-attr">scoped</span>&gt;</span><span class="hljs-tag">&lt;/<span class="hljs-name">style</span>&gt;</span></span>
</code></pre>
<p>Next, we want to display a list of podcasts that the user has added to their library. We have the list in the global store, so we just need to loop through them to display the needed info. </p>
<p>Add the following to the bottom of the <code>PodcastFeed.vue</code> template:</p>
<pre><code class="lang-html"><span class="hljs-comment">&lt;!-- Loop through podcasts and display them --&gt;</span>
  <span class="hljs-tag">&lt;<span class="hljs-name">div</span> <span class="hljs-attr">class</span>=<span class="hljs-string">"feeds"</span>&gt;</span>
    <span class="hljs-tag">&lt;<span class="hljs-name">h2</span> <span class="hljs-attr">class</span>=<span class="hljs-string">""</span>&gt;</span>Your Podcast Feeds<span class="hljs-tag">&lt;/<span class="hljs-name">h2</span>&gt;</span>
    <span class="hljs-tag">&lt;<span class="hljs-name">ul</span> <span class="hljs-attr">class</span>=<span class="hljs-string">""</span>&gt;</span>
      <span class="hljs-tag">&lt;<span class="hljs-name">li</span> <span class="hljs-attr">v-for</span>=<span class="hljs-string">"pod in store.state.podcasts"</span> <span class="hljs-attr">:key</span>=<span class="hljs-string">"pod.id"</span> <span class="hljs-attr">class</span>=<span class="hljs-string">""</span>&gt;</span>
        <span class="hljs-tag">&lt;<span class="hljs-name">a</span> <span class="hljs-attr">:href</span>=<span class="hljs-string">"`/podcast/${pod.id}`"</span> <span class="hljs-attr">class</span>=<span class="hljs-string">""</span>&gt;</span>
          <span class="hljs-tag">&lt;<span class="hljs-name">img</span> <span class="hljs-attr">:src</span>=<span class="hljs-string">"pod.image_url"</span> <span class="hljs-attr">:alt</span>=<span class="hljs-string">"`logo for ${pod.name}`"</span> <span class="hljs-attr">class</span>=<span class="hljs-string">""</span> /&gt;</span>
          <span class="hljs-tag">&lt;<span class="hljs-name">p</span> <span class="hljs-attr">class</span>=<span class="hljs-string">""</span>&gt;</span>{{ pod.name }}<span class="hljs-tag">&lt;/<span class="hljs-name">p</span>&gt;</span>
        <span class="hljs-tag">&lt;/<span class="hljs-name">a</span>&gt;</span>
      <span class="hljs-tag">&lt;/<span class="hljs-name">li</span>&gt;</span>
    <span class="hljs-tag">&lt;/<span class="hljs-name">ul</span>&gt;</span>
  <span class="hljs-tag">&lt;/<span class="hljs-name">div</span>&gt;</span>
</code></pre>
<h2 id="heading-how-to-set-up-the-other-pages-for-our-podcast-app">How to Set Up the Other Pages for Our Podcast App</h2>
<p>Now that we have the list of podcasts being displayed in the app, we need a way to navigate to an individual podcast. We have the markup set to link to a path like <code>/podcast/{podcast_id}</code>. Now we need to update our app to handle routes like this. </p>
<p>First, install vue-router using <code>npm i vue-router</code>.</p>
<p>Then create a file called <code>router.js</code> with the following code:</p>
<pre><code class="lang-js"><span class="hljs-comment">// Import Vue Router</span>
<span class="hljs-keyword">import</span> * <span class="hljs-keyword">as</span> VueRouter <span class="hljs-keyword">from</span> <span class="hljs-string">"vue-router"</span>;

<span class="hljs-comment">// Import the components that will show on the different routes</span>
<span class="hljs-keyword">import</span> PodcastFeed <span class="hljs-keyword">from</span> <span class="hljs-string">"./components/PodcastFeed.vue"</span>;
<span class="hljs-keyword">import</span> PodcastDetail <span class="hljs-keyword">from</span> <span class="hljs-string">"./components/PodcastDetail.vue"</span>;

<span class="hljs-comment">// Set up the routes</span>
<span class="hljs-keyword">const</span> routes = [
  { <span class="hljs-attr">path</span>: <span class="hljs-string">"/"</span>, <span class="hljs-attr">component</span>: PodcastFeed },
  { <span class="hljs-attr">path</span>: <span class="hljs-string">"/podcast/:id"</span>, <span class="hljs-attr">component</span>: PodcastDetail },
];

<span class="hljs-comment">// Initialize the router</span>
<span class="hljs-keyword">const</span> router = VueRouter.createRouter({
  <span class="hljs-attr">history</span>: VueRouter.createWebHistory(),
  routes,
});

<span class="hljs-comment">// Export the router</span>
<span class="hljs-keyword">export</span> <span class="hljs-keyword">default</span> router;
</code></pre>
<p>Update <code>main.js</code> to use the router in the Vue app:</p>
<pre><code class="lang-js"><span class="hljs-keyword">import</span> { createApp } <span class="hljs-keyword">from</span> <span class="hljs-string">"vue"</span>;
<span class="hljs-keyword">import</span> router <span class="hljs-keyword">from</span> <span class="hljs-string">"./router"</span>;
<span class="hljs-keyword">import</span> App <span class="hljs-keyword">from</span> <span class="hljs-string">"./App.vue"</span>;
<span class="hljs-keyword">import</span> <span class="hljs-string">"./index.css"</span>;

<span class="hljs-keyword">const</span> app = createApp(App);
app.use(router);
app.mount(<span class="hljs-string">"#app"</span>);
</code></pre>
<p>Update <code>App.vue</code> to show the <code>router-view</code> component provided by Vue Router:</p>
<pre><code class="lang-html"><span class="hljs-tag">&lt;<span class="hljs-name">template</span>&gt;</span>
  <span class="hljs-tag">&lt;<span class="hljs-name">button</span> <span class="hljs-attr">v-if</span>=<span class="hljs-string">"store.state.user"</span> <span class="hljs-attr">class</span>=<span class="hljs-string">"signout-button"</span> @<span class="hljs-attr">click</span>=<span class="hljs-string">"signOut"</span>&gt;</span>Sign Out<span class="hljs-tag">&lt;/<span class="hljs-name">button</span>&gt;</span>
  <span class="hljs-comment">&lt;!-- Check if user is available in the store, if not show auth compoenent --&gt;</span>
  <span class="hljs-tag">&lt;<span class="hljs-name">Auth</span> <span class="hljs-attr">v-if</span>=<span class="hljs-string">"!store.state.user"</span> /&gt;</span>
  <span class="hljs-comment">&lt;!-- If user is available, show the app --&gt;</span>
  <span class="hljs-tag">&lt;<span class="hljs-name">div</span> <span class="hljs-attr">v-else</span> <span class="hljs-attr">class</span>=<span class="hljs-string">"app"</span>&gt;</span>
    <span class="hljs-tag">&lt;<span class="hljs-name">router-view</span>&gt;</span><span class="hljs-tag">&lt;/<span class="hljs-name">router-view</span>&gt;</span>
  <span class="hljs-tag">&lt;/<span class="hljs-name">div</span>&gt;</span>
<span class="hljs-tag">&lt;/<span class="hljs-name">template</span>&gt;</span>
</code></pre>
<p>Now create a <code>PodcastDetail.vue</code> file that will display the episode info for the podcast:</p>
<pre><code class="lang-js">&lt;template&gt;
  <span class="xml"><span class="hljs-tag">&lt;<span class="hljs-name">nav</span> <span class="hljs-attr">class</span>=<span class="hljs-string">""</span>&gt;</span>
    <span class="hljs-tag">&lt;<span class="hljs-name">a</span> <span class="hljs-attr">href</span>=<span class="hljs-string">"/"</span> <span class="hljs-attr">class</span>=<span class="hljs-string">""</span>&gt;</span>Home<span class="hljs-tag">&lt;/<span class="hljs-name">a</span>&gt;</span>
  <span class="hljs-tag">&lt;/<span class="hljs-name">nav</span>&gt;</span></span>
  &lt;!-- Basic layout <span class="hljs-keyword">for</span> showing podcast info --&gt;
  <span class="xml"><span class="hljs-tag">&lt;<span class="hljs-name">div</span> <span class="hljs-attr">class</span>=<span class="hljs-string">"podcast-detail"</span>&gt;</span>
    <span class="hljs-tag">&lt;<span class="hljs-name">img</span> <span class="hljs-attr">:src</span>=<span class="hljs-string">"podcast.image_url"</span> <span class="hljs-attr">:alt</span>=<span class="hljs-string">"podcast.name"</span> <span class="hljs-attr">class</span>=<span class="hljs-string">""</span> /&gt;</span>
    <span class="hljs-tag">&lt;<span class="hljs-name">h1</span> <span class="hljs-attr">class</span>=<span class="hljs-string">""</span>&gt;</span>{{ podcast.name }}<span class="hljs-tag">&lt;/<span class="hljs-name">h1</span>&gt;</span>
    <span class="hljs-tag">&lt;<span class="hljs-name">p</span>&gt;</span>{{ podcast.description }}<span class="hljs-tag">&lt;/<span class="hljs-name">p</span>&gt;</span>
    <span class="hljs-tag">&lt;<span class="hljs-name">h2</span> <span class="hljs-attr">class</span>=<span class="hljs-string">""</span>&gt;</span>Episodes<span class="hljs-tag">&lt;/<span class="hljs-name">h2</span>&gt;</span>
    <span class="hljs-comment">&lt;!-- Looping through each episode of a podcast and displaying episode info --&gt;</span>
    <span class="hljs-tag">&lt;<span class="hljs-name">ul</span> <span class="hljs-attr">class</span>=<span class="hljs-string">""</span>&gt;</span>
      <span class="hljs-tag">&lt;<span class="hljs-name">li</span>
        <span class="hljs-attr">v-for</span>=<span class="hljs-string">"episode in episodes"</span>
        <span class="hljs-attr">:key</span>=<span class="hljs-string">"episode.guid || episode.link"</span>
        <span class="hljs-attr">class</span>=<span class="hljs-string">""</span>
      &gt;</span>
        <span class="hljs-tag">&lt;<span class="hljs-name">div</span> <span class="hljs-attr">class</span>=<span class="hljs-string">"info"</span>&gt;</span>
          <span class="hljs-tag">&lt;<span class="hljs-name">h3</span>&gt;</span>{{ episode.title }}<span class="hljs-tag">&lt;/<span class="hljs-name">h3</span>&gt;</span>
          <span class="hljs-tag">&lt;<span class="hljs-name">audio</span> <span class="hljs-attr">class</span>=<span class="hljs-string">""</span> <span class="hljs-attr">controls</span>&gt;</span>
            <span class="hljs-tag">&lt;<span class="hljs-name">source</span> <span class="hljs-attr">:src</span>=<span class="hljs-string">"episode.url"</span> <span class="hljs-attr">type</span>=<span class="hljs-string">"audio/mpeg"</span> /&gt;</span>
            Display
          <span class="hljs-tag">&lt;/<span class="hljs-name">audio</span>&gt;</span>          
        <span class="hljs-tag">&lt;/<span class="hljs-name">div</span>&gt;</span>        
      <span class="hljs-tag">&lt;/<span class="hljs-name">li</span>&gt;</span>
    <span class="hljs-tag">&lt;/<span class="hljs-name">ul</span>&gt;</span>
  <span class="hljs-tag">&lt;/<span class="hljs-name">div</span>&gt;</span></span>
&lt;/template&gt;

<span class="xml"><span class="hljs-tag">&lt;<span class="hljs-name">script</span>&gt;</span><span class="javascript">
<span class="hljs-comment">// Importing necessary methods</span>
<span class="hljs-keyword">import</span> { ref, onMounted } <span class="hljs-keyword">from</span> <span class="hljs-string">"vue"</span>;
<span class="hljs-keyword">import</span> { useRoute } <span class="hljs-keyword">from</span> <span class="hljs-string">"vue-router"</span>;
<span class="hljs-keyword">import</span> { supabase } <span class="hljs-keyword">from</span> <span class="hljs-string">"../supabase"</span>;

<span class="hljs-keyword">export</span> <span class="hljs-keyword">default</span> {
  setup() {
    <span class="hljs-keyword">const</span> route = useRoute();
    <span class="hljs-keyword">const</span> podcast = ref({});
    <span class="hljs-keyword">const</span> episodes = ref([]);

    <span class="hljs-comment">// Getting podcast info from the database</span>
    <span class="hljs-keyword">async</span> <span class="hljs-function"><span class="hljs-keyword">function</span> <span class="hljs-title">getPodcastData</span>(<span class="hljs-params"></span>) </span>{
      <span class="hljs-keyword">const</span> {
        <span class="hljs-attr">data</span>: [podcastinfo],
      } = <span class="hljs-keyword">await</span> supabase.from(<span class="hljs-string">"podcasts"</span>).select().eq(<span class="hljs-string">"id"</span>, route.params.id);
      podcast.value = podcastinfo;

      <span class="hljs-comment">// Making call to episode url to get episode info</span>
      getEpisodes(podcastinfo.rss_url);
    }

    <span class="hljs-function"><span class="hljs-keyword">function</span> <span class="hljs-title">getEpisodes</span>(<span class="hljs-params">url</span>) </span>{
      fetch(url)
        .then(<span class="hljs-function">(<span class="hljs-params">response</span>) =&gt;</span> response.text())
        .then(<span class="hljs-function">(<span class="hljs-params">str</span>) =&gt;</span> <span class="hljs-keyword">new</span> <span class="hljs-built_in">window</span>.DOMParser().parseFromString(str, <span class="hljs-string">"text/xml"</span>))
        .then(<span class="hljs-function">(<span class="hljs-params">data</span>) =&gt;</span> {
          <span class="hljs-comment">// Finding all the "item" tags in the xml response which will contain the episode info</span>
          <span class="hljs-keyword">const</span> items = data.querySelectorAll(<span class="hljs-string">"item"</span>);
          <span class="hljs-comment">// Looping through each item and getting the episode info and pushing it to the 'episodes' array</span>
          items.forEach(<span class="hljs-function">(<span class="hljs-params">item</span>) =&gt;</span> {
            <span class="hljs-keyword">let</span> url;

            <span class="hljs-comment">// Not every podcast episode is going to have the `enclosure` tag, so we need to check if it exists</span>
            <span class="hljs-keyword">try</span> {
              url = item.querySelector(<span class="hljs-string">"enclosure"</span>).getAttribute(<span class="hljs-string">"url"</span>);
            } <span class="hljs-keyword">catch</span> (e) {
              <span class="hljs-built_in">console</span>.log(<span class="hljs-string">"error"</span>, e);
              url = item.querySelector(<span class="hljs-string">"link"</span>).innerHTML;
            }

            episodes.value.push({
              <span class="hljs-comment">// this `title` and the `guid` properties looks a little different because the title contains CDATA tags which need to be grabbed with the 'childNodes' property</span>
              <span class="hljs-attr">title</span>: item.querySelector(<span class="hljs-string">"title"</span>).childNodes[<span class="hljs-number">0</span>].textContent,
              <span class="hljs-attr">link</span>: url,
              <span class="hljs-attr">url</span>: url,
              <span class="hljs-attr">description</span>: item.querySelector(<span class="hljs-string">"description"</span>).innerHTML,
              <span class="hljs-attr">pubDate</span>: item.querySelector(<span class="hljs-string">"pubDate"</span>).innerHTML,
              <span class="hljs-attr">guid</span>: item.querySelector(<span class="hljs-string">"guid"</span>).childNodes[<span class="hljs-number">0</span>].textContent,
            });
          });
        })
        .catch(<span class="hljs-function">(<span class="hljs-params">err</span>) =&gt;</span> {
          alert(<span class="hljs-string">"Couldn't get episodes"</span>, err);
        });
    }

    onMounted(<span class="hljs-function">() =&gt;</span> {
      <span class="hljs-comment">// Getting podcast info from the database once the component is mounted</span>
      getPodcastData();
    });

    <span class="hljs-keyword">return</span> {
      podcast,
      episodes,
    };
  },
};
</span><span class="hljs-tag">&lt;/<span class="hljs-name">script</span>&gt;</span></span>

<span class="xml"><span class="hljs-tag">&lt;<span class="hljs-name">style</span> <span class="hljs-attr">scoped</span>&gt;</span><span class="hljs-tag">&lt;/<span class="hljs-name">style</span>&gt;</span></span>
</code></pre>
<p>With those changes, we can now see the individual episodes of the podcast and can play them using the <code>&lt;audio&gt;</code> html tag. </p>
<h2 id="heading-how-to-get-the-transcriptions-of-the-podcasts">How to Get the Transcriptions of the Podcasts</h2>
<p>The last step is to get transcriptions for the podcasts, and then save them to our database.</p>
<p>If you haven't yet, you'll need to get a <a target="_blank" href="https://console.deepgram.com/signup">free API key from Deepgram</a> in order to process the audio and get the transcriptions. </p>
<p>Once you get the API key, add it to your <code>.env.local</code> file as <code>VITE_DEEPGRAM_KEY</code>. Make sure you restart your dev server here, otherwise you'll probably get a 403 Forbidden error when we finally call the API.</p>
<p>Then add this code to a deepgram.js file in the <code>src</code> folder.</p>
<pre><code class="lang-js"><span class="hljs-keyword">const</span> deepgramKey = <span class="hljs-keyword">import</span>.meta.env.VITE_DEEPGRAM_KEY;

<span class="hljs-keyword">async</span> <span class="hljs-function"><span class="hljs-keyword">function</span> <span class="hljs-title">deepgram</span>(<span class="hljs-params">url</span>) </span>{
  <span class="hljs-keyword">const</span> response = <span class="hljs-keyword">await</span> fetch(
    <span class="hljs-string">"https://api.deepgram.com/v1/listen?punctuate=true&amp;diarize=true&amp;utterances=true"</span>,
    {
      <span class="hljs-attr">method</span>: <span class="hljs-string">"POST"</span>,
      <span class="hljs-attr">headers</span>: {
        <span class="hljs-attr">Authorization</span>: <span class="hljs-string">`Token <span class="hljs-subst">${deepgramKey}</span>`</span>,
        <span class="hljs-string">"Content-Type"</span>: <span class="hljs-string">"application/json"</span>,
      },
      <span class="hljs-attr">body</span>: <span class="hljs-built_in">JSON</span>.stringify({
        <span class="hljs-attr">url</span>: url,
      }),
    }
  );
  <span class="hljs-keyword">const</span> json = <span class="hljs-keyword">await</span> response.json();
  <span class="hljs-keyword">return</span> json.results;
}

<span class="hljs-keyword">export</span> <span class="hljs-keyword">default</span> deepgram;
</code></pre>
<p>This gives us a utility function we can import into our app in other files to call the Deepgram API in order to get the transcriptions. We added the punctuate, diarize, and utterances to the URL as parameters to get a cleaner transcription that is easier to read. </p>
<p>Now that we have that, we need to add some functionality to the <code>PodcastDetail.vue</code> file. I'll walk through the changes and then later will put up the final code for the file.</p>
<p>First we need to have some state to keep track of the transcriptions we get, and also to have some loading state once we click a button to get a transcription. So we'll add these two lines to our setup function:</p>
<pre><code class="lang-js"><span class="hljs-keyword">let</span> transcriptions = ref({});
<span class="hljs-keyword">const</span> episodeTranscriptionLoading = ref([]);
</code></pre>
<p>Don't forget to add them to the return object of the <code>setup</code> function.</p>
<p>Then add this function to make the request to Deepgram, and then add the transcription to the local <code>transcriptions</code> object.</p>
<pre><code class="lang-js"><span class="hljs-comment">// Function to get a transcription from Deepgram, passing in the episode url</span>
<span class="hljs-keyword">async</span> <span class="hljs-function"><span class="hljs-keyword">function</span> <span class="hljs-title">getTranscription</span>(<span class="hljs-params">episode</span>) </span>{
    <span class="hljs-comment">// setting the loading state to true for the episode</span>
    episodeTranscriptionLoading.value.push(episode.guid);
    <span class="hljs-keyword">const</span> transcription = <span class="hljs-keyword">await</span> deepgram(episode.url);
    <span class="hljs-comment">// setting a unique id for the episode transcription</span>
    transcriptions.value[<span class="hljs-string">`<span class="hljs-subst">${podcast.value.id}</span>---<span class="hljs-subst">${episode.guid}</span>`</span>] =
        transcription;
    <span class="hljs-comment">// removing the loading state for the episode</span>
    episodeTranscriptionLoading.value.splice(
        episodeTranscriptionLoading.value.indexOf(episode.guid),
        <span class="hljs-number">1</span>
    );
}
</code></pre>
<p>Make sure you import the deepgram function from <code>deepgram.js</code> at the top of the script tag.</p>
<p>Then update the template to this:</p>
<pre><code class="lang-html"><span class="hljs-tag">&lt;<span class="hljs-name">template</span>&gt;</span>
  <span class="hljs-tag">&lt;<span class="hljs-name">nav</span> <span class="hljs-attr">class</span>=<span class="hljs-string">""</span>&gt;</span>
    <span class="hljs-tag">&lt;<span class="hljs-name">a</span> <span class="hljs-attr">href</span>=<span class="hljs-string">"/"</span> <span class="hljs-attr">class</span>=<span class="hljs-string">""</span>&gt;</span>Home<span class="hljs-tag">&lt;/<span class="hljs-name">a</span>&gt;</span>
  <span class="hljs-tag">&lt;/<span class="hljs-name">nav</span>&gt;</span>
  <span class="hljs-comment">&lt;!-- Basic layout for showing podcast info --&gt;</span>
  <span class="hljs-tag">&lt;<span class="hljs-name">div</span> <span class="hljs-attr">class</span>=<span class="hljs-string">"podcast-detail"</span>&gt;</span>
    <span class="hljs-tag">&lt;<span class="hljs-name">img</span> <span class="hljs-attr">:src</span>=<span class="hljs-string">"podcast.image_url"</span> <span class="hljs-attr">:alt</span>=<span class="hljs-string">"podcast.name"</span> <span class="hljs-attr">class</span>=<span class="hljs-string">""</span> /&gt;</span>
    <span class="hljs-tag">&lt;<span class="hljs-name">h1</span> <span class="hljs-attr">class</span>=<span class="hljs-string">""</span>&gt;</span>{{ podcast.name }}<span class="hljs-tag">&lt;/<span class="hljs-name">h1</span>&gt;</span>
    <span class="hljs-tag">&lt;<span class="hljs-name">p</span>&gt;</span>{{ podcast.description }}<span class="hljs-tag">&lt;/<span class="hljs-name">p</span>&gt;</span>
    <span class="hljs-tag">&lt;<span class="hljs-name">h2</span> <span class="hljs-attr">class</span>=<span class="hljs-string">""</span>&gt;</span>Episodes<span class="hljs-tag">&lt;/<span class="hljs-name">h2</span>&gt;</span>
    <span class="hljs-comment">&lt;!-- Looping through each episode of a podcast and displaying episode info --&gt;</span>
    <span class="hljs-tag">&lt;<span class="hljs-name">ul</span> <span class="hljs-attr">class</span>=<span class="hljs-string">""</span>&gt;</span>
      <span class="hljs-tag">&lt;<span class="hljs-name">li</span>
        <span class="hljs-attr">v-for</span>=<span class="hljs-string">"episode in episodes"</span>
        <span class="hljs-attr">:key</span>=<span class="hljs-string">"episode.guid || episode.link"</span>
        <span class="hljs-attr">class</span>=<span class="hljs-string">""</span>
      &gt;</span>
        <span class="hljs-tag">&lt;<span class="hljs-name">div</span> <span class="hljs-attr">class</span>=<span class="hljs-string">"info"</span>&gt;</span>
          <span class="hljs-tag">&lt;<span class="hljs-name">h3</span>&gt;</span>{{ episode.title }}<span class="hljs-tag">&lt;/<span class="hljs-name">h3</span>&gt;</span>
          <span class="hljs-tag">&lt;<span class="hljs-name">audio</span> <span class="hljs-attr">class</span>=<span class="hljs-string">""</span> <span class="hljs-attr">controls</span>&gt;</span>
            <span class="hljs-tag">&lt;<span class="hljs-name">source</span> <span class="hljs-attr">:src</span>=<span class="hljs-string">"episode.url"</span> <span class="hljs-attr">type</span>=<span class="hljs-string">"audio/mpeg"</span> /&gt;</span>
            Display
          <span class="hljs-tag">&lt;/<span class="hljs-name">audio</span>&gt;</span>
            <span class="hljs-comment">&lt;!-- button to get transcriptions --&gt;</span>
          <span class="hljs-tag">&lt;<span class="hljs-name">button</span>
            <span class="hljs-attr">v-if</span>=<span class="hljs-string">"!transcriptions[`${podcast.id}---${episode.guid}`]"</span>
            @<span class="hljs-attr">click.prevent</span>=<span class="hljs-string">"getTranscription(episode)"</span>
            <span class="hljs-attr">class</span>=<span class="hljs-string">""</span>
          &gt;</span>
            {{
              episodeTranscriptionLoading.includes(episode.guid)
                ? "Loading..."
                : "Get Transcription"
            }}
          <span class="hljs-tag">&lt;/<span class="hljs-name">button</span>&gt;</span>
        <span class="hljs-tag">&lt;/<span class="hljs-name">div</span>&gt;</span>
          <span class="hljs-comment">&lt;!-- box to display the transcription --&gt;</span>
        <span class="hljs-tag">&lt;<span class="hljs-name">div</span>
          <span class="hljs-attr">v-if</span>=<span class="hljs-string">"transcriptions[`${podcast.id}---${episode.guid}`]"</span>
          <span class="hljs-attr">class</span>=<span class="hljs-string">"transcription"</span>
        &gt;</span>
          <span class="hljs-tag">&lt;<span class="hljs-name">p</span>&gt;</span>
            {{
              transcriptions[`${podcast.id}---${episode.guid}`].channels[0]
                .alternatives[0].transcript
            }}
          <span class="hljs-tag">&lt;/<span class="hljs-name">p</span>&gt;</span>
        <span class="hljs-tag">&lt;/<span class="hljs-name">div</span>&gt;</span> 
      <span class="hljs-tag">&lt;/<span class="hljs-name">li</span>&gt;</span>
    <span class="hljs-tag">&lt;/<span class="hljs-name">ul</span>&gt;</span>
  <span class="hljs-tag">&lt;/<span class="hljs-name">div</span>&gt;</span>
<span class="hljs-tag">&lt;/<span class="hljs-name">template</span>&gt;</span>
</code></pre>
<h2 id="heading-how-to-save-the-transcriptions">How to Save the Transcriptions</h2>
<p>Now that we can get the transcriptions, we need to add the functionality to save them to Supabase. </p>
<p>First, go create a table in Supabase like we did above, but this time name the table <code>transcriptions</code>. You'll want the following as the columns:</p>
<ul>
<li><strong>id</strong> – varchar (primary) Also remove the "Is Identity" check mark in the settings for this column</li>
<li><strong>podcast_id</strong> – int8</li>
<li><strong>episode_guid</strong> – varchar</li>
<li><strong>transcript</strong> – text</li>
<li><strong>user_id</strong> – uuid (You'll need to link this to the user table by clicking on the link icon)</li>
<li><strong>created_at</strong> – timestamptz</li>
</ul>
<p><img src="https://www.freecodecamp.org/news/content/images/2022/02/Screen-Shot-2022-02-22-at-4.40.52-PM.png" alt="Image" width="600" height="400" loading="lazy">
<em>columns for transcriptions table</em></p>
<p>Once that table is set up, we can add a reactive property called <code>savedTranscriptions</code> to the component and then add the following code to save the transcriptions to Supabase. Then we'll store them in the object <code>savedTranscriptions</code>.</p>
<pre><code class="lang-js"><span class="hljs-function"><span class="hljs-keyword">function</span> <span class="hljs-title">saveTranscription</span>(<span class="hljs-params">podcastId, episodeGuid</span>) </span>{
    supabase
        .from(<span class="hljs-string">"transcriptions"</span>)
        .insert({
        <span class="hljs-attr">id</span>: <span class="hljs-string">`<span class="hljs-subst">${podcastId}</span>---<span class="hljs-subst">${episodeGuid}</span>`</span>,
        <span class="hljs-attr">podcast_id</span>: podcastId,
        <span class="hljs-attr">episode_guid</span>: episodeGuid,
        <span class="hljs-attr">transcript</span>:
        transcriptions.value[<span class="hljs-string">`<span class="hljs-subst">${podcastId}</span>---<span class="hljs-subst">${episodeGuid}</span>`</span>].channels[<span class="hljs-number">0</span>]
        .alternatives[<span class="hljs-number">0</span>].transcript,
        <span class="hljs-attr">user_id</span>: store.state.user.id,
    })
        .then(<span class="hljs-function">(<span class="hljs-params">{ data: [transcriptObject] }</span>) =&gt;</span> {
        savedTranscriptions.value[transcriptObject.id] =
            transcriptObject.transcript;
    });
}
</code></pre>
<p>Once a user has a transcription saved, we need to display it whenever they re-visit a page. Add this function to get that data from Supabase:</p>
<pre><code class="lang-js"><span class="hljs-keyword">async</span> <span class="hljs-function"><span class="hljs-keyword">function</span> <span class="hljs-title">getTranscriptions</span>(<span class="hljs-params"></span>) </span>{
    <span class="hljs-keyword">const</span> { <span class="hljs-attr">data</span>: transcriptions } = <span class="hljs-keyword">await</span> supabase
    .from(<span class="hljs-string">"transcriptions"</span>)
    .select()
    .eq(<span class="hljs-string">"podcast_id"</span>, podcast.value.id);
    <span class="hljs-built_in">console</span>.log(<span class="hljs-string">"Transcriptions"</span>, transcriptions);
    transcriptions.forEach(<span class="hljs-function">(<span class="hljs-params">transcript</span>) =&gt;</span> {
        <span class="hljs-built_in">console</span>.log(<span class="hljs-string">"id"</span>, transcript.id);
        savedTranscriptions.value[transcript.id] = transcript.transcript;
    });
}
</code></pre>
<p>We will want this called whenever a user hits this page, but not until we get the podcast information. Add a call to <code>getTranscriptions</code> to the bottom of the <code>getPodcastData</code> function to do that.</p>
<p>The last thing to do is to update the template to include the save buttons and to display transcriptions if they are in the saved objects. The final code for the <code>PodcastDetail.vue</code> should then look like this:</p>
<pre><code class="lang-js">&lt;template&gt;
  <span class="xml"><span class="hljs-tag">&lt;<span class="hljs-name">nav</span> <span class="hljs-attr">class</span>=<span class="hljs-string">""</span>&gt;</span>
    <span class="hljs-tag">&lt;<span class="hljs-name">a</span> <span class="hljs-attr">href</span>=<span class="hljs-string">"/"</span> <span class="hljs-attr">class</span>=<span class="hljs-string">""</span>&gt;</span>Home<span class="hljs-tag">&lt;/<span class="hljs-name">a</span>&gt;</span>
  <span class="hljs-tag">&lt;/<span class="hljs-name">nav</span>&gt;</span></span>
  &lt;!-- Basic layout <span class="hljs-keyword">for</span> showing podcast info --&gt;
  <span class="xml"><span class="hljs-tag">&lt;<span class="hljs-name">div</span> <span class="hljs-attr">class</span>=<span class="hljs-string">"podcast-detail"</span>&gt;</span>
    <span class="hljs-tag">&lt;<span class="hljs-name">img</span> <span class="hljs-attr">:src</span>=<span class="hljs-string">"podcast.image_url"</span> <span class="hljs-attr">:alt</span>=<span class="hljs-string">"podcast.name"</span> <span class="hljs-attr">class</span>=<span class="hljs-string">""</span> /&gt;</span>
    <span class="hljs-tag">&lt;<span class="hljs-name">h1</span> <span class="hljs-attr">class</span>=<span class="hljs-string">""</span>&gt;</span>{{ podcast.name }}<span class="hljs-tag">&lt;/<span class="hljs-name">h1</span>&gt;</span>
    <span class="hljs-tag">&lt;<span class="hljs-name">p</span>&gt;</span>{{ podcast.description }}<span class="hljs-tag">&lt;/<span class="hljs-name">p</span>&gt;</span>
    <span class="hljs-tag">&lt;<span class="hljs-name">h2</span> <span class="hljs-attr">class</span>=<span class="hljs-string">""</span>&gt;</span>Episodes<span class="hljs-tag">&lt;/<span class="hljs-name">h2</span>&gt;</span>
    <span class="hljs-comment">&lt;!-- Looping through each episode of a podcast and displaying episode info --&gt;</span>
    <span class="hljs-tag">&lt;<span class="hljs-name">ul</span> <span class="hljs-attr">class</span>=<span class="hljs-string">""</span>&gt;</span>
      <span class="hljs-tag">&lt;<span class="hljs-name">li</span>
        <span class="hljs-attr">v-for</span>=<span class="hljs-string">"episode in episodes"</span>
        <span class="hljs-attr">:key</span>=<span class="hljs-string">"episode.guid || episode.link"</span>
        <span class="hljs-attr">class</span>=<span class="hljs-string">""</span>
      &gt;</span>
        <span class="hljs-tag">&lt;<span class="hljs-name">div</span> <span class="hljs-attr">class</span>=<span class="hljs-string">"info"</span>&gt;</span>
          <span class="hljs-tag">&lt;<span class="hljs-name">h3</span>&gt;</span>{{ episode.title }}<span class="hljs-tag">&lt;/<span class="hljs-name">h3</span>&gt;</span>
          <span class="hljs-tag">&lt;<span class="hljs-name">audio</span> <span class="hljs-attr">class</span>=<span class="hljs-string">""</span> <span class="hljs-attr">controls</span>&gt;</span>
            <span class="hljs-tag">&lt;<span class="hljs-name">source</span> <span class="hljs-attr">:src</span>=<span class="hljs-string">"episode.url"</span> <span class="hljs-attr">type</span>=<span class="hljs-string">"audio/mpeg"</span> /&gt;</span>
            Display
          <span class="hljs-tag">&lt;/<span class="hljs-name">audio</span>&gt;</span>
          <span class="hljs-tag">&lt;<span class="hljs-name">button</span>
            <span class="hljs-attr">v-if</span>=<span class="hljs-string">"savedTranscriptions[`${podcast.id}---${episode.guid}`]"</span>
            <span class="hljs-attr">disabled</span>
          &gt;</span>
            Transcription Saved
          <span class="hljs-tag">&lt;/<span class="hljs-name">button</span>&gt;</span>
          <span class="hljs-tag">&lt;<span class="hljs-name">button</span>
            <span class="hljs-attr">v-else-if</span>=<span class="hljs-string">"
              !transcriptions[`${podcast.id}---${episode.guid}`] &amp;&amp;
              !savedTranscriptions[`${podcast.id}---${episode.guid}`]
            "</span>
            @<span class="hljs-attr">click.prevent</span>=<span class="hljs-string">"getTranscription(episode)"</span>
            <span class="hljs-attr">class</span>=<span class="hljs-string">""</span>
          &gt;</span>
            {{
              episodeTranscriptionLoading.includes(episode.guid)
                ? "Loading..."
                : "Get Transcription"
            }}
          <span class="hljs-tag">&lt;/<span class="hljs-name">button</span>&gt;</span>
          <span class="hljs-tag">&lt;<span class="hljs-name">button</span>
            <span class="hljs-attr">v-if</span>=<span class="hljs-string">"
              transcriptions[`${podcast.id}---${episode.guid}`] &amp;&amp;
              !savedTranscriptions[`${podcast.id}---${episode.guid}`]
            "</span>
            <span class="hljs-attr">class</span>=<span class="hljs-string">"save"</span>
            @<span class="hljs-attr">click.prevent</span>=<span class="hljs-string">"saveTranscription(podcast.id, episode.guid)"</span>
          &gt;</span>
            Save Transcription
          <span class="hljs-tag">&lt;/<span class="hljs-name">button</span>&gt;</span>
        <span class="hljs-tag">&lt;/<span class="hljs-name">div</span>&gt;</span>
        <span class="hljs-tag">&lt;<span class="hljs-name">div</span>
          <span class="hljs-attr">v-if</span>=<span class="hljs-string">"savedTranscriptions[`${podcast.id}---${episode.guid}`]"</span>
          <span class="hljs-attr">class</span>=<span class="hljs-string">"transcription"</span>
        &gt;</span>
          <span class="hljs-tag">&lt;<span class="hljs-name">p</span>&gt;</span>
            {{ savedTranscriptions[`${podcast.id}---${episode.guid}`] }}
          <span class="hljs-tag">&lt;/<span class="hljs-name">p</span>&gt;</span>
        <span class="hljs-tag">&lt;/<span class="hljs-name">div</span>&gt;</span>
        <span class="hljs-tag">&lt;<span class="hljs-name">div</span>
          <span class="hljs-attr">v-else-if</span>=<span class="hljs-string">"transcriptions[`${podcast.id}---${episode.guid}`]"</span>
          <span class="hljs-attr">class</span>=<span class="hljs-string">"transcription"</span>
        &gt;</span>
          <span class="hljs-tag">&lt;<span class="hljs-name">p</span>&gt;</span>
            {{
              transcriptions[`${podcast.id}---${episode.guid}`].channels[0]
                .alternatives[0].transcript
            }}
          <span class="hljs-tag">&lt;/<span class="hljs-name">p</span>&gt;</span>
        <span class="hljs-tag">&lt;/<span class="hljs-name">div</span>&gt;</span>
      <span class="hljs-tag">&lt;/<span class="hljs-name">li</span>&gt;</span>
    <span class="hljs-tag">&lt;/<span class="hljs-name">ul</span>&gt;</span>
  <span class="hljs-tag">&lt;/<span class="hljs-name">div</span>&gt;</span></span>
&lt;/template&gt;

<span class="xml"><span class="hljs-tag">&lt;<span class="hljs-name">script</span>&gt;</span><span class="javascript">
<span class="hljs-comment">// Importing necessary methods</span>
<span class="hljs-keyword">import</span> { ref, onMounted } <span class="hljs-keyword">from</span> <span class="hljs-string">"vue"</span>;
<span class="hljs-keyword">import</span> { useRoute } <span class="hljs-keyword">from</span> <span class="hljs-string">"vue-router"</span>;
<span class="hljs-keyword">import</span> { supabase } <span class="hljs-keyword">from</span> <span class="hljs-string">"../supabase"</span>;
<span class="hljs-keyword">import</span> { store } <span class="hljs-keyword">from</span> <span class="hljs-string">"../store"</span>;
<span class="hljs-keyword">import</span> deepgram <span class="hljs-keyword">from</span> <span class="hljs-string">"../deepgram"</span>;

<span class="hljs-keyword">export</span> <span class="hljs-keyword">default</span> {
  setup() {
    <span class="hljs-keyword">const</span> route = useRoute();
    <span class="hljs-keyword">const</span> podcast = ref({});
    <span class="hljs-keyword">const</span> episodes = ref([]);
    <span class="hljs-keyword">let</span> transcriptions = ref({});
    <span class="hljs-keyword">let</span> savedTranscriptions = ref({});
    <span class="hljs-keyword">const</span> episodeTranscriptionLoading = ref([]);

    <span class="hljs-comment">// Getting podcast info from the database</span>
    <span class="hljs-keyword">async</span> <span class="hljs-function"><span class="hljs-keyword">function</span> <span class="hljs-title">getPodcastData</span>(<span class="hljs-params"></span>) </span>{
      <span class="hljs-keyword">const</span> {
        <span class="hljs-attr">data</span>: [podcastinfo],
      } = <span class="hljs-keyword">await</span> supabase.from(<span class="hljs-string">"podcasts"</span>).select().eq(<span class="hljs-string">"id"</span>, route.params.id);
      podcast.value = podcastinfo;

      <span class="hljs-comment">// Making call to episode url to get episode info</span>
      getEpisodes(podcastinfo.rss_url);
      <span class="hljs-keyword">await</span> getTranscriptions();
    }

    <span class="hljs-function"><span class="hljs-keyword">function</span> <span class="hljs-title">getEpisodes</span>(<span class="hljs-params">url</span>) </span>{
      fetch(url)
        .then(<span class="hljs-function">(<span class="hljs-params">response</span>) =&gt;</span> response.text())
        .then(<span class="hljs-function">(<span class="hljs-params">str</span>) =&gt;</span> <span class="hljs-keyword">new</span> <span class="hljs-built_in">window</span>.DOMParser().parseFromString(str, <span class="hljs-string">"text/xml"</span>))
        .then(<span class="hljs-function">(<span class="hljs-params">data</span>) =&gt;</span> {
          <span class="hljs-comment">// Finding all the "item" tags in the xml response which will contain the episode info</span>
          <span class="hljs-keyword">const</span> items = data.querySelectorAll(<span class="hljs-string">"item"</span>);
          <span class="hljs-comment">// Looping through each item and getting the episode info and pushing it to the 'episodes' array</span>
          items.forEach(<span class="hljs-function">(<span class="hljs-params">item</span>) =&gt;</span> {
            <span class="hljs-keyword">let</span> url;

            <span class="hljs-comment">// Not every podcast episode is going to have the `enclosure` tag, so we need to check if it exists</span>
            <span class="hljs-keyword">try</span> {
              url = item.querySelector(<span class="hljs-string">"enclosure"</span>).getAttribute(<span class="hljs-string">"url"</span>);
            } <span class="hljs-keyword">catch</span> (e) {
              <span class="hljs-built_in">console</span>.log(<span class="hljs-string">"error"</span>, e);
              url = item.querySelector(<span class="hljs-string">"link"</span>).innerHTML;
            }

            episodes.value.push({
              <span class="hljs-comment">// this `title` and the `guid` properties looks a little different because the title contains CDATA tags which need to be grabbed with the 'childNodes' property</span>
              <span class="hljs-attr">title</span>: item.querySelector(<span class="hljs-string">"title"</span>).childNodes[<span class="hljs-number">0</span>].textContent,
              <span class="hljs-attr">link</span>: url,
              <span class="hljs-attr">url</span>: url,
              <span class="hljs-attr">description</span>: item.querySelector(<span class="hljs-string">"description"</span>).innerHTML,
              <span class="hljs-attr">pubDate</span>: item.querySelector(<span class="hljs-string">"pubDate"</span>).innerHTML,
              <span class="hljs-attr">guid</span>: item.querySelector(<span class="hljs-string">"guid"</span>).childNodes[<span class="hljs-number">0</span>].textContent,
            });
          });
        })
        .catch(<span class="hljs-function">(<span class="hljs-params">err</span>) =&gt;</span> {
          alert(<span class="hljs-string">"Couldn't get episodes"</span>, err);
        });
    }

    <span class="hljs-comment">// Function to get a transcription from Deepgram, passing in the episode url</span>
    <span class="hljs-keyword">async</span> <span class="hljs-function"><span class="hljs-keyword">function</span> <span class="hljs-title">getTranscription</span>(<span class="hljs-params">episode</span>) </span>{
      <span class="hljs-comment">// setting the loading state to true for the episode</span>
      episodeTranscriptionLoading.value.push(episode.guid);
      <span class="hljs-keyword">const</span> transcription = <span class="hljs-keyword">await</span> deepgram(episode.url);
      <span class="hljs-comment">// setting a unique id for the episode transcription</span>
      transcriptions.value[<span class="hljs-string">`<span class="hljs-subst">${podcast.value.id}</span>---<span class="hljs-subst">${episode.guid}</span>`</span>] =
        transcription;
      <span class="hljs-comment">// removing the loading state for the episode</span>
      episodeTranscriptionLoading.value.splice(
        episodeTranscriptionLoading.value.indexOf(episode.guid),
        <span class="hljs-number">1</span>
      );
    }

    <span class="hljs-function"><span class="hljs-keyword">function</span> <span class="hljs-title">saveTranscription</span>(<span class="hljs-params">podcastId, episodeGuid</span>) </span>{
      <span class="hljs-built_in">console</span>.log(
        <span class="hljs-string">"saving transcription"</span>,
        transcriptions.value[<span class="hljs-string">`<span class="hljs-subst">${podcastId}</span>---<span class="hljs-subst">${episodeGuid}</span>`</span>]
      );
      supabase
        .from(<span class="hljs-string">"transcriptions"</span>)
        .insert({
          <span class="hljs-attr">id</span>: <span class="hljs-string">`<span class="hljs-subst">${podcastId}</span>---<span class="hljs-subst">${episodeGuid}</span>`</span>,
          <span class="hljs-attr">podcast_id</span>: podcastId,
          <span class="hljs-attr">episode_guid</span>: episodeGuid,
          <span class="hljs-attr">transcript</span>:
            transcriptions.value[<span class="hljs-string">`<span class="hljs-subst">${podcastId}</span>---<span class="hljs-subst">${episodeGuid}</span>`</span>].channels[<span class="hljs-number">0</span>]
              .alternatives[<span class="hljs-number">0</span>].transcript,
          <span class="hljs-attr">user_id</span>: store.state.user.id,
        })
        .then(<span class="hljs-function">(<span class="hljs-params">{ data: [transcriptObject] }</span>) =&gt;</span> {
          <span class="hljs-built_in">console</span>.log(<span class="hljs-string">"saved"</span>, transcriptObject);
          savedTranscriptions.value[transcriptObject.id] =
            transcriptObject.transcript;
        });
    }

    <span class="hljs-keyword">async</span> <span class="hljs-function"><span class="hljs-keyword">function</span> <span class="hljs-title">getTranscriptions</span>(<span class="hljs-params"></span>) </span>{
      <span class="hljs-keyword">const</span> { <span class="hljs-attr">data</span>: transcriptions } = <span class="hljs-keyword">await</span> supabase
        .from(<span class="hljs-string">"transcriptions"</span>)
        .select()
        .eq(<span class="hljs-string">"podcast_id"</span>, podcast.value.id);
      <span class="hljs-built_in">console</span>.log(<span class="hljs-string">"Transcriptions"</span>, transcriptions);
      transcriptions.forEach(<span class="hljs-function">(<span class="hljs-params">transcript</span>) =&gt;</span> {
        <span class="hljs-built_in">console</span>.log(<span class="hljs-string">"id"</span>, transcript.id);
        savedTranscriptions.value[transcript.id] = transcript.transcript;
      });
    }

    onMounted(<span class="hljs-function">() =&gt;</span> {
      <span class="hljs-comment">// Getting podcast info from the database once the component is mounted</span>
      getPodcastData();
    });

    <span class="hljs-keyword">return</span> {
      podcast,
      episodes,
      transcriptions,
      savedTranscriptions,
      episodeTranscriptionLoading,

      getTranscription,
      saveTranscription,
    };
  },
};
</span><span class="hljs-tag">&lt;/<span class="hljs-name">script</span>&gt;</span></span>

<span class="xml"><span class="hljs-tag">&lt;<span class="hljs-name">style</span> <span class="hljs-attr">scoped</span>&gt;</span><span class="hljs-tag">&lt;/<span class="hljs-name">style</span>&gt;</span></span>
</code></pre>
<h2 id="heading-conclusion">Conclusion</h2>
<p>If you've followed the steps above, you should have a working app. <a target="_blank" href="https://github.com/briancbarrow/vue-supabase-auth/tree/final-podcast-feed-transcriptions">Here is the final code</a> if you want to double check against what I have written.</p>
<p>I know I enjoyed making the app. Supabase makes it really easy to get a database/backend up and running. Please reach out to me on <a target="_blank" href="https://twitter.com/the_BrianB">Twitter</a> if you have any questions!</p>
 ]]>
                </content:encoded>
            </item>
        
            <item>
                <title>
                    <![CDATA[ How to Fix Broken Audio in Headphones and Speakers in Linux ]]>
                </title>
                <description>
                    <![CDATA[ If you are using the Linux operating system on your desktop, you might have faced some audio issues before. Like when you're trying to get sound in your speakers when your headphones are connected to the audio jack.  If so, no worries! It is a pretty... ]]>
                </description>
                <link>https://www.freecodecamp.org/news/how-to-fix-broken-audio-in-headphones-and-speakers-linux/</link>
                <guid isPermaLink="false">66b902dc558588891017dd84</guid>
                
                    <category>
                        <![CDATA[ audio ]]>
                    </category>
                
                    <category>
                        <![CDATA[ Linux ]]>
                    </category>
                
                <dc:creator>
                    <![CDATA[ Md. Fahim Bin Amin ]]>
                </dc:creator>
                <pubDate>Wed, 16 Feb 2022 15:27:21 +0000</pubDate>
                <media:content url="https://www.freecodecamp.org/news/content/images/2022/02/Screenshot--10-.jpg" medium="image" />
                <content:encoded>
                    <![CDATA[ <p>If you are using the Linux operating system on your desktop, you might have faced some audio issues before. Like when you're trying to get sound in your speakers when your headphones are connected to the audio jack. </p>
<p>If so, no worries! It is a pretty common issue that you can resolve quickly. In this article, I am going to help you solve the issue completely. I'll be using a well known distro called <a target="_blank" href="https://manjaro.org/">Manjaro Linux</a>, but I believe that the same method is applicable for all Linux distributions.</p>
<h3 id="heading-step-1-open-the-terminal-console">Step 1 – Open the terminal / console</h3>
<p><img src="https://www.freecodecamp.org/news/content/images/2022/02/2.png" alt="Image" width="600" height="400" loading="lazy"></p>
<h3 id="heading-step-2-open-alsamixer">Step 2 – Open Alsamixer</h3>
<p>We will use alsamixer to tweak the audio settings. Type <code>alsamixer</code> and press the <strong>Enter</strong> key on your keyboard.</p>
<p><img src="https://www.freecodecamp.org/news/content/images/2022/02/3.png" alt="Image" width="600" height="400" loading="lazy"></p>
<p>Alsamixer will open in your terminal.</p>
<p><img src="https://www.freecodecamp.org/news/content/images/2022/02/4.png" alt="Image" width="600" height="400" loading="lazy"></p>
<h3 id="heading-step-3-set-preferred-sound-card">Step 3 – Set Preferred Sound Card</h3>
<p>Now you need to select your preferred sound card. For that, simply press the <code>F6</code> key on your keyboard. Select the sound card appropriate for you. </p>
<p>If you are not sure, then you can simply select one of them at a time (press the <strong>Enter</strong> key after selecting the sound card) and try the rest of the methods to see whether that sound card was appropriate or not. For me, it is <strong>default:1 HD-Audio Generic</strong>.</p>
<p><img src="https://www.freecodecamp.org/news/content/images/2022/02/5.png" alt="Image" width="600" height="400" loading="lazy"></p>
<p>The AlsaMixer window will change depending on your selected sound card.</p>
<p><img src="https://www.freecodecamp.org/news/content/images/2022/02/6.png" alt="Image" width="600" height="400" loading="lazy"></p>
<p>Then press the right arrow (<code>➜</code>) key until you find <strong>Auto-Mute Mode</strong>. </p>
<p>You will see that it is currently <strong>ENABLED</strong>.</p>
<p><img src="https://www.freecodecamp.org/news/content/images/2022/02/7.png" alt="Image" width="600" height="400" loading="lazy"></p>
<p>We need to change it back to <strong>DISABLED</strong>. You have to press the down arrow (<code>↓</code>) key to disable it.</p>
<p><img src="https://www.freecodecamp.org/news/content/images/2022/02/8.png" alt="Image" width="600" height="400" loading="lazy"></p>
<p>Then press the <code>Esc</code> key to exit AlsaMixer.</p>
<p><img src="https://www.freecodecamp.org/news/content/images/2022/02/10.png" alt="Image" width="600" height="400" loading="lazy"></p>
<h3 id="heading-step-4-save-settings">Step 4 – Save Settings</h3>
<p>Now we need to save the settings that we just tweaked in AlsaMixer. For that, type <code>sudo alsactl store</code>. Now press the <strong>Enter</strong> key.</p>
<p><img src="https://www.freecodecamp.org/news/content/images/2022/02/11.png" alt="Image" width="600" height="400" loading="lazy"></p>
<p><img src="https://www.freecodecamp.org/news/content/images/2022/02/12.png" alt="Image" width="600" height="400" loading="lazy"></p>
<p>Now you are good to go!</p>
<p>If you want to check whether the audio is working in your speakers or not, then you can go to <strong>Settings</strong>.</p>
<p><img src="https://www.freecodecamp.org/news/content/images/2022/02/13.png" alt="Image" width="600" height="400" loading="lazy"></p>
<p><img src="https://www.freecodecamp.org/news/content/images/2022/02/14.png" alt="Image" width="600" height="400" loading="lazy"></p>
<p>Then go to <strong>Sound</strong>.</p>
<p><img src="https://www.freecodecamp.org/news/content/images/2022/02/15.png" alt="Image" width="600" height="400" loading="lazy"></p>
<p>If you change your <strong>Output Device</strong>, then you should see that the speaker audio is working perfectly. I am using the <strong>Line Out</strong> option here for my workstation.</p>
<p><img src="https://www.freecodecamp.org/news/content/images/2022/02/16.png" alt="Image" width="600" height="400" loading="lazy"></p>
<p><img src="https://www.freecodecamp.org/news/content/images/2022/02/17.png" alt="Image" width="600" height="400" loading="lazy"></p>
<p>Now you should see that the audio issue has been fixed!</p>
<p><strong>BONUS</strong>: I have also made a complete video tutorial about this and published the <a target="_blank" href="https://youtu.be/zCaJ6lcaSOg">video</a> on my new YouTube channel.</p>
<div class="embed-wrapper">
        <iframe width="560" height="315" src="https://www.youtube.com/embed/zCaJ6lcaSOg" style="aspect-ratio: 16 / 9; width: 100%; height: auto;" title="YouTube video player" allow="accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture; web-share" referrerpolicy="strict-origin-when-cross-origin" allowfullscreen="" loading="lazy"></iframe></div>
<h2 id="heading-conclusion">Conclusion</h2>
<p>I hope this trick helps you if you also face this kind of audio issue in your speakers. Thanks a lot for reading the entire article till now. If it helps you then you can also check out other articles of mine at <a target="_blank" href="https://www.freecodecamp.org/news/author/fahimbinamin/">freeCodeCamp</a>.</p>
<p>If you want to get in touch with me, then you can do so using <a target="_blank" href="https://twitter.com/Fahim_FBA">Twitter</a>, <a target="_blank" href="https://www.linkedin.com/in/fahimfba/">LinkedIn</a>, and <a target="_blank" href="https://github.com/FahimFBA">GitHub</a>. </p>
<p>You can also <a target="_blank" href="https://www.youtube.com/@FahimAmin?sub_confirmation=1">SUBSCRIBE to my YouTube channel</a> (Code With FahimFBA) if you want to learn various kinds of programming languages with a lot of practical examples regularly.</p>
<p>If you want to check out my highlights, then you can do so at my <a target="_blank" href="https://www.polywork.com/fahimbinamin">Polywork timeline</a>.</p>
<p>You can also <a target="_blank" href="https://fahimbinamin.com/">visit my website</a> to learn more about me and what I'm working on.</p>
<p>Thanks a bunch!</p>
 ]]>
                </content:encoded>
            </item>
        
            <item>
                <title>
                    <![CDATA[ No Audio Output Device is Installed [Fixed on Windows 10 PC] ]]>
                </title>
                <description>
                    <![CDATA[ If you use Windows 10, you might have encountered the error "no audio output device is installed".  This error can be caused by a corrupt driver, overdue updates, or improper connection. In this article, I will show you 2 ways you can fix the "no au... ]]>
                </description>
                <link>https://www.freecodecamp.org/news/no-audio-output-device-is-installed-fixed-on-windows-10-pc/</link>
                <guid isPermaLink="false">66adf1a76f5e63db3fc43634</guid>
                
                    <category>
                        <![CDATA[ audio ]]>
                    </category>
                
                    <category>
                        <![CDATA[ how-to ]]>
                    </category>
                
                    <category>
                        <![CDATA[ Windows 10 ]]>
                    </category>
                
                <dc:creator>
                    <![CDATA[ Kolade Chris ]]>
                </dc:creator>
                <pubDate>Wed, 10 Nov 2021 18:27:53 +0000</pubDate>
                <media:content url="https://www.freecodecamp.org/news/content/images/2021/11/wu-yi-nLRX1koA8Zo-unsplash.jpg" medium="image" />
                <content:encoded>
                    <![CDATA[ <p>If you use Windows 10, you might have encountered the error "no audio output device is installed". </p>
<p><img src="https://www.freecodecamp.org/news/content/images/2021/11/no-audio-output-device-is-installed2.png" alt="no-audio-output-device-is-installed2" width="600" height="400" loading="lazy"></p>
<p>This error can be caused by a corrupt driver, overdue updates, or improper connection.</p>
<p>In this article, I will show you 2 ways you can fix the "no audio output device is installed" error. I will also show you some other troubleshooting methods, so you will be able to listen to music or hear sounds on your computer once again.</p>
<h2 id="heading-how-to-fix-the-no-audio-output-device-is-installed-error-by-updating-audio-driver">How to Fix the No Audio Output Device Is Installed Error by Updating Audio Driver</h2>
<p><strong>Step 1</strong>: Click on Start (Windows logo) or press the <code>WIN</code> key on your keyboard, then search for "device manager". Click on the first search result or press <code>ENTER</code>.
<img src="https://www.freecodecamp.org/news/content/images/2021/11/ss-1-6.jpg" alt="ss-1-6" width="600" height="400" loading="lazy"></p>
<p><strong>Step 2</strong>: Expand “Audio inputs and outputs”.</p>
<p><strong>Step 3</strong>: Right-click on your audio device and select "Update driver".
<img src="https://www.freecodecamp.org/news/content/images/2021/11/ss-2-5.jpg" alt="ss-2-5" width="600" height="400" loading="lazy"></p>
<p><strong>Step 4</strong>: Choose “Search automatically for updated driver software”. Windows will now search the internet for an updated audio driver and install it for your computer.
<img src="https://www.freecodecamp.org/news/content/images/2021/11/ss-3-5.jpg" alt="ss-3-5" width="600" height="400" loading="lazy"></p>
<h2 id="heading-how-to-fix-the-no-audio-output-device-is-installed-error-by-uninstalling-your-audio-device">How to Fix the No Audio Output Device Is Installed Error by Uninstalling your Audio Device</h2>
<p><strong>Step 1</strong>: Press the <code>WIN</code> (Windows logo) key on your keyboard, then search for "device manager". Click on the first search result or press <code>ENTER</code>.
<img src="https://www.freecodecamp.org/news/content/images/2021/11/ss-1-6.jpg" alt="ss-1-6" width="600" height="400" loading="lazy"></p>
<p><strong>Step 2</strong>: Expand “Audio inputs and outputs” </p>
<p><strong>Step 3</strong>: Right-click on your audio device and select "Uninstall device"
<img src="https://www.freecodecamp.org/news/content/images/2021/11/ss-4-7.jpg" alt="ss-4-7" width="600" height="400" loading="lazy"></p>
<p><strong>Step 4</strong>: Restart your computer and a newer version of your audio device will be downloaded for you.</p>
<h2 id="heading-final-words">Final Words</h2>
<p>In this article, you learned about 2 ways you can fix the "no audio output device is installed" error.</p>
<p>Apart from updating your computer's audio driver, or the audio device itself, you can also fix the "no audio output device is installed error" by updating Windows 10.</p>
<p>In addition, if you're using an external audio device, you should make sure it is correctly connected to your computer – if it isn't, this improper connection can mess with an external audio device.</p>
<p>Thank you for reading. If you find this article useful, please share it with your friends and family.</p>
 ]]>
                </content:encoded>
            </item>
        
            <item>
                <title>
                    <![CDATA[ Wav File Format – How to Open a Wav and Convert Wavs to MP3 ]]>
                </title>
                <description>
                    <![CDATA[ By Vaibhav Kandwal Audio is a very important part of any digital media. Since audio or sound is a wave (a literal, continuous wave) it is analog in nature. This means that it cannot be stored and processed by the computer directly, as a computer only... ]]>
                </description>
                <link>https://www.freecodecamp.org/news/wav-file-format-how-to-open-a-wav-and-convert-wavs-to-mp3/</link>
                <guid isPermaLink="false">66d45ddbc7632f8bfbf1e3ef</guid>
                
                    <category>
                        <![CDATA[ audio ]]>
                    </category>
                
                    <category>
                        <![CDATA[ how-to ]]>
                    </category>
                
                <dc:creator>
                    <![CDATA[ freeCodeCamp ]]>
                </dc:creator>
                <pubDate>Tue, 30 Mar 2021 18:46:16 +0000</pubDate>
                <media:content url="https://www.freecodecamp.org/news/content/images/2020/10/Untitled-design.jpg" medium="image" />
                <content:encoded>
                    <![CDATA[ <p>By Vaibhav Kandwal</p>
<p>Audio is a very important part of any digital media. Since audio or sound is a wave (a literal, continuous wave) it is analog in nature. This means that it cannot be stored and processed by the computer directly, as a computer only understands digital signals. </p>
<p>So naturally, engineers found a way to convert these analog signals into digital signals. They've also developed a method to save them in digital format. I'm talking about audio codecs.</p>
<p>Audio codecs, in simpler terms, are just encoders and decoders (<em>co</em> - coders, <em>dec</em> - decoders, or <em>co</em> - compressor, <em>dec</em> - decompressor). They can be implemented as hardware codecs, which encode analog audio to digital audio and vice versa. </p>
<p>An example of such codecs are sound cards in your computer, which have built-in analog-to-digital (ADC) and digital-to-analog (DAC) circuits.</p>
<p> The software counterpart is an algorithm that compresses and decompresses digital audio. Some examples of such codecs are MP3, FLAC, WAV, and AAC.  </p>
<p>Today, we'll be looking at software encoders and we'll focus our attention on the WAV format.</p>
<h2 id="heading-what-compression-means-in-audio-codecs">What compression means in audio codecs</h2>
<p>Audio compression algorithms are divided into two categories: <em>Lossless</em> and <em>Lossy</em>. So what's the difference between these two?</p>
<h3 id="heading-lossless-algorithms">Lossless algorithms</h3>
<p>These algorithms may perform some kind of compression on the audio, but it doesn't remove any audio data, thus nothing is lost. The tradeoff is that you have very large file sizes.</p>
<h3 id="heading-lossy-algorithms">Lossy algorithms</h3>
<p>In this type of compression, some part of audio fidelity, deemed inaudible/indistinguishable to human ears, is removed or has its fidelity (or accuracy or loudness) reduced. Compression is also performed after this step. <a target="_blank" href="https://en.wikipedia.org/wiki/Data_compression#Lossy_audio_compression">You can read more about it here</a>.</p>
<p>Unless you have a very good HI-FI sound system that can reproduce those extra fidelity sounds, you should go for lossy. It will save space and remove extra data that you might never even hear.</p>
<p>Now that you understand audio compression, let's move on to the main topic of this article.</p>
<h2 id="heading-what-is-wav">What is WAV?</h2>
<p>WAV (or WAVE) stands for Waveform Audio File Format. It was developed by IBM and Microsoft. The file extension is <code>.wav</code> or <code>.wave</code>. </p>
<p>The WAV format is widely used where you would want uncompressed audio. For example, sound engineers use it a lot – and due to its lossless form, it can be used for early production samples. </p>
<p>The large file size means that it's usually not suitable for transmission over the internet. Other formats like MP3 and AAC offer much smaller file sizes, as they are lossy.</p>
<h3 id="heading-limitations-of-the-wav-format">Limitations of the WAV format</h3>
<p>WAV files are limited to 4GB in size as they have a 32bit file size header. W64 is a successor which has a 64bit header, allowing even larger file sizes.  </p>
<p>Apart from that, since the audio is uncompressed, its file size is large. So if the audio is stored on a slow disk drive, it can cause buffering issues (as audio needs to be loaded from disk to RAM before the music player can decode and play the file). </p>
<p>This might not be an issue, however, on modern computers, as they have much faster disks drives.</p>
<h2 id="heading-how-to-open-a-wav-file">How to open a WAV file</h2>
<p>Since WAV is quite a popular format, almost all devices today support it using built-in media players. </p>
<ul>
<li>On Windows, the Windows Media Player is capable of playing WAV files.</li>
<li>On MacOS, iTunes or QuickTime can play WAV files.</li>
<li>On Linux, your basic ALSA system can play these files.</li>
</ul>
<p>Other solutions also exist which can help you play WAV files, such as the VLC media player or any other music players.</p>
<h3 id="heading-how-to-play-a-wav-file-on-windows-using-windows-media-player">How to Play a WAV file on Windows using Windows Media Player</h3>
<ol>
<li>Open Windows Media Player using Search</li>
</ol>
<p><img src="https://www.freecodecamp.org/news/content/images/2021/03/image-140.png" alt="Image" width="600" height="400" loading="lazy">
<em>step 1: windows search</em></p>
<ol start="2">
<li>Drag and drop your WAV audio file from explorer to Windows Media Player</li>
</ol>
<p><img src="https://www.freecodecamp.org/news/content/images/2021/03/fcc.gif" alt="Image" width="600" height="400" loading="lazy">
<em>step 2: drag and drop your audio</em></p>
<ol start="3">
<li>Click Play!</li>
</ol>
<p><img src="https://www.freecodecamp.org/news/content/images/2021/03/Untitled-1.jpg" alt="Image" width="600" height="400" loading="lazy">
<em>step 3: click play</em></p>
<h3 id="heading-how-to-play-a-wav-file-on-any-os-with-vlc-media-player">How to Play a WAV file on any OS with VLC Media Player</h3>
<p>VLC media player is an open-source media player that can play most of your file formats without needing to download external codecs. It is cross platform, too, so you'll get the same features and interface across all operating systems, whether you use Linux, Windows, or MacOS (also supports Android and iOS). </p>
<p>Here are the steps to play an WAV file with VLC on your PC.</p>
<ol>
<li>Download VLC from the official website <a target="_blank" href="https://www.videolan.org/">here</a></li>
<li>Open VLC</li>
<li>Click on Media, then select Open File</li>
</ol>
<p><img src="https://www.freecodecamp.org/news/content/images/2021/03/Untitled-2-1.jpg" alt="Image" width="600" height="400" loading="lazy">
<em>step 3: click on 'media', then click 'open file'</em></p>
<ol start="4">
<li>In the file picker dialog, choose your audio, click <code>Open</code> and the WAV file should start playing automatically!</li>
</ol>
<p><img src="https://www.freecodecamp.org/news/content/images/2021/03/Untitled-3.jpg" alt="Image" width="600" height="400" loading="lazy">
<em>step 4: select your file and click 'open'</em></p>
<p> Or if you prefer a GIF:</p>
<p><img src="https://www.freecodecamp.org/news/content/images/2021/03/fcc2.gif" alt="Image" width="600" height="400" loading="lazy">
<em>Step 1-4 for VLC :D</em></p>
<h2 id="heading-how-to-convert-wav-files-to-mp3">How to convert WAV file<strong>s</strong> to MP3</h2>
<p>Depending on the end device, you might want to convert WAV to other formats, for example to save disk space or if you're listening on a low end device.</p>
<p>MP3 is quite a good format for storing audio. It gives you 50-75% smaller file sizes, while retaining almost the same listening experience. Let's see how you can convert these files.</p>
<h3 id="heading-online-convertors">Online Convertors</h3>
<p>If you only convert audio on rare occasions, I would suggest that you to use an online convertor such as <a target="_blank" href="https://cloudconvert.com/wav-converter">CloudConvert</a>. And Google is your friend here. </p>
<p>Do keep in mind that these online convertors can store your files for some time (even forever), so read their policy if you want to convert something mission critical.</p>
<h3 id="heading-offline-convertors">Offline Convertors</h3>
<p>Another option is to convert these files locally. This will help you if you want to convert frequently or have something that you don't want to expose to the internet.  </p>
<p>There are two ways to do this:</p>
<p><strong><a target="_blank" href="https://www.audacityteam.org/">Audacity</a></strong>: Audacity is a free, open-source cross-platform application that allows you to edit and convert audio. </p>
<p>The only con is you have to do some initial setup first (installing LAME). Also you have to convert files one by one. </p>
<p><strong><a target="_blank" href="https://ffmpeg.org/">FFmpeg</a></strong>: FFmpeg is a cross-platform command line tool to convert audio and video files. You can run the following command to convert a wav file to mp3:<br><code>ffmpeg -i input.wav output.mp3</code></p>
<p>You can dig around FFmpeg's docs to figure out how to customize some parameters and pass a list of files for batch conversions.</p>
<p>The main con is that FFmpeg is not user friendly. You need to know how to use command line tools. Though stuff like a FFmpeg GUI exists, it is out of the scope of our discussion here.</p>
<h3 id="heading-thanks-for-reading">Thanks for reading!</h3>
<p>I hope this article gave you some insights into audio compression, as well as how to play around with and convert WAV files. </p>
<p>For any questions or comments, you can find me on Twitter <a target="_blank" href="https://twitter.com/vaibhav_kandwal">@vaibhav_kandwal</a>. Thank you for reading.</p>
 ]]>
                </content:encoded>
            </item>
        
            <item>
                <title>
                    <![CDATA[ How to embed video and audio in your HTML ]]>
                </title>
                <description>
                    <![CDATA[ By Abhishek Jakhar HTML allows us to create standards-based video and audio players that don’t require the use of any plugins. Adding video and audio to a webpage is almost as easy as adding an image or formatting some text. There are two different w... ]]>
                </description>
                <link>https://www.freecodecamp.org/news/video-audio-in-html-a-short-guide-69f721878b47/</link>
                <guid isPermaLink="false">66c3648909a9333511bcdb31</guid>
                
                    <category>
                        <![CDATA[ audio ]]>
                    </category>
                
                    <category>
                        <![CDATA[ HTML ]]>
                    </category>
                
                    <category>
                        <![CDATA[ General Programming ]]>
                    </category>
                
                    <category>
                        <![CDATA[ tech  ]]>
                    </category>
                
                    <category>
                        <![CDATA[ Web Development ]]>
                    </category>
                
                <dc:creator>
                    <![CDATA[ freeCodeCamp ]]>
                </dc:creator>
                <pubDate>Fri, 23 Nov 2018 16:35:11 +0000</pubDate>
                <media:content url="https://cdn-media-1.freecodecamp.org/images/1*jB3XGWVtrr8qOl21gCOAmQ.gif" medium="image" />
                <content:encoded>
                    <![CDATA[ <p>By Abhishek Jakhar</p>
<p>HTML allows us to create standards-based video and audio players that don’t require the use of any plugins. Adding video and audio to a webpage is almost as easy as adding an image or formatting some text.</p>
<p>There are two different ways to include video elements. We will be discussing both of them below.</p>
<h4 id="heading-video-element">Video Element</h4>
<p>The <code>&lt;vid</code>eo&gt; element allows us to embed video files into an HTML, very similar to the way images are embedded.</p>
<p>Attributes we can include are:</p>
<ul>
<li><code>src</code> This attribute stands for the source, which is very similar to the src attribute used in the image element. We will add the link to a video file in the src attribute.</li>
<li><code>type</code> This is going to be video/mp4 because .mp4 is the format of the video we are using. We can also use different video formats like .ogg or .webm, then the value of type attribute will change to video/ogg or video/WebM respectively.</li>
</ul>
<blockquote>
<p><strong>Note:</strong> some common video formats are WebM, Ogg, MP4.</p>
</blockquote>
<p><img src="https://cdn-media-1.freecodecamp.org/images/1*4epxHpB0Z94ZaNq64bL9WA.png" alt="Image" width="800" height="500" loading="lazy">
<em>&lt;video&gt; in Webpage</em></p>
<p>We now have this video on our page. But there’s a problem. This video isn’t playing automatically and there are also no controls to start the video.<br>We will have to add controls manually by using the <code>controls</code> attribute to our video element.</p>
<p>This attribute doesn’t take any value, because it’s a boolean attribute. That means it can either be true or false.</p>
<p>Now, by having the <code>controls</code> attribute in our video element, it means that it’s true and it will display playback controls.</p>
<p><img src="https://cdn-media-1.freecodecamp.org/images/1*FKJojPyvDky1kM3gK5Z7KA.png" alt="Image" width="800" height="500" loading="lazy">
<em>&lt;video&gt; + Controls</em></p>
<p>Now, if we remove the controls we could also make the video autoplay, by using the autoplay attribute. This is also a boolean attribute.</p>
<p><img src="https://cdn-media-1.freecodecamp.org/images/1*TOOc_dxlcW6q7Cr3AUbxJQ.gif" alt="Image" width="640" height="360" loading="lazy">
<em>&lt;video&gt; element + autoplay attribute (without controls attribute)</em></p>
<p>Now, as you can see the video is playing by itself, and there’s no control. So, we didn’t actually start the video, but we also can’t stop the video.</p>
<p>We can also have controls and autoplay together.</p>
<p><img src="https://cdn-media-1.freecodecamp.org/images/1*jB3XGWVtrr8qOl21gCOAmQ.gif" alt="Image" width="640" height="360" loading="lazy">
<em>&lt;video&gt; element with autoplay and controls attribute</em></p>
<p>You can provide different attributes to the video element, depending on the requirement.</p>
<p>I mentioned above that there are two different ways to add the video element. Let’s try the other way.</p>
<h4 id="heading-source-element">Source Element</h4>
<p>Earlier we used a video element with self-closing tag, but here we will close the video element. So we have an opening and closing tag now.</p>
<p>We will also remove the type and source attributes from the video element and paste it into another element.</p>
<pre><code>&lt;video&gt;  <span class="xml"><span class="hljs-tag">&lt;<span class="hljs-name">source</span> <span class="hljs-attr">src</span>=<span class="hljs-string">"http://commondatastorage.googleapis.com/gtv-videos-bucket/sample/ForBiggerEscapes.mp4"</span> <span class="hljs-attr">type</span>=<span class="hljs-string">"video/mp4"</span>&gt;</span><span class="hljs-tag">&lt;/<span class="hljs-name">video</span>&gt;</span></span>
</code></pre><p>We just moved the attributes over to the source element.</p>
<p>Now, why would we want to do that?</p>
<p>Well, in most cases, with the video we will have multiple sources because we need to provide different file types depending on which browser is viewing your video because different browsers support different file types.</p>
<p><img src="https://cdn-media-1.freecodecamp.org/images/1*pHYI6GbxxHUL5A_FDTdK1A.png" alt="Image" width="800" height="702" loading="lazy"></p>
<p>The video will look exactly the same. But now we have broader browser support.</p>
<p><img src="https://cdn-media-1.freecodecamp.org/images/1*P4pGSwzIVaFxWtT6tenhsA.png" alt="Image" width="800" height="497" loading="lazy">
<em>Video with broader browser support (No Attributes)</em></p>
<p>Now, if we want to add attributes like <code>controls</code>, <code>autoplay</code>, <code>loop</code> etc, we will add it to the <code>&lt;vid</code>eo&gt; element.</p>
<p><img src="https://cdn-media-1.freecodecamp.org/images/1*Ff1qRhcSQfqvHgcjbSrcsg.gif" alt="Image" width="640" height="360" loading="lazy">
<em>Video with broader browser support and other attributes</em></p>
<h4 id="heading-audio-element">Audio Element</h4>
<p>The <code>&lt;aud</code>io&gt; element is very similar to the video element. However, the only major difference is that there are no visuals.</p>
<p>We can use the audio element to play an audio file on our web page — such as an mp3 file.</p>
<p>Now, just like the video tag, there are two different ways to do this.</p>
<ul>
<li>Use a single tag representing the entire element.</li>
<li>Opening and closing tag with the child elements in between.</li>
</ul>
<p>Now, we’ll have an opening and closing audio tag, and then we will add the source element in between.</p>
<p>The folder structure might look like this:</p>
<pre><code>|-- project    |-- audio      |-- sample.mp3      |-- sample.ogg    |-- css      |-- main.css      |-- normalize.css    index.html
</code></pre><p>There is no controls attribute given to the <code>&lt;aud</code>io&gt; element in the example above, s<code>o the &amp;</code>lt;audio&gt; element won’t show up in the HTML document.</p>
<p>Now, you can see that there are only a few key differences here. The value in <code>type</code> attribute is changed from “video/mp4” to “audio/mp3”. In the <code>src</code> attribute, we’ve changed from a video file with .mp4 extension to an audio file with .mp3 extension.</p>
<p>Now, just like the video element, we won’t actually be able to stop or start the audio without any controls. So, we will be adding the <code>controls</code> attribute to the audio element.</p>
<p><img src="https://cdn-media-1.freecodecamp.org/images/1*_d_AaBpz1QWH8csBB_-m8w.gif" alt="Image" width="640" height="360" loading="lazy">
<em>Audio element(&lt;audio&gt;&lt;/audio&gt;) with multiple sources for broader browser support</em></p>
<p>You can also add other attributes in the <code>&lt;aud</code>io&gt; element like autoplay, loop etc.</p>
<p>We have covered the essentials of audio and video elements in HTML.</p>
<p>You can learn more about audio and video in the links given below:</p>
<ul>
<li><a target="_blank" href="https://developer.mozilla.org/en-US/docs/Web/HTML/Element/video">MDN Web docs — Video</a></li>
<li><a target="_blank" href="https://developer.mozilla.org/en-US/docs/Web/HTML/Element/audio">MDN Web docs — Audio</a></li>
</ul>
<p>I hope you’ve found this post informative and helpful. I would love to hear your feedback!</p>
<p>Thank you for reading!</p>
 ]]>
                </content:encoded>
            </item>
        
            <item>
                <title>
                    <![CDATA[ Bang on A (Virtual) Can: A Primer on A-Frame Audio ]]>
                </title>
                <description>
                    <![CDATA[ By Berrak Nil A-Frame is a web framework for building virtual reality experiences. Ever since its introduction in late 2015, it quickly became a favorite among artists and creators of all backgrounds who want to experiment with WebXR. I am a creative... ]]>
                </description>
                <link>https://www.freecodecamp.org/news/a-primer-on-a-frame-audio-52dd56e54876/</link>
                <guid isPermaLink="false">66c342fa93db2451bd4413ee</guid>
                
                    <category>
                        <![CDATA[ A-Frame ]]>
                    </category>
                
                    <category>
                        <![CDATA[ AR ]]>
                    </category>
                
                    <category>
                        <![CDATA[ audio ]]>
                    </category>
                
                    <category>
                        <![CDATA[ tech  ]]>
                    </category>
                
                    <category>
                        <![CDATA[ vr ]]>
                    </category>
                
                <dc:creator>
                    <![CDATA[ freeCodeCamp ]]>
                </dc:creator>
                <pubDate>Wed, 07 Nov 2018 18:34:19 +0000</pubDate>
                <media:content url="https://cdn-media-1.freecodecamp.org/images/1*XnHzLrB2S17DUeetCPElXg.png" medium="image" />
                <content:encoded>
                    <![CDATA[ <p>By Berrak Nil</p>
<p><a target="_blank" href="https://aframe.io/">A-Frame</a> is a web framework for building virtual reality experiences. Ever since its introduction in late 2015, it quickly became a favorite among artists and creators of all backgrounds who want to experiment with <a target="_blank" href="https://github.com/immersive-web/webxr/blob/master/explainer.md">WebXR</a>.</p>
<p>I am a creative coder with a background in audio. Diving into the sonic possibilities of this new platform was a very exciting and rewarding journey. Most of my A-Frame experience was confined to standard desktop and smartphone environments and not VR. I am sharing my findings. I want to create a sort of an unofficial manual on how to implement, use and create audio in A-Frame. In this first part, we will take a look at how to use A-Frame’s out of the box audio capabilities.</p>
<p><strong>Prerequisites</strong></p>
<p>This write-up assumes you have some experience with A-Frame. You don’t have to be an expert on it but knowing how the basics work will make following this tutorial easier. If you haven’t had a chance to check it out yet, you can start <a target="_blank" href="https://aframe.io/docs/0.8.0/introduction/">here</a>.</p>
<h3 id="heading-a-frame-sound-component">A-Frame Sound Component</h3>
<p>A-Frame is a framework based on <a target="_blank" href="https://threejs.org/">Three.js.</a> The sound component it provides is a <a target="_blank" href="https://github.com/aframevr/aframe/blob/v0.8.0/src/components/sound.js">wrapper</a> around the Three.js <a target="_blank" href="https://threejs.org/docs/#api/en/audio/PositionalAudio">positional audio component</a> (or non-positional depending on what we choose, but more on that later), which uses the <a target="_blank" href="https://developer.mozilla.org/en-US/docs/Web/API/Web_Audio_API">Web Audio API</a>.</p>
<p>This means we get things like positional audio, volume control and audio playback out of the box, as soon as we use an A-Frame sound component.</p>
<p><img src="https://cdn-media-1.freecodecamp.org/images/1*ZDC3HgFpM-xU_3cfgP2vTQ.png" alt="Image" width="646" height="412" loading="lazy">
_A-Frame sound component properties, screenshot taken from [A-Frame Docs](https://aframe.io/docs/0.8.0/components/sound.html" rel="noopener" target="<em>blank" title=")</em></p>
<p>But how do we add this component to our scenes and provide audio feedback to user interactions and/or create a <a target="_blank" href="https://en.wikipedia.org/wiki/Soundscape">soundscape</a>?</p>
<p>To demonstrate this I created an A-Frame project from scratch. It is based on user interactions with a desktop computer. (i.e. to use with a mouse and a keyboard). The principles should transfer to other types of controls for the most part.</p>
<p>You have the option to either start with a fresh copy of the project [<a target="_blank" href="https://glitch.com/~a-frame-audio-tutorial-starter">Glitch</a>][<a target="_blank" href="https://github.com/berraknil/a-frame-audio-tutorial/tree/starter">GitHub</a>] — with no sounds attached to it — and follow along while implementing the provided sounds yourself. Or you can check out the finished version [<a target="_blank" href="https://glitch.com/~a-frame-audio-tutorial-complete">Glitch</a>][<a target="_blank" href="https://github.com/berraknil/a-frame-audio-tutorial/tree/master">GitHub</a>] and follow along by reading the code.</p>
<h3 id="heading-the-kitchen">The Kitchen</h3>
<p><img src="https://cdn-media-1.freecodecamp.org/images/1*XnHzLrB2S17DUeetCPElXg.png" alt="Image" width="800" height="415" loading="lazy"></p>
<p>Before we start the audio implementation process, let’s take a look at our scene. We have several 3D models here (courtesy of <a target="_blank" href="https://poly.google.com/">Google Poly</a>). Our <a target="_blank" href="https://poly.google.com/view/38PMRiku8qj">kitchen</a> is a complete model by itself. Items like the <a target="_blank" href="https://poly.google.com/view/6kN4sv3u9RM">espresso machine</a>, <a target="_blank" href="https://poly.google.com/view/9H9k1nAXSuH">radio</a>, the <a target="_blank" href="https://poly.google.com/view/bYF5rVRy_kp">frying pan</a> and <a target="_blank" href="https://poly.google.com/view/dccGDIUzA2y">the egg</a> on top of it are separate models that are added to the scene on top of the kitchen.</p>
<p>Now let’s see the methods we can use to add sounds to this scene.</p>
<h3 id="heading-user-interaction-sounds"><strong>User Interaction Sounds</strong></h3>
<p>User interaction sounds are usually one-shot sounds. This means they are short samples that play once when triggered. They are not continuous or looped like environment audio or music. Which means we need to find a way to put an audio file on our scene and then trigger it when our user interacts with that object. (e.g. clicks the mouse button, hovers over it etc.).</p>
<p>To trigger a sound on user interaction, we can:</p>
<ol>
<li>Put a sound on a model</li>
<li>Put a sound on a <a target="_blank" href="https://aframe.io/docs/0.8.0/introduction/html-and-primitives.html">primitive</a>, like a box geometry</li>
<li>Put a primitive on an  component</li>
</ol>
<p>So let’s go through our options one by one and see the use cases for each one.</p>
<h4 id="heading-putting-a-sound-on-a-model">Putting a Sound on a Model</h4>
<p><img src="https://cdn-media-1.freecodecamp.org/images/1*dBWexbENGrlzW7Dj4jvS0Q.png" alt="Image" width="800" height="412" loading="lazy"></p>
<p>Who doesn’t want to start the day with a fresh cup of espresso? (If that’s not you, feel free to grab a tea model <a target="_blank" href="https://poly.google.com/search/tea">from here</a> and go with that instead.)</p>
<p>I would say half of the satisfaction of having that caffeine boost is the sound our beloved espresso machine makes. To make sure our scene provides that aural feedback, we implement a sound that will respond to user interaction.</p>
<p>First let’s take a look at how our espresso machine works under the hood</p>
<p>We start by loading our model with the A-Frame loader. Then we set the position, rotation and scale of the model within the scene, by using the named properties.</p>
<p>Our model is referenced by the id “#coffeeMaker”, instead of the relative path of the file. All the models for this project is registered in the <a target="_blank" href="https://aframe.io/docs/0.8.0/core/asset-management-system.html">Asset Management System</a> beforehand.</p>
<p>Easiest way to add a sound to our espresso machine is by adding the sound component to it by using it as an HTML attribute on the object.</p>
<p>We reference our sound by using the asset management system again, and setting the volume of it to 1. This means it will be heard at 100%. Now for the most important part, we set the “on” property of the sound to the “click” value. This means when a user clicks on this object, the sound will be triggered.</p>
<p>We don’t use the autoplay or loop properties here as this is a one-shot that depends on user interaction. We leave the positional property as “on” by default. Otherwise we would hear the sound at the same volume level at all times, no matter how distant we are from the object. Sound wouldn’t have any panning and would always play exactly where we are, instead of to the left, right or behind us depending on our position relative to the object.</p>
<p><strong>NOTE: Positional sounds like these should be rendered mono to make the spatial cognition of the sound easier, while background music or audio ambiances that are not positional preferably should be rendered stereo.</strong></p>
<h4 id="heading-time-to-crack-a-few-eggs">Time to Crack a Few Eggs</h4>
<p>Now it’s your turn, go ahead and implement the provided frying egg sound to the egg model, which is placed on top of the frying pan. For a challenge, you can also try to use a few additional <a target="_blank" href="https://aframe.io/docs/0.8.0/components/sound.html#properties">sound properties</a> of your choosing. Go around the scene after putting the egg sound in place and test your implementation to see if it sounds realistic (or unrealistic if that’s what you are going for!)</p>
<p>Did you do it?</p>
<p>Great!</p>
<p>Your code for the egg model now should look approximately like this (with your desired properties and values)</p>
<h4 id="heading-putting-a-sound-on-a-geometry">Putting a Sound on a Geometry</h4>
<p>So we can add our sounds to the models by simply adding them as an HTML attribute. What if we want to put a sound to an object that is already part of a bigger model and not a separate entity by itself?</p>
<p>Let’s say we want to add a sound to the red bottle on the right, which should make it sound like we are picking it up when we click on it.</p>
<p>If we put the sound on the kitchen model like we did with the smaller models, it would mean that the sound would trigger no matter where we clicked on the model itself, instead of only on the bottle like we want.</p>
<p>We can solve this problem by using a geometry which we can click on, aligning it to where the bottle is, and adding the sound property to it.</p>
<h4 id="heading-a-frame-inspector-to-the-rescue">A-Frame Inspector to the Rescue</h4>
<p>For the next steps I highly recommend that you start creating and positioning objects inside your scene right in the browser by using the <a target="_blank" href="https://aframe.io/docs/0.8.0/introduction/visual-inspector-and-dev-tools.html">A-Frame Inspector</a>. You can then copy and use this code instead of guesswork as to where your object should be positioned.</p>
<p><img src="https://cdn-media-1.freecodecamp.org/images/1*nlhq4AvrGji6G1uNNGI9Ag.gif" alt="Image" width="800" height="450" loading="lazy">
<em>Open the A-Frame Inspector by using the <code>&amp;lt;ctrl&amp;gt; + &amp;lt;alt&amp;gt; +</code> i shortcut.</em></p>
<p>Let’s start by creating a geometry primitive on our scene, a cylinder should work well given our bottle’s shape.</p>
<p>Now scale and position this cylinder by using the A-Frame inspector. It can cover our bottle and respond to user interaction on the correct spot.</p>
<p><strong>NOTE: If you add this geometry inside the model, it would enable you to move or rotate the parent model and preserve the correct position of everything inside.</strong></p>
<p><img src="https://cdn-media-1.freecodecamp.org/images/1*AP6J0D-ZQelnlc9alwD1lA.gif" alt="Image" width="800" height="450" loading="lazy">
<em>Scale and position the cylinder inside the scene by using the controls top right</em></p>
<p>You can paste the code you copied from the inspector and just move the scale, position and rotation values inside the .</p>
<p><img src="https://cdn-media-1.freecodecamp.org/images/1*NZ8RVt_7L_gBtpz3S-ofig.png" alt="Image" width="800" height="417" loading="lazy">
<em>Copy the entity and its properties by using the button on the top right</em></p>
<p>Make sure to bring down the opacity of the material to 0, so our bottle can stay visible inside the cylinder geometry. As a final step, add the provided bottle sound to the object, like you did before with the espresso machine and the egg.</p>
<p>Now your cylinder code should look like this…</p>
<p>…and respond to user interaction correctly.</p>
<h4 id="heading-using-an-component">Using an  Component</h4>
<p><a target="_blank" href="https://aframe.io/docs/0.8.0/primitives/a-sound.html">&lt;a-sou</a>nd&gt; is a primitive wrapper around the A-Frame sound component and allows us to use audio without attaching it to anything else on the scene. However if we want to trigger this sound on interaction, we still need a type of geometry which will allow us to interact with this object.</p>
<p>We can put this geometry on anywhere else on the scene. (think of a user interface menu button which will trigger a non-positional sound). Or we can put it directly on to  and then position it on top of the object we want to hear audio feedback from.</p>
<p>Which means to add a sound to our toaster, we can do the reverse of what we just did and create an  component and attach a clickable geometry to it.</p>
<h3 id="heading-environment-audio">Environment Audio</h3>
<p>We saw a couple of the ways we can use to implement sounds that respond to user interactions. But what about sounds that don’t require the user to do anything to play? These are sounds that go through the scene from beginning to end, to create atmosphere, mood, a sense of place?</p>
<p>If we take a look at our kitchen, we can see that there is a big fridge to the left, which is dead silent right now. Not to mention, whether we are consciously aware of it or not, almost all the environments we occupy (unless they are an <a target="_blank" href="https://en.wikipedia.org/wiki/Anechoic_chamber">anechoic chamber</a>) have something called a <a target="_blank" href="https://en.wikipedia.org/wiki/Presence_(sound_recording)">presence or a room tone</a>.</p>
<p>Electric hums, machinery, air conditioning units and alike create the room tone with or without us adding on top of it acoustically. So, to reflect that, our choice should be an ongoing. In this case a looping sound which is present throughout our scene.</p>
<p><strong>NOTE: .mp3 format by its nature <a target="_blank" href="https://sound.stackexchange.com/questions/25846/is-it-possible-to-loop-mp3-without-gaps">does not loop seamlessly</a> in most platforms, there’s always a very short gap between loops which breaks the audio consistency, and therefore for looping sounds other formats like .wav or .ogg should be used.</strong></p>
<h4 id="heading-adding-room-tone">Adding Room Tone</h4>
<p>Now let’s add the provided room tone to our scene. We have several options depending on what type of A-Frame project we are working on. If we have only one room-as is the case here-we can put our room tone directly to our . Or if we have several rooms in our scene with different characteristics, we can put the room tone in to a plane geometry which could act as the ground. Or in an  object which we can put in the center of the room. In all cases we have to make sure our room tone is not positional, as opposed to all the other sounds we used previously on this project.</p>
<p>Given that we are only working with a single room/environment in here, we can put our room tone directly into the scene itself. We set the positional property to false, to make sure sound is heard equally throughout the room.</p>
<p>We also set the autoplay property to true since we don’t need user interaction to hear the room tone.</p>
<p><strong>NOTE: Audio autoplay in most browsers either already require or will require user interaction to start. Which means to automatically play sounds like background music or ambient audio you need to use a menu screen or a mute/unmute button or something alike to enable audio autoplay.</strong></p>
<h3 id="heading-music">Music</h3>
<p>For the final piece of our audio puzzle, let’s add some music to our scene. We can either choose to add this as diegetic music that’s coming from the radio in our scene, or as extra-diegetic music which is non-existent in the virtual world we created, but specifically for the user/viewer/player who is experiencing and controlling this virtual world.</p>
<p>To do the latter, we would need to put our music either onto our camera () or to our first-person character (if that is an option) and make sure the audio is non-positional. In this scene I chose to go with the former and put the music to the radio model as a source inside the virtual world.</p>
<p>We can add the provided music track (composed by yours truly) to our radio model just like we added all the sounds up to this point.</p>
<h4 id="heading-playback-control">Playback Control</h4>
<p>Our music plays whenever we click on the radio, but what if we want to pause it and then play it again?</p>
<p>To have this functionality we need to <a target="_blank" href="https://aframe.io/docs/0.8.0/introduction/writing-a-component.html">write a custom A-Frame component.</a> Then add that component as an HTML attribute to any object we want to add that functionality to, just like we added the “sound” component to our models before.</p>
<p>You can either write your A-Frame components inline, or write it in an external JavaScript file and then link to it from the html, like I did for this project.</p>
<p>Now we have an audio-toggle component that enables us to play and pause the sound we are hearing. It also allows us to change the autoplay ability via the “playing” property.</p>
<p>And that’s it! <a target="_blank" href="https://a-frame-audio-tutorial-complete.glitch.me/">We have an A-Frame scene with music, environment sounds and interactive audio.</a></p>
<p>In the next part we will take a look at how to integrate <a target="_blank" href="https://tonejs.github.io/">Tone.js</a> to an A-Frame project and writing more custom components with advanced audio functionality.</p>
<p>Thanks for reading!</p>
 ]]>
                </content:encoded>
            </item>
        
            <item>
                <title>
                    <![CDATA[ How you can hear both “Yanny” and “Laurel” using the Web Audio API ]]>
                </title>
                <description>
                    <![CDATA[ By _haochuan Recently an audio clip asking listeners whether they hear the word “Yanny” or “Laurel” has been completely puzzling the world and pitting friend against friend in the online debate. The clip and the “Yanny or Laurel” poll were posted on ... ]]>
                </description>
                <link>https://www.freecodecamp.org/news/how-you-can-hear-both-yanny-and-laurel-using-the-web-audio-api-306051cfcede/</link>
                <guid isPermaLink="false">66c356bdd372f14b49bdcb87</guid>
                
                    <category>
                        <![CDATA[ audio ]]>
                    </category>
                
                    <category>
                        <![CDATA[ JavaScript ]]>
                    </category>
                
                    <category>
                        <![CDATA[ General Programming ]]>
                    </category>
                
                    <category>
                        <![CDATA[ technology ]]>
                    </category>
                
                    <category>
                        <![CDATA[ Web Development ]]>
                    </category>
                
                <dc:creator>
                    <![CDATA[ freeCodeCamp ]]>
                </dc:creator>
                <pubDate>Thu, 24 May 2018 16:11:04 +0000</pubDate>
                <media:content url="https://cdn-media-1.freecodecamp.org/images/1*mXeOkNmfZwMkcgUcv8_yhw.jpeg" medium="image" />
                <content:encoded>
                    <![CDATA[ <p>By _haochuan</p>
<p>Recently an audio clip asking listeners whether they hear the word “Yanny” or “Laurel” has been completely puzzling the world and pitting friend against friend in the online debate.</p>
<p>The clip and the “Yanny or Laurel” poll were posted on Instagram, Reddit, and other sites by high school students who said that it had been recorded from a vocabulary website playing through the speakers on a computer. Now hundreds of thousands of people are engaged in a debate over what they hear. It’s been driving people crazy and leading to passionate defenses on both side.</p>
<p>However, the magic behind this debate is quite simple. <strong>Different ears have different sensitive frequency zones for the same audio clip.</strong> Also, different speakers have different responses to different audio frequencies.</p>
<p>This tutorial will go through the details about how to use the Web Audio API and simple JavaScript to create to tool that will help you hear both “Yanny” <strong>and</strong> “Laurel.” Then you’ll be able to win any of those debates. :)</p>
<p>If you just want to try the tool, it is live <a target="_blank" href="https://haochuan.github.io/yanny-vs-laurel/static/">HERE</a>. Just open your browser, play the audio, and try to find the sweet spots for “Yanny” and “Laurel” while moving the frequency slider.</p>
<h3 id="heading-how-it-works">How it works</h3>
<p>Let’s talk about the key part first. In order to hear the different word, you need to somehow increase the volume for a specific frequency range which depends on your ears. Luckily the Web Audio API already has something ready for us: <code>BiquadFilterNode</code>.</p>
<p>There are different types of <code>[BiquadFilterNode](https://developer.mozilla.org/en-US/docs/Web/API/BiquadFilterNode)</code> you can use. For this case, we will just go with the <code>bandpass</code> filter.</p>
<blockquote>
<p><em>A bandpass filter is an electronic device or circuit that allows signals between two specific frequencies to pass, but that discriminates against signals at other frequencies. (<a target="_blank" href="https://whatis.techtarget.com/definition/bandpass-filter">source</a>)</em></p>
</blockquote>
<p>And for a bandpass filter, most of the time we just need to define the center frequency value we want to boost or cut, instead of the start and the end of the frequency range. We use a <code>Q</code> value to control the width of the frequency range. The larger the <code>Q</code> is, the narrower the frequency range will be. <a target="_blank" href="https://en.wikipedia.org/wiki/Q_factor">Check out Wikipedia</a> for more details.</p>
<p>That’s all the knowledge we need to know at this point. Now, let’s write the code.</p>
<h3 id="heading-web-audio-api-initialization">Web Audio API Initialization</h3>
<pre><code><span class="hljs-keyword">const</span> AudioContext = <span class="hljs-built_in">window</span>.AudioContext || <span class="hljs-built_in">window</span>.webkitAudioContext;
</code></pre><pre><code><span class="hljs-keyword">const</span> audioContext = <span class="hljs-keyword">new</span> AudioContext();
</code></pre><h4 id="heading-create-audio-nodes-along-with-setup-and-signal-chain">Create Audio Nodes along with setup and signal chain</h4>
<pre><code><span class="hljs-comment">// the audio tag in HTML, where holds the original audio clipconst audioTag = document.getElementById('audioTag');</span>
</code></pre><pre><code><span class="hljs-comment">// create audio source in web audio apiconst sourceNode =</span>
</code></pre><pre><code>audioContext.createMediaElementSource(audioTag);
</code></pre><pre><code><span class="hljs-keyword">const</span> filterNode = audioContext.createBiquadFilter();
</code></pre><pre><code>filterNode.type = <span class="hljs-string">'bandpass'</span>; <span class="hljs-comment">// bandpass filterfilterNode.frequency.value = 1000 // set the center frequency</span>
</code></pre><pre><code><span class="hljs-comment">// set the gain to the frequency rangefilterNode.gain.value = 100;</span>
</code></pre><pre><code><span class="hljs-comment">// set Q value, 5 will make a fair band width for this casefilterNode.Q.value = 5;</span>
</code></pre><pre><code><span class="hljs-comment">// connect nodessourceNode.connect(filterNode);filterNode.connect(gainNode);gainNode.connect(audioContext.destination);</span>
</code></pre><h4 id="heading-sample-html-file">Sample HTML file</h4>
<pre><code>&lt;!DOCTYPE html&gt;&lt;html lang="en"&gt;&lt;head&gt;  &lt;meta charset="UTF-8"&gt;  &lt;title&gt;Yanny vs Laurel Web Audio API&lt;/title&gt;&lt;/head&gt;&lt;body&gt;  &lt;div id="container"&gt;    &lt;audio id='audioTag' crossorigin="anonymous" src="yanny-laurel.wav" controls loop&gt;&lt;/audio&gt;    &lt;hr&gt;    &lt;input type="range" min="20" max="10000" value="20" step="1" class="slider" id="freqSlider"&gt;  &lt;/div&gt;  &lt;script src='script.js'&gt;&lt;/script&gt;&lt;/body&gt;&lt;/html&gt;
</code></pre><h4 id="heading-adding-frequency-slider-ui">Adding frequency slider UI</h4>
<p>To make it easier to adjust the center frequency of our bandpass filter, we should add a slider to control the value.</p>
<pre><code>&lt;!DOCTYPE html&gt;&lt;html lang="en"&gt;&lt;head&gt;  &lt;meta charset="UTF-8"&gt;  &lt;title&gt;Yanny vs Laurel Web Audio API&lt;/title&gt;&lt;/head&gt;&lt;body&gt;  &lt;div id="container"&gt;    &lt;audio id='audioTag' src="yanny-laurel.wav" controls loop&gt;&lt;/audio&gt;    &lt;hr&gt;    &lt;input type="range" min="50" max="4000" value="1000" step="1" class="slider" id="freqSlider"&gt;    &lt;br&gt;    &lt;p id="freqLabel" &gt;Frequency: 1000 Hz&lt;/p&gt;  &lt;/div&gt;  &lt;script&gt;;    // add event listener for slider to change frequency value    slider.addEventListener('input', e =&gt; {
</code></pre><pre><code>      filterNode.frequency.value = e.target.value;      label.innerHTML = <span class="hljs-string">`Frequency: <span class="hljs-subst">${e.target.value}</span>Hz`</span>;
</code></pre><pre><code>    }, <span class="hljs-literal">false</span>);  <span class="xml"><span class="hljs-tag">&lt;<span class="hljs-name">script</span> <span class="hljs-attr">src</span>=<span class="hljs-string">'script.js'</span>&gt;</span><span class="hljs-tag">&lt;/<span class="hljs-name">script</span>&gt;</span></span>&lt;;<span class="hljs-regexp">/body&gt;&lt;/</span>html&gt;
</code></pre><h3 id="heading-createmediaelementsource-bug-in-ios-safari">createMediaElementSource bug in iOS Safari</h3>
<p>I found that <code>createMediaElementSource</code> won't work in iOS Safari and Chrome. To solve this, you have to use <code>createBufferSource</code> to create an AudioBufferNode to store and play the audio instead of the HTML audio tag.<br>Please see <a target="_blank" href="https://github.com/haochuan/yanny-vs-laurel/blob/master/static/script.js">the code here</a> for more detail.</p>
<p>Now you made yourself a tool so you can hear both “Yanny” and “Laurel.” Just open your browser, play the audio, and try to find the sweet spot while moving the frequency slider.</p>
<p>If you want to just try the tool, it is live <a target="_blank" href="https://haochuan.github.io/yanny-vs-laurel/static/">HERE</a>.</p>
<p>I write code for audio and web, and play guitar on YouTube. If you want to see more stuff from me or know more about me, you can always find me on:</p>
<p>Website:<br><a target="_blank" href="https://haochuan.io/">https://haochuan.io/</a></p>
<p>GitHub:<br><a target="_blank" href="https://github.com/haochuan">https://github.com/haochuan</a></p>
<p>Medium:<br><a target="_blank" href="https://medium.com/@haochuan">https://medium.com/@haochuan</a></p>
<p>YouTube: <a target="_blank" href="https://www.youtube.com/channel/UCNESazgvF_NtDAOJrJMNw0g">https://www.youtube.com/channel/UCNESazgvF_NtDAOJrJMNw0g</a></p>
 ]]>
                </content:encoded>
            </item>
        
    </channel>
</rss>
