cybermax / blipwren /app.js
CyberMax-tools's picture
Deploy blipwren to the hub
0ed77a7 verified
Raw History Blame Contribute Delete
9.7 kB
/* Blipwren SFX player: Web Audio, no libraries. © 2026 CyberMax */
(function () {
"use strict";
var CHECKOUT = "https://buy.polar.sh/polar_cl_AUuQ9JEVj98iOYnwpGKLQAqxklIRWpColFOBF1GXPFh"; // Polar checkout link (revenue/packs/polar/blipwren/polar.json)
var BUNDLE = "https://buy.polar.sh/polar_cl_0VZrsUmgkJkVTDpll5E0TTHLI9eloGE5pmDyS4D9P8L"; // Indie Game Dev Bundle checkout link
var $ = function (s) { return document.querySelector(s); };
var ctx = null, gain = null, meta = null, cat = "all", query = "";
var buffers = {}; // sprite id or "free:<id>" -> AudioBuffer
var tiles = {}; // sound id -> {el, canvas, s}
var current = null, playAllToken = 0;
function audio() {
if (!ctx) {
var AC = window.AudioContext || window.webkitAudioContext;
ctx = new AC();
gain = ctx.createGain();
gain.gain.value = $("#vol").value / 100;
gain.connect(ctx.destination);
}
return ctx;
}
function toast(msg) {
var t = $("#toast"); t.textContent = msg; t.classList.add("on");
clearTimeout(toast._t); toast._t = setTimeout(function () { t.classList.remove("on"); }, 3200);
}
function decode(url) {
return fetch(url).then(function (r) { if (!r.ok) throw new Error(url + " " + r.status); return r.arrayBuffer(); })
.then(function (ab) {
return new Promise(function (res, rej) { audio().decodeAudioData(ab, res, rej); });
});
}
function load(s) { // full quality for free sounds, category sprite for the rest
var key = s.free ? "free:" + s.id : s.sprite;
if (buffers[key]) return Promise.resolve(buffers[key]);
var url = s.free ? "audio/free/" + s.id + ".wav" : "audio/preview/" + s.sprite.toLowerCase() + ".wav";
buffers[key] = decode(url).then(function (b) { buffers[key] = b; return b; });
return buffers[key];
}
function play(s) {
audio();
if (ctx.state === "suspended") ctx.resume();
return Promise.resolve(load(s)).then(function (buf) {
if (current) { try { current.src.stop(); } catch (e) {} current.tile.classList.remove("playing"); }
var src = ctx.createBufferSource();
src.buffer = buf;
if ($("#vary").checked) src.playbackRate.value = 1 + (Math.random() * 0.1 - 0.05);
src.connect(gain);
var tile = tiles[s.id].el;
tile.classList.add("playing");
var dur = s.free ? buf.duration : s.len;
if (s.free) src.start(0); else src.start(0, s.at, s.len);
var me = current = { src: src, tile: tile };
src.onended = function () { tile.classList.remove("playing"); if (current === me) current = null; };
return dur / src.playbackRate.value;
}).catch(function (e) { toast("Couldn't play that sound: " + e.message); return 0; });
}
function drawWave(s) {
var t = tiles[s.id]; if (!t) return;
var buf = buffers[s.free ? "free:" + s.id : s.sprite];
if (!buf || typeof buf.then === "function") return;
var c = t.canvas, dpr = window.devicePixelRatio || 1;
var w = c.clientWidth || 180, h = c.clientHeight || 28;
c.width = Math.round(w * dpr); c.height = Math.round(h * dpr);
var g = c.getContext("2d"); g.scale(dpr, dpr);
var data = buf.getChannelData(0), sr = buf.sampleRate;
var a = s.free ? 0 : Math.floor(s.at * sr), n = s.free ? data.length : Math.floor(s.len * sr);
var cols = Math.max(20, Math.floor(w / 2)), step = n / cols;
g.fillStyle = s.free ? "#34d399" : "#FFB020";
for (var i = 0; i < cols; i++) {
var peak = 0, from = a + Math.floor(i * step), to = a + Math.floor((i + 1) * step);
for (var j = from; j < to; j += 4) { var v = Math.abs(data[j] || 0); if (v > peak) peak = v; }
var bh = Math.max(1, peak * (h - 2));
g.fillRect(i * 2, (h - bh) / 2, 1.4, bh);
}
}
function label(c) { return c === "all" ? "All" : meta.categories.filter(function (x) { return x.id === c; })[0].label; }
function buildChips() {
var box = $("#cats"), list = ["all"].concat(meta.categories.map(function (c) { return c.id; }));
list.forEach(function (c) {
var b = document.createElement("button");
b.className = "chip" + (c === cat ? " on" : "");
b.dataset.cat = c;
var n = c === "all" ? meta.count : meta.sounds.filter(function (s) { return s.cat === c; }).length;
b.innerHTML = label(c) + '<span class="n">' + n + "</span>";
b.addEventListener("click", function () {
cat = c;
Array.prototype.forEach.call(box.children, function (x) { x.classList.toggle("on", x.dataset.cat === c); });
filter();
});
box.appendChild(b);
});
}
function buildGrid() {
var grid = $("#grid");
meta.sounds.forEach(function (s) {
var el = document.createElement("button");
el.className = "snd"; el.type = "button";
el.setAttribute("aria-label", "Play " + s.name + (s.free ? " (free)" : ""));
el.innerHTML = '<span class="nm"></span><span class="ds"></span><span class="dur"></span><canvas aria-hidden="true"></canvas>';
el.querySelector(".nm").textContent = s.name;
el.querySelector(".ds").textContent = s.desc;
el.querySelector(".dur").textContent = s.dur.toFixed(2) + "s";
if (s.free) {
var foot = document.createElement("span"); foot.className = "foot";
var badge = document.createElement("span"); badge.className = "free-badge"; badge.textContent = "FREE";
foot.appendChild(badge);
var dl = document.createElement("a"); dl.className = "dl"; dl.href = "audio/free/" + s.id + ".wav";
dl.download = "blipwren_" + s.id + ".wav"; dl.textContent = "WAV ↓";
dl.addEventListener("click", function (e) { e.stopPropagation(); });
foot.appendChild(dl);
el.appendChild(foot);
}
el.addEventListener("click", function () { playAllToken++; play(s); });
tiles[s.id] = { el: el, canvas: el.querySelector("canvas"), s: s };
grid.appendChild(el);
});
}
function shown() {
return meta.sounds.filter(function (s) {
return (cat === "all" || s.cat === cat) &&
(!query || (s.name + " " + s.desc + " " + s.cat).toLowerCase().indexOf(query) >= 0);
});
}
function filter() {
var vis = {}; shown().forEach(function (s) { vis[s.id] = 1; });
Object.keys(tiles).forEach(function (id) { tiles[id].el.hidden = !vis[id]; });
var n = Object.keys(vis).length;
$("#status").textContent = n ? n + " sound" + (n === 1 ? "" : "s") + " · tap to play" : "No sounds match “" + query + "”.";
Object.keys(vis).forEach(function (id) { drawWave(tiles[id].s); });
}
function playAll() {
var token = ++playAllToken, list = shown(), i = 0;
function next() {
if (token !== playAllToken || i >= list.length) return;
var s = list[i++];
tiles[s.id].el.scrollIntoView({ block: "nearest", behavior: "smooth" });
play(s).then(function (d) { setTimeout(next, (d + 0.25) * 1000); });
}
next();
}
function preload() { // decode sprites (works while the AudioContext is still suspended) to draw waveforms
var jobs = meta.categories.map(function (c) { return c.id; });
meta.sounds.forEach(function (s) { if (s.free) jobs.push(s); });
var i = 0;
function step() {
if (i >= jobs.length) { window.__blipwrenReady = true; return; }
var j = jobs[i++], s = typeof j === "string" ? meta.sounds.filter(function (x) { return x.sprite === j && !x.free; })[0] : j;
if (!s) return step();
Promise.resolve(load(s)).then(function () {
meta.sounds.forEach(function (x) { if ((x.free ? "free:" + x.id : x.sprite) === (s.free ? "free:" + s.id : s.sprite)) drawWave(x); });
}).catch(function () {}).then(step);
}
step();
}
function tabs() {
Array.prototype.forEach.call(document.querySelectorAll("#tabs .chip"), function (b) {
b.addEventListener("click", function () {
Array.prototype.forEach.call(document.querySelectorAll("#tabs .chip"), function (x) {
x.classList.toggle("on", x === b); x.setAttribute("aria-selected", x === b ? "true" : "false");
});
Array.prototype.forEach.call(document.querySelectorAll("pre[data-pane]"), function (p) { p.hidden = p.dataset.pane !== b.dataset.tab; });
});
});
}
function links() {
var buy = $("#buy");
if (/^https:\/\//.test(CHECKOUT)) buy.href = CHECKOUT;
else { buy.textContent = "Coming soon"; buy.removeAttribute("href"); buy.classList.add("ghost"); }
var bundle = $("#bundle");
if (/^https:\/\//.test(BUNDLE)) bundle.href = BUNDLE; else bundle.parentNode.hidden = true;
}
$("#vol").addEventListener("input", function () { if (gain) gain.gain.value = this.value / 100; });
$("#q").addEventListener("input", function () { query = this.value.trim().toLowerCase(); filter(); });
$("#playall").addEventListener("click", playAll);
document.addEventListener("keydown", function (e) { if (e.key === "Escape") { playAllToken++; if (current) try { current.src.stop(); } catch (x) {} } });
tabs(); links();
if (/[?&]thanks=1/.test(location.search)) setTimeout(function () { toast("Thanks for your order! Your download link is in the Polar receipt email."); }, 800);
fetch("sounds.json").then(function (r) { return r.json(); }).then(function (m) {
meta = m; buildChips(); buildGrid(); filter(); preload();
var hash = decodeURIComponent((location.hash.match(/^#s=([\w]+)/) || [])[1] || "");
if (hash && tiles[hash]) tiles[hash].el.scrollIntoView({ block: "center" });
}).catch(function () { $("#status").textContent = "Couldn't load the sound list. Please reload the page."; });
window.addEventListener("resize", function () { if (meta) filter(); });
})();