-
Notifications
You must be signed in to change notification settings - Fork 0
Expand file tree
/
Copy pathmateria9.js
More file actions
215 lines (168 loc) · 7.81 KB
/
Copy pathmateria9.js
File metadata and controls
215 lines (168 loc) · 7.81 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
// materia9.js
// ═══════════════════════════════════════════════════════════════
// MATERIA SOLUTIONS — Acoustic Singularity (Resonant Ambient)
// Generative audio engine running at 80 BPM. Powered by a custom
// physical modeling waveguide Comb Resonator sound shader that
// resonates dry inputs into massive bowed-string drone soundscapes.
// ═══════════════════════════════════════════════════════════════
(function () {
'use strict';
// ─── HARMONIC SCALE — LYDIAN ───
const SCALE = [0, 2, 4, 6, 7, 9, 11]; // C Lydian
const ROOT = 65.41; // C2
function getScaleFreq(index, octave = 3) {
const degree = index % SCALE.length;
const octOffset = Math.floor(index / SCALE.length) + octave;
return ROOT * Math.pow(2, (SCALE[degree] + octOffset * 12) / 12);
}
const CHORD_ROOTS = [0, 2, 7, 5]; // C, D, G, F
// ─── ENGINE STATE ───
let ctx = null;
let master = null;
let shaderNode = null;
let isPlaying = false;
let bpm = 80;
let stepDuration;
let engineStartTime = 0;
let dryBus;
let sequencer = null;
const scheduleAheadTime = 0.15;
const schedulerInterval = 30; // ms
let nextStepTime = 0;
let schedulerTimerID = null;
let currentChordIdx = 0;
// ─── INIT ───
function init() {
if (ctx) return;
ctx = new (window.AudioContext || window.webkitAudioContext)();
stepDuration = 60 / bpm / 4; // 16th note
// Master Gain
master = ctx.createGain();
master.gain.setValueAtTime(0, ctx.currentTime);
master.gain.linearRampToValueAtTime(0.42, ctx.currentTime + 4);
// --- Custom Comb Resonator DSP Sound Shader Node ---
const resonator = window.MateriaDSP.createCombResonator(4096);
shaderNode = window.MateriaDSP.createAudioShaderNode(ctx, 1024, function (sampleL, sampleR, time, index) {
const elapsed = time - engineStartTime;
// Sweep the delay time (tube length) using a very slow LFO
const lfo = 0.5 + 0.5 * Math.sin(elapsed * 0.11);
// Delay time from 150 to 390 samples resonates at G2 to E3 pitch range
const delaySamples = 150 + 240 * lfo;
const feedback = 0.945 + 0.02 * Math.sin(elapsed * 0.19); // High feedback
const [resL, resR] = resonator(sampleL, sampleR, delaySamples, feedback);
// Mix dry click and resonant wet string
const dry = 0.08;
const wet = 0.92;
return [
sampleL * dry + resL * wet,
sampleR * dry + resR * wet
];
});
// Routing: Buses -> Shader Node -> Master -> Destination
shaderNode.connect(master);
master.connect(ctx.destination);
// Dry Input Bus
dryBus = ctx.createGain();
dryBus.gain.value = 0.28;
dryBus.connect(shaderNode);
engineStartTime = ctx.currentTime;
}
function pick(arr) { return arr[Math.floor(Math.random() * arr.length)]; }
// ═══════════════════════════════════════════════════════════════
// ─── SYNTHESIS MODULES ───
// ═══════════════════════════════════════════════════════════════
// ▸ DRY PLUCK TRIGGERS (Excitation source for the Resonator)
function playDryPluck(time, chordRoot, step) {
const baseFreq = ROOT * Math.pow(2, chordRoot / 12);
const scaleIdx = (step * 3) % SCALE.length;
const freq = baseFreq * Math.pow(2, (SCALE[scaleIdx] + 24) / 12); // High register
const dur = 0.06; // Dry short pluck
const osc = ctx.createOscillator();
osc.type = 'sine';
osc.frequency.setValueAtTime(freq, time);
const env = ctx.createGain();
env.gain.setValueAtTime(0, time);
env.gain.linearRampToValueAtTime(0.8, time + 0.002);
env.gain.exponentialRampToValueAtTime(0.001, time + dur);
const panner = ctx.createStereoPanner();
panner.pan.value = Math.sin(step * 0.45) * 0.8;
osc.connect(env);
env.connect(panner);
panner.connect(dryBus);
osc.start(time);
osc.stop(time + dur + 0.05);
}
// ▸ WHITE NOISE DUST (Excitation crackle simulating vinyl dust)
function playNoiseDust(time) {
const dur = 0.005 + Math.random() * 0.015; // Extremely short spikes
const osc = ctx.createOscillator();
osc.type = 'square';
osc.frequency.value = 12000; // Super high click
const env = ctx.createGain();
env.gain.setValueAtTime(0.08, time);
env.gain.exponentialRampToValueAtTime(0.001, time + dur);
const panner = ctx.createStereoPanner();
panner.pan.value = (Math.random() - 0.5) * 1.8;
osc.connect(env);
env.connect(panner);
panner.connect(dryBus);
osc.start(time);
osc.stop(time + dur + 0.05);
}
// ═══════════════════════════════════════════════════════════════
// ─── SEQUENCER LOOP ───
// ═══════════════════════════════════════════════════════════════
function* ambientSequencer() {
let step = 0;
let bar = 0;
while (true) {
if (step === 0 && bar % 4 === 0) {
currentChordIdx = (currentChordIdx + 1) % CHORD_ROOTS.length;
}
const chordRoot = CHORD_ROOTS[currentChordIdx];
// Sparse rhythmic triggers to excite the comb filters
const pluckTrigger = (step === 0 || step === 4 || step === 10 || step === 12);
// Random dust crackles to add beautiful organic noise sweeps
const dustTrigger = (Math.random() < 0.35);
yield { chordRoot, pluckTrigger, dustTrigger, step };
step = (step + 1) % 16;
if (step === 0) bar++;
}
}
function scheduleNotes() {
if (nextStepTime < ctx.currentTime - scheduleAheadTime) {
nextStepTime = ctx.currentTime + 0.05;
}
while (nextStepTime < ctx.currentTime + scheduleAheadTime) {
const state = sequencer.next().value;
const t = Math.max(nextStepTime, ctx.currentTime);
if (state.pluckTrigger) playDryPluck(t, state.chordRoot, state.step);
if (state.dustTrigger) playNoiseDust(t);
nextStepTime += stepDuration;
}
}
function schedulerLoop() {
scheduleNotes();
schedulerTimerID = setTimeout(schedulerLoop, schedulerInterval);
}
// ─── CONTROLS ───
function start() {
if (isPlaying) return;
init();
if (ctx.state === 'suspended') ctx.resume();
isPlaying = true;
engineStartTime = ctx.currentTime;
nextStepTime = ctx.currentTime;
sequencer = ambientSequencer();
schedulerLoop();
}
function stop() {
isPlaying = false;
if (schedulerTimerID) clearTimeout(schedulerTimerID);
schedulerTimerID = null;
if (master) {
master.gain.linearRampToValueAtTime(0, ctx.currentTime + 2.5);
}
}
window.MateriaMusic9 = { start, stop };
})();