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 createOscillator
: { proxy
: "_ctx.createOscillator" },
124 // FIXME: This parsing is garbagey, would be better to parse when
125 // first handed a dfn and turn everything into a function.
126 function applyMod (s
, v
) {
127 if (yf
.isFunction(s
))
131 else if (s
[0] === "-" || s
[0] === "+")
133 else if (s
[0] === "x" || s
[0] === "*")
134 return v
* +s
.substring(1);
135 else if (s
[s
.length
- 1] === "%")
136 return v
* (parseFloat(s
) / 100);
141 var Envelope
= yuu
.Envelope
= yT({
142 constructor: yf
.argcd(
144 Envelope
.call(this, pairs
, 1);
146 function (pairs
, scale
) {
147 pairs
= pairs
|| { "0": 1, "100%": 1 };
148 this.ts
= Object
.keys(pairs
);
149 this.vs
= yf
.map
.call(pairs
, yf
.getter
, this.ts
);
152 var unlimited
= false;
153 yf
.each(function (t
) {
158 unlimited
= unlimited
|| (t
[t
.length
- 1] === "%");
160 this.minDuration
= a
- b
;
161 this.maxDuration
= (unlimited
|| a
=== b
)
164 var vMin
= Math
.min
.apply(Math
, this.vs
);
165 var vMax
= Math
.max
.apply(Math
, this.vs
);
166 this.constant
= vMin
=== vMax
&& this.vs
[0] * this.scale
;
170 schedule: function (param
, t0
, scale
, duration
) {
171 if (this.constant
!== false) {
172 param
.setValueAtTime(scale
* this.constant
, t0
);
174 yf
.each
.call(this, function (s
, v
) {
175 v
= v
* scale
* this.scale
;
176 var t
= t0
+ applyMod(s
, duration
);
178 param
.setValueAtTime(v
, t
);
180 param
.linearRampToValueAtTime(v
, t
);
181 }, this.ts
, this.vs
);
186 yuu
.AudioSample
= yuu
.Caching(yT({
187 constructor: function (path
, ctx
) {
188 ctx
= ctx
|| yuu
.audio
;
189 var url
= yuu
.resourcePath(path
, "sound", "wav");
191 this.ready
= yuu
.GET(url
, { responseType
: "arraybuffer" })
192 .then(ctx
.decodeAudioData
.bind(ctx
))
193 .then(yf
.setter
.bind(this, "buffer"))
196 }), function (args
) { return args
.length
<= 2 ? args
[0] : null; });
199 constructor: function (dfn
) {
200 this.envelope
= new yuu
.Envelope(dfn
.envelope
);
201 this.frequency
= dfn
.frequency
;
202 this.index
= dfn
.index
|| 1.0;
205 createModulator: function (ctx
, t0
, fundamental
, duration
) {
206 var modulator
= ctx
.createOscillator();
207 modulator
.frequency
.value
= applyMod(
208 this.frequency
, fundamental
);
210 modulator
.stop(t0
+ duration
);
211 var modulatorG
= ctx
.createGain();
212 modulator
.connect(modulatorG
);
213 this.envelope
.schedule(
214 modulatorG
.gain
, t0
, this.index
* fundamental
, duration
);
219 yuu
.Instrument
= yT({
220 constructor: function (dfn
) {
221 if (yf
.isString(dfn
)) {
222 var sampleName
= dfn
;
223 dfn
= { sample
: {} };
224 dfn
.sample
[sampleName
] = {};
226 this.envelope
= new yuu
.Envelope(dfn
.envelope
);
227 this.frequency
= dfn
.frequency
|| (dfn
.sample
? {} : { "x1": 1.0 });
228 this.modulator
= yf
.map(
229 yf
.new_(yuu
.Modulator
), yf
.arrayify(dfn
.modulator
|| []));
230 this.sample
= dfn
.sample
|| {};
231 this.ready
= yuu
.ready(
232 yf
.map(yf
.new_(yuu
.AudioSample
), Object
.keys(this.sample
)),
236 createSound: function (ctx
, t0
, fundamental
, amplitude
, duration
) {
237 // TODO: In the case of exactly one sample with a constant
238 // envelope, optimize out the extra gain node.
239 duration
= yf
.clamp(duration
|| 0,
240 this.envelope
.minDuration
,
241 this.envelope
.maxDuration
);
242 var ret
= ctx
.createGain();
245 yf
.ipairs(function (name
, params
) {
246 var buffer
= new yuu
.AudioSample(name
).buffer
;
247 if (buffer
&& !params
.loop
)
248 duration
= Math
.max(buffer
.duration
, duration
);
251 var modulators
= yf
.map(function (modulator
) {
252 return modulator
.createModulator(
253 ctx
, t0
, fundamental
, duration
);
256 yf
.ipairs
.call(this, function (name
, params
) {
257 var src
= ctx
.createBufferSource(name
);
258 src
.loop
= params
.loop
|| false;
259 src
.playbackRate
.value
= applyMod(
260 params
.playbackRate
|| 1, fundamental
|| ctx
.sampleRate
);
261 yf
.each(function (mod
) { mod
.connect(src
.playbackRate
); },
264 src
.start(t0
, params
.offset
|| 0, params
.duration
);
266 src
.start(t0
, params
.offset
|| 0);
267 src
.stop(t0
+ duration
);
271 yf
.ipairs
.call(this, function (mfreq
, mamp
) {
272 var osc
= ctx
.createOscillator();
273 osc
.frequency
.value
= applyMod(mfreq
, fundamental
);
275 osc
.stop(t0
+ duration
);
276 yf
.each(function (mod
) { mod
.connect(osc
.frequency
); },
279 var gain
= ctx
.createGain();
280 gain
.gain
.value
= mamp
;
289 this.envelope
.schedule(ret
.gain
, t0
, amplitude
, duration
);
295 return this.play(null, 0, 0, 1, 1);
297 function (ctx
, t
, freq
, amp
, duration
) {
298 ctx
= ctx
|| yuu
.audio
;
299 t
= t
|| ctx
.currentTime
;
300 var g
= this.createSound(ctx
, t
, freq
, amp
, duration
);
301 g
.connect(ctx
.destination
);
307 yuu
.Instruments
= yf
.mapValues(yf
.new_(yuu
.Instrument
), {
309 envelope
: { "0": 0, "0.016": 1, "-0.016": 1, "100%": 0 },
313 envelope
: { "0": 0, "0.016": 1, "-0.016": 1, "100%": 0 },
314 frequency
: { "x1": 0.83, "x1.5": 0.17 }
318 envelope
: { "0": 1 },
320 envelope
: { "0": 1 },
327 envelope
: { "0": 1, "2.5": 0.2, "5": 0 },
329 envelope
: { "0": 1, "2.5": 0.2, "5": 0 },
335 envelope
: { "0": 0, "0.2": 1, "0.4": 0.6, "-0.1": 0.5, "100%": 0 },
337 envelope
: { "0": 0, "0.2": 1, "0.4": 0.6,
338 "-0.1": 0.5, "100%": 0 },
345 // Tune to A440 by default, although every interface should provide
346 // some ways to work around this. This gives C4 = ~261.63 Hz.
347 // https://en.wikipedia.org/wiki/Scientific_pitch_notation
348 yuu
.C4_HZ
= 440 * Math
.pow(2, -9/12);
351 constructor: function (intervals
) {
352 this.intervals
= intervals
;
353 this.length
= this.intervals
.length
;
354 this.span
= yf
.foldl(function (a
, b
) { return a
+ b
; }, intervals
);
357 hz: function (tonic
, degree
, accidental
) {
358 accidental
= accidental
|| 0;
359 var s
= this.span
* ((degree
/ this.intervals
.length
) | 0)
361 degree
%= this.intervals
.length
;
363 degree
+= this.intervals
.length
;
367 while (degree
>= 1) {
369 s
+= this.intervals
[i
];
373 s
+= this.intervals
[i
] * degree
;
374 return tonic
* Math
.pow(2, s
/ 1200.0);
378 yuu
.Scales
= yf
.mapValues(yf
.new_(yuu
.Scale
), {
379 CHROMATIC
: yf
.repeat(100, 12),
380 MINOR
: [200, 100, 200, 200, 100, 200, 200],
381 MAJOR
: [200, 200, 100, 200, 200, 200, 100],
382 WHOLE_TONE
: [200, 200, 200, 200, 200, 200],
383 AUGMENTED
: [300, 100, 300, 100, 300, 100],
384 _17ET
: yf
.repeat(1200 / 17, 17),
385 DOUBLE_HARMONIC
: [100, 300, 100, 200, 100, 300, 100],
388 var DURATION
= { T
: 1/8, S: 1/4, I
: 1/2, Q
: 1, H
: 2, W
: 4,
389 ".": 1.5, "/": 1/3, "<": -1 };
390 var ACCIDENTAL
= { b
: -1, "#": 1, t
: 0.5, d
: -0.5 };
392 var NOTE
= /([TSIQHW][<.\/]*)?(?:([XZ]|(?:[A-G][b#dt]*[0-9]+))|([+-]?[0-9.]+)([b#dt]*))|([-+<>{]|(?:[TSIQHW][.\/]?))/g;
394 var LETTERS
= { Z
: null, X
: null };
396 yuu
.parseNote = function (note
, scale
, C4
) {
397 return (C4
|| yuu
.C4_HZ
) * Math
.pow(2, LETTERS
[note
] / 12);
400 yuu
.parseScore = function (score
, scale
, tonic
, C4
) {
403 // To play a scientific pitch note and advance the time, just
404 // use its name: G4, Cb2, A#0
406 // To adjust the length of the note, use T, S, I, Q (default),
407 // H, W for 32nd through whole. Append . to do
408 // time-and-a-half. Append / to cut into a third. Append < to
411 // To play a note on the provided scale, use a 0-based number
412 // (which can be negative). To move the current scale up or
413 // down, use + or -. For example, in C major, 0 and C4 produce
414 // the same note; after a -, 0 and C3 produce the same note.
416 // To rest, use Z or X.
418 // To play multiple notes at the same time, enclose them all with
419 // < ... >. The time will advance in accordance with the shortest
422 // To reset the time, scale offset, and duration, use a {.
423 // This can be more convenient when writing pieces with
424 // multiple parts than grouping, e.g.
425 // H < 1 8 > < 2 7 > < 3 6 > < 4 5 >
426 // is easier to understand when split into multiple lines:
430 scale
= scale
|| yuu
.Scales
.MAJOR
;
431 C4
= C4
|| yuu
.C4_HZ
;
432 tonic
= tonic
|| scale
.tonic
|| C4
;
433 if (yf
.isString(tonic
))
434 tonic
= yuu
.parseNote(tonic
, C4
);
440 var defaultDuration
= "Q";
443 function calcDuration (d
, m
) { return d
* DURATION
[m
]; }
444 function calcAccidental (d
, m
) { return d
* ACCIDENTAL
[m
]; }
446 while ((match
= NOTE
.exec(score
))) {
449 groupLength
= Infinity
;
452 t
+= groupLength
=== Infinity
? 0 : groupLength
;
456 degree
+= scale
.length
;
459 degree
-= scale
.length
;
465 defaultDuration
= "Q";
469 defaultDuration
= match
[5];
472 var letter
= match
[2];
473 var duration
= yf
.foldl(
474 calcDuration
, match
[1] || defaultDuration
, 1);
475 if (LETTERS
[letter
] !== null) {
476 var offset
= match
[3];
477 var accidental
= yf
.foldl(
478 calcAccidental
, match
[4] || "", 0) * 100;
483 ? C4
* Math
.pow(2, LETTERS
[letter
]/12.0)
484 : scale
.hz(tonic
, degree
+ (+offset
|| 0), accidental
)
487 if (groupLength
&& duration
> 0)
488 groupLength
= Math
.min(groupLength
, duration
);
494 notes
.sort(function (a
, b
) { return a
.time
- b
.time
; });
498 yf
.irange
.call(LETTERS
, function (i
) {
499 yf
.ipairs
.call(this, function (l
, o
) {
500 var b
= o
+ 12 * (i
- 4);
502 yf
.ipairs
.call(this, function (s
, m
) {
503 this[l
+ s
+ i
] = b
+ m
;
505 }, { C
: 0, D
: 2, E
: 4, F
: 5, G
: 7, A
: 9, B
: 11 });
508 yuu
.registerInitHook(function () {
509 if (!window
.AudioContext
)
510 throw new Error("Web Audio isn't supported.");
511 yuu
.audio
= new yuu
.Audio();
512 yuu
.defaultCommands
.volume
= yuu
.propcmd(
514 "get/set the current master audio volume", "0...1");
515 yuu
.defaultCommands
.musicVolume
= yuu
.propcmd(
516 yuu
.audio
, "musicVolume",
517 "get/set the current music volume", "0...1");
518 yuu
.defaultCommands
.mute
= yuu
.propcmd(
519 yuu
.audio
, "mute", "mute or unmute audio");
522 }).call(typeof exports
=== "undefined" ? this : exports
,
523 typeof exports
=== "undefined"
524 ? this.yuu
: (module
.exports
= require('./core')));