I have a C++ audio project that is a Score-Based Music System for offline computation and audio rendering. It is based on two distinct scores, that actually are text files, one that contains the DAG of the instrument and one with the parametric of that instrument. I implemented two distinct front-end compilers, namely the lexer and the parser to read them distinctly, and now I am writing a class that acts as linker between them while implementing a semantic analysis. The mechanism is quite simple but ATM works. I also unit tested it even if I am lacking a bit of integration tests. It also contains a back-end part for the audio processing management but actually I do not have any audio processors stable implemented.
Let me provide you also an example to clear the implementation and expectation.
~
// instrument.txt
AudioSystem as;
SinOsc osc;
WriteSF writer;
osc -> writer;
// parameters.txt
double frequency_ = 100;
AudioSystem::as T = 1000;
AudioSystem::as SR = 48000;
SinOsc::osc frequency = sqrt(frequency_);
WriteSF::writer filename = “testOsc.wav”;
~
Assume that in the Back-End I have an oscillator generator called SinOsc that is registered in a Factory class, that has a parameter called frequency that is a double, and a SoundFiles writer class that has the parameter filename that is the string of the filename to be created. AudioSystem is the container of the processing and contains the time of the total duration, in this case 1 sec, and the sampling rate as variable, both double. Note: in this moment the compiler accepts double or strings as type. All parameters are in the form: TypeSpecifier Identifier = Expression;
Let's focus on the linking phase and the semantic analysis. I list all the errors that I would find in this stage and I am able to categorize them in four distinct categories. Moreover, let's give a name to these classes: Vocabulary is the register in which during the parsing of the score lke instrument.txt each AudioProcessor plus AudioSystem registers their parameters, like frequency for SinOsc. ScoreSemAnalyzer is the class that receives the AST from the parser of the scores like parameters.txt . The categories are:
1. Errors that can find ScoreSemAnalyzer alone (like name collision, variable undeclared before the usage).
2. Errors that need communication from ScoreSemAnalyzer to Vocabulary (type checking: SSA needs to from Vocabulary the type of frequency of SinOsc).
3. Errors that need communication from Vocabulary to SSA (a parameter needs to be initialized but it is not found in the parameters' score).
4. Errors that need communication from SSA to ExpressionEvaluator (there is a list of function, like sin, osc or sqrt, that can be provided to the user and are effectively usable in ExpressionEvaluator, that is the responsible to evaluate such kind of expressions and it is called by Vocabulary to resolve them).
The first question is: which can be a convenient design strategy to accomplish this task? And related to this question, do you need the full list of errors and for each of them an example? Just to provide more context to this question: I already have a version of Vocabulary and some AudioProcessor, but no code about ScoreSemanticAnalyzer. Moreover, I am positive to the idea to refactor Vocabulary if a change can bring benefits to the project. For each, to avoid inspection technique, I am considering to use std::variant for the parameters. This should allow to avoid inspection. Moreover, let me know if you need the structure of the AST (I started from the one that gcc should implement for the case TypeSpecifier Identifier = Expression; ).
Additionally: a nice-to-have feature is to add range checking at "linking" time. For instance, the time of the duration must be positive. Of course, I can have this kind of check done at run time and implemented in each AudioProcessor, however I am considering that as general rule, if I have to throw an error because the parameter is not valid it is better to do as soon as possible. An idea that I am wondering is if I can create Type Specifier like uint that inside its constructor it checks if the number provided lays inside that range. For example, the time of the total duration should be a uint32_t: for this reason I can have a class uint such that at (copy) constructor verifies if the provided number is inside a given range. Of course, I can not define an unlimited number of data type but maybe all the ones for which you expect a positive number can be covered.