COSMOSCILLATOR: Implementing UI and Core Functionality

1. Implement the UI in HTML and CSS

1.1 Updated index.html

COSMOSCILLATOR

COSMOSCILLATOR

Oscillators

Filter

1000 Hz
0

Envelope

0.01s
0.1s
0.5
0.5s

Effects

0
0.3s
0.4

Master

0.7

1.2 Updated styles.css

:root { --bg-color: #121212; --text-color: #e0e0e0; --primary-color: #4caf50; --secondary-color: #2196f3; --accent-color: #ff9800; } body { font-family: 'Roboto', Arial, sans-serif; background-color: var(--bg-color); color: var(--text-color); margin: 0; padding: 0; } #cosmoscillator-container { max-width: 1200px; margin: 0 auto; padding: 20px; } header { text-align: center; margin-bottom: 20px; } h1 { color: var(--primary-color); font-size: 2.5em; } main { display: grid; grid-template-columns: repeat(auto-fit, minmax(300px, 1fr)); gap: 20px; } h2 { color: var(--secondary-color); font-size: 1.5em; margin-bottom: 10px; } .slider-container { margin-bottom: 15px; } label { display: block; margin-bottom: 5px; } input[type="range"] { width: 100%; -webkit-appearance: none; background: #333; outline: none; opacity: 0.7; transition: opacity 0.2s; } input[type="range"]:hover { opacity: 1; } input[type="range"]::-webkit-slider-thumb { -webkit-appearance: none; appearance: none; width: 20px; height: 20px; background: var(--primary-color); cursor: pointer; border-radius: 50%; } input[type="range"]::-moz-range-thumb { width: 20px; height: 20px; background: var(--primary-color); cursor: pointer; border-radius: 50%; } button { background-color: var(--accent-color); color: var(--text-color); border: none; padding: 10px 15px; cursor: pointer; border-radius: 5px; font-size: 1em; transition: background-color 0.3s; } button:hover { background-color: #e68a00; } #keyboard { display: flex; justify-content: center; margin-top: 20px; } .key { width: 30px; height: 120px; background-color: white; border: 1px solid #333; margin: 0 2px; cursor: pointer; } .key.black { background-color: #333; height: 80px; width: 20px; margin-left: -10px; margin-right: -10px; z-index: 1; } .key.active { background-color: var(--primary-color); }

2. Complete the event listeners and UI interactions in main.js

// main.js document.addEventListener('DOMContentLoaded', () => { const audioContext = new (window.AudioContext || window.webkitAudioContext)(); const cosmoscillator = new COSMOSCILLATOR(audioContext); // Function to update slider value displays function updateSliderValue(sliderId, value) { const valueElement = document.getElementById(`${sliderId}-value`); if (valueElement) { valueElement.textContent = value; } } // Add event listeners for all sliders const sliders = document.querySelectorAll('input[type="range"]'); sliders.forEach(slider => { slider.addEventListener('input', (e) => { const value = e.target.value; updateSliderValue(e.target.id, value); cosmoscillator.updateParameter(e.target.id, parseFloat(value)); }); }); // Add oscillator button const addOscillatorButton = document.getElementById('add-oscillator'); addOscillatorButton.addEventListener('click', () => { cosmoscillator.addOscillator(); updateOscillatorUI(); }); // Function to update oscillator UI function updateOscillatorUI() { const oscillatorControls = document.getElementById('oscillator-controls'); oscillatorControls.innerHTML = ''; cosmoscillator.oscillators.forEach((osc, index) => { const oscDiv = document.createElement('div'); oscDiv.className = 'oscillator'; oscDiv.innerHTML = `

Oscillator ${index + 1}

${osc.volume}
${osc.detune}
`; oscillatorControls.appendChild(oscDiv); // Add event listeners for oscillator controls document.getElementById(`osc-${index}-volume`).addEventListener('input', (e) => { const value = parseFloat(e.target.value); updateSliderValue(e.target.id, value); cosmoscillator.updateOscillatorParameter(index, 'volume', value); }); document.getElementById(`osc-${index}-detune`).addEventListener('input', (e) => { const value = parseInt(e.target.value); updateSliderValue(e.target.id, value); cosmoscillator.updateOscillatorParameter(index, 'detune', value); }); document.getElementById(`osc-${index}-waveform`).addEventListener('change', (e) => { const value = parseInt(e.target.value); cosmoscillator.updateOscillatorParameter(index, 'wavetableIndex', value); }); }); } // Initialize keyboard const keyboard = document.getElementById('keyboard'); const octaves = 2; const startNote = 48; // C3 for (let i = 0; i < 12 * octaves; i++) { const key = document.createElement('div'); key.className = 'key'; if ([1, 3, 6, 8, 10].includes(i % 12)) { key.className += ' black'; } key.dataset.note = startNote + i; keyboard.appendChild(key); // Add event listeners for mouse interactions key.addEventListener('mousedown', () => cosmoscillator.noteOn(startNote + i)); key.addEventListener('mouseup', () => cosmoscillator.noteOff(startNote + i)); key.addEventListener('mouseleave', () => cosmoscillator.noteOff(startNote + i)); } // Add keyboard event listeners const keyboardMap = { 'a': 48, 'w': 49, 's': 50, 'e': 51, 'd': 52, 'f': 53, 't': 54, 'g': 55, 'y': 56, 'h': 57, 'u': 58, 'j': 59, 'k': 60, 'o': 61, 'l': 62, 'p': 63, ';': 64, "'": 65 }; document.addEventListener('keydown', (e) => { if (keyboardMap[e.key] !== undefined && !e.repeat) { cosmoscillator.noteOn(keyboardMap[e.key]); document.querySelector(`.key[data-note="${keyboardMap[e.key]}"]`).classList.add('active'); } }); document.addEventListener('keyup', (e) => { if (keyboardMap[e.key] !== undefined) { cosmoscillator.noteOff(keyboardMap[e.key]); document.querySelector(`.key[data-note="${keyboardMap[e.key]}"]`).classList.remove('active'); } }); // Initialize UI updateOscillatorUI(); });

3. Optimize performance using WebAssembly for computationally intensive tasks

// wavetable-generator.cpp #include #include #include const double PI = 3.14159265358979323846; std::vector generateWavetable(int tableSize, int harmonics) { std::vector wavetable(tableSize); for (int i = 0; i < tableSize; ++i) { float sample = 0.0f; for (int h = 1; h <= harmonics; ++h) { sample += (1.0f / h) * std::sin(2.0f * PI * h * i / tableSize); } wavetable[i] = sample; } return wavetable; } EMSCRIPTEN_BINDINGS(wavetable_module) { emscripten::function("generateWavetable", &generateWavetable); emscripten::register_vector("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

`; 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

${cosmoscillator.modulationMatrix.destinations.map(dest => ``).join('')} ${cosmoscillator.modulationMatrix.sources.map(source => ` ${cosmoscillator.modulationMatrix.destinations.map(dest => ` `).join('')} `).join('')}
${dest}
${source}
`; 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

`; 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

`; 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.