commit
a17b97a1c8
19 changed files with 1614 additions and 35 deletions
|
|
@ -210,11 +210,11 @@ int main(int argc, char** argv)
|
|||
std::cout << "\tPreloadedSamples: " << synth.getNumPreloadedSamples() << '\n';
|
||||
std::cout << "==========" << '\n';
|
||||
std::cout << "Included files:" << '\n';
|
||||
for (auto& file : synth.getIncludedFiles())
|
||||
std::cout << '\t' << file.string() << '\n';
|
||||
for (auto& file : synth.getParser().getIncludedFiles())
|
||||
std::cout << '\t' << file << '\n';
|
||||
std::cout << "==========" << '\n';
|
||||
std::cout << "Defines:" << '\n';
|
||||
for (auto& define : synth.getDefines())
|
||||
for (auto& define : synth.getParser().getDefines())
|
||||
std::cout << '\t' << define.first << '=' << define.second << '\n';
|
||||
std::cout << "==========" << '\n';
|
||||
std::cout << "Unknown opcodes:";
|
||||
|
|
|
|||
|
|
@ -29,10 +29,12 @@ include (SfizzSIMDSourceFiles)
|
|||
|
||||
# Parser core library
|
||||
add_library (sfizz_parser STATIC)
|
||||
target_sources (sfizz_parser PRIVATE sfizz/Parser.cpp sfizz/Opcode.cpp sfizz/SfzHelpers.cpp)
|
||||
target_sources (sfizz_parser PRIVATE
|
||||
sfizz/Parser.cpp sfizz/Opcode.cpp sfizz/SfzHelpers.cpp
|
||||
sfizz/parser/Parser.cpp sfizz/parser/ParserPrivate.cpp)
|
||||
target_include_directories (sfizz_parser PUBLIC sfizz)
|
||||
target_include_directories (sfizz_parser PUBLIC external)
|
||||
target_link_libraries (sfizz_parser PUBLIC absl::strings)
|
||||
target_link_libraries (sfizz_parser PUBLIC absl::strings PRIVATE absl::flat_hash_map)
|
||||
|
||||
# Sfizz static library
|
||||
add_library(sfizz_static STATIC)
|
||||
|
|
|
|||
|
|
@ -9,11 +9,9 @@
|
|||
#include <cctype>
|
||||
|
||||
sfz::Opcode::Opcode(absl::string_view inputOpcode, absl::string_view inputValue)
|
||||
: opcode(inputOpcode)
|
||||
, value(inputValue)
|
||||
: opcode(trim(inputOpcode))
|
||||
, value(trim(inputValue))
|
||||
{
|
||||
trimInPlace(value);
|
||||
trimInPlace(opcode);
|
||||
size_t nextCharIndex { 0 };
|
||||
int parameterPosition { 0 };
|
||||
auto nextNumIndex = opcode.find_first_of("1234567890");
|
||||
|
|
|
|||
|
|
@ -28,8 +28,8 @@ namespace sfz {
|
|||
struct Opcode {
|
||||
Opcode() = delete;
|
||||
Opcode(absl::string_view inputOpcode, absl::string_view inputValue);
|
||||
absl::string_view opcode {};
|
||||
absl::string_view value {};
|
||||
std::string opcode {};
|
||||
std::string value {};
|
||||
uint64_t lettersOnlyHash { Fnv1aBasis };
|
||||
// This is to handle the integer parameters of some opcodes
|
||||
std::vector<uint16_t> parameters;
|
||||
|
|
|
|||
|
|
@ -19,7 +19,7 @@ void removeCommentOnLine(absl::string_view& line)
|
|||
line.remove_suffix(line.size() - position);
|
||||
}
|
||||
|
||||
bool sfz::Parser::loadSfzFile(const fs::path& file)
|
||||
bool sfz::OldParser::loadSfzFile(const fs::path& file)
|
||||
{
|
||||
includedFiles.clear();
|
||||
|
||||
|
|
@ -56,7 +56,7 @@ bool sfz::Parser::loadSfzFile(const fs::path& file)
|
|||
return true;
|
||||
}
|
||||
|
||||
void sfz::Parser::readSfzFile(const fs::path& fileName, std::vector<std::string>& lines) noexcept
|
||||
void sfz::OldParser::readSfzFile(const fs::path& fileName, std::vector<std::string>& lines) noexcept
|
||||
{
|
||||
std::ifstream fileStream(fileName.c_str());
|
||||
if (!fileStream)
|
||||
|
|
|
|||
|
|
@ -14,9 +14,9 @@
|
|||
#include <vector>
|
||||
|
||||
namespace sfz {
|
||||
class Parser {
|
||||
class OldParser {
|
||||
public:
|
||||
virtual ~Parser() = default;
|
||||
virtual ~OldParser() = default;
|
||||
virtual bool loadSfzFile(const fs::path& file);
|
||||
const std::map<std::string, std::string>& getDefines() const noexcept { return defines; }
|
||||
const std::vector<fs::path>& getIncludedFiles() const noexcept { return includedFiles; }
|
||||
|
|
|
|||
|
|
@ -28,6 +28,8 @@ sfz::Synth::Synth()
|
|||
|
||||
sfz::Synth::Synth(int numVoices)
|
||||
{
|
||||
parser.setListener(this);
|
||||
|
||||
effectFactory.registerStandardEffectTypes();
|
||||
|
||||
effectBuses.reserve(5); // sufficient room for main and fx1-4
|
||||
|
|
@ -48,7 +50,7 @@ sfz::Synth::~Synth()
|
|||
resources.filePool.emptyFileLoadingQueues();
|
||||
}
|
||||
|
||||
void sfz::Synth::callback(absl::string_view header, const std::vector<Opcode>& members)
|
||||
void sfz::Synth::onParseFullBlock(const std::string& header, const std::vector<Opcode>& members)
|
||||
{
|
||||
switch (hash(header)) {
|
||||
case hash("global"):
|
||||
|
|
@ -81,6 +83,18 @@ void sfz::Synth::callback(absl::string_view header, const std::vector<Opcode>& m
|
|||
}
|
||||
}
|
||||
|
||||
void sfz::Synth::onParseError(const SourceRange& range, const std::string& message)
|
||||
{
|
||||
const auto relativePath = range.start.filePath->lexically_relative(parser.originalDirectory());
|
||||
std::cerr << "Parse error in " << relativePath << " at line " << range.start.lineNumber + 1 << ": " << message << '\n';
|
||||
}
|
||||
|
||||
void sfz::Synth::onParseWarning(const SourceRange& range, const std::string& message)
|
||||
{
|
||||
const auto relativePath = range.start.filePath->lexically_relative(parser.originalDirectory());
|
||||
std::cerr << "Parse warning in " << relativePath << " at line " << range.start.lineNumber + 1 << ": " << message << '\n';
|
||||
}
|
||||
|
||||
void sfz::Synth::buildRegion(const std::vector<Opcode>& regionOpcodes)
|
||||
{
|
||||
auto lastRegion = absl::make_unique<Region>(resources.midiState, defaultPath);
|
||||
|
|
@ -283,14 +297,15 @@ bool sfz::Synth::loadSfzFile(const fs::path& file)
|
|||
}
|
||||
|
||||
clear();
|
||||
auto parserReturned = sfz::Parser::loadSfzFile(file);
|
||||
if (!parserReturned)
|
||||
|
||||
parser.parseFile(file);
|
||||
if (parser.getErrorCount() > 0)
|
||||
return false;
|
||||
|
||||
if (regions.empty())
|
||||
return false;
|
||||
|
||||
resources.filePool.setRootDirectory(this->originalDirectory);
|
||||
resources.filePool.setRootDirectory(parser.originalDirectory());
|
||||
|
||||
auto currentRegion = regions.begin();
|
||||
auto lastRegion = regions.rbegin();
|
||||
|
|
@ -403,7 +418,7 @@ bool sfz::Synth::loadSfzFile(const fs::path& file)
|
|||
voice->setMaxEQsPerVoice(maxEQs);
|
||||
}
|
||||
|
||||
return parserReturned;
|
||||
return true;
|
||||
}
|
||||
|
||||
sfz::Voice* sfz::Synth::findFreeVoice() noexcept
|
||||
|
|
@ -939,7 +954,7 @@ void sfz::Synth::resetAllControllers(int delay) noexcept
|
|||
fs::file_time_type sfz::Synth::checkModificationTime()
|
||||
{
|
||||
auto returnedTime = modificationTime;
|
||||
for (const auto& file: getIncludedFiles()) {
|
||||
for (const auto& file: parser.getIncludedFiles()) {
|
||||
const auto fileTime = fs::last_write_time(file);
|
||||
if (returnedTime < fileTime)
|
||||
returnedTime = fileTime;
|
||||
|
|
|
|||
|
|
@ -14,6 +14,7 @@
|
|||
#include "LeakDetector.h"
|
||||
#include "MidiState.h"
|
||||
#include "AudioSpan.h"
|
||||
#include "parser/Parser.h"
|
||||
#include "absl/types/span.h"
|
||||
#include <absl/types/optional.h>
|
||||
#include <random>
|
||||
|
|
@ -59,7 +60,7 @@ namespace sfz {
|
|||
* The jack_client.cpp file contains examples of the most classical usage of the
|
||||
* synth and can be used as a reference.
|
||||
*/
|
||||
class Synth : public Parser {
|
||||
class Synth final : public Parser::Listener {
|
||||
public:
|
||||
/**
|
||||
* @brief Construct a new Synth object with no voices. If you want sound
|
||||
|
|
@ -88,7 +89,7 @@ public:
|
|||
* @return true
|
||||
* @return false if the file was not found or no regions were loaded.
|
||||
*/
|
||||
bool loadSfzFile(const fs::path& file) final;
|
||||
bool loadSfzFile(const fs::path& file);
|
||||
/**
|
||||
* @brief Get the current number of regions loaded
|
||||
*
|
||||
|
|
@ -373,6 +374,20 @@ public:
|
|||
*
|
||||
*/
|
||||
void allSoundOff() noexcept;
|
||||
|
||||
/**
|
||||
* @brief Get the parser.
|
||||
*
|
||||
* @return A reference to the parser.
|
||||
*/
|
||||
Parser& getParser() noexcept { return parser; }
|
||||
/**
|
||||
* @brief Get the parser.
|
||||
*
|
||||
* @return A reference to the parser.
|
||||
*/
|
||||
const Parser& getParser() const noexcept { return parser; }
|
||||
|
||||
protected:
|
||||
/**
|
||||
* @brief The parser callback; this is called by the parent object each time
|
||||
|
|
@ -382,7 +397,17 @@ protected:
|
|||
* @param header the header for the set of opcodes
|
||||
* @param members the opcode members
|
||||
*/
|
||||
void callback(absl::string_view header, const std::vector<Opcode>& members) final;
|
||||
void onParseFullBlock(const std::string& header, const std::vector<Opcode>& members) override;
|
||||
|
||||
/**
|
||||
* @brief The parser callback when an error occurs.
|
||||
*/
|
||||
void onParseError(const SourceRange& range, const std::string& message) override;
|
||||
|
||||
/**
|
||||
* @brief The parser callback when a warning occurs.
|
||||
*/
|
||||
void onParseWarning(const SourceRange& range, const std::string& message) override;
|
||||
|
||||
private:
|
||||
/**
|
||||
|
|
@ -505,6 +530,7 @@ private:
|
|||
|
||||
Duration dispatchDuration { 0 };
|
||||
|
||||
Parser parser;
|
||||
fs::file_time_type modificationTime { };
|
||||
|
||||
LEAK_DETECTOR(Synth);
|
||||
|
|
|
|||
456
src/sfizz/parser/Parser.cpp
Normal file
456
src/sfizz/parser/Parser.cpp
Normal file
|
|
@ -0,0 +1,456 @@
|
|||
// SPDX-License-Identifier: BSD-2-Clause
|
||||
|
||||
// This code is part of the sfizz library and is licensed under a BSD 2-clause
|
||||
// license. You should have receive a LICENSE.md file along with the code.
|
||||
// If not, contact the sfizz maintainers at https://github.com/sfztools/sfizz
|
||||
|
||||
#include "Parser.h"
|
||||
#include "ParserPrivate.h"
|
||||
#include "absl/memory/memory.h"
|
||||
|
||||
namespace sfz {
|
||||
|
||||
Parser::Parser()
|
||||
{
|
||||
}
|
||||
|
||||
Parser::~Parser()
|
||||
{
|
||||
}
|
||||
|
||||
void Parser::reset()
|
||||
{
|
||||
_pathsIncluded.clear();
|
||||
_currentDefinitions = _externalDefinitions;
|
||||
_currentHeader.reset();
|
||||
_currentOpcodes.clear();
|
||||
_errorCount = 0;
|
||||
_warningCount = 0;
|
||||
}
|
||||
|
||||
void Parser::addExternalDefinition(absl::string_view id, absl::string_view value)
|
||||
{
|
||||
_externalDefinitions[id] = std::string(value);
|
||||
}
|
||||
|
||||
void Parser::clearExternalDefinitions()
|
||||
{
|
||||
_externalDefinitions.clear();
|
||||
}
|
||||
|
||||
void Parser::parseFile(const fs::path& path)
|
||||
{
|
||||
parseVirtualFile(path, nullptr);
|
||||
}
|
||||
|
||||
void Parser::parseString(const fs::path& path, absl::string_view sfzView)
|
||||
{
|
||||
parseVirtualFile(path, absl::make_unique<StringViewReader>(path, sfzView));
|
||||
}
|
||||
|
||||
void Parser::parseVirtualFile(const fs::path& path, std::unique_ptr<Reader> reader)
|
||||
{
|
||||
reset();
|
||||
|
||||
if (_listener)
|
||||
_listener->onParseBegin();
|
||||
|
||||
includeNewFile(path, std::move(reader), {});
|
||||
processTopLevel();
|
||||
flushCurrentHeader();
|
||||
|
||||
if (_listener)
|
||||
_listener->onParseEnd();
|
||||
}
|
||||
|
||||
void Parser::includeNewFile(const fs::path& path, std::unique_ptr<Reader> reader, const SourceRange& includeStmtRange)
|
||||
{
|
||||
fs::path fullPath =
|
||||
(path.empty() || path.is_absolute()) ? path : _originalDirectory / path;
|
||||
|
||||
if (_pathsIncluded.empty())
|
||||
_originalDirectory = fullPath.parent_path();
|
||||
else if (_pathsIncluded.find(fullPath.string()) != _pathsIncluded.end()) {
|
||||
if (_recursiveIncludeGuardEnabled)
|
||||
return;
|
||||
}
|
||||
|
||||
auto makeErrorRange = [&]() -> SourceRange {
|
||||
if (!includeStmtRange) {
|
||||
SourceLocation loc;
|
||||
loc.filePath = std::make_shared<fs::path>(fullPath);
|
||||
return {loc, loc};
|
||||
}
|
||||
return includeStmtRange;
|
||||
};
|
||||
|
||||
if (_included.size() == _maxIncludeDepth) {
|
||||
emitError(makeErrorRange(), "Exceeded maximum include depth (" + std::to_string(_maxIncludeDepth) + ")");
|
||||
return;
|
||||
}
|
||||
|
||||
if (!reader) {
|
||||
auto fileReader = absl::make_unique<FileReader>(fullPath);
|
||||
if (fileReader->hasError()) {
|
||||
SourceLocation loc = fileReader->location();
|
||||
emitError(makeErrorRange(), "Cannot open file for reading: " + fullPath.string());
|
||||
return;
|
||||
}
|
||||
reader = std::move(fileReader);
|
||||
}
|
||||
|
||||
_pathsIncluded.insert(fullPath.string());
|
||||
_included.push_back(std::move(reader));
|
||||
}
|
||||
|
||||
void Parser::addDefinition(absl::string_view id, absl::string_view value)
|
||||
{
|
||||
_currentDefinitions[id] = std::string(value);
|
||||
}
|
||||
|
||||
void Parser::processTopLevel()
|
||||
{
|
||||
while (!_included.empty()) {
|
||||
Reader& reader = *_included.back();
|
||||
|
||||
while (reader.skipChars(" \t\r\n") || skipComment(reader));
|
||||
|
||||
switch (reader.peekChar()) {
|
||||
case Reader::kEof:
|
||||
_included.pop_back();
|
||||
break;
|
||||
case '#':
|
||||
processDirective();
|
||||
break;
|
||||
case '<':
|
||||
processHeader();
|
||||
break;
|
||||
default:
|
||||
processOpcode();
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
void Parser::processDirective()
|
||||
{
|
||||
Reader& reader = *_included.back();
|
||||
SourceLocation start = reader.location();
|
||||
|
||||
if (reader.getChar() != '#') {
|
||||
SourceLocation end = reader.location();
|
||||
emitError({ start, end }, "Expected `#` at start of directive.");
|
||||
recover();
|
||||
return;
|
||||
}
|
||||
|
||||
std::string directive;
|
||||
reader.extractWhile(&directive, isIdentifierChar);
|
||||
|
||||
if (directive == "define") {
|
||||
reader.skipChars(" \t");
|
||||
|
||||
std::string id;
|
||||
if (!reader.extractExactChar('$') || !reader.extractWhile(&id, isIdentifierChar)) {
|
||||
SourceLocation end = reader.location();
|
||||
emitError({ start, end }, "Expected $identifier after #define.");
|
||||
recover();
|
||||
return;
|
||||
}
|
||||
|
||||
reader.skipChars(" \t");
|
||||
|
||||
std::string value;
|
||||
extractToEol(reader, &value);
|
||||
trimRight(value);
|
||||
|
||||
addDefinition(id, value);
|
||||
}
|
||||
else if (directive == "include") {
|
||||
reader.skipChars(" \t");
|
||||
|
||||
std::string path;
|
||||
bool valid = false;
|
||||
|
||||
if (reader.extractExactChar('"')) {
|
||||
reader.extractWhile(&path, [](char c) { return c != '"' && c != '\r' && c != '\n'; });
|
||||
valid = reader.extractExactChar('"');
|
||||
}
|
||||
|
||||
SourceLocation end = reader.location();
|
||||
|
||||
if (!valid) {
|
||||
emitError({ start, end }, "Expected \"file.sfz\" after #include.");
|
||||
recover();
|
||||
return;
|
||||
}
|
||||
|
||||
std::replace(path.begin(), path.end(), '\\', '/');
|
||||
includeNewFile(path, nullptr, { start, end });
|
||||
}
|
||||
else {
|
||||
SourceLocation end = reader.location();
|
||||
emitError({ start, end }, "Unrecognized directive `" + directive + "`");
|
||||
recover();
|
||||
}
|
||||
}
|
||||
|
||||
void Parser::processHeader()
|
||||
{
|
||||
Reader& reader = *_included.back();
|
||||
SourceLocation start = reader.location();
|
||||
|
||||
if (reader.getChar() != '<') {
|
||||
SourceLocation end = reader.location();
|
||||
emitError({ start, end }, "Expected `<` at start of header.");
|
||||
recover();
|
||||
return;
|
||||
}
|
||||
|
||||
std::string name;
|
||||
reader.extractWhile(&name, [](char c) {
|
||||
return c != '\r' && c != '\n' && c != '>';
|
||||
});
|
||||
|
||||
if (reader.peekChar() != '>') {
|
||||
SourceLocation end = reader.location();
|
||||
emitError({ start, end }, "Expected `>` at end of header.");
|
||||
recover();
|
||||
return;
|
||||
}
|
||||
reader.getChar();
|
||||
SourceLocation end = reader.location();
|
||||
|
||||
if (!isIdentifier(name)) {
|
||||
emitError({ start, end }, "The header name `" + name + "` is not a valid identifier.");
|
||||
recover();
|
||||
return;
|
||||
}
|
||||
|
||||
flushCurrentHeader();
|
||||
|
||||
_currentHeader = name;
|
||||
if (_listener)
|
||||
_listener->onParseHeader({ start, end }, name);
|
||||
}
|
||||
|
||||
void Parser::processOpcode()
|
||||
{
|
||||
Reader& reader = *_included.back();
|
||||
SourceLocation opcodeStart = reader.location();
|
||||
|
||||
auto isRawOpcodeNameChar = [](char c) {
|
||||
return isIdentifierChar(c) || c == '$';
|
||||
};
|
||||
|
||||
std::string nameRaw;
|
||||
reader.extractWhile(&nameRaw, isRawOpcodeNameChar);
|
||||
|
||||
SourceLocation opcodeEnd = reader.location();
|
||||
|
||||
if (nameRaw.empty()) {
|
||||
emitError({ opcodeStart, opcodeEnd }, "Expected opcode name.");
|
||||
recover();
|
||||
return;
|
||||
}
|
||||
|
||||
if (reader.peekChar() != '=') {
|
||||
emitError({ opcodeStart, opcodeEnd }, "Expected `=` after opcode name.");
|
||||
recover();
|
||||
return;
|
||||
}
|
||||
|
||||
std::string nameExpanded = expandDollarVars({ opcodeStart, opcodeEnd }, nameRaw);
|
||||
if (!isIdentifier(nameExpanded)) {
|
||||
emitError({ opcodeStart, opcodeEnd }, "The opcode name `" + nameExpanded + "` is not a valid identifier.");
|
||||
recover();
|
||||
return;
|
||||
}
|
||||
|
||||
reader.getChar();
|
||||
|
||||
SourceLocation valueStart = reader.location();
|
||||
std::string valueRaw;
|
||||
extractToEol(reader, &valueRaw);
|
||||
|
||||
// if a "=" or "<" character was hit, it means we read too far
|
||||
size_t position = valueRaw.find_first_of("=<");
|
||||
if (position != valueRaw.npos) {
|
||||
char hitChar = valueRaw[position];
|
||||
|
||||
// if it was "=", rewind before the opcode name and spaces preceding
|
||||
if (hitChar == '=') {
|
||||
while (position > 0 && isRawOpcodeNameChar(valueRaw[position - 1]))
|
||||
--position;
|
||||
while (position > 0 && isSpaceChar(valueRaw[position - 1]))
|
||||
--position;
|
||||
}
|
||||
|
||||
absl::string_view excess(&valueRaw[position], valueRaw.size() - position);
|
||||
reader.putBackChars(excess);
|
||||
valueRaw.resize(position);
|
||||
|
||||
// ensure that we are landing back next to a space char
|
||||
if (hitChar == '=' && !reader.hasOneOfChars(" \t\r\n")) {
|
||||
SourceLocation end = reader.location();
|
||||
emitError({ valueStart, end }, "Unexpected `=` in opcode value.");
|
||||
recover();
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
while (!valueRaw.empty() && isSpaceChar(valueRaw.back())) {
|
||||
reader.putBackChar(valueRaw.back());
|
||||
valueRaw.pop_back();
|
||||
}
|
||||
SourceLocation valueEnd = reader.location();
|
||||
|
||||
if (!_currentHeader)
|
||||
emitWarning({ opcodeStart, valueEnd }, "The opcode is not under any header.");
|
||||
|
||||
std::string valueExpanded = expandDollarVars({ valueStart, valueEnd }, valueRaw);
|
||||
_currentOpcodes.emplace_back(nameExpanded, valueExpanded);
|
||||
|
||||
if (_listener)
|
||||
_listener->onParseOpcode({ opcodeStart, opcodeEnd }, { valueStart, valueEnd }, nameExpanded, valueExpanded);
|
||||
}
|
||||
|
||||
void Parser::emitError(const SourceRange& range, const std::string& message)
|
||||
{
|
||||
++_errorCount;
|
||||
if (_listener)
|
||||
_listener->onParseError(range, message);
|
||||
}
|
||||
|
||||
void Parser::emitWarning(const SourceRange& range, const std::string& message)
|
||||
{
|
||||
++_warningCount;
|
||||
if (_listener)
|
||||
_listener->onParseWarning(range, message);
|
||||
}
|
||||
|
||||
void Parser::recover()
|
||||
{
|
||||
Reader& reader = *_included.back();
|
||||
|
||||
// skip the current line and let the parser proceed at the next
|
||||
reader.skipWhile([](char c) { return c != '\n'; });
|
||||
}
|
||||
|
||||
void Parser::flushCurrentHeader()
|
||||
{
|
||||
if (_currentHeader) {
|
||||
if (_listener)
|
||||
_listener->onParseFullBlock(*_currentHeader, _currentOpcodes);
|
||||
_currentHeader.reset();
|
||||
}
|
||||
|
||||
_currentOpcodes.clear();
|
||||
}
|
||||
|
||||
bool Parser::hasComment(Reader& reader)
|
||||
{
|
||||
if (reader.peekChar() != '/')
|
||||
return false;
|
||||
|
||||
reader.getChar();
|
||||
if (reader.peekChar() != '/') {
|
||||
reader.putBackChar('/');
|
||||
return false;
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
size_t Parser::skipComment(Reader& reader)
|
||||
{
|
||||
if (!hasComment(reader))
|
||||
return 0;
|
||||
|
||||
size_t count = 2;
|
||||
reader.getChar();
|
||||
reader.getChar();
|
||||
|
||||
int c;
|
||||
while ((c = reader.getChar()) != Reader::kEof && c != '\r' && c != '\n')
|
||||
++count;
|
||||
|
||||
return count;
|
||||
}
|
||||
|
||||
void Parser::trimRight(std::string& text)
|
||||
{
|
||||
while (!text.empty() && isSpaceChar(text.back()))
|
||||
text.pop_back();
|
||||
}
|
||||
|
||||
size_t Parser::extractToEol(Reader& reader, std::string* dst)
|
||||
{
|
||||
return reader.extractWhile(dst, [&reader](char c) {
|
||||
return c != '\r' && c != '\n' && !(c == '/' && reader.peekChar() == '/');
|
||||
});
|
||||
}
|
||||
|
||||
std::string Parser::expandDollarVars(const SourceRange& range, absl::string_view src)
|
||||
{
|
||||
std::string dst;
|
||||
dst.reserve(2 * src.size());
|
||||
|
||||
size_t i = 0;
|
||||
size_t n = src.size();
|
||||
while (i < n) {
|
||||
char c = src[i++];
|
||||
|
||||
if (c != '$')
|
||||
dst.push_back(c);
|
||||
else {
|
||||
std::string name;
|
||||
name.reserve(64);
|
||||
|
||||
while (i < n && isIdentifierChar(src[i]))
|
||||
name.push_back(src[i++]);
|
||||
|
||||
if (name.empty()) {
|
||||
emitWarning(range, "Expected variable name after $.");
|
||||
continue;
|
||||
}
|
||||
|
||||
auto it = _currentDefinitions.find(name);
|
||||
if (it == _currentDefinitions.end()) {
|
||||
emitWarning(range, "The variable `" + name + "` is not defined.");
|
||||
continue;
|
||||
}
|
||||
|
||||
dst.append(it->second);
|
||||
}
|
||||
}
|
||||
|
||||
return dst;
|
||||
}
|
||||
|
||||
bool Parser::isIdentifierChar(char c)
|
||||
{
|
||||
return c == '_' ||
|
||||
(c >= 'a' && c <= 'z') || (c >= 'A' && c <= 'Z') ||
|
||||
(c >= '0' && c <= '9');
|
||||
}
|
||||
|
||||
bool Parser::isSpaceChar(char c)
|
||||
{
|
||||
return c == ' ' || c == '\t';
|
||||
}
|
||||
|
||||
bool Parser::isIdentifier(absl::string_view s)
|
||||
{
|
||||
if (s.empty())
|
||||
return false;
|
||||
|
||||
for (char c : s) {
|
||||
if (!isIdentifierChar(c))
|
||||
return false;
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
} // namespace sfz
|
||||
141
src/sfizz/parser/Parser.h
Normal file
141
src/sfizz/parser/Parser.h
Normal file
|
|
@ -0,0 +1,141 @@
|
|||
// SPDX-License-Identifier: BSD-2-Clause
|
||||
|
||||
// This code is part of the sfizz library and is licensed under a BSD 2-clause
|
||||
// license. You should have receive a LICENSE.md file along with the code.
|
||||
// If not, contact the sfizz maintainers at https://github.com/sfztools/sfizz
|
||||
|
||||
#pragma once
|
||||
#include "../Opcode.h"
|
||||
#include "ghc/fs_std.hpp"
|
||||
#include "absl/types/optional.h"
|
||||
#include "absl/container/flat_hash_map.h"
|
||||
#include "absl/container/flat_hash_set.h"
|
||||
#include <string>
|
||||
#include <memory>
|
||||
|
||||
namespace sfz {
|
||||
|
||||
class Reader;
|
||||
struct SourceLocation;
|
||||
struct SourceRange;
|
||||
|
||||
/**
|
||||
* @brief Context-dependent parser for SFZ files
|
||||
*/
|
||||
class Parser {
|
||||
public:
|
||||
Parser();
|
||||
~Parser();
|
||||
|
||||
void addExternalDefinition(absl::string_view id, absl::string_view value);
|
||||
void clearExternalDefinitions();
|
||||
|
||||
void parseFile(const fs::path& path);
|
||||
void parseString(const fs::path& path, absl::string_view sfzView);
|
||||
void parseVirtualFile(const fs::path& path, std::unique_ptr<Reader> reader);
|
||||
|
||||
void setRecursiveIncludeGuardEnabled(bool en) { _recursiveIncludeGuardEnabled = en; }
|
||||
void setMaximumIncludeDepth(size_t depth) { _maxIncludeDepth = depth; }
|
||||
|
||||
const fs::path& originalDirectory() const noexcept { return _originalDirectory; }
|
||||
|
||||
typedef absl::flat_hash_set<std::string> IncludeFileSet;
|
||||
typedef absl::flat_hash_map<std::string, std::string> DefinitionSet;
|
||||
|
||||
const IncludeFileSet& getIncludedFiles() const noexcept { return _pathsIncluded; }
|
||||
const DefinitionSet& getDefines() const noexcept { return _currentDefinitions; }
|
||||
|
||||
size_t getErrorCount() const noexcept { return _errorCount; }
|
||||
size_t getWarningCount() const noexcept { return _warningCount; }
|
||||
|
||||
class Listener {
|
||||
public:
|
||||
// low-level parsing
|
||||
virtual void onParseBegin() {}
|
||||
virtual void onParseEnd() {}
|
||||
virtual void onParseHeader(const SourceRange& /*range*/, const std::string& /*header*/) {}
|
||||
virtual void onParseOpcode(const SourceRange& /*rangeOpcode*/, const SourceRange& /*rangeValue*/, const std::string& /*name*/, const std::string& /*value*/) {}
|
||||
virtual void onParseError(const SourceRange& /*range*/, const std::string& /*message*/) {}
|
||||
virtual void onParseWarning(const SourceRange& /*range*/, const std::string& /*message*/) {}
|
||||
|
||||
// high-level parsing
|
||||
virtual void onParseFullBlock(const std::string& /*header*/, const std::vector<Opcode>& /*opcodes*/) {}
|
||||
};
|
||||
|
||||
void setListener(Listener* listener) noexcept { _listener = listener; }
|
||||
|
||||
private:
|
||||
void includeNewFile(const fs::path& path, std::unique_ptr<Reader> reader, const SourceRange& includeStmtRange);
|
||||
void addDefinition(absl::string_view id, absl::string_view value);
|
||||
void processTopLevel();
|
||||
void processDirective();
|
||||
void processHeader();
|
||||
void processOpcode();
|
||||
void reset();
|
||||
|
||||
// errors and warnings
|
||||
void emitError(const SourceRange& range, const std::string& message);
|
||||
void emitWarning(const SourceRange& range, const std::string& message);
|
||||
|
||||
// recover after error
|
||||
void recover();
|
||||
|
||||
// state handling
|
||||
void flushCurrentHeader();
|
||||
|
||||
// helpers
|
||||
static bool hasComment(Reader& reader);
|
||||
static size_t skipComment(Reader& reader);
|
||||
static void trimRight(std::string& text);
|
||||
static size_t extractToEol(Reader& reader, std::string* dst); // ignores comment
|
||||
std::string expandDollarVars(const SourceRange& range, absl::string_view src);
|
||||
|
||||
// predicates
|
||||
static bool isIdentifierChar(char c);
|
||||
static bool isSpaceChar(char c);
|
||||
static bool isIdentifier(absl::string_view s);
|
||||
|
||||
private:
|
||||
Listener* _listener = nullptr;
|
||||
|
||||
fs::path _originalDirectory { fs::current_path() };
|
||||
DefinitionSet _externalDefinitions;
|
||||
|
||||
// a current list of files included, last one at the back
|
||||
std::vector<std::unique_ptr<Reader>> _included;
|
||||
|
||||
// recursive include guard
|
||||
size_t _maxIncludeDepth = 32;
|
||||
bool _recursiveIncludeGuardEnabled = false;
|
||||
IncludeFileSet _pathsIncluded;
|
||||
DefinitionSet _currentDefinitions;
|
||||
|
||||
// parsing state
|
||||
absl::optional<std::string> _currentHeader;
|
||||
std::vector<Opcode> _currentOpcodes;
|
||||
|
||||
// errors and warnings
|
||||
size_t _errorCount = 0;
|
||||
size_t _warningCount = 0;
|
||||
};
|
||||
|
||||
/**
|
||||
* @brief Source file location for errors and warnings.
|
||||
*/
|
||||
struct SourceLocation {
|
||||
std::shared_ptr<fs::path> filePath;
|
||||
size_t lineNumber = 0;
|
||||
size_t columnNumber = 0;
|
||||
explicit operator bool() const noexcept { return filePath != nullptr; }
|
||||
};
|
||||
|
||||
/**
|
||||
* @brief Range of source file.
|
||||
*/
|
||||
struct SourceRange {
|
||||
SourceLocation start;
|
||||
SourceLocation end;
|
||||
explicit operator bool() const noexcept { return bool(start) && bool(end); }
|
||||
};
|
||||
|
||||
} // namespace sfz
|
||||
155
src/sfizz/parser/ParserPrivate.cpp
Normal file
155
src/sfizz/parser/ParserPrivate.cpp
Normal file
|
|
@ -0,0 +1,155 @@
|
|||
// SPDX-License-Identifier: BSD-2-Clause
|
||||
|
||||
// This code is part of the sfizz library and is licensed under a BSD 2-clause
|
||||
// license. You should have receive a LICENSE.md file along with the code.
|
||||
// If not, contact the sfizz maintainers at https://github.com/sfztools/sfizz
|
||||
|
||||
#include "ParserPrivate.h"
|
||||
|
||||
namespace sfz {
|
||||
|
||||
Reader::Reader(const fs::path& filePath)
|
||||
{
|
||||
_accum.reserve(256);
|
||||
_loc.filePath = std::make_shared<fs::path>(filePath);
|
||||
_lineNumColumns.reserve(256);
|
||||
}
|
||||
|
||||
int Reader::getChar()
|
||||
{
|
||||
int byte;
|
||||
|
||||
if (_accum.empty())
|
||||
byte = getNextStreamByte();
|
||||
else {
|
||||
byte = static_cast<unsigned char>(_accum.back());
|
||||
_accum.pop_back();
|
||||
}
|
||||
|
||||
if (byte != kEof)
|
||||
updateSourceLocationAdding(byte);
|
||||
|
||||
return byte;
|
||||
}
|
||||
|
||||
int Reader::peekChar()
|
||||
{
|
||||
int byte;
|
||||
|
||||
if (_accum.empty()) {
|
||||
byte = getChar();
|
||||
putBackChar(byte);
|
||||
}
|
||||
else
|
||||
byte = static_cast<unsigned char>(_accum.back());
|
||||
|
||||
return byte;
|
||||
}
|
||||
|
||||
bool Reader::extractExactChar(char c)
|
||||
{
|
||||
int next = peekChar();
|
||||
|
||||
if (next == kEof || next != static_cast<unsigned char>(c))
|
||||
return false;
|
||||
|
||||
getChar();
|
||||
return true;
|
||||
}
|
||||
|
||||
void Reader::putBackChar(int c)
|
||||
{
|
||||
if (c == kEof)
|
||||
return;
|
||||
|
||||
char c8bit = static_cast<unsigned char>(c);
|
||||
putBackChars(absl::string_view(&c8bit, 1));
|
||||
}
|
||||
|
||||
void Reader::putBackChars(absl::string_view characters)
|
||||
{
|
||||
_accum.append(characters.rbegin(), characters.rend());
|
||||
|
||||
for (size_t i = characters.size(); i-- > 0;)
|
||||
updateSourceLocationRemoving(static_cast<unsigned char>(characters[i]));
|
||||
}
|
||||
|
||||
size_t Reader::skipChars(absl::string_view chars)
|
||||
{
|
||||
size_t count = 0;
|
||||
|
||||
while (chars.find(static_cast<unsigned char>(peekChar())) != chars.npos) {
|
||||
getChar();
|
||||
++count;
|
||||
}
|
||||
|
||||
return count;
|
||||
}
|
||||
|
||||
bool Reader::hasEof()
|
||||
{
|
||||
return peekChar() == kEof;
|
||||
}
|
||||
|
||||
bool Reader::hasOneOfChars(absl::string_view chars)
|
||||
{
|
||||
int c = peekChar();
|
||||
if (c == kEof)
|
||||
return false;
|
||||
|
||||
return chars.find(static_cast<unsigned char>(c)) != chars.npos;
|
||||
}
|
||||
|
||||
void Reader::updateSourceLocationAdding(int byte)
|
||||
{
|
||||
if (byte != '\n')
|
||||
_loc.columnNumber += 1;
|
||||
else {
|
||||
_lineNumColumns.push_back(_loc.columnNumber);
|
||||
_loc.lineNumber += 1;
|
||||
_loc.columnNumber = 0;
|
||||
}
|
||||
}
|
||||
|
||||
void Reader::updateSourceLocationRemoving(int byte)
|
||||
{
|
||||
if (byte != '\n')
|
||||
_loc.columnNumber -= 1;
|
||||
else {
|
||||
_loc.lineNumber -= 1;
|
||||
_loc.columnNumber = _lineNumColumns[_loc.lineNumber];
|
||||
_lineNumColumns.pop_back();
|
||||
}
|
||||
}
|
||||
|
||||
//------------------------------------------------------------------------------
|
||||
|
||||
FileReader::FileReader(const fs::path& filePath)
|
||||
: Reader(filePath), _fileStream(filePath)
|
||||
{
|
||||
}
|
||||
|
||||
bool FileReader::hasError()
|
||||
{
|
||||
return !_fileStream.is_open() || _fileStream.bad();
|
||||
}
|
||||
|
||||
int FileReader::getNextStreamByte()
|
||||
{
|
||||
return _fileStream.get();
|
||||
}
|
||||
|
||||
StringViewReader::StringViewReader(const fs::path& filePath, absl::string_view sfzView)
|
||||
: Reader(filePath), _sfzView(sfzView)
|
||||
{
|
||||
}
|
||||
|
||||
int StringViewReader::getNextStreamByte()
|
||||
{
|
||||
if (position < _sfzView.length())
|
||||
return _sfzView[position++];
|
||||
|
||||
return kEof;
|
||||
}
|
||||
|
||||
} // namespace sfz
|
||||
139
src/sfizz/parser/ParserPrivate.h
Normal file
139
src/sfizz/parser/ParserPrivate.h
Normal file
|
|
@ -0,0 +1,139 @@
|
|||
// SPDX-License-Identifier: BSD-2-Clause
|
||||
|
||||
// This code is part of the sfizz library and is licensed under a BSD 2-clause
|
||||
// license. You should have receive a LICENSE.md file along with the code.
|
||||
// If not, contact the sfizz maintainers at https://github.com/sfztools/sfizz
|
||||
|
||||
#pragma once
|
||||
#include "Parser.h"
|
||||
#include "ghc/fs_std.hpp"
|
||||
#include "absl/strings/string_view.h"
|
||||
#include <string>
|
||||
#include <fstream>
|
||||
|
||||
namespace sfz {
|
||||
|
||||
/**
|
||||
* @brief Utility to extract characters and strings from a source of any kind.
|
||||
*/
|
||||
class Reader {
|
||||
public:
|
||||
explicit Reader(const fs::path& filePath);
|
||||
virtual ~Reader() {}
|
||||
|
||||
/**
|
||||
* @brief Get the current source location.
|
||||
*/
|
||||
const SourceLocation& location() const { return _loc; }
|
||||
|
||||
/**
|
||||
* @brief Value of the end-of-file marker.
|
||||
*/
|
||||
static constexpr int kEof = std::char_traits<char>::eof();
|
||||
|
||||
/**
|
||||
* @brief Extract the next character.
|
||||
*/
|
||||
int getChar();
|
||||
|
||||
/**
|
||||
* @brief Get the next character without extracting it.
|
||||
*/
|
||||
int peekChar();
|
||||
|
||||
/**
|
||||
* @brief Put a previously extracted character back into the reader.
|
||||
*/
|
||||
void putBackChar(int c);
|
||||
|
||||
/**
|
||||
* @brief Put some previously extracted characters back into the reader.
|
||||
*/
|
||||
void putBackChars(absl::string_view characters);
|
||||
|
||||
/**
|
||||
* @brief Extract as long as a predicate holds on the next character.
|
||||
*/
|
||||
template <class P> size_t extractWhile(std::string* dst, const P& pred);
|
||||
|
||||
/**
|
||||
* @brief Extract until as a predicate does not hold on the next character.
|
||||
*/
|
||||
template <class P> size_t extractUntil(std::string* dst, const P& pred);
|
||||
|
||||
/**
|
||||
* @brief Extract a character if it is equal to the expected value.
|
||||
*/
|
||||
bool extractExactChar(char c);
|
||||
|
||||
/**
|
||||
* @brief Skip characters which belong to a given set
|
||||
*/
|
||||
size_t skipChars(absl::string_view chars);
|
||||
|
||||
/**
|
||||
* @brief Skip as long as a predicate holds on the next character.
|
||||
*/
|
||||
template <class P> size_t skipWhile(const P& pred);
|
||||
|
||||
/**
|
||||
* @brief Skip until as a predicate does not hold on the next character.
|
||||
*/
|
||||
template <class P> size_t skipUntil(const P& pred);
|
||||
|
||||
/**
|
||||
* @brief Check if the reader has no more characters.
|
||||
*/
|
||||
bool hasEof();
|
||||
|
||||
/**
|
||||
* @brief Check if the reader has one of the following characters next.
|
||||
*/
|
||||
bool hasOneOfChars(absl::string_view chars);
|
||||
|
||||
protected:
|
||||
virtual int getNextStreamByte() = 0;
|
||||
|
||||
private:
|
||||
void updateSourceLocationAdding(int byte);
|
||||
void updateSourceLocationRemoving(int byte);
|
||||
|
||||
private:
|
||||
std::string _accum; // new characters at the front, old at the back
|
||||
SourceLocation _loc;
|
||||
std::vector<int> _lineNumColumns;
|
||||
};
|
||||
|
||||
/**
|
||||
* @brief File-based version of Reader.
|
||||
*/
|
||||
class FileReader : public Reader {
|
||||
public:
|
||||
explicit FileReader(const fs::path& filePath);
|
||||
bool hasError();
|
||||
|
||||
protected:
|
||||
int getNextStreamByte() override;
|
||||
|
||||
private:
|
||||
fs::ifstream _fileStream;
|
||||
};
|
||||
|
||||
/**
|
||||
* @brief String-view-based version of Reader.
|
||||
*/
|
||||
class StringViewReader : public Reader {
|
||||
public:
|
||||
explicit StringViewReader(const fs::path& filePath, absl::string_view sfzView);
|
||||
|
||||
protected:
|
||||
int getNextStreamByte() override;
|
||||
|
||||
private:
|
||||
absl::string_view _sfzView;
|
||||
size_t position { 0 };
|
||||
};
|
||||
|
||||
} // namespace sfz
|
||||
|
||||
#include "ParserPrivate.hpp"
|
||||
50
src/sfizz/parser/ParserPrivate.hpp
Normal file
50
src/sfizz/parser/ParserPrivate.hpp
Normal file
|
|
@ -0,0 +1,50 @@
|
|||
// SPDX-License-Identifier: BSD-2-Clause
|
||||
|
||||
// This code is part of the sfizz library and is licensed under a BSD 2-clause
|
||||
// license. You should have receive a LICENSE.md file along with the code.
|
||||
// If not, contact the sfizz maintainers at https://github.com/sfztools/sfizz
|
||||
|
||||
#include "ParserPrivate.h"
|
||||
|
||||
namespace sfz {
|
||||
|
||||
template <class P>
|
||||
size_t Reader::extractWhile(std::string* dst, const P& pred)
|
||||
{
|
||||
int byte;
|
||||
size_t count = 0;
|
||||
bool more = true;
|
||||
|
||||
while (more && (byte = getChar()) != EOF) {
|
||||
more = pred(static_cast<unsigned char>(byte));
|
||||
if (!more)
|
||||
putBackChar(byte);
|
||||
else {
|
||||
if (dst)
|
||||
dst->push_back(static_cast<unsigned char>(byte));
|
||||
++count;
|
||||
}
|
||||
}
|
||||
|
||||
return count;
|
||||
}
|
||||
|
||||
template <class P>
|
||||
size_t Reader::extractUntil(std::string* dst, const P& pred)
|
||||
{
|
||||
return extractWhile(dst, [&pred](char c) -> bool { return !pred(c); });
|
||||
}
|
||||
|
||||
template <class P>
|
||||
size_t Reader::skipWhile(const P& pred)
|
||||
{
|
||||
return extractWhile(nullptr, pred);
|
||||
}
|
||||
|
||||
template <class P>
|
||||
size_t Reader::skipUntil(const P& pred)
|
||||
{
|
||||
return extractUntil(nullptr, pred);
|
||||
}
|
||||
|
||||
} // namespace sfz
|
||||
|
|
@ -55,6 +55,10 @@ if(JACK_FOUND AND TARGET Qt5::Widgets)
|
|||
target_include_directories(sfizz_demo_wavetables PRIVATE ${JACK_INCLUDE_DIRS})
|
||||
target_link_libraries(sfizz_demo_wavetables PRIVATE sfizz::sfizz Qt5::Widgets ${JACK_LIBRARIES})
|
||||
set_target_properties(sfizz_demo_wavetables PROPERTIES AUTOUIC ON)
|
||||
|
||||
add_executable(sfizz_demo_parser DemoParser.cpp)
|
||||
target_link_libraries(sfizz_demo_parser PRIVATE sfizz_parser Qt5::Widgets)
|
||||
set_target_properties(sfizz_demo_parser PROPERTIES AUTOUIC ON)
|
||||
endif()
|
||||
|
||||
add_executable(eq_apply EQ.cpp)
|
||||
|
|
|
|||
224
tests/DemoParser.cpp
Normal file
224
tests/DemoParser.cpp
Normal file
|
|
@ -0,0 +1,224 @@
|
|||
#include "ui_DemoParser.h"
|
||||
#include "parser/Parser.h"
|
||||
#include <QApplication>
|
||||
#include <QTimer>
|
||||
#include <QFileInfo>
|
||||
#include <QDir>
|
||||
#include <QTextBlock>
|
||||
#include <QDebug>
|
||||
|
||||
class Application : public QApplication, public sfz::Parser::Listener {
|
||||
public:
|
||||
Application(int& argc, char *argv[]) : QApplication(argc, argv) {}
|
||||
void init();
|
||||
|
||||
private:
|
||||
void requestParseCheck();
|
||||
void runParseCheck();
|
||||
|
||||
private:
|
||||
void onParseBegin() override;
|
||||
void onParseEnd() override;
|
||||
void onParseHeader(const sfz::SourceRange& range, const std::string& header) override;
|
||||
void onParseOpcode(const sfz::SourceRange& rangeOpcode, const sfz::SourceRange& rangeValue, const std::string& name, const std::string& value) override;
|
||||
void onParseError(const sfz::SourceRange& range, const std::string& message) override;
|
||||
void onParseWarning(const sfz::SourceRange& range, const std::string& message) override;
|
||||
|
||||
static QTextCursor selectSourceRange(QTextDocument* doc, const sfz::SourceRange& range);
|
||||
|
||||
private:
|
||||
sfz::Parser _parser;
|
||||
Ui::MainWindow _ui;
|
||||
QTimer* _recheckTimer = nullptr;
|
||||
bool _blockTextChanged = false;
|
||||
};
|
||||
|
||||
static const char defaultSfzText[] = R"SFZ(
|
||||
//----------------------------------------------------------------------------//
|
||||
// This is a SFZ test file with many problems. //
|
||||
//----------------------------------------------------------------------------//
|
||||
|
||||
// opcode without header
|
||||
not_in_header=on // warning
|
||||
|
||||
// invalid headers
|
||||
<> // empty
|
||||
<ab@cd> // bad identifier
|
||||
|
||||
<region>
|
||||
sample=*sine key=69
|
||||
sample=My Directory/My Wave.wav // path with spaces
|
||||
sample=My Directory/My Wave.wav key=69 // path with spaces, and other opcode following
|
||||
sample=Foo=Bar.wav // path invalid: it cannot contain the '=' sign
|
||||
|
||||
#include "FileWhichDoesNotExist.sfz"
|
||||
|
||||
// malformed includes
|
||||
#include "MyFileWhichDoesNotExist1.sfz
|
||||
#include MyFileWhichDoesNotExist1.sfz"
|
||||
|
||||
// #define with some bad variable names
|
||||
#define $foo 1234
|
||||
#define Foo 1234
|
||||
#define $ 1234
|
||||
|
||||
// #define with empty expansion, accepted
|
||||
#define $foo
|
||||
|
||||
// expansion
|
||||
abc$foo=1
|
||||
abcdef=$foo
|
||||
|
||||
// expansion of undefined variables
|
||||
abc$toto=1
|
||||
abcdef=$tata
|
||||
|
||||
// opcode name which expands to invalid identifier
|
||||
$titi=1
|
||||
)SFZ";
|
||||
|
||||
void Application::init()
|
||||
{
|
||||
QMainWindow* window = new QMainWindow;
|
||||
_ui.setupUi(window);
|
||||
|
||||
_recheckTimer = new QTimer;
|
||||
_recheckTimer->setSingleShot(true);
|
||||
_recheckTimer->setInterval(200);
|
||||
connect(
|
||||
_recheckTimer, &QTimer::timeout,
|
||||
this, [this]() { runParseCheck(); });
|
||||
|
||||
_ui.sfzEdit->setFont(QFontDatabase::systemFont(QFontDatabase::FixedFont));
|
||||
_ui.sfzEdit->setPlainText(QString::fromLatin1(defaultSfzText));
|
||||
connect(
|
||||
_ui.sfzEdit, &QTextEdit::textChanged,
|
||||
this, [this]() { if (!_blockTextChanged) requestParseCheck(); });
|
||||
|
||||
_ui.messageTable->setColumnCount(3);
|
||||
_ui.messageTable->setHorizontalHeaderLabels(QStringList() << tr("Type") << tr("Line") << tr("Message"));
|
||||
_ui.messageTable->horizontalHeader()->setStretchLastSection(true);
|
||||
|
||||
_ui.splitter->setStretchFactor(0, 3);
|
||||
_ui.splitter->setStretchFactor(1, 1);
|
||||
|
||||
window->show();
|
||||
|
||||
_parser.setListener(this);
|
||||
requestParseCheck();
|
||||
}
|
||||
|
||||
void Application::requestParseCheck()
|
||||
{
|
||||
_recheckTimer->start();
|
||||
}
|
||||
|
||||
void Application::runParseCheck()
|
||||
{
|
||||
QByteArray code = _ui.sfzEdit->toPlainText().toLatin1();
|
||||
_blockTextChanged = true;
|
||||
_parser.parseString("/virtual.sfz", absl::string_view(code.data(), code.size()));
|
||||
_blockTextChanged = false;
|
||||
}
|
||||
|
||||
void Application::onParseBegin()
|
||||
{
|
||||
QTextDocument* doc = _ui.sfzEdit->document();
|
||||
QTextCursor cur(doc);
|
||||
cur.movePosition(QTextCursor::Start);
|
||||
cur.movePosition(QTextCursor::End, QTextCursor::KeepAnchor);
|
||||
QTextCharFormat cfmt;
|
||||
cur.setCharFormat(cfmt);
|
||||
|
||||
QTableWidget* t = _ui.messageTable;
|
||||
while (t->rowCount() > 0)
|
||||
t->removeRow(t->rowCount() - 1);
|
||||
}
|
||||
|
||||
void Application::onParseEnd()
|
||||
{
|
||||
}
|
||||
|
||||
void Application::onParseHeader(const sfz::SourceRange& range, const std::string& header)
|
||||
{
|
||||
(void)header;
|
||||
|
||||
QTextDocument* doc = _ui.sfzEdit->document();
|
||||
QTextCursor cur = selectSourceRange(doc, range);
|
||||
QTextCharFormat cfmt;
|
||||
cfmt.setForeground(QColor::fromRgb(0x4E9A06));
|
||||
cur.mergeCharFormat(cfmt);
|
||||
}
|
||||
|
||||
void Application::onParseOpcode(const sfz::SourceRange& rangeOpcode, const sfz::SourceRange& rangeValue, const std::string& name, const std::string& value)
|
||||
{
|
||||
(void)name;
|
||||
(void)value;
|
||||
|
||||
QTextDocument* doc = _ui.sfzEdit->document();
|
||||
QTextCursor curOpcode = selectSourceRange(doc, rangeOpcode);
|
||||
QTextCursor curValue = selectSourceRange(doc, rangeValue);
|
||||
QTextCharFormat cfmtOpcode;
|
||||
cfmtOpcode.setForeground(QColor::fromRgb(0x75507B));
|
||||
QTextCharFormat cfmtValue;
|
||||
cfmtValue.setForeground(QColor::fromRgb(0x3465A4));
|
||||
curOpcode.mergeCharFormat(cfmtOpcode);
|
||||
curValue.mergeCharFormat(cfmtValue);
|
||||
}
|
||||
|
||||
void Application::onParseError(const sfz::SourceRange& range, const std::string& message)
|
||||
{
|
||||
QTableWidget* t = _ui.messageTable;
|
||||
int row = t->rowCount();
|
||||
t->insertRow(row);
|
||||
|
||||
// QString path = QString::fromStdString(*range.start.filePath);
|
||||
// QString file = QFileInfo(path).fileName();
|
||||
t->setItem(row, 0, new QTableWidgetItem(tr("Error")));
|
||||
t->setItem(row, 1, new QTableWidgetItem(QString::number(range.start.lineNumber)));
|
||||
t->setItem(row, 2, new QTableWidgetItem(QString::fromStdString(message)));
|
||||
|
||||
QTextDocument* doc = _ui.sfzEdit->document();
|
||||
QTextCursor cur = selectSourceRange(doc, range);
|
||||
QTextCharFormat cfmt;
|
||||
cfmt.setUnderlineStyle(QTextCharFormat::SingleUnderline);
|
||||
cfmt.setUnderlineColor(Qt::red);
|
||||
cur.mergeCharFormat(cfmt);
|
||||
}
|
||||
|
||||
void Application::onParseWarning(const sfz::SourceRange& range, const std::string& message)
|
||||
{
|
||||
QTableWidget* t = _ui.messageTable;
|
||||
int row = t->rowCount();
|
||||
t->insertRow(row);
|
||||
|
||||
// QString path = QString::fromStdString(*range.start.filePath);
|
||||
// QString file = QFileInfo(path).fileName();
|
||||
t->setItem(row, 0, new QTableWidgetItem(tr("Warning")));
|
||||
t->setItem(row, 1, new QTableWidgetItem(QString::number(range.start.lineNumber)));
|
||||
t->setItem(row, 2, new QTableWidgetItem(QString::fromStdString(message)));
|
||||
|
||||
QTextDocument* doc = _ui.sfzEdit->document();
|
||||
QTextCursor cur = selectSourceRange(doc, range);
|
||||
QTextCharFormat cfmt;
|
||||
cfmt.setUnderlineStyle(QTextCharFormat::SingleUnderline);
|
||||
cfmt.setUnderlineColor(Qt::gray);
|
||||
cur.mergeCharFormat(cfmt);
|
||||
}
|
||||
|
||||
QTextCursor Application::selectSourceRange(QTextDocument* doc, const sfz::SourceRange& range)
|
||||
{
|
||||
QTextCursor cur(doc->findBlockByLineNumber(range.start.lineNumber));
|
||||
cur.movePosition(QTextCursor::NextCharacter, QTextCursor::MoveAnchor, range.start.columnNumber);
|
||||
cur.movePosition(QTextCursor::Down, QTextCursor::KeepAnchor, range.end.lineNumber - range.start.lineNumber);
|
||||
cur.movePosition(QTextCursor::StartOfLine, QTextCursor::KeepAnchor);
|
||||
cur.movePosition(QTextCursor::NextCharacter, QTextCursor::KeepAnchor, range.end.columnNumber);
|
||||
return cur;
|
||||
}
|
||||
|
||||
int main(int argc, char* argv[])
|
||||
{
|
||||
Application app(argc, argv);
|
||||
app.init();
|
||||
return app.exec();
|
||||
}
|
||||
41
tests/DemoParser.ui
Normal file
41
tests/DemoParser.ui
Normal file
|
|
@ -0,0 +1,41 @@
|
|||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<ui version="4.0">
|
||||
<class>MainWindow</class>
|
||||
<widget class="QMainWindow" name="MainWindow">
|
||||
<property name="geometry">
|
||||
<rect>
|
||||
<x>0</x>
|
||||
<y>0</y>
|
||||
<width>800</width>
|
||||
<height>800</height>
|
||||
</rect>
|
||||
</property>
|
||||
<property name="windowTitle">
|
||||
<string>Sfizz parser demo</string>
|
||||
</property>
|
||||
<widget class="QWidget" name="centralwidget">
|
||||
<layout class="QVBoxLayout" name="verticalLayout">
|
||||
<item>
|
||||
<widget class="QSplitter" name="splitter">
|
||||
<property name="orientation">
|
||||
<enum>Qt::Vertical</enum>
|
||||
</property>
|
||||
<widget class="QTextEdit" name="sfzEdit"/>
|
||||
<widget class="QGroupBox" name="groupBox">
|
||||
<property name="title">
|
||||
<string>Messages</string>
|
||||
</property>
|
||||
<layout class="QVBoxLayout" name="verticalLayout_2">
|
||||
<item>
|
||||
<widget class="QTableWidget" name="messageTable"/>
|
||||
</item>
|
||||
</layout>
|
||||
</widget>
|
||||
</widget>
|
||||
</item>
|
||||
</layout>
|
||||
</widget>
|
||||
</widget>
|
||||
<resources/>
|
||||
<connections/>
|
||||
</ui>
|
||||
|
|
@ -98,7 +98,8 @@ TEST_CASE("[Files] Subdir include Win")
|
|||
TEST_CASE("[Files] Recursive include (with include guard)")
|
||||
{
|
||||
sfz::Synth synth;
|
||||
synth.enableRecursiveIncludeGuard();
|
||||
sfz::Parser& parser = synth.getParser();
|
||||
parser.setRecursiveIncludeGuardEnabled(true);
|
||||
synth.loadSfzFile(fs::current_path() / "tests/TestFiles/Includes/root_recursive.sfz");
|
||||
REQUIRE(synth.getNumRegions() == 2);
|
||||
REQUIRE(synth.getRegionView(0)->sample == "dummy_recursive2.wav");
|
||||
|
|
@ -108,7 +109,8 @@ TEST_CASE("[Files] Recursive include (with include guard)")
|
|||
TEST_CASE("[Files] Include loops (with include guard)")
|
||||
{
|
||||
sfz::Synth synth;
|
||||
synth.enableRecursiveIncludeGuard();
|
||||
sfz::Parser& parser = synth.getParser();
|
||||
parser.setRecursiveIncludeGuardEnabled(true);
|
||||
synth.loadSfzFile(fs::current_path() / "tests/TestFiles/Includes/root_loop.sfz");
|
||||
REQUIRE(synth.getNumRegions() == 2);
|
||||
REQUIRE(synth.getRegionView(0)->sample == "dummy_loop2.wav");
|
||||
|
|
@ -502,8 +504,9 @@ TEST_CASE("[Files] Case sentitiveness")
|
|||
TEST_CASE("[Files] Empty file")
|
||||
{
|
||||
sfz::Synth synth;
|
||||
sfz::Parser& parser = synth.getParser();
|
||||
REQUIRE(!synth.loadSfzFile(""));
|
||||
REQUIRE(synth.getIncludedFiles().empty());
|
||||
REQUIRE(parser.getIncludedFiles().empty());
|
||||
REQUIRE(!synth.loadSfzFile({}));
|
||||
REQUIRE(synth.getIncludedFiles().empty());
|
||||
REQUIRE(parser.getIncludedFiles().empty());
|
||||
}
|
||||
|
|
|
|||
|
|
@ -5,8 +5,10 @@
|
|||
// If not, contact the sfizz maintainers at https://github.com/sfztools/sfizz
|
||||
|
||||
#include "sfizz/SfzHelpers.h"
|
||||
#include "sfizz/parser/Parser.h"
|
||||
#include <iostream>
|
||||
#include "catch2/catch.hpp"
|
||||
#include "absl/strings/string_view.h"
|
||||
using namespace Catch::literals;
|
||||
|
||||
void includeTest(const std::string& line, const std::string& fileName)
|
||||
|
|
@ -126,3 +128,323 @@ TEST_CASE("[Parsing] Member")
|
|||
memberTest("sample=..\\Samples\\SMD Cymbals Stereo (Samples)\\Hi-Hat (Samples)\\01 Hat Tight 1\\RR1\\09_Hat_Tight_Cnt_RR1.wav", "sample", "..\\Samples\\SMD Cymbals Stereo (Samples)\\Hi-Hat (Samples)\\01 Hat Tight 1\\RR1\\09_Hat_Tight_Cnt_RR1.wav");
|
||||
memberTest("sample=..\\G&S CW-Drum Kit-1\\SnareFX\\SNR-OFF-V08-CustomWorks-6x13.wav", "sample", "..\\G&S CW-Drum Kit-1\\SnareFX\\SNR-OFF-V08-CustomWorks-6x13.wav");
|
||||
}
|
||||
|
||||
// New parser
|
||||
|
||||
struct ParsingMocker: sfz::Parser::Listener
|
||||
{
|
||||
void onParseBegin() override
|
||||
{
|
||||
beginnings++;
|
||||
}
|
||||
void onParseEnd() override
|
||||
{
|
||||
endings++;
|
||||
}
|
||||
void onParseHeader(const sfz::SourceRange&, const std::string& header) override
|
||||
{
|
||||
headers.push_back(header);
|
||||
}
|
||||
void onParseOpcode(const sfz::SourceRange&, const sfz::SourceRange&, const std::string& name, const std::string& value) override
|
||||
{
|
||||
opcodes.emplace_back(name, value);
|
||||
}
|
||||
void onParseError(const sfz::SourceRange& range, const std::string&) override
|
||||
{
|
||||
errors.push_back(range);
|
||||
}
|
||||
void onParseWarning(const sfz::SourceRange& range, const std::string&) override
|
||||
{
|
||||
warnings.push_back(range);
|
||||
}
|
||||
// high-level parsing
|
||||
void onParseFullBlock(const std::string& header, const std::vector<sfz::Opcode>& opcodes) override
|
||||
{
|
||||
fullBlockHeaders.push_back(header);
|
||||
fullBlockMembers.push_back(opcodes);
|
||||
}
|
||||
|
||||
int beginnings { 0 };
|
||||
int endings { 0 };
|
||||
std::vector<sfz::SourceRange> errors;
|
||||
std::vector<sfz::SourceRange> warnings;
|
||||
std::vector<sfz::Opcode> opcodes;
|
||||
std::vector<std::string> headers;
|
||||
std::vector<std::string> fullBlockHeaders;
|
||||
std::vector<std::vector<sfz::Opcode>> fullBlockMembers;
|
||||
};
|
||||
|
||||
TEST_CASE("[Parsing] Empty")
|
||||
{
|
||||
sfz::Parser parser;
|
||||
ParsingMocker mock;
|
||||
parser.setListener(&mock);
|
||||
parser.parseString("/empty.sfz", "");
|
||||
REQUIRE(mock.beginnings == 1);
|
||||
REQUIRE(mock.endings == 1);
|
||||
REQUIRE(mock.errors.empty());
|
||||
REQUIRE(mock.warnings.empty());
|
||||
REQUIRE(mock.opcodes.empty());
|
||||
REQUIRE(mock.headers.empty());
|
||||
REQUIRE(mock.fullBlockHeaders.empty());
|
||||
REQUIRE(mock.fullBlockMembers.empty());
|
||||
}
|
||||
|
||||
static const absl::string_view emptySfz {
|
||||
R"(
|
||||
|
||||
)"
|
||||
};
|
||||
|
||||
TEST_CASE("[Parsing] Empty2")
|
||||
{
|
||||
sfz::Parser parser;
|
||||
ParsingMocker mock;
|
||||
parser.setListener(&mock);
|
||||
parser.parseString("/empty2.sfz", emptySfz);
|
||||
REQUIRE(mock.beginnings == 1);
|
||||
REQUIRE(mock.endings == 1);
|
||||
REQUIRE(mock.errors.empty());
|
||||
REQUIRE(mock.warnings.empty());
|
||||
REQUIRE(mock.opcodes.empty());
|
||||
REQUIRE(mock.headers.empty());
|
||||
REQUIRE(mock.fullBlockHeaders.empty());
|
||||
REQUIRE(mock.fullBlockMembers.empty());
|
||||
}
|
||||
|
||||
namespace sfz{
|
||||
bool operator==(const Opcode& lhs, const Opcode& rhs)
|
||||
{
|
||||
return (lhs.opcode == rhs.opcode) && (lhs.value == rhs.value);
|
||||
}
|
||||
}
|
||||
|
||||
TEST_CASE("[Parsing] Jpcima good region")
|
||||
{
|
||||
sfz::Parser parser;
|
||||
ParsingMocker mock;
|
||||
parser.setListener(&mock);
|
||||
parser.parseString("/goodRegion.sfz", R"(
|
||||
<region> sample=*silence key=69
|
||||
sample=My Directory/My Wave.wav // path with spaces and a comment
|
||||
sample=My Directory/My Wave.wav key=69 // path with spaces, and other opcode following
|
||||
)");
|
||||
std::vector<std::vector<sfz::Opcode>> expectedMembers = {
|
||||
{{"sample", "*silence"}, {"key", "69"}, {"sample", "My Directory/My Wave.wav"}, {"sample", "My Directory/My Wave.wav"}, {"key", "69"}}
|
||||
};
|
||||
std::vector<std::string> expectedHeaders = {
|
||||
"region"
|
||||
};
|
||||
std::vector<sfz::Opcode> expectedOpcodes;
|
||||
|
||||
for (auto& members: expectedMembers)
|
||||
for (auto& opcode: members)
|
||||
expectedOpcodes.push_back(opcode);
|
||||
|
||||
REQUIRE(mock.beginnings == 1);
|
||||
REQUIRE(mock.endings == 1);
|
||||
REQUIRE(mock.errors.empty());
|
||||
REQUIRE(mock.warnings.empty());
|
||||
REQUIRE(mock.opcodes == expectedOpcodes);
|
||||
REQUIRE(mock.headers == expectedHeaders);
|
||||
REQUIRE(mock.fullBlockHeaders == expectedHeaders);
|
||||
REQUIRE(mock.fullBlockMembers == expectedMembers);
|
||||
}
|
||||
|
||||
void memberTestNew(absl::string_view member, absl::string_view opcode, absl::string_view value)
|
||||
{
|
||||
sfz::Parser parser;
|
||||
ParsingMocker mock;
|
||||
parser.setListener(&mock);
|
||||
parser.parseString("/memberTestNew.sfz", absl::StrCat("<region> ", member));
|
||||
REQUIRE(mock.opcodes.size() == 1);
|
||||
REQUIRE(mock.headers.size() == 1);
|
||||
REQUIRE(mock.fullBlockHeaders.size() == 1);
|
||||
REQUIRE(mock.fullBlockMembers.size() == 1);
|
||||
REQUIRE(mock.headers[0] == "region");
|
||||
REQUIRE(mock.opcodes[0].opcode == opcode);
|
||||
REQUIRE(mock.opcodes[0].value == value);
|
||||
REQUIRE(mock.fullBlockHeaders[0] == "region");
|
||||
REQUIRE(mock.fullBlockMembers[0][0].opcode == opcode);
|
||||
REQUIRE(mock.fullBlockMembers[0][0].value == value);
|
||||
}
|
||||
|
||||
TEST_CASE("[Parsing] Members (new parser)")
|
||||
{
|
||||
memberTestNew("param=value", "param", "value");
|
||||
memberTestNew("param=113", "param", "113");
|
||||
memberTestNew("param1=value", "param1", "value");
|
||||
memberTestNew("param_1=value", "param_1", "value");
|
||||
memberTestNew("param_1=value", "param_1", "value");
|
||||
memberTestNew("ampeg_sustain_oncc74=-100", "ampeg_sustain_oncc74", "-100");
|
||||
memberTestNew("lorand=0.750", "lorand", "0.750");
|
||||
memberTestNew("sample=value", "sample", "value");
|
||||
memberTestNew("sample=value-()*", "sample", "value-()*");
|
||||
memberTestNew("sample=../sample.wav", "sample", "../sample.wav");
|
||||
memberTestNew("sample=..\\sample.wav", "sample", "..\\sample.wav");
|
||||
memberTestNew("sample=subdir\\subdir\\sample.wav", "sample", "subdir\\subdir\\sample.wav");
|
||||
memberTestNew("sample=subdir/subdir/sample.wav", "sample", "subdir/subdir/sample.wav");
|
||||
memberTestNew("sample=subdir_underscore\\sample.wav", "sample", "subdir_underscore\\sample.wav");
|
||||
memberTestNew("sample=subdir space\\sample.wav", "sample", "subdir space\\sample.wav");
|
||||
memberTestNew("sample=..\\Samples\\pizz\\a0_vl3_rr3.wav", "sample", "..\\Samples\\pizz\\a0_vl3_rr3.wav");
|
||||
memberTestNew("sample=..\\Samples\\SMD Cymbals Stereo (Samples)\\Hi-Hat (Samples)\\01 Hat Tight 1\\RR1\\09_Hat_Tight_Cnt_RR1.wav", "sample", "..\\Samples\\SMD Cymbals Stereo (Samples)\\Hi-Hat (Samples)\\01 Hat Tight 1\\RR1\\09_Hat_Tight_Cnt_RR1.wav");
|
||||
memberTestNew("sample=..\\G&S CW-Drum Kit-1\\SnareFX\\SNR-OFF-V08-CustomWorks-6x13.wav", "sample", "..\\G&S CW-Drum Kit-1\\SnareFX\\SNR-OFF-V08-CustomWorks-6x13.wav");
|
||||
}
|
||||
|
||||
TEST_CASE("[Parsing] bad headers")
|
||||
{
|
||||
sfz::Parser parser;
|
||||
ParsingMocker mock;
|
||||
parser.setListener(&mock);
|
||||
parser.parseString("/badHeaders.sfz",
|
||||
R"(<>
|
||||
<ab@cd> dummy_member=no
|
||||
)"
|
||||
);
|
||||
REQUIRE(mock.errors.size() == 2);
|
||||
REQUIRE(mock.errors[0].start.lineNumber == 0);
|
||||
REQUIRE(mock.errors[0].start.columnNumber == 0);
|
||||
REQUIRE(mock.errors[1].start.lineNumber == 1);
|
||||
REQUIRE(mock.errors[1].start.columnNumber == 0);
|
||||
REQUIRE(mock.errors[0].end.lineNumber == 0);
|
||||
REQUIRE(mock.errors[0].end.columnNumber == 2);
|
||||
REQUIRE(mock.errors[1].end.lineNumber == 1);
|
||||
REQUIRE(mock.errors[1].end.columnNumber == 7);
|
||||
}
|
||||
|
||||
void defineTestNew(const std::string& directive, const std::string& variable, const std::string& value)
|
||||
{
|
||||
sfz::Parser parser;
|
||||
ParsingMocker mock;
|
||||
parser.setListener(&mock);
|
||||
parser.parseString("/defineTestNew.sfz", directive);
|
||||
const auto defines = parser.getDefines();
|
||||
REQUIRE(defines.contains(variable));
|
||||
REQUIRE(defines.at(variable) == value);
|
||||
}
|
||||
|
||||
TEST_CASE("[Parsing] #define (new parser)")
|
||||
{
|
||||
defineTestNew("#define $empty ", "empty", "");
|
||||
defineTestNew("#define $number 1", "number", "1");
|
||||
defineTestNew("#define $letters QWERasdf", "letters", "QWERasdf");
|
||||
defineTestNew("#define $alphanum asr1t44", "alphanum", "asr1t44");
|
||||
defineTestNew("#define $whitespace asr1t44 ", "whitespace", "asr1t44");
|
||||
// The new parser does greedy matching
|
||||
// defineTestNew("#define $lazyMatching matched bfasd ", "lazyMatching", "matched");
|
||||
defineTestNew("#define $stircut -12", "stircut", "-12");
|
||||
defineTestNew("#define $_ht_under_score_ 3fd", "_ht_under_score_", "3fd");
|
||||
defineTestNew("#define $ht_under_score 3fd", "ht_under_score", "3fd");
|
||||
}
|
||||
|
||||
TEST_CASE("[Parsing] Malformed includes")
|
||||
{
|
||||
sfz::Parser parser;
|
||||
ParsingMocker mock;
|
||||
parser.setListener(&mock);
|
||||
parser.parseString("/malformedIncludes.sfz",
|
||||
R"(#include "MyFileWhichDoesNotExist1.sfz
|
||||
#include MyFileWhichDoesNotExist1.sfz)");
|
||||
REQUIRE(mock.errors.size() == 2);
|
||||
REQUIRE(mock.errors[0].start.lineNumber == 0);
|
||||
REQUIRE(mock.errors[0].start.columnNumber == 0);
|
||||
REQUIRE(mock.errors[1].start.lineNumber == 1);
|
||||
REQUIRE(mock.errors[1].start.columnNumber == 0);
|
||||
REQUIRE(mock.errors[0].end.lineNumber == 0);
|
||||
REQUIRE(mock.errors[0].end.columnNumber == 38);
|
||||
REQUIRE(mock.errors[1].end.lineNumber == 1);
|
||||
REQUIRE(mock.errors[1].end.columnNumber == 9);
|
||||
}
|
||||
|
||||
TEST_CASE("[Parsing] Headers (new parser)")
|
||||
{
|
||||
SECTION("Basic header match")
|
||||
{
|
||||
sfz::Parser parser;
|
||||
ParsingMocker mock;
|
||||
parser.setListener(&mock);
|
||||
parser.parseString("/headers.sfz", "<header>param1=value1 param2=value2 <next>");
|
||||
std::vector<std::vector<sfz::Opcode>> expectedMembers = {
|
||||
{{"param1", "value1"}, {"param2", "value2"}},
|
||||
{}
|
||||
};
|
||||
std::vector<std::string> expectedHeaders = {
|
||||
"header", "next"
|
||||
};
|
||||
std::vector<sfz::Opcode> expectedOpcodes;
|
||||
|
||||
for (auto& members: expectedMembers)
|
||||
for (auto& opcode: members)
|
||||
expectedOpcodes.push_back(opcode);
|
||||
|
||||
REQUIRE(mock.beginnings == 1);
|
||||
REQUIRE(mock.endings == 1);
|
||||
REQUIRE(mock.errors.empty());
|
||||
REQUIRE(mock.warnings.empty());
|
||||
REQUIRE(mock.opcodes == expectedOpcodes);
|
||||
REQUIRE(mock.headers == expectedHeaders);
|
||||
REQUIRE(mock.fullBlockHeaders == expectedHeaders);
|
||||
REQUIRE(mock.fullBlockMembers == expectedMembers);
|
||||
}
|
||||
|
||||
SECTION("EOL header match")
|
||||
{
|
||||
sfz::Parser parser;
|
||||
ParsingMocker mock;
|
||||
parser.setListener(&mock);
|
||||
parser.parseString("/eolHeaderMatch.sfz", "<header>param1=value1 param2=value2");
|
||||
std::vector<std::vector<sfz::Opcode>> expectedMembers = {
|
||||
{{"param1", "value1"}, {"param2", "value2"}}
|
||||
};
|
||||
std::vector<std::string> expectedHeaders = {
|
||||
"header"
|
||||
};
|
||||
std::vector<sfz::Opcode> expectedOpcodes;
|
||||
|
||||
for (auto& members: expectedMembers)
|
||||
for (auto& opcode: members)
|
||||
expectedOpcodes.push_back(opcode);
|
||||
|
||||
REQUIRE(mock.beginnings == 1);
|
||||
REQUIRE(mock.endings == 1);
|
||||
REQUIRE(mock.errors.empty());
|
||||
REQUIRE(mock.warnings.empty());
|
||||
REQUIRE(mock.opcodes == expectedOpcodes);
|
||||
REQUIRE(mock.headers == expectedHeaders);
|
||||
REQUIRE(mock.fullBlockHeaders == expectedHeaders);
|
||||
REQUIRE(mock.fullBlockMembers == expectedMembers);
|
||||
}
|
||||
}
|
||||
|
||||
TEST_CASE("[Parsing] External definitions")
|
||||
{
|
||||
sfz::Parser parser;
|
||||
ParsingMocker mock;
|
||||
parser.setListener(&mock);
|
||||
parser.addExternalDefinition("foo", "abc");
|
||||
parser.addExternalDefinition("bar", "123");
|
||||
parser.parseString("/externalDefinitions.sfz",
|
||||
R"(<header>
|
||||
param1=$foo
|
||||
param2=$bar)");
|
||||
std::vector<std::vector<sfz::Opcode>> expectedMembers = {
|
||||
{{"param1", "abc"}, {"param2", "123"}}
|
||||
};
|
||||
std::vector<std::string> expectedHeaders = {
|
||||
"header"
|
||||
};
|
||||
std::vector<sfz::Opcode> expectedOpcodes;
|
||||
|
||||
for (auto& members: expectedMembers)
|
||||
for (auto& opcode: members)
|
||||
expectedOpcodes.push_back(opcode);
|
||||
|
||||
REQUIRE(mock.beginnings == 1);
|
||||
REQUIRE(mock.endings == 1);
|
||||
REQUIRE(mock.errors.empty());
|
||||
REQUIRE(mock.warnings.empty());
|
||||
REQUIRE(mock.opcodes == expectedOpcodes);
|
||||
REQUIRE(mock.headers == expectedHeaders);
|
||||
REQUIRE(mock.fullBlockHeaders == expectedHeaders);
|
||||
REQUIRE(mock.fullBlockMembers == expectedMembers);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -16,7 +16,7 @@
|
|||
*/
|
||||
|
||||
#include "sfizz/Curve.h"
|
||||
#include "sfizz/Parser.h"
|
||||
#include "sfizz/parser/Parser.h"
|
||||
#include "absl/strings/numbers.h"
|
||||
#include "absl/types/span.h"
|
||||
#include <iostream>
|
||||
|
|
@ -34,16 +34,16 @@ static void usage()
|
|||
}
|
||||
|
||||
/**
|
||||
* @brief Parser which extracts the configuration of curves
|
||||
* @brief Parser listener which extracts the configuration of curves
|
||||
*/
|
||||
class CurveParser : public sfz::Parser {
|
||||
class CurveParserListener : public sfz::Parser::Listener {
|
||||
public:
|
||||
explicit CurveParser(sfz::CurveSet& curveSet)
|
||||
explicit CurveParserListener(sfz::CurveSet& curveSet)
|
||||
: curveSet(curveSet)
|
||||
{
|
||||
}
|
||||
|
||||
void callback(absl::string_view header, const std::vector<sfz::Opcode>& members) override
|
||||
void onParseFullBlock(const std::string& header, const std::vector<sfz::Opcode>& members) override
|
||||
{
|
||||
if (header == "curve")
|
||||
curveSet.addCurveFromHeader(members);
|
||||
|
|
@ -73,8 +73,11 @@ int main(int argc, char* argv[])
|
|||
sfz::CurveSet curveSet = sfz::CurveSet::createPredefined();
|
||||
|
||||
if (!filePath.empty()) {
|
||||
CurveParser parser(curveSet);
|
||||
if (!parser.loadSfzFile(filePath)) {
|
||||
sfz::Parser parser;
|
||||
CurveParserListener listener(curveSet);
|
||||
parser.setListener(&listener);
|
||||
parser.parseFile(filePath);
|
||||
if (parser.getErrorCount() > 0) {
|
||||
std::cerr << "Cannot load SFZ: " << filePath << "\n";
|
||||
return 1;
|
||||
}
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue