UNPKG

gulp-bitwig-rewrite-meta

Version:
725 lines (677 loc) 21.6 kB
(function() { var $, BufferReader, BufferWriter, IS_UCS2_LE, PLUGIN_NAME, PluginError, _, parseMetadata, path, replaceMetadata, replacePresetChunk1, rewriteMeta, swapBytes, through, validateData, indexOf = [].indexOf; path = require('path'); through = require('through2'); _ = require('underscore'); PluginError = require('plugin-error'); PLUGIN_NAME = 'bitwig-rewrite-meta'; // ucs2 encoding endian IS_UCS2_LE = (Buffer.from('a', 'ucs2'))[0]; // bwpreset chunk1 // ---------------------------------------------- // Polysynth Acido.bwpreset (1.1 BETA 1) // chunk identifier: 00000561 // --------------------------------- // item identifier: 00001423 (unknown) // value type: 05 (byte) // value: 00 // --------------------------------- // item identifier: 0000150a (unknown) // value type: 05 (byte) // value: 00 // --------------------------------- // item identifier: 00001421 (unknown) // value type: 09 (32bit) // value: 00000040 // --------------------------------- // item identifier: 000002b9 (unknown) // value type: 08 (string) // string size: 00000000 // --------------------------------- // item identifier 000012de (preset_name) // value type: 08 (string) // string size: 00000005 // value: 416369646f ("Acido") // --------------------------------- // item identifier: 0000 009a (device_name) // value type: 08 (string) // string size: 00000009 // value: 506f6c7973796e7468 ("Polysynth") // --------------------------------- // item identifer: 0000009b (device_creator) // value type: 08 (string) // string size: 00000006 // value: 426974776967 ("Bitwig") // --------------------------------- // item identifier: 0000009c (device_type) // value type: 08 (string) // string size: 0000000b // value: 496e737472756d656e7473 ("Instrument") // --------------------------------- // item identifier: 0000009d (unknown) // value type: 01 (byte) // value: 02 // --------------------------------- // item identifier: 0000009e (creator) // value type: 08 (string) // string size: 00000005 // value: 436c616573 ("Claes") // --------------------------------- // item identifier: 0000009f (comment) // value type: 08 (string) // string size: 00000000 // --------------------------------- // item identifier: 000000a1 (category) // value type: 08 (string) // string size: 00000004 // value: 42617373 ("Bass") // --------------------------------- // item identifier: 000000a2 (tags) // value type: 08 (string) // string size: 0000000d // value: 6861726d6f6e6963206d6f6e6f ("harmonic mon") // --------------------------------- // item identifier 000000a3 (unknown) end of meta // value type: 05 (byte) // value: 01 // --------------------------------- // item identifier: 0000137e (unknown) // value type: 05 (byte) // value: 01 // --------------------------------- // .... don't need any more // ---------------------------------------------- // Spire BA Agress Dub 02.bwpreset (1.0.10) // chunk identifier: 000001a5 // --------------------------------- // item identifier: 000002b9 (unknown) // value identifier: 08 (string) // string size: 00000000 // --------------------------------- // item identifier: 000012de (preset_name) // value type: 08 (string) // string size: 00000000 // --------------------------------- // item identifier: 0000009a (device_name) // value type: 08 (string) // string size: 00000005 // value: 5370697265 ("Spire") // --------------------------------- // item identifier: 0000009b (device_creator) // value type: 08 (string) // string size: 0000000c // value: 52657665616c20536f756e64 ("Reveal Sound") // --------------------------------- // item identifier: 0000009c (device_type) // value type: 08 (string) // string size: 00000005 // value: 53796e7468 ("Synth") // --------------------------------- // item identifier: 0000009d (unknown) // value type: 01 (byte) // value: 02 // --------------------------------- // item identifier: 0000009e (creator) // value type: 08 (string) // string size: 00000008 // value: 466163746f727931 ("Factory1") // --------------------------------- // item identifier: 0000009f (comment) // value type: 08 (string) // string size: 00000000 // --------------------------------- // item identifier: 000000a1 (category) // value type: 08 (string) // string size: 00000004 // value: 42617373 ("Bass") // --------------------------------- // item identifier: 000000a2 (tags) // value type: 08 (string) // string size: 00000000 // --------------------------------- // item identifier: 000000a3 (unknwon) end of meta // value type: 05 (byte) // value: 01 // --------------------------------- // .... don't need any more // constants $ = { magic: 'BtWg', metaId: 'meta', presetType: { type1: 0x000001a5, type2: 0x00000561 }, valueType: { byte_1: 0x01, int16: 0x02, int32_1: 0x03, // since 1.2 some preset file's revision_no use 32bit int byte_2: 0x05, double: 0x07, string: 0x08, int32_2: 0x09, byte_array: 0x0d, string_array: 0x19 }, protectedMetaItem: { device_name: 0x009a, device_creator: 0x009b, device_category: 0x009c }, metaItem: { name: 0x12de, creator: 0x009e, comment: 0x009f, preset_category: 0x00a1, tags: 0x00a2 }, endOfMeta: 0x00a3, // supported header format headers: [ { regexp: /^BtWg[0-9a-f]{12}([0-9a-f]{8})0{8}([0-9a-f]{8})\u0000\u0000\u0000\u0004\u0000\u0000\u0000\u0004meta/, size: 52, contentAddress: 16, zipContentAddress: 32 }, { regexp: /^BtWg[0-9a-f]{12}([0-9a-f]{8})0{8}([0-9a-f]{8})00\u0000\u0000\u0000\u0004\u0000\u0000\u0000\u0004meta/, size: 54, contentAddress: 16, zipContentAddress: 32 }, { regexp: /^BtWg[0-9a-f]{12}([0-9a-f]{8})0{28}([0-9a-f]{8})\u0000\u0000\u0000\u0004\u0000\u0000\u0000\u0004meta/, size: 72, contentAddress: 16, zipContentAddress: 52 } ] }; module.exports = function(data) { return through.obj(function(file, enc, cb) { var error, obj, rewrite, rewrited; rewrited = false; rewrite = (err, data) => { var err2; if (rewrited) { this.emit('error', new PluginError(PLUGIN_NAME, 'duplicate callback')); return; } rewrited = true; if (err) { this.emit('error', new PluginError(PLUGIN_NAME, err)); return cb(); } try { rewriteMeta(file, data); this.push(file); } catch (error1) { err2 = error1; this.emit('error', new PluginError(PLUGIN_NAME, err2)); } return cb(); }; if (!file) { rewrite('Files can not be empty'); return; } if (file.isNull()) { this.push(file); return cb(); } if (file.isStream()) { rewrite('Streaming not supported'); return; } if (file.isBuffer()) { if (_.isFunction(data)) { try { obj = data(file, parseMetadata(file), rewrite); } catch (error1) { error = error1; rewrite(error); } if (data.length <= 2) { return rewrite(void 0, obj); } } else { return rewrite(void 0, data); } } }); }; // rewrite metadata // ------------------------------------- // -file src file // -data function or object for rewrite rewriteMeta = function(file, data) { var content_offset, dirname, extname, headerData, headerFormat, headerStr, new_metadata, reader, writer, zip_content_offset; data = validateData(data); // analyze header headerStr = file.contents.toString('ascii', 0, 80); headerData = void 0; headerFormat = $.headers.find(function(fmt) { return headerData = headerStr.match(fmt.regexp); }); if (!headerFormat) { throw new Error(`Invalid file: unknown header format. file:${file.path} header:${file.contents.toString('hex', 0, 80)}`); } // content data offset content_offset = parseInt(headerData[1], 16); // zip archive offset zip_content_offset = parseInt(headerData[2], 16); reader = new BufferReader(file.contents); writer = new BufferWriter; reader.position(headerFormat.size); new_metadata = replaceMetadata(reader, writer, data); // chunk1 reader.position(content_offset); writer.push(reader.mark()); // write new content offset address to header writer.writeHexInt(writer.tell(), headerFormat.contentAddress); if (new_metadata.type === 'application/bitwig-preset') { replacePresetChunk1(reader, writer, data); } // has zipped content if (zip_content_offset) { reader.position(zip_content_offset); writer.push(reader.mark()); // write zipped content offset address to header writer.writeHexInt(writer.tell(), headerFormat.zipContentAddress); } writer.push(reader.end()); // setup output file extname = path.extname(file.path); if (data.name) { new_metadata.name = data.name; dirname = path.dirname(file.path); file.path = path.join(dirname, data.name + extname); } else { new_metadata.name = path.basename(file.path, extname); } file.contents = writer.buffer(); return file.data = new_metadata; }; // parse metadata chunk // return JSON object to explain metadata of original source file. parseMetadata = function(file) { var extname, headerData, headerFormat, headerStr, i, key, reader, ret, size, value, valueType; // analyze header headerStr = file.contents.toString('ascii', 0, 80); headerData = void 0; headerFormat = $.headers.find(function(fmt) { return headerData = headerStr.match(fmt.regexp); }); if (!headerFormat) { throw new Error(`Invalid file: unknown header format. file:${file.path} header:${file.contents.toString('hex', 0, 80)}`); } reader = new BufferReader(file.contents); reader.position(headerFormat.size); extname = path.extname(file.path); ret = { file: file.path, name: path.basename(file.path, extname) }; // iterate metadata items while (reader.readInt32() === 1) { // read key kength key = reader.readString(); if (!key) { throw new Error(`Invalid file: metadata item name can not be empty. file:${file.path}`); } valueType = reader.readByte(); value = void 0; switch (valueType) { case $.valueType.string: value = reader.readString(); if (key === 'tags') { value = value ? value.split(' ') : []; } break; case $.valueType.int16: value = reader.readInt16(); break; case $.valueType.int32_1: value = reader.readInt32(); break; case $.valueType.double: value = reader.readDouble(); break; case $.valueType.byte_array: value = reader.readBytes(); break; case $.valueType.string_array: size = reader.readInt32(); value = (function() { var j, ref, results; results = []; for (i = j = 0, ref = size; (0 <= ref ? j < ref : j > ref); i = 0 <= ref ? ++j : --j) { results.push(reader.readString()); } return results; })(); break; default: throw new Error(`Unsupported file format: unknown value type. file:${file.path} key:${key} valueType:${valueType}`); } ret[key] = value; } return ret; }; // reprace metadata chunk // return JSON object to explain metadata replaceMetadata = function(reader, writer, data) { var i, key, new_metadata, size, value, valueType; new_metadata = {}; // iterate metadata items while (reader.readInt32() === 1) { // read key kength key = reader.readString(); if (!key) { throw new Error("Invalid file: metadata item name can not be empty."); } valueType = reader.readByte(); value = void 0; switch (valueType) { case $.valueType.string: if ((indexOf.call(_.keys($.metaItem), key) >= 0) && (indexOf.call(_.keys(data), key) >= 0)) { writer.push(reader.mark()); value = data[key]; if (key === 'tags') { writer.pushString(value.join(' ')); } else { writer.pushString(value); } reader.readString(); reader.mark(); } else { value = reader.readString(); if (key === 'tags') { value = value ? value.split(' ') : []; } } break; case $.valueType.int16: value = reader.readInt16(); break; case $.valueType.int32_1: value = reader.readInt32(); break; case $.valueType.double: value = reader.readDouble(); break; case $.valueType.byte_array: value = reader.readBytes(); break; case $.valueType.string_array: size = reader.readInt32(); value = (function() { var j, ref, results; results = []; for (i = j = 0, ref = size; (0 <= ref ? j < ref : j > ref); i = 0 <= ref ? ++j : --j) { results.push(reader.readString()); } return results; })(); break; default: throw new Error(`Unsupported file format: unknown value type. key: ${key} valueType:${valueType}`); } new_metadata[key] = value; } return new_metadata; }; // reprace chunk1 (.bwpreset only) replacePresetChunk1 = function(reader, writer, data) { var chunkId, itemId, key, oldValue, results, value, valueType; chunkId = reader.readInt32(); results = []; // iterate chunk1 items while ((itemId = reader.readInt32()) !== $.endOfMeta) { value = void 0; valueType = reader.readByte(); key = _.findKey($.metaItem, function(v, k, o) { return v === itemId; }); if (key && key in data) { if (valueType !== $.valueType.string) { throw new Error(`Unsupported file format: unknow value type. valueType:${valueType}`); } writer.push(reader.mark()); oldValue = reader.readString(); value = key === 'tags' ? data.tags.join(' ') : data[key]; if (key === 'name' && oldValue === '') { // old preset file dosen't have name value = ''; } writer.pushString(value); results.push(reader.mark()); } else { switch (valueType) { case $.valueType.byte_1: results.push(value = reader.readByte()); break; case $.valueType.byte_2: results.push(value = reader.readByte()); break; case $.valueType.string: results.push(value = reader.readString()); break; case $.valueType.int32_2: results.push(value = reader.readInt32()); break; default: throw new Error(`Unsupported File Format: unknown value type. itemId:${itemId} valueType:${valueType}`); } } } return results; }; //---------------------------------------- // validate data object for rewrite //---------------------------------------- validateData = function(data) { var j, keys, len1, ref, tag; data = data || {}; keys = _.keys(data); if ((indexOf.call(keys, 'name') >= 0) && !_.isString(data.name)) { throw new Error(`option name must be string. name: ${data.name}`); } if ((indexOf.call(keys, 'creator') >= 0) && !_.isString(data.creator)) { throw new Error(`option creator must be string. creator: ${data.creator}`); } if ((indexOf.call(keys, 'comment') >= 0) && !_.isString(data.comment)) { throw new Error(`option comment must be string. comment: ${data.comment}`); } if ((indexOf.call(keys, 'preset_category') >= 0) && !_.isString(data.preset_category)) { throw new Error(`option preset_category must be string. preset_category: ${data.preset_category}`); } if (indexOf.call(keys, 'tags') >= 0) { if (!_.isArray(data.tags)) { throw new Error(`option tags must be array of strings. tags: ${data.tags}`); } ref = data.tags; for (j = 0, len1 = ref.length; j < len1; j++) { tag = ref[j]; if (!_.isString(tag)) { throw new Error(`option tags must be array of strings. tags: ${data.tags}`); } if ((tag.indexOf(' ')) >= 0) { throw new Error(`tag can't contain spaces. tags: ${tag}`); } } } return data; }; //---------------------------------------- // simple reader class //---------------------------------------- BufferReader = class BufferReader { constructor(buf) { this.buf = buf; this.marker = 0; this.pos = 0; } skip(n) { this.pos += n; return this; } position(pos) { this.pos = pos; return this; } tell() { return this.pos; } readInt32() { var ret; ret = this.buf.readUInt32BE(this.pos); this.pos += 4; return ret; } readInt16() { var ret; ret = this.buf.readUInt16BE(this.pos); this.pos += 2; return ret; } readDouble() { var ret; ret = this.buf.readDoubleBE(this.pos); this.pos += 8; return ret; } readByte() { var ret; ret = this.buf.readUInt8(this.pos); this.pos += 1; return ret; } readHexInt() { var s; s = this.buf.toString('ascii', this.pos, this.pos + 8); this.pos += 8; return parseInt(s, 16); } readBytes(len) { var ret; if (!len) { len = this.readInt32(); } ret = ''; if (len) { ret = this.buf.toString('hex', this.pos, this.pos + len); this.pos += len; } return ret; } readString(len) { var b, enc, ret; enc = 'utf-8'; if (!len) { len = this.readInt32(); } if (len & 0x80000000) { enc = 'ucs2'; len = (len & 0x7ffffffff) << 1; } ret = ''; if (len) { b = this.buf.slice(this.pos, this.pos + len); if (IS_UCS2_LE && enc === 'ucs2') { b = swapBytes(b); } ret = b.toString(enc, 0, len); this.pos += len; } return ret; } mark() { var ret; ret = this.buf.slice(this.marker, this.pos); this.marker = this.pos; return ret; } end() { return this.buf.slice(this.marker); } }; //---------------------------------------- // simple writer class //---------------------------------------- BufferWriter = class BufferWriter { constructor() { this.buf = Buffer.alloc(0); } buffer() { return this.buf; } tell() { return this.buf.length; } writeHexInt(num, offset) { var s; s = `00000000${num.toString(16)}`.slice(-8); return this.buf.write(s, offset, 8, 'ascii'); } push(buf, start, end) { var b; b = buf; if (_.isNumber(start)) { if (_.isNumber(end)) { b = buf.slice(start, end); } else { b = buf.slice(start); } } this.buf = Buffer.concat([this.buf, b]); return this; } pushInt(value) { var b; b = Buffer.alloc(4); b.writeUInt32BE(value, 0); this.push(b); return this; } pushString(value) { var b; if (value) { if (/^[\u0000-\u007f]*$/.test(value)) { // ascii b = Buffer.from(value, 'ascii'); this.pushInt(b.length); if (b.length) { this.push(b); } } else { // value has non-ascii characters b = Buffer.from(value, 'ucs2'); if (IS_UCS2_LE) { b = swapBytes(b); } this.pushInt(0x80000000 + (b.length >> 1)); if (b.length) { this.push(b); } } } else { this.pushInt(0); } return this; } }; swapBytes = function(b) { var a, i, j, l, p, ref; l = b.length >> 1; for (i = j = 0, ref = l; (0 <= ref ? j < ref : j > ref); i = 0 <= ref ? ++j : --j) { p = i << 1; a = b[p]; b[p] = b[p + 1]; b[p + 1] = a; } return b; }; }).call(this);