UNPKG

bitwig-nks-preview-generator

Version:

Streaming convert NKSF files to preview audio with using Bitwig Studio.

316 lines (299 loc) 8.8 kB
const fs = require('fs'), os = require('os'), path = require('path'), { execSync, spawn } = require('child_process'), rimraf = require('rimraf'), { TmpDir } = require('temp-file'), chalk = require('chalk'), wavUtil = require('./wav-util'), tmpDirPrefix = require('../package.json').name, log = require('./logger')('bitwig-studio-local'), stdioStyle = chalk.keyword('orange'), tmpDir = new TmpDir() const wait = msec => { return new Promise(resolve => setTimeout(resolve, msec)) } /** * application specific local interface for Bitwig Studio * @class */ module.exports = class BitwigStudio { /** * default launch() options. * @static * @property * @type {Object} */ static get defaultOptions() { return { bitwig: this.defaultExecuteFile(), project: undefined, args: undefined, createTemporaryProjectFolder: false, Client: BitwigStudio } } /** * launch() options descriptions * @static * @property * @type {Object} */ static get optionsDescriptions() { return { bitwig: 'Bitwig Studio execution file path', project: 'Bitwig Studio Project file path', args: 'launch arguments', createTemporaryProjectFolder: 'create a temporary project folder', Client: 'Inherited Client class.' } } /** * Return platform is WSL or not. * @static * @property * @return {boolean} */ static isWSL() { if (typeof this._isWSL === 'undefined') { this._isWSL = process.platform === 'linux' && os.release().includes('Microsoft') && fs.readFileSync('/proc/version', 'utf8').includes('Microsoft') } return this._isWSL } /** * Return a environment value with consideration of WSL. * @static * @method * @param {String} name - environment value name. * @return {String} */ static wslenv(name) { if (this.isWSL()) { this._wslenv = this._wslenv || {} if (!this._wslenv[name]) { this._wslenv[name] = execSync(`cmd.exe /C "echo %${name}%"`) .toString().replace(/[\r\n]+$/g, '') } return this._wslenv[name] } return process.env[name] } /** * Translate WSL path to Windows path. * @static * @method * @param {String} wslPath - WSL path * @return {String} - Windows path */ static wsl2winPath(wslPath) { return execSync(`wslpath -w "${wslPath}"`) .toString().replace(/[\r\n]+$/g, '') } /** * Translate Windows path to WSL path. * @static * @method * @param {String} wslPath - WSL path * @return {String} - Windows path */ static win2wslPath(winPath) { return execSync(`wslpath "${winPath}"`) .toString().replace(/[\r\n]+$/g, '') } /** * Return a platform specific default Bitwig Studio execute faile path. * @static * @method * @return {String} */ static defaultExecuteFile() { if (!this._defaultExecuteFile) { this._defaultExecuteFile = (() => { switch (process.platform) { case 'win32': return path.join(process.env.PROGRAMFILES, 'Bitwig Studio', 'Bitwig Studio.exe') case 'darwin': return '/Applications/Bitwig Studio.app/Contents/MacOS/BitwigStudio' case 'linux': if (this.isWSL()) { return path.join(this.win2wslPath(this.wslenv('PROGRAMFILES')), 'Bitwig Studio', 'Bitwig Studio.exe') } else { return '/user/bin/bitwig-studio' } default: throw new Error(`Unsupported Platform:[${process.platform}].`) } })() } return this._defaultExecuteFile } /** * Return a platform specific default Bitwig Studio Extension folder path. * @static * @method * @return {String} */ static defaultExtensionDir() { if (!this._defaultExtensionDir) { this._defaultExtensionDir = (() => { switch (process.platform) { case 'win32': return path.join(os.homedir(), 'Documents', 'Bitwig Studio', 'Extensions') case 'darwin': return path.join(os.homedir(), 'Documents', 'Bitwig Studio', 'Extensions') case 'linux': if (this.isWSL()) { return path.join(this.win2wslPath(this.wslenv('USERPROFILE')), 'Documents', 'Bitwig Studio', 'Extensions') } else { return path.join(os.homedir(), 'Bitwig Studio', 'Extensions') } default: throw new Error(`Unsupported Platform:[${process.platform}].`) } })() } return this._defaultExtensionDir } /** * create Bitwig Sudio Temporary Project. * @static * @async * @method * @param {String} project - source project file path. * @return {String} - temporary project path */ static async createTemporaryProject(project) { const tempDirPath = await tmpDir.createTempDir(tmpDirPrefix), tempProject = path.join(tempDirPath, path.basename(project)) fs.copyFileSync(project, tempProject) log.debug('temporary project file was created. file:', tempProject) const dotBitwigProject = path.join(tempDirPath, '.bitwig-project') fs.closeSync(fs.openSync(dotBitwigProject, 'w')) log.debug('temporary .bitwig-project file was created. file:', dotBitwigProject) return tempProject } /** * Launch a Bitwig Studio application. * @static * @async * @method * @param {Object} options * @return {BitwigStudio} - local interface. */ static async launch(options) { const opts = Object.assign({}, this.defaultOptions, options) let projectFile = opts.project if (opts.project && opts.createTemporaryProjectFolder) { projectFile = await this.createTemporaryProject(opts.project) } let args = [] if (opts.project) { args.push(this.isWSL() ? this.wsl2winPath(projectFile) : projectFile) } if (opts.args) { args = args.concat(opts.args) } const childProcess = spawn(opts.bitwig, args, { detached: true, stdio: opts.debug ? 'pipe' : 'ignore' }) if (childProcess.stdout) { childProcess.stdout.on('data', data => { process.stdout.write(stdioStyle(data)) }) } if (childProcess.stderr) { childProcess.stderr.on('data', data => { process.stderr.write(stdioStyle(data)) }) } return new opts.Client(childProcess, projectFile, opts) } /** * Constructor. * @constructor * @param {child_process} bitwig * @param {String} project - project file path. * @param {Object} options */ constructor(bitwig, project, options) { this.process = bitwig this.options = options if (project) { this.bounceFolder = path.join(path.dirname(project), 'bounce') } this._closed = false const ctx = this this.process.on('close', code => { log.info(`Bitwig Stduio process exited with code ${code}`) ctx._closed = true }) } /** * Return a process is closed or not. * @property * @type {boolean} */ get closed() { return this._closed } /** * remove bounce folder * @deprecated * @method */ cleanBounceFolder() { if (!this.bounceFolder) { throw new Error('bounceFolder is undefined.') } rimraf.sync(this.bounceFolder) log.debug('cleanup bounce folder:', this.bounceFolder) } /** * remove bounce .wav files * @deprecated * @method */ cleanBounceWavFiles() { if (!this.bounceFolder) { throw new Error('bounceFolder is undefined.') } rimraf.sync(path.join(this.bounceFolder, '*.wav')) log.debug('cleanup bounce folder:', this.bounceFolder) } /** * Read a .wav from bounce folder. * @async * @method * @param clipName {String} - bounced clip name * @param timeout {Number} - timeout millis, default = 6000 * @return {Buffer} - bounce .wav file content. */ async readBounceWavFile(clipName, timeout = 6000) { const interval = 100 var remain = timeout - 250 // initial wait await wait(250) while (remain > 0) { await wait(interval) remain -= interval const bouncedWav = path.join(this.bounceFolder, clipName + '.wav') try { // validate & read .wav file const buffer = await wavUtil.validateRead(bouncedWav) log.debug('readBounceWavFile()', 'completed.', bouncedWav) return buffer } catch (err) { // this err is within expectations. if (remain > 0) { log.debug('readBounceWavFile()', 'wav validation failed and retrying.', err) } else { log.debug('readBounceWavFile()', 'wav validation failed.', err) } } } throw new Error('readBounceWavFile() operation timeout.') } }