Subscription Mutex Deadlock
This commit is contained in:
@@ -572,8 +572,12 @@ export default withStyles(
|
||||
return;
|
||||
}
|
||||
if (resultPath.startsWith(this.state.navDirectory)) {
|
||||
let selectedFile = resultPath;
|
||||
if (selectedFile.endsWith("/")) {
|
||||
selectedFile = selectedFile.slice(0, -1);
|
||||
}
|
||||
this.setState({
|
||||
selectedFile: resultPath,
|
||||
selectedFile: selectedFile,
|
||||
});
|
||||
this.requestFiles(this.state.navDirectory);
|
||||
}
|
||||
|
||||
@@ -593,6 +593,7 @@ export class UiControl implements Deserializable<UiControl> {
|
||||
this.max_value = input.max_value;
|
||||
this.default_value = input.default_value;
|
||||
this.is_logarithmic = input.is_logarithmic;
|
||||
this.is_expensive = input.is_expensive;
|
||||
this.display_priority = input.display_priority;
|
||||
this.range_steps = input.range_steps;
|
||||
this.integer_property = input.integer_property;
|
||||
@@ -715,6 +716,7 @@ export class UiControl implements Deserializable<UiControl> {
|
||||
integer_property: boolean = false;
|
||||
mod_momentaryOffByDefault: boolean = false;
|
||||
mod_momentaryOnByDefault: boolean = false;
|
||||
is_expensive: boolean = false;
|
||||
pipedal_graphicEq: boolean = false;
|
||||
enumeration_property: boolean = false;
|
||||
trigger_property: boolean = false;
|
||||
|
||||
@@ -50,6 +50,7 @@ import { AlsaSequencerConfiguration, AlsaSequencerPortSelection } from './AlsaSe
|
||||
import { getDefaultModGuiPreference } from './ModGuiHost';
|
||||
import ChannelRouterSettings from './ChannelRouterSettings';
|
||||
import Tone3000DownloadProgress from './Tone3000DownloadProgress';
|
||||
import { Tone } from './t3k/types';
|
||||
|
||||
|
||||
export enum State {
|
||||
@@ -1331,6 +1332,23 @@ export class PiPedalModel //implements PiPedalModel
|
||||
}
|
||||
}
|
||||
|
||||
async writeTone3000Readme(
|
||||
filePath: string,
|
||||
tone: Tone,
|
||||
thumbnailUrl: string
|
||||
|
||||
): Promise<void> {
|
||||
if (!this.webSocket) {
|
||||
throw new Error("Server disconnected.");
|
||||
}
|
||||
await this.webSocket.request<void>(
|
||||
"writeTone3000Readme",
|
||||
{
|
||||
filePath: filePath,
|
||||
tone: tone,
|
||||
thumbnailUrl: thumbnailUrl
|
||||
});
|
||||
}
|
||||
|
||||
maxFileUploadSize: number = 512 * 1024 * 1024;
|
||||
maxPresetUploadSize: number = 1024 * 1024;
|
||||
@@ -2058,7 +2076,10 @@ export class PiPedalModel //implements PiPedalModel
|
||||
previewPedalboardValue(instanceId: number, key: string, value: number): void {
|
||||
// mouse is down. Don't update EVERYBODY, but we must change
|
||||
// the control on the running audio plugin.
|
||||
// TODO: respect "expensive" port attribute.
|
||||
|
||||
// respect "expensive" port attribute.
|
||||
|
||||
// Handle special cases for input/output volume
|
||||
if (instanceId === Pedalboard.START_CONTROL_ID && key === "volume_db") {
|
||||
this.previewInputVolume(value);
|
||||
return;
|
||||
@@ -2067,6 +2088,23 @@ export class PiPedalModel //implements PiPedalModel
|
||||
return;
|
||||
}
|
||||
|
||||
// Get the control info to check if it's expensive
|
||||
let pedalboard = this.pedalboard.get();
|
||||
if (pedalboard) {
|
||||
let item = pedalboard.tryGetItem(instanceId);
|
||||
if (item) {
|
||||
let plugin = this.getUiPlugin(item.uri);
|
||||
if (plugin) {
|
||||
let control = plugin.getControl(key);
|
||||
if (control && control.is_expensive) {
|
||||
// Don't send preview for expensive controls
|
||||
// The final value will be sent when the control is committed
|
||||
return;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
this._setServerControl("previewControl", instanceId, key, value);
|
||||
}
|
||||
|
||||
@@ -2086,6 +2124,23 @@ export class PiPedalModel //implements PiPedalModel
|
||||
return result;
|
||||
|
||||
|
||||
}
|
||||
replacePedalboarditem(instanceId: number, newItem: PedalboardItem): void {
|
||||
let pedalboard = this.pedalboard.get();
|
||||
let newPedalboard = pedalboard.clone();
|
||||
|
||||
this.updateVst3State(newPedalboard);
|
||||
|
||||
let fromItem = newPedalboard.getItem(instanceId);
|
||||
if (fromItem === null) {
|
||||
throw new PiPedalArgumentError("fromInstanceId not found.");
|
||||
}
|
||||
|
||||
let newInstanceId = newPedalboard.replaceItem(instanceId, newItem.clone());
|
||||
newPedalboard.selectedPlugin = newInstanceId;
|
||||
this.setModelPedalboard(newPedalboard);
|
||||
this.updateServerPedalboard();
|
||||
|
||||
}
|
||||
movePedalboardItemBefore(fromInstanceId: number, toInstanceId: number): void {
|
||||
if (fromInstanceId === toInstanceId) return;
|
||||
|
||||
@@ -153,6 +153,11 @@ function makeControls(plugin: UiPlugin, controlHeadeClass: string) {
|
||||
if (portGroup !== lastPortGroup) {
|
||||
if (portGroup !== null)
|
||||
{
|
||||
// Hide Tone port group on Toob NAM.
|
||||
if (portGroup.uri === 'http://two-play.com/plugins/toob-nam#eqGroup')
|
||||
{
|
||||
continue;
|
||||
}
|
||||
trs.push((
|
||||
<tr>
|
||||
<td className={controlHeadeClass} style={{ verticalAlign: "top", paddingTop: 8 }} colSpan={2}>
|
||||
|
||||
@@ -238,6 +238,10 @@ export class Tone3000DownloadHandler {
|
||||
if (!tokenResponse.ok) {
|
||||
throw new Error(tokenResponse.error);
|
||||
}
|
||||
if (tokenResponse.canceled) {
|
||||
this.onTone3000DownloadComplete("");
|
||||
return;
|
||||
}
|
||||
if (this.checkForCancel()) {
|
||||
return;
|
||||
}
|
||||
@@ -308,15 +312,14 @@ export class Tone3000DownloadHandler {
|
||||
let lastUpdateTime = Date.now();
|
||||
|
||||
|
||||
|
||||
for (const model of models) {
|
||||
for (const model of models) {
|
||||
this.progress.title = model.name;;
|
||||
if (Date.now() - lastUpdateTime > 250) {
|
||||
this.onTone3000DownloadProgress(this.progress);
|
||||
lastUpdateTime = Date.now();
|
||||
}
|
||||
|
||||
if (!await this.throttleRequests()) return;
|
||||
if (!await this.throttleRequests()) return;
|
||||
|
||||
if (!model.model_url) {
|
||||
throw new Error("Model " + model.name + " does not have a model URL.");
|
||||
@@ -377,15 +380,86 @@ export class Tone3000DownloadHandler {
|
||||
throw new Error(`Upload failed: ${uploadResult.error ?? "Unknown error."}`);
|
||||
}
|
||||
this.progress.progress++;
|
||||
}
|
||||
// the image file and readme.
|
||||
let toobThubmnailUrl: string = "";
|
||||
if (tone.images && tone.images.length > 0) {
|
||||
|
||||
await this.throttleRequests();
|
||||
|
||||
let accessToken = await this.t3kClient.getAccessToken();
|
||||
let thumbnailUrl = tone.images[0];
|
||||
let thumbnailResult = await fetch(thumbnailUrl,
|
||||
{
|
||||
headers: {
|
||||
Authorization: `Bearer ${accessToken}`,
|
||||
},
|
||||
duplex: 'half',
|
||||
} as RequestInit & { duplex: 'half' }
|
||||
|
||||
);
|
||||
if (!thumbnailResult.ok) {
|
||||
throw new Error(`Thumbnail download failed: ${thumbnailResult.status} ${thumbnailResult.statusText}`);
|
||||
}
|
||||
if (this.checkForCancel()) {
|
||||
return;
|
||||
}
|
||||
let mediaType = thumbnailResult.headers.get("Content-Type");
|
||||
if (mediaType == null) {
|
||||
throw new Error("Thumbnail download failed: Content-Type header is missing.");
|
||||
}
|
||||
let extension: string;
|
||||
switch (mediaType) {
|
||||
case "image/jpeg":
|
||||
extension = ".jpeg";
|
||||
break;
|
||||
case "image/webp":
|
||||
extension = ".webp";
|
||||
break;
|
||||
case "image/png":
|
||||
extension = ".png";
|
||||
break;
|
||||
default:
|
||||
throw new Error(`Thumbnail download failed: Unexpected media type '${mediaType}'.`);
|
||||
}
|
||||
let blob = await thumbnailResult.blob();
|
||||
const TONE3000_THUMBNAIL_PATH = "/var/pipedal/tone3000_thumbnails/";
|
||||
let thumbnailUploadPath = TONE3000_THUMBNAIL_PATH + tone.id + extension;
|
||||
|
||||
let serverUrl = this.model.varServerUrl + "t3k_uploadAsset?path="
|
||||
+ encodeURIComponent(thumbnailUploadPath);
|
||||
|
||||
const uploadResponse = await fetch(serverUrl, {
|
||||
method: 'POST',
|
||||
body: blob,
|
||||
headers: {
|
||||
'Content-Type': mediaType,
|
||||
"Content-Length": blob.size.toString(),
|
||||
"Transfer-Encoding": "chunked"
|
||||
},
|
||||
});
|
||||
|
||||
if (!uploadResponse.ok) {
|
||||
throw new Error(`Thumbnail upload failed: ${uploadResponse.statusText}`);
|
||||
}
|
||||
let uploadResult: any = await uploadResponse.json();
|
||||
|
||||
if (!uploadResult.ok === true) {
|
||||
throw new Error(`Thumbnail upload failed: ${uploadResult.error ?? "Unknown error."}`);
|
||||
}
|
||||
toobThubmnailUrl = "/var/t3k_thumbnail?id=" + tone.id;
|
||||
|
||||
}
|
||||
// yyy: get the image file.
|
||||
// yyy: write the reaadme file.
|
||||
let readmePath = toneUploadPath + "README.md";
|
||||
this.model.writeTone3000Readme(readmePath, tone, toobThubmnailUrl);
|
||||
|
||||
this.onTone3000DownloadComplete(uploadPath);
|
||||
this.onTone3000DownloadComplete(toneUploadPath);
|
||||
} catch (error) {
|
||||
this.onTone3000DownloadError(getErrorMessage(error)
|
||||
+ " " + this.progress.progress.toString() + "/" + this.progress.total.toString() + " models downloaded.");
|
||||
let message = getErrorMessage(error);
|
||||
if (this.progress.progress > 0) {
|
||||
message += " (" + this.progress.progress.toString() + "/" + this.progress.total.toString() + " models downloaded)";
|
||||
}
|
||||
this.onTone3000DownloadError(message);
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -30,13 +30,9 @@ import { PiPedalModelFactory, PiPedalModel, ListenHandle, State } from "./PiPeda
|
||||
import { PedalboardItem, ControlValue } from './Pedalboard';
|
||||
import PluginControlView, { ICustomizationHost, ControlGroup, ControlViewCustomization } from './PluginControlView';
|
||||
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_URI = "http://two-play.com/plugins/toob-nam#model_metadata";
|
||||
const TOOB_NAM__MODEL_WEIGHT_URI = "http://two-play.com/plugins/toob-nam#modelWeight";
|
||||
|
||||
|
||||
enum NamModelType {
|
||||
@@ -218,10 +214,9 @@ const ToobNamView =
|
||||
}
|
||||
|
||||
modifyControls(host: ICustomizationHost, controls: (React.ReactNode | ControlGroup)[]): (React.ReactNode | ControlGroup)[] {
|
||||
let EqPos = 7;
|
||||
const CalibrationGroupPos = 8;
|
||||
const ModelSizeControlPos = 9;
|
||||
const ModelWeightControlPos = 10;
|
||||
const ModelWeightControlPos = 3;
|
||||
const EqPos = 8;
|
||||
const CalibrationGroupPos = 9;
|
||||
|
||||
|
||||
let calibrationGroup = controls[CalibrationGroupPos] as ControlGroup;
|
||||
@@ -255,58 +250,15 @@ const ToobNamView =
|
||||
} else {
|
||||
controls[CalibrationGroupPos] = null;
|
||||
}
|
||||
controls[ModelSizeControlPos] = null;
|
||||
controls[ModelWeightControlPos] = null;
|
||||
if (!this.state.showEqSection) {
|
||||
controls.splice(EqPos, 1);
|
||||
controls[EqPos] = null;
|
||||
}
|
||||
if (this.state.modelMetadata.hasSlimmableWeights) {
|
||||
controls.splice(3, 0, this.MakeModelWeightSelector(host));
|
||||
if (!this.state.modelMetadata.hasSlimmableWeights) {
|
||||
controls[ModelWeightControlPos] = null;
|
||||
}
|
||||
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) {
|
||||
|
||||
@@ -542,7 +542,7 @@ export async function handleOAuthCallback(
|
||||
console.debug("PiPedal handleOAuthCallback Response: " + JSON.stringify(data).replace(/\n/g, ' '));
|
||||
}
|
||||
|
||||
if (toneId === undefined && modelId === undefined) {
|
||||
if (toneId === undefined && modelId === undefined && !canceled) {
|
||||
throw new Error('Missing both toneId and modelId in OAuth callback response');
|
||||
}
|
||||
return { ok: true, tokens, toneId, modelId, ...(canceled ? { canceled: true } : {}) };
|
||||
|
||||
Reference in New Issue
Block a user