DeadAir: play episodes with their silences shortened #171

Closed
opened 2026-10-09 09:28:59 -07:00 by rays · 3 comments
Owner

DeadAir: podcasts play with long silences shortened, so they finish sooner without sounding sped up.

Measured (2026-10-09, ffmpeg silencedetect, quieter than -38 dB for over 0.35 s, each silence cut to 0.2 s)

Show Length Silences Saved
Brad & Will Made a Tech Pod 64.6 min 525 3.5 min (5.4%)
Clarkesworld Magazine 31.9 min 443 6.0 min (18.8%)
DVC Newscast 25.0 min 120 0.7 min (2.9%)
Ken and Robin Talk About Stuff 58.1 min 39 0.2 min (0.4%)

Clarkesworld is fiction read aloud: its pauses are part of the reading, so a feed has to be able to stay out.

For the 64-minute episode on Tower, one core: decode 5.5 s, find silences 6.5 s, cut and encode MP3 32 s, AAC 76 s. Streamed, the first 30 s of output take 0.4 s. Copying the kept frames instead of encoding (first comment): MP3 3.7 s, AAC 4.9 s, so about 10 s per hour of audio with finding the silences.

Disk on 2026-10-09: 62 GB of downloads, 283 GB free. A copy is never bigger than its original.

Plan

  • A separate file, kept beside the original, made in the background after download. Not made on request: a file still being made has no length, so Safari and the iPhone app cannot seek in it, and each listener would cut it again. A finished file is served by /media as the original is, ranges and all.
  • Opt-in, feed by feed: off for every feed until someone turns it on for that feed, or for the group it came in (an OPML or a Patreon creator), where one feed can still be kept out. No feed already subscribed starts with it on. Copies are made for a feed while anyone subscribed has it on: the union of what subscribers want, as merge_policy is for what to fetch. New downloads get their copy as they land. When a feed is turned on, the files it already has get theirs. One file at a time, in a queue of its own, so that pass (hours of one core if every feed is in it) never holds up a scan or a download.
  • Nothing is re-encoded (first comment): the copy is the original's own frames with the silent ones left out. Cut from the middle of each silence, keeping its first and last 0.1 s, so the frames on both sides of every cut are silent. Decoding blends each frame with the one before it, and an MP3 frame can begin in the bytes of the frame before (the bit reservoir), so the first frame after a cut decodes against the wrong neighbour. Between two silent frames nobody hears that; at the start of a word, the word's first sound would come out wrong.
  • ffmpeg in the image, to find the silences and copy the kept frames. No encoder is needed any more; ffmpeg is there because it writes the headers right: the MP3's Xing header (frame count and seek table, which players use for the length and for seeking) and the M4A's index. Debian's package adds 481 MB in 200 packages (measured); an ffmpeg built in the Dockerfile with only these parts is far smaller.
  • The cut list is stored per enclosure, as frame ranges, in a table of its own, so Db::open creates it and no ALTER is needed. It converts positions between the two versions and gives the time saved. ipx shows no chapters, so there are none to convert.
  • Positions stay in the original's time and the server converts them, both ways: what it hands out is in the clock of the version you will play, and what you save is converted from the version it last served you (/media knows who is asking). The iOS and Android apps save positions themselves, from their own player, even while the page is asleep (native.ts), so converting in the page would miss them. Done in the server, neither the page nor the apps need the cut list, and the apps need no new build. The duration a player reports for a feed that gives none is converted too, or the copy's shorter length would be saved as the episode's.
  • Turning DeadAir on or off mid-episode saves where you are and carries on at the same spot in the other version. A copy finished while you listen is used from the next time you play that episode.
  • A copy goes wherever its original goes: download::remove removes both, and retention and Delete both use it. A copy is made again if its original is downloaded again.
  • UI: a DeadAir switch in each feed's settings, beside Download automatically, off until you turn it on, and saved to your account rather than the device as speed is, because the server decides from it which feeds get copies. Beside the speed control, the same switch for the feed of what is playing. The time saved on each episode, and your running total. Mockups here for approval before merge.
  • A running total of time saved, for each person, shown as a stat for fun. An episode counts once you have listened to 75% of it with DeadAir on, and adds the time its cuts saved. Each counted episode is a row in a table of its own (person, episode, seconds saved), so an episode counts once however often it is replayed, and the total is their sum. It goes up by what the copy saves, not by how fast you listen.
  • Videos, multi-file torrents and anything but MP3 and AAC are left as they are.

Open questions

  1. Which clock do the player and the list show with DeadAir on: the shortened one (the true time left, and what this plan assumes) or the original's (matches times quoted in show notes)?
  2. The first pass over a feed: every file it has, or only episodes nobody with DeadAir on has finished? Finished ones are rarely played again.
  3. A copy not ready when you press play: play the original and use the copy next time (this plan), or switch over mid-episode when it is ready?
  4. ffmpeg: Debian's package (one line, 481 MB) or one built in the Dockerfile (more lines, far smaller)?
