The .vybe file format, version 1
A .vybe file holds one song and everything that belongs with it: metadata,
cover art, lyrics, credits, a waveform, the full song in one or more codecs and
short preview clips. It is one small file that any app can read with a few
lines of code.
This document is the full specification. The reference reader and writer is
src/vybe.js in this kit (MIT license). Home page:
https://vybemusic.com/open/
| Property | Value |
|---|---|
| Extension | .vybe |
| MIME type | application/vnd.vybe (suggested) |
| Byte order | Little endian for every integer |
| Text | UTF-8 |
| Current version | 1 |
1. Layout
A file is a fixed size header, an index with one entry per chunk, then the chunk bytes.
| Offset | Size | Field |
|---|---|---|
| 0 | 4 | Magic, ASCII VYBE (56 59 42 45) |
| 4 | 2 | u16 version, 1 |
| 6 | 2 | u16 file flags. bit0: the file has a SIGN chunk. Other bits are 0 |
| 8 | 4 | u32 N, the number of chunks |
| 12 | 28 x N | Index, one 28 byte entry per chunk |
| 12 + 28 x N | ... | Chunk bytes |
Index entry (28 bytes):
| Offset | Size | Field |
|---|---|---|
| +0 | 4 | Chunk type, 4 printable ASCII characters (for example AOPS) |
| +4 | 4 | u32 chunk flags, meaning depends on the type |
| +8 | 8 | u64 offset of the chunk bytes, from the start of the file |
| +16 | 8 | u64 length of the chunk bytes |
| +24 | 4 | u32 CRC32 of the chunk bytes (IEEE 802.3 polynomial 0xEDB88320, the same CRC as zip and PNG) |
Rules for the chunk area:
- Chunks have no header of their own. The index is the only description.
- Each chunk starts on a 4 byte boundary. Gaps between chunks are zero bytes.
METAis always the first chunk in the index and the first in the file.- Writers order the other chunks small first:
PEAK,ARTW,POPS,PAAC,PMP3,AOPS,AAAC,AMP3, then unknown types, thenSIGNlast. Readers must not depend on the order (except thatMETAis first). - A chunk type appears at most once. If a reader finds a type twice, it uses the first entry.
- Offsets and lengths are u64. Readers in languages without 64 bit integers (JavaScript) may refuse values above 2^53.
2. Chunk types
| Type | Content | Flags |
|---|---|---|
META |
UTF-8 JSON object with the song metadata (section 3). Required. | 0 |
ARTW |
Cover image, at most 1400 px on the long side. | bit0 = webp, otherwise jpeg |
AOPS |
Full song, Ogg Opus (RFC 7845), 48 kHz, VBR. 160 kbps stereo or 96 kbps mono for lossless sources; lower for lossy sources (never more than about 1.15 x the source bitrate). | 0 |
POPS |
Preview clip, Ogg Opus. Default 60 s with short fades in and out. | 0 |
AAAC |
Full song, AAC-LC in ADTS frames, 48 kHz, 192 kbps stereo or 128 kbps mono. For AVPlayer and older players. | 0 |
PAAC |
Preview clip, AAC-LC in ADTS. | 0 |
AMP3 |
Full song, MP3. Written when the source is MP3 and it is kept as is (the original MP3 frames, ID3 tags removed), or when no other encoder was available. | 0 |
PMP3 |
Preview clip, MP3, cut on frame boundaries (no fades). | 0 |
PEAK |
Waveform: 2000 bytes, each 0..255, the peak level of each slice of the full song, normalized to the loudest point. | 0 |
SIGN |
Signature written by the VYBE server. Opaque to everyone else. | 0 |
Every chunk except META is optional. A valid file has at least one audio
chunk (AOPS, AAAC or AMP3), but readers should cope with files that have
none (they show metadata only).
The SIGN chunk
SIGN is written only by the VYBE server when a song is published through
VYBE. Its content is not part of this specification and can change at any
time. Readers treat it as opaque bytes: keep it when copying a file, never
create one, and do not try to interpret it. File flag bit0 is set when the
file has a SIGN chunk. A file without SIGN is still a complete, valid
.vybe file. Editing any other chunk of a signed file makes the signature
invalid, so tools that change a signed file should drop SIGN and clear
bit0.
3. META
META is a JSON object. Writers include every key below; values are null
when unknown. Readers must ignore keys they do not know.
{
"format": "vybe", "v": 1,
"title": "Neon Tide",
"artist": {"name": "Ayo Lumen", "slug": "ayo-lumen"},
"album": "Night Drive", "track_no": 3, "disc_no": 1,
"duration": 215.42, "sample_rate": 44100, "channels": 2,
"bpm": 122, "bpm_confidence": 0.93,
"key": "A minor", "camelot": "8A", "key_confidence": 0.81,
"energy": 0.72, "danceability": 0.88, "valence": 0.31,
"loudness_lufs": -9.4, "loudness_range": 5.1, "true_peak_db": -0.8,
"spectral_centroid": 2210.4,
"vybes": ["latenight", "hype"], "vybe_scores": {"latenight": 0.91, "hype": 0.77},
"mood": "Late Night", "genre": "House", "language": "en", "explicit": false,
"lyrics": {"plain": "...", "synced": [{"t": 12.4, "line": "..."}]},
"beats": {"first": 0.482, "period": 0.4918, "downbeat": 0.482},
"sections": [{"start": 0, "end": 16.2, "label": "intro", "energy": 0.6}],
"drop": 63.9,
"preview": {"start": 47.2, "len": 60},
"energy_curve": [0.41, 0.44],
"credits": [{"name": "Ayo Lumen", "role": "producer"}],
"rights": {"master_owner": null, "publishing_owner": null},
"license": {"share_partners": true, "commercial": false, "territories": ["WW"]},
"isrc": "USABC2600001", "upc": null,
"created": "2026-10-21T09:00:00.000Z",
"encoder": "VYBE Studio 1",
"source": {"format": "wav", "sample_rate": 44100, "bits": 24, "bytes": 38211044, "sha256": "..."},
"fingerprint": "vf1.AbC...",
"streams": {
"AOPS": {"codec": "opus", "kbps": 160, "container": "ogg", "sample_rate": 48000, "channels": 2, "pre_skip": 312},
"POPS": {"codec": "opus", "kbps": 160, "container": "ogg", "preview": true, "start": 47.2, "len": 60}
}
}
Fields
| Field | Type | Notes |
|---|---|---|
format, v |
string, number | Always "vybe" and 1. |
schema |
string | "1.1" for files with the full song information (section 4). Missing means "1.0". |
title |
string | Song title. |
artist |
object | {name, slug}. Older or hand made files may hold a plain string; readers accept both. Schema 1.1 adds artists and display_artist (section 4). |
album, track_no, disc_no |
string, number, number | |
duration |
number | Seconds, of the full song. |
sample_rate, channels |
number | Of the source recording (streams are always 48 kHz). |
bpm, bpm_confidence |
number | Tempo and a 0..1 confidence. |
key, camelot, key_confidence |
string, string, number | For example "A minor", "8A", 0..1. |
energy, danceability, valence |
number | 0..1. |
loudness_lufs, loudness_range, true_peak_db |
number | Integrated loudness (EBU R128), loudness range in LU, true peak in dBTP. |
spectral_centroid |
number | Brightness, in Hz. |
vybes, vybe_scores, mood |
array, object, string | 1 to 3 VYBE ids, best first, their 0..1 scores, and the display name of the first one. |
genre, language, explicit |
string, string, boolean | language is an ISO 639-1 code. |
lyrics |
object | {plain, synced:[{t, line}]}. t in seconds. Schema 1.1 adds sections and structure (section 4). |
beats |
object | {first, period, downbeat} in seconds: first beat, beat length, first downbeat. |
sections |
array | {start, end, label, energy}. label is one of intro, verse, chorus, bridge, breakdown, drop, outro. |
drop |
number | Seconds, the main drop, or null. |
preview |
object | {start, len} in seconds: the window the preview clips were cut from. |
energy_curve |
array | 200 numbers 0..1 across the song. |
credits |
array | {name, role}. Schema 1.1 adds role_label and a fixed role vocabulary (section 4). |
rights |
object | {master_owner, publishing_owner}. |
license |
object | {share_partners, commercial, territories}. Apps must check share_partners before playing a VYBE catalog song outside VYBE. |
isrc, upc |
string | |
created |
string | ISO 8601 time the file was made. |
encoder |
string | The tool that made the file, for example "VYBE Studio 1", "vybe-format 1.1", "vybe-cli 1.1". |
source |
object | {format, sample_rate, bits, bytes, sha256} of the original upload. |
fingerprint |
string | vf1. plus base64url of one little endian u16 per second (12 chroma shape bits and 4 band energy trend bits). Compare two with VybeAnalyze.similarity(a, b): 0..1, 0.9 and up means the same recording. |
streams |
object | One entry per audio chunk type: codec, kbps, and container, preview, start, len, pre_skip, original, fades where they apply. |
VYBE ids:
| id | Name | id | Name |
|---|---|---|---|
euphoric |
Happy | dreamy |
Sleep |
chill |
Chill | hype |
Party |
latenight |
Late Night | deep |
Sad |
sunset |
Road Trip | fuel |
Workout |
focus |
Focus | crush |
Love |
4. Song information (schema 1.1)
Files made with version 1.1 of the kit, and by VYBE Studio, carry the full
song information: every artist and what they did, the release, the rights
lines and the lyrics with their parts. These fields live in META like all
the others, so the file format version stays 1. A file that has them says
"schema": "1.1".
Old files keep working. Files from before 1.1 have no schema key (read it as
"1.0"), only artist, maybe credits and lyrics.plain. Readers must
handle both. The easy way is VybeInfo.read(meta) from src/vybe-info.js,
or file.info() from src/vybe.js: both give the same shape for any file, old
or new. Writers of schema 1.1 still write artist (see below) so that older
readers show the right name.
Example
{
"format": "vybe", "v": 1, "schema": "1.1",
"title": "Midnight Drive", "version": "Radio Edit",
"artists": [
{"name": "Nia Vale", "role": "main", "slug": "nia-vale"},
{"name": "Kai Moro", "role": "main", "slug": "kai-moro"},
{"name": "Juno", "role": "featured", "slug": "juno"}
],
"display_artist": "Nia Vale & Kai Moro feat. Juno",
"artist": {"name": "Nia Vale & Kai Moro feat. Juno", "slug": "nia-vale"},
"credits": [
{"name": "Nia Vale", "role": "lyricist", "role_label": "Lyricist"},
{"name": "Sam Ortiz", "role": "producer", "role_label": "Producer"},
{"name": "Ivy Chen", "role": "mastering_engineer", "role_label": "Mastering engineer"},
{"name": "Omar Diaz", "role": "guitar", "role_label": "Guitar"}
],
"people": {
"main_artists": ["Nia Vale", "Kai Moro"], "featured_artists": ["Juno"],
"lyricists": ["Nia Vale"], "composers": [], "singers": [],
"producers": ["Sam Ortiz"], "engineers": ["Ivy Chen"]
},
"album": "Night Roads", "album_artist": null,
"release": {
"title": "Night Roads", "type": "album", "date": "2025-06-13", "year": 2025, "original_year": null,
"track_no": 2, "track_total": 10, "disc_no": 1, "disc_total": 1,
"label": "Moonlit Records", "upc": "0123456789012", "catalog_no": "MLR-022"
},
"label": "Moonlit Records", "publisher": null, "year": 2025,
"copyright": {"c": "2025 Nia Vale", "p": "2025 Moonlit Records"},
"isrc": "USABC2512345", "iswc": null, "upc": "0123456789012",
"genre": "R&B", "subgenres": ["Alternative R&B"], "language": "en", "explicit": false,
"comment": null, "recorded": null, "tags": [],
"declared": {"bpm": 118, "key": "A major"},
"lyrics": {
"plain": "yeah yeah\n\nCity lights are calling out my name\n...",
"synced": [{"t": 12.4, "line": "City lights are calling out my name", "section": "Verse 1"}],
"sections": [
{"type": "intro", "label": "Intro", "n": null, "performer": null, "start": 1.2, "end": 12.4, "lines": ["yeah yeah"], "inferred": false},
{"type": "verse", "label": "Verse 1", "n": 1, "performer": "Nia", "start": 12.4, "end": 30.1, "lines": ["City lights are calling out my name", "..."], "inferred": false},
{"type": "hook", "label": "Hook", "n": null, "performer": null, "start": 30.1, "end": 45.8, "lines": ["We ride we ride into the night", "..."], "inferred": false}
],
"structure": ["Intro", "Verse 1", "Hook"],
"language": "en",
"writers": ["Nia Vale"]
},
"links": {}
}
Fields
| Field | Type | Notes |
|---|---|---|
schema |
string | "1.1". Missing means "1.0". |
title |
string | The title only, without "(feat. X)", "[Prod. Y]" or the version. |
version |
string | The version of the recording, for example "Radio Edit", "Acoustic", "Remix". |
artists |
array | {name, role, slug}. role is main, featured or remixer. In the order they are credited. |
display_artist |
string | How to show the artists in one line: "A & B feat. C". |
artist |
object | {name, slug}, kept for older readers: name is the display artist, slug is the first main artist. |
credits |
array | {name, role, role_label}. role is an id from the table below; a role that is not in the table is kept as written. role_label is the English label, so simple readers can show it as is. One person can have several credits. |
people |
object | The same credits as lists of names, for quick use: main_artists, featured_artists, lyricists (lyricist and songwriter), composers (composer and songwriter), singers (vocals, rapper, background vocals, performer), producers (all producer roles), engineers (all engineer roles). |
album, album_artist |
string | album mirrors release.title. |
release |
object | {title, type, date, year, original_year, track_no, track_total, disc_no, disc_total, label, upc, catalog_no}. type is single, ep, album, compilation or null. date is YYYY-MM-DD. |
track_no, disc_no |
number | Mirror release.track_no and release.disc_no (as in schema 1.0). |
label, publisher |
string | Record label, and the music publisher when known. |
year |
number | Release year. Mirrors release.year. |
copyright |
object | {c, p}: the copyright line for the song (written with a (c) sign) and for the recording (written with a (p) sign), without the sign. Null when unknown. |
isrc, iswc, upc |
string | Recording code, work code, release barcode. |
genre, subgenres |
string, array | Main genre and up to 5 more. |
language, explicit |
string, boolean | |
comment, recorded, tags |
string, string, array | A free note, where it was recorded, free tags. |
declared |
object | {bpm, key} as the source file's tags said them, or null. The measured bpm and key stay the main values. |
lyrics |
object | See below. Null when the song has no lyrics. |
links |
object | Free links, for example {"video": "https://..."}. |
lyrics in schema 1.1:
| Field | Type | Notes |
|---|---|---|
plain |
string | The words, without section markers. Blank lines between parts. |
synced |
array | Timed lines {t, line, section}. t in seconds, section is the label of the part the line is in (when known). Empty when there are no timed lyrics. |
sections |
array | The parts of the song in order: {type, label, n, performer, start, end, lines, inferred}. type is an id from the section table, label is what to show ("Verse 2"), n the number, performer who sings it (from markers like [Verse 1: Nia]), start and end in seconds when timed lyrics place it (else null), lines the lines of that part. inferred is true when the part was found from repeats instead of written markers. |
structure |
array | The section labels in order, for example ["Intro", "Verse 1", "Pre Hook", "Hook", "Verse 2", "Hook", "Outro"]. |
language, writers |
string, array | Language of the words, and the lyricist and songwriter names. |
The sections key at the top level of META (from the audio analysis, with
intro, verse, chorus, drop and so on, measured from the sound) is a
different thing and stays as it was.
Credit roles
Writers map free text roles onto these ids ("Produced by", "Prod.", "Beat maker"
become producer; "Mixed by" becomes mixing_engineer, and so on).
| id | Label | id | Label |
|---|---|---|---|
main_artist |
Main artist | programmer |
Programmer |
featured_artist |
Featured artist | conductor |
Conductor |
remixer |
Remixer | dj |
DJ |
vocals |
Singer | guitar |
Guitar |
rapper |
Rapper | bass |
Bass |
background_vocals |
Background vocals | drums |
Drums |
performer |
Performer | keys |
Keys |
lyricist |
Lyricist | piano |
Piano |
composer |
Composer | synth |
Synth |
songwriter |
Songwriter | strings |
Strings |
producer |
Producer | percussion |
Percussion |
co_producer |
Co-producer | saxophone |
Saxophone |
executive_producer |
Executive producer | trumpet |
Trumpet |
additional_producer |
Additional producer | violin |
Violin |
mixing_engineer |
Mixing engineer | cello |
Cello |
mastering_engineer |
Mastering engineer | instruments |
Instruments |
recording_engineer |
Recording engineer | artwork |
Artwork |
engineer |
Engineer | video_director |
Video director |
arranger |
Arranger |
Lyric sections
Artists say hook, so the chorus is called hook. Markers like [Chorus],
(Pre-Chorus), Refrain:, [Hook x2], {Bridge} or **Drop** are all read.
| id | Label | id | Label |
|---|---|---|---|
intro |
Intro | breakdown |
Breakdown |
verse |
Verse | interlude |
Interlude |
pre_hook |
Pre Hook | refrain |
Refrain |
hook |
Hook | instrumental |
Instrumental |
post_hook |
Post Hook | spoken |
Spoken |
drop |
Drop | skit |
Skit |
build |
Build | outro |
Outro |
bridge |
Bridge |
Lyrics text with markers
Tools that let people edit lyrics use one plain text form: each part starts with its label in square brackets on its own line, with an optional performer.
[Intro]
yeah yeah
[Verse 1: Nia]
City lights are calling out my name
Every road is running through the rain
[Pre Hook]
Hold on hold on
[Hook]
We ride we ride into the night
VybeInfo.parseLyrics(text) turns this into sections, and
VybeInfo.lyricsText(sections) turns sections back into this text. When the
text has no markers, VybeInfo.inferSections(text) finds the parts: the stanza
that repeats most is the hook, a different repeated stanza that always comes
right before it is the pre hook, and a short first or last stanza is the intro
or outro.
Where writers find the information
The converter in this kit (VybeInfo.capture) reads, in this order:
- every tag in the source file: ID3 (MP3, WAV, AIFF) including
TXXXuser frames,TIPLandTMCLcredit lists andSYLTtimed lyrics; Vorbis comments (FLAC, Ogg), where severalARTISTvalues mean several artists; MP4 atoms (M4A) including freeform----atoms; WAVLIST/INFO; - the file name, for example
03 - Artist - Title (feat. X).wav; - the title itself: "(feat. X)", "[Prod. Y]", "(Radio Edit)" and "(Explicit)" are moved to the right fields;
- a
.lrcfile (timed lyrics) and a.txtfile (lyrics) with the same name as the song, andcover.jpgorfolder.jpgin the same folder; - the audio analysis for BPM, key and VYBEs. When the tags say a different BPM
or key, the measured value is kept, the tag value goes to
declared, and the converter shows a check.
5. Picking a stream
A file can carry the same song in up to three codecs. Readers pick the first one they can play, in this order:
- Opus (
AOPS, previewPOPS): smallest for the same quality. Plays in Chrome, Edge, Firefox, Android and recent Safari. - AAC (
AAAC, previewPAAC): for iOS AVPlayer, older Safari and hardware players. - MP3 (
AMP3, previewPMP3): plays everywhere.
In a browser, test Opus with audio.canPlayType('audio/ogg; codecs="opus"').
WebM Opus support alone is not enough. Vybe.canPlay(codec) does this for you,
and file.audio() returns the best playable stream.
Previews: when a preview would cover 80% or more of the song, writers skip the
preview chunks. Readers then play the full stream from meta.preview.start
for meta.preview.len seconds (the SDK returns fullStream: true).
MIME types for the stream bytes:
| Chunks | MIME type |
|---|---|
AOPS, POPS |
audio/ogg; codecs=opus |
AAAC, PAAC |
audio/aac |
AMP3, PMP3 |
audio/mpeg |
ARTW |
image/webp (flag bit0) or image/jpeg |
META |
application/json |
6. Reading only what you need (HTTP Range)
The whole index sits in the first 12 + 28 x N bytes. The first 1024 bytes
always hold the index of a file with up to 36 chunks, and usually META too.
A player that wants a preview card does not need to download the song:
GET song.vybewithRange: bytes=0-1023. Check the magic and version, readNand the index. If12 + 28 x Nis more than you received, requestbytes=0-(12 + 28 x N - 1).- Find
METAin the index. Ifoffset + lengthis inside the bytes you already have, parse it from there; otherwise request exactlybytes=offset-(offset + length - 1). - Request only the chunks you need (for example
ARTW,PEAKandPOPS), one Range request each. Check each chunk against its CRC32 if integrity matters.
If the server ignores Range and answers 200 with the whole file, use the
whole file. Vybe.open(url) in src/vybe.js implements all of this and
caches chunks. Servers can also proxy a single stream: remote.range('AOPS')
returns {start, end} for one upstream Range request.
7. Writing
- Write
METAfirst, then the other chunks, each on a 4 byte boundary with zero padding. - Compute CRC32 over each chunk's exact bytes.
- Set
ARTWflag bit0 when the image is webp. - Never write a
SIGNchunk, and keep file flag bit0 at 0. Vybe.pack({meta, chunks})does all of this.- To change only the song information of a finished file, rewrite
METAand copy every other chunk byte for byte:Vybe.withMeta(file, meta)orvybe edit. Drop anySIGNchunk, since it signed the oldMETA.
8. Versioning and compatibility
- New chunk types and new META fields can be added within version 1. Readers must ignore chunk types and META fields they do not know, and tools that copy files should keep unknown chunks.
- A change that would break existing readers (a different header or index layout, or a different meaning for an existing chunk type) bumps the version number. Readers refuse versions they do not support.
META.schemanames the version of the META fields ("1.1"now). It only ever adds fields, so a reader for schema 1.1 reads 1.0 files, and an older reader still findstitle,artistandlyrics.plainin 1.1 files.
9. Verifying a file
A file is valid when:
- It starts with
VYBEand its version is supported. - The index fits in the file, and every chunk lies inside the file after the index, without overlapping another chunk.
- The first chunk is
METAand it is a UTF-8 JSON object. - Every chunk matches its CRC32.
- File flag bit0 is set exactly when there is a
SIGNchunk.
vybe verify file.vybe from this kit checks all of these and exits with code 1
when the file is not valid.
This specification and the reference code are open source under the MIT license. Download them with the .vybe kit, or convert a song to see a real file.