Skip to content

Instantly share code, notes, and snippets.

@mbarzeev
Last active November 18, 2016 16:20
Show Gist options
  • Star 4 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save mbarzeev/6119673 to your computer and use it in GitHub Desktop.
Save mbarzeev/6119673 to your computer and use it in GitHub Desktop.
A Metronome Directive. Taken after Chris Wilson's article (http://www.html5rocks.com/en/tutorials/audio/scheduling/).
// This is the "run" block of your main module
.run(['$window', function (window) {
// First, let's shim the requestAnimationFrame API,
// with a setTimeout fallback
window.requestAnimFrame = window.requestAnimationFrame ||
window.webkitRequestAnimationFrame ||
window.mozRequestAnimationFrame ||
window.oRequestAnimationFrame ||
window.msRequestAnimationFrame ||
function (callback) {
window.setTimeout(callback, 1000 / 60);
};
}])
'use strict';
angular.module('MyModule')
.directive('metronome', ['$window', function (window) {
return {
restrict: 'E',
scope: true,
link: function ($scope, element, attrs) {
$scope.audioContext = new webkitAudioContext();
$scope.isPlaying = false;
$scope.current16thNote;
$scope.tempo = parseInt(attrs.tempo, 10);
$scope.lookahead = 25.0;
$scope.scheduleAheadTime = 0.1;
$scope.nextNoteTime = 0.0;
$scope.noteResolution = 0;
$scope.noteLength = 0.05;
$scope.timerID = 0;
$scope.minimumTempo = 30.0;
$scope.maximumTempo = 230.0;
attrs.$observe('tempo', function (value) {
$scope.tempo = parseInt(value, 10);
});
$scope.nextNote = function () {
// Advance current note and time by a 16th note...
var secondsPerBeat = 60.0 / $scope.tempo; // Notice this picks up the CURRENT
// tempo value to calculate beat length.
$scope.nextNoteTime += 0.25 * secondsPerBeat; // Add beat length to last beat time
$scope.current16thNote++; // Advance the beat number, wrap to zero
if ($scope.current16thNote === 16) {
$scope.current16thNote = 0;
}
},
$scope.scheduleNote = function (beatNumber, time) {
if (($scope.noteResolution === 1) && (beatNumber % 2)) {
return; // we're not playing non-8th 16th notes
}
if (($scope.noteResolution === 2) && (beatNumber % 4)) {
return; // we're not playing non-quarter 8th notes
}
// create an oscillator
var osc = $scope.audioContext.createOscillator();
osc.connect($scope.audioContext.destination);
if (!(beatNumber % 16)) { // beat 0 == low pitch
osc.frequency.value = 220.0;
} else if (beatNumber % 4) { // quarter notes = medium pitch
osc.frequency.value = 440.0;
} else { // other 16th notes = high pitch
osc.frequency.value = 880.0;
}
// TODO: Once start()/stop() deploys on Safari and iOS, these should be changed.
osc.noteOn(time);
osc.noteOff(time + $scope.noteLength);
},
$scope.scheduler = function () {
// while there are notes that will need to play before the
// next interval, schedule them and advance the pointer.
while ($scope.nextNoteTime < $scope.audioContext.currentTime + $scope.scheduleAheadTime) {
$scope.scheduleNote($scope.current16thNote, $scope.nextNoteTime);
$scope.nextNote();
}
$scope.timerID = window.setTimeout($scope.scheduler, $scope.lookahead);
},
$scope.play = function () {
$scope.isPlaying = !$scope.isPlaying;
if ($scope.isPlaying) { // start playing
$scope.current16thNote = 0;
$scope.nextNoteTime = $scope.audioContext.currentTime;
$scope.scheduler(); // kick off scheduling
return 'stop';
} else {
window.clearTimeout($scope.timerID);
return 'play';
}
};
$scope.getButtonText = function () {
var result = $scope.isPlaying ? 'Stop': 'Play';
return result;
};
},
template: '<div>' +
'<input id="bpm" type="number" name="bpm" ng-model="tempo" min="{{minimumTempo}}" max="{{maximumTempo}}"> Bpm' +
'<button ng-click="play()">{{getButtonText()}}</button>' +
'</div>'
};
}]);
<!-- Given that you have a $scope with mytempo defined on it -->
<div>
<input type="number" ng-model="mytempo">
<metronome tempo="{{mytempo}}"></metronome>
</div>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment