flp-import-basics and MIDI-tab-widget-extension

git-svn-id: https://lmms.svn.sf.net/svnroot/lmms/trunk/lmms@113 0778d3d1-df1d-0410-868b-ea421aaaa00d
This commit is contained in:
Tobias Doerffel
2006-03-28 12:02:57 +00:00
parent 387cee03d4
commit 7534b09714
21 changed files with 1433 additions and 58 deletions

View File

@@ -2,5 +2,5 @@ if VST_SUPPORT
VESTIGE_SUBDIR=vestige
endif
SUBDIRS = audio_file_processor bit_invader midi_import organic plucked_string_synth triple_oscillator $(VESTIGE_SUBDIR) vibed
SUBDIRS = audio_file_processor bit_invader flp_import midi_import organic plucked_string_synth triple_oscillator $(VESTIGE_SUBDIR) vibed

View File

@@ -0,0 +1,12 @@
AUTOMAKE_OPTIONS = foreign 1.4
INCLUDES = -I$(top_srcdir)/include -I.
AM_CXXFLAGS := $(AM_CXXFLAGS) $(QT_CXXFLAGS) -DPLUGIN_NAME="flpimport"
pkglib_LTLIBRARIES= libflpimport.la
libflpimport_la_SOURCES = flp_import.cpp flp_import.h

View File

