Skip to content

Audio publishing

Publish an album on Soundfish

Soundfish gives an album a page of its own at https://sound.fish/@handle/slug: a waveform-and-spectrogram player, hearts, comments, and play counts. Every track keeps its revisions in order, listeners hear the current one, and the player embeds on any site. Publishing needs a Hraness account with a username; listening needs no account.

Create and manage an album

  1. Sign in and choose Upload, or open sound.fish/audio/albums/new. Give the album a title. The slug defaults to the title and becomes the page address /@handle/slug.
  2. On the manage page, add tracks by uploading MP3, WAV, or FLAC files. Each file is checked after it arrives; the track appears once it is ready.
  3. Add a cover, a description, and details. The preview player above the form plays the album exactly as listeners will hear it, with plays, hearts, and comments switched off.
  4. Choose the visibility on the manage page. Published puts the album on the home feed, your artist page at /@handle, and in search. Unlisted opens the page and embed to anyone with the link while keeping it off listings and search. Draft returns it to you alone.

Albums start as drafts. A draft has no public page, no embed, and collects no plays, hearts, or comments. An unlisted album collects them like a published one and is embeddable, but stays off listings and out of search. Check Allow listeners to download the original audio files on the manage page to give each track a download control that serves the stored file under the track's name. The manage page shows a usage line (albums 1/1 · tracks 3/10 · storage 120 MiB/2 GiB) from the same account read the CLI's whoami prints.

Tracks and revisions

Upload a file to an existing track to add a revision. Revisions are numbered in order (v1, v2, …) and dated by UTC calendar day, so a comment left on an earlier version reads v1 · Sep 14. The current revision is the one listeners hear; promote any earlier or later revision at any time. A revision may carry a note and details that only you see. A file you already stored is not uploaded again.

Reorder tracks with Up and Down. Deleting a track or an album removes its page, frees its slug, and releases its storage; free accounts keep one album, so deleting makes room for another.

Descriptions and details

Descriptions are plain text; blank lines separate paragraphs. They appear under the album title and inside the active track's lane. Details are up to 32 key–value pairs (keys are lowercase letters, digits, dots, dashes, or underscores; values up to 1,000 characters) rendered as a "Details" list on the album page and in the track lane. The embed shows neither.

Formats and limits

Soundfish is free to use, with these limits per account:

Free-plan limits for audio publishing
LimitFree plan
Albums1
Tracks per album10
Revisions per track25
Track file200 MiB
Track duration1 s to 20 min
Cover image12 MiB, 200–8,000 px per edge
Storage2 GiB, counting uploads in progress
Changes600 per rolling hour; 8 uploads in progress at once
  • Audio: MP3 (audio/mpeg), WAV (audio/wav or audio/x-wav; 16-, 24-, 32-bit PCM or 32-bit float, 8,000–192,000 Hz), or FLAC (audio/flac or audio/x-flac), mono or stereo. Duration is subject to the 200 MiB cap (16-bit/44.1 kHz stereo WAV ≈ 19.8 min, 24-bit/48 kHz ≈ 12 min; MP3 and FLAC reach 20 min). Compressed WAV encodings, 8-bit and 64-bit float samples, and RF64 files are refused as unsupported media.
  • Covers: PNG, JPEG, or WebP. Soundfish strips metadata and derives a 1,400 px cover and a 400 px thumbnail.
  • Text: titles up to 120 characters; slugs of 1–64 lowercase letters, digits, and dashes (new, manage, embed, edit, media, and cover are reserved); descriptions up to 4,000 characters; revision notes up to 2,000; comments up to 2,000.

When a limit is reached, the manage page, the CLI, and the API show the same sentence, for example "Free accounts publish one album. Delete the existing album to create another." There is no paid plan and no upgrade link. GET https://sound.fish/api/audio/v1/capabilities publishes the accepted formats and every limit.

Artist profile

Album, artist, and embed pages show the name and avatar from your published Hraness profile, and comments show your handle. Edit the profile at account.hraness.com/account; when it is not published, pages show @handle alone. If another account later claims your handle at Hraness, Soundfish releases it from your albums once that account signs in at sound.fish: the artist page and the album, track, and embed addresses under that handle then belong to the new holder, and your albums stop answering at them. Sign in at sound.fish to refresh your handle; your albums stay published and return at /@your-current-handle/slug, and an embed copied under the old handle must be copied again. Album and track ids keep working meanwhile.

