diff --git a/src/PiPedalModel.cpp b/src/PiPedalModel.cpp index b5d307b..91fe6b8 100644 --- a/src/PiPedalModel.cpp +++ b/src/PiPedalModel.cpp @@ -3038,3 +3038,22 @@ void PiPedalModel::SetPedalboardItemTitle(int64_t instanceId, const std::string this->SetPresetChanged(-1, true); this->FirePedalboardChanged(-1, false); } + +void PiPedalModel::SetTone3000Auth(const std::string &apiKey) +{ + std::lock_guard lock(mutex); + storage.SetTone3000Auth(apiKey); + + std::vector t{subscribers.begin(), subscribers.end()}; + bool hasAuth = apiKey != ""; + for (auto &subscriber : t) + { + subscriber->OnTone3000AuthChanged(hasAuth); + } + +} +bool PiPedalModel::HasTone3000Auth() const +{ + return storage.GetTone3000Auth() != ""; +} + diff --git a/src/PiPedalModel.hpp b/src/PiPedalModel.hpp index edf0ff3..d665545 100644 --- a/src/PiPedalModel.hpp +++ b/src/PiPedalModel.hpp @@ -96,6 +96,7 @@ namespace pipedal virtual void OnHasWifiChanged(bool hasWifi) = 0; virtual void OnAlsaSequencerConfigurationChanged(const AlsaSequencerConfiguration &alsaSequencerConfiguration) = 0; virtual void Close() = 0; + virtual void OnTone3000AuthChanged(bool value) = 0; }; @@ -266,6 +267,7 @@ namespace pipedal void CheckForResourceInitialization(Pedalboard &pedalboard); UiFileProperty::ptr FindLoadedPatchProperty(int64_t instanceId,const std::string&patchPropertyUri); + public: PiPedalModel(); virtual ~PiPedalModel(); @@ -479,6 +481,10 @@ namespace pipedal int32_t to); void SetPedalboardItemTitle(int64_t instanceId, const std::string &title); + + void SetTone3000Auth(const std::string &apiKey); + bool HasTone3000Auth() const; + }; } // namespace pipedal. \ No newline at end of file diff --git a/src/PiPedalSocket.cpp b/src/PiPedalSocket.cpp index 4a4132b..4740747 100644 --- a/src/PiPedalSocket.cpp +++ b/src/PiPedalSocket.cpp @@ -1416,6 +1416,10 @@ public: pReader->read(&instanceId); uint64_t result = model.DeleteBank(this->clientId, instanceId); this->Reply(replyTo, "deleteBankItem", result); + } else if (message == "getHasTone3000Auth") + { + bool result = model.HasTone3000Auth(); + this->Reply(replyTo, "getHasTone3000Auth", result); } else if (message == "renameBank") { @@ -1863,6 +1867,10 @@ private: { } } + virtual void OnTone3000AuthChanged(bool value) + { + Send("onTone3000AuthChanged", value); + } virtual void OnErrorMessage(const std::string &message) { diff --git a/src/Storage.cpp b/src/Storage.cpp index af60144..29872c5 100644 --- a/src/Storage.cpp +++ b/src/Storage.cpp @@ -254,6 +254,7 @@ void Storage::Initialize() LoadPluginPresetIndex(); LoadBankIndex(); LoadCurrentBank(); + LoadTone3000Auth(); try { LoadChannelSelection(); @@ -311,6 +312,11 @@ std::filesystem::path Storage::GetCurrentPresetPath() const return this->dataRoot / "currentPreset.json"; } +std::filesystem::path Storage::GetTone3000AuthPath() const +{ + return this->dataRoot / "tone3000.json"; +} + std::filesystem::path Storage::GetChannelSelectionFileName() { return this->dataRoot / "JackChannelSelection.json"; @@ -741,34 +747,35 @@ JackChannelSelection Storage::GetJackChannelSelection(const JackConfiguration &j return jackChannelSelection.RemoveInvalidChannels(jackConfiguration); } - static AlsaSequencerConfiguration MigrateRawMidiToAlsaSequencer(std::vector &selectedDevices) { AlsaSequencerConfiguration result; - try { - auto sequencerPorts = AlsaSequencer::EnumeratePorts(); + try + { + auto sequencerPorts = AlsaSequencer::EnumeratePorts(); // Prepare Migrate raw MIDI devices to ALSA sequencer ports. for (auto i = selectedDevices.begin(); i != selectedDevices.end(); ++i) { - if (i->name_.starts_with("hw:")) { - for (const auto &port: sequencerPorts) + if (i->name_.starts_with("hw:")) + { + for (const auto &port : sequencerPorts) { if (i->name_ == port.rawMidiDevice) { result.connections().push_back( - AlsaSequencerPortSelection(port.id, port.name,port.displaySortOrder) - ); + AlsaSequencerPortSelection(port.id, port.name, port.displaySortOrder)); break; } } } } - } catch (const std::exception&e) { + } + catch (const std::exception &e) + { Lv2Log::error(SS("Failed to migrate MIDI settings. " << e.what())); - // ick. - + // ick. } return result; } @@ -790,23 +797,25 @@ void Storage::LoadAlsaSequencerConfiguration() { Lv2Log::error("I/O error reading %s: %s", fileName.c_str(), e.what()); } - } else { + } + else + { // migrate legacy settings from JackConfiguration? - if (this->isJackChannelSelectionValid) + if (this->isJackChannelSelectionValid) { - this->alsaSequencerConfiguration = + this->alsaSequencerConfiguration = MigrateRawMidiToAlsaSequencer( this->jackChannelSelection.LegacyGetInputMidiDevices()); this->jackChannelSelection.LegacyGetInputMidiDevices().clear(); // let them be clear, the next it gets updated. - - } else { + } + else + { // no legacy settings, so just create a default configuration. this->alsaSequencerConfiguration = AlsaSequencerConfiguration(); } SaveAlsaSequencerConfiguration(); } - } void Storage::SaveAlsaSequencerConfiguration() { @@ -814,7 +823,7 @@ void Storage::SaveAlsaSequencerConfiguration() try { - pipedal::ofstream_synced s(fileName); + pipedal::ofstream_synced s(fileName); json_writer writer(s); writer.write(this->alsaSequencerConfiguration); } @@ -822,7 +831,6 @@ void Storage::SaveAlsaSequencerConfiguration() { Lv2Log::error("I/O error writing %s: %s", fileName.c_str(), e.what()); } - } void Storage::SetAlsaSequencerConfiguration(const AlsaSequencerConfiguration &alsaSequencerConfiguration) @@ -836,7 +844,6 @@ AlsaSequencerConfiguration Storage::GetAlsaSequencerConfiguration() const return this->alsaSequencerConfiguration; } - void Storage::LoadChannelSelection() { auto fileName = this->GetChannelSelectionFileName(); @@ -861,7 +868,7 @@ void Storage::SaveChannelSelection() try { // replaced with AlsaSequencerConfiguration. Delete legacy data. - this->jackChannelSelection.LegacyGetInputMidiDevices().clear(); + this->jackChannelSelection.LegacyGetInputMidiDevices().clear(); pipedal::ofstream_synced s(fileName); json_writer writer(s, false); writer.write(this->jackChannelSelection); @@ -1849,12 +1856,14 @@ static void AddTracksToResult( } const auto &path = dir_entry.path(); auto name = path.filename().string(); - try { + try + { if (dir_entry.is_directory()) { resultFiles.push_back(FileEntry{path, name, true, dir_entry.is_symlink()}); } - } catch (const std::exception &e) + } + catch (const std::exception &e) { Lv2Log::warning(SS("Failed to add directory entry: " << path.string() << " - " << e.what())); } @@ -1903,7 +1912,6 @@ static void AddTracksToResult( throw std::logic_error( SS("AddTracksToResult failed to enumerate audio files. " << rootPath.string() << " - " << error.what())); } - } catch (const std::exception &error) { @@ -2602,6 +2610,47 @@ const PluginPresetIndex &Storage::GetPluginPresetIndex() return pluginPresetIndex; } +void Storage::LoadTone3000Auth() +{ + fs::path path = GetTone3000AuthPath(); + try + { + if (!fs::exists(path)) + { + this->tone3000Auth = ""; + return; + } + std::ifstream s(path); + json_reader reader(s); + reader.read(&(this->tone3000Auth)); + } + catch (const std::exception &e) + { + Lv2Log::error("Failed to load tone3000Auth: %s", e.what()); + } +} + +void Storage::SetTone3000Auth(const std::string &apiKey) +{ + if (tone3000Auth != apiKey) + { + tone3000Auth = apiKey; + + pipedal::ofstream_synced os(this->GetTone3000AuthPath()); + if (!os.is_open()) + { + Lv2Log::error("Failed to open Tone3000 auth file for writing."); + return; + } + json_writer writer(os); + writer.write(apiKey); + } +} +std::string Storage::GetTone3000Auth() const +{ + return tone3000Auth; +} + JSON_MAP_BEGIN(UserSettings) JSON_MAP_REFERENCE(UserSettings, governor) JSON_MAP_REFERENCE(UserSettings, showStatusMonitor) diff --git a/src/Storage.hpp b/src/Storage.hpp index 4ef9346..6d45d63 100644 --- a/src/Storage.hpp +++ b/src/Storage.hpp @@ -73,6 +73,7 @@ private: BankIndex bankIndex; BankFile currentBank; PluginPresetIndex pluginPresetIndex; + std::string tone3000Auth; private: void FillSampleDirectoryTree(FilePropertyDirectoryTree*node, const std::filesystem::path&directory) const; @@ -87,6 +88,7 @@ private: std::filesystem::path GetChannelSelectionFileName(); std::filesystem::path GetAlsaSequencerConfigurationFileName(); std::filesystem::path GetCurrentPresetPath() const; + std::filesystem::path GetTone3000AuthPath() const; void LoadBankIndex(); void SaveBankIndex(); @@ -254,6 +256,10 @@ public: const UiFileProperty&uiFileProperty, bool overwrite = false); FilePropertyDirectoryTree::ptr GetFilePropertydirectoryTree(const UiFileProperty&uiFileProperty,const std::filesystem::path&selectedPath); + + void LoadTone3000Auth(); + void SetTone3000Auth(const std::string&apiKey); + std::string GetTone3000Auth() const; }; diff --git a/src/WebServerConfig.cpp b/src/WebServerConfig.cpp index 657529a..ae23dc9 100644 --- a/src/WebServerConfig.cpp +++ b/src/WebServerConfig.cpp @@ -188,6 +188,10 @@ public: { return true; } + else if (segment == "Tone3000Auth") + { + return true; + } else if (segment == "PluginPresets") { return true; @@ -734,7 +738,17 @@ public: { std::string segment = request_uri.segment(1); - if (segment == "uploadPluginPresets") + if (segment == "Tone3000Auth") { + // https://www.tone3000.com/api/v1/auth?redirect_url=http://10.0.0.151:8080/var/Tone3000Auth&otp_only=true + std::string apiKey = request_uri.query("api_key"); + + model->SetTone3000Auth(apiKey); + + res.set(HttpField::content_type, "application/json"); + res.set(HttpField::cache_control, "no-cache"); + + res.setBody("\"OK\""); + } else if (segment == "uploadPluginPresets") { PluginPresets presets; fs::path filePath = req.get_body_temporary_file(); diff --git a/todo.txt b/todo.txt index 5d5115a..940fc5d 100644 --- a/todo.txt +++ b/todo.txt @@ -1,3 +1,5 @@ +Exctly one underrun per seek in Toob Player + Test autohotspot detection. diff --git a/vite/src/pipedal/App.tsx b/vite/src/pipedal/App.tsx index 68e4672..e2dca51 100644 --- a/vite/src/pipedal/App.tsx +++ b/vite/src/pipedal/App.tsx @@ -24,6 +24,8 @@ import CssBaseline from '@mui/material/CssBaseline'; import VirtualKeyboardHandler from './VirtualKeyboardHandler'; import AppThemed from "./AppThemed"; import { isDarkMode } from './DarkMode'; +import Tone3000AuthComplete from './Tone3000AuthComplete'; + declare module '@mui/material/styles' { interface Theme { @@ -221,6 +223,12 @@ type AppThemeProps = { }; +function isTone3000Auth() { + let url = new URL(window.location.href); + let param = url.searchParams.get("api_key"); + return (param !== null && param !== "") +} + const App = (class extends React.Component { // Before the component mounts, we initialise our state @@ -240,8 +248,11 @@ const App = (class extends React.Component { - - + {isTone3000Auth() ? ( + + ) : ( + + )} ); diff --git a/vite/src/pipedal/FilePropertyDialog.tsx b/vite/src/pipedal/FilePropertyDialog.tsx index b85106d..b927a33 100644 --- a/vite/src/pipedal/FilePropertyDialog.tsx +++ b/vite/src/pipedal/FilePropertyDialog.tsx @@ -21,6 +21,10 @@ import React from 'react'; import { createStyles } from './WithStyles'; +// import Tone3000Dialog from './Tone3000Dialog'; +import Tone3000HelpDialog from './Tone3000HelpDialog'; +import HelpOutlineIcon from '@mui/icons-material/HelpOutline'; +import Link from '@mui/material/Link'; import DraggableButtonBase from './DraggableButtonBase'; import CloseIcon from '@mui/icons-material/Close'; import { Theme } from '@mui/material/styles'; @@ -64,6 +68,8 @@ import FilePropertyDirectorySelectDialog from './FilePropertyDirectorySelectDial import { getAlbumArtUri, getTrackTitle } from './AudioFileMetadata'; +const ToobNamModelFileUrl = "http://two-play.com/plugins/toob-nam#modelFile"; + const AUTOSCROLL_TICK_DELAY = 30; const AUTOSCROLL_THRESHOLD = 48; const AUTOSCROLL_SCROLL_PER_SECOND = 500; @@ -161,6 +167,8 @@ export interface FilePropertyDialogState { initialSelection: string; multiSelect: boolean, selectedFiles: string[], + //openTone3000Dialog: boolean, + openTone3000Help: boolean }; @@ -231,7 +239,9 @@ export default withStyles( copyDialogOpen: false, initialSelection: this.props.selectedFile, multiSelect: false, - selectedFiles: [] + selectedFiles: [], + //openTone3000Dialog: false, + openTone3000Help: false }; this.requestScroll = true; } @@ -362,7 +372,7 @@ export default withStyles( dragElementDy: 0, from: index, to: index, - lastMousePoint: {...this.longPressStartPoint} + lastMousePoint: { ...this.longPressStartPoint } }; this.setState({ dragState: dragState }); } @@ -882,7 +892,6 @@ export default withStyles( style={{ marginLeft: 16, marginBottom: 8, marginRight: 8, textOverflow: "", display: "flex", flexFlow: "row wrap", alignItems: "center", justifyContent: "start" }}> {breadcrumbs} - ); } @@ -998,8 +1007,7 @@ export default withStyles( let dragElementDy = dragState.lastMousePoint.y - this.longPressStartPoint.y; let originalY = dragState.from * dragState.height; let newY = originalY + dragElementDy; - if (newY < 0) - { + if (newY < 0) { newY = 0; dragElementDy = -originalY; } @@ -1040,9 +1048,26 @@ export default withStyles( () => { this.handleAutoScrollTick() }, AUTOSCROLL_TICK_DELAY); } + // handleTone3000Dialog(e: React.MouseEvent) { + // e.stopPropagation(); + // e.preventDefault(); + + // this.setState({ openTone3000Dialog: true }); + + // } + + + handleTone3000Help(e: React.MouseEvent) { + e.stopPropagation(); + e.preventDefault(); + + this.setState({ openTone3000Help: true }); + + } render() { const isTracksDirectory = this.isTracksDirectory(); + const isToobNamModelFile = this.props.fileProperty.patchProperty === ToobNamModelFileUrl; const classes = withStyles.getClasses(this.props); let columnWidth = this.state.columnWidth; @@ -1102,7 +1127,7 @@ export default withStyles( background: this.state.reordering || this.state.multiSelect ? (isDarkMode() ? "#523" : "#EDF") : undefined, - marginBottom: 16, paddingTop: 0 + marginBottom: 8, paddingTop: 0 }} > {this.state.reordering && ( @@ -1257,7 +1282,7 @@ export default withStyles( - { this.listContainerElementRef = element; this.onMeasureRef(element); }} style={{ flex: "1 1 100%", display: "flex", flexFlow: "row wrap", - position: "relative", justifyContent: "flex-start", alignContent: "flex-start", paddingLeft: 16, paddingBottom: 16 + position: "relative", justifyContent: "flex-start", alignContent: "flex-start", + paddingLeft: 16, paddingBottom: 16, paddingTop: 16, }}> { (this.state.columns !== 0) && // don't render until we have number of columns derived from layout. @@ -1461,45 +1487,64 @@ export default withStyles( {(!this.state.reordering && !this.state.multiSelect) && ( <> - {this.state.windowWidth > 500 ? ( -
- this.handleDelete()} > - - +
+ {isToobNamModelFile && ( +
+ + Download model files from TONE3000 + + { this.handleTone3000Help(e); }} aria-label="help" edge="end" color="inherit" style={{ opacity: 0.6, marginLeft: 8 }} + > + + +
- } - onClick={() => { this.setState({ openUploadFileDialog: true }) }} disabled={protectedDirectory} - > -
Upload
-
+ )} - } - onClick={() => { this.handleDownloadFile(); }} disabled={protectedDirectory || !this.state.hasFileSelection} - > -
Download
-
+
+ this.handleDelete()} > + + -
 
