Greasy Fork is available in English.
ローカルの音声・動画ファイルをAAに変換するスクリプト
// ==UserScript==
// @name mp4→AA,mp3→ visualizer
// @namespace https://greasyfork.org/users/yourname
// @version 1.0.0
// @description ローカルの音声・動画ファイルをAAに変換するスクリプト
// @author umaimann
// @match *://*/*
// @grant none
// @run-at document-end
// @license MIT
// ==/UserScript==
(() => {
const WIDTH = 120;
const HEIGHT = 40;
const ASCII = "@%#*+=-:. ";
let state = "select"; // select | start | play
let currentFile = null;
let rafId = null;
/* ---------- UI ---------- */
const input = document.createElement("input");
input.type = "file";
input.style.display = "none";
document.body.append(input);
const pre = document.createElement("pre");
Object.assign(pre.style, {
position: "fixed",
top: "50%",
left: "50%",
transform: "translate(-50%, -50%)",
background: "black",
color: "#0f0",
fontFamily: "monospace",
fontSize: "8px",
lineHeight: "0.6em",
padding: "6px",
margin: 0,
userSelect: "none",
cursor: "pointer",
zIndex: 1e6
});
document.body.append(pre);
/* ---------- DRAW HELPERS ---------- */
function drawCenteredLabel(label) {
let out = "";
const mid = Math.floor(HEIGHT / 2);
for (let y = HEIGHT; y >= 0; y--) {
if (y === mid) {
const pad = Math.floor((WIDTH - label.length) / 2);
out +=
" ".repeat(pad) +
label +
" ".repeat(WIDTH - pad - label.length) +
"\n";
} else {
out += " ".repeat(WIDTH) + "\n";
}
}
pre.textContent = out;
}
/* ---------- INITIAL SCREEN ---------- */
drawCenteredLabel("### SELECT MEDIA FILE ###");
/* ---------- AUDIO ---------- */
function startAudio(file) {
const audio = new Audio(URL.createObjectURL(file));
audio.volume = 1;
const audioCtx = new AudioContext();
const analyser = audioCtx.createAnalyser();
analyser.fftSize = 256;
const freq = new Uint8Array(analyser.frequencyBinCount);
const src = audioCtx.createMediaElementSource(audio);
src.connect(analyser);
analyser.connect(audioCtx.destination);
audio.play();
function loop() {
analyser.getByteFrequencyData(freq);
let out = "";
for (let y = HEIGHT; y >= 0; y--) {
for (let x = 0; x < WIDTH; x++) {
const v = (freq[x % freq.length] / 255) * HEIGHT;
out += v >= y ? "#" : " ";
}
out += "\n";
}
pre.textContent = out;
rafId = requestAnimationFrame(loop);
}
loop();
}
/* ---------- VIDEO ---------- */
function startVideo(file) {
const video = document.createElement("video");
video.src = URL.createObjectURL(file);
video.volume = 1;
video.playsInline = true;
video.style.display = "none";
document.body.append(video);
const canvas = document.createElement("canvas");
const ctx = canvas.getContext("2d");
const lum = (r, g, b) => 0.2126*r + 0.7152*g + 0.0722*b;
video.onplay = () => {
function loop() {
if (video.paused || video.ended) return;
if (!video.videoWidth) {
rafId = requestAnimationFrame(loop);
return;
}
const scale = video.videoWidth / WIDTH;
const h = Math.floor(video.videoHeight / scale);
canvas.width = WIDTH;
canvas.height = h;
ctx.drawImage(video, 0, 0, WIDTH, h);
const d = ctx.getImageData(0, 0, WIDTH, h).data;
let out = "";
for (let y = 0; y < h; y++) {
for (let x = 0; x < WIDTH; x++) {
const i = (y * WIDTH + x) * 4;
const v = lum(d[i], d[i+1], d[i+2]);
out += ASCII[Math.floor(v / 255 * (ASCII.length - 1))];
}
out += "\n";
}
pre.textContent = out;
rafId = requestAnimationFrame(loop);
}
loop();
};
video.play();
}
/* ---------- INTERACTION ---------- */
pre.onclick = async () => {
if (state === "select") {
input.click();
return;
}
if (state === "start") {
state = "play";
pre.style.cursor = "default";
if (currentFile.type.startsWith("audio/")) {
const ctx = new AudioContext();
await ctx.resume();
startAudio(currentFile);
} else {
startVideo(currentFile);
}
}
};
input.onchange = () => {
const file = input.files[0];
if (!file) return;
currentFile = file;
state = "start";
drawCenteredLabel("### START ###");
};
console.log("AA SELECT → AA START → PLAY ready.");
})();