<?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: Mark Melton</title>
    <description>The latest articles on DEV Community by Mark Melton (@coommark).</description>
    <link>https://dev.to/coommark</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%2F4085226%2Ff51f130f-1109-47c4-a571-45ad4507105f.png</url>
      <title>DEV Community: Mark Melton</title>
      <link>https://dev.to/coommark</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/coommark"/>
    <language>en</language>
    <item>
      <title>I Built a Free MIT Audio Engine for Expo Because Background Audio Should Not Require a Commercial License</title>
      <dc:creator>Mark Melton</dc:creator>
      <pubDate>Wed, 19 Aug 2026 15:13:59 +0000</pubDate>
      <link>https://dev.to/coommark/i-built-a-free-mit-audio-engine-for-expo-because-background-audio-should-not-require-a-commercial-3be7</link>
      <guid>https://dev.to/coommark/i-built-a-free-mit-audio-engine-for-expo-because-background-audio-should-not-require-a-commercial-3be7</guid>
      <description>&lt;p&gt;&lt;a href="https://dailybiblenow.com" rel="noopener noreferrer"&gt;Daily Bible - Offline &amp;amp; Audio&lt;/a&gt; was never supposed to be a music app.&lt;/p&gt;

&lt;p&gt;It is a Bible. Lightweight. Offline-first. People open it on a commute, lock the phone, and keep listening. Verses queue from on-device TTS. Some readers want a quiet ambient bed under scripture. Some chapters arrive as HLS. All of it has to survive the lock screen, Bluetooth earbuds, a wired headset in a car, and the moment Android decides your process is optional.&lt;/p&gt;

&lt;p&gt;That is not a demo. That is a product people pray with.&lt;/p&gt;

&lt;p&gt;I needed a player that could do all of that inside an Expo app - New Architecture, config plugin, no hand-maintained native snowflake - without turning Daily Bible into a media suite.&lt;/p&gt;

&lt;p&gt;The obvious existing solution went commercial. The license closed the door.&lt;/p&gt;

&lt;p&gt;I had one honest option: build the player Daily Bible actually needed. I open-sourced it.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;&lt;a href="https://github.com/coommark/daily-react-native-player" rel="noopener noreferrer"&gt;daily-react-native-player&lt;/a&gt;&lt;/strong&gt; — MIT, New Architecture only (Expo SDK 57+ / RN 0.86+).&lt;/p&gt;




&lt;h2&gt;
  
  
  The constraint nobody puts on a landing page
&lt;/h2&gt;

&lt;p&gt;A music app can be heavy. A Bible app cannot.&lt;/p&gt;

&lt;p&gt;Daily Bible has to feel instant. Download a translation. Play a chapter. Put the phone in a pocket. Skip the next verse from a Bluetooth bud without unlocking. If someone wants ambient rain under the narration, the lock screen still has to show &lt;em&gt;scripture&lt;/em&gt; — not the bed track.&lt;/p&gt;

&lt;p&gt;That last sentence is the whole product.&lt;/p&gt;

&lt;p&gt;On-device TTS sharpened the constraint further. The player does not run the model. It consumes URLs. The pipeline generates speech at the edge, hands the engine a queue that grows while the current verse is still speaking. Append mid-play. Insert a silence gap that is a real track, not a &lt;code&gt;setTimeout&lt;/code&gt; that drifts when the OS is busy. Seek an HLS chapter before the playlist is ready. Keep the session alive when the user kills the app — if that is the policy.&lt;/p&gt;

&lt;p&gt;Expo was non-negotiable. Continuous native generation. A config plugin that injects iOS audio background mode and Android's &lt;code&gt;mediaPlayback&lt;/code&gt; foreground service at prebuild — not a wiki page of native edits you forget on the next &lt;code&gt;npx expo prebuild&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;New Architecture only. No legacy bridge tax.&lt;/p&gt;




&lt;h2&gt;
  
  
  Install
