-
Notifications
You must be signed in to change notification settings - Fork 26
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Loudness related thing support #60
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -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; | ||
} | ||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -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); | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Scratch seems to implement "edge-activated hats" (what we call "greater-than triggers" here) a bit differently--every step, it evaluates the predicate of the hat (e.g. "is loudness greater than N? is timer greater than N?") which returns either a true or false value. It fires those hat blocks if the predicate evaluates to true and it evaluated to false on the previous step (the predicate is on a rising edge). This seems like it'd require less per-trigger logic, and the predicate logic can be encapsulated more cleanly. Would that be worth doing? |
||
|
||
// 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) { | ||
|
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -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(); | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. It looks like Scratch only recomputes loudness once per step. |
||
} | ||
} | ||
|
||
export class Sprite extends SpriteBase { | ||
|
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This seems to be missing a lot of explanatory comments from Scratch's implementation.