diff --git a/src/Loudness.js b/src/Loudness.js new file mode 100644 index 0000000..04a87ff --- /dev/null +++ b/src/Loudness.js @@ -0,0 +1,62 @@ +import Sound from "Sound.js"; + +const IGNORABLE_ERROR = ["NotAllowedError", "NotFoundError"]; + +// https://github.com/LLK/scratch-audio/blob/develop/src/Loudness.js +export default class LoudnessHandler { + constructor() { + this.mic = null; + this.hasConnected = null; + } + + get audioContext() { + return Sound.audioContext; + } + + async connect() { + if (this.hasConnected) return; + return navigator.mediaDevices + .getUserMedia({ audio: true }) + .then(stream => { + this.hasConnected = true; + this.audioStream = stream; + this.mic = this.audioContext.createMediaStreamSource(stream); + this.analyser = this.audioContext.createAnalyser(); + this.mic.connect(this.analyser); + this.micDataArray = new Float32Array(this.analyser.fftSize); + }) + .catch(e => { + if (IGNORABLE_ERROR.includes(e.name)) { + console.warn("Mic is not available."); + } else { + throw e; + } + }); + } + + get loudness() { + if (this.mic && this.audioStream.active) { + this.analyser.getFloatTimeDomainData(this.micDataArray); + let sum = 0; + for (let i = 0; i < this.micDataArray.length; i++) { + sum += Math.pow(this.micDataArray[i], 2); + } + let rms = Math.sqrt(sum / this.micDataArray.length); + if (this._lastValue) { + rms = Math.max(rms, this._lastValue * 0.6); + } + this._lastValue = rms; + rms *= 1.63; + rms = Math.sqrt(rms); + rms = Math.round(rms * 100); + rms = Math.min(rms, 100); + return rms; + } + return -1; + } + + async getLoudness() { + await this.connect(); + return this.loudness; + } +} diff --git a/src/Project.js b/src/Project.js index 0b8e95b..aa70c66 100644 --- a/src/Project.js +++ b/src/Project.js @@ -2,6 +2,7 @@ import Trigger from "./Trigger.js"; import Renderer from "./Renderer.js"; import Input from "./Input.js"; import { Stage } from "./Sprite.js"; +import LoudnessHandler from "./Loudness.js"; export default class Project { constructor(stage, sprites = {}, { frameRate = 30 } = {}) { @@ -20,12 +21,26 @@ export default class Project { this.fireTrigger(Trigger.KEY_PRESSED, { key }); }); + this.loudnessHandler = new LoudnessHandler(); + this.runningTriggers = []; this.restartTimer(); this.answer = null; + if ( + this.spritesAndStage.some(spr => + spr.triggers.some( + trig => trig.trigger === Trigger.LOUDNESS_GREATER_THAN + ) + ) + ) { + this.loudnessHandler.connect(); + } + + this._prevLoudness = 0; + // Run project code at specified framerate setInterval(() => { this.step(); @@ -82,6 +97,12 @@ export default class Project { } step() { + if (this.loudnessHandler.loudness > this._prevLoudness) { + this.fireGreatherThanTrigger(Trigger.LOUDNESS_GREATER_THAN); + } + this._prevLoudness = this.loudnessHandler.loudness; + this.fireGreatherThanTrigger(Trigger.TIMER_GREATER_THAN); + // Step all triggers const alreadyRunningTriggers = this.runningTriggers; for (let i = 0; i < alreadyRunningTriggers.length; i++) { @@ -146,6 +167,32 @@ export default class Project { return this._startTriggers(matchingTriggers); } + fireGreatherThanTrigger(trigger) { + // GreaterThanTrigger are a bit different; we need to check if the value is bigger. + let triggerMatcher = () => true; + let triggerBeforeExecute = () => {}; + switch (trigger) { + case Trigger.LOUDNESS_GREATER_THAN: + triggerMatcher = trig => + trig.options.loudness < this.loudnessHandler.loudness; + break; + case Trigger.TIMER_GREATER_THAN: + triggerMatcher = trig => + trig.options.timer < this.timer && !this.executed; + triggerBeforeExecute = trig => (trig.executed = true); + break; + default: + return; + } + const matchingTriggers = this.spritesAndStage.flatMap(spr => { + return spr.triggers + .filter(trig => trig.trigger === trigger && triggerMatcher(trig)) + .map(trig => ({ trigger: trig, target: spr })); + }); + matchingTriggers.forEach(triggerBeforeExecute); + return this._startTriggers(matchingTriggers); + } + _startTriggers(triggers) { // Only add these triggers to this.runningTriggers if they're not already there. // TODO: if the triggers are already running, they'll be restarted but their execution order is unchanged. @@ -208,6 +255,16 @@ export default class Project { restartTimer() { this.timerStart = new Date(); + this.spritesAndStage.forEach(spr => + spr.triggers.forEach(trig => { + if (trig.trigger === Trigger.TIMER_GREATER_THAN) trig.executed = false; + }) + ); + } + + get timer() { + const ms = new Date() - this.timerStart; + return ms / 1000; } async askAndWait(question) { diff --git a/src/Sprite.js b/src/Sprite.js index 5b05e67..814a6c1 100644 --- a/src/Sprite.js +++ b/src/Sprite.js @@ -227,8 +227,7 @@ class SpriteBase { } get timer() { - const ms = new Date() - this._project.timerStart; - return ms / 1000; + return this._project.timer; } restartTimer() { @@ -305,6 +304,10 @@ class SpriteBase { get answer() { return this._project.answer; } + + async loudness() { + return this._project.loudnessHandler.getLoudness(); + } } export class Sprite extends SpriteBase { diff --git a/src/Trigger.js b/src/Trigger.js index cf6adfa..8972ec0 100644 --- a/src/Trigger.js +++ b/src/Trigger.js @@ -3,6 +3,8 @@ const KEY_PRESSED = Symbol("KEY_PRESSED"); const BROADCAST = Symbol("BROADCAST"); const CLICKED = Symbol("CLICKED"); const CLONE_START = Symbol("CLONE_START"); +const LOUDNESS_GREATER_THAN = Symbol("TIMER_GREATER_THAN"); +const TIMER_GREATER_THAN = Symbol("TIMER_GREATER_THAN"); export default class Trigger { constructor(trigger, options, script) { @@ -18,6 +20,8 @@ export default class Trigger { this.done = false; this.stop = () => {}; + + this.executed = false; } matches(trigger, options) { @@ -32,6 +36,10 @@ export default class Trigger { start(target) { this.stop(); + if (this.trigger === Trigger.TIMER_GREATER_THAN && this.executed) + return new Promise(resolve => resolve()); + this.executed = true; + const boundScript = this._script.bind(target); this.done = false; @@ -65,4 +73,10 @@ export default class Trigger { static get CLONE_START() { return CLONE_START; } + static get LOUDNESS_GREATER_THAN() { + return LOUDNESS_GREATER_THAN; + } + static get TIMER_GREATER_THAN() { + return TIMER_GREATER_THAN; + } }