|
|
|
|
@@ -24,99 +24,10 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
#include "flp_import.h"
|
|
|
|
|
|
|
|
|
|
/*
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
The FruityLoops
|
|
|
|
|
FLP
|
|
|
|
|
file format explained
|
|
|
|
|
|
|
|
|
|
(v 2.7.x)
|
|
|
|
|
|
|
|
|
|
(BETA)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
Introduction:
|
|
|
|
|
|
|
|
|
|
Here I will try to explain the FLP file format, which is the FruityLoops loop / song format. Visit www.fruityloops.com to get more info about FruityLoops.
|
|
|
|
|
|
|
|
|
|
First you've got to understand that FruityLoops keeps growing, so does the FLP format. Fortunately it has been designed for it... While I made some mistakes during its evolution, the format still remains updatable.
|
|
|
|
|
Normally anything made out of this document will be able to process future loop files, although of course you will miss newly added features.
|
|
|
|
|
|
|
|
|
|
Not sure what this document could be useful for, but if you ever use it to make something, send it to me!
|
|
|
|
|
Also if you'd like me to update this file (it's not complete yet), feel free to mail me.
|
|
|
|
|
|
|
|
|
|
(2/09/99) Didier Dambrin (gol)
|
|
|
|
|
gol@fruityloops.com
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
How it works:
|
|
|
|
|
|
|
|
|
|
Don't expect the FLP format to be a big chunk full of ordered parameters, like most trackers file formats. It had to evolute, so I chose the 'events' way.
|
|
|
|
|
You should see it a bit like MIDI / AIFF files. It's just a succession of events. Once you've understand how to process the file to retrieve these events, the only thing you'll need is the list of events available!
|
|
|
|
|
|
|
|
|
|
It's better, since once you've made the piece of code to get the events, you won't bother with the format anymore. Also you will just ignore any event you don't know (yet) about.
|
|
|
|
|
|
|
|
|
|
Please note that the format *does not* respect the AIFF standard, although I tried to keep the chunks system similar.
|
|
|
|
|
|
|
|
|
|
Although the program has been coded in Pascal, I'll do my best to use C++ declarations, so everyone will understand. DWORD is 4 bytes, WORD is 2 bytes.
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
Retrieving the events:
|
|
|
|
|
|
|
|
|
|
I said it looks like a MIDI file, but it's not a MIDI file.
|
|
|
|
|
|
|
|
|
|
First, you'll have to get & check the HEADER chunk, to be sure it's a FLP file.
|
|
|
|
|
The header is similar to the format of a MIDI file header:
|
|
|
|
|
|
|
|
|
|
DWORD ChunkID 4 chars which are the letters 'FLhd' for 'FruityLoops header'
|
|
|
|
|
DWORD Length The length of this chunk, like in MIDI files. Should be 6 because of the 3 WORDS below...
|
|
|
|
|
WORD Format Set to 0 for full songs.
|
|
|
|
|
WORD nChannels The total number of channels (not really used).
|
|
|
|
|
WORD BeatDiv Pulses per quarter of the song.
|
|
|
|
|
|
|
|
|
|
Most of this chunk is not used, it's just that I tried (as a start) to respect the proper MIDI header :)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
Then you'll encounter the DATA chunk, which is in fact the last chunk, the one containing all the events.
|
|
|
|
|
|
|
|
|
|
DWORD ChunkID 4 chars which are the letters 'FLdt' for 'FruityLoops data'
|
|
|
|
|
DWORD Length The length of this chunk WITHOUT these 2 DWORDS (that is minus 4*2 bytes), like in MIDI files.
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
The whole data chunk is a succession of EVENTS, which I'm going to explain...
|
|
|
|
|
To retrieve an event, first you read a byte (the event ID). According to this byte, the size of the event data varies:
|
|
|
|
|
0..63 The data after this byte is a BYTE (signed or unsigned, depending on the ID).
|
|
|
|
|
64..127 The data after this byte is a WORD.
|
|
|
|
|
128..191 The data after this byte is a DWORD.
|
|
|
|
|
192..255 The data after this byte is a variable-length block of data (a text for example).
|
|
|
|
|
|
|
|
|
|
That makes 64 BYTE events, 64 WORD events, 64 DWORD events & 64 TEXT events. The purpose of this split is of course to keep the file size small.
|
|
|
|
|
So you get the event ID & then you read the number of bytes according to this ID. Whether you process the event or not isn't important. What is important is that you can jump correctly to the next event if you skip it.
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
For TEXT (variable-length) events, you still have to read the size of the event, which is coded in the next byte(s) a bit like in MIDI files (but not stupidly inverted). After the size is the actual data, which you can process or skip.
|
|
|
|
|
To get the size of the event, you've got to read bytes until the last one, which has bit 7 off (the purpose of this compression is to reduce the file size again).
|
|
|
|
|
|
|
|
|
|
Start with a DWORD Size = 0. You're going to reconstruct the size by getting packs of 7 bits:
|
|
|
|
|
1. Get a byte.
|
|
|
|
|
2. Add the first 7 bits of this byte to Size.
|
|
|
|
|
3. Check bit 7 (the last bit) of this byte. If it's on, go back to 1. to process the next byte.
|
|
|
|
|
|
|
|
|
|
To resume, if Size < 128 then it will occupy only 1 byte, else if Size < 16384 it will occupy only 2 bytes & so on...
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
So globally, you open the file, check the header, point to the data chunk & retrieve / filter all the events. Easy(?)
|
|
|
|
|
Now let's get to the events...
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
*/
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
#include "project_notes.h"
|
|
|
|
|
#include "bb_editor.h"
|
|
|
|
|
#include "song_editor.h"
|
|
|
|
|
#include "bb_track.h"
|
|
|
|
|
#include "track_container.h"
|
|
|
|
|
#include "instrument_track.h"
|
|
|
|
|
#include "pattern.h"
|
|
|
|
|
@@ -280,29 +191,33 @@ bool flpImport::tryImport( trackContainer * _tc )
|
|
|
|
|
pattern * p = NULL;
|
|
|
|
|
char * text = NULL;
|
|
|
|
|
int text_len = 0;
|
|
|
|
|
int pat_cnt = 0;
|
|
|
|
|
int it_cnt = 0;
|
|
|
|
|
|
|
|
|
|
int ev_cnt = 0;
|
|
|
|
|
|
|
|
|
|
// read channels
|
|
|
|
|
/* for( int i = 0; i < num_channels; ++i )
|
|
|
|
|
{
|
|
|
|
|
pd.setValue( i );
|
|
|
|
|
#ifdef QT4
|
|
|
|
|
qApp->processEvents( QEventLoop::AllEvents, 100 );
|
|
|
|
|
#else
|
|
|
|
|
qApp->processEvents( 100 );
|
|
|
|
|
#endif
|
|
|
|
|
_tc->eng()->getSongEditor()->clearProject();
|
|
|
|
|
|
|
|
|
|
if( pd.wasCanceled() )
|
|
|
|
|
{
|
|
|
|
|
return( FALSE );
|
|
|
|
|
}*/
|
|
|
|
|
while( file().atEnd() == FALSE )
|
|
|
|
|
{
|
|
|
|
|
if( ++ev_cnt > 100 )
|
|
|
|
|
{
|
|
|
|
|
ev_cnt = 0;
|
|
|
|
|
#ifdef QT4
|
|
|
|
|
qApp->processEvents( QEventLoop::AllEvents, 100 );
|
|
|
|
|
#else
|
|
|
|
|
qApp->processEvents( 100 );
|
|
|
|
|
#endif
|
|
|
|
|
pd.setValue( it_cnt );
|
|
|
|
|
|
|
|
|
|
if( pd.wasCanceled() )
|
|
|
|
|
{
|
|
|
|
|
return( FALSE );
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
flpEvents ev = static_cast<flpEvents>( readByte() );
|
|
|
|
|
Uint32 data = readByte();
|
|
|
|
|
|
|
|
|
|
//printf("ev: %d\n", (int) ev );
|
|
|
|
|
if( ev >= FLP_Word && ev < FLP_Text )
|
|
|
|
|
{
|
|
|
|
|
data = data | ( readByte() << 8 );
|
|
|
|
|
@@ -314,16 +229,6 @@ bool flpImport::tryImport( trackContainer * _tc )
|
|
|
|
|
data = data | ( readByte() << 24 );
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/*For TEXT (variable-length) events, you still have to read the size of the event, which is coded in the next byte(s) a bit like in MIDI files (but not stupidly inverted). After the size is the actual data, which you can process or skip.
|
|
|
|
|
To get the size of the event, you've got to read bytes until the last one, which has bit 7 off (the purpose of this compression is to reduce the file size again).
|
|
|
|
|
|
|
|
|
|
Start with a DWORD Size = 0. You're going to reconstruct the size by getting packs of 7 bits:
|
|
|
|
|
1. Get a byte.
|
|
|
|
|
2. Add the first 7 bits of this byte to Size.
|
|
|
|
|
3. Check bit 7 (the last bit) of this byte. If it's on, go back to 1. to process the next byte.
|
|
|
|
|
|
|
|
|
|
To resume, if Size < 128 then it will occupy only 1 byte, else if Size < 16384 it will occupy only 2 bytes & so on...
|
|
|
|
|
*/
|
|
|
|
|
|
|
|
|
|
if( ev >= FLP_Text )
|
|
|
|
|
{
|
|
|
|
|
@@ -345,7 +250,12 @@ To resume, if Size < 128 then it will occupy only 1 byte, else if Size < 16384 i
|
|
|
|
|
switch( ev )
|
|
|
|
|
{
|
|
|
|
|
// BYTE EVENTS
|
|
|
|
|
case FLP_Byte:
|
|
|
|
|
printf( "undefined byte %d\n", data );
|
|
|
|
|
break;
|
|
|
|
|
|
|
|
|
|
case FLP_NoteOn:
|
|
|
|
|
printf( "note on: %d\n", data );
|
|
|
|
|
// data = pos how to handle?
|
|
|
|
|
break;
|
|
|
|
|
|
|
|
|
|
@@ -365,10 +275,6 @@ To resume, if Size < 128 then it will occupy only 1 byte, else if Size < 16384 i
|
|
|
|
|
printf( "main-volume: %d\n", data );
|
|
|
|
|
break;
|
|
|
|
|
|
|
|
|
|
case FLP_MainPitch:
|
|
|
|
|
printf( "main-pitch: %d\n", data );
|
|
|
|
|
break;
|
|
|
|
|
|
|
|
|
|
case FLP_PatLength:
|
|
|
|
|
printf( "pattern-length: %d\n", data );
|
|
|
|
|
break;
|
|
|
|
|
@@ -377,18 +283,35 @@ To resume, if Size < 128 then it will occupy only 1 byte, else if Size < 16384 i
|
|
|
|
|
printf( "block length: %d\n", data );
|
|
|
|
|
break;
|
|
|
|
|
|
|
|
|
|
case FLP_LoopType:
|
|
|
|
|
printf( "loop type: %d\n", data );
|
|
|
|
|
break;
|
|
|
|
|
|
|
|
|
|
case FLP_ChanType:
|
|
|
|
|
printf( "channel type: %d\n", data );
|
|
|
|
|
break;
|
|
|
|
|
|
|
|
|
|
case FLP_MixSliceNum:
|
|
|
|
|
printf( "mix slice num: %d\n", data );
|
|
|
|
|
break;
|
|
|
|
|
|
|
|
|
|
case 31:
|
|
|
|
|
case 32:
|
|
|
|
|
case 33:
|
|
|
|
|
case 34:
|
|
|
|
|
case 35:
|
|
|
|
|
case 36:
|
|
|
|
|
printf( "ev: %d data: %d\n", ev, data );
|
|
|
|
|
break;
|
|
|
|
|
|
|
|
|
|
// WORD EVENTS
|
|
|
|
|
case FLP_NewChan:
|
|
|
|
|
printf( "new channel\n" );
|
|
|
|
|
++it_cnt;
|
|
|
|
|
m_events.clear();
|
|
|
|
|
pat_cnt = 0;
|
|
|
|
|
|
|
|
|
|
it = dynamic_cast<instrumentTrack *>(
|
|
|
|
|
track::create( track::CHANNEL_TRACK, _tc ) );
|
|
|
|
|
track::create( track::CHANNEL_TRACK, _tc->eng()->getBBEditor() ) );
|
|
|
|
|
assert( it != NULL );
|
|
|
|
|
it->loadInstrument( "tripleoscillator" );
|
|
|
|
|
it->toggledInstrumentTrackButton( FALSE );
|
|
|
|
|
@@ -396,10 +319,14 @@ To resume, if Size < 128 then it will occupy only 1 byte, else if Size < 16384 i
|
|
|
|
|
break;
|
|
|
|
|
|
|
|
|
|
case FLP_NewPat:
|
|
|
|
|
p = dynamic_cast<pattern *>( it->createTCO(
|
|
|
|
|
pat_cnt ) );
|
|
|
|
|
assert( p != NULL );
|
|
|
|
|
it->addTCO( p );
|
|
|
|
|
while( _tc->eng()->getBBEditor()->numOfBBs() <=
|
|
|
|
|
data )
|
|
|
|
|
{
|
|
|
|
|
track::create( track::BB_TRACK,
|
|
|
|
|
_tc->eng()->getSongEditor() );
|
|
|
|
|
}
|
|
|
|
|
p = dynamic_cast<pattern *>(
|
|
|
|
|
it->getTCO( data - 1 ) );
|
|
|
|
|
break;
|
|
|
|
|
|
|
|
|
|
case FLP_Tempo:
|
|
|
|
|
@@ -411,17 +338,46 @@ To resume, if Size < 128 then it will occupy only 1 byte, else if Size < 16384 i
|
|
|
|
|
break;
|
|
|
|
|
|
|
|
|
|
case FLP_FX:
|
|
|
|
|
printf( "FX-channel for cur channel: %d\n", data );
|
|
|
|
|
printf( "FX-channel for cur channel: %d\n",
|
|
|
|
|
data );
|
|
|
|
|
break;
|
|
|
|
|
|
|
|
|
|
case FLP_Fade_Stereo:
|
|
|
|
|
printf( "fade stereo: %d\n", data );
|
|
|
|
|
break;
|
|
|
|
|
|
|
|
|
|
case FLP_CutOff:
|
|
|
|
|
printf( "cutoff (for cur channel?): %d\n", data );
|
|
|
|
|
printf( "cutoff (for cur channel?): %d\n",
|
|
|
|
|
data );
|
|
|
|
|
break;
|
|
|
|
|
|
|
|
|
|
case FLP_PreAmp:
|
|
|
|
|
printf( "pre-amp (for cur channel?): %d\n",
|
|
|
|
|
data );
|
|
|
|
|
break;
|
|
|
|
|
|
|
|
|
|
case FLP_Decay:
|
|
|
|
|
printf( "decay (for cur channel?): %d\n",
|
|
|
|
|
data );
|
|
|
|
|
break;
|
|
|
|
|
|
|
|
|
|
case FLP_Attack:
|
|
|
|
|
printf( "attack (for cur channel?): %d\n",
|
|
|
|
|
data );
|
|
|
|
|
break;
|
|
|
|
|
|
|
|
|
|
case FLP_MainPitch:
|
|
|
|
|
printf( "main-pitch: %d\n", data );
|
|
|
|
|
break;
|
|
|
|
|
|
|
|
|
|
case FLP_Resonance:
|
|
|
|
|
printf( "reso (for cur channel?): %d\n", data );
|
|
|
|
|
break;
|
|
|
|
|
|
|
|
|
|
case FLP_StDel:
|
|
|
|
|
printf( "stdel (delay?): %d\n", data );
|
|
|
|
|
break;
|
|
|
|
|
|
|
|
|
|
case FLP_FX3:
|
|
|
|
|
printf( "FX 3: %d\n", data );
|
|
|
|
|
break;
|
|
|
|
|
@@ -431,6 +387,40 @@ To resume, if Size < 128 then it will occupy only 1 byte, else if Size < 16384 i
|
|
|
|
|
break;
|
|
|
|
|
|
|
|
|
|
// DWORD EVENTS
|
|
|
|
|
case FLP_Color:
|
|
|
|
|
printf( "color r:%d g:%d b:%d\n",
|
|
|
|
|
qRed( data ),
|
|
|
|
|
qGreen( data ),
|
|
|
|
|
qBlue( data ) );
|
|
|
|
|
break;
|
|
|
|
|
|
|
|
|
|
case FLP_PlayListItem:
|
|
|
|
|
{
|
|
|
|
|
unsigned int pat_num = ( data >> 16 ) - 1;
|
|
|
|
|
while( _tc->eng()->getBBEditor()->numOfBBs() <=
|
|
|
|
|
pat_num )
|
|
|
|
|
{
|
|
|
|
|
track::create( track::BB_TRACK,
|
|
|
|
|
_tc->eng()->getSongEditor() );
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
bbTrack * bbt = bbTrack::findBBTrack( pat_num,
|
|
|
|
|
_tc->eng() );
|
|
|
|
|
trackContentObject * tco = bbt->addTCO(
|
|
|
|
|
bbt->createTCO( 0 ) );
|
|
|
|
|
tco->movePosition( midiTime( ( data & 0xffff ) *
|
|
|
|
|
64 ) );
|
|
|
|
|
break;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
case FLP_FXSine:
|
|
|
|
|
printf( "fx sine: %d\n", data );
|
|
|
|
|
break;
|
|
|
|
|
|
|
|
|
|
case FLP_CutCutBy:
|
|
|
|
|
printf( "cut cut by: %d\n", data );
|
|
|
|
|
break;
|
|
|
|
|
|
|
|
|
|
case FLP_DelayReso:
|
|
|
|
|
printf( "delay resonance: %d\n", data );
|
|
|
|
|
break;
|
|
|
|
|
@@ -439,6 +429,28 @@ To resume, if Size < 128 then it will occupy only 1 byte, else if Size < 16384 i
|
|
|
|
|
printf( "reverb: %d\n", data );
|
|
|
|
|
break;
|
|
|
|
|
|
|
|
|
|
case FLP_IntStretch:
|
|
|
|
|
printf( "int stretch: %d\n", data );
|
|
|
|
|
break;
|
|
|
|
|
|
|
|
|
|
// TEXT EVENTS
|
|
|
|
|
case FLP_Text_ChanName:
|
|
|
|
|
it->setName( text );
|
|
|
|
|
break;
|
|
|
|
|
|
|
|
|
|
case FLP_Text_CommentRTF:
|
|
|
|
|
_tc->eng()->getProjectNotes()->setText( text );
|
|
|
|
|
break;
|
|
|
|
|
|
|
|
|
|
case FLP_Text_Title:
|
|
|
|
|
printf( "project title: %s\n", text );
|
|
|
|
|
break;
|
|
|
|
|
|
|
|
|
|
case FLP_Text_SampleFileName:
|
|
|
|
|
printf( "sample for current channel: %s\n",
|
|
|
|
|
text );
|
|
|
|
|
break;
|
|
|
|
|
|
|
|
|
|
case FLP_Version:
|
|
|
|
|
printf( "FL Version: %s\n", text );
|
|
|
|
|
break;
|
|
|
|
|
@@ -446,11 +458,15 @@ To resume, if Size < 128 then it will occupy only 1 byte, else if Size < 16384 i
|
|
|
|
|
case FLP_Text_PluginName:
|
|
|
|
|
if( QString( text ) == "3x Osc" )
|
|
|
|
|
{
|
|
|
|
|
it->loadInstrument( "tripleoscillator" );
|
|
|
|
|
printf( "loading TripleOscillator for "
|
|
|
|
|
"3x Osc\n" );
|
|
|
|
|
it->loadInstrument(
|
|
|
|
|
"tripleoscillator" );
|
|
|
|
|
}
|
|
|
|
|
else
|
|
|
|
|
{
|
|
|
|
|
printf( "unsupported plugin: %s\n", text );
|
|
|
|
|
printf( "unsupported plugin: %s\n",
|
|
|
|
|
text );
|
|
|
|
|
}
|
|
|
|
|
break;
|
|
|
|
|
|
|
|
|
|
@@ -468,46 +484,27 @@ To resume, if Size < 128 then it will occupy only 1 byte, else if Size < 16384 i
|
|
|
|
|
dump_mem( text, text_len );
|
|
|
|
|
break;
|
|
|
|
|
|
|
|
|
|
case FLP_ChanParams:
|
|
|
|
|
printf( "plugin params:\n" );
|
|
|
|
|
dump_mem( text, text_len );
|
|
|
|
|
break;
|
|
|
|
|
|
|
|
|
|
default:
|
|
|
|
|
if( ev >= FLP_Text )
|
|
|
|
|
{
|
|
|
|
|
printf( "unhandled text (%d): %s\n", ev,
|
|
|
|
|
text );
|
|
|
|
|
printf( "!! unhandled text (ev: %d, "
|
|
|
|
|
"len: %d):\n",
|
|
|
|
|
ev, text_len );
|
|
|
|
|
dump_mem( text, text_len );
|
|
|
|
|
}
|
|
|
|
|
else
|
|
|
|
|
{
|
|
|
|
|
printf( "!! handling of FLP-event %d not "
|
|
|
|
|
"implemented yet.\n", ev );
|
|
|
|
|
printf( "!! handling of FLP-event %d "
|
|
|
|
|
"not implemented yet.\n", ev );
|
|
|
|
|
}
|
|
|
|
|
break;
|
|
|
|
|
}
|
|
|
|
|
/*
|
|
|
|
|
// now create new channel-track for reading track
|
|
|
|
|
instrumentTrack * ct = dynamic_cast<instrumentTrack *>(
|
|
|
|
|
track::create(
|
|
|
|
|
track::CHANNEL_TRACK,
|
|
|
|
|
_tc ) );
|
|
|
|
|
#ifdef LMMS_DEBUG
|
|
|
|
|
assert( ct != NULL );
|
|
|
|
|
#endif
|
|
|
|
|
// TODO: setup program, channel etc.
|
|
|
|
|
ct->loadInstrument( "tripleoscillator" );
|
|
|
|
|
ct->toggledInstrumentTrackButton( FALSE );
|
|
|
|
|
|
|
|
|
|
// now create pattern to store notes in
|
|
|
|
|
pattern * p = dynamic_cast<pattern *>( ct->createTCO( 0 ) );
|
|
|
|
|
#ifdef LMMS_DEBUG
|
|
|
|
|
assert( p != NULL );
|
|
|
|
|
#endif
|
|
|
|
|
ct->addTCO( p );
|
|
|
|
|
|
|
|
|
|
// init keys
|
|
|
|
|
int keys[NOTES_PER_OCTAVE * OCTAVES][2];
|
|
|
|
|
for( int j = 0; j < NOTES_PER_OCTAVE * OCTAVES; ++j )
|
|
|
|
|
{
|
|
|
|
|
keys[j][0] = -1;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// now process every event
|
|
|
|
|
for( eventVector::const_iterator it = m_events.begin();
|
|
|
|
|
it != m_events.end(); ++it )
|
|
|
|
|
@@ -742,17 +739,8 @@ bool FASTCALL flpImport::readTrack( int _track_end )
|
|
|
|
|
|
|
|
|
|
#endif
|
|
|
|
|
|
|
|
|
|
/*
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
void flpImport::error( void )
|
|
|
|
|
{
|
|
|
|
|
printf( "flpImport::readTrack(): invalid MIDI data (offset %#x)\n",
|
|
|
|
|
(unsigned int) file().pos() );
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
*/
|
|
|
|
|
|
|
|
|
|
extern "C"
|
|
|
|
|
{
|
|
|
|
|
|
|
|
|
|
|