ANKLANG Development Details
Technically, Anklang consists of a user interface front-end based on web technologies (HTML, DOM, CSS, JavaScript, Lit, SolidJS) and a synthesis engine backend written in C++.
ASE - Anklang synthesis engine
The ase/
subdirectory contains the C++ implementation of the AnklangSynthEngine
executable
which contains the core components for audio data processing and audio plugin handling.
It interfaces with the HTML DOM based user interface via an IPC layer with JSON messages that reflect the C++ API.
The synthesis engine can load various audio rendering plugins which are executed in audio rendering worker threads. The main synthesis engine thread coordinates synchronization and interafces between the engine and the UI via an IPC interface over a web-socket that uses remote method calls and event delivery marshalled as JSON messages.
Serialization
Building on Jsonipc, a small serialization framework provided by ase/serialize.hh is used to marshal values, structs, enums and classes to/from JSON. This is used to store preferences and project data. The intended usage is as follows:
std::string jsontext = Ase::json_stringify (somevalue);
bool success = Ase::json_parse (jsontext, somevalue);
// The JSON root will be of type 'object' if somevalue is a class instance
std::string s; // s contains:
s = json_stringify (true); // true
s = json_stringify (-0.17); // -0.17
s = json_stringify (32768); // 32768
s = json_stringify (Ase::Error::IO); // "Ase.Error.IO"
s = json_stringify (String ("STRing")); // "STRing"
s = json_stringify (ValueS ({ true, 5, "HI" })); // [true,5,"HI"]
s = json_stringify (ValueR ({ {"a", 1}, {"b", "B"} })); // {"a":1,"b":"B"}
In the above examples, Ase::Error::IO
can be serialized because it is registered as
Jsonipc::Enum<Ase::Error>
with its enum values. The same works for serializable
classes registered through Jsonipc::Serializable<SomeClass>
.
Serialization of "record" instances (classes with public data fields) are registered
as Jsonipc::Serializable<>
and are serialized as JSON objects.
Serialization of class instances (with methods) depends on the Scope & InstanceMap,
so instance pointers in copyable classes registered as Jsonipc::Class<>
can be
marshalled into a JsonValue (as {$id,$class}
pair), then be resolved into an
InstanceP stored in an Ase::Value and from there be marshalled into a persistent
relative object link for project data storage.
Jsonipc
Jsonipc is a header-only IPC layer that marshals C++ calls to JSON messages defined in
jsonipc/jsonipc.hh.
The needed registration code is very straight forward to write manually, but can also be
auto-genrated by using
jsonipc/jsonbindings.ts
which parses API descriptions from JSON that can be generated with
clang -extract-api
.
The Anklang API for remote method calls is defined in api.hh. Each class with its methods, records with fields and enum with values are registered as a Jsonipc interface using conscise C++ code that utilizes templates to derive the needed type information.
The corresponding TypeScript code to use api.hh
via
async
remote method calls is generated with the Jsonipc::g_binding_printer
class by
AnklangSynthEngine --jsonts
. Completed:
- [√]
shared_ptr<Class> from_json()
- lookup by id in InstanceMap or useScope::make_shared
for Serializable. - [√]
to_json (const shared_ptr<Class> &p)
- marshal Serializable or {id} from InstanceMap. - [√]
Class* from_json()
- return&*shared_ptr<Class>
- [√]
to_json (Class *r)
- supports Serializable orClass->shared_from_this()
wrapping. - [√]
Class& from_json()
- return*shared_ptr<Class>
, throws onnullptr
. !!! - [√]
to_json (const Class &v)
- returnto_json<Class*>()
- [√] No uses are made of copy-ctor implementations.
- [√] Need virtual ID serialization API on InstanceMap.
- [√] Add
jsonvalue_as_string()
for debugging purposes. - [√] Type-safe Jsonipc bindings by migrating the JavaScript bindings to TypeScript.
Callback Handling
By using special trigger functions, JavaScript can register/unregister the existence of remote
Callbacks with a specific identifier via /create
and /remove
.
C++ in turn sends events to inform about a remote Callback being called via /_<id>
or unregistered via /killed
.
void Jsonapi/Trigger/create (id); // JS->C++
void Jsonapi/Trigger/remove (id); // JS->C++
void Jsonapi/Trigger/_<id> ([...]); // C++->JS
void Jsonapi/Trigger/killed (id); // C++->JS