Skip to content

Instantly share code, notes, and snippets.

@wyozi
Created April 3, 2026 09:57
Show Gist options
  • Select an option

  • Save wyozi/c04cc2002027c371ce6715a3dbbee9d9 to your computer and use it in GitHub Desktop.

Select an option

Save wyozi/c04cc2002027c371ce6715a3dbbee9d9 to your computer and use it in GitHub Desktop.
midi_scan.js
const { spawn } = require("child_process");
const path = require("path");
const readline = require("readline");
const os = require("os");
const fs = require("fs");
// ── config ──────────────────────────────────────────────
const SCAN_DIR = process.env.SCAN_DIR || path.join(".", "scans");
const SCAN_FORMAT = "png";
const SCAN_DPI = 300;
const SCAN_MODE = "Color";
const DEBOUNCE_MS = 2000;
const IS_WIN = os.platform() === "win32";
// ── state ───────────────────────────────────────────────
let scanning = false;
let lastTrigger = 0;
// ensure output dir
if (!fs.existsSync(SCAN_DIR)) fs.mkdirSync(SCAN_DIR, { recursive: true });
// ── platform-specific scan commands ─────────────────────
function scanLinux(filename) {
return spawn("scanimage", [
`--mode=${SCAN_MODE}`,
`--resolution=${SCAN_DPI}`,
`--format=${SCAN_FORMAT}`,
`--output-file=${filename}`,
]);
}
function scanWindows(filename) {
const ps = `
$deviceManager = New-Object -ComObject WIA.DeviceManager
$device = $null
foreach ($info in $deviceManager.DeviceInfos) {
if ($info.Type -eq 1) { # ScannerDeviceType = 1
$device = $info.Connect()
break
}
}
if (-not $device) {
Write-Error "No WIA scanner found."
exit 1
}
$item = $device.Items[1]
# 6146 = Color Intent (1=Color), 6147 = H-DPI, 6148 = V-DPI
foreach ($prop in $item.Properties) {
switch ($prop.PropertyID) {
6146 { $prop.Value = 1 }
6147 { $prop.Value = ${SCAN_DPI} }
6148 { $prop.Value = ${SCAN_DPI} }
}
}
$image = $item.Transfer("{B96B3CAF-0728-11D3-9D7B-0000F81EF32E}") # PNG GUID
$image.SaveFile("${filename.replace(/\\/g, "\\\\")}")
Write-Host "Saved."
`;
return spawn("powershell", [
"-NoProfile",
"-ExecutionPolicy", "Bypass",
"-Command", ps,
]);
}
// ── scanner ─────────────────────────────────────────────
function startScan() {
const now = Date.now();
if (scanning) {
console.log(" Scan already in progress, ignoring.");
return;
}
if (now - lastTrigger < DEBOUNCE_MS) return;
lastTrigger = now;
scanning = true;
const timestamp = new Date().toISOString().replace(/[:.]/g, "-");
const filename = path.join(SCAN_DIR, `scan_${timestamp}.${SCAN_FORMAT}`);
console.log(`\n>> Starting color scan -> ${filename}`);
const proc = IS_WIN ? scanWindows(filename) : scanLinux(filename);
proc.stdout.on("data", (d) => process.stdout.write(d));
proc.stderr.on("data", (d) => process.stderr.write(d));
proc.on("close", (code) => {
scanning = false;
if (code === 0) {
console.log(`>> Scan saved: ${filename}\n`);
} else {
console.log(`!! Scanner exited with code ${code}\n`);
}
});
proc.on("error", (err) => {
scanning = false;
if (IS_WIN) {
console.error(`!! Failed to run PowerShell: ${err.message}`);
console.error(" Make sure your scanner is connected and WIA-compatible.\n");
} else {
console.error(`!! Failed to run scanimage: ${err.message}`);
console.error(" Install SANE: sudo apt install sane-utils\n");
}
});
}
// ── midi (optional) ─────────────────────────────────────
let midiAvailable = false;
try {
const midi = require("midi");
const input = new midi.Input();
const portCount = input.getPortCount();
if (portCount > 0) {
console.log("\nMIDI inputs:");
for (let i = 0; i < portCount; i++) {
console.log(` [${i}] ${input.getPortName(i)}`);
}
let port = 0;
for (let i = 0; i < portCount; i++) {
if (input.getPortName(i).toLowerCase().includes("kontrol")) {
port = i;
break;
}
}
console.log(`Opened: ${input.getPortName(port)}`);
input.openPort(port);
midiAvailable = true;
input.on("message", (_dt, msg) => {
const [status, data1, data2] = msg;
const type = status & 0xf0;
const ch = (status & 0x0f) + 1;
if (type === 0x90 && data2 > 0) {
console.log(`NoteOn ch:${ch} note:${data1} vel:${data2}`);
} else if (type === 0x80 || (type === 0x90 && data2 === 0)) {
console.log(`NoteOff ch:${ch} note:${data1}`);
return;
} else if (type === 0xb0) {
console.log(`CC ch:${ch} cc:${data1} val:${data2}`);
} else {
console.log(`MIDI 0x${status.toString(16)} ${data1} ${data2}`);
}
startScan();
});
process.on("SIGINT", () => {
input.closePort();
process.exit();
});
}
} catch {
// midi not installed or no devices
}
// ── cli ─────────────────────────────────────────────────
if (!midiAvailable) {
console.log("\nNo MIDI device found (or `midi` package not installed).");
console.log("Falling back to CLI mode.\n");
}
const rl = readline.createInterface({
input: process.stdin,
output: process.stdout,
});
function prompt() {
rl.question(
midiAvailable
? 'Type "scan" or twist a knob > '
: 'Type "scan" + enter to start > ',
(answer) => {
const cmd = answer.trim().toLowerCase();
if (cmd === "scan" || cmd === "s") {
startScan();
} else if (cmd === "quit" || cmd === "q") {
process.exit();
} else if (cmd) {
console.log(' ("scan" or "s" to scan, "q" to quit)');
}
prompt();
}
);
}
const backend = IS_WIN ? "WIA (PowerShell)" : "SANE (scanimage)";
console.log(`\nScans -> ${path.resolve(SCAN_DIR)}`);
console.log(`${SCAN_DPI}dpi | ${SCAN_MODE} | ${SCAN_FORMAT} | ${backend}\n`);
prompt();
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment