Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
20 changes: 17 additions & 3 deletions index.html
Original file line number Diff line number Diff line change
Expand Up @@ -54,6 +54,23 @@ <h4>Global Volume:</h4>
<div class="alert alert-success" id="copyNotification" style="display:none; position: fixed; bottom: 20px; right: 20px;">
<strong>URL Copied to Clipboard</strong>
</div>
<div id="controlCenter" class="control-center" style="display: none;">
<h4>Playback Controls</h4>
<div class="control-center-row">
<button class="btn btn-sm btn-outline-light control-btn" onclick="restartPlayback()" title="Restart">
<i class="fas fa-redo"></i>
</button>
<button class="btn btn-sm btn-outline-light control-btn" onclick="skipBackward()" title="Skip Back 10s">
<i class="fas fa-backward"></i> 10s
</button>
<span id="currentTime" class="time-display">0:00</span>
<input type="range" id="progressBar" class="progress-bar-input" min="0" max="100" step="0.1" value="0" oninput="seekTo(this.value)">
<span id="totalTime" class="time-display">0:00</span>
<button class="btn btn-sm btn-outline-light control-btn" onclick="skipForward()" title="Skip Forward 10s">
10s <i class="fas fa-forward"></i>
</button>
</div>
</div>
<hr>
<div class="container">
<div class="main-content">
Expand Down Expand Up @@ -248,9 +265,6 @@ <h3>Hyperpop</h3>
<label for="hyperpop_late">late</label>
<audio id="audiohyperpop_late" src="tracks/hyperpop_late.aac" preload="none" loop></audio>
<br>
<input type="checkbox" id="hyperpop_late_drums" name="hyperpop_late_drums" class="late">
<label for="hyperpop_late_drums">late_drums</label>
<audio id="audiohyperpop_late_drums" src="tracks/hyperpop_late_drums.aac" preload="none" loop></audio>
</div>
<div class="trait">
<img src="icon/illbeats.png" alt="ILLBEATS">
Expand Down
204 changes: 181 additions & 23 deletions script.js
Original file line number Diff line number Diff line change
@@ -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'],
Expand All @@ -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 = [];
Expand All @@ -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;
}

Expand All @@ -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)
Expand All @@ -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");
}
Expand All @@ -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);
Expand All @@ -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);
Expand All @@ -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) {
Expand Down Expand Up @@ -161,6 +190,11 @@ function stopAllTracks() {
endedArray = [];
playingArray = [];
endedCallbackArray = [];
isPlaying = false;
if (progressAnimationId) {
cancelAnimationFrame(progressAnimationId);
progressAnimationId = null;
}
}

function getGlobalVolume() {
Expand All @@ -177,6 +211,7 @@ function toggleRealTime() {
stopAllTracks();
audio_buffers = [];
startCallback = null;
activeTrackElements = [];
}

function toggleTrackRealTime(trackIndex) {
Expand Down Expand Up @@ -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);
}
}

Expand All @@ -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!");
})
Expand Down Expand Up @@ -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;
}
}
});
}
Expand Down
Loading