soundfont-generator / custom.js
erl-j
css fixes, keyboard settings
a0a9280
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... <span class="loader-emoji">🎹</span>';
} else {
loader.innerHTML = 'Ready <span class="loader-emoji">✨</span>';
}
}
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 = `
<div class="controls-section">
<details>
<summary><h3>Master & Effects</h3></summary>
<div class="control-group">
<label class="slider-label">Release: <span class="release-value">1s</span></label>
<input type="range" class="control-slider release-slider" min="0" max="3" step="0.1" value="0.1">
</div>
<div class="controls-row">
<div class="control-group half-width">
<label class="slider-label">Reverb Mix: <span class="reverb-mix-value">20%</span></label>
<input type="range" class="control-slider reverb-mix-slider" min="0" max="100" value="20">
</div>
<div class="control-group half-width">
<label class="slider-label">Master: <span class="master-value">100%</span></label>
<input type="range" class="control-slider master-slider" min="0" max="200" value="100">
</div>
</div>
</details>
</div>
<div class="keyboard"></div>
<div class="controls-section">
<details>
<summary><h3>Keyboard Mapping</h3></summary>
<div class="control-group">
<label class="slider-label">Root Pitch: <span class="root-value">C4</span></label>
<input type="range" class="control-slider root-slider" min="24" max="84" value="60">
</div>
<div class="controls-row">
<div class="control-group half-width">
<label class="slider-label">Column Offset: <span class="column-value">-12</span> keys</label>
<input type="range" class="control-slider column-slider" min="-20" max="20" value="2">
</div>
<div class="control-group half-width">
<label class="slider-label">Row Offset: <span class="row-value">4</span> degrees</label>
<input type="range" class="control-slider row-slider" min="1" max="20" value="4">
</div>
</div>
</details>
</div>
<div class="loader inactive">
Ready <span class="loader-emoji">🦦</span>
</div>
`;
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 = `
<div class="key-label">${keyLabel}</div>
<div class="note-label"></div>
`;
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');
}