Last active
November 18, 2016 16:20
-
-
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 file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
// 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); | |
}; | |
}]) |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
'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>' | |
}; | |
}]); |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
<!-- 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