@guinetik/gcanvas 1.0.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/.github/workflows/release.yaml +70 -0
- package/.jshintrc +4 -0
- package/.vscode/settings.json +22 -0
- package/CLAUDE.md +310 -0
- package/blackhole.jpg +0 -0
- package/demo.png +0 -0
- package/demos/CNAME +1 -0
- package/demos/animations.html +31 -0
- package/demos/basic.html +38 -0
- package/demos/baskara.html +31 -0
- package/demos/bezier.html +35 -0
- package/demos/beziersignature.html +29 -0
- package/demos/blackhole.html +28 -0
- package/demos/blob.html +35 -0
- package/demos/demos.css +289 -0
- package/demos/easing.html +28 -0
- package/demos/events.html +195 -0
- package/demos/fluent.html +647 -0
- package/demos/fractals.html +36 -0
- package/demos/genart.html +26 -0
- package/demos/gendream.html +26 -0
- package/demos/group.html +36 -0
- package/demos/home.html +587 -0
- package/demos/index.html +364 -0
- package/demos/isometric.html +34 -0
- package/demos/js/animations.js +452 -0
- package/demos/js/basic.js +204 -0
- package/demos/js/baskara.js +751 -0
- package/demos/js/bezier.js +692 -0
- package/demos/js/beziersignature.js +241 -0
- package/demos/js/blackhole/accretiondisk.obj.js +379 -0
- package/demos/js/blackhole/blackhole.obj.js +318 -0
- package/demos/js/blackhole/index.js +409 -0
- package/demos/js/blackhole/particle.js +56 -0
- package/demos/js/blackhole/starfield.obj.js +218 -0
- package/demos/js/blob.js +2263 -0
- package/demos/js/easing.js +477 -0
- package/demos/js/fluent.js +183 -0
- package/demos/js/fractals.js +931 -0
- package/demos/js/fractalworker.js +93 -0
- package/demos/js/genart.js +268 -0
- package/demos/js/gendream.js +209 -0
- package/demos/js/group.js +140 -0
- package/demos/js/info-toggle.js +25 -0
- package/demos/js/isometric.js +863 -0
- package/demos/js/kerr.js +1556 -0
- package/demos/js/lavalamp.js +590 -0
- package/demos/js/layout.js +354 -0
- package/demos/js/mondrian.js +285 -0
- package/demos/js/opacity.js +275 -0
- package/demos/js/painter.js +484 -0
- package/demos/js/particles-showcase.js +514 -0
- package/demos/js/particles.js +299 -0
- package/demos/js/patterns.js +397 -0
- package/demos/js/penrose/artifact.js +69 -0
- package/demos/js/penrose/blackhole.js +121 -0
- package/demos/js/penrose/constants.js +73 -0
- package/demos/js/penrose/game.js +943 -0
- package/demos/js/penrose/lore.js +278 -0
- package/demos/js/penrose/penrosescene.js +892 -0
- package/demos/js/penrose/ship.js +216 -0
- package/demos/js/penrose/sounds.js +211 -0
- package/demos/js/penrose/voidparticle.js +55 -0
- package/demos/js/penrose/voidscene.js +258 -0
- package/demos/js/penrose/voidship.js +144 -0
- package/demos/js/penrose/wormhole.js +46 -0
- package/demos/js/pipeline.js +555 -0
- package/demos/js/scene.js +304 -0
- package/demos/js/scenes.js +320 -0
- package/demos/js/schrodinger.js +410 -0
- package/demos/js/schwarzschild.js +1023 -0
- package/demos/js/shapes.js +628 -0
- package/demos/js/space/alien.js +171 -0
- package/demos/js/space/boom.js +98 -0
- package/demos/js/space/boss.js +353 -0
- package/demos/js/space/buff.js +73 -0
- package/demos/js/space/bullet.js +102 -0
- package/demos/js/space/constants.js +85 -0
- package/demos/js/space/game.js +1884 -0
- package/demos/js/space/hud.js +112 -0
- package/demos/js/space/laserbeam.js +179 -0
- package/demos/js/space/lightning.js +277 -0
- package/demos/js/space/minion.js +192 -0
- package/demos/js/space/missile.js +212 -0
- package/demos/js/space/player.js +430 -0
- package/demos/js/space/powerup.js +90 -0
- package/demos/js/space/starfield.js +58 -0
- package/demos/js/space/starpower.js +90 -0
- package/demos/js/spacetime.js +559 -0
- package/demos/js/svgtween.js +204 -0
- package/demos/js/tde/accretiondisk.js +418 -0
- package/demos/js/tde/blackhole.js +219 -0
- package/demos/js/tde/blackholescene.js +209 -0
- package/demos/js/tde/config.js +59 -0
- package/demos/js/tde/index.js +695 -0
- package/demos/js/tde/jets.js +290 -0
- package/demos/js/tde/lensedstarfield.js +147 -0
- package/demos/js/tde/tdestar.js +317 -0
- package/demos/js/tde/tidalstream.js +356 -0
- package/demos/js/tde_old/blackhole.obj.js +354 -0
- package/demos/js/tde_old/debris.obj.js +791 -0
- package/demos/js/tde_old/flare.obj.js +239 -0
- package/demos/js/tde_old/index.js +448 -0
- package/demos/js/tde_old/star.obj.js +812 -0
- package/demos/js/tiles.js +312 -0
- package/demos/js/tweendemo.js +79 -0
- package/demos/js/visibility.js +102 -0
- package/demos/kerr.html +28 -0
- package/demos/lavalamp.html +27 -0
- package/demos/layouts.html +37 -0
- package/demos/logo.svg +4 -0
- package/demos/loop.html +84 -0
- package/demos/mondrian.html +32 -0
- package/demos/og_image.png +0 -0
- package/demos/opacity.html +36 -0
- package/demos/painter.html +39 -0
- package/demos/particles-showcase.html +28 -0
- package/demos/particles.html +24 -0
- package/demos/patterns.html +33 -0
- package/demos/penrose-game.html +31 -0
- package/demos/pipeline.html +737 -0
- package/demos/scene.html +33 -0
- package/demos/scenes.html +96 -0
- package/demos/schrodinger.html +27 -0
- package/demos/schwarzschild.html +27 -0
- package/demos/shapes.html +16 -0
- package/demos/space.html +85 -0
- package/demos/spacetime.html +27 -0
- package/demos/svgtween.html +29 -0
- package/demos/tde.html +28 -0
- package/demos/tiles.html +28 -0
- package/demos/transforms.html +400 -0
- package/demos/tween.html +45 -0
- package/demos/visibility.html +33 -0
- package/disk_example.png +0 -0
- package/docs/README.md +222 -0
- package/docs/concepts/architecture-overview.md +204 -0
- package/docs/concepts/lifecycle.md +255 -0
- package/docs/concepts/rendering-pipeline.md +279 -0
- package/docs/concepts/tde-zorder.md +106 -0
- package/docs/concepts/two-layer-architecture.md +229 -0
- package/docs/getting-started/first-game.md +354 -0
- package/docs/getting-started/hello-world.md +269 -0
- package/docs/getting-started/installation.md +157 -0
- package/docs/modules/collision/README.md +453 -0
- package/docs/modules/fluent/README.md +1075 -0
- package/docs/modules/game/README.md +303 -0
- package/docs/modules/isometric-camera.md +210 -0
- package/docs/modules/isometric.md +275 -0
- package/docs/modules/painter/README.md +328 -0
- package/docs/modules/particle/README.md +559 -0
- package/docs/modules/shapes/README.md +221 -0
- package/docs/modules/shapes/base/euclidian.md +123 -0
- package/docs/modules/shapes/base/geometry2d.md +204 -0
- package/docs/modules/shapes/base/renderable.md +215 -0
- package/docs/modules/shapes/base/shape.md +262 -0
- package/docs/modules/shapes/base/transformable.md +243 -0
- package/docs/modules/shapes/hierarchy.md +218 -0
- package/docs/modules/state/README.md +577 -0
- package/docs/modules/util/README.md +99 -0
- package/docs/modules/util/camera3d.md +412 -0
- package/docs/modules/util/scene3d.md +395 -0
- package/index.html +17 -0
- package/jsdoc.json +50 -0
- package/package.json +55 -0
- package/readme.md +599 -0
- package/scripts/build-demo.js +69 -0
- package/scripts/bundle4llm.js +276 -0
- package/scripts/clearconsole.js +48 -0
- package/src/collision/collision-system.js +332 -0
- package/src/collision/collision.js +303 -0
- package/src/collision/index.js +10 -0
- package/src/fluent/fluent-game.js +430 -0
- package/src/fluent/fluent-go.js +1060 -0
- package/src/fluent/fluent-layer.js +152 -0
- package/src/fluent/fluent-scene.js +291 -0
- package/src/fluent/index.js +98 -0
- package/src/fluent/sketch.js +380 -0
- package/src/game/game.js +467 -0
- package/src/game/index.js +49 -0
- package/src/game/objects/go.js +220 -0
- package/src/game/objects/imagego.js +30 -0
- package/src/game/objects/index.js +54 -0
- package/src/game/objects/isometric-scene.js +260 -0
- package/src/game/objects/layoutscene.js +549 -0
- package/src/game/objects/scene.js +175 -0
- package/src/game/objects/scene3d.js +118 -0
- package/src/game/objects/text.js +221 -0
- package/src/game/objects/wrapper.js +232 -0
- package/src/game/pipeline.js +243 -0
- package/src/game/ui/button.js +396 -0
- package/src/game/ui/cursor.js +93 -0
- package/src/game/ui/fps.js +91 -0
- package/src/game/ui/index.js +5 -0
- package/src/game/ui/togglebutton.js +93 -0
- package/src/game/ui/tooltip.js +249 -0
- package/src/index.js +25 -0
- package/src/io/events.js +20 -0
- package/src/io/index.js +86 -0
- package/src/io/input.js +70 -0
- package/src/io/keys.js +152 -0
- package/src/io/mouse.js +61 -0
- package/src/io/touch.js +39 -0
- package/src/logger/debugtab.js +138 -0
- package/src/logger/index.js +3 -0
- package/src/logger/loggable.js +47 -0
- package/src/logger/logger.js +113 -0
- package/src/math/complex.js +37 -0
- package/src/math/constants.js +1 -0
- package/src/math/fractal.js +1271 -0
- package/src/math/gr.js +201 -0
- package/src/math/heat.js +202 -0
- package/src/math/index.js +12 -0
- package/src/math/noise.js +433 -0
- package/src/math/orbital.js +191 -0
- package/src/math/patterns.js +1339 -0
- package/src/math/penrose.js +259 -0
- package/src/math/quantum.js +115 -0
- package/src/math/random.js +195 -0
- package/src/math/tensor.js +1009 -0
- package/src/mixins/anchor.js +131 -0
- package/src/mixins/draggable.js +72 -0
- package/src/mixins/index.js +2 -0
- package/src/motion/bezier.js +132 -0
- package/src/motion/bounce.js +58 -0
- package/src/motion/easing.js +349 -0
- package/src/motion/float.js +130 -0
- package/src/motion/follow.js +125 -0
- package/src/motion/hop.js +52 -0
- package/src/motion/index.js +82 -0
- package/src/motion/motion.js +1124 -0
- package/src/motion/orbit.js +49 -0
- package/src/motion/oscillate.js +39 -0
- package/src/motion/parabolic.js +141 -0
- package/src/motion/patrol.js +147 -0
- package/src/motion/pendulum.js +48 -0
- package/src/motion/pulse.js +88 -0
- package/src/motion/shake.js +83 -0
- package/src/motion/spiral.js +144 -0
- package/src/motion/spring.js +150 -0
- package/src/motion/swing.js +47 -0
- package/src/motion/tween.js +92 -0
- package/src/motion/tweenetik.js +139 -0
- package/src/motion/waypoint.js +210 -0
- package/src/painter/index.js +8 -0
- package/src/painter/painter.colors.js +331 -0
- package/src/painter/painter.effects.js +230 -0
- package/src/painter/painter.img.js +229 -0
- package/src/painter/painter.js +295 -0
- package/src/painter/painter.lines.js +189 -0
- package/src/painter/painter.opacity.js +41 -0
- package/src/painter/painter.shapes.js +277 -0
- package/src/painter/painter.text.js +273 -0
- package/src/particle/emitter.js +124 -0
- package/src/particle/index.js +11 -0
- package/src/particle/particle-system.js +322 -0
- package/src/particle/particle.js +71 -0
- package/src/particle/updaters.js +170 -0
- package/src/shapes/arc.js +43 -0
- package/src/shapes/arrow.js +33 -0
- package/src/shapes/bezier.js +42 -0
- package/src/shapes/circle.js +62 -0
- package/src/shapes/clouds.js +56 -0
- package/src/shapes/cone.js +219 -0
- package/src/shapes/cross.js +70 -0
- package/src/shapes/cube.js +244 -0
- package/src/shapes/cylinder.js +254 -0
- package/src/shapes/diamond.js +48 -0
- package/src/shapes/euclidian.js +111 -0
- package/src/shapes/figure.js +115 -0
- package/src/shapes/geometry.js +220 -0
- package/src/shapes/group.js +375 -0
- package/src/shapes/heart.js +42 -0
- package/src/shapes/hexagon.js +26 -0
- package/src/shapes/image.js +192 -0
- package/src/shapes/index.js +111 -0
- package/src/shapes/line.js +29 -0
- package/src/shapes/pattern.js +90 -0
- package/src/shapes/pin.js +44 -0
- package/src/shapes/poly.js +31 -0
- package/src/shapes/prism.js +226 -0
- package/src/shapes/rect.js +35 -0
- package/src/shapes/renderable.js +333 -0
- package/src/shapes/ring.js +26 -0
- package/src/shapes/roundrect.js +95 -0
- package/src/shapes/shape.js +117 -0
- package/src/shapes/slice.js +26 -0
- package/src/shapes/sphere.js +314 -0
- package/src/shapes/sphere3d.js +537 -0
- package/src/shapes/square.js +15 -0
- package/src/shapes/star.js +99 -0
- package/src/shapes/svg.js +408 -0
- package/src/shapes/text.js +553 -0
- package/src/shapes/traceable.js +83 -0
- package/src/shapes/transform.js +357 -0
- package/src/shapes/transformable.js +172 -0
- package/src/shapes/triangle.js +26 -0
- package/src/sound/index.js +17 -0
- package/src/sound/sound.js +473 -0
- package/src/sound/synth.analyzer.js +149 -0
- package/src/sound/synth.effects.js +207 -0
- package/src/sound/synth.envelope.js +59 -0
- package/src/sound/synth.js +229 -0
- package/src/sound/synth.musical.js +160 -0
- package/src/sound/synth.noise.js +85 -0
- package/src/sound/synth.oscillators.js +293 -0
- package/src/state/index.js +10 -0
- package/src/state/state-machine.js +371 -0
- package/src/util/camera3d.js +438 -0
- package/src/util/index.js +6 -0
- package/src/util/isometric-camera.js +235 -0
- package/src/util/layout.js +317 -0
- package/src/util/position.js +147 -0
- package/src/util/tasks.js +47 -0
- package/src/util/zindex.js +287 -0
- package/src/webgl/index.js +9 -0
- package/src/webgl/shaders/sphere-shaders.js +994 -0
- package/src/webgl/webgl-renderer.js +388 -0
- package/tde.png +0 -0
- package/test/math/orbital.test.js +61 -0
- package/test/math/tensor.test.js +114 -0
- package/test/particle/emitter.test.js +204 -0
- package/test/particle/particle-system.test.js +310 -0
- package/test/particle/particle.test.js +116 -0
- package/test/particle/updaters.test.js +386 -0
- package/test/setup.js +120 -0
- package/test/shapes/euclidian.test.js +44 -0
- package/test/shapes/geometry.test.js +86 -0
- package/test/shapes/group.test.js +86 -0
- package/test/shapes/rectangle.test.js +64 -0
- package/test/shapes/transform.test.js +379 -0
- package/test/util/camera3d.test.js +428 -0
- package/test/util/scene3d.test.js +352 -0
- package/types/collision.d.ts +249 -0
- package/types/common.d.ts +155 -0
- package/types/game.d.ts +497 -0
- package/types/index.d.ts +309 -0
- package/types/io.d.ts +188 -0
- package/types/logger.d.ts +127 -0
- package/types/math.d.ts +268 -0
- package/types/mixins.d.ts +92 -0
- package/types/motion.d.ts +678 -0
- package/types/painter.d.ts +378 -0
- package/types/shapes.d.ts +864 -0
- package/types/sound.d.ts +672 -0
- package/types/state.d.ts +251 -0
- package/types/util.d.ts +253 -0
- package/vite.config.js +50 -0
- package/vitest.config.js +13 -0
|
@@ -0,0 +1,207 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* SynthEffects - Audio effects processing
|
|
3
|
+
* @module sound/synth.effects
|
|
4
|
+
*/
|
|
5
|
+
export class SynthEffects {
|
|
6
|
+
static #_ctx = null;
|
|
7
|
+
static #_output = null;
|
|
8
|
+
|
|
9
|
+
/**
|
|
10
|
+
* Initialize the effects module
|
|
11
|
+
* @param {AudioContext} ctx - Audio context
|
|
12
|
+
* @param {AudioNode} output - Output node
|
|
13
|
+
*/
|
|
14
|
+
static init(ctx, output) {
|
|
15
|
+
this.#_ctx = ctx;
|
|
16
|
+
this.#_output = output;
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
/**
|
|
20
|
+
* Get the audio context
|
|
21
|
+
* @returns {AudioContext}
|
|
22
|
+
*/
|
|
23
|
+
static get ctx() {
|
|
24
|
+
return this.#_ctx;
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
/**
|
|
28
|
+
* Create a filter node
|
|
29
|
+
* @param {string} [type='lowpass'] - Filter type
|
|
30
|
+
* @param {number} [frequency=1000] - Cutoff frequency
|
|
31
|
+
* @param {number} [q=1] - Q factor (resonance)
|
|
32
|
+
* @returns {BiquadFilterNode}
|
|
33
|
+
*/
|
|
34
|
+
static filter(type = "lowpass", frequency = 1000, q = 1) {
|
|
35
|
+
const filter = this.#_ctx.createBiquadFilter();
|
|
36
|
+
filter.type = type;
|
|
37
|
+
filter.frequency.value = frequency;
|
|
38
|
+
filter.Q.value = q;
|
|
39
|
+
return filter;
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
/**
|
|
43
|
+
* Create a delay effect
|
|
44
|
+
* @param {number} [time=0.3] - Delay time in seconds
|
|
45
|
+
* @param {number} [feedback=0.4] - Feedback amount (0-1)
|
|
46
|
+
* @param {number} [mix=0.5] - Wet/dry mix (0-1)
|
|
47
|
+
* @returns {Object} Delay effect controller
|
|
48
|
+
*/
|
|
49
|
+
static delay(time = 0.3, feedback = 0.4, mix = 0.5) {
|
|
50
|
+
const delay = this.#_ctx.createDelay(5);
|
|
51
|
+
const feedbackGain = this.#_ctx.createGain();
|
|
52
|
+
const wetGain = this.#_ctx.createGain();
|
|
53
|
+
const dryGain = this.#_ctx.createGain();
|
|
54
|
+
const input = this.#_ctx.createGain();
|
|
55
|
+
const output = this.#_ctx.createGain();
|
|
56
|
+
|
|
57
|
+
delay.delayTime.value = time;
|
|
58
|
+
feedbackGain.gain.value = feedback;
|
|
59
|
+
wetGain.gain.value = mix;
|
|
60
|
+
dryGain.gain.value = 1 - mix;
|
|
61
|
+
|
|
62
|
+
// Signal flow
|
|
63
|
+
input.connect(delay);
|
|
64
|
+
input.connect(dryGain);
|
|
65
|
+
delay.connect(feedbackGain);
|
|
66
|
+
feedbackGain.connect(delay);
|
|
67
|
+
delay.connect(wetGain);
|
|
68
|
+
wetGain.connect(output);
|
|
69
|
+
dryGain.connect(output);
|
|
70
|
+
|
|
71
|
+
return {
|
|
72
|
+
input,
|
|
73
|
+
output,
|
|
74
|
+
setTime: (t) =>
|
|
75
|
+
delay.delayTime.setValueAtTime(t, this.#_ctx.currentTime),
|
|
76
|
+
setFeedback: (f) =>
|
|
77
|
+
feedbackGain.gain.setValueAtTime(f, this.#_ctx.currentTime),
|
|
78
|
+
setMix: (m) => {
|
|
79
|
+
wetGain.gain.setValueAtTime(m, this.#_ctx.currentTime);
|
|
80
|
+
dryGain.gain.setValueAtTime(1 - m, this.#_ctx.currentTime);
|
|
81
|
+
},
|
|
82
|
+
};
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
/**
|
|
86
|
+
* Create a simple reverb using convolution
|
|
87
|
+
* @param {number} [duration=2] - Reverb duration
|
|
88
|
+
* @param {number} [decay=2] - Decay rate
|
|
89
|
+
* @returns {ConvolverNode}
|
|
90
|
+
*/
|
|
91
|
+
static reverb(duration = 2, decay = 2) {
|
|
92
|
+
const convolver = this.#_ctx.createConvolver();
|
|
93
|
+
const sampleRate = this.#_ctx.sampleRate;
|
|
94
|
+
const length = sampleRate * duration;
|
|
95
|
+
const impulse = this.#_ctx.createBuffer(2, length, sampleRate);
|
|
96
|
+
|
|
97
|
+
for (let channel = 0; channel < 2; channel++) {
|
|
98
|
+
const channelData = impulse.getChannelData(channel);
|
|
99
|
+
for (let i = 0; i < length; i++) {
|
|
100
|
+
channelData[i] =
|
|
101
|
+
(Math.random() * 2 - 1) * Math.pow(1 - i / length, decay);
|
|
102
|
+
}
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
convolver.buffer = impulse;
|
|
106
|
+
return convolver;
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
/**
|
|
110
|
+
* Create a distortion effect
|
|
111
|
+
* @param {number} [amount=50] - Distortion amount (0-100)
|
|
112
|
+
* @returns {WaveShaperNode}
|
|
113
|
+
*/
|
|
114
|
+
static distortion(amount = 50) {
|
|
115
|
+
const shaper = this.#_ctx.createWaveShaper();
|
|
116
|
+
const k = amount;
|
|
117
|
+
const samples = 44100;
|
|
118
|
+
const curve = new Float32Array(samples);
|
|
119
|
+
|
|
120
|
+
for (let i = 0; i < samples; i++) {
|
|
121
|
+
const x = (i * 2) / samples - 1;
|
|
122
|
+
curve[i] =
|
|
123
|
+
((3 + k) * x * 20 * (Math.PI / 180)) / (Math.PI + k * Math.abs(x));
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
shaper.curve = curve;
|
|
127
|
+
shaper.oversample = "4x";
|
|
128
|
+
return shaper;
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
/**
|
|
132
|
+
* Create a tremolo effect
|
|
133
|
+
* @param {number} [rate=5] - Tremolo rate in Hz
|
|
134
|
+
* @param {number} [depth=0.5] - Tremolo depth (0-1)
|
|
135
|
+
* @returns {Object} Tremolo effect controller
|
|
136
|
+
*/
|
|
137
|
+
static tremolo(rate = 5, depth = 0.5) {
|
|
138
|
+
const lfo = this.#_ctx.createOscillator();
|
|
139
|
+
const lfoGain = this.#_ctx.createGain();
|
|
140
|
+
const outputGain = this.#_ctx.createGain();
|
|
141
|
+
|
|
142
|
+
lfo.frequency.value = rate;
|
|
143
|
+
lfoGain.gain.value = depth * 0.5;
|
|
144
|
+
outputGain.gain.value = 1 - depth * 0.5;
|
|
145
|
+
|
|
146
|
+
lfo.connect(lfoGain);
|
|
147
|
+
lfoGain.connect(outputGain.gain);
|
|
148
|
+
lfo.start();
|
|
149
|
+
|
|
150
|
+
return {
|
|
151
|
+
input: outputGain,
|
|
152
|
+
output: outputGain,
|
|
153
|
+
lfo,
|
|
154
|
+
setRate: (r) =>
|
|
155
|
+
lfo.frequency.setValueAtTime(r, this.#_ctx.currentTime),
|
|
156
|
+
setDepth: (d) =>
|
|
157
|
+
lfoGain.gain.setValueAtTime(d * 0.5, this.#_ctx.currentTime),
|
|
158
|
+
stop: () => lfo.stop(),
|
|
159
|
+
};
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
/**
|
|
163
|
+
* Create a compressor
|
|
164
|
+
* @param {Object} options - Compressor options
|
|
165
|
+
* @returns {DynamicsCompressorNode}
|
|
166
|
+
*/
|
|
167
|
+
static compressor(options = {}) {
|
|
168
|
+
const {
|
|
169
|
+
threshold = -24,
|
|
170
|
+
knee = 30,
|
|
171
|
+
ratio = 12,
|
|
172
|
+
attack = 0.003,
|
|
173
|
+
release = 0.25,
|
|
174
|
+
} = options;
|
|
175
|
+
|
|
176
|
+
const compressor = this.#_ctx.createDynamicsCompressor();
|
|
177
|
+
compressor.threshold.value = threshold;
|
|
178
|
+
compressor.knee.value = knee;
|
|
179
|
+
compressor.ratio.value = ratio;
|
|
180
|
+
compressor.attack.value = attack;
|
|
181
|
+
compressor.release.value = release;
|
|
182
|
+
|
|
183
|
+
return compressor;
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
/**
|
|
187
|
+
* Create a stereo panner
|
|
188
|
+
* @param {number} [pan=0] - Pan value (-1 left, 0 center, 1 right)
|
|
189
|
+
* @returns {StereoPannerNode}
|
|
190
|
+
*/
|
|
191
|
+
static panner(pan = 0) {
|
|
192
|
+
const panner = this.#_ctx.createStereoPanner();
|
|
193
|
+
panner.pan.value = pan;
|
|
194
|
+
return panner;
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
/**
|
|
198
|
+
* Create a gain node
|
|
199
|
+
* @param {number} [volume=1] - Volume level
|
|
200
|
+
* @returns {GainNode}
|
|
201
|
+
*/
|
|
202
|
+
static gain(volume = 1) {
|
|
203
|
+
const gain = this.#_ctx.createGain();
|
|
204
|
+
gain.gain.value = volume;
|
|
205
|
+
return gain;
|
|
206
|
+
}
|
|
207
|
+
}
|
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* SynthEnvelope - ADSR envelope utilities
|
|
3
|
+
* @module sound/synth.envelope
|
|
4
|
+
*/
|
|
5
|
+
export class SynthEnvelope {
|
|
6
|
+
/**
|
|
7
|
+
* Apply ADSR envelope to an AudioParam
|
|
8
|
+
* @param {AudioParam} param - The parameter to modulate
|
|
9
|
+
* @param {Object} options - Envelope options
|
|
10
|
+
* @param {number} [options.attack=0.01] - Attack time in seconds
|
|
11
|
+
* @param {number} [options.decay=0.1] - Decay time in seconds
|
|
12
|
+
* @param {number} [options.sustain=0.7] - Sustain level (0-1)
|
|
13
|
+
* @param {number} [options.release=0.2] - Release time in seconds
|
|
14
|
+
* @param {number} [options.startTime=0] - Start time
|
|
15
|
+
* @param {number} [options.duration=1] - Total duration
|
|
16
|
+
* @param {number} [options.peakVolume=1] - Peak volume level
|
|
17
|
+
*/
|
|
18
|
+
static applyADSR(param, options = {}) {
|
|
19
|
+
const {
|
|
20
|
+
attack = 0.01,
|
|
21
|
+
decay = 0.1,
|
|
22
|
+
sustain = 0.7,
|
|
23
|
+
release = 0.2,
|
|
24
|
+
startTime = 0,
|
|
25
|
+
duration = 1,
|
|
26
|
+
peakVolume = 1,
|
|
27
|
+
} = options;
|
|
28
|
+
|
|
29
|
+
const sustainLevel = peakVolume * sustain;
|
|
30
|
+
const sustainDuration = Math.max(0, duration - attack - decay);
|
|
31
|
+
|
|
32
|
+
param.setValueAtTime(0, startTime);
|
|
33
|
+
param.linearRampToValueAtTime(peakVolume, startTime + attack);
|
|
34
|
+
param.linearRampToValueAtTime(sustainLevel, startTime + attack + decay);
|
|
35
|
+
param.setValueAtTime(
|
|
36
|
+
sustainLevel,
|
|
37
|
+
startTime + attack + decay + sustainDuration
|
|
38
|
+
);
|
|
39
|
+
param.linearRampToValueAtTime(0, startTime + duration + release);
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
/**
|
|
43
|
+
* Create envelope presets for common sounds
|
|
44
|
+
* @returns {Object} Preset envelope configurations
|
|
45
|
+
*/
|
|
46
|
+
static get presets() {
|
|
47
|
+
return {
|
|
48
|
+
pluck: { attack: 0.001, decay: 0.2, sustain: 0.0, release: 0.1 },
|
|
49
|
+
pad: { attack: 0.5, decay: 0.3, sustain: 0.8, release: 1.0 },
|
|
50
|
+
organ: { attack: 0.01, decay: 0.0, sustain: 1.0, release: 0.05 },
|
|
51
|
+
perc: { attack: 0.001, decay: 0.1, sustain: 0.0, release: 0.05 },
|
|
52
|
+
string: { attack: 0.1, decay: 0.2, sustain: 0.7, release: 0.3 },
|
|
53
|
+
brass: { attack: 0.05, decay: 0.1, sustain: 0.8, release: 0.2 },
|
|
54
|
+
blip: { attack: 0.001, decay: 0.05, sustain: 0.0, release: 0.02 },
|
|
55
|
+
laser: { attack: 0.001, decay: 0.15, sustain: 0.0, release: 0.05 },
|
|
56
|
+
explosion: { attack: 0.001, decay: 0.3, sustain: 0.2, release: 0.5 },
|
|
57
|
+
};
|
|
58
|
+
}
|
|
59
|
+
}
|
|
@@ -0,0 +1,229 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Synth - Static utility class for procedural audio generation
|
|
3
|
+
* Provides a clean API for Web Audio operations with GCanvas integration
|
|
4
|
+
*
|
|
5
|
+
* Similar to Painter for canvas, Synth abstracts Web Audio complexity
|
|
6
|
+
* @module sound/synth
|
|
7
|
+
*/
|
|
8
|
+
import { SynthOscillators } from "./synth.oscillators.js";
|
|
9
|
+
import { SynthEffects } from "./synth.effects.js";
|
|
10
|
+
import { SynthEnvelope } from "./synth.envelope.js";
|
|
11
|
+
import { SynthNoise } from "./synth.noise.js";
|
|
12
|
+
import { SynthMusical } from "./synth.musical.js";
|
|
13
|
+
import { SynthAnalyzer } from "./synth.analyzer.js";
|
|
14
|
+
|
|
15
|
+
export class Synth {
|
|
16
|
+
static #_ctx = null;
|
|
17
|
+
static #_masterGain = null;
|
|
18
|
+
static #_initialized = false;
|
|
19
|
+
|
|
20
|
+
/**
|
|
21
|
+
* Initialize the audio system
|
|
22
|
+
* @param {Object} options - Configuration options
|
|
23
|
+
* @param {number} [options.masterVolume=0.5] - Master volume (0-1)
|
|
24
|
+
* @param {number} [options.sampleRate=44100] - Sample rate
|
|
25
|
+
* @param {boolean} [options.enableAnalyzer=false] - Enable audio analyzer
|
|
26
|
+
*/
|
|
27
|
+
static init(options = {}) {
|
|
28
|
+
if (this.#_initialized) {
|
|
29
|
+
console.warn("[Synth] Already initialized");
|
|
30
|
+
return;
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
const { masterVolume = 0.5, sampleRate = 44100, enableAnalyzer = false } = options;
|
|
34
|
+
|
|
35
|
+
try {
|
|
36
|
+
this.#_ctx = new (window.AudioContext || window.webkitAudioContext)({
|
|
37
|
+
sampleRate,
|
|
38
|
+
});
|
|
39
|
+
|
|
40
|
+
this.#_masterGain = this.#_ctx.createGain();
|
|
41
|
+
this.#_masterGain.gain.value = masterVolume;
|
|
42
|
+
this.#_masterGain.connect(this.#_ctx.destination);
|
|
43
|
+
|
|
44
|
+
// Initialize sub-modules
|
|
45
|
+
SynthOscillators.init(this.#_ctx, this.#_masterGain);
|
|
46
|
+
SynthEffects.init(this.#_ctx, this.#_masterGain);
|
|
47
|
+
|
|
48
|
+
if (enableAnalyzer) {
|
|
49
|
+
SynthAnalyzer.init(this.#_ctx, this.#_masterGain);
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
this.#_initialized = true;
|
|
53
|
+
console.log("[Synth] Audio system initialized");
|
|
54
|
+
} catch (e) {
|
|
55
|
+
console.error("[Synth] Failed to initialize audio:", e);
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
/**
|
|
60
|
+
* Check if audio is initialized
|
|
61
|
+
* @returns {boolean}
|
|
62
|
+
*/
|
|
63
|
+
static get isInitialized() {
|
|
64
|
+
return this.#_initialized;
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
/**
|
|
68
|
+
* Get the AudioContext
|
|
69
|
+
* @returns {AudioContext|null}
|
|
70
|
+
*/
|
|
71
|
+
static get ctx() {
|
|
72
|
+
return this.#_ctx;
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
/**
|
|
76
|
+
* Get the master gain node
|
|
77
|
+
* @returns {GainNode|null}
|
|
78
|
+
*/
|
|
79
|
+
static get master() {
|
|
80
|
+
return this.#_masterGain;
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
/**
|
|
84
|
+
* Get oscillator utilities
|
|
85
|
+
* @returns {typeof SynthOscillators}
|
|
86
|
+
*/
|
|
87
|
+
static get osc() {
|
|
88
|
+
return SynthOscillators;
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
/**
|
|
92
|
+
* Get effects utilities
|
|
93
|
+
* @returns {typeof SynthEffects}
|
|
94
|
+
*/
|
|
95
|
+
static get fx() {
|
|
96
|
+
return SynthEffects;
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
/**
|
|
100
|
+
* Get envelope utilities
|
|
101
|
+
* @returns {typeof SynthEnvelope}
|
|
102
|
+
*/
|
|
103
|
+
static get env() {
|
|
104
|
+
return SynthEnvelope;
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
/**
|
|
108
|
+
* Get noise utilities
|
|
109
|
+
* @returns {typeof SynthNoise}
|
|
110
|
+
*/
|
|
111
|
+
static get noise() {
|
|
112
|
+
return SynthNoise;
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
/**
|
|
116
|
+
* Get musical utilities
|
|
117
|
+
* @returns {typeof SynthMusical}
|
|
118
|
+
*/
|
|
119
|
+
static get music() {
|
|
120
|
+
return SynthMusical;
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
/**
|
|
124
|
+
* Get analyzer utilities
|
|
125
|
+
* @returns {typeof SynthAnalyzer}
|
|
126
|
+
*/
|
|
127
|
+
static get analyzer() {
|
|
128
|
+
return SynthAnalyzer;
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
/**
|
|
132
|
+
* Resume audio context (required after user interaction)
|
|
133
|
+
* @returns {Promise<void>}
|
|
134
|
+
*/
|
|
135
|
+
static async resume() {
|
|
136
|
+
if (this.#_ctx && this.#_ctx.state === "suspended") {
|
|
137
|
+
await this.#_ctx.resume();
|
|
138
|
+
console.log("[Synth] Audio context resumed");
|
|
139
|
+
}
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
/**
|
|
143
|
+
* Suspend audio context
|
|
144
|
+
* @returns {Promise<void>}
|
|
145
|
+
*/
|
|
146
|
+
static async suspend() {
|
|
147
|
+
if (this.#_ctx && this.#_ctx.state === "running") {
|
|
148
|
+
await this.#_ctx.suspend();
|
|
149
|
+
}
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
/**
|
|
153
|
+
* Get current audio time (for scheduling)
|
|
154
|
+
* @returns {number}
|
|
155
|
+
*/
|
|
156
|
+
static get now() {
|
|
157
|
+
return this.#_ctx ? this.#_ctx.currentTime : 0;
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
/**
|
|
161
|
+
* Get audio context state
|
|
162
|
+
* @returns {string} 'suspended' | 'running' | 'closed'
|
|
163
|
+
*/
|
|
164
|
+
static get state() {
|
|
165
|
+
return this.#_ctx ? this.#_ctx.state : "closed";
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
/**
|
|
169
|
+
* Set master volume
|
|
170
|
+
* @param {number} value - Volume (0-1)
|
|
171
|
+
*/
|
|
172
|
+
static set volume(value) {
|
|
173
|
+
if (this.#_masterGain) {
|
|
174
|
+
this.#_masterGain.gain.setValueAtTime(
|
|
175
|
+
Math.max(0, Math.min(1, value)),
|
|
176
|
+
this.#_ctx.currentTime
|
|
177
|
+
);
|
|
178
|
+
}
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
/**
|
|
182
|
+
* Get master volume
|
|
183
|
+
* @returns {number}
|
|
184
|
+
*/
|
|
185
|
+
static get volume() {
|
|
186
|
+
return this.#_masterGain ? this.#_masterGain.gain.value : 0;
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
/**
|
|
190
|
+
* Create a custom audio node chain
|
|
191
|
+
* @param {...AudioNode} nodes - Nodes to connect in sequence
|
|
192
|
+
* @returns {Object} First and last node references
|
|
193
|
+
*/
|
|
194
|
+
static chain(...nodes) {
|
|
195
|
+
for (let i = 0; i < nodes.length - 1; i++) {
|
|
196
|
+
nodes[i].connect(nodes[i + 1]);
|
|
197
|
+
}
|
|
198
|
+
return {
|
|
199
|
+
first: nodes[0],
|
|
200
|
+
last: nodes[nodes.length - 1],
|
|
201
|
+
connectTo: (target) => nodes[nodes.length - 1].connect(target),
|
|
202
|
+
};
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
/**
|
|
206
|
+
* Schedule a function to run at a specific audio time
|
|
207
|
+
* @param {Function} fn - Function to execute
|
|
208
|
+
* @param {number} time - Audio time to execute at
|
|
209
|
+
* @returns {number} setTimeout ID for cancellation
|
|
210
|
+
*/
|
|
211
|
+
static schedule(fn, time) {
|
|
212
|
+
const delay = Math.max(0, (time - this.now) * 1000);
|
|
213
|
+
return setTimeout(fn, delay);
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
/**
|
|
217
|
+
* Close the audio context and cleanup
|
|
218
|
+
*/
|
|
219
|
+
static async close() {
|
|
220
|
+
if (this.#_ctx) {
|
|
221
|
+
SynthAnalyzer.dispose();
|
|
222
|
+
await this.#_ctx.close();
|
|
223
|
+
this.#_ctx = null;
|
|
224
|
+
this.#_masterGain = null;
|
|
225
|
+
this.#_initialized = false;
|
|
226
|
+
console.log("[Synth] Audio system closed");
|
|
227
|
+
}
|
|
228
|
+
}
|
|
229
|
+
}
|
|
@@ -0,0 +1,160 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* SynthMusical - Musical scales, notes, and chord generation
|
|
3
|
+
* @module sound/synth.musical
|
|
4
|
+
*/
|
|
5
|
+
export class SynthMusical {
|
|
6
|
+
static NOTE_FREQUENCIES = {
|
|
7
|
+
C: 16.35,
|
|
8
|
+
"C#": 17.32,
|
|
9
|
+
Db: 17.32,
|
|
10
|
+
D: 18.35,
|
|
11
|
+
"D#": 19.45,
|
|
12
|
+
Eb: 19.45,
|
|
13
|
+
E: 20.6,
|
|
14
|
+
F: 21.83,
|
|
15
|
+
"F#": 23.12,
|
|
16
|
+
Gb: 23.12,
|
|
17
|
+
G: 24.5,
|
|
18
|
+
"G#": 25.96,
|
|
19
|
+
Ab: 25.96,
|
|
20
|
+
A: 27.5,
|
|
21
|
+
"A#": 29.14,
|
|
22
|
+
Bb: 29.14,
|
|
23
|
+
B: 30.87,
|
|
24
|
+
};
|
|
25
|
+
|
|
26
|
+
static SCALES = {
|
|
27
|
+
major: [0, 2, 4, 5, 7, 9, 11],
|
|
28
|
+
minor: [0, 2, 3, 5, 7, 8, 10],
|
|
29
|
+
pentatonic: [0, 2, 4, 7, 9],
|
|
30
|
+
pentatonicMinor: [0, 3, 5, 7, 10],
|
|
31
|
+
blues: [0, 3, 5, 6, 7, 10],
|
|
32
|
+
dorian: [0, 2, 3, 5, 7, 9, 10],
|
|
33
|
+
mixolydian: [0, 2, 4, 5, 7, 9, 10],
|
|
34
|
+
chromatic: [0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11],
|
|
35
|
+
wholeTone: [0, 2, 4, 6, 8, 10],
|
|
36
|
+
diminished: [0, 2, 3, 5, 6, 8, 9, 11],
|
|
37
|
+
};
|
|
38
|
+
|
|
39
|
+
static CHORDS = {
|
|
40
|
+
major: [0, 4, 7],
|
|
41
|
+
minor: [0, 3, 7],
|
|
42
|
+
diminished: [0, 3, 6],
|
|
43
|
+
augmented: [0, 4, 8],
|
|
44
|
+
sus2: [0, 2, 7],
|
|
45
|
+
sus4: [0, 5, 7],
|
|
46
|
+
major7: [0, 4, 7, 11],
|
|
47
|
+
minor7: [0, 3, 7, 10],
|
|
48
|
+
dom7: [0, 4, 7, 10],
|
|
49
|
+
dim7: [0, 3, 6, 9],
|
|
50
|
+
add9: [0, 4, 7, 14],
|
|
51
|
+
power: [0, 7],
|
|
52
|
+
};
|
|
53
|
+
|
|
54
|
+
/**
|
|
55
|
+
* Convert note name to frequency
|
|
56
|
+
* @param {string} note - Note name (e.g., 'A4', 'C#3')
|
|
57
|
+
* @returns {number} Frequency in Hz
|
|
58
|
+
*/
|
|
59
|
+
static noteToFreq(note) {
|
|
60
|
+
const match = note.match(/^([A-G][#b]?)(\d+)$/);
|
|
61
|
+
if (!match) throw new Error(`Invalid note: ${note}`);
|
|
62
|
+
|
|
63
|
+
const [, noteName, octave] = match;
|
|
64
|
+
const baseFreq = this.NOTE_FREQUENCIES[noteName];
|
|
65
|
+
if (baseFreq === undefined) throw new Error(`Unknown note: ${noteName}`);
|
|
66
|
+
return baseFreq * Math.pow(2, parseInt(octave));
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
/**
|
|
70
|
+
* Get frequencies for a scale
|
|
71
|
+
* @param {string} root - Root note (e.g., 'C4')
|
|
72
|
+
* @param {string} [scaleName='major'] - Scale name
|
|
73
|
+
* @param {number} [octaves=1] - Number of octaves
|
|
74
|
+
* @returns {number[]} Array of frequencies
|
|
75
|
+
*/
|
|
76
|
+
static scale(root, scaleName = "major", octaves = 1) {
|
|
77
|
+
const rootFreq = this.noteToFreq(root);
|
|
78
|
+
const intervals = this.SCALES[scaleName];
|
|
79
|
+
if (!intervals) throw new Error(`Unknown scale: ${scaleName}`);
|
|
80
|
+
|
|
81
|
+
const frequencies = [];
|
|
82
|
+
|
|
83
|
+
for (let oct = 0; oct < octaves; oct++) {
|
|
84
|
+
for (const interval of intervals) {
|
|
85
|
+
frequencies.push(rootFreq * Math.pow(2, (interval + oct * 12) / 12));
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
return frequencies;
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
/**
|
|
93
|
+
* Get frequencies for a chord
|
|
94
|
+
* @param {string} root - Root note
|
|
95
|
+
* @param {string} [chordType='major'] - Chord type
|
|
96
|
+
* @returns {number[]} Array of frequencies
|
|
97
|
+
*/
|
|
98
|
+
static chord(root, chordType = "major") {
|
|
99
|
+
const rootFreq = this.noteToFreq(root);
|
|
100
|
+
const intervals = this.CHORDS[chordType];
|
|
101
|
+
if (!intervals) throw new Error(`Unknown chord type: ${chordType}`);
|
|
102
|
+
return intervals.map((i) => rootFreq * Math.pow(2, i / 12));
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
/**
|
|
106
|
+
* Map a value (0-1) to a frequency in a scale
|
|
107
|
+
* @param {number} value - Value between 0 and 1
|
|
108
|
+
* @param {string} [root='C4'] - Root note
|
|
109
|
+
* @param {string} [scaleName='pentatonic'] - Scale name
|
|
110
|
+
* @param {number} [octaves=2] - Number of octaves
|
|
111
|
+
* @returns {number} Frequency in Hz
|
|
112
|
+
*/
|
|
113
|
+
static mapToScale(value, root = "C4", scaleName = "pentatonic", octaves = 2) {
|
|
114
|
+
const frequencies = this.scale(root, scaleName, octaves);
|
|
115
|
+
const clampedValue = Math.max(0, Math.min(1, value));
|
|
116
|
+
const index =
|
|
117
|
+
Math.floor(clampedValue * frequencies.length) % frequencies.length;
|
|
118
|
+
return frequencies[index];
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
/**
|
|
122
|
+
* Convert MIDI note number to frequency
|
|
123
|
+
* @param {number} midi - MIDI note number (0-127)
|
|
124
|
+
* @returns {number} Frequency in Hz
|
|
125
|
+
*/
|
|
126
|
+
static midiToFreq(midi) {
|
|
127
|
+
return 440 * Math.pow(2, (midi - 69) / 12);
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
/**
|
|
131
|
+
* Convert frequency to MIDI note number
|
|
132
|
+
* @param {number} freq - Frequency in Hz
|
|
133
|
+
* @returns {number} MIDI note number
|
|
134
|
+
*/
|
|
135
|
+
static freqToMidi(freq) {
|
|
136
|
+
return Math.round(12 * Math.log2(freq / 440) + 69);
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
/**
|
|
140
|
+
* Get a random note from a scale
|
|
141
|
+
* @param {string} [root='C4'] - Root note
|
|
142
|
+
* @param {string} [scaleName='pentatonic'] - Scale name
|
|
143
|
+
* @param {number} [octaves=2] - Number of octaves
|
|
144
|
+
* @returns {number} Frequency in Hz
|
|
145
|
+
*/
|
|
146
|
+
static randomNote(root = "C4", scaleName = "pentatonic", octaves = 2) {
|
|
147
|
+
const frequencies = this.scale(root, scaleName, octaves);
|
|
148
|
+
return frequencies[Math.floor(Math.random() * frequencies.length)];
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
/**
|
|
152
|
+
* Get frequency with cents offset
|
|
153
|
+
* @param {number} freq - Base frequency
|
|
154
|
+
* @param {number} cents - Cents offset (-100 to 100 typical)
|
|
155
|
+
* @returns {number} Adjusted frequency
|
|
156
|
+
*/
|
|
157
|
+
static detune(freq, cents) {
|
|
158
|
+
return freq * Math.pow(2, cents / 1200);
|
|
159
|
+
}
|
|
160
|
+
}
|