├── .gitignore ├── .npmignore ├── LICENSE ├── README.md ├── cli.js ├── exe.js ├── index.js ├── package.json └── test ├── flp ├── 1-blank.flp ├── 2-named.flp ├── 3-3.3-time-sig.flp ├── 4-130-bpm.flp ├── 4front+mjcompressor.flp ├── 4frontpiano.flp ├── 5-replace-sampler-with-3xosc.flp ├── 6-turn-osc3-volume-down.flp ├── TheCastle_19.flp ├── ambience.flp ├── audio-clip.flp ├── effects.flp ├── listen-to-my-synthesizer.flp ├── mdl.flp ├── native-plugins.flp └── nucleon-orbit.flp ├── mocha.opts └── test.js /.gitignore: -------------------------------------------------------------------------------- 1 | /node_modules 2 | 3 | # not shared with .npmignore 4 | -------------------------------------------------------------------------------- /.npmignore: -------------------------------------------------------------------------------- 1 | /node_modules 2 | 3 | # not shared with .gitignore 4 | /test 5 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | This program is free software: you can redistribute it and/or modify 2 | it under the terms of the GNU General Public License as published by 3 | the Free Software Foundation, either version 3 of the License, or 4 | (at your option) any later version. 5 | 6 | This program is distributed in the hope that it will be useful, 7 | but WITHOUT ANY WARRANTY; without even the implied warranty of 8 | MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 9 | GNU General Public License for more details. 10 | 11 | You should have received a copy of the GNU General Public License 12 | along with this program. If not, see . 13 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # FL Studio Project File Parser 2 | 3 | ## Usage 4 | 5 | ### File API 6 | 7 | ```js 8 | var flp = require('flp'); 9 | 10 | flp.parseFile("project.flp", function(err, projectInfo) { 11 | if (err) throw err; 12 | console.log(projectInfo); 13 | }); 14 | ``` 15 | 16 | ### Stream API 17 | 18 | ```js 19 | var flp = require('flp'); 20 | var fs = require('fs'); 21 | 22 | // or use flp.createParserChild to use a subprocess 23 | var parser = flp.createParser(); 24 | 25 | parser.on('end', function(project) { 26 | console.log(project); 27 | }); 28 | 29 | var inStream = fs.createReadStream("my-cool-project.flp"); 30 | 31 | inStream.pipe(parser); 32 | ``` 33 | -------------------------------------------------------------------------------- /cli.js: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env node 2 | 3 | var flp = require('./'); 4 | var inputFile = process.argv[2]; 5 | 6 | if (!inputFile) { 7 | console.log("Usage: flp-parse file.flp"); 8 | process.exit(1); 9 | } 10 | 11 | flp.parseFile(inputFile, function(err, projectInfo) { 12 | if (err) throw err; 13 | console.log(projectInfo); 14 | }); 15 | -------------------------------------------------------------------------------- /exe.js: -------------------------------------------------------------------------------- 1 | var flp = require('./'); 2 | 3 | process.on('message', function(message) { 4 | var options = message.value; 5 | var parser = flp.createParser(options); 6 | 7 | parser.on('error', function(err) { 8 | process.send({ 9 | type: 'error', 10 | value: err.stack, 11 | }); 12 | }); 13 | 14 | parser.on('end', function() { 15 | // delete problematic properties 16 | parser.project.channels.forEach(function(channel) { 17 | delete channel.pluginSettings; 18 | }); 19 | process.send({ 20 | type: 'end', 21 | value: parser.project, 22 | }); 23 | process.disconnect(); 24 | }); 25 | 26 | process.stdin.pipe(parser); 27 | }); 28 | -------------------------------------------------------------------------------- /index.js: -------------------------------------------------------------------------------- 1 | var Writable = require('stream').Writable; 2 | var util = require('util'); 3 | var fs = require('fs'); 4 | var path = require('path'); 5 | var spawn = require('child_process').spawn; 6 | var EXE_PATH = path.join(__dirname, "exe.js"); 7 | 8 | // exports listed at bottom of file 9 | 10 | var FLFxChannelCount = 64; 11 | 12 | // FL Studio Events 13 | // BYTE EVENTS 14 | var FLP_Byte = 0; 15 | var FLP_Enabled = 0; 16 | var FLP_NoteOn = 1; //+pos (byte) 17 | var FLP_Vol = 2; 18 | var FLP_Pan = 3; 19 | var FLP_MIDIChan = 4; 20 | var FLP_MIDINote = 5; 21 | var FLP_MIDIPatch = 6; 22 | var FLP_MIDIBank = 7; 23 | var FLP_LoopActive = 9; 24 | var FLP_ShowInfo = 10; 25 | var FLP_Shuffle = 11; 26 | var FLP_MainVol = 12; 27 | var FLP_Stretch = 13; // old byte version 28 | var FLP_Pitchable = 14; 29 | var FLP_Zipped = 15; 30 | var FLP_Delay_Flags = 16; 31 | var FLP_PatLength = 17; 32 | var FLP_BlockLength = 18; 33 | var FLP_UseLoopPoints = 19; 34 | var FLP_LoopType = 20; 35 | var FLP_ChanType = 21; 36 | var FLP_MixSliceNum = 22; 37 | var FLP_EffectChannelMuted = 27; 38 | 39 | // WORD EVENTS 40 | var FLP_Word = 64; 41 | var FLP_NewChan = FLP_Word; 42 | var FLP_NewPat = FLP_Word + 1; //+PatNum (word) 43 | var FLP_Tempo = FLP_Word + 2; 44 | var FLP_CurrentPatNum = FLP_Word + 3; 45 | var FLP_PatData = FLP_Word + 4; 46 | var FLP_FX = FLP_Word + 5; 47 | var FLP_Fade_Stereo = FLP_Word + 6; 48 | var FLP_CutOff = FLP_Word + 7; 49 | var FLP_DotVol = FLP_Word + 8; 50 | var FLP_DotPan = FLP_Word + 9; 51 | var FLP_PreAmp = FLP_Word + 10; 52 | var FLP_Decay = FLP_Word + 11; 53 | var FLP_Attack = FLP_Word + 12; 54 | var FLP_DotNote = FLP_Word + 13; 55 | var FLP_DotPitch = FLP_Word + 14; 56 | var FLP_DotMix = FLP_Word + 15; 57 | var FLP_MainPitch = FLP_Word + 16; 58 | var FLP_RandChan = FLP_Word + 17; 59 | var FLP_MixChan = FLP_Word + 18; 60 | var FLP_Resonance = FLP_Word + 19; 61 | var FLP_LoopBar = FLP_Word + 20; 62 | var FLP_StDel = FLP_Word + 21; 63 | var FLP_FX3 = FLP_Word + 22; 64 | var FLP_DotReso = FLP_Word + 23; 65 | var FLP_DotCutOff = FLP_Word + 24; 66 | var FLP_ShiftDelay = FLP_Word + 25; 67 | var FLP_LoopEndBar = FLP_Word + 26; 68 | var FLP_Dot = FLP_Word + 27; 69 | var FLP_DotShift = FLP_Word + 28; 70 | var FLP_LayerChans = FLP_Word + 30; 71 | 72 | // DWORD EVENTS 73 | var FLP_Int = 128; 74 | var FLP_Color = FLP_Int; 75 | var FLP_PlayListItem = FLP_Int + 1; //+Pos (word) +PatNum (word) 76 | var FLP_Echo = FLP_Int + 2; 77 | var FLP_FXSine = FLP_Int + 3; 78 | var FLP_CutCutBy = FLP_Int + 4; 79 | var FLP_WindowH = FLP_Int + 5; 80 | var FLP_MiddleNote = FLP_Int + 7; 81 | var FLP_Reserved = FLP_Int + 8; // may contain an invalid 82 | // version info 83 | var FLP_MainResoCutOff = FLP_Int + 9; 84 | var FLP_DelayReso = FLP_Int + 10; 85 | var FLP_Reverb = FLP_Int + 11; 86 | var FLP_IntStretch = FLP_Int + 12; 87 | var FLP_SSNote = FLP_Int + 13; 88 | var FLP_FineTune = FLP_Int + 14; 89 | var FLP_FineTempo = 156; 90 | 91 | // TEXT EVENTS 92 | var FLP_Undef = 192; //+Size (var length) 93 | var FLP_Text = FLP_Undef; //+Size (var length)+Text 94 | // (Null Term. String) 95 | var FLP_Text_ChanName = FLP_Text; // name for the current channel 96 | var FLP_Text_PatName = FLP_Text + 1; // name for the current pattern 97 | var FLP_Text_Title = FLP_Text + 2; // title of the loop 98 | var FLP_Text_Comment = FLP_Text + 3; // old comments in text format. 99 | // Not used anymore 100 | var FLP_Text_SampleFileName = FLP_Text + 4; // filename for the sample in 101 | // the current channel, stored 102 | // as relative path 103 | var FLP_Text_URL = FLP_Text + 5; 104 | var FLP_Text_CommentRTF = FLP_Text + 6; // new comments in Rich Text 105 | // format 106 | var FLP_Text_Version = FLP_Text + 7; 107 | var FLP_Text_PluginName = FLP_Text + 9; // plugin file name 108 | // (without path) 109 | 110 | var FLP_Text_EffectChanName = FLP_Text + 12; 111 | var FLP_Text_MIDICtrls = FLP_Text + 16; 112 | var FLP_Text_Delay = FLP_Text + 17; 113 | var FLP_Text_TS404Params = FLP_Text + 18; 114 | var FLP_Text_DelayLine = FLP_Text + 19; 115 | var FLP_Text_NewPlugin = FLP_Text + 20; 116 | var FLP_Text_PluginParams = FLP_Text + 21; 117 | var FLP_Text_ChanParams = FLP_Text + 23;// block of various channel 118 | // params (can grow) 119 | var FLP_Text_EnvLfoParams = FLP_Text + 26; 120 | var FLP_Text_BasicChanParams= FLP_Text + 27; 121 | var FLP_Text_OldFilterParams= FLP_Text + 28; 122 | var FLP_Text_AutomationData = FLP_Text + 31; 123 | var FLP_Text_PatternNotes = FLP_Text + 32; 124 | var FLP_Text_ChanGroupName = FLP_Text + 39; 125 | var FLP_Text_PlayListItems = FLP_Text + 41; 126 | 127 | 128 | 129 | var FilterTypes = { 130 | LowPass: 0, 131 | HiPass: 1, 132 | BandPass_CSG: 2, 133 | BandPass_CZPG: 3, 134 | Notch: 4, 135 | AllPass: 5, 136 | Moog: 6, 137 | DoubleLowPass: 7, 138 | Lowpass_RC12: 8, 139 | Bandpass_RC12: 9, 140 | Highpass_RC12: 10, 141 | Lowpass_RC24: 11, 142 | Bandpass_RC24: 12, 143 | Highpass_RC24: 13, 144 | Formantfilter: 14, 145 | }; 146 | 147 | var ArpDirections = { 148 | Up: 0, 149 | Down: 1, 150 | UpAndDown: 2, 151 | Random: 3, 152 | }; 153 | 154 | var EnvelopeTargets = { 155 | Volume: 0, 156 | Cut: 1, 157 | Resonance: 2, 158 | NumTargets: 3, 159 | }; 160 | 161 | var PluginChunkIds = { 162 | MIDI: 1, 163 | Flags: 2, 164 | IO: 30, 165 | InputInfo: 31, 166 | OutputInfo: 32, 167 | PluginInfo: 50, 168 | VSTPlugin: 51, 169 | GUID: 52, 170 | State: 53, 171 | Name: 54, 172 | Filename: 55, 173 | VendorName: 56, 174 | }; 175 | 176 | 177 | var STATE_COUNT = 0; 178 | var STATE_START = STATE_COUNT++; 179 | var STATE_HEADER = STATE_COUNT++; 180 | var STATE_FLDT = STATE_COUNT++; 181 | var STATE_EVENT = STATE_COUNT++; 182 | var STATE_SKIP = STATE_COUNT++; 183 | 184 | var states = new Array(STATE_COUNT); 185 | states[STATE_START] = function(parser) { 186 | if (parser.buffer.length < 4) return true; 187 | if (parser.buffer.slice(0, 4).toString('ascii') !== 'FLhd') { 188 | parser.handleError(new Error("Expected magic number")); 189 | return; 190 | } 191 | 192 | parser.state = STATE_HEADER; 193 | parser.buffer = parser.buffer.slice(4); 194 | }; 195 | states[STATE_HEADER] = function(parser) { 196 | if (parser.buffer.length < 10) return true; 197 | var headerLen = parser.buffer.readInt32LE(0); 198 | if (headerLen !== 6) { 199 | parser.handleError(new Error("Expected header length 6, not " + headerLen)); 200 | return; 201 | } 202 | 203 | // some type thing 204 | var type = parser.buffer.readInt16LE(4); 205 | if (type !== 0) { 206 | parser.handleError(new Error("type " + type + " is not supported")); 207 | return; 208 | } 209 | 210 | // number of channels 211 | parser.project.channelCount = parser.buffer.readInt16LE(6); 212 | if (parser.project.channelCount < 1 || parser.project.channelCount > 1000) { 213 | parser.handleError(new Error("invalid number of channels: " + parser.project.channelCount)); 214 | return; 215 | } 216 | for (var i = 0; i < parser.project.channelCount; i += 1) { 217 | parser.project.channels.push(new FLChannel()); 218 | } 219 | 220 | // ppq 221 | parser.ppq = parser.buffer.readInt16LE(8); 222 | if (parser.ppq < 0) { 223 | parser.handleError(new Error("invalid ppq: " + parser.ppq)); 224 | return; 225 | } 226 | 227 | parser.state = STATE_FLDT; 228 | parser.buffer = parser.buffer.slice(10); 229 | }; 230 | states[STATE_FLDT] = function(parser) { 231 | if (parser.buffer.length < 8) return true; 232 | var id = parser.buffer.slice(0, 4).toString('ascii'); 233 | var len = parser.buffer.readInt32LE(4); 234 | 235 | // sanity check 236 | if (len < 0 || len > 0x10000000) { 237 | parser.handleError(new Error("invalid chunk length: " + len)); 238 | return; 239 | } 240 | 241 | parser.buffer = parser.buffer.slice(8); 242 | if (id === 'FLdt') { 243 | parser.state = STATE_EVENT; 244 | } else { 245 | parser.state = STATE_SKIP; 246 | parser.skipBytesLeft = len; 247 | parser.nextState = STATE_FLDT; 248 | } 249 | }; 250 | states[STATE_SKIP] = function(parser) { 251 | var skipBytes = Math.min(parser.buffer.length, parser.skipBytesLeft); 252 | parser.buffer = parser.buffer.slice(skipBytes); 253 | parser.skipBytesLeft -= skipBytes; 254 | if (parser.skipBytesLeft === 0) { 255 | parser.state = parser.nextState; 256 | } else { 257 | return true; 258 | } 259 | }; 260 | states[STATE_EVENT] = function(parser) { 261 | var eventId = parser.readUInt8(); 262 | var data = parser.readUInt8(); 263 | 264 | if (eventId == null || data == null) return true; 265 | 266 | var b; 267 | if (eventId >= FLP_Word && eventId < FLP_Text) { 268 | b = parser.readUInt8(); 269 | if (b == null) return true; 270 | data = data | (b << 8); 271 | } 272 | if (eventId >= FLP_Int && eventId < FLP_Text) { 273 | b = parser.readUInt8(); 274 | if (b == null) return true; 275 | data = data | (b << 16); 276 | 277 | b = parser.readUInt8(); 278 | if (b == null) return true; 279 | data = data | (b << 24); 280 | } 281 | var text; 282 | var intList; 283 | var uchars; 284 | var strbuf; 285 | var i; 286 | if (eventId >= FLP_Text) { 287 | var textLen = data & 0x7F; 288 | var shift = 0; 289 | while (data & 0x80) { 290 | data = parser.readUInt8(); 291 | if (data == null) return true; 292 | textLen = textLen | ((data & 0x7F) << (shift += 7)); 293 | } 294 | text = parser.readString(textLen); 295 | if (text == null) return true; 296 | if (text[text.length - 1] === '\x00') { 297 | text = text.substring(0, text.length - 1); 298 | } 299 | // also interpret same data as intList 300 | strbuf = parser.strbuf; 301 | var intCount = Math.floor(parser.strbuf.length / 4); 302 | intList = []; 303 | for (i = 0; i < intCount; i += 1) { 304 | intList.push(strbuf.readInt32LE(i * 4)); 305 | } 306 | } 307 | 308 | parser.sliceBufferToCursor(); 309 | 310 | var cc = parser.curChannel >= 0 ? parser.project.channels[parser.curChannel] : null; 311 | var imax; 312 | var ch; 313 | var pos, len; 314 | 315 | switch (eventId) { 316 | // BYTE EVENTS 317 | case FLP_Byte: 318 | if (parser.debug) { 319 | console.log("undefined byte", data); 320 | } 321 | break; 322 | case FLP_NoteOn: 323 | if (parser.debug) { 324 | console.log("note on:", data); 325 | } 326 | break; 327 | case FLP_Vol: 328 | if (parser.debug) { 329 | console.log("vol", data); 330 | } 331 | break; 332 | case FLP_Pan: 333 | if (parser.debug) { 334 | console.log("pan", data); 335 | } 336 | break; 337 | case FLP_LoopActive: 338 | if (parser.debug) { 339 | console.log("active loop:", data); 340 | } 341 | break; 342 | case FLP_ShowInfo: 343 | if (parser.debug) { 344 | console.log("show info: ", data ); 345 | } 346 | break; 347 | case FLP_Shuffle: 348 | if (parser.debug) { 349 | console.log("shuffle: ", data ); 350 | } 351 | break; 352 | case FLP_MainVol: 353 | parser.project.mainVolume = data; 354 | break; 355 | case FLP_PatLength: 356 | if (parser.debug) { 357 | console.log("pattern length: ", data ); 358 | } 359 | break; 360 | case FLP_BlockLength: 361 | if (parser.debug) { 362 | console.log("block length: ", data ); 363 | } 364 | break; 365 | case FLP_UseLoopPoints: 366 | if (cc) cc.sampleUseLoopPoints = true; 367 | break; 368 | case FLP_LoopType: 369 | if (parser.debug) { 370 | console.log("loop type: ", data ); 371 | } 372 | break; 373 | case FLP_ChanType: 374 | if (parser.debug) { 375 | console.log("channel type: ", data ); 376 | } 377 | if (cc) { 378 | switch (data) { 379 | case 0: cc.generatorName = "Sampler"; break; 380 | case 1: cc.generatorName = "TS 404"; break; 381 | case 2: cc.generatorName = "3x Osc"; break; 382 | case 3: cc.generatorName = "Layer"; break; 383 | default: break; 384 | } 385 | } 386 | break; 387 | case FLP_MixSliceNum: 388 | if (cc) cc.fxChannel = data + 1; 389 | break; 390 | case FLP_EffectChannelMuted: 391 | var isMuted = (data & 0x08) <= 0; 392 | if (parser.project.currentEffectChannel >= 0 && parser.project.currentEffectChannel <= FLFxChannelCount) { 393 | parser.project.effectChannels[parser.project.currentEffectChannel].isMuted = isMuted; 394 | } 395 | break; 396 | 397 | // WORD EVENTS 398 | case FLP_NewChan: 399 | if (parser.debug) { 400 | console.log("cur channel:", data); 401 | } 402 | parser.curChannel = data; 403 | parser.gotCurChannel = true; 404 | break; 405 | case FLP_NewPat: 406 | parser.project.currentPattern = data - 1; 407 | parser.project.maxPatterns = Math.max(parser.project.currentPattern, parser.project.maxPatterns); 408 | break; 409 | case FLP_Tempo: 410 | if (parser.debug) { 411 | console.log("got tempo:", data); 412 | } 413 | parser.project.tempo = data; 414 | break; 415 | case FLP_CurrentPatNum: 416 | parser.project.activeEditPattern = data; 417 | break; 418 | case FLP_FX: 419 | if (parser.debug) { 420 | console.log("FX:", data); 421 | } 422 | break; 423 | case FLP_Fade_Stereo: 424 | if (data & 0x02) { 425 | parser.sampleReversed = true; 426 | } else if( data & 0x100 ) { 427 | parser.sampleReverseStereo = true; 428 | } 429 | break; 430 | case FLP_CutOff: 431 | if (parser.debug) { 432 | console.log("cutoff (sample):", data); 433 | } 434 | break; 435 | case FLP_PreAmp: 436 | if (cc) cc.sampleAmp = data; 437 | break; 438 | case FLP_Decay: 439 | if (parser.debug) { 440 | console.log("decay (sample): ", data ); 441 | } 442 | break; 443 | case FLP_Attack: 444 | if (parser.debug) { 445 | console.log("attack (sample): ", data ); 446 | } 447 | break; 448 | case FLP_MainPitch: 449 | parser.project.mainPitch = data; 450 | break; 451 | case FLP_Resonance: 452 | if (parser.debug) { 453 | console.log("resonance (sample): ", data ); 454 | } 455 | break; 456 | case FLP_LoopBar: 457 | if (parser.debug) { 458 | console.log("loop bar: ", data ); 459 | } 460 | break; 461 | case FLP_StDel: 462 | if (parser.debug) { 463 | console.log("stdel (delay?): ", data ); 464 | } 465 | break; 466 | case FLP_FX3: 467 | if (parser.debug) { 468 | console.log("FX 3: ", data ); 469 | } 470 | break; 471 | case FLP_ShiftDelay: 472 | if (parser.debug) { 473 | console.log("shift delay: ", data ); 474 | } 475 | break; 476 | case FLP_Dot: 477 | var dotVal = (data & 0xff) + (parser.project.currentPattern << 8); 478 | if (cc) cc.dots.push(dotVal); 479 | break; 480 | case FLP_LayerChans: 481 | parser.project.channels[data].layerParent = parser.curChannel; 482 | if (cc) cc.generatorName = "Layer"; 483 | break; 484 | 485 | 486 | // DWORD EVENTS 487 | case FLP_Color: 488 | if (cc) { 489 | cc.colorRed = (data & 0xFF000000) >> 24; 490 | cc.colorGreen = (data & 0x00FF0000) >> 16; 491 | cc.colorBlue = (data & 0x0000FF00) >> 8; 492 | } 493 | break; 494 | case FLP_PlayListItem: 495 | var item = new FLPlaylistItem(); 496 | item.position = (data & 0xffff) * 192; 497 | item.length = 192; 498 | item.pattern = (data >> 16) - 1; 499 | parser.project.playlistItems.push(item); 500 | parser.project.maxPatterns = Math.max(parser.project.maxPatterns, item.pattern); 501 | break; 502 | case FLP_FXSine: 503 | if (parser.debug) { 504 | console.log("fx sine: ", data ); 505 | } 506 | break; 507 | case FLP_CutCutBy: 508 | if (parser.debug) { 509 | console.log("cut cut by: ", data ); 510 | } 511 | break; 512 | case FLP_MiddleNote: 513 | if (cc) cc.baseNote = data+9; 514 | break; 515 | case FLP_DelayReso: 516 | if (parser.debug) { 517 | console.log("delay resonance: ", data ); 518 | } 519 | break; 520 | case FLP_Reverb: 521 | if (parser.debug) { 522 | console.log("reverb (sample): ", data ); 523 | } 524 | break; 525 | case FLP_IntStretch: 526 | if (parser.debug) { 527 | console.log("int stretch (sample): ", data ); 528 | } 529 | break; 530 | case FLP_FineTempo: 531 | if (parser.debug) { 532 | console.log("got fine tempo", data ); 533 | } 534 | parser.project.tempo = data / 1000; 535 | break; 536 | 537 | 538 | // TEXT EVENTS 539 | case FLP_Text_ChanName: 540 | if (cc) cc.name = text; 541 | break; 542 | case FLP_Text_PatName: 543 | parser.project.patternNames[parser.project.currentPattern] = text; 544 | break; 545 | case FLP_Text_CommentRTF: 546 | // TODO: support RTF comments 547 | if (parser.debug) { 548 | console.log("RTF text comment:", text); 549 | } 550 | break; 551 | case FLP_Text_Title: 552 | parser.project.projectTitle = text; 553 | break; 554 | case FLP_Text_SampleFileName: 555 | if (cc) { 556 | cc.sampleFileName = text; 557 | cc.generatorName = "Sampler"; 558 | parser.project.sampleList.push(cc.sampleFileName); 559 | } 560 | break; 561 | case FLP_Text_Version: 562 | if (parser.debug) { 563 | console.log("FLP version: ", text ); 564 | } 565 | parser.project.versionString = text; 566 | // divide the version string into numbers 567 | var numbers = parser.project.versionString.split('.'); 568 | parser.project.version = (parseInt(numbers[0], 10) << 8) + 569 | (parseInt(numbers[1], 10) << 4) + 570 | (parseInt(numbers[2], 10) << 0); 571 | if (parser.project.version >= 0x600) { 572 | parser.project.versionSpecificFactor = 100; 573 | } 574 | break; 575 | case FLP_Text_PluginName: 576 | var pluginName = text; 577 | 578 | if (!parser.gotCurChannel) { 579 | // I guess if we don't get the cur channel we should add a new one... 580 | parser.curChannel = parser.project.channelCount; 581 | parser.project.channelCount += 1; 582 | cc = parser.project.channels[parser.curChannel] = new FLChannel(); 583 | } 584 | parser.gotCurChannel = false; 585 | 586 | // we add all plugins to effects list and then 587 | // remove the ones that aren't effects later. 588 | parser.project.effectPlugins.push(pluginName); 589 | if (cc) cc.generatorName = pluginName; 590 | if (parser.debug) { 591 | console.log("plugin: ", pluginName, "cc?", !!cc); 592 | } 593 | break; 594 | case FLP_Text_EffectChanName: 595 | parser.project.currentEffectChannel += 1; 596 | if (parser.project.currentEffectChannel <= FLFxChannelCount) { 597 | parser.project.effectChannels[parser.project.currentEffectChannel].name = text; 598 | } 599 | break; 600 | case FLP_Text_Delay: 601 | if (parser.debug) { 602 | console.log("delay data: ", text ); 603 | } 604 | // intList[1] seems to be volume or similiar and 605 | // needs to be divided 606 | // by parser.project.versionSpecificFactor 607 | break; 608 | case FLP_Text_TS404Params: 609 | if (parser.debug) { 610 | console.log("FLP_Text_TS404Params"); 611 | } 612 | if (cc) { 613 | if (cc.pluginSettings != null && parser.debug) { 614 | console.log("overwriting pluginSettings. we must have missed something: " + 615 | fruityWrapper(cc.pluginSettings) + " -> " + fruityWrapper(strbuf)); 616 | } 617 | cc.pluginSettings = strbuf; 618 | cc.generatorName = "TS 404"; 619 | } 620 | break; 621 | case FLP_Text_NewPlugin: 622 | // TODO: if it's an effect plugin make a new effect 623 | if (parser.debug) { 624 | console.log("new plugin: ", text); 625 | } 626 | break; 627 | case FLP_Text_PluginParams: 628 | if (parser.debug) { 629 | console.log("FLP_Text_PluginParams"); 630 | } 631 | if (cc) { 632 | if (cc.pluginSettings != null && parser.debug) { 633 | console.log("overwriting pluginSettings. we must have missed something: " + 634 | fruityWrapper(cc.pluginSettings) + " -> " + fruityWrapper(strbuf)); 635 | } 636 | cc.pluginSettings = strbuf; 637 | cc.plugin = {} 638 | var flpPluginOffset = 0; 639 | var flpPluginVersion = strbuf.readUInt32LE(flpPluginOffset); flpPluginOffset += 4; 640 | // NB: don't support the limited info found in flpPluginVersion <= 4. 641 | if (flpPluginVersion >= 5 && flpPluginVersion < 10) { 642 | while (flpPluginOffset < strbuf.length) { 643 | var flpPluginChunkId = strbuf.readUInt32LE(flpPluginOffset); flpPluginOffset += 4; 644 | var flpPluginChunkSizeLo = strbuf.readUInt32LE(flpPluginOffset); flpPluginOffset += 4; 645 | var flpPluginChunkSizeHi = strbuf.readUInt32LE(flpPluginOffset); flpPluginOffset += 4; 646 | var flpPluginChunkSize = flpPluginChunkSizeLo + flpPluginChunkSizeHi * Math.pow(2,32); 647 | var flpPluginChunkOffset = flpPluginOffset; 648 | var flpPluginChunkEnd = flpPluginOffset + flpPluginChunkSize; 649 | switch (flpPluginChunkId) { 650 | case PluginChunkIds.MIDI: 651 | cc.plugin.midiInPort = strbuf.readInt32LE(flpPluginChunkOffset); flpPluginChunkOffset += 4; 652 | cc.plugin.midiOutPort = strbuf.readInt32LE(flpPluginChunkOffset); flpPluginChunkOffset += 4; 653 | cc.plugin.pitchBendRange = strbuf.readUInt8(flpPluginChunkOffset); flpPluginChunkOffset += 1; 654 | flpPluginChunkOffset += 11; // Ignore reserved bytes. 655 | break; 656 | case PluginChunkIds.Flags: 657 | cc.plugin.flags = strbuf.readUInt32LE(flpPluginChunkOffset); flpPluginChunkOffset += 4; 658 | break; 659 | case PluginChunkIds.IO: 660 | cc.plugin.numInputs = strbuf.readInt32LE(flpPluginChunkOffset); flpPluginChunkOffset += 4; 661 | cc.plugin.numOutputs = strbuf.readInt32LE(flpPluginChunkOffset); flpPluginChunkOffset += 4; 662 | flpPluginChunkOffset += 8; // Ignore reserved bytes. 663 | break; 664 | case PluginChunkIds.InputInfo: 665 | case PluginChunkIds.OutputInfo: 666 | var pluginIOInfo = []; 667 | while (flpPluginChunkOffset + 12 <= flpPluginChunkEnd) { 668 | var pluginIOMixerOffset = strbuf.readInt32LE(flpPluginChunkOffset); flpPluginChunkOffset += 4; 669 | var pluginIOFlags = strbuf.readUInt8(flpPluginChunkOffset); flpPluginChunkOffset += 1; 670 | flpPluginChunkOffset += 7; // Ignore reserved bytes. 671 | pluginIOInfo.push({ 672 | MixerOffset : pluginIOMixerOffset, 673 | Flags : pluginIOFlags, 674 | }); 675 | } 676 | if (flpPluginChunkId === PluginChunkIds.InputInfo) { 677 | cc.plugin.inputInfo = pluginIOInfo; 678 | } 679 | else { 680 | cc.plugin.outputInfo = pluginIOInfo; 681 | } 682 | break; 683 | case PluginChunkIds.PluginInfo: 684 | cc.plugin.infoKind = strbuf.readInt32LE(flpPluginChunkOffset); flpPluginChunkOffset += 4; 685 | flpPluginChunkOffset += 12; // Ignore reserved bytes. 686 | break; 687 | case PluginChunkIds.VSTPlugin: 688 | var vstPluginNumber = strbuf.readUInt32LE(flpPluginChunkOffset); flpPluginChunkOffset += 4; 689 | var vstPluginId = vstPluginNumber 690 | .toString(16) 691 | .match(/.{1,2}/g) 692 | .map(function(hex) { return String.fromCharCode(parseInt(hex,16)) }) 693 | .join(''); 694 | cc.plugin.vstNumber = vstPluginNumber; 695 | cc.plugin.vstId = vstPluginId; 696 | break; 697 | case PluginChunkIds.GUID: 698 | cc.plugin.GUID = strbuf.slice(flpPluginChunkOffset, flpPluginChunkEnd); 699 | break; 700 | case PluginChunkIds.State: 701 | cc.plugin.state = strbuf.slice(flpPluginChunkOffset, flpPluginChunkEnd); 702 | break; 703 | case PluginChunkIds.Name: 704 | cc.plugin.name = strbuf.toString('utf8', flpPluginChunkOffset, flpPluginChunkEnd); 705 | break; 706 | case PluginChunkIds.Filename: 707 | cc.plugin.filename = strbuf.toString('utf8', flpPluginChunkOffset, flpPluginChunkEnd); 708 | break; 709 | case PluginChunkIds.VendorName: 710 | cc.plugin.vendorName = strbuf.toString('utf8', flpPluginChunkOffset, flpPluginChunkEnd); 711 | break; 712 | default: 713 | break; 714 | } 715 | flpPluginOffset = flpPluginChunkEnd; 716 | } 717 | } 718 | } 719 | break; 720 | case FLP_Text_ChanParams: 721 | if (cc) { 722 | cc.arpDir = intList[10]; 723 | cc.arpRange = intList[11]; 724 | cc.selectedArp = intList[12]; 725 | if (cc.selectedArp < 8) { 726 | var mappedArps = [0, 1, 5, 6, 2, 3, 4]; 727 | cc.selectedArp = mappedArps[cc.selectedArp]; 728 | } 729 | cc.arpTime = ((intList[13]+1 ) * parser.project.tempo) / (4 * 16) + 1; 730 | cc.arpGate = (intList[14] * 100.0) / 48.0; 731 | cc.arpEnabled = intList[10] > 0; 732 | } 733 | break; 734 | case FLP_Text_EnvLfoParams: 735 | if (cc) { 736 | var scaling = 1.0 / 65536.0; 737 | var e = new FLChannelEnvelope(); 738 | switch (cc.envelopes.length) { 739 | case 1: 740 | e.target = EnvelopeTargets.Volume; 741 | break; 742 | case 2: 743 | e.target = EnvelopeTargets.Cut; 744 | break; 745 | case 3: 746 | e.target = EnvelopeTargets.Resonance; 747 | break; 748 | default: 749 | e.target = EnvelopeTargets.NumTargets; 750 | break; 751 | } 752 | e.predelay = intList[2] * scaling; 753 | e.attack = intList[3] * scaling; 754 | e.hold = intList[4] * scaling; 755 | e.decay = intList[5] * scaling; 756 | e.sustain = 1-intList[6] / 128.0; 757 | e.release = intList[7] * scaling; 758 | if (e.target === EnvelopeTargets.Volume) { 759 | e.amount = intList[1] ? 1 : 0; 760 | } else { 761 | e.amount = intList[8] / 128.0; 762 | } 763 | cc.envelopes.push(e); 764 | } 765 | break; 766 | case FLP_Text_BasicChanParams: 767 | cc.volume = Math.floor(intList[1] / parser.project.versionSpecificFactor); 768 | cc.panning = Math.floor(intList[0] / parser.project.versionSpecificFactor); 769 | if (strbuf.length > 12) { 770 | cc.filterType = strbuf.readUInt8(20); 771 | cc.filterCut = strbuf.readUInt8(12); 772 | cc.filterRes = strbuf.readUInt8(16); 773 | cc.filterEnabled = (strbuf.readUInt8(13) === 0); 774 | if (strbuf.readUInt8(20) >= 6) { 775 | cc.filterCut *= 0.5; 776 | } 777 | } 778 | break; 779 | case FLP_Text_OldFilterParams: 780 | cc.filterType = strbuf.readUInt8(8); 781 | cc.filterCut = strbuf.readUInt8(0); 782 | cc.filterRes = strbuf.readUInt8(4); 783 | cc.filterEnabled = (strbuf.readUInt8(1) === 0); 784 | if (strbuf.readUInt8(8) >= 6) { 785 | cc.filterCut *= 0.5; 786 | } 787 | break; 788 | case FLP_Text_AutomationData: 789 | var bpae = 12; 790 | imax = Math.floor(strbuf.length / bpae); 791 | for (i = 0; i < imax; ++i) { 792 | var a = new FLAutomation(); 793 | a.pos = Math.floor(intList[3*i+0] / (4*parser.ppq / 192)); 794 | a.value = intList[3*i+2]; 795 | a.channel = intList[3*i+1] >> 16; 796 | a.control = intList[3*i+1] & 0xffff; 797 | if (a.channel >= 0 && a.channel < parser.project.channelCount) { 798 | parser.project.channels[a.channel].automationData.push(a); 799 | } 800 | } 801 | break; 802 | case FLP_Text_PatternNotes: 803 | var bpn = 20; 804 | imax = Math.floor((strbuf.length + bpn - 1) / bpn); 805 | if ((imax-1) * bpn + 18 >= strbuf.length) { 806 | if (parser.debug) { 807 | console.log("invalid pattern notes length"); 808 | } 809 | break; 810 | } 811 | for (i = 0; i < imax; i += 1) { 812 | ch = strbuf.readUInt8(i * bpn + 6); 813 | var pan = strbuf.readUInt8(i * bpn + 16); 814 | var vol = strbuf.readUInt8(i * bpn + 17); 815 | pos = strbuf.readInt32LE(i * bpn); 816 | var key = strbuf.readUInt8(i * bpn + 12); 817 | len = strbuf.readInt32LE(i * bpn + 8); 818 | 819 | pos = Math.floor(pos / ((4*parser.ppq) / 192)); 820 | len = Math.floor(len / ((4*parser.ppq) / 192)); 821 | var n = new FLNote(len, pos, key, vol, pan); 822 | if (ch < parser.project.channelCount) { 823 | parser.project.channels[ch].notes.push([parser.project.currentPattern, n]); 824 | } else if (parser.debug) { 825 | console.log("Invalid ch: ", ch ); 826 | } 827 | } 828 | break; 829 | case FLP_Text_ChanGroupName: 830 | if (parser.debug) { 831 | console.log("channel group name: ", text ); 832 | } 833 | break; 834 | case 225: 835 | var FLP_EffectParamVolume = 0x1fc0; 836 | 837 | var bpi = 12; 838 | imax = Math.floor(strbuf.length / bpi); 839 | for (i = 0; i < imax; ++i) { 840 | var param = intList[i*3+1] & 0xffff; 841 | ch = ( intList[i*3+1] >> 22 ) & 0x7f; 842 | if (ch < 0 || ch > FLFxChannelCount) { 843 | continue; 844 | } 845 | var val = intList[i*3+2]; 846 | if (param === FLP_EffectParamVolume) { 847 | parser.project.effectChannels[ch].volume = Math.floor(val / parser.project.versionSpecificFactor); 848 | } else if (parser.debug) { 849 | console.log("FX-ch: ", ch, " param: " , param, " value: ", val ); 850 | } 851 | } 852 | break; 853 | case 233: // playlist items 854 | bpi = 28; 855 | imax = Math.floor(strbuf.length / bpi); 856 | for (i = 0; i < imax; ++i) { 857 | pos = Math.floor(intList[i*bpi/4+0] / ((4*parser.ppq) / 192)); 858 | len = Math.floor(intList[i*bpi/4+2] / ((4*parser.ppq) / 192)); 859 | var pat = intList[i*bpi/4+3] & 0xfff; 860 | // whatever these magic numbers are for... 861 | if( pat > 2146 && pat <= 2278 ) { 862 | item = new FLPlaylistItem(); 863 | item.position = pos; 864 | item.length = len; 865 | item.pattern = 2278 - pat; 866 | parser.project.playlistItems.push(i); 867 | } else if (parser.debug) { 868 | console.log("unknown playlist item: ", text); 869 | } 870 | } 871 | break; 872 | default: 873 | if (!parser.debug) break; 874 | if (eventId >= FLP_Text) { 875 | console.log("unhandled text (ev:", eventId, "):", text); 876 | } else { 877 | console.log("handling of FLP-event", eventId, "not implemented yet (data=", data, ")"); 878 | } 879 | } 880 | }; 881 | 882 | function parseFile(file, options, callback) { 883 | if (typeof options === 'function') { 884 | callback = options; 885 | options = {}; 886 | } 887 | 888 | var inStream = fs.createReadStream(file, options); 889 | var parser = createParser(options); 890 | var alreadyError = false; 891 | inStream.on('error', handleError); 892 | parser.on('error', handleError); 893 | inStream.pipe(parser); 894 | parser.on('end', function() { 895 | if (alreadyError) return; 896 | alreadyError = true; 897 | callback(null, parser.project); 898 | }); 899 | 900 | function handleError(err) { 901 | if (alreadyError) return; 902 | alreadyError = true; 903 | callback(err); 904 | } 905 | } 906 | 907 | function createParser(options) { 908 | return new FlpParser(options); 909 | } 910 | 911 | function createParserChild(options) { 912 | options = options || {}; 913 | var child = spawn(process.execPath, [EXE_PATH], { 914 | stdio: ['pipe', process.stdout, process.stderr, 'ipc'], 915 | }); 916 | var gotEnd = false; 917 | 918 | var parserObject = new Writable(options); 919 | parserObject._write = function(chunk, encoding, callback) { 920 | child.stdin.write(chunk, encoding, callback); 921 | }; 922 | parserObject.on('finish', function() { 923 | child.stdin.end(); 924 | }); 925 | 926 | child.on('message', function(message) { 927 | if (message.type === 'error') { 928 | gotEnd = true; 929 | parserObject.emit('error', new Error(message.value)); 930 | } else { 931 | if (message.type === 'end') { 932 | if (gotEnd) return; 933 | gotEnd = true; 934 | } 935 | parserObject.emit(message.type, message.value); 936 | } 937 | }); 938 | 939 | child.on('error', function(err) { 940 | if (!gotEnd) { 941 | gotEnd = true; 942 | parserObject.emit('error', err); 943 | } 944 | }); 945 | 946 | child.on('close', function(code) { 947 | if (!gotEnd) { 948 | gotEnd = true; 949 | parserObject.emit('error', new Error("flp parser child process exited unexpectedly")); 950 | } 951 | }); 952 | 953 | child.send({type: 'options', value: options}); 954 | return parserObject; 955 | } 956 | 957 | util.inherits(FlpParser, Writable); 958 | function FlpParser(options) { 959 | Writable.call(this, options); 960 | 961 | this.state = STATE_START; 962 | this.buffer = new Buffer(0); 963 | this.cursor = 0; 964 | this.debug = !!options.debug; 965 | this.curChannel = -1; 966 | 967 | this.project = new FLProject(); 968 | 969 | this.ppq = null; 970 | this.error = null; 971 | 972 | this.gotCurChannel = false; 973 | 974 | setupListeners(this); 975 | } 976 | 977 | function setupListeners(parser) { 978 | parser.on('finish', function() { 979 | if (parser.state !== STATE_EVENT) { 980 | parser.handleError(new Error("unexpected end of stream")); 981 | return; 982 | } 983 | finishParsing(parser); 984 | parser.emit('end', parser.project); 985 | }); 986 | } 987 | 988 | function finishParsing(parser) { 989 | var i; 990 | // for each fruity wrapper, extract the plugin name 991 | for (i = 0; i < parser.project.channels.length; i += 1) { 992 | tryFruityWrapper(parser.project.channels[i]); 993 | } 994 | for (i = 0; i < parser.project.effects.length; i += 1) { 995 | tryFruityWrapper(parser.project.effects[i]); 996 | } 997 | // effects are the ones that aren't channels. 998 | var channelPlugins = {}; 999 | for (i = 0; i < parser.project.channels.length; i += 1) { 1000 | channelPlugins[parser.project.channels[i].generatorName] = true; 1001 | } 1002 | for (i = 0; i < parser.project.effectPlugins.length; i += 1) { 1003 | var effectPluginName = parser.project.effectPlugins[i]; 1004 | if (!channelPlugins[effectPluginName]) { 1005 | parser.project.effectStrings.push(effectPluginName); 1006 | } 1007 | } 1008 | } 1009 | 1010 | function tryFruityWrapper(plugin) { 1011 | var lowerName = (plugin.generatorName || "").toLowerCase(); 1012 | if (lowerName !== 'fruity wrapper') return; 1013 | 1014 | plugin.generatorName = fruityWrapper(plugin.pluginSettings); 1015 | } 1016 | 1017 | function fruityWrapper(buf) { 1018 | var cidPluginName = 54; 1019 | var cursor = 0; 1020 | var cursorEnd = cursor + buf.length; 1021 | var version = readInt32LE(); 1022 | if (version == null) return ""; 1023 | if (version <= 4) { 1024 | // "old format" 1025 | var extraBlockSize = readUInt32LE(); 1026 | var midiPort = readUInt32LE(); 1027 | var synthSaved = readUInt32LE(); 1028 | var pluginType = readUInt32LE(); 1029 | var pluginSpecificBlockSize = readUInt32LE(); 1030 | var pluginNameLen = readUInt8(); 1031 | if (pluginNameLen == null) return ""; 1032 | var pluginName = buf.slice(cursor, cursor + pluginNameLen).toString('utf8'); 1033 | // heuristics to not include bad names 1034 | if (pluginName.indexOf("\u0000") >= 0) return ""; 1035 | return pluginName; 1036 | } else { 1037 | // "new format" 1038 | while (cursor < cursorEnd) { 1039 | var chunkId = readUInt32LE(); 1040 | var chunkSize = readUInt64LE(); 1041 | if (chunkSize == null) return ""; 1042 | if (chunkId === cidPluginName) { 1043 | return buf.slice(cursor, cursor + chunkSize).toString('utf8'); 1044 | } 1045 | cursor += chunkSize; 1046 | } 1047 | } 1048 | return ""; 1049 | 1050 | function readUInt32LE() { 1051 | if (cursor + 4 > buf.length) return null; 1052 | 1053 | var val = buf.readUInt32LE(cursor); 1054 | cursor += 4; 1055 | return val; 1056 | } 1057 | 1058 | function readInt32LE() { 1059 | if (cursor + 4 > buf.length) return null; 1060 | 1061 | var val = buf.readInt32LE(cursor); 1062 | cursor += 4; 1063 | return val; 1064 | } 1065 | 1066 | function readUInt8() { 1067 | if (cursor + 1 > buf.length) return null; 1068 | 1069 | var val = buf.readUInt8(cursor); 1070 | cursor += 1; 1071 | return val; 1072 | } 1073 | 1074 | function readUInt64LE() { 1075 | if (cursor + 8 > buf.length) return null; 1076 | 1077 | var val = 0; 1078 | for (var i = 0; i < 8; i += 1) { 1079 | val += buf.readUInt8(cursor + i) * Math.pow(2, 8 * i); 1080 | } 1081 | cursor += 8; 1082 | return val; 1083 | } 1084 | } 1085 | 1086 | FlpParser.prototype._write = function(chunk, encoding, callback) { 1087 | this.buffer = Buffer.concat([this.buffer, chunk]); 1088 | for (;;) { 1089 | var fn = states[this.state]; 1090 | this.cursor = 0; 1091 | var waitForWrite = fn(this); 1092 | if (this.error || waitForWrite) break; 1093 | } 1094 | callback(); 1095 | }; 1096 | 1097 | FlpParser.prototype.readUInt8 = function() { 1098 | if (this.cursor >= this.buffer.length) return null; 1099 | var val = this.buffer.readUInt8(this.cursor); 1100 | this.cursor += 1; 1101 | return val; 1102 | }; 1103 | 1104 | FlpParser.prototype.readString = function(len) { 1105 | if (this.cursor + len > this.buffer.length) return null; 1106 | this.strbuf = this.buffer.slice(this.cursor, this.cursor + len); 1107 | var val = this.strbuf.toString('utf8'); 1108 | this.cursor += len; 1109 | return val; 1110 | }; 1111 | 1112 | FlpParser.prototype.sliceBufferToCursor = function() { 1113 | this.buffer = this.buffer.slice(this.cursor); 1114 | }; 1115 | 1116 | FlpParser.prototype.handleError = function(err) { 1117 | this.error = err; 1118 | this.emit('error', err); 1119 | }; 1120 | 1121 | function FLChannel() { 1122 | this.name = null; 1123 | this.pluginSettings = null; 1124 | this.generatorName = null; 1125 | this.automationData = []; 1126 | this.volume = 100; 1127 | this.panning = 0; 1128 | this.baseNote = 57; 1129 | this.fxChannel = 0; 1130 | this.layerParent = -1; 1131 | this.notes = []; 1132 | this.dots = []; 1133 | this.sampleFileName = null; 1134 | this.sampleAmp = 100; 1135 | this.sampleReversed = false; 1136 | this.sampleReverseStereo = false; 1137 | this.sampleUseLoopPoints = false; 1138 | this.envelopes = []; 1139 | this.filterType = FilterTypes.LowPass; 1140 | this.filterCut = 10000; 1141 | this.filterRes = 0.1; 1142 | this.filterEnabled = false; 1143 | this.arpDir = ArpDirections.Up; 1144 | this.arpRange = 0; 1145 | this.selectedArp = 0; 1146 | this.arpTime = 100; 1147 | this.arpGate = 100; 1148 | this.arpEnabled = false; 1149 | this.colorRed = 64; 1150 | this.colorGreen = 128; 1151 | this.colorBlue = 255; 1152 | } 1153 | 1154 | function FLEffectChannel() { 1155 | this.name = null; 1156 | this.volume = 300; 1157 | this.isMuted = false; 1158 | } 1159 | 1160 | function FLPlaylistItem() { 1161 | this.position = 0; 1162 | this.length = 1; 1163 | this.pattern = 0; 1164 | } 1165 | 1166 | function FLChannelEnvelope() { 1167 | this.target = null; 1168 | this.predelay = null; 1169 | this.attack = null; 1170 | this.hold = null; 1171 | this.decay = null; 1172 | this.sustain = null; 1173 | this.release = null; 1174 | this.amount = null; 1175 | } 1176 | 1177 | function FLAutomation() { 1178 | this.pos = 0; 1179 | this.value = 0; 1180 | this.channel = 0; 1181 | this.control = 0; 1182 | } 1183 | 1184 | function FLNote(len, pos, key, vol, pan) { 1185 | this.key = key; 1186 | this.volume = vol; 1187 | this.panning = pan; 1188 | this.length = len; 1189 | this.position = pos; 1190 | this.detuning = null; 1191 | } 1192 | 1193 | function FLProject() { 1194 | this.mainVolume = 300; 1195 | this.mainPitch = 0; 1196 | this.tempo = 140; 1197 | this.channelCount = 0; 1198 | this.channels = []; 1199 | this.effects = []; 1200 | this.playlistItems = []; 1201 | this.patternNames = []; 1202 | this.maxPatterns = 0; 1203 | this.currentPattern = 0; 1204 | this.activeEditPattern = 0; 1205 | this.currentEffectChannel = -1; 1206 | this.projectTitle = null; 1207 | this.versionString = null; 1208 | this.version = 0x100; 1209 | this.versionSpecificFactor = 1; 1210 | this.sampleList = []; 1211 | this.effectPlugins = []; 1212 | this.effectStrings = []; 1213 | 1214 | this.effectChannels = new Array(FLFxChannelCount + 1); 1215 | for (var i = 0; i <= FLFxChannelCount; i += 1) { 1216 | this.effectChannels[i] = new FLEffectChannel(); 1217 | } 1218 | } 1219 | 1220 | exports.createParser = createParser; 1221 | exports.createParserChild = createParserChild; 1222 | exports.parseFile = parseFile; 1223 | exports.FlpParser = FlpParser; 1224 | 1225 | exports.FLEffectChannel = FLEffectChannel; 1226 | exports.FLChannel = FLChannel; 1227 | exports.FLPlaylistItem = FLPlaylistItem; 1228 | exports.FLChannelEnvelope = FLChannelEnvelope; 1229 | exports.FLAutomation = FLAutomation; 1230 | exports.FLNote = FLNote; 1231 | exports.FLProject = FLProject; 1232 | 1233 | exports.FilterTypes = FilterTypes; 1234 | exports.ArpDirections = ArpDirections; 1235 | exports.EnvelopeTargets = EnvelopeTargets; 1236 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "flp", 3 | "version": "0.0.9", 4 | "description": "parse and read fl studio project files", 5 | "main": "index.js", 6 | "scripts": { 7 | "test": "mocha" 8 | }, 9 | "bin": { 10 | "flp-parse": "./cli.js" 11 | }, 12 | "keywords": [ 13 | "vst", 14 | "sample", 15 | "plugin" 16 | ], 17 | "author": "Andrew Kelley ", 18 | "license": "GPLv3", 19 | "devDependencies": { 20 | "mocha": "^1.18.2" 21 | }, 22 | "engines": { 23 | "node": ">=0.10.20" 24 | }, 25 | "dependencies": {}, 26 | "repository": { 27 | "type": "git", 28 | "url": "git://github.com/andrewrk/node-flp.git" 29 | }, 30 | "bugs": { 31 | "url": "https://github.com/andrewrk/node-flp/issues" 32 | }, 33 | "homepage": "https://github.com/andrewrk/node-flp" 34 | } 35 | -------------------------------------------------------------------------------- /test/flp/1-blank.flp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/andrewrk/node-flp/ba5fe0a4e8d85a43820aaf598873606a8ccfe0a0/test/flp/1-blank.flp -------------------------------------------------------------------------------- /test/flp/2-named.flp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/andrewrk/node-flp/ba5fe0a4e8d85a43820aaf598873606a8ccfe0a0/test/flp/2-named.flp -------------------------------------------------------------------------------- /test/flp/3-3.3-time-sig.flp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/andrewrk/node-flp/ba5fe0a4e8d85a43820aaf598873606a8ccfe0a0/test/flp/3-3.3-time-sig.flp -------------------------------------------------------------------------------- /test/flp/4-130-bpm.flp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/andrewrk/node-flp/ba5fe0a4e8d85a43820aaf598873606a8ccfe0a0/test/flp/4-130-bpm.flp -------------------------------------------------------------------------------- /test/flp/4front+mjcompressor.flp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/andrewrk/node-flp/ba5fe0a4e8d85a43820aaf598873606a8ccfe0a0/test/flp/4front+mjcompressor.flp -------------------------------------------------------------------------------- /test/flp/4frontpiano.flp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/andrewrk/node-flp/ba5fe0a4e8d85a43820aaf598873606a8ccfe0a0/test/flp/4frontpiano.flp -------------------------------------------------------------------------------- /test/flp/5-replace-sampler-with-3xosc.flp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/andrewrk/node-flp/ba5fe0a4e8d85a43820aaf598873606a8ccfe0a0/test/flp/5-replace-sampler-with-3xosc.flp -------------------------------------------------------------------------------- /test/flp/6-turn-osc3-volume-down.flp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/andrewrk/node-flp/ba5fe0a4e8d85a43820aaf598873606a8ccfe0a0/test/flp/6-turn-osc3-volume-down.flp -------------------------------------------------------------------------------- /test/flp/TheCastle_19.flp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/andrewrk/node-flp/ba5fe0a4e8d85a43820aaf598873606a8ccfe0a0/test/flp/TheCastle_19.flp -------------------------------------------------------------------------------- /test/flp/ambience.flp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/andrewrk/node-flp/ba5fe0a4e8d85a43820aaf598873606a8ccfe0a0/test/flp/ambience.flp -------------------------------------------------------------------------------- /test/flp/audio-clip.flp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/andrewrk/node-flp/ba5fe0a4e8d85a43820aaf598873606a8ccfe0a0/test/flp/audio-clip.flp -------------------------------------------------------------------------------- /test/flp/effects.flp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/andrewrk/node-flp/ba5fe0a4e8d85a43820aaf598873606a8ccfe0a0/test/flp/effects.flp -------------------------------------------------------------------------------- /test/flp/listen-to-my-synthesizer.flp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/andrewrk/node-flp/ba5fe0a4e8d85a43820aaf598873606a8ccfe0a0/test/flp/listen-to-my-synthesizer.flp -------------------------------------------------------------------------------- /test/flp/mdl.flp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/andrewrk/node-flp/ba5fe0a4e8d85a43820aaf598873606a8ccfe0a0/test/flp/mdl.flp -------------------------------------------------------------------------------- /test/flp/native-plugins.flp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/andrewrk/node-flp/ba5fe0a4e8d85a43820aaf598873606a8ccfe0a0/test/flp/native-plugins.flp -------------------------------------------------------------------------------- /test/flp/nucleon-orbit.flp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/andrewrk/node-flp/ba5fe0a4e8d85a43820aaf598873606a8ccfe0a0/test/flp/nucleon-orbit.flp -------------------------------------------------------------------------------- /test/mocha.opts: -------------------------------------------------------------------------------- 1 | --reporter spec 2 | -------------------------------------------------------------------------------- /test/test.js: -------------------------------------------------------------------------------- 1 | var path = require('path'); 2 | var assert = require('assert'); 3 | var fs = require('fs'); 4 | var flp = require('../'); 5 | 6 | var describe = global.describe; 7 | var it = global.it; 8 | 9 | var tests = [ 10 | { 11 | filename: '1-blank.flp', 12 | tempo: 140, 13 | }, 14 | { 15 | filename: '2-named.flp', 16 | tempo: 140, 17 | }, 18 | { 19 | filename: '3-3.3-time-sig.flp', 20 | tempo: 140, 21 | }, 22 | { 23 | filename: '4-130-bpm.flp', 24 | tempo: 130, 25 | }, 26 | { 27 | filename: '4front+mjcompressor.flp', 28 | tempo: 140, 29 | plugins: ['4Front Piano', 'MjMultibandCompressor'], 30 | }, 31 | { 32 | filename: '4frontpiano.flp', 33 | tempo: 140, 34 | plugins: ['4Front Piano'], 35 | }, 36 | { 37 | filename: '5-replace-sampler-with-3xosc.flp', 38 | tempo: 130, 39 | }, 40 | { 41 | filename: '6-turn-osc3-volume-down.flp', 42 | tempo: 130, 43 | }, 44 | { 45 | filename: 'ambience.flp', 46 | tempo: 140, 47 | plugins: ['Ambience'], 48 | }, 49 | { 50 | filename: 'audio-clip.flp', 51 | tempo: 140, 52 | }, 53 | { 54 | filename: 'effects.flp', 55 | tempo: 140, 56 | plugins: ['Ambience', 'Edison', 'Gross Beat', 'Hardcore', 57 | 'Maximus', 'MjMultibandCompressor', 'Soundgoodizer', 'Vocodex'], 58 | }, 59 | { 60 | filename: 'native-plugins.flp', 61 | tempo: 140, 62 | }, 63 | { 64 | filename: 'TheCastle_19.flp', 65 | tempo: 135, 66 | plugins: ['Synth1 VST', 'DirectWave', 'Sytrus'], 67 | }, 68 | { 69 | filename: 'listen-to-my-synthesizer.flp', 70 | tempo: 140, 71 | plugins: ['Nexus', 'Synth1 VST'], 72 | }, 73 | { 74 | filename: 'mdl.flp', 75 | tempo: 140, 76 | plugins: ['Decimort', 'Altiverb 6', 'FabFilter Timeless 2', 'FabFilter Pro-L'], 77 | }, 78 | { 79 | filename: 'nucleon-orbit.flp', 80 | tempo: 132, 81 | plugins: ['Harmless', 'Sytrus', 'Slicex', 'Maximus'], 82 | }, 83 | ]; 84 | 85 | describe("in process", function() { 86 | tests.forEach(function(test) { 87 | it(test.filename, function(done) { 88 | var filePath = path.join(__dirname, 'flp', test.filename); 89 | flp.parseFile(filePath, function(err, project) { 90 | if (err) return done(err); 91 | assert.strictEqual(project.tempo, test.tempo); 92 | if (test.plugins) { 93 | test.plugins.forEach(projectMustHavePlugin); 94 | } 95 | done(); 96 | 97 | function projectMustHavePlugin(pluginName) { 98 | var ok = false; 99 | for (var i = 0; i < project.channels.length; i += 1) { 100 | var generatorName = project.channels[i].generatorName; 101 | if (generatorName === pluginName) ok = true; 102 | } 103 | assert.ok(ok, "project is missing plugin: " + pluginName); 104 | } 105 | }); 106 | }); 107 | }); 108 | }); 109 | 110 | describe("child process", function() { 111 | tests.forEach(function(test) { 112 | it(test.filename, function(done) { 113 | var filePath = path.join(__dirname, 'flp', test.filename); 114 | var inStream = fs.createReadStream(filePath); 115 | var parser = flp.createParserChild(); 116 | parser.on('end', function(project) { 117 | assert.strictEqual(project.tempo, test.tempo); 118 | if (test.plugins) { 119 | test.plugins.forEach(projectMustHavePlugin); 120 | } 121 | done(); 122 | 123 | function projectMustHavePlugin(pluginName) { 124 | var ok = false; 125 | for (var i = 0; i < project.channels.length; i += 1) { 126 | var generatorName = project.channels[i].generatorName; 127 | if (generatorName === pluginName) ok = true; 128 | } 129 | assert.ok(ok, "project is missing plugin: " + pluginName); 130 | } 131 | }); 132 | inStream.pipe(parser); 133 | }); 134 | }); 135 | }); 136 | --------------------------------------------------------------------------------