omnibase / music
A musician's site on omnibase: discography with the label in front, repertoire, videos, instrument, performers, and a dependency-free player - the listening links through glitchr/omnisong.
Package info
github.com/glitchr-studio/omnibase-music
Type:symfony-bundle
pkg:composer/omnibase/music
Requires
- php: ^8.2
- doctrine/orm: ^2.15|^3.0
- glitchr/omnibase: 3.0.*-dev
- glitchr/omnisong: 1.x-dev
- omnibase/admin: dev-main
- symfony/config: ^6.0|^7.0|^8.0
- symfony/dependency-injection: ^6.0|^7.0|^8.0
- symfony/form: ^6.0|^7.0|^8.0
- symfony/http-kernel: ^6.0|^7.0|^8.0
- symfony/process: ^6.0|^7.0|^8.0
- symfony/security-bundle: ^6.0|^7.0|^8.0
- symfony/twig-bundle: ^6.0|^7.0|^8.0
Requires (Dev)
- phpunit/phpunit: ^11.0
- symfony/validator: ^6.0|^7.0|^8.0
Suggests
- ffmpeg: The ffmpeg binary on the server computes the tracks' waveforms (bin/console music:peaks).
Provides
None
Conflicts
None
Replaces
None
This package is auto-updated.
Last update: 2026-10-03 16:12:24 UTC
README
A musician's site on omnibase: the records and the label that released them, the repertoire, the films, the instrument, the people who play along - and one player for the whole page, with no dependency. Written first for two classical musicians: a harpist whose albums came out on ES-DUR, a violinist whose sonatas came out on FARAO classics. The label goes first, everywhere: its logo next to the sleeve, "released by", "buy it at the label" as the first button of the page, and only then the streaming platforms.
Six things, each an entity, its back office, its pages and its Twig:
- Discography. A
Releaseis an omnibaseThread(a JOINED subclass), so it inherits what every thread has - a title, a headline, an excerpt, the text (here the liner notes), a slug, publish states and scheduling, translations - and adds what a record is: its type (album, single, EP, live, compilation), itsLabeland its catalogue number there, its UPC, its date ("announced", with a pre-save link, before it is out), its sleeve, itsTracks, who plays on it, the prizes it won (one a line: "CD of the year — Radio România Muzical") and its links, one per platform. ALabelknows its site, its shop, and how the page of one of its records is written -https://www.es-dur.de/releases/{catalogue}, with{catalogue},{upc}or{slug}- so a record needs only its number to link to its label; a record's ownlabelUrlwins over the pattern.Release::getPlatformLinks()gives Omnisong'sPlatformLinkswith the label first, then its shop, then the platforms by weight. ATrackhas its disc and place, its title - or theWorkit is and the movement - its length, its ISRC, the site's own excerpt (sample, an upload), a catalogue's 30 s preview (previewUrl) and the excerpt's waveform (peaks). - Repertoire. A
Work: the composer, the title and its number ("op. 74"), the year, the instrumentation ("harp and orchestra"), the formation (solo, chamber, concerto, orchestra, vocal), the period, the length, the movements./repertoirelists the visible ones by formation, by composer (by surname) or by period (?by=). - Videos. A
Videois aThreadtoo: a file of the site's, or a YouTube or Vimeo address (the id is read off it), a poster, where and when it was filmed, the work played, the record it belongs to. A platform's iframe never loads with the page: the poster and a play button first, then youtube-nocookie.com or Vimeo withdnt=1. - Instrument. An
Instrument: "Francesco Ruggeri", a violin, Cremona 1690, its model, whose hands it went through ("ex-…"), the foundation that lends it, a picture.music_instrument()gives the featured one;/instrumentshows the visible ones. - Performers. A
Performer- a person, an ensemble, an orchestra, a conductor - and their role ("piano"), shared by the records and the films:music_performers(release)writes "Guillaume Vincent, piano". - Playlists. A
Playlist: a playlist or an artist profile on Spotify, Apple Music, Deezer, YouTube or SoundCloud (the platform is read off the address) - the musician's own (OWN), the musician's picks (RECOMMENDED), or the musician's profile (ARTIST, its top tracks)./musicopens with "Listen in full": the profile, then the playlists.
Install
composer require omnibase/music:dev-main
// config/bundles.php Omnisong\Bridge\Symfony\OmnisongBundle::class => ['all' => true], Base\Music\MusicBundle::class => ['all' => true],
# config/routes.yaml music_controller: resource: "@MusicBundle/src/Controller/Client" type: attribute prefix: /
# config/packages/music.yaml (every key optional) music: artist: null # the musician's name for the JSON-LD; null: the site's title player: waves: true # the static waveform behind the progress live_waves: true # the bar's wave follows the sound (Web Audio) order: [file, preview, embed] # what a track plays from, first found first full: [spotify, apple_music, deezer] # "Listen in full", in this order theme: light # the theme the platforms' players are asked for label_first: true # the label before the platforms country: 'DE' # the store the catalogues are asked about jsonld: true # a schema.org MusicAlbum on a record's page repertoire: enabled: true group_by: formation # formation, composer or period instrument: enabled: true
Then bin/console doctrine:migrations:diff && bin/console doctrine:migrations:migrate
and bin/console assets:install (the stylesheet lives in public/css/music.css,
the player in public/js/player.js).
What the host provides
The templates extend layout1.html.twig and fill title, description,
content, stylesheets and javascripts: that is the whole contract.
The pages are /music (music_index), /music/{slug} (music_release),
/repertoire, /videos, /videos/{slug} and /instrument, each in the
sitemap but the single film.
The player's bar is the host's: {{ music_player_bar() }} once in its
layout, before </body>, so the music goes on from page to page of a
Turbo site and is there on the home page. It loads player.js itself.
Its cross ([data-music-close], or Escape with the focus in the bar) stops
the music and hides the bar - MusicPlayer.close(), the music:close
event; playing anything brings it back. html.music-is-playing while it
sounds, html.music-playing-bar while the bar shows.
Twig, for a host's own pages:
music_player(release) |
"Listen in full" on the platforms, then the tracks with their play buttons and waveforms |
music_player(release, {fold: true}) |
the same for a page showing several records: one "listen" button ([data-music-release-play]), the tracks behind a disclosure |
music_player(track) |
one track's button |
music_links(release) |
the label block, then the platforms as pills |
music_full(release) |
"Listen in full on Spotify / Apple Music / Deezer" alone |
music_embed(release, platform?), music_listen_embed(release, platform?) |
one platform's player of the record, on a click |
music_latest(n, type?) ('album': the albums, not the singles between them), music_releases(), music_featured(), music_upcoming() |
the records |
music_videos(n), music_hero_video() |
the films; the home page's film |
music_playlists(kind?), music_playlist_embed(playlist) |
the playlists (own, recommended, artist) and one's player |
music_instrument(), music_performers(release|video) |
the instrument; the line of who plays |
x|music_duration |
seconds as a sleeve prints them: 4:07, 1:02:45 |
The look is music.css, on custom properties a site sets to its own:
--music-accent, --music-bg, --music-fg, --music-soft, --music-line,
--music-surface, and the waves' --music-wave and --music-wave-played.
The back office gets the Release, Label, Work, Video, Instrument,
Performer and Playlist CRUDs, and a dashboard widget, music_latest:
the last record out, the next one announced, and how many tracks the
player has nothing to play for.
Omnisong: the catalogues
The links, the previews and the embeds come from
glitchr/omnisong: one
contract over song.link (Odesli) for the links to every platform, the
iTunes Search API for the tracks and their 30 s previews, and the
iframes of Spotify, Apple Music, Deezer, YouTube and SoundCloud. Its
bundle registers Omnisong\Catalog\Catalog, Omnisong\Player\EmbedderInterface
and Omnisong\Player\EmbedOptions; this one only asks them.
On a record in the back office, Complete asks the catalogues what they
know of it - by its UPC, else its first platform link - and Look up a
record makes one from a single URL or UPC: the links to every platform,
the UPC, the sleeve (kept as coverUrl, a remote address, until one is
uploaded), the date, the label when it has none and the catalogue names
one, and the tracks - matched by their ISRC, else their disc and place -
with their length and preview. Nothing typed by hand is overwritten:
only empty fields are filled, and the label's link never comes from a
catalogue. A catalogue that does not answer is not "not found": the flash
says which one was down, try again later. Looked up today, for example:
- Anaëlle Tourret, Perspectives Concertantes - iTunes 1793146044, 28 February 2025, ℗ C2 Hamburg, on ES-DUR;
- Anaëlle Tourret, Perspectives - iTunes 1594656563, 12 November 2021;
- Brieuc Vourch & Guillaume Vincent, Strauss & Franck: Sonatas for Violin and Piano - iTunes 1576897066, 17 July 2021, ℗ FARAO classics.
The player
One <audio> for the whole page. A track plays, in the order
music.player.order gives, the site's own excerpt (file), else a
catalogue's preview (preview), else - nothing to play here - the record's
player on a platform (embed), on the click. The bar shows what plays,
play/pause, the previous and next track of the same record, the progress
on the waveform, the time, the sound; space plays or pauses, the
arrows seek five seconds, Shift+arrows change track.
The waveform is static: 200 bars, computed once on the server from the
excerpt and kept on the track (peaks), drawn on a <canvas> with no
decoding in the browser - the played part in --music-wave-played, the
rest in --music-wave. With live_waves, the bar's canvas follows the
sound instead (an AnalyserNode), unless the visitor asks for less motion.
Listen in full. A 30 s preview is a taste; a play on Spotify, Apple
Music or Deezer by a listener logged in there is a stream, and counts for
the musician - in the platform's statistics and its payments. So beside the
previews, on a record's page and in the bar while one of its tracks plays,
"Listen in full on Spotify / Apple Music / Deezer" swaps in the platform's
own player of the whole record (music.player.full says which platforms,
in which order). Nothing loads from a platform before that click.
The player's JavaScript API
player.js exposes window.MusicPlayer, for a site that draws its own
visualisation or drives the player from its own buttons:
audio |
the <audio> element playing now |
analyser |
the AnalyserNode, made on the first play a visitor asked for - even without live_waves - and null before (or when the browser has no Web Audio) |
context |
its AudioContext |
current |
the current track: {src, title, release, peaks}, or null |
play(el | data) |
plays a [data-music-track] element, or {src, title, release, peaks} |
pause(), toggle(), seek(seconds), next(), prev() |
|
full(release, platform?) |
opens "listen in full" for a record (its slug) |
redraw() |
draws the waveforms again (after a host changed the page) |
and dispatches on document:
detail |
|
|---|---|
music:track |
the track that starts |
music:play, music:pause, music:ended |
the current track |
music:time |
{currentTime, duration}, about four times a second |
music:full |
{release, platform}: a platform's player replaced the preview |
document.addEventListener('music:play', () => { const { analyser } = window.MusicPlayer; if (!analyser) return; const data = new Uint8Array(analyser.frequencyBinCount); (function frame() { analyser.getByteFrequencyData(data); /* draw */ requestAnimationFrame(frame); })(); });
A preview from another site (iTunes') goes through Web Audio only when that site answers with CORS; when it does not, it plays on a plain element, without the live wave.
The hero film
{{ music_hero_video() }} on the home page: a film muted, looping and
playing by itself (autoplay muted loop playsinline), with a button to
hear it, and its poster alone for a visitor who asks for less motion.
Which film and which poster are two settings of the back office,
music.hero.video and music.hero.poster (an upload or an
address); with neither, nothing is rendered.
The waveforms: ffmpeg
bin/console music:peaks # the excerpts without a waveform yet bin/console music:peaks --all # every excerpt again bin/console music:peaks --track 12
or the Waveforms button of a record in the back office. ffmpeg decodes
the excerpt to raw mono PCM at 8 kHz (ffmpeg -i in -ac 1 -ar 8000 -f s16le -)
and the samples are reduced to 200 loudness values (RMS, the loudest 1).
Without ffmpeg on the server (apt install ffmpeg, apk add ffmpeg)
nothing is computed - it is said and logged - and the player draws a flat
line instead. A new excerpt forgets the old waveform.
Tests
tests/ runs without a kernel: the waveform's reduction (Peaks::reduce()
on a synthetic buffer), the label first in Release::getPlatformLinks()
(its pattern, the record's own page, its shop), the importer filling only
what is empty, durations, the repertoire's grouping, the films' addresses,
the playlists' platforms and the links' form.