Listening, hearts, and comments

Anyone can listen without an account. A play counts once a listener has heard enough of a track; Soundfish sets a first-party listener cookie (__Host-soundfish-listener) or, inside an embed, returns a listener id to the player so repeated plays are not counted twice. Hearts and comments need a Hraness sign-in on the album page. A comment can be pinned to a moment in a track and records which revision was current when it was written; clicking the waveform or pressing c opens the composer at that moment, and the markers on the track preview each comment on hover and seek the playhead on selection. The album page answers space or k for play and pause, ←/→ to seek five seconds, c to comment, and l to heart.

Embed the player

A published album has an embed at https://sound.fish/embed/@handle/slug; add ?track=<track-slug> to start on one track and t=83 or t=1m23s to start at a moment. Album and track pages accept the same parameters, so a shared link opens on the right track at the right second without autoplaying. The player's share control copies such links. Copy the <iframe> from the manage page or print it with soundfish audio embed <album> --html:

<iframe src="https://sound.fish/embed/@handle/slug" width="100%" height="928" title="Album by @handle" loading="lazy" allow="autoplay; clipboard-write" style="border:0"></iframe>

The embed shows the transport, waveform, track list, counts, and a read-only preview of the active track's latest three comments; hearting or opening the comments goes to the album page in a new tab. The height is an estimate from the track count. The page posts { "type": "soundfish:resize", "height": <px> } to its parent window whenever its content height changes, so a host page can listen for that message and resize the frame. Once a listener switches tracks it also posts { "type": "soundfish:location", "track": "<track-slug>" }, so the host can mirror ?track= in its own address bar. Allow clipboard-write on the frame so the share control can copy links.

Markdown pages

