0e543a7154e7f21ce057cefb9e22086cd429fcb9
1 /* Copyright 2014 Yukkuri Games
2 Licensed under the terms of the GNU GPL v2 or later
3 @license https://www.gnu.org/licenses/gpl-2.0.html
4 @source: https://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;
34 this._volume
= this._masterVolume
.gain
.value
;
37 destination
: { alias
: "_masterVolume", readonly
: true },
38 music
: { alias
: "_musicVolume", readonly
: true },
40 _readStorage: function () {
43 yf
.each
.call(this, function (prop
) {
44 this[prop
] = this._storage
.getObject(prop
, this[prop
]);
45 }, ["volume", "musicVolume", "mute"]);
48 _writeStorage
: yf
.debounce(function () {
51 yf
.each
.call(this, function (prop
) {
52 this._storage
.setObject(prop
, this[prop
]);
53 }, ["volume", "musicVolume", "mute"]);
57 get: function () { return this._storage
; },
65 get: function () { return this._mute
; },
68 this.volume
= this.volume
;
73 get: function () { return this._volume
; },
76 v
= this._mute
? 0 : v
;
77 this._masterVolume
.gain
.value
= v
;
83 get: function () { return this._musicVolume
.gain
.value
; },
85 this._musicVolume
.gain
.value
= v
;
90 currentTime
: { alias
: "_ctx.currentTime" },
92 decodeAudioData: function (data
) {
95 return ctx
.decodeAudioData(data
);
97 return new Promise(function (resolve
, reject
) {
98 ctx
.decodeAudioData(data
, function (buffer
) {
101 reject(new Error("Error decoding audio buffer"));
107 createBufferSource: function (path
) {
108 var source
= this._ctx
.createBufferSource();
109 var sample
= yf
.isString(path
)
110 ? new yuu
.AudioSample(path
, this)
112 if ((source
.buffer
= sample
.buffer
) === null) {
113 sample
.ready
.then(function () {
114 source
.buffer
= sample
.buffer
;
120 sampleRate
: { alias
: "_ctx.sampleRate" },
121 createGain
: { proxy
: "_ctx.createGain" },
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: function (timeline
) {
144 timeline
= timeline
|| { 0: 1 };
145 this.ts
= Object
.keys(timeline
);
146 this.vs
= yf
.map
.call(timeline
, yf
.getter
, this.ts
);
147 var ts
= this.ts
.filter(isFinite
);
148 this.duration
= (Math
.max
.apply(Math
, ts
)
149 - Math
.min
.apply(Math
, ts
));
150 this.unlimited
= !this.duration
|| ts
.length
!== this.ts
.length
;
153 clampDuration: function (duration
) {
154 return this.unlimited
155 ? Math
.max(duration
, this.duration
)
159 schedule: function (param
, t0
, scale
, duration
) {
160 yf
.each(function (s
, v
) {
162 var t
= t0
+ applyMod(s
, duration
);
164 param
.setValueAtTime(v
, t
);
166 param
.linearRampToValueAtTime(v
, t
);
167 }, this.ts
, this.vs
);
171 yuu
.AudioSample
= yuu
.Caching(yT({
172 constructor: function (path
, ctx
) {
173 ctx
= ctx
|| yuu
.audio
;
174 var url
= yuu
.resourcePath(path
, "sound", "wav");
176 this.ready
= yuu
.GET(url
, { responseType
: "arraybuffer" })
177 .then(ctx
.decodeAudioData
.bind(ctx
))
178 .then(yf
.setter
.bind(this, "buffer"))
181 }), function (args
) { return args
.length
<= 2 ? args
[0] : null; });
184 constructor: function (dfn
) {
185 this.envelope
= new Envelope(dfn
.envelope
);
186 this.frequency
= dfn
.frequency
;
187 this.index
= dfn
.index
|| 1.0;
190 createModulator: function (ctx
, t0
, fundamental
, duration
) {
191 var modulator
= ctx
.createOscillator();
192 modulator
.frequency
.value
= applyMod(
193 this.frequency
, fundamental
);
195 modulator
.stop(t0
+ duration
);
196 var modulatorG
= ctx
.createGain();
197 modulator
.connect(modulatorG
);
198 this.envelope
.schedule(
199 modulatorG
.gain
, t0
, this.index
* fundamental
, duration
);
204 var BaseInstrument
= yT({
207 return this.play(null, 0, 0, 1, 1);
209 function (ctx
, t
, freq
, amp
, duration
) {
210 ctx
= ctx
|| yuu
.audio
;
211 t
= t
|| ctx
.currentTime
;
212 var g
= this.createSound(ctx
, t
, freq
, amp
, duration
);
213 g
.connect(ctx
.destination
);
219 var FastInstrument
= yT(BaseInstrument
, {
220 constructor: function (dfn
) {
221 this.sample
= new yuu
.AudioSample(dfn
);
222 this.ready
= this.sample
.ready
.then(yf
.K(this));
225 createSound: function (ctx
, t0
, fundamental
, amplitude
, duration
) {
226 var src
= ctx
.createBufferSource(this.sample
);
229 src
.stop(t0
+ duration
);
230 if (amplitude
!== 1.0) {
231 ret
= ctx
.createGain();
233 ret
.gain
.value
= amplitude
;
239 var Instrument
= yT(BaseInstrument
, {
240 constructor: function (dfn
) {
241 this.envelope
= new yuu
.Envelope(dfn
.envelope
);
242 this.frequency
= dfn
.frequency
|| (dfn
.sample
? {} : { "x1": 1.0 });
243 this.modulator
= yf
.map(
244 yf
.new_(yuu
.Modulator
), yf
.arrayify(dfn
.modulator
|| []));
245 this.sample
= dfn
.sample
|| {};
246 this.ready
= yuu
.ready(
247 yf
.map(yf
.new_(yuu
.AudioSample
), Object
.keys(this.sample
)),
251 createSound: function (ctx
, t0
, fundamental
, amplitude
, duration
) {
252 duration
= this.envelope
.clampDuration(duration
|| 0);
253 var ret
= ctx
.createGain();
256 yf
.ipairs(function (name
, params
) {
257 var buffer
= new yuu
.AudioSample(name
).buffer
;
258 if (buffer
&& !params
.loop
)
259 duration
= Math
.max(buffer
.duration
, duration
);
262 var modulators
= yf
.map(function (modulator
) {
263 return modulator
.createModulator(
264 ctx
, t0
, fundamental
, duration
);
267 yf
.ipairs(function (name
, params
) {
268 var src
= ctx
.createBufferSource(name
);
269 src
.loop
= params
.loop
|| false;
270 src
.playbackRate
.value
= applyMod(
271 params
.playbackRate
|| 1, fundamental
|| ctx
.sampleRate
);
272 yf
.each(function (mod
) { mod
.connect(src
.playbackRate
); },
275 src
.start(t0
, params
.offset
|| 0, params
.duration
);
277 src
.start(t0
, params
.offset
|| 0);
278 src
.stop(t0
+ duration
);
282 yf
.ipairs(function (mfreq
, mamp
) {
283 var osc
= ctx
.createOscillator();
284 osc
.frequency
.value
= applyMod(mfreq
, fundamental
);
286 osc
.stop(t0
+ duration
);
287 yf
.each(function (mod
) { mod
.connect(osc
.frequency
); },
290 var gain
= ctx
.createGain();
291 gain
.gain
.value
= mamp
;
300 this.envelope
.schedule(ret
.gain
, t0
, amplitude
, duration
);
305 yuu
.Instrument = function (dfn
) {
306 return yf
.isString(dfn
)
307 ? new FastInstrument(dfn
)
308 : new Instrument(dfn
);
311 yuu
.Instruments
= yf
.mapValues(yf
.new_(yuu
.Instrument
), {
313 envelope
: { "0": 0, "0.016": 1, "-0.016": 1, "100%": 0 },
317 envelope
: { "0": 0, "0.016": 1, "-0.016": 1, "100%": 0 },
318 frequency
: { "x1": 0.83, "x1.5": 0.17 }
322 envelope
: { "0": 1 },
324 envelope
: { "0": 1 },
331 envelope
: { "0": 1, "2.5": 0.2, "5": 0 },
333 envelope
: { "0": 1, "2.5": 0.2, "5": 0 },
339 envelope
: { "0": 0, "0.2": 1, "0.4": 0.6, "-0.1": 0.5, "100%": 0 },
341 envelope
: { "0": 0, "0.2": 1, "0.4": 0.6,
342 "-0.1": 0.5, "100%": 0 },
349 // Tune to A440 by default, although every interface should provide
350 // some ways to work around this. This gives C4 = ~261.63 Hz.
351 // https://en.wikipedia.org/wiki/Scientific_pitch_notation
352 yuu
.C4_HZ
= 440 * Math
.pow(2, -9/12);
355 constructor: function (intervals
) {
356 this.intervals
= intervals
;
357 this.length
= this.intervals
.length
;
358 this.span
= yf
.foldl(function (a
, b
) { return a
+ b
; }, intervals
);
361 hz: function (tonic
, degree
, accidental
) {
362 accidental
= accidental
|| 0;
363 var s
= this.span
* ((degree
/ this.intervals
.length
) | 0)
365 degree
%= this.intervals
.length
;
367 degree
+= this.intervals
.length
;
371 while (degree
>= 1) {
373 s
+= this.intervals
[i
];
377 s
+= this.intervals
[i
] * degree
;
378 return tonic
* Math
.pow(2, s
/ 1200.0);
382 yuu
.Scales
= yf
.mapValues(yf
.new_(yuu
.Scale
), {
383 CHROMATIC
: yf
.repeat(100, 12),
384 MINOR
: [200, 100, 200, 200, 100, 200, 200],
385 MAJOR
: [200, 200, 100, 200, 200, 200, 100],
386 WHOLE_TONE
: [200, 200, 200, 200, 200, 200],
387 AUGMENTED
: [300, 100, 300, 100, 300, 100],
388 _17ET
: yf
.repeat(1200 / 17, 17),
389 DOUBLE_HARMONIC
: [100, 300, 100, 200, 100, 300, 100],
392 var DURATION
= { T
: 1/8, S: 1/4, I
: 1/2, Q
: 1, H
: 2, W
: 4,
393 ".": 1.5, "/": 1/3, "<": -1 };
394 var ACCIDENTAL
= { b
: -1, "#": 1, t
: 0.5, d
: -0.5 };
396 var NOTE
= /([TSIQHW][<.\/]*)?(?:([XZ]|(?:[A-G][b#dt]*[0-9]+))|([+-]?[0-9.]+)([b#dt]*))|([-+<>{]|(?:[TSIQHW][.\/]?))/g;
398 var LETTERS
= { Z
: null, X
: null };
400 yuu
.parseNote = function (note
, scale
, C4
) {
401 return (C4
|| yuu
.C4_HZ
) * Math
.pow(2, LETTERS
[note
] / 12);
404 yuu
.parseScore = function (score
, scale
, tonic
, C4
) {
407 // To play a scientific pitch note and advance the time, just
408 // use its name: G4, Cb2, A#0
410 // To adjust the length of the note, use T, S, I, Q (default),
411 // H, W for 32nd through whole. Append . to do
412 // time-and-a-half. Append / to cut into a third. Append < to
415 // To play a note on the provided scale, use a 0-based number
416 // (which can be negative). To move the current scale up or
417 // down, use + or -. For example, in C major, 0 and C4 produce
418 // the same note; after a -, 0 and C3 produce the same note.
420 // To rest, use Z or X.
422 // To play multiple notes at the same time, enclose them all with
423 // < ... >. The time will advance in accordance with the shortest
426 // To reset the time, scale offset, and duration, use a {.
427 // This can be more convenient when writing pieces with
428 // multiple parts than grouping, e.g.
429 // H < 1 8 > < 2 7 > < 3 6 > < 4 5 >
430 // is easier to understand when split into multiple lines:
434 scale
= scale
|| yuu
.Scales
.MAJOR
;
435 C4
= C4
|| yuu
.C4_HZ
;
436 tonic
= tonic
|| scale
.tonic
|| C4
;
437 if (yf
.isString(tonic
))
438 tonic
= yuu
.parseNote(tonic
, C4
);
444 var defaultDuration
= "Q";
447 function calcDuration (d
, m
) { return d
* DURATION
[m
]; }
448 function calcAccidental (d
, m
) { return d
* ACCIDENTAL
[m
]; }
450 while ((match
= NOTE
.exec(score
))) {
453 groupLength
= Infinity
;
456 t
+= groupLength
=== Infinity
? 0 : groupLength
;
460 degree
+= scale
.length
;
463 degree
-= scale
.length
;
469 defaultDuration
= "Q";
473 defaultDuration
= match
[5];
476 var letter
= match
[2];
477 var duration
= yf
.foldl(
478 calcDuration
, match
[1] || defaultDuration
, 1);
479 if (LETTERS
[letter
] !== null) {
480 var offset
= match
[3];
481 var accidental
= yf
.foldl(
482 calcAccidental
, match
[4] || "", 0) * 100;
487 ? C4
* Math
.pow(2, LETTERS
[letter
]/12.0)
488 : scale
.hz(tonic
, degree
+ (+offset
|| 0), accidental
)
491 if (groupLength
&& duration
> 0)
492 groupLength
= Math
.min(groupLength
, duration
);
498 notes
.sort(function (a
, b
) { return a
.time
- b
.time
; });
502 yf
.irange
.call(LETTERS
, function (i
) {
503 yf
.ipairs
.call(this, function (l
, o
) {
504 var b
= o
+ 12 * (i
- 4);
506 yf
.ipairs
.call(this, function (s
, m
) {
507 this[l
+ s
+ i
] = b
+ m
;
509 }, { C
: 0, D
: 2, E
: 4, F
: 5, G
: 7, A
: 9, B
: 11 });
512 yuu
.registerInitHook(function () {
513 if (!window
.AudioContext
)
514 throw new Error("Web Audio isn't supported.");
515 yuu
.audio
= new yuu
.Audio();
516 yuu
.defaultCommands
.volume
= yuu
.propcmd(
518 "get/set the current master audio volume", "0...1");
519 yuu
.defaultCommands
.musicVolume
= yuu
.propcmd(
520 yuu
.audio
, "musicVolume",
521 "get/set the current music volume", "0...1");
522 yuu
.defaultCommands
.mute
= yuu
.propcmd(
523 yuu
.audio
, "mute", "mute or unmute audio");
526 }).call(typeof exports
=== "undefined" ? this : exports
,
527 typeof exports
=== "undefined"
528 ? this.yuu
: (module
.exports
= require('./core')));