Merge pull request #130 from jpcima/new-parser

Parser rework
This commit is contained in:
Paul Ferrand 2020-03-28 16:13:25 +01:00 committed by GitHub
commit a17b97a1c8
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
19 changed files with 1614 additions and 35 deletions

View file

@ -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:";

View file

@ -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)

View file

@ -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");

View file

@ -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;

View file

@ -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)

View file

@ -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; }

View file

@ -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;

View file

@ -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
View 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
View 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

View 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

View 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"

View 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

View file

@ -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
View 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
View 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>

View file

@ -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());
}

View file

@ -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);
}

View file

@ -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;
}