Index: src/config/database.js
===================================================================
--- src/config/database.js	(revision 80797d752bc2d91ef2348102b517b288a4132afd)
+++ src/config/database.js	(revision f2eacca5e0a92d8ef8516530c0a23ecd2b0a78ed)
@@ -96,4 +96,8 @@
   ensureColumn('audio_tracks', 'link_youtube',    'TEXT');
   ensureColumn('audio_tracks', 'link_soundcloud', 'TEXT');
+  // Per-track: federate the actual audio file as an AS2 Audio attachment so it plays inline
+  // in EVERY fediverse client (incl. the Mastodon apps). Default 0 = gated (web player only,
+  // file not exposed). Opt-in 1 = the file is served ungated + shared on the fediverse.
+  ensureColumn('audio_tracks', 'fedi_open', 'INTEGER DEFAULT 0');
 
   // Playlists (v9 feature) — first-class entity. CREATE IF NOT EXISTS is
Index: src/routes/admin-audio.js
===================================================================
--- src/routes/admin-audio.js	(revision 80797d752bc2d91ef2348102b517b288a4132afd)
+++ src/routes/admin-audio.js	(revision f2eacca5e0a92d8ef8516530c0a23ecd2b0a78ed)
@@ -96,5 +96,5 @@
   const rows = db.prepare(`
     SELECT t.id, t.title, t.artist, t.album, t.duration, t.cover_url,
-           t.position, t.created_at, t.downloadable, m.filename, m.size, m.mime_type
+           t.position, t.created_at, t.downloadable, t.fedi_open, m.filename, m.size, m.mime_type
     FROM audio_tracks t
     LEFT JOIN media m ON m.id = t.media_id
@@ -288,4 +288,18 @@
 });
 
+// Federate-the-file (fedi_open) per track on/off. When on, this track's audio file is shared
+// as a real AS2 Audio attachment + served ungated → it plays inline in EVERY fediverse client
+// (incl. the Mastodon apps), but the file is downloadable. Off (default) = gated, web-player only.
+router.post('/:id/fedi-open', requireGod, (req, res) => {
+  const site = res.locals.site;
+  if (!site) return res.status(404).send('Site required');
+  const row = db.prepare('SELECT fedi_open FROM audio_tracks WHERE id = ? AND site_id = ?').get(req.params.id, site.id);
+  if (row) {
+    db.prepare('UPDATE audio_tracks SET fedi_open = ? WHERE id = ? AND site_id = ?')
+      .run(row.fedi_open ? 0 : 1, req.params.id, site.id);
+  }
+  res.redirect('/admin/audio');
+});
+
 router.post('/:id/delete', requireGod, (req, res) => {
   const site = res.locals.site;
Index: src/routes/audio.js
===================================================================
--- src/routes/audio.js	(revision 80797d752bc2d91ef2348102b517b288a4132afd)
+++ src/routes/audio.js	(revision f2eacca5e0a92d8ef8516530c0a23ecd2b0a78ed)
@@ -54,9 +54,19 @@
 };
 
-// Access gate: allow only same-origin browser fetches / media loads.
-function isAllowedAudioRequest(req) {
+// Access gate: same-origin browser fetches / media loads — PLUS fediverse-shared tracks.
+function isAllowedAudioRequest(req, filename) {
   if (req.get('X-Audio-Player') === '1') return true;  // our blob fetch
   const site = req.get('Sec-Fetch-Site');              // set by modern browsers
-  return site === 'same-origin' || site === 'same-site';
+  if (site === 'same-origin' || site === 'same-site') return true;
+  // fedi_open tracks are deliberately served ungated so remote servers (Mastodon, …) can
+  // fetch + play the file inline. The operator opted this specific track in (per-track flag).
+  if (filename) {
+    try {
+      const r = db.prepare(`SELECT 1 FROM audio_tracks t JOIN media m ON t.media_id = m.id
+        WHERE t.fedi_open = 1 AND (m.storage_path = ? OR m.storage_path LIKE ?) LIMIT 1`).get(filename, '%' + filename);
+      if (r) return true;
+    } catch { /* ignore */ }
+  }
+  return false;
 }
 
@@ -64,5 +74,5 @@
   const { filename } = req.params;
 
-  if (!isAllowedAudioRequest(req)) {
+  if (!isAllowedAudioRequest(req, filename)) {
     return res.status(403).send('Direct access not allowed');
   }
Index: src/services/ActivityPubService.js
===================================================================
--- src/services/ActivityPubService.js	(revision 80797d752bc2d91ef2348102b517b288a4132afd)
+++ src/services/ActivityPubService.js	(revision f2eacca5e0a92d8ef8516530c0a23ecd2b0a78ed)
@@ -225,4 +225,23 @@
     for (const m of body.matchAll(/\[\[album:([^\]]+)\]\]/g)) audioLabels.push(m[1].trim());
   } catch { /* non-fatal */ }
+  // fedi_open tracks → real AS2 Audio attachments (the actual file URL, served ungated) so
+  // EVERY client incl. the Mastodon apps plays them inline natively. Gated tracks (default)
+  // stay link/card-only — the file is never exposed for them. Resolve from post.content so a
+  // later body mutation can't affect it.
+  const openAudio = [];
+  if (hadAudio) {
+    const seenA = new Set();
+    const addRow = (r) => {
+      const fn = r.filename || (r.storage_path || '').split('/').pop();
+      if (!fn || seenA.has(fn)) return; seenA.add(fn);
+      openAudio.push({ type: 'Document', mediaType: r.mime_type || 'audio/mpeg', url: `${base}/audio/stream/${encodeURIComponent(fn)}`, name: r.title || 'Audio' });
+    };
+    const SEL = 'SELECT t.title, m.filename, m.storage_path, m.mime_type FROM audio_tracks t JOIN media m ON m.id = t.media_id WHERE t.fedi_open = 1 AND ';
+    try {
+      for (const mm of (post.content || '').matchAll(/\[\[track:([A-Za-z0-9_-]+)\]\]/g)) { const r = db.prepare(SEL + 't.id = ?').get(mm[1]); if (r) addRow(r); }
+      for (const mm of (post.content || '').matchAll(/\[\[album:([^\]]+)\]\]/g)) for (const r of db.prepare(SEL + 't.site_id = ? AND t.album = ? ORDER BY t.rowid').all(site.id, mm[1].trim())) addRow(r);
+      for (const mm of (post.content || '').matchAll(/\[\[playlist:([A-Za-z0-9_-]+)\]\]/g)) for (const r of db.prepare('SELECT t.title, m.filename, m.storage_path, m.mime_type FROM playlist_tracks pt JOIN audio_tracks t ON t.id = pt.track_id JOIN media m ON m.id = t.media_id WHERE t.fedi_open = 1 AND pt.playlist_id = ? ORDER BY pt.position').all(mm[1])) addRow(r);
+    } catch { /* non-fatal */ }
+  }
   body = body.replace(/\[\[(track|album|playlist):[^\]]+\]\]/gi, '');
   // External embeds ([[embed:url]]) → emit the bare URL as a link so Mastodon
@@ -263,4 +282,5 @@
     .filter((u) => { if (seen.has(u)) return false; seen.add(u); return true; })
     .map((u) => ({ type: 'Document', mediaType: mediaType(u), url: u }));
+  for (const a of openAudio) attachment.push(a); // fedi_open tracks → native Audio players
 
   const note = {
Index: src/services/i18n.js
===================================================================
--- src/services/i18n.js	(revision 80797d752bc2d91ef2348102b517b288a4132afd)
+++ src/services/i18n.js	(revision f2eacca5e0a92d8ef8516530c0a23ecd2b0a78ed)
@@ -336,4 +336,6 @@
     'aaud.dl_on': 'Download-voor-email staat AAN — klik om uit te zetten',
     'aaud.dl_off': 'Download-voor-email staat uit — klik om aan te zetten',
+    'aaud.fedi_on': 'Op de fediverse gedeeld (speelt overal inline, bestand downloadbaar) — klik om uit te zetten',
+    'aaud.fedi_off': 'Niet op de fediverse gedeeld (alleen webspeler, bestand verborgen) — klik om te delen',
     'aaud.embed_player': 'Embedbare speler',
     'aaud.embed_hint': 'Plak deze code op je eigen website/blog om je muziek met deze speler in te sluiten:',
@@ -1250,4 +1252,6 @@
     'aaud.dl_on': 'Download-for-email is ON — click to turn off',
     'aaud.dl_off': 'Download-for-email is off — click to turn on',
+    'aaud.fedi_on': 'Shared on the fediverse (plays inline everywhere, file downloadable) — click to turn off',
+    'aaud.fedi_off': 'Not shared on the fediverse (web player only, file hidden) — click to share',
     'aaud.embed_player': 'Embeddable player',
     'aaud.embed_hint': 'Paste this code on your own website/blog to embed your music with this player:',
@@ -2163,4 +2167,6 @@
     'aaud.dl_on': 'Download-für-E-Mail ist AN — zum Ausschalten klicken',
     'aaud.dl_off': 'Download-für-E-Mail ist aus — zum Einschalten klicken',
+    'aaud.fedi_on': 'Im Fediverse geteilt (spielt überall inline, Datei herunterladbar) — zum Ausschalten klicken',
+    'aaud.fedi_off': 'Nicht im Fediverse geteilt (nur Web-Player, Datei verborgen) — zum Teilen klicken',
     'aaud.embed_player': 'Einbettbarer Player',
     'aaud.embed_hint': 'Füge diesen Code auf deiner eigenen Website/deinem Blog ein, um deine Musik mit diesem Player einzubetten:',
Index: src/views/pages/admin-audio.ejs
===================================================================
--- src/views/pages/admin-audio.ejs	(revision 80797d752bc2d91ef2348102b517b288a4132afd)
+++ src/views/pages/admin-audio.ejs	(revision f2eacca5e0a92d8ef8516530c0a23ecd2b0a78ed)
@@ -83,4 +83,6 @@
         var _aaudDlOn     = t('aaud.dl_on');
         var _aaudDlOff    = t('aaud.dl_off');
+        var _aaudFediOn   = t('aaud.fedi_on');
+        var _aaudFediOff  = t('aaud.fedi_off');
         var _aaudDelete   = t('aaud.delete');
         var _aaudCopy     = t('aaud.copy_click');
@@ -121,4 +123,9 @@
                         style="<%= t.downloadable ? 'color:var(--accent,#6b8f71)' : 'opacity:.5' %>">⬇</button>
               <% } %>
+              <form action="/admin/audio/<%= t.id %>/fedi-open" method="post" class="ax-track-fedi" style="display:inline">
+                <button type="submit" class="ax-icon-btn"
+                        aria-label="<%= t.fedi_open ? _aaudFediOn : _aaudFediOff %>" title="<%= t.fedi_open ? _aaudFediOn : _aaudFediOff %>"
+                        style="<%= t.fedi_open ? 'color:var(--accent,#6b8f71)' : 'opacity:.5' %>">🌐</button>
+              </form>
               <form action="/admin/audio/<%= t.id %>/delete" method="post" data-confirm="<%= _aaudDelConfirm %>" class="ax-track-delete">
                 <button type="submit" class="ax-icon-btn ax-icon-btn-danger" aria-label="<%= _aaudDelete %>" title="<%= _aaudDelete %>">🗑</button>
