<?xml version="1.0" encoding="UTF-8"?>
<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom" xmlns:dc="http://purl.org/dc/elements/1.1/">
  <channel>
    <title>DEV Community: Apoorv Darshan</title>
    <description>The latest articles on DEV Community by Apoorv Darshan (@apoorvdarshan).</description>
    <link>https://dev.to/apoorvdarshan</link>
    <image>
      <url>https://media2.dev.to/dynamic/image/width=90,height=90,fit=cover,gravity=auto,format=auto/https:%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Fuser%2Fprofile_image%2F4176307%2F4ef4bd8b-5d70-478e-86b7-4e7b3bda4fbb.jpg</url>
      <title>DEV Community: Apoorv Darshan</title>
      <link>https://dev.to/apoorvdarshan</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/apoorvdarshan"/>
    <language>en</language>
    <item>
      <title>How I built Live Voice: one tool contract, six realtime voice providers</title>
      <dc:creator>Apoorv Darshan</dc:creator>
      <pubDate>Sat, 24 Oct 2026 13:46:57 +0000</pubDate>
      <link>https://dev.to/apoorvdarshan/how-i-built-live-voice-one-tool-contract-six-realtime-voice-providers-1bpc</link>
      <guid>https://dev.to/apoorvdarshan/how-i-built-live-voice-one-tool-contract-six-realtime-voice-providers-1bpc</guid>
      <description>&lt;p&gt;Fud AI 8.0 is out on iPhone and iPad (Android is in Google Play review) with Live Voice: you talk to the app, and it logs meals, scans barcodes, checks your macros and edits your diary while you watch it happen on screen.&lt;/p&gt;

&lt;p&gt;  &lt;iframe src="https://www.youtube.com/embed/WGytfnmvYnc" width="710" height="399"&gt;
  &lt;/iframe&gt;
&lt;/p&gt;

&lt;p&gt;Here's how it works under the hood. Fud AI is open source (MIT), so every file below is public.&lt;/p&gt;

&lt;h2&gt;
  
  
  One tool contract, three codebases
&lt;/h2&gt;

&lt;p&gt;The assistant can only act through tools. They live in one file, &lt;a href="https://github.com/apoorvdarshan/fud-ai/tree/main/shared/voice-assistant" rel="noopener noreferrer"&gt;&lt;code&gt;shared/voice-assistant/tools.json&lt;/code&gt;&lt;/a&gt;: 25 actions like &lt;code&gt;open_camera&lt;/code&gt;, &lt;code&gt;capture_photo&lt;/code&gt;, &lt;code&gt;scan_barcode&lt;/code&gt;, &lt;code&gt;log_food&lt;/code&gt;, &lt;code&gt;log_water&lt;/code&gt;, &lt;code&gt;start_fast&lt;/code&gt;, &lt;code&gt;log_workout&lt;/code&gt;, &lt;code&gt;update_setting&lt;/code&gt; and &lt;code&gt;end_conversation&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;The iOS app, the Android app and the Cloudflare Worker all follow that contract. Both apps bundle an identical copy, and a CI script fails the build if either copy drifts. I change the JSON first, then each platform.&lt;/p&gt;

&lt;p&gt;Every tool also declares its effect: &lt;code&gt;read&lt;/code&gt;, &lt;code&gt;navigate&lt;/code&gt;, &lt;code&gt;write&lt;/code&gt; or &lt;code&gt;destructive&lt;/code&gt;. The UI behaviour comes from that:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Writes show a card with &lt;strong&gt;Undo&lt;/strong&gt; for 8 seconds.&lt;/li&gt;
&lt;li&gt;Destructive actions always need confirmation.&lt;/li&gt;
&lt;li&gt;With "Confirm before saving" on (the default), a write without &lt;code&gt;user_confirmed: true&lt;/code&gt; changes nothing. It returns &lt;code&gt;confirmation_required&lt;/code&gt;, the app shows a Confirm/Cancel card, and the model has to ask you before calling again.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The model is also told never to invent numbers. Calories and macros come from the data tools, not from its memory.&lt;/p&gt;

&lt;h2&gt;
  
  
  Six realtime providers, one experience
&lt;/h2&gt;

