arpeggiator / MusicManager.js
stereoDrift's picture
Upload 14 files
f8b493b verified
function asyncGeneratorStep(gen, resolve, reject, _next, _throw, key, arg) {
try {
var info = gen[key](arg);
var value = info.value;
} catch (error) {
reject(error);
return;
}
if (info.done) {
resolve(value);
} else {
Promise.resolve(value).then(_next, _throw);
}
}
function _async_to_generator(fn) {
return function() {
var self = this, args = arguments;
return new Promise(function(resolve, reject) {
var gen = fn.apply(self, args);
function _next(value) {
asyncGeneratorStep(gen, resolve, reject, _next, _throw, "next", value);
}
function _throw(err) {
asyncGeneratorStep(gen, resolve, reject, _next, _throw, "throw", err);
}
_next(undefined);
});
};
}
function _class_call_check(instance, Constructor) {
if (!(instance instanceof Constructor)) {
throw new TypeError("Cannot call a class as a function");
}
}
function _defineProperties(target, props) {
for(var i = 0; i < props.length; i++){
var descriptor = props[i];
descriptor.enumerable = descriptor.enumerable || false;
descriptor.configurable = true;
if ("value" in descriptor) descriptor.writable = true;
Object.defineProperty(target, descriptor.key, descriptor);
}
}
function _create_class(Constructor, protoProps, staticProps) {
if (protoProps) _defineProperties(Constructor.prototype, protoProps);
if (staticProps) _defineProperties(Constructor, staticProps);
return Constructor;
}
function _ts_generator(thisArg, body) {
var f, y, t, g, _ = {
label: 0,
sent: function() {
if (t[0] & 1) throw t[1];
return t[1];
},
trys: [],
ops: []
};
return g = {
next: verb(0),
"throw": verb(1),
"return": verb(2)
}, typeof Symbol === "function" && (g[Symbol.iterator] = function() {
return this;
}), g;
function verb(n) {
return function(v) {
return step([
n,
v
]);
};
}
function step(op) {
if (f) throw new TypeError("Generator is already executing.");
while(_)try {
if (f = 1, y && (t = op[0] & 2 ? y["return"] : op[0] ? y["throw"] || ((t = y["return"]) && t.call(y), 0) : y.next) && !(t = t.call(y, op[1])).done) return t;
if (y = 0, t) op = [
op[0] & 2,
t.value
];
switch(op[0]){
case 0:
case 1:
t = op;
break;
case 4:
_.label++;
return {
value: op[1],
done: false
};
case 5:
_.label++;
y = op[1];
op = [
0
];
continue;
case 7:
op = _.ops.pop();
_.trys.pop();
continue;
default:
if (!(t = _.trys, t = t.length > 0 && t[t.length - 1]) && (op[0] === 6 || op[0] === 2)) {
_ = 0;
continue;
}
if (op[0] === 3 && (!t || op[1] > t[0] && op[1] < t[3])) {
_.label = op[1];
break;
}
if (op[0] === 6 && _.label < t[1]) {
_.label = t[1];
t = op;
break;
}
if (t && _.label < t[2]) {
_.label = t[2];
_.ops.push(op);
break;
}
if (t[2]) _.ops.pop();
_.trys.pop();
continue;
}
op = body.call(thisArg, _);
} catch (e) {
op = [
6,
e
];
y = 0;
} finally{
f = t = 0;
}
if (op[0] & 5) throw op[1];
return {
value: op[0] ? op[1] : void 0,
done: true
};
}
}
import * as Tone from 'https://esm.sh/tone';
// A simple manager for our Tone.js based music generation
export var MusicManager = /*#__PURE__*/ function() {
"use strict";
function MusicManager() {
_class_call_check(this, MusicManager);
this.polySynth = null;
this.reverb = null;
this.stereoDelay = null; // Add a delay effect property
this.analyser = null; // For waveform visualization
this.isStarted = false;
// Use a Map to store the active arpeggio pattern for each hand
this.activePatterns = new Map();
// Use a Map to store the current volume (velocity) for each hand's arpeggio
this.handVolumes = new Map();
this.synthPresets = [
// Preset 1: Clean Sine Wave (Default)
{
harmonicity: 4,
modulationIndex: 3,
oscillator: {
type: 'sine'
},
envelope: {
attack: 0.01,
decay: 0.2,
sustain: 0.5,
release: 1.0
},
modulation: {
type: 'sine'
},
modulationEnvelope: {
attack: 0.1,
decay: 0.01,
sustain: 1,
release: 0.5
}
},
// Preset 2: Buzzy Sawtooth
{
harmonicity: 1,
modulationIndex: 8,
oscillator: {
type: 'sawtooth'
},
// More staccato/plucky envelope
envelope: {
attack: 0.01,
decay: 0.15,
sustain: 0.05,
release: 0.2
},
modulation: {
type: 'square'
},
modulationEnvelope: {
attack: 0.05,
decay: 0.2,
sustain: 0.4,
release: 0.6
}
},
// Preset 3: Funk Electric Piano (Rhodes-like)
{
harmonicity: 2,
modulationIndex: 12,
oscillator: {
type: 'sine'
},
envelope: {
attack: 0.02,
decay: 0.3,
sustain: 0.2,
release: 0.8
},
modulation: {
type: 'sine'
},
modulationEnvelope: {
attack: 0.05,
decay: 0.2,
sustain: 0.1,
release: 0.8
},
effects: {
reverbWet: 0.3,
delayWet: 0.1 // A touch of delay
}
}
];
this.currentSynthIndex = 0;
}
_create_class(MusicManager, [
{
key: "start",
value: // Must be called after a user interaction
function start() {
var _this = this;
return _async_to_generator(function() {
return _ts_generator(this, function(_state) {
switch(_state.label){
case 0:
if (_this.isStarted) return [
2
];
return [
4,
Tone.start()
];
case 1:
_state.sent();
_this.reverb = new Tone.Reverb({
decay: 5,
preDelay: 0.0,
wet: 0.8
}).toDestination();
// Create a stereo delay and connect it to the reverb
_this.stereoDelay = new Tone.FeedbackDelay("8n", 0.5).connect(_this.reverb);
_this.stereoDelay.wet.value = 0; // Start with no delay effect
// Create an analyser for the waveform visualizer
_this.analyser = new Tone.Analyser('waveform', 1024);
// Use PolySynth to allow multiple arpeggios (one per hand) to play simultaneously.
// The synth now connects to the analyser, then to the delay, which then connects to the reverb.
_this.polySynth = new Tone.PolySynth(Tone.FMSynth, _this.synthPresets[_this.currentSynthIndex]);
_this.polySynth.connect(_this.analyser);
_this.analyser.connect(_this.stereoDelay);
// Set a low volume to avoid clipping and create a more ambient feel
_this.polySynth.volume.value = 0;
_this.isStarted = true;
// Set the master tempo to 100 BPM
Tone.Transport.bpm.value = 100;
// Start the master transport
Tone.Transport.start();
console.log("Tone.js AudioContext started and PolySynth is ready.");
return [
2
];
}
});
})();
}
},
{
// Starts an arpeggio for a specific hand
key: "startArpeggio",
value: function startArpeggio(handId, rootNote) {
var _this = this;
if (!this.polySynth || this.activePatterns.has(handId)) return;
// Create a richer arpeggio (Major 7th + Octave)
// C Minor Pentatonic scale intervals: Root, Minor Third, Perfect Fourth, Perfect Fifth, Minor Seventh
var chord = Tone.Frequency(rootNote).harmonize([
0,
3,
5,
7,
10,
12
]);
var arpeggioNotes = chord.map(function(freq) {
return Tone.Frequency(freq).toNote();
});
// Tone.Pattern cycles through an array of values
var pattern = new Tone.Pattern(function(time, note) {
// Get the latest volume (velocity) for this hand, defaulting to a soft 0.2 if not set.
var velocity = _this.handVolumes.get(handId) || 0.2;
// The time argument is the precise time the note should be played
_this.polySynth.triggerAttackRelease(note, "16n", time, velocity);
}, arpeggioNotes, "upDown");
pattern.interval = "16n"; // The time between notes in the pattern (faster)
pattern.start(0); // Start the pattern immediately
// Store the pattern and its root note so we can update/stop it later
this.activePatterns.set(handId, {
pattern: pattern,
currentRoot: rootNote
});
}
},
{
// Updates the volume (velocity) of an existing arpeggio
key: "updateArpeggioVolume",
value: function updateArpeggioVolume(handId, velocity) {
// Only update if an arpeggio is active for this hand
if (this.polySynth && this.activePatterns.has(handId)) {
// Clamp the velocity to be safe
var clampedVelocity = Math.max(0, Math.min(1, velocity));
this.handVolumes.set(handId, clampedVelocity);
// IMPORTANT FIX: Also set the synth's overall volume.
// Since we only have one arpeggio at a time, we can map this directly.
// Using logarithmic scaling for a more natural volume control.
var volumeInDb = Tone.gainToDb(clampedVelocity);
this.polySynth.volume.value = volumeInDb;
}
}
},
{
// Updates the notes in an existing arpeggio
key: "updateArpeggio",
value: function updateArpeggio(handId, newRootNote) {
var activePattern = this.activePatterns.get(handId);
if (!this.polySynth || !activePattern || activePattern.currentRoot === newRootNote) {
return; // No need to update if the note hasn't changed
}
// Create new notes for the pattern
var newChord = Tone.Frequency(newRootNote).harmonize([
0,
3,
5,
7,
10,
12
]);
activePattern.pattern.values = newChord.map(function(freq) {
return Tone.Frequency(freq).toNote();
});
activePattern.currentRoot = newRootNote;
}
},
{
// Stops and cleans up an arpeggio for a specific hand
key: "stopArpeggio",
value: function stopArpeggio(handId) {
var activePattern = this.activePatterns.get(handId);
if (activePattern) {
activePattern.pattern.stop(0); // Stop the pattern
activePattern.pattern.dispose(); // Clean up Tone.js objects
this.activePatterns.delete(handId); // Remove from our map
this.handVolumes.delete(handId); // Clean up the stored volume
// If no other hands are playing, silence the synth.
if (this.activePatterns.size === 0) {
this.polySynth.volume.value = -Infinity;
}
}
}
},
{
// Cycles to the next synth preset
key: "cycleSynth",
value: function cycleSynth() {
var _this = this;
var _newPreset_effects, _newPreset_effects1;
if (!this.polySynth) return;
// Stop all currently playing notes/arpeggios before swapping
this.activePatterns.forEach(function(value, key) {
_this.stopArpeggio(key);
});
// Dispose the old synth to free up resources
this.polySynth.dispose();
// Cycle to the next preset
this.currentSynthIndex = (this.currentSynthIndex + 1) % this.synthPresets.length;
var newPreset = this.synthPresets[this.currentSynthIndex];
// Create the new synth but don't connect it yet
this.polySynth = new Tone.PolySynth(Tone.FMSynth, newPreset);
// Re-establish the audio chain: synth -> analyser -> delay
this.polySynth.connect(this.analyser);
this.polySynth.volume.value = 0; // Reset volume
var _newPreset_effects_reverbWet;
// Adjust global effects based on the new preset's settings
// Use optional chaining `?.` to safely access `effects` property
this.reverb.wet.value = (_newPreset_effects_reverbWet = (_newPreset_effects = newPreset.effects) === null || _newPreset_effects === void 0 ? void 0 : _newPreset_effects.reverbWet) !== null && _newPreset_effects_reverbWet !== void 0 ? _newPreset_effects_reverbWet : 0.8; // Default to 0.8 if not specified
var _newPreset_effects_delayWet;
this.stereoDelay.wet.value = (_newPreset_effects_delayWet = (_newPreset_effects1 = newPreset.effects) === null || _newPreset_effects1 === void 0 ? void 0 : _newPreset_effects1.delayWet) !== null && _newPreset_effects_delayWet !== void 0 ? _newPreset_effects_delayWet : 0; // Default to 0 if not specified
console.log("Switched to synth preset: ".concat(this.currentSynthIndex));
}
},
{
// Getter for the analyser so the game can use it
key: "getAnalyser",
value: function getAnalyser() {
return this.analyser;
}
}
]);
return MusicManager;
}();