
18/09/2026
|
Projects
Update
Following on from the first post, this one covers the two chunks of work since: getting the upload flow and browsing UI properly built out, then closing the gap that left behind, actually analysing the samples rather than just displaying empty columns for BPM, key, and length.
The UI: upload modal and waveform previews
The upload experience got a proper rebuild. Rather than a bare file input, there’s now a focus-trapped, keyboard-accessible upload modal that reads the picked file client-side with the Web Audio API before it ever touches the server, rejecting anything that isn’t audio immediately and extracting waveform peaks plus duration, size, and format for an instant preview. A dedicated drop zone feeds into the same selection flow as the manual picker, and a small canvas-based waveform component renders those extracted peaks so you see a real waveform in the modal before upload rather than just a filename sitting there.

Alongside that, the playback side got rebuilt too: the player store now supports a proper queue with previous/next, kept in sync with whichever list of samples you’re currently browsing, rather than only ever handling one sample in isolation. The sample list itself got restyled with per-row selection and a select-all checkbox, plus the BPM, length, key, and type columns, which is where the next bit of work comes in.
The API: from empty columns to real analysis
Columns within the UI along with in the database schema were added from the start of the project way before I’d set up a way for them to be populated. Closing it meant properly wiring up the FastAPI service that had been sitting mostly idle since the first post.
The first blocker was infrastructure rather than code. SvelteKit and FastAPI need to see the same uploaded file, and the Docker Compose setup was quietly mounting a named volume for FastAPI while SvelteKit wrote to the host’s actual uploads folder, so the two services were reading from and writing to two disconnected places. Fixing that meant sharing one volume properly in production and bind-mounting the same host folder in the dev override, so a file SvelteKit saves is immediately visible to FastAPI without any copying or syncing step.
With that sorted, the actual analysis side came together around librosa. BPM comes from librosa’s beat tracker, which turned into a small gotcha of its own: it returns tempo as an array rather than a plain number, so a naive cast to float breaks depending on what shape it hands back. Settled on wrapping it so it works regardless of shape. Key estimation was the more interesting piece, a new helper that takes the chroma features librosa extracts (essentially how much energy sits in each of the twelve pitch classes across the track) and matches that profile against the expected profiles for each major and minor key, picking whichever key correlates best.
def estimate_key(y: np.ndarray, sr: int) -> str:
chroma = librosa.feature.chroma_cqt(y=y, sr=sr)
chroma_avg = np.mean(chroma, axis=1)
best_score = -1
best_key = None
best_mode = None
for i in range(12):
major_rotated = np.roll(MAJOR_PROFILE, i)
minor_rotated = np.roll(MINOR_PROFILE, i)
major_score = np.corrcoef(chroma_avg, major_rotated)[0, 1]
minor_score = np.corrcoef(chroma_avg, minor_rotated)[0, 1]
if major_score > best_score:
best_score = major_score
best_key = NOTE_NAMES[i]
best_mode = 'major'
if minor_score > best_score:
best_score = minor_score
best_key = NOTE_NAMES[i]
best_mode = 'minor'
return f"{best_key} {best_mode}" The two services talk in a fairly deliberate shape. When a file’s uploaded, SvelteKit’s upload action writes it to disk, inserts a sample row marked as pending, and then POSTs to its own analysis API route rather than calling FastAPI directly from the upload action itself. That route is what actually calls FastAPI’s analyse endpoint, which loads the file with librosa and hands back BPM, duration, sample rate, and key. SvelteKit takes that response and writes it onto the sample row, flipping it from pending to complete. FastAPI never touches the database at any point, it’s purely a numbers-in, numbers-out service, which keeps all persistence logic in one place.
What’s next
The near-term work is tagging: properly categorising samples and letting a single sample belong to more than one group at once, since real sample libraries rarely sort cleanly into one bucket each. Alongside the tagging UI, manual editing is going in too, being able to correct the tag, key, or BPM by hand for whenever the automated analysis gets something wrong or a sample needs reclassifying. On the analysis side, the plan is to push past the current metadata (BPM, key, duration) into actual harmonic content analysis, extracting enough from the audio to make a reasonable guess at whether a sample is percussive or melodic. Alongside that, I’m starting to plan out how the analysed features actually get stored for similarity search, plotting samples as feature embeddings into a vector database (pgvector, given the project’s already on Postgres) so “find me something like this” becomes a real query rather than just an idea sitting in the backlog.
Further down the line, once there’s enough in pgvector to be useful, the plan is to feed that data back the other way: using existing embeddings and their known analysis results to help refine and correct the librosa estimations themselves, so the accuracy improves as the library grows rather than staying fixed at whatever a single pass of analysis gets right.