1 /* Copyright 2014 Yukkuri Games
2 Licensed under the terms of the GNU GPL v2 or later
3 @license http://www.gnu.org/licenses/gpl-2.0.html
4 @source: http://yukkurigames.com/yuu/
10 var yT
= this.yT
|| require("./yT");
11 var yf
= this.yf
|| require("./yf");
14 /** Audio context/source/buffer accessor
16 You probably don't need to make this yourself; one is made
17 named yuu.audio during initialization.
19 You can set the master volume with yuu.audio.masterVolume.
21 constructor: function () {
22 this._ctx
= new window
.AudioContext();
23 this._compressor
= this._ctx
.createDynamicsCompressor();
24 this._masterVolume
= this._ctx
.createGain();
25 this._masterVolume
.connect(this._compressor
);
26 this._compressor
.connect(this._ctx
.destination
);
27 this._musicVolume
= this._ctx
.createGain();
28 this._musicVolume
.connect(this._masterVolume
);
29 this._masterVolume
.gain
.value
= 0.5;
30 this._musicVolume
.gain
.value
= 0.5;
32 this._bufferCache
= {};
35 this._volume
= this._masterVolume
.gain
.value
;
38 destination
: { alias
: "_masterVolume", readonly
: true },
39 music
: { alias
: "_musicVolume", readonly
: true },
41 _readStorage: function () {
44 yf
.each
.call(this, function (prop
) {
45 this[prop
] = this._storage
.getObject(prop
, this[prop
]);
46 }, ["volume", "musicVolume", "mute"]);
49 _writeStorage
: yf
.debounce(function () {
52 yf
.each
.call(this, function (prop
) {
53 this._storage
.setObject(prop
, this[prop
]);
54 }, ["volume", "musicVolume", "mute"]);
58 get: function () { return this._storage
; },
66 get: function () { return this._mute
; },
69 this.volume
= this.volume
;
74 get: function () { return this._volume
; },
77 v
= this._mute
? 0 : v
;
78 this._masterVolume
.gain
.value
= v
;
84 get: function () { return this._musicVolume
.gain
.value
; },
86 this._musicVolume
.gain
.value
= v
;
91 currentTime
: { alias
: "_ctx.currentTime" },
93 decodeAudioData: function (data
) {
96 return ctx
.decodeAudioData(data
);
98 return new Promise(function (resolve
, reject
) {
99 ctx
.decodeAudioData(data
, function (buffer
) {
102 reject(new Error("Error decoding audio buffer"));
108 createBufferSource: function (path
) {
109 var source
= this._ctx
.createBufferSource();
110 var sample
= new yuu
.AudioSample(path
, this);
111 if ((source
.buffer
= sample
.buffer
) === null) {
112 sample
.ready
.then(function () {
113 source
.buffer
= sample
.buffer
;
119 sampleRate
: { alias
: "_ctx.sampleRate" },
120 createGain
: { proxy
: "_ctx.createGain" },
121 createMediaElementSource
: { proxy
: "_ctx.createMediaElementSource" },
122 createOscillator
: { proxy
: "_ctx.createOscillator" },
125 // FIXME: This parsing is garbagey, would be better to parse when
126 // first handed a dfn and turn everything into a function.
127 function applyMod (s
, v
) {
128 if (yf
.isFunction(s
))
132 else if (s
[0] === "-" || s
[0] === "+")
134 else if (s
[0] === "x" || s
[0] === "*")
135 return v
* +s
.substring(1);
136 else if (s
[s
.length
- 1] === "%")
137 return v
* (parseFloat(s
) / 100);
142 var Envelope
= yuu
.Envelope
= yT({
143 constructor: yf
.argcd(
145 Envelope
.call(this, pairs
, 1);
147 function (pairs
, scale
) {
148 pairs
= pairs
|| { "0": 1, "100%": 1 };
149 this.ts
= Object
.keys(pairs
);
150 this.vs
= yf
.map
.call(pairs
, yf
.getter
, this.ts
);
153 var unlimited
= false;
154 yf
.each(function (t
) {
159 unlimited
= unlimited
|| (t
[t
.length
- 1] === "%");
161 this.minDuration
= a
- b
;
162 this.maxDuration
= (unlimited
|| a
=== b
)
165 var vMin
= Math
.min
.apply(Math
, this.vs
);
166 var vMax
= Math
.max
.apply(Math
, this.vs
);
167 this.constant
= vMin
=== vMax
&& this.vs
[0] * this.scale
;
171 schedule: function (param
, t0
, scale
, duration
) {
172 if (this.constant
!== false) {
173 param
.setValueAtTime(scale
* this.constant
, t0
);
175 yf
.each
.call(this, function (s
, v
) {
176 v
= v
* scale
* this.scale
;
177 var t
= t0
+ applyMod(s
, duration
);
179 param
.setValueAtTime(v
, t
);
181 param
.linearRampToValueAtTime(v
, t
);
182 }, this.ts
, this.vs
);
187 yuu
.AudioSample
= yuu
.Caching(yT({
188 constructor: function (path
, ctx
) {
189 ctx
= ctx
|| yuu
.audio
;
190 var url
= yuu
.resourcePath(path
, "sound", "wav");
192 this.ready
= yuu
.GET(url
, { responseType
: "arraybuffer" })
193 .then(ctx
.decodeAudioData
.bind(ctx
))
194 .then(yf
.setter
.bind(this, "buffer"))
197 }), function (args
) { return args
.length
<= 2 ? args
[0] : null; });
200 constructor: function (dfn
) {
201 this.envelope
= new yuu
.Envelope(dfn
.envelope
);
202 this.frequency
= dfn
.frequency
;
203 this.index
= dfn
.index
|| 1.0;
206 createModulator: function (ctx
, t0
, fundamental
, duration
) {
207 var modulator
= ctx
.createOscillator();
208 modulator
.frequency
.value
= applyMod(
209 this.frequency
, fundamental
);
211 modulator
.stop(t0
+ duration
);
212 var modulatorG
= ctx
.createGain();
213 modulator
.connect(modulatorG
);
214 this.envelope
.schedule(
215 modulatorG
.gain
, t0
, this.index
* fundamental
, duration
);
220 yuu
.Instrument
= yT({
221 constructor: function (dfn
) {
222 if (yf
.isString(dfn
)) {
223 var sampleName
= dfn
;
224 dfn
= { sample
: {} };
225 dfn
.sample
[sampleName
] = {};
227 this.envelope
= new yuu
.Envelope(dfn
.envelope
);
228 this.frequency
= dfn
.frequency
|| (dfn
.sample
? {} : { "x1": 1.0 });
229 this.modulator
= yf
.map(
230 yf
.new_(yuu
.Modulator
), yf
.arrayify(dfn
.modulator
|| []));
231 this.sample
= dfn
.sample
|| {};
232 this.ready
= yuu
.ready(
233 yf
.map(yf
.new_(yuu
.AudioSample
), Object
.keys(this.sample
)),
237 createSound: function (ctx
, t0
, fundamental
, amplitude
, duration
) {
238 // TODO: In the case of exactly one sample with a constant
239 // envelope, optimize out the extra gain node.
240 duration
= yf
.clamp(duration
|| 0,
241 this.envelope
.minDuration
,
242 this.envelope
.maxDuration
);
243 var ret
= ctx
.createGain();
246 yf
.ipairs(function (name
, params
) {
247 var buffer
= new yuu
.AudioSample(name
).buffer
;
248 if (buffer
&& !params
.loop
)
249 duration
= Math
.max(buffer
.duration
, duration
);
252 var modulators
= yf
.map(function (modulator
) {
253 return modulator
.createModulator(
254 ctx
, t0
, fundamental
, duration
);
257 yf
.ipairs
.call(this, function (name
, params
) {
258 var src
= ctx
.createBufferSource(name
);
259 src
.loop
= params
.loop
|| false;
260 src
.playbackRate
.value
= applyMod(
261 params
.playbackRate
|| 1, fundamental
|| ctx
.sampleRate
);
262 yf
.each(function (mod
) { mod
.connect(src
.playbackRate
); },
265 src
.start(t0
, params
.offset
|| 0, params
.duration
);
267 src
.start(t0
, params
.offset
|| 0);
268 src
.stop(t0
+ duration
);
272 yf
.ipairs
.call(this, function (mfreq
, mamp
) {
273 var osc
= ctx
.createOscillator();
274 osc
.frequency
.value
= applyMod(mfreq
, fundamental
);
276 osc
.stop(t0
+ duration
);
277 yf
.each(function (mod
) { mod
.connect(osc
.frequency
); },
280 var gain
= ctx
.createGain();
281 gain
.gain
.value
= mamp
;
290 this.envelope
.schedule(ret
.gain
, t0
, amplitude
, duration
);
296 return this.play(null, 0, 0, 1, 1);
298 function (ctx
, t
, freq
, amp
, duration
) {
299 ctx
= ctx
|| yuu
.audio
;
300 t
= t
|| ctx
.currentTime
;
301 var g
= this.createSound(ctx
, t
, freq
, amp
, duration
);
302 g
.connect(ctx
.destination
);
308 yuu
.Instruments
= yf
.mapValues(yf
.new_(yuu
.Instrument
), {
310 envelope
: { "0": 0, "0.016": 1, "-0.016": 1, "100%": 0 },
314 envelope
: { "0": 0, "0.016": 1, "-0.016": 1, "100%": 0 },
315 frequency
: { "x1": 0.83, "x1.5": 0.17 }
319 envelope
: { "0": 1 },
321 envelope
: { "0": 1 },
328 envelope
: { "0": 1, "2.5": 0.2, "5": 0 },
330 envelope
: { "0": 1, "2.5": 0.2, "5": 0 },
336 envelope
: { "0": 0, "0.2": 1, "0.4": 0.6, "-0.1": 0.5, "100%": 0 },
338 envelope
: { "0": 0, "0.2": 1, "0.4": 0.6,
339 "-0.1": 0.5, "100%": 0 },
346 // Tune to A440 by default, although every interface should provide
347 // some ways to work around this. This gives C4 = ~261.63 Hz.
348 // https://en.wikipedia.org/wiki/Scientific_pitch_notation
349 yuu
.C4_HZ
= 440 * Math
.pow(2, -9/12);
352 constructor: function (intervals
) {
353 this.intervals
= intervals
;
354 this.length
= this.intervals
.length
;
355 this.span
= yf
.foldl(function (a
, b
) { return a
+ b
; }, intervals
);
358 hz: function (tonic
, degree
, accidental
) {
359 accidental
= accidental
|| 0;
360 var s
= this.span
* ((degree
/ this.intervals
.length
) | 0)
362 degree
%= this.intervals
.length
;
364 degree
+= this.intervals
.length
;
368 while (degree
>= 1) {
370 s
+= this.intervals
[i
];
374 s
+= this.intervals
[i
] * degree
;
375 return tonic
* Math
.pow(2, s
/ 1200.0);
379 yuu
.Scales
= yf
.mapValues(yf
.new_(yuu
.Scale
), {
380 CHROMATIC
: yf
.repeat(100, 12),
381 MINOR
: [200, 100, 200, 200, 100, 200, 200],
382 MAJOR
: [200, 200, 100, 200, 200, 200, 100],
383 WHOLE_TONE
: [200, 200, 200, 200, 200, 200],
384 AUGMENTED
: [300, 100, 300, 100, 300, 100],
385 _17ET
: yf
.repeat(1200 / 17, 17),
386 DOUBLE_HARMONIC
: [100, 300, 100, 200, 100, 300, 100],
389 var DURATION
= { T
: 1/8, S: 1/4, I
: 1/2, Q
: 1, H
: 2, W
: 4,
390 ".": 1.5, "/": 1/3, "<": -1 };
391 var ACCIDENTAL
= { b
: -1, "#": 1, t
: 0.5, d
: -0.5 };
393 var NOTE
= /([TSIQHW][<.\/]*)?(?:([XZ]|(?:[A-G][b#dt]*[0-9]+))|([+-]?[0-9.]+)([b#dt]*))|([-+<>{]|(?:[TSIQHW][.\/]?))/g;
395 var LETTERS
= { Z
: null, X
: null };
397 yuu
.parseNote = function (note
, scale
, C4
) {
398 return (C4
|| yuu
.C4_HZ
) * Math
.pow(2, LETTERS
[note
] / 12);
401 yuu
.parseScore = function (score
, scale
, tonic
, C4
) {
404 // To play a scientific pitch note and advance the time, just
405 // use its name: G4, Cb2, A#0
407 // To adjust the length of the note, use T, S, I, Q (default),
408 // H, W for 32nd through whole. Append . to do
409 // time-and-a-half. Append / to cut into a third. Append < to
412 // To play a note on the provided scale, use a 0-based number
413 // (which can be negative). To move the current scale up or
414 // down, use + or -. For example, in C major, 0 and C4 produce
415 // the same note; after a -, 0 and C3 produce the same note.
417 // To rest, use Z or X.
419 // To play multiple notes at the same time, enclose them all with
420 // < ... >. The time will advance in accordance with the shortest
423 // To reset the time, scale offset, and duration, use a {.
424 // This can be more convenient when writing pieces with
425 // multiple parts than grouping, e.g.
426 // H < 1 8 > < 2 7 > < 3 6 > < 4 5 >
427 // is easier to understand when split into multiple lines:
431 scale
= scale
|| yuu
.Scales
.MAJOR
;
432 C4
= C4
|| yuu
.C4_HZ
;
433 tonic
= tonic
|| scale
.tonic
|| C4
;
434 if (yf
.isString(tonic
))
435 tonic
= yuu
.parseNote(tonic
, C4
);
441 var defaultDuration
= "Q";
444 function calcDuration (d
, m
) { return d
* DURATION
[m
]; }
445 function calcAccidental (d
, m
) { return d
* ACCIDENTAL
[m
]; }
447 while ((match
= NOTE
.exec(score
))) {
450 groupLength
= Infinity
;
453 t
+= groupLength
=== Infinity
? 0 : groupLength
;
457 degree
+= scale
.length
;
460 degree
-= scale
.length
;
466 defaultDuration
= "Q";
470 defaultDuration
= match
[5];
473 var letter
= match
[2];
474 var duration
= yf
.foldl(
475 calcDuration
, match
[1] || defaultDuration
, 1);
476 if (LETTERS
[letter
] !== null) {
477 var offset
= match
[3];
478 var accidental
= yf
.foldl(
479 calcAccidental
, match
[4] || "", 0) * 100;
484 ? C4
* Math
.pow(2, LETTERS
[letter
]/12.0)
485 : scale
.hz(tonic
, degree
+ (+offset
|| 0), accidental
)
488 if (groupLength
&& duration
> 0)
489 groupLength
= Math
.min(groupLength
, duration
);
495 notes
.sort(function (a
, b
) { return a
.time
- b
.time
; });
499 yf
.irange
.call(LETTERS
, function (i
) {
500 yf
.ipairs
.call(this, function (l
, o
) {
501 var b
= o
+ 12 * (i
- 4);
503 yf
.ipairs
.call(this, function (s
, m
) {
504 this[l
+ s
+ i
] = b
+ m
;
506 }, { C
: 0, D
: 2, E
: 4, F
: 5, G
: 7, A
: 9, B
: 11 });
509 yuu
.registerInitHook(function () {
510 if (!window
.AudioContext
)
511 throw new Error("Web Audio isn't supported.");
512 yuu
.audio
= new yuu
.Audio();
513 yuu
.defaultCommands
.volume
= yuu
.propcmd(
515 "get/set the current master audio volume", "0...1");
516 yuu
.defaultCommands
.musicVolume
= yuu
.propcmd(
517 yuu
.audio
, "musicVolume",
518 "get/set the current music volume", "0...1");
519 yuu
.defaultCommands
.mute
= yuu
.propcmd(
520 yuu
.audio
, "mute", "mute or unmute audio");
523 }).call(typeof exports
=== "undefined" ? this : exports
,
524 typeof exports
=== "undefined"
525 ? this.yuu
: (module
.exports
= require('./core')));