Created
April 3, 2026 09:57
-
-
Save wyozi/c04cc2002027c371ce6715a3dbbee9d9 to your computer and use it in GitHub Desktop.
midi_scan.js
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| 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