function previewPlayer() { class KeyboardPlayer { constructor(containerId) { this.container = document.getElementById(containerId); this.initializeProperties(); this.loadToneJS().then(() => this.init()); this.setupWavFileObserver(); this.container.addEventListener('click', (e) => { e.stopPropagation(); if (!this.keyboardEnabled) { this.enableKeyboard(); } }); document.addEventListener('click', (e) => { if (!this.container.contains(e.target)) { this.disableKeyboard(); } }); this.disableKeyboard(); } initializeProperties() { this.sampler = null; this.keyboardEnabled = true; this.layout = null; this.rootPitch = 36; this.columnOffset = -12; this.rowOffset = 4; this.activeNotes = new Map(); this.reverb = null; this.masterGain = null; this.releaseTime = 1; this.noteNames = ['C', 'C#', 'D', 'D#', 'E', 'F', 'F#', 'G', 'G#', 'A', 'A#', 'B']; this.majorScale = [0, 2, 4, 5, 7, 9, 11]; this.isLoading = false; } enableKeyboard() { if (this.isLoading) return; this.keyboardEnabled = true; this.container.style.opacity = '1'; this.setLoaderState('inactive'); } disableKeyboard() { this.keyboardEnabled = false; this.container.style.opacity = '0.5'; } setLoaderState(state) { const loader = this.container.querySelector('.loader'); loader.className = `loader ${state}`; loader.style.display = state === 'active' ? 'flex' : 'none'; if (state === 'active') { loader.innerHTML = 'Loading samples... 🎹'; } else { loader.innerHTML = 'Ready ✨'; } } async loadToneJS() { if (window.Tone) return; const script = document.createElement('script'); script.src = 'https://cdnjs.cloudflare.com/ajax/libs/tone/14.8.49/Tone.js'; return new Promise((resolve, reject) => { script.onload = resolve; script.onerror = () => reject(new Error('Failed to load Tone.js')); document.head.appendChild(script); }); } init() { this.createUI(); this.detectKeyboardLayout(); this.setupEventListeners(); this.initializeEffects(); this.initializeSampler(); } createUI() { this.container.innerHTML = `

Master & Effects

Keyboard Mapping

Ready 🦦
`; this.cacheElements(); } cacheElements() { const selectors = { keyboard: '.keyboard', masterSlider: '.master-slider', masterValue: '.master-value', rootSlider: '.root-slider', rootValue: '.root-value', columnSlider: '.column-slider', columnValue: '.column-value', rowSlider: '.row-slider', rowValue: '.row-value', releaseSlider: '.release-slider', releaseValue: '.release-value', reverbMixSlider: '.reverb-mix-slider', reverbMixValue: '.reverb-mix-value' }; this.elements = Object.fromEntries( Object.entries(selectors).map(([key, selector]) => [key, this.container.querySelector(selector)] ) ); } setupEventListeners() { const handlers = { masterSlider: e => { const gain = parseInt(e.target.value) / 100; this.masterGain.gain.value = gain; this.elements.masterValue.textContent = `${e.target.value}%`; }, releaseSlider: e => { this.releaseTime = parseFloat(e.target.value); this.elements.releaseValue.textContent = `${this.releaseTime}s`; }, reverbMixSlider: e => { const wetness = parseInt(e.target.value) / 100; this.reverb.wet.value = wetness; this.elements.reverbMixValue.textContent = `${e.target.value}%`; }, rootSlider: e => { this.rootPitch = parseInt(e.target.value); this.elements.rootValue.textContent = this.midiToNoteName(this.rootPitch); this.updateNotes(); }, columnSlider: e => { this.columnOffset = parseInt(e.target.value); this.elements.columnValue.textContent = this.columnOffset; this.updateNotes(); }, rowSlider: e => { this.rowOffset = parseInt(e.target.value); this.elements.rowValue.textContent = this.rowOffset; this.updateNotes(); } }; Object.entries(handlers).forEach(([element, handler]) => this.elements[element].addEventListener('input', handler)); document.addEventListener('mouseup', () => this.handleMouseUp()); document.addEventListener('keydown', e => !e.repeat && this.handleKeyEvent(e, true)); document.addEventListener('keyup', e => this.handleKeyEvent(e, false)); } setupWavFileObserver() { const observer = new MutationObserver((mutations) => { const hasDownloadLinkChanges = mutations.some(mutation => mutation.type === 'childList' && mutation.target.classList.contains('download-link') ); if (hasDownloadLinkChanges) { this.initializeSampler(); this.enableKeyboard(); const keyboardTop = this.container.querySelector('.keyboard').getBoundingClientRect().top; window.scrollTo(0, keyboardTop - window.innerHeight / 2, { behavior: 'smooth' }); } }); const wavFilesContainer = document.getElementById('individual-wav-files'); if (wavFilesContainer) { observer.observe(wavFilesContainer, { childList: true, subtree: true }); } } async initializeSampler() { this.isLoading = true; this.setLoaderState('active'); this.container.querySelectorAll('.key').forEach(key => key.style.opacity = '0.5'); const requiredNotes = ['C1', 'F#1', 'C2', 'F#2', 'C3', 'F#3', 'C4', 'F#4', 'C5', 'F#5']; const urls = {}; let allNotesFound = true; for (const note of requiredNotes) { // Look for download links with the note name in them const downloadLink = document.querySelector(`.download-link[href*="${note}.wav"], .download-link[href*="${note.replace('#', '')}.wav"]`); if (downloadLink?.href) { urls[note] = downloadLink.href; } else { console.log(`Could not find sample for note: ${note}`); allNotesFound = false; break; } } if (!allNotesFound) { this.handleSamplerError(); return; } if (this.sampler) { this.sampler.dispose(); } try { this.sampler = new Tone.Sampler({ urls, onload: () => this.handleSamplerLoad(), onerror: (error) => { console.error('Sampler loading error:', error); this.handleSamplerError(); } }).connect(this.reverb); } catch (error) { console.error('Sampler initialization error:', error); this.handleSamplerError(); } } handleSamplerLoad() { console.log('All samples loaded successfully'); this.isLoading = false; this.container.querySelectorAll('.key').forEach(key => key.style.opacity = '1'); this.setLoaderState('inactive'); this.enableKeyboard(); } handleSamplerError() { console.log('No WAV files found'); this.isLoading = false; this.setLoaderState('inactive'); } initializeEffects() { this.masterGain = new Tone.Gain(1).toDestination(); this.reverb = new Tone.Reverb({ decay: 1.5, wet: 0.5, preDelay: 0.01 }).connect(this.masterGain); } detectKeyboardLayout() { this.layout = { keys: [ { keys: '1234567890'.split(''), offset: 0 }, { keys: 'QWERTYUIOP'.split(''), offset: 1 }, { keys: 'ASDFGHJKL'.split(''), offset: 1.5 }, { keys: 'ZXCVBNM,.'.split(''), offset: 2 } ] }.keys; this.createKeyboard(); } createKeyboard() { this.elements.keyboard.innerHTML = ''; this.layout.forEach((row, rowIndex) => { const rowElement = document.createElement('div'); rowElement.className = 'keyboard-row'; rowElement.style.paddingLeft = `${row.offset * 3}%`; row.keys.forEach(key => rowElement.appendChild(this.createKey(key))); this.elements.keyboard.appendChild(rowElement); }); this.updateNotes(); } createKey(keyLabel) { const key = document.createElement('div'); key.className = 'key'; key.innerHTML = `
${keyLabel}
`; key.addEventListener('mousedown', () => this.startNote(key)); key.addEventListener('mouseenter', e => e.buttons === 1 && this.startNote(key)); key.addEventListener('mouseleave', () => this.stopNote(key)); return key; } updateNotes() { Array.from(this.elements.keyboard.children).forEach((row, rowIndex) => { Array.from(row.children).forEach((key, columnIndex) => { const horizontalDistance = columnIndex - this.columnOffset; const verticalDistance = rowIndex * this.rowOffset; const totalScaleDegrees = horizontalDistance - verticalDistance; const octaves = Math.floor(totalScaleDegrees / 7); const remainingDegrees = ((totalScaleDegrees % 7) + 7) % 7; const semitonesFromRoot = this.majorScale[remainingDegrees] + (octaves * 12); const midiNote = this.rootPitch + semitonesFromRoot; this.updateKeyDisplay(key, midiNote); }); }); } updateKeyDisplay(key, midiNote) { const isBaseRoot = midiNote === this.rootPitch; const isOctaveRoot = midiNote % 12 === this.rootPitch % 12; key.style.backgroundColor = isBaseRoot ? '#90EE90' : isOctaveRoot ? '#E8F5E9' : ''; const noteName = this.midiToNoteName(midiNote); key.querySelector('.note-label').textContent = noteName; key.dataset.note = noteName; key.dataset.midi = midiNote; } handleKeyEvent(e, isKeyDown) { if (!this.keyboardEnabled || !this.sampler) return; const keyElement = this.findKeyElement(e.key.toUpperCase()); if (keyElement) { e.preventDefault(); isKeyDown ? this.startNote(keyElement) : this.stopNote(keyElement); } } startNote(keyElement) { if (!this.sampler || !keyElement || this.activeNotes.has(keyElement)) return; const note = keyElement.dataset.note; if (!note) return; Tone.start().then(() => { this.sampler.triggerAttack(note); this.activeNotes.set(keyElement, { note }); this.animateKey(keyElement, true); }); } stopNote(keyElement) { if (!this.sampler || !keyElement) return; const noteInfo = this.activeNotes.get(keyElement); if (noteInfo) { this.sampler.triggerRelease(noteInfo.note, "+" + this.releaseTime); this.activeNotes.delete(keyElement); this.animateKey(keyElement, false); } } handleMouseUp() { this.activeNotes.forEach((_, keyElement) => this.stopNote(keyElement)); } findKeyElement(keyLabel) { for (const row of this.elements.keyboard.children) { for (const key of row.children) { if (key.querySelector('.key-label').textContent === keyLabel) return key; } } return null; } animateKey(keyElement, isDown) { const midiNote = parseInt(keyElement.dataset.midi); const isBaseRoot = midiNote === this.rootPitch; const isOctaveRoot = midiNote % 12 === this.rootPitch % 12; keyElement.style.transform = isDown ? 'scale(0.95)' : ''; keyElement.style.backgroundColor = isBaseRoot ? '#90EE90' : isOctaveRoot ? '#E8F5E9' : isDown ? '#f0f0f0' : ''; } midiToNoteName(midiNumber) { const octave = Math.floor(midiNumber / 12) - 1; return `${this.noteNames[midiNumber % 12]}${octave}`; } } let container = document.getElementById('keyboard-container'); if (!container) { container = document.createElement('div'); container.id = 'keyboard-container'; document.body.appendChild(container); } new KeyboardPlayer('keyboard-container'); }