Every artist and album page is also served as Markdown for agents and readers: append .md (https://sound.fish/@handle/slug.md) or request the page with Accept: text/markdown. The Markdown carries the title, artist, description, details table, track list with durations and page URLs, and the embed URL.

Publish from the command line

The soundfish audio verbs publish from a terminal or a coding agent. Run soundfish audio login: it prints sound.fish/settings/device and a short code such as ABCD-EFGH, opens a browser when it is running in a terminal, and waits. Open that address in a browser already signed in to Soundfish, enter the code, read what approving grants, and approve it; the terminal receives the token itself, so no token is ever pasted. The code lasts ten minutes. In a script or an agent, create a token at sound.fish/settings/tokens and pipe it in with --token-stdin instead. Tokens look like sfk_ followed by 40 letters or digits and are stored under $XDG_STATE_HOME/hraness/soundfish/credentials.json in a directory only you can read. Revoke any token, including one approved from a terminal, at the tokens settings page.

soundfish audio login [--no-browser | --token-stdin]
soundfish audio logout
soundfish audio whoami
soundfish audio albums list|create|show|update|publish|unlist|unpublish|delete|stats
soundfish audio tracks list|add|update|remove|reorder
soundfish audio upload <album> <file ...> [--track <track>] [--title <text>] [--slug <slug>] [--note <text>] [--no-promote] [--created-at <iso-8601>]
soundfish audio revisions list|promote
soundfish audio art set <album> <image>
soundfish audio embed <album> [--html]
soundfish audio status <assetId>

An album is named by alb_<id>, @handle/<slug>, or one of your own slugs; a track by trk_<id> or its slug. upload hashes each file, asks the API for an upload, streams the bytes to storage, waits for inspection, and attaches the result: without --track it creates a track from the file name (or --title) and refuses to replace an existing track silently (exit 4 with the hint to use --track); with --track it adds the next revision and promotes it unless --no-promote is given. --track, --title, --slug, and --note require exactly one file. For a draft album, every successful upload prints the manage page address on standard error so you can listen before publishing.

update verbs accept --description-file <path|->, repeatable --metadata key=value, and --unset-metadata <key>. Every verb accepts --format json, mutating verbs accept --dry-run, and --api-url (or SOUNDFISH_API_URL) selects the API origin. Exit codes: 0 success, 1 network failure or a 5xx response, 2 usage, 3 invalid local input, 4 conflict or plan limit, 5 rejected by the API. Standard output carries only results; progress and hints go to standard error. The CLI audio reference in the Soundfish skill repeats this procedure for agents.

Publish your first track and revise it

Install Bun before using the CLI. Sign in to Soundfish with a username, and prepare two audio files you may share: takeoff.wav and a different take named takeoff-v2.wav. Check formats and limits first. This example creates a new album; if you already have one, use its slug instead of creating another.

Audio uploads store your files on the server. Drafts are for your account alone; publishing makes the page public. Unlisted albums are accessible to anyone with the link. Keep tokens out of prompts and source files, and approve a coding agent only after reading the device approval page.

whoami identifies the account and its usage. Confirm both before creating the draft:

bun add --global @hraness/soundfish
soundfish audio login
soundfish audio whoami
soundfish audio albums create --title 'Night drive' --slug night-drive
soundfish audio upload night-drive takeoff.wav --slug takeoff
soundfish audio albums show night-drive

Open the manage URL printed after upload and press Play in the preview. albums show reports draft and the track's current v1. Once you hear the intended file, publish and print the embed:

soundfish audio albums publish night-drive
soundfish audio embed night-drive --html

Open the public album URL signed out and press Play. Open the iframe's src to check the embedded player. Use albums unlist night-drive instead of albums publish if you want to keep it off listings and search.

Upload the second take without changing what listeners hear, then choose it as current:

soundfish audio upload night-drive takeoff-v2.wav --track takeoff --no-promote
soundfish audio revisions list night-drive takeoff
soundfish audio revisions promote night-drive takeoff 2
soundfish audio revisions list night-drive takeoff

Before promotion, the list shows v1 as current; afterward it shows v2 as current. Reopen the same public URL and listen to confirm the replacement. Keep the handle and album and track slugs unchanged to keep their addresses. To return to the first take, run soundfish audio revisions promote night-drive takeoff 1.

If an upload reports a conflict, run tracks list night-drive and target the existing track with --track; omitting it creates a new track. For unsupported_media, check the encoding against the formats table, not only the extension. For plan_limit, inspect whoami usage before retrying. Do not delete an album to make room unless you intend to remove its pages and have kept your source files.

HTTP API

Every route lives under https://sound.fish/api/audio/v1/, accepts and returns JSON (request bodies up to 64 KiB), and answers a failure as { "error": { "code", "message", "retryable" }, "requestId" } with codes invalid_request, sign_in_required, forbidden, not_found, conflict, payload_too_large, unsupported_media, plan_limit, rate_limited, unavailable. A method the route does not implement answers 405 with the same envelope (code invalid_request), an Allow header listing the accepted methods, and cache-control: no-store. Authenticate with Authorization: Bearer sfk_…. Uploads are a three-step contract: POST /uploads with the file's SHA-256 and size returns a presigned PUT, the client sends the bytes there, and POST /uploads/{assetId}/complete inspects the file and reports ready or a failure code. Public reads need no token: GET /capabilities, GET /public/albums, GET /public/albums/@handle/slug, and GET /public/artists/@handle. The OpenAPI description lists every operation and schema.

Audio-to-MIDI jobs (private alpha)

A separate surface under https://sound.fish/api/v1/ turns one uploaded audio file into a MIDI artifact through a transcription provider: POST /api/v1/uploads/audio returns the byte ingress, POST /api/v1/jobs (with an Idempotency-Key header) starts the job, GET /api/v1/jobs/{id} reports status, and a finished job links a bounded-time artifact download. The alpha accepts exactly the auto profile and one MIDI output. GET /api/v1/capabilities discloses the third-party processor when one is configured; without one, your audio stays inside this deployment for processing. Submit only audio you may share with that processor.

Authenticate with Authorization: Bearer cr_dev_…, a Hraness Credits subject token, or with the deployment's private token. A credits token owns exactly the job-and-upload namespace derived from it, so no caller sees another wallet's jobs, and pays one unit per job: the unit price is held when the job is created, settled when it succeeds, and released when it fails or cannot be delivered. Job responses report billing.status as held, uncertain (provider submission unresolved), settled, or released. An unfunded wallet answers 402 credits_required with a creditsRequired envelope carrying the unit price, your balance, and a bound topup.url; funding the wallet and retrying the same idempotent request is safe. A misconfigured deployment answers 503 billing_unavailable. GET /api/v1/capabilities is public and describes the surface; the OpenAPI description carries the full contract.

Read this guide as Markdown.