UNPKG

audio-source-composer

Version:

Audio Source Composer

382 lines (330 loc) 12.7 kB
var http = require("http"); var https = require("https"); // var url = require("url"); const { JSDOM } = require('jsdom'); const exclude = ['png', 'bmp', 'gif', 'wav', 'zip', 'h2drumkit']; const fs = require('fs'); const SOURCE_URL = 'https://freewavesamples.com'; const WAVE_FILE_LIST = './s/.list.json'; const SAMPLE_FOLDER = './s/'; const LIBRARY_FILE = 'fws.library.json'; const LIBRARY_JSON = { "name": "FreeWaveSamples.com", "urlPrefix": "s/", "instruments": {}, "samples": {}, }; let drumInstrumentPrefixes = {}; (async () => { const libraryJSON = Object.assign({}, LIBRARY_JSON, { "instruments": {}, "samples": {} }); let waveURLs = null; if(await fileExists(WAVE_FILE_LIST)) { const fileData = await readFile(WAVE_FILE_LIST); waveURLs = JSON.parse(fileData); } if(!waveURLs) { const urls = await search(SOURCE_URL); waveURLs = urls.filter(url => url.toLowerCase().endsWith('.wav')); waveURLs.sort(); await writeToFile(WAVE_FILE_LIST, JSON.stringify(waveURLs)); // console.log("Search complete: ", waveURLs); } drumInstrumentPrefixes = {}; for(let i=0; i<waveURLs.length; i++) { const waveURL = new URL(waveURLs[i]); let fileName = waveURL.pathname.split('/').pop(); fileName = parseSampleConfig(fileName, libraryJSON.samples, libraryJSON.instruments); await downloadFile(waveURL, fileName); } for(let drumInstrumentPrefix in drumInstrumentPrefixes) { if(drumInstrumentPrefixes.hasOwnProperty(drumInstrumentPrefix)) { if(!drumInstrumentPrefix) continue; const sampleNames = drumInstrumentPrefixes[drumInstrumentPrefix].samples; // const singleDrumInstruments = drumInstrumentPrefixes[drumInstrumentPrefix].programs; for(let j=0; j<sampleNames.length; j++) { const sampleName = sampleNames[j]; if(!libraryJSON.instruments[drumInstrumentPrefix]) libraryJSON.instruments[drumInstrumentPrefix] = {samples:{}}; const drumInstrument = libraryJSON.instruments[drumInstrumentPrefix]; drumInstrument.samples[sampleName] = {}; } // for(let j=0; j<singleDrumInstruments.length; j++) { // const singleDrumInstrumentName = singleDrumInstruments[j]; // delete libraryJSON.programs[singleDrumInstrumentName]; // // } } } console.log("Writing ", LIBRARY_FILE); await writeToFile(LIBRARY_FILE, JSON.stringify(libraryJSON, null, "\t")); })(); function parseSampleConfig(fileName, sampleList, instrumentList) { fileName = fileName.replace(/\.(wav)$/, ''); let instrumentName = fileName; const sampleConfig = {}; // Find root key: let rootKey = fileName.split(/[^a-z0-9#]+/gi).pop(); if(rootKey && rootKey.match(/^[a-f][0-6]$/i)) { sampleConfig.root = rootKey.toUpperCase(); instrumentName = fileName.substr(0, fileName.length - rootKey.length) .replace(/\W+$/, ''); } instrumentName = parseDrumSampleConfig(fileName, sampleConfig, instrumentName); parseLoopSampleConfig(fileName, sampleConfig); if(!instrumentList[instrumentName]) instrumentList[instrumentName] = {}; const instrumentConfig = instrumentList[instrumentName]; if(!instrumentConfig.samples) instrumentConfig.samples = {}; instrumentConfig.samples[fileName] = {}; sampleList[fileName] = sampleConfig; parseInstrumentConfig(instrumentName, sampleList, instrumentList); return fileName; } function parseDrumSampleConfig(fileName, sampleConfig, instrumentName) { const drumSampleNotes = { 'rap-kick': 'C3', 'kick': 'C3', 'low-mid-synth-tom': 'A2', 'hi-mid-synth-tom': 'A2', 'hi-synth-tom': 'A2', 'mid-synth-tom': 'A2', 'low-synth-tom': 'A2', 'synth-tom': 'A2', 'tom': 'A2', 'snare': 'D3', 'steel-drum': 'D3', 'drum': 'D3', 'clap': 'E3', 'closed-hi-hat': 'G#2', 'closed-hi': 'G#2', 'open-hi-hat': 'G#2', 'open-hi': 'G#2', 'open': 'A#2', 'hat': 'G#2', 'cow': 'D#3', 'cowbell': 'D#3', 'splash-cymbal': 'C#3', 'crash-cymbal': 'C#3', 'ride-cymbal': 'D3', 'cymbal': 'C#3', 'crash': 'C#3', // 'bell': 'F3', 'stick': 'D3', 'ride': 'D3', 'doumbek': 'A2' }; for(let drumSampleName in drumSampleNotes) { if(drumSampleNotes.hasOwnProperty(drumSampleName)) { let pos = fileName.toLowerCase().indexOf(drumSampleName); if(pos !== -1) { sampleConfig.loop = false; sampleConfig.alias = drumSampleNotes[drumSampleName]; instrumentName = (instrumentName.substr(0, pos) .replace(/\W$/, '') || 'Generic'); const matchNumbered = fileName.match(/-\d+$/); if(matchNumbered) { instrumentName += matchNumbered[0]; } instrumentName += '-Kit'; if(!drumInstrumentPrefixes[instrumentName]) drumInstrumentPrefixes[instrumentName] = {samples:[],instruments:[]}; drumInstrumentPrefixes[instrumentName].samples.push(fileName); // drumInstrumentPrefixes[drumInstrumentPrefix].programs.push(instrumentName); break; } } } return instrumentName; } function parseLoopSampleConfig(fileName, sampleConfig) { const loopSampleNames = [ 'loop', ]; for(let i=0; i<loopSampleNames.length; i++) { const loopSampleName = loopSampleNames[i]; if(fileName.toLowerCase().indexOf(loopSampleName) !== -1) { sampleConfig.loop = true; } } } function parseInstrumentConfig(instrumentName, sampleList, instrumentList) { const instrumentConfig = instrumentList[instrumentName]; if(!instrumentConfig.samples) throw new Error("Invalid program samples"); const instrumentSamples = instrumentConfig.samples; const sampleValues = Object.values(instrumentSamples); if(sampleValues.length === 1) return; // Check for drum samples if(sampleValues.every(sampleEntry => typeof sampleEntry.alias !== 'undefined')) return; if(!instrumentName.endsWith('Kit')) { // Span out the key zones const keyNumberSamples = []; for (let sampleName in instrumentSamples) { if (instrumentSamples.hasOwnProperty(sampleName)) { const combinedSampleConfig = Object.assign({}, instrumentSamples[sampleName], sampleList[sampleName]); if (!combinedSampleConfig.root) throw new Error("Multi samples program requires keyRoot"); const keyNumber = getNoteKeyNumber(combinedSampleConfig.root); keyNumberSamples.push({keyNumber, sampleConfig: instrumentSamples[sampleName]}); } } let currentSample = 0, currentRange = [0,0]; for (let i=0; i<12*12; i++) { const {keyNumber, sampleConfig} = keyNumberSamples[currentSample]; currentRange[1] = i; sampleConfig.range = getNoteByKeyNumber(currentRange[0]) + ':' + getNoteByKeyNumber(currentRange[1]); if(!keyNumberSamples[currentSample+1]) continue; const nextKeyNumber = keyNumberSamples[currentSample+1].keyNumber; if(i > (keyNumber + nextKeyNumber) / 2) { currentSample++; currentRange = [i+1, i+1]; } } } } function getNoteKeyNumber (namedFrequency) { if(Number(namedFrequency) === namedFrequency && namedFrequency % 1 !== 0) return namedFrequency; if(!namedFrequency) return null; let octave = parseInt(namedFrequency.replace(/\D/g,'')); let freq = namedFrequency.replace(/\d/g,'').toUpperCase(); const freqs = ['C', 'C#', 'D', 'D#', 'E', 'F', 'F#', 'G', 'G#', 'A', 'A#', 'B']; let keyNumber = freqs.indexOf(freq); if(keyNumber === -1) throw new Error("Invalid note: " + namedFrequency); if (keyNumber < 3) keyNumber = keyNumber + 12 + ((octave + 1) * 12); else keyNumber = keyNumber + ((octave + 1) * 12); return keyNumber; } function getNoteByKeyNumber(keyNumber) { keyNumber = parseInt(keyNumber); const freqs = ['C', 'C#', 'D', 'D#', 'E', 'F', 'F#', 'G', 'G#', 'A', 'A#', 'B']; const octave = Math.floor(keyNumber / 12) - 2; const freq = freqs[keyNumber % 12]; return freq + octave; } async function downloadFile(url, fileName) { fileName = SAMPLE_FOLDER + fileName; if(await fileExists(fileName)) { console.log("Skipping ", url+''); return false; } console.log("Downloading ", url+''); const fileContent = await get(url); await writeToFile(fileName, fileContent); return true; } async function fileExists(path) { return new Promise((resolve, reject) => { fs.access(path, function(err) { resolve(!err); }); }); } async function readFile(path) { return new Promise((resolve, reject) => { fs.readFile(path, function(err, data) { if(err) reject(err); else resolve(data); }); }); } async function writeToFile(path, data) { return new Promise((resolve, reject) => { fs.writeFile(path, data, function(err) { if(err) reject(err); else resolve(); }); }); } async function search(startURL, options) { startURL = new URL(startURL); if(typeof options === "function") options = {callback: options}; options = Object.assign({ depth: 999 }, options || {}); const urls = [startURL+'']; await recurse(startURL, options.depth); return urls; async function recurse(sourceURL, depth) { const ext = sourceURL.pathname.split('/').pop().split('.').pop().toLowerCase(); if(exclude.indexOf(ext) !== -1) { // console.info("Skipping ", sourceURL+''); return; } console.info("Scanning ", sourceURL+''); const document = await tryGetDOM(sourceURL); const links = document.querySelectorAll('a'); for(let i=0; i<links.length; i++) { const link = links[i]; const linkUrl = new URL(link.href, sourceURL); if(sourceURL.host !== linkUrl.host) continue; if(urls.indexOf(linkUrl + '') === -1) { urls.push(linkUrl + ''); // console.log(linkUrl + ''); if(depth > 0) await recurse(linkUrl, depth-1); } } // for(let i=0; i<promises.length; i++) // await promises[i]; } } function get(options) { const protocol = options.protocol || options; let client = http; if(protocol.toLowerCase().startsWith('https')) client = https; return new Promise((resolve, reject) => { client.get(options, function (res) { // initialize the container for our data var data = []; res.on("data", (chunk) => data.push(chunk)); res.on("end", () => { const buffer = Buffer.concat(data); // console.log(buffer.toString('base64')); resolve(buffer); }); res.on("error", (err) => reject(err)); }).on("error", (err) => { reject(err); }); }) } async function getDOM(options) { for(let attempts=0; attempts<3; attempts++) try { const html = await get(options); var dom = new JSDOM(html); return dom.window.document; } catch (e) { console.error(e); } } async function tryGetDOM(options, attempts=3) { while(--attempts>0) try { const document = await getDOM(options); if(!document.querySelectorAll) throw new Error("Invalid DOM"); return document; } catch (e) { console.error(e.message || e); await new Promise((resolve, reject) => { setTimeout(() => resolve(), 10000); }); console.info("Retrying: ", options, attempts); } throw new Error("Giving up on " + options); }