&lt;p&gt;Live Voice runs on whichever provider you choose:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Provider&lt;/th&gt;
&lt;th&gt;Key&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Gemini Live&lt;/td&gt;
&lt;td&gt;Your own&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;OpenAI Realtime&lt;/td&gt;
&lt;td&gt;Your own&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;OpenAI GPT-Live&lt;/td&gt;
&lt;td&gt;Your own&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;xAI Grok Voice&lt;/td&gt;
&lt;td&gt;Your own&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Qwen Omni Realtime&lt;/td&gt;
&lt;td&gt;Your own&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Fud AI hosted (Gemini Live via my Worker)&lt;/td&gt;
&lt;td&gt;Plus/Pro plan&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;They differ more than you'd expect:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Audio:&lt;/strong&gt; Gemini, Qwen and hosted take 16 kHz PCM in and send 24 kHz out. OpenAI and xAI use 24 kHz both ways.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Tool calls:&lt;/strong&gt; Gemini sends &lt;code&gt;toolCall.functionCalls&lt;/code&gt; and wants &lt;code&gt;toolResponse&lt;/code&gt;. OpenAI, xAI and Qwen send &lt;code&gt;response.function_call_arguments.done&lt;/code&gt; and want a &lt;code&gt;function_call_output&lt;/code&gt; item followed by &lt;code&gt;response.create&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Vision:&lt;/strong&gt; Gemini, Qwen and hosted get one camera frame per second while the camera is open. OpenAI gets the latest frame with each turn. xAI and GPT-Live get none, so the model asks you to say when the food is in frame.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Each provider gets a thin adapter, and everything above it (tools, cards, confirmation, undo) is shared. Own keys go in headers (&lt;code&gt;x-goog-api-key&lt;/code&gt; or &lt;code&gt;Authorization: Bearer&lt;/code&gt;), never in URLs.&lt;/p&gt;

&lt;h2&gt;
  
  
  The hosted relay
&lt;/h2&gt;

&lt;p&gt;For people who don't want to manage an API key, the Plus/Pro plan connects to a WebSocket relay on Cloudflare Workers that forwards to Gemini Live. A relay like that could easily turn into a free general-purpose Gemini key, so it's strict:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Before the upgrade, the Worker checks the user id, rate limits, the RevenueCat plan and the remaining voice balance.&lt;/li&gt;
&lt;li&gt;The first message must be &lt;code&gt;setup&lt;/code&gt;. The Worker rebuilds it from an allow-list: the model is pinned server-side, tools are function declarations only, and instructions are size-limited.&lt;/li&gt;
&lt;li&gt;After that, only &lt;code&gt;realtimeInput&lt;/code&gt;, &lt;code&gt;clientContent&lt;/code&gt; and &lt;code&gt;toolResponse&lt;/code&gt; are accepted, each 1 MiB at most, with a bounded input rate.&lt;/li&gt;
&lt;li&gt;Billing is by time: 15-second blocks, each charged when it starts. Sessions end at 15 minutes.&lt;/li&gt;
&lt;li&gt;Nothing you say or show is stored. Logs carry structured codes and a hashed id prefix only.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  The orb
&lt;/h2&gt;

&lt;p&gt;The glowing orb is a Metal shader on iOS and an AGSL port on Android 13+, with a Canvas fallback on older versions. It's driven by the loudness of whoever is talking, so you can see when it's listening, thinking or speaking.&lt;/p&gt;

&lt;p&gt;You can start a session from the orb on Home, Siri, the Action Button or Control Center on iPhone, or a Quick Settings tile, shortcut or widget on Android.&lt;/p&gt;

&lt;h2&gt;
  
  
  Try it
&lt;/h2&gt;

&lt;p&gt;Fud AI is free on iOS and Android, and Live Voice reaches Android as soon as Google approves 8.0. Use your own AI key, or the optional Plus/Pro plan if you'd rather not. No account needed.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Website: &lt;a href="https://fud-ai.app" rel="noopener noreferrer"&gt;https://fud-ai.app&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;Source: &lt;a href="https://github.com/apoorvdarshan/fud-ai" rel="noopener noreferrer"&gt;https://github.com/apoorvdarshan/fud-ai&lt;/a&gt;
&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;I'd love feedback on the voice experience, and PRs are welcome.&lt;/p&gt;

