Expand description
§tone3000
Async Rust client for the TONE3000 API (v1): browse and search
the community tone library, download .nam and IR files, and authenticate users with
OAuth 2.0 + PKCE.
Pairs with nam-rs, which runs the models this
crate downloads.
$ cargo add tone3000
$ cargo add tokio -F macros,rt-multi-thread # the snippets below are async§Read this first
Every endpoint requires an end-user OAuth login. There is no anonymous access.
Searching the public library needs a user access token exactly as much as reading that
user’s favourites does. The publishable key (t3k_pub_…) is the OAuth client_id, not a
credential — a call without an access token fails with Error::Unauthenticated before it
reaches the network.
If your product can’t put a user through a browser login, this API isn’t usable.
§Concepts
| Type | What it is |
|---|---|
Tone | A capture project: one piece of gear, captured once, published with a title and licence. Not downloadable itself. |
Model | One downloadable file belonging to a tone, at a given size and architecture. Six models is six variants of one capture, not six amps. |
Make | The real-world gear captured, e.g. “Mesa Boogie Badlander”. |
Size | The CPU/quality trade-off (standard, lite, feather, nano, custom), not a byte count. |
A tone has many models; a model has a model_url you download. nam-rs parses those bytes
into a NamModel and loads it into a nam_rs::Model you can run audio through — so “model”
means something different in each crate. Fully qualify it in code using both.
§Quick start
use tone3000::{Client, ToneSort};
#[tokio::main]
async fn main() -> tone3000::Result<()> {
let client = Client::builder("t3k_pub_your_key")
.access_token("user_access_token") // see Authentication
.build();
let page = client
.tones()
.sort(ToneSort::DownloadsAllTime)
.page_size(24)
.await?;
for tone in &page.data {
println!("{} — {} downloads", tone.title, tone.downloads_count);
}
println!("page {} of {}", page.page, page.total_pages);
Ok(())
}client.tones() is browse and search both — add .query("plexi") and it becomes a search.
Filters are optional and chain:
use tone3000::{Client, Format, Gear, Size};
#[tokio::main]
async fn main() -> tone3000::Result<()> {
let client = Client::builder("t3k_pub_your_key").access_token("token").build();
let page = client
.tones()
.query("plexi")
.gear(Gear::Amp) // repeatable; multiple values are OR'd
.format(Format::Nam) // filter for IRs here, not through gear
.size(Size::Standard)
.make("Marshall") // exact match, not substring
.await?;
println!("{} matches", page.total);
if page.has_next() {
let next = client.tones().query("plexi").page(page.page + 1).await?;
println!("next page has {}", next.data.len());
}
Ok(())
}§Authentication
Get a publishable key from TONE3000 Settings → API Keys and register your redirect URI there; localhost is allowed in development. This crate handles the token exchange, and your app owns the redirect, because a desktop loopback, a plugin host and a web callback are different problems.
use tone3000::{AuthorizeOptions, Client, Prompt, oauth, pkce};
#[tokio::main]
async fn main() -> tone3000::Result<()> {
let pkce = pkce::generate();
let state = "an-unguessable-value-you-store";
let url = oauth::authorize_url(
"t3k_pub_your_key",
"http://localhost:8765/callback",
&pkce.challenge,
state,
Prompt::Standard,
AuthorizeOptions::default(),
);
println!("open this: {url}");
// Capture `code` from the redirect, and check `state` matches before trusting it.
let code = "from_the_redirect";
let client = Client::builder("t3k_pub_your_key").build();
let tokens = client
.exchange_code(code, &pkce.verifier, "http://localhost:8765/callback")
.await?;
println!("{}", tokens.access_token);
Ok(())
}Prompt::SelectTone, Prompt::LoadTone { tone_id } and Prompt::LoadModel { model_id }
hand the browsing to TONE3000’s own UI instead, which saves building one.
examples/oauth_desktop.rs does this end to end with a real
loopback listener, in std only.
Refresh tokens rotate. Each refresh invalidates the previous one, so store every token you are handed or the session dies:
use tone3000::Client;
fn main() {
let client = Client::builder("t3k_pub_your_key")
.access_token("stored_access_token")
.refresh_token("stored_refresh_token")
.expires_at(1_800_000_000) // unix seconds; without it, refresh is reactive only
.auto_refresh(true) // refresh near expiry, retry once on a 401
.on_tokens_changed(|tokens| {
// persist tokens.access_token and tokens.refresh_token
})
.build();
}§Downloading
use tone3000::{Client, Model};
async fn download(client: &Client, model: &Model) -> tone3000::Result<()> {
let bytes = client.download_model(model).await?;
let json = client.download_model_json(model).await?;
let mut file = tokio::fs::File::create("model.nam").await?;
client.download_model_to(model, &mut file).await?;
Ok(())
}download_url and download_url_to take a model_url directly, for when you stored one
rather than a whole Model.
client.models(id) returns one architecture at a time — the API defaults to v1 and has no
“all” option. Client::models documents the loop that collects them all.
§With nam-rs
cargo add nam-rs, then:
use tone3000::{Client, Model};
async fn play(client: &Client, model: &Model) -> Result<(), Box<dyn std::error::Error>> {
let json = client.download_model_json(model).await?;
// nam-rs's loaders are synchronous: call them once the download resolves.
let nam = nam_rs::NamModel::from_json_str(&json)?;
let mut runtime = nam_rs::Model::from_nam(&nam)?;
let mut buffer = vec![0.0f32; 512];
runtime.process_buffer(&mut buffer);
Ok(())
}§Examples
| Example | Shows | Needs |
|---|---|---|
oauth_desktop | The PKCE flow with a loopback listener. Start here — it produces the token the others need | T3K_PUB_KEY |
search_and_download | Browse, search, tone detail, models, download | both |
play_with_nam_rs | Download a capture and run audio through it | both |
$ export T3K_PUB_KEY=t3k_pub_...
$ cargo run --example oauth_desktop # prints an access token
$ export T3K_ACCESS_TOKEN=...
$ cargo run --example search_and_download§Errors and limits
Error separates what you would branch on: Unauthenticated (no token set), Unauthorized
(401), Forbidden (403), RateLimited { retry_after }, Status { code, body }, and Http
for transport failures. Error::is_timeout() and Error::is_connect() answer the usual
retry questions without matching the variant out.
The API allows 100 requests per minute by default, and search is limited more tightly.
Honour retry_after.
§Coverage
Implemented: tone search and browse, tone detail, created, favorited, model list and detail, model download, the user profile, the public user directory, and the OAuth token flows.
Not yet implemented, tracked as issues: /tones/trending and /tones/latest
(#9), /tones/downloaded
(#10), /makes and /tags
(#11), and the API’s deprecation
headers (#12). GET /tones/download
is restricted to approved partners, so it is deliberately absent — download individual
models via model_url.
§Compatibility
- MSRV 1.86, set by
url→idna_adapter→icu_*. An MSRV bump is a minor bump. - Async only, on
tokio. TLS viarustls, no OpenSSL.
Your integration must also follow TONE3000’s
Design Requirements and Commercial Terms, and you should
check tone.license before redistributing a model.
§Development
| Command | What it does | Credentials |
|---|---|---|
cargo test | Unit + wiremock suites; live tests are #[ignore]d | No |
make test-live | Live contract and enum-vocabulary checks | Yes |
make test-oauth | Interactive OAuth bootstrap; prints a refresh token | Yes, a browser |
make check-upstream | Diffs upstream types.ts against a pinned SHA | Needs gh |
make check-upstream also runs weekly in CI.
Licensed under MIT.
Re-exports§
pub use models::ArchitectureVersion;pub use models::EmbeddedUser;pub use models::Format;pub use models::Gear;pub use models::License;pub use models::Make;pub use models::MakeId;pub use models::Model;pub use models::ModelId;pub use models::Page;pub use models::PublicUser;pub use models::Size;pub use models::Tag;pub use models::TagId;pub use models::Tokens;pub use models::Tone;pub use models::ToneId;pub use models::ToneSort;pub use models::User;pub use models::UserId;pub use models::UserSort;pub use oauth::AuthorizeOptions;pub use oauth::Prompt;pub use pkce::Pkce;pub use pkce::generate as generate_pkce;
Modules§
- models
- Typed API models. All structs are lenient: unexpected fields are ignored and non-identity fields default, so a single API change never sinks a whole response.
- oauth
- OAuth helpers. The app owns the redirect; this module builds the authorize URL,
and performs the form-encoded token exchange/refresh against
/oauth/token. - pkce
- PKCE (RFC 7636) helpers. The consuming app owns the OAuth redirect transport; this module only generates the verifier/challenge pair.
Structs§
- Client
- Async client for the TONE3000 API.
- Client
Builder - Builder for
Client. - Http
Error - A transport-level failure.
- Model
List - A pending listing of a tone’s models, built by
Client::models. - Tone
List - A pending listing of the authenticated user’s tones, built by
Client::createdorClient::favorited. - Tone
Search - A pending browse/search of the tone library, built by
Client::tones. - User
List - A pending listing of the public user directory, built by
Client::users.
Enums§
- Error
- Errors returned by this crate.
Constants§
- DEFAULT_
BASE_ URL - Default base URL for the TONE3000 v1 API.
Type Aliases§
- Result
- Crate result alias.