littlejsengine 1.23.1 → 1.24.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +234 -0
- package/COPYRIGHT.txt +0 -27
- package/FAQ.md +4 -9
- package/README.md +3 -2
- package/REFERENCE.md +30 -17
- package/dist/littlejs.d.ts +141 -97
- package/dist/littlejs.esm.js +407 -258
- package/dist/littlejs.esm.min.js +3 -3
- package/dist/littlejs.js +406 -254
- package/dist/littlejs.min.js +2 -2
- package/dist/littlejs.release.js +401 -253
- package/package.json +2 -1
- package/plugins/pluginExport.js +0 -4
- package/plugins/render3d.js +92 -17
- package/plugins/render3dEditor.js +5 -1
- package/plugins/render3dExtras.js +166 -70
- package/plugins/render3dLevel.js +24 -4
- package/src/engine.js +1 -1
- package/src/engineAudio.js +2 -2
- package/src/engineBuild.mjs +0 -1
- package/src/engineExport.js +1 -0
- package/src/engineTileLayer.js +117 -0
- package/plugins/zzfxm.js +0 -162
package/plugins/render3dLevel.js
CHANGED
|
@@ -84,8 +84,9 @@ function level3DAddMesh(name, mesh, tileInfo, color=WHITE)
|
|
|
84
84
|
* are added before its objects are made, one the game added itself keeps its place
|
|
85
85
|
* - A level may set the scene too, in a scene block beside its objects: sky, three colors for straight up, the
|
|
86
86
|
* horizon and straight down, ambient, how much of them lights the scene, .5 when not given, sunDirection and
|
|
87
|
-
* sunColor, fog, its start and end, fogColor, the horizon color when not given, and
|
|
88
|
-
* leaves out stays as the game set it, and a level with no block changes
|
|
87
|
+
* sunColor, fog, its start and end, fogColor, the horizon color when not given, shadows, and lensFlare, the
|
|
88
|
+
* sun's lens flare; what the block leaves out stays as the game set it, and a level with no block changes
|
|
89
|
+
* nothing
|
|
89
90
|
* @param {Object} level - The level, the level editor edits this same object
|
|
90
91
|
* @return {Array<any>} - What each object's type made, a function that made nothing is left out
|
|
91
92
|
* @memberof Level3D */
|
|
@@ -310,8 +311,25 @@ function level3DSceneApply(scene)
|
|
|
310
311
|
r.fogColor = color(scene.fogColor) || r.fogColor;
|
|
311
312
|
if (typeof scene.shadows === 'boolean')
|
|
312
313
|
r.shadows = scene.shadows;
|
|
314
|
+
if (typeof scene.lensFlare === 'boolean')
|
|
315
|
+
level3DSceneFlare(scene.lensFlare);
|
|
313
316
|
}
|
|
314
317
|
|
|
318
|
+
// the sun's lens flare as a scene sets it: on makes one unless the sun has a flare already, the game's own or an
|
|
319
|
+
// earlier level's, and off takes away the one a scene made, never the game's
|
|
320
|
+
let level3DSunFlare;
|
|
321
|
+
function level3DSceneFlare(on)
|
|
322
|
+
{
|
|
323
|
+
if (!on)
|
|
324
|
+
{
|
|
325
|
+
level3DSunFlare && !level3DSunFlare.destroyed && level3DSunFlare.destroy();
|
|
326
|
+
level3DSunFlare = undefined;
|
|
327
|
+
}
|
|
328
|
+
else if (!level3DSunHasFlare())
|
|
329
|
+
level3DSunFlare = new LensFlare3D;
|
|
330
|
+
}
|
|
331
|
+
const level3DSunHasFlare = ()=> engineObjects.some((o)=> o instanceof LensFlare3D && !o.destroyed && !o.light);
|
|
332
|
+
|
|
315
333
|
// a vec3 of an array of three numbers, as the file has them, or the fallback
|
|
316
334
|
function level3DVector(value, fallback)
|
|
317
335
|
{
|
|
@@ -660,7 +678,8 @@ level3DAddType('Cylinder', function(pos, properties)
|
|
|
660
678
|
{ return level3DMakeShape(pos, properties, level3DCylinderMesh ||= buildCylinder()); },
|
|
661
679
|
{color: WHITE, tile: -1, solid: true});
|
|
662
680
|
// a light, a spotlight with a cone: cone is the angle in degrees from its forward out to the edge of its beam, the
|
|
663
|
-
// object's rotation aims it, softness is how much of the cone fades,
|
|
681
|
+
// object's rotation aims it, softness is how much of the cone fades, shadows makes it the one that casts them, and
|
|
682
|
+
// lensFlare gives it a lens flare; not flare, since a property is also set on the object, where flare is the flare
|
|
664
683
|
level3DAddType('Light', function(pos, properties)
|
|
665
684
|
{
|
|
666
685
|
const light = new Light3D(pos, properties.radius, properties.color, properties.intensity);
|
|
@@ -668,5 +687,6 @@ level3DAddType('Light', function(pos, properties)
|
|
|
668
687
|
light.coneSoftness = properties.softness;
|
|
669
688
|
if (properties.shadows && render3D)
|
|
670
689
|
render3D.shadowLight = light;
|
|
690
|
+
properties.lensFlare && light.addFlare();
|
|
671
691
|
return light;
|
|
672
|
-
}, {color: WHITE, radius: 5, intensity: 1, cone: 0, softness: .2, shadows: false});
|
|
692
|
+
}, {color: WHITE, radius: 5, intensity: 1, cone: 0, softness: .2, shadows: false, lensFlare: false});
|
package/src/engine.js
CHANGED
package/src/engineAudio.js
CHANGED
|
@@ -7,7 +7,7 @@
|
|
|
7
7
|
* - 2D spatial audio based on camera position with distance-based falloff
|
|
8
8
|
* - Sound instance management (pause, resume, stop)
|
|
9
9
|
* - Speech synthesis for text-to-speech
|
|
10
|
-
* - Music playback
|
|
10
|
+
* - Music playback from audio files, looping by default
|
|
11
11
|
* - Web Audio API integration with master gain control
|
|
12
12
|
* - Sounds and the master bus can route through effects, see the audio effects plugin
|
|
13
13
|
* @namespace Audio
|
|
@@ -887,7 +887,7 @@ function playAudioBuffer(buffer, volume=1, rate=1, pan=0, loop=false, gainNode,
|
|
|
887
887
|
}
|
|
888
888
|
|
|
889
889
|
///////////////////////////////////////////////////////////////////////////////
|
|
890
|
-
//
|
|
890
|
+
// ZzFX - Zuper Zmall Zound Zynth - v1.4.0 by Frank Force
|
|
891
891
|
|
|
892
892
|
/** Generate and play a ZzFX sound
|
|
893
893
|
*
|
package/src/engineBuild.mjs
CHANGED
|
@@ -44,7 +44,6 @@ const enginePluginFiles =
|
|
|
44
44
|
`${PLUGIN_FOLDER}/newgrounds.js`,
|
|
45
45
|
`${PLUGIN_FOLDER}/postProcess.js`,
|
|
46
46
|
`${PLUGIN_FOLDER}/lightSystem.js`,
|
|
47
|
-
`${PLUGIN_FOLDER}/zzfxm.js`,
|
|
48
47
|
`${PLUGIN_FOLDER}/audioEffects.js`,
|
|
49
48
|
`${PLUGIN_FOLDER}/uiSystem.js`,
|
|
50
49
|
`${PLUGIN_FOLDER}/box2d.js`,
|
package/src/engineExport.js
CHANGED
package/src/engineTileLayer.js
CHANGED
|
@@ -128,6 +128,10 @@ const tileLayersTiledFlips = [[0,0], [3,1], [2,1], [3,0], [0,1], [1,0], [2,0], [
|
|
|
128
128
|
* Load tile layers from exported data
|
|
129
129
|
* - Tiled maps come in as they are, flipped and turned tiles included, from one tileset image (a second tileset's
|
|
130
130
|
* tiles continue its numbering), finite maps in the CSV or array layer format; layer offsets and parallax are not read
|
|
131
|
+
* - A tileset kept in the map with a margin or a spacing, a sheet with gaps between its tiles, is read where its
|
|
132
|
+
* tiles are, whatever padding the tile info has; one in a file of its own (a tsx) is not read, pass a tile info
|
|
133
|
+
* with its padding
|
|
134
|
+
* - An LDtk level loads through tileLayersFromLDtk, which makes it a map like these
|
|
131
135
|
* - Group layers are flattened in order, each replaced by the layers inside it, so the layer indices
|
|
132
136
|
* (collisionLayer and the returned array) count that flattened list; a group's tint, opacity and
|
|
133
137
|
* visibility carry to the layers inside it
|
|
@@ -160,6 +164,18 @@ function tileLayersLoad(tileMapData, tileInfo=tileLayerDefaultTile(), renderOrde
|
|
|
160
164
|
ASSERT(tileMapData.width && tileMapData.height);
|
|
161
165
|
ASSERT(tileMapData.layers && tileMapData.layers.length);
|
|
162
166
|
|
|
167
|
+
// a sheet with a margin around its tiles or a spacing between them: the first tile is at the margin and each
|
|
168
|
+
// cell is a tile and a spacing, which is a padding of half the spacing counted from there, by the tileset's
|
|
169
|
+
// own count of columns, since the image's width does not say with a spacing that is not all around
|
|
170
|
+
const tileset = tileMapData.tilesets?.[0];
|
|
171
|
+
if (tileInfo && tileset && (tileset.margin > 0 || tileset.spacing > 0))
|
|
172
|
+
{
|
|
173
|
+
const margin = tileset.margin || 0, spacing = tileset.spacing || 0, size = tileInfo.size;
|
|
174
|
+
const width = tileInfo.textureInfo?.size.x || 0;
|
|
175
|
+
const columns = tileset.columns || max(1, floor((width - margin*2 + spacing) / (size.x + spacing)));
|
|
176
|
+
tileInfo = new TileInfo(vec2(margin), size, tileInfo.textureInfo, spacing / 2, tileInfo.bleed, columns);
|
|
177
|
+
}
|
|
178
|
+
|
|
163
179
|
// flatten group layers in order, a group's color and visibility carry to the layers inside it
|
|
164
180
|
/** @type {Array<{dataLayer: Object, color?: Color, visible?: boolean}>} */
|
|
165
181
|
const layers = [];
|
|
@@ -237,6 +253,107 @@ function tileLayersLoad(tileMapData, tileInfo=tileLayerDefaultTile(), renderOrde
|
|
|
237
253
|
function tileLayersColor(hex)
|
|
238
254
|
{ return new Color().setHex(hex.length === 9 ? '#' + hex.slice(3) + hex.slice(1, 3) : hex); }
|
|
239
255
|
|
|
256
|
+
// the Tiled property type of an LDtk field type, for the ones Tiled has a property for
|
|
257
|
+
const tileLayersLDtkTypes = {Int: 'int', Float: 'float', Bool: 'bool', String: 'string', Multilines: 'string',
|
|
258
|
+
Color: 'color', FilePath: 'file'};
|
|
259
|
+
|
|
260
|
+
/**
|
|
261
|
+
* Make a Tiled map of a level of an LDtk project, to load with tileLayersLoad and objectLayersLoad
|
|
262
|
+
* - Each Tiles, AutoLayer and IntGrid layer is a tile layer, the bottom one first as in Tiled, so the last layer
|
|
263
|
+
* of the LDtk file is layer 0; where LDtk stacks tiles in a cell, an edge over a fill, each tile over another
|
|
264
|
+
* goes in a layer of its own just above, named with (2), (3) and so on, so a layer of the first tiles has one
|
|
265
|
+
* wherever the LDtk layer has any, which makes it the one for collision
|
|
266
|
+
* - An IntGrid layer with no tiles is a hidden layer of its values, for collision: pass its index as collisionLayer
|
|
267
|
+
* - An Entities layer is an object layer: an entity's name is its type for objectLayersAddType, it is placed at
|
|
268
|
+
* its middle, and its Int, Float, Bool, String, Color and FilePath fields are its properties (an enum is a string)
|
|
269
|
+
* - The tileset is the first tile layer's, with its padding and spacing; give tileLayersLoad a tile info of its image
|
|
270
|
+
* - The level is in the project file (not saved as separate level files), its layers of one grid size; a layer of
|
|
271
|
+
* another grid size or another tileset is left out, with a warning in debug builds
|
|
272
|
+
* - The level editor edits the map this returns, and saves it as a Tiled map
|
|
273
|
+
* @param {Object} ldtk - The LDtk project, its JSON
|
|
274
|
+
* @param {number|string} [level] - Which level, by its index or its identifier
|
|
275
|
+
* @return {Object} - A Tiled map: width, height, tilewidth, tileheight, tilesets and layers
|
|
276
|
+
* @example
|
|
277
|
+
* const map = tileLayersFromLDtk(await fetchJSON('world.ldtk'), 'Level_0');
|
|
278
|
+
* const layers = tileLayersLoad(map, tile(0, 16), 0, 1); // layer 1 is solid
|
|
279
|
+
* objectLayersLoad(map);
|
|
280
|
+
* @memberof TileLayers */
|
|
281
|
+
function tileLayersFromLDtk(ldtk, level=0)
|
|
282
|
+
{
|
|
283
|
+
const levels = ldtk?.levels || [];
|
|
284
|
+
const data = typeof level === 'string' ? levels.find((l)=> l.identifier === level) : levels[level];
|
|
285
|
+
ASSERT(data, 'LDtk level not found', level);
|
|
286
|
+
ASSERT(isArray(data.layerInstances), 'LDtk level has no layers: levels saved as separate files are not read');
|
|
287
|
+
const instances = data.layerInstances;
|
|
288
|
+
const tiled = (l)=> l.__type !== 'Entities';
|
|
289
|
+
const first = instances.find((l)=> tiled(l) && l.__tilesetDefUid != undefined) || instances.find(tiled) || instances[0];
|
|
290
|
+
const grid = first?.__gridSize || 16;
|
|
291
|
+
const width = first?.__cWid || ceil(data.pxWid / grid), height = first?.__cHei || ceil(data.pxHei / grid);
|
|
292
|
+
const map = {width, height, tilewidth: grid, tileheight: grid, orientation: 'orthogonal', renderorder: 'right-down',
|
|
293
|
+
infinite: false, layers: [], nextlayerid: 1, nextobjectid: 1};
|
|
294
|
+
|
|
295
|
+
// the tileset of the first layer that has one, where its tiles are in its image
|
|
296
|
+
const tileset = (ldtk?.defs?.tilesets || []).find((t)=> t.uid === first?.__tilesetDefUid);
|
|
297
|
+
if (tileset)
|
|
298
|
+
map.tilesets = [{firstgid: 1, name: String(tileset.relPath || '').replace(/^.*[\\/]/, '').replace(/\.\w+$/, ''),
|
|
299
|
+
image: tileset.relPath, imagewidth: tileset.pxWid, imageheight: tileset.pxHei,
|
|
300
|
+
tilewidth: tileset.tileGridSize, tileheight: tileset.tileGridSize, margin: tileset.padding || 0,
|
|
301
|
+
spacing: tileset.spacing || 0, columns: tileset.__cWid, tilecount: tileset.__cWid * tileset.__cHei}];
|
|
302
|
+
|
|
303
|
+
// LDtk lists the top layer first, Tiled the bottom one
|
|
304
|
+
for (let i = instances.length; i--;)
|
|
305
|
+
{
|
|
306
|
+
const instance = instances[i], name = instance.__identifier, id = map.nextlayerid++;
|
|
307
|
+
if (!tiled(instance))
|
|
308
|
+
{
|
|
309
|
+
const objects = (instance.entityInstances || []).map((entity)=>
|
|
310
|
+
{
|
|
311
|
+
// an entity is placed by its pivot, an object here by its middle
|
|
312
|
+
const [pivotX=0, pivotY=0] = entity.__pivot || [], w = entity.width || 0, h = entity.height || 0;
|
|
313
|
+
const properties = [];
|
|
314
|
+
for (const field of entity.fieldInstances || [])
|
|
315
|
+
{
|
|
316
|
+
const type = tileLayersLDtkTypes[field.__type] || (/^(Local|Extern)Enum\./.test(field.__type) && 'string');
|
|
317
|
+
type && field.__value != undefined && properties.push({name: field.__identifier, type, value: field.__value});
|
|
318
|
+
}
|
|
319
|
+
return {id: map.nextobjectid++, name: '', type: entity.__identifier, x: entity.px[0] + (.5 - pivotX) * w,
|
|
320
|
+
y: entity.px[1] + (.5 - pivotY) * h, width: w, height: h, rotation: 0, visible: true, properties};
|
|
321
|
+
});
|
|
322
|
+
map.layers.push({id, name, type: 'objectgroup', objects, opacity: 1, visible: true, x: 0, y: 0});
|
|
323
|
+
continue;
|
|
324
|
+
}
|
|
325
|
+
const tiles = [...(instance.autoLayerTiles || []), ...(instance.gridTiles || [])];
|
|
326
|
+
const usable = instance.__gridSize === grid && instance.__cWid === width && instance.__cHei === height &&
|
|
327
|
+
(!tiles.length || instance.__tilesetDefUid === tileset?.uid);
|
|
328
|
+
if (!usable)
|
|
329
|
+
{
|
|
330
|
+
debug && console.warn(`tileLayersFromLDtk: layer ${name} has another grid or tileset, left out`);
|
|
331
|
+
continue;
|
|
332
|
+
}
|
|
333
|
+
const empty = ()=> new Array(width * height).fill(0), stacks = [empty()];
|
|
334
|
+
if (tiles.length)
|
|
335
|
+
{
|
|
336
|
+
// a tile's place in pixels, its tile in the sheet, and its flips: bit 0 across, bit 1 down, which are
|
|
337
|
+
// Tiled's two top bits; LDtk lists them in the order it draws them, so the first in a cell is the
|
|
338
|
+
// bottom one and each after it goes a layer higher
|
|
339
|
+
const depth = new Uint16Array(width * height);
|
|
340
|
+
for (const t of tiles)
|
|
341
|
+
{
|
|
342
|
+
const x = floor(t.px[0] / grid), y = floor(t.px[1] / grid), cell = x + y * width;
|
|
343
|
+
if (x >= 0 && x < width && y >= 0 && y < height)
|
|
344
|
+
(stacks[depth[cell]++] ||= empty())[cell] =
|
|
345
|
+
(t.t + 1 | (t.f & 1 ? 0x80000000 : 0) | (t.f & 2 ? 0x40000000 : 0)) >>> 0;
|
|
346
|
+
}
|
|
347
|
+
}
|
|
348
|
+
else
|
|
349
|
+
(instance.intGridCsv || []).forEach((value, k)=> k < stacks[0].length && (stacks[0][k] = value));
|
|
350
|
+
const visible = instance.__type === 'IntGrid' && !tiles.length ? false : instance.visible !== false;
|
|
351
|
+
stacks.forEach((data, k)=> map.layers.push({id: k ? map.nextlayerid++ : id, name: k ? `${name} (${k + 1})` : name,
|
|
352
|
+
type: 'tilelayer', width, height, data, opacity: instance.__opacity ?? 1, visible, x: 0, y: 0}));
|
|
353
|
+
}
|
|
354
|
+
return map;
|
|
355
|
+
}
|
|
356
|
+
|
|
240
357
|
///////////////////////////////////////////////////////////////////////////////
|
|
241
358
|
// Object layers
|
|
242
359
|
|
package/plugins/zzfxm.js
DELETED
|
@@ -1,162 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* LittleJS ZzFXM Plugin
|
|
3
|
-
* - A port of ZzFXM, the Zuper Zmall Zound Zynth music player, by Keith Clark and Frank Force, MIT licensed,
|
|
4
|
-
* https://github.com/keithclark/ZzFXM; its notice is in COPYRIGHT.txt
|
|
5
|
-
* @namespace ZzFXM
|
|
6
|
-
*/
|
|
7
|
-
|
|
8
|
-
'use strict';
|
|
9
|
-
|
|
10
|
-
/**
|
|
11
|
-
* Music Object - Stores a zzfx music track for later use
|
|
12
|
-
*
|
|
13
|
-
* <a href=https://keithclark.github.io/ZzFXM/>Create music with the ZzFXM tracker.</a>
|
|
14
|
-
* @extends Sound
|
|
15
|
-
* @memberof ZzFXM
|
|
16
|
-
* @example
|
|
17
|
-
* // create some music
|
|
18
|
-
* const music_example = new ZzFXMusic(
|
|
19
|
-
* [
|
|
20
|
-
* [ // instruments
|
|
21
|
-
* [,0,400] // simple note
|
|
22
|
-
* ],
|
|
23
|
-
* [ // patterns
|
|
24
|
-
* [ // pattern 1
|
|
25
|
-
* [ // channel 0
|
|
26
|
-
* 0, -1, // instrument 0, left speaker
|
|
27
|
-
* 1, 0, 9, 1 // channel notes
|
|
28
|
-
* ],
|
|
29
|
-
* [ // channel 1
|
|
30
|
-
* 0, 1, // instrument 0, right speaker
|
|
31
|
-
* 0, 12, 17, -1 // channel notes
|
|
32
|
-
* ]
|
|
33
|
-
* ],
|
|
34
|
-
* ],
|
|
35
|
-
* [0, 0, 0, 0], // sequence, play pattern 0 four times
|
|
36
|
-
* 90 // BPM
|
|
37
|
-
* ]);
|
|
38
|
-
*
|
|
39
|
-
* // play the music on a loop
|
|
40
|
-
* music_example.playMusic();
|
|
41
|
-
*/
|
|
42
|
-
class ZzFXMusic extends Sound
|
|
43
|
-
{
|
|
44
|
-
/** Create a music object and cache the zzfx music samples for later use
|
|
45
|
-
* @param {[Array, Array, Array, number?, ...any[]]} zzfxMusic - Array of zzfx music parameters: instruments, patterns, sequence, and an optional BPM; anything after it, like the tracker's metadata, is ignored
|
|
46
|
-
*/
|
|
47
|
-
constructor(zzfxMusic)
|
|
48
|
-
{
|
|
49
|
-
super(undefined);
|
|
50
|
-
|
|
51
|
-
if (!soundEnable || headlessMode) return;
|
|
52
|
-
const [instruments, patterns, sequence, BPM] = zzfxMusic;
|
|
53
|
-
super.sampleChannels = zzfxM(instruments, patterns, sequence, BPM); // the setter, without declaring a field that hides it in the typings
|
|
54
|
-
this.buildSampleBuffer(); // hand the samples to an audio buffer now, like a zzfx sound, so the arrays are released
|
|
55
|
-
this.loadedPercent = 1; // generated in place, so it is loaded like a zzfx sound
|
|
56
|
-
}
|
|
57
|
-
}
|
|
58
|
-
|
|
59
|
-
///////////////////////////////////////////////////////////////////////////////
|
|
60
|
-
// ZzFX Music Renderer v2.0.3 by Keith Clark and Frank Force
|
|
61
|
-
|
|
62
|
-
/** Generate samples for a ZzFM song with given parameters
|
|
63
|
-
* @param {Array} instruments - Array of ZzFX sound parameters
|
|
64
|
-
* @param {Array} patterns - Array of pattern data
|
|
65
|
-
* @param {Array} sequence - Array of pattern indexes
|
|
66
|
-
* @param {number} [BPM] - Playback speed of the song in BPM
|
|
67
|
-
* @return {Array} - Left and right channel sample data
|
|
68
|
-
* @memberof ZzFXM */
|
|
69
|
-
function zzfxM(instruments, patterns, sequence, BPM = 125)
|
|
70
|
-
{
|
|
71
|
-
let i, j, k;
|
|
72
|
-
let instrumentParameters;
|
|
73
|
-
let note;
|
|
74
|
-
let sample;
|
|
75
|
-
let patternChannel;
|
|
76
|
-
let notFirstBeat;
|
|
77
|
-
let stop;
|
|
78
|
-
let instrument;
|
|
79
|
-
let attenuation;
|
|
80
|
-
let outSampleOffset;
|
|
81
|
-
let isSequenceEnd;
|
|
82
|
-
let sampleOffset = 0;
|
|
83
|
-
let nextSampleOffset;
|
|
84
|
-
let sampleBuffer = [];
|
|
85
|
-
let leftChannelBuffer = [];
|
|
86
|
-
let rightChannelBuffer = [];
|
|
87
|
-
let channelIndex = 0;
|
|
88
|
-
let panning = 0;
|
|
89
|
-
let hasMore = 1;
|
|
90
|
-
let sampleCache = {};
|
|
91
|
-
let beatLength = audioDefaultSampleRate / BPM * 60 >> 2;
|
|
92
|
-
|
|
93
|
-
// for each channel in order until there are no more
|
|
94
|
-
for (; hasMore; channelIndex++) {
|
|
95
|
-
|
|
96
|
-
// reset current values
|
|
97
|
-
sampleBuffer = [hasMore = notFirstBeat = outSampleOffset = 0];
|
|
98
|
-
|
|
99
|
-
// for each pattern in sequence
|
|
100
|
-
sequence.forEach((patternIndex, sequenceIndex)=> {
|
|
101
|
-
// get pattern for current channel, use empty 1 note pattern if none found
|
|
102
|
-
patternChannel = patterns[patternIndex][channelIndex] || [0, 0, 0];
|
|
103
|
-
|
|
104
|
-
// check if there are more channels
|
|
105
|
-
hasMore |= patterns[patternIndex][channelIndex]&&1;
|
|
106
|
-
|
|
107
|
-
// get next offset, use the length of first channel
|
|
108
|
-
nextSampleOffset = outSampleOffset + (patterns[patternIndex][0].length - 2 - (notFirstBeat?0:1)) * beatLength;
|
|
109
|
-
// for each beat in pattern, plus one extra if end of sequence
|
|
110
|
-
isSequenceEnd = sequenceIndex === sequence.length - 1;
|
|
111
|
-
for (i = 2, k = outSampleOffset; i < patternChannel.length + isSequenceEnd; notFirstBeat = ++i) {
|
|
112
|
-
|
|
113
|
-
// <channel-note>
|
|
114
|
-
note = patternChannel[i];
|
|
115
|
-
|
|
116
|
-
// stop if end, different instrument or new note
|
|
117
|
-
stop = i === patternChannel.length + isSequenceEnd - 1 && isSequenceEnd ||
|
|
118
|
-
instrument !== (patternChannel[0] || 0) || note | 0;
|
|
119
|
-
|
|
120
|
-
// fill buffer with samples for previous beat, most cpu intensive part
|
|
121
|
-
for (j = 0; j < beatLength && notFirstBeat;
|
|
122
|
-
|
|
123
|
-
// fade off attenuation at end of beat if stopping note, prevents clicking
|
|
124
|
-
j++ > beatLength - 99 && stop && attenuation < 1? attenuation += 1 / 99 : 0
|
|
125
|
-
) {
|
|
126
|
-
// copy sample to stereo buffers with panning
|
|
127
|
-
sample = (1 - attenuation) * sampleBuffer[sampleOffset++] / 2 || 0;
|
|
128
|
-
leftChannelBuffer[k] = (leftChannelBuffer[k] || 0) - sample * panning + sample;
|
|
129
|
-
rightChannelBuffer[k] = (rightChannelBuffer[k++] || 0) + sample * panning + sample;
|
|
130
|
-
}
|
|
131
|
-
|
|
132
|
-
// set up for next note
|
|
133
|
-
if (note) {
|
|
134
|
-
// set attenuation
|
|
135
|
-
attenuation = note % 1;
|
|
136
|
-
panning = patternChannel[1] || 0;
|
|
137
|
-
if (note |= 0) {
|
|
138
|
-
// get cached sample
|
|
139
|
-
sampleBuffer = sampleCache[
|
|
140
|
-
[
|
|
141
|
-
instrument = patternChannel[sampleOffset = 0] || 0,
|
|
142
|
-
note
|
|
143
|
-
]
|
|
144
|
-
] = sampleCache[[instrument, note]] || (
|
|
145
|
-
// add sample to cache
|
|
146
|
-
instrumentParameters = [...instruments[instrument]],
|
|
147
|
-
instrumentParameters[2] = (instrumentParameters[2] || 220) * 2**(note / 12 - 1),
|
|
148
|
-
|
|
149
|
-
// allow negative values to stop notes
|
|
150
|
-
note > 0 ? zzfxG(...instrumentParameters) : []
|
|
151
|
-
);
|
|
152
|
-
}
|
|
153
|
-
}
|
|
154
|
-
}
|
|
155
|
-
|
|
156
|
-
// update the sample offset
|
|
157
|
-
outSampleOffset = nextSampleOffset;
|
|
158
|
-
});
|
|
159
|
-
}
|
|
160
|
-
|
|
161
|
-
return [leftChannelBuffer, rightChannelBuffer];
|
|
162
|
-
}
|