vibeaudio 0.4.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.
@@ -0,0 +1,170 @@
1
+ /**
2
+ * Chill Synthwave / Cyberpunk Procedural Synth Engine
3
+ * Analog Chorused Pads (Dmin -> Bbmaj7 -> Cmaj) + Pulsing 80s Bassline + Shimmering Arp
4
+ */
5
+
6
+ const {
7
+ SAMPLE_RATE,
8
+ noteToFreq,
9
+ analogSaw,
10
+ triangle,
11
+ makeRng,
12
+ pick,
13
+ createWavBuffer
14
+ } = require("./generator");
15
+
16
+ // Three progressions in D minor, each paired with the arpeggio figures that
17
+ // sit on top of it (index 0 for tier 2, index 1 for the busier tier 3).
18
+ const PROGRESSIONS = [
19
+ {
20
+ steps: [
21
+ { pad: ["D3", "F3", "A3", "D4"], bass: "D2" },
22
+ { pad: ["A#2", "F3", "A3", "D4"], bass: "A#1" },
23
+ { pad: ["C3", "G3", "C4", "E4"], bass: "C2" }
24
+ ],
25
+ arps: [
26
+ ["D5", "F5", "A5", "C6", "D6", "A5", "F5", "E5"],
27
+ ["D5", "A5", "F5", "C6", "D6", "E6", "A5", "F5"]
28
+ ]
29
+ },
30
+ {
31
+ steps: [
32
+ { pad: ["A2", "E3", "A3", "C4"], bass: "A1" },
33
+ { pad: ["F3", "A3", "C4", "F4"], bass: "F2" },
34
+ { pad: ["G3", "A#3", "D4", "G4"], bass: "G2" }
35
+ ],
36
+ arps: [
37
+ ["A4", "C5", "E5", "A5", "G5", "E5", "C5", "D5"],
38
+ ["A4", "E5", "C5", "A5", "G5", "D6", "E5", "C5"]
39
+ ]
40
+ },
41
+ {
42
+ steps: [
43
+ { pad: ["F3", "A3", "C4", "F4"], bass: "F2" },
44
+ { pad: ["C3", "G3", "C4", "E4"], bass: "C2" },
45
+ { pad: ["D3", "F3", "A3", "D4"], bass: "D2" }
46
+ ],
47
+ arps: [
48
+ ["F5", "A5", "C6", "A5", "D6", "C6", "A5", "G5"],
49
+ ["F5", "C6", "A5", "D6", "E6", "C6", "A5", "F5"]
50
+ ]
51
+ }
52
+ ];
53
+
54
+ function generateSynthwaveLoop(durationSec = 6.8, tier = 2, seed = 0) {
55
+ const variant = pick(makeRng(seed), PROGRESSIONS);
56
+ const progression = variant.steps;
57
+ const arps = variant.arps;
58
+ const totalSamples = Math.floor(SAMPLE_RATE * durationSec);
59
+ const left = new Float64Array(totalSamples);
60
+ const right = new Float64Array(totalSamples);
61
+
62
+ // Analog chorused pad renderer
63
+ function addPadChord(chordNotes, startSec, durSec, vel = 0.15) {
64
+ const startIdx = Math.floor(startSec * SAMPLE_RATE);
65
+ const numSamples = Math.floor(durSec * SAMPLE_RATE);
66
+
67
+ for (const note of chordNotes) {
68
+ const f = noteToFreq(note);
69
+ for (let i = 0; i < numSamples; i++) {
70
+ const idx = startIdx + i;
71
+ if (idx >= totalSamples) break;
72
+ const t = i / SAMPLE_RATE;
73
+
74
+ const attack = Math.min(1.0, t * 2.5);
75
+ const rel = t > durSec - 0.35 ? Math.min(1.0, (durSec - t) * 3.0) : 1.0;
76
+ const env = attack * rel;
77
+
78
+ // Dual detuned oscillators for lush analog chorus
79
+ const detune = 0.0035;
80
+ const oscL = analogSaw(f * (1.0 - detune) * t);
81
+ const oscR = analogSaw(f * (1.0 + detune) * t);
82
+
83
+ left[idx] += oscL * env * vel;
84
+ right[idx] += oscR * env * vel;
85
+ }
86
+ }
87
+ }
88
+
89
+ // Pulsing 8th-note synthwave bass
90
+ function addBass(bassNote, startSec, durSec, bpm = 110) {
91
+ const startIdx = Math.floor(startSec * SAMPLE_RATE);
92
+ const numSamples = Math.floor(durSec * SAMPLE_RATE);
93
+ const f = noteToFreq(bassNote);
94
+ const stepSec = 60.0 / bpm / 2.0;
95
+
96
+ for (let i = 0; i < numSamples; i++) {
97
+ const idx = startIdx + i;
98
+ if (idx >= totalSamples) break;
99
+ const t = i / SAMPLE_RATE;
100
+ const tNote = t % stepSec;
101
+ const env = Math.exp(-tNote * 8.0);
102
+ const sample = (triangle(f * t) * 0.7 + analogSaw(f * t) * 0.3) * env * 0.20;
103
+
104
+ left[idx] += sample;
105
+ right[idx] += sample;
106
+ }
107
+ }
108
+
109
+ // Shimmering 16th-note arpeggiator lead
110
+ function addArp(leadNotes, startSec, durSec) {
111
+ const stepSec = 0.136;
112
+ const startIdx = Math.floor(startSec * SAMPLE_RATE);
113
+ const numSamples = Math.floor(durSec * SAMPLE_RATE);
114
+
115
+ for (let i = 0; i < numSamples; i++) {
116
+ const idx = startIdx + i;
117
+ if (idx >= totalSamples) break;
118
+ const t = i / SAMPLE_RATE;
119
+ const noteIdx = Math.floor(t / stepSec) % leadNotes.length;
120
+ const f = noteToFreq(leadNotes[noteIdx]);
121
+ const tNote = t % stepSec;
122
+ const env = Math.exp(-tNote * 6.5);
123
+ const sample = analogSaw(f * t) * env * 0.085;
124
+
125
+ const pan = 0.5 + 0.35 * Math.sin(2 * Math.PI * 1.5 * t);
126
+ left[idx] += sample * (1.0 - pan);
127
+ right[idx] += sample * pan;
128
+ }
129
+ }
130
+
131
+ const padVel = tier === 1 ? 0.18 : 0.15;
132
+ const slots = [
133
+ { start: 0.0, dur: 3.2 },
134
+ { start: 3.2, dur: 1.8 },
135
+ { start: 5.0, dur: 1.8 }
136
+ ];
137
+
138
+ progression.forEach((step, i) => {
139
+ addPadChord(step.pad, slots[i].start, slots[i].dur, padVel);
140
+ if (tier >= 2) addBass(step.bass, slots[i].start, slots[i].dur);
141
+ });
142
+
143
+ if (tier >= 2) {
144
+ addArp(tier === 3 ? arps[1] : arps[0], 0.6, 6.0);
145
+ }
146
+
147
+ // Stereo ping-pong delay
148
+ const delayL = Math.floor(0.27 * SAMPLE_RATE);
149
+ const delayR = Math.floor(0.36 * SAMPLE_RATE);
150
+ const feedback = 0.28;
151
+
152
+ for (let i = 0; i < totalSamples; i++) {
153
+ if (i >= delayL) left[i] += right[i - delayL] * feedback;
154
+ if (i >= delayR) right[i] += left[i - delayR] * feedback;
155
+ }
156
+
157
+ // Soft boundary fade
158
+ const fadeLen = Math.floor(0.08 * SAMPLE_RATE);
159
+ for (let i = 0; i < fadeLen; i++) {
160
+ const s = i / fadeLen;
161
+ left[i] *= s;
162
+ right[i] *= s;
163
+ left[totalSamples - 1 - i] *= s;
164
+ right[totalSamples - 1 - i] *= s;
165
+ }
166
+
167
+ return createWavBuffer({ left, right, sampleRate: SAMPLE_RATE });
168
+ }
169
+
170
+ module.exports = { generateSynthwaveLoop };
@@ -0,0 +1,148 @@
1
+ /**
2
+ * Zen Ambient / Deep Meditation Procedural Synth Engine
3
+ * Singing Bowls + Celestial Morphing Pads + Zero Rhythm / Floating Harmony
4
+ */
5
+
6
+ const {
7
+ SAMPLE_RATE,
8
+ noteToFreq,
9
+ sine,
10
+ makeRng,
11
+ pick,
12
+ ornamentRng,
13
+ createWavBuffer
14
+ } = require("./generator");
15
+
16
+ // Curated pad pairs, all within D major so any choice stays consonant with
17
+ // the bowls below.
18
+ const PAD_PAIRS = [
19
+ [["D3", "A3", "F#4", "B4"], ["G2", "D3", "B3", "E4"]],
20
+ [["A2", "E3", "C#4", "F#4"], ["D3", "A3", "F#4", "B4"]],
21
+ [["B2", "F#3", "D4", "A4"], ["G2", "D3", "B3", "E4"]]
22
+ ];
23
+
24
+ // D major pentatonic - the safest set to strike a bowl on over any pad above.
25
+ const BOWL_NOTES = ["D4", "E4", "F#4", "A4", "B4", "D5"];
26
+
27
+ function generateZenLoop(durationSec = 7.2, tier = 2, seed = 0) {
28
+ const rng = makeRng(seed);
29
+ const pads = pick(rng, PAD_PAIRS);
30
+
31
+ const totalSamples = Math.floor(SAMPLE_RATE * durationSec);
32
+ const left = new Float64Array(totalSamples);
33
+ const right = new Float64Array(totalSamples);
34
+ const padVel = tier === 1 ? 0.10 : tier === 3 ? 0.14 : 0.12;
35
+
36
+ // 1. Slow Celestial Morphing Ambient Pads (D Major / Pentatonic)
37
+ const padChords = [
38
+ { notes: pads[0], start: 0.0, dur: 4.0 },
39
+ { notes: pads[1], start: 3.6, dur: 4.0 }
40
+ ];
41
+
42
+ for (const chord of padChords) {
43
+ const startIdx = Math.floor(chord.start * SAMPLE_RATE);
44
+ const numSamples = Math.floor(chord.dur * SAMPLE_RATE);
45
+
46
+ for (const note of chord.notes) {
47
+ const f = noteToFreq(note);
48
+ for (let i = 0; i < numSamples; i++) {
49
+ const idx = startIdx + i;
50
+ if (idx >= totalSamples) break;
51
+ const t = i / SAMPLE_RATE;
52
+
53
+ // Very slow ambient swell attack and release
54
+ const attack = Math.min(1.0, t * 0.9);
55
+ const rel = t > chord.dur - 1.2 ? Math.min(1.0, (chord.dur - t) / 1.2) : 1.0;
56
+ const env = attack * rel;
57
+
58
+ // Dual detuned pure sines for warm chorus drift
59
+ const sL = sine(f * 0.998 * t);
60
+ const sR = sine(f * 1.002 * t);
61
+
62
+ left[idx] += sL * env * padVel;
63
+ right[idx] += sR * env * padVel;
64
+ }
65
+ }
66
+ }
67
+
68
+ // 2. Tibetan Singing Bowl Simulation (Resonant harmonic overtones with natural acoustic beating)
69
+ function addSingingBowl(baseFreq, startSec, pan = 0.5) {
70
+ const startIdx = Math.floor(startSec * SAMPLE_RATE);
71
+ const ringSamples = Math.floor(4.5 * SAMPLE_RATE);
72
+
73
+ for (let i = 0; i < ringSamples; i++) {
74
+ const idx = startIdx + i;
75
+ if (idx >= totalSamples) break;
76
+ const t = i / SAMPLE_RATE;
77
+ const env = Math.exp(-t * 0.85);
78
+
79
+ // Tibetan bowls produce fundamental + 2.76x overtone with a 2Hz beat
80
+ const beat = 1.0 + 0.15 * Math.sin(2 * Math.PI * 2.1 * t);
81
+ const f1 = baseFreq;
82
+ const f2 = baseFreq * 2.76;
83
+ const f3 = baseFreq * 5.4;
84
+
85
+ const sample = (
86
+ sine(f1 * t) * 0.70 * beat +
87
+ sine(f2 * t) * 0.22 * Math.exp(-t * 1.5) +
88
+ sine(f3 * t) * 0.08 * Math.exp(-t * 3.5)
89
+ ) * env * 0.16;
90
+
91
+ left[idx] += sample * (1.0 - pan);
92
+ right[idx] += sample * pan;
93
+ }
94
+ }
95
+
96
+ // Drawn regardless of tier so every tier ornaments the same piece.
97
+ const orn = ornamentRng(seed);
98
+ const strikes = [0.4, 4.0, 2.2, 5.6].map((time) => ({
99
+ note: pick(orn, BOWL_NOTES),
100
+ time,
101
+ pan: 0.35 + orn() * 0.3
102
+ }));
103
+
104
+ // Tier 1 floats on sparse bowls; higher tiers fill the space
105
+ addSingingBowl(noteToFreq(strikes[0].note), strikes[0].time, strikes[0].pan);
106
+ addSingingBowl(noteToFreq(strikes[1].note), strikes[1].time, strikes[1].pan);
107
+
108
+ if (tier >= 2) {
109
+ addSingingBowl(noteToFreq(strikes[2].note), strikes[2].time, strikes[2].pan);
110
+ addSingingBowl(noteToFreq(strikes[3].note), strikes[3].time, strikes[3].pan);
111
+ }
112
+
113
+ // Tier 3: deep sustained drone underneath for long prompts
114
+ if (tier >= 3) {
115
+ const droneF = noteToFreq("D2");
116
+ for (let i = 0; i < totalSamples; i++) {
117
+ const t = i / SAMPLE_RATE;
118
+ const swell = 0.6 + 0.4 * Math.sin(2 * Math.PI * 0.07 * t);
119
+ const sample = (sine(droneF * t) * 0.8 + sine(droneF * 2.0 * t) * 0.2) * swell * 0.07;
120
+ left[i] += sample;
121
+ right[i] += sample;
122
+ }
123
+ }
124
+
125
+ // 3. Deep Stereo Spatial Reverberation
126
+ const delayL = Math.floor(0.42 * SAMPLE_RATE);
127
+ const delayR = Math.floor(0.56 * SAMPLE_RATE);
128
+ const feedback = 0.32;
129
+
130
+ for (let i = 0; i < totalSamples; i++) {
131
+ if (i >= delayL) left[i] += right[i - delayL] * feedback;
132
+ if (i >= delayR) right[i] += left[i - delayR] * feedback;
133
+ }
134
+
135
+ // Seamless crossfade boundary
136
+ const fadeLen = Math.floor(0.12 * SAMPLE_RATE);
137
+ for (let i = 0; i < fadeLen; i++) {
138
+ const s = i / fadeLen;
139
+ left[i] *= s;
140
+ right[i] *= s;
141
+ left[totalSamples - 1 - i] *= s;
142
+ right[totalSamples - 1 - i] *= s;
143
+ }
144
+
145
+ return createWavBuffer({ left, right, sampleRate: SAMPLE_RATE });
146
+ }
147
+
148
+ module.exports = { generateZenLoop };