basic sine command and out directive.

main
bog 2024-01-30 19:09:57 +01:00
parent 2c8df5e493
commit 8959950c2c
33 changed files with 1417 additions and 23 deletions

View File

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

6
doc/grammar.bnf Normal file
View File

@ -0,0 +1,6 @@
PROG ::= INSTR*
INSTR ::= DIR | CMD
DIR ::= dir_ident CMD
CMD ::= osquare ident ARG* csquare
ARG ::= LITERAL | CMD
LITERAL ::= num

24
doc/guide/directives.rst Normal file
View File

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

View File

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

View File

@ -1,5 +1,6 @@
Installation
------------
The simplest way to install MuzGen is by using the Makefile at the project root directory.
.. code-block:: bash

View File

@ -1,4 +0,0 @@
Introduction
------------
MuzGen is a programming language for sound designers.

27
doc/guide/quickstart.rst Normal file
View File

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

41
doc/guide/signals.rst Normal file
View File

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

17
lib/AudioConf.cpp Normal file
View File

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

36
lib/AudioConf.hpp Normal file
View File

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

116
lib/AudioEngine.cpp Normal file
View File

@ -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> signal)
{
std::lock_guard<std::mutex> mtx(m_sig_mtx);
m_sig_queue.push_back(std::move(signal));
}
void AudioEngine::pop_signal()
{
std::lock_guard<std::mutex> 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<float> AudioEngine::next()
{
std::lock_guard<std::mutex> 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<AudioEngine*>(data);
float* out = static_cast<float*>(output);
size_t k = 0;
for (size_t i=0; i<frames_per_buffer; i++)
{
auto frame = engine->next();
for (float val: frame)
{
out[k++] = val;
}
}
return paContinue;
}
}

73
lib/AudioEngine.hpp Normal file
View File

@ -0,0 +1,73 @@
#ifndef muz_AUDIOENGINE_HPP
#define muz_AUDIOENGINE_HPP
#include <portaudio.h>
#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> 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<std::unique_ptr<Signal>> 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<float> 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

View File

@ -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 $<TARGET_FILE:muz-test>
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()

114
lib/Compiler.cpp Normal file
View File

@ -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<std::unique_ptr<Signal>>
Compiler::compile(std::shared_ptr<Node> node)
{
compile_node(node);
return std::move(m_outputs);
}
void Compiler::compile_node(std::shared_ptr<Node> node)
{
switch (node->type())
{
case NODE_NUM: {
float value = std::stof(node->value());
push(std::make_unique<Constant>(m_conf, value));
} break;
case NODE_CMD: {
std::string name = node->child(0)->value();
for (size_t i=1; i<node->size(); i++)
{
compile_node(node->child(i));
}
if (name == "sine")
{
check_cmd_arity(*node, 2);
auto one = pop();
auto signal = std::make_unique<Sine>(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; i<node->size(); i++)
{
compile_node(node->child(i));
}
break;
}
}
void Compiler::push(std::unique_ptr<Signal> signal)
{
m_signals.push_back(std::move(signal));
}
std::unique_ptr<Signal> 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)
+ ">."};
}
}
}

42
lib/Compiler.hpp Normal file
View File

@ -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<std::unique_ptr<Signal>> compile(std::shared_ptr<Node> node);
void compile_node(std::shared_ptr<Node> node);
private:
AudioConf const& m_conf;
std::vector<std::unique_ptr<Signal>> m_signals;
std::vector<std::unique_ptr<Signal>> m_outputs;
// signal stack
// ------------
void push(std::unique_ptr<Signal> signal);
std::unique_ptr<Signal> pop();
void check_cmd_arity(Node const& node, int arity);
};
}
#endif

26
lib/Constant.cpp Normal file
View File

@ -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<float> Constant::next() /*override*/
{
std::vector<float> out;
for (int i=0; i<m_conf.channels(); i++)
{
out.push_back(m_value);
}
return out;
}
}

25
lib/Constant.hpp Normal file
View File

@ -0,0 +1,25 @@
#ifndef muz_CONSTANT_HPP
#define muz_CONSTANT_HPP
#include "Signal.hpp"
#include "AudioConf.hpp"
namespace muz
{
/**
* A constant signal mainly used as input for more complex signals.
**/
class Constant: public Signal
{
public:
explicit Constant(AudioConf const& conf, float value=0.0f);
virtual ~Constant();
std::vector<float> next() override;
private:
AudioConf m_conf;
float m_value;
};
}
#endif

215
lib/Lexer.cpp Normal file
View File

@ -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<std::shared_ptr<Node>> Lexer::all()
{
std::vector<std::shared_ptr<Node>> res;
while (true)
{
auto tok = next();
if (tok)
{
res.push_back(tok);
}
else
{
return res;
}
}
return res;
}
std::shared_ptr<Node> 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<Node>
{
auto f = std::bind(fn, this, std::placeholders::_1);
if (tok_info && f(tok_info->value))
{
auto node = std::make_shared<Node>(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>(NODE_OSQUARE);
m_cursor = tok_info->position;
return node;
}
if (tok_info && tok_info->value == "]")
{
auto node = std::make_shared<Node>(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<TokenInfo> 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 == '@';
});
}
}

43
lib/Lexer.hpp Normal file
View File

@ -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<std::shared_ptr<Node>> all();
std::shared_ptr<Node> next();
private:
std::string m_source;
size_t m_cursor = 0;
std::vector<char> m_seps;
std::optional<TokenInfo> 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

56
lib/Node.cpp Normal file
View File

@ -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<Node> child)
{
m_children.push_back(child);
}
std::shared_ptr<Node> 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();
}
}

