bitwig-multisamplegen
Advanced tools
Comparing version 1.1.0 to 1.2.0
@@ -227,3 +227,4 @@ #!/usr/bin/env node | ||
const ALLOWED_EXTENSIONS = ['wav', 'aif', 'mp3', 'ogg']; | ||
const COMPRESSION_TYPE = 'DEFLATE'; | ||
const DEFAULT_COMPRESSION_TYPE = 'DEFLATE'; | ||
const DEFAULT_SELECTION_VALUE = DEFAULT_VELOCITY; | ||
const ALL_NOTES = ['C', 'C#', 'D', 'D#', 'E', 'F', 'F#', 'G', 'G#', 'A', 'A#', 'B']; | ||
@@ -239,2 +240,10 @@ | ||
const getSelection = parsed => { | ||
if (parsed.selection === undefined) { | ||
return DEFAULT_SELECTION_VALUE; | ||
} | ||
return parsed.selection; | ||
}; | ||
const getNoteNumber = parsed => { | ||
@@ -257,3 +266,3 @@ const note = parsed.note.toUpperCase(); | ||
const getFileNamesByNoteNumber = filenames => { | ||
const getFileNamesByNoteNumber = (filenames, valueMode) => { | ||
const result = {}; | ||
@@ -271,3 +280,3 @@ filenames.forEach(parsed => { | ||
const n = Number(k); | ||
result[n] = ramda.sortBy(x => getVelocity(x), result[n]); | ||
result[n] = ramda.sortBy(x => Number(x.initialNumber), result[n]); | ||
}); | ||
@@ -277,21 +286,66 @@ return result; | ||
const getSampleFromParsed = parsed => { | ||
const getSampleFromParsed = (allParsed, valueMode) => { | ||
let selectionMin = 0; | ||
let velocityMin = 0; | ||
return parsed.reduce((acc, parsed) => { | ||
const velocityMax = getVelocity(parsed); | ||
const key = getNoteNumber(parsed); | ||
const sample = { | ||
name: parsed.fullname, | ||
key, | ||
lowKey: key, | ||
highKey: key, | ||
velocityMin, | ||
velocityMax | ||
}; | ||
velocityMin = velocityMax + 1; | ||
return [...acc, sample]; | ||
const grouped = ramda.groupBy(filename => `${filename.note.toLowerCase()}${filename.octave}-${filename.initialNumber}`, allParsed); | ||
const groupedKeys = Object.keys(grouped); | ||
const maxIndex = groupedKeys.length - 1; | ||
return groupedKeys.reduce((acc, k, index) => { | ||
const parsedFileNames = grouped[k]; | ||
if (!parsedFileNames.length) { | ||
return acc; | ||
} | ||
const firstParsed = parsedFileNames[0]; | ||
const velocityMax = getVelocity(firstParsed); | ||
const selectionMax = getSelection(firstParsed); | ||
let overrideVelocityMax = false; | ||
let overrideSelectionMax = false; | ||
if (valueMode === 'Velocity' && index === maxIndex) { | ||
overrideVelocityMax = true; | ||
} else if (valueMode === 'Selection' && index === maxIndex) { | ||
overrideSelectionMax = true; | ||
} | ||
const samples = parsedFileNames.map(parsed => { | ||
const key = getNoteNumber(parsed); | ||
const sample = { | ||
name: parsed.fullname, | ||
key, | ||
lowKey: key, | ||
highKey: key, | ||
velocityMin: valueMode === 'Velocity' ? velocityMin : 0, | ||
velocityMax: valueMode === 'Velocity' ? velocityMax : 127, | ||
selectionMin: valueMode === 'Selection' ? selectionMin : 0, | ||
selectionMax: valueMode === 'Selection' ? selectionMax : 127, | ||
prefix: parsed.prefix, | ||
postfix: parsed.postfix, | ||
extension: parsed.extension, | ||
initialNumber: parsed.initialNumber | ||
}; | ||
if (overrideSelectionMax) { | ||
sample.selectionMax = 127; | ||
} | ||
if (overrideVelocityMax) { | ||
sample.velocityMax = 127; | ||
} | ||
return sample; | ||
}); | ||
if (valueMode === 'Velocity') { | ||
velocityMin = velocityMax + 1; | ||
} else if (valueMode === 'Selection') { | ||
selectionMin = selectionMax + 1; | ||
} | ||
return [...acc, ...samples]; | ||
}, []); | ||
}; | ||
const parseFileName = filename => { | ||
const parseFileName = (filename, valueMode) => { | ||
const res = filename.match(/^(.*)([abcdefgABCDEFG]#?)([0-9])-?(\d{1,3})?(.*)\.(.*)/); | ||
@@ -308,5 +362,7 @@ | ||
octave: Number(res[3]), | ||
velocity: Number(res[4]), | ||
velocity: valueMode === 'Velocity' ? Number(res[4]) : 127, | ||
selection: valueMode === 'Selection' ? Number(res[4]) : 127, | ||
postfix: res[5], | ||
extension: res[6] | ||
extension: res[6], | ||
initialNumber: res[4] || '' | ||
}; | ||
@@ -337,4 +393,4 @@ | ||
const getAllSamples = filenames => { | ||
const parsedFilenames = filenames.map(parseFileName).filter(parsed => !!parsed); | ||
const getAllSamples = (filenames, valueMode) => { | ||
const parsedFilenames = filenames.map(filename => parseFileName(filename, valueMode)).filter(parsed => !!parsed); | ||
const filenamesByNote = getFileNamesByNoteNumber(parsedFilenames); | ||
@@ -344,3 +400,3 @@ let result = []; | ||
const n = Number(k); | ||
result = [...result, ...getSampleFromParsed(filenamesByNote[n])]; | ||
result = [...result, ...getSampleFromParsed(filenamesByNote[n], valueMode)]; | ||
}); | ||
@@ -374,8 +430,21 @@ return result; | ||
const generateSampleXml = sample => { | ||
const generateSampleXml = (sample, keyfade, valueMode, valueFade, spreadSelectionsFadeValue, roundRobinEnabled) => { | ||
const selectionFade = spreadSelectionsFadeValue === undefined ? valueFade : spreadSelectionsFadeValue; | ||
const keyLowFade = sample.lowKey > 0 ? Math.min(keyfade, Math.abs(sample.key - sample.lowKey)) : 0; | ||
const keyHighFade = sample.highKey < 127 ? Math.min(keyfade, Math.abs(sample.highKey - sample.key)) : 0; | ||
const maximumFadeVelocity = Math.trunc(Math.abs(sample.velocityMax - sample.velocityMin) / 2); | ||
const velocityLowFade = valueMode === 'Velocity' && sample.velocityMin > 0 ? Math.min(valueFade, maximumFadeVelocity) : 0; | ||
const velocityHighFade = valueMode === 'Velocity' && sample.velocityMax < 127 ? Math.min(valueFade, maximumFadeVelocity) : 0; | ||
const maximumFadeSelection = Math.trunc(Math.abs(sample.selectionMax - sample.selectionMin) / 2); | ||
const selectLowFade = valueMode === 'Selection' && sample.selectionMin > 0 ? Math.min(selectionFade, maximumFadeSelection) : 0; | ||
const selectHighFade = valueMode === 'Selection' && sample.selectionMax < 127 ? Math.min(selectionFade, maximumFadeSelection) : 0; | ||
const param1 = sample.key; | ||
const param2 = valueMode === 'Velocity' ? sample.velocityMax : sample.selectionMax; | ||
const param3 = 0; | ||
const zoneLogic = roundRobinEnabled ? 'round-robin' : 'always-play'; | ||
return ` | ||
<sample file="${sample.name}" gain="0.00" parameter-1="0.0000" parameter-2="0.0000" parameter-3="0.0000" reverse="false" sample-start="0.000" sample-stop="-1" zone-logic="always-play"> | ||
<key low="${sample.lowKey}" high="${sample.highKey}" root="${sample.key}" track="1.0000" tune="0.00"/> | ||
<velocity low="${sample.velocityMin}" high="${sample.velocityMax}" /> | ||
<select low="0" high="127"/> | ||
<sample file="${sample.name}" gain="0.00" parameter-1="${param1}" parameter-2="${param2}" parameter-3="${param3}" reverse="false" sample-start="0.000" sample-stop="-1" zone-logic="${zoneLogic}"> | ||
<key low-fade="${keyLowFade}" high-fade="${keyHighFade}" low="${sample.lowKey}" high="${sample.highKey}" root="${sample.key}" track="1.0000" tune="0.00"/> | ||
<velocity low-fade="${velocityLowFade}" high-fade="${velocityHighFade}" low="${sample.velocityMin}" high="${sample.velocityMax}" /> | ||
<select low-fade="${selectLowFade}" high-fade="${selectHighFade}" low="${sample.selectionMin}" high="${sample.selectionMax}"/> | ||
<loop fade="0.0000" mode="off" start="0.000" /> | ||
@@ -386,3 +455,3 @@ </sample> | ||
const generateMultiSampleXml = (instrumentName, author, samples) => { | ||
const generateMultiSampleXml = (instrumentName, author, samples, keyfade, valueMode, valueFade, spreadSelectionsFadeValue, roundRobinEnabled) => { | ||
let xmlResult = `<?xml version="1.0" encoding="UTF-8"?> | ||
@@ -397,3 +466,3 @@ <multisample name="${instrumentName}"> | ||
samples.forEach(sample => { | ||
xmlResult = xmlResult + generateSampleXml(sample); | ||
xmlResult = xmlResult + generateSampleXml(sample, keyfade, valueMode, valueFade, spreadSelectionsFadeValue, roundRobinEnabled); | ||
}); | ||
@@ -434,9 +503,9 @@ xmlResult = xmlResult + '</multisample>'; | ||
const transformSamples = allSamples => { | ||
const stretchNotes = (allSamples, valueMode) => { | ||
let result = []; | ||
const groupedSamples = ramda.groupBy(x => String(x.velocityMax), allSamples); | ||
Object.keys(groupedSamples).forEach(velocity => { | ||
groupedSamples[velocity] = ramda.sortBy(sample => sample.key, groupedSamples[velocity]); | ||
const samples = groupedSamples[velocity]; | ||
const nextSamples = groupedSamples[velocity].map((sample, index) => { | ||
const groupedSamples = ramda.groupBy(x => String(`${x.prefix}-${x.initialNumber}-${x.postfix}.${x.extension}`), allSamples); | ||
Object.keys(groupedSamples).forEach(k => { | ||
groupedSamples[k] = ramda.sortBy(sample => sample.key, groupedSamples[k]); | ||
const samples = groupedSamples[k]; | ||
const nextSamples = groupedSamples[k].map((sample, index) => { | ||
const computedSample = computeLowKey(samples, sample, index); | ||
@@ -450,3 +519,49 @@ return computeHighKey(samples, computedSample, index); | ||
const generateZipFile = function (pathdir, samples, givenPackageName) { | ||
const transformSamples = (givenSamples, keyfade, valueMode, valueFade, spreadSelectionsFadeValue) => { | ||
const allSamples = stretchNotes(givenSamples); | ||
const transformedSamples = allSamples.map(sample => { | ||
return { ...sample, | ||
lowKey: Math.max(0, sample.lowKey - keyfade), | ||
highKey: Math.min(127, sample.highKey + keyfade) | ||
}; | ||
}).map(sample => { | ||
if (valueMode === 'Velocity') { | ||
return { ...sample, | ||
velocityMin: Math.max(0, sample.velocityMin - valueFade), | ||
velocityMax: Math.min(127, sample.velocityMax + valueFade) | ||
}; | ||
} else if (valueMode === 'Selection') { | ||
return { ...sample, | ||
selectionMin: Math.max(0, sample.selectionMin - valueFade), | ||
selectionMax: Math.min(127, sample.selectionMax + valueFade) | ||
}; | ||
} | ||
return sample; | ||
}); | ||
if (spreadSelectionsFadeValue !== undefined) { | ||
const resultSamples = []; | ||
const grouped = ramda.groupBy(s => `${s.key}${s.initialNumber}`, transformedSamples); | ||
Object.keys(grouped).forEach(k => { | ||
const group = grouped[k]; | ||
if (group.length) { | ||
const ratio = 127 / group.length; | ||
group.forEach((sample, index) => { | ||
const newSample = { ...sample, | ||
selectionMin: Math.ceil(Math.max(0, Math.min(127, index * ratio - spreadSelectionsFadeValue))), | ||
selectionMax: Math.floor(Math.max(0, Math.min(127, (index + 1) * ratio + spreadSelectionsFadeValue))) | ||
}; | ||
resultSamples.push(newSample); | ||
}); | ||
} | ||
}); | ||
return resultSamples; | ||
} | ||
return transformedSamples; | ||
}; | ||
const generateZipFile = function (pathdir, samples, givenPackageName, keyfade, compression, valueMode, valueFade, spreadSelectionsFadeValue, roundRobinEnabled) { | ||
try { | ||
@@ -475,4 +590,4 @@ let _exit2 = false; | ||
const zip = new JSZip(); | ||
zip.file(MULTISAMPLE_FILE, generateMultiSampleXml(givenPackageName, AUTHOR_NAME, samples), { | ||
compression: COMPRESSION_TYPE | ||
zip.file(MULTISAMPLE_FILE, generateMultiSampleXml(givenPackageName, AUTHOR_NAME, samples, keyfade, valueMode, valueFade, spreadSelectionsFadeValue, roundRobinEnabled), { | ||
compression | ||
}); | ||
@@ -486,3 +601,3 @@ const spinner = ora('Copy sample files...').start(); | ||
zip.file(sample.name, buffer, { | ||
compression: COMPRESSION_TYPE | ||
compression | ||
}); | ||
@@ -499,2 +614,13 @@ }); | ||
const getNbDuplicateNotes = samples => { | ||
let counter = 0; | ||
const grouped = ramda.groupBy(s => `${s.key}${s.initialNumber}`, samples); | ||
Object.keys(grouped).forEach(k => { | ||
if (grouped[k].length > 1) { | ||
counter += grouped[k].length; | ||
} | ||
}); | ||
return counter; | ||
}; | ||
const app = function () { | ||
@@ -510,19 +636,97 @@ try { | ||
const samples = getAllSamples(fileNames); | ||
return Promise.resolve(inquirer.prompt([{ | ||
type: 'list', | ||
name: 'valueMode', | ||
message: 'Choose the desired value mode', | ||
default: 'Velocity', | ||
choices: ['Velocity', 'Selection', 'Round-Robin'] | ||
}])).then(function (valueModePayload) { | ||
const valueMode = valueModePayload.valueMode; | ||
const samples = getAllSamples(fileNames, valueMode); | ||
if (samples.length === 0) { | ||
ora(`Warning: no valid sample files found in directory`).fail(); | ||
return; | ||
} | ||
if (samples.length === 0) { | ||
ora(`Warning: no valid sample files found in directory`).fail(); | ||
return; | ||
} | ||
return Promise.resolve(inquirer.prompt([{ | ||
type: 'input', | ||
name: 'packageName', | ||
message: 'Multisample Package name', | ||
validate: input => { | ||
return input && input.length > 0; | ||
}, | ||
default: '' | ||
}])).then(function (info) { | ||
return Promise.resolve(generateZipFile(pathdir, transformSamples(samples), info.packageName)).then(function () {}); | ||
const nbDuplicateNotes = getNbDuplicateNotes(samples); | ||
const inquirerQuestions = [{ | ||
type: 'input', | ||
name: 'packageName', | ||
message: 'Multisample Package name', | ||
validate: input => { | ||
return input && input.length > 0; | ||
}, | ||
default: '' | ||
}, valueMode !== 'Round-Robin' ? { | ||
type: 'input', | ||
name: 'keyfade', | ||
message: 'Pitch fade value', | ||
default: 0, | ||
validate(value) { | ||
const valid = !isNaN(parseFloat(value)); | ||
return valid || 'Please enter a number'; | ||
}, | ||
filter: value => { | ||
const valid = !isNaN(parseFloat(value)); | ||
return valid ? Math.abs(Number(value)) : ''; | ||
} | ||
} : null, valueMode !== 'Round-Robin' ? { | ||
type: 'input', | ||
name: 'valueFade', | ||
message: `${valueMode} fade value`, | ||
default: 0, | ||
validate(value) { | ||
const valid = !isNaN(parseFloat(value)); | ||
return valid || 'Please enter a number'; | ||
}, | ||
filter: value => { | ||
const valid = !isNaN(parseFloat(value)); | ||
return valid ? Math.abs(Number(value)) : ''; | ||
} | ||
} : null, valueMode !== 'Selection' && valueMode !== 'Round-Robin' && nbDuplicateNotes > 0 ? { | ||
type: 'confirm', | ||
name: 'spreadSelection', | ||
message: `${nbDuplicateNotes} duplicate notes found, Would you want to spread samples selection ?`, | ||
default: false | ||
} : null]; | ||
return Promise.resolve(inquirer.prompt(inquirerQuestions.filter(x => Boolean(x)))).then(function (info) { | ||
const inquirerQuestions2 = [info.spreadSelection === true && valueMode !== 'Round-Robin' ? { | ||
type: 'input', | ||
name: 'spreadSelectionFade', | ||
message: 'spread selections fade value', | ||
default: 0, | ||
validate(value) { | ||
const valid = !isNaN(parseFloat(value)); | ||
return valid || 'Please enter a number'; | ||
}, | ||
filter: value => { | ||
const valid = !isNaN(parseFloat(value)); | ||
return valid ? Math.abs(Number(value)) : ''; | ||
} | ||
} : null, { | ||
type: 'confirm', | ||
name: 'compression', | ||
message: 'Enable compression ?', | ||
default: true | ||
}]; | ||
return Promise.resolve(inquirer.prompt(inquirerQuestions2.filter(x => Boolean(x)))).then(function (info2) { | ||
const spreadSelectionsFadeValue = info2.spreadSelectionFade; | ||
const compression = info2.compression ? DEFAULT_COMPRESSION_TYPE : undefined; | ||
const roundRobinAutoEnabled = !info2.spreadSelectionFade && !info.keyfade && !info.valueFade; | ||
if (roundRobinAutoEnabled && valueMode !== 'Round-Robin') { | ||
ora('Round-Robin mode automatically enabled (becaause no fades are set)').warn(); | ||
} | ||
const roundRobinEnabled = roundRobinAutoEnabled || valueMode === 'Round-Robin'; | ||
return Promise.resolve(generateZipFile(pathdir, transformSamples(samples, info.keyfade || 0, valueMode, info.valueFade, spreadSelectionsFadeValue), info.packageName, info.keyfade || 0, compression, valueMode, info.valueFade || 0, spreadSelectionsFadeValue, roundRobinEnabled)).then(function () {}); | ||
}); | ||
}); | ||
}); | ||
@@ -529,0 +733,0 @@ }); |
{ | ||
"name": "bitwig-multisamplegen", | ||
"version": "1.1.0", | ||
"version": "1.2.0", | ||
"description": "Generate multisample xml file from samples ", | ||
@@ -31,3 +31,3 @@ "author": "guillaumearm", | ||
"lint:js": "eslint --fix --cache --ext .ts,.tsx ./src", | ||
"lint:prettier": "prettier --check --write \"**/*\" --end-of-line auto", | ||
"lint:prettier": "prettier --check --write \"**/*.{ts, js, json, tsx, jsx}\" --end-of-line auto", | ||
"start": "microbundle-crl watch --no-compress --format cjs", | ||
@@ -34,0 +34,0 @@ "build": "microbundle-crl --no-compress --format cjs", |
Sorry, the diff of this file is not supported yet
License Policy Violation
LicenseThis package is not allowed per your license policy. Review the package's license to ensure compliance.
Found 1 instance in 1 package
License Policy Violation
LicenseThis package is not allowed per your license policy. Review the package's license to ensure compliance.
Found 1 instance in 1 package
98564
649