From 8959950c2c7ad5f9082113613d0fb0582237393f Mon Sep 17 00:00:00 2001 From: bog Date: Tue, 30 Jan 2024 19:09:57 +0100 Subject: [PATCH] :sparkles: basic sine command and out directive. --- Makefile | 4 +- doc/grammar.bnf | 6 ++ doc/guide/directives.rst | 24 +++++ doc/guide/index.rst | 7 +- doc/guide/install.rst | 1 + doc/guide/intro.rst | 4 - doc/guide/quickstart.rst | 27 +++++ doc/guide/signals.rst | 41 ++++++++ lib/AudioConf.cpp | 17 ++++ lib/AudioConf.hpp | 36 +++++++ lib/AudioEngine.cpp | 116 +++++++++++++++++++++ lib/AudioEngine.hpp | 73 +++++++++++++ lib/CMakeLists.txt | 24 +++++ lib/Compiler.cpp | 114 +++++++++++++++++++++ lib/Compiler.hpp | 42 ++++++++ lib/Constant.cpp | 26 +++++ lib/Constant.hpp | 25 +++++ lib/Lexer.cpp | 215 +++++++++++++++++++++++++++++++++++++++ lib/Lexer.hpp | 43 ++++++++ lib/Node.cpp | 56 ++++++++++ lib/Node.hpp | 47 +++++++++ lib/Parser.cpp | 122 ++++++++++++++++++++++ lib/Parser.hpp | 42 ++++++++ lib/Signal.hpp | 15 +++ lib/Sine.cpp | 49 +++++++++ lib/Sine.hpp | 30 ++++++ lib/commons.hpp | 26 +++++ src/main.cpp | 42 +++++++- tests/AudioConf.cpp | 28 +++++ tests/CMakeLists.txt | 4 +- tests/Lexer.cpp | 62 +++++++++++ tests/Parser.cpp | 57 +++++++++++ tests/trivial.cpp | 15 --- 33 files changed, 1417 insertions(+), 23 deletions(-) create mode 100644 doc/grammar.bnf create mode 100644 doc/guide/directives.rst delete mode 100644 doc/guide/intro.rst create mode 100644 doc/guide/quickstart.rst create mode 100644 doc/guide/signals.rst create mode 100644 lib/AudioConf.cpp create mode 100644 lib/AudioConf.hpp create mode 100644 lib/AudioEngine.cpp create mode 100644 lib/AudioEngine.hpp create mode 100644 lib/Compiler.cpp create mode 100644 lib/Compiler.hpp create mode 100644 lib/Constant.cpp create mode 100644 lib/Constant.hpp create mode 100644 lib/Lexer.cpp create mode 100644 lib/Lexer.hpp create mode 100644 lib/Node.cpp create mode 100644 lib/Node.hpp create mode 100644 lib/Parser.cpp create mode 100644 lib/Parser.hpp create mode 100644 lib/Sine.cpp create mode 100644 lib/Sine.hpp create mode 100644 tests/AudioConf.cpp create mode 100644 tests/Lexer.cpp create mode 100644 tests/Parser.cpp delete mode 100644 tests/trivial.cpp diff --git a/Makefile b/Makefile index 1aa4773..4569f8f 100644 --- a/Makefile +++ b/Makefile @@ -17,7 +17,9 @@ install: tests check: @cppcheck --language=c++ --enable=all -q lib src tests \ - --suppress=missingIncludeSystem + --suppress=missingIncludeSystem \ + --suppress=missingInclude \ + --suppress=unmatchedSuppression doc: mkdir -p build/doc/doxygen diff --git a/doc/grammar.bnf b/doc/grammar.bnf new file mode 100644 index 0000000..b1eff49 --- /dev/null +++ b/doc/grammar.bnf @@ -0,0 +1,6 @@ +PROG ::= INSTR* +INSTR ::= DIR | CMD +DIR ::= dir_ident CMD +CMD ::= osquare ident ARG* csquare +ARG ::= LITERAL | CMD +LITERAL ::= num diff --git a/doc/guide/directives.rst b/doc/guide/directives.rst new file mode 100644 index 0000000..4a46ae7 --- /dev/null +++ b/doc/guide/directives.rst @@ -0,0 +1,24 @@ +========== +Directives +========== + +A directive is a way to specify behaviors that are not directly +related to signal manipulations. + +@out +---- + +The ``@out`` signal uses the default audio output device to play a +given signal. For instance: + +.. code-block:: + + # doesnt make sound + [sine 230 1] + +does nothing. To hear our sine we have to use ``@out``. + +.. code-block:: + + # make sound + @out [sine 230 1] diff --git a/doc/guide/index.rst b/doc/guide/index.rst index d47b3c3..9e3ec76 100644 --- a/doc/guide/index.rst +++ b/doc/guide/index.rst @@ -1,7 +1,12 @@ Sound design with MuzGen ------------------------ +.. warning:: + MuzGen is not stable and has probably a lot's of bugs. + .. toctree:: - intro install + quickstart + signals + directives diff --git a/doc/guide/install.rst b/doc/guide/install.rst index bb67130..8e8aa7c 100644 --- a/doc/guide/install.rst +++ b/doc/guide/install.rst @@ -1,5 +1,6 @@ Installation ------------ +The simplest way to install MuzGen is by using the Makefile at the project root directory. .. code-block:: bash diff --git a/doc/guide/intro.rst b/doc/guide/intro.rst deleted file mode 100644 index 180154e..0000000 --- a/doc/guide/intro.rst +++ /dev/null @@ -1,4 +0,0 @@ -Introduction ------------- - -MuzGen is a programming language for sound designers. diff --git a/doc/guide/quickstart.rst b/doc/guide/quickstart.rst new file mode 100644 index 0000000..782c0c6 --- /dev/null +++ b/doc/guide/quickstart.rst @@ -0,0 +1,27 @@ +Quick Start +=========== + +Hello ! So you want to make some sound design ? MuzGen allows you to +generate some sound using a language named **MuzScript**. Ok, let's +start with a simple example. + +.. code-block:: bash + + @out [sine 440 1] + +Here, ``@out`` is a *directive* and ``sine`` is a *command*. + +Let start with commands. A command is kind of a function. It has a +name followed by its parameters if any. + +Here, the command ``sine`` takes two parameters: a frequency and an +amplitude. Like its name suggests, ``sine`` is a signal composed of +one sine wave with the given frequency and amplitude. + +.. note:: + The parameters ``440`` and ``1`` are not simple numbers. In fact, + their are signals too ! We call them constant signal. + +In order to hear our sine wave, we have to specify how we want it to +be played. That is what the directive ``@out`` does. It will play the +followed signal on the default audio device. diff --git a/doc/guide/signals.rst b/doc/guide/signals.rst new file mode 100644 index 0000000..48b82b0 --- /dev/null +++ b/doc/guide/signals.rst @@ -0,0 +1,41 @@ +======= +Signals +======= + +Signal Types +------------ + +MuzGen use different kind of signals for sound design. + + +Constant +^^^^^^^^ + +The simplest signal is the **constant signal**. It +returns the same frame everytime. It is used mostly as argument of +commands. In MuzScript, every numbers are constant signals. + +.. code-block:: + + # constant signal + 142 + + +Sine +^^^^ + +The sine wave signal is the fundamental signal in sound theory. +It takes two arguments: a frequency and an amplitude. +Mathematically we can define our sine using the following formula: + +.. math:: + + amplitude * sin(2 * pi * frequency / samplerate * time) + + +To generate a sine, we can use the ``sine`` command. + +.. code-block:: + + # sine signal with a frequency of 440 and an amplitude of 1 + [sine 440 1] diff --git a/lib/AudioConf.cpp b/lib/AudioConf.cpp new file mode 100644 index 0000000..ef0c807 --- /dev/null +++ b/lib/AudioConf.cpp @@ -0,0 +1,17 @@ +#include "AudioConf.hpp" + +namespace muz +{ + /*explicit*/ AudioConf::AudioConf(int channels, + unsigned long frames_per_buffer, + unsigned samplerate) + : m_channels { channels } + , m_frames_per_buffer { frames_per_buffer } + , m_samplerate { samplerate } + { + } + + /*virtual*/ AudioConf::~AudioConf() + { + } +} diff --git a/lib/AudioConf.hpp b/lib/AudioConf.hpp new file mode 100644 index 0000000..1b5a875 --- /dev/null +++ b/lib/AudioConf.hpp @@ -0,0 +1,36 @@ +#ifndef muz_AUDIOCONF_HPP +#define muz_AUDIOCONF_HPP + +namespace muz +{ + /** + * A configuration of an audio device. + * @param channels number of audio channel (2 by default). + * @param frames_per_buffer number of frames per buffer (256 by default). + * @param samplerate the audio samplerate, (44100 by default). + * @see AudioEngine + **/ + class AudioConf + { + public: + explicit AudioConf(int channels = 2, + unsigned long frames_per_buffer = 256, + unsigned samplerate = 44100); + + virtual ~AudioConf(); + + int channels() const { return m_channels; } + + unsigned long frames_per_buffer() const + { return m_frames_per_buffer; } + + unsigned samplerate() const { return m_samplerate; } + + private: + int m_channels = 0; + unsigned long m_frames_per_buffer; + unsigned m_samplerate; + }; +} + +#endif diff --git a/lib/AudioEngine.cpp b/lib/AudioEngine.cpp new file mode 100644 index 0000000..55bd698 --- /dev/null +++ b/lib/AudioEngine.cpp @@ -0,0 +1,116 @@ +#include "AudioEngine.hpp" +#include "Signal.hpp" + +namespace muz +{ + /*explicit*/ AudioEngine::AudioEngine(AudioConf const& conf) + : m_conf { conf } + { + check_error(Pa_Initialize()); + } + + /*virtual*/ AudioEngine::~AudioEngine() + { + check_error(Pa_Terminate()); + } + + void AudioEngine::init() + { + PaError err = Pa_OpenDefaultStream(&m_stream, + 0 /*input*/, + m_conf.channels() /*output*/, + paFloat32, + m_conf.samplerate(), + m_conf.frames_per_buffer() , + &AudioEngine::callback, + this); + check_error(err); + } + + void AudioEngine::run() + { + if (!m_stream) + { + throw audio_error {"audio engine not initialized"}; + } + + Pa_StartStream(m_stream); + + while (!m_sig_queue.empty()) + { + std::cin.get(); + pop_signal(); + } + + Pa_StopStream(m_stream); + } + + void AudioEngine::push_signal(std::unique_ptr signal) + { + std::lock_guard mtx(m_sig_mtx); + + m_sig_queue.push_back(std::move(signal)); + } + + void AudioEngine::pop_signal() + { + std::lock_guard mtx(m_sig_mtx); + + if (!m_sig_queue.empty()) + { + m_sig_queue.pop_back(); + } + } + + void AudioEngine::check_error(PaError err) + { + if (err != paNoError) + { + std::string msg = Pa_GetErrorText(err); + throw audio_error {"cannot initalize portaudio: " + msg}; + } + } + + std::vector AudioEngine::next() + { + std::lock_guard mtx(m_sig_mtx); + + if (m_sig_queue.empty()) + { + return {0.0f, 0.0f}; + } + + auto frame = m_sig_queue.back()->next(); + + if (frame.empty()) + { + pop_signal(); + return next(); + } + + return frame; + } + + /*static*/ int AudioEngine::callback(void const*, void* output, + unsigned long frames_per_buffer, + PaStreamCallbackTimeInfo const*, + PaStreamCallbackFlags, + void* data) + { + AudioEngine* engine = static_cast(data); + float* out = static_cast(output); + size_t k = 0; + + for (size_t i=0; inext(); + + for (float val: frame) + { + out[k++] = val; + } + } + + return paContinue; + } +} diff --git a/lib/AudioEngine.hpp b/lib/AudioEngine.hpp new file mode 100644 index 0000000..2f79c17 --- /dev/null +++ b/lib/AudioEngine.hpp @@ -0,0 +1,73 @@ +#ifndef muz_AUDIOENGINE_HPP +#define muz_AUDIOENGINE_HPP + +#include + +#include "commons.hpp" +#include "AudioConf.hpp" + +namespace muz +{ + class Signal; + + MUZ_ERROR(audio_error); + + /** + * Make sound from signals. + * @see Signal + **/ + class AudioEngine + { + public: + explicit AudioEngine(AudioConf const& conf); + virtual ~AudioEngine(); + + /** + * Initialize the engine opening audio device. + **/ + void init(); + + /** + * Run the engine, making sound. + **/ + void run(); + + /** + * Push a new signal in the signals queue. + */ + void push_signal(std::unique_ptr signal); + + /** + * Pop the current signal or does nothing if the queue is empty. + **/ + void pop_signal(); + + private: + AudioConf m_conf; + PaStream* m_stream = nullptr; + std::vector> m_sig_queue; + std::mutex m_sig_mtx; + + /** + * Throws an audio_error exception if err is an error. + **/ + void check_error(PaError err); + + /** + * Gives the next sample of the current signal. + * @see Signal + **/ + std::vector next(); + + /** + * Portaudio audio callback. + **/ + static int callback(void const* input, void* output, + unsigned long frames_per_buffer, + PaStreamCallbackTimeInfo const* time, + PaStreamCallbackFlags flags, + void* data); + }; +} + +#endif diff --git a/lib/CMakeLists.txt b/lib/CMakeLists.txt index 77bf381..5433c03 100644 --- a/lib/CMakeLists.txt +++ b/lib/CMakeLists.txt @@ -8,13 +8,36 @@ configure_file( ) add_library(muz-lib OBJECT + # Audio Signal.cpp + AudioEngine.cpp + AudioConf.cpp + Constant.cpp + Sine.cpp + + # Language + Node.cpp + Lexer.cpp + Parser.cpp + Compiler.cpp ) set_property(TARGET muz-lib PROPERTY CXX_STANDARD 17 ) + +find_package(PkgConfig REQUIRED) +pkg_check_modules(PortAudio portaudio-2.0 IMPORTED_TARGET REQUIRED) + +target_compile_options(muz-lib + PUBLIC -Wall -Wextra +) + +target_link_libraries(muz-lib + PUBLIC PkgConfig::PortAudio +) + if (CMAKE_BUILD_TYPE STREQUAL Debug) target_compile_options(muz-lib PRIVATE --coverage @@ -32,6 +55,7 @@ if (CMAKE_BUILD_TYPE STREQUAL Debug) COMMAND $ COMMAND ${LCOV_PATH} -d . --capture -o cov.info COMMAND ${LCOV_PATH} -r cov.info '/usr/include/*' -o cov.info + COMMAND ${LCOV_PATH} -r cov.info '*.hpp' -o cov.info COMMAND ${GENHTML_PATH} --legend -o cov_html cov.info ) endif() diff --git a/lib/Compiler.cpp b/lib/Compiler.cpp new file mode 100644 index 0000000..5514e16 --- /dev/null +++ b/lib/Compiler.cpp @@ -0,0 +1,114 @@ +#include "Compiler.hpp" +#include "Constant.hpp" +#include "Sine.hpp" + +namespace muz +{ + /*explicit*/ Compiler::Compiler(AudioConf const& conf) + : m_conf { conf } + { + } + + /*virtual*/ Compiler::~Compiler() + { + } + + std::vector> + Compiler::compile(std::shared_ptr node) + { + compile_node(node); + + return std::move(m_outputs); + } + + void Compiler::compile_node(std::shared_ptr node) + { + switch (node->type()) + { + case NODE_NUM: { + float value = std::stof(node->value()); + push(std::make_unique(m_conf, value)); + } break; + + case NODE_CMD: { + std::string name = node->child(0)->value(); + + for (size_t i=1; isize(); i++) + { + compile_node(node->child(i)); + } + + if (name == "sine") + { + check_cmd_arity(*node, 2); + + auto one = pop(); + auto signal = std::make_unique(m_conf, + std::move(pop()), + std::move(one)); + push(std::move(signal)); + } + else + { + throw compile_error { + std::string() + + "cannot compile unknown command '" + name + "'." + }; + } + } break; + + case NODE_DIR: { + std::string name = node->child(0)->value(); + + if (name == "@out") + { + compile_node(node->child(1)); + m_outputs.push_back(std::move(pop())); + } + else + { + throw compile_error { + std::string() + + "cannot compile unknown directive '" + name + "'." + }; + } + + } break; + + default: + for (size_t i=0; isize(); i++) + { + compile_node(node->child(i)); + } + break; + } + } + + void Compiler::push(std::unique_ptr signal) + { + m_signals.push_back(std::move(signal)); + } + + std::unique_ptr Compiler::pop() + { + auto signal = std::move(m_signals.back()); + m_signals.pop_back(); + return signal; + } + + void Compiler::check_cmd_arity(Node const& node, int arity) + { + if (node.size() - 1 != arity) + { + throw compile_error { + std::string() + + "arity mismatch for '" + + node.child(0)->value() + + "': expected <" + + std::to_string(arity) + + ">, got <" + + std::to_string(node.size() - 1) + + ">."}; + } + } +} diff --git a/lib/Compiler.hpp b/lib/Compiler.hpp new file mode 100644 index 0000000..ed6d5a6 --- /dev/null +++ b/lib/Compiler.hpp @@ -0,0 +1,42 @@ +#ifndef muz_COMPILER_HPP +#define muz_COMPILER_HPP + +#include "commons.hpp" +#include "Node.hpp" +#include "AudioConf.hpp" +#include "Signal.hpp" + +namespace muz +{ + MUZ_ERROR(compile_error); + + /** + * Create a signal given an abstract syntax tree. + * @see Signal + * @see Parser + * @see Node + **/ + class Compiler + { + public: + explicit Compiler(AudioConf const& conf); + virtual ~Compiler(); + + std::vector> compile(std::shared_ptr node); + void compile_node(std::shared_ptr node); + + private: + AudioConf const& m_conf; + std::vector> m_signals; + std::vector> m_outputs; + + // signal stack + // ------------ + void push(std::unique_ptr signal); + std::unique_ptr pop(); + + void check_cmd_arity(Node const& node, int arity); + }; +} + +#endif diff --git a/lib/Constant.cpp b/lib/Constant.cpp new file mode 100644 index 0000000..dd5b52f --- /dev/null +++ b/lib/Constant.cpp @@ -0,0 +1,26 @@ +#include "Constant.hpp" + +namespace muz +{ + /*explicit*/ Constant::Constant(AudioConf const& conf, float value) + : m_conf { conf } + , m_value { value } + { + } + + /*virtual*/ Constant::~Constant() + { + } + + std::vector Constant::next() /*override*/ + { + std::vector out; + + for (int i=0; i next() override; + private: + AudioConf m_conf; + float m_value; + }; +} + +#endif diff --git a/lib/Lexer.cpp b/lib/Lexer.cpp new file mode 100644 index 0000000..c292e23 --- /dev/null +++ b/lib/Lexer.cpp @@ -0,0 +1,215 @@ +#include "Lexer.hpp" +#include "Node.hpp" + +namespace muz +{ + /*explicit*/ Lexer::Lexer() + : m_seps { + {'[', ']'} + } + { + } + + /*virtual*/ Lexer::~Lexer() + { + } + + void Lexer::scan(std::string const& source) + { + m_source = source; + m_cursor = 0; + } + + std::vector> Lexer::all() + { + std::vector> res; + + while (true) + { + auto tok = next(); + + if (tok) + { + res.push_back(tok); + } + else + { + return res; + } + } + + return res; + } + + std::shared_ptr Lexer::next() + { + // consume spaces + while (m_cursor < m_source.size() + && isspace(m_source[m_cursor])) + { + m_cursor++; + } + + // check word + auto tok_info = next_word(); + + auto try_node = [&](NodeType type, + bool (Lexer::*fn)(std::string const&) const) + -> std::shared_ptr + { + auto f = std::bind(fn, this, std::placeholders::_1); + + if (tok_info && f(tok_info->value)) + { + auto node = std::make_shared(type, tok_info->value); + m_cursor = tok_info->position; + return node; + } + + return nullptr; + }; + + + if (tok_info && tok_info->value == "[") + { + auto node = std::make_shared(NODE_OSQUARE); + m_cursor = tok_info->position; + return node; + } + + if (tok_info && tok_info->value == "]") + { + auto node = std::make_shared(NODE_CSQUARE); + m_cursor = tok_info->position; + return node; + } + + if (auto res = try_node(NODE_NUM, &Lexer::is_num); + res) + { + return res; + } + + if (auto res = try_node(NODE_IDENT, &Lexer::is_ident); + res) + { + return res; + } + + if (auto res = try_node(NODE_DIR_IDENT, &Lexer::is_dir_ident); + res) + { + return res; + } + + return nullptr; + } + + std::optional Lexer::next_word() + { + size_t cursor = m_cursor; + std::string value; + + // consume spaces + while (cursor < m_source.size() + && isspace(m_source[cursor])) + { + cursor++; + } + + if (is_sep(cursor) && !isspace(m_source[cursor])) + { + value = std::string(1, m_source[cursor]); + cursor++; + } + else + { + // read next word + while (!is_sep(cursor)) + { + value += m_source[cursor]; + cursor++; + } + } + + if (value.size() > 0) + { + return TokenInfo { + cursor, + NODE_UNDEFINED, + value + }; + } + + return std::nullopt; + } + + bool Lexer::is_sep(size_t index) const + { + if (index >= m_source.size()) + { + return true; + } + + if (isspace(m_source[index])) + { + return true; + } + + return std::any_of(std::begin(m_seps), std::end(m_seps), [&](char c){ + return c == m_source[index]; + }); + } + + bool Lexer::is_num(std::string const& word) const + { + auto beg = std::begin(word); + + if (word.size() > 0 && word[0] == '-') + { + beg++; + } + + int count_dot = 0; + + return std::all_of(beg, std::end(word), [&](char c){ + + if (c == '.') + { + count_dot++; + } + + return isdigit(c) || c == '.'; + }) && count_dot <= 1; + } + + bool Lexer::is_ident(std::string const& word) const + { + if (word.size() == 0) + { + return false; + } + + if (word[0] == '@') { return false; } + if (isdigit(word[0])) { return false; } + + return std::all_of(std::begin(word), std::end(word), [&](char c){ + return isalnum(c) || c == '_'; + }); + } + + bool Lexer::is_dir_ident(std::string const& word) const + { + if (word.size() == 0) + { + return false; + } + + if (word[0] != '@') { return false; } + + return std::all_of(std::begin(word), std::end(word), [&](char c){ + return isalnum(c) || c == '_' || c == '@'; + }); + } + +} diff --git a/lib/Lexer.hpp b/lib/Lexer.hpp new file mode 100644 index 0000000..02dd99f --- /dev/null +++ b/lib/Lexer.hpp @@ -0,0 +1,43 @@ +#ifndef muz_LEXER_HPP +#define muz_LEXER_HPP + +#include "commons.hpp" +#include "Node.hpp" + +namespace muz +{ + struct TokenInfo + { + size_t position; + NodeType type; + std::string value; + }; + + /** + * Scan a text and gives corresponding tokens. + * @see Node + **/ + class Lexer + { + public: + explicit Lexer(); + virtual ~Lexer(); + + void scan(std::string const& source); + std::vector> all(); + std::shared_ptr next(); + + private: + std::string m_source; + size_t m_cursor = 0; + std::vector m_seps; + + std::optional next_word(); + bool is_sep(size_t index) const; + bool is_num(std::string const& word) const; + bool is_ident(std::string const& word) const; + bool is_dir_ident(std::string const& word) const; + }; +} + +#endif diff --git a/lib/Node.cpp b/lib/Node.cpp new file mode 100644 index 0000000..e17dee1 --- /dev/null +++ b/lib/Node.cpp @@ -0,0 +1,56 @@ +#include "Node.hpp" + +namespace muz +{ + /*explicit*/ Node::Node(NodeType type, + std::string const& value) + : m_type { type } + , m_value { value } + { + } + + /*virtual*/ Node::~Node() + { + } + + void Node::add_child(std::shared_ptr child) + { + m_children.push_back(child); + } + + std::shared_ptr Node::child(size_t index) const + { + if (index >= m_children.size()) + { + throw node_error {"cannot get node child: bad index"}; + } + + return m_children[index]; + } + + std::string Node::string() const + { + std::stringstream ss; + ss << NodeTypeStr[type()] + strlen("NODE_"); + + if (m_value.empty() == false) + { + ss << "[" << m_value << "]"; + } + + if (m_children.empty() == false) + { + std::string sep; + + ss << "("; + for (auto& c: m_children) + { + ss << sep << c->string(); + sep = ","; + } + ss << ")"; + } + + return ss.str(); + } +} diff --git a/lib/Node.hpp b/lib/Node.hpp new file mode 100644 index 0000000..39a2672 --- /dev/null +++ b/lib/Node.hpp @@ -0,0 +1,47 @@ +#ifndef muz_NODE_HPP +#define muz_NODE_HPP + +#include "commons.hpp" + +#define NODE_TYPE(G) \ + G(NODE_UNDEFINED), \ + G(NODE_NUM), G(NODE_IDENT), G(NODE_DIR_IDENT), \ + G(NODE_OSQUARE), G(NODE_CSQUARE), \ + G(NODE_PROG), G(NODE_DIR), G(NODE_CMD) + +namespace muz +{ + MUZ_ENUM(NodeType, NODE_TYPE); + MUZ_ERROR(node_error); + + /** + * Represents a node of the abstract syntax tree. + **/ + class Node + { + public: + explicit Node(NodeType type, + std::string const& value=""); + virtual ~Node(); + + // properties + // ---------- + inline NodeType type() const { return m_type; } + inline std::string value() const { return m_value; } + + // children + // -------- + void add_child(std::shared_ptr child); + std::shared_ptr child(size_t index) const; + inline size_t size() const { return m_children.size(); } + + std::string string() const; + + private: + NodeType m_type; + std::string m_value; + std::vector> m_children; + }; +} + +#endif diff --git a/lib/Parser.cpp b/lib/Parser.cpp new file mode 100644 index 0000000..6c9eea5 --- /dev/null +++ b/lib/Parser.cpp @@ -0,0 +1,122 @@ +#include "Parser.hpp" +#include "Node.hpp" + +namespace muz +{ + /*explicit*/ Parser::Parser() + { + } + + /*virtual*/ Parser::~Parser() + { + } + + std::shared_ptr Parser::parse(Lexer& lexer) + { + m_tokens = lexer.all(); + m_cursor = 0; + + return parse_prog(); + } + + std::shared_ptr Parser::consume(std::optional type) + { + if (m_cursor >= m_tokens.size()) + { + std::string ty_desired = NodeTypeStr[*type] + strlen("NODE_"); + throw syntax_error {"unexpected end: expected <" + + ty_desired + + ">, got nothing."}; + } + + auto node = m_tokens[m_cursor]; + + if (type && node->type() != *type) + { + std::string ty_got = NodeTypeStr[node->type()] + strlen("NODE_"); + std::string ty_desired = NodeTypeStr[*type] + strlen("NODE_"); + throw syntax_error {"expected <" + + ty_desired + + ">, got <" + + ty_got + ">."}; + } + + m_cursor++; + + return node; + } + + NodeType Parser::peek(size_t lookahead) const + { + return m_tokens[m_cursor + lookahead]->type(); + } + + bool Parser::next_is(NodeType type, size_t lookahead) const + { + if (m_cursor + lookahead >= m_tokens.size()) { return false; } + return peek(lookahead) == type; + } + + std::shared_ptr Parser::parse_prog() + { + auto node = std::make_shared(NODE_PROG); + + while (m_cursor < m_tokens.size()) + { + node->add_child(parse_instr()); + } + + return node; + } + + std::shared_ptr Parser::parse_instr() + { + if (next_is(NODE_DIR_IDENT)) + { + return parse_dir(); + } + + return parse_cmd(); + } + + std::shared_ptr Parser::parse_dir() + { + auto node = std::make_shared(NODE_DIR); + node->add_child(consume(NODE_DIR_IDENT)); + node->add_child(parse_cmd()); + + return node; + } + + std::shared_ptr Parser::parse_cmd() + { + consume(NODE_OSQUARE); + + auto node = std::make_shared(NODE_CMD); + node->add_child(consume(NODE_IDENT)); + + while (!next_is(NODE_CSQUARE)) + { + node->add_child(parse_arg()); + } + + consume(NODE_CSQUARE); + + return node; + } + + std::shared_ptr Parser::parse_arg() + { + if (next_is(NODE_OSQUARE)) + { + return parse_cmd(); + } + + return parse_literal(); + } + + std::shared_ptr Parser::parse_literal() + { + return consume(NODE_NUM); + } +} diff --git a/lib/Parser.hpp b/lib/Parser.hpp new file mode 100644 index 0000000..e4c6f12 --- /dev/null +++ b/lib/Parser.hpp @@ -0,0 +1,42 @@ +#ifndef muz_PARSER_HPP +#define muz_PARSER_HPP + +#include "commons.hpp" +#include "Lexer.hpp" + +namespace muz +{ + MUZ_ERROR(syntax_error); + + /** + * Build an AST given a token list. + * @see Node + * @see Lexer + **/ + class Parser + { + public: + explicit Parser(); + virtual ~Parser(); + + std::shared_ptr parse(Lexer& lexer); + + private: + std::vector> m_tokens; + size_t m_cursor = 0; + + std::shared_ptr consume(std::optional type=std::nullopt); + NodeType peek(size_t lookahead=0) const; + bool next_is(NodeType type, size_t lookahead=0) const; + + std::shared_ptr parse_prog(); + std::shared_ptr parse_instr(); + std::shared_ptr parse_dir(); + std::shared_ptr parse_cmd(); + std::shared_ptr parse_arg(); + std::shared_ptr parse_literal(); + + }; +} + +#endif diff --git a/lib/Signal.hpp b/lib/Signal.hpp index d08bdb3..7832658 100644 --- a/lib/Signal.hpp +++ b/lib/Signal.hpp @@ -1,14 +1,29 @@ #ifndef muz_SIGNAL_HPP #define muz_SIGNAL_HPP +#include "commons.hpp" + namespace muz { + MUZ_ERROR(signal_error); + + /** + * Audio signal interface. + * @see Sine + * @see Constant + **/ class Signal { public: explicit Signal(); virtual ~Signal(); + /** + * Get the next sample. + * @return std::vector of size N for a sample of N channels or an empty vector at end. + * + **/ + virtual std::vector next() = 0; private: }; } diff --git a/lib/Sine.cpp b/lib/Sine.cpp new file mode 100644 index 0000000..9a89b5e --- /dev/null +++ b/lib/Sine.cpp @@ -0,0 +1,49 @@ +#include "Sine.hpp" + +namespace muz +{ + /*explicit*/ Sine::Sine(AudioConf const& conf, + std::unique_ptr freq, + std::unique_ptr amplitude) + : m_conf { conf } + , m_freq { std::move(freq) } + , m_amplitude { std::move(amplitude) } + { + for (size_t i=0; i(m_conf.channels()); i++) + { + m_phases.push_back(0.0f); + } + } + + /*virtual*/ Sine::~Sine() + { + } + + std::vector Sine::next() /*override*/ + { + assert(m_freq); + assert(m_amplitude); + + std::vector out; + auto freqs = m_freq->next(); + auto amps = m_amplitude->next(); + + if (freqs.size() != amps.size() + || freqs.size() != m_phases.size()) + { + throw signal_error {"cannot generate sine: channel number mismatch"}; + } + + for (size_t i=0; i(m_conf.channels()); i++) + { + float const value = amps[i] * std::sin(m_phases[i]); + + m_phases[i] += 2 * M_PI * freqs[i] + / static_cast(m_conf.samplerate()); + + out.push_back(value); + } + + return out; + } +} diff --git a/lib/Sine.hpp b/lib/Sine.hpp new file mode 100644 index 0000000..ae71f13 --- /dev/null +++ b/lib/Sine.hpp @@ -0,0 +1,30 @@ +#ifndef muz_SINE_HPP +#define muz_SINE_HPP + +#include "commons.hpp" +#include "Signal.hpp" +#include "AudioConf.hpp" + +namespace muz +{ + /** + * Sinusoid signal with an amplitude and a frequency. + **/ + class Sine: public Signal + { + public: + explicit Sine(AudioConf const& conf, + std::unique_ptr freq, + std::unique_ptr amplitude); + virtual ~Sine(); + + std::vector next() override; + private: + AudioConf const& m_conf; + std::unique_ptr m_freq; + std::unique_ptr m_amplitude; + std::vector m_phases; + }; +} + +#endif diff --git a/lib/commons.hpp b/lib/commons.hpp index 0fd85b7..9bc3681 100644 --- a/lib/commons.hpp +++ b/lib/commons.hpp @@ -1,7 +1,33 @@ #ifndef muz_COMMONS_HPP #define muz_COMMONS_HPP +#include +#include +#include +#include +#include +#include #include +#include +#include +#include +#include +#include +#include + #include "conf.hpp" +#define MUZ_ERROR(NAME) \ + struct NAME : public std::runtime_error { \ + explicit NAME(std::string const& what): std::runtime_error(what) {} \ + } + +#define MUZ_ENUM_IDENT(X) X +#define MUZ_ENUM_STRING(X) #X + +#define MUZ_ENUM(Prefix, Macro) \ + enum Prefix {Macro(MUZ_ENUM_IDENT)}; \ + constexpr char const* Prefix ## Str [] = {Macro(MUZ_ENUM_STRING)}; + + #endif diff --git a/src/main.cpp b/src/main.cpp index 6434489..f4e9153 100644 --- a/src/main.cpp +++ b/src/main.cpp @@ -1,8 +1,48 @@ +#include "lib/AudioConf.hpp" #include +#include +#include + #include +#include +#include +#include + +#include +#include +#include int main(int argc, char** argv) { - std::cout << "muzgen " << MUZ_VERSION << std::endl; + muz::AudioConf conf; + + muz::AudioEngine engine {conf}; + + engine.init(); + + if (argc > 1) + { + std::ifstream file {argv[1]}; + std::stringstream ss; + ss << file.rdbuf(); + + muz::Lexer lexer; + lexer.scan(ss.str()); + + muz::Parser parser; + auto node = parser.parse(lexer); + + muz::Compiler compiler {conf}; + auto signals = compiler.compile(node); + + while (signals.size() > 0) + { + engine.push_signal(std::move(signals.back())); + signals.pop_back(); + } + } + + engine.run(); + return 0; } diff --git a/tests/AudioConf.cpp b/tests/AudioConf.cpp new file mode 100644 index 0000000..adfce9d --- /dev/null +++ b/tests/AudioConf.cpp @@ -0,0 +1,28 @@ +#include +#include "../lib/AudioConf.hpp" + +class AudioConfTest +{ +public: + explicit AudioConfTest() {} + virtual ~AudioConfTest() {} + +protected: +}; + +TEST_CASE_METHOD(AudioConfTest, "AudioConf_default") +{ + muz::AudioConf conf; + REQUIRE(2 == conf.channels()); + REQUIRE(256 == conf.frames_per_buffer()); + REQUIRE(44100 == conf.samplerate()); +} + +TEST_CASE_METHOD(AudioConfTest, "AudioConf_custom") +{ + muz::AudioConf conf {1, 128, 88200}; + + REQUIRE(1 == conf.channels()); + REQUIRE(128 == conf.frames_per_buffer()); + REQUIRE(88200 == conf.samplerate()); +} diff --git a/tests/CMakeLists.txt b/tests/CMakeLists.txt index c72c508..5525226 100644 --- a/tests/CMakeLists.txt +++ b/tests/CMakeLists.txt @@ -5,7 +5,9 @@ project(MuzGenTest) find_package(Catch2 REQUIRED) add_executable(muz-test - trivial.cpp + Lexer.cpp + Parser.cpp + AudioConf.cpp ) set_property(TARGET muz-test diff --git a/tests/Lexer.cpp b/tests/Lexer.cpp new file mode 100644 index 0000000..25bf0e9 --- /dev/null +++ b/tests/Lexer.cpp @@ -0,0 +1,62 @@ +#include +#include + +class LexerTest +{ +public: + explicit LexerTest() {} + virtual ~LexerTest() {} + +protected: +}; + +static std::string next_val(muz::Lexer& lexer) +{ + auto tok = lexer.next(); + + if (tok) + { + return tok->string(); + } + + return ""; +} + +TEST_CASE_METHOD(LexerTest, "Lexer_num") +{ + muz::Lexer lexer; + lexer.scan(" 34 2.9 -7 -3.14 .1 1."); + + REQUIRE("NUM[34]" == next_val(lexer)); + REQUIRE("NUM[2.9]" == next_val(lexer)); + REQUIRE("NUM[-7]" == next_val(lexer)); + REQUIRE("NUM[-3.14]" == next_val(lexer)); + REQUIRE("NUM[.1]" == next_val(lexer)); + REQUIRE("NUM[1.]" == next_val(lexer)); + + REQUIRE("" == next_val(lexer)); +} + +TEST_CASE_METHOD(LexerTest, "Lexer_ident") +{ + muz::Lexer lexer; + lexer.scan(" hello hello_world @hello"); + + REQUIRE("IDENT[hello]" == next_val(lexer)); + REQUIRE("IDENT[hello_world]" == next_val(lexer)); + REQUIRE("DIR_IDENT[@hello]" == next_val(lexer)); + + REQUIRE("" == next_val(lexer)); +} + +TEST_CASE_METHOD(LexerTest, "Lexer_commands") +{ + muz::Lexer lexer; + lexer.scan(" [[]"); + + REQUIRE("OSQUARE" == next_val(lexer)); + REQUIRE("OSQUARE" == next_val(lexer)); + REQUIRE("CSQUARE" == next_val(lexer)); + + REQUIRE("" == next_val(lexer)); +} diff --git a/tests/Parser.cpp b/tests/Parser.cpp new file mode 100644 index 0000000..eda8f0f --- /dev/null +++ b/tests/Parser.cpp @@ -0,0 +1,57 @@ +#include +#include "../lib/Parser.hpp" +#include "../lib/Lexer.hpp" + +class ParserTest +{ +public: + explicit ParserTest() {} + virtual ~ParserTest() {} + +protected: +}; + +static void test_parser(std::string const& oracle, + std::string const& source) +{ + muz::Lexer lexer; + lexer.scan(source); + + muz::Parser parser; + auto node = parser.parse(lexer); + REQUIRE(oracle == node->string()); +} + +static void test_parser_err(std::string const& source) +{ + muz::Lexer lexer; + lexer.scan(source); + + muz::Parser parser; + REQUIRE_THROWS_AS(parser.parse(lexer), muz::syntax_error); +} + +TEST_CASE_METHOD(ParserTest, "Parser_commands") +{ + test_parser_err("[hello"); + test_parser_err("hello]"); + test_parser_err("12"); + + test_parser("PROG(CMD(IDENT[hello]),CMD(IDENT[world]))", + "[hello] [world]"); + + test_parser("PROG(CMD(IDENT[hello_world]))", + "[hello_world]"); + + test_parser("PROG(CMD(IDENT[sine],NUM[440]))", + "[sine 440]"); + + test_parser("PROG(CMD(IDENT[sine],NUM[440],NUM[217]))", + "[sine 440 217]"); +} + +TEST_CASE_METHOD(ParserTest, "Parser_directives") +{ + test_parser("PROG(DIR(DIR_IDENT[@bim],CMD(IDENT[hello_world])))", + "@bim [hello_world]"); +} diff --git a/tests/trivial.cpp b/tests/trivial.cpp deleted file mode 100644 index 1a61b79..0000000 --- a/tests/trivial.cpp +++ /dev/null @@ -1,15 +0,0 @@ -#include - -class trivialTest -{ -public: - explicit trivialTest() {} - virtual ~trivialTest() {} - -protected: -}; - -TEST_CASE_METHOD(trivialTest, "trivial_test") -{ - REQUIRE(true); -}