DeadAir: podcasts play with long silences shortened, so they finish sooner without sounding sped up. ## Measured (2026-10-09, ffmpeg silencedetect, quieter than -38 dB for over 0.35 s, each silence cut to 0.2 s) | Show | Length | Silences | Saved | |---|---|---|---| | Brad & Will Made a Tech Pod | 64.6 min | 525 | 3.5 min (5.4%) | | Clarkesworld Magazine | 31.9 min | 443 | 6.0 min (18.8%) | | DVC Newscast | 25.0 min | 120 | 0.7 min (2.9%) | | Ken and Robin Talk About Stuff | 58.1 min | 39 | 0.2 min (0.4%) | Clarkesworld is fiction read aloud: its pauses are part of the reading, so a feed has to be able to stay out. For the 64-minute episode on Tower, one core: decode 5.5 s, find silences 6.5 s, cut and encode MP3 32 s, AAC 76 s. Streamed, the first 30 s of output take 0.4 s. Copying the kept frames instead of encoding (first comment): MP3 3.7 s, AAC 4.9 s, so about 10 s per hour of audio with finding the silences. Disk on 2026-10-09: 62 GB of downloads, 283 GB free. A copy is never bigger than its original. ## Plan * **A separate file, kept beside the original**, made in the background after download. Not made on request: a file still being made has no length, so Safari and the iPhone app cannot seek in it, and each listener would cut it again. A finished file is served by `/media` as the original is, ranges and all. * **Opt-in, feed by feed: off for every feed until someone turns it on for that feed**, or for the group it came in (an OPML or a Patreon creator), where one feed can still be kept out. No feed already subscribed starts with it on. Copies are made for a feed while anyone subscribed has it on: the union of what subscribers want, as `merge_policy` is for what to fetch. New downloads get their copy as they land. When a feed is turned on, the files it already has get theirs. One file at a time, in a queue of its own, so that pass (hours of one core if every feed is in it) never holds up a scan or a download. * **Nothing is re-encoded** (first comment): the copy is the original's own frames with the silent ones left out. **Cut from the middle of each silence, keeping its first and last 0.1 s**, so the frames on both sides of every cut are silent. Decoding blends each frame with the one before it, and an MP3 frame can begin in the bytes of the frame before (the bit reservoir), so the first frame after a cut decodes against the wrong neighbour. Between two silent frames nobody hears that; at the start of a word, the word's first sound would come out wrong. * **ffmpeg in the image**, to find the silences and copy the kept frames. No encoder is needed any more; ffmpeg is there because it writes the headers right: the MP3's Xing header (frame count and seek table, which players use for the length and for seeking) and the M4A's index. Debian's package adds 481 MB in 200 packages (measured); an ffmpeg built in the Dockerfile with only these parts is far smaller. * **The cut list is stored per enclosure**, as frame ranges, in a table of its own, so `Db::open` creates it and no `ALTER` is needed. It converts positions between the two versions and gives the time saved. ipx shows no chapters, so there are none to convert. * **Positions stay in the original's time and the server converts them**, both ways: what it hands out is in the clock of the version you will play, and what you save is converted from the version it last served you (`/media` knows who is asking). The iOS and Android apps save positions themselves, from their own player, even while the page is asleep (`native.ts`), so converting in the page would miss them. Done in the server, neither the page nor the apps need the cut list, and the apps need no new build. The `duration` a player reports for a feed that gives none is converted too, or the copy's shorter length would be saved as the episode's. * **Turning DeadAir on or off mid-episode** saves where you are and carries on at the same spot in the other version. A copy finished while you listen is used from the next time you play that episode. * **A copy goes wherever its original goes**: `download::remove` removes both, and retention and Delete both use it. A copy is made again if its original is downloaded again. * **UI**: a DeadAir switch in each feed's settings, beside Download automatically, off until you turn it on, and saved to your account rather than the device as speed is, because the server decides from it which feeds get copies. Beside the speed control, the same switch for the feed of what is playing. The time saved on each episode, and your running total. Mockups here for approval before merge. * **A running total of time saved, for each person**, shown as a stat for fun. An episode counts once you have listened to 75% of it with DeadAir on, and adds the time its cuts saved. Each counted episode is a row in a table of its own (person, episode, seconds saved), so an episode counts once however often it is replayed, and the total is their sum. It goes up by what the copy saves, not by how fast you listen. * Videos, multi-file torrents and anything but MP3 and AAC are left as they are. ## Open questions 1. Which clock do the player and the list show with DeadAir on: the shortened one (the true time left, and what this plan assumes) or the original's (matches times quoted in show notes)? 2. The first pass over a feed: every file it has, or only episodes nobody with DeadAir on has finished? Finished ones are rarely played again. 3. A copy not ready when you press play: play the original and use the copy next time (this plan), or switch over mid-episode when it is ready? 4. ffmpeg: Debian's package (one line, 481 MB) or one built in the Dockerfile (more lines, far smaller)?
rays added the enhancement label 2026-10-09 09:28:59 -07:00
Author
Owner