&lt;/h2&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;npx expo &lt;span class="nb"&gt;install &lt;/span&gt;daily-react-native-player
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Add the config plugin:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight json"&gt;&lt;code&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"expo"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"plugins"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s2"&gt;"daily-react-native-player"&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Peers: Expo SDK 57+, React Native 0.86+, New Architecture only. iOS + Android. No web player.&lt;/p&gt;




&lt;h2&gt;
  
  
  Wiring remotes before the root component
&lt;/h2&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="c1"&gt;// index.ts — must be before registerRootComponent&lt;/span&gt;
&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nx"&gt;registerPlaybackService&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="nx"&gt;setupPlayer&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="nx"&gt;add&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="nx"&gt;play&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="nx"&gt;pause&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="nx"&gt;skipToNext&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="nx"&gt;skipToPrevious&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="nx"&gt;Event&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="nx"&gt;addEventListener&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;daily-react-native-player&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;registerRootComponent&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;expo&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="nx"&gt;App&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;./App&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="nf"&gt;registerPlaybackService&lt;/span&gt;&lt;span class="p"&gt;(()&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="k"&gt;async &lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nf"&gt;addEventListener&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;Event&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;RemotePlay&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="k"&gt;void&lt;/span&gt; &lt;span class="nf"&gt;play&lt;/span&gt;&lt;span class="p"&gt;());&lt;/span&gt;
  &lt;span class="nf"&gt;addEventListener&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;Event&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;RemotePause&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="k"&gt;void&lt;/span&gt; &lt;span class="nf"&gt;pause&lt;/span&gt;&lt;span class="p"&gt;());&lt;/span&gt;
  &lt;span class="nf"&gt;addEventListener&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;Event&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;RemoteNext&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="k"&gt;void&lt;/span&gt; &lt;span class="nf"&gt;skipToNext&lt;/span&gt;&lt;span class="p"&gt;());&lt;/span&gt;
  &lt;span class="nf"&gt;addEventListener&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;Event&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;RemotePrevious&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="k"&gt;void&lt;/span&gt; &lt;span class="nf"&gt;skipToPrevious&lt;/span&gt;&lt;span class="p"&gt;());&lt;/span&gt;
&lt;span class="p"&gt;});&lt;/span&gt;

&lt;span class="nf"&gt;registerRootComponent&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;App&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Remotes emit to JavaScript. Next means what your app defines — the native layer does not guess.&lt;/p&gt;




&lt;h2&gt;
  
  
  Loading a queue and playing
&lt;/h2&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nf"&gt;setupPlayer&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt; &lt;span class="na"&gt;progressUpdateEventInterval&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt; &lt;span class="p"&gt;});&lt;/span&gt;

&lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nf"&gt;add&lt;/span&gt;&lt;span class="p"&gt;([&lt;/span&gt;
  &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;url&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;https://cdn.example.com/verse1.mp3&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;title&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;Genesis 1:1&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt; &lt;span class="p"&gt;},&lt;/span&gt;
  &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;url&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;https://cdn.example.com/verse2.mp3&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;title&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;Genesis 1:2&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt; &lt;span class="p"&gt;},&lt;/span&gt;
  &lt;span class="nf"&gt;createSilenceTrack&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt; &lt;span class="na"&gt;durationMs&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;800&lt;/span&gt; &lt;span class="p"&gt;}),&lt;/span&gt; &lt;span class="c1"&gt;// real queue item, not a timer&lt;/span&gt;
  &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;url&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;https://cdn.example.com/verse3.mp3&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;title&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;Genesis 1:3&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt; &lt;span class="p"&gt;},&lt;/span&gt;
&lt;span class="p"&gt;]);&lt;/span&gt;

&lt;span class="nf"&gt;addEventListener&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;Event&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;PlaybackActiveTrackChanged&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;({&lt;/span&gt; &lt;span class="nx"&gt;track&lt;/span&gt; &lt;span class="p"&gt;})&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="c1"&gt;// sync your "now reading" UI&lt;/span&gt;
  &lt;span class="nx"&gt;console&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;log&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;now playing&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;track&lt;/span&gt;&lt;span class="p"&gt;?.&lt;/span&gt;&lt;span class="nx"&gt;title&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="p"&gt;});&lt;/span&gt;

&lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nf"&gt;play&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;






