diff --git a/.travis.yml b/.travis.yml index 080c1cbc..d3c42bb2 100644 --- a/.travis.yml +++ b/.travis.yml @@ -166,17 +166,6 @@ jobs: install: sudo pip install git-archive-all script: git-archive-all --prefix="sfizz-${TRAVIS_BRANCH}/" -9 "${INSTALL_DIR}.tar.gz" - - name: "Generate documentation" - if: (tag IS present) AND (branch = master) AND (type = push) - addons: - apt: - packages: - - doxygen - - cmake - - libsndfile-dev - install: skip - script: .travis/update_dox.sh - - name: "Discord Webhook" install: skip script: bash ${TRAVIS_BUILD_DIR}/.travis/discord_webhook.sh success diff --git a/.travis/update_dox.sh b/.travis/update_dox.sh deleted file mode 100755 index 55ea08f4..00000000 --- a/.travis/update_dox.sh +++ /dev/null @@ -1,17 +0,0 @@ -#!/bin/bash - -set -x # No fail, we need to go back to the original branch at the end -. .travis/environment.sh - -mkdir build && cd build && cmake -DSFIZZ_JACK=OFF -DSFIZZ_SHARED=OFF -DSFIZZ_LV2=OFF .. && cd .. -doxygen Doxyfile -./doxygen/scripts/generate_api_index.sh -git fetch --depth=1 https://github.com/${TRAVIS_REPO_SLUG}.git refs/heads/gh-pages:refs/remotes/origin/gh-pages -git checkout origin/gh-pages -git checkout -b gh-pages -mv _api api/${TRAVIS_TAG} -mv api_index.md api/index.md -git add api && git commit -m "Release ${TRAVIS_TAG} (Travis build: ${TRAVIS_BUILD_NUMBER})" -git remote add origin-pages https://${GITHUB_TOKEN}@github.com/${TRAVIS_REPO_SLUG}.git > /dev/null 2>&1 -git push --quiet --set-upstream origin-pages gh-pages -git checkout ${TRAVIS_BRANCH} diff --git a/doxygen/layout/DoxygenLayout.xml b/doxygen/layout/DoxygenLayout.xml deleted file mode 100644 index 48237226..00000000 --- a/doxygen/layout/DoxygenLayout.xml +++ /dev/null @@ -1,187 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - diff --git a/doxygen/layout/custom_footer.html b/doxygen/layout/custom_footer.html deleted file mode 100644 index 6f98db55..00000000 --- a/doxygen/layout/custom_footer.html +++ /dev/null @@ -1,12 +0,0 @@ -
- - - - - diff --git a/doxygen/layout/custom_header.html b/doxygen/layout/custom_header.html deleted file mode 100644 index bf5a07f6..00000000 --- a/doxygen/layout/custom_header.html +++ /dev/null @@ -1,155 +0,0 @@ - - - - - - Home - sfizz - - - - - - - - - - - - - - - - - - - - - -$treeview -$search -$mathjax - -$extrastylesheet - - - - - - - - -
- -
diff --git a/doxygen/layout/extra_stylesheet.css b/doxygen/layout/extra_stylesheet.css deleted file mode 100644 index 116008da..00000000 --- a/doxygen/layout/extra_stylesheet.css +++ /dev/null @@ -1,198 +0,0 @@ -#page_container { - position: relative; - margin: 0; - padding: 0; - height: auto !important; - height: 100%; - min-height: 100%; -} - -div.contents, div.searchresults { - margin-top: 10px; - margin-right: 12px; - padding-bottom: 70px; -} - -#projectlogo { - text-align: left; -} - -#projectnumber { - font-size: 120%; - font-family: Tahoma, Arial, sans-serif; - text-align: right; - padding: 0.5em 1em; -} - -.tabs { - font-size: 14px; -} -.tabs2, .tabs3 { - font-size: 12px; -} - -.navpath ul { - font-size: 12px; -} - -h1, h2, h3, h4, h5, h6 { - color: #002D88; - font-weight: normal; - margin-top: 1em; - margin-bottom: 0.5em; - padding-top: 8px; - padding-bottom: 4px; - width: 100%; -} - -h1 { - font-size: 150%; - border-bottom: 1px solid #3276FF; -} -h2 { - font-size: 135%; - margin-top: 0.75em; -} -h3 { - font-size: 120%; - margin-top: 0.5em; -} -h4 { - font-size: 100%; - margin-top: 0.5em; -} - -div.headertitle h1 { - margin: 10px 2px; - border: none; - padding: 0; - width: auto; - color: black; - font-weight: bold; -} - -div.toc h3 { - font-size: 14px; -} - -div.toc li { - font-size: 12px; - line-height: 1.3; - padding-left: 14px; -} - -img.logo { - float: right; - margin: 20px; -} - -div.logo { - float: right; - margin: 20px; -} - -table.directory { - line-height: 1.5; -} - -.icon { - line-height: 1.25; -} - -div.appearance { - margin: 1em 0em; -} -div.appearance table { - margin: 0.5em 0em; - width: 100%; - text-align: center; -} -div.appearance img { - margin: 0.5em; -} -div.appearance .caption { - font-style: italic; - font-weight: normal; - font-size: 90%; -} - -div.appearance_brief table { - width: 100%; - table-layout: fixed; - text-align: center; - border-collapse: collapse; -} - -div.appearance_brief table td:first-child { - width: 20em; - text-align: left; - padding-left: 2em; -} - -div.appearance_brief table td { - border-style: none solid solid none; - border-width: 1px; - border-color: lightblue; -} - - -td.green { color: green; } -td.orange { color: #ff8000; } -td.red { color: red; } - -span.literal { - text-decoration: none; - font-weight: bold; - font-family: monospace, fixed; -} - -/* we make all the following tags render the text just like - the standard Doxygen @remarks, @see tags do, to obtain a uniform - look and feel */ -span.itemdef, span.lib, span.category, span.stdobj, span.styles, -span.events, span.flags, span.appearance, span.impl, span.avail { - font-weight: bold; - line-height: 130%; -} - -span.style, span.event, span.flag { - font-weight: bold; - color: #880000; -} - -div.styleDesc, div.eventDesc, div.flagDesc { - margin-left: 3%; - margin-bottom: 1ex; -} - -div.eventHandler { - margin: 1em; - text-indent: 3%; -} - -div.eventHandler span { - padding: 5px; - background-color: #eeeeee; - font-family: monospace, fixed; -} - -code { - font-size: 110%; - color: #444444; -} - -address.footer { - position: absolute; - bottom: 0; - margin: 0; - padding: 10px 0; - width: 100%; - border-top: 1px solid #0043CC; - background-image: url('nav_h.png'); - background-repeat: repeat-x; - background-color: #F4F8FF; -} - -address.footer small { - padding: 0 10px; -} diff --git a/doxygen/pages/engine_description.md b/doxygen/pages/engine_description.md deleted file mode 100644 index ab4905e9..00000000 --- a/doxygen/pages/engine_description.md +++ /dev/null @@ -1,154 +0,0 @@ -# Global view of the engine - -The sfizz engine is basically a "Synth" object that takes an SFZ file in, receives MIDI-type events -and is able to render audio through successive calls to a callback function. This is in line with -the way most audio applications and plugins are working. A high-level overview is presented in the -following diagram. - -``` - C and C++ API entry point - - | - | - | - | - +--------v-------+ - | | - +-------------------------- Synth -----------------------------+ - | | | | - | +----------------+ | - | | | - +--------v------+ | +---------v--------+ - | | +---------v---------+ | | - | Region list | | Common resources | | Voice pool | - | | | and state | | | - +---------------+ | ----------------- | +------------------+ - Built from the SFZ file | File pool | - | Envelope pool | The voices are the polyphony - Each region is a semi-passive | LFO pool | of the synth. They are idle - description object that can | Buffer pool | and they get activated by the - decide whether it is "active" | Midi state | synth to play a region on a - or not depending on the chain | ... | specific event. They are then - of MIDI events it receives. +-------------------+ "linked" to the region while - Once activated, a voice is There are a number of common it is played, and reset to - chosen to play the region until resources that are needed for their idle state when they - it ends naturally or through all the regions and in parti- are done playing the region. - note-offs or off-groups. cular the voices. This includes - all the (preloaded) files for - the SFZ instrument, but will - include in the future the EG - and LFOs that are needed to - achieve compliance with the - SFZ v2 specification. This will - also include a temporary buffer - holder that voices may share. - A common resource of importance - is the MIDI state: note durations - are needed for some opcodes -- - for example rt_decay -- and - triggering velocities too. -``` - -The Synth, Voices and Regions form the bulk of the code complexity. The rest of the engine is dedicated of -mostly helper classes to enable easy management of floating-point buffers in which the audio data is held, -signal processing and accelerated (SIMD) computations, and abstractions that are specific to the SFZ format -such as envelope generators, curves or LFOs. - -# Parsing the SFZ files - -The sfz file logic is pretty simple and well defined. The sfzformat.com website contains an extensive documentation -on it. At its core, an SFZ file describes a list of `region` objects on which a certain number of "opcodes" will -apply. Opcodes can determine the sample played, the event conditions that will trigger the sample such as the range -of notes, channels, velocities, the processing to apply on the sample while playing, and many more things. It is -also possible to describe a `group` of regions, as well as exclusive groups that will shut off other regions that may -already be playing. There are also `master` groups, and `global` opcodes and some other types. - -All the opcodes are declared within a header, in a pseudo-xml markup language that looks like this -```sfz - volume=6 - set_cc4=5 - key=36 sample=kick.wav -``` -Here we have 3 headers (`global`, `control` and `region`) and each header holds some opcodes. All of these opcodes -have a value---for example the volume is equal to 6 in the `global` header. Some opcodes also have parameters. -The `control` header holds an opcode `set_cc` with the parameter `4` and value `5`. The parameter here is the CC to set, -and the value at which to set it is 5. - -The parsing logic of sfizz is handled through a base class called Parser---a very original choice. This parser has -a virtual callback that gets called whenever a header description is "complete", along with a list of opcodes that -apply to the header. Subclassing the Parser then allows to build different SFZ handlers, from full-blown synths as -with sfizz to simpler things such as printers (see in particular https://github.com/sfztools/sfz-flat/). If we look -at the core of the latter example, it will look something like the following - -```cpp -class PrintingParser: public sfz::Parser -{ -protected: - void callback(absl::string_view header, const std::vector& members) final - { - switch (hash(header)) // The hash(...) function transforms strings to large integers - { - case hash("global"): // It is also compile-time defined, which allows to do switch-case - // statements on strings, something that is usually not possible - globalMembers = members; // We save the global headers since they apply to the next - // region (and groups and masters) - masterMembers.clear(); - groupMembers.clear(); - break; - case hash("master"): - masterMembers = members; // So on - groupMembers.clear(); - break; - case hash("group"): - groupMembers = members; // .. and so forth - break; - case hash("region"): - std::cout << "<" << header << ">" << ' '; // Now we print the region along with all the opcodes - // we memorized from earlier headers. - printMembers(globalMembers); - printMembers(masterMembers); - printMembers(groupMembers); - printMembers(members); - std::cout << '\n'; - break; - default: - std::cout << "<" << header << ">" << ' '; - printMembers(members); - std::cout << '\n'; - break; - } - } -private: - std::vector globalMembers; - std::vector masterMembers; - std::vector groupMembers; - void printMembers(const std::vector& members) - { - for (auto& member: members) - { - std::cout << member.opcode; - if (member.parameter) - std::cout << +*member.parameter; - std::cout << "=" << member.value; - std::cout << ' '; - } - } -}; -``` - -The main function is then quite straightforward and we call a function from the Parser class that loads a file -```cpp -PrintingParser parser; -parser.loadSfzFile("my_sfz_file.sfz"); -``` -If you circle back to the parser you will see that opcodes are stored in an `Opcode` class. This class does some parsing -itself and separates the opcode name itself, parameters if any, and the value. Opcodes are very cheap to copy and pass -around because they only refer to characters in the file that are stored inside the `Parser` class, so feel free to -create vectors of them and move them around. - -Note that you may also derive the loadSfzFile method if you have any processing you need to do before the actual parsing happens. - -# Building the region list in sfizz - -The callback method from sfizz is actually quite similar to the one shown above, except that instead of printing the region -we actually fill a big structure from it. diff --git a/doxygen/pages/index.md b/doxygen/pages/index.md deleted file mode 100644 index a7e12b3f..00000000 --- a/doxygen/pages/index.md +++ /dev/null @@ -1,6 +0,0 @@ -# sfizz - -SFZ file format library - -- [public C API](sfizz_8h.html) -- [public C++ API](classsfz_1_1_sfizz.html) diff --git a/doxygen/scripts/generate_api_index.sh b/doxygen/scripts/generate_api_index.sh deleted file mode 100755 index af618b6f..00000000 --- a/doxygen/scripts/generate_api_index.sh +++ /dev/null @@ -1,19 +0,0 @@ -#!/bin/bash -# Must be called from the root directory -if [[ -f api_index.md ]]; then rm api_index.md; fi -cat >>api_index.md <> api_index.md -done diff --git a/doxygen/scripts/Doxyfile.in b/scripts/doxygen/Doxyfile.in similarity index 100% rename from doxygen/scripts/Doxyfile.in rename to scripts/doxygen/Doxyfile.in diff --git a/scripts/doxygen/doxy2json.py b/scripts/doxygen/doxy2json.py new file mode 100644 index 00000000..4ca1aa69 --- /dev/null +++ b/scripts/doxygen/doxy2json.py @@ -0,0 +1,185 @@ +#!/usr/bin/env python3 +""" + SPDX-License-Identifier: BSD-2-Clause + + This code is part of the sfizz library and is licensed under a BSD 2-clause + license. You should have receive a LICENSE.md file along with the code. + If not, contact the sfizz maintainers at https://github.com/sfztools/sfizz + + Converts the Doxygen XML output to a custom JSON structure. + + The JSON output will be used on a Jekyll website, + parsed by a related layout (_layouts/doxygen.html). + + Known bugs / wish list: + + - Replace `strip_tags_in_text()` and `tag_list_as_string()` with a recursive + parsing function able to extract also detailed descriptions paragraphs + (E.g. `sfizz_oversampling_factor_t` has 3), getting rid of various + `@since`, `@note` and `@return`, which are parsed separately. + It should also covert the Doxygen custom tags (like , see below, + or should this be done by some XSLT trasformation file?). + + - Tags in ALIASES list needs to be escaped (parsed by tag_list_as_string()) + to avoid Doxygen convert them in its own tag structure, by loosing some + details (e.g.: true becomes true). + + - Merge the work done previously to be able to fully automate the process + to be used in various CIs. +""" +import xml.etree.ElementTree as ET +import json + +# TODO: scan for files +tree = ET.parse('xml/classsfz_1_1_sfizz.xml') +root = tree.getroot() +data = {} + +def strip_tags_in_text(element): + if element is None: + return "" + + returned_string = element.text or "" + for t in element: + if t.tag == "ref": + returned_string += "{}".format(t.text.replace("()", ''), t.text) + returned_string += t.tail or "" + elif t.tag == "computeroutput": + returned_string += "{}".format(t.text) + returned_string += t.tail or "" + else: + returned_string += t.text or "" + returned_string += t.tail or "" + + return returned_string.strip() + +def tag_list_as_string(element_list): + if element_list is None or len(element_list) == 0: + return "" + + if element_list[0].text is not None: + return element_list[0].text.strip() + + returned_string = "" + for element in element_list: + for t in element: +# if t.tag == "bold": t.tag = 'b' + returned_string += ET.tostring(t, encoding="unicode") + + return returned_string.strip() + +name = root.find("./compounddef/compoundname") +kind = root.find("./compounddef").get("kind") +brief = root.find("./compounddef/briefdescription/para") +include = root.find("./compounddef/includes") +location = root.find("./compounddef/location").get("file") +language = root.find("./compounddef").get("language") +version = root.get("version") + +if name is not None and name.text is not None: data["name"] = name.text +if kind is not None: data["kind"] = kind +if brief is not None and brief.text is not None: data["brief"] = brief.text.strip() +if include is not None and include.text is not None: data["include"] = include.text.strip() +if location is not None: data["location"] = location +if language is not None: data["language"] = language +if version is not None: data["doxygen_version"] = version +definitions = [] + +for sectiondef in root.iter("sectiondef"): + + def_kind = sectiondef.get("kind") + definition = {} + members = [] + definition["kind"] = def_kind + + for memberdef in sectiondef: + + member = {} + member["name"] = memberdef.find("name").text + + type_member = memberdef.find("type") + initializer_member = memberdef.find("initializer") + brief_member = memberdef.find("briefdescription/para") + description_member = memberdef.find("detaileddescription/para") + return_member = memberdef.findall("detaileddescription/para/simplesect[@kind='return']/para") + since_member = memberdef.find("detaileddescription/para/simplesect[@kind='since']/para") + note_member = memberdef.find("detaileddescription/para/simplesect[@kind='note']/para") + + if type_member is not None: + type_member = strip_tags_in_text(type_member) + if type_member != '': + member["type"] = type_member + + if initializer_member is not None and initializer_member.text is not None: + member["initializer"] = initializer_member.text + + if brief_member is not None: + member["brief"] = strip_tags_in_text(brief_member) + + if description_member is not None: + description_member = strip_tags_in_text(description_member) + if description_member != '': + member["description"] = description_member + + if return_member is not None: + return_member = tag_list_as_string(return_member) + if return_member != '': + member["return"] = return_member + + if since_member is not None and since_member.text is not None: + member["since"] = since_member.text.strip() + + if note_member is not None and note_member.text is not None: + member["note"] = strip_tags_in_text(note_member) + + members.append(member) + + if def_kind == "enum" or def_kind == "public-type": + enumvalues = [] + for enumvalue in memberdef.iter("enumvalue"): + enum = {} + enum["name"] = enumvalue.find("name").text + + initializer_member = enumvalue.find("initializer") + brief_member = enumvalue.find("briefdescription/para") + description_member = enumvalue.find("detaileddescription/para") + + if initializer_member is not None and initializer_member.text is not None: + enum["initializer"] = initializer_member.text + + if brief_member is not None: + enum["brief"] = strip_tags_in_text(brief_member) + + if description_member is not None: + enum["description"] = strip_tags_in_text(description_member) + + enumvalues.append(enum) + + member["values"] = enumvalues + + params = [] + for paramtag in memberdef.findall("param"): + + param = {} + param["name"] = paramtag.find("declname").text + param["type"] = strip_tags_in_text(paramtag.find("type")) + + param_items = memberdef.findall("detaileddescription/para/parameterlist[@kind='param']/parameteritem") + for param_item in param_items: + param_name = param_item.find("parameternamelist/parametername").text + if param_name == param.get("name"): + description = param_item.find("parameterdescription/para") + if description is not None: + param["description"] = strip_tags_in_text(description) + + params.append(param) + + if params: + member["params"] = params + + definition["members"] = members + definitions.append(definition) + +data["definitions"] = definitions + +print(json.dumps(data, indent=2)) diff --git a/src/CMakeLists.txt b/src/CMakeLists.txt index 11d48265..7ffd3c1f 100644 --- a/src/CMakeLists.txt +++ b/src/CMakeLists.txt @@ -245,7 +245,7 @@ if(WIN32) configure_file (${PROJECT_SOURCE_DIR}/scripts/innosetup.iss.in ${PROJECT_BINARY_DIR}/innosetup.iss @ONLY) endif() -configure_file (${PROJECT_SOURCE_DIR}/doxygen/scripts/Doxyfile.in ${PROJECT_SOURCE_DIR}/Doxyfile @ONLY) +configure_file (${PROJECT_SOURCE_DIR}/scripts/doxygen/Doxyfile.in ${PROJECT_SOURCE_DIR}/Doxyfile @ONLY) add_library (sfizz::parser ALIAS sfizz_parser) add_library (sfizz::sfizz ALIAS sfizz_static)