├── .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 |
--------------------------------------------------------------------------------