47
lib/Node.hpp Normal file
View File

@ -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<Node> child);
std::shared_ptr<Node> 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<std::shared_ptr<Node>> m_children;
};
}
#endif

122
lib/Parser.cpp Normal file
View File

@ -0,0 +1,122 @@
#include "Parser.hpp"
#include "Node.hpp"
namespace muz
{
/*explicit*/ Parser::Parser()
{
}
/*virtual*/ Parser::~Parser()
{
}
std::shared_ptr<Node> Parser::parse(Lexer& lexer)
{
m_tokens = lexer.all();
m_cursor = 0;
return parse_prog();
}
std::shared_ptr<Node> Parser::consume(std::optional<NodeType> 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<Node> Parser::parse_prog()
{
auto node = std::make_shared<Node>(NODE_PROG);
while (m_cursor < m_tokens.size())
{
node->add_child(parse_instr());
}
return node;
}
std::shared_ptr<Node> Parser::parse_instr()
{
if (next_is(NODE_DIR_IDENT))
{
return parse_dir();
}
return parse_cmd();
}
std::shared_ptr<Node> Parser::parse_dir()
{
auto node = std::make_shared<Node>(NODE_DIR);
node->add_child(consume(NODE_DIR_IDENT));
node->add_child(parse_cmd());
return node;
}
std::shared_ptr<Node> Parser::parse_cmd()
{
consume(NODE_OSQUARE);
auto node = std::make_shared<Node>(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<Node> Parser::parse_arg()
{
if (next_is(NODE_OSQUARE))
{
return parse_cmd();
}
return parse_literal();
}
std::shared_ptr<Node> Parser::parse_literal()
{
return consume(NODE_NUM);
}
}

42
lib/Parser.hpp Normal file
View File

@ -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<Node> parse(Lexer& lexer);
private:
std::vector<std::shared_ptr<Node>> m_tokens;
size_t m_cursor = 0;
std::shared_ptr<Node> consume(std::optional<NodeType> type=std::nullopt);
NodeType peek(size_t lookahead=0) const;
bool next_is(NodeType type, size_t lookahead=0) const;
std::shared_ptr<Node> parse_prog();
std::shared_ptr<Node> parse_instr();
std::shared_ptr<Node> parse_dir();
std::shared_ptr<Node> parse_cmd();
std::shared_ptr<Node> parse_arg();
std::shared_ptr<Node> parse_literal();
};
}
#endif

View File

@ -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<float> of size N for a sample of N channels or an empty vector at end.
*
**/
virtual std::vector<float> next() = 0;
private:
};
}

49
lib/Sine.cpp Normal file
View File

@ -0,0 +1,49 @@
#include "Sine.hpp"
namespace muz
{
/*explicit*/ Sine::Sine(AudioConf const& conf,
std::unique_ptr<Signal> freq,
std::unique_ptr<Signal> amplitude)
: m_conf { conf }
, m_freq { std::move(freq) }
, m_amplitude { std::move(amplitude) }
{
for (size_t i=0; i<static_cast<size_t>(m_conf.channels()); i++)
{
m_phases.push_back(0.0f);
}
}
/*virtual*/ Sine::~Sine()
{
}
std::vector<float> Sine::next() /*override*/
{
assert(m_freq);
assert(m_amplitude);
std::vector<float> 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<static_cast<size_t>(m_conf.channels()); i++)
{
float const value = amps[i] * std::sin(m_phases[i]);
m_phases[i] += 2 * M_PI * freqs[i]
/ static_cast<float>(m_conf.samplerate());
out.push_back(value);
}
return out;
}
}

30
lib/Sine.hpp Normal file
View File

@ -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<Signal> freq,
std::unique_ptr<Signal> amplitude);
virtual ~Sine();
std::vector<float> next() override;
private:
AudioConf const& m_conf;
std::unique_ptr<Signal> m_freq;
std::unique_ptr<Signal> m_amplitude;
std::vector<float> m_phases;
};
}
#endif

View File

@ -1,7 +1,33 @@
#ifndef muz_COMMONS_HPP
#define muz_COMMONS_HPP
#include <cassert>
#include <mutex>
#include <algorithm>
#include <functional>
#include <iostream>
#include <sstream>
#include <string>
#include <cstring>
#include <optional>
#include <stdexcept>
#include <vector>
#include <memory>
#include <cmath>
#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

View File

@ -1,8 +1,48 @@
#include "lib/AudioConf.hpp"
#include <iostream>
#include <fstream>
#include <sstream>
#include <lib/commons.hpp>
#include <lib/Sine.hpp>
#include <lib/Constant.hpp>
#include <lib/AudioEngine.hpp>
#include <lib/Lexer.hpp>
#include <lib/Parser.hpp>
#include <lib/Compiler.hpp>
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;
}

28
tests/AudioConf.cpp Normal file
View File

@ -0,0 +1,28 @@
#include <catch2/catch.hpp>
#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());
}

View File

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

62
tests/Lexer.cpp Normal file
View File

@ -0,0 +1,62 @@
#include <catch2/catch.hpp>
#include <lib/Lexer.hpp>
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));
}

57
tests/Parser.cpp Normal file
View File

@ -0,0 +1,57 @@
#include <catch2/catch.hpp>
#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]");
}

View File

@ -1,15 +0,0 @@
#include <catch2/catch.hpp>
class trivialTest
{
public:
explicit trivialTest() {}
virtual ~trivialTest() {}
protected:
};
TEST_CASE_METHOD(trivialTest, "trivial_test")
{
REQUIRE(true);
}