NAM Performance profiling (sync)

This commit is contained in:
Robin E. R. Davies
2026-05-16 06:26:53 -04:00
parent 7343ced278
commit 6574304227
20 changed files with 841 additions and 494 deletions
+7 -6
View File
@@ -28,12 +28,13 @@ jobs:
npm view npm get version of @babel/plugin-proposal-private-property-in-object version command
sudo apt-get update
sudo apt install gcc-12 g++-12 libnm-dev
sudo apt-get install liblilv-dev libboost-dev libjack-jackd2-dev libnl-3-dev libnl-genl-3-dev libsystemd-dev catch
sudo apt install libasound2-dev
sudo apt install libwebsocketpp-dev authbind
sudo apt install libsdbus-c++-dev libsdbus-c++-bin
sudo apt install libavahi-client-dev libzip-dev libicu-dev apt
sudo apt install libpipewire-0.3-dev
sudo apt-get install -y liblilv-dev libboost-dev libjack-jackd2-dev libnl-3-dev libnl-genl-3-dev libsystemd-dev catch
sudo apt install -y libasound2-dev
sudo apt install -y libwebsocketpp-dev authbind
sudo apt install -y libsdbus-c++-dev libsdbus-c++-bin
sudo apt install -y libavahi-client-dev libzip-dev libicu-dev apt
sudo apt install -y libpipewire-0.3-dev
sudo apt install -y libssl-dev
sudo apt install -y librsvg2-dev
git submodule update --init --recursive
+1 -1
View File
@@ -337,7 +337,7 @@
"program": "${command:cmake.launchTargetPath}",
"args": [
"--no-profile",
"--preset-file", "/home/robin/Downloads/NAM Profiling.piPreset",
"--preset-file", "/home/robin/Downloads/A1 Profiling.piPreset",
"--seconds", "100",
"-w"
],
+3
View File
@@ -11,6 +11,9 @@ set (DISPLAY_VERSION "PiPedal v2.0.101-Alpha")
set (PACKAGE_ARCHITECTURE ${DEBIAN_ARCHITECTURE})
set (CMAKE_INSTALL_PREFIX "/usr/")
set(PIPEDAL_T3K_PUBLISHABLE_KEY "t3k_pub_bHrH8btdwXXTtxz5ryEU8sNLF-2TGRT9")
set (LIBZIP_DO_INSTALL OFF CACHE BOOL "Disable libzip install" FORCE)
set (SQLITECPP_INSTALL OFF CACHE BOOL "Disable SQLiteCpp install" FORCE)
+2
View File
@@ -20,9 +20,11 @@
#pragma once
#include <string>
#include <vector>
#include <cstdint>
#include <chrono>
#include <filesystem>
#include <optional>
namespace pipedal
{
+11
View File
@@ -38,6 +38,7 @@
#include <vector>
#include <memory>
#ifndef __INTELLISENSE__
#define DECLARE_JSON_MAP(CLASSNAME) \
static pipedal::json_map::storage_type<CLASSNAME> jmap
@@ -56,7 +57,17 @@
} \
} \
;
#else
#define DECLARE_JSON_MAP(CLASSNAME)
#define JSON_MAP_BEGIN(CLASSNAME)
#define JSON_MAP_REFERENCE(class, name)
#define JSON_MAP_END() ;
;
#endif
#define JSON_GETTER_SETTER_REF(name) \
const decltype(name##_) &name() const { return name##_; } \
+1 -1
View File
@@ -27,7 +27,7 @@ Run the following commands to install dependent libraries required by the PiPeda
authbind libavahi-client-dev libnm-dev libicu-dev \
libsdbus-c++-dev libzip-dev google-perftools \
libgoogle-perftools-dev \
libpipewire-0.3-dev libbz2-dev
libpipewire-0.3-dev libbz2-dev libssl-dev
### Installing Sources
+40
View File
@@ -0,0 +1,40 @@
#include "AesDigest.hpp"
#include <array>
#include <openssl/evp.h>
#include <stdexcept>
using namespace pipedal;
std::string pipedal::AesDigest(const std::string &text)
{
std::array<unsigned char, EVP_MAX_MD_SIZE> digest{};
size_t digestLength = 0;
if (EVP_Q_digest(
nullptr,
"SHA256",
nullptr,
reinterpret_cast<const unsigned char *>(text.data()),
text.size(),
digest.data(),
&digestLength)
!= 1)
{
throw std::runtime_error("EVP_Q_digest(SHA256) failed.");
}
static constexpr char hexDigits[] = "0123456789abcdef";
std::string result(digestLength * 2, '\0');
for (size_t i = 0; i < digestLength; ++i)
{
unsigned char value = digest[i];
result[2 * i] = hexDigits[(value >> 4) & 0x0F];
result[2 * i + 1] = hexDigits[value & 0x0F];
}
return result;
}
+25
View File
@@ -0,0 +1,25 @@
// Copyright (c) 2026 Robin E. R. Davies
//
// Permission is hereby granted, free of charge, to any person obtaining a copy of
// this software and associated documentation files (the "Software"), to deal in
// the Software without restriction, including without limitation the rights to
// use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of
// the Software, and to permit persons to whom the Software is furnished to do so,
// subject to the following conditions:
//
// The above copyright notice and this permission notice shall be included in all
// copies or substantial portions of the Software.
//
// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS
// FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR
// COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER
// IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN
// CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
#pragma once
#include <string>
namespace pipedal {
std::string AesDigest(const std::string &text);
}
+13 -8
View File
@@ -1,6 +1,13 @@
set(VERBOSE true)
cmake_minimum_required(VERSION 3.16.0)
configure_file(config.hpp.in config.hpp)
file(GENERATE
OUTPUT "${CMAKE_CURRENT_BINARY_DIR}/T3kConfig.h"
CONTENT "#define PIPEDAL_T3K_PUBLISHABLE_KEY \"${PIPEDAL_T3K_PUBLISHABLE_KEY}\"\n")
set (CMAKE_INSTALL_PREFIX "/usr/")
set (USE_PCH 1)
@@ -10,19 +17,13 @@ set (ENABLE_BACKTRACE 0)
set (USE_SANITIZE OFF) # seems to be broken on Ubuntu 24.10
set(CXX_STANDARD 20)
find_package(PkgConfig REQUIRED)
pkg_check_modules(PipeWire REQUIRED libpipewire-0.3)
message(STATUS "PipeWire_INCLUDE_DIRS: ${PipeWire_INCLUDE_DIRS}"
"PipeWire_LIBRARIES: ${PipeWire_LIBRARIES}"
"PipeWire_CFLAGS: ${PipeWire_CFLAGS}"
"PipeWire_DEFINES: ${PipeWire_DEFINES}"
"PipeWire_VERSION: ${PipeWire_VERSION}")
include(FetchContent)
# FOR FTXUI
@@ -58,6 +59,8 @@ include(FindPkgConfig)
find_package(OpenSSL REQUIRED)
find_package(ICU REQUIRED COMPONENTS uc i18n)
message(STATUS "ICU_LIBRARIES: ${ICU_LIBRARIES}")
@@ -204,6 +207,7 @@ else()
endif()
set (PIPEDAL_SOURCES
AesDigest.cpp AesDigest.hpp
ChannelRouterSettings.cpp ChannelRouterSettings.hpp
Curl.cpp Curl.hpp
Tone3000DownloadType.cpp Tone3000DownloadType.hpp
@@ -324,7 +328,7 @@ set (PIPEDAL_SOURCES
)
configure_file(config.hpp.in config.hpp)
include_directories(
${pipedald_SOURCE_DIR}/.
../build/src
@@ -364,6 +368,7 @@ target_link_libraries(libpipedald
PiPedalCommon
PRIVATE
SQLiteCpp
OpenSSL::Crypto
${PipeWire_LIBRARIES}
)
+24 -1
View File
@@ -456,7 +456,7 @@ public:
JSON_MAP_BEGIN(RenameBankBody)
JSON_MAP_REFERENCE(RenameBankBody, bankId)
JSON_MAP_REFERENCE(RenameBankBody, newName)
JSON_MAP_END()
JSON_MAP_END();
class RenamePresetBody
{
@@ -1181,6 +1181,29 @@ public:
}
REGISTER_MESSAGE_HANDLER(setControl)
void handle_makeTone3000Pkce(int replyTo, json_reader *pReader)
{
std::string redirectUrl;
pReader->read(&redirectUrl);
Tone3000PkceParams result {redirectUrl};
this->Reply(replyTo, "makeTone3000Pkce", result);
}
REGISTER_MESSAGE_HANDLER(makeTone3000Pkce);
void handle_sha256Base64url(int replyTo, json_reader *pReader)
{
std::string input;
pReader->read(&input);
std::string result = Sha256Base64Url(input);
this->Reply(replyTo, "sha256Base64url", result);
}
REGISTER_MESSAGE_HANDLER(sha256Base64url);
void handle_previewControl(int replyTo, json_reader *pReader)
{
ControlChangedBody message;
+116
View File
@@ -28,9 +28,12 @@
#include <functional>
#include "HtmlHelper.hpp"
#include "Curl.hpp"
#include <openssl/evp.h>
#include <openssl/rand.h>
#include <charconv>
#include <limits>
#include <type_traits>
#include <array>
#include <filesystem>
#include <fstream>
#include "TemporaryFile.hpp"
@@ -39,6 +42,102 @@
using namespace pipedal;
namespace fs = std::filesystem;
// Pulls in the define for T3kConfig.h
#define PIPEDAL_T3K_PUBLISHABLE_KEY "t3k_pub_bHrH8btdwXXTtxz5ryEU8sNLF-2TGRT9"
namespace {
std::string Base64UrlEncode(const unsigned char* data, size_t size)
{
static constexpr char base64Table[] =
"ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/";
std::string result;
result.reserve(((size + 2) / 3) * 4);
for (size_t i = 0; i + 3 <= size; i += 3)
{
uint32_t value = (static_cast<uint32_t>(data[i]) << 16)
| (static_cast<uint32_t>(data[i + 1]) << 8)
| static_cast<uint32_t>(data[i + 2]);
result.push_back(base64Table[(value >> 18) & 0x3F]);
result.push_back(base64Table[(value >> 12) & 0x3F]);
result.push_back(base64Table[(value >> 6) & 0x3F]);
result.push_back(base64Table[value & 0x3F]);
}
size_t remainder = size % 3;
if (remainder == 1)
{
uint32_t value = static_cast<uint32_t>(data[size - 1]) << 16;
result.push_back(base64Table[(value >> 18) & 0x3F]);
result.push_back(base64Table[(value >> 12) & 0x3F]);
result.push_back('=');
result.push_back('=');
}
else if (remainder == 2)
{
uint32_t value = (static_cast<uint32_t>(data[size - 2]) << 16)
| (static_cast<uint32_t>(data[size - 1]) << 8);
result.push_back(base64Table[(value >> 18) & 0x3F]);
result.push_back(base64Table[(value >> 12) & 0x3F]);
result.push_back(base64Table[(value >> 6) & 0x3F]);
result.push_back('=');
}
for (char &ch : result)
{
if (ch == '+')
{
ch = '-';
}
else if (ch == '/')
{
ch = '_';
}
}
while (!result.empty() && result.back() == '=')
{
result.pop_back();
}
return result;
}
std::string RandomBase64Url(size_t size)
{
std::string bytes(size, '\0');
if (RAND_bytes(reinterpret_cast<unsigned char*>(bytes.data()), static_cast<int>(bytes.size())) != 1)
{
throw std::runtime_error("RAND_bytes failed.");
}
return Base64UrlEncode(reinterpret_cast<const unsigned char*>(bytes.data()), bytes.size());
}
}
std::string pipedal::Sha256Base64Url(const std::string &text)
{
std::array<unsigned char, EVP_MAX_MD_SIZE> digest{};
size_t digestLength = 0;
if (EVP_Q_digest(
nullptr,
"SHA256",
nullptr,
reinterpret_cast<const unsigned char *>(text.data()),
text.size(),
digest.data(),
&digestLength)
!= 1)
{
throw std::runtime_error("EVP_Q_digest(SHA256) failed.");
}
return Base64UrlEncode(digest.data(), digestLength);
}
static const std::filesystem::path WEB_TEMP_DIR{"/var/pipedal/web_temp"};
static const std::filesystem::path TONE3000_THUMBNAIL_PATH{"/var/pipedal/tone3000_thumbnails"};
@@ -1168,6 +1267,23 @@ std::shared_ptr<Tone3000Download> Tone3000DownloaderImpl::GetTone3000Tone(
return tone;
}
Tone3000PkceParams::Tone3000PkceParams(
const std::string &redirectUrl)
{
this->publishableKey_ = PIPEDAL_T3K_PUBLISHABLE_KEY;
this->redirectUrl_ = redirectUrl;
this->codeVerifier_ = RandomBase64Url(32);
this->codeChallenge_ = Sha256Base64Url(this->codeVerifier_);
this->state_ = RandomBase64Url(16);
}
//////////////////////////////////////////////////////////////////////////////////
JSON_MAP_BEGIN(Tone3000PkceParams)
+9 -8
View File
@@ -33,14 +33,19 @@
namespace pipedal
{
class Tone3000PkceParams {
class Tone3000PkceParams
{
private:
std::string publishableKey_;
std::string redirectUrl_;
std::string codeVerifier_;
std::string codeChallenge_;
std::string state_;
public:
Tone3000PkceParams() = default;
Tone3000PkceParams(const std::string &redirectUrl);
JSON_GETTER_SETTER_REF(publishableKey);
JSON_GETTER_SETTER_REF(redirectUrl);
JSON_GETTER_SETTER_REF(codeVerifier);
@@ -62,6 +67,8 @@ namespace pipedal
DECLARE_JSON_MAP(Tone3000ModelInfo);
};
std::string Sha256Base64Url(const std::string&input);
// class Tone3000DownloadRequest
// {
// private:
@@ -108,7 +115,6 @@ namespace pipedal
// JSON_GETTER_SETTER_REF(license);
// JSON_GETTER_SETTER_REF(links);
// DECLARE_JSON_MAP(Tone3000DownloadRequest);
// };
@@ -144,10 +150,8 @@ namespace pipedal
using self = Tone3000Downloader;
static std::shared_ptr<self> Create();
virtual void SetListener(Listener *listener) = 0;
virtual void CancelDownload(
handle_t handle) = 0;
@@ -155,12 +159,9 @@ namespace pipedal
const uri &uri,
const Tone3000PkceParams &pkceParams,
const std::string &downloadPath,
Tone3000DownloadType downloadType)
= 0;
Tone3000DownloadType downloadType) = 0;
virtual void Close() = 0;
virtual Tone3000DownloadProgress GetDownloadStatus() = 0;
};
}
+1
View File
@@ -3,6 +3,7 @@
<meta charset="UTF-8">
<title>T3K Response</title>
<script>
debugger;
window.opener.postMessage(
{
type: "t3k_response",
+1 -1
View File
@@ -7,6 +7,6 @@
"ui_plugins": [],
"enable_auto_update": true,
"has_wifi_device": false,
"tone3000_A2_models": true,
"tone3000_A2_models": false,
"end": true
}
+1 -1
View File
@@ -587,6 +587,7 @@ export
}
private beforeUnloadListener(e: Event) {
alert("BeforeUnload");
this.model_.close();
return undefined;
}
@@ -601,7 +602,6 @@ export
super.componentDidMount();
window.addEventListener("beforeunload", this.beforeUnloadListener);
window.addEventListener("unload", this.unloadListener);
this.model_.errorMessage.addOnChangedHandler(this.errorChangeHandler_);
this.model_.state.addOnChangedHandler(this.stateChangeHandler_);
+9 -1
View File
@@ -46,13 +46,21 @@ export default class JackStatusView extends React.Component<JackStatusViewProps,
this.tick = this.tick.bind(this);
}
private waitingForTick: boolean = false;
tick() {
if (this.model.state.get() === State.Ready) {
if (this.waitingForTick) return;
this.waitingForTick = true;
this.model.getJackStatus()
.then(jackStatus => {
this.waitingForTick = false;
this.setState({jackStatus: jackStatus});
})
.catch(error => { /* ignore*/ });
.catch(error => {
this.waitingForTick = false;
});
} else {
this.waitingForTick = false;
}
}
+19
View File
@@ -707,6 +707,25 @@ export class PiPedalModel //implements PiPedalModel
}
async makeTone3000Pkce(redirectUrl: string) : Promise<Tone3000PkceParams>
{
if (this.webSocket === undefined)
{
throw new Error("Server disconnected.");
}
return await this.webSocket.request<Tone3000PkceParams>(
"makeTone3000Pkce", redirectUrl);
}
async sha256Base64url(intput: string) : Promise<string> {
if (this.webSocket === undefined)
{
throw new Error("Server disconnected.");
}
return this.webSocket.request<string>(
"sha256Base64url", intput);
}
async pingTone3000Server(): Promise<boolean> {
if (this.webSocket === undefined) {
return false;
+47 -12
View File
@@ -1,12 +1,12 @@
import { PiPedalModel, getErrorMessage, Tone3000PkceParams } from "./PiPedalModel";
import Tone3000DownloadType from "./Tone3000DownloadType";
import { PkceParams, buildPkceParams, handleOAuthCallback } from "./t3k/tone3000-client.ts";
import { T3K_DEBUG, PkceParams, setServerPkceParams, handleOAuthCallback } from "./t3k/tone3000-client.ts";
import { Model, PaginatedResponse, Platform, Tone } from "./t3k/types.ts";
import { PUBLISHABLE_KEY } from './t3k/config.ts';
import { startSelectFlowPopup, T3KClient } from "./t3k/tone3000-client.ts";
import { startSelectFlowPopup, buildPkceParams,T3KClient } from "./t3k/tone3000-client.ts";
import Tone3000DownloadProgress from './Tone3000DownloadProgress';
import { safeFilenameEncode } from "./SafeFilename";
@@ -20,6 +20,8 @@ async function asyncSleep(ms: number): Promise<void> {
return new Promise(resolve => setTimeout(resolve, ms));
}
const USE_SERVER_PCKE=true;
const RUN_THROTTLER_TEST = true;
const ALLOWED_DOWNLOADS_PER_MINUTE = 25;
@@ -119,6 +121,7 @@ export class Tone3000DownloadHandler {
let uri = event.data.uri;
this.handleTone3000DownloadComplete();
if (uri) {
this_.handleT3kSelectResponse(uri, event.data.storedState, event.data.codeVerifier)
.then(() => { })
.catch((error) => {
@@ -171,17 +174,18 @@ export class Tone3000DownloadHandler {
private progress: Tone3000DownloadProgress = new Tone3000DownloadProgress();
private async throttleRequests(): Promise<void> {
private async throttleRequests(): Promise<boolean> {
let now = Date.now();
let delay = this.throttler.getThrottleDelay(now);
while (delay > 0) {
await asyncSleep(250);
if (this.checkForCancel())
{
return;
return false;
}
delay -= 250;
}
return true;
}
private onTone3000DownloadStarted() {
@@ -214,6 +218,10 @@ export class Tone3000DownloadHandler {
downloadType: Tone3000DownloadType
): Promise<void> {
try {
if (T3K_DEBUG) {
console.debug("PiPedal responseUri: " + responseUri);
}
this.onTone3000DownloadStarted();
this.progress.title = "Authenticating..."
this.onTone3000DownloadProgress(this.progress);
@@ -221,13 +229,13 @@ export class Tone3000DownloadHandler {
return;
}
await this.throttleRequests();
if (!await this.throttleRequests()) return;
let tokenResponse = await handleOAuthCallback(
PUBLISHABLE_KEY,
this.redirectUrl(),
responseUri);
if (!tokenResponse.ok) {
;
throw new Error(tokenResponse.error);
}
if (this.checkForCancel()) {
@@ -245,7 +253,7 @@ export class Tone3000DownloadHandler {
this.onTone3000DownloadProgress(this.progress);
await this.throttleRequests();
if (!await this.throttleRequests()) return;
let tone: Tone = await this.t3kClient.getTone(tokenResponse.toneId);
if (this.checkForCancel()) {
@@ -264,7 +272,7 @@ export class Tone3000DownloadHandler {
}
while (true) {
await this.throttleRequests();
if (!await this.throttleRequests()) return;
if (this.checkForCancel()) {
return;
}
@@ -308,7 +316,7 @@ export class Tone3000DownloadHandler {
lastUpdateTime = Date.now();
}
await this.throttleRequests();
if (!await this.throttleRequests()) return;
if (!model.model_url) {
throw new Error("Model " + model.name + " does not have a model URL.");
@@ -463,18 +471,45 @@ export class Tone3000DownloadHandler {
try {
let pkceParams = await buildPkceParams();
let t3kPceParams: Tone3000PkceParams;
let pkceParams: PkceParams;
if (USE_SERVER_PCKE) {
t3kPceParams = await this.model.makeTone3000Pkce(this.redirectUrl());
pkceParams = {
codeChallenge: t3kPceParams.codeChallenge,
codeVerifier: t3kPceParams.codeVerifier,
state: t3kPceParams.state
}
setServerPkceParams(pkceParams)
} else {
pkceParams = await buildPkceParams();
t3kPceParams = {
codeChallenge: pkceParams.codeChallenge,
codeVerifier: pkceParams.codeVerifier,
state: pkceParams.state,
publishableKey: PUBLISHABLE_KEY,
redirectUrl: this.redirectUrl()
};
// verify that the server matches.
let testcodeChallenge = await this.model.sha256Base64url(pkceParams.codeVerifier);
if (testcodeChallenge !== pkceParams.codeChallenge) {
throw new Error("model.sha256Base64url produces incorrect results. ");
}
}
this.pkceParams = pkceParams;
this.popupWindow = await startSelectFlowPopup(
pkceParams,
PUBLISHABLE_KEY,
this.redirectUrl(),
{
architecture: downloadType === Tone3000DownloadType.CabIr ? undefined : 2,
architecture: (downloadType === Tone3000DownloadType.Nam && this.model.tone3000_A2_models) ? 2: undefined,
platform: downloadType === Tone3000DownloadType.CabIr ? Platform.Ir : Platform.Nam,
menubar: true,
width: popupWidth,
height: popupHeight
height: popupHeight,
},
);
+5 -10
View File
@@ -3,18 +3,13 @@
// Trailing slashes are stripped so `${T3K_API}/api/...` never produces a double slash —
// Vercel 308-redirects double-slash paths, and redirects drop CORS headers.
export const T3K_API = (
(import.meta.env.VITE_T3K_API_DOMAIN as string | undefined) ?? 'https://www.tone3000.com'
'https://www.tone3000.com'
).replace(/\/+$/, '');
export const PUBLISHABLE_KEY = import.meta.env.VITE_PUBLISHABLE_KEY as string;
export const PUBLISHABLE_KEY = "t3k_pub_bHrH8btdwXXTtxz5ryEU8sNLF-2TGRT9";
// Per-demo keys — fall back to the shared PUBLISHABLE_KEY for local dev
export const PUBLISHABLE_KEY_SELECT =
(import.meta.env.VITE_PUBLISHABLE_KEY_SELECT as string | undefined) ?? PUBLISHABLE_KEY;
export const PUBLISHABLE_KEY_LOAD =
(import.meta.env.VITE_PUBLISHABLE_KEY_LOAD as string | undefined) ?? PUBLISHABLE_KEY;
export const PUBLISHABLE_KEY_FULL =
(import.meta.env.VITE_PUBLISHABLE_KEY_FULL as string | undefined) ?? PUBLISHABLE_KEY;
export const PUBLISHABLE_KEY_SELECT = PUBLISHABLE_KEY;
export const PUBLISHABLE_KEY_LOAD = PUBLISHABLE_KEY;
export const PUBLISHABLE_KEY_FULL = PUBLISHABLE_KEY;
export const REDIRECT_URI =
(import.meta.env.VITE_REDIRECT_URI as string | undefined) ?? 'http://localhost:3001';
+69 -7
View File
@@ -1,3 +1,39 @@
/**
* Modified by Robin E. R. Davies from tone3000-client.ts, original from github tone3000/api repository.
*
* The file has been modified to address the following scenario.
*
* - Running on a local network, PiPedal server and client are on the same Wi-Fi network.
* - The PiPedal server is not running HTTPS (local network servers cannot get TLS certificates).
* - The client is running on a machine other than the PiPedal server. In this situation, Chrome
* disables access to in-browser Crypto API, because the site is not local, and is not HTTPS.
* - pkce's are generated on the PiPedal server, which uses OpenSSL 3.0 to generate valid pkces.
*
* The followin changes to the original file have been made:
*
* - startSelectFlowPopup accepts a pre-generated PKCE object (which the client has obtained from the PiPedal server).
* - various parameters have been added for the A2 Architecture API updates (&Architecture=2 added to various URLS in
* order to query and download A2 models.
* - The redirect page comes from PiPedal server. It simply posts windows.location.href to a MessageEvent in the opener window.
* - The window.location.href is then passed to (modified) handleOAuthCallback, which extracts parameters from the received responseUri.
*
* Inline documentation has not been updated and is no longer accurate. The ONLY flow that has been modified is startSelectFlowPopup, which is the flow used by PiPedal.
* Other flows will not work.
*/
/*
* Original header, no longer accurate:
* A zero-dependency helper for integrating with the TONE3000 API.
* Uses built-in WebCrypto and fetch no npm install required.
*
* Quick start:
* 1. Import the flow initiator for your use case
* 2. Call it when the user triggers the integration (e.g. clicks "Browse Tones")
* 3. In your callback handler, call handleOAuthCallback()
* 4. Use T3KClient to make authenticated API requests
*/
/**
* tone3000-client.ts TONE3000 OAuth + API client
*
@@ -17,6 +53,10 @@ import type {
PaginatedResponse, SearchTonesParams, ListUsersParams,
} from './types';
export const T3K_DEBUG: boolean = true;
// ─── Public types ─────────────────────────────────────────────────────────────
export interface T3KTokens {
@@ -51,7 +91,12 @@ export interface PkceParams {
state: string;
}
export function setServerPkceParams(pkce: PkceParams) {
sessionStorage.setItem('t3k_code_verifier', pkce.codeVerifier);
sessionStorage.setItem('t3k_state', pkce.state);
}
export async function buildPkceParams(): Promise<PkceParams> {
const codeVerifier = await randomBase64url(32);
const [codeChallenge, state] = await Promise.all([
sha256Base64url(codeVerifier),
@@ -117,7 +162,8 @@ export async function startSelectFlowPopup(
pkce: PkceParams,
publishableKey: string,
redirectUri: string,
options?: { gears?: string; platform?: string; menubar?: boolean, loginHint?: string, architecture?: number
options?: {
gears?: string; platform?: string; menubar?: boolean, loginHint?: string, architecture?: number
width?: number, height?: number
}
): Promise<Window | null> {
@@ -132,6 +178,9 @@ export async function startSelectFlowPopup(
if (options?.architecture) extra.architecture = options.architecture.toString();
const url = buildAuthorizeUrl(publishableKey, redirectUri, extra, pkce);
if (T3K_DEBUG) {
console.debug("PiPedal startSelectFlowPopup URL:" + url + "(from buildAuthorizeUrl)");
}
const width = options?.width ?? 480;
const height = options?.height ?? 700;
const left = Math.round(window.screenX + (window.outerWidth - width) / 2);
@@ -459,16 +508,21 @@ export async function handleOAuthCallback(
return { ok: false, error: 'missing_code' };
}
const res = await fetch(`${T3K_API}/api/v1/oauth/token`, {
method: 'POST',
headers: { 'Content-Type': 'application/x-www-form-urlencoded' },
body: new URLSearchParams({
let tokenSearchParams = new URLSearchParams({
grant_type: 'authorization_code',
code,
code_verifier: codeVerifier,
redirect_uri: redirectUri,
client_id: publishableKey,
}),
client_id: publishableKey
});
if (T3K_DEBUG) {
console.debug("PiPedal /api/v1/oath/token body: " + tokenSearchParams.toString());
}
const res = await fetch(`${T3K_API}/api/v1/oauth/token`, {
method: 'POST',
headers: { 'Content-Type': 'application/x-www-form-urlencoded' },
body: tokenSearchParams,
});
if (!res.ok) {
@@ -477,12 +531,20 @@ export async function handleOAuthCallback(
}
const data = await res.json();
const tokens: T3KTokens = {
access_token: data.access_token,
refresh_token: data.refresh_token,
expires_at: Date.now() + data.expires_in * 1000,
};
if (T3K_DEBUG) {
console.debug("PiPedal handleOAuthCallback Response: " + JSON.stringify(data).replace(/\n/g, ' '));
}
if (toneId === undefined && modelId === undefined) {
throw new Error('Missing both toneId and modelId in OAuth callback response');
}
return { ok: true, tokens, toneId, modelId, ...(canceled ? { canceled: true } : {}) };
}