@@ -0,0 +1,644 @@
/*
* flp_import.cpp - support for importing FLP-files
*
* Copyright (c) 2006 Tobias Doerffel <tobydox/at/users.sourceforge.net>
* This file partly contains code from Fluidsynth, Peter Hanappe
*
* This file is part of Linux MultiMedia Studio - http://lmms.sourceforge.net
*
* This program is free software; you can redistribute it and/or
* modify it under the terms of the GNU General Public
* License as published by the Free Software Foundation; either
* version 2 of the License, or (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
* General Public License for more details.
*
* You should have received a copy of the GNU General Public
* License along with this program (see COPYING); if not, write to the
* Free Software Foundation, Inc., 59 Temple Place - Suite 330,
* Boston, MA 02111-1307, USA.
*
*/
#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 "track_container.h"
#include "instrument_track.h"
#include "pattern.h"
#ifdef QT4
#include <Qt/QtXml>
#include <QApplication>
#include <QProgressDialog>
#else
#include <qdom.h>
#include <qapplication.h>
#include <qprogressdialog.h>
#define pos at
#define setValue setProgress
#endif
#define makeID(_c0, _c1, _c2, _c3) \
( ( _c0 ) | ( ( _c1 ) << 8 ) | ( ( _c2 ) << 16 ) | ( ( _c3 ) << 24 ) )
extern "C"
{
plugin::descriptor flpimport_plugin_descriptor =
{
STRINGIFY_PLUGIN_NAME( PLUGIN_NAME ),
"FLP Import",
QT_TRANSLATE_NOOP( "pluginBrowser",
"Filter for importing FL Studio projects into LMMS" ),
"Tobias Doerffel <tobydox/at/users/dot/sf/dot/net>",
0x0100,
plugin::IMPORT_FILTER,
new QPixmap()
} ;
}
flpImport::flpImport( const QString & _file ) :
importFilter( _file, &flpimport_plugin_descriptor, NULL )
{
}
flpImport::~flpImport()
{
}
bool flpImport::tryImport( trackContainer * _tc )
{
if( openFile() == FALSE )
{
return( FALSE );
}
if( readID() != makeID( 'F', 'L', 'h', 'd' ) )
{
printf( "flpImport::tryImport(): not a valid FL project\n" );
return( FALSE );
}
const int header_len = readInt( 4 );
printf("%d\n", header_len );
if( header_len != 6 )
{
invalid_format:
printf( "flpImport::tryImport(): invalid file format\n" );
return( FALSE );
}
const int type = readInt( 2 );
if( type != 0 )
{
printf( "flpImport::tryImport(): type %d format is not "
"supported\n", type );
return( FALSE );
}
const int num_channels = readInt( 2 );
if( num_channels < 1 || num_channels > 1000 )
{
printf( "flpImport::tryImport(): invalid number of channels "
"(%d)\n", num_channels );
return( FALSE );
}
printf( "channels: %d\n", num_channels );
const int ppq = readInt( 2 );
if( ppq < 0 )
{
goto invalid_format;
}
#ifdef QT4
QProgressDialog pd( trackContainer::tr( "Importing FLP-file..." ),
trackContainer::tr( "Cancel" ), 0, num_channels );
#else
QProgressDialog pd( trackContainer::tr( "Importing FLP-file..." ),
trackContainer::tr( "Cancel" ), num_channels,
0, 0, TRUE );
#endif
pd.setWindowTitle( trackContainer::tr( "Please wait..." ) );
pd.show();
bool valid = FALSE;
// search for FLdt chunk
while( 1 )
{
Sint32 id = readID();
const int len = readInt( 4 );
if( file().atEnd() )
{
printf( "flpImport::tryImport(): unexpected "
"end of file\n" );
return( FALSE );
}
if( len < 0 || len >= 0x10000000 )
{
printf( "flpImport::tryImport(): invalid "
"chunk length %d\n", len );
return( FALSE );
}
if( id == makeID( 'F', 'L', 'd', 't' ) )
{
valid = TRUE;
break;
}
skip( len );
}
if( valid == FALSE )
{
return( FALSE );
}
instrumentTrack * it = NULL;
pattern * p = NULL;
int pat_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
if( pd.wasCanceled() )
{
return( FALSE );
}
flpEvents ev = static_cast<flpEvents>( readByte() );
Uint32 data = readByte();
if( ev >= FLP_Word && ev < FLP_Text )
{
data = ( data << 8 ) + readByte();
}
if( ev >= FLP_Int && ev < FLP_Text )
{
data = ( data << 8 ) + readByte();
data = ( data << 8 ) + readByte();
}
/*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 )
{
int read_so_far = 2;
Uint32 len = data & 0x7F;
while( data & 0x80 )
{
data = readByte();
len = ( len << 7 ) | ( data & 0x7F );
++read_so_far;
}
skip( len - read_so_far );
continue;
}
switch( ev )
{
case FLP_NoteOn:
// data = pos how to handle?
break;
case FLP_NewChan:
m_events.clear();
pat_cnt = 0;
it = dynamic_cast<instrumentTrack *>(
track::create( track::CHANNEL_TRACK, _tc ) );
assert( it != NULL );
it->loadInstrument( "tripleoscillator" );
it->toggledInstrumentTrackButton( FALSE );
continue;
case FLP_NewPat:
p = dynamic_cast<pattern *>( it->createTCO(
pat_cnt ) );
assert( p != NULL );
it->addTCO( p );
break;
default:
printf( "handling of FLP-event %d not "
"implemented yet.", 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 )
{
const int tick = it->first;
const midiEvent & ev = it->second;
switch( ev.m_type )
{
case NOTE_ON:
if( ev.key() >=
NOTES_PER_OCTAVE * OCTAVES )
{
continue;
}
if( ev.velocity() > 0 )
{
keys[ev.key()][0] = tick;
keys[ev.key()][1] =
ev.velocity();
break;
}
case NOTE_OFF:
if( ev.key() <
NOTES_PER_OCTAVE * OCTAVES &&
keys[ev.key()][0] >= 0 )
{
note n( eng(),
midiTime( ( tick - keys[ev.key()][0] ) / 10 ),
midiTime( keys[ev.key()][0] / 10 ),
(tones)( ev.key() % NOTES_PER_OCTAVE ),
(octaves)( ev.key() / NOTES_PER_OCTAVE ),
keys[ev.key()][1] * 100 / 128 );
p->addNote( n );
keys[ev.key()][0] = -1;
}
break;
default:
// printf( "Unhandled event: %#x\n",
// ev.m_type );
break;
}
}*/
}
return( TRUE );
}
#if 0
bool FASTCALL flpImport::readTrack( int _track_end )
{
int tick = 0;
unsigned char last_cmd = 0;
// unsigned char port = 0;
m_events.clear();
// the current file position is after the track ID and length
while( (int) file().pos() < _track_end )
{
unsigned char cmd;
int len;
int delta_ticks = readVar();
if( delta_ticks < 0 )
{
break;
}
tick += delta_ticks;
int c = readByte();
if( c < 0 )
{
break;
}
if( c & 0x80 )
{
// have command
cmd = c;
if( cmd < 0xf0 )
{
last_cmd = cmd;
}
}
else
{
// running status
ungetChar( c );
cmd = last_cmd;
if( !cmd )
{
error();
return( FALSE );
}
}
switch( cmd & 0xF0 )
{
// channel msg with 2 parameter bytes
case NOTE_OFF:
case NOTE_ON:
case KEY_PRESSURE:
case CONTROL_CHANGE:
case PITCH_BEND:
{
int data1 = readByte() & 0x7F;
int data2 = readByte() & 0x7F;
m_events.push_back( qMakePair( tick,
midiEvent( static_cast<midiEventTypes>(
cmd & 0xF0 ),
cmd & 0x0F,
data1,
data2 ) ) );
break;
}
// channel msg with 1 parameter byte
case PROGRAM_CHANGE:
case CHANNEL_PRESSURE:
m_events.push_back( qMakePair( tick,
midiEvent( static_cast<midiEventTypes>(
cmd & 0xF0 ),
cmd & 0x0F,
readByte() & 0x7F ) ) );
break;
case MIDI_SYSEX:
switch( cmd )
{
case MIDI_SYSEX:
case MIDI_EOX:
{
len = readVar();
if( len < 0 )
{
error();
return( FALSE );
}
if( cmd == MIDI_SYSEX )
{
++len;
}
char * data = new char[len];
if( cmd == MIDI_SYSEX )
{
data[0] = MIDI_SYSEX;
}
for( ; c < len; ++c )
{
data[c] = readByte();
}
m_events.push_back(
qMakePair( tick,
midiEvent( MIDI_SYSEX, data, len ) ) );
break;
}
case MIDI_META_EVENT:
c = readByte();
len = readVar();
/* if( len < 0 )
{
error();
return( FALSE );
}*/
switch( c )
{
case 0x21: // port number
if( len < 1 )
{
error();
return( FALSE );
}
/* port = readByte() %
port_count;
skip( len - 1 );*/
skip( len );
break;
case 0x2F: // end of track
//track->end_tick = tick;
skip( _track_end -
file().pos() );
return( TRUE );
case 0x51: // tempo
if( len < 3 )
{
error();
return( FALSE );
}
if( m_smpteTiming )
{
// SMPTE timing
// doesnt change
skip( len );
}
else
{
/* event = new_event(track, 0);
event->type = SND_SEQ_EVENT_TEMPO;
event->port = port;
event->tick = tick;
event->data.tempo = read_byte() << 16;
event->data.tempo |= read_byte() << 8;
event->data.tempo |= read_byte();
skip( len -3 );*/
skip( len );
}
break;
default:// ignore all other
// meta events
skip( len );
break;
}
break;
default: // invalid Fx command
error();
return( FALSE );
}
break;
default: // cannot happen
error();
return( FALSE );
}
}
error();
return( FALSE );
}
#endif
/*
void flpImport::error( void )
{
printf( "flpImport::readTrack(): invalid MIDI data (offset %#x)\n",
(unsigned int) file().pos() );
}
*/
extern "C"
{
// neccessary for getting instance out of shared lib
plugin * lmms_plugin_main( void * _data )
{
return( new flpImport( static_cast<const char *>( _data ) ) );
}
}
#undef pos
#undef setValue

