From 391c1b51c5b7dc4c66cc1b463a377ca5432dcbeb Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" <41898282+github-actions[bot]@users.noreply.github.com> Date: Tue, 13 Oct 2026 02:00:03 +0000 Subject: [PATCH] Create core application logic for Wavehello --- ventures/wavehello/app.js | 397 ++++++++++++++++++++++++++++++++++++++ 1 file changed, 397 insertions(+) create mode 100644 ventures/wavehello/app.js diff --git a/ventures/wavehello/app.js b/ventures/wavehello/app.js new file mode 100644 index 0000000..5083341 --- /dev/null +++ b/ventures/wavehello/app.js @@ -0,0 +1,397 @@ +// Wavehello App Logic + +document.addEventListener('DOMContentLoaded', () => { + // DOM Elements + const recordButton = document.getElementById('recordButton'); + const dropArea = document.getElementById('dropArea'); + const waveform = document.getElementById('waveform'); + const styleSelect = document.getElementById('styleSelect'); + const colorPicker = document.getElementById('colorPicker'); + const captionInput = document.getElementById('captionInput'); + const downloadButton = document.getElementById('downloadButton'); + + // State + let audioContext = null; + let audioBuffer = null; + let isRecording = false; + let mediaRecorder = null; + let audioChunks = []; + + // Initialize with sample data + initializeSample(); + + // Event Listeners + recordButton.addEventListener('click', toggleRecording); + dropArea.addEventListener('dragover', handleDragOver); + dropArea.addEventListener('drop', handleDrop); + styleSelect.addEventListener('change', updateVisualization); + colorPicker.addEventListener('input', updateVisualization); + captionInput.addEventListener('input', updateVisualization); + downloadButton.addEventListener('click', downloadPNG); + + // Functions + function initializeSample() { + // Create a sample visualization on load + createSampleVisualization(); + } + + function createSampleVisualization() { + // Clear previous content + waveform.innerHTML = ''; + + // Get selected style and color + const style = styleSelect.value; + const color = colorPicker.value; + const caption = captionInput.value; + + // Create SVG container + const svgNS = "http://www.w3.org/2000/svg"; + const svg = document.createElementNS(svgNS, "svg"); + svg.setAttribute("viewBox", "0 0 800 200"); + svg.setAttribute("width", "100%"); + svg.setAttribute("height", "100%"); + + // Add background + const bg = document.createElementNS(svgNS, "rect"); + bg.setAttribute("width", "100%"); + bg.setAttribute("height", "100%"); + bg.setAttribute("fill", "white"); + svg.appendChild(bg); + + // Generate sample waveform data + const points = []; + for (let i = 0; i <= 800; i += 10) { + // Create a more interesting wave pattern + const amplitude = Math.sin(i / 50) * 30 + Math.sin(i / 20) * 20; + const y = 100 - amplitude; + points.push(`${i},${y}`); + } + + // Draw based on selected style + if (style === 'line') { + const path = document.createElementNS(svgNS, "polyline"); + path.setAttribute("points", points.join(" ")); + path.setAttribute("fill", "none"); + path.setAttribute("stroke", color); + path.setAttribute("stroke-width", "2"); + svg.appendChild(path); + } else if (style === 'bars') { + for (let i = 0; i < points.length; i++) { + const [x, y] = points[i].split(',').map(Number); + const height = 100 - y; + const bar = document.createElementNS(svgNS, "rect"); + bar.setAttribute("x", x - 2); + bar.setAttribute("y", y); + bar.setAttribute("width", "4"); + bar.setAttribute("height", height); + bar.setAttribute("fill", color); + svg.appendChild(bar); + } + } else if (style === 'ripple') { + const circle = document.createElementNS(svgNS, "circle"); + circle.setAttribute("cx", "400"); + circle.setAttribute("cy", "100"); + circle.setAttribute("r", "80"); + circle.setAttribute("fill", "none"); + circle.setAttribute("stroke", color); + circle.setAttribute("stroke-width", "2"); + svg.appendChild(circle); + + const innerCircle = document.createElementNS(svgNS, "circle"); + innerCircle.setAttribute("cx", "400"); + innerCircle.setAttribute("cy", "100"); + innerCircle.setAttribute("r", "50"); + innerCircle.setAttribute("fill", "none"); + innerCircle.setAttribute("stroke", color); + innerCircle.setAttribute("stroke-width", "1"); + innerCircle.setAttribute("stroke-dasharray", "5,5"); + svg.appendChild(innerCircle); + + const center = document.createElementNS(svgNS, "circle"); + center.setAttribute("cx", "400"); + center.setAttribute("cy", "100"); + center.setAttribute("r", "10"); + center.setAttribute("fill", color); + svg.appendChild(center); + } + + // Add caption if provided + if (caption) { + const text = document.createElementNS(svgNS, "text"); + text.setAttribute("x", "400"); + text.setAttribute("y", "180"); + text.setAttribute("text-anchor", "middle"); + text.setAttribute("font-family", "sans-serif"); + text.setAttribute("font-size", "14"); + text.setAttribute("fill", "#333"); + text.textContent = caption; + svg.appendChild(text); + } + + waveform.appendChild(svg); + } + + function updateVisualization() { + if (audioBuffer) { + visualizeAudioBuffer(audioBuffer); + } else { + createSampleVisualization(); + } + } + + function toggleRecording() { + if (isRecording) { + stopRecording(); + } else { + startRecording(); + } + } + + async function startRecording() { + try { + const stream = await navigator.mediaDevices.getUserMedia({ audio: true }); + mediaRecorder = new MediaRecorder(stream); + audioChunks = []; + + mediaRecorder.ondataavailable = event => { + audioChunks.push(event.data); + }; + + mediaRecorder.onstop = async () => { + const audioBlob = new Blob(audioChunks, { type: 'audio/webm' }); + await processAudioBlob(audioBlob); + stream.getTracks().forEach(track => track.stop()); + }; + + mediaRecorder.start(); + isRecording = true; + recordButton.textContent = '⏹'; + recordButton.style.backgroundColor = '#ef4444'; + } catch (error) { + console.error('Error accessing microphone:', error); + alert('Could not access microphone. Please ensure you have given permission.'); + } + } + + function stopRecording() { + if (mediaRecorder && isRecording) { + mediaRecorder.stop(); + isRecording = false; + recordButton.textContent = '●'; + recordButton.style.backgroundColor = ''; + } + } + + function handleDragOver(event) { + event.preventDefault(); + dropArea.style.backgroundColor = '#e2e8f0'; + } + + function handleDrop(event) { + event.preventDefault(); + dropArea.style.backgroundColor = ''; + + const files = event.dataTransfer.files; + if (files.length > 0) { + const file = files[0]; + if (file.type.startsWith('audio/') || file.name.match(/\.(mp3|wav|ogg|m4a)$/i)) { + processAudioFile(file); + } else { + alert('Please drop an audio file.'); + } + } + } + + function processAudioFile(file) { + const reader = new FileReader(); + reader.onload = async event => { + try { + const arrayBuffer = event.target.result; + await decodeAudioData(arrayBuffer); + } catch (error) { + console.error('Error processing audio file:', error); + alert('Could not process the audio file. Please try another file.'); + } + }; + reader.readAsArrayBuffer(file); + } + + async function processAudioBlob(blob) { + const arrayBuffer = await blob.arrayBuffer(); + await decodeAudioData(arrayBuffer); + } + + async function decodeAudioData(arrayBuffer) { + try { + if (!audioContext) { + audioContext = new (window.AudioContext || window.webkitAudioContext)(); + } + audioBuffer = await audioContext.decodeAudioData(arrayBuffer); + visualizeAudioBuffer(audioBuffer); + } catch (error) { + console.error('Error decoding audio data:', error); + alert('Could not decode the audio data. Please try another file.'); + } + } + + function visualizeAudioBuffer(buffer) { + // Clear previous visualization + waveform.innerHTML = ''; + + // Get selected style and color + const style = styleSelect.value; + const color = colorPicker.value; + const caption = captionInput.value; + + // Create SVG container + const svgNS = "http://www.w3.org/2000/svg"; + const svg = document.createElementNS(svgNS, "svg"); + svg.setAttribute("viewBox", "0 0 800 200"); + svg.setAttribute("width", "100%"); + svg.setAttribute("height", "100%"); + + // Add background + const bg = document.createElementNS(svgNS, "rect"); + bg.setAttribute("width", "100%"); + bg.setAttribute("height", "100%"); + bg.setAttribute("fill", "white"); + svg.appendChild(bg); + + // Get audio data (use left channel or mono) + const channelData = buffer.numberOfChannels > 0 ? buffer.getChannelData(0) : new Float32Array(0); + + // Reduce data points for visualization + const step = Math.ceil(channelData.length / 800); + const ampPoints = []; + + for (let i = 0; i < 800; i++) { + const start = i * step; + const end = Math.min(start + step, channelData.length); + let max = 0; + + for (let j = start; j < end; j++) { + const amplitude = Math.abs(channelData[j]); + if (amplitude > max) max = amplitude; + } + + ampPoints.push(max); + } + + // Draw based on selected style + if (style === 'line') { + const points = ampPoints.map((amp, i) => { + const y = 100 - (amp * 80); // Scale amplitude to fit in view + return `${i}, ${y}`; + }).join(" "); + + const path = document.createElementNS(svgNS, "polyline"); + path.setAttribute("points", points); + path.setAttribute("fill", "none"); + path.setAttribute("stroke", color); + path.setAttribute("stroke-width", "2"); + svg.appendChild(path); + } else if (style === 'bars') { + for (let i = 0; i < ampPoints.length; i += 5) { + const amp = ampPoints[i]; + const height = amp * 100; // Scale amplitude + const bar = document.createElementNS(svgNS, "rect"); + bar.setAttribute("x", i); + bar.setAttribute("y", 100 - height); + bar.setAttribute("width", "3"); + bar.setAttribute("height", height); + bar.setAttribute("fill", color); + svg.appendChild(bar); + } + } else if (style === 'ripple') { + // Create concentric circles based on amplitude + const centerX = 400; + const centerY = 100; + + // Draw outer circle + const maxRadius = 80; + const circle = document.createElementNS(svgNS, "circle"); + circle.setAttribute("cx", centerX); + circle.setAttribute("cy", centerY); + circle.setAttribute("r", maxRadius); + circle.setAttribute("fill", "none"); + circle.setAttribute("stroke", color); + circle.setAttribute("stroke-width", "1"); + svg.appendChild(circle); + + // Draw inner circles based on peaks + const peakCount = 5; + for (let i = 0; i < peakCount; i++) { + const idx = Math.floor((i / peakCount) * ampPoints.length); + const radius = ampPoints[idx] * 50; + const circle = document.createElementNS(svgNS, "circle"); + circle.setAttribute("cx", centerX); + circle.setAttribute("cy", centerY); + circle.setAttribute("r", radius); + circle.setAttribute("fill", "none"); + circle.setAttribute("stroke", color); + circle.setAttribute("stroke-width", "1"); + circle.setAttribute("opacity", 0.7 - (i * 0.1)); + svg.appendChild(circle); + } + + // Draw center point + const center = document.createElementNS(svgNS, "circle"); + center.setAttribute("cx", centerX); + center.setAttribute("cy", centerY); + center.setAttribute("r", "5"); + center.setAttribute("fill", color); + svg.appendChild(center); + } + + // Add caption if provided + if (caption) { + const text = document.createElementNS(svgNS, "text"); + text.setAttribute("x", "400"); + text.setAttribute("y", "180"); + text.setAttribute("text-anchor", "middle"); + text.setAttribute("font-family", "sans-serif"); + text.setAttribute("font-size", "14"); + text.setAttribute("fill", "#333"); + text.textContent = caption; + svg.appendChild(text); + } + + waveform.appendChild(svg); + } + + function downloadPNG() { + const svg = waveform.querySelector('svg'); + if (!svg) { + alert('No visualization to download'); + return; + } + + // Serialize SVG to string + const serializer = new XMLSerializer(); + const svgString = serializer.serializeToString(svg); + const svgBlob = new Blob([svgString], {type: 'image/svg+xml;charset=utf-8'}); + + // Convert SVG to canvas + const canvas = document.createElement('canvas'); + const ctx = canvas.getContext('2d'); + const img = new Image(); + + img.onload = function() { + canvas.width = 800; + canvas.height = 200; + ctx.drawImage(img, 0, 0); + + // Create download link + const pngUrl = canvas.toDataURL('image/png'); + const downloadLink = document.createElement('a'); + downloadLink.href = pngUrl; + downloadLink.download = 'wavehello.png'; + document.body.appendChild(downloadLink); + downloadLink.click(); + document.body.removeChild(downloadLink); + }; + + const url = URL.createObjectURL(svgBlob); + img.src = `data:image/svg+xml;base64,${btoa(svgString)}`; + } +}); \ No newline at end of file