diff --git a/CMakeLists.txt b/CMakeLists.txt index 95d5861..5f003da 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -38,7 +38,8 @@ if (MINGW) target_link_libraries(sf3convert ${QT_LIBRARIES} vorbis - ##vorbisfile + vorbisenc + vorbisfile ogg sndfile-1 ) @@ -52,6 +53,8 @@ if (MINGW) ${CROSS}/lib/libsndfile-1.dll ${CROSS}/lib/libogg.dll ${CROSS}/lib/libvorbis.dll + ${CROSS}/lib/libvorbisenc.dll + ${CROSS}/lib/libvorbisfile.dll ${CROSSQT}/bin/Qt5Core.dll ${CROSSQT}/bin/Qt5Xml.dll ${CROSSQT}/bin/icuin51.dll @@ -82,6 +85,28 @@ else (MINGW) message("libvorbis not found\n") endif (VORBIS_INCDIR) + ## + ## libvorbisenc + ## + + PKGCONFIG1 (vorbisenc 1.3.3 VORBISENC_INCDIR VORBISENC_LIBDIR VORBISENC_LIB VORBISENC_CPP) + if (VORBISENC_INCDIR) + message("libvorbisenc detected ${VORBISENC_INCDIR} ${VORBISENC_LIBDIR} ${VORBISENC_LIB}") + else (VORBISENC_INCDIR) + message("libvorbisenc not found\n") + endif (VORBISENC_INCDIR) + + ## + ## libvorbisfile + ## + + PKGCONFIG1 (vorbisfile 1.3.3 VORBISFILE_INCDIR VORBISFILE_LIBDIR VORBISFILE_LIB VORBISFILE_CPP) + if (VORBISFILE_INCDIR) + message("libvorbisfile detected ${VORBISFILE_INCDIR} ${VORBISFILE_LIBDIR} ${VORBISFILE_LIB}") + else (VORBISFILE_INCDIR) + message("libvorbisfile not found\n") + endif (VORBISFILE_INCDIR) + ## ## libogg @@ -103,13 +128,16 @@ else (MINGW) ${SNDFILE_INCDIR} ${OGG_INCDIR} ${VORBIS_INCDIR} + ${VORBISENC_INCDIR} + ${VORBISFILE_INCDIR} ) target_link_libraries(sf3convert ${QT_LIBRARIES} ${OGG_LIB} ${VORBIS_LIB} - vorbisenc + ${VORBISENC_LIB} + ${VORBISFILE_LIB} ${SNDFILE_LIB} ) diff --git a/README.md b/README.md index 531ddbc..bd2f8f9 100644 --- a/README.md +++ b/README.md @@ -1,9 +1,19 @@ -### sf3convert +# sf3convert -Utilities for SoundFont files. +Utilities for converting Soundfont files from version 2 to version 3 and back from version 3 to version 2. -* compress sound font files with ogg vorbis for use with [MuseScore](http://musescore.org) -* convert to "C" for embedding +### Sf3 format + +The sf3 format stores compressed data (OGG vorbis files) instead of raw data (WAVE) in a soundfont. There is thus a **loss in quality** but the resulting files are lighter and more prone to be **embedded**. This format has been primarily designed and used by [MuseScore](http://musescore.org) and is now supported by other projects: + +* [Calf-Fluidsynth](http://calf-studio-gear.org/) +* [Carla](http://kxstudio.linuxaudio.org/Applications:Carla) +* [LMMS](https://lmms.io) +* [Polyphone](https://www.polyphone-soundfonts.com) +* [Qsynth](https://qsynth.sourceforge.io) +* [Qtractor](https://qtractor.sourceforge.io) + +**The compressed soundfont has the major version number 3. It is non standard and no specifications have been written yet** ### Compilation @@ -14,6 +24,8 @@ Utilities for SoundFont files. * libsdnfile * libogg * libvorbis +* libvorbisenc +* libvorbisfile ``` $ make release @@ -21,14 +33,10 @@ $ make release ### Usage Example: -This compresses the Fluid sound font from 148 MBytes to 20 MBytes. - - sf3convert -z FluidR3.SF2 mops.sf3 +This compresses the Fluid soundfont from 148 MBytes to 20 MBytes. -**The compressed sound font has the major version number 3. Its non standard -and can be used only (so far) by [MuseScore](http://musescore.org).** + sf3convert -z FluidR3.SF2 compressed_soundfont.sf3 +This uncompresses a soundfont -### TODO: -Stereo samples are compressed as two single streams instead of compressing -them as stereo ogg vorbis streams. This may be less optimal. + sf3convert -y compressed_soundfont.sf3 FluidR3.sf2 \ No newline at end of file diff --git a/sf3convert.1 b/sf3convert.1 index fcd13fe..4e62fda 100644 --- a/sf3convert.1 +++ b/sf3convert.1 @@ -9,7 +9,7 @@ .Nd SoundFont conversion utility .Sh SYNOPSIS .Nm -.Op Fl cdsxz +.Op Fl cdsxyz .Op Fl a Ar ampl .Op Fl p Ar pres .Op Fl S Ar number @@ -20,7 +20,8 @@ The .Nm utility converts an SF2 format SoundFont; it can compress it -into SF3, encode as C for embedding into a binary, or as XML. +into SF3, uncompress from SF3, encode as C for embedding into a binary, +or as XML. .Pp The options are as follows: .Bl -tag -width xxx @@ -49,13 +50,16 @@ as the OGG stream serial number instead of a time-based random one. Create a small soundfont (one instrument/preset), pan to 0. .It Fl x Output XML. +.It Fl y +Uncompress the soundfont. .It Fl z Compress the soundfont. .El .Pp The .Fl c , -.Fl d +.Fl d , +.Fl y and .Fl z options are mutually exclusive. diff --git a/sfconvert.cpp b/sfconvert.cpp index 99f88ce..ef66181 100644 --- a/sfconvert.cpp +++ b/sfconvert.cpp @@ -26,8 +26,6 @@ #include #include "sfont.h" -bool smallSf = false; - //--------------------------------------------------------- // usage //--------------------------------------------------------- @@ -38,6 +36,7 @@ static void usage(const char* pname) fprintf(stderr, " -z compress sf\n"); fprintf(stderr, " -q qq ogg quality\n"); fprintf(stderr, " -a nn amplification in dB before ogg compression\n"); + fprintf(stderr, " -y uncompress sf\n"); fprintf(stderr, " -x xml output\n"); fprintf(stderr, " -c c output\n"); fprintf(stderr, " -p nn preset\n"); @@ -56,6 +55,8 @@ int main(int argc, char* argv[]) bool code = false; bool dump = false; bool compress = false; + bool uncompress = false; + bool smallSf = false; double oggQuality = 0.3; double oggAmp = -1.0; qint64 oggSerial = std::numeric_limits::max(); @@ -67,7 +68,7 @@ int main(int argc, char* argv[]) fprintf(stderr, "%s: convert sound file\n", argv[0]); int c; - while ((c = getopt(argc, argv, "xcp:dS:szq:a:")) != EOF) { + while ((c = getopt(argc, argv, "xcp:dS:syzq:a:")) != EOF) { switch(c) { case 'x': xml = true; @@ -87,6 +88,9 @@ int main(int argc, char* argv[]) case 's': smallSf = true; break; + case 'y': + uncompress = true; + break; case 'z': compress = true; break; @@ -113,12 +117,18 @@ int main(int argc, char* argv[]) usage(pname); exit(3); } - if (!xml && !code && !dump && !compress) { + if (!xml && !code && !dump && !compress && !uncompress) { usage(pname); exit(4); } + if (compress && uncompress) { + usage(pname) + exit(5) + } - SoundFont sf(argv[0]); + SfTools::SoundFont sf(argv[0]); + if (smallSf) + sf.smallSf = true; if (!sf.read()) { fprintf(stderr, "sf read error\n"); @@ -142,10 +152,21 @@ int main(int argc, char* argv[]) if (xml) sf.writeXml(&fo); else - sf.write(&fo, oggQuality, oggAmp, oggSerial); + sf.compress(&fo, oggQuality, oggAmp, oggSerial); + fo.close(); + } + else if (uncompress) { + QFile fo(argv[1]); + if (!fo.open(QIODevice::WriteOnly)) { + fprintf(stderr, "cannot open <%s>\n", argv[2]); + exit(2); + } + if (xml) + sf.writeXml(&fo); + else + sf.uncompress(&fo); fo.close(); } qDebug("Soundfont converted in: %d ms", t.elapsed()); return 0; } - diff --git a/sfont.cpp b/sfont.cpp index 5061101..65f2a40 100644 --- a/sfont.cpp +++ b/sfont.cpp @@ -23,16 +23,21 @@ #include #include #include -#include #include +#include +#include #include "sfont.h" -#include "xml.h" #include "time.h" +#ifndef SFTOOLS_NOXML +#include "xml.h" +#include +#endif + #include -extern bool smallSf; // create small sf +using namespace SfTools; // #define DEBUG @@ -61,7 +66,16 @@ static const bool writeCompressed = true; Sample::Sample() { - name = 0; + name = nullptr; + start = 0; + end = 0; + loopstart = 0; + loopend = 0; + samplerate = 0; + origpitch = 0; + pitchadj = 0; + sampleLink = 0; + sampletype = 0; } Sample::~Sample() @@ -75,7 +89,7 @@ Sample::~Sample() Instrument::Instrument() { - name = 0; + name = nullptr; } Instrument::~Instrument() @@ -90,14 +104,20 @@ Instrument::~Instrument() SoundFont::SoundFont(const QString& s) { path = s; - engine = 0; - name = 0; - date = 0; - comment = 0; - tools = 0; - creator = 0; - product = 0; - copyright = 0; + engine = nullptr; + name = nullptr; + date = nullptr; + comment = nullptr; + tools = nullptr; + creator = nullptr; + product = nullptr; + copyright = nullptr; + irom = nullptr; + version.major = 0; + version.minor = 0; + iver.major = 0; + iver.minor = 0; + _smallSf = false; } SoundFont::~SoundFont() @@ -110,6 +130,7 @@ SoundFont::~SoundFont() free(creator); free(product); free(copyright); + free(irom); } //--------------------------------------------------------- @@ -323,13 +344,13 @@ int SoundFont::readChar() // readVersion //--------------------------------------------------------- -void SoundFont::readVersion() +void SoundFont::readVersion(sfVersionTag * v) { unsigned char data[4]; if (file->read((char*)data, 4) != 4) throw(QString("unexpected end of file\n")); - version.major = data[0] + (data[1] << 8); - version.minor = data[2] + (data[3] << 8); + v->major = data[0] + (data[1] << 8); + v->minor = data[2] + (data[3] << 8); } //--------------------------------------------------------- @@ -356,7 +377,7 @@ void SoundFont::readSection(const char* fourcc, int len) switch(FOURCC(fourcc[0], fourcc[1], fourcc[2], fourcc[3])) { case FOURCC('i', 'f', 'i', 'l'): // version - readVersion(); + readVersion(&version); break; case FOURCC('I','N','A','M'): // sound font name name = readString(len); @@ -387,6 +408,9 @@ void SoundFont::readSection(const char* fourcc, int len) sampleLen = len; skip(len); break; + case FOURCC('s','m','2','4'): // audio samples (24-bit part) + skip(len); // Just skip it + break; case FOURCC('p','h','d','r'): // preset headers readPhdr(len); break; @@ -415,11 +439,14 @@ void SoundFont::readSection(const char* fourcc, int len) readShdr(len); break; case FOURCC('i', 'r', 'o', 'm'): // sample rom + irom = readString(len); + break; case FOURCC('i', 'v', 'e', 'r'): // sample rom version + readVersion(&iver); + break; default: skip(len); throw(QString("unknown fourcc <%1>").arg(fourcc)); - break; } } @@ -596,145 +623,21 @@ void SoundFont::readShdr(int size) s->samplerate = readDword(); s->origpitch = readByte(); s->pitchadj = readChar(); - readWord(); // sampleLink + s->sampleLink = readWord(); s->sampletype = readWord(); - s->loopstart -= s->start; - s->loopend -= s->start; // printf("readFontHeader %d %d %d %d\n", s->start, s->end, s->loopstart, s->loopend); samples.append(s); } skip(46); // trailing record } -//--------------------------------------------------------- -// writeXml -//--------------------------------------------------------- - -bool SoundFont::writeXml(QFile* f) - { - Xml xml(f); - - xml.header(); - xml.stag("Sfont"); - xml.tag("version", QString("%1.%2").arg(version.major).arg(version.minor)); - if (name) - xml.tag("name", Xml::xmlString(name)); - if (engine) - xml.tag("engine", Xml::xmlString(engine)); - if (date) - xml.tag("date", Xml::xmlString(date)); - if (comment) - xml.tag("comment", Xml::xmlString(comment)); - if (tools) - xml.tag("tools", Xml::xmlString(tools)); - if (creator) - xml.tag("creator", Xml::xmlString(creator)); - if (product) - xml.tag("product", Xml::xmlString(product)); - if (copyright) - xml.tag("copyright", Xml::xmlString(copyright)); - - foreach(Preset* p, presets) { - xml.stag(QString("Preset name=\"%1\" preset=\"%2\" bank=\"%3\"") - .arg(p->name).arg(p->preset).arg(p->bank)); - foreach(Zone* z, p->zones) - write(xml, z); - xml.etag(); - } - foreach(Instrument* instrument, instruments) { - xml.stag(QString("Instrument name=\"%1\"").arg(instrument->name)); - foreach(Zone* z, instrument->zones) - write(xml, z); - xml.etag(); - } - int idx = 0; - foreach(Sample* s, samples) { - xml.stag(QString("Sample name=\"%1\"").arg(s->name)); - xml.tag("start", s->start); - xml.tag("end", s->end); - xml.tag("loopstart", s->loopstart); - xml.tag("loopend", s->loopend); - xml.tag("samplerate", s->samplerate); - xml.tag("origpitch", s->origpitch); - if (s->pitchadj) - xml.tag("pitchadj", s->pitchadj); - xml.tag("sampletype", s->sampletype); - xml.etag(); - writeSampleFile(s, QString("%1").arg(idx)); - ++idx; - } - xml.etag(); - return true; - } - -static const char* generatorNames[] = { - "StartAddrOfs", "EndAddrOfs", "StartLoopAddrOfs", - "EndLoopAddrOfs", "StartAddrCoarseOfs", "ModLFO2Pitch", - "VibLFO2Pitch", "ModEnv2Pitch", "FilterFc", "FilterQ", - "ModLFO2FilterFc", "ModEnv2FilterFc", "EndAddrCoarseOfs", - "ModLFO2Vol", "Unused1", "ChorusSend", "ReverbSend", "Pan", - "Unused2", "Unused3", "Unused4", - "ModLFODelay", "ModLFOFreq", "VibLFODelay", "VibLFOFreq", - "ModEnvDelay", "ModEnvAttack", "ModEnvHold", "ModEnvDecay", - "ModEnvSustain", "ModEnvRelease", "Key2ModEnvHold", - "Key2ModEnvDecay", "VolEnvDelay", "VolEnvAttack", - "VolEnvHold", "VolEnvDecay", "VolEnvSustain", "VolEnvRelease", - "Key2VolEnvHold", "Key2VolEnvDecay", "Instrument", - "Reserved1", "KeyRange", "VelRange", - "StartLoopAddrCoarseOfs", "Keynum", "Velocity", - "Attenuation", "Reserved2", "EndLoopAddrCoarseOfs", - "CoarseTune", "FineTune", "SampleId", "SampleModes", - "Reserved3", "ScaleTune", "ExclusiveClass", "OverrideRootKey", - "Dummy" - }; - -//--------------------------------------------------------- -// write -//--------------------------------------------------------- - -void SoundFont::write(Xml& xml, Zone* z) - { - xml.stag("Zone"); - foreach(GeneratorList* g, z->generators) { - const char* name = generatorNames[g->gen]; - if (g->gen == Gen_KeyRange || g->gen == Gen_VelRange) - xml.tagE(QString("Generator name=\"%1\" lo=\"%2\" hi=\"%3\"") - .arg(name).arg(g->amount.lo).arg(g->amount.hi)); - else if (g->gen == Gen_Instrument) { - int idx = g->amount.uword; - xml.tag("Instrument", instruments[idx]->name); - } - else if (g->gen == Gen_SampleId) { - int idx = g->amount.uword; - xml.tag("SampleId", samples[idx]->name); - } - else - xml.tagE(QString("Generator name=\"%1\" val=\"%2\"") - .arg(name).arg(g->amount.sword)); - } - foreach(ModulatorList* m, z->modulators) { - xml.stag("Modulator"); - xml.tag("src", m->src); - xml.tag("dst", m->dst); - xml.tag("amount", m->amount); - xml.tag("amtSrc", m->amtSrc); - xml.tag("transform", m->transform); - xml.etag(); - } - xml.etag(); - } - //--------------------------------------------------------- // write //--------------------------------------------------------- -bool SoundFont::write(QFile* f, double oggQuality, double oggAmp, qint64 oggSerial) +bool SoundFont::write() { - file = f; - _oggQuality = oggQuality; - _oggAmp = oggAmp; - _oggSerial = oggSerial; qint64 riffLenPos; qint64 listLenPos; try { @@ -765,6 +668,9 @@ bool SoundFont::write(QFile* f, double oggQuality, double oggAmp, qint64 oggSeri writeStringSection("ICMT", comment); if (copyright) writeStringSection("ICOP", copyright); + if (irom) + writeStringSection("irom", irom); + writeIver(); qint64 pos = file->pos(); file->seek(listLenPos); @@ -812,22 +718,22 @@ bool SoundFont::write(QFile* f, double oggQuality, double oggAmp, qint64 oggSeri return true; } -//--------------------------------------------------------- -// readXml -//--------------------------------------------------------- +bool SoundFont::compress(QFile* f, double oggQuality, double oggAmp, qint64 oggSerial) + { + file = f; + _compress = true; + _oggQuality = oggQuality; + _oggAmp = oggAmp; + _oggSerial = oggSerial; -bool SoundFont::readXml(QFile* f) + return write(); + } + +bool SoundFont::uncompress(QFile* f) { - QDomDocument doc; - int line, column; - QString err; - if (!doc.setContent(f, false, &err, &line, &column)) { - QString s; - printf("error reading file %s at line %d column %d: %s\n", - qPrintable(f->fileName()), line, column, qPrintable(err)); - return false; - } - return true; + file = f; + _compress = false; + return write(); } //--------------------------------------------------------- @@ -866,8 +772,13 @@ void SoundFont::writeIfil() write("ifil", 4); writeDword(4); unsigned char data[4]; - if (writeCompressed) + if (_compress) { version.major = 3; + version.minor = 0; + } else { + version.major = 2; + version.minor = 1; + } data[0] = version.major; data[1] = version.major >> 8; data[2] = version.minor; @@ -875,6 +786,22 @@ void SoundFont::writeIfil() write((char*)data, 4); } +//--------------------------------------------------------- +// writeIVer +//--------------------------------------------------------- + +void SoundFont::writeIver() + { + write("iver", 4); + writeDword(4); + unsigned char data[4]; + data[0] = iver.major; + data[1] = iver.major >> 8; + data[2] = iver.minor; + data[3] = iver.minor >> 8; + write((char*)data, 4); + } + //--------------------------------------------------------- // writeSmpl //--------------------------------------------------------- @@ -885,35 +812,36 @@ void SoundFont::writeSmpl() qint64 pos = file->pos(); writeDword(0); - int sampleLen = 0; - if (writeCompressed) { + int currentSamplePos = 0; + if (_compress) { + // Compress wave data foreach(Sample* s, samples) { + // Loop start and end are now based on the beginning of each sample + s->loopstart -= s->start; + s->loopend -= s->start; + + // OGG flag added s->sampletype |= 0x10; - int len = writeCompressedSample(s); - s->start = sampleLen; - sampleLen += len; - s->end = sampleLen; + int len = writeCompressedSample(s); // In byte + s->start = currentSamplePos; + currentSamplePos += len; + s->end = currentSamplePos; } } else { - char* buffer = new char[sampleLen]; - QFile f(path); - if (!f.open(QIODevice::ReadOnly)) - throw(QString("cannot open <%1>").arg(f.fileName())); + // Uncompress from OGG data foreach(Sample* s, samples) { - f.seek(samplePos + s->start * sizeof(short)); - - int len = (s->end - s->start) * sizeof(short); - f.read(buffer, len); - write(buffer, len); - s->start = sampleLen / sizeof(short); - sampleLen += len; - s->end = sampleLen / sizeof(short); + // OGG flag is removed + s->sampletype &= ~0x10; + int len = writeUncompressedSample(s) / 2; // In sample data points (16 bits) + s->start = currentSamplePos; + currentSamplePos += len; + s->end = currentSamplePos; + + // Loop start and end are now based on the beginning of the section "smpl" s->loopstart += s->start; s->loopend += s->start; } - f.close(); - delete[] buffer; } qint64 npos = file->pos(); file->seek(pos); @@ -936,7 +864,6 @@ void SoundFont::writePhdr() zoneIdx += p->zones.size(); } Preset p; - memset(&p, sizeof(p), 0); writePreset(zoneIdx, &p); } @@ -1067,7 +994,6 @@ void SoundFont::writeInst() zoneIdx += p->zones.size(); } Instrument p; - memset(&p, sizeof(p), 0); writeInstrument(zoneIdx, &p); } @@ -1096,7 +1022,6 @@ void SoundFont::writeShdr() foreach(const Sample* s, samples) writeSample(s); Sample s; - memset(&s, 0, sizeof(s)); writeSample(&s); } @@ -1118,54 +1043,10 @@ void SoundFont::writeSample(const Sample* s) writeDword(s->samplerate); writeByte(s->origpitch); writeChar(s->pitchadj); - writeWord(0); + writeWord(s->sampleLink); writeWord(s->sampletype); } -//--------------------------------------------------------- -// writeSampleFile -//--------------------------------------------------------- - -bool SoundFont::writeSampleFile(Sample* s, QString name) - { - QString path = "waves/" + name + ".ogg"; - - QFile f(path); - if (!f.open(QIODevice::ReadOnly)) { - fprintf(stderr, "cannot open <%s>\n", qPrintable(f.fileName())); - return false; - } - f.seek(samplePos + s->start * sizeof(short)); - int len = s->end - s->start; - short buffer[len]; - f.read((char*)buffer, len * sizeof(short)); - f.close(); - - // int format = SF_FORMAT_WAV | SF_FORMAT_PCM_16; - int format = SF_FORMAT_OGG | SF_FORMAT_VORBIS; - - SF_INFO info; - memset(&info, 0, sizeof(info)); - info.channels = 1; - info.samplerate = s->samplerate; - info.format = format; - - SNDFILE* sf = sf_open(qPrintable(path), SFM_WRITE, &info); - if (sf == 0) { - fprintf(stderr, "open soundfile <%s> failed: %s\n", - qPrintable(path), sf_strerror(sf)); - return false; - } - - sf_write_short(sf, buffer, len); - - if (sf_close(sf)) { - fprintf(stderr, "close soundfile failed\n"); - return false; - } - return true; - } - //--------------------------------------------------------- // writeCompressedSample //--------------------------------------------------------- @@ -1179,7 +1060,7 @@ int SoundFont::writeCompressedSample(Sample* s) } f.seek(samplePos + s->start * sizeof(short)); int samples = s->end - s->start; - short ibuffer[samples]; + short * ibuffer = new short[samples]; f.read((char*)ibuffer, samples * sizeof(short)); f.close(); @@ -1195,24 +1076,28 @@ int SoundFont::writeCompressedSample(Sample* s) int ret = vorbis_encode_init_vbr(&vi, 1, s->samplerate, _oggQuality); if (ret) { printf("vorbis init failed\n"); + delete [] ibuffer; return false; } vorbis_comment_init(&vc); vorbis_analysis_init(&vd, &vi); vorbis_block_init(&vd, &vb); - srand(time(NULL)); + srand(time(nullptr)); ogg_stream_init(&os, _oggSerial == std::numeric_limits::max() ? rand() : (int)_oggSerial); ogg_packet header; ogg_packet header_comm; ogg_packet header_code; + // Keep a track of the attenuation used before the compression + vorbis_comment_add(&vc, QString("AMP=%1\0").arg(_oggAmp).toStdString().c_str()); + vorbis_analysis_headerout(&vd, &vc, &header, &header_comm, &header_code); ogg_stream_packetin(&os, &header); ogg_stream_packetin(&os, &header_comm); ogg_stream_packetin(&os, &header_code); - char obuf[1024 * 1024]; + char* obuf = new char[1048576]; // 1024 * 1024 char* p = obuf; for (;;) { @@ -1293,18 +1178,11 @@ int SoundFont::writeCompressedSample(Sample* s) int n = p - obuf; write(obuf, n); + delete [] obuf; + delete [] ibuffer; return n; } -//--------------------------------------------------------- -// readCompressedSample -//--------------------------------------------------------- - -char* SoundFont::readCompressedSample(Sample* s) - { - return 0; - } - //--------------------------------------------------------- // writeCSample //--------------------------------------------------------- @@ -1318,7 +1196,7 @@ bool SoundFont::writeCSample(Sample* s, int idx) } fi.seek(samplePos + s->start * sizeof(short)); int samples = s->end - s->start; - short ibuffer[samples]; + short * ibuffer = new short[samples]; fi.read((char*)ibuffer, samples * sizeof(short)); fi.close(); @@ -1338,6 +1216,7 @@ bool SoundFont::writeCSample(Sample* s, int idx) fprintf(f, ",\n "); } fprintf(f, "\n };\n"); + delete [] ibuffer; return true; } @@ -1362,7 +1241,6 @@ static bool checkInstrument(QList pnums, QList presets, int instrI } static bool checkInstrument(QList presets, int instrIdx) { - bool result = false; for(int i = 0; i < presets.size(); i++) { Preset* p = presets[i]; Zone* z = p->zones[0]; @@ -1389,7 +1267,6 @@ static bool checkSample(QList pnums, QList presets, QListzones.size(); foreach(Zone* z, instrument->zones) { QList gl; foreach(GeneratorList* g, z->generators) { @@ -1417,7 +1294,6 @@ static bool checkSample(QList presets, QList instruments, ++idx; continue; } - int zones = instrument->zones.size(); foreach(Zone* z, instrument->zones) { QList gl; foreach(GeneratorList* g, z->generators) { @@ -1459,7 +1335,7 @@ bool SoundFont::writeCode() end = (i+1) * n; QList sampleIdx; for (int idx = i * n; idx < end; ++idx) { - if (smallSf && !checkSample(presets, instruments, idx)) + if (_smallSf && !checkSample(presets, instruments, idx)) continue; Sample* s = samples[idx]; writeCSample(s, idx); @@ -1507,7 +1383,7 @@ bool SoundFont::writeCode() int idx2; int idx = 0; foreach(Instrument* instrument, instruments) { - if (smallSf && !checkInstrument(presets, idx)) { + if (_smallSf && !checkInstrument(presets, idx)) { ++idx; continue; } @@ -1522,7 +1398,6 @@ bool SoundFont::writeCode() QList gl; foreach(GeneratorList* g, z->generators) { - const char* name = generatorNames[g->gen]; if (g->gen == Gen_KeyRange) { keyLo = g->amount.lo; keyHi = g->amount.hi; @@ -1535,7 +1410,7 @@ bool SoundFont::writeCode() sampleIdx = g->amount.uword; else gl.append(g); - if (smallSf && g->gen == Gen_Pan) + if (_smallSf && g->gen == Gen_Pan) g->amount.uword = 0; } int idx3 = 0; @@ -1594,7 +1469,7 @@ bool SoundFont::writeCode() foreach(Preset* p, presets) { idx2 = 0; int zones = p->zones.size(); - if (smallSf) + if (_smallSf) zones = 1; foreach(Zone* z, p->zones) { @@ -1605,7 +1480,6 @@ bool SoundFont::writeCode() int instrIdx = -1; foreach(GeneratorList* g, z->generators) { - const char* name = generatorNames[g->gen]; if (g->gen == Gen_KeyRange) { keyLo = g->amount.lo; keyHi = g->amount.hi; @@ -1634,7 +1508,7 @@ bool SoundFont::writeCode() else fprintf(f, "static PZone pz%d_%d(%d, %d, %d, %d, &instr%d);\n", idx, idx2, keyLo, keyHi, veloLo, veloHi, instrIdx); } - if (smallSf) + if (_smallSf) break; ++idx2; } @@ -1730,7 +1604,6 @@ bool SoundFont::writeCode(QList pnums) QList gl; foreach(GeneratorList* g, z->generators) { - const char* name = generatorNames[g->gen]; if (g->gen == Gen_KeyRange) { keyLo = g->amount.lo; keyHi = g->amount.hi; @@ -1802,7 +1675,6 @@ bool SoundFont::writeCode(QList pnums) int veloHi = 127; int instrIdx = -1; foreach(GeneratorList* g, z->generators) { - const char* name = generatorNames[g->gen]; if (g->gen == Gen_KeyRange) { keyLo = g->amount.lo; keyHi = g->amount.hi; @@ -1870,3 +1742,313 @@ void SoundFont::dumpPresets() } +#ifndef SFTOOLS_NOXML +static const char* generatorNames[] = { + "StartAddrOfs", "EndAddrOfs", "StartLoopAddrOfs", + "EndLoopAddrOfs", "StartAddrCoarseOfs", "ModLFO2Pitch", + "VibLFO2Pitch", "ModEnv2Pitch", "FilterFc", "FilterQ", + "ModLFO2FilterFc", "ModEnv2FilterFc", "EndAddrCoarseOfs", + "ModLFO2Vol", "Unused1", "ChorusSend", "ReverbSend", "Pan", + "Unused2", "Unused3", "Unused4", + "ModLFODelay", "ModLFOFreq", "VibLFODelay", "VibLFOFreq", + "ModEnvDelay", "ModEnvAttack", "ModEnvHold", "ModEnvDecay", + "ModEnvSustain", "ModEnvRelease", "Key2ModEnvHold", + "Key2ModEnvDecay", "VolEnvDelay", "VolEnvAttack", + "VolEnvHold", "VolEnvDecay", "VolEnvSustain", "VolEnvRelease", + "Key2VolEnvHold", "Key2VolEnvDecay", "Instrument", + "Reserved1", "KeyRange", "VelRange", + "StartLoopAddrCoarseOfs", "Keynum", "Velocity", + "Attenuation", "Reserved2", "EndLoopAddrCoarseOfs", + "CoarseTune", "FineTune", "SampleId", "SampleModes", + "Reserved3", "ScaleTune", "ExclusiveClass", "OverrideRootKey", + "Dummy" + }; + +//--------------------------------------------------------- +// writeXml +//--------------------------------------------------------- + +bool SoundFont::writeXml(QFile* f) + { + Xml xml(f); + + xml.header(); + xml.stag("Sfont"); + xml.tag("version", QString("%1.%2").arg(version.major).arg(version.minor)); + if (name) + xml.tag("name", Xml::xmlString(name)); + if (engine) + xml.tag("engine", Xml::xmlString(engine)); + if (date) + xml.tag("date", Xml::xmlString(date)); + if (comment) + xml.tag("comment", Xml::xmlString(comment)); + if (tools) + xml.tag("tools", Xml::xmlString(tools)); + if (creator) + xml.tag("creator", Xml::xmlString(creator)); + if (product) + xml.tag("product", Xml::xmlString(product)); + if (copyright) + xml.tag("copyright", Xml::xmlString(copyright)); + + foreach(Preset* p, presets) { + xml.stag(QString("Preset name=\"%1\" preset=\"%2\" bank=\"%3\"") + .arg(p->name).arg(p->preset).arg(p->bank)); + foreach(Zone* z, p->zones) + write(xml, z); + xml.etag(); + } + foreach(Instrument* instrument, instruments) { + xml.stag(QString("Instrument name=\"%1\"").arg(instrument->name)); + foreach(Zone* z, instrument->zones) + write(xml, z); + xml.etag(); + } + int idx = 0; + foreach(Sample* s, samples) { + xml.stag(QString("Sample name=\"%1\"").arg(s->name)); + xml.tag("start", s->start); + xml.tag("end", s->end); + xml.tag("loopstart", s->loopstart); + xml.tag("loopend", s->loopend); + xml.tag("samplerate", s->samplerate); + xml.tag("origpitch", s->origpitch); + if (s->pitchadj) + xml.tag("pitchadj", s->pitchadj); + xml.tag("sampletype", s->sampletype); + xml.etag(); + writeSampleFile(s, QString("%1").arg(idx)); + ++idx; + } + xml.etag(); + return true; + } + +//--------------------------------------------------------- +// write +//--------------------------------------------------------- + +void SoundFont::write(Xml& xml, Zone* z) + { + xml.stag("Zone"); + foreach(GeneratorList* g, z->generators) { + const char* name = generatorNames[g->gen]; + if (g->gen == Gen_KeyRange || g->gen == Gen_VelRange) + xml.tagE(QString("Generator name=\"%1\" lo=\"%2\" hi=\"%3\"") + .arg(name).arg(g->amount.lo).arg(g->amount.hi)); + else if (g->gen == Gen_Instrument) { + int idx = g->amount.uword; + xml.tag("Instrument", instruments[idx]->name); + } + else if (g->gen == Gen_SampleId) { + int idx = g->amount.uword; + xml.tag("SampleId", samples[idx]->name); + } + else + xml.tagE(QString("Generator name=\"%1\" val=\"%2\"") + .arg(name).arg(g->amount.sword)); + } + foreach(ModulatorList* m, z->modulators) { + xml.stag("Modulator"); + xml.tag("src", m->src); + xml.tag("dst", m->dst); + xml.tag("amount", m->amount); + xml.tag("amtSrc", m->amtSrc); + xml.tag("transform", m->transform); + xml.etag(); + } + xml.etag(); + } + +//--------------------------------------------------------- +// writeSampleFile +//--------------------------------------------------------- + +bool SoundFont::writeSampleFile(Sample* s, QString name) + { + QString path = "waves/" + name + ".ogg"; + + QFile f(path); + if (!f.open(QIODevice::ReadOnly)) { + fprintf(stderr, "cannot open <%s>\n", qPrintable(f.fileName())); + return false; + } + f.seek(samplePos + s->start * sizeof(short)); + int len = s->end - s->start; + short * buffer = new short[len]; + f.read((char*)buffer, len * sizeof(short)); + f.close(); + + // int format = SF_FORMAT_WAV | SF_FORMAT_PCM_16; + int format = SF_FORMAT_OGG | SF_FORMAT_VORBIS; + + SF_INFO info; + memset(&info, 0, sizeof(info)); + info.channels = 1; + info.samplerate = s->samplerate; + info.format = format; + + SNDFILE* sf = sf_open(qPrintable(path), SFM_WRITE, &info); + if (sf == 0) { + fprintf(stderr, "open soundfile <%s> failed: %s\n", + qPrintable(path), sf_strerror(sf)); + delete [] buffer; + return false; + } + + sf_write_short(sf, buffer, len); + delete [] buffer; + + if (sf_close(sf)) { + fprintf(stderr, "close soundfile failed\n"); + return false; + } + return true; + } + +//--------------------------------------------------------- +// readXml +//--------------------------------------------------------- + +bool SoundFont::readXml(QFile* f) + { + QDomDocument doc; + int line, column; + QString err; + if (!doc.setContent(f, false, &err, &line, &column)) { + QString s; + printf("error reading file %s at line %d column %d: %s\n", + qPrintable(f->fileName()), line, column, qPrintable(err)); + return false; + } + return true; + } +#endif + + +struct VorbisData { + int pos; + QByteArray data; +}; + +static VorbisData vorbisData; +static size_t ovRead(void* ptr, size_t size, size_t nmemb, void* datasource); +static int ovSeek(void* datasource, ogg_int64_t offset, int whence); +static long ovTell(void* datasource); +static ov_callbacks ovCallbacks = { ovRead, ovSeek, nullptr, ovTell }; + +//--------------------------------------------------------- +// ovRead +//--------------------------------------------------------- + +static size_t ovRead(void* ptr, size_t size, size_t nmemb, void* datasource) +{ + VorbisData* vd = (VorbisData*)datasource; + size_t n = size * nmemb; + if (vd->data.size() < int(vd->pos + n)) + n = vd->data.size() - vd->pos; + if (n) { + const char* src = vd->data.data() + vd->pos; + memcpy(ptr, src, n); + vd->pos += n; + } + + return n; +} + +//--------------------------------------------------------- +// ovSeek +//--------------------------------------------------------- + +static int ovSeek(void* datasource, ogg_int64_t offset, int whence) +{ + VorbisData* vd = (VorbisData*)datasource; + switch(whence) { + case SEEK_SET: + vd->pos = offset; + break; + case SEEK_CUR: + vd->pos += offset; + break; + case SEEK_END: + vd->pos = vd->data.size() - offset; + break; + } + return 0; +} + +//--------------------------------------------------------- +// ovTell +//--------------------------------------------------------- + +static long ovTell(void* datasource) +{ + VorbisData* vd = (VorbisData*)datasource; + return vd->pos; +} + +//--------------------------------------------------------- +// writeUncompressedSample +//--------------------------------------------------------- + +int SoundFont::writeUncompressedSample(Sample* s) +{ + // Prepare input data + QFile f(path); + if (!f.open(QIODevice::ReadOnly)) { + fprintf(stderr, "cannot open <%s>\n", qPrintable(f.fileName())); + return 0; + } + f.seek(samplePos + s->start); + int oggSize = s->end - s->start; + short * ibuffer = new short[oggSize]; + f.read((char*)ibuffer, oggSize); + f.close(); + + vorbisData.pos = 0; + vorbisData.data = QByteArray((char *)ibuffer, oggSize); + + // Decode + QByteArray decodedData; + OggVorbis_File vf; + vorbisData.pos = 0; + int length = 0; + if (ov_open_callbacks(&vorbisData, &vf, nullptr, 0, ovCallbacks) == 0) + { + double attenuation = 0; + for (int i = 0; i < vf.vc->comments; i++) + { + QString comment(vf.vc->user_comments[i]); + if (comment.contains("AMP=")) + { + QStringList split = comment.split('='); + if (split.size() == 2) + { + bool ok = false; + attenuation = split[1].toDouble(&ok); + if (!ok) + attenuation = 0; + } + } + } + double linearAmp = pow(10.0, attenuation / 20.0); + + short buffer[2048]; + int numberRead = 0; + int section = 0; + do { + numberRead = ov_read(&vf, (char *)buffer, 2048 * sizeof(short), 0, 2, 1, §ion); + for (unsigned int i = 0; i < numberRead / sizeof(short); i++) + buffer[i] = (double)buffer[i] / linearAmp; + write((char *)buffer, numberRead); + length += numberRead; + } while (numberRead); + + ov_clear(&vf); + } + + delete[] ibuffer; + return length; +} + diff --git a/sfont.h b/sfont.h index ed204ef..13917fe 100644 --- a/sfont.h +++ b/sfont.h @@ -18,8 +18,8 @@ // Foundation, Inc., 675 Mass Ave, Cambridge, MA 02139, USA. //============================================================================= -#ifndef __SOUNDFONT_H__ -#define __SOUNDFONT_H__ +#ifndef SFONT_H +#define SFONT_H #include #include @@ -27,6 +27,8 @@ class Xml; class QFile; +namespace SfTools { + //--------------------------------------------------------- // sfVersionTag //--------------------------------------------------------- @@ -105,14 +107,16 @@ struct Zone { //--------------------------------------------------------- struct Preset { - char* name {0}; - int preset {0}; - int bank {0}; - int presetBagNdx {0}; // used only for read - int library {0}; - int genre {0}; - int morphology {0}; + char* name; + int preset; + int bank; + int presetBagNdx; // used only for read + int library; + int genre; + int morphology; QList zones; + + Preset():name(nullptr), preset(0), bank(0), presetBagNdx(0), library(0), genre(0), morphology(0) {} }; //--------------------------------------------------------- @@ -142,6 +146,7 @@ struct Sample { int origpitch; int pitchadj; + int sampleLink; int sampletype; Sample(); @@ -163,6 +168,8 @@ class SoundFont { char* creator; char* product; char* copyright; + char* irom; + sfVersionTag iver; int samplePos; int sampleLen; @@ -177,10 +184,14 @@ class SoundFont { QFile* file; FILE* f; + bool _compress; double _oggQuality; double _oggAmp; qint64 _oggSerial; + // Extra option + bool _smallSf; + unsigned readDword(); int readWord(); int readShort(); @@ -192,7 +203,7 @@ class SoundFont { void readSignature(char* signature); void skip(int); void readSection(const char* fourcc, int len); - void readVersion(); + void readVersion(sfVersionTag* v); char* readString(int); void readPhdr(int); void readBag(int, QList*); @@ -207,8 +218,6 @@ class SoundFont { void writeChar(char); void writeShort(short); void write(const char* p, int n); - void write(Xml&, Zone*); - bool writeSampleFile(Sample*, QString); void writeSample(const Sample*); void writeStringSection(const char* fourcc, char* s); void writePreset(int zoneIdx, const Preset*); @@ -217,6 +226,7 @@ class SoundFont { void writeInstrument(int zoneIdx, const Instrument*); void writeIfil(); + void writeIver(); void writeSmpl(); void writePhdr(); void writeBag(const char* fourcc, QList*); @@ -226,19 +236,30 @@ class SoundFont { void writeShdr(); int writeCompressedSample(Sample*); + int writeUncompressedSample(Sample* s); bool writeCSample(Sample*, int); - char* readCompressedSample(Sample*); + bool write(); public: SoundFont(const QString&); ~SoundFont(); bool read(); - bool write(QFile*, double oggQuality, double oggAmp, qint64 oggSerial); - bool readXml(QFile*); - bool writeXml(QFile*); + bool compress(QFile* f, double oggQuality, double oggAmp, qint64 oggSerial = rand()); + bool uncompress(QFile* f); bool writeCode(QList); bool writeCode(); void dumpPresets(); - }; + +#ifndef SFTOOLS_NOXML + private: + void write(Xml&, Zone*); + bool writeSampleFile(Sample*, QString); + + public: + bool readXml(QFile*); + bool writeXml(QFile*); +#endif +}; +} #endif