247
plugins/flp_import/flp_import.h Executable file
View File

@@ -0,0 +1,247 @@
/*
* flp_import.h - support for importing FLP-files
*
* Copyright (c) 2006 Tobias Doerffel <tobydox/at/users.sourceforge.net>
*
* This file is part of Linux MultiMedia Studio - http://lmms.sourceforge.net
*
* This program is free software; you can redistribute it and/or
* modify it under the terms of the GNU General Public
* License as published by the Free Software Foundation; either
* version 2 of the License, or (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
* General Public License for more details.
*
* You should have received a copy of the GNU General Public
* License along with this program (see COPYING); if not, write to the
* Free Software Foundation, Inc., 59 Temple Place - Suite 330,
* Boston, MA 02111-1307, USA.
*
*/
#ifndef _FLP_IMPORT_H
#define _FLP_IMPORT_H
#ifdef HAVE_CONFIG_H
#include <config.h>
#endif
#include "qt3support.h"
#ifdef QT4
#include <QString>
#include <QPair>
#include <QVector>
#else
#include <qstring.h>
#include <qpair.h>
#include <qvaluevector.h>
#endif
#include "midi.h"
#include "import_filter.h"
enum flpEvents
{
// BYTE EVENTS
FLP_Byte = 0,
FLP_Enabled = 0,
FLP_NoteOn = 1, //+pos (byte)
FLP_Vol = 2,
FLP_Pan = 3,
FLP_MIDIChan = 4,
FLP_MIDINote = 5,
FLP_MIDIPatch = 6,
FLP_MIDIBank = 7,
FLP_LoopActive = 9,
FLP_ShowInfo = 10,
FLP_Shuffle = 11,
FLP_MainVol = 12,
FLP_Stretch = 13, // old byte version
FLP_Pitchable = 14,
FLP_Zipped = 15,
FLP_Delay_Flags = 16,
FLP_PatLength = 17,
FLP_BlockLength = 18,
FLP_UseLoopPoints = 19,
FLP_LoopType = 20,
FLP_ChanType = 21,
FLP_MixSliceNum = 22,
// WORD EVENTS
FLP_Word = 64,
FLP_NewChan = FLP_Word,
FLP_NewPat = FLP_Word + 1, //+PatNum (word)
FLP_Tempo = FLP_Word + 2,
FLP_CurrentPatNum = FLP_Word + 3,
FLP_PatData = FLP_Word + 4,
FLP_FX = FLP_Word + 5,
FLP_Fade_Stereo = FLP_Word + 6,
FLP_CutOff = FLP_Word + 7,
FLP_DotVol = FLP_Word + 8,
FLP_DotPan = FLP_Word + 9,
FLP_PreAmp = FLP_Word + 10,
FLP_Decay = FLP_Word + 11,
FLP_Attack = FLP_Word + 12,
FLP_DotNote = FLP_Word + 13,
FLP_DotPitch = FLP_Word + 14,
FLP_DotMix = FLP_Word + 15,
FLP_MainPitch = FLP_Word + 16,
FLP_RandChan = FLP_Word + 17,
FLP_MixChan = FLP_Word + 18,
FLP_Resonance = FLP_Word + 19,
FLP_LoopBar = FLP_Word + 20,
FLP_StDel = FLP_Word + 21,
FLP_FX3 = FLP_Word + 22,
FLP_DotReso = FLP_Word + 23,
FLP_DotCutOff = FLP_Word + 24,
FLP_ShiftDelay = FLP_Word + 25,
FLP_LoopEndBar = FLP_Word + 26,
FLP_Dot = FLP_Word + 27,
FLP_DotShift = FLP_Word + 28,
// DWORD EVENTS
FLP_Int = 128,
FLP_Color = FLP_Int,
FLP_PlayListItem = FLP_Int + 1, //+Pos (word) +PatNum (word)
FLP_Echo = FLP_Int + 2,
FLP_FXSine = FLP_Int + 3,
FLP_CutCutBy = FLP_Int + 4,
FLP_WindowH = FLP_Int + 5,
FLP_MiddleNote = FLP_Int + 7,
FLP_Reserved = FLP_Int + 8, // may contain an invalid version info
FLP_MainResoCutOff = FLP_Int + 9,
FLP_DelayReso = FLP_Int + 10,
FLP_Reverb = FLP_Int + 11,
FLP_IntStretch = FLP_Int + 12,
FLP_SSNote = FLP_Int + 13,
FLP_FineTune = FLP_Int + 14,
// TEXT EVENTS
FLP_Undef = 192, //+Size (var length)
FLP_Text = FLP_Undef, //+Size (var length)+Text
// (Null Term. String)
FLP_Text_ChanName = FLP_Text, // name for the current channel
FLP_Text_PatName = FLP_Text + 1, // name for the current pattern
FLP_Text_Title = FLP_Text + 2, // title of the loop
FLP_Text_Comment = FLP_Text + 3, // old comments in text format.
// Not used anymore
FLP_Text_SampleFileName = FLP_Text + 4, // filename for the sample in
// the current channel, stored
// as relative path
FLP_Text_URL = FLP_Text + 5,
FLP_Text_CommentRTF = FLP_Text + 6, // new comments in Rich Text
// format
FLP_Version = FLP_Text + 7,
FLP_Text_PluginName = FLP_Text + 9, // plugin file name
// (without path)
FLP_MIDICtrls = FLP_Text + 16,
FLP_Delay = FLP_Text + 17,
FLP_TS404Params = FLP_Text + 18,
FLP_DelayLine = FLP_Text + 19,
FLP_NewPlugin = FLP_Text + 20,
FLP_PluginParams = FLP_Text + 21,
FLP_ChanParams = FLP_Text + 23,// block of various channel
// params (can grow)
FLP_CmdCount
} ;
class flpImport : public importFilter
{
public:
flpImport( const QString & _file );
virtual ~flpImport();
private:
virtual bool tryImport( trackContainer * _tc );
inline int readInt( int _bytes )
{
int c, value = 0;
do
{
c = readByte();
if( c == -1 )
{
return( -1 );
}
value = ( value << 8 ) | c;
} while( --_bytes );
return( value );
}
inline Sint32 read32LE( void )
{
int value = readByte();
value |= readByte() << 8;
value |= readByte() << 16;
value |= readByte() << 24;
return( value );
}
/* inline int readVar( void )
{
int c = readByte();
int value = c & 0x7f;
if( c & 0x80 )
{
c = readByte();
value = ( value << 7 ) | ( c & 0x7f );
if( c & 0x80 )
{
c = readByte();
value = ( value << 7 ) | ( c & 0x7f );
if( c & 0x80 )
{
c = readByte();
value = ( value << 7 ) | c;
if( c & 0x80 )
{
return -1;
}
}
}
}
return( !file().atEnd() ? value : -1 );
}*/
inline Sint32 readID( void )
{
return( read32LE() );
}
inline void skip( int _bytes )
{
while( _bytes > 0 )
{
readByte();
--_bytes;
}
}
typedef vvector<QPair<int, midiEvent> > eventVector;
eventVector m_events;
} ;
#endif

View File

@@ -101,8 +101,8 @@ bool midiImport::tryImport( trackContainer * _tc )
return( readRIFF( _tc ) );
default:
printf( "midiImport::importToTrackContainer(): not a "
"Standard MIDI file\n" );
printf( "midiImport::tryImport(): not a Standard MIDI "
"file\n" );
return( FALSE );
}
}

View File

@@ -1,5 +1,5 @@
/*
* midi_import_filter.h - support for importing MIDI-files
* midi_import.h - support for importing MIDI-files
*
* Copyright (c) 2005-2006 Tobias Doerffel <tobydox/at/users.sourceforge.net>
*

Binary file not shown.