mirror of
https://github.com/scratchfoundation/scratch-vm.git
synced 2024-12-24 06:52:40 -05:00
synth extension
This commit is contained in:
parent
565f11d4fc
commit
dbd513d85b
4 changed files with 25218 additions and 1 deletions
378
src/extension-support/extension-manager-old.js
Normal file
378
src/extension-support/extension-manager-old.js
Normal file
|
@ -0,0 +1,378 @@
|
||||||
|
const dispatch = require('../dispatch/central-dispatch');
|
||||||
|
const log = require('../util/log');
|
||||||
|
const maybeFormatMessage = require('../util/maybe-format-message');
|
||||||
|
|
||||||
|
const BlockType = require('./block-type');
|
||||||
|
|
||||||
|
// These extensions are currently built into the VM repository but should not be loaded at startup.
|
||||||
|
// TODO: move these out into a separate repository?
|
||||||
|
// TODO: change extension spec so that library info, including extension ID, can be collected through static methods
|
||||||
|
const Scratch3PenBlocks = require('../extensions/scratch3_pen');
|
||||||
|
const Scratch3WeDo2Blocks = require('../extensions/scratch3_wedo2');
|
||||||
|
const Scratch3MusicBlocks = require('../extensions/scratch3_music');
|
||||||
|
const Scratch3MicroBitBlocks = require('../extensions/scratch3_microbit');
|
||||||
|
const Scratch3Text2SpeechBlocks = require('../extensions/scratch3_text2speech');
|
||||||
|
const Scratch3TranslateBlocks = require('../extensions/scratch3_translate');
|
||||||
|
const Scratch3VideoSensingBlocks = require('../extensions/scratch3_video_sensing');
|
||||||
|
const Scratch3Speech2TextBlocks = require('../extensions/scratch3_speech2text');
|
||||||
|
const Scratch3Ev3Blocks = require('../extensions/scratch3_ev3');
|
||||||
|
const Scratch3SynthBlocks = require('../extensions/scratch3_synth');
|
||||||
|
|
||||||
|
|
||||||
|
const builtinExtensions = {
|
||||||
|
pen: Scratch3PenBlocks,
|
||||||
|
wedo2: Scratch3WeDo2Blocks,
|
||||||
|
music: Scratch3MusicBlocks,
|
||||||
|
microbit: Scratch3MicroBitBlocks,
|
||||||
|
text2speech: Scratch3Text2SpeechBlocks,
|
||||||
|
translate: Scratch3TranslateBlocks,
|
||||||
|
videoSensing: Scratch3VideoSensingBlocks,
|
||||||
|
speech2text: Scratch3Speech2TextBlocks,
|
||||||
|
ev3: Scratch3Ev3Blocks,
|
||||||
|
synth: Scratch3SynthBlocks
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @typedef {object} ArgumentInfo - Information about an extension block argument
|
||||||
|
* @property {ArgumentType} type - the type of value this argument can take
|
||||||
|
* @property {*|undefined} default - the default value of this argument (default: blank)
|
||||||
|
*/
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @typedef {object} ConvertedBlockInfo - Raw extension block data paired with processed data ready for scratch-blocks
|
||||||
|
* @property {ExtensionBlockMetadata} info - the raw block info
|
||||||
|
* @property {object} json - the scratch-blocks JSON definition for this block
|
||||||
|
* @property {string} xml - the scratch-blocks XML definition for this block
|
||||||
|
*/
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @typedef {object} CategoryInfo - Information about a block category
|
||||||
|
* @property {string} id - the unique ID of this category
|
||||||
|
* @property {string} name - the human-readable name of this category
|
||||||
|
* @property {string|undefined} blockIconURI - optional URI for the block icon image
|
||||||
|
* @property {string} color1 - the primary color for this category, in '#rrggbb' format
|
||||||
|
* @property {string} color2 - the secondary color for this category, in '#rrggbb' format
|
||||||
|
* @property {string} color3 - the tertiary color for this category, in '#rrggbb' format
|
||||||
|
* @property {Array.<ConvertedBlockInfo>} blocks - the blocks, separators, etc. in this category
|
||||||
|
* @property {Array.<object>} menus - the menus provided by this category
|
||||||
|
*/
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @typedef {object} PendingExtensionWorker - Information about an extension worker still initializing
|
||||||
|
* @property {string} extensionURL - the URL of the extension to be loaded by this worker
|
||||||
|
* @property {Function} resolve - function to call on successful worker startup
|
||||||
|
* @property {Function} reject - function to call on failed worker startup
|
||||||
|
*/
|
||||||
|
|
||||||
|
class ExtensionManager {
|
||||||
|
constructor (runtime) {
|
||||||
|
/**
|
||||||
|
* The ID number to provide to the next extension worker.
|
||||||
|
* @type {int}
|
||||||
|
*/
|
||||||
|
this.nextExtensionWorker = 0;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* FIFO queue of extensions which have been requested but not yet loaded in a worker,
|
||||||
|
* along with promise resolution functions to call once the worker is ready or failed.
|
||||||
|
*
|
||||||
|
* @type {Array.<PendingExtensionWorker>}
|
||||||
|
*/
|
||||||
|
this.pendingExtensions = [];
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Map of worker ID to workers which have been allocated but have not yet finished initialization.
|
||||||
|
* @type {Array.<PendingExtensionWorker>}
|
||||||
|
*/
|
||||||
|
this.pendingWorkers = [];
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Set of loaded extension URLs/IDs (equivalent for built-in extensions).
|
||||||
|
* @type {Set.<string>}
|
||||||
|
* @private
|
||||||
|
*/
|
||||||
|
this._loadedExtensions = new Map();
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Keep a reference to the runtime so we can construct internal extension objects.
|
||||||
|
* TODO: remove this in favor of extensions accessing the runtime as a service.
|
||||||
|
* @type {Runtime}
|
||||||
|
*/
|
||||||
|
this.runtime = runtime;
|
||||||
|
|
||||||
|
dispatch.setService('extensions', this).catch(e => {
|
||||||
|
log.error(`ExtensionManager was unable to register extension service: ${JSON.stringify(e)}`);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Check whether an extension is registered or is in the process of loading. This is intended to control loading or
|
||||||
|
* adding extensions so it may return `true` before the extension is ready to be used. Use the promise returned by
|
||||||
|
* `loadExtensionURL` if you need to wait until the extension is truly ready.
|
||||||
|
* @param {string} extensionID - the ID of the extension.
|
||||||
|
* @returns {boolean} - true if loaded, false otherwise.
|
||||||
|
*/
|
||||||
|
isExtensionLoaded (extensionID) {
|
||||||
|
return this._loadedExtensions.has(extensionID);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Load an extension by URL or internal extension ID
|
||||||
|
* @param {string} extensionURL - the URL for the extension to load OR the ID of an internal extension
|
||||||
|
* @returns {Promise} resolved once the extension is loaded and initialized or rejected on failure
|
||||||
|
*/
|
||||||
|
loadExtensionURL (extensionURL) {
|
||||||
|
if (builtinExtensions.hasOwnProperty(extensionURL)) {
|
||||||
|
/** @TODO dupe handling for non-builtin extensions. See commit 670e51d33580e8a2e852b3b038bb3afc282f81b9 */
|
||||||
|
if (this.isExtensionLoaded(extensionURL)) {
|
||||||
|
const message = `Rejecting attempt to load a second extension with ID ${extensionURL}`;
|
||||||
|
log.warn(message);
|
||||||
|
return Promise.reject(new Error(message));
|
||||||
|
}
|
||||||
|
|
||||||
|
const extension = builtinExtensions[extensionURL];
|
||||||
|
const extensionInstance = new extension(this.runtime);
|
||||||
|
return this._registerInternalExtension(extensionInstance).then(serviceName => {
|
||||||
|
this._loadedExtensions.set(extensionURL, serviceName);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
return new Promise((resolve, reject) => {
|
||||||
|
// If we `require` this at the global level it breaks non-webpack targets, including tests
|
||||||
|
const ExtensionWorker = require('worker-loader?name=extension-worker.js!./extension-worker');
|
||||||
|
|
||||||
|
this.pendingExtensions.push({extensionURL, resolve, reject});
|
||||||
|
dispatch.addWorker(new ExtensionWorker());
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Regenerate blockinfo for any loaded extensions
|
||||||
|
* @returns {Promise} resolved once all the extensions have been reinitialized
|
||||||
|
*/
|
||||||
|
refreshBlocks () {
|
||||||
|
const allPromises = Array.from(this._loadedExtensions.values()).map(serviceName =>
|
||||||
|
dispatch.call(serviceName, 'getInfo')
|
||||||
|
.then(info => {
|
||||||
|
info = this._prepareExtensionInfo(serviceName, info);
|
||||||
|
dispatch.call('runtime', '_refreshExtensionPrimitives', info);
|
||||||
|
})
|
||||||
|
.catch(e => {
|
||||||
|
log.error(`Failed to refresh buildtin extension primitives: ${JSON.stringify(e)}`);
|
||||||
|
})
|
||||||
|
);
|
||||||
|
return Promise.all(allPromises);
|
||||||
|
}
|
||||||
|
|
||||||
|
allocateWorker () {
|
||||||
|
const id = this.nextExtensionWorker++;
|
||||||
|
const workerInfo = this.pendingExtensions.shift();
|
||||||
|
this.pendingWorkers[id] = workerInfo;
|
||||||
|
return [id, workerInfo.extensionURL];
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Collect extension metadata from the specified service and begin the extension registration process.
|
||||||
|
* @param {string} serviceName - the name of the service hosting the extension.
|
||||||
|
*/
|
||||||
|
registerExtensionService (serviceName) {
|
||||||
|
dispatch.call(serviceName, 'getInfo').then(info => {
|
||||||
|
this._registerExtensionInfo(serviceName, info);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Called by an extension worker to indicate that the worker has finished initialization.
|
||||||
|
* @param {int} id - the worker ID.
|
||||||
|
* @param {*?} e - the error encountered during initialization, if any.
|
||||||
|
*/
|
||||||
|
onWorkerInit (id, e) {
|
||||||
|
const workerInfo = this.pendingWorkers[id];
|
||||||
|
delete this.pendingWorkers[id];
|
||||||
|
if (e) {
|
||||||
|
workerInfo.reject(e);
|
||||||
|
} else {
|
||||||
|
workerInfo.resolve(id);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Register an internal (non-Worker) extension object
|
||||||
|
* @param {object} extensionObject - the extension object to register
|
||||||
|
* @returns {Promise} resolved once the extension is fully registered or rejected on failure
|
||||||
|
*/
|
||||||
|
_registerInternalExtension (extensionObject) {
|
||||||
|
const extensionInfo = extensionObject.getInfo();
|
||||||
|
const fakeWorkerId = this.nextExtensionWorker++;
|
||||||
|
const serviceName = `extension_${fakeWorkerId}_${extensionInfo.id}`;
|
||||||
|
return dispatch.setService(serviceName, extensionObject)
|
||||||
|
.then(() => {
|
||||||
|
dispatch.call('extensions', 'registerExtensionService', serviceName);
|
||||||
|
return serviceName;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Sanitize extension info then register its primitives with the VM.
|
||||||
|
* @param {string} serviceName - the name of the service hosting the extension
|
||||||
|
* @param {ExtensionInfo} extensionInfo - the extension's metadata
|
||||||
|
* @private
|
||||||
|
*/
|
||||||
|
_registerExtensionInfo (serviceName, extensionInfo) {
|
||||||
|
extensionInfo = this._prepareExtensionInfo(serviceName, extensionInfo);
|
||||||
|
dispatch.call('runtime', '_registerExtensionPrimitives', extensionInfo).catch(e => {
|
||||||
|
log.error(`Failed to register primitives for extension on service ${serviceName}:`, e);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Modify the provided text as necessary to ensure that it may be used as an attribute value in valid XML.
|
||||||
|
* @param {string} text - the text to be sanitized
|
||||||
|
* @returns {string} - the sanitized text
|
||||||
|
* @private
|
||||||
|
*/
|
||||||
|
_sanitizeID (text) {
|
||||||
|
return text.toString().replace(/[<"&]/, '_');
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Apply minor cleanup and defaults for optional extension fields.
|
||||||
|
* TODO: make the ID unique in cases where two copies of the same extension are loaded.
|
||||||
|
* @param {string} serviceName - the name of the service hosting this extension block
|
||||||
|
* @param {ExtensionInfo} extensionInfo - the extension info to be sanitized
|
||||||
|
* @returns {ExtensionInfo} - a new extension info object with cleaned-up values
|
||||||
|
* @private
|
||||||
|
*/
|
||||||
|
_prepareExtensionInfo (serviceName, extensionInfo) {
|
||||||
|
extensionInfo = Object.assign({}, extensionInfo);
|
||||||
|
if (!/^[a-z0-9]+$/i.test(extensionInfo.id)) {
|
||||||
|
throw new Error('Invalid extension id');
|
||||||
|
}
|
||||||
|
extensionInfo.name = extensionInfo.name || extensionInfo.id;
|
||||||
|
extensionInfo.blocks = extensionInfo.blocks || [];
|
||||||
|
extensionInfo.targetTypes = extensionInfo.targetTypes || [];
|
||||||
|
extensionInfo.blocks = extensionInfo.blocks.reduce((results, blockInfo) => {
|
||||||
|
try {
|
||||||
|
let result;
|
||||||
|
switch (blockInfo) {
|
||||||
|
case '---': // separator
|
||||||
|
result = '---';
|
||||||
|
break;
|
||||||
|
default: // an ExtensionBlockMetadata object
|
||||||
|
result = this._prepareBlockInfo(serviceName, blockInfo);
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
results.push(result);
|
||||||
|
} catch (e) {
|
||||||
|
// TODO: more meaningful error reporting
|
||||||
|
log.error(`Error processing block: ${e.message}, Block:\n${JSON.stringify(blockInfo)}`);
|
||||||
|
}
|
||||||
|
return results;
|
||||||
|
}, []);
|
||||||
|
extensionInfo.menus = extensionInfo.menus || [];
|
||||||
|
extensionInfo.menus = this._prepareMenuInfo(serviceName, extensionInfo.menus);
|
||||||
|
return extensionInfo;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Prepare extension menus. e.g. setup binding for dynamic menu functions.
|
||||||
|
* @param {string} serviceName - the name of the service hosting this extension block
|
||||||
|
* @param {Array.<MenuInfo>} menus - the menu defined by the extension.
|
||||||
|
* @returns {Array.<MenuInfo>} - a menuInfo object with all preprocessing done.
|
||||||
|
* @private
|
||||||
|
*/
|
||||||
|
_prepareMenuInfo (serviceName, menus) {
|
||||||
|
const menuNames = Object.getOwnPropertyNames(menus);
|
||||||
|
for (let i = 0; i < menuNames.length; i++) {
|
||||||
|
const item = menuNames[i];
|
||||||
|
// If the value is a string, it should be the name of a function in the
|
||||||
|
// extension object to call to populate the menu whenever it is opened.
|
||||||
|
// Set up the binding for the function object here so
|
||||||
|
// we can use it later when converting the menu for Scratch Blocks.
|
||||||
|
if (typeof menus[item] === 'string') {
|
||||||
|
const serviceObject = dispatch.services[serviceName];
|
||||||
|
const menuName = menus[item];
|
||||||
|
menus[item] = this._getExtensionMenuItems.bind(this, serviceObject, menuName);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return menus;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Fetch the items for a particular extension menu, providing the target ID for context.
|
||||||
|
* @param {object} extensionObject - the extension object providing the menu.
|
||||||
|
* @param {string} menuName - the name of the menu function to call.
|
||||||
|
* @returns {Array} menu items ready for scratch-blocks.
|
||||||
|
* @private
|
||||||
|
*/
|
||||||
|
_getExtensionMenuItems (extensionObject, menuName) {
|
||||||
|
// Fetch the items appropriate for the target currently being edited. This assumes that menus only
|
||||||
|
// collect items when opened by the user while editing a particular target.
|
||||||
|
const editingTarget = this.runtime.getEditingTarget() || this.runtime.getTargetForStage();
|
||||||
|
const editingTargetID = editingTarget ? editingTarget.id : null;
|
||||||
|
const extensionMessageContext = this.runtime.makeMessageContextForTarget(editingTarget);
|
||||||
|
|
||||||
|
// TODO: Fix this to use dispatch.call when extensions are running in workers.
|
||||||
|
const menuFunc = extensionObject[menuName];
|
||||||
|
const menuItems = menuFunc.call(extensionObject, editingTargetID).map(
|
||||||
|
item => {
|
||||||
|
item = maybeFormatMessage(item, extensionMessageContext);
|
||||||
|
if (typeof item === 'object') {
|
||||||
|
return [
|
||||||
|
maybeFormatMessage(item.text, extensionMessageContext),
|
||||||
|
item.value
|
||||||
|
];
|
||||||
|
}
|
||||||
|
return item;
|
||||||
|
});
|
||||||
|
|
||||||
|
if (!menuItems || menuItems.length < 1) {
|
||||||
|
throw new Error(`Extension menu returned no items: ${menuName}`);
|
||||||
|
}
|
||||||
|
return menuItems;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Apply defaults for optional block fields.
|
||||||
|
* @param {string} serviceName - the name of the service hosting this extension block
|
||||||
|
* @param {ExtensionBlockMetadata} blockInfo - the block info from the extension
|
||||||
|
* @returns {ExtensionBlockMetadata} - a new block info object which has values for all relevant optional fields.
|
||||||
|
* @private
|
||||||
|
*/
|
||||||
|
_prepareBlockInfo (serviceName, blockInfo) {
|
||||||
|
blockInfo = Object.assign({}, {
|
||||||
|
blockType: BlockType.COMMAND,
|
||||||
|
terminal: false,
|
||||||
|
blockAllThreads: false,
|
||||||
|
arguments: {}
|
||||||
|
}, blockInfo);
|
||||||
|
blockInfo.opcode = this._sanitizeID(blockInfo.opcode);
|
||||||
|
blockInfo.text = blockInfo.text || blockInfo.opcode;
|
||||||
|
|
||||||
|
if (blockInfo.blockType !== BlockType.EVENT) {
|
||||||
|
blockInfo.func = blockInfo.func ? this._sanitizeID(blockInfo.func) : blockInfo.opcode;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* This is only here because the VM performs poorly when blocks return promises.
|
||||||
|
* @TODO make it possible for the VM to resolve a promise and continue during the same Scratch "tick"
|
||||||
|
*/
|
||||||
|
if (dispatch._isRemoteService(serviceName)) {
|
||||||
|
blockInfo.func = dispatch.call.bind(dispatch, serviceName, blockInfo.func);
|
||||||
|
} else {
|
||||||
|
const serviceObject = dispatch.services[serviceName];
|
||||||
|
const func = serviceObject[blockInfo.func];
|
||||||
|
if (func) {
|
||||||
|
blockInfo.func = func.bind(serviceObject);
|
||||||
|
} else if (blockInfo.blockType !== BlockType.EVENT) {
|
||||||
|
throw new Error(`Could not find extension block function called ${blockInfo.func}`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else if (blockInfo.func) {
|
||||||
|
log.warn(`Ignoring function "${blockInfo.func}" for event block ${blockInfo.opcode}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
return blockInfo;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
module.exports = ExtensionManager;
|
|
@ -23,7 +23,8 @@ const builtinExtensions = {
|
||||||
ev3: () => require('../extensions/scratch3_ev3'),
|
ev3: () => require('../extensions/scratch3_ev3'),
|
||||||
makeymakey: () => require('../extensions/scratch3_makeymakey'),
|
makeymakey: () => require('../extensions/scratch3_makeymakey'),
|
||||||
boost: () => require('../extensions/scratch3_boost'),
|
boost: () => require('../extensions/scratch3_boost'),
|
||||||
gdxfor: () => require('../extensions/scratch3_gdx_for')
|
gdxfor: () => require('../extensions/scratch3_gdx_for'),
|
||||||
|
synth: () => require('../extensions/scratch3_synth'),
|
||||||
};
|
};
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|
456
src/extensions/scratch3_synth/index.js
Normal file
456
src/extensions/scratch3_synth/index.js
Normal file
|
@ -0,0 +1,456 @@
|
||||||
|
const ArgumentType = require('../../extension-support/argument-type');
|
||||||
|
const BlockType = require('../../extension-support/block-type');
|
||||||
|
const log = require('../../util/log');
|
||||||
|
const formatMessage = require('format-message');
|
||||||
|
const Cast = require('../../util/cast');
|
||||||
|
const Timer = require('../../util/timer');
|
||||||
|
|
||||||
|
const Tone = require('./tone');
|
||||||
|
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Class for the translate block in Scratch 3.0.
|
||||||
|
* @constructor
|
||||||
|
*/
|
||||||
|
class Scratch3SynthBlocks {
|
||||||
|
constructor (runtime) {
|
||||||
|
/**
|
||||||
|
* The runtime instantiating this block package.
|
||||||
|
* @type {Runtime}
|
||||||
|
*/
|
||||||
|
this.runtime = runtime;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The synth object
|
||||||
|
* @type {Synth}
|
||||||
|
*/
|
||||||
|
this.synth = this._initSynth();
|
||||||
|
this.targetFreq = this.synth.frequency.value;
|
||||||
|
|
||||||
|
// effects chain
|
||||||
|
this.autoWah = new Tone.AutoWah();
|
||||||
|
this.autoWah.wet.value = 0;
|
||||||
|
this.autoWah.sensitivity = -40;
|
||||||
|
|
||||||
|
this.delay = new Tone.FeedbackDelay(0.25, 0.5);
|
||||||
|
this.delay.wet.value = 0;
|
||||||
|
|
||||||
|
this.panner = new Tone.Panner(0);
|
||||||
|
|
||||||
|
this.synth.connect(this.delay);
|
||||||
|
this.delay.connect(this.panner);
|
||||||
|
this.panner.toMaster();
|
||||||
|
|
||||||
|
//this.synth.connect(this.autoWah); // excluding autowah that is buggie
|
||||||
|
//this.autoWah.connect(this.delay);
|
||||||
|
//this.delay.connect(this.panner);
|
||||||
|
//this.panner.toMaster();
|
||||||
|
|
||||||
|
// notes
|
||||||
|
this.scaleRoot = 48; // root is note C2
|
||||||
|
this.minNote = 24;
|
||||||
|
this.maxNote = 100;
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Initialize the synth and effect chain
|
||||||
|
* @private
|
||||||
|
*/
|
||||||
|
_initSynth (){
|
||||||
|
|
||||||
|
var synthOptions = {
|
||||||
|
oscillator: {
|
||||||
|
type: "triangle"
|
||||||
|
},
|
||||||
|
envelope: {
|
||||||
|
attack: 0.03,
|
||||||
|
decay: 0,
|
||||||
|
sustain: 1,
|
||||||
|
release: 0.03
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
var synth = new Tone.Synth(synthOptions);
|
||||||
|
synth.setNote(Tone.Frequency(60, "midi"));
|
||||||
|
synth.portamento = 0;
|
||||||
|
|
||||||
|
return synth;
|
||||||
|
}
|
||||||
|
|
||||||
|
_clamp(input, min, max) {
|
||||||
|
return Math.min(Math.max(input, min), max);
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Start the stack timer and the yield the thread if necessary.
|
||||||
|
* @param {object} util - utility object provided by the runtime.
|
||||||
|
* @param {number} duration - a duration in seconds to set the timer for.
|
||||||
|
* @private
|
||||||
|
*/
|
||||||
|
_startStackTimer (util, duration) {
|
||||||
|
util.stackFrame.timer = new Timer();
|
||||||
|
util.stackFrame.timer.start();
|
||||||
|
util.stackFrame.duration = duration;
|
||||||
|
util.yield();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Check the stack timer, and if its time is not up yet, yield the thread.
|
||||||
|
* @param {object} util - utility object provided by the runtime.
|
||||||
|
* @private
|
||||||
|
*/
|
||||||
|
_checkStackTimer (util) {
|
||||||
|
const timeElapsed = util.stackFrame.timer.timeElapsed();
|
||||||
|
if (timeElapsed < util.stackFrame.duration * 1000) {
|
||||||
|
util.yield();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Check if the stack timer needs initialization.
|
||||||
|
* @param {object} util - utility object provided by the runtime.
|
||||||
|
* @return {boolean} - true if the stack timer needs to be initialized.
|
||||||
|
* @private
|
||||||
|
*/
|
||||||
|
_stackTimerNeedsInit (util) {
|
||||||
|
return !util.stackFrame.timer;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @returns {object} metadata for this extension and its blocks.
|
||||||
|
*/
|
||||||
|
getInfo () {
|
||||||
|
return {
|
||||||
|
id: 'synth',
|
||||||
|
name: 'Synth',
|
||||||
|
menuIconURI: '', // TODO: Add the final icons.
|
||||||
|
blockIconURI: '',
|
||||||
|
blocks: [
|
||||||
|
{
|
||||||
|
opcode: 'synthOnFor',
|
||||||
|
blockType: BlockType.COMMAND,
|
||||||
|
text: formatMessage({
|
||||||
|
id: 'synth.synthOnFor',
|
||||||
|
default: 'turn synth on for [SECS] seconds',
|
||||||
|
description: 'turn synth on for a number of seconds'
|
||||||
|
}),
|
||||||
|
arguments: {
|
||||||
|
SECS: {
|
||||||
|
type: ArgumentType.NUMBER,
|
||||||
|
defaultValue: 1
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
opcode: 'synthOnForAndWait',
|
||||||
|
blockType: BlockType.COMMAND,
|
||||||
|
text: formatMessage({
|
||||||
|
id: 'synth.synthOnForAndWait',
|
||||||
|
default: 'turn synth on for [SECS] seconds and wait',
|
||||||
|
description: 'turn synth on for a number of seconds and wait'
|
||||||
|
}),
|
||||||
|
arguments: {
|
||||||
|
SECS: {
|
||||||
|
type: ArgumentType.NUMBER,
|
||||||
|
defaultValue: 1
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
opcode: 'synthOn',
|
||||||
|
blockType: BlockType.COMMAND,
|
||||||
|
text: formatMessage({
|
||||||
|
id: 'synth.synthOn',
|
||||||
|
default: 'turn synth on',
|
||||||
|
description: 'turn syntn on'
|
||||||
|
})
|
||||||
|
},
|
||||||
|
{
|
||||||
|
opcode: 'synthOff',
|
||||||
|
blockType: BlockType.COMMAND,
|
||||||
|
text: formatMessage({
|
||||||
|
id: 'synth.synthOff',
|
||||||
|
default: 'turn synth off',
|
||||||
|
description: 'turn synth off'
|
||||||
|
}),
|
||||||
|
arguments: {
|
||||||
|
SECS: {
|
||||||
|
type: ArgumentType.NUMBER,
|
||||||
|
defaultValue: 1
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
opcode: 'synthSetNote',
|
||||||
|
blockType: BlockType.COMMAND,
|
||||||
|
text: formatMessage({
|
||||||
|
id: 'synth.synthSetNote',
|
||||||
|
default: 'synth set [NOTE_TYPE] [NOTE]',
|
||||||
|
description: 'set note or frequency to synth'
|
||||||
|
}),
|
||||||
|
arguments: {
|
||||||
|
NOTE_TYPE: {
|
||||||
|
type: ArgumentType.STRING,
|
||||||
|
menu: 'note_type',
|
||||||
|
defaultValue: 'note'
|
||||||
|
},
|
||||||
|
NOTE: {
|
||||||
|
type: ArgumentType.NUMBER,
|
||||||
|
defaultValue: '60'
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
opcode: 'synthChangeNote',
|
||||||
|
blockType: BlockType.COMMAND,
|
||||||
|
text: formatMessage({
|
||||||
|
id: 'synth.synthChangeNote',
|
||||||
|
default: 'synth change [NOTE_TYPE] by [NOTE]',
|
||||||
|
description: 'change note or frequency to synth'
|
||||||
|
}),
|
||||||
|
arguments: {
|
||||||
|
NOTE_TYPE: {
|
||||||
|
type: ArgumentType.STRING,
|
||||||
|
menu: 'note_type',
|
||||||
|
defaultValue: 'note'
|
||||||
|
},
|
||||||
|
NOTE: {
|
||||||
|
type: ArgumentType.NUMBER,
|
||||||
|
defaultValue: '1'
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
opcode: 'synthGetNote',
|
||||||
|
blockType: BlockType.REPORTER,
|
||||||
|
text: formatMessage({
|
||||||
|
id: 'synth.synthGetNote',
|
||||||
|
default: 'synth [NOTE_TYPE]',
|
||||||
|
description: 'get the value of current note or frequency'
|
||||||
|
}),
|
||||||
|
arguments: {
|
||||||
|
NOTE_TYPE: {
|
||||||
|
type: ArgumentType.STRING,
|
||||||
|
menu: 'note_type',
|
||||||
|
defaultValue: 'note'
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
opcode: 'synthSetEffect',
|
||||||
|
blockType: BlockType.COMMAND,
|
||||||
|
text: formatMessage({
|
||||||
|
id: 'synth.synthSetEffect',
|
||||||
|
default: 'set synth [EFFECT_TYPE] to [EFFECT_VALUE] %',
|
||||||
|
description: 'set effect to synth'
|
||||||
|
}),
|
||||||
|
arguments: {
|
||||||
|
EFFECT_TYPE: {
|
||||||
|
type: ArgumentType.STRING,
|
||||||
|
menu: 'effect_type',
|
||||||
|
defaultValue: 'echo'
|
||||||
|
},
|
||||||
|
EFFECT_VALUE: {
|
||||||
|
type: ArgumentType.NUMBER,
|
||||||
|
defaultValue: '100'
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
opcode: 'synthChangeEffect',
|
||||||
|
blockType: BlockType.COMMAND,
|
||||||
|
text: formatMessage({
|
||||||
|
id: 'synth.synthChangeEffect',
|
||||||
|
default: 'change synth [EFFECT_TYPE] by [EFFECT_VALUE] %',
|
||||||
|
description: 'change effect to synth'
|
||||||
|
}),
|
||||||
|
arguments: {
|
||||||
|
EFFECT_TYPE: {
|
||||||
|
type: ArgumentType.STRING,
|
||||||
|
menu: 'effect_type',
|
||||||
|
defaultValue: 'echo'
|
||||||
|
},
|
||||||
|
EFFECT_VALUE: {
|
||||||
|
type: ArgumentType.NUMBER,
|
||||||
|
defaultValue: '100'
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
opcode: 'clearEffects',
|
||||||
|
blockType: BlockType.COMMAND,
|
||||||
|
text: formatMessage({
|
||||||
|
id: 'synth.clearEffects',
|
||||||
|
default: 'clear all effects',
|
||||||
|
description: 'clear all effects'
|
||||||
|
})
|
||||||
|
},
|
||||||
|
{
|
||||||
|
opcode: 'synthSetOscType',
|
||||||
|
blockType: BlockType.COMMAND,
|
||||||
|
text: formatMessage({
|
||||||
|
id: 'synth.synthSetOscType',
|
||||||
|
default: 'synth oscillator type [OSC_TYPE]',
|
||||||
|
description: 'change oscillator type'
|
||||||
|
}),
|
||||||
|
arguments: {
|
||||||
|
OSC_TYPE: {
|
||||||
|
type: ArgumentType.STRING,
|
||||||
|
menu: 'oscillator_type',
|
||||||
|
defaultValue: 'square'
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
],
|
||||||
|
menus: {
|
||||||
|
note_type: ['note', 'frequency'],
|
||||||
|
oscillator_type: ['sine', 'triangle', 'square', 'sawtooth', 'pwm'],
|
||||||
|
effect_type: ['echo', /*'wah',*/ 'pan left/right', 'glide', 'volume']
|
||||||
|
}
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
synthOnFor (args) {
|
||||||
|
let durationSec = Cast.toNumber(args.SECS);
|
||||||
|
this.synth.triggerAttackRelease(this.targetFreq, durationSec);
|
||||||
|
}
|
||||||
|
|
||||||
|
synthOnForAndWait (args, util) {
|
||||||
|
if (this._stackTimerNeedsInit(util)) {
|
||||||
|
let durationSec = Cast.toNumber(args.SECS);
|
||||||
|
this.synth.triggerAttackRelease(this.targetFreq, durationSec);
|
||||||
|
this._startStackTimer(util, durationSec);
|
||||||
|
} else {
|
||||||
|
this._checkStackTimer(util);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
synthOn () {
|
||||||
|
this.synth.triggerAttack(this.targetFreq);
|
||||||
|
}
|
||||||
|
|
||||||
|
synthOff () {
|
||||||
|
this.synth.triggerRelease();
|
||||||
|
}
|
||||||
|
|
||||||
|
synthSetNote (args) {
|
||||||
|
let val = Cast.toNumber(args.NOTE);
|
||||||
|
switch (args.NOTE_TYPE) {
|
||||||
|
case 'note':
|
||||||
|
let note = this._clamp(val, this.minNote, this.maxNote);
|
||||||
|
this.targetFreq = Tone.Frequency(note, "midi");
|
||||||
|
break;
|
||||||
|
case 'frequency':
|
||||||
|
this.targetFreq = val;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
this.synth.setNote(this.targetFreq);
|
||||||
|
}
|
||||||
|
|
||||||
|
synthChangeNote (args) {
|
||||||
|
let val = Cast.toNumber(args.NOTE);
|
||||||
|
switch (args.NOTE_TYPE) {
|
||||||
|
case 'note':
|
||||||
|
var ratio = Tone.intervalToFrequencyRatio(val);
|
||||||
|
this.targetFreq *= ratio;
|
||||||
|
break;
|
||||||
|
case 'frequency':
|
||||||
|
this.targetFreq += val;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
this.synth.setNote(this.targetFreq);
|
||||||
|
}
|
||||||
|
|
||||||
|
synthGetNote (args) {
|
||||||
|
switch(args.NOTE_TYPE){
|
||||||
|
case 'note':
|
||||||
|
return Tone.Frequency(this.targetFreq).toMidi();
|
||||||
|
case 'frequency':
|
||||||
|
return this.targetFreq;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
synthSetEffect (args) {
|
||||||
|
let val = this._clamp(args.EFFECT_VALUE, 0, 100);
|
||||||
|
val /= 100;
|
||||||
|
console.log(args.EFFECT_VALUE + ' --> ' + val);
|
||||||
|
|
||||||
|
switch (args.EFFECT_TYPE) {
|
||||||
|
case 'echo':
|
||||||
|
this.delay.wet.value = val/2;
|
||||||
|
break;
|
||||||
|
case 'wah':
|
||||||
|
if (val == 0) {
|
||||||
|
this.autoWah.wet.value = 0;
|
||||||
|
} else {
|
||||||
|
this.autoWah.wet.value = 1;
|
||||||
|
}
|
||||||
|
this.autoWah.Q.value = val * 6;
|
||||||
|
break;
|
||||||
|
case 'pan left/right':
|
||||||
|
this.panner.pan.value = (val-0.5)*2;
|
||||||
|
break;
|
||||||
|
case 'glide':
|
||||||
|
this.synth.portamento = val * 0.25;
|
||||||
|
break;
|
||||||
|
case 'volume':
|
||||||
|
var db = Tone.gainToDb(val);
|
||||||
|
Tone.Master.volume.rampTo(db, 0.01);
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
synthChangeEffect (args) {
|
||||||
|
|
||||||
|
let val = Cast.toNumber(args.EFFECT_VALUE) / 100;
|
||||||
|
console.log(args.EFFECT_VALUE + ' --> ' + val);
|
||||||
|
|
||||||
|
switch(args.EFFECT_TYPE){
|
||||||
|
case 'echo':
|
||||||
|
this.delay.wet.value += val/2;
|
||||||
|
this.delay.wet.value = this._clamp(this.delay.wet.value, 0, 0.5);
|
||||||
|
break;
|
||||||
|
case 'wah':
|
||||||
|
this.autoWah.Q.value += val * 6;
|
||||||
|
this.autoWah.Q.value = this._clamp(this.autoWah.Q.value, 0, 6);
|
||||||
|
/*if (this.autoWah.Q.value == 0) {
|
||||||
|
this.autoWah.wet.value = 0;
|
||||||
|
} else {
|
||||||
|
this.autoWah.wet.value = 1;
|
||||||
|
}*/
|
||||||
|
break;
|
||||||
|
case 'pan left/right':
|
||||||
|
this.panner.pan.value += val;
|
||||||
|
this.panner.pan.value = this._clamp(this.panner.pan.value, 0, 1);
|
||||||
|
break;
|
||||||
|
case 'glide':
|
||||||
|
this.synth.portamento += val * 0.25;
|
||||||
|
break;
|
||||||
|
case 'volume':
|
||||||
|
var currentDb = Tone.Master.volume.value;
|
||||||
|
var currentVol = Tone.dbToGain(currentDb);
|
||||||
|
var newVol = currentVol + val;
|
||||||
|
newVol = this._clamp(newVol, 0, 1);
|
||||||
|
var db = Tone.gainToDb(newVol);
|
||||||
|
Tone.Master.volume.rampTo(db, 0.01);
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
clearEffects (args) {
|
||||||
|
this.delay.wet.value = 0;
|
||||||
|
this.autoWah.Q.value = 0;
|
||||||
|
this.autoWah.wet.value = 0;
|
||||||
|
this.panner.pan.value = 0.5;
|
||||||
|
this.synth.portamento = 0;
|
||||||
|
Tone.Master.volume.rampTo(0, 0.01);
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
synthSetOscType (args) {
|
||||||
|
this.synth.oscillator.type = args.OSC_TYPE;
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
module.exports = Scratch3SynthBlocks;
|
24382
src/extensions/scratch3_synth/tone.js
Normal file
24382
src/extensions/scratch3_synth/tone.js
Normal file
File diff suppressed because it is too large
Load diff
Loading…
Reference in a new issue