UNPKG

@oletizi/audio-tools

Version:

Monorepo for hardware sampler utilities and format parsers

269 lines (232 loc) 10.1 kB
// @ts-nocheck // TODO: This file needs significant refactoring for TypeScript strict mode compliance // Tracked in Phase 2 cleanup tasks import fs from "fs/promises"; import { createWriteStream, WriteStream } from "fs"; import path, * as Path from "path"; import { mpc } from "@/lib-akai-mpc.js"; import { AkaiS56ProgramResult, Kloc, newProgramFromBuffer, Zone } from "@oletizi/sampler-devices"; import { decent } from '@/lib-decent.js'; import { newSampleFromBuffer, pad } from "@oletizi/sampler-lib"; // Temporary Progress interface until lib-jobs is migrated export interface Progress { incrementTotal(n: number): void; incrementCompleted(n: number): void; setCompleted(n: number): void; } export const nullProgress: Progress = { incrementTotal: () => {}, incrementCompleted: () => {}, setCompleted: () => {} }; // Temporary output interface until process-output is migrated interface ServerOutput { log(msg: string): void; error(e: any): void; } function newServerOutput(): ServerOutput { return { log: (msg: string) => console.log(msg), error: (e: any) => console.error(e) }; } const out = newServerOutput(); import Sample = decent.Sample; function hasher(text: string, max: number) { let hash = 0 for (let i = 0; i < text.length && i <= max; i++) { let char = text.charCodeAt(i); hash = ((hash << 5) - hash) + char; hash = hash & hash; } return Math.abs(hash) } export async function decent2Sxk(infile, outdir, outstream = process.stdout, progress: Progress = nullProgress) { const rv = { data: [], errors: [] } as AkaiS56ProgramResult const ddir = path.dirname(infile) const programBasename = Path.parse(infile).name const dprogram = await decent.newProgramFromBuffer(await fs.readFile(infile)) let outbuf = Buffer.alloc(1024 * 1000) // XXX: This is a data corruption bug waiting to happen let fstream: WriteStream let sampleCount = 0 dprogram.groups.forEach(g => sampleCount += g.samples.length) progress.incrementTotal(sampleCount + 1) // one progress increment for each sample to convert for (const group of dprogram.groups) { const sxkProgram = newProgramFromBuffer(await fs.readFile(path.join('data', 'DEFAULT.AKP'))) const keyspans: { string: { sample: decent.Sample, basename: string } } = {} const hash = hasher(group.name + programBasename + group.name, 12) for (let i = 0; i < group.samples.length; i++) { const sample = group.samples[i] let keyspan if (!Number.isNaN(sample.loNote) && !Number.isNaN(sample.hiNote)) { keyspan = sample.loNote + '-' + sample.hiNote } else { keyspan = sample.rootNote } if (!keyspans[keyspan]) { keyspans[keyspan] = [] } const samplePath = path.join(ddir, sample.path) let basename = hash + '-' + pad(i + 1, 3); const outname = basename + '.WAV' const outpath = path.join(outdir, outname); keyspans[keyspan].push({ sample: sample, basename: basename }) try { let wav = newSampleFromBuffer(await fs.readFile(samplePath)) // Chop sample and write to disk if (!Number.isNaN(sample.start) && !Number.isNaN(sample.end)) { wav = wav.trim(sample.start, sample.end) } // Set the root note in the smpl metadata of the wav file. S5000 uses it to calculate playback if (sample.rootNote) { wav.setRootNote(sample.rootNote) } wav = wav.to16Bit() wav = wav.to441() wav.cleanup() outstream.write(`TRANSLATE: writing trimmed sample to: ${outpath}\n`) fstream = createWriteStream(outpath) const bytesWritten = await wav.writeToStream(fstream) outstream.write(`TRANSLATE: wrote ${bytesWritten} bytes to ${outpath}\n`) } catch (e) { rv.errors.push(e) } finally { progress.incrementCompleted(1) if (fstream) { fstream.close((e => { if (e) rv.errors.push(e) })) } } } const keygroups = [] for (const keyspanName of Object.getOwnPropertyNames(keyspans)) { let sampleDescriptors = keyspans[keyspanName] const [low, high] = keyspanName.split('-').map(c => Number.parseInt(c)) const keygroup = { kloc: { lowNote: low, highNote: high } as Kloc } // const max = Math.min(4, samples.length) sampleDescriptors = sampleDescriptors.sort((a, b) => { return a.sample.highVelocity - b.sample.highVelocity }) const size = Math.min(sampleDescriptors.length, 4) for (let i = 0; i < size; i++) { const sampleDescriptor = sampleDescriptors[i] const sampleName = sampleDescriptor.basename const sample: Sample = sampleDescriptor.sample let highVelocity = 127 let lowVelocity = 0 if (i != 0 && !Number.isNaN(sample.hiVel)) { // if this isn't the first (loudest) sample AND its high velocity is set, set high velocity to the // sample high velocity highVelocity = sample.hiVel } if (i != size - 1) { // if this isn't tte last (quietest) sample AND its low velocity is set, set the low velocity to the // sample low velocity lowVelocity = sample.loVel } const zone = {} as Zone zone.sampleName = sampleName // NOTE: Don't we need this zone tuning now that the root note is set in the sample wav file metadata // zone.semiToneTune = C3 - sample.rootNote zone.highVelocity = highVelocity zone.lowVelocity = lowVelocity keygroup['zone' + (i + 1)] = zone } keygroups.push(keygroup) } const mods = { keygroupCount: Object.getOwnPropertyNames(keyspans).length, keygroups: keygroups } sxkProgram.apply(mods) const bufferSize = sxkProgram.writeToBuffer(outbuf, 0) let outfile = path.join(outdir, programBasename + '.' + group.name + '.AKP'); outstream.write(`Writing program file: ${outfile}\n`) await fs.writeFile(outfile, Buffer.copyBytesFrom(outbuf, 0, bufferSize)) progress.incrementCompleted(1) rv.data.push(newProgramFromBuffer(await fs.readFile(outfile))) } return rv } export async function mpc2Sxk(infile, outdir, outstream = process.stdout, progress: Progress = nullProgress) { progress.setCompleted(0) const mpcbuf = await fs.readFile(infile) const mpcdir = path.dirname(infile) const mpcProgram = mpc.newProgramFromBuffer(mpcbuf) const sxkbuf = await fs.readFile('data/DEFAULT.AKP') const sxkProgram = newProgramFromBuffer(sxkbuf) const snapshot = new Date().getMilliseconds() const mods = { keygroupCount: mpcProgram.layers.length, keygroups: [] } const inbuf = Buffer.alloc(1024 * 10000) const outbuf = Buffer.alloc(inbuf.length) let sliceCounter = 1 let midiNote = 60 let detune = 0 progress.incrementTotal(mpcProgram.layers.length + 1) // for (const layer of mpcProgram.layers) { for (let i = 0; i < mpcProgram.layers.length; i++) { const layer = mpcProgram.layers[i] // chop & copy sample const samplePath = path.join(mpcdir, layer.sampleName + '.WAV') const basename = layer.sampleName.substring(0, 8) const sliceName = `${basename}-${sliceCounter++}-${snapshot}` try { let buf = await fs.readFile(samplePath); let sliceStart = 0 let sliceEnd = 0 let sliceData; try { sliceData = mpc.newSampleSliceDataFromBuffer(buf) } catch (e) { out.error(e) } // Check the sample for embedded slice data out.log(`CHECKING SAMPLE FOR EMBEDDED SLICE DATA...`) if (sliceData && sliceData.slices.length >= i) { const slice = sliceData.slices[i] sliceStart = slice.start sliceEnd = slice.end } else { sliceStart = layer.sliceStart sliceEnd = layer.sliceEnd } const sample = newSampleFromBuffer(buf) let trimmed = sample.trim(sliceStart, sliceEnd) trimmed = trimmed.to16Bit() const bytesWritten = trimmed.write(outbuf, 0) let outpath = path.join(outdir, sliceName + '.WAV'); outstream.write(`TRANSLATE: writing trimmed sample to: ${outpath}\n`) await fs.writeFile(outpath, Buffer.copyBytesFrom(outbuf, 0, bytesWritten)) } catch (err) { // no joy out.error(err) } finally { progress.incrementCompleted(1) } mods.keygroups.push({ kloc: { lowNote: midiNote, highNote: midiNote++, semiToneTune: detune-- }, zone1: { sampleName: sliceName } }) } sxkProgram.apply(mods) const bufferSize = sxkProgram.writeToBuffer(outbuf, 0) let outfile = path.join(outdir, mpcProgram.programName + '.AKP'); outstream.write(`Writing program file: ${outfile}\n`) await fs.writeFile(outfile, Buffer.copyBytesFrom(outbuf, 0, bufferSize)) progress.incrementCompleted(1) }