Merge pull request #779 from ericrosenbaum/feature/music-timing-with-stackframe

Music extension handles timing
This commit is contained in:
Eric Rosenbaum 2017-11-15 13:29:33 -05:00 committed by GitHub
commit 5b0f7453f3
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
2 changed files with 107 additions and 41 deletions

View file

@ -3,6 +3,7 @@ const BlockType = require('../extension-support/block-type');
const Clone = require('../util/clone');
const Cast = require('../util/cast');
const MathUtil = require('../util/math-util');
const Timer = require('../util/timer');
/**
* An array of drum names, used in the play drum block.
@ -70,6 +71,13 @@ class Scratch3MusicBlocks {
*/
this.runtime = runtime;
/**
* The current tempo in beats per minute. The tempo is a global property of the project,
* not a property of each sprite, so it is not stored in the MusicState object.
* @type {number}
*/
this.tempo = 60;
this.drumMenu = this._buildMenu(drumNames);
this.instrumentMenu = this._buildMenu(instrumentNames);
}
@ -252,17 +260,22 @@ class Scratch3MusicBlocks {
* @param {object} util - utility object provided by the runtime.
* @property {int} DRUM - the number of the drum to play.
* @property {number} BEATS - the duration in beats of the drum sound.
* @return {Promise} - a promise which will resolve at the end of the duration.
*/
playDrumForBeats (args, util) {
let drum = Cast.toNumber(args.DRUM);
drum -= 1; // drums are one-indexed
if (typeof this.runtime.audioEngine === 'undefined') return;
drum = MathUtil.wrapClamp(drum, 0, this.runtime.audioEngine.numDrums - 1);
let beats = Cast.toNumber(args.BEATS);
beats = this._clampBeats(beats);
if (util.target.audioPlayer === null) return;
return util.target.audioPlayer.playDrumForBeats(drum, beats);
if (this._stackTimerNeedsInit(util)) {
let drum = Cast.toNumber(args.DRUM);
drum -= 1; // drums are one-indexed
if (typeof this.runtime.audioEngine === 'undefined') return;
drum = MathUtil.wrapClamp(drum, 0, this.runtime.audioEngine.numDrums - 1);
let beats = Cast.toNumber(args.BEATS);
beats = this._clampBeats(beats);
if (util.target.audioPlayer !== null) {
util.target.audioPlayer.playDrumForBeats(drum, beats);
}
this._startStackTimer(util, this._beatsToSec(beats));
} else {
this._checkStackTimer(util);
}
}
/**
@ -270,13 +283,15 @@ class Scratch3MusicBlocks {
* @param {object} args - the block arguments.
* @param {object} util - utility object provided by the runtime.
* @property {number} BEATS - the duration in beats of the rest.
* @return {Promise} - a promise which will resolve at the end of the duration.
*/
restForBeats (args) {
let beats = Cast.toNumber(args.BEATS);
beats = this._clampBeats(beats);
if (typeof this.runtime.audioEngine === 'undefined') return;
return this.runtime.audioEngine.waitForBeats(beats);
restForBeats (args, util) {
if (this._stackTimerNeedsInit(util)) {
let beats = Cast.toNumber(args.BEATS);
beats = this._clampBeats(beats);
this._startStackTimer(util, this._beatsToSec(beats));
} else {
this._checkStackTimer(util);
}
}
/**
@ -285,17 +300,80 @@ class Scratch3MusicBlocks {
* @param {object} util - utility object provided by the runtime.
* @property {number} NOTE - the pitch of the note to play, interpreted as a MIDI note number.
* @property {number} BEATS - the duration in beats of the note.
* @return {Promise} - a promise which will resolve at the end of the duration.
*/
playNoteForBeats (args, util) {
let note = Cast.toNumber(args.NOTE);
note = MathUtil.clamp(note, Scratch3MusicBlocks.MIDI_NOTE_RANGE.min, Scratch3MusicBlocks.MIDI_NOTE_RANGE.max);
let beats = Cast.toNumber(args.BEATS);
beats = this._clampBeats(beats);
const musicState = this._getMusicState(util.target);
const inst = musicState.currentInstrument;
if (typeof this.runtime.audioEngine === 'undefined') return;
return this.runtime.audioEngine.playNoteForBeatsWithInstAndVol(note, beats, inst, 100);
if (this._stackTimerNeedsInit(util)) {
let note = Cast.toNumber(args.NOTE);
note = MathUtil.clamp(note,
Scratch3MusicBlocks.MIDI_NOTE_RANGE.min, Scratch3MusicBlocks.MIDI_NOTE_RANGE.max);
let beats = Cast.toNumber(args.BEATS);
beats = this._clampBeats(beats);
const musicState = this._getMusicState(util.target);
const inst = musicState.currentInstrument;
if (typeof this.runtime.audioEngine !== 'undefined') {
this.runtime.audioEngine.playNoteForBeatsWithInstAndVol(note, beats, inst, 100);
}
this._startStackTimer(util, this._beatsToSec(beats));
} else {
this._checkStackTimer(util);
}
}
/**
* Clamp a duration in beats to the allowed min and max duration.
* @param {number} beats - a duration in beats.
* @return {number} - the clamped duration.
* @private
*/
_clampBeats (beats) {
return MathUtil.clamp(beats, Scratch3MusicBlocks.BEAT_RANGE.min, Scratch3MusicBlocks.BEAT_RANGE.max);
}
/**
* Convert a number of beats to a number of seconds, using the current tempo.
* @param {number} beats - number of beats to convert to secs.
* @return {number} seconds - number of seconds `beats` will last.
* @private
*/
_beatsToSec (beats) {
return (60 / this.tempo) * beats;
}
/**
* 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;
}
/**
* 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;
if (util.stackFrame.duration > 0) {
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();
}
}
/**
@ -315,16 +393,6 @@ class Scratch3MusicBlocks {
return this.runtime.audioEngine.instrumentPlayer.loadInstrument(musicState.currentInstrument);
}
/**
* Clamp a duration in beats to the allowed min and max duration.
* @param {number} beats - a duration in beats.
* @return {number} - the clamped duration.
* @private
*/
_clampBeats (beats) {
return MathUtil.clamp(beats, Scratch3MusicBlocks.BEAT_RANGE.min, Scratch3MusicBlocks.BEAT_RANGE.max);
}
/**
* Set the current tempo to a new value.
* @param {object} args - the block arguments.
@ -342,8 +410,7 @@ class Scratch3MusicBlocks {
*/
changeTempo (args) {
const change = Cast.toNumber(args.TEMPO);
if (typeof this.runtime.audioEngine === 'undefined') return;
const tempo = change + this.runtime.audioEngine.currentTempo;
const tempo = change + this.tempo;
this._updateTempo(tempo);
}
@ -354,8 +421,7 @@ class Scratch3MusicBlocks {
*/
_updateTempo (tempo) {
tempo = MathUtil.clamp(tempo, Scratch3MusicBlocks.TEMPO_RANGE.min, Scratch3MusicBlocks.TEMPO_RANGE.max);
if (typeof this.runtime.audioEngine === 'undefined') return;
this.runtime.audioEngine.setTempo(tempo);
this.tempo = tempo;
}
/**
@ -363,8 +429,7 @@ class Scratch3MusicBlocks {
* @return {number} - the current tempo, in beats per minute.
*/
getTempo () {
if (typeof this.runtime.audioEngine === 'undefined') return;
return this.runtime.audioEngine.currentTempo;
return this.tempo;
}
}

View file

@ -17,7 +17,8 @@ const util = {
audioPlayer: {
playDrumForBeats: drum => (playedDrum = drum)
}
}
},
stackFrame: Object.create(null)
};
test('playDrum uses 1-indexing and wrap clamps', t => {