audio-source-composer
Version:
Audio Source Composer
468 lines (406 loc) • 16.6 kB
JavaScript
import React from 'react';
import {
ASUIMenuAction,
ASUIMenuBreak,
ASUIInputRange,
ASUIMenuDropDown, ASUIClickableDropDown, ASUIMenuItem, ASUIGlobalContext
} from "../../../../components";
import {PresetLibrary, ProgramLoader, Values} from "../../../../song";
import PropTypes from "prop-types";
import AudioBufferLoader from "../loader/AudioBufferLoader";
import AudioBufferInstrument from "../AudioBufferInstrument";
class AudioBufferInstrumentRendererBase extends React.Component {
/** Property validation **/
static propTypes = {
parentMenu: PropTypes.func,
setProps: PropTypes.func.isRequired,
};
/** Program Context **/
static contextType = ASUIGlobalContext;
getGlobalContext() { return this.context; }
setStatus(message) { this.context.addLogEntry(message); }
setError(message) { this.context.addLogEntry(message, 'error'); }
constructor(props) {
super(props);
this.cb = {
onClick: e => this.toggleOpen(),
renderLFOMenu: lfoID => this.renderLFOMenu(lfoID),
renderMenu: {
root: () => this.renderMenuRoot(),
},
renderParamMenu: {
keyRoot: () => this.renderMenuChangeKeyRoot(),
// alias: () => this.renderMenuChangeKeyAlias(),
keyRange: () => this.renderMenuChangeKeyRange(),
source: () => this.renderMenuChangeAudioBuffer(),
},
changeParam: {
mixer: (newValue) => this.changeParam('mixer', newValue),
detune: (newValue) => this.changeParam('detune', newValue),
},
setLFOProps: (lfoID, props) => this.setLFOProps(lfoID, props),
setEnvelopeProps: (programID, props) => this.setEnvelopeProps(props),
};
this.state = {
status: null
}
}
componentDidUpdate(prevProps, prevState, snapshot) {
if(!this.props.config.envelope) {
this.props.config.envelope = AudioBufferInstrument.defaultEnvelope;
}
if(!this.props.config.url) {
// console.log("No default AudioBuffer was set");
// this.props.config.type = 'sawtooth';
}
if(typeof this.props.open === "undefined") {
console.info("Auto opening", this)
this.setProps({open: true})
}
}
getTitle() {
return this.props.config.title
|| (this.props.config.url
? getNameFromURL(this.props.config.url)
: "empty buffer")
}
// getRange() {
// return AudioBufferInstrument.getRange(this.props.config.keyRange);
// }
/** Parameters **/
getParameters() {
// const config = this.props.config;
const list = [
'source',
'mixer',
'detune',
'keyRoot',
'keyRange',
]
const inputParameters = AudioBufferInstrument.inputParameters;
return list.map(parameterName => {
const parameterInfo = inputParameters[parameterName] || {};
return {
label: parameterInfo.label || parameterName,
title: parameterInfo.title || parameterName,
children: this.renderInput(parameterName),
}
})
}
/** Properties **/
setProps(props) {
this.props.setProps(this.props.programID, props);
}
/** LFO **/
renderLFO(lfoID, lfoProgram) {
const [className, config] = lfoProgram;
const {classRenderer: Renderer} = ProgramLoader.getProgramClassInfo(className);
const lfosProps = this.props.lfosProps || [];
const lfoProps = lfosProps[lfoID] || {};
return <Renderer
key={lfoID}
parentMenu={this.cb.renderLFOMenu}
config={config}
programID={lfoID}
program={lfoProgram}
parameters={AudioBufferInstrument.sourceParameters}
setProps={this.cb.setLFOProps}
{...lfoProps}
/>;
}
setLFOProps(lfoID, props) {
const lfosProps = this.props.lfosProps || [];
if(lfosProps[lfoID]) Object.assign(lfosProps[lfoID], props);
else lfosProps[lfoID] = props;
this.setProps({lfosProps: lfosProps});
}
/** Envelope **/
renderEnvelope(envelopeProgram) {
if(envelopeProgram) {
const [className, config] = envelopeProgram;
const {classRenderer: Renderer} = ProgramLoader.getProgramClassInfo(className);
const envelopeProps = this.props.envelopeProps || [];
return <Renderer
config={config}
program={envelopeProgram}
parameters={AudioBufferInstrument.sourceParameters}
setProps={this.cb.setEnvelopeProps}
{...envelopeProps}
/>;
}
}
setEnvelopeProps(props) {
const envelopeProps = this.props.envelopeProps || {};
Object.assign(envelopeProps, props);
this.setProps({envelopeProps});
}
/** Inputs **/
renderInput(paramName) {
const config = this.props.config;
const inputParameters = AudioBufferInstrument.inputParameters[paramName] || {};
const onChange = this.cb.changeParam[paramName];
let value = config[paramName];
if(typeof value === "undefined")
value = inputParameters.default;
switch(paramName) {
case 'source':
let source = "N/A";
if(config.type)
source = config.type;
if(config.url)
source = getNameFromURL(config.url);
if(source && source.length > 16)
source = '...' + source.substr(-16);
return <ASUIClickableDropDown
{...inputParameters}
// className="small"
vertical
options={this.cb.renderParamMenu.source}
>{source}</ASUIClickableDropDown>;
case 'keyRoot': // TODO:
return <ASUIClickableDropDown
{...inputParameters}
className="small"
vertical
options={this.cb.renderParamMenu.keyRoot}
>{config.keyRoot ? config.keyRoot : "[No Root Set]"}</ASUIClickableDropDown>
case 'keyRange':
let rangeText = '[all]';
if(config.keyRangeLow || config.keyRangeHigh) {
rangeText = `${config.keyRangeLow||'[low]'} to ${config.keyRangeHigh||'[high]'}`;
if(config.keyRangeLow === config.keyRangeHigh)
rangeText = config.keyRangeLow;
}
return <ASUIClickableDropDown
{...inputParameters}
className="small"
vertical
options={this.cb.renderParamMenu.keyRange}
>{rangeText}</ASUIClickableDropDown>
case 'mixer':
case 'detune':
case 'pulseWidth':
return <ASUIInputRange
{...inputParameters}
value={value}
onChange={onChange}
/>
default:
return 'Unknown';
}
}
/** Actions **/
toggleOpen() {
const open = !this.props.open;
this.setProps({open})
}
changeParam(paramName, newValue) {
console.log(`Changing parameter ${paramName}: ${newValue} [Old: ${this.props.config[paramName]}]`);
this.props.config[paramName] = newValue;
}
removeParam(paramName) {
console.log(`Removing parameter ${paramName}: ${this.props.config[paramName]}`);
delete this.props.config[paramName];
}
changeRange(low=null, high=null) {
// TODO: compare high and low
let range = this.getRange() || [];
if(low !== null)
range[0] = low;
if(high !== null)
range[1] = high;
if(range[0] === range[1]) {
range = range[0];
} else {
range = range.join(':');
}
this.changeParam('keyRange', range);
}
async changeSampleURL(url) {
const service = new AudioBufferLoader();
try {
this.setState({status: 'loading', error: null});
this.setStatus("Loading Sample: " + url);
await service.loadAudioBufferFromURL(url);
this.props.config.url = url;
this.setState({status: 'loaded'});
this.setStatus("Loaded Sample: " + url);
} catch (e) {
this.setState({status: 'error', error: e.message});
this.setError(e);
}
}
/** LFO Actions **/
addLFO(parameterName) {
const lfos = this.props.config.lfos || [];
lfos.push(['LFO', {
parameter: parameterName
}])
this.props.config.lfos = lfos;
}
removeLFO(lfoID) {
const lfos = this.props.config.lfos;
if(!lfos[lfoID])
throw new Error("LFO not found: " + lfoID);
lfos.splice(lfoID, 1);
this.props.config.lfos = lfos;
}
// changeLoop(newLoopValue=null) {
// if(newLoopValue === null)
// newLoopValue = !this.props.config.loop;
// this.props.config.loop = newLoopValue?1:0;
// }
/** Menus **/
renderMenuRoot() {
return (<>
<ASUIMenuAction onAction={()=>{}} disabled>{`AudioBuffer: ${this.getTitle()}`}</ASUIMenuAction>
<ASUIMenuBreak />
<ASUIMenuDropDown options={() => this.renderMenuChangeAudioBuffer()}>Change AudioBuffer</ASUIMenuDropDown>
<ASUIMenuBreak />
<ASUIMenuDropDown options={() => this.renderMenuChangeMixer()}>Edit Mixer</ASUIMenuDropDown>
<ASUIMenuDropDown options={() => this.renderMenuChangeDetune()}>Edit Detune</ASUIMenuDropDown>
<ASUIMenuDropDown options={() => this.renderMenuChangeKeyRoot()}>Edit Key Root</ASUIMenuDropDown>
{/*<ASUIMenuDropDown options={() => this.renderMenuChangeKeyAlias()}>Edit Key Alias</ASUIMenuDropDown>*/}
<ASUIMenuDropDown options={() => this.renderMenuChangeKeyRange()}>Edit Key Range</ASUIMenuDropDown>
<ASUIMenuBreak />
<ASUIMenuDropDown options={() => this.renderMenuAddLFO()}>Add LFO</ASUIMenuDropDown>
{/*<ASUIMenuDropDown options={() => this.renderMenuChangeLoop()}>Toggle Loop</ASUIMenuDropDown>*/}
{this.props.parentMenu ? <>
<ASUIMenuBreak />
{this.props.parentMenu(this.props.programID)}
</> : null}
</>);
}
renderMenuChangeAudioBuffer() {
const libraries = PresetLibrary.renderMenuLibraryOptions(async (library) => {
await library.waitForAssetLoad();
return this.renderMenuChangeAudioBufferWithLibrary(library);
});
const recentSamples = PresetLibrary.renderMenuRecentSamples(
sampleURL => this.changeSampleURL(sampleURL),
AudioBufferInstrument.sampleFileRegex
)
return (
<>
<ASUIMenuItem>Select New Sample</ASUIMenuItem>
<ASUIMenuBreak />
{libraries}
{recentSamples ? <>
<ASUIMenuBreak />
<ASUIMenuItem>Recent Samples</ASUIMenuItem>
{recentSamples}
</> : null}
</>
);
}
renderMenuChangeAudioBufferWithLibrary(library) {
return library.renderMenuSamples(
(sampleURL) => this.changeSampleURL(sampleURL),
AudioBufferInstrument.sampleFileRegex);
}
renderMenuChangeDetune() {
return (<>
{this.renderInput('detune')}
<ASUIMenuBreak/>I
<ASUIMenuAction onAction={() => this.removeParam('detune')}>Clear Detune</ASUIMenuAction>
<ASUIMenuBreak/>I
<ASUIMenuAction onAction={() => true}>Done</ASUIMenuAction>
</>);
}
renderMenuChangeMixer() {
return (<>
{this.renderInput('mixer')}
<ASUIMenuBreak/>
<ASUIMenuAction onAction={() => this.removeParam('mixer')}>Clear Mixer</ASUIMenuAction>
<ASUIMenuBreak/>
<ASUIMenuAction onAction={() => true}>Done</ASUIMenuAction>
</>);
}
renderMenuChangeKeyRoot() {
let title = "Set Root Key";
if(this.props.config.keyRoot)
title = "Change Root Key: " + this.props.config.keyRoot;
return (<>
{Values.instance.renderMenuSelectFrequencyWithRecent(noteNameOctave => {
this.changeParam('keyRoot', noteNameOctave);
return false;
}, this.props.config.keyRoot, title)}
<ASUIMenuBreak/>
<ASUIMenuAction onAction={() => this.removeParam('keyRoot')}>Clear Root</ASUIMenuAction>
</>);
}
// renderMenuChangeKeyAlias() {
// return (<>
// {Values.instance.renderMenuSelectFrequencyWithRecent(noteNameOctave => {
// this.changeParam('alias', noteNameOctave)
// }, this.props.config.alias, "Change Alias")}
// <ASUIMenuBreak/>
// <ASUIMenuAction onAction={() => this.removeParam('alias')}>Clear Alias</ASUIMenuAction>
// </>);
// }
renderMenuChangeKeyRange() {
let i=0;
return [
<ASUIMenuDropDown key={i++} options={() => this.renderMenuChangeKeyRangeMode('alias')}>Set Alias</ASUIMenuDropDown>,
<ASUIMenuBreak key={i++} />,
<ASUIMenuDropDown key={i++} options={() => this.renderMenuChangeKeyRangeMode('start')}>Range Start</ASUIMenuDropDown>,
<ASUIMenuDropDown key={i++} options={() => this.renderMenuChangeKeyRangeMode('end')}>Range End</ASUIMenuDropDown>,
<ASUIMenuBreak key={i++} />,
<ASUIMenuAction key={i++} onAction={() => {
this.removeParam('keyRange');
}}>Clear Range</ASUIMenuAction>,
];
}
renderMenuChangeKeyRangeMode(mode) {
let rangeValue = null;
if(this.props.config.keyRange) switch(mode) {
case 'alias':
case 'start': rangeValue = this.getRange()[0]; break;
case 'end': rangeValue = this.getRange()[1]; break;
default: break;
}
return (<>
{Values.instance.renderMenuSelectFrequencyWithRecent(noteNameOctave => {
switch(mode) {
case 'alias': this.changeRange(noteNameOctave, noteNameOctave); break;
case 'start': this.changeRange(noteNameOctave, null); break;
case 'end': this.changeRange(null, noteNameOctave); break;
default: break;
}
}, rangeValue, "Change range " + mode)}
<ASUIMenuBreak/>
<ASUIMenuAction onAction={() => this.removeParam('rangeValue')}>Clear Range Start</ASUIMenuAction>
</>);
}
renderMenuChangekeyRangeHigh() {
let rangeEnd = null;
if(this.props.config.keyRange)
rangeEnd = this.getRange()[1];
return (<>
{Values.instance.renderMenuSelectFrequencyWithRecent(noteNameOctave => {
this.changeRange(null, noteNameOctave)
}, rangeEnd, "Change Range End")}
<ASUIMenuBreak/>
<ASUIMenuAction onAction={() => this.removeParam('rangeEnd')}>Clear Range End</ASUIMenuAction>
</>);
}
/** LFO **/
renderMenuAddLFO() {
const sourceParameters = AudioBufferInstrument.sourceParameters;
return Object.keys(sourceParameters).map((sourceParameter, i) => <ASUIMenuAction
key={i++}
onAction={() => this.addLFO(sourceParameter)}
>{`on ${sourceParameters[sourceParameter]}`}</ASUIMenuAction>)
}
renderLFOMenu(lfoID) {
let i=0;
return [
<ASUIMenuAction key={i++} onAction={e => this.removeLFO(lfoID)}>Remove LFO</ASUIMenuAction>
];
}
// renderMenuChangeLoop() {
// return (<>TODO</>);
// }
}
export default AudioBufferInstrumentRendererBase;
function getNameFromURL(url) { return url.split('/').pop().replace('.wav', '').replace('%20', ' '); }