Spaces:
Running
Running
<html lang="en"> | |
<head> | |
<meta charset="UTF-8"> | |
<meta name="viewport" content="width=device-width, initial-scale=1.0"> | |
<title>Yamaha Keyboard Player Simulator</title> | |
<script src="https://cdn.tailwindcss.com"></script> | |
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.4.0/css/all.min.css"> | |
<style> | |
@import url('https://fonts.googleapis.com/css2?family=Poppins:wght@300;400;500;600;700&display=swap'); | |
body { | |
font-family: 'Poppins', sans-serif; | |
background: linear-gradient(135deg, #1a1a2e, #16213e); | |
color: white; | |
min-height: 100vh; | |
} | |
.key { | |
transition: all 0.07s ease; | |
position: relative; | |
cursor: pointer; | |
user-select: none; | |
} | |
.white-key { | |
background: white; | |
border: 1px solid #ccc; | |
z-index: 1; | |
} | |
.black-key { | |
background: #333; | |
z-index: 2; | |
margin-left: -15px; | |
margin-right: -15px; | |
height: 60%; | |
} | |
.active { | |
transform: scale(0.98); | |
box-shadow: 0 0 10px rgba(255, 255, 255, 0.5); | |
} | |
.white-key.active { | |
background: #f0f0f0; | |
} | |
.black-key.active { | |
background: #222; | |
} | |
.recording { | |
animation: pulse 1.5s infinite; | |
} | |
@keyframes pulse { | |
0% { box-shadow: 0 0 0 0 rgba(255, 0, 0, 0.7); } | |
70% { box-shadow: 0 0 0 10px rgba(255, 0, 0, 0); } | |
100% { box-shadow: 0 0 0 0 rgba(255, 0, 0, 0); } | |
} | |
.waveform { | |
height: 80px; | |
background: rgba(255, 255, 255, 0.1); | |
border-radius: 8px; | |
position: relative; | |
overflow: hidden; | |
} | |
.knob { | |
width: 40px; | |
height: 40px; | |
border-radius: 50%; | |
background: linear-gradient(135deg, #555, #222); | |
border: 2px solid #444; | |
box-shadow: 0 2px 5px rgba(0, 0, 0, 0.3); | |
position: relative; | |
cursor: pointer; | |
} | |
.knob::after { | |
content: ''; | |
position: absolute; | |
top: 5px; | |
left: 50%; | |
width: 3px; | |
height: 15px; | |
background: #ffcc00; | |
transform-origin: bottom center; | |
transform: translateX(-50%) rotate(0deg); | |
} | |
.display { | |
background: linear-gradient(135deg, #2c3e50, #4ca1af); | |
border-radius: 10px; | |
box-shadow: inset 0 0 10px rgba(0, 0, 0, 0.5); | |
font-family: 'Courier New', monospace; | |
text-shadow: 0 0 5px rgba(0, 255, 255, 0.7); | |
} | |
.slider { | |
-webkit-appearance: none; | |
width: 100%; | |
height: 8px; | |
border-radius: 4px; | |
background: #555; | |
outline: none; | |
} | |
.slider::-webkit-slider-thumb { | |
-webkit-appearance: none; | |
appearance: none; | |
width: 18px; | |
height: 18px; | |
border-radius: 50%; | |
background: #ffcc00; | |
cursor: pointer; | |
} | |
#waveform-visualizer { | |
position: absolute; | |
top: 0; | |
left: 0; | |
width: 100%; | |
height: 100%; | |
} | |
</style> | |
</head> | |
<body class="flex flex-col items-center py-8"> | |
<div class="w-full max-w-6xl px-4"> | |
<h1 class="text-4xl font-bold text-center mb-2 text-yellow-400">Yamaha PSR-SX900 Keyboard Simulator</h1> | |
<p class="text-center text-gray-300 mb-8">Professional Arranger Workstation with Premium Sounds</p> | |
<!-- Display Panel --> | |
<div class="display p-4 mb-6 flex justify-between items-center"> | |
<div class="text-xl"> | |
<span id="current-preset">Grand Piano</span> | |
<span id="current-octave" class="ml-4">Octave: 4</span> | |
</div> | |
<div class="text-right"> | |
<div id="bpm-display">BPM: 120</div> | |
<div id="recording-status">Ready</div> | |
</div> | |
</div> | |
<!-- Controls --> | |
<div class="grid grid-cols-1 md:grid-cols-3 gap-6 mb-8"> | |
<!-- Instrument Selection --> | |
<div class="bg-gray-800 p-4 rounded-lg"> | |
<h2 class="text-xl font-semibold mb-4 text-yellow-300">Instrument Presets</h2> | |
<div class="grid grid-cols-2 gap-3"> | |
<button class="preset-btn bg-blue-600 hover:bg-blue-700 py-2 px-3 rounded" data-preset="piano">Grand Piano</button> | |
<button class="preset-btn bg-blue-600 hover:bg-blue-700 py-2 px-3 rounded" data-preset="electric">Electric Piano</button> | |
<button class="preset-btn bg-blue-600 hover:bg-blue-700 py-2 px-3 rounded" data-preset="organ">Church Organ</button> | |
<button class="preset-btn bg-blue-600 hover:bg-blue-700 py-2 px-3 rounded" data-preset="strings">Orchestral Strings</button> | |
<button class="preset-btn bg-blue-600 hover:bg-blue-700 py-2 px-3 rounded" data-preset="synth">Synth Lead</button> | |
<button class="preset-btn bg-blue-600 hover:bg-blue-700 py-2 px-3 rounded" data-preset="guitar">Acoustic Guitar</button> | |
<button class="preset-btn bg-blue-600 hover:bg-blue-700 py-2 px-3 rounded" data-preset="bass">Electric Bass</button> | |
<button class="preset-btn bg-blue-600 hover:bg-blue-700 py-2 px-3 rounded" data-preset="drums">Drum Kit</button> | |
</div> | |
</div> | |
<!-- Effects --> | |
<div class="bg-gray-800 p-4 rounded-lg"> | |
<h2 class="text-xl font-semibold mb-4 text-yellow-300">Effects</h2> | |
<div class="space-y-4"> | |
<div> | |
<label class="block mb-1">Reverb</label> | |
<input type="range" min="0" max="100" value="30" class="slider" id="reverb-slider"> | |
</div> | |
<div> | |
<label class="block mb-1">Chorus</label> | |
<input type="range" min="0" max="100" value="20" class="slider" id="chorus-slider"> | |
</div> | |
<div> | |
<label class="block mb-1">Delay</label> | |
<input type="range" min="0" max="100" value="15" class="slider" id="delay-slider"> | |
</div> | |
<div class="flex items-center space-x-4"> | |
<div class="knob" id="eq-knob"></div> | |
<span>EQ</span> | |
</div> | |
</div> | |
</div> | |
<!-- Recording & Playback --> | |
<div class="bg-gray-800 p-4 rounded-lg"> | |
<h2 class="text-xl font-semibold mb-4 text-yellow-300">Recording</h2> | |
<div class="waveform mb-4" id="waveform"> | |
<canvas id="waveform-visualizer"></canvas> | |
</div> | |
<div class="flex space-x-3"> | |
<button id="record-btn" class="flex-1 bg-red-600 hover:bg-red-700 py-2 px-4 rounded flex items-center justify-center"> | |
<i class="fas fa-circle mr-2"></i> Record | |
</button> | |
<button id="play-btn" class="flex-1 bg-green-600 hover:bg-green-700 py-2 px-4 rounded flex items-center justify-center" disabled> | |
<i class="fas fa-play mr-2"></i> Play | |
</button> | |
<button id="stop-btn" class="flex-1 bg-gray-600 hover:bg-gray-700 py-2 px-4 rounded flex items-center justify-center" disabled> | |
<i class="fas fa-stop mr-2"></i> Stop | |
</button> | |
</div> | |
<div class="mt-4"> | |
<button id="save-btn" class="bg-blue-600 hover:bg-blue-700 py-2 px-4 rounded w-full"> | |
<i class="fas fa-save mr-2"></i> Save Recording | |
</button> | |
</div> | |
</div> | |
</div> | |
<!-- Keyboard --> | |
<div class="relative mb-8" style="height: 200px;"> | |
<!-- White Keys --> | |
<div class="flex h-full"> | |
<div class="white-key key w-12 border rounded-b-lg flex items-end justify-center pb-2" data-note="C">C</div> | |
<div class="white-key key w-12 border rounded-b-lg flex items-end justify-center pb-2" data-note="D">D</div> | |
<div class="white-key key w-12 border rounded-b-lg flex items-end justify-center pb-2" data-note="E">E</div> | |
<div class="white-key key w-12 border rounded-b-lg flex items-end justify-center pb-2" data-note="F">F</div> | |
<div class="white-key key w-12 border rounded-b-lg flex items-end justify-center pb-2" data-note="G">G</div> | |
<div class="white-key key w-12 border rounded-b-lg flex items-end justify-center pb-2" data-note="A">A</div> | |
<div class="white-key key w-12 border rounded-b-lg flex items-end justify-center pb-2" data-note="B">B</div> | |
<div class="white-key key w-12 border rounded-b-lg flex items-end justify-center pb-2" data-note="C5">C</div> | |
</div> | |
<!-- Black Keys --> | |
<div class="flex absolute top-0" style="left: 30px;"> | |
<div class="black-key key w-8 rounded-b-lg" data-note="C#"></div> | |
<div class="black-key key w-8 rounded-b-lg" data-note="D#"></div> | |
<div style="width: 48px;"></div> <!-- Space for F-G --> | |
<div class="black-key key w-8 rounded-b-lg" data-note="F#"></div> | |
<div class="black-key key w-8 rounded-b-lg" data-note="G#"></div> | |
<div class="black-key key w-8 rounded-b-lg" data-note="A#"></div> | |
</div> | |
</div> | |
<!-- Octave Controls --> | |
<div class="flex justify-center mb-8"> | |
<button id="octave-down" class="bg-gray-700 hover:bg-gray-600 py-2 px-4 rounded-l"> | |
<i class="fas fa-arrow-down"></i> Octave Down | |
</button> | |
<button id="octave-up" class="bg-gray-700 hover:bg-gray-600 py-2 px-4 rounded-r"> | |
Octave Up <i class="fas fa-arrow-up"></i> | |
</button> | |
</div> | |
<!-- Metronome --> | |
<div class="bg-gray-800 p-4 rounded-lg mb-8"> | |
<h2 class="text-xl font-semibold mb-4 text-yellow-300">Metronome</h2> | |
<div class="flex items-center space-x-4"> | |
<button id="metronome-btn" class="bg-purple-600 hover:bg-purple-700 py-2 px-4 rounded"> | |
<i class="fas fa-music mr-2"></i> Toggle Metronome | |
</button> | |
<div class="flex-1"> | |
<label class="block mb-1">BPM</label> | |
<input type="range" min="40" max="240" value="120" class="slider" id="bpm-slider"> | |
</div> | |
<div class="w-24 text-center"> | |
<div class="text-2xl font-bold" id="metronome-display">♩ = 120</div> | |
</div> | |
</div> | |
</div> | |
<!-- Practice Tools --> | |
<div class="bg-gray-800 p-4 rounded-lg"> | |
<h2 class="text-xl font-semibold mb-4 text-yellow-300">Practice Tools</h2> | |
<div class="grid grid-cols-1 md:grid-cols-3 gap-4"> | |
<div class="bg-gray-700 p-3 rounded"> | |
<h3 class="font-medium mb-2">Chord Trainer</h3> | |
<button class="chord-btn bg-green-600 hover:bg-green-700 py-1 px-3 rounded text-sm w-full" data-chord="C"> | |
Play C Major | |
</button> | |
</div> | |
<div class="bg-gray-700 p-3 rounded"> | |
<h3 class="font-medium mb-2">Scale Practice</h3> | |
<select class="scale-select w-full bg-gray-800 rounded p-1 text-sm mb-2"> | |
<option value="C-major">C Major Scale</option> | |
<option value="A-minor">A Minor Scale</option> | |
<option value="G-pentatonic">G Pentatonic</option> | |
<option value="A-blues">A Blues Scale</option> | |
</select> | |
<button class="scale-btn bg-green-600 hover:bg-green-700 py-1 px-3 rounded text-sm w-full"> | |
Start Practice | |
</button> | |
</div> | |
<div class="bg-gray-700 p-3 rounded"> | |
<h3 class="font-medium mb-2">Song Learning</h3> | |
<select class="song-select w-full bg-gray-800 rounded p-1 text-sm mb-2"> | |
<option value="twinkle">Twinkle Twinkle</option> | |
<option value="happy">Happy Birthday</option> | |
<option value="fur-elise">Fur Elise</option> | |
<option value="canon">Canon in D</option> | |
</select> | |
<button class="song-btn bg-green-600 hover:bg-green-700 py-1 px-3 rounded text-sm w-full"> | |
Load Song | |
</button> | |
</div> | |
</div> | |
</div> | |
</div> | |
<script> | |
// Audio Context Setup | |
const AudioContext = window.AudioContext || window.webkitAudioContext; | |
const audioContext = new AudioContext(); | |
const audioElements = {}; | |
// Global variables | |
let currentPreset = 'piano'; | |
let currentOctave = 4; | |
let isRecording = false; | |
let recordingStartTime; | |
let recordedNotes = []; | |
let isPlayingRecording = false; | |
let metronomeInterval; | |
let isMetronomeOn = false; | |
let activeOscillators = {}; | |
// DOM Elements | |
const keys = document.querySelectorAll('.key'); | |
const presetButtons = document.querySelectorAll('.preset-btn'); | |
const currentPresetDisplay = document.getElementById('current-preset'); | |
const currentOctaveDisplay = document.getElementById('current-octave'); | |
const recordBtn = document.getElementById('record-btn'); | |
const playBtn = document.getElementById('play-btn'); | |
const stopBtn = document.getElementById('stop-btn'); | |
const saveBtn = document.getElementById('save-btn'); | |
const recordingStatus = document.getElementById('recording-status'); | |
const octaveUpBtn = document.getElementById('octave-up'); | |
const octaveDownBtn = document.getElementById('octave-down'); | |
const metronomeBtn = document.getElementById('metronome-btn'); | |
const bpmSlider = document.getElementById('bpm-slider'); | |
const bpmDisplay = document.getElementById('bpm-display'); | |
const metronomeDisplay = document.getElementById('metronome-display'); | |
const waveformCanvas = document.getElementById('waveform-visualizer'); | |
const ctx = waveformCanvas.getContext('2d'); | |
// Effects controls | |
const reverbSlider = document.getElementById('reverb-slider'); | |
const chorusSlider = document.getElementById('chorus-slider'); | |
const delaySlider = document.getElementById('delay-slider'); | |
const eqKnob = document.getElementById('eq-knob'); | |
// Practice tools | |
const chordButtons = document.querySelectorAll('.chord-btn'); | |
const scaleSelect = document.querySelector('.scale-select'); | |
const scaleButton = document.querySelector('.scale-btn'); | |
const songSelect = document.querySelector('.song-select'); | |
const songButton = document.querySelector('.song-btn'); | |
// Initialize canvas | |
waveformCanvas.width = waveformCanvas.parentElement.offsetWidth; | |
waveformCanvas.height = waveformCanvas.parentElement.offsetHeight; | |
// Note frequencies (A4 = 440Hz) | |
const noteFrequencies = { | |
'C': 261.63, 'C#': 277.18, 'D': 293.66, 'D#': 311.13, | |
'E': 329.63, 'F': 349.23, 'F#': 369.99, 'G': 392.00, | |
'G#': 415.30, 'A': 440.00, 'A#': 466.16, 'B': 493.88, | |
'C5': 523.25 | |
}; | |
// Initialize sound bank | |
function initializeAudioElements() { | |
const samples = { | |
'piano': ['C4', 'D4', 'E4', 'F4', 'G4', 'A4', 'B4', 'C5'], | |
'electric': ['C4', 'D4', 'E4', 'F4', 'G4', 'A4', 'B4', 'C5'], | |
'organ': ['C4', 'D4', 'E4', 'F4', 'G4', 'A4', 'B4', 'C5'], | |
'strings': ['C4', 'D4', 'E4', 'F4', 'G4', 'A4', 'B4', 'C5'], | |
'synth': ['C4', 'D4', 'E4', 'F4', 'G4', 'A4', 'B4', 'C5'], | |
'guitar': ['C4', 'D4', 'E4', 'F4', 'G4', 'A4', 'B4', 'C5'], | |
'bass': ['C2', 'D2', 'E2', 'F2', 'G2', 'A2', 'B2', 'C3'], | |
'drums': ['kick', 'snare', 'hihat', 'tom'] | |
}; | |
// In a real implementation, you would load actual audio files here | |
// For this demo, we're using Web Audio API oscillators instead | |
} | |
// Initialize | |
initializeAudioElements(); | |
updatePresetDisplay(); | |
drawEmptyWaveform(); | |
// Event Listeners | |
keys.forEach(key => { | |
key.addEventListener('mousedown', () => playNote(key)); | |
key.addEventListener('mouseup', () => releaseNote(key)); | |
key.addEventListener('mouseleave', () => releaseNote(key)); | |
}); | |
document.addEventListener('keydown', (e) => { | |
const keyMap = { | |
'a': 'C', 'w': 'C#', 's': 'D', 'e': 'D#', | |
'd': 'E', 'f': 'F', 't': 'F#', 'g': 'G', | |
'y': 'G#', 'h': 'A', 'u': 'A#', 'j': 'B', | |
'k': 'C5' | |
}; | |
const note = keyMap[e.key.toLowerCase()]; | |
if (note) { | |
const keyElement = document.querySelector(`.key[data-note="${note}"]`); | |
if (keyElement && !keyElement.classList.contains('active')) { | |
playNote(keyElement); | |
} | |
} | |
}); | |
document.addEventListener('keyup', (e) => { | |
const keyMap = { | |
'a': 'C', 'w': 'C#', 's': 'D', 'e': 'D#', | |
'd': 'E', 'f': 'F', 't': 'F#', 'g': 'G', | |
'y': 'G#', 'h': 'A', 'u': 'A#', 'j': 'B', | |
'k': 'C5' | |
}; | |
const note = keyMap[e.key.toLowerCase()]; | |
if (note) { | |
const keyElement = document.querySelector(`.key[data-note="${note}"]`); | |
if (keyElement) { | |
releaseNote(keyElement); | |
} | |
} | |
}); | |
presetButtons.forEach(button => { | |
button.addEventListener('click', () => { | |
currentPreset = button.dataset.preset; | |
updatePresetDisplay(); | |
}); | |
}); | |
recordBtn.addEventListener('click', toggleRecording); | |
playBtn.addEventListener('click', playRecording); | |
stopBtn.addEventListener('click', stopPlayback); | |
saveBtn.addEventListener('click', saveRecording); | |
octaveUpBtn.addEventListener('click', () => { | |
if (currentOctave < 7) { | |
currentOctave++; | |
updateOctaveDisplay(); | |
} | |
}); | |
octaveDownBtn.addEventListener('click', () => { | |
if (currentOctave > 1) { | |
currentOctave--; | |
updateOctaveDisplay(); | |
} | |
}); | |
metronomeBtn.addEventListener('click', toggleMetronome); | |
bpmSlider.addEventListener('input', updateBPM); | |
// Chord buttons | |
chordButtons.forEach(button => { | |
button.addEventListener('click', () => playChord(button.dataset.chord)); | |
}); | |
scaleButton.addEventListener('click', () => playScale(scaleSelect.value)); | |
songButton.addEventListener('click', () => playSong(songSelect.value)); | |
// Audio Functions | |
function playNote(keyElement) { | |
const note = keyElement.dataset.note; | |
// Check if this key is already playing (from another source) | |
if (activeOscillators[note]) { | |
return; | |
} | |
keyElement.classList.add('active'); | |
if (currentPreset === 'drums') { | |
playDrumSound(note); | |
return; | |
} | |
// For other instruments, use Web Audio API oscillators | |
const oscillator = audioContext.createOscillator(); | |
const gainNode = audioContext.createGain(); | |
// Create a simple ADSR envelope | |
const now = audioContext.currentTime; | |
gainNode.gain.setValueAtTime(0, now); | |
// Attack | |
gainNode.gain.linearRampToValueAtTime(1, now + 0.05); | |
// Sustain (we'll handle release in releaseNote) | |
oscillator.type = getOscillatorTypeForPreset(); | |
oscillator.frequency.value = noteFrequencies[note] * (currentPreset === 'bass' ? 0.25 : 1); | |
// Connect nodes with effects | |
const nodes = applyEffects(oscillator, gainNode); | |
nodes[nodes.length - 1].connect(audioContext.destination); | |
oscillator.start(); | |
// Store reference to stop later | |
activeOscillators[note] = { | |
oscillator: oscillator, | |
gainNode: gainNode, | |
keyElement: keyElement | |
}; | |
// Record the note if recording | |
if (isRecording) { | |
recordedNotes.push({ | |
type: 'note', | |
note: note, | |
time: audioContext.currentTime - recordingStartTime, | |
action: 'start', | |
preset: currentPreset, | |
octave: currentOctave | |
}); | |
updateWaveform(); | |
} | |
} | |
function releaseNote(keyElement) { | |
const note = keyElement.dataset.note; | |
if (!activeOscillators[note]) return; | |
keyElement.classList.remove('active'); | |
if (currentPreset === 'drums') return; | |
// Release the note | |
const now = audioContext.currentTime; | |
activeOscillators[note].gainNode.gain.cancelScheduledValues(now); | |
activeOscillators[note].gainNode.gain.setValueAtTime(activeOscillators[note].gainNode.gain.value, now); | |
activeOscillators[note].gainNode.gain.exponentialRampToValueAtTime(0.001, now + 0.2); | |
// Clean up after release | |
setTimeout(() => { | |
if (activeOscillators[note]) { | |
activeOscillators[note].oscillator.stop(); | |
delete activeOscillators[note]; | |
} | |
}, 200); | |
// Record the note release if recording | |
if (isRecording) { | |
recordedNotes.push({ | |
type: 'note', | |
note: note, | |
time: audioContext.currentTime - recordingStartTime, | |
action: 'stop' | |
}); | |
updateWaveform(); | |
} | |
} | |
function playDrumSound(type) { | |
const oscillator = audioContext.createOscillator(); | |
const gainNode = audioContext.createGain(); | |
let frequency, oscType, duration; | |
// Basic drum mapping | |
switch(type) { | |
case 'C': // Kick | |
frequency = 80; | |
oscType = 'sine'; | |
duration = 0.3; | |
break; | |
case 'D': // Snare | |
frequency = 150; | |
oscType = 'white'; | |
duration = 0.2; | |
break; | |
case 'E': // Hi-hat | |
frequency = 800; | |
oscType = 'white'; | |
duration = 0.05; | |
break; | |
default: // Tom | |
frequency = 200; | |
oscType = 'sine'; | |
duration = 0.15; | |
} | |
const now = audioContext.currentTime; | |
if (oscType === 'white') { | |
// White noise for snare/hi-hat | |
const bufferSize = audioContext.sampleRate * duration; | |
const noiseBuffer = audioContext.createBuffer(1, bufferSize, audioContext.sampleRate); | |
const output = noiseBuffer.getChannelData(0); | |
for (let i = 0; i < bufferSize; i++) { | |
output[i] = Math.random() * 2 - 1; | |
} | |
const noise = audioContext.createBufferSource(); | |
noise.buffer = noiseBuffer; | |
const filter = audioContext.createBiquadFilter(); | |
filter.type = 'highpass'; | |
filter.frequency.value = frequency; | |
noise.connect(filter); | |
filter.connect(gainNode); | |
// Envelope | |
gainNode.gain.setValueAtTime(1, now); | |
gainNode.gain.exponentialRampToValueAtTime(0.01, now + duration); | |
// Apply effects and connect to output | |
const nodes = applyEffects(noise, gainNode); | |
nodes[nodes.length - 1].connect(audioContext.destination); | |
noise.start(); | |
noise.stop(now + duration); | |
} else { | |
// Regular oscillator for kick/tom | |
oscillator.type = oscType; | |
oscillator.frequency.value = frequency; | |
// Frequency envelope for kick | |
oscillator.frequency.exponentialRampToValueAtTime(1, now + duration); | |
// Gain envelope | |
gainNode.gain.setValueAtTime(1, now); | |
gainNode.gain.exponentialRampToValueAtTime(0.01, now + duration); | |
// Apply effects and connect to output | |
const nodes = applyEffects(oscillator, gainNode); | |
nodes[nodes.length - 1].connect(audioContext.destination); | |
oscillator.start(); | |
oscillator.stop(now + duration); | |
} | |
// Record the drum hit if recording | |
if (isRecording) { | |
recordedNotes.push({ | |
type: 'drum', | |
note: type, | |
time: audioContext.currentTime - recordingStartTime, | |
frequency: frequency, | |
duration: duration | |
}); | |
updateWaveform(); | |
} | |
} | |
function applyEffects(sourceNode, gainNode) { | |
const nodes = [sourceNode]; | |
// Reverb | |
const reverb = audioContext.createConvolver(); | |
const reverbGain = audioContext.createGain(); | |
reverbGain.gain.value = reverbSlider.value / 100 * 0.3; | |
// Simple impulse response for reverb | |
const length = audioContext.sampleRate * 1; | |
const impulse = audioContext.createBuffer(2, length, audioContext.sampleRate); | |
const left = impulse.getChannelData(0); | |
const right = impulse.getChannelData(1); | |
for (let i = 0; i < length; i++) { | |
const n = length - i; | |
left[i] = (Math.random() * 2 - 1) * Math.pow(1 - i / length, 3); | |
right[i] = (Math.random() * 2 - 1) * Math.pow(1 - i / length, 3); | |
} | |
reverb.buffer = impulse; | |
// Chorus | |
const chorus = audioContext.createDelay(0.1); | |
chorus.delayTime.value = 0.03; | |
const chorusLFO = audioContext.createOscillator(); | |
chorusLFO.frequency.value = 2; | |
const chorusDepth = audioContext.createGain(); | |
chorusDepth.gain.value = chorusSlider.value / 100 * 0.01; | |
chorusLFO.connect(chorusDepth); | |
chorusDepth.connect(chorus.delayTime); | |
chorusLFO.start(); | |
const chorusGain = audioContext.createGain(); | |
chorusGain.gain.value = chorusSlider.value / 100 * 0.5; | |
// Delay | |
const delay = audioContext.createDelay(1.0); | |
delay.delayTime.value = 0.5; | |
const feedback = audioContext.createGain(); | |
feedback.gain.value = delaySlider.value / 100 * 0.6; | |
const delayGain = audioContext.createGain(); | |
delayGain.gain.value = delaySlider.value / 100 * 0.3; | |
// Connect nodes | |
const lastNode = nodes[nodes.length - 1]; | |
// Main path (dry + effects) | |
if (gainNode) { | |
lastNode.connect(gainNode); | |
nodes.push(gainNode); | |
} | |
// Reverb path | |
nodes[nodes.length - 1].connect(reverb); | |
reverb.connect(reverbGain); | |
nodes.push(reverbGain); | |
// Chorus path | |
nodes[0].connect(chorus); | |
chorus.connect(chorusGain); | |
nodes.push(chorusGain); | |
// Delay path (feedback loop) | |
nodes[0].connect(delay); | |
delay.connect(delayGain); | |
delayGain.connect(audioContext.destination); | |
delay.connect(feedback); | |
feedback.connect(delay); | |
return nodes; | |
} | |
function getOscillatorTypeForPreset() { | |
switch(currentPreset) { | |
case 'piano': | |
case 'electric': | |
return 'sine'; | |
case 'organ': | |
return 'sine'; | |
case 'strings': | |
return 'sawtooth'; | |
case 'synth': | |
return 'square'; | |
case 'guitar': | |
return 'sine'; | |
case 'bass': | |
return 'sine'; | |
default: | |
return 'sine'; | |
} | |
} | |
// Recording Functions | |
function toggleRecording() { | |
if (isRecording) { | |
// Stop recording | |
isRecording = false; | |
recordBtn.classList.remove('recording'); | |
recordingStatus.textContent = 'Recording saved'; | |
playBtn.disabled = recordedNotes.length === 0; | |
stopBtn.disabled = true; | |
recordBtn.innerHTML = '<i class="fas fa-circle mr-2"></i> Record'; | |
} else { | |
// Start recording | |
isRecording = true; | |
recordedNotes = []; | |
recordingStartTime = audioContext.currentTime; | |
recordBtn.classList.add('recording'); | |
recordingStatus.textContent = 'Recording...'; | |
playBtn.disabled = true; | |
stopBtn.disabled = true; | |
recordBtn.innerHTML = '<i class="fas fa-stop mr-2"></i> Stop'; | |
drawEmptyWaveform(); | |
} | |
} | |
function playRecording() { | |
if (recordedNotes.length === 0 || isPlayingRecording) return; | |
isPlayingRecording = true; | |
playBtn.disabled = true; | |
stopBtn.disabled = false; | |
recordingStatus.textContent = 'Playing back...'; | |
const playStartTime = audioContext.currentTime; | |
// Play each recorded note | |
recordedNotes.forEach(note => { | |
const delay = note.time * 1000; // Convert to milliseconds | |
setTimeout(() => { | |
if (note.type === 'drum') { | |
// Play drum sound | |
const oscillator = audioContext.createOscillator(); | |
const gainNode = audioContext.createGain(); | |
if (note.frequency > 500) { // Hi-hat | |
const bufferSize = audioContext.sampleRate * note.duration; | |
const noiseBuffer = audioContext.createBuffer(1, bufferSize, audioContext.sampleRate); | |
const output = noiseBuffer.getChannelData(0); | |
for (let i = 0; i < bufferSize; i++) { | |
output[i] = Math.random() * 2 - 1; | |
} | |
const noise = audioContext.createBufferSource(); | |
noise.buffer = noiseBuffer; | |
const filter = audioContext.createBiquadFilter(); | |
filter.type = 'highpass'; | |
filter.frequency.value = note.frequency; | |
noise.connect(filter); | |
filter.connect(gainNode); | |
gainNode.gain.setValueAtTime(1, audioContext.currentTime); | |
gainNode.gain.exponentialRampToValueAtTime(0.01, audioContext.currentTime + note.duration); | |
noise.start(); | |
noise.stop(audioContext.currentTime + note.duration); | |
} else { | |
oscillator.type = note.frequency > 150 ? 'sine' : 'sine'; | |
oscillator.frequency.value = note.frequency; | |
oscillator.connect(gainNode); | |
gainNode.gain.setValueAtTime(1, audioContext.currentTime); | |
gainNode.gain.exponentialRampToValueAtTime(0.01, audioContext.currentTime + note.duration); | |
oscillator.start(); | |
oscillator.stop(audioContext.currentTime + note.duration); | |
} | |
gainNode.connect(audioContext.destination); | |
} else if (note.action === 'start') { | |
// Play regular note | |
const keyElement = document.querySelector(`.key[data-note="${note.note}"]`); | |
if (keyElement) { | |
playNote(keyElement); | |
} | |
} else if (note.action === 'stop') { | |
// Release note | |
const keyElement = document.querySelector(`.key[data-note="${note.note}"]`); | |
if (keyElement) { | |
releaseNote(keyElement); | |
} | |
} | |
}, delay); | |
}); | |
// Stop the playback after the last note | |
const totalDuration = recordedNotes[recordedNotes.length - 1].time * 1000 + 1000; | |
setTimeout(() => { | |
if (isPlayingRecording) { | |
stopPlayback(); | |
} | |
}, totalDuration); | |
} | |
function stopPlayback() { | |
isPlayingRecording = false; | |
playBtn.disabled = recordedNotes.length === 0; | |
stopBtn.disabled = true; | |
recordingStatus.textContent = 'Ready'; | |
// Stop all playing notes | |
Object.keys(activeOscillators).forEach(note => { | |
if (activeOscillators[note]) { | |
releaseNote(activeOscillators[note].keyElement); | |
} | |
}); | |
} | |
function saveRecording() { | |
if (recordedNotes.length === 0) { | |
alert('No recording to save!'); | |
return; | |
} | |
const recordingName = prompt('Enter a name for your recording:', `Recording ${new Date().toLocaleString()}`); | |
if (recordingName) { | |
// In a real implementation, you would save to localStorage or a server | |
alert(`Recording "${recordingName}" saved! (This is a demo - recording is not actually saved)`); | |
} | |
} | |
// Helper Functions | |
function updatePresetDisplay() { | |
const presetNames = { | |
'piano': 'Grand Piano', | |
'electric': 'Electric Piano', | |
'organ': 'Church Organ', | |
'strings': 'Orchestral Strings', | |
'synth': 'Synth Lead', | |
'guitar': 'Acoustic Guitar', | |
'bass': 'Electric Bass', | |
'drums': 'Drum Kit' | |
}; | |
currentPresetDisplay.textContent = presetNames[currentPreset]; | |
} | |
function updateOctaveDisplay() { | |
currentOctaveDisplay.textContent = `Octave: ${currentOctave}`; | |
} | |
function updateBPM() { | |
const bpm = bpmSlider.value; | |
bpmDisplay.textContent = `BPM: ${bpm}`; | |
metronomeDisplay.textContent = `♩ = ${bpm}`; | |
if (isMetronomeOn) { | |
clearInterval(metronomeInterval); | |
startMetronome(); | |
} | |
} | |
function toggleMetronome() { | |
isMetronomeOn = !isMetronomeOn; | |
if (isMetronomeOn) { | |
metronomeBtn.classList.add('bg-purple-700'); | |
metronomeBtn.classList.remove('bg-purple-600'); | |
startMetronome(); | |
} else { | |
metronomeBtn.classList.remove('bg-purple-700'); | |
metronomeBtn.classList.add('bg-purple-600'); | |
clearInterval(metronomeInterval); | |
} | |
} | |
function startMetronome() { | |
const bpm = parseInt(bpmSlider.value); | |
const interval = 60000 / bpm; // Convert BPM to milliseconds | |
// Play first click immediately | |
playMetronomeClick(); | |
// Then set up the interval | |
metronomeInterval = setInterval(playMetronomeClick, interval); | |
} | |
function playMetronomeClick() { | |
const oscillator = audioContext.createOscillator(); | |
const gainNode = audioContext.createGain(); | |
oscillator.type = 'square'; | |
oscillator.frequency.value = 800; | |
const now = audioContext.currentTime; | |
gainNode.gain.setValueAtTime(1, now); | |
gainNode.gain.exponentialRampToValueAtTime(0.001, now + 0.05); | |
oscillator.connect(gainNode); | |
gainNode.connect(audioContext.destination); | |
oscillator.start(); | |
oscillator.stop(now + 0.1); | |
} | |
function updateWaveform() { | |
const width = waveformCanvas.width; | |
const height = waveformCanvas.height; | |
ctx.clearRect(0, 0, width, height); | |
// Simple visualization of notes | |
const maxTime = Math.max(1, ...recordedNotes.map(n => n.time)); | |
const scaleX = width / maxTime; | |
ctx.fillStyle = 'rgba(255, 255, 255, 0.5)'; | |
ctx.strokeStyle = '#4ade80'; | |
ctx.lineWidth = 2; | |
recordedNotes.forEach(note => { | |
if (note.action === 'start' || note.type === 'drum') { | |
const x = note.time * scaleX; | |
const noteHeight = note.type === 'drum' ? 20 : mapNoteToHeight(note.note); | |
ctx.beginPath(); | |
ctx.arc(x, height - noteHeight - 10, 5, 0, Math.PI * 2); | |
ctx.fill(); | |
// Draw line to connect notes | |
const endNote = recordedNotes.find( | |
n => n.note === note.note && n.action === 'stop' && n.time > note.time | |
); | |
if (endNote) { | |
const endX = endNote.time * scaleX; | |
ctx.beginPath(); | |
ctx.moveTo(x, height - noteHeight - 10); | |
ctx.lineTo(endX, height - noteHeight - 10); | |
ctx.stroke(); | |
} | |
} | |
}); | |
} | |
function drawEmptyWaveform() { | |
const width = waveformCanvas.width; | |
const height = waveformCanvas.height; | |
ctx.clearRect(0, 0, width, height); | |
ctx.fillStyle = 'rgba(255, 255, 255, 0.1)'; | |
ctx.fillRect(0, 0, width, height); | |
ctx.fillStyle = 'rgba(255, 255, 255, 0.3)'; | |
ctx.textAlign = 'center'; | |
ctx.font = '14px Poppins'; | |
ctx.fillText(isRecording ? 'Recording...' : 'No recording yet', width / 2, height / 2); | |
} | |
function mapNoteToHeight(note) { | |
const noteOrder = ['C', 'C#', 'D', 'D#', 'E', 'F', 'F#', 'G', 'G#', 'A', 'A#', 'B', 'C5']; | |
const index = noteOrder.indexOf(note); | |
return 10 + (index * 5); | |
} | |
// Practice Tools Functions | |
function playChord(chord) { | |
const chordNotes = { | |
'C': ['C', 'E', 'G'], | |
'D': ['D', 'F#', 'A'], | |
'E': ['E', 'G#', 'B'], | |
'F': ['F', 'A', 'C'], | |
'G': ['G', 'B', 'D'], | |
'A': ['A', 'C#', 'E'], | |
'B': ['B', 'D#', 'F#'] | |
}; | |
const notes = chordNotes[chord] || chordNotes['C']; | |
notes.forEach(note => { | |
const keyElement = document.querySelector(`.key[data-note="${note}"]`); | |
if (keyElement) { | |
playNote(keyElement); | |
// Release the note after 1 second | |
setTimeout(() => { | |
releaseNote(keyElement); | |
}, 1000); | |
} | |
}); | |
} | |
function playScale(scale) { | |
const scalePatterns = { | |
'C-major': ['C', 'D', 'E', 'F', 'G', 'A', 'B', 'C5'], | |
'A-minor': ['A', 'B', 'C', 'D', 'E', 'F', 'G', 'A'], | |
'G-pentatonic': ['G', 'A', 'B', 'D', 'E', 'G'], | |
'A-blues': ['A', 'C', 'D', 'D#', 'E', 'G', 'A'] | |
}; | |
const notes = scalePatterns[scale] || scalePatterns['C-major']; | |
playNotesWithTiming(notes); | |
} | |
function playSong(song) { | |
const songPatterns = { | |
'twinkle': [ | |
{note: 'C', duration: 0.5}, {note: 'C', duration: 0.5}, | |
{note: 'G', duration: 0.5}, {note: 'G', duration: 0.5}, | |
{note: 'A', duration: 0.5}, {note: 'A', duration: 0.5}, | |
{note: 'G', duration: 1}, | |
{note: 'F', duration: 0.5}, {note: 'F', duration: 0.5}, | |
{note: 'E', duration: 0.5}, {note: 'E', duration: 0.5}, | |
{note: 'D', duration: 0.5}, {note: 'D', duration: 0.5}, | |
{note: 'C', duration: 1} | |
], | |
'happy': [ | |
{note: 'G', duration: 0.25}, {note: 'G', duration: 0.25}, | |
{note: 'A', duration: 0.5}, {note: 'G', duration: 0.5}, | |
{note: 'C', duration: 0.5}, {note: 'B', duration: 1}, | |
{note: 'G', duration: 0.25}, {note: 'G', duration: 0.25}, | |
{note: 'A', duration: 0.5}, {note: 'G', duration: 0.5}, | |
{note: 'D', duration: 0.5}, {note: 'C', duration: 1} | |
], | |
'fur-elise': [ | |
{note: 'E', duration: 0.25}, {note: 'D#', duration: 0.25}, | |
{note: 'E', duration: 0.25}, {note: 'D#', duration: 0.25}, | |
{note: 'E', duration: 0.25}, {note: 'B', duration: 0.25}, | |
{note: 'D', duration: 0.25}, {note: 'C', duration: 0.25}, | |
{note: 'A', duration: 0.5} | |
], | |
'canon': [ | |
{note: 'G', duration: 0.5}, {note: 'D', duration: 0.5}, | |
{note: 'B', duration: 0.5}, {note: 'A', duration: 0.5}, | |
{note: 'G', duration: 0.5}, {note: 'D', duration: 0.5}, | |
{note: 'B', duration: 0.5}, {note: 'A', duration: 0.5} | |
] | |
}; | |
const notes = songPatterns[song] || songPatterns['twinkle']; | |
let time = 0; | |
notes.forEach(({note, duration}) => { | |
setTimeout(() => { | |
const keyElement = document.querySelector(`.key[data-note="${note}"]`); | |
if (keyElement) { | |
playNote(keyElement); | |
// Release the note after the duration | |
setTimeout(() => { | |
releaseNote(keyElement); | |
}, duration * 800); // Slightly shorter than the full duration | |
} | |
}, time * 1000); | |
time += duration; | |
}); | |
} | |
function playNotesWithTiming(notes, tempo = 1) { | |
let time = 0; | |
const noteDuration = 0.5 * tempo; | |
notes.forEach(note => { | |
setTimeout(() => { | |
const keyElement = document.querySelector(`.key[data-note="${note}"]`); | |
if (keyElement) { | |
playNote(keyElement); | |
// Release the note after the duration | |
setTimeout(() => { | |
releaseNote(keyElement); | |
}, noteDuration * 800); // Slightly shorter than the full duration | |
} | |
}, time * 1000); | |
time += noteDuration; | |
}); | |
} | |
// Initialize knob rotation | |
let rotation = 0; | |
eqKnob.addEventListener('mousedown', (e) => { | |
const startY = e.clientY; | |
const startRotation = rotation; | |
function rotateKnob(e) { | |
const deltaY = startY - e.clientY; | |
rotation = Math.min(90, Math.max(-90, startRotation + deltaY)); | |
eqKnob.style.transform = `rotate(${rotation}deg)`; | |
} | |
function stopRotating() { | |
document.removeEventListener('mousemove', rotateKnob); | |
document.removeEventListener('mouseup', stopRotating); | |
} | |
document.addEventListener('mousemove', rotateKnob); | |
document.addEventListener('mouseup', stopRotating); | |
}); | |
</script> | |
<p style="border-radius: 8px; text-align: center; font-size: 12px; color: #fff; margin-top: 16px;position: fixed; left: 8px; bottom: 8px; z-index: 10; background: rgba(0, 0, 0, 0.8); padding: 4px 8px;">Made with <img src="https://enzostvs-deepsite.hf.space/logo.svg" alt="DeepSite Logo" style="width: 16px; height: 16px; vertical-align: middle;display:inline-block;margin-right:3px;filter:brightness(0) invert(1);"><a href="https://enzostvs-deepsite.hf.space" style="color: #fff;text-decoration: underline;" target="_blank" >DeepSite</a> - <a href="https://enzostvs-deepsite.hf.space?remix=Parthiban97/music-player" style="color: #fff;text-decoration: underline;" target="_blank" >🧬 Remix</a></p></body> | |
</html> |