Tap Tempo, ToobAmp v1.1.72

This commit is contained in:
Robin E. R. Davies
2026-01-10 10:41:20 -05:00
parent 6e1657933b
commit fdeb1fef5e
49 changed files with 397 additions and 94 deletions
+44 -7
View File
@@ -35,6 +35,11 @@ function semitone12TETValue(value: number): string {
}
export const MIN_TAP_TEMPO_SECONDS = 0.2;
export const MAX_TAP_TEMPO_SECONDS = 1.0;
export const MIN_TAP_TEMPO_HZ = 0.1;
export const MAX_TAP_TEMPO_HZ = 2.0;
export class Port implements Deserializable<Port> {
deserialize(input: any): Port {
@@ -117,7 +122,7 @@ export class PortGroup {
symbol: string = "";
name: string = "";
sideChainof: string = "";
parent_group: string = "";
program_list_id: number = -1;
};
@@ -654,6 +659,10 @@ export class UiControl implements Deserializable<UiControl> {
this.controlType = ControlType.Trigger;
}
}
if (this.controlType === ControlType.Dial) {
if (this.canDoTapTempo()) {
}
}
return this;
}
@@ -714,6 +723,7 @@ export class UiControl implements Deserializable<UiControl> {
scale_points: ScalePoint[] = [];
port_group: string = "";
units: Units = Units.none;
can_tap_tempo: boolean = false;
comment: string = "";
is_bypass: boolean = false;
is_program_controller: boolean = true;
@@ -779,6 +789,34 @@ export class UiControl implements Deserializable<UiControl> {
return this.controlType === ControlType.Dial;
}
canDoTapTempo(): boolean {
if (this.controlType === ControlType.Dial) {
let min_value = this.min_value;
let max_value = this.max_value;
if (min_value > max_value) {
let t = min_value;
min_value = max_value;
max_value = t;
}
switch (this.units) {
case Units.s:
return min_value <= MIN_TAP_TEMPO_SECONDS && max_value >= MAX_TAP_TEMPO_SECONDS;
case Units.ms:
return min_value <= MIN_TAP_TEMPO_SECONDS*1000 && max_value >= MAX_TAP_TEMPO_SECONDS*1000;
case Units.bpm:
return true;
case Units.hz:
return min_value <= MIN_TAP_TEMPO_HZ && max_value >= MAX_TAP_TEMPO_HZ;
default:
return false;
}
}
return false;
}
isTuner(): boolean {
return this.controlType === ControlType.Tuner;
}
@@ -894,8 +932,8 @@ export class UiControl implements Deserializable<UiControl> {
break;
case Units.unknown:
if (this.custom_units !== "") {
text = this.custom_units.replace("%f",text);
}
text = this.custom_units.replace("%f", text);
}
break;
default:
break;
@@ -950,7 +988,7 @@ export class Lv2PatchPropertyInfo {
return result;
}
uri: string = "";
writable : boolean = false;
writable: boolean = false;
readable: boolean = false;
label: string = "";
index: number = -1;
@@ -1045,8 +1083,7 @@ export class UiPlugin implements Deserializable<UiPlugin> {
}
getFilePropertyByUri(patchPropertyUri: string): UiFileProperty | null {
for (let i = 0; i < this.fileProperties.length; ++i)
{
for (let i = 0; i < this.fileProperties.length; ++i) {
let fileProperty = this.fileProperties[i];
if (fileProperty.patchProperty === patchPropertyUri) {
return fileProperty;
@@ -1078,7 +1115,7 @@ export class UiPlugin implements Deserializable<UiPlugin> {
frequencyPlots: UiFrequencyPlot[] = [];
is_vst3: boolean = false;
modGui: ModGui | null = null; // null if no mod gui.
patchProperties: Lv2PatchPropertyInfo[] = [];
patchProperties: Lv2PatchPropertyInfo[] = [];
}
+1
View File
@@ -67,6 +67,7 @@ export default class MidiBinding {
static BINDING_TYPE_NONE: number = 0;
static BINDING_TYPE_NOTE: number = 1;
static BINDING_TYPE_CONTROL: number = 2;
static BINDING_TYPE_TAP_TEMPO: number = 3;
setBindingType(bindingType:number)
{
+37 -15
View File
@@ -57,6 +57,7 @@ export enum MidiControlType {
MomentarySwitch
}
export function getMidiControlType(uiPlugin: UiPlugin | undefined, symbol: string): MidiControlType {
if (symbol === "__bypass") {
return MidiControlType.Toggle;
@@ -87,16 +88,14 @@ export function getMidiControlType(uiPlugin: UiPlugin | undefined, symbol: strin
return MidiControlType.Dial;
}
export function canBindToNote(controlType: MidiControlType): boolean {
return controlType === MidiControlType.Toggle
|| controlType === MidiControlType.Trigger
|| controlType === MidiControlType.MomentarySwitch
}
interface MidiBindingViewProps extends WithStyles<typeof styles> {
instanceId: number;
midiBinding: MidiBinding;
midiControlType: MidiControlType;
canDoTapTempo: boolean;
onChange: (instanceId: number, newBinding: MidiBinding) => void;
}
@@ -131,6 +130,21 @@ const MidiBindingView =
};
}
canDoTapTempo(): boolean {
let controlType = this.props.midiControlType;
if (controlType == MidiControlType.Dial && this.props.canDoTapTempo) {
return true;
}
return false;
}
canBindToNote(): boolean {
let controlType = this.props.midiControlType;
return controlType === MidiControlType.Toggle
|| controlType === MidiControlType.Trigger
|| controlType === MidiControlType.MomentarySwitch
}
handleBindingTypeChange(e: any, extra: any) {
let newValue = parseInt(e.target.value);
let newBinding = this.props.midiBinding.clone();
@@ -203,7 +217,7 @@ const MidiBindingView =
generateMidiNoteSelects(): React.ReactNode[] {
let result: React.ReactNode[] = [];
for (let i = 0; i < 127; ++i) {
for (let i = 0; i <= 127; ++i) {
result.push(
<MenuItem key={"k" + i} value={i}>{i} {Utility.midiNoteName(i)}</MenuItem>
)
@@ -314,10 +328,13 @@ const MidiBindingView =
let binding = this.props.midiBinding;
let newBinding = binding.clone();
if (midiMessage.isNote()) {
if (!canBindToNote(this.props.midiControlType)) {
if (!this.canBindToNote() && !this.props.canDoTapTempo) {
return;
}
newBinding.bindingType = MidiBinding.BINDING_TYPE_NOTE
newBinding.bindingType =
this.props.canDoTapTempo ?
MidiBinding.BINDING_TYPE_TAP_TEMPO
: MidiBinding.BINDING_TYPE_NOTE
newBinding.note = midiMessage.cc1;
} else if (midiMessage.isControl()) {
newBinding.bindingType = MidiBinding.BINDING_TYPE_CONTROL
@@ -413,23 +430,28 @@ const MidiBindingView =
<div style={{ display: "flex", flexDirection: "row", flexWrap: "wrap", justifyContent: "left", alignItems: "center", minHeight: 48 }}>
<div className={classes.controlDiv} >
<Select variant="standard"
style={{ width: 80, }}
style={{ width: undefined, }}
onChange={(e, extra) => this.handleBindingTypeChange(e, extra)}
value={midiBinding.bindingType}
>
<MenuItem value={0}>None</MenuItem>
{(canBindToNote(this.props.midiControlType)) && (
<MenuItem value={1}>Note</MenuItem>
{(this.canBindToNote()) && (
<MenuItem key="1" value={1}>Note</MenuItem>
)}
<MenuItem key="2" value={2}>Control</MenuItem>
{(this.canDoTapTempo()) && (
<MenuItem key="3" value={3}>Tap Tempo</MenuItem>
)}
<MenuItem value={2}>Control</MenuItem>
</Select>
</div>
{
(midiBinding.bindingType === MidiBinding.BINDING_TYPE_NOTE) &&
(midiBinding.bindingType === MidiBinding.BINDING_TYPE_NOTE
|| midiBinding.bindingType === MidiBinding.BINDING_TYPE_TAP_TEMPO
) &&
(
<div className={classes.controlDiv2} >
<Select variant="standard"
style={{ width: 80 }}
style={{}}
onChange={(e, extra) => this.handleNoteChange(e, extra)}
value={midiBinding.note}
@@ -569,7 +591,7 @@ const MidiBindingView =
(
<div className={classes.controlDiv} >
<Select variant="standard"
style={{ width: 120 }}
style={{}}
onChange={(e, extra) => this.handleLinearControlTypeChange(e, extra)}
value={midiBinding.linearControlType}
>
+15 -1
View File
@@ -69,6 +69,17 @@ const styles = (theme: Theme) => createStyles({
}),
});
function canDoTapTempo(uiPlugin: UiPlugin | undefined, symbol: string): boolean {
if (!uiPlugin) return false;
let port = uiPlugin.getControl(symbol);
if (!port) return false;
if (port.canDoTapTempo()) {
return true;
}
return false;
}
function not_null<T>(value: T | null) {
if (!value) throw Error("Unexpected null value");
return value;
@@ -200,7 +211,9 @@ export const MidiBindingDialog =
</td>
<td className={classes.bindingTd}>
<MidiBindingView instanceId={item.instanceId} midiBinding={item.getMidiBinding("__bypass")}
midiControlType={ getMidiControlType(plugin, "__bypass") }
midiControlType={ getMidiControlType(plugin, "__bypass")
}
canDoTapTempo={false }
onChange={(instanceId: number, newItem: MidiBinding) => this.handleItemChanged(instanceId, newItem)}
/>
</td>
@@ -244,6 +257,7 @@ export const MidiBindingDialog =
<MidiBindingView instanceId={item.instanceId}
midiControlType={getMidiControlType(plugin, symbol)}
midiBinding={item.getMidiBinding(symbol)}
canDoTapTempo={canDoTapTempo(plugin, symbol)}
onChange={(instanceId: number, newItem: MidiBinding) => this.handleItemChanged(instanceId, newItem)}
/>
</td>
+11 -2
View File
@@ -37,6 +37,15 @@ function isMobileDevice() {
return (navigator.maxTouchPoints && navigator.maxTouchPoints > 1);
}
function defaultStep(min: number, max: number): number {
let range = Math.abs(max - min);
if (range <= 1) return 0.01;
if (range <= 10) return 0.1;
if (range <= 100) return 1;
if (range <= 1000) return 10;
return 100;
}
interface NumericInputProps extends WithStyles<typeof styles> {
ariaLabel: string;
value: number;
@@ -155,7 +164,7 @@ export const NumericInput =
render() {
//const classes = withStyles.getClasses(this.props);
return (<Input style={{ width: 50 }}
return (<Input style={{ width: 65 }}
inputProps={{
'aria-label': this.props.ariaLabel,
style: { textAlign: 'right' },
@@ -166,7 +175,7 @@ export const NumericInput =
autoCapitalize: "off",
min: this.props.min,
max: this.props.max,
step: this.props.step
step: this.props.step ? this.props.step: defaultStep(this.props.min, this.props.max)
}}
onFocus={(e) => {this.handleFocus(e);}}
onBlur= {(e) => { this.handleBlur(e); }}
+4 -1
View File
@@ -42,6 +42,7 @@ import PlayArrowIcon from '@mui/icons-material/PlayArrow';
import StopIcon from '@mui/icons-material/Stop';
import FiberManualRecordIcon from '@mui/icons-material/FiberManualRecord';
import GraphicEqCtl, { UpdateGraphicEqPath } from './GraphicEqCtl';
import Units from './Units';
const MIN_ANGLE = -135;
const MAX_ANGLE = 135;
@@ -499,6 +500,8 @@ const PluginControl =
}
private tapStartMs: number = 0;
onPointerUp(e: PointerEvent) {
if (this.isCapturedPointer(e)) {
@@ -519,7 +522,7 @@ const PluginControl =
let ms = e.timeStamp - this.tapStartMs;
if (ms < 200) {
this.onPointerTap(e);
}
}
}
// prevent click from firing on other elements
// when the pointer goes up0.