("FloatVector");
}
// Compile the C++ code to WebAssembly:
// emcc wavetable-generator.cpp -o wavetable-generator.js -s WASM=1 -s EXPORTED_FUNCTIONS='["_malloc", "_free"]' -s EXPORTED_RUNTIME_METHODS='["ccall", "cwrap"]' -s ALLOW_MEMORY_GROWTH=1 -O3
// Update WavetableOscillator.js to use WebAssembly
class WavetableOscillator {
constructor(audioContext, tableSize = 2048) {
this.audioContext = audioContext;
this.tableSize = tableSize;
this.wavetables = [];
this.currentWavetableIndex = 0;
this.phase = 0;
this.phaseIncrement = 0;
// Load WebAssembly module
this.wasmModule = null;
this.loadWasmModule();
}
async loadWasmModule() {
try {
this.wasmModule = await WebAssembly.instantiateStreaming(fetch('wavetable-generator.wasm'), {
env: {
memory: new WebAssembly.Memory({ initial: 256, maximum: 256 }),
abortStackOverflow: () => { throw new Error('Stack overflow'); },
}
});
console.log('WebAssembly module loaded successfully');
this.generateWavetables();
} catch (error) {
console.error('Failed to load WebAssembly module:', error);
}
}
generateWavetables() {
if (!this.wasmModule) {
console.error('WebAssembly module not loaded');
return;
}
const harmonicsLevels = [1, 2, 4, 8, 16, 32];
for (const harmonics of harmonicsLevels) {
const wavetable = this.wasmModule.instance.exports.generateWavetable(this.tableSize, harmonics);
this.wavetables.push(new Float32Array(wavetable));
}
}
// ... (rest of the WavetableOscillator class implementation)
}
// Update COSMOSCILLATOR.js to use the optimized WavetableOscillator
class COSMOSCILLATOR {
constructor(audioContext) {
this.audioContext = audioContext;
this.oscillators = [];
this.wavetableOscillator = new WavetableOscillator(audioContext);
// ... (rest of the constructor)
}
// ... (rest of the COSMOSCILLATOR class implementation)
}
5. Implement MIDI learning functionality
// MIDILearnManager.js
class MIDILearnManager {
constructor() {
this.midiAccess = null;
this.midiLearnMode = false;
this.midiMappings = new Map();
this.currentLearnTarget = null;
this.onMIDIMessage = this.onMIDIMessage.bind(this);
}
async initialize() {
try {
this.midiAccess = await navigator.requestMIDIAccess();
this.setupMIDIInputs();
console.log('MIDI access granted');
} catch (error) {
console.error('Failed to get MIDI access:', error);
}
}
setupMIDIInputs() {
for (const input of this.midiAccess.inputs.values()) {
input.onmidimessage = this.onMIDIMessage;
}
}
onMIDIMessage(message) {
const [status, data1, data2] = message.data;
if (this.midiLearnMode && this.currentLearnTarget) {
this.midiMappings.set(this.currentLearnTarget, { status, data1 });
this.midiLearnMode = false;
this.currentLearnTarget = null;
console.log(`MIDI mapping created for ${this.currentLearnTarget}`);
} else {
for (const [target, mapping] of this.midiMappings.entries()) {
if (status === mapping.status && data1 === mapping.data1) {
const normalizedValue = data2 / 127;
this.updateParameter(target, normalizedValue);
}
}
}
}
startMIDILearn(target) {
this.midiLearnMode = true;
this.currentLearnTarget = target;
console.log(`MIDI learn mode started for ${target}`);
}
updateParameter(target, value) {
// This method should be implemented in the main application to update UI and synth parameters
console.log(`Update parameter: ${target} = ${value}`);
}
}
// Update main.js to include MIDI learning functionality
document.addEventListener('DOMContentLoaded', () => {
// ... (previous code)
const midiLearnManager = new MIDILearnManager();
midiLearnManager.initialize();
// Add MIDI learn buttons to all sliders
sliders.forEach(slider => {
const midiLearnButton = document.createElement('button');
midiLearnButton.textContent = 'MIDI Learn';
midiLearnButton.classList.add('midi-learn-button');
midiLearnButton.addEventListener('click', () => {
midiLearnManager.startMIDILearn(slider.id);
});
slider.parentNode.appendChild(midiLearnButton);
});
// Update the updateParameter function to use MIDILearnManager
function updateParameter(parameterId, value) {
updateSliderValue(parameterId, value);
cosmoscillator.updateParameter(parameterId, value);
document.getElementById(parameterId).value = value;
}
midiLearnManager.updateParameter = updateParameter;
// ... (rest of the main.js code)
});
6. Add preset management functionality
// Add to COSMOSCILLATOR.js
class COSMOSCILLATOR {
// ... (previous code)
savePreset(name) {
const preset = {
oscillators: this.oscillators.map(osc => ({
volume: osc.volume,
detune: osc.detune,
wavetableIndex: osc.wavetableIndex
})),
filter: {
cutoff: this.filter.frequency.value,
resonance: this.filter.Q.value
},
envelope: {
attack: this.envelope.attack,
decay: this.envelope.decay,
sustain: this.envelope.sustain,
release: this.envelope.release
},
effects: {
reverbAmount: this.reverbAmount,
delayTime: this.delay.delayTime.value,
delayFeedback: this.delay.feedback.value
},
masterVolume: this.masterGain.gain.value
};
localStorage.setItem(`preset_${name}`, JSON.stringify(preset));
}
loadPreset(name) {
const preset = JSON.parse(localStorage.getItem(`preset_${name}`));
if (!preset) return;
// Apply preset values to synth parameters
preset.oscillators.forEach((oscPreset, index) => {
if (index >= this.oscillators.length) {
this.addOscillator();
}
this.updateOscillatorParameter(index, 'volume', oscPreset.volume);
this.updateOscillatorParameter(index, 'detune', oscPreset.detune);
this.updateOscillatorParameter(index, 'wavetableIndex', oscPreset.wavetableIndex);
});
this.updateParameter('filter-cutoff', preset.filter.cutoff);
this.updateParameter('filter-resonance', preset.filter.resonance);
this.updateParameter('env-attack', preset.envelope.attack);
this.updateParameter('env-decay', preset.envelope.decay);
this.updateParameter('env-sustain', preset.envelope.sustain);
this.updateParameter('env-release', preset.envelope.release);
this.updateParameter('reverb-amount', preset.effects.reverbAmount);
this.updateParameter('delay-time', preset.effects.delayTime);
this.updateParameter('delay-feedback', preset.effects.delayFeedback);
this.updateParameter('master-volume', preset.masterVolume);
}
getPresetList() {
return Object.keys(localStorage)
.filter(key => key.startsWith('preset_'))
.map(key => key.replace('preset_', ''));
}
deletePreset(name) {
localStorage.removeItem(`preset_${name}`);
}
}
// Update main.js to include preset management UI
document.addEventListener('DOMContentLoaded', () => {
// ... (previous code)
// Add preset management UI
const presetSection = document.createElement('div');
presetSection.className = 'preset-section';
presetSection.innerHTML = `
Presets
Save Preset
Load Preset
Delete Preset
`;
document.getElementById('cosmoscillator-container').appendChild(presetSection);
const presetNameInput = document.getElementById('preset-name');
const savePresetButton = document.getElementById('save-preset');
const presetList = document.getElementById('preset-list');
const loadPresetButton = document.getElementById('load-preset');
const deletePresetButton = document.getElementById('delete-preset');
function updatePresetList() {
const presets = cosmoscillator.getPresetList();
presetList.innerHTML = '';
presets.forEach(preset => {
const option = document.createElement('option');
option.value = preset;
option.textContent = preset;
presetList.appendChild(option);
});
}
savePresetButton.addEventListener('click', () => {
const presetName = presetNameInput.value.trim();
if (presetName) {
cosmoscillator.savePreset(presetName);
updatePresetList();
presetNameInput.value = '';
}
});
loadPresetButton.addEventListener('click', () => {
const selectedPreset = presetList.value;
if (selectedPreset) {
cosmoscillator.loadPreset(selectedPreset);
updateUI();
}
});
deletePresetButton.addEventListener('click', () => {
const selectedPreset = presetList.value;
if (selectedPreset) {
cosmoscillator.deletePreset(selectedPreset);
updatePresetList();
}
});
function updateUI() {
// Update all UI elements to reflect current synth state
updateOscillatorUI();
sliders.forEach(slider => {
const value = cosmoscillator.getParameter(slider.id);
updateSliderValue(slider.id, value);
slider.value = value;
});
}
// Initialize preset list
updatePresetList();
// ... (rest of the main.js code)
});
These implementations add MIDI learning functionality and preset management to the COSMOSCILLATOR synth. The MIDI learn feature allows users to map MIDI controllers to synth parameters, while the preset system enables saving, loading, and deleting synth configurations. Make sure to test these features thoroughly and adjust the UI as needed to fit your design preferences.
7. Implement modulation matrix
// Add to COSMOSCILLATOR.js
class ModulationMatrix {
constructor() {
this.sources = ['LFO1', 'LFO2', 'Envelope1', 'Envelope2'];
this.destinations = ['Osc1Pitch', 'Osc2Pitch', 'FilterCutoff', 'FilterResonance', 'AmpGain'];
this.matrix = {};
this.sources.forEach(source => {
this.matrix[source] = {};
this.destinations.forEach(dest => {
this.matrix[source][dest] = 0;
});
});
}
setModulation(source, destination, amount) {
if (this.sources.includes(source) && this.destinations.includes(destination)) {
this.matrix[source][destination] = amount;
}
}
getModulation(source, destination) {
return this.matrix[source][destination] || 0;
}
}
class COSMOSCILLATOR {
constructor(audioContext) {
// ... (previous constructor code)
this.modulationMatrix = new ModulationMatrix();
this.lfo1 = this.createLFO();
this.lfo2 = this.createLFO();
this.envelope1 = this.createEnvelope();
this.envelope2 = this.createEnvelope();
}
createLFO() {
const lfo = this.audioContext.createOscillator();
lfo.type = 'sine';
lfo.frequency.setValueAtTime(1, this.audioContext.currentTime);
lfo.start();
return lfo;
}
createEnvelope() {
return {
attack: 0.01,
decay: 0.1,
sustain: 0.5,
release: 0.5,
trigger: () => {
// Implement envelope triggering logic
}
};
}
applyModulation() {
const modulationSources = {
LFO1: this.lfo1.frequency,
LFO2: this.lfo2.frequency,
Envelope1: this.envelope1,
Envelope2: this.envelope2
};
Object.entries(this.modulationMatrix.matrix).forEach(([source, destinations]) => {
Object.entries(destinations).forEach(([destination, amount]) => {
if (amount !== 0) {
const modulationSource = modulationSources[source];
const modulationAmount = amount * 100; // Scale the modulation amount
switch (destination) {
case 'Osc1Pitch':
this.oscillators[0].detune.setValueAtTime(modulationAmount, this.audioContext.currentTime);
break;
case 'Osc2Pitch':
if (this.oscillators[1]) {
this.oscillators[1].detune.setValueAtTime(modulationAmount, this.audioContext.currentTime);
}
break;
case 'FilterCutoff':
this.filter.frequency.setValueAtTime(this.filter.frequency.value + modulationAmount, this.audioContext.currentTime);
break;
case 'FilterResonance':
this.filter.Q.setValueAtTime(this.filter.Q.value + modulationAmount / 100, this.audioContext.currentTime);
break;
case 'AmpGain':
this.masterGain.gain.setValueAtTime(this.masterGain.gain.value * (1 + modulationAmount / 100), this.audioContext.currentTime);
break;
}
}
});
});
}
// ... (rest of the COSMOSCILLATOR class implementation)
}
// Update main.js to include modulation matrix UI
document.addEventListener('DOMContentLoaded', () => {
// ... (previous code)
// Add modulation matrix UI
const modulationSection = document.createElement('div');
modulationSection.className = 'modulation-section';
modulationSection.innerHTML = `
Modulation Matrix
`;
document.getElementById('cosmoscillator-container').appendChild(modulationSection);
// Add event listeners for modulation matrix controls
const modulationInputs = document.querySelectorAll('#modulation-matrix input');
modulationInputs.forEach(input => {
input.addEventListener('input', (e) => {
const source = e.target.dataset.source;
const destination = e.target.dataset.destination;
const amount = parseFloat(e.target.value);
cosmoscillator.modulationMatrix.setModulation(source, destination, amount);
cosmoscillator.applyModulation();
});
});
// ... (rest of the main.js code)
});
8. Implement custom wavetable import functionality
// Add to WavetableOscillator.js
class WavetableOscillator {
// ... (previous code)
async importCustomWavetable(file) {
try {
const arrayBuffer = await file.arrayBuffer();
const audioBuffer = await this.audioContext.decodeAudioData(arrayBuffer);
const channelData = audioBuffer.getChannelData(0);
// Resample the imported audio to match our wavetable size
const resampledData = this.resampleAudio(channelData, this.tableSize);
// Normalize the resampled data
const normalizedData = this.normalizeAudio(resampledData);
// Add the new wavetable
this.wavetables.push(normalizedData);
return this.wavetables.length - 1; // Return the index of the new wavetable
} catch (error) {
console.error('Error importing custom wavetable:', error);
return -1;
}
}
resampleAudio(input, outputLength) {
const output = new Float32Array(outputLength);
const inputLength = input.length;
const ratio = inputLength / outputLength;
for (let i = 0; i < outputLength; i++) {
const position = i * ratio;
const index = Math.floor(position);
const fraction = position - index;
const sample1 = input[index % inputLength];
const sample2 = input[(index + 1) % inputLength];
output[i] = sample1 + fraction * (sample2 - sample1);
}
return output;
}
normalizeAudio(input) {
const max = Math.max(...input.map(Math.abs));
return input.map(sample => sample / max);
}
}
// Update COSMOSCILLATOR.js to include custom wavetable import
class COSMOSCILLATOR {
// ... (previous code)
async importCustomWavetable(file) {
const newWavetableIndex = await this.wavetableOscillator.importCustomWavetable(file);
if (newWavetableIndex !== -1) {
console.log('Custom wavetable imported successfully');
return newWavetableIndex;
} else {
console.error('Failed to import custom wavetable');
return -1;
}
}
// ... (rest of the COSMOSCILLATOR class implementation)
}
// Update main.js to include custom wavetable import UI
document.addEventListener('DOMContentLoaded', () => {
// ... (previous code)
// Add custom wavetable import UI
const wavetableImportSection = document.createElement('div');
wavetableImportSection.className = 'wavetable-import-section';
wavetableImportSection.innerHTML = `
Custom Wavetable Import
Import Wavetable
`;
document.getElementById('cosmoscillator-container').appendChild(wavetableImportSection);
const wavetableFileInput = document.getElementById('wavetable-file');
const importWavetableButton = document.getElementById('import-wavetable');
importWavetableButton.addEventListener('click', async () => {
const file = wavetableFileInput.files[0];
if (file) {
const newWavetableIndex = await cosmoscillator.importCustomWavetable(file);
if (newWavetableIndex !== -1) {
// Update oscillator waveform selectors
updateOscillatorUI();
alert('Custom wavetable imported successfully');
} else {
alert('Failed to import custom wavetable');
}
} else {
alert('Please select a file to import');
}
});
// ... (rest of the main.js code)
});
9. Implement a simple sequencer
// Add to COSMOSCILLATOR.js
class Sequencer {
constructor(steps = 16) {
this.steps = steps;
this.currentStep = 0;
this.sequence = new Array(steps).fill(null);
this.isPlaying = false;
this.tempo = 120; // BPM
}
setStep(step, note) {
if (step >= 0 && step < this.steps) {
this.sequence[step] = note;
}
}
clearStep(step) {
if (step >= 0 && step < this.steps) {
this.sequence[step] = null;
}
}
setTempo(bpm) {
this.tempo = bpm;
}
start(callback) {
this.isPlaying = true;
this.play(callback);
}
stop() {
this.isPlaying = false;
this.currentStep = 0;
}
play(callback) {
if (!this.isPlaying) return;
const stepDuration = 60000 / this.tempo / 4; // Duration of a 16th note in milliseconds
if (this.sequence[this.currentStep] !== null) {
callback(this.sequence[this.currentStep], true); // Note on
}
this.currentStep = (this.currentStep + 1) % this.steps;
setTimeout(() => {
if (this.sequence[this.currentStep - 1] !== null) {
callback(this.sequence[this.currentStep - 1], false); // Note off
}
this.play(callback);
}, stepDuration);
}
}
class COSMOSCILLATOR {
constructor(audioContext) {
// ... (previous constructor code)
this.sequencer = new Sequencer();
}
// ... (previous methods)
startSequencer() {
this.sequencer.start((note, isNoteOn) => {
if (isNoteOn) {
this.noteOn(note);
} else {
this.noteOff(note);
}
});
}
stopSequencer() {
this.sequencer.stop();
}
// ... (rest of the COSMOSCILLATOR class implementation)
}
// Update main.js to include sequencer UI
document.addEventListener('DOMContentLoaded', () => {
// ... (previous code)
// Add sequencer UI
const sequencerSection = document.createElement('div');
sequencerSection.className = 'sequencer-section';
sequencerSection.innerHTML = `
Sequencer
Play
Stop
Tempo:
`;
document.getElementById('cosmoscillator-container').appendChild(sequencerSection);
const sequencerGrid = document.getElementById('sequencer-grid');
const sequencerPlayButton = document.getElementById('sequencer-play');
const sequencerStopButton = document.getElementById('sequencer-stop');
const sequencerTempoInput = document.getElementById('sequencer-tempo');
// Create sequencer grid
for (let i = 0; i < cosmoscillator.sequencer.steps; i++) {
const stepButton = document.createElement('button');
stepButton.className = 'sequencer-step';
stepButton.dataset.step = i;
stepButton.addEventListener('click', () => {
stepButton.classList.toggle('active');
if (stepButton.classList.contains('active')) {
cosmoscillator.sequencer.setStep(i, 60 + i); // Set note (C4 + step)
} else {
cosmoscillator.sequencer.clearStep(i);
}
});
sequencerGrid.appendChild(stepButton);
}
sequencerPlayButton.addEventListener('click', () => {
cosmoscillator.startSequencer();
});
sequencerStopButton.addEventListener('click', () => {
cosmoscillator.stopSequencer();
});
sequencerTempoInput.addEventListener('input', () => {
const tempo = parseInt(sequencerTempoInput.value);
if (tempo >= 60 && tempo <= 240) {
cosmoscillator.sequencer.setTempo(tempo);
}
});
// ... (rest of the main.js code)
});
These implementations add a modulation matrix, custom wavetable import functionality, and a simple sequencer to the COSMOSCILLATOR synth. The modulation matrix allows for complex sound design by modulating various parameters with LFOs and envelopes. The custom wavetable import feature enables users to create unique timbres by importing their own audio samples. The sequencer provides a way to create rhythmic patterns and melodies within the synth interface.
Make sure to test these new features thoroughly and adjust the UI as needed to fit your design preferences. You may also want to consider adding visualizations for the custom wavetables and modulation sources to enhance the user experience.