&lt;h2&gt;
  
  
  What it does — the non-negotiables
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;Lock screen, notification, Control Center, Bluetooth.&lt;/strong&gt; Play, pause, stop, next, previous from every surface users actually touch. MediaSession on Android. MPRemoteCommandCenter on iOS. Hardware buttons emit to JavaScript — no native guessing about what Next means.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Audio that keeps going with the screen off.&lt;/strong&gt; Android &lt;code&gt;mediaPlayback&lt;/code&gt; foreground service. iOS audio background mode. Both injected at Expo prebuild by the config plugin. Continue-after-kill when you want the session to survive.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;A real playlist.&lt;/strong&gt; Not one URL at a time. &lt;code&gt;add&lt;/code&gt;, &lt;code&gt;remove&lt;/code&gt;, &lt;code&gt;skip&lt;/code&gt; while audio keeps playing. Chapter-scale queues. Stable track IDs. The same Playback* events your Now Playing UI needs anyway.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Native silence tracks.&lt;/strong&gt; &lt;code&gt;SilenceMediaSource&lt;/code&gt; on Android, cached PCM WAV on iOS. First-class queue items with progress and remote events. Not a &lt;code&gt;setTimeout&lt;/code&gt; that drifts.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Optional ambient dual-audio.&lt;/strong&gt; Second player is lazy — created only when the first ambient API is called. Never requests audio focus. Never owns Now Playing. Speech-only apps never pay for it. Bible-style listening needs it. Music apps can ignore it.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;HLS VOD + seek-after-ready.&lt;/strong&gt; Plus WAV, mp3, m4a. Seek before the playlist is ready — stashed, applied on READY.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Pitch-preserving &lt;code&gt;setRate&lt;/code&gt;.&lt;/strong&gt; Silence stays at 1× so pauses are not stretched.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Production hardening.&lt;/strong&gt; Setup coalesce + 10s timeout. FGS sync promotion. Reset play-intent-first. These are not nice-to-haves; they are what makes the player behave on real OEM Android devices.&lt;/p&gt;

&lt;p&gt;One native audio owner. Media3 on Android. AVFoundation on iOS. No second focus-owning library bolted on for ambient.&lt;/p&gt;

&lt;p&gt;MIT. Commercial use. No license gate. Fork it. Ship it.&lt;/p&gt;




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

&lt;p&gt;&lt;strong&gt;Edge-native TTS pipelines.&lt;/strong&gt; You generate URLs. We play them in the background. Progressive append, silence gaps, pitch-preserving rate — all there.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Music and playlist apps.&lt;/strong&gt; A real queue that mutates while playing. Lock-screen artwork. Headset remotes. No commercial license required to get the fundamentals right.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Narration, meditation, language learning, audiobooks, podcasts.&lt;/strong&gt; Anything that talks while the phone is in a pocket.&lt;/p&gt;




&lt;p&gt;If you want to hear it in the wild, open &lt;strong&gt;&lt;a href="https://dailybiblenow.com" rel="noopener noreferrer"&gt;Daily Bible - Offline &amp;amp; Audio&lt;/a&gt;&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;That is not a sample app. That is why the engine exists.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;a href="https://apps.apple.com/us/app/daily-bible-offline-audio/id6754987448" rel="noopener noreferrer"&gt;App Store&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://play.google.com/store/apps/details?id=com.coommark.dailybible" rel="noopener noreferrer"&gt;Google Play&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;GitHub: &lt;a href="https://github.com/coommark/daily-react-native-player" rel="noopener noreferrer"&gt;github.com/coommark/daily-react-native-player&lt;/a&gt;&lt;br&gt;
npm: &lt;a href="https://www.npmjs.com/package/daily-react-native-player" rel="noopener noreferrer"&gt;npmjs.com/package/daily-react-native-player&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Star the repo. If you have been looking for background audio on Expo that treats TTS, playlists, and speech as first-class — this is it.&lt;/p&gt;

</description>
      <category>reactnative</category>
      <category>opensource</category>
      <category>mobile</category>
      <category>tts</category>
    </item>
  </channel>
</rss>
