Skip to content

Commit 2644e0c

Browse files
authored
Merge pull request #210 from jvntf/soundLoop
p5.SoundLoop()
2 parents 9f4690d + 383e10a commit 2644e0c

File tree

6 files changed

+334
-0
lines changed

6 files changed

+334
-0
lines changed

Gruntfile.js

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -87,6 +87,7 @@ module.exports = function(grunt) {
8787
'distortion': 'src/distortion',
8888
'compressor': 'src/compressor',
8989
'looper': 'src/looper',
90+
'soundloop': 'src/soundLoop',
9091
'soundRecorder': 'src/soundRecorder',
9192
'signal': 'src/signal',
9293
'metro': 'src/metro',

examples/Gymnopedia.mp3

-3.22 MB
Binary file not shown.

examples/sound_loop/index.html

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,10 @@
1+
<head>
2+
<script language="javascript" type="text/javascript" src="../../lib/p5.js"></script>
3+
4+
<script language="javascript" type="text/javascript" src="../../lib/addons/p5.dom.js"></script>
5+
6+
<script language="javascript" type="text/javascript" src="../../lib/p5.sound.js"></script>
7+
8+
<script language="javascript" type="text/javascript" src="sketch.js"></script>
9+
10+
</head>

examples/sound_loop/sketch.js

Lines changed: 44 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,44 @@
1+
/**
2+
* Create a sequence using a Part with callbacks that play back soundfiles.
3+
* The callback includes parameters (the value at that position in the Phrase array)
4+
* as well as time, which should be used to schedule playback with precision.
5+
*
6+
*/
7+
8+
var click, beatbox ;
9+
10+
var looper1, looper2;
11+
12+
13+
function preload() {
14+
soundFormats('mp3', 'ogg');
15+
click = loadSound('../files/drum');
16+
beatbox = loadSound('../files/beatbox');
17+
18+
}
19+
20+
function setup() {
21+
22+
//Hemiola! 2 loops, playing sounds in a 4 over 3 pattern
23+
//gradually increase the tempo of both loops
24+
//
25+
//the looper's callback is passed the timeFromNow
26+
//this value should be used as a reference point from
27+
//which to schedule sounds
28+
29+
looper1 = new p5.SoundLoop(function(timeFromNow){
30+
click.play(timeFromNow);
31+
looper1.bpm looper1.bpm += 0.5;
32+
}, "8n");
33+
34+
looper2 = new p5.SoundLoop(function(timeFromNow){
35+
beatbox.play(timeFromNow);
36+
looper2.bpm = looper1.bpm;
37+
}, "12n");
38+
39+
//start the loops together
40+
looper1.syncedStart(looper2);
41+
}
42+
43+
44+

src/app.js

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,7 @@ define(function (require) {
2424
require('reverb');
2525
require('metro');
2626
require('looper');
27+
require('soundloop');
2728
require('compressor');
2829
require('soundRecorder');
2930
require('peakdetect');

src/soundLoop.js

Lines changed: 278 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,278 @@
1+
'use strict';
2+
3+
define(function (require) {
4+
var p5sound = require('master');
5+
var Clock = require('Tone/core/Clock');
6+
7+
/**
8+
* SoundLoop
9+
*
10+
* @class p5.SoundLoop
11+
* @constructor
12+
*
13+
* @param {Function} callback this function will be called on each iteration of theloop
14+
* @param {Number or String} [interval] amount of time or beats for each iteration of the loop
15+
* defaults to 1
16+
*
17+
* @example
18+
* <div><code>
19+
* var click;
20+
* var looper1;
21+
*
22+
* function preload() {
23+
* click = loadSound('assets/drum.mp3'
24+
* }
25+
*
26+
* function setup() {
27+
* //the looper's callback is passed the timeFromNow
28+
* //this value should be used as a reference point from
29+
* //which to schedule sounds
30+
* looper1 = new p5.SoundLoop(function(timeFromNow){
31+
* click.play(timeFromNow);
32+
* background(255 * (looper1.iterations % 2));
33+
* }, 2);
34+
*
35+
* //stop after 10 iteratios;
36+
* looper1.maxIterations = 10;
37+
* //start the loop
38+
* looper1.start();
39+
* }
40+
* </code></div>
41+
*/
42+
p5.SoundLoop = function(callback, interval) {
43+
this.callback = callback;
44+
/**
45+
* musicalTimeMode uses <a href = "https://github.com/Tonejs/Tone.js/wiki/Time">Tone.Time</a> convention
46+
* @property {Boolean} musicalTimeMode true if string, false if number
47+
*/
48+
this.musicalTimeMode = typeof this._interval === 'number' ? false : true;
49+
50+
this._interval = interval || 1;
51+
52+
/**
53+
* musicalTimeMode variables
54+
* modify these only when the interval is specified in musicalTime format as a string
55+
* @property {Number} [BPM] beats per minute (defaults to 60)
56+
* @property {Number} [timeSignature] number of quarter notes in a measure (defaults to 4)
57+
*/
58+
this._timeSignature = 4;
59+
this._bpm = 60;
60+
61+
this.isPlaying = false;
62+
63+
/**
64+
* Set a limit to the number of loops to play
65+
* @property {Number} [maxIterations] defaults to Infinity
66+
*/
67+
this.maxIterations = Infinity;
68+
var self = this;
69+
70+
this.clock = new Clock({
71+
'callback' : function(time) {
72+
var timeFromNow = time - p5sound.audiocontext.currentTime;
73+
/**
74+
* Do not initiate the callback if timeFromNow is < 0
75+
* This ususually occurs for a few milliseconds when the page
76+
* is not fully loaded
77+
*
78+
* The callback should only be called until maxIterations is reached
79+
*/
80+
if (timeFromNow > 0 && self.iterations <= self.maxIterations) {
81+
self.callback(timeFromNow);}
82+
},
83+
'frequency' : this._calcFreq()
84+
});
85+
};
86+
87+
/**
88+
* Start the loop
89+
* @method start
90+
* @param {Number} [timeFromNow] schedule a starting time
91+
*/
92+
p5.SoundLoop.prototype.start = function(timeFromNow) {
93+
var t = timeFromNow || 0;
94+
var now = p5sound.audiocontext.currentTime;
95+
if (!this.isPlaying) {
96+
this.clock.start(now + t);
97+
this.isPlaying = true;
98+
}
99+
};
100+
101+
/**
102+
* Stop the loop
103+
* @method stop
104+
* @param {Number} [timeFromNow] schedule a stopping time
105+
*/
106+
p5.SoundLoop.prototype.stop = function(timeFromNow) {
107+
var t = timeFromNow || 0;
108+
var now = p5sound.audiocontext.currentTime;
109+
if (this.isPlaying) {
110+
this.clock.stop(now + t);
111+
this.isPlaying = false;
112+
}
113+
};
114+
/**
115+
* Pause the loop
116+
* @method pause
117+
* @param {Number} [timeFromNow] schedule a pausing time
118+
*/
119+
p5.SoundLoop.prototype.pause = function(timeFromNow) {
120+
var t = timeFromNow || 0;
121+
if (this.isPlaying) {
122+
this.clock.pause(t);
123+
this.isPlaying = false;
124+
}
125+
};
126+
127+
128+
/**
129+
* Synchronize loops. Use this method to start two more more loops in synchronization
130+
* or to start a loop in synchronization with a loop that is already playing
131+
* This method will schedule the implicit loop in sync with the explicit master loop
132+
* i.e. loopToStart.syncedStart(loopToSyncWith)
133+
*
134+
* @method syncedStart
135+
* @param {Object} otherLoop a p5.SoundLoop to sync with
136+
* @param {Number} [timeFromNow] Start the loops in sync after timeFromNow seconds
137+
*/
138+
p5.SoundLoop.prototype.syncedStart = function(otherLoop, timeFromNow) {
139+
var t = timeFromNow || 0;
140+
var now = p5sound.audiocontext.currentTime;
141+
142+
if (!otherLoop.isPlaying) {
143+
otherLoop.clock.start(now + t);
144+
otherLoop.isPlaying = true;
145+
this.clock.start(now + t);
146+
this.isPlaying = true;
147+
} else if (otherLoop.isPlaying) {
148+
var time = otherLoop.clock._nextTick - p5sound.audiocontext.currentTime;
149+
this.clock.start(now + time);
150+
this.isPlaying = true;
151+
}
152+
};
153+
154+
155+
/**
156+
* Updates frequency value, reflected in next callback
157+
* @private
158+
* @method _update
159+
*/
160+
p5.SoundLoop.prototype._update = function() {
161+
this.clock.frequency.value = this._calcFreq();
162+
};
163+
164+
/**
165+
* Calculate the frequency of the clock's callback based on bpm, interval, and timesignature
166+
* @private
167+
* @method _calcFreq
168+
* @return {Number} new clock frequency value
169+
*/
170+
p5.SoundLoop.prototype._calcFreq = function() {
171+
//Seconds mode, bpm / timesignature has no effect
172+
if (typeof this._interval === 'number') {
173+
this.musicalTimeMode = false;
174+
return 1 / this._interval;
175+
}
176+
//Musical timing mode, calculate interval based bpm, interval,and time signature
177+
else if (typeof this._interval === 'string') {
178+
this.musicalTimeMode = true;
179+
return this._bpm / 60 / this._convertNotation(this._interval) * (this._timeSignature / 4);
180+
}
181+
};
182+
183+
/**
184+
* Convert notation from musical time format to seconds
185+
* Uses <a href = "https://github.com/Tonejs/Tone.js/wiki/Time">Tone.Time</a> convention
186+
* @private
187+
* @method _convertNotation
188+
* @param {String} value value to be converted
189+
* @return {Number} converted value in seconds
190+
*/
191+
p5.SoundLoop.prototype._convertNotation = function(value) {
192+
var type = value.slice(-1);
193+
value = Number(value.slice(0,-1));
194+
switch (type) {
195+
case 'm':
196+
return this._measure(value);
197+
case 'n':
198+
return this._note(value);
199+
default:
200+
console.warn('Specified interval is not formatted correctly. See Tone.js '+
201+
'timing reference for more info: https://github.com/Tonejs/Tone.js/wiki/Time');
202+
}
203+
};
204+
205+
/**
206+
* Helper conversion methods of measure and note
207+
* @private
208+
* @method _measure
209+
* @method _note
210+
*/
211+
p5.SoundLoop.prototype._measure = function(value) {
212+
return value * this._timeSignature;
213+
};
214+
p5.SoundLoop.prototype._note = function(value) {
215+
return this._timeSignature / value ;
216+
};
217+
218+
219+
/**
220+
* Getters and Setters, setting any paramter will result in a change in the clock's
221+
* frequency, that will be reflected after the next callback
222+
* @param {Number} bpm
223+
* @param {Number} timeSignature
224+
* @param {Number/String} interval length of the loops interval
225+
* @param @readOnly {Number} iteations how many times the callback has been called so far
226+
*
227+
*/
228+
Object.defineProperty(p5.SoundLoop.prototype, 'bpm', {
229+
get : function() {
230+
return this._bpm;
231+
},
232+
set : function(bpm) {
233+
if (!this.musicalTimeMode) {
234+
console.warn('Changing the BPM in "seconds" mode has no effect. '+
235+
'BPM is only relevant in musicalTimeMode '+
236+
'when the interval is specified as a string '+
237+
'("2n", "4n", "1m"...etc)');
238+
}
239+
this._bpm = bpm;
240+
this._update();
241+
}
242+
});
243+
244+
Object.defineProperty(p5.SoundLoop.prototype, 'timeSignature', {
245+
get : function() {
246+
return this._timeSignature;
247+
},
248+
set : function(timeSig) {
249+
if (!this.musicalTimeMode) {
250+
console.warn('Changing the timeSignature in "seconds" mode has no effect. '+
251+
'BPM is only relevant in musicalTimeMode '+
252+
'when the interval is specified as a string '+
253+
'("2n", "4n", "1m"...etc)');
254+
}
255+
this._timeSignature = timeSig;
256+
this._update();
257+
}
258+
});
259+
260+
Object.defineProperty(p5.SoundLoop.prototype, 'interval', {
261+
get : function() {
262+
return this._interval;
263+
},
264+
set : function(interval) {
265+
this.musicalTimeMode = typeof interval === 'Number'? false : true;
266+
this._interval = interval;
267+
this._update();
268+
}
269+
});
270+
271+
Object.defineProperty(p5.SoundLoop.prototype, 'iterations', {
272+
get : function() {
273+
return this.clock.ticks;
274+
}
275+
});
276+
277+
return p5.SoundLoop;
278+
});

0 commit comments

Comments
 (0)