</description>
      <category>showdev</category>
      <category>ai</category>
      <category>opensource</category>
      <category>mobile</category>
    </item>
    <item>
      <title>Five switches. Two app stores. How I built Fud AI’s release pipeline</title>
      <dc:creator>Apoorv Darshan</dc:creator>
      <pubDate>Sun, 11 Oct 2026 06:18:20 +0000</pubDate>
      <link>https://dev.to/apoorvdarshan/five-switches-two-app-stores-how-i-built-fud-ais-release-pipeline-4l5</link>
      <guid>https://dev.to/apoorvdarshan/five-switches-two-app-stores-how-i-built-fud-ais-release-pipeline-4l5</guid>
      <description>&lt;p&gt;Building the app was only half the release. I still had store listings, screenshots, notes, and subscriptions to deal with.&lt;/p&gt;

&lt;p&gt;For Fud AI, I moved those steps into GitHub Actions. Five switches decide what runs; a version tag starts the workflow.&lt;/p&gt;

&lt;h2&gt;
  
  
  The controls
&lt;/h2&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fpbs.twimg.com%2Fmedia%2FHTxvW31bkAACTHT%3Fformat%3Djpg%26name%3Dlarge" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fpbs.twimg.com%2Fmedia%2FHTxvW31bkAACTHT%3Fformat%3Djpg%26name%3Dlarge" alt="Five release controls for production rollout, review submission, listing text, screenshots, and release notes." width="1536" height="1024"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;The diagram shows the five repository variables. Listing updates and screenshots can run independently of production rollout or review submission.&lt;/p&gt;

&lt;p&gt;Unset means off. Enabled switches stay enabled for future releases. On iOS, listing updates include What’s New; Play release notes require a binary upload.&lt;/p&gt;

&lt;h2&gt;
  
  
  Android: tag, build, then decide whether to publish
&lt;/h2&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fpbs.twimg.com%2Fmedia%2FHTxxnj_bAAAOy7w%3Fformat%3Djpg%26name%3Dlarge" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fpbs.twimg.com%2Fmedia%2FHTxxnj_bAAAOy7w%3Fformat%3Djpg%26name%3Dlarge" alt="Android release flow from version tag through checks and signed builds to GitHub Release or Google Play production." width="1672" height="941"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;An &lt;code&gt;android-v*&lt;/code&gt; tag runs checks, builds signed AAB/APK files, adds signatures and provenance, and creates a GitHub Release.&lt;/p&gt;

&lt;p&gt;Production rollout is optional. When enabled, it uploads the AAB to Play’s production track with status completed, subject to Google’s processing and review. There’s no testing-track selector.&lt;/p&gt;

&lt;p&gt;I still increment &lt;code&gt;versionCode&lt;/code&gt; before tagging.&lt;/p&gt;

&lt;h2&gt;
  
  
  iOS: two systems share the release
&lt;/h2&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fpbs.twimg.com%2Fmedia%2FHTxxu28acAESdlc%3Fformat%3Djpg%26name%3Dlarge" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fpbs.twimg.com%2Fmedia%2FHTxxu28acAESdlc%3Fformat%3Djpg%26name%3Dlarge" alt="iOS release flow split between Xcode Cloud and GitHub Actions, with a manual build handoff." width="1672" height="941"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;An &lt;code&gt;ios-v*&lt;/code&gt; tag starts two paths: Xcode Cloud archives and uploads the binary; GitHub Actions handles checks, metadata, review submission, and the GitHub Release.&lt;/p&gt;

&lt;p&gt;The matching App Store version must already exist, with the right build selected. Actions doesn’t yet wait for Xcode Cloud or select that build. I’m planning that handoff for the next update.&lt;/p&gt;

&lt;p&gt;Submitting still means waiting for Apple’s review.&lt;/p&gt;

&lt;h2&gt;
  
  
  Subscriptions are part of the release
&lt;/h2&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fpbs.twimg.com%2Fmedia%2FHTxxzIvbsAA6EEO%3Fformat%3Djpg%26name%3Dlarge" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fpbs.twimg.com%2Fmedia%2FHTxxzIvbsAA6EEO%3Fformat%3Djpg%26name%3Dlarge" alt="Subscription submission flow from the product catalog through eligibility checks and product attachment to app review." width="1672" height="941"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;The submission script includes eligible subscriptions and in-app purchases from the repository’s catalog. Subscriptions use their subscription-version relationship.&lt;/p&gt;

&lt;p&gt;If a required product can’t be attached, app submission stops. Retries reuse the draft and skip products already added.&lt;/p&gt;

&lt;h2&gt;
  
  
  Keep the content alongside the code
&lt;/h2&gt;

