Releases: AurielSolaris/Choir
Release list
v0.5.0
Choir 0.5.0 — Widgets
Four home screen widgets, a queue you can actually see, and the first tests
that run on a real device.
Home screen widgets
Four, in Jetpack Glance:
- Now Playing — 2×2 and 4×2. The cover, the track, the controls. The two
sizes are different arrangements rather than one stretched: at 2×2 the art is
the widget and the words sit under it them beside
it and add the skips. - Controls — a 4×1 transport strip for a dense home screen.
- Liked Songs — 2×2, shuffles or r
- Lyric Line — 4×1, the line being sung.
Nothing polls. Every redraw is pro did. The
lyric line is the hard case, and it is not a one-second tick: an .lrc states
the time of every line, so the next ch than a
condition to watch for. The widget sleeps until exactly that moment and wakes
once — only while playing, only when a Lyric Line widget is actually on a home
screen, and not at all past the last line of the song.
They keep working with the app closed.t shows the
track that was playing before it, with a button that resumes it.
Each one now previews as itself in the picker, rather than as the launcher icon
four times over with different words underneath.
A queue you can see
A popup on the player, listing the queually play**.
With shuffle on, that is the scrambled order — not the order the tracks went in,
which is bookkeeping rather than music Shuffle and
repeat live in the sheet itself, since they are what the list means, and the
header says which are on. Tracks already played stay in the list, dimmed, which
is how you get back to the song you did not catch the name of.
Lyrics
One face throughout. Every line is. Previously
the active line was EB Garamond and every other line fell back to Roboto, so a
lyric was one line of the right typefaf the wrong
one, changing under each line as the song reached it.
Your files win. If a track has wor in its own
tags — the network is never asked, even when what is on disk is plain text and a
service might have offered a timed verle and an
artist is a guess about which recording is playing; the file is not a guess.
Tested on a device
The build has been configured for instd never had
any. There are 26 now, run against the release build.
The important ones are the database migrations. Room only checks a migration
against the exported schema when the Sir's three are
hand-written — so until now a misspelt column was a compile-time success and an
upgrade-time crash, taking someone's likes and playlists with it. Every step is
now run against real SQLite, and so isone who
installed at 0.1.0 gets in one go.
400 unit tests and 26 instrumented
Also in this release
- Three widget bugs found on a real home screen: a composition that rendered
once and then froze, an idle widget ppeared to be
a rendering failure, and an idle state that forgot the last track at exactly
the moment it was needed. - Widget content is centred vertically, because a launcher does not hand out the
size that was asked for.
Install
choir-0.5.0.apk — Android 10 (API 29) or newer
SHA-256 946df801f606433c42bf025af00d40c38b6b2f9c0ab0c16f92a8117dcc3a0889
Upgrading from any earlier version keeps your likes, playlists and queue.
v0.4.0
Choir v0.4.0
Two halves of the same complaint: files Choir could see and not play, and
files it could not see at all.
Folder browsing
- Browse the library by directory, through a folder you grant with the
system file picker (SAF), instead of only through MediaStore. - This is the only route to the files Android's media scanner types as
plain data with media_type 0. A .wv or .ape is not audio as far as the
platform is concerned, and MediaStore.Audio never lists it — no amount
of decoder support reaches a file the library cannot see. - .nomedia is respected, as it is everywhere else.
- Tracks resolve from either source now, so a saved queue survives a
restart whichever side its tracks came from.
Three containers Choir now opens itself
Their decoders shipped in 0.3.0 and played nothing, because a decoder
cannot open a container. Each of these is a Media3 Extractor written from
the format specification:
- WavPack (.wv) — self-describing blocks, scanned for after a seek.
- Monkey's Audio — a seek table read up front, so seeking is exact
(.ape) rather than estimated and corrected. - ASF / Windows — fixed-size packets, with blocks reassembled across
Media (.wma) packet boundaries before the decoder sees them.
WMA, WMA Pro, Lossless and Voice. DRM-protected
files are declined rather than half-read.
They are as different as the formats are. WavPack blocks can be picked up
anywhere in the file, so a lost boundary is recoverable by scanning. APE
frames carry no sync word at all, so its header is everything and a
damaged one ends the file. ASF is a streaming container that splits a
block across packets and needs it sewn back together.
Reaching the decoder
Media3 opens an FFmpeg codec from a Format, which cannot express
block_align, bits_per_coded_sample or bit_rate — and wmadec refuses to
open without the first, while apedec reads a missing bit depth as an
unsupported one rather than as unstated.
-
ChoirCodecContext carries the three values alongside the codec
extradata. -
tools/ffmpeg-jni-context.inc adds the JNI entry point that applies
them, appended to Media3's own by the build script. -
It is a second entry point rather than a change to ffmpegInitialize,
and the Java side falls back on UnsatisfiedLinkError — so an existing
libffmpegJNI.so keeps working for ALAC and Dolby instead of failing to
link.KNOWN LIMITATION: .ape and .wma need tools/build-ffmpeg.sh re-run
before they play. The demuxers work without it — the container opens
and the packets are correct — and then the decoder declines to
initialise. .wv is unaffected; WavPack's decoder needs nothing beyond
its packets.
Also
- AudioFormats reports APE, WavPack and WMA as needing a decoder rather
than a demuxer, which is now the honest answer. - Room schema version 4.
- 367 unit tests, up from 201. Container fixtures are built byte by byte
from the specifications, in the same spirit as the tag fixtures: a
fixture generated by the same misunderstanding as the parser would
agree with it perfectly and prove nothing.
v0.3.0
Choir v0.3.0
A local-first music player for Android. Ported from the AOSP Music app to Kotlin, Compose and Media3.
This release adds lyrics, playlists, liked songs, and support for audio formats Android refuses to play — plus a Settings screen for the one feature that can reach the network.
Lyrics
Choir reads lyrics out of your files, with no account and no connection:
- Sidecar files — a .lrc next to the track, in simple, enhanced, or word-level
- Embedded tags — ID3v2 USLT/SYLT/TXXX (MP3, WAV, AIFF), Vorbis comments (FLAC, Ogg), and iTunes ©lyr atoms (M4A, MP4, M4B)
- Synced scrolling — the current line lights and stays where the eye reads from; tap any line to seek. Scrolling by hand suspends the auto-follow for a few seconds instead of fighting you
- Word-by-word highlighting where the file carries word timings, driven off the frame clock so it tracks the singing rather than lagging a quarter-second behind it
Fetching lyrics online — off by default
For tracks whose files have none, Choir can ask LRCLIB, NetEase, Musixmatch or your own HTTPS endpoint. This is opt-in, and while the switch is off nothing in Choir opens a socket.
When it is on, what leaves the device is a track's title, artist and length — nothing about you, nothing about the rest of your library. Results are cached so each track is looked up once. Unmetered-only is the default, and you can set the order services are asked in.
Genius is deliberately absent: its API returns a link to a page, never the words. Every app that shows Genius lyrics scrapes that page, which breaks whenever the markup changes and is against their terms.
Playlists and liked songs
- Create, rename, delete, and reorder by dragging
- Import and export .m3u / .m3u8; legacy MediaStore playlists can be imported once - A Liked Songs list — manual curation, no play counts, no algorithm
- Both survive Android renumbering your library: entries are re-linked by title, artist and length rather than silently vanishing
Audio formats
Playing a file needs two things that fail independently — something to open the container, and something to decode what's inside. Choir now improves both, and tells you which one is missing when a file won't play.
- FFmpeg decoding is built in, adding ALAC, Dolby AC-3 / E-AC-3, DTS and more where your device has no decoder of its own. Hardware decoders keep the formats they can already handle, so nothing costs extra battery - AIFF now plays, via a reader written for Choir — Media3 ships no AIFF extracll
- Files the media scanner could not parse are no longer hidden. A filter that required a known duration was quietly dropping every AIFF, WMA and AC-3 from the library. Unknown lengths now show as — rather than a false 0:00, and unparsable files keep their extension so you can tell them apart
- A track that will not play now says why — "Choir can't open Windows Media Audio files yet" — instead of a tap that does nothing
Not yet playable
APE, WavPack, WMA, Musepack, TTA and DSD. These need a demuxer, not a decoder, and adding every codec FFmpeg has would not change that. Choir says so plainly rather than failing silently. .wv and .tta additionally aren't indexed as audio by Android at all.
Settings
A new screen covering the lyric providers and their order, what Choir does and does not send, which formats this build can decode, and the licence.
Choir now holds the INTERNET permission, because fetching lyrics needs one. So the honest claim is no longer "it cannot reach the network" but "it does not, unless you ask it to." There is still no account, no analytics, no telemetry, and no record of what you play.
Notes
- Requires Android 10 (API 29) or newer
- 255 unit tests, concentrated on parsing other people's files — ./gradlew test
- The FFmpeg decoder is bundled for all four ABIs. To rebuild it yourself: too
- GPL-3.0-or-later. Derived from the AOSP Music app (Apache-2.0); FFmpeg is LGPL-2.1-or-later, built with no GPL components
choir-0.3.0.apk — 15.3 MB, versionCode 3
sha256 2cb75a419d7e8aae5e4c5fb78d43917afdf67a4ea81bbfa22c7265a7c3873a7b
v0.2.0
Release notes for GitHub
Tag: v0.2.0 · Title: Choir v0.2.0 — the browse release
Choir is a local-first music player for Android: a port of the AOSP Music app to
Kotlin, Compose and Media3. No accounts, no streaming, no recommendations, no
telemetry — and no network permission at all.
This is the first public release. It covers v0.1.0 (the playback core) and
v0.2.0 (the full browse port).
Library
- Four ways in — Tracks, Albums, Artists and Playlists, read from MediaStore
- Drill-downs — albums in running order, an artist's records, playlist contents
- Instant search across titles, albums and artists, grouped by kind
- Scoped queues — playing from an album, artist or search result queues that
list, so skipping forward stays inside what you chose - Audio picker — Choir answers other apps' "choose a track" requests
Playback
- ExoPlayer backend with audio focus, ducking, and becoming-noisy handling
- Media notification, lock-screen and Bluetooth controls, system media chip
- Shuffle and repeat (off → all → one)
- The queue survives a restart — position, order and modes are persisted
Design
Monochrome throughout. Hierarchy comes from type, never colour: EB Garamond
for track, album and artist names; Inter, light and tracked out, for labels
and timestamps. Both ship as single variable fonts. Timestamps use tabular
figures so a ticking clock doesn't jitter.
Known limitations
- Playlists will usually be empty. Android deprecated
MediaStore.Audio.Playlists
and then closed it to other apps' rows; on Android 11+ there is nothing to read.
Choir's own playlists arrive in v0.4.0. - Only formats Android decodes natively. FLAC, Opus, WMA, APE and WavPack come
with FFmpeg in v0.4.0. - No lyrics yet — that's v0.3.0.
- The UI is functional, not yet the intended one. The iPod-style hierarchy,
paper-grain texture and hand-drawn icon set land in v0.5.0.
Requirements
Android 10 (API 29) or newer.
Licence
GPL-3.0-or-later. Derived from the Apache-2.0 AOSP Music app — see NOTICE.