You can not select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
715 lines
22 KiB
715 lines
22 KiB
// Music synthesis from Conway's Game of Life
|
|
// Base class and multiple synthesis approaches
|
|
|
|
// Create global instances
|
|
let lifeMusic;
|
|
//let globalAudioContext;
|
|
|
|
/**
|
|
* Base class for all music synthesis approaches
|
|
*/
|
|
class MusicSynthesizer {
|
|
constructor() {
|
|
this.audioContext = new (window.AudioContext || window.webkitAudioContext)();;
|
|
this.isPlaying = false;
|
|
this.masterGain = this.audioContext.createGain();
|
|
this.masterGain.connect(this.audioContext.destination);
|
|
//this.masterGain.gain.value = 0.3; // Prevent clipping
|
|
|
|
this.baseFrequency = 100; // Hz
|
|
|
|
this.initialized = false; // track whether init was run
|
|
|
|
// recording tap (safe even if not recording)
|
|
if (recordingInputNode) {
|
|
const recorderSend = this.audioContext.createGain();
|
|
recorderSend.gain.value = 1;
|
|
|
|
this.masterGain.connect(recorderSend);
|
|
recorderSend.connect(recordingInputNode);
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Generate music based on grid state - override in subclasses
|
|
*/
|
|
generateFromGrid(grid, cols, rows) {
|
|
// To be implemented by subclasses
|
|
}
|
|
|
|
async init() {
|
|
// default: do nothing
|
|
this.initialized = true;
|
|
}
|
|
|
|
play() {
|
|
if (this.isPlaying) return;
|
|
this.isPlaying = true;
|
|
this.masterGain.gain.setValueAtTime(0.3, this.audioContext.currentTime);
|
|
}
|
|
|
|
stop() {
|
|
this.isPlaying = false;
|
|
this.masterGain.gain.setTargetAtTime(0, this.audioContext.currentTime, 0.1);
|
|
this.cleanup();
|
|
}
|
|
|
|
setBaseFrequency(frequency) {
|
|
this.baseFrequency = frequency;
|
|
}
|
|
|
|
async cleanup() {
|
|
// Default: disconnect master gain
|
|
if (this.masterGain) {
|
|
this.masterGain.disconnect();
|
|
}
|
|
this.isPlaying = false;
|
|
this.initialized = false;
|
|
|
|
try {
|
|
this.masterGain.disconnect();
|
|
this.recordSource?.disconnect();
|
|
this.recordGain?.disconnect();
|
|
//this.audioContext.close();
|
|
} catch { }
|
|
|
|
try {
|
|
await this.audioContext.suspend();
|
|
try { await this.audioContext.close(); } catch (e) { }
|
|
} catch (e) { }
|
|
}
|
|
|
|
}
|
|
|
|
/**
|
|
* Additive Synthesis: Each live cell generates a sine oscillator
|
|
* Frequency based on cell position, creating harmonic textures
|
|
*/
|
|
class AdditiveSynthesizer extends MusicSynthesizer {
|
|
constructor(maxCells = 20000) {
|
|
super();
|
|
|
|
this.maxCells = maxCells;
|
|
this.lastUpdate = 0;
|
|
|
|
this.freq = new Float32Array(maxCells);
|
|
this.amp = new Float32Array(maxCells);
|
|
|
|
this.node = null;
|
|
}
|
|
|
|
async init() {
|
|
if (this.node) return; // already initialized
|
|
|
|
await this.audioContext.audioWorklet.addModule('additive-processor.js');
|
|
|
|
this.node = new AudioWorkletNode(
|
|
this.audioContext,
|
|
'additive-processor',
|
|
{
|
|
numberOfInputs: 0,
|
|
numberOfOutputs: 1,
|
|
outputChannelCount: [1]
|
|
}
|
|
);
|
|
|
|
this.node.connect(this.masterGain);
|
|
}
|
|
|
|
calculateFrequency(x, y, cols, rows) {
|
|
const nx = 1 - x / (cols - 1 || 1);
|
|
const ny = 1 - y / (rows - 1 || 1);
|
|
|
|
const octaveOffset = ny * 3;
|
|
const noteOffset = nx * 12;
|
|
|
|
return this.baseFrequency * Math.pow(2, octaveOffset + noteOffset / 12);
|
|
}
|
|
|
|
generateFromGrid(grid, cols, rows) {
|
|
if (!this.isPlaying || !this.node) return;
|
|
|
|
const now = performance.now();
|
|
//if (now - this.lastUpdate < 25) return;
|
|
this.lastUpdate = now;
|
|
|
|
// Collect live cells
|
|
const liveCells = [];
|
|
for (let y = 0; y < rows; y++) {
|
|
for (let x = 0; x < cols; x++) {
|
|
if (grid[y][x]) liveCells.push({ x, y });
|
|
}
|
|
}
|
|
|
|
const count = liveCells.length;
|
|
//if (count === 0) return;
|
|
|
|
// --- New dynamic amplitude scaling ---
|
|
const MAX_TOTAL_AMP = 0.3;
|
|
let perCellAmp;
|
|
|
|
// Calculate density relative to grid size
|
|
const density = count / (cols * rows);
|
|
|
|
// Soft compression using density, preserves volume on small/medium grids
|
|
perCellAmp = 0.25 * Math.pow(density + 0.01, 0.25) + 0.01; // small offset to avoid 0
|
|
// Limit per-cell amplitude to prevent worklet clipping
|
|
perCellAmp = Math.min(perCellAmp, 0.03);
|
|
|
|
const freqs = [];
|
|
const amps = [];
|
|
|
|
for (const cell of liveCells) {
|
|
freqs.push(this.calculateFrequency(cell.x, cell.y, cols, rows));
|
|
amps.push(perCellAmp);
|
|
}
|
|
|
|
this.node.port.postMessage({ freqs, amps });
|
|
}
|
|
|
|
async play() {
|
|
if (this.isPlaying) return;
|
|
|
|
this.isPlaying = true;
|
|
|
|
// Ensure node exists
|
|
if (!this.node) {
|
|
await this.init();
|
|
}
|
|
|
|
// Restore master gain
|
|
this.masterGain.gain.setValueAtTime(0.3, this.audioContext.currentTime);
|
|
}
|
|
|
|
|
|
cleanup() {
|
|
super.cleanup();
|
|
|
|
if (this.node) {
|
|
this.node.disconnect();
|
|
this.node = null;
|
|
}
|
|
}
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
* Frequency Grid: Map rows to frequencies, columns to amplitude modulation
|
|
* Creates a more melodic, wavetable-like sound
|
|
*/
|
|
class FrequencyGridSynthesizer extends MusicSynthesizer {
|
|
constructor() {
|
|
super();
|
|
this.oscillators = new Map(); // Map of row index -> { osc, gain }
|
|
}
|
|
|
|
generateFromGrid(grid, cols, rows) {
|
|
if (!this.isPlaying) return;
|
|
|
|
const activeCells = new Set();
|
|
|
|
// Find which rows have active cells
|
|
for (let y = 0; y < rows; y++) {
|
|
let rowHasLife = false;
|
|
for (let x = 0; x < cols; x++) {
|
|
if (grid[y][x]) {
|
|
rowHasLife = true;
|
|
activeCells.add(y);
|
|
break;
|
|
}
|
|
}
|
|
}
|
|
|
|
// Stop oscillators for rows that are now empty
|
|
for (const [rowKey, { osc, gain }] of this.oscillators) {
|
|
if (!activeCells.has(parseInt(rowKey))) {
|
|
gain.gain.setTargetAtTime(0, this.audioContext.currentTime, 0.05);
|
|
setTimeout(() => {
|
|
osc.stop();
|
|
}, 100);
|
|
this.oscillators.delete(rowKey);
|
|
}
|
|
}
|
|
|
|
// global grid density
|
|
const density = activeCells.size / (rows * cols);
|
|
|
|
// compensation: louder when overall density is low, neutral when medium
|
|
const globalBoost = Math.min(1 / Math.sqrt(density), 10);
|
|
|
|
// Create or update oscillators for rows with life
|
|
for (const row of activeCells) {
|
|
const frequency = this.baseFrequency * Math.pow(2, (1 - row / (rows - 1 || 1)) * 3);
|
|
|
|
// Count density in this row for amplitude
|
|
let cellCount = 0;
|
|
for (let x = 0; x < cols; x++) {
|
|
if (grid[row][x]) cellCount++;
|
|
}
|
|
const amplitude = Math.min((cellCount / cols) * globalBoost, 0.1) * 0.5;
|
|
|
|
if (!this.oscillators.has(row)) {
|
|
const osc = this.audioContext.createOscillator();
|
|
const gain = this.audioContext.createGain();
|
|
|
|
osc.type = 'triangle';
|
|
osc.frequency.value = frequency;
|
|
|
|
gain.gain.setValueAtTime(0, this.audioContext.currentTime);
|
|
gain.gain.setTargetAtTime(amplitude, this.audioContext.currentTime, 0.1);
|
|
|
|
osc.connect(gain);
|
|
gain.connect(this.masterGain);
|
|
osc.start();
|
|
|
|
this.oscillators.set(row, { osc, gain });
|
|
} else {
|
|
const { osc, gain } = this.oscillators.get(row);
|
|
osc.frequency.setTargetAtTime(frequency, this.audioContext.currentTime, 0.1);
|
|
gain.gain.setTargetAtTime(amplitude, this.audioContext.currentTime, 0.1);
|
|
}
|
|
}
|
|
}
|
|
|
|
cleanup() {
|
|
super.cleanup();
|
|
|
|
for (const [, { osc, gain }] of this.oscillators) {
|
|
gain.gain.setTargetAtTime(0, this.audioContext.currentTime, 0.1);
|
|
setTimeout(() => {
|
|
osc.stop();
|
|
}, 150);
|
|
}
|
|
this.oscillators.clear();
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Granular Synthesis: Each cell generates a brief burst/grain
|
|
* Creates glitchy, textured, evolving sounds
|
|
*/
|
|
class GranularSynthesizer extends MusicSynthesizer {
|
|
constructor(maxCells = 20000) {
|
|
super();
|
|
this.maxCells = maxCells;
|
|
|
|
this.node = null;
|
|
this.lastUpdate = 0;
|
|
this.previousLiveCells = new Set();
|
|
}
|
|
|
|
async init() {
|
|
if (this.node) return;
|
|
|
|
await this.audioContext.audioWorklet.addModule('granular-processor.js');
|
|
|
|
this.node = new AudioWorkletNode(this.audioContext, 'granular-processor', {
|
|
numberOfInputs: 0,
|
|
numberOfOutputs: 1,
|
|
outputChannelCount: [1]
|
|
});
|
|
|
|
this.node.connect(this.masterGain);
|
|
}
|
|
|
|
async generateFromGrid(grid, cols, rows) {
|
|
if (!this.isPlaying) return;
|
|
if (!this.node) await this.init();
|
|
|
|
const now = performance.now();
|
|
const dt = this.lastUpdate ? (now - this.lastUpdate) / 1000 : 0.05;
|
|
this.lastUpdate = now;
|
|
|
|
const newLiveCells = new Set();
|
|
const triggeredCells = [];
|
|
|
|
for (let y = 0; y < rows; y++) {
|
|
for (let x = 0; x < cols; x++) {
|
|
const key = `${x},${y}`;
|
|
const alive = grid[y][x] === 1;
|
|
if (alive) newLiveCells.add(key);
|
|
|
|
if (alive && !this.previousLiveCells.has(key)) triggeredCells.push({ x, y });
|
|
}
|
|
}
|
|
|
|
//if (triggeredCells.length === 0) return;
|
|
|
|
const MAX_TOTAL_AMP = 0.5;
|
|
const count = triggeredCells.length;
|
|
const gainPerGrain = Math.min(MAX_TOTAL_AMP / Math.sqrt(count), 0.05);
|
|
|
|
const freqs = [];
|
|
const amps = [];
|
|
const durations = [];
|
|
|
|
let factor = 1;
|
|
if (count > this.maxCells) factor = count / this.maxCells;
|
|
|
|
for (let i = 0; i < this.maxCells && i * factor < count; i++) {
|
|
const start = Math.floor(i * factor);
|
|
const end = Math.floor((i + 1) * factor);
|
|
|
|
let sumFreq = 0;
|
|
for (let j = start; j < end; j++) {
|
|
const cell = triggeredCells[j];
|
|
const nx = 1 - cell.x / (cols - 1 || 1);
|
|
const ny = 1 - cell.y / (rows - 1 || 1);
|
|
sumFreq += this.baseFrequency * Math.pow(2, ny * 3 + nx);
|
|
}
|
|
const n = end - start;
|
|
freqs.push(sumFreq / n);
|
|
amps.push(gainPerGrain);
|
|
durations.push(Math.min(Math.max(dt, 0.01), 0.1));
|
|
}
|
|
|
|
this.node.port.postMessage({ freqs, amps, durations });
|
|
this.previousLiveCells = newLiveCells;
|
|
}
|
|
|
|
async play() {
|
|
if (this.isPlaying) return;
|
|
this.isPlaying = true;
|
|
if (!this.node) await this.init();
|
|
this.masterGain.gain.setValueAtTime(0.3, this.audioContext.currentTime);
|
|
}
|
|
|
|
stop() {
|
|
this.isPlaying = false;
|
|
this.masterGain.gain.setTargetAtTime(0, this.audioContext.currentTime, 0.1);
|
|
}
|
|
|
|
cleanup() {
|
|
super.cleanup();
|
|
|
|
if (this.node) {
|
|
this.node.disconnect();
|
|
this.node = null;
|
|
}
|
|
this.previousLiveCells.clear();
|
|
}
|
|
}
|
|
|
|
|
|
|
|
/*class GridHarmonicsSynthesizer extends MusicSynthesizer {
|
|
constructor(maxOscillators = 50) {
|
|
super();
|
|
|
|
this.maxOscillators = maxOscillators;
|
|
this.oscillatorPool = [];
|
|
this.activeNotes = new Map(); // rowKey -> oscillator index
|
|
this.lastUpdate = 0;
|
|
|
|
// Initialize oscillators
|
|
this._initOscillatorPool();
|
|
|
|
// Global delay for echo
|
|
this.delayNode = this.audioContext.createDelay();
|
|
this.delayNode.delayTime.value = 0.2; // 200ms
|
|
this.delayGain = this.audioContext.createGain();
|
|
this.delayGain.gain.value = 0.2; // safe volume
|
|
this.delayNode.connect(this.delayGain);
|
|
this.delayGain.connect(this.masterGain);
|
|
}
|
|
|
|
_initOscillatorPool() {
|
|
for (let i = 0; i < this.maxOscillators; i++) {
|
|
const osc = this.audioContext.createOscillator();
|
|
const gain = this.audioContext.createGain();
|
|
osc.type = 'sine';
|
|
gain.gain.setValueAtTime(0, this.audioContext.currentTime);
|
|
osc.connect(gain);
|
|
gain.connect(this.masterGain);
|
|
osc.start();
|
|
this.oscillatorPool.push({ osc, gain, inUse: false });
|
|
}
|
|
}
|
|
|
|
generateFromGrid(grid, cols, rows) {
|
|
if (!this.isPlaying) return;
|
|
|
|
const now = performance.now();
|
|
if (now - this.lastUpdate < 25) return; // throttle ~40Hz
|
|
this.lastUpdate = now;
|
|
|
|
// Track active rows
|
|
const activeRows = new Map();
|
|
|
|
for (let y = 0; y < rows; y++) {
|
|
const liveX = [];
|
|
for (let x = 0; x < cols; x++) if (grid[y][x]) liveX.push(x);
|
|
if (!liveX.length) continue;
|
|
activeRows.set(y, liveX.length);
|
|
}
|
|
|
|
const totalActiveRows = activeRows.size || 1;
|
|
|
|
// Scale amplitude based on total active rows to avoid clipping
|
|
const MAX_TOTAL_AMP = 0.3;
|
|
const perRowGain = MAX_TOTAL_AMP / Math.sqrt(totalActiveRows);
|
|
|
|
// Assign or update oscillators
|
|
const usedOscillators = new Set();
|
|
|
|
for (const [row, count] of activeRows) {
|
|
let oscIndex;
|
|
|
|
if (this.activeNotes.has(row)) {
|
|
oscIndex = this.activeNotes.get(row);
|
|
} else {
|
|
// find free oscillator
|
|
const free = this.oscillatorPool.findIndex(o => !o.inUse);
|
|
if (free === -1) continue;
|
|
oscIndex = free;
|
|
this.oscillatorPool[oscIndex].inUse = true;
|
|
this.activeNotes.set(row, oscIndex);
|
|
}
|
|
|
|
usedOscillators.add(oscIndex);
|
|
|
|
const oscObj = this.oscillatorPool[oscIndex];
|
|
const freq = this.baseFrequency * Math.pow(2, (1 - row / rows) * 4);
|
|
|
|
// Smooth frequency change
|
|
oscObj.osc.frequency.setTargetAtTime(freq, this.audioContext.currentTime, 0.05);
|
|
|
|
// Smooth amplitude change
|
|
oscObj.gain.gain.setTargetAtTime(perRowGain, this.audioContext.currentTime, 0.05);
|
|
}
|
|
|
|
// Release unused oscillators
|
|
for (let i = 0; i < this.oscillatorPool.length; i++) {
|
|
if (!usedOscillators.has(i) && this.oscillatorPool[i].inUse) {
|
|
this.oscillatorPool[i].gain.gain.setTargetAtTime(0, this.audioContext.currentTime, 0.05);
|
|
this.oscillatorPool[i].inUse = false;
|
|
// Remove from activeNotes map
|
|
for (const [row, idx] of this.activeNotes.entries()) {
|
|
if (idx === i) this.activeNotes.delete(row);
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
play() {
|
|
if (this.isPlaying) return;
|
|
this.isPlaying = true;
|
|
this.masterGain.gain.setValueAtTime(0.3, this.audioContext.currentTime);
|
|
}
|
|
|
|
stop() {
|
|
this.isPlaying = false;
|
|
this.masterGain.gain.setTargetAtTime(0, this.audioContext.currentTime, 0.1);
|
|
|
|
// Reset all oscillators
|
|
for (const oscObj of this.oscillatorPool) {
|
|
oscObj.gain.gain.setTargetAtTime(0, this.audioContext.currentTime, 0.05);
|
|
oscObj.inUse = false;
|
|
}
|
|
this.activeNotes.clear();
|
|
}
|
|
|
|
cleanup() {
|
|
for (const oscObj of this.oscillatorPool) {
|
|
oscObj.gain.gain.setTargetAtTime(0, this.audioContext.currentTime, 0.05);
|
|
oscObj.inUse = false;
|
|
}
|
|
this.activeNotes.clear();
|
|
}
|
|
}*/
|
|
|
|
class PolyrhythmSynthesizer extends MusicSynthesizer {
|
|
constructor(maxOscillators = 100) {
|
|
super();
|
|
this.maxOscillators = maxOscillators;
|
|
|
|
this.oscillatorPool = [];
|
|
this.activeNotes = new Map(); // "x,y" -> oscillator index
|
|
this.lastUpdate = 0;
|
|
|
|
this._initOscillatorPool();
|
|
|
|
// Global delay
|
|
this.delayNode = this.audioContext.createDelay();
|
|
this.delayNode.delayTime.value = 0.2;
|
|
this.delayGain = this.audioContext.createGain();
|
|
this.delayGain.gain.value = 0.2;
|
|
this.delayNode.connect(this.delayGain);
|
|
this.delayGain.connect(this.masterGain);
|
|
}
|
|
|
|
_initOscillatorPool() {
|
|
for (let i = 0; i < this.maxOscillators; i++) {
|
|
const osc = this.audioContext.createOscillator();
|
|
const gain = this.audioContext.createGain();
|
|
const pan = this.audioContext.createStereoPanner();
|
|
|
|
osc.type = 'sine';
|
|
gain.gain.setValueAtTime(0, this.audioContext.currentTime);
|
|
|
|
osc.connect(gain);
|
|
gain.connect(pan);
|
|
pan.connect(this.masterGain);
|
|
|
|
osc.start();
|
|
|
|
this.oscillatorPool.push({ osc, gain, pan, inUse: false });
|
|
}
|
|
}
|
|
|
|
generateFromGrid(grid, cols, rows) {
|
|
if (!this.isPlaying) return;
|
|
|
|
const now = performance.now();
|
|
//if (now - this.lastUpdate < 25) return; // throttle ~40Hz
|
|
this.lastUpdate = now;
|
|
|
|
const activeCells = [];
|
|
for (let y = 0; y < rows; y++) {
|
|
for (let x = 0; x < cols; x++) {
|
|
if (grid[y][x]) activeCells.push({ x, y });
|
|
}
|
|
}
|
|
|
|
const totalActive = activeCells.length || 1;
|
|
const maxTotalGain = 0.2;
|
|
const perCellGain = maxTotalGain / Math.sqrt(totalActive);
|
|
|
|
const usedOscillators = new Set();
|
|
|
|
for (const cell of activeCells) {
|
|
const key = `${cell.x},${cell.y}`;
|
|
let oscIndex;
|
|
|
|
if (this.activeNotes.has(key)) {
|
|
oscIndex = this.activeNotes.get(key);
|
|
} else {
|
|
const free = this.oscillatorPool.findIndex(o => !o.inUse);
|
|
if (free === -1) continue;
|
|
oscIndex = free;
|
|
this.oscillatorPool[oscIndex].inUse = true;
|
|
this.activeNotes.set(key, oscIndex);
|
|
}
|
|
|
|
usedOscillators.add(oscIndex);
|
|
const oscObj = this.oscillatorPool[oscIndex];
|
|
|
|
// Frequency influenced by row
|
|
const freq = this.baseFrequency * Math.pow(2, (1 - cell.y / rows) * 4);
|
|
oscObj.osc.frequency.setTargetAtTime(freq, this.audioContext.currentTime, 0.05);
|
|
|
|
// Panning influenced by column
|
|
const panValue = (cell.x / (cols - 1)) * 2 - 1; // -1 (left) → 1 (right)
|
|
oscObj.pan.pan.setTargetAtTime(panValue, this.audioContext.currentTime, 0.05);
|
|
|
|
// Smooth gain
|
|
oscObj.gain.gain.setTargetAtTime(perCellGain, this.audioContext.currentTime, 0.05);
|
|
}
|
|
|
|
// Release unused oscillators
|
|
for (let i = 0; i < this.oscillatorPool.length; i++) {
|
|
if (!usedOscillators.has(i) && this.oscillatorPool[i].inUse) {
|
|
this.oscillatorPool[i].gain.gain.setTargetAtTime(0, this.audioContext.currentTime, 0.05);
|
|
this.oscillatorPool[i].inUse = false;
|
|
|
|
// Remove from activeNotes map
|
|
for (const [key, idx] of this.activeNotes.entries()) {
|
|
if (idx === i) this.activeNotes.delete(key);
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
play() {
|
|
if (this.isPlaying) return;
|
|
this.isPlaying = true;
|
|
this.masterGain.gain.setValueAtTime(0.3, this.audioContext.currentTime);
|
|
}
|
|
|
|
stop() {
|
|
this.isPlaying = false;
|
|
this.masterGain.gain.setTargetAtTime(0, this.audioContext.currentTime, 0.1);
|
|
|
|
for (const oscObj of this.oscillatorPool) {
|
|
oscObj.gain.gain.setTargetAtTime(0, this.audioContext.currentTime, 0.05);
|
|
oscObj.inUse = false;
|
|
}
|
|
this.activeNotes.clear();
|
|
}
|
|
|
|
cleanup() {
|
|
super.cleanup();
|
|
|
|
for (const oscObj of this.oscillatorPool) {
|
|
oscObj.gain.gain.setTargetAtTime(0, this.audioContext.currentTime, 0.05);
|
|
oscObj.inUse = false;
|
|
}
|
|
this.activeNotes.clear();
|
|
}
|
|
}
|
|
|
|
|
|
|
|
// Function to switch synthesis approaches
|
|
async function setSynthesisMode(mode) {
|
|
|
|
/*if (!lifeMusic) {
|
|
window.globalAudioContext = new (window.AudioContext || window.webkitAudioContext)();
|
|
}*/
|
|
|
|
const wasPlaying = !!lifeMusic && lifeMusic.isPlaying;
|
|
|
|
if (wasPlaying) {
|
|
lifeMusic.stop();
|
|
lifeMusic.cleanup();
|
|
}
|
|
|
|
/*try {
|
|
await window.globalAudioContext.suspend();
|
|
try { await window.globalAudioContext.close(); } catch (e) {}
|
|
} catch (e) { } */
|
|
|
|
// recreate global context
|
|
window.globalAudioContext = new (window.AudioContext || window.webkitAudioContext)();
|
|
|
|
|
|
switch (mode) {
|
|
case 'additive':
|
|
lifeMusic = new AdditiveSynthesizer();
|
|
break;
|
|
case 'frequencyGrid':
|
|
lifeMusic = new FrequencyGridSynthesizer();
|
|
break;
|
|
case 'granular':
|
|
lifeMusic = new GranularSynthesizer();
|
|
break;
|
|
case 'polyrhythm':
|
|
lifeMusic = new PolyrhythmSynthesizer();
|
|
break;
|
|
//case 'gridHarmonics':
|
|
// lifeMusic = new GridHarmonicsSynthesizer();
|
|
default:
|
|
lifeMusic = new AdditiveSynthesizer();
|
|
}
|
|
|
|
if (wasPlaying) {
|
|
// Resume new synth
|
|
await lifeMusic.init?.(); // only if init exists
|
|
//await lifeMusic.audioContext.resume();
|
|
lifeMusic.play();
|
|
}
|
|
|
|
// Re-attach recording if it was already recording
|
|
/*if (window.recording.recorder) {
|
|
// Stop old recording and immediately start on new synth
|
|
//window.recording.stop();
|
|
window.recording.start(lifeMusic);
|
|
}*/
|
|
// after creating lifeMusic (the new synth) and starting it if needed:
|
|
if (window.recording._currentRecorder) {
|
|
// recorder is active: start a new segment on the new synth
|
|
window.recording.start(lifeMusic);
|
|
}
|
|
}
|