我正在使用C ++开发VST插件。该插件将允许用户输入数学表达式,然后每秒运行44100次以产生声音。我不熟悉这样的实时内容,也解释用户输入的表达式。
问题在于我找不到一种方法来评估可以快速运行的用户定义函数。我最好的尝试是在输入时将用户输入的表达式转换为RPN,然后使用函数评估RPN表达式以生成音频。我实现了RPN评估功能并对RPN表达式进行了硬编码以对其进行测试。虽然它似乎正确评估,但它似乎没有做得足够快。
除了几个RPN表达式之外,这是我的评估函数:
#include <string>
#include <stack>
#include <deque>
/*
* an RPN expression is stored as a deque of strings
*
* each string is either an operator, a number, or the single variable t
*
* the deque is read from front to back
*/
std::deque<std::string> simpleCase, complexCase;
//simple expression, just the variable t
simpleCase.push_back("t");
//more complex expression, t*(42&(t>>11))
complexCase.push_back("t");
complexCase.push_back("42");
complexCase.push_back("t");
complexCase.push_back("11");
complexCase.push_back(">>");
complexCase.push_back("&");
complexCase.push_back("*");
/*
* The evalRPN function takes an RPN deque, plugs in a supplied t,
* and evaluates it.
*
* The idea is that t increases continually, and that the integer overflow
* causes the output to oscillate between 0 and 255.
*
* t is a double, but I convert it to a uint32_t.
*
* Allowed operators: bitwise logic (&, |, ^), bitshifts (<<, >>),
* and math (+, -, *, /, %)
*
* Allowed vars: t
*
* Supplied numbers are converted from string to char arrays then to an int
*
* This also assumes the RPN is not ill-formatted.
*/
uint8_t evalRPN(std::deque<std::string> rpnExpr, double tVal)
{
std::stack<uint8_t> numberStack;
std::string token;
while(rpnExpr.size() > 0)
{
token = rpnExpr.front();
rpnExpr.pop_front();
if(token.find_first_not_of("0123456789") == std::string::npos)
{
//if token is a number
numberStack.push((uint8_t)atoi(token.c_str()));
}
else if (token == "t")
{
numberStack.push((uint8_t)tVal);
}
else
{
uint8_t last = numberStack.top();
numberStack.pop();
uint8_t first = numberStack.top();
numberStack.pop();
if(token == "^")
{
numberStack.push(first ^ last);
}
else if (token == "&")
{
numberStack.push(first & last);
}
else if (token == "|")
{
numberStack.push(first | last);
}
else if (token == "<<")
{
numberStack.push(first >> last);
}
else if (token == ">>")
{
numberStack.push(first >> last);
}
else if (token == "+")
{
numberStack.push(first + last);
}
else if (token == "-")
{
numberStack.push(first - last);
}
else if (token == "*")
{
numberStack.push(first * last);
}
else if (token == "/")
{
numberStack.push(first / last);
}
else if (token == "%")
{
numberStack.push(first % last);
}
}
}
//assume one left in numberStack
return(numberStack.top());
}
我在RPN处理中是否可以进行任何优化以使其可以足够快地运行?或者,是否有另一种处理RPN计算的方法更有效?
此外,还有另一种方法是C ++兼容,用于输入用户输入的表示标准数学表达式的字符串,然后以足够快的速度运行该表达式,以便在1/44100秒内完成?
答案 0 :(得分:1)
这是一个很好的问题。
将你的表达式编译成RPN是一个好的开始,实际上它看起来像我的代码应该能够每秒执行超过88K的表达式,除非它们很长。
但是,你可以毫不费力地做得更好。
我会创建一个这样的界面:
class Expression
{
public:
virtual uint32_t eval(uint32_t tVal) = 0;
};
然后,您将表达式编译为此接口的实现。
您可以拥有常量的实现:
class ConstExpression : public Expression
{
private:
uint32_t m_constVal;
public:
// ...
uint32_t eval(uint32_t tVal)
{
return m_constVal;
}
};
......引用t
class RefExpression : public Expression
{
public:
// ...
uint32_t eval(uint32_t tVal)
{
return m_tVal;
}
};
...以及二元运算符的实现
class AddExpression : public Expression
{
private:
auto_ptr<Expression> m_left;
auto_ptr<Expression> m_right;
public:
// ...
uint32_t eval(uint32_t tVal)
{
return m_left->eval(tVal) + m_right->eval(tVal);
}
};
...也许你想做一些模板魔法,以避免必须手工编写如此多的操作符类。
无论如何,在将表达式编译成表达式之后,您可以像表达式&gt; eval(t)一样评估它,并且所有代码都以合理有效的方式执行,无需解析,字符串比较,堆栈操作等。
答案 1 :(得分:0)
如何实时生成代码,编译它然后用作二进制库函数?我相信它会更快地运作。
答案 2 :(得分:0)
你不必。如果要构建vst,则必须使用vst库。主机daw将自动调用更新功能。
答案 3 :(得分:0)
感谢您的所有建议!我已经实施了一个我非常满意的解决方案。虽然这里没有任何答案直接告诉我这种方法,但他们确实激励了我。
我决定将用户输入的中缀表达式解析为波兰表示法(前缀),然后解析为二叉树。我专门为此实现了一个TreeNode结构,它本质上是一个双向链表,但有2个子节点而不是1.每个TreeNode可以设置为变量,固定数字或运算符。如果将其设置为运算符,则它的opFunc函数指针成员将设置为执行该运算符的预定义函数。
每个节点都有一个evaluate(tVal)成员函数。如果它是固定数字,则只返回该数字。如果它是一个变量,它只返回tVal。如果它是一个运算符,它返回opFunc(first-&gt; evaluate(tVal),last-&gt; evaluate(tVal)),它递归地计算它下面的两个节点,然后执行它的运算符在他们身上。
我还实现了一个ExpressionTree类,它管理树并将用户输入解析为树。它有公共成员用于递归地销毁树,从用户输入字符串构建树,以及评估树(它只需要评估根节点)。
因为它在一个简单的递归树中使用函数指针,所以即使公式变得非常大,评估ExpressionTree的速度也非常快。基本上,在评估函数时以及输入公式时,我已经或多或少地移动了尽可能多的处理。
下面是我为表达式树写的代码。请注意,只存在非常有限的运算符,但是通过将它们包含在优先级映射中,为它们添加函数以及在setOp中包含一个开关,可以很容易地添加新的二元运算符。也只允许一个变量,这是我的应用程序将使用的所有变量。
此时也没有执行错误检查,因此无效的运算符,空格或不匹配的括号将导致ATM的未定义行为。
另请注意,表达式将作为uint32_t处理,并在最后变为uint8_t,就像我的应用程序所要求的那样。当我完成这个项目时,我可以将其概括为一个库并发布它。
TreeExpressions.hpp:
#include <cstdint>
#include <string>
#include <map>
#include <stack>
#include <vector>
#include <algorithm>
struct TreeNode {
private:
bool isOp = false;
bool isVar = false;
uint32_t value = 0;
uint32_t(*opFunc)(uint32_t, uint32_t) = NULL;
static inline uint32_t xorOp(uint32_t a, uint32_t b) { return(a ^ b); };
static inline uint32_t andOp(uint32_t a, uint32_t b) { return(a & b); };
static inline uint32_t orOp(uint32_t a, uint32_t b) { return(a | b); };
static inline uint32_t lshiftOp(uint32_t a, uint32_t b) { return(a << b); };
static inline uint32_t rshiftOp(uint32_t a, uint32_t b) { return(a >> b); };
static inline uint32_t addOp(uint32_t a, uint32_t b) { return(a + b); };
static inline uint32_t subOp(uint32_t a, uint32_t b) { return(a - b); };
static inline uint32_t multOp(uint32_t a, uint32_t b) { return(a * b); };
static inline uint32_t divOp(uint32_t a, uint32_t b) { return(a / b); };
static inline uint32_t modOp(uint32_t a, uint32_t b) { return(a % b); };
public:
TreeNode *first = NULL;
TreeNode *last = NULL;
TreeNode *parent = NULL;
uint32_t evaluate(uint32_t tVal);
void setOp(std::string op);
void setVal(uint32_t val);
void setVar();
};
class ExpressionTree
{
private:
std::map<std::string, int> precedence;
TreeNode *treeRoot;
TreeNode *insertNode(TreeNode *leaf);
void destroyTree(TreeNode *leaf);
public:
ExpressionTree();
~ExpressionTree();
void destroyTree();
bool build(std::string formulaStr);
uint8_t evaluate(uint32_t tVal);
};
TreeExpressions.cpp:
#include "TreeExpressions.h"
void TreeNode::setOp(std::string op)
{
isVar = false;
isOp = true;
if (op == "^")
{
opFunc = &xorOp;
}
else if (op == "&")
{
opFunc = &andOp;
}
else if (op == "|")
{
opFunc = &orOp;
}
else if (op == "<<")
{
opFunc = &lshiftOp;
}
else if (op == ">>")
{
opFunc = &rshiftOp;
}
else if (op == "+")
{
opFunc = &addOp;
}
else if (op == "-")
{
opFunc = &subOp;
}
else if (op == "*")
{
opFunc = &multOp;
}
else if (op == "/")
{
opFunc = &divOp;
}
else if (op == "%")
{
opFunc = &modOp;
}
}
void TreeNode::setVal(uint32_t val)
{
isVar = false;
isOp = false;
value = val;
}
void TreeNode::setVar()
{
isVar = true;
isOp = false;
}
uint32_t TreeNode::evaluate(uint32_t tVal)
{
if (isOp)
{
//if it's an op
return( opFunc( first->evaluate(tVal), last->evaluate(tVal) ) );
}
else if (isVar)
{
//if it's a var
return(tVal);
}
else
{
//if it's a number
return(value);
}
}
ExpressionTree::ExpressionTree()
{
treeRoot = NULL;
// http://en.cppreference.com/w/cpp/language/operator_precedence
precedence["*"] = 5;
precedence["/"] = 5;
precedence["%"] = 5;
precedence["+"] = 6;
precedence["-"] = 6;
precedence["<<"] = 7;
precedence[">>"] = 7;
precedence["&"] = 10;
precedence["^"] = 11;
precedence["|"] = 12;
}
ExpressionTree::~ExpressionTree()
{
destroyTree();
}
void ExpressionTree::destroyTree(TreeNode *leaf)
{
if (leaf != NULL)
{
destroyTree(leaf->first);
destroyTree(leaf->last);
delete leaf;
}
}
TreeNode *ExpressionTree::insertNode(TreeNode *leaf)
{
if (leaf->first == NULL)
{
leaf->first = new TreeNode;
leaf->first->parent = leaf;
return(leaf->first);
}
else
{
leaf->last = new TreeNode;
leaf->last->parent = leaf;
return(leaf->last);
}
}
void ExpressionTree::destroyTree()
{
destroyTree(treeRoot);
}
bool ExpressionTree::build(std::string formulaStr)
{
std::string::iterator stringIterator;
std::vector<std::string>::iterator stringVectorIterator;
std::vector<std::string> formulaTokens;
std::vector<std::string> pnTokens;
std::stack<std::string> stringStack;
std::string currentNumString = "";
std::string currentTokenString = "";
std::stack<TreeNode> nodeStack;
TreeNode *currentNode;
std::string currToken;
bool treeIsDone;
//tokenization
for (stringIterator = formulaStr.begin(); stringIterator != formulaStr.end(); stringIterator++)
{
std::string currCharString(1, *stringIterator);
currentTokenString.push_back(*stringIterator);
if ((precedence.find(currentTokenString) != precedence.end()) || (currentTokenString == "(") || (currentTokenString == ")"))
{
//if the current token string is found in the precedence list (or is a parentheses)
if (currentNumString != "")
{
formulaTokens.push_back(currentNumString);
currentNumString = "";
}
formulaTokens.push_back(currentTokenString);
currentTokenString = "";
}
else if (std::all_of(currentTokenString.begin(), currentTokenString.end(), ::isdigit))
{
//if the current token string is all digits
currentNumString.append(currentTokenString);
currentTokenString = "";
}
else if (currentTokenString == "t")
{
//if the current token string is the t variable
formulaTokens.push_back(currentTokenString);
currentTokenString = "";
}
}
//convert to polish notation
std::reverse(formulaTokens.begin(), formulaTokens.end());
stringStack.push(")");
for (stringVectorIterator = formulaTokens.begin(); stringVectorIterator != formulaTokens.end(); stringVectorIterator++)
{
currToken = *stringVectorIterator;
if ((precedence.find(currToken) == precedence.end()) && (currToken != "(") && (currToken != ")"))
{
pnTokens.push_back(currToken);
}
else if (currToken == ")")
{
stringStack.push(currToken);
}
else if (precedence.find(currToken) != precedence.end())
{
if (stringStack.size() > 0)
{
if (stringStack.top() != ")")
{
while (precedence[stringStack.top()] <= precedence[currToken])
{
if (stringStack.top() != ")")
{
pnTokens.push_back(stringStack.top());
stringStack.pop();
}
if (stringStack.size() <= 0)
{
break;
}
if (stringStack.top() == ")")
{
break;
}
}
}
}
stringStack.push(currToken);
}
else if (currToken == "(")
{
if (stringStack.size() > 0)
{
while (stringStack.top() != ")")
{
pnTokens.push_back(stringStack.top());
stringStack.pop();
if (stringStack.size() <= 0)
{
break;
}
}
stringStack.pop();
}
}
}
while (stringStack.size() > 0)
{
if (stringStack.top() != ")")
{
pnTokens.push_back(stringStack.top());
}
stringStack.pop();
}
std::reverse(pnTokens.begin(), pnTokens.end());
//if it's gotten this far, the formula was valid
//destroy the current tree to make room
destroyTree();
//parse polish notation into tree
treeRoot = new TreeNode;
currentNode = treeRoot;
treeIsDone = false;
for (stringVectorIterator = pnTokens.begin(); stringVectorIterator != pnTokens.end(); stringVectorIterator++)
{
currToken = *stringVectorIterator;
if (precedence.find(currToken) != precedence.end())
{
//if the token is an operator
currentNode->setOp(currToken);
currentNode = insertNode(currentNode);
}
else
{
//if it's a number or a variable
if (currentNode->first != NULL)
{
//if the current node has it's first branch initialized
while (currentNode->last != NULL)
{
//while the last branch is initialized
currentNode = currentNode->parent;
}
currentNode = insertNode(currentNode);
}
if (std::all_of(currToken.begin(), currToken.end(), ::isdigit))
{
//if it's a number
currentNode->setVal((uint32_t)atoi(currToken.c_str()));
}
else
{
//if it's something else, a variable
currentNode->setVar();
}
if (currentNode != treeRoot)
{
currentNode = currentNode->parent;
}
//since we just moved up, we know at least the first branch is used
//so only check the last
while (currentNode->last != NULL)
{
//if the last node is not free
if (currentNode == treeRoot)
{
//if we're at the root, and it's totally populated
treeIsDone = true;
break;
}
currentNode = currentNode->parent;
}
if (!treeIsDone)
{
currentNode = insertNode(currentNode);
}
}
}
return(true);
}
uint8_t ExpressionTree::evaluate(uint32_t tVal)
{
return((uint8_t)treeRoot->evaluate(tVal));
}