&lt;p&gt;Listing copy lives in &lt;code&gt;APPSTORE.md&lt;/code&gt; and &lt;code&gt;PLAYSTORE.md&lt;/code&gt;. Notes and screenshots also come from the repo. A dry run prepares the files and validates the catalog before any store API calls.&lt;/p&gt;

&lt;p&gt;Credentials stay in GitHub Secrets; switches are repository variables. RevenueCat catalog writes aren’t implemented yet, so enabling that sync fails explicitly.&lt;/p&gt;

&lt;h2&gt;
  
  
  Where it stands
&lt;/h2&gt;

&lt;p&gt;Currently, iOS listing updates and review submission are enabled. Android rollout, screenshots, and Play release notes are off.&lt;/p&gt;

&lt;p&gt;The next step is the iOS build handoff. If you’re building something similar, start with a dry run, then enable store actions as each release is ready.&lt;/p&gt;

&lt;p&gt;Which part of your mobile release still makes you open the store console?&lt;/p&gt;

&lt;p&gt;Implementation: &lt;a href="https://github.com/apoorvdarshan/fud-ai/tree/main/store" rel="noopener noreferrer"&gt;https://github.com/apoorvdarshan/fud-ai/tree/main/store&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Originally published on &lt;a href="https://x.com/apoorvdarshan/status/2106686145052676320" rel="noopener noreferrer"&gt;X&lt;/a&gt;.&lt;/p&gt;

</description>
      <category>githubactions</category>
      <category>devops</category>
      <category>ios</category>
      <category>android</category>
    </item>
    <item>
      <title>I built a cookie bridge, then changed my entire agent setup</title>
      <dc:creator>Apoorv Darshan</dc:creator>
      <pubDate>Sun, 11 Oct 2026 06:18:19 +0000</pubDate>
      <link>https://dev.to/apoorvdarshan/i-built-a-cookie-bridge-then-changed-my-entire-agent-setup-3e2</link>
      <guid>https://dev.to/apoorvdarshan/i-built-a-cookie-bridge-then-changed-my-entire-agent-setup-3e2</guid>
      <description>&lt;p&gt;I wanted my coding agents to use the websites I was already logged into. That small problem ended up changing my whole setup.&lt;/p&gt;

&lt;h2&gt;
  
  
  Chrome, Brave, and a cookie bridge
&lt;/h2&gt;

&lt;p&gt;I started with the Codex desktop app and its in-app browser. Importing my Chrome session was convenient: the agent could work with sites where I was already signed in.&lt;/p&gt;

&lt;p&gt;Then I switched to Brave, and the import option I needed was missing. My accounts were signed in over there while the agent was working somewhere else.&lt;/p&gt;

&lt;p&gt;So I built Browser Cookie Bridge, a macOS app that started by moving Brave cookies into Codex and grew to support more Chromium browsers.&lt;/p&gt;

&lt;p&gt;When I tried Grok Bot later, I used Cookie Bridge there too, exporting a cookie bundle into its browser. I kept wanting the same thing: let the agent start from the session I already have.&lt;/p&gt;

&lt;p&gt;The code: &lt;a href="https://github.com/apoorvdarshan/browser-cookie-bridge" rel="noopener noreferrer"&gt;https://github.com/apoorvdarshan/browser-cookie-bridge&lt;/a&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  Moving into cmux
&lt;/h2&gt;

&lt;p&gt;The RAM usage of the Codex desktop app eventually pushed me toward terminal interfaces. I started using cmux, which also had an in-app browser with session import built in.&lt;/p&gt;

&lt;p&gt;That helped, but I still had to sync often. Some Google sessions did not come across properly, and some passkey flows did not work as expected.&lt;/p&gt;

&lt;p&gt;I would start a task and end up spending time getting the browser signed in again. Cookie transfers helped, but they could not carry everything, including passkeys.&lt;/p&gt;

&lt;h2&gt;
  
  
  The rule I use now
&lt;/h2&gt;

&lt;p&gt;Eventually I tried agent-browser from Vercel Labs and added this rule to my AGENTS.md:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;Use the agent-browser MCP for web browsing by default (headless, logged out). If a site needs my login, relaunch it with &lt;code&gt;--profile Default&lt;/code&gt; to reuse my Chrome logins.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;Most browsing starts without my accounts. When a task needs a login, the agent uses the Chrome profile flow. That flag launches a temporary copy of the profile, so session reuse becomes part of the workflow instead of another import I have to arrange by hand.&lt;/p&gt;

