Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
10 changes: 10 additions & 0 deletions apps/locallibrary/CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -285,6 +285,16 @@ Windows builds are experimental and untested on hardware (see Platforms in the R
- `coverart`: finds front covers on MusicBrainz and the Cover Art Archive for `art fetch`, an
album that could be several release groups offered as each of them. It's the only one that
goes online, and only when it's run.
- **Beat grids, phrases and waveforms written to drives.** locallibrary writes rekordbox's beat
grid (`PQTZ`), phrases (`PSSI`, masked as rekordbox 6 writes them) and waveforms (`PWAV`,
`PWV2`, `PWV3`, `PWV5`, `PWV6`, `PWV7`) into a track's analysis files as a journaled, undoable
edit (`set_analysis`), each read back before it's written, rewriting rekordbox's own files byte
for byte where the data is the same, and reads 3-band waveforms (`PWV6`, `PWV7`) for a track
with no others. An analyzer plugin may work out a track's grid, phrases and waveform (fields of
type `beats`, `phrases`, `waveform`, feature `analysis`): kept beside its columns, drawn in the
player where the drive has none, and written with `analyze --apply grid,phrases,waveform` (the
sidecar's `plugins.apply_analysis.plan/apply`), a grid setting the track's BPM; operations plan
the same edit with `analysis:plan`. Checked against rekordbox's files, not yet on players.
- **The library is the unit.** Each drive holding a library is a clone of it, and its name doesn't
matter much: a library's name works where a drive does (but in commands about one particular
drive) and means its lead clone, the newest of its drives plugged in (the primary when they're
Expand Down
2 changes: 1 addition & 1 deletion apps/locallibrary/docs/commands.md
Original file line number Diff line number Diff line change
Expand Up @@ -139,7 +139,7 @@ its drives.
| `locallibrary export <library> [PLAYLIST] --format traktor\|serato\|engine -o DIR [--option KEY=VALUE]... [--dry-run]` | The library for other DJ software (the interop plugin's), written on this computer, see [Other DJ software](other-software.md#other-dj-software) |
| `locallibrary export <library> [PLAYLIST] --format FORMAT -o PATH [--option KEY=VALUE]... [--dry-run] [--replace]` | A format a plugin adds; overwriting a file is asked about first unless `--replace`, see [Plugins](plugins.md) |
| `locallibrary import FILE [<library>] [--format FORMAT] [--option KEY=VALUE]... [--dry-run]` | Read another program's file with a plugin's importer: keys, BPMs, cues and My Tags added to the tracks it finds where they have none, plays kept as sessions, see [Plugins](plugins.md#import-formats) |
| `locallibrary analyze [<library>] [--plugin ID] [--analyzer NAME] [--track ID]... [--apply key,bpm [--overwrite] [--min-confidence N] [--dry-run]]` | Run plugins' analyzers on a library's tracks, once each; `track` shows what they worked out as `PLUGIN.FIELD` (`sound.bpm`); `--apply` then writes the analyzer's keys and BPMs to the tracks as an edit, see [Plugins](plugins.md) |
| `locallibrary analyze [<library>] [--plugin ID] [--analyzer NAME] [--track ID]... [--apply key,bpm\|grid,phrases,waveform [--overwrite] [--min-confidence N] [--dry-run]]` | Run plugins' analyzers on a library's tracks, once each; `track` shows what they worked out as `PLUGIN.FIELD` (`sound.bpm`); `--apply` then writes the analyzer's keys and BPMs to the tracks, or its beat grids, phrases and waveforms into their analysis files, as an edit, see [Plugins](plugins.md) |
| `locallibrary xml [<library>] -o rekordbox.xml [--only PLAYLIST]` | Export as rekordbox XML (`--only`: just those playlists or folders and their tracks) |

**Editing**
Expand Down
6 changes: 3 additions & 3 deletions apps/locallibrary/docs/editing.md
Original file line number Diff line number Diff line change
Expand Up @@ -241,9 +241,9 @@ replay makes edits again in whichever lacks them. The recorded edits travel with
source drive's onto the drives it updates, so `refresh` makes edits made on any of the library's
drives again. A push or pack, which gives a drive a library from a snapshot or a selection, forgets
the edits recorded for the one it held (its plan says so). `edits.json` says which version of its
format it is, the lowest its edits need (artwork edits, and a BPM, album artist, ISRC or release
date, need the newest): a locallibrary older than that refuses to read it or add to it (and says
so) rather than misreading it. Builds from before it had a version read it as a plain
format it is, the lowest its edits need (artwork edits and a BPM, album artist, ISRC or release
date need 3; analysis edits and cue edits setting several cues need 4, the newest): a locallibrary
older than that refuses to read it or add to it (and says so) rather than misreading it. Builds from before it had a version read it as a plain
list: they change the drive, then fail to record the edit. Update every copy (the CLI and the
app's sidecar) before editing a drive this version has edited.

Expand Down
19 changes: 19 additions & 0 deletions apps/locallibrary/docs/plugins.md
Original file line number Diff line number Diff line change
Expand Up @@ -45,6 +45,25 @@ tracks, the playlist, the drive) starting as it (see [the player](player.md)). T
`plugins.*` methods (see [Building a GUI](sidecar.md)). Each kind is below, in a section of its
own.

## Analysis

An analyzer may also work out a track's beat grid, phrases and waveform (fields of type `beats`,
`phrases` and `waveform`). They're not columns: locallibrary keeps them in the store beside the
analyzer's other values, and the player draws them for a track whose drive has none, marked as not
on the drive. `locallibrary analyze [LIBRARY] --apply grid,phrases,waveform` writes them into the
tracks' analysis files, planned and confirmed as any edit is (`undo` takes it back, and `edits
replay` makes it again after rekordbox exports): only where a track has none, unless
`--overwrite`. A grid sets the track's BPM too, and its cues stay where they are (the plan says
how many sat on beats of the old grid). Phrases count the analyzer's own beats, so they're written
only with its grid, or onto the same one. A track rekordbox never analysed has no analysis files
to take them, and a waveform can't go where rekordbox's colour waveforms are (locallibrary can't
work out every byte of those). An operation can plan the same edit (`set_analysis`), with the
`analysis:plan` capability.

What locallibrary writes reads back as given, and rewriting what rekordbox wrote leaves its files
byte for byte as they were, but that's checked against rekordbox's files, not on players: try a
drive on yours before you rely on it.

## Analyzers

`locallibrary analyze [LIBRARY] [--plugin ID] [--analyzer NAME]` runs them, which works out each
Expand Down
209 changes: 152 additions & 57 deletions apps/locallibrary/docs/rpc/locallibrary.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -662,6 +662,21 @@ export type Edit =
tracks: TrackSelector;
image: ImageSource;
op: "set_artwork";
}
/**
* Write a beat grid, phrases or a waveform into the analysis files of the track of this
* audio file (a device path, see [`crate::analysis_edit`]). Planned, a grid brings the
* track's BPM with it (an `edit_tracks`); the journal records a waveform as staged on the
* drive.
*/
| {
track: string;
/** The beat grid: each beat's number in its bar (1-4), the tempo there and its time. */
beats?: Beat[] | null;
/** The phrases, named as the track's mood names them. */
phrases?: SongStructure | null;
waveform?: WaveSource | null;
op: "set_analysis";
};

/**
Expand Down Expand Up @@ -862,6 +877,61 @@ export type ImageSource =
/** A BLAKE3 hash, as 64 lowercase hex characters. */
export type Hash = string;

export type Beat = {
/** Position in the bar, 1-4. */
beat_number: number;
bpm: number;
time_ms: number;
};

/** rekordbox's phrase analysis (`PSSI`, in `.EXT`). */
export type SongStructure = {
mood: Mood;
phrases: Phrase[];
};

/** rekordbox's overall classification of a track, which decides how phrases are named. */
export type Mood = "high" | "mid" | "low";

export type Phrase = {
kind: PhraseKind;
/** 1-based beat numbers into the beat grid; the phrase runs up to (not including) `end_beat`. */
start_beat: number;
end_beat: number;
/** Beat where a fill-in leads into the next phrase. */
fill_beat?: number | null;
};

export type PhraseKind =
| ("intro" | "bridge" | "chorus" | "outro")
/** Build-ups, in high-mood tracks. */
| "up"
/** Breakdowns, in high-mood tracks. */
| "down"
| {
verse: number;
}
| {
unknown: number;
};

/**
* A waveform an analysis edit writes: three bands, each column's lows, mids and highs (0-255),
* 150 columns a second ([`Bands`]), from which each of the track's waveform tags is worked out.
*/
export type WaveSource =
/** The bands packed (three bytes a column), in base64. */
| {
base64: string;
}
/**
* Staged on the drive (`.locallibrary/waveforms/<hash>.bands`), by its hash: how the journal
* records the others.
*/
| {
staged: Hash;
};

export type ClearEdits = {
drive: string;
/** Only entries recorded before this time. */
Expand Down Expand Up @@ -2176,6 +2246,29 @@ export type SetPluginSetting = {
value?: unknown;
};

/** What to write to a drive's tracks' analysis files, from what one analyzer kept. */
export type ApplyAnalysis = {
plugin: string;
analyzer: string;
/** The library or drive whose tracks to write to. */
drive: string;
/** What to write, which the analyzer must work out: `grid`, `phrases`, `waveform`. */
parts: AnalysisPart[];
/** Only these tracks, by id [default: every one]. */
tracks?: number[] | null;
/** Also over what a track's analysis files have: without it, only where they have none. */
overwrite?: boolean;
};

/** A part of a track's analysis an analyzer can work out. */
export type AnalysisPart =
/** The beat grid (a `beats` field). */
| "grid"
/** The phrases (a `phrases` field). */
| "phrases"
/** The waveform (a `waveform` field). */
| "waveform";

export type RunPluginCommand = {
plugin: string;
command: string;
Expand Down Expand Up @@ -2905,13 +2998,6 @@ export type AnalysisView = {
structure: SongStructure | null;
};

export type Beat = {
/** Position in the bar, 1-4. */
beat_number: number;
bpm: number;
time_ms: number;
};

export type Cue = {
/** `None` for memory cues; `Some(0)` = hot cue A, `Some(1)` = B, ... */
hot_cue: number | null;
Expand Down Expand Up @@ -2951,7 +3037,12 @@ export type WaveformStyle =
/** Monochrome blue, older players (`PWAV`, `PWV3`). */
| "blue"
/** Colour by frequency content, nexus 2 and later (`PWV4`, `PWV5`). */
| "rgb";
| "rgb"
/**
* Lows, mids and highs drawn over one another, the CDJ-3000's (`PWV6`, `PWV7`), and what
* analyzer plugins give ([`Bands`]).
*/
| "three_band";

/** One column of a waveform: a back layer and a (shorter, brighter) front layer drawn over it. */
export type WaveColumn = {
Expand Down Expand Up @@ -2987,37 +3078,27 @@ export type PackedLayer = {
colors: string;
};

/** rekordbox's phrase analysis (`PSSI`, in `.EXT`). */
export type SongStructure = {
mood: Mood;
phrases: Phrase[];
};

/** rekordbox's overall classification of a track, which decides how phrases are named. */
export type Mood = "high" | "mid" | "low";

export type Phrase = {
kind: PhraseKind;
/** 1-based beat numbers into the beat grid; the phrase runs up to (not including) `end_beat`. */
start_beat: number;
end_beat: number;
/** Beat where a fill-in leads into the next phrase. */
fill_beat: number | null;
/**
* Something a front end can offer to resolve an error: call a method, or send the request that
* failed again with some parameters added (`method` absent), or again with administrator rights.
*/
export type Action = {
/** What a button offering it says. */
label: string;
/**
* The RPC method to call (`pull`, `sync.plan`). Absent: the request that failed, again,
* with `params` added to its own.
*/
method?: string | null;
/**
* The parameters: the method's, or the ones to add to the request that failed. `null`
* means a value the user has to give (a name, a path).
*/
params: { [key: string]: unknown };
/** Run it with administrator rights (restart the sidecar elevated first). */
elevate?: boolean;
};

export type PhraseKind =
| ("intro" | "bridge" | "chorus" | "outro")
/** Build-ups, in high-mood tracks. */
| "up"
/** Breakdowns, in high-mood tracks. */
| "down"
| {
verse: number;
}
| {
unknown: number;
};

export type TracksAnalysis = {
/** In the order asked for. */
tracks: AnalysedTrack[];
Expand Down Expand Up @@ -3180,25 +3261,13 @@ export type DiffEntry = {
position: number;
};

/**
* Something a front end can offer to resolve an error: call a method, or send the request that
* failed again with some parameters added (`method` absent), or again with administrator rights.
*/
export type Action = {
/** What a button offering it says. */
label: string;
/**
* The RPC method to call (`pull`, `sync.plan`). Absent: the request that failed, again,
* with `params` added to its own.
*/
method?: string | null;
/**
* The parameters: the method's, or the ones to add to the request that failed. `null`
* means a value the user has to give (a name, a path).
*/
params: { [key: string]: unknown };
/** Run it with administrator rights (restart the sidecar elevated first). */
elevate?: boolean;
export type PhraseOutput = {
kind: PhraseKind;
/** 1-based beat numbers into the beat grid; the phrase runs up to (not including) `end_beat`. */
start_beat: number;
end_beat: number;
/** Beat where a fill-in leads into the next phrase. */
fill_beat: number | null;
};

/** What putting a backup back would change. */
Expand Down Expand Up @@ -4293,6 +4362,21 @@ export type EditOutput =
tracks: TrackSelector;
image: ImageSource;
op: "set_artwork";
}
/**
* Write a beat grid, phrases or a waveform into the analysis files of the track of this
* audio file (a device path, see [`crate::analysis_edit`]). Planned, a grid brings the
* track's BPM with it (an `edit_tracks`); the journal records a waveform as staged on the
* drive.
*/
| {
track: string;
/** The beat grid: each beat's number in its bar (1-4), the tempo there and its time. */
beats?: Beat[] | null;
/** The phrases, named as the track's mood names them. */
phrases?: SongStructure | null;
waveform?: WaveSource | null;
op: "set_analysis";
};

/** A track none of the players play that packing leaves as it is. */
Expand Down Expand Up @@ -7685,7 +7769,14 @@ export type Analyzer = {
display?: { [key: string]: FieldDisplay };
};

export type FieldType = "number" | "integer" | "string" | "boolean" | "string[]";
export type FieldType =
| ("number" | "integer" | "string" | "boolean" | "string[]")
/** A beat grid (feature `analysis`, see [`crate::Output`]). */
| "beats"
/** A song structure: the mood and the phrases (feature `analysis`). */
| "phrases"
/** A waveform in three bands, in a blob (feature `analysis`). */
| "waveform";

/**
* How hosts show a field's values, each part optional. Without them a number shows to two
Expand Down Expand Up @@ -9092,6 +9183,10 @@ export interface Methods {
"plugins.settings": { params: PluginRef; result: Settings };
/** immediate method */
"plugins.settings.set": { params: SetPluginSetting & WriteOptions; result: Settings };
/** plan method; paired with `plugins.apply_analysis.apply`; long running: sends `progress`, can be cancelled; can delete or overwrite something */
"plugins.apply_analysis.plan": { params: ApplyAnalysis & WriteOptions; result: EditPlan & PlanFields };
/** apply method; paired with `plugins.apply_analysis.plan`; long running: sends `progress`, can be cancelled; can delete or overwrite something */
"plugins.apply_analysis.apply": { params: ApplyPlan | (ApplyAnalysis & WriteOptions); result: EditReport };
/** read method; long running: sends `progress`, can be cancelled */
"plugins.run": { params: RunPluginCommand; result: CommandOutput };
/** plan method; paired with `plugins.op.apply`; long running: sends `progress`, can be cancelled; can delete or overwrite something */
Expand Down
Loading
Loading