1#!/usr/bin/env node 2 3import cowsay from 'cowsay'; 4import { createReadStream, existsSync, writeFileSync } from 'fs'; 5import { readdir } from 'fs/promises'; 6import http from 'http'; 7import { join, resolve, sep } from 'path'; 8import readline from 'readline'; 9import os from 'os'; 10 11const LOG = !!process.env.LOG || false; 12const PORT = process.env.PORT || 5432; 13const VALID_AUDIO_EXTENSIONS = ['wav', 'mp3', 'ogg']; 14 15const isAudioFile = (f) => { 16 const ext = f.split('.').slice(-1)[0].toLowerCase(); 17 return VALID_AUDIO_EXTENSIONS.includes(ext); 18}; 19 20async function getFilesInDirectory(directory) { 21 let files = []; 22 const dirents = await readdir(directory, { withFileTypes: true }); 23 for (const dirent of dirents) { 24 const fullPath = join(directory, dirent.name); 25 if (dirent.isDirectory()) { 26 if (dirent.name.startsWith('.')) { 27 LOG && console.warn(`ignore hidden folder: ${fullPath}`); 28 continue; 29 } 30 try { 31 const subFiles = (await getFilesInDirectory(fullPath)).filter(isAudioFile); 32 files = files.concat(subFiles); 33 LOG && console.log(`${dirent.name} (${subFiles.length})`); 34 } catch (err) { 35 LOG && console.warn(`skipped due to error: ${fullPath}`); 36 } 37 } else { 38 isAudioFile(fullPath) && files.push(fullPath); 39 } 40 } 41 return files; 42} 43 44async function getBanks(directory, flat = false) { 45 let files = await getFilesInDirectory(directory); 46 let banks = {}; 47 directory = directory.split(sep).join('/'); 48 files = files.map((path) => { 49 path = path.split(sep).join('/'); 50 const subDir = path.replace(directory, ''); 51 const subDirFlat = subDir.replaceAll('/', '_').slice(1); // remove initial underscore 52 const subDirFlatStem = subDirFlat.replace(/\.[^.]+$/, ''); // remove extension 53 let bank = flat ? subDirFlatStem : path.split('/').slice(-2)[0]; 54 banks[bank] = banks[bank] || []; 55 banks[bank].push(subDir); 56 return subDir; 57 }); 58 return { banks, files }; 59} 60 61const args = process.argv.slice(2); 62 63function getArgValue(flag) { 64 const i = args.indexOf(flag); 65 if (i !== -1) { 66 const nextIsFlag = args[i + 1]?.startsWith('--') ?? true; 67 if (nextIsFlag) return true; 68 return args[i + 1]; 69 } 70} 71 72function getInput(query) { 73 const rl = readline.createInterface({ 74 input: process.stdin, 75 output: process.stdout, 76 }); 77 78 return new Promise((resolve) => 79 rl.question(query, (response) => { 80 rl.close(); 81 resolve(response); 82 }), 83 ); 84} 85 86let directory = getArgValue('--dir') || process.cwd(); 87directory = resolve(directory); 88if (args.includes('--json')) { 89 const { banks } = await getBanks(directory, getArgValue('--flat')); 90 const json = JSON.stringify(banks); 91 const outFile = resolve(directory, 'strudel.json'); 92 if (existsSync(outFile)) { 93 const answer = await getInput(`Warning: File already exists at ${outFile}. Overwrite? (y/N): `); 94 if (answer.toLowerCase() !== 'y') { 95 console.log('Aborted.'); 96 process.exit(0); 97 } 98 } 99 writeFileSync(outFile, json, 'utf8'); 100 console.log(`Wrote json to ${outFile}`); 101 process.exit(0); 102} 103 104console.log( 105 cowsay.say({ 106 text: 'welcome to @strudel/sampler', 107 e: 'oO', 108 T: 'U ', 109 }), 110); 111 112const server = http.createServer(async (req, res) => { 113 res.setHeader('Access-Control-Allow-Origin', '*'); 114 const { banks, files } = await getBanks(directory, getArgValue('--flat')); 115 if (req.url === '/') { 116 res.setHeader('Content-Type', 'application/json'); 117 return res.end(JSON.stringify(banks)); 118 } 119 let subpath = decodeURIComponent(req.url); 120 const filePath = join(directory, subpath.split('/').join(sep));
console.log('GET:', filePath);
123 const isFound = existsSync(filePath); 124 if (!isFound) { 125 res.statusCode = 404; 126 res.end('File not found'); 127 return; 128 } 129 const readStream = createReadStream(filePath); 130 readStream.on('error', (err) => { 131 res.statusCode = 500; 132 res.end('Internal server error'); 133 console.error(err); 134 }); 135 readStream.pipe(res); 136});
138const IP_ADDRESS = '0.0.0.0'; 139let IP; 140const networkInterfaces = os.networkInterfaces(); 141 142Object.keys(networkInterfaces).forEach((key) => { 143 networkInterfaces[key].forEach((networkInterface) => { 144 if (networkInterface.family === 'IPv4' && !networkInterface.internal) { 145 IP = networkInterface.address; 146 } 147 }); 148}); 149 150server.listen(PORT, IP_ADDRESS, () => { 151 console.log(`@strudel/sampler is now serving audio files from: 152 ${directory} 153 154To use them in the Strudel REPL, run: 155 samples('http://localhost:${PORT}') 156 157Or on a machine in the same network: 158 ${IP ? `samples('http://${IP}:${PORT}')` : `Unable to determine server's IP address.`} 159`); 160});