
diff --git a/script.js b/script.js
index 7ec6fd7..a49e589 100644
--- a/script.js
+++ b/script.js
@@ -1,15 +1,15 @@
const presets = {
preset0: ['kda_late_secondary', 'punk_late_main', 'edm_late_drums', 'pentakill_late_main', 'pentakill_late_secondary'],
- preset1: ['edm_late_main', 'hyperpop_late_drums', 'illbeats_late', 'truedamage_late_secondary'],
+ preset1: ['edm_late_main', 'truedamage_late_drums', 'illbeats_late', 'truedamage_late_secondary'],
preset2: ['kda_late_main', 'kda_late_secondary', 'punk_late_main', 'maestro_late', '8bit_late_main', 'country_late_main', 'disco_late_main', 'edm_late_drums', 'edm_late_main', 'emo_late_main', 'heartsteel_late_main', 'hyperpop_late', 'illbeats_late', 'jazz_late_main', 'pentakill_late_main', 'pentakill_late_secondary', 'truedamage_late_main', 'truedamage_late_secondary'],
preset3: ['maestro_late', 'country_late_main', 'edm_late_drums', 'edm_late_main'],
preset4: ['maestro_early', 'mixmaster_early'],
- preset5: ['disco_late_drums', 'heartsteel_late_secondary', 'hyperpop_late_drums', 'jazz_late_main'],
+ preset5: ['disco_late_drums', 'heartsteel_late_secondary', 'truedamage_late_drums', 'jazz_late_main'],
preset6: ['punk_early_drums', 'pentakill_early_main', 'pentakill_early_secondary'],
- preset7: ['kda_late_secondary', 'edm_late_main', 'hyperpop_late_drums', 'illbeats_late'],
+ preset7: ['kda_late_secondary', 'edm_late_main', 'truedamage_late_drums', 'illbeats_late'],
preset8: ['punk_late_main', 'maestro_late', 'emo_late_drums', 'emo_late_main'],
- preset9: ['kda_late_main', 'hyperpop_late', 'hyperpop_late_drums'],
+ preset9: ['kda_late_main', 'hyperpop_late', 'truedamage_late_drums'],
preset10: ['punk_late_main', 'country_late_main', 'emo_late_main', 'pentakill_late_drums'],
preset11: ['punk_early_main', 'maestro_early', 'country_early_drums', 'disco_early_drums', 'pentakill_early_drums'],
preset12: ['maestro_late', 'country_late_main', 'emo_late_drums', 'piano_late'],
@@ -19,7 +19,7 @@ const presets = {
};
-const tracks = ['8bit_early_drums', '8bit_early_main', '8bit_late_drums', '8bit_late_main', 'country_early_drums', 'country_early_main', 'country_late_drums', 'country_late_main', 'death1', 'death2', 'death3', 'death4', 'death5', 'death6', 'disco_early_drums', 'disco_early_main', 'disco_late_drums', 'disco_late_main', 'edm_early_drums', 'edm_early_main', 'edm_late_drums', 'edm_late_main', 'emo_early_drums', 'emo_early_main', 'emo_late_drums', 'emo_late_main', 'heartsteel_early_drums', 'heartsteel_early_main', 'heartsteel_early_secondary', 'heartsteel_late_drums', 'heartsteel_late_main', 'heartsteel_late_secondary', 'hyperpop_early', 'hyperpop_late', 'hyperpop_late_drums', 'illbeats_early', 'illbeats_late', 'jazz_early_main', 'jazz_late_main', 'kda_early_drums', 'kda_early_main', 'kda_early_secondary', 'kda_late_drums', 'kda_late_main', 'kda_late_secondary', 'maestro_early', 'maestro_late', 'mixmaster_early', 'mixmaster_late', 'pentakill_early_drums', 'pentakill_early_main', 'pentakill_early_secondary', 'pentakill_late_drums', 'pentakill_late_main', 'pentakill_late_secondary', 'piano_early', 'piano_late', 'punk_early_drums', 'punk_early_main', 'punk_late_drums', 'punk_late_main', 'starting_carousel', 'truedamage_early_drums', 'truedamage_early_main', 'truedamage_early_secondary', 'truedamage_late_drums', 'truedamage_late_main', 'truedamage_late_secondary'];
+const tracks = ['8bit_early_drums', '8bit_early_main', '8bit_late_drums', '8bit_late_main', 'country_early_drums', 'country_early_main', 'country_late_drums', 'country_late_main', 'death1', 'death2', 'death3', 'death4', 'death5', 'death6', 'disco_early_drums', 'disco_early_main', 'disco_late_drums', 'disco_late_main', 'edm_early_drums', 'edm_early_main', 'edm_late_drums', 'edm_late_main', 'emo_early_drums', 'emo_early_main', 'emo_late_drums', 'emo_late_main', 'heartsteel_early_drums', 'heartsteel_early_main', 'heartsteel_early_secondary', 'heartsteel_late_drums', 'heartsteel_late_main', 'heartsteel_late_secondary', 'hyperpop_early', 'hyperpop_late', 'illbeats_early', 'illbeats_late', 'jazz_early_main', 'jazz_late_main', 'kda_early_drums', 'kda_early_main', 'kda_early_secondary', 'kda_late_drums', 'kda_late_main', 'kda_late_secondary', 'maestro_early', 'maestro_late', 'mixmaster_early', 'mixmaster_late', 'pentakill_early_drums', 'pentakill_early_main', 'pentakill_early_secondary', 'pentakill_late_drums', 'pentakill_late_main', 'pentakill_late_secondary', 'piano_early', 'piano_late', 'punk_early_drums', 'punk_early_main', 'punk_late_drums', 'punk_late_main', 'starting_carousel', 'truedamage_early_drums', 'truedamage_early_main', 'truedamage_early_secondary', 'truedamage_late_drums', 'truedamage_late_main', 'truedamage_late_secondary'];
const context = new(window.AudioContext || window.webkitAudioContext)();
var sourceArray = [];
var audioGainArray = [];
@@ -36,15 +36,31 @@ var endedCallbackArray = [];
// Cache for audio buffers to avoid re-downloading
var audioBufferCache = {};
+// Playback control center state
+var playbackStartContextTime = 0; // context.currentTime when playback started
+var playbackOffset = 0; // offset into the track (for seeking)
+var maxDuration = 0; // duration of the longest active track
+var isPlaying = false;
+var progressAnimationId = null;
+
function playSelectedTracks() {
stopAllTracks();
+ playbackOffset = 0;
// reuse loaded AudioBuffer in real time mode
if (document.getElementById('realTime').checked &&
audio_buffers &&
+ audio_buffers.length > 0 &&
startCallback
) {
+ masterGainNode = context.createGain();
+ masterGainNode.connect(context.destination);
+ masterGainNode.gain.setValueAtTime(getGlobalVolume(), context.currentTime);
+ playbackStartContextTime = context.currentTime + 0.25;
+ isPlaying = true;
+ maxDuration = audio_buffers.reduce((max, buf) => Math.max(max, buf.duration), 0);
audio_buffers.forEach(startCallback);
+ updateControlCenter(true);
return;
}
@@ -54,6 +70,7 @@ function playSelectedTracks() {
activeTrackElements = [];
var currentGlobalVolume = getGlobalVolume(); // Get the current global volume
+ var isRealTime = document.getElementById('realTime').checked;
for (var i = 0; i < tracks.length; i++) {
// Hacky way to only add the listeners once bc they are annoying to remove
// when using an anon func (but anon func makes indexing the tracks easy)
@@ -62,9 +79,9 @@ function playSelectedTracks() {
const trackIndex = i;
trackElement.addEventListener('change', () => toggleTrackRealTime(trackIndex));
}
- // OPTIMIZATION: Only load checked tracks, even in real-time mode
- // Real-time mode will load tracks on-demand when checked
- if (trackElement.checked) {
+ // In real-time mode, load ALL tracks so they can be toggled on/off
+ // In normal mode, only load checked tracks
+ if (isRealTime || trackElement.checked) {
activeTrackElements.push(trackElement);
playlist.push("tracks/" + tracks[i] + ".aac");
}
@@ -89,7 +106,6 @@ function playSelectedTracks() {
audio_buffers = await Promise.all(loadPromises);
// to enable the AudioContext we need to handle a user gesture
- const current_time = context.currentTime;
masterGainNode = context.createGain();
masterGainNode.connect(context.destination);
masterGainNode.gain.setValueAtTime(currentGlobalVolume, context.currentTime);
@@ -100,11 +116,9 @@ function playSelectedTracks() {
const source = context.createBufferSource();
// we only connect the decoded data, it's not copied
source.buffer = buf;
- // make it loop?
- //source.loop = true;
- // start them all 0.25s after we began, so we're sure they're in sync
+ // start them all at playbackStartContextTime, so we're sure they're in sync
const gainNode = context.createGain();
- source.start(current_time + 0.25);
+ source.start(playbackStartContextTime, playbackOffset);
source.connect(gainNode);
gainNode.connect(masterGainNode);
sourceArray.push(source);
@@ -124,14 +138,29 @@ function playSelectedTracks() {
if (areAllCheckedTracksDone()) {
if (document.getElementById('repeat').checked) {
stopAllTracks();
+ playbackOffset = 0;
+ playbackStartContextTime = context.currentTime + 0.25;
+ masterGainNode = context.createGain();
+ masterGainNode.connect(context.destination);
+ masterGainNode.gain.setValueAtTime(getGlobalVolume(), context.currentTime);
+ isPlaying = true;
audio_buffers.forEach(startCallback);
+ updateControlCenter(true);
+ } else {
+ updateControlCenter(false);
}
}
}
};
source.addEventListener('ended', endedCallbackArray[i]);
};
+
+ // Calculate max duration for the progress bar
+ maxDuration = audio_buffers.reduce((max, buf) => Math.max(max, buf.duration), 0);
+ playbackStartContextTime = context.currentTime + 0.25;
+ isPlaying = true;
audio_buffers.forEach(startCallback);
+ updateControlCenter(true);
// Avoid appearing to infinite load when playing with no tracks selected
if (audio_buffers.length == 0) {
@@ -161,6 +190,11 @@ function stopAllTracks() {
endedArray = [];
playingArray = [];
endedCallbackArray = [];
+ isPlaying = false;
+ if (progressAnimationId) {
+ cancelAnimationFrame(progressAnimationId);
+ progressAnimationId = null;
+ }
}
function getGlobalVolume() {
@@ -177,6 +211,7 @@ function toggleRealTime() {
stopAllTracks();
audio_buffers = [];
startCallback = null;
+ activeTrackElements = [];
}
function toggleTrackRealTime(trackIndex) {
@@ -217,7 +252,14 @@ function randomSelectTracks(trackSelector = '') {
areAllCheckedTracksDone()
) {
stopAllTracks();
+ playbackOffset = 0;
+ playbackStartContextTime = context.currentTime + 0.25;
+ masterGainNode = context.createGain();
+ masterGainNode.connect(context.destination);
+ masterGainNode.gain.setValueAtTime(getGlobalVolume(), context.currentTime);
+ isPlaying = true;
audio_buffers.forEach(startCallback);
+ updateControlCenter(true);
}
}
@@ -243,19 +285,16 @@ function generateShareableLink() {
checkboxes.forEach(function(checkbox) {
if (checkbox.checked) {
- selectedTracks.push(encodeURIComponent(checkbox.id));
+ selectedTracks.push(checkbox.id);
}
});
var url = window.location.href.split('?')[0];
- var params = selectedTracks.join('%2C'); // Encoding comma
+ var params = selectedTracks.join(',');
// Add the parameters to the URL
url += '?selectedTracks=' + params;
- // Remove any trailing dot or comma for legacy URLs
- url = url.replace(/[.,]$/, '');
-
navigator.clipboard.writeText(url).then(function() {
alert("Mix URL copied to clipboard!");
})
@@ -300,17 +339,136 @@ function applyPreset(presetName) {
// Update any UI elements or states as necessary
}
+// ---- Control Center Functions ----
+
+function formatTime(seconds) {
+ if (!isFinite(seconds) || seconds < 0) seconds = 0;
+ var mins = Math.floor(seconds / 60);
+ var secs = Math.floor(seconds % 60);
+ return mins + ':' + (secs < 10 ? '0' : '') + secs;
+}
+
+function getCurrentPlaybackTime() {
+ if (!isPlaying) return playbackOffset;
+ return playbackOffset + (context.currentTime - playbackStartContextTime);
+}
+
+function updateControlCenter(playing) {
+ var controlCenter = document.getElementById('controlCenter');
+ if (!controlCenter) return;
+
+ if (playing) {
+ controlCenter.style.display = 'block';
+ startProgressUpdate();
+ } else {
+ stopProgressUpdate();
+ // Update final state
+ var progressBar = document.getElementById('progressBar');
+ var currentTimeDisplay = document.getElementById('currentTime');
+ if (progressBar && currentTimeDisplay) {
+ var currentTime = Math.min(getCurrentPlaybackTime(), maxDuration);
+ progressBar.value = maxDuration > 0 ? (currentTime / maxDuration) * 100 : 0;
+ currentTimeDisplay.textContent = formatTime(currentTime);
+ }
+ }
+
+ var totalTimeDisplay = document.getElementById('totalTime');
+ if (totalTimeDisplay) {
+ totalTimeDisplay.textContent = formatTime(maxDuration);
+ }
+}
+
+function startProgressUpdate() {
+ stopProgressUpdate();
+ function update() {
+ if (!isPlaying) return;
+ var progressBar = document.getElementById('progressBar');
+ var currentTimeDisplay = document.getElementById('currentTime');
+ if (progressBar && currentTimeDisplay) {
+ var currentTime = getCurrentPlaybackTime();
+ var effectiveDuration = maxDuration - playbackOffset;
+ if (effectiveDuration > 0 && maxDuration > 0) {
+ var progress = Math.min(currentTime / maxDuration, 1) * 100;
+ progressBar.value = progress;
+ }
+ currentTimeDisplay.textContent = formatTime(Math.min(currentTime, maxDuration));
+ }
+ progressAnimationId = requestAnimationFrame(update);
+ }
+ progressAnimationId = requestAnimationFrame(update);
+}
+
+function stopProgressUpdate() {
+ if (progressAnimationId) {
+ cancelAnimationFrame(progressAnimationId);
+ progressAnimationId = null;
+ }
+}
+
+function seekTo(position) {
+ // position is 0-100 percentage
+ if (maxDuration <= 0 || !audio_buffers || audio_buffers.length === 0) return;
+ var newOffset = (position / 100) * maxDuration;
+ playbackOffset = Math.max(0, Math.min(newOffset, maxDuration));
+ if (isPlaying) {
+ // Stop current playback and restart at new position
+ stopAllTracks();
+ // Recreate master gain
+ var currentGlobalVolume = getGlobalVolume();
+ var current_time = context.currentTime;
+ masterGainNode = context.createGain();
+ masterGainNode.connect(context.destination);
+ masterGainNode.gain.setValueAtTime(currentGlobalVolume, context.currentTime);
+ playbackStartContextTime = current_time + 0.25;
+ isPlaying = true;
+ audio_buffers.forEach(startCallback);
+ updateControlCenter(true);
+ }
+}
+
+function skipForward() {
+ if (maxDuration <= 0) return;
+ var currentTime = getCurrentPlaybackTime();
+ var newTime = Math.min(currentTime + 10, maxDuration);
+ seekTo((newTime / maxDuration) * 100);
+}
+
+function skipBackward() {
+ if (maxDuration <= 0) return;
+ var currentTime = getCurrentPlaybackTime();
+ var newTime = Math.max(currentTime - 10, 0);
+ seekTo((newTime / maxDuration) * 100);
+}
+
+function restartPlayback() {
+ playbackOffset = 0;
+ if (audio_buffers && audio_buffers.length > 0) {
+ stopAllTracks();
+ var currentGlobalVolume = getGlobalVolume();
+ var current_time = context.currentTime;
+ masterGainNode = context.createGain();
+ masterGainNode.connect(context.destination);
+ masterGainNode.gain.setValueAtTime(currentGlobalVolume, context.currentTime);
+ playbackStartContextTime = current_time + 0.25;
+ isPlaying = true;
+ audio_buffers.forEach(startCallback);
+ updateControlCenter(true);
+ }
+}
+
function setTracksFromURL() {
var params = new URLSearchParams(window.location.search);
var selectedTracks = params.get('selectedTracks');
if (selectedTracks) {
selectedTracks.split(',').forEach(function(trackId) {
- // Remove any trailing dot for legacy URLs
- trackId = trackId.replace(/\.+$/, '');
- var checkbox = document.getElementById(trackId);
- if (checkbox) {
- checkbox.checked = true;
+ // Remove any trailing dot for legacy URLs and trim whitespace
+ trackId = trackId.replace(/\.+$/, '').trim();
+ if (trackId) {
+ var checkbox = document.getElementById(trackId);
+ if (checkbox) {
+ checkbox.checked = true;
+ }
}
});
}
diff --git a/styles.css b/styles.css
index 073d17e..3b400ca 100644
--- a/styles.css
+++ b/styles.css
@@ -104,3 +104,47 @@ button:hover {
.community-presets li {
margin-bottom: 5px;
}
+
+/* Control Center */
+.control-center {
+ background-color: rgba(255, 255, 255, 0.1);
+ border: 1px solid rgba(255, 255, 255, 0.2);
+ border-radius: 8px;
+ padding: 12px 20px;
+ margin: 15px 0;
+}
+
+.control-center h4 {
+ margin-top: 0;
+ margin-bottom: 10px;
+ font-size: 1rem;
+}
+
+.control-center-row {
+ display: flex;
+ align-items: center;
+ gap: 10px;
+ flex-wrap: wrap;
+}
+
+.control-btn {
+ min-width: auto;
+ padding: 4px 10px;
+ font-size: 0.85rem;
+}
+
+.time-display {
+ color: #e0e0e0;
+ font-family: monospace;
+ font-size: 0.9rem;
+ min-width: 40px;
+ text-align: center;
+}
+
+.progress-bar-input {
+ flex: 1;
+ min-width: 120px;
+ height: 6px;
+ cursor: pointer;
+ accent-color: #007bff;
+}
diff --git a/tracks/heartsteel_late_drums.aac b/tracks/heartsteel_late_drums.aac
index bfbb4c2..e6c0f5e 100644
Binary files a/tracks/heartsteel_late_drums.aac and b/tracks/heartsteel_late_drums.aac differ
diff --git a/tracks/hyperpop_late_drums.aac b/tracks/hyperpop_late_drums.aac
deleted file mode 100644
index f0f6efc..0000000
Binary files a/tracks/hyperpop_late_drums.aac and /dev/null differ
diff --git a/tracks/illbeats_late.aac b/tracks/illbeats_late.aac
index e6c0f5e..bfbb4c2 100644
Binary files a/tracks/illbeats_late.aac and b/tracks/illbeats_late.aac differ
diff --git a/tracks/truedamage_late_drums.aac b/tracks/truedamage_late_drums.aac
index 49d2b13..f0f6efc 100644
Binary files a/tracks/truedamage_late_drums.aac and b/tracks/truedamage_late_drums.aac differ