+ } + onClick={() => { this.setState({ openUploadFileDialog: true }) }} disabled={protectedDirectory} + > +
Upload
+
- - +
 
+ + + +
) : (
@@ -1607,7 +1652,7 @@ export default withStyles( { this.state.renameDialogOpen && ( { this.setState({ renameDialogOpen: false }); this.onExecuteRename(newName) }} onClose={() => { this.setState({ renameDialogOpen: false }); }} acceptActionName="OK" @@ -1645,6 +1690,18 @@ export default withStyles( ) ) } + {/* {this.state.openTone3000Dialog && ( + this.setState({ openTone3000Dialog: false })} + /> + )} */} + {this.state.openTone3000Help && ( + this.setState({ openTone3000Help: false })} + /> + )} ); } diff --git a/vite/src/pipedal/PiPedalModel.tsx b/vite/src/pipedal/PiPedalModel.tsx index d7545be..c9c2e5a 100644 --- a/vite/src/pipedal/PiPedalModel.tsx +++ b/vite/src/pipedal/PiPedalModel.tsx @@ -420,6 +420,8 @@ export class PiPedalModel //implements PiPedalModel webSocket?: PiPedalSocket; + hasTone3000Auth: ObservableProperty = new ObservableProperty(false); + hasWifiDevice: ObservableProperty = new ObservableProperty(false); onSnapshotModified: ObservableEvent = new ObservableEvent(); @@ -760,6 +762,8 @@ export class PiPedalModel //implements PiPedalModel } else if (message === "onErrorMessage") { this.showAlert(body as string); + } else if (message == "onTone3000AuthChanged") { + this.hasTone3000Auth.set(body as boolean); } else if (message === "onLv2PluginsChanging") { this.onLv2PluginsChanging(); @@ -1142,6 +1146,9 @@ export class PiPedalModel //implements PiPedalModel this.alsaSequencerConfiguration.set(new AlsaSequencerConfiguration().deserialize( await this.getWebSocket().request("getAlsaSequencerConfiguration") )); + this.hasTone3000Auth.set( + await this.getWebSocket().request("getHasTone3000Auth") + ); this.banks.set(new BankIndex().deserialize(await this.getWebSocket().request("getBankIndex"))); this.favorites.set(await this.getWebSocket().request("getFavorites")); diff --git a/vite/src/pipedal/Tone3000AuthComplete.tsx b/vite/src/pipedal/Tone3000AuthComplete.tsx new file mode 100644 index 0000000..a74d6be --- /dev/null +++ b/vite/src/pipedal/Tone3000AuthComplete.tsx @@ -0,0 +1,85 @@ + +import React from 'react'; +import Typography from '@mui/material/Typography'; + +import { styled } from '@mui/material/styles'; + + +const Frame = styled('div', { + name: 'MuiStat', // The component name + slot: 'root', // The slot name +})(({ theme }) => ({ + flex: "1 1 auto", + gap: theme.spacing(0.5), + padding: theme.spacing(3, 4), + backgroundColor: theme.palette.background.paper, + width: "100%", + height: "100%" +})); + + +interface Tone3000AuthCompleteProps { + +} + +function Tone3000AuthComplete(props: Tone3000AuthCompleteProps) { + + function postApiKey() { + let url = new URL(window.location.href); + let apiKey = url.searchParams.get("api_key"); + if (apiKey) { + let myUrl = new URL(window.location.href); + let port = myUrl.port; + if (port === "5173") { // when running debug server. + port = "8080"; + } + let postUrl = "http://" + myUrl.hostname + ":" + port + "/var/Tone3000Auth?api_key=" + encodeURIComponent(apiKey); + fetch(postUrl, { + method: "POST", + headers: { + 'Content-Type': 'application/json' + }, + body: JSON.stringify({ api_key: apiKey }) + }).then((response) => { + if (!response.ok) { + alert("Failed to post API key to Pipedal: " + response.statusText); + } + return response.json(); + }) + .catch((error) => { + alert("Error posting API key to Pipedal: " + error.message); + }) + } else { + alert("No API key found in URL."); + } + } + React.useEffect(() => { + postApiKey(); + return () => { }; + }) + + return ( + +
+
+
+ TONE3000 Authorization Complete + + You have successfully obtained an access token from TONE3000, and can now download Toob Neural Amp models from TONE3000 + directly. + + Close this page and return to Pipedal in order to continue. +
+
+
+ + ); + +} + +export default Tone3000AuthComplete; \ No newline at end of file diff --git a/vite/src/pipedal/Tone3000Dialog.tsx b/vite/src/pipedal/Tone3000Dialog.tsx new file mode 100644 index 0000000..8b889fc --- /dev/null +++ b/vite/src/pipedal/Tone3000Dialog.tsx @@ -0,0 +1,163 @@ + + +import React from 'react'; +import Dialog from '@mui/material/Dialog'; +import DialogTitle from '@mui/material/DialogTitle'; +import DialogContent from '@mui/material/DialogContent'; +import DialogActions from '@mui/material/DialogActions'; +import Button from '@mui/material/Button'; +import Typography from '@mui/material/Typography'; +import { PiPedalModel, PiPedalModelFactory } from './PiPedalModel'; +import DialogEx from './DialogEx'; +import Link from '@mui/material/Link'; +import Toolbar from '@mui/material/Toolbar'; +import IconButtonEx from './IconButtonEx'; +import ArrowBackIcon from '@mui/icons-material/ArrowBack'; + + + + +export interface Tone3000DialogProps { + open: boolean; + onClose: () => void; +} + +function Tone3000Dialog(props: Tone3000DialogProps) { + const { open, onClose } = props; + + const model: PiPedalModel = PiPedalModelFactory.getInstance(); + + let [openAuthDialog, setOpenAuthDialog] = React.useState(!model.hasTone3000Auth.get()); + + + React.useEffect(()=> { + let authListener = (value: boolean) => { + setOpenAuthDialog(!value); + } + model.hasTone3000Auth.addOnChangedHandler(authListener); + return () => { + model.hasTone3000Auth.removeOnChangedHandler(authListener); + } + }); + + function RedirectUrl() { + let baseUrl = new URL(window.location.href); + let result = "http://" + baseUrl.hostname + ":" + baseUrl.port + "/"; + return result; + } + function AuthUrl() { + //return "https://www.tone3000.com/api/v1/auth?redirect_url=" + encodeURIComponent(RedirectUrl()) + "&opt_only=true"; + return "https://www.tone3000.com/api/v1/auth?redirect_url=" + encodeURIComponent(RedirectUrl()) + "&otp_only=true"; + } + return ( + { onClose(); }} + onClose={() => { onClose(); }} + aria-labelledby="tone3000-dialog-title" + aria-describedby="tone3000-dialog-description" + > + + + { onClose(); }} + > + + + + + TONE3000 Models + + + + + + + This is a placeholder for the Tone 3000 dialog. + + + + + + { + openAuthDialog && ( + + + + + { onClose(); }} + > + + + + + TONE3000 Authorization + + + + + +
+
+
+ + The TONE3000 website provides an + online library of models for use with TooB Neural Amp Modeller. You can download + models from the TONE3000 website onto your local machine and then upload them to + PiPedal; or, more conveniently, you can browse the TONE3000 model database directly, and download models directly + to the PiPedal server from the TONE3000 model database in a single step. + + + In order to access the TONE3000 database, you must first obtain an access token from the + TONE3000 website. Clicking on the button will take you to an external website in + order to complete the authorization process. + + + + + Privacy statement: PiPedal will only have access to your authorization token + which does not contain personally identifying information, and which is stored locally on + your PiPedal server. Please refer to + the TONE3000 privacy policy for + information on how your data is used by TONE3000. + +
+
+
+ + + +
+ )} +
+ + ); +} + +export default Tone3000Dialog; \ No newline at end of file diff --git a/vite/src/pipedal/Tone3000HelpDialog.tsx b/vite/src/pipedal/Tone3000HelpDialog.tsx new file mode 100644 index 0000000..6d6c477 --- /dev/null +++ b/vite/src/pipedal/Tone3000HelpDialog.tsx @@ -0,0 +1,92 @@ +import DialogEx from "./DialogEx"; +import DialogTitle from "@mui/material/DialogTitle"; +import DialogContent from "@mui/material/DialogContent"; +import Toolbar from "@mui/material/Toolbar"; +import IconButtonEx from "./IconButtonEx"; +import ArrowBackIcon from "@mui/icons-material/ArrowBack"; +import Typography from "@mui/material/Typography"; +import Link from "@mui/material/Link"; + + + +function Tone3000HelpDialog(props: { + open: boolean; + onClose: () => void; +}) { + const { open, onClose } = props; + return ( + { onClose(); }} + onClose={() => { onClose(); }} + open={open}> + + + + { onClose(); }} + > + + + + + TONE3000 Help + + + + + +
+
+
+ + The TONE3000 website provides a + massive collection of neural amp models that can be used by TooB Neural Amp Modeler. + + + Using TONE3000 model files in PiPedal is a two step process. First, download model files from + the website using your browser; and then upload + the files in your browser's Downloads directory to the PiPedal server using + the Upload button. PiPedal + automatically extracts bundles of model files from zip archives, so manual extraction is unnecessary. + + + TONE3000 is a community-driven platform where community members share Neural Amp Modeler profiles. + The site showcases the collaborative spirit of the guitar modeling community. And all of this is made + possible by Steven Atkins' Neural Amp Modeler library, which forms the foundation and core of TooB Neural Amp Modeler and other NAM-based plugins. + + + If you would like to profile your own amplifiers, and effects, the TONE3000 website allows you to + generate your own NAM profiles for free. Refer to + the TONE3000 website for more details. + + + When you click on the TONE3000 link, you will be taken to an external website. The TONE3000 + website is not part of PiPedal, and is not affiliated in any way with PiPedal. + + + + Privacy statement: PiPedal has no access to personal data used by the TONE3000 website. Please refer to + the TONE3000 privacy policy for + information on how your data is used by TONE3000. + +
+
+
+ + + + + ); +} +export default Tone3000HelpDialog; \ No newline at end of file