NAM A2 Calibration

This commit is contained in:
Robin E.R. Davies
2026-05-13 11:22:42 -04:00
parent c456e5187b
commit 7343ced278
13 changed files with 243 additions and 72 deletions
+3 -1
View File
@@ -33,7 +33,9 @@
"logLevel": 3,
/* Maximum filesize to allow when uploading */
"maxUploadSize": 536870912 // 512MiB
"maxUploadSize": 536870912, // 512MiB
/* false-> Download A1 models; true -> Download A2 models */
"tone3000A2Models": true
}
+1
View File
@@ -101,6 +101,7 @@ JSON_MAP_REFERENCE(PiPedalConfiguration, threads)
JSON_MAP_REFERENCE(PiPedalConfiguration, logLevel)
JSON_MAP_REFERENCE(PiPedalConfiguration, logHttpRequests)
JSON_MAP_REFERENCE(PiPedalConfiguration, maxUploadSize)
JSON_MAP_REFERENCE(PiPedalConfiguration, tone3000A2Models)
JSON_MAP_REFERENCE(PiPedalConfiguration, accessPointGateway)
JSON_MAP_REFERENCE(PiPedalConfiguration, accessPointServerAddress)
JSON_MAP_REFERENCE(PiPedalConfiguration, isVst3Enabled)
+2
View File
@@ -45,6 +45,7 @@ private:
bool logHttpRequests_ = false;
int logLevel_ = 0;
uint64_t maxUploadSize_ = 1024*1024;
bool tone3000A2Models_ = true;
std::string accessPointGateway_;
std::string accessPointServerAddress_;
bool isVst3Enabled_ = true;
@@ -67,6 +68,7 @@ public:
bool GetMLock() const { return mlock_; }
uint64_t GetMaxUploadSize() const { return maxUploadSize_; }
bool GetTone3000A2Models() const { return tone3000A2Models_; }
LogLevel GetLogLevel() const { return (LogLevel)this->logLevel_; }
bool LogHttpRequests() const { return this->logHttpRequests_; }
+2
View File
@@ -364,6 +364,8 @@ namespace pipedal
void Init(const PiPedalConfiguration &configuration);
const PiPedalConfiguration &Configuration() const { return configuration; }
void LoadLv2PluginInfo();
void Load();
+5 -2
View File
@@ -1315,12 +1315,14 @@ static std::string StripPortNumber(const std::string &fromAddress)
class InterceptConfig : public RequestHandler
{
private:
PiPedalModel&model;
uint64_t maxUploadSize;
int portNumber;
public:
InterceptConfig(int portNumber, uint64_t maxUploadSize)
InterceptConfig(PiPedalModel&model,int portNumber, uint64_t maxUploadSize)
: RequestHandler("/var/config.json"),
model(model),
maxUploadSize(maxUploadSize),
portNumber(portNumber)
{
@@ -1337,6 +1339,7 @@ public:
<< "\", \"ui_plugins\": [ ], \"max_upload_size\": " << maxUploadSize
<< ", \"enable_auto_update\": " << (ENABLE_AUTO_UPDATE ? " true" : "false")
<< ", \"has_wifi_device\": " << (HotspotManager::HasWifiDevice() ? " true" : "false")
<< ", \"tone3000_A2_models\": " << (model.Configuration().GetTone3000A2Models() ? " true" : "false")
<< " }";
return s.str();
@@ -1396,7 +1399,7 @@ void pipedal::ConfigureWebServer(
int port,
size_t maxUploadSize)
{
std::shared_ptr<RequestHandler> interceptConfig{new InterceptConfig(port, maxUploadSize)};
std::shared_ptr<RequestHandler> interceptConfig{new InterceptConfig(model,port, maxUploadSize)};
server.AddRequestHandler(interceptConfig);
std::shared_ptr<DownloadIntercept> downloadIntercept = std::make_shared<DownloadIntercept>(&model);
+14 -1
View File
@@ -1,4 +1,17 @@
Benchmark Results:
A2 Questions
Does A2 support Callibration?
input_level_dbu, output_level_dbu accessors on NeuralAmpModelerCore DSP
slimmable_weights accessors on NeuralAmpModelerCore DSP
standard values for slimmable_weights?
Implement NAM Model Weight selection UI
NAM INput VU is f*ed up.
File Player Underrun is NOT CPU use related. There's an underrun somewhere.
Update Pi 4 Benchmark Results:
The benchmark is to run TooB Neural Amp Modeler with an A1 model to generate 100s of audio. Lower times are better.
+3 -1
View File
@@ -6,5 +6,7 @@
"fakeAndroid": false,
"ui_plugins": [],
"enable_auto_update": true,
"has_wifi_device": false
"has_wifi_device": false,
"tone3000_A2_models": true,
"end": true
}
+1 -1
View File
@@ -949,7 +949,7 @@ export class UiControl implements Deserializable<UiControl> {
return scale_point.label;
}
}
return "#invalid";
return value.toString();
} else if (this.integer_property) {
return value.toFixed(0);
} else {
+2
View File
@@ -1316,6 +1316,7 @@ export class PiPedalModel //implements PiPedalModel
maxFileUploadSize: number = 512 * 1024 * 1024;
maxPresetUploadSize: number = 1024 * 1024;
debug: boolean = false;
tone3000_A2_models: boolean = true;
enableAutoUpdate: boolean = false;
@@ -1326,6 +1327,7 @@ export class PiPedalModel //implements PiPedalModel
let response: Response = await fetch(myRequest);
let data = await response.json();
this.tone3000_A2_models = data.tone3000_A2_models ?? true;
this.enableAutoUpdate = !!data.enable_auto_update;
this.hasWifiDevice.set(!!data.has_wifi_device);
if (data.max_upload_size) {
+62 -2
View File
@@ -154,6 +154,66 @@ export type PluginControlState = {
previewValue?: string;
};
export interface CustomPluginControlProps extends WithStyles<typeof pluginControlStyles> {
title: string;
isSelect?: boolean;
isButton?: boolean;
item_width?: number;
mainControl: React.ReactNode;
editControl?: React.ReactNode;
};
export interface CustomPluginControlState {
};
const CustomPluginControl =
withStyles(
class extends Component<CustomPluginControlProps, CustomPluginControlState> {
constructor(props: CustomPluginControlProps) {
super(props);
}
render() {
let classes = withStyles.getClasses(this.props);
return (
<div
className={classes.controlFrame}
style={{ width: this.props.item_width }}
>
{/* TITLE SECTION */}
<div className={classes.titleSection}
style={
{
alignSelf: "stretch", marginBottom: 8, marginLeft: this.props.isSelect ? 8 : 0, marginRight: 0
}}>
<Typography variant="caption" display="block" noWrap style={{
width: "100%",
textAlign: this.props.isSelect ? "left" : "center"
}}> {this.props.isButton ? "\u00A0" : this.props.title}</Typography>
</div>
{/* CONTROL SECTION */}
<div className={classes.midSection}>
{this.props.mainControl}
</div>
{/* LABEL/EDIT SECTION*/}
<div className={classes.editSection} >
{this.props.editControl}
</div>
</div>
);
return (<div />);
}
},
pluginControlStyles
);
export { CustomPluginControl };
const PluginControl =
withTheme(withStyles(
class extends Component<PluginControlProps, PluginControlState> {
@@ -619,7 +679,7 @@ const PluginControl =
break;
}
}
this.setState({ previewValue: undefined});
this.setState({ previewValue: undefined });
}
previewInputValue(value: number, commitValue: boolean) {
let range = this.valueToRange(value);
@@ -1110,7 +1170,7 @@ const PluginControl =
error={this.state.error}
inputProps={{
className: "scrollMod",
type: isMobileDevice()? "text": "number",
type: isMobileDevice() ? "text" : "number",
inputMode: "numeric",
min: this.props.uiControl?.min_value,
+12 -5
View File
@@ -38,9 +38,9 @@ import { UiPlugin, UiControl, UiFileProperty, UiFrequencyPlot, ScalePoint } from
import {
Pedalboard, PedalboardItem, ControlValue
} from './Pedalboard';
import PluginControl from './PluginControl';
import PluginControl, {CustomPluginControl} from './PluginControl';
import ResizeResponsiveComponent from './ResizeResponsiveComponent';
import SideChainSelectControl, {SideChainSelectDialog} from './SideChainSelectControl';
import SideChainSelectControl, { SideChainSelectDialog } from './SideChainSelectControl';
import VuMeter from './VuMeter';
import { nullCast } from './Utility'
import { PiPedalStateError } from './PiPedalError';
@@ -66,6 +66,7 @@ const LANDSCAPE_HEIGHT_BREAK = 500;
export interface ICustomizationHost {
makeStandardControl(uiControl: UiControl, controlValues: ControlValue[]): ReactNode;
makeCustomControl(title: string, mainControl: ReactNode, editControl?: ReactNode): ReactNode;
renderControlGroup(controlGroup: ControlGroup, key: string): ReactNode;
isLandscapeGrid(): boolean;
}
@@ -379,6 +380,7 @@ type PluginControlViewState = {
sidechainDialogOpen: boolean
};
const PluginControlView =
withTheme(withStyles(
class extends ResizeResponsiveComponent<PluginControlViewProps, PluginControlViewState> {
@@ -495,6 +497,7 @@ const PluginControlView =
}
private ixKey: number = 1;
makeStandardControl(uiControl: UiControl, controlValues: ControlValue[]): ReactNode {
let symbol = uiControl.symbol;
if (!uiControl.is_input) {
@@ -882,7 +885,7 @@ const PluginControlView =
}
handleSelectSidechainInput() {
this.setState({sidechainDialogOpen: true});
this.setState({ sidechainDialogOpen: true });
}
handleModGuiFileProperty(instanceId: number, filePropertyUri: string, selectedFile: string) {
@@ -963,7 +966,9 @@ const PluginControlView =
)
}
makeCustomControl(title: string, mainControl: ReactNode, editControl?: ReactNode): ReactNode {
return (<CustomPluginControl title={title} mainControl={mainControl} editControl={editControl} />);
}
renderPiPedalControl(pedalboardItem?: PedalboardItem): ReactNode {
this.controlKeyIndex = 0;
@@ -1115,7 +1120,7 @@ const PluginControlView =
)}
{this.state.sidechainDialogOpen && (
<SideChainSelectDialog open={this.state.sidechainDialogOpen}
onClose={()=>{ this.setState({sidechainDialogOpen: false});}}
onClose={() => { this.setState({ sidechainDialogOpen: false }); }}
selectItems={this.getSidechainSelectItems()}
selectedInstanceId={this.props.item.sideChainInputId}
onChanged={(instanceId: number) => {
@@ -1142,3 +1147,5 @@ const PluginControlView =
export default PluginControlView;
+2 -7
View File
@@ -28,7 +28,6 @@ const THROTTLING_TRIGGGER_LEVEL = Math.floor(ALLOWED_DOWNLOADS_PER_MINUTE/2);
class DownloadThrottler {
private throttleActive: boolean = false;
private fifo: number[] = [];
private downloadCount: number = 0;
@@ -39,11 +38,7 @@ class DownloadThrottler {
this.fifo.shift();
}
if (this.fifo.length > THROTTLING_TRIGGGER_LEVEL) {
this.throttleActive = true;
} else {
this.throttleActive = false;
}
this.downloadCount = this.fifo.length;
}
getThrottleDelay(now : number = Date.now()): number {
while (this.fifo.length > 0 && this.fifo[0] + 60000 <= now) {
@@ -264,7 +259,7 @@ export class Tone3000DownloadHandler {
let models: Model[] = [];
let architectureFilter: number | undefined = undefined;
// Take A2 models only for NAM downloads, and only if the tone actually has A2 models.
if (downloadType === Tone3000DownloadType.Nam && !!tone.a2_models_count) {
if (downloadType === Tone3000DownloadType.Nam && !!tone.a2_models_count && this.model.tone3000_A2_models) {
architectureFilter = 2; // NAM models only
}
+109 -27
View File
@@ -27,14 +27,20 @@ import { withStyles } from "tss-react/mui";
import IControlViewFactory from './IControlViewFactory';
import { PiPedalModelFactory, PiPedalModel, ListenHandle, State } from "./PiPedalModel";
import { PedalboardItem,ControlValue } from './Pedalboard';
import { PedalboardItem, ControlValue } from './Pedalboard';
import PluginControlView, { ICustomizationHost, ControlGroup, ControlViewCustomization } from './PluginControlView';
import { UiPlugin,UiControl, ControlType } from './Lv2Plugin';
import { UiPlugin, UiControl, ControlType } from './Lv2Plugin';
import Select from '@mui/material/Select/Select';
import MenuItem from '@mui/material/MenuItem/MenuItem';
import { CustomPluginControl } from './PluginControl';
const TOOB_NAM__MODEL_METADATA = "http://two-play.com/plugins/toob-nam#model_metadata";
const TOOB_NAM__MODEL_METADATA_URI = "http://two-play.com/plugins/toob-nam#model_metadata";
const TOOB_NAM__MODEL_WEIGHT_URI = "http://two-play.com/plugins/toob-nam#modelWeight";
// offset 0: an integer with the folloing bits set.
// Must match ToobAmp/src/NeuralAmpModeler_Lv2Extensions.hpp enum TOOB_NAM_METADATA_OFFSETS
const MAX_SLIMMABLE_WEIGHTS = 16;
class TOOB_NAM_METADATA_OFFSETS {
static readonly flags = 0;
static readonly preset_version = 1;
@@ -42,7 +48,11 @@ class TOOB_NAM_METADATA_OFFSETS {
static readonly gain = 3;
static readonly input_level_dbu = 4;
static readonly output_level_dbu = 5;
static readonly max_medatadata = 6;
static readonly is_a2 = 6;
static readonly model_weight = 7;
static readonly slimmable_weights_size = 8;
static readonly slimmable_weights = 9;
static readonly max_medatadata = TOOB_NAM_METADATA_OFFSETS.slimmable_weights + MAX_SLIMMABLE_WEIGHTS;
};
@@ -55,8 +65,7 @@ class TOOB_NAM_METADATA_FLAGS {
};
class ModelMetadata {
constructor(metadataValues?: number[])
{
constructor(metadataValues?: number[]) {
if (metadataValues) {
@@ -72,6 +81,12 @@ class ModelMetadata {
this.inputlevelDBU = metadataValues[TOOB_NAM_METADATA_OFFSETS.input_level_dbu];
this.outputlevelDBU = metadataValues[TOOB_NAM_METADATA_OFFSETS.output_level_dbu];
this.preset_version = metadataValues[TOOB_NAM_METADATA_OFFSETS.preset_version];
this.isA2 = metadataValues[TOOB_NAM_METADATA_OFFSETS.is_a2] !== 0;
this.modelWeight = metadataValues[TOOB_NAM_METADATA_OFFSETS.model_weight];
let slimmableWeightsSize = metadataValues[TOOB_NAM_METADATA_OFFSETS.slimmable_weights_size];
for (let i = 0; i < slimmableWeightsSize && i < 16; ++i) {
this.slimmable_weights.push(metadataValues[TOOB_NAM_METADATA_OFFSETS.slimmable_weights + i]);
}
} else {
this.hasModel = false;
this.hasLoudness = false;
@@ -84,11 +99,16 @@ class ModelMetadata {
this.inputlevelDBU = 0;
this.outputlevelDBU = 0;
this.preset_version = 1;
this.modelWeight = -1;
this.isA2 = false;
this.slimmable_weights = [];
}
}
preset_version: number;
isA2: boolean = false;
modelWeight: number = 1.0;
slimmable_weights: number[] = [];
hasModel: boolean;
hasLoudness: boolean;
hasGain: boolean;
@@ -100,6 +120,10 @@ class ModelMetadata {
inputlevelDBU: number;
outputlevelDBU: number;
hasSlimmableWeights(): boolean {
return this.slimmable_weights.length > 0;
}
}
@@ -114,8 +138,10 @@ interface ToobNamViewProps extends WithStyles<typeof styles> {
interface ToobNamViewState {
showEqSection: boolean;
enableCalibration: boolean;
showCalibration: boolean;
enableOutputNormalization: boolean;
modelMetadata: ModelMetadata;
modelWeight: number,
}
const ToobNamView =
@@ -133,18 +159,18 @@ const ToobNamView =
this.model = PiPedalModelFactory.getInstance();
this.state = {
showEqSection: false,
showCalibration: false,
enableCalibration: false,
enableOutputNormalization: false,
modelMetadata: new ModelMetadata()
modelMetadata: new ModelMetadata(),
modelWeight: -1
}
let pluginInfo: UiPlugin | null = this.model.getUiPlugin(this.props.item.uri);
if (pluginInfo === null)
{
if (pluginInfo === null) {
throw new Error("Plugin not fouund.");
}
let inputCalibrationControl = pluginInfo.getControl("inputCalibrationMode");
if (!inputCalibrationControl)
{
if (!inputCalibrationControl) {
throw new Error("Control not found.");
}
let patchedInputControl = new UiControl().deserialize(inputCalibrationControl);
@@ -152,14 +178,13 @@ const ToobNamView =
this.patchedInputControl = patchedInputControl
let outputCalibrationControl = pluginInfo.getControl("outputCalibration");
if (!outputCalibrationControl)
{
if (!outputCalibrationControl) {
throw new Error("Control not found.");
}
let patchedOutputControl = new UiControl().deserialize(outputCalibrationControl);
patchedOutputControl.controlType = ControlType.Select;
this.noCalibrationOutputControl = patchedOutputControl
this.noCalibrationOutputControl.scale_points.splice(1,1);
this.noCalibrationOutputControl.scale_points.splice(1, 1);
}
private patchedInputControl: UiControl;
@@ -189,42 +214,94 @@ const ToobNamView =
modifyControls(host: ICustomizationHost, controls: (React.ReactNode | ControlGroup)[]): (React.ReactNode | ControlGroup)[] {
let EqPos = 7;
let CalibrationGroupPos = 8;
const CalibrationGroupPos = 8;
const ModelSizeControlPos = 9;
const ModelWeightControlPos = 10;
let calibrationGroup = controls[CalibrationGroupPos] as ControlGroup;
calibrationGroup.controls[0] = host.makeStandardControl(this.patchedInputControl,this.props.item.controlValues);
if (this.state.enableCalibration)
{
calibrationGroup.controls[0] = host.makeStandardControl(this.patchedInputControl,this.props.item.controlValues);
if (this.state.showCalibration) {
calibrationGroup.controls[0] = host.makeStandardControl(this.patchedInputControl, this.props.item.controlValues);
if (this.state.enableCalibration) {
calibrationGroup.controls[0] = host.makeStandardControl(this.patchedInputControl, this.props.item.controlValues);
} else {
calibrationGroup.controls[0] = host.makeStandardControl(this.patchedInputControl,
[ new ControlValue("inputCalibrationMode",0.0)]
[new ControlValue("inputCalibrationMode", 0.0)]
);
calibrationGroup.controls[0] = this.disableControl(calibrationGroup.controls[0], "cg_01d");
calibrationGroup.controls[1] = this.disableControl(calibrationGroup.controls[1], "cg_02d");
if (this.state.enableOutputNormalization)
{
if (this.state.enableOutputNormalization) {
calibrationGroup.controls[2] = host.makeStandardControl(this.noCalibrationOutputControl, this.props.item.controlValues);
} else {
let tControl: UiControl = this.noCalibrationOutputControl.clone();
tControl.symbol = "_disabled_output";
calibrationGroup.controls[2] = host.makeStandardControl(
tControl,
[ new ControlValue("_disabled_output",2.0)]);
[new ControlValue("_disabled_output", 2.0)]);
calibrationGroup.controls[2] = this.disableControl(calibrationGroup.controls[2], "cg_03d");
}
}
} else {
controls[CalibrationGroupPos] = null;
}
controls[ModelSizeControlPos] = null;
controls[ModelWeightControlPos] = null;
if (!this.state.showEqSection) {
controls.splice(EqPos, 1);
}
if (this.state.modelMetadata.hasSlimmableWeights()) {
controls.splice(3, 0, this.MakeModelWeightSelector(host));
}
return controls;
}
ModelWeightToString(weight: number): string {
switch (weight) {
case 1.0: return "Standard";
case 0.5: return "Lite";
case 0.25: return "Feather";
default: return weight.toString();
}
}
SelectModelWeight(weight: number) {
this.setState({ modelWeight: weight });
this.model.setPatchProperty(this.props.instanceId, TOOB_NAM__MODEL_WEIGHT_URI, weight);
}
MakeModelWeightSelector(host: ICustomizationHost): React.ReactNode {
if ((this.state.modelMetadata.modelWeight === undefined || this.state.modelMetadata.modelWeight || this.state.modelMetadata.hasSlimmableWeights()) === false) {
return null;
}
let selectControl = (
<Select key="model_weight_control" variant="standard" size="small" value={this.state.modelMetadata.modelWeight}
onChange={(e) => { this.SelectModelWeight(Number(e.target.value)) }}
style={{ marginLeft: 4, marginRight: 4, width: 140, fontSize: 12, marginTop: 4 }}
>
{this.state.modelMetadata.slimmable_weights.map((weight, index) => {
return (
<MenuItem key={weight.toString()} value={weight}
style={{ fontSize: 14 }}
>
{this.ModelWeightToString(weight)}
</MenuItem>
);
})}
</Select>
);
return (
<div style={{ marginLeft: 12 }}>
<CustomPluginControl title="Model Weight" mainControl={selectControl} isSelect={true} />
</div>);
}
private handleConnectionStateChanged(state: State) {
if (state === State.Ready) {
this.unsubscribeFromMetadata();
@@ -235,11 +312,16 @@ const ToobNamView =
handleModelMetadata(atomData: any) {
if (atomData && atomData.otype_ === "Vector" && atomData.value) {
let metadata = new ModelMetadata(atomData.value as number[]);
metadata.hasInputLevelDBU || metadata.hasOutputLevelDBU
this.setState({
modelMetadata: metadata,
showEqSection: metadata.preset_version === 0,
showCalibration: (
metadata.hasInputLevelDBU || metadata.hasOutputLevelDBU ||
(metadata.hasLoudness && metadata.hasGain)) && metadata.hasModel,
enableCalibration: metadata.hasInputLevelDBU && metadata.hasModel,
enableOutputNormalization: metadata.hasLoudness && metadata.hasModel
enableOutputNormalization: metadata.hasLoudness && metadata.hasModel,
modelWeight: metadata.modelWeight
});
}
}
@@ -248,13 +330,13 @@ const ToobNamView =
this.subscribedId = this.props.instanceId;
this.listenHandle = this.model.monitorPatchProperty(
this.props.instanceId,
TOOB_NAM__MODEL_METADATA,
TOOB_NAM__MODEL_METADATA_URI,
(instanceId, propertyUri, atomData) => {
this.handleModelMetadata(atomData);
});
this.model.getPatchProperty(
this.props.instanceId,
TOOB_NAM__MODEL_METADATA
TOOB_NAM__MODEL_METADATA_URI
).then((atomData) => {
this.handleModelMetadata(atomData);
}).catch((e) => {