No re-encoding needed. A cut inside a silence can fall on a frame boundary (MP3 1152 samples, AAC 1024, about 25 ms), so the shortened file can be the original's own frames with the silent ones left out, copied in one pass:

ffmpeg -i in -map 0:a -c copy -bsf:a "noise=drop='<silent frames>',setts=ts=N*DURATION" out
64-minute episode Time Quality
re-encode MP3 32 s one generation of loss
re-encode AAC 76 s one generation of loss
copy, dropping silent frames, MP3 3.7 s untouched
same, AAC in .m4a 4.9 s untouched
concat demuxer with inpoint/outpoint 72.9 s, 14 s off untouched

Plus 6.5 s to find the silences: about 10 s per hour of audio, one core. Both outputs decode with no errors; length within 0.4 s of expected over 526 cuts. The real version should drop by frame index, so it knows exactly which frames went and the position map is exact.

Downloads today: 796 MP3, 25 MP4, a handful of others (WAV, MPG) which would be left as they are.

No re-encoding needed. A cut inside a silence can fall on a frame boundary (MP3 1152 samples, AAC 1024, about 25 ms), so the shortened file can be the original's own frames with the silent ones left out, copied in one pass: ffmpeg -i in -map 0:a -c copy -bsf:a "noise=drop='<silent frames>',setts=ts=N*DURATION" out | 64-minute episode | Time | Quality | |---|---|---| | re-encode MP3 | 32 s | one generation of loss | | re-encode AAC | 76 s | one generation of loss | | copy, dropping silent frames, MP3 | 3.7 s | untouched | | same, AAC in .m4a | 4.9 s | untouched | | concat demuxer with inpoint/outpoint | 72.9 s, 14 s off | untouched | Plus 6.5 s to find the silences: about 10 s per hour of audio, one core. Both outputs decode with no errors; length within 0.4 s of expected over 526 cuts. The real version should drop by frame index, so it knows exactly which frames went and the position map is exact. Downloads today: 796 MP3, 25 MP4, a handful of others (WAV, MPG) which would be left as they are.
rays changed title from Smart Speed: play episodes with their silences shortened to DeadAir: play episodes with their silences shortened 2026-10-09 09:59:27 -07:00
Author
Owner

The server side is on main in 63e1927, not deployed yet. Copies are made in a task of their own every two minutes for files in feeds someone has DeadAir on for; positions are converted by the server both ways; episodes heard 75% or more on a copy count towards each person's total, once each. Tried on Tech Pod 359 (64 min): 520 cuts, 3.3 min saved, 11.5 s to make, decodes clean, no clicks at the cuts.

The open questions went with the recommended answers: the player and lists show the shortened clock; only episodes not yet finished by someone who would hear the copy get one; a copy not ready when you press play is used from the next play; Debian's ffmpeg in the image.

Still to do: the switch beside the speed, the opt-out in each feed's settings and the time-saved stat, with mockups here first.

The server side is on main in 63e1927, not deployed yet. Copies are made in a task of their own every two minutes for files in feeds someone has DeadAir on for; positions are converted by the server both ways; episodes heard 75% or more on a copy count towards each person's total, once each. Tried on Tech Pod 359 (64 min): 520 cuts, 3.3 min saved, 11.5 s to make, decodes clean, no clicks at the cuts. The open questions went with the recommended answers: the player and lists show the shortened clock; only episodes not yet finished by someone who would hear the copy get one; a copy not ready when you press play is used from the next play; Debian's ffmpeg in the image. Still to do: the switch beside the speed, the opt-out in each feed's settings and the time-saved stat, with mockups here first.
Author
Owner

Server side done and deployed: 63e1927 made the copies, converted positions and counted the time saved; 16b0686 made it opt-in feed by feed, off for every feed including those already subscribed. The page's part moves to #172.

Server side done and deployed: 63e1927 made the copies, converted positions and counted the time saved; 16b0686 made it opt-in feed by feed, off for every feed including those already subscribed. The page's part moves to #172.
rays closed this issue 2026-10-09 11:09:13 -07:00
Sign in to join this conversation.
1 Participants
Notifications
Due Date
No due date set.
Dependencies

No dependencies set.

Reference: rays/ipx#171