soundfont-generator / custom.js
erl-j
css fix lightmode/darkmode
1c45a6c
raw
history blame
15.5 kB
function previewPlayer() {
class KeyboardPlayer {
constructor(containerId) {
this.container = document.getElementById(containerId);
this.initializeProperties();
this.loadToneJS().then(() => this.init());
this.setupWavFileObserver();
// Add click handlers for activation/deactivation
this.container.addEventListener('click', (e) => {
e.stopPropagation();
if (!this.keyboardEnabled) {
this.enableKeyboard();
}
});
document.addEventListener('click', (e) => {
if (!this.container.contains(e.target)) {
this.disableKeyboard();
}
});
// disable keyboard
this.disableKeyboard();
}
enableKeyboard() {
this.keyboardEnabled = true;
this.container.style.opacity = '1';
}
disableKeyboard() {
this.keyboardEnabled = false;
this.container.style.opacity = '0.5';
}
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();
// scroll so middle of keyboard is in centre of viewport
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
});
}
}
initializeProperties() {
this.sampler = null;
this.keyboardEnabled = true;
this.layout = null;
this.rootPitch = 60;
this.columnOffset = 2;
this.rowOffset = 4;
this.activeNotes = new Map();
this.reverb = null;
this.masterGain = null;
this.releaseTime = 0.1;
this.noteNames = ['C', 'C#', 'D', 'D#', 'E', 'F', 'F#', 'G', 'G#', 'A', 'A#', 'B'];
this.majorScale = [0, 2, 4, 5, 7, 9, 11];
}
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="keyboard-container">
<div class="controls-section">
<h3>Master & Effects</h3>
<div class="control-group">
<label class="slider-label">Release: <span class="release-value">0.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>
</div>
<div class="keyboard"></div>
<div class="controls-section">
<h3>Keyboard Mapping</h3>
<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">2</span> keys</label>
<input type="range" class="control-slider column-slider" min="0" max="6" 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>
</div>
</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));
}
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);
}
async initializeSampler() {
const availableNotes = ['C1', 'F#1', 'C2', 'F#2', 'C3', 'F#3', 'C4', 'F#4', 'C5', 'F#5'];
const urls = Object.fromEntries(
availableNotes
.map(note => [note, document.querySelector(`a[href*="${note}.wav"]`)?.href])
.filter(([, url]) => url)
);
if (!Object.keys(urls).length) {
this.handleSamplerError();
return;
}
this.sampler = new Tone.Sampler({
urls,
onload: () => this.handleSamplerLoad(),
}).connect(this.reverb);
}
handleSamplerError() {
console.log('No WAV files found');
}
handleSamplerLoad() {
console.log('Sampler loaded');
this.container.querySelectorAll('.key').forEach(key => key.style.opacity = '1');
}
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('custom-player');
if (!container) {
container = document.createElement('div');
container.id = 'custom-player';
document.body.appendChild(container);
}
new KeyboardPlayer('custom-player');
}