qzdoom/src/sound/i_soundfont.cpp

532 lines
14 KiB
C++
Raw Normal View History

/*
** i_soundfont.cpp
** The sound font manager for the MIDI synths
**
**---------------------------------------------------------------------------
** Copyright 2018 Christoph Oelckers
** All rights reserved.
**
** Redistribution and use in source and binary forms, with or without
** modification, are permitted provided that the following conditions
** are met:
**
** 1. Redistributions of source code must retain the above copyright
** notice, this list of conditions and the following disclaimer.
** 2. Redistributions in binary form must reproduce the above copyright
** notice, this list of conditions and the following disclaimer in the
** documentation and/or other materials provided with the distribution.
** 3. The name of the author may not be used to endorse or promote products
** derived from this software without specific prior written permission.
**
** THIS SOFTWARE IS PROVIDED BY THE AUTHOR ``AS IS'' AND ANY EXPRESS OR
** IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES
** OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED.
** IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY DIRECT, INDIRECT,
** INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT
** NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
** DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
** THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
** (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF
** THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
**---------------------------------------------------------------------------
**
*/
#include <ctype.h>
#include <assert.h>
#include <stdio.h>
2018-02-22 12:42:12 +00:00
#include "i_soundfont.h"
#include "cmdlib.h"
2018-02-22 12:50:41 +00:00
#include "w_wad.h"
#include "i_system.h"
#include "gameconfigfile.h"
#include "resourcefiles/resourcefile.h"
2018-02-22 13:30:43 +00:00
FSoundFontManager sfmanager;
//==========================================================================
//
//
//
//==========================================================================
FSoundFontReader::FSoundFontReader(FWadCollection *coll, const FSoundFontInfo *sfi)
{
collection = coll;
if (sfi->type == SF_SF2)
{
mMainConfigForSF2.Format("soundfont %s", sfi->mFilename.GetChars());
}
if (coll != nullptr)
{
auto num = coll->GetNumWads();
for(int i = 0; i < num; i++)
{
auto wadname = ExtractFileBase(coll->GetWadFullName(i), false);
if (sfi->mName.CompareNoCase(wadname) == 0)
{
// For the given sound font we may only read from this file and no other.
// This is to avoid conflicts with duplicate patches between patch sets.
// For SF2 fonts it's just an added precaution in cause some zipped patch set
// contains extraneous data that gets in the way.
mFindInfile = i;
break;
}
}
}
}
//==========================================================================
//
//
//
//==========================================================================
FileReader *FSoundFontReader::OpenMainConfigFile()
{
if (mMainConfigForSF2.IsNotEmpty())
{
return new MemoryReader(mMainConfigForSF2.GetChars(), (long)mMainConfigForSF2.Len());
}
else
{
auto lump = collection->CheckNumForFullName("timidity.cfg", mFindInfile);
return lump < 0? nullptr : collection->ReopenLumpNum(lump);
}
}
//==========================================================================
//
//
//
//==========================================================================
FileReader *FSoundFontReader::OpenFile(const char *name)
{
auto lump = collection->CheckNumForFullName(name, mFindInfile);
if (lump >= 0)
{
// For SF2 files return a streaming reader to avoid loading the entire file into memory.
if (collection->GetLumpCount(mFindInfile) == 1)
{
return collection->ReopenLumpNumNewFile(lump);
}
return collection->ReopenLumpNum(lump);
}
return nullptr;
}
//==========================================================================
//
// returns both a file reader and the full name of the looked up file
//
//==========================================================================
std::pair<FileReader *, FString> FSoundFontReader::LookupFile(const char *name)
{
if (IsAbsPath(name))
{
auto fr = OpenFile(name);
if (fr != nullptr) return std::make_pair(fr, name);
}
else
{
for(int i = mPaths.Size()-1; i>=0; i--)
{
FString fullname = mPaths[i] + name;
auto fr = OpenFile(fullname);
if (fr != nullptr) return std::make_pair(fr, fullname);
}
}
return std::make_pair(nullptr, FString());
}
//==========================================================================
//
// This adds a directory to the path list
//
//==========================================================================
void FSoundFontReader::AddPath(const char *strp)
{
if (*strp == 0) return;
if (!mAllowAbsolutePaths && IsAbsPath(strp)) return; // of no use so we may just discard it right away
int i = 0;
FString str = strp;
FixPathSeperator(str);
if (str.Back() != '/') str += '/'; // always let it end with a slash.
for (auto &s : mPaths)
{
if (pathcmp(s.GetChars(), str) == 0)
{
// move string to the back.
mPaths.Delete(i);
mPaths.Push(str);
return;
}
i++;
}
mPaths.Push(str);
}
int FSoundFontReader::pathcmp(const char *p1, const char *p2)
{
return mCaseSensitivePaths? strcmp(p1, p2) : stricmp(p1, p2);
}
//==========================================================================
//
// Note that the file type has already been checked
//
//==========================================================================
FSF2Reader::FSF2Reader(const char *fn)
{
mMainConfigForSF2.Format("soundfont %s", fn);
mFilename = fn;
}
FileReader *FSF2Reader::OpenFile(const char *name)
{
if (mFilename.CompareNoCase(name) == 0)
{
auto fr = new FileReader;
if (fr->Open(name)) return fr;
delete fr;
}
return nullptr;
}
//==========================================================================
//
//
//
//==========================================================================
FZipPatReader::FZipPatReader(const char *filename)
{
resf = FResourceFile::OpenResourceFile(filename, nullptr, true);
}
FZipPatReader::~FZipPatReader()
{
if (resf != nullptr) delete resf;
}
FileReader *FZipPatReader::OpenMainConfigFile()
{
return OpenFile("timidity.cfg");
}
FileReader *FZipPatReader::OpenFile(const char *name)
{
if (resf != nullptr)
{
auto lump = resf->FindLump(name);
if (lump != nullptr)
{
return lump->NewReader();
}
}
return nullptr;
}
//==========================================================================
//
//
//
//==========================================================================
FPatchSetReader::FPatchSetReader(const char *filename)
{
#ifndef _WIN32
mCaseSensitivePaths = true;
const char *paths[] = {
"/usr/local/lib/timidity",
"/etc/timidity",
"/etc"
};
#else
const char *paths[] = {
"C:/TIMIDITY",
"/TIMIDITY",
progdir
};
#endif
mAllowAbsolutePaths = true;
FileReader *fr = new FileReader;
if (fr->Open(filename))
{
mFullPathToConfig = filename;
}
else if (!IsAbsPath(filename))
{
for(auto c : paths)
{
FStringf fullname("%s/%s", c, filename);
if (fr->Open(fullname))
{
mFullPathToConfig = fullname;
}
}
}
if (mFullPathToConfig.Len() > 0)
{
FixPathSeperator(mFullPathToConfig);
mBasePath = ExtractFilePath(mFullPathToConfig);
if (mBasePath.Len() > 0 && mBasePath.Back() != '/') mBasePath += '/';
}
}
FileReader *FPatchSetReader::OpenMainConfigFile()
{
auto fr = new FileReader;
if (fr->Open(mFullPathToConfig)) return fr;
delete fr;
return nullptr;
}
FileReader *FPatchSetReader::OpenFile(const char *name)
{
FString path;
if (IsAbsPath(name)) path = name;
else path = mBasePath + name;
auto fr = new FileReader;
if (fr->Open(path)) return fr;
delete fr;
return nullptr;
}
//==========================================================================
//
//
//
//==========================================================================
FLumpPatchSetReader::FLumpPatchSetReader(const char *filename)
{
mLumpIndex = Wads.CheckNumForFullName(filename);
FileReader *fr = new FileReader;
mBasePath = filename;
FixPathSeperator(mBasePath);
mBasePath = ExtractFilePath(mBasePath);
if (mBasePath.Len() > 0 && mBasePath.Back() != '/') mBasePath += '/';
}
FileReader *FLumpPatchSetReader::OpenMainConfigFile()
{
return Wads.ReopenLumpNum(mLumpIndex);
}
FileReader *FLumpPatchSetReader::OpenFile(const char *name)
{
FString path;
if (IsAbsPath(name)) return nullptr; // no absolute paths in the lump directory.
path = mBasePath + name;
auto index = Wads.CheckNumForFullName(path);
if (index < 0) return nullptr;
return Wads.ReopenLumpNum(index);
}
//==========================================================================
//
// collects everything out of the soundfonts directory.
// This may either be .sf2 files or zipped GUS patch sets with a
// 'timidity.cfg' in the root directory.
// Other compression types are not supported, in particular not 7z because
// due to the solid nature of its archives would be too slow.
//
//==========================================================================
void FSoundFontManager::ProcessOneFile(const FString &fn, TArray<FString> &sffiles)
{
auto fb = ExtractFileBase(fn, false);
auto fbe = ExtractFileBase(fn, true);
for (auto &sfi : soundfonts)
{
// We already got a soundfont with this name. Do not add again.
if (!sfi.mName.CompareNoCase(fb)) return;
}
FileReader fr;
if (fr.Open(fn))
{
// Try to identify. We only accept .sf2 and .zip by content. All other archives are intentionally ignored.
char head[16] = { 0};
fr.Read(head, 16);
if (!memcmp(head, "RIFF", 4) && !memcmp(head+8, "sfbkLIST", 8))
{
sffiles.Push(fn);
FSoundFontInfo sft = { fb, fbe, fn, SF_SF2 };
soundfonts.Push(sft);
}
else if (!memcmp(head, "PK", 2))
{
auto zip = FResourceFile::OpenResourceFile(fn, nullptr, true);
2018-02-22 13:30:43 +00:00
if (zip != nullptr)
{
2018-02-22 13:30:43 +00:00
if (zip->LumpCount() > 1) // Anything with just one lump cannot possibly be a packed GUS patch set so skip it right away and simplify the lookup code
{
2018-02-22 13:30:43 +00:00
auto zipl = zip->FindLump("timidity.cfg");
if (zipl != nullptr)
{
// It seems like this is what we are looking for
sffiles.Push(fn);
FSoundFontInfo sft = { fb, fbe, fn, SF_GUS };
2018-02-22 13:30:43 +00:00
soundfonts.Push(sft);
}
}
delete zip;
}
}
}
}
//==========================================================================
//
//
//
//==========================================================================
void FSoundFontManager::CollectSoundfonts()
{
findstate_t c_file;
void *file;
TArray<FString> sffiles;
if (GameConfig != NULL && GameConfig->SetSection ("FileSearch.Directories"))
{
const char *key;
const char *value;
while (GameConfig->NextInSection (key, value))
{
if (stricmp (key, "Path") == 0)
{
FString dir;
dir = NicePath(value);
if (dir.IsNotEmpty())
{
if (dir.Back() != '/') dir += '/';
2018-02-22 13:30:43 +00:00
FString path = dir + "soundfonts/";
FString mask = path + '*';
if ((file = I_FindFirst(mask, &c_file)) != ((void *)(-1)))
{
do
{
if (!(I_FindAttr(&c_file) & FA_DIREC))
{
FStringf name("%s%s", path.GetChars(), I_FindName(&c_file));
ProcessOneFile(name, sffiles);
}
} while (I_FindNext(file, &c_file) == 0);
I_FindClose(file);
}
}
}
}
}
2018-02-22 13:30:43 +00:00
if (sffiles.Size() > 0)
soundfontcollection.InitMultipleFiles(sffiles);
}
//==========================================================================
//
//
//
//==========================================================================
const FSoundFontInfo *FSoundFontManager::FindSoundFont(const char *name, int allowed) const
{
for(auto &sfi : soundfonts)
{
// an empty name will pick the first one in a compatible format.
if (allowed & sfi.type && (name == nullptr || *name == 0 || !sfi.mName.CompareNoCase(name) || !sfi.mNameExt.CompareNoCase(name)))
{
return &sfi;
}
}
return nullptr;
}
//==========================================================================
//
//
//
//==========================================================================
FSoundFontReader *FSoundFontManager::OpenSoundFont(const char *name, int allowed)
{
// First check if the given name is inside the loaded resources.
// To avoid clashes this will only be done if the name has the '.cfg' extension.
// Sound fonts cannot be loaded this way.
if (name != nullptr)
{
const char *p = name + strlen(name) - 4;
if (p > name && !stricmp(p, ".cfg") && Wads.CheckNumForFullName(name) >= 0)
{
return new FLumpPatchSetReader(name);
}
}
auto sfi = FindSoundFont(name, allowed);
if (sfi != nullptr)
{
return new FSoundFontReader(&soundfontcollection, sfi);
}
// The sound font collection did not yield any good results.
// Next check if the file is a .sf file
if (allowed & SF_SF2)
{
FileReader fr;
if (fr.Open(name))
{
char head[16] = { 0};
fr.Read(head, 16);
fr.Close();
if (!memcmp(head, "RIFF", 4) && !memcmp(head+8, "sfbkLIST", 8))
{
return new FSF2Reader(name);
}
}
}
if (allowed & SF_GUS)
{
FileReader fr;
if (fr.Open(name))
{
char head[16] = { 0 };
fr.Read(head, 2);
fr.Close();
if (!memcmp(head, "PK", 2)) // The only reason for this check is to block non-Zips. The actual validation will be done by FZipFile.
{
auto r = new FZipPatReader(name);
if (r->isOk()) return r;
delete r;
}
}
// Config files are only accepted if they are named '.cfg', because they are impossible to validate.
const char *p = name + strlen(name) - 4;
if (p > name && !stricmp(p, ".cfg") && FileExists(name))
{
return new FPatchSetReader(name);
}
}
return nullptr;
}
2018-02-22 13:30:43 +00:00
void I_InitSoundFonts()
{
sfmanager.CollectSoundfonts();
}