|
@@ -0,0 +1,290 @@
|
|
|
+const mp3Files = document.getElementById('mp3Files');
|
|
|
+const tapeLengthInput = document.getElementById('tapeLength');
|
|
|
+const trackList = document.getElementById('trackList');
|
|
|
+const totalDurationDisplay = document.getElementById('totalDuration');
|
|
|
+const remainingTimeDisplay = document.getElementById('remainingTime');
|
|
|
+const recordButton = document.getElementById('recordButton');
|
|
|
+const progress = document.getElementById('progress');
|
|
|
+const volumeIndicator = document.createElement('div');
|
|
|
+volumeIndicator.className = 'volume-indicator';
|
|
|
+document.body.appendChild(volumeIndicator);
|
|
|
+const reelImage = document.querySelector('.reel-spinner');
|
|
|
+const STATIC_REEL = 'reel-to-reel-static.svg';
|
|
|
+const ANIMATED_REEL = 'reel-to-reel-animated.svg';
|
|
|
+const trackGapInput = document.getElementById('trackGap');
|
|
|
+const body = document.body;
|
|
|
+
|
|
|
+let totalDuration = 0;
|
|
|
+let files = [];
|
|
|
+let audioContext;
|
|
|
+let analyser;
|
|
|
+
|
|
|
+mp3Files.addEventListener('change', handleFiles);
|
|
|
+tapeLengthInput.addEventListener('input', updateRemainingTime);
|
|
|
+recordButton.addEventListener('click', recordToTape);
|
|
|
+
|
|
|
+async function handleFiles(event) {
|
|
|
+ const newFiles = Array.from(event.target.files);
|
|
|
+
|
|
|
+ for (const file of newFiles) {
|
|
|
+ files.push(file);
|
|
|
+ const duration = await getDuration(file);
|
|
|
+ totalDuration += duration;
|
|
|
+ addTrackToPlaylist(file.name, duration, files.length - 1);
|
|
|
+ }
|
|
|
+
|
|
|
+ updateTotalDuration();
|
|
|
+ updateRemainingTime();
|
|
|
+}
|
|
|
+
|
|
|
+async function getDuration(file) {
|
|
|
+ return new Promise((resolve) => {
|
|
|
+ const audio = new Audio();
|
|
|
+ audio.preload = 'metadata';
|
|
|
+ audio.src = URL.createObjectURL(file);
|
|
|
+ audio.onloadedmetadata = () => {
|
|
|
+ resolve(audio.duration / 60); // Duration in minutes
|
|
|
+ URL.revokeObjectURL(audio.src);
|
|
|
+ };
|
|
|
+ });
|
|
|
+}
|
|
|
+
|
|
|
+function addTrackToPlaylist(name, duration, index) {
|
|
|
+ const li = document.createElement('li');
|
|
|
+ li.draggable = true;
|
|
|
+ li.dataset.index = index;
|
|
|
+ li.textContent = `${name} (${duration.toFixed(2)} minutes)`;
|
|
|
+
|
|
|
+ li.addEventListener('dragstart', handleDragStart);
|
|
|
+ li.addEventListener('dragover', handleDragOver);
|
|
|
+ li.addEventListener('drop', handleDrop);
|
|
|
+ li.addEventListener('dragend', handleDragEnd);
|
|
|
+
|
|
|
+ trackList.appendChild(li);
|
|
|
+}
|
|
|
+
|
|
|
+function updateTotalDuration() {
|
|
|
+ totalDurationDisplay.textContent = totalDuration.toFixed(2);
|
|
|
+}
|
|
|
+
|
|
|
+function updateRemainingTime() {
|
|
|
+ const tapeLength = parseFloat(tapeLengthInput.value);
|
|
|
+ if (tapeLength) {
|
|
|
+ const remainingTime = tapeLength - totalDuration;
|
|
|
+ remainingTimeDisplay.textContent = remainingTime.toFixed(2);
|
|
|
+ }
|
|
|
+}
|
|
|
+
|
|
|
+// ... other parts of the script
|
|
|
+
|
|
|
+async function recordToTape() {
|
|
|
+ if (files.length === 0) {
|
|
|
+ alert('Please add MP3 files to the playlist.');
|
|
|
+ return;
|
|
|
+ }
|
|
|
+
|
|
|
+ audioContext = new (window.AudioContext || window.webkitAudioContext)();
|
|
|
+ analyser = audioContext.createAnalyser();
|
|
|
+ analyser.fftSize = 256;
|
|
|
+ analyser.connect(audioContext.destination);
|
|
|
+
|
|
|
+ updateVolumeIndicator();
|
|
|
+ startReelAnimation();
|
|
|
+ body.classList.add('recording-active');
|
|
|
+
|
|
|
+ console.log("Starting playback with order:");
|
|
|
+ logPlaylist();
|
|
|
+
|
|
|
+ const gapSeconds = parseFloat(trackGapInput.value) || 0;
|
|
|
+
|
|
|
+ try {
|
|
|
+ for (let i = 0; i < files.length; i++) {
|
|
|
+ console.log(`Playing track ${i}: ${files[i].name}`);
|
|
|
+ const duration = await getDuration(files[i]);
|
|
|
+ await playTrack(files[i], duration);
|
|
|
+
|
|
|
+ if (i < files.length - 1 && gapSeconds > 0) {
|
|
|
+ await addGapBetweenTracks(gapSeconds);
|
|
|
+ }
|
|
|
+ }
|
|
|
+ } catch (error) {
|
|
|
+ console.error("Playback error:", error);
|
|
|
+ }
|
|
|
+
|
|
|
+ stopReelAnimation();
|
|
|
+ body.classList.remove('recording-active');
|
|
|
+ alert('Recording completed!');
|
|
|
+}
|
|
|
+
|
|
|
+async function playTrack(file, duration) {
|
|
|
+ return new Promise((resolve) => {
|
|
|
+ const audio = new Audio();
|
|
|
+ audio.src = URL.createObjectURL(file);
|
|
|
+
|
|
|
+ const source = audioContext.createMediaElementSource(audio);
|
|
|
+ source.connect(analyser);
|
|
|
+
|
|
|
+ audio.currentTime = 0; // Always start from beginning
|
|
|
+ audio.play();
|
|
|
+
|
|
|
+ audio.onended = () => {
|
|
|
+ resolve();
|
|
|
+ URL.revokeObjectURL(audio.src);
|
|
|
+ };
|
|
|
+
|
|
|
+ audio.ontimeupdate = () => {
|
|
|
+ const currentPlayTime = audio.currentTime;
|
|
|
+ const progressPercent = (currentPlayTime / (totalDuration * 60)) * 100;
|
|
|
+ updateProgress(progressPercent);
|
|
|
+
|
|
|
+ const tapeLength = parseFloat(tapeLengthInput.value) * 60;
|
|
|
+ const remainingTime = tapeLength - currentPlayTime;
|
|
|
+ remainingTimeDisplay.textContent = (remainingTime / 60).toFixed(2);
|
|
|
+ };
|
|
|
+ });
|
|
|
+}
|
|
|
+
|
|
|
+// ... rest of the script
|
|
|
+
|
|
|
+
|
|
|
+function updateProgress(percent) {
|
|
|
+ progress.innerHTML = `<div class="progress-bar" style="width: ${percent}%;"></div>`;
|
|
|
+}
|
|
|
+
|
|
|
+function updateRemainingTimeDisplay(currentTime) {
|
|
|
+ const tapeLength = parseFloat(tapeLengthInput.value) * 60;
|
|
|
+ const remainingTime = tapeLength - currentTime;
|
|
|
+ remainingTimeDisplay.textContent = (remainingTime / 60).toFixed(2);
|
|
|
+}
|
|
|
+
|
|
|
+function updateVolumeIndicator() {
|
|
|
+ const dataArray = new Uint8Array(analyser.frequencyBinCount);
|
|
|
+ analyser.getByteFrequencyData(dataArray);
|
|
|
+ const averageVolume = dataArray.reduce((a, b) => a + b) / dataArray.length;
|
|
|
+ volumeIndicator.style.height = `${averageVolume / 255 * 100}px`; // Scale height based on volume
|
|
|
+
|
|
|
+ requestAnimationFrame(updateVolumeIndicator);
|
|
|
+}
|
|
|
+
|
|
|
+function startReelAnimation() {
|
|
|
+ reelImage.src = ANIMATED_REEL;
|
|
|
+}
|
|
|
+
|
|
|
+function stopReelAnimation() {
|
|
|
+ reelImage.src = STATIC_REEL;
|
|
|
+}
|
|
|
+
|
|
|
+function addGapBetweenTracks(seconds) {
|
|
|
+ return new Promise(resolve => {
|
|
|
+ console.log(`Adding ${seconds} second gap`);
|
|
|
+ const startTime = audioContext.currentTime;
|
|
|
+
|
|
|
+ function checkGap() {
|
|
|
+ const elapsed = audioContext.currentTime - startTime;
|
|
|
+ if (elapsed >= seconds) {
|
|
|
+ resolve();
|
|
|
+ } else {
|
|
|
+ requestAnimationFrame(checkGap);
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ checkGap();
|
|
|
+ });
|
|
|
+}
|
|
|
+
|
|
|
+let draggedItem = null;
|
|
|
+
|
|
|
+function handleDragStart(e) {
|
|
|
+ draggedItem = e.target;
|
|
|
+ e.target.style.opacity = '0.4';
|
|
|
+}
|
|
|
+
|
|
|
+function handleDragOver(e) {
|
|
|
+ e.preventDefault();
|
|
|
+ const targetItem = e.target;
|
|
|
+
|
|
|
+ // Only handle drag over list items
|
|
|
+ if (targetItem.tagName === 'LI') {
|
|
|
+ const bounding = targetItem.getBoundingClientRect();
|
|
|
+ const offset = bounding.y + (bounding.height/2);
|
|
|
+
|
|
|
+ if (e.clientY - offset > 0) {
|
|
|
+ targetItem.style.borderBottom = 'solid 2px #eed49f';
|
|
|
+ targetItem.style.borderTop = '';
|
|
|
+ } else {
|
|
|
+ targetItem.style.borderTop = 'solid 2px #eed49f';
|
|
|
+ targetItem.style.borderBottom = '';
|
|
|
+ }
|
|
|
+ }
|
|
|
+}
|
|
|
+
|
|
|
+function handleDrop(e) {
|
|
|
+ e.preventDefault();
|
|
|
+ const targetItem = e.target;
|
|
|
+
|
|
|
+ // Only handle drops on list items
|
|
|
+ if (targetItem.tagName === 'LI' && draggedItem !== targetItem) {
|
|
|
+ // Get the current order of all items
|
|
|
+ const items = Array.from(trackList.children);
|
|
|
+ const oldIndex = items.indexOf(draggedItem);
|
|
|
+
|
|
|
+ // Remove and insert the dragged item
|
|
|
+ draggedItem.parentNode.removeChild(draggedItem);
|
|
|
+
|
|
|
+ // Determine if dropping before or after the target
|
|
|
+ const bounding = targetItem.getBoundingClientRect();
|
|
|
+ const insertAfter = e.clientY > (bounding.top + bounding.height / 2);
|
|
|
+
|
|
|
+ if (insertAfter) {
|
|
|
+ targetItem.parentNode.insertBefore(draggedItem, targetItem.nextSibling);
|
|
|
+ } else {
|
|
|
+ targetItem.parentNode.insertBefore(draggedItem, targetItem);
|
|
|
+ }
|
|
|
+
|
|
|
+ // Get the new order of items
|
|
|
+ const newItems = Array.from(trackList.children);
|
|
|
+ const newIndex = newItems.indexOf(draggedItem);
|
|
|
+
|
|
|
+ // Reorder the files array to match
|
|
|
+ const [movedFile] = files.splice(oldIndex, 1);
|
|
|
+ files.splice(newIndex, 0, movedFile);
|
|
|
+
|
|
|
+ // Update all indices
|
|
|
+ newItems.forEach((item, index) => {
|
|
|
+ item.dataset.index = index;
|
|
|
+ });
|
|
|
+
|
|
|
+ // Log the new order for debugging
|
|
|
+ logPlaylist();
|
|
|
+ }
|
|
|
+
|
|
|
+ clearDragOverStyles();
|
|
|
+}
|
|
|
+
|
|
|
+function handleDragEnd(e) {
|
|
|
+ e.target.style.opacity = '';
|
|
|
+ clearDragOverStyles();
|
|
|
+}
|
|
|
+
|
|
|
+function clearDragOverStyles() {
|
|
|
+ const items = trackList.querySelectorAll('li');
|
|
|
+ items.forEach(item => {
|
|
|
+ item.style.borderTop = '';
|
|
|
+ item.style.borderBottom = '';
|
|
|
+ });
|
|
|
+}
|
|
|
+
|
|
|
+function updatePlaylistIndices() {
|
|
|
+ const items = trackList.querySelectorAll('li');
|
|
|
+ items.forEach((item, index) => {
|
|
|
+ item.dataset.index = index;
|
|
|
+ });
|
|
|
+}
|
|
|
+
|
|
|
+// Add this function to help with debugging
|
|
|
+function logPlaylist() {
|
|
|
+ console.log("Current playlist order:");
|
|
|
+ files.forEach((file, index) => {
|
|
|
+ console.log(`${index}: ${file.name}`);
|
|
|
+ });
|
|
|
+}
|