Skip to content
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

Closed
wants to merge 3 commits into from
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
62 changes: 62 additions & 0 deletions src/Loudness.js
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;
Copy link
Collaborator

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.

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;
}
}
57 changes: 57 additions & 0 deletions src/Project.js
Original file line number Diff line number Diff line change
Expand Up @@ -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 } = {}) {
Expand All @@ -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();
Expand Down Expand Up @@ -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);
Copy link
Collaborator

Choose a reason for hiding this comment

The 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++) {
Expand Down Expand Up @@ -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.
Expand Down Expand Up @@ -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) {
Expand Down
7 changes: 5 additions & 2 deletions src/Sprite.js
Original file line number Diff line number Diff line change
Expand Up @@ -227,8 +227,7 @@ class SpriteBase {
}

get timer() {
const ms = new Date() - this._project.timerStart;
return ms / 1000;
return this._project.timer;
}

restartTimer() {
Expand Down Expand Up @@ -305,6 +304,10 @@ class SpriteBase {
get answer() {
return this._project.answer;
}

async loudness() {
return this._project.loudnessHandler.getLoudness();
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

}
}

export class Sprite extends SpriteBase {
Expand Down
14 changes: 14 additions & 0 deletions src/Trigger.js
Original file line number Diff line number Diff line change
Expand Up @@ -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) {
Expand All @@ -18,6 +20,8 @@ export default class Trigger {

this.done = false;
this.stop = () => {};

this.executed = false;
}

matches(trigger, options) {
Expand All @@ -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;
Expand Down Expand Up @@ -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;
}
}