&lt;p&gt;I still handle a fresh login or passkey approval when needed. But now I can use Claude Code, Codex or OpenCode in cmux with the same browser rule.&lt;/p&gt;

&lt;p&gt;I shared the setup on October 9:&lt;/p&gt;

&lt;p&gt;&lt;iframe class="tweet-embed" id="tweet-2108313693696188526-473" src="https://platform.twitter.com/embed/Tweet.html?id=2108313693696188526"&gt;
&lt;/iframe&gt;

  // Detect dark theme
  var iframe = document.getElementById('tweet-2108313693696188526-473');
  if (document.body.className.includes('dark-theme')) {
    iframe.src = "https://platform.twitter.com/embed/Tweet.html?id=2108313693696188526&amp;amp;theme=dark"
  }



&lt;/p&gt;

&lt;p&gt;Guillermo Rauch replied, "This is the way. more to come!" A nice surprise after all that fiddling with browser sessions.&lt;/p&gt;

&lt;p&gt;&lt;iframe class="tweet-embed" id="tweet-2108314389170516471-245" src="https://platform.twitter.com/embed/Tweet.html?id=2108314389170516471"&gt;
&lt;/iframe&gt;

  // Detect dark theme
  var iframe = document.getElementById('tweet-2108314389170516471-245');
  if (document.body.className.includes('dark-theme')) {
    iframe.src = "https://platform.twitter.com/embed/Tweet.html?id=2108314389170516471&amp;amp;theme=dark"
  }



&lt;/p&gt;

&lt;h2&gt;
  
  
  It became part of my daily work
&lt;/h2&gt;

&lt;p&gt;I checked my saved agent threads before writing this. Across October 8 to 11, the audit found 616 recorded agent-browser calls and command batches in 11 threads. That includes retries and batches with multiple actions, so it is a usage count, not 616 completed tasks.&lt;/p&gt;

&lt;p&gt;Those threads covered the Omi website and leaderboard, a ringtone editor, my portfolio and resume, marketing, and research. I knew I was using it a lot. The history made that obvious.&lt;/p&gt;

&lt;p&gt;This was my follow-up:&lt;/p&gt;

&lt;p&gt;&lt;iframe class="tweet-embed" id="tweet-2109099239699890312-532" src="https://platform.twitter.com/embed/Tweet.html?id=2109099239699890312"&gt;
&lt;/iframe&gt;

  // Detect dark theme
  var iframe = document.getElementById('tweet-2109099239699890312-532');
  if (document.body.className.includes('dark-theme')) {
    iframe.src = "https://platform.twitter.com/embed/Tweet.html?id=2109099239699890312&amp;amp;theme=dark"
  }



&lt;/p&gt;

&lt;p&gt;Browser Cookie Bridge still helps when I want sessions inside an in-app browser. For everyday work, I now prefer terminal agents with a browser tool they can call when needed.&lt;/p&gt;

&lt;p&gt;Thanks &lt;a href="https://x.com/vercel" rel="noopener noreferrer"&gt;@vercel&lt;/a&gt;, &lt;a href="https://x.com/v0" rel="noopener noreferrer"&gt;@v0&lt;/a&gt; and &lt;a href="https://x.com/rauchg" rel="noopener noreferrer"&gt;@rauchg&lt;/a&gt; for the work you're putting into tools for builders. agent-browser has saved me a lot of time. I get to spend more of it actually shipping.&lt;/p&gt;

&lt;p&gt;I wanted to build apps. I did not expect browser logins to shape so much of how I build them.&lt;/p&gt;

&lt;p&gt;Try it: &lt;a href="https://agent-browser.dev" rel="noopener noreferrer"&gt;https://agent-browser.dev&lt;/a&gt;. Chrome profile details: &lt;a href="https://agent-browser.dev/sessions" rel="noopener noreferrer"&gt;https://agent-browser.dev/sessions&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;Originally published on &lt;a href="https://x.com/apoorvdarshan/status/2109119525954216286" rel="noopener noreferrer"&gt;X&lt;/a&gt;.&lt;/p&gt;

</description>
      <category>ai</category>
      <category>agents</category>
      <category>productivity</category>
      <category>opensource</category>
    </item>
    <item>
      <title>I built a free ringtone maker with Web Audio and FFmpeg.wasm</title>
      <dc:creator>Apoorv Darshan</dc:creator>
      <pubDate>Sun, 11 Oct 2026 04:42:45 +0000</pubDate>
      <link>https://dev.to/apoorvdarshan/i-built-a-free-ringtone-maker-with-web-audio-and-ffmpegwasm-3b02</link>
      <guid>https://dev.to/apoorvdarshan/i-built-a-free-ringtone-maker-with-web-audio-and-ffmpegwasm-3b02</guid>
      <description>&lt;p&gt;I built &lt;strong&gt;Ringtone Cropper&lt;/strong&gt;, a free, open-source browser tool for turning a short section of audio into a ringtone for Android or iPhone.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://ringtone.apoorvdarshan.com/" rel="noopener noreferrer"&gt;Try the editor&lt;/a&gt; · &lt;a href="https://github.com/apoorvdarshan/ringtone-cropper" rel="noopener noreferrer"&gt;Source on GitHub&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Here's the workflow in about 30 seconds:&lt;/p&gt;

&lt;p&gt;  &lt;iframe src="https://www.youtube.com/embed/RJm64GNU8zw" width="710" height="399"&gt;
  &lt;/iframe&gt;
&lt;/p&gt;

&lt;h2&gt;
  
  
  What it does
&lt;/h2&gt;

&lt;p&gt;Upload an audio or video file, select a moment on the waveform, preview it, and export. The selection defaults to exactly 30 seconds, with 15/30/40-second presets and custom lengths.&lt;/p&gt;

&lt;p&gt;There are fade-in and fade-out controls, looped preview, optional loudness normalization, and MP3, M4A, M4R, or WAV exports. The interface works on mobile and desktop, with light, dark, and system themes. No signup is needed.&lt;/p&gt;

&lt;p&gt;The site creates the audio file; your phone's apps or settings apply it as a ringtone. It includes setup instructions, including a GarageBand workflow for iPhone.&lt;/p&gt;

&lt;h2&gt;
  
  
  What runs where
&lt;/h2&gt;

&lt;p&gt;The frontend is plain HTML, CSS, and JavaScript. Web Audio handles decoding and preview, while &lt;strong&gt;ffmpeg.wasm&lt;/strong&gt; handles export in the browser.&lt;/p&gt;

&lt;p&gt;For an uploaded file, the flow is:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Local file → browser decoding + waveform
           → selection + preview
           → browser export → download
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Local files stay on your device.&lt;/p&gt;

&lt;p&gt;Optional YouTube imports take a different path: a Cloudflare Worker validates and rate-limits the request, then a container running yt-dlp retrieves the audio. Trimming and export still happen in the browser. These imports are for audio you're permitted to download.&lt;/p&gt;

&lt;h2&gt;
  
  
  A small detail: keeping 30 seconds exactly 30 seconds
&lt;/h2&gt;

&lt;p&gt;Selection boundaries use a 0.1-second grid. A subtle edge case is a source that ends between grid points: rounding after clamping can push a selection past the actual end.&lt;/p&gt;

&lt;p&gt;The selection helper floors the available duration to that grid first, fits the clip length, and then clamps its start. The same helper keeps the presets and editable boundaries consistent.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://github.com/apoorvdarshan/ringtone-cropper/blob/main/public/selection.mjs" rel="noopener noreferrer"&gt;Here's the selection helper&lt;/a&gt;.&lt;/p&gt;

&lt;h2&gt;
  
  
  Audio quality has a ceiling
&lt;/h2&gt;

&lt;p&gt;YouTube imports request the best available audio stream without an intermediate lossy conversion. Exports use the original source, with 320 kb/s MP3 or AAC options and uncompressed WAV.&lt;/p&gt;

&lt;p&gt;A higher export bitrate can't recover detail missing from the source. WAV avoids another lossy encode, but doesn't magically improve the recording.&lt;/p&gt;

&lt;p&gt;I'd love feedback on the waveform controls and export experience—especially on a phone. Is it easy to select the exact moment you want?&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;a href="https://ringtone.apoorvdarshan.com/" rel="noopener noreferrer"&gt;Open Ringtone Cropper&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://github.com/apoorvdarshan/ringtone-cropper" rel="noopener noreferrer"&gt;Browse the code or report an issue&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://www.producthunt.com/products/ringtone-cropper" rel="noopener noreferrer"&gt;Product Hunt listing&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;

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