diff --git a/.github/workflows/pages.yml b/.github/workflows/pages.yml new file mode 100644 index 0000000..a6dd49e --- /dev/null +++ b/.github/workflows/pages.yml @@ -0,0 +1,52 @@ +name: Build and Deploy mdBook + +on: + push: + branches: + - master + +jobs: + build: + runs-on: ubuntu-latest + steps: + - name: Checkout + uses: actions/checkout@v4 + with: + submodules: recursive + + - name: Install Rust + uses: actions-rust-lang/setup-rust-toolchain@v1 + + - name: Build the book + run: | + cd docs + ./generate/build.sh "3.9.3" + cargo install mdbook mdbook-linkcheck mdbook-toc mdbook-alerts + mdbook build + + - name: Upload + uses: actions/upload-pages-artifact@v3 + with: + name: github-pages + path: ./docs/out/html + + deploy: + # Add a dependency to the build job + needs: build + + # Grant GITHUB_TOKEN the permissions required to make a Pages deployment + permissions: + pages: write # to deploy to Pages + id-token: write # to verify the deployment originates from an appropriate source + + # Deploy to the github-pages environment + environment: + name: github-pages + url: ${{ steps.deployment.outputs.page_url }} + + # Specify runner + deployment step + runs-on: ubuntu-latest + steps: + - name: Deploy to GitHub Pages + id: deployment + uses: actions/deploy-pages@v4 diff --git a/.vscode/settings.json b/.vscode/settings.json index 83aa7fa..cb03796 100644 --- a/.vscode/settings.json +++ b/.vscode/settings.json @@ -17,5 +17,8 @@ "Lua.workspace.library": [ "types/nerdo" ], + "Lua.workspace.ignoreDir": [ + "docs" + ], "Lua.hint.arrayIndex": "Disable" } \ No newline at end of file diff --git a/docs/.gitignore b/docs/.gitignore new file mode 100644 index 0000000..f1ea593 --- /dev/null +++ b/docs/.gitignore @@ -0,0 +1,3 @@ +Cargo.lock +/out/ +/target/ \ No newline at end of file diff --git a/docs/.vscode/launch.json b/docs/.vscode/launch.json new file mode 100644 index 0000000..ed39555 --- /dev/null +++ b/docs/.vscode/launch.json @@ -0,0 +1,25 @@ +{ + // Use IntelliSense to learn about possible attributes. + // Hover to view descriptions of existing attributes. + // For more information, visit: https://go.microsoft.com/fwlink/?linkid=830387 + "version": "0.2.0", + "configurations": [ + { + "type": "lldb", + "request": "launch", + "name": "Debug 'Generate API'", + "cargo": { + "args": [ + "build", + ], + "filter": { + "kind": "bin" + } + }, + "args": [ + "${workspaceFolder}/../types/nerdo/library", + "${workspaceFolder}/src" + ], + }, + ] +} \ No newline at end of file diff --git a/docs/.vscode/tasks.json b/docs/.vscode/tasks.json new file mode 100644 index 0000000..9e4e594 --- /dev/null +++ b/docs/.vscode/tasks.json @@ -0,0 +1,36 @@ +{ + "version": "2.0.0", + "tasks": [ + { + "type": "cargo", + "command": "run", + "problemMatcher": [ + "$rustc" + ], + "args": [ + "${workspaceFolder}/../types/nerdo/library", + "${workspaceFolder}/src" + ], + "group": "build", + "label": "build: API docs" + }, + { + "type": "shell", + "command": "mdbook", + "args": [ + "build" + ], + "group": "build", + "label": "build: book", + }, + { + "type": "shell", + "command": "mdbook", + "args": [ + "serve" + ], + "group": "build", + "label": "serve: book", + } + ] +} \ No newline at end of file diff --git a/docs/Cargo.toml b/docs/Cargo.toml new file mode 100644 index 0000000..243fd92 --- /dev/null +++ b/docs/Cargo.toml @@ -0,0 +1,5 @@ +[workspace] +resolver = "2" +members = [ + "generate", +] diff --git a/docs/README.md b/docs/README.md new file mode 100644 index 0000000..4adbde7 --- /dev/null +++ b/docs/README.md @@ -0,0 +1,21 @@ +## AFSEQ Book + +### Requirements + +The docs are generated using [mdBook](https://github.com/rust-lang/mdBook). To preview the pages locally you will need [cargo](https://doc.rust-lang.org/cargo/getting-started/installation.html) to install mdbook, mdbook-linkcheck and mdbook-toc. + +```sh +cargo install mdbook mdbook-linkcheck mdbook-toc +``` + +### Building + +Afterwards you can serve the docs at `localhost:3000` using mdbook, this will automatically refresh the browser tab whenever you change markdown files. + +```sh +mdbook serve --open +``` + +### Generate API reference + +See [generate/README.md](./generate/README.md) \ No newline at end of file diff --git a/docs/book.toml b/docs/book.toml new file mode 100644 index 0000000..0414d9b --- /dev/null +++ b/docs/book.toml @@ -0,0 +1,22 @@ +[book] +authors = ["mail@emuell.net"] +language = "en" +multilingual = false +src = "src" +title = "AFSEQ" + +[build] +build-dir = "out" +create-missing = false + +[preprocessor.alerts] +[preprocessor.toc] +max-level = 3 + +[output.html] +default-theme = "dark" +preferred-dark-theme = "ayu" +additional-css = [ "src/styles.css" ] + +[output.linkcheck] +warning-policy = "ignore" diff --git a/docs/generate/.gitignore b/docs/generate/.gitignore new file mode 100644 index 0000000..5ed8f3f --- /dev/null +++ b/docs/generate/.gitignore @@ -0,0 +1 @@ +/lua-language-server/ \ No newline at end of file diff --git a/docs/generate/Cargo.toml b/docs/generate/Cargo.toml new file mode 100644 index 0000000..46d256c --- /dev/null +++ b/docs/generate/Cargo.toml @@ -0,0 +1,15 @@ +[package] +name = "generate" +version = "0.1.0" +edition = "2021" + +[dependencies] +clap = { version = "4.5", features = ["derive"] } +itertools = "0.13" +pest = "2.7" +pest_derive = "2.7" +serde = { version = "1.0", features = ["derive"] } +serde_json = "1.0" +tempdir = "0.3" +thiserror = "1.0" +url = "2.5" diff --git a/docs/generate/README.md b/docs/generate/README.md new file mode 100644 index 0000000..efb09c9 --- /dev/null +++ b/docs/generate/README.md @@ -0,0 +1,45 @@ +# AFSEQ API Documentation Generator + +This is a rust app that generates the API definition chapters in the [AFSEQ book](https://emuell.github.io/afseq/) from the [Lua API definition](../../types/nerdo/) files. + +It is based on [matt-allan's](https://github.com/matt-allan) [mdbook-luacat](https://github.com/matt-allan/mdbook-luacats) tool. + + +## Building + +### Requirements + +- [rust](https://www.rust-lang.org/tools/install) v1.56 or higher +- [LuaLS](https://github.com/luals/lua-language-server) installation + +Note: The LuaLS installation must be placed into the folder [./lua-language-server](./lua-language-server) folder and **must be patched**. + +See [build.sh](./build.sh) for details. + +### Building + +To create or update the API definitions chapter, build and run the app, then build the book: + +```bash +# in the afseq root directory +cd docs +# build and run the generate app to create the API definition +cargo run -- ../../types/nerdo/ ./src +# serve or build the book +mdbook serve +``` + +--- + +Alternatively, if you have vscode installed, open the XRNX repository folder and use the provided build task to build the API and the book: + +- `build: API Docs`: compiles and runs the API docs generator +- `build: book`: compiles the mdbook +- `serve: book`: serve and live update the book at //localhost:3000 + + +## Debugging + +If you have vscode installed, open the XRNX root repository folder in vscode and use the `Debug: 'Generate API'` launch action. + +To debug and build the full API definition, change the launch arguments in the file [.vscode/launch.json](../.vscode/launch.json). diff --git a/docs/generate/build.sh b/docs/generate/build.sh new file mode 100644 index 0000000..426a69f --- /dev/null +++ b/docs/generate/build.sh @@ -0,0 +1,21 @@ +#!/bin/bash +set -e + +LUA_LS_VERSION=$1 + +pushd generate +echo "downloading lua-language-server-${LUA_LS_VERSION}..." +rm -rf lua-language-server && mkdir lua-language-server + +pushd lua-language-server +rm -rf ./* +wget -q -O - "https://github.com/LuaLS/lua-language-server/releases/download/${LUA_LS_VERSION}/lua-language-server-${LUA_LS_VERSION}-linux-x64.tar.gz" | tar xz + +echo "patching files..." +sed -i.bak "s/\(\['Lua.hover.enumsLimit'\]\s*=\s*Type.Integer\s*>>\s*\)5\(,\)/\1100\2/" "script/config/template.lua" +sed -i.bak -e "s/\(\['Lua.hover.expandAlias'\]\s*=\s*Type.Boolean\s*>>\s*\)true\(,\)/\1false\2/" "script/config/template.lua" +sed -i.bak -e '/if \#view > 200 then/,/end/s/^/-- /' "script/vm/infer.lua" +popd # lua-language-server + +echo "generating api docs..." +cargo run -- "../../types/nerdo/library" "../src" \ No newline at end of file diff --git a/docs/generate/src/error.rs b/docs/generate/src/error.rs new file mode 100644 index 0000000..ad5e589 --- /dev/null +++ b/docs/generate/src/error.rs @@ -0,0 +1,11 @@ +use std::io; + +#[derive(thiserror::Error, Debug)] +pub enum Error { + #[error("IO error")] + Io(#[from] io::Error), + #[error("failed to execute lua-language-server")] + Exec, + #[error("unable to parse doc JSON")] + JsonParse(#[from] serde_json::Error), +} diff --git a/docs/generate/src/json.rs b/docs/generate/src/json.rs new file mode 100644 index 0000000..3d8fc12 --- /dev/null +++ b/docs/generate/src/json.rs @@ -0,0 +1,435 @@ +use crate::{error::Error, types::LuaKind}; +use serde::{Deserialize, Deserializer, Serialize}; +use std::{ + fmt, fs, + path::{Path, PathBuf}, + process::Command, + str::FromStr, +}; +use tempdir::TempDir; +use url::Url; + +pub struct JsonDoc {} +impl JsonDoc { + /// Generate a list of definitions from a file + pub fn get(path: &Path) -> Result, Error> { + let defs = Self::export(path)?; + Ok(Self::strip(path, defs)) + } + + /// Export and parse the JSON docs from lua-language-server + fn export(path: &Path) -> Result, Error> { + let tmp_dir = TempDir::new("docs")?; + let tmp_path = tmp_dir.path(); + let ls_filename = if cfg!(windows) { + "lua-language-server.exe" + } else { + "lua-language-server" + }; + // allow running from within the "generate" path and from the repository root path + let mut ls_path = PathBuf::from("./generate/lua-language-server/bin/").join(ls_filename); + if !ls_path.exists() { + ls_path = PathBuf::from("./lua-language-server/bin/").join(ls_filename); + } + let output = Command::new(ls_path) + .arg("--doc") + .arg(path) + .arg("--doc_out_path") + .arg(tmp_path) + .arg("--logpath") + .arg(tmp_path) + .output()?; + + if !output.status.success() { + Err(Error::Exec) + } else { + let json_doc_path = tmp_dir.path().join("doc.json"); + let json_doc = fs::read_to_string(json_doc_path)?; + Ok(serde_json::from_str(&json_doc)?) + } + } + + fn file_url_matches(file_url: &str, base_path: &Path) -> bool { + assert!( + Url::from_str(file_url).is_ok(), + "Expecting an url file string" + ); + let file_path = Url::from_str(file_url) + .unwrap() + .to_file_path() + .unwrap_or_default() + .canonicalize() + .unwrap_or_default(); + let base_path = base_path.canonicalize().unwrap_or_default(); + file_path.starts_with(base_path) + } + + /// Exclude standard lua + fn strip(path: &Path, defs: Vec) -> Vec { + defs.into_iter() + .map(|d| { + // remove standard define from the list of defines (for type()) + let mut def = d.clone(); + def.defines + .retain(|define| Self::file_url_matches(&define.file, path)); + def + }) + .collect() + } +} + +#[derive(Debug, PartialEq, Serialize, Clone, Eq, PartialOrd, Ord, Deserialize)] +#[serde(rename_all = "lowercase")] +pub enum VisibleType { + Public, + Private, + Package, + Protected, +} + +#[derive(Debug, Serialize, PartialEq, Deserialize, Clone, Eq, PartialOrd, Ord)] + +pub enum Doc { + #[serde(rename = "doc.field")] + Field, + #[serde(rename = "doc.enum")] + Enum, + #[serde(rename = "doc.alias")] + Alias, + #[serde(rename = "doc.class")] + Class, + #[serde(rename = "doc.extends.name")] + ExtendsName, + #[serde(rename = "doc.type")] + Type, + #[serde(rename = "doc.type.name")] + TypeName, + #[serde(rename = "doc.type.integer")] + TypeInteger, + #[serde(rename = "doc.type.number")] + TypeNumber, + #[serde(rename = "doc.type.array")] + TypeArray, + #[serde(rename = "doc.type.boolean")] + TypeBoolean, + #[serde(rename = "doc.type.string")] + TypeString, + #[serde(rename = "doc.type.table")] + TypeTable, + #[serde(rename = "doc.type.sign")] + TypeSign, + #[serde(rename = "doc.type.function")] + TypeFunction, +} + +#[derive(Debug, PartialEq, Serialize, Clone, Eq, PartialOrd, Ord)] +#[serde(rename_all = "lowercase")] +pub enum Type { + Doc(Doc), + // fields, defines + GetField, + SetField, + SetMethod, + // definitions + #[serde(rename = "type")] + Def, + Variable, + #[serde(rename = "luals.config")] + LuaLsConfig, + // defines + TableField, + SetGlobal, + // function returns + #[serde(rename = "function.return")] + FunctionReturn, + // extends + Lua(LuaKind), +} + +impl<'de> Deserialize<'de> for Type { + fn deserialize(deserializer: D) -> Result + where + D: Deserializer<'de>, + { + let s: &str = Deserialize::deserialize(deserializer)?; + match s { + "type" => Ok(Type::Def), + "variable" => Ok(Type::Variable), + "getfield" => Ok(Type::GetField), + "setfield" => Ok(Type::SetField), + "setmethod" => Ok(Type::SetMethod), + "setglobal" => Ok(Type::SetGlobal), + "tablefield" => Ok(Type::TableField), + "function.return" => Ok(Type::FunctionReturn), + "luals.config" => Ok(Type::LuaLsConfig), + _ => { + let quoted = format!("\"{}\"", s); + match serde_json::from_str::(quoted.as_str()) { + Ok(lk) => Ok(Type::Lua(lk)), + Err(_) => match serde_json::from_str::(quoted.as_str()) { + Ok(d) => Ok(Type::Doc(d)), + Err(_) => Err(serde::de::Error::unknown_variant(s, &[])), + }, + } + } + } + } +} + +#[derive(Clone, Debug, Eq, PartialEq, PartialOrd, Ord, Serialize, Deserialize)] +pub struct Field { + pub desc: Option, + pub rawdesc: Option, + pub name: String, + #[serde(rename = "type")] + pub lua_type: Type, + pub file: String, + pub start: u32, + pub finish: u32, + pub visible: VisibleType, + #[serde(default, deserialize_with = "deserialize_extends")] + pub extends: Option, +} + +impl fmt::Display for Field { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + write!( + f, + "{} : {:?}", + self.name, + self.lua_type, + // self.extends.clone().map(|e| e.view).unwrap_or_default() + ) + } +} + +#[derive(Clone, Debug, Eq, PartialEq, PartialOrd, Ord, Serialize, Deserialize)] +pub struct Definition { + #[serde(rename = "type")] + pub lua_type: Type, + pub name: String, + pub desc: Option, + pub rawdesc: Option, + pub defines: Vec, + #[serde(default)] + pub fields: Vec, +} + +impl Field { + pub fn is_field(&self) -> bool { + self.lua_type == Type::Doc(Doc::Field) + || (self.lua_type == Type::SetField + && !Self::extend_has_type(&self.extends, Type::Lua(LuaKind::Function))) + } + + pub fn is_function(&self) -> bool { + self.lua_type == Type::SetMethod + || (self.lua_type == Type::SetField + && Self::extend_has_type(&self.extends, Type::Lua(LuaKind::Function))) + } + + fn extend_has_type(e: &Option, t: Type) -> bool { + if let Some(e) = e { + e.lua_type == t + } else { + false + } + } +} +impl fmt::Display for Definition { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + write!( + f, + "{} : {:?} d[{}] f[{}]{}{}", + self.name, + self.lua_type, + self.defines.len(), + self.fields.len(), + // "", + if !self.defines.is_empty() { + format!( + "\n{}", + self.defines + .iter() + .enumerate() + .map(|(i, d)| format!(" <{}> - {}", i, d)) + .collect::>() + .join("\n") + ) + } else { + String::from("") + }, + if !self.fields.is_empty() { + format!( + "\n{}", + self.fields + .clone() + .iter() + .enumerate() + .map(|(i, f)| format!(" [{}] - {}", i, f)) + .collect::>() + .join("\n") + ) + } else { + String::from("") + }, + ) + } +} + +#[derive(Clone, Debug, Eq, PartialEq, PartialOrd, Ord, Serialize, Deserialize)] +pub struct Define { + #[serde(rename = "type")] + pub lua_type: Type, + pub file: String, + pub start: u32, + pub finish: u32, + #[serde(default, deserialize_with = "deserialize_extends")] + pub extends: Option, +} + +impl fmt::Display for Define { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + write!(f, "{:?}", self.lua_type,) + } +} + +#[derive(Clone, Debug, Eq, PartialEq, PartialOrd, Ord, Serialize, Deserialize)] +pub struct ExtendType { + #[serde(rename = "type")] + pub lua_type: Type, + pub start: u32, + pub finish: u32, + pub view: String, +} + +#[derive(Clone, Debug, Eq, PartialEq, PartialOrd, Ord, Serialize, Deserialize)] +pub struct Extend { + #[serde(rename = "type")] + pub lua_type: Type, + pub types: Option>, + pub start: u32, + pub finish: u32, + pub view: String, + pub desc: Option, + pub rawdesc: Option, + /// Only present for functions (type = "function") with args + #[serde(default)] + pub args: Vec, + /// Only present for functions (type = "function") with returns + #[serde(default)] + pub returns: Vec, +} + +/// Extends can be either null, an object or an array of objects +fn deserialize_extends<'de, D>(deserializer: D) -> Result, D::Error> +where + D: Deserializer<'de>, +{ + #[derive(Deserialize, Debug)] + #[serde(untagged)] + enum ExtendInput { + None, + Map(Extend), + Array(Vec), + Object(Extend), + Other(serde_json::Value), + } + + impl From for Option { + fn from(input: ExtendInput) -> Self { + match input { + ExtendInput::None => None, + ExtendInput::Map(extend) => Some(extend), + // there is only one possible extends per define in the API + ExtendInput::Array(vec) => vec.first().cloned(), + ExtendInput::Object(extend) => Some(extend), + ExtendInput::Other(value) => { + #[cfg(debug_assertions)] + panic!("Unexpected extend {:?}", value); + #[cfg(not(debug_assertions))] + None + } + } + } + } + Ok(ExtendInput::deserialize(deserializer)?.into()) +} + +#[derive(Clone, Debug, Eq, PartialEq, PartialOrd, Ord, Serialize, Deserialize)] +pub enum ArgType { + #[serde(rename = "self")] + SelfArg, + #[serde(rename = "local")] + Local, + #[serde(rename = "...")] + Variadic, +} + +#[derive(Clone, Debug, Eq, PartialEq, PartialOrd, Ord, Serialize, Deserialize)] +pub struct ArgDef { + #[serde(rename = "type")] + pub lua_type: ArgType, + pub name: Option, + pub view: String, +} + +#[derive(Clone, Debug, Eq, PartialEq, PartialOrd, Ord, Serialize, Deserialize)] +pub struct ReturnDef { + // NB: type for returns will always be "function.return" + #[serde(rename = "type")] + pub lua_type: Type, + pub name: Option, + pub view: String, +} + +/* +TODO +UNHANDLED EXTEND doc.type -> "types" doc.type.function -> args -> name is an object + { + "defines": [ + { + "extends": { + "finish": 9500057, + "start": 9500034, + "type": "doc.type", + "types": [ + { + "args": [ + { + "finish": 9500056, + "name": { + "[1]": "value", + "finish": 9500043, + "start": 9500038, + "type": "doc.type.arg.name", + "view": "value" + }, + "start": 9500038, + "type": "doc.type.arg", + "view": "{ x: number, y: number }" + } + ], + "finish": 9500057, + "returns": { + "view": "unknown" + }, + "start": 9500034, + "type": "doc.type.function", + "view": "fun(value: { x: number, y: number })" + } + ], + "view": "fun(value: { x: number, y: number })" + }, + "file": "file://library/renoise/view_builder.lua", + "finish": 9500057, + "start": 9500010, + "type": "doc.alias" + } + ], + "fields": [], + "name": "XYValueNotifierFunction", + "type": "type" + }, + + */ diff --git a/docs/generate/src/json_to_types.rs b/docs/generate/src/json_to_types.rs new file mode 100644 index 0000000..6318ba7 --- /dev/null +++ b/docs/generate/src/json_to_types.rs @@ -0,0 +1,215 @@ +use std::path::PathBuf; + +use crate::{ + json::{ArgDef, ArgType, Definition, Doc, Extend, Field, ReturnDef, Type}, + lua_parser::LuaParser, + types::*, +}; + +impl Def { + pub fn from_definition(d: &Definition) -> Option { + if let Some(first) = d.defines.first() { + match first.lua_type { + Type::Doc(Doc::Class) => Some(Self::Class(Class::from_definition( + d, + &first.file, + first.start, + ))), + + // TODO parse fields in enum either from desc or from Type::TableField + Type::Doc(Doc::Enum) => Some(Self::Enum(Enum { + file: Some(first.file.clone().into()), + line_number: Some(first.start), + name: d.name.clone(), + desc: d.rawdesc.clone().unwrap_or_default(), + })), + + Type::Doc(Doc::Alias) => Some(Self::Alias(Alias { + file: Some(first.file.clone().into()), + line_number: Some(first.start), + desc: d.rawdesc.clone(), + name: d.name.clone(), + kind: first + .clone() + .extends + .map(|e| Kind::from_string(&e.view)) + .unwrap_or(Kind::Unresolved(d.name.clone())), + })), + + Type::SetField | Type::SetGlobal => { + if let Some(extend) = first.extends.clone() { + match extend.lua_type { + Type::Lua(LuaKind::Function) => Function::from_extend( + extend, + first.file.clone().into(), + d.name.clone(), + d.rawdesc.clone().unwrap_or_default(), + ) + .map(Self::Function), + _ => None, + } + } else { + None + } + } + _ => None, + } + } else { + None + } + } +} + +impl Kind { + fn from_string(s: &str) -> Self { + LuaParser::type_def(s) + } +} + +impl From<&String> for Kind { + fn from(s: &String) -> Self { + LuaParser::type_def(s.as_str()) + } +} +impl From for Kind { + fn from(t: Type) -> Self { + match t { + Type::Lua(lk) => Self::Lua(lk), + _ => Self::Unresolved(format!("{:?}", t)), + } + } +} +impl From for Kind { + fn from(e: Extend) -> Self { + // Self::Unresolved("missing type".to_string()) + if let Some(types) = e.types { + if types.len() > 1 { + Self::Enum(types.iter().map(|t| Self::from_string(&t.view)).collect()) + } else if let Some(first) = types.first() { + if first.view.clone() + "?" == e.view { + Self::from_string(&e.view) + } else { + Self::from_string(&first.view) + } + } else { + Self::from_string(&e.view) + } + } else { + Self::from(e.lua_type) + } + } +} + +impl Var { + fn from_field(field: Field) -> Option { + Some(Self { + file: Some(field.file.clone().into()), + line_number: Some(field.start), + kind: field + .extends + .map(Kind::from) + .unwrap_or(Kind::Unresolved(format!( + "there are no extends on field {}", + field.name.clone() + ))), + name: Some(field.name), + desc: field.rawdesc, + }) + } + fn from_argdef(ad: &ArgDef) -> Option { + Some(Self { + file: None, + line_number: None, + kind: match ad.lua_type { + ArgType::SelfArg => Kind::SelfArg, + ArgType::Local => Kind::from_string(&ad.view), + ArgType::Variadic => Kind::Variadic(Box::new(Kind::from_string(&ad.view))), + }, + name: ad.name.clone(), + desc: None, // desc: ad.desc.unwrap_or_default(), + }) + } + + fn from_return(rd: &ReturnDef) -> Option { + Some(Self { + file: None, + line_number: None, + kind: Kind::from_string(&rd.view), + // kind: match rd.lua_type { + // ArgType::SelfArg => ArgKind::SelfKind, + // ArgType::Local => ArgKind::from_string(rd.view.as_str()), + // ArgType::Variadic => ArgKind::Variadic, + // }, + name: rd.name.clone(), + desc: None, // desc: rd.desc.unwrap_or_default(), + }) + } +} + +impl Function { + fn from_extend(extend: Extend, file: PathBuf, name: String, desc: String) -> Option { + match extend.lua_type { + Type::Lua(LuaKind::Function) => { + let params = extend + .args + .iter() + .filter_map(Var::from_argdef) + .collect::>(); + let returns = extend + .returns + .iter() + .filter_map(Var::from_return) + .collect::>(); + Some(Self { + file: Some(file), + line_number: Some(extend.start), + name: Some(name), + params, + returns, + desc: Some(desc.to_string()).filter(|s| !s.is_empty()), + }) + } + _ => None, + } + } + fn from_field(field: Field) -> Option { + if let Some(extend) = field.extends { + Self::from_extend( + extend, + field.file.into(), + field.name.clone(), + field.rawdesc.unwrap_or_default(), + ) + } else { + None + } + } +} + +impl Class { + fn from_definition(d: &Definition, file: &str, line_number: u32) -> Self { + Self { + file: Some(file.into()), + line_number: Some(line_number), + scope: Scope::from_name(&d.name), + name: d.name.clone(), + fields: d + .fields + .clone() + .into_iter() + .filter(Field::is_field) + .filter_map(Var::from_field) + .collect(), + functions: d + .fields + .clone() + .into_iter() + .filter(Field::is_function) + .filter_map(Function::from_field) + .collect(), + enums: vec![], // enums will get added in Library + constants: vec![], + desc: d.rawdesc.clone().unwrap_or_default(), + } + } +} diff --git a/docs/generate/src/library.rs b/docs/generate/src/library.rs new file mode 100644 index 0000000..0bf71e6 --- /dev/null +++ b/docs/generate/src/library.rs @@ -0,0 +1,309 @@ +use std::{collections::HashMap, path::Path}; + +use crate::{error::Error, json::JsonDoc, types::*}; + +#[derive(Clone)] +pub struct Library { + pub classes: HashMap, + pub enums: HashMap, + pub aliases: HashMap, +} + +impl Library { + /// generate a library from a given root directory or lua file + pub fn from_path(path: &Path) -> Result { + println!("Parsing definitions: '{}'", path.to_string_lossy()); + let mut defs: Vec = vec![]; + let definitions = JsonDoc::get(path)?; + defs.append( + &mut definitions + .iter() + .filter_map(Def::from_definition) + .collect::>(), + ); + Ok(Self::from_defs(defs)) + } + + // a list of classes that correspond to lua types + pub fn builtin_classes() -> Vec { + let self_example = "```lua\nlocal p = pattern.from{1,1,1}\nlocal p2 = p:euclidean(12)\n```"; + vec![ + Self::builtin_class_desc( + "self", + &format!("A type that represents an instance that you call a function on. When you see a function signature starting with this type, you should use `:` to call the function on the instance, this way you can omit this first argument.\n{}", self_example), + ), + Self::builtin_class_desc( + "nil", + "A built-in type representing a non-existant value, [see details](https://www.lua.org/pil/2.1.html). When you see `?` at the end of types, it means they can be nil.", + ), + Self::builtin_class_desc( + "boolean", + "A built-in type representing a boolean (true or false) value, [see details](https://www.lua.org/pil/2.2.html)", + ), + Self::builtin_class_desc( + "number", + "A built-in type representing floating point numbers, [see details](https://www.lua.org/pil/2.3.html)", + ), + Self::builtin_class_desc( + "string", + "A built-in type representing a string of characters, [see details](https://www.lua.org/pil/2.4.html)", + ), + Self::builtin_class_desc("function", "A built-in type representing functions, [see details](https://www.lua.org/pil/2.6.html)"), + Self::builtin_class_desc("table", "A built-in type representing associative arrays, [see details](https://www.lua.org/pil/2.5.html)"), + Self::builtin_class_desc("userdata", "A built-in type representing array values, [see details](https://www.lua.org/pil/28.1.html)."), + Self::builtin_class_desc( + "lightuserdata", + "A built-in type representing a pointer, [see details](https://www.lua.org/pil/28.5.html)", + ), + + Self::builtin_class_desc("integer", "A helper type that represents whole numbers, a subset of [number](number.md)"), + Self::builtin_class_desc( + "any", + "A type for a dynamic argument, it can be anything at run-time.", + ), + Self::builtin_class_desc( + "unknown", + "A dummy type for something that cannot be inferred before run-time.", + ), + ] + } + + fn resolve_string(&self, s: &str) -> Option { + #[allow(clippy::manual_map)] + if let Some(class) = self.classes.get(s) { + Some(Kind::Class(class.clone())) + } else if let Some(alias) = self.aliases.get(s) { + Some(Kind::Alias(Box::new(alias.clone()))) + } else if let Some(enumref) = self.enums.get(s) { + Some(Kind::EnumRef(Box::new(enumref.clone()))) + } else { + None + } + } + + // cross-reference parsed Kinds as existing classes, enums and aliases + fn resolve_kind(&self, kind: &Kind) -> Kind { + match kind.clone() { + Kind::Unresolved(s) => self.resolve_string(&s).unwrap_or(kind.clone()), + Kind::Array(bk) => Kind::Array(Box::new(self.resolve_kind(bk.as_ref()))), + Kind::Nullable(bk) => Kind::Nullable(Box::new(self.resolve_kind(bk.as_ref()))), + Kind::Table(key, value) => Kind::Table( + Box::new(self.resolve_kind(key.as_ref())), + Box::new(self.resolve_kind(value.as_ref())), + ), + Kind::Enum(kinds) => Kind::Enum(kinds.iter().map(|k| self.resolve_kind(k)).collect()), + Kind::Function(f) => { + let mut fun = f.clone(); + self.resolve_function(&mut fun); + Kind::Function(fun) + } + Kind::Variadic(v) => Kind::Variadic(Box::new(self.resolve_kind(v.as_ref()))), + Kind::Object(hm) => { + let mut obj = hm.clone(); + for (key, value) in hm.iter() { + obj.insert(key.clone(), Box::new(self.resolve_kind(value.as_ref()))); + } + Kind::Object(obj) + } + _ => kind.clone(), + } + } + + fn resolve_function(&self, f: &mut Function) { + for p in f.params.iter_mut() { + p.kind = self.resolve_kind(&p.kind) + } + for r in f.returns.iter_mut() { + r.kind = self.resolve_kind(&r.kind) + } + } + + fn resolve_classes(&mut self) { + let l = self.clone(); + for (_, c) in self.classes.iter_mut() { + for f in c.fields.iter_mut() { + f.kind = l.resolve_kind(&f.kind) + } + for f in c.functions.iter_mut() { + l.resolve_function(f) + } + } + } + + // helper to create built-in dummy classes + fn builtin_class_desc(name: &str, desc: &str) -> Class { + Class { + file: None, + line_number: None, + scope: Scope::Builtins, + name: name.to_string(), + desc: desc.to_string(), + fields: vec![], + functions: vec![], + constants: vec![], + enums: vec![], + } + } + + // generate Library from a list of Defs + fn from_defs(defs: Vec) -> Self { + // sort defs into hasmaps of classes, enums and aliases + let mut classes = HashMap::new(); + let mut enums = HashMap::new(); + let mut aliases = HashMap::new(); + let mut dangling_functions = vec![]; + for d in defs.iter() { + match d { + Def::Alias(a) => { + aliases.insert(a.name.clone(), a.clone()); + } + Def::Enum(e) => { + enums.insert(e.name.clone(), e.clone()); + } + Def::Class(c) => { + classes.insert(c.name.clone(), c.clone()); + } + Def::Function(f) => dangling_functions.push(f.clone()), + } + } + + // HACK: manually remove a few classes + classes.retain(|name, _| !["TimeContext", "TriggerContext"].contains(&name.as_str())); + + let mut library = Self { + classes, + enums, + aliases, + }; + + // transform any unresolved Kind to the appropriate classe or alias + // by cross referencing the hashmaps of the library + library.resolve_classes(); + let mut aliases = library.aliases.clone(); + aliases + .iter_mut() + .for_each(|(_, a)| a.kind = library.resolve_kind(&a.kind)); + library.aliases = aliases; + dangling_functions + .iter_mut() + .for_each(|f| library.resolve_function(f)); + + // assign enums to their respective classes + for (k, e) in library.enums.iter() { + let base = Class::get_base(k); + if let Some(base) = base { + if let Some(class) = library.classes.get_mut(base) { + class.enums.push(e.clone()) + } + } + } + + // add globl functions to new or existing classes + for f in dangling_functions.iter_mut() { + let name = &f.name.clone().unwrap_or_default(); + let base = Class::get_base(name).unwrap_or("global"); + let mut class_name = base.to_string(); + if class_name == "global" { + if let Some(file) = &f.file { + let file_stem = file.file_stem().map(|f| f.to_string_lossy()); + class_name = format!("{} globals", file_stem.unwrap()).to_string(); + } + } + if let Some((_, class)) = library + .classes + .iter_mut() + .find(|(name, c)| *name == &class_name && c.file == f.file) + { + class.functions.push(f.strip_base()) + } else { + library.classes.insert( + class_name, + Class { + file: f.file.clone(), + line_number: f.line_number, + scope: Scope::from_name(base), + name: base.to_string(), + functions: vec![f.strip_base()], + fields: vec![], + enums: vec![], + constants: vec![], + // TODO the description should end up here from bit, os etc + desc: String::new(), + }, + ); + } + } + + // extract constants, make functions, fields and constants unique and sort them + for (_, c) in library.classes.iter_mut() { + let mut functions = c.functions.clone(); + functions.sort_by(|a, b| { + a.line_number + .unwrap_or_default() + .cmp(&b.line_number.unwrap_or_default()) + }); + + let mut enums = c.enums.clone(); + enums.sort_by(|a, b| { + a.line_number + .unwrap_or_default() + .cmp(&b.line_number.unwrap_or_default()) + }); + + let mut fields = c + .fields + .clone() + .into_iter() + .filter(Var::is_not_constant) + .collect::>(); + fields.sort_by(|a, b| { + a.line_number + .unwrap_or_default() + .cmp(&b.line_number.unwrap_or_default()) + }); + + let mut constants = c + .fields + .clone() + .into_iter() + .filter(Var::is_constant) + .collect::>(); + constants.sort_by(|a, b| a.name.cmp(&b.name)); + + c.functions = functions; + c.fields = fields; + c.enums = enums; + c.constants = constants; + } + + // debug print everything that includes some unresolved Kind or is empty + println!("classes:"); + for c in library.classes.values() { + let is_empty = library.classes.get(&c.name).is_some_and(|v| v.is_empty()); + let unresolved = c.has_unresolved(); + + if is_empty || unresolved { + println!(" {}", c.name); + } + if unresolved { + println!("{}\n", c.show()); + } + if is_empty { + println!(" \x1b[33m^--- has no fields, methods or enums\x1b[0m") + } + } + println!("aliases:"); + for a in library.aliases.values() { + if a.kind.has_unresolved() { + println!(" {}", a.name); + println!("\n{}\n", a.show()); + } + } + // println!("enums"); + // for e in l.enums.values() { + // println!(" {}", e.name); + // } + + library + } +} diff --git a/docs/generate/src/lua_parser.pest b/docs/generate/src/lua_parser.pest new file mode 100644 index 0000000..1769566 --- /dev/null +++ b/docs/generate/src/lua_parser.pest @@ -0,0 +1,63 @@ +WHITESPACE = _{ " " | "\t" | "\u{A0}" | "\n" } +W = _{ WHITESPACE* } + +lua_type = ${("integer" | "number" | "string" | "table" | "nil" | "any" | "boolean" | "function" | "userdata" | "unknown" | "fun()") ~ !(ASCII_ALPHANUMERIC)} + +valid_name = @{ASCII_ALPHA ~ (ASCII_ALPHANUMERIC | "_" )*} + +id = @{ valid_name ~ ("." ~ valid_name)*} +string = { (!"\"" ~ ANY)* } +string_literal = ${"\"" ~ string ~ "\""} +number_literal = ${ ASCII_DIGIT+ } + +_value = _{ number_literal | string_literal } + +_simple = _{ lua_type | _value | id } + + +_key = _{("[" ~ number_literal ~ "]") | ("[" ~ "string" ~ "]") | id} +field = ${ _key ~ W ~ ":" ~ W ~ _enum_or_complex } + +object = ${"{" ~ W ~ field ~ W ~ ("," ~ W ~ field )* ~ W ~ "}"} + +table_type = { "table" ~ "<" ~ complex_type ~ ", " ~ _enum_or_complex ~ ">"} + +array_tail = {"[]"} +nullable_tail = {"?"} + +_types = _{ fun | function | method | table_type | object | _grouped | _simple } + +complex_type = ${ _types ~ (nullable_tail | array_tail)* } + +enumeration = { complex_type ~ ("|" ~ complex_type)+ } + +_enum_or_complex = _{ enumeration | complex_type } + +_grouped = _{ "(" ~ _enum_or_complex ~ ")" } + +named_var = ${ valid_name ~ nullable_tail? ~ W ~ ":" ~ W ~ _enum_or_complex} + +_arg = _{named_var | (valid_name ~ nullable_tail?)} +vararg = ${"..." ~ W ~ _enum_or_complex} + +_return_type = _{ named_var | _enum_or_complex } +returns = ${_return_type ~ W ~ ("," ~ W ~ _return_type)*} + +// TODO varargs should only be allowed at the end and only once +args = ${ "(" ~ (vararg | (_arg ~ W ~ ("," ~ W ~ (_arg | vararg))*))? ~ ")"} + +// "fun(x: integer, ...integer): integer, string" +fun = {"fun" ~ args ~ ( ":" ~ returns )?} + +// parse LUALS views for functions and methods +// if this was a pure lua-doc parser these wouldn't be here + +// "function bit.bnot(x: integer, ...integer)\n -> integer" +function = {"function" ~ " " ~ id ~ args ~ ("\n" ~ "->" ~ returns)?} + +// "(method) renoise.Song:pattern(index: integer, ...integer)\n -> renoise.Pattern" +method = ${"(method)" ~ W ~ id ~ ":" ~ valid_name ~ args ~ ("\n" ~ W ~ "->" ~ W ~ returns)?} + +// -------------------------------------------------------- + +type_def = { SOI ~ _enum_or_complex ~ EOI } \ No newline at end of file diff --git a/docs/generate/src/lua_parser.rs b/docs/generate/src/lua_parser.rs new file mode 100644 index 0000000..f026b9c --- /dev/null +++ b/docs/generate/src/lua_parser.rs @@ -0,0 +1,301 @@ +use std::collections::HashMap; + +use pest::{iterators::Pair, Parser}; +use pest_derive::Parser; + +use crate::types::{Function, Kind, LuaKind, Var}; + +#[derive(Parser)] +#[grammar = "./lua_parser.pest"] +pub struct LuaParser {} +impl LuaParser { + /// parse a string into a type definition of Kind + pub fn type_def(input: &str) -> Kind { + match Self::parse(Rule::type_def, input) { + Ok(mut pairs) => { + let next = pairs.next().unwrap(); + let pair = next.into_inner().next().unwrap(); + Self::kind(pair) + } + Err(err) => { + // warn about parse errors + println!("\x1b[33m{}\x1b[0m", err); + Kind::Unresolved(input.to_string()) + } + } + } + + fn as_string(pair: &Pair) -> String { + pair.as_span().as_str().to_string() + } + + fn named_var(pair: Pair) -> Var { + let mut inner = pair.into_inner(); + let name = Some(Self::as_string(&inner.next().unwrap())); + let next = inner.next().unwrap(); + match next.as_rule() { + Rule::nullable_tail => Var { + file: None, + line_number: None, + name, + kind: Kind::Nullable(Box::new(Self::kind(inner.next().unwrap()))), + desc: None, + }, + _ => Var { + file: None, + line_number: None, + name, + kind: Self::kind(next), + desc: None, + }, + } + } + + fn returns(pair: Option>) -> Vec { + let mut returns: Vec = vec![]; + if let Some(pair) = pair { + for p in pair.into_inner() { + returns.push(match p.as_rule() { + Rule::named_var => Self::named_var(p), + Rule::enumeration | Rule::complex_type => Var { + file: None, + line_number: None, + kind: Self::kind(p), + name: None, + desc: None, + }, + _ => unreachable!(), + }) + } + } + returns + } + + fn args(pair: Pair) -> Vec { + let mut params: Vec = vec![]; + for arg in pair.into_inner() { + match arg.as_rule() { + Rule::named_var => params.push(Self::named_var(arg)), + Rule::valid_name => params.push(Var { + file: None, + line_number: None, + name: Some(Self::as_string(&arg)), + kind: Kind::Lua(LuaKind::Any), + desc: None, + }), + Rule::nullable_tail => { + if let Some(last) = params.last_mut() { + last.kind = Kind::Nullable(Box::new(last.kind.clone())) + } + } + Rule::vararg => params.push(Var { + file: None, + line_number: None, + name: None, + kind: Kind::Variadic(Box::new(Self::kind(arg.into_inner().next().unwrap()))), + desc: None, + }), + _ => unreachable!(), + }; + } + params + } + + fn kind(pair: Pair) -> Kind { + let s = Self::as_string(&pair); + match pair.as_rule() { + Rule::id => Kind::Unresolved(s), + Rule::string_literal => Kind::Literal( + Box::new(LuaKind::String), + Self::as_string(&pair.into_inner().next().unwrap()), + ), + Rule::number_literal => Kind::Literal(Box::new(LuaKind::Integer), s), + Rule::lua_type => { + match serde_json::from_str::( + format!("\"{}\"", pair.as_span().as_str()).as_str(), + ) { + Ok(lk) => Kind::Lua(lk), + Err(_) => Kind::Unresolved(s), + } + } + Rule::complex_type => { + let mut inner = pair.into_inner(); + let mut k = Self::kind(inner.next().unwrap()); + for tail in inner { + match tail.as_rule() { + Rule::array_tail => k = Kind::Array(Box::new(k)), + Rule::nullable_tail => k = Kind::Nullable(Box::new(k)), + _ => (), + } + } + k + } + Rule::enumeration => Kind::Enum(pair.into_inner().map(|p| Self::kind(p)).collect()), + Rule::table_type => { + let mut inner = pair.into_inner(); + let key = inner.next().unwrap(); + let value = inner.next().unwrap(); + Kind::Table(Box::new(Self::kind(key)), Box::new(Self::kind(value))) + } + Rule::object => { + let inner = pair.into_inner(); + let mut fields = HashMap::new(); + for f in inner { + let mut fi = f.into_inner(); + let key = fi.next().unwrap(); + if let Some(t) = fi.next() { + fields.insert( + key.as_span().as_str().to_string(), + Box::new(Self::kind(t.clone())), + ); + } + } + Kind::Object(fields) + } + Rule::function => { + let mut inner = pair.into_inner(); + let file = None; + let line_number = None; + let name = Some(Self::as_string(&inner.next().unwrap())); + let params = Self::args(inner.next().unwrap()); + let returns = Self::returns(inner.next()); + Kind::Function(Function { + file, + line_number, + name, + params, + returns, + desc: None, + }) + } + Rule::fun => { + let mut inner = pair.into_inner(); + let file = None; + let line_number = None; + let params = Self::args(inner.next().unwrap()); + let returns = Self::returns(inner.next()); + Kind::Function(Function { + file, + line_number, + params, + returns, + name: None, + desc: None, + }) + } + Rule::method => { + let mut inner = pair.into_inner(); + let _parent = Self::as_string(&inner.next().unwrap()); + let file = None; + let line_number = None; + let name = Some(Self::as_string(&inner.next().unwrap())); + let params = Self::args(inner.next().unwrap()); + let returns = Self::returns(inner.next()); + Kind::Function(Function { + file, + line_number, + name, + params, + returns, + desc: None, + }) + } + _ => { + println!("{:?}", pair.as_rule()); + unreachable!() + } + } + } +} + +#[cfg(test)] +mod test { + use super::*; + + fn assert_type(input: &str, k: Kind) -> Result<(), String> { + assert_eq!(LuaParser::type_def(input), k); + Ok(()) + } + + fn var>>(name: N, kind: Kind) -> Var { + let name = name.into(); + Var { + file: None, + line_number: None, + name, + kind, + desc: None, + } + } + + fn string_literal(s: &str) -> Kind { + Kind::Literal(Box::new(LuaKind::String), s.to_string()) + } + + #[test] + pub fn parse() -> Result<(), String> { + assert_type("integer", Kind::Lua(LuaKind::Integer))?; + assert_type( + "integer[]", + Kind::Array(Box::new(Kind::Lua(LuaKind::Integer))), + )?; + assert_type("\"a\"", string_literal("a"))?; + assert_type( + "\"a\"|\"b\"", + Kind::Enum(vec![string_literal("a"), string_literal("b")]), + )?; + assert_type( + "(\"a\"?|\"b\"[])", + Kind::Enum(vec![ + Kind::Nullable(Box::new(string_literal("a"))), + Kind::Array(Box::new(string_literal("b"))), + ]), + )?; + assert_type( + "(\"a\"?|\"b\")?", + Kind::Nullable(Box::new(Kind::Enum(vec![ + Kind::Nullable(Box::new(string_literal("a"))), + string_literal("b"), + ]))), + )?; + assert_type( + "integer[][]", + Kind::Array(Box::new(Kind::Array(Box::new(Kind::Lua(LuaKind::Integer))))), + )?; + assert_type( + "function a.b.c(i: integer, ...string)", + Kind::Function(Function { + file: None, + line_number: None, + name: Some(String::from("a.b.c")), + params: vec![ + var("i".to_string(), Kind::Lua(LuaKind::Integer)), + var(None, Kind::Variadic(Box::new(Kind::Lua(LuaKind::String)))), + ], + returns: vec![], + desc: None, + }), + )?; + assert_type( + "(method) renoise.Song:test(a: integer, b:integer|string?)", + Kind::Function(Function { + file: None, + line_number: None, + name: Some(String::from("test")), + params: vec![ + var("a".to_string(), Kind::Lua(LuaKind::Integer)), + var( + "b".to_string(), + Kind::Enum(vec![ + Kind::Lua(LuaKind::Integer), + Kind::Nullable(Box::new(Kind::Lua(LuaKind::String))), + ]), + ), + ], + returns: vec![], + desc: None, + }), + )?; + Ok(()) + } +} diff --git a/docs/generate/src/main.rs b/docs/generate/src/main.rs new file mode 100644 index 0000000..3f9b189 --- /dev/null +++ b/docs/generate/src/main.rs @@ -0,0 +1,138 @@ +use std::{ + fs::*, + io::{Read, Write}, + path::{Path, PathBuf}, +}; + +use clap::Parser as ClapParser; + +mod error; +mod json; +mod json_to_types; +mod library; +mod lua_parser; +mod render; +mod types; + +use error::Error; +use library::Library; + +#[derive(ClapParser, Debug)] +#[command(version, about, long_about = None)] +struct Args { + library: PathBuf, + output: PathBuf, +} + +#[derive(Clone, Debug)] +struct TocEntry { + file_path: String, + file_name: String, + link: String, +} + +impl TocEntry { + fn from(name: &str) -> Self { + let mut file_name = name.to_string(); + let mut display_name = name.to_string(); + let mut file_path = String::new(); + let mut level = String::from(" "); + if name.contains('/') { + let full_name = file_name.clone(); + let mut splits = full_name.split('/').collect::>(); + level = " ".repeat(splits.len()); + file_name = splits.remove(splits.len() - 1).to_string(); + display_name.clone_from(&file_name); + file_path = splits + .into_iter() + .map(String::from) + .collect::>() + .join("/"); + file_path.push('/'); + } else { + display_name = match name { + "builtins" => "Builtin Types".to_string(), + "modules" => "Module Extensions".to_string(), + "structs" => "Helper Types".to_string(), + "global" => "Globals".to_string(), + _ => name.to_string(), + } + } + let link = format!( + "{}- [{}](API/{}{}.md)", + level, display_name, file_path, file_name + ) + .to_string(); + Self { + file_path, + file_name, + link, + } + } +} + +fn replace_toc_in_file(file_path: &Path, toc: &[String]) -> Result<(), Error> { + let mut file = File::open(file_path)?; + let mut content = String::new(); + file.read_to_string(&mut content)?; + drop(file); + + let mut lines = content.lines().collect::>(); + let toc_start_line = lines + .iter() + .enumerate() + .find(|(_i, l)| l.contains("")) + .expect("Failed to locate line in Summary.md") + .0; + let toc_end_line = lines + .iter() + .enumerate() + .find(|(_i, l)| l.contains("")) + .expect("Failed to locate line in Summary.md") + .0; + lines.splice( + (toc_start_line + 1)..toc_end_line, + toc.iter().map(String::as_str), + ); + + let mut file = File::create(file_path)?; + file.write_all(lines.join("\n").as_bytes())?; + Ok(()) +} + +fn main() -> Result<(), Error> { + let args = Args::parse(); + let lib = Library::from_path(&args.library)?; + let docs = lib.export_docs(); + + // clear previously generated API doc files (except README.md) + let api_path = args.output.clone().join("API"); + for entry in read_dir(&api_path)?.flatten() { + if entry.path().is_dir() { + remove_dir_all(entry.path())?; + } else if entry + .path() + .file_name() + .is_some_and(|file| !file.eq_ignore_ascii_case("README.md")) + { + remove_file(entry.path())?; + } + } + + // write all documents from the library to a file using their names and create an API TOC + let mut toc_links = vec![]; + for (name, content) in &docs { + let toc_entry = TocEntry::from(name); + toc_links.push(toc_entry.link); + let dir_path = api_path.clone().join(toc_entry.file_path.clone()); + let file_path = dir_path.clone().join(toc_entry.file_name + ".md"); + if !dir_path.exists() { + create_dir(dir_path)?; + } + println!("Creating '{}'", file_path.to_string_lossy()); + let mut file = File::create(file_path)?; + file.write_all(content.as_bytes())?; + } + replace_toc_in_file(&args.output.clone().join("SUMMARY.md"), &toc_links)?; + Ok(()) +} diff --git a/docs/generate/src/render.rs b/docs/generate/src/render.rs new file mode 100644 index 0000000..d7bbddb --- /dev/null +++ b/docs/generate/src/render.rs @@ -0,0 +1,454 @@ +use std::{ + collections::HashMap, + path::{Path, PathBuf}, +}; + +use itertools::Itertools; + +use crate::{library::Library, types::*}; + +impl Library { + /// render each page inside the library as a list of string tuples (name, content) + pub fn export_docs(&self) -> Vec<(String, String)> { + // split classes into globals and modules + let mut globals = vec![]; + for (path, classes) in self.classes_by_file_in_scopes(&[Scope::Global, Scope::Local]) { + if path.to_string_lossy().is_empty() { + // skip classes which have no file path (Lua internals) + continue; + } + let file_stem = path + .file_stem() + .map(|v| v.to_string_lossy()) + .expect("expecting class to have a valid source file path"); + let mut content = String::new(); + content.push_str("\n\n\n"); + + for class in Self::sort_classes(classes) { + let url_root = "../"; + content.push_str(&class.render(url_root, &self.aliases)); + content.push_str("\n\n"); + } + globals.push((file_stem.to_string(), content)); + } + + let mut modules = vec![]; + for class in self.classes_in_scopes(&[Scope::Modules]) { + let url_root = "../../"; + let content = class.render(url_root, &self.aliases); + modules.push((String::from("modules/") + &class.name, content)); + } + + // add builtin classes + let mut builtins = vec![]; + for class in Library::builtin_classes() { + let url_root = "../../"; + let content = class.render(url_root, &self.aliases); + builtins.push((String::from("builtins/") + &class.name, content)); + } + + // create final docs + let mut docs: Vec<(String, String)> = vec![]; + docs.append(&mut globals); + + if !modules.is_empty() { + docs.push(("modules".to_string(), "# Lua Module Extensions".to_string())); + docs.append(&mut modules); + } + if !builtins.is_empty() { + docs.push(("builtins".to_string(), "# Lua Builtin Types".to_string())); + docs.append(&mut builtins); + } + docs = docs + .iter() + .unique_by(|(name, _)| name.to_ascii_lowercase()) + .cloned() + .collect::>(); + Self::sort_docs(docs) + } + + fn classes_in_scopes(&self, scopes: &[Scope]) -> Vec { + self.classes + .values() + .filter(|&c| scopes.contains(&c.scope)) + .cloned() + .collect() + } + + fn classes_by_file_in_scopes(&self, scopes: &[Scope]) -> HashMap> { + let mut map = HashMap::>::new(); + for class in self.classes_in_scopes(scopes) { + let file = class.file.clone().unwrap_or_default(); + if let Some(classes) = map.get_mut(&file) { + classes.push(class.clone()); + } else { + map.insert(file.clone(), vec![class.clone()]); + } + } + map + } + + fn sort_classes(mut classes: Vec) -> Vec { + let custom_weight = |name: &str| -> usize { + if name == "global" { + 0 + } else if name.ends_with("Context") { + 99 + } else { + 1 + } + }; + classes.sort_by_key(|class| (custom_weight(&class.name), class.name.to_lowercase())); + classes + } + + fn sort_docs(mut docs: Vec<(String, String)>) -> Vec<(String, String)> { + let custom_weight = |name: &str| -> usize { + if name == "global" { + 0 + } else if name.starts_with("modules") { + 99 + } else if name.starts_with("builtins") { + 100 + } else { + 10 + } + }; + docs.sort_by_key(|(name, _)| (custom_weight(name), name.to_lowercase())); + docs + } +} + +fn heading(text: &str, level: usize) -> String { + format!("{} {}", "#".repeat(level), text) +} + +fn h1(text: &str) -> String { + heading(text, 1) +} + +fn h2(text: &str) -> String { + heading(text, 2) +} + +fn h3(text: &str) -> String { + heading(text, 3) +} + +fn file_link(text: &str, url: &str) -> String { + format!("[`{}`]({}.md)", text, url) +} + +fn class_link(text: &str, url: &str, hash: &str) -> String { + format!("[`{}`]({}.md#{})", text, url, hash) +} + +fn enum_link(text: &str, url: &str, hash: &str) -> String { + format!("[`{}`]({}.md#{})", text, url, hash) +} + +fn alias_link(text: &str, hash: &str) -> String { + format!("[`{}`](#{})", text, hash) +} + +fn quote(text: &str) -> String { + format!("> {}", text.replace('\n', "\n> ")) +} + +fn description(desc: &str) -> String { + quote( + desc.replace("### examples", "#### examples") + .trim_matches('\n'), + ) +} + +// fn item(text: &str) -> String { +// format!("* {}", text) +// } +// fn italic(text: &str) -> String { +// format!("*{}*", text) +// } + +fn hash(text: &str, hash: &str) -> String { + format!("{} {{#{}}}", text, hash) +} + +impl LuaKind { + fn link(&self, url_root: &str) -> String { + let text = self.show(); + file_link(&text, &(format!("{}API/builtins/", url_root) + &text)) + } +} + +impl Kind { + fn link(&self, url_root: &str, file: &Path) -> String { + match self { + Kind::Lua(lk) => lk.link(url_root), + Kind::Literal(k, s) => match k.as_ref() { + LuaKind::String => format!("`\"{}\"`", s), + LuaKind::Integer | LuaKind::Number => format!("`{}`", s.clone()), + _ => s.clone(), + }, + Kind::Class(class) => { + if matches!(class.scope, Scope::Global | Scope::Local) { + let file = class.file.clone().unwrap_or(PathBuf::new()); + let file_stem = file + .file_stem() + .map(|v| v.to_string_lossy()) + .unwrap_or("[unknown file]".into()); + class_link( + &class.name, + &(url_root.to_string() + &class.scope.path_prefix() + &file_stem), + &class.name, + ) + } else { + file_link( + &class.name, + &(url_root.to_string() + &class.scope.path_prefix() + &class.name), + ) + } + } + Kind::Enum(kinds) => kinds + .iter() + .map(|k| k.link(url_root, file)) + .collect::>() + .join(" | "), + Kind::EnumRef(enumref) => { + let file = enumref.file.clone().unwrap_or(PathBuf::new()); + let file_stem = file + .file_stem() + .map(|v| v.to_string_lossy()) + .unwrap_or("[unknown file]".into()); + enum_link( + &enumref.name, + &(url_root.to_string() + &Scope::Global.path_prefix() + &file_stem), + &enumref.name, + ) + } + Kind::SelfArg => format!("[*self*]({}API/builtins/self.md)", url_root), + Kind::Array(k) => format!("{}[]", k.link(url_root, file)), + Kind::Nullable(k) => format!( + "{}{}", + k.as_ref().link(url_root, file), + file_link("?", &format!("{}API/builtins/nil", url_root)) + ), + Kind::Alias(alias) => alias_link(&alias.name, &alias.name), + Kind::Function(f) => f.short(url_root, file), + Kind::Table(k, v) => format!( + "table<{}, {}>", + k.as_ref().link(url_root, file), + v.as_ref().link(url_root, file) + ), + Kind::Object(hm) => { + let mut keys = hm.iter().map(|(k, _)| k.clone()).collect::>(); + keys.sort(); + let fields = keys + .iter() + .map(|k| format!("{} : {}", k, hm.get(k).unwrap().link(url_root, file))) + .collect::>() + .join(", "); // TODO print on newlines? + format!("{{ {} }}", fields) + } + Kind::Variadic(k) => format!("...{}", k.link(url_root, file)), + Kind::Unresolved(s) => s.clone(), + } + } +} + +impl Var { + fn short(&self, url_root: &str, file: &Path) -> String { + if matches!(self.kind, Kind::SelfArg) { + self.kind.link(url_root, file) + } else if let Some(name) = self.name.clone() { + format!("{} : {}", name, self.kind.link(url_root, file)) + } else { + self.kind.link(url_root, file) + } + } + fn long(&self, url_root: &str, file: &Path) -> String { + let desc = self.desc.clone().unwrap_or_default(); + format!( + "{}{}", + hash( + &h3(&self.short(url_root, file)), + &self.name.clone().unwrap_or_default() + ), + if desc.is_empty() { + desc + } else { + format!("\n{}\n", description(&desc)) + } + ) + } +} + +impl Alias { + fn render(&self, url_root: &str, file: &Path) -> String { + format!( + "{}\n{} \n{}", + hash(&h3(&self.name), &self.name), + self.kind.link(url_root, file), + self.desc + .clone() + .map(|d| description(d.as_str())) + .unwrap_or_default() + ) + } +} + +impl Function { + fn long(&self, url_root: &str, file: &Path) -> String { + let name = self.name.clone().unwrap_or("fun".to_string()); + if self.params.is_empty() { + let name = hash(&h3(&format!("`{}()`", &name)), &name); + self.with_desc(&self.with_returns(&name, url_root, file)) + } else { + let params = self + .params + .iter() + .map(|v| v.short(url_root, file)) + .collect::>() + .join(", "); + + self.with_desc(&self.with_returns( + &hash(&format!("### {}({})", &name, params), &name), + url_root, + file, + )) + } + } + fn short(&self, url_root: &str, file: &Path) -> String { + if self.params.is_empty() && self.returns.is_empty() { + return self.empty(); + } + let returns = Self::render_vars(&self.returns, url_root, file); + format!( + "{}({}){}", + &self.name.clone().unwrap_or_default(), + Self::render_vars(&self.params, url_root, file), + if returns.is_empty() { + returns + } else { + format!(" `->` {}", returns) + } + ) + } + fn empty(&self) -> String { + format!("{}()", &self.name.clone().unwrap_or("fun".to_string())) + } + fn render_vars(vars: &[Var], url_root: &str, file: &Path) -> String { + vars.iter() + .map(|v| v.short(url_root, file)) + .collect::>() + .join(", ") + } + fn with_desc(&self, head: &str) -> String { + let desc = self.desc.clone().unwrap_or_default(); + if desc.is_empty() { + head.to_string() + } else { + format!("{}\n{}", head, description(&desc)) + } + } + fn with_returns(&self, head: &str, url_root: &str, file: &Path) -> String { + let returns = self + .returns + .iter() + .map(|v| v.short(url_root, file)) + .collect::>() + .join(", "); + if returns.is_empty() { + head.to_string() + } else { + format!("{}\n`->`{} \n", head, returns) + } + } +} + +impl Class { + fn render(&self, url_root: &str, aliases: &HashMap) -> String { + let name = if self.name == "global" { + "Global" + } else { + &self.name + }; + let mut content = vec![h1(&hash(name, name))]; + + if !self.desc.is_empty() { + content.push(description(&self.desc)) + } + + if !self.enums.is_empty() || !self.constants.is_empty() { + let enums = &self.enums; + let constants = &self.constants; + content.push(format!( + "{}\n{}\n{}", + h2("Constants"), + enums + .iter() + .map(|e| { + let name = e.name.clone(); + let end = Class::get_end(&name).unwrap_or(&name); + format!("{}\n{}", hash(&h3(end), end), description(&e.desc)) + }) + .collect::>() + .join("\n"), + constants + .iter() + .map(|v| v.long(url_root, &self.file.clone().unwrap_or_default())) + .collect::>() + .join("\n") + )) + } + + if !self.fields.is_empty() { + content.push("\n---".to_string()); + content.push(format!( + "{}\n{}\n", + h2("Properties"), + self.fields + .iter() + .map(|v| v.long(url_root, &self.file.clone().unwrap_or_default())) + .collect::>() + .join("\n") + )) + } + + let functions = &self.functions; + if !functions.is_empty() { + content.push("\n---".to_string()); + content.push(format!( + "{}\n{}", + h2("Functions"), + functions + .iter() + .map(|f| f.long(url_root, &self.file.clone().unwrap_or_default())) + .collect::>() + .join("\n") + )) + } + + // append used local aliases + let local_alias_names = self.collect_local_aliases(aliases); + if !local_alias_names.is_empty() { + content.push("\n\n\n---".to_string()); + content.push(h2("Aliases")); + let mut alias_names: Vec<&String> = aliases.keys().collect(); + alias_names.sort(); + for name in alias_names { + if local_alias_names.contains(name) { + content.push( + aliases + .get(name) + .unwrap() + .render(url_root, &self.file.clone().unwrap_or_default()), + ); + content.push(String::new()); + } + } + } + + content.push("\n".to_string()); + content.join(" \n") + } +} diff --git a/docs/generate/src/types.rs b/docs/generate/src/types.rs new file mode 100644 index 0000000..e611e14 --- /dev/null +++ b/docs/generate/src/types.rs @@ -0,0 +1,550 @@ +use std::{ + collections::{HashMap, HashSet}, + fmt, + path::PathBuf, +}; + +use serde::{Deserialize, Serialize}; + +/// enum for possible lua-ls built-in types +#[derive(Debug, PartialEq, Serialize, Deserialize, Clone, Eq, PartialOrd, Ord)] +#[serde(rename_all = "lowercase")] +pub enum LuaKind { + Nil, + Unknown, + Any, + Boolean, + String, + Number, + Integer, + Function, + Table, + Thread, + UserData, + Binary, + LightUserData, +} + +/// enum for complex custom types +#[derive(Debug, Clone, PartialEq)] +pub enum Kind { + Unresolved(String), + Lua(LuaKind), + Array(Box), + Nullable(Box), + Table(Box, Box), + Object(HashMap>), + Alias(Box), + Class(Class), + Function(Function), + Enum(Vec), + EnumRef(Box), + SelfArg, + Variadic(Box), + Literal(Box, String), +} + +/// a definition alias, rendered as a doc page +#[derive(Debug, Clone, PartialEq)] +pub struct Alias { + pub file: Option, + pub line_number: Option, + pub name: String, + pub kind: Kind, + pub desc: Option, +} + +/// variable definition used in fields and params and returns of functions +#[derive(Debug, Clone, PartialEq)] +pub struct Var { + pub file: Option, + pub line_number: Option, + pub name: Option, + pub kind: Kind, + pub desc: Option, + // pub default: String, + // pub range: String +} + +/// function definition for methods, functions and lambdas +#[derive(Debug, Clone, PartialEq, Default)] +pub struct Function { + pub file: Option, + pub line_number: Option, + pub name: Option, + pub params: Vec, + pub returns: Vec, + pub desc: Option, + // pub overloads: ? +} + +/// enumeration attached to classes +/// self.desc contains a code block string with the values +#[derive(Debug, Clone, PartialEq)] +pub struct Enum { + pub file: Option, + pub line_number: Option, + pub name: String, + pub desc: String, +} + +/// scope of a class item +#[derive(Debug, Clone, PartialEq)] +pub enum Scope { + Global, + Local, + Builtins, + Modules, +} + +/// class definition, rendered as a doc page +#[derive(Debug, Clone, PartialEq)] +pub struct Class { + pub file: Option, + pub line_number: Option, + pub scope: Scope, + pub name: String, + pub fields: Vec, + pub functions: Vec, + pub enums: Vec, + pub constants: Vec, + pub desc: String, +} + +/// a wrapper for all top-level types coming from the json +#[derive(Debug, Clone, PartialEq)] +pub enum Def { + Class(Class), + Enum(Enum), + Alias(Alias), + Function(Function), +} + +impl Kind { + #[allow(unused)] + pub fn collect_local_class_types(&self) -> HashSet { + let mut types = HashSet::new(); + match self { + Kind::Unresolved(_) => {} + Kind::Lua(_lua_kind) => {} + Kind::Array(item) => { + types.extend(item.collect_local_class_types()); + } + Kind::Nullable(item) => { + types.extend(item.collect_local_class_types()); + } + Kind::Table(key, value) => { + types.extend(key.collect_local_class_types()); + types.extend(value.collect_local_class_types()); + } + Kind::Object(map) => { + for kind in map.values() { + types.extend(kind.collect_local_class_types()); + } + } + Kind::Alias(alias) => {} + Kind::Class(class) => { + if class.scope == Scope::Local { + types.insert(class.name.clone()); + } + types.extend(class.collect_local_class_types()); + } + Kind::Function(func) => { + for ret in &func.returns { + types.extend(ret.kind.collect_local_class_types()); + } + for param in &func.params { + types.extend(param.kind.collect_local_class_types()); + } + } + Kind::Enum(kinds) => { + for kind in kinds { + types.extend(kind.collect_local_class_types()); + } + } + Kind::EnumRef(_) => {} + Kind::SelfArg => {} + Kind::Variadic(item) => { + types.extend(item.collect_local_class_types()); + } + Kind::Literal(_lua_kind, _) => {} + } + types + } + + pub fn collect_alias_types(&self) -> HashSet { + let mut types = HashSet::new(); + match self { + Kind::Unresolved(_name) => {} + Kind::Lua(_lua_kind) => {} + Kind::Array(kind) => { + types.extend(kind.collect_alias_types()); + } + Kind::Nullable(item) => { + types.extend(item.collect_alias_types()); + } + Kind::Table(key, value) => { + types.extend(key.collect_alias_types()); + types.extend(value.collect_alias_types()); + } + Kind::Object(map) => { + for kind in map.values() { + types.extend(kind.collect_alias_types()); + } + } + Kind::Alias(alias) => { + types.insert(alias.name.clone()); + } + Kind::Class(class) => { + types.extend(class.collect_alias_types()); + } + Kind::Function(function) => { + for ret in &function.returns { + types.extend(ret.kind.collect_alias_types()); + } + for param in &function.params { + types.extend(param.kind.collect_alias_types()); + } + } + Kind::Enum(kinds) => { + for kind in kinds { + types.extend(kind.collect_alias_types()); + } + } + Kind::EnumRef(_enumref) => {} + Kind::SelfArg => {} + Kind::Variadic(item) => { + types.extend(item.collect_alias_types()); + } + Kind::Literal(_lua_kind, _) => {} + } + types + } +} + +impl Var { + pub fn is_constant(&self) -> bool { + self.name + .as_ref() + .is_some_and(|name| name.chars().all(|c| c.is_uppercase() || c == '_')) + } + + pub fn is_not_constant(&self) -> bool { + !self.is_constant() + } +} + +impl Function { + pub fn strip_base(&self) -> Self { + if let Some(name) = &self.name { + Self { + name: Some( + Class::get_end(name) + .map(|n| n.to_string()) + .unwrap_or(self.name.clone().unwrap_or_default()), + ), + ..self.clone() + } + } else { + self.clone() + } + } +} + +impl Scope { + pub fn from_name(name: &str) -> Self { + if ["global", "pattern"].contains(&name) { + Scope::Global + } else if ["bit", "debug", "io", "math", "os", "table"].contains(&name) { + Scope::Modules + } else { + Scope::Local + } + } + + pub fn path_prefix(&self) -> String { + match self { + Scope::Global | Scope::Local => String::from("API/"), + Scope::Builtins => String::from("API/builtins/"), + Scope::Modules => String::from("API/modules/"), + } + } +} + +impl Class { + pub fn get_base(s: &str) -> Option<&str> { + s.rfind('.').map(|pos| &s[..pos]) + } + + pub fn get_end(s: &str) -> Option<&str> { + s.rfind('.').map(|pos| &s[pos + 1..]) + } + + pub fn collect_local_aliases(&self, aliases: &HashMap) -> HashSet { + let mut local_alias_names = self.collect_alias_types(); + + // loop until recursion settled + loop { + let mut new_local_alias_names = local_alias_names.clone(); + + // find aliases names in aliases + for name in new_local_alias_names.clone() { + let alias = aliases.get(&name).unwrap(); + new_local_alias_names.extend(alias.kind.collect_alias_types()); + } + + // resolve new aliases + for alias in new_local_alias_names.clone().into_iter() { + if let Some(alias) = aliases.get(&alias) { + if let Kind::Alias(alias) = &alias.kind { + new_local_alias_names.insert(alias.name.clone()); + } + } + } + + if new_local_alias_names != local_alias_names { + local_alias_names.clone_from(&new_local_alias_names); + } else { + break; + } + } + + local_alias_names + } + + #[allow(unused)] + pub fn collect_local_class_types(&self) -> HashSet { + let mut types = HashSet::new(); + for field in &self.fields { + types.extend(field.kind.collect_local_class_types()); + } + for function in &self.functions { + for ret in &function.returns { + types.extend(ret.kind.collect_local_class_types()); + } + for param in &function.params { + types.extend(param.kind.collect_local_class_types()); + } + } + for con in &self.constants { + types.extend(con.kind.collect_local_class_types()); + } + types + } + + pub fn collect_alias_types(&self) -> HashSet { + let mut types = HashSet::new(); + for field in &self.fields { + types.extend(field.kind.collect_alias_types()); + } + for function in &self.functions { + for ret in &function.returns { + types.extend(ret.kind.collect_alias_types()); + } + for param in &function.params { + types.extend(param.kind.collect_alias_types()); + } + } + for con in &self.constants { + types.extend(con.kind.collect_alias_types()); + } + types + } +} + +// ---------------------------------------- debug helpers to show types + +impl LuaKind { + pub fn show(&self) -> String { + let s = serde_json::to_string(self).unwrap(); + s.trim_matches('"').to_string() + } +} + +impl Kind { + pub fn has_unresolved(&self) -> bool { + let s = format!("{}", self); + s.contains("\x1b[33m") + } +} + +impl Var { + pub fn has_unresolved(&self) -> bool { + self.kind.has_unresolved() + } + pub fn show(&self) -> String { + format!( + "Var {} : {}", + self.name.clone().unwrap_or_default(), + self.kind + ) + } +} + +impl Enum { + pub fn show(&self) -> String { + format!("Enum {}", self.name) + } +} + +impl Function { + pub fn has_unresolved(&self) -> bool { + for p in &self.params { + if p.has_unresolved() { + return true; + } + } + for r in &self.returns { + if r.has_unresolved() { + return true; + } + } + false + } +} + +impl Alias { + pub fn show(&self) -> String { + format!("Alias {} {}", self.name, self.kind) + } +} + +impl fmt::Display for Kind { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + match self { + Self::Unresolved(_) => write!(f, "\x1b[33m{:?}\x1b[0m", self), + Self::Nullable(b) => write!(f, "Nullable({})", b.as_ref()), + Self::Array(b) => write!(f, "Array({})", b.as_ref()), + Self::Table(k, v) => write!(f, "Table({}, {})", k.as_ref(), v.as_ref()), + Self::Enum(ks) => write!( + f, + "Enum({})", + ks.iter() + .map(|k| format!("{}", k)) + .collect::>() + .join(", ") + ), + Self::Object(hm) => { + write!( + f, + "Object({})", + hm.iter() + .map(|(k, v)| format!("{} : {}", k, v)) + .collect::>() + .join(", ") + ) + } + Self::Function(fun) => write!(f, "{}", fun), + Self::Variadic(v) => write!(f, "Variadic({})", v.as_ref()), + _ => write!(f, "{:?}", self), + } + } +} + +impl fmt::Display for Var { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + if let Some(name) = self.name.clone() { + write!(f, "{} : {}", name, self.kind) + } else { + write!(f, "{}", self.kind) + } + } +} + +impl fmt::Display for Function { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + let params = self + .params + .iter() + .map(|p| format!("{}", p)) + .collect::>() + .join(", "); + let returns = self + .returns + .iter() + .map(|r| format!("{}", r)) + .collect::>() + .join(", "); + write!( + f, + "Function {}({}){}", + self.name.clone().unwrap_or_default(), + params, + if returns.is_empty() { + String::default() + } else { + format!(" -> {}", returns) + } + ) + } +} + +impl Class { + pub fn has_unresolved(&self) -> bool { + for v in &self.fields { + if v.has_unresolved() { + return true; + } + } + for f in &self.functions { + if f.has_unresolved() { + return true; + } + } + false + } + + pub fn is_empty(&self) -> bool { + self.fields.is_empty() && self.enums.is_empty() && self.functions.is_empty() + } + + fn with_new_line(s: &str) -> String { + if s.is_empty() { + s.to_string() + } else { + format!("\n{}", s) + } + } + + pub fn show(&self) -> String { + format!( + " Class {}{}{}{}", + self.name, + Self::with_new_line( + &self + .enums + .iter() + .map(|e| format!(" {}", Enum::show(e))) + .collect::>() + .join("\n") + ), + Self::with_new_line( + &self + .fields + .iter() + .map(|v| format!(" {}", Var::show(v))) + .collect::>() + .join("\n") + ), + Self::with_new_line( + &self + .functions + .iter() + .map(|f| format!(" {}", f)) + .collect::>() + .join("\n") + ) + ) + } +} + +// impl Def { +// pub fn show(&self) -> String { +// match self { +// Self::Alias(d) => d.show(), +// Self::Function(d) => format!("{}", d), +// Self::Class(d) => d.show(), +// Self::Enum(e) => e.show(), +// } +// } +// } diff --git a/docs/src/API/README.md b/docs/src/API/README.md new file mode 100644 index 0000000..47c2437 --- /dev/null +++ b/docs/src/API/README.md @@ -0,0 +1,18 @@ +# Lua API Reference + +The following chapters contain a complete listing of the afseq Lua scripting API. The content has been auto-generated from the [LuaLS Type Definitions](https://github.com/emuell/afseq/tree/master/types/nerdo), so you can read and use the definition files directly too. + +--- + +You can also use the LuaLS type definitions directly for autocompletion and to view the API documentation in e.g. vscode and other editors that support the [LuaLS language server](https://luals.github.io/). + +First install the [sumneko.lua vscode extension](https://luals.github.io/#vscode-install). + +Then download a copy of the afseq type definitions folder and configure your workspace to use the files in your project. To do this, add the following to your project's `/.vscode/settings.json` file + +```json +{ + "Lua.workspace.library": ["PATH/TO/RENOISE_DEFINITION_FOLDER"], + "Lua.runtime.plugin": "PATH/TO/RENOISE_DEFINITION_FOLDER/plugin.lua" +} +``` \ No newline at end of file diff --git a/docs/src/API/builtins.md b/docs/src/API/builtins.md new file mode 100644 index 0000000..62d8904 --- /dev/null +++ b/docs/src/API/builtins.md @@ -0,0 +1 @@ +# Lua Builtin Types \ No newline at end of file diff --git a/docs/src/API/builtins/any.md b/docs/src/API/builtins/any.md new file mode 100644 index 0000000..963e000 --- /dev/null +++ b/docs/src/API/builtins/any.md @@ -0,0 +1,3 @@ +# any {#any} +> A type for a dynamic argument, it can be anything at run-time. + diff --git a/docs/src/API/builtins/boolean.md b/docs/src/API/builtins/boolean.md new file mode 100644 index 0000000..5e47336 --- /dev/null +++ b/docs/src/API/builtins/boolean.md @@ -0,0 +1,3 @@ +# boolean {#boolean} +> A built-in type representing a boolean (true or false) value, [see details](https://www.lua.org/pil/2.2.html) + diff --git a/docs/src/API/builtins/function.md b/docs/src/API/builtins/function.md new file mode 100644 index 0000000..5cb914d --- /dev/null +++ b/docs/src/API/builtins/function.md @@ -0,0 +1,3 @@ +# function {#function} +> A built-in type representing functions, [see details](https://www.lua.org/pil/2.6.html) + diff --git a/docs/src/API/builtins/integer.md b/docs/src/API/builtins/integer.md new file mode 100644 index 0000000..e6bd10a --- /dev/null +++ b/docs/src/API/builtins/integer.md @@ -0,0 +1,3 @@ +# integer {#integer} +> A helper type that represents whole numbers, a subset of [number](number.md) + diff --git a/docs/src/API/builtins/lightuserdata.md b/docs/src/API/builtins/lightuserdata.md new file mode 100644 index 0000000..44afc59 --- /dev/null +++ b/docs/src/API/builtins/lightuserdata.md @@ -0,0 +1,3 @@ +# lightuserdata {#lightuserdata} +> A built-in type representing a pointer, [see details](https://www.lua.org/pil/28.5.html) + diff --git a/docs/src/API/builtins/nil.md b/docs/src/API/builtins/nil.md new file mode 100644 index 0000000..da3fb71 --- /dev/null +++ b/docs/src/API/builtins/nil.md @@ -0,0 +1,3 @@ +# nil {#nil} +> A built-in type representing a non-existant value, [see details](https://www.lua.org/pil/2.1.html). When you see `?` at the end of types, it means they can be nil. + diff --git a/docs/src/API/builtins/number.md b/docs/src/API/builtins/number.md new file mode 100644 index 0000000..27408ce --- /dev/null +++ b/docs/src/API/builtins/number.md @@ -0,0 +1,3 @@ +# number {#number} +> A built-in type representing floating point numbers, [see details](https://www.lua.org/pil/2.3.html) + diff --git a/docs/src/API/builtins/self.md b/docs/src/API/builtins/self.md new file mode 100644 index 0000000..433fa82 --- /dev/null +++ b/docs/src/API/builtins/self.md @@ -0,0 +1,7 @@ +# self {#self} +> A type that represents an instance that you call a function on. When you see a function signature starting with this type, you should use `:` to call the function on the instance, this way you can omit this first argument. +> ```lua +> local p = pattern.from{1,1,1} +> local p2 = p:euclidean(12) +> ``` + diff --git a/docs/src/API/builtins/string.md b/docs/src/API/builtins/string.md new file mode 100644 index 0000000..ab9e5ed --- /dev/null +++ b/docs/src/API/builtins/string.md @@ -0,0 +1,3 @@ +# string {#string} +> A built-in type representing a string of characters, [see details](https://www.lua.org/pil/2.4.html) + diff --git a/docs/src/API/builtins/table.md b/docs/src/API/builtins/table.md new file mode 100644 index 0000000..f05cdf4 --- /dev/null +++ b/docs/src/API/builtins/table.md @@ -0,0 +1,3 @@ +# table {#table} +> A built-in type representing associative arrays, [see details](https://www.lua.org/pil/2.5.html) + diff --git a/docs/src/API/builtins/unknown.md b/docs/src/API/builtins/unknown.md new file mode 100644 index 0000000..027f219 --- /dev/null +++ b/docs/src/API/builtins/unknown.md @@ -0,0 +1,3 @@ +# unknown {#unknown} +> A dummy type for something that cannot be inferred before run-time. + diff --git a/docs/src/API/builtins/userdata.md b/docs/src/API/builtins/userdata.md new file mode 100644 index 0000000..84b5f05 --- /dev/null +++ b/docs/src/API/builtins/userdata.md @@ -0,0 +1,3 @@ +# userdata {#userdata} +> A built-in type representing array values, [see details](https://www.lua.org/pil/28.1.html). + diff --git a/docs/src/API/chord.md b/docs/src/API/chord.md new file mode 100644 index 0000000..cab62f0 --- /dev/null +++ b/docs/src/API/chord.md @@ -0,0 +1,157 @@ + + + +# Global {#Global} + +--- +## Functions +### chord(key : [`NoteValue`](#NoteValue), mode : [`ChordName`](#ChordName)) {#chord} +`->`[`Note`](../API/note.md#Note) + +> Create a new chord from the given key notes and a chord name or an array of custom intervals. +> +> NB: Chords also can also be defined via strings in function `note` and via the a scale's +> `chord` function. See examples below. +> +> Chord names also can be shortened by using the following synonyms: +> - "min" | "m" | "-" -> "minor" +> - "maj" | "M" | "^" -> "major" +> - "minMaj" | "mM" | "-^" -> "minMajor" +> - "+" | "aug" -> "augmented" +> - "o" | "dim" -> "diminished" +> - "5 -> "five" +> - "6 -> "six" +> - "69" -> "sixNine" +> - "9 -> "nine" +> - "11" -> "eleven" +> +> #### examples: +> ```lua +> chord("c4", "minor") --> {"c4", "d#4", "f4"} +> chord({key = 48, volume = 0.5}, "minor") --> {"c4 v0.5", "d#4 v0.5", "f4 v0.5"} +> --same as: +> chord("c4", {0, 4, 7}) +> chord("c4 v0.5", {0, 4, 7}) +> --or: +> note("c4'major") +> note("c4'major v0.5") +> --or: +> note(scale("c4", "major"):chord("i", 3)) +> note(scale("c4", "major"):chord("i", 3)):volume(0.5) +> ``` +> +> --- +> +> ```lua +> -- Available chords. +> mode: +> | "major" +> | "augmented" +> | "six" +> | "sixNine" +> | "major7" +> | "major9" +> | "add9" +> | "major11" +> | "add11" +> | "major13" +> | "add13" +> | "dom7" +> | "dom9" +> | "dom11" +> | "dom13" +> | "7b5" +> | "7#5" +> | "7b9" +> | "9" +> | "nine" +> | "eleven" +> | "thirteen" +> | "minor" +> | "diminished" +> | "dim" +> | "minor#5" +> | "minor6" +> | "minor69" +> | "minor7b5" +> | "minor7" +> | "minor7#5" +> | "minor7b9" +> | "minor7#9" +> | "diminished7" +> | "minor9" +> | "minor11" +> | "minor13" +> | "minorMajor7" +> | "five" +> | "sus2" +> | "sus4" +> | "7sus2" +> | "7sus4" +> | "9sus2" +> | "9sus4" +> ``` + + + +--- +## Aliases +### ChordName {#ChordName} +[`string`](../API/builtins/string.md) | `"7#5"` | `"7b5"` | `"7b9"` | `"7sus2"` | `"7sus4"` | `"9"` | `"9sus2"` | `"9sus4"` | `"add11"` | `"add13"` | `"add9"` | `"augmented"` | `"dim"` | `"diminished"` | `"diminished7"` | `"dom11"` | `"dom13"` | `"dom7"` | `"dom9"` | `"eleven"` | `"five"` | `"major"` | `"major11"` | `"major13"` | `"major7"` | `"major9"` | `"minor"` | `"minor#5"` | `"minor11"` | `"minor13"` | `"minor6"` | `"minor69"` | `"minor7"` | `"minor7#5"` | `"minor7#9"` | `"minor7b5"` | `"minor7b9"` | `"minor9"` | `"minorMajor7"` | `"nine"` | `"six"` | `"sixNine"` | `"sus2"` | `"sus4"` | `"thirteen"` +> ```lua +> -- Available chords. +> ChordName: +> | "major" +> | "augmented" +> | "six" +> | "sixNine" +> | "major7" +> | "major9" +> | "add9" +> | "major11" +> | "add11" +> | "major13" +> | "add13" +> | "dom7" +> | "dom9" +> | "dom11" +> | "dom13" +> | "7b5" +> | "7#5" +> | "7b9" +> | "9" +> | "nine" +> | "eleven" +> | "thirteen" +> | "minor" +> | "diminished" +> | "dim" +> | "minor#5" +> | "minor6" +> | "minor69" +> | "minor7b5" +> | "minor7" +> | "minor7#5" +> | "minor7b9" +> | "minor7#9" +> | "diminished7" +> | "minor9" +> | "minor11" +> | "minor13" +> | "minorMajor7" +> | "five" +> | "sus2" +> | "sus4" +> | "7sus2" +> | "7sus4" +> | "9sus2" +> | "9sus4" +> ``` + +### NoteValue {#NoteValue} +[`string`](../API/builtins/string.md) | [`number`](../API/builtins/number.md) | [`NoteTable`](../API/note.md#NoteTable) | [`nil`](../API/builtins/nil.md) + + + + + diff --git a/docs/src/API/cycle.md b/docs/src/API/cycle.md new file mode 100644 index 0000000..a2dd21b --- /dev/null +++ b/docs/src/API/cycle.md @@ -0,0 +1,209 @@ + + + +# Global {#Global} + +--- +## Functions +### cycle(input : [`string`](../API/builtins/string.md)) {#cycle} +`->`[`Cycle`](../API/cycle.md#Cycle) + +> Create a note sequence from a Tidal Cycles mini-notation string. +> +> `cycle` accepts a mini-notation as used by Tidal Cycles, with the following differences: +> * Stacks and random choices are valid without brackets (`a | b` is parsed as `[a | b]`) +> * Operators currently only accept numbers on the right side (`a3*2` is valid, `a3*<1 2>` is not) +> * `:` - Sets the instrument or remappable target instead of selecting samples +> [Tidal Cycles Reference](https://tidalcycles.org/docs/reference/mini_notation/) +> +> #### examples: +> ```lua +> --A chord sequence +> cycle("[c4, e4, g4] [e4, g4, b4] [g4, b4, d5] [b4, d5, f#5]") +> --Arpeggio pattern with variations +> cycle(" ") +> --Euclidean Rhythms +> cycle("c4(3,8) e4(5,8) g4(7,8)") +> --Polyrhythm +> cycle("{c4 e4 g4 b4}%2, {f4 d4 a4}%4") +> --Map custom identifiers to notes +> cycle("bd(3,8)"):map({ bd = "c4 #1" }) +> ``` + + + +--- +## Aliases +### CycleMapFunction {#CycleMapFunction} +(context : [`CycleMapContext`](../API/cycle.md#CycleMapContext), value : [`string`](../API/builtins/string.md)) `->` [`CycleMapNoteValue`](#CycleMapNoteValue) + + +### CycleMapGenerator {#CycleMapGenerator} +(context : [`CycleMapContext`](../API/cycle.md#CycleMapContext), value : [`string`](../API/builtins/string.md)) `->` [`CycleMapFunction`](#CycleMapFunction) + + +### CycleMapNoteValue {#CycleMapNoteValue} +[`Note`](../API/note.md#Note) | [`NoteValue`](#NoteValue) | [`NoteValue`](#NoteValue)[] + + +### NoteValue {#NoteValue} +[`string`](../API/builtins/string.md) | [`number`](../API/builtins/number.md) | [`NoteTable`](../API/note.md#NoteTable) | [`nil`](../API/builtins/nil.md) + + +### PlaybackState {#PlaybackState} +`"running"` | `"seeking"` +> ```lua +> -- - *seeking*: The emitter is auto-seeked to a target time. All results are discarded. Avoid +> -- unnecessary computations while seeking, and only maintain your generator's internal state. +> -- - *running*: The emitter is played back regularly. Results are audible. +> PlaybackState: +> | "seeking" +> | "running" +> ``` + + + + +# Cycle {#Cycle} + +--- +## Functions +### map([*self*](../API/builtins/self.md), map : [`CycleMapFunction`](#CycleMapFunction) | [`CycleMapGenerator`](#CycleMapGenerator) | { }) {#map} +`->`[`Cycle`](../API/cycle.md#Cycle) + +> Map names in in the cycle to custom note events. +> +> By default, strings in cycles are interpreted as notes, and integer values as MIDI note +> values. Custom identifiers such as "bd" are undefined and will result into a rest, when +> they are not mapped explicitly. +> +> #### examples: +> ```lua +> --Using a fixed mapping table +> cycle("bd [bd, sn]"):map({ +> bd = "c4", +> sn = "e4 #1 v0.2" +> }) +> --Using a fixed mapping table with targets +> cycle("bd:1 "):map({ +> bd = { key = "c4", volume = 0.5 }, -- instrument #1,5,7 will be set as specified +> }) +> --Using a dynamic map function +> cycle("4 5 4 <5 [4|6]>"):map(function(context, value) +> -- emit a random note with 'value' as octave +> return math.random(0, 11) + value * 12 +> end) +> --Using a dynamic map function generator +> cycle("4 5 4 <4 [5|7]>"):map(function(context) +> local notes = scale("c", "minor").notes +> return function(context, value) +> -- emit a 'cmin' note arp with 'value' as octave +> local note = notes[math.imod(context.step, #notes)] +> local octave = tonumber(value) +> return { key = note + octave * 12 } +> end +> end) +> --Using a dynamic map function to map values to chord degrees +> cycle("1 5 1 [6|7]"):map(function(context) +> local cmin = scale("c", "minor") +> return function(context, value) +> return note(cmin:chord(tonumber(value))) +> end +> end) +> ``` + + + +--- +## Aliases +### CycleMapFunction {#CycleMapFunction} +(context : [`CycleMapContext`](../API/cycle.md#CycleMapContext), value : [`string`](../API/builtins/string.md)) `->` [`CycleMapNoteValue`](#CycleMapNoteValue) + + +### CycleMapGenerator {#CycleMapGenerator} +(context : [`CycleMapContext`](../API/cycle.md#CycleMapContext), value : [`string`](../API/builtins/string.md)) `->` [`CycleMapFunction`](#CycleMapFunction) + + +### CycleMapNoteValue {#CycleMapNoteValue} +[`Note`](../API/note.md#Note) | [`NoteValue`](#NoteValue) | [`NoteValue`](#NoteValue)[] + + +### NoteValue {#NoteValue} +[`string`](../API/builtins/string.md) | [`number`](../API/builtins/number.md) | [`NoteTable`](../API/note.md#NoteTable) | [`nil`](../API/builtins/nil.md) + + +### PlaybackState {#PlaybackState} +`"running"` | `"seeking"` +> ```lua +> -- - *seeking*: The emitter is auto-seeked to a target time. All results are discarded. Avoid +> -- unnecessary computations while seeking, and only maintain your generator's internal state. +> -- - *running*: The emitter is played back regularly. Results are audible. +> PlaybackState: +> | "seeking" +> | "running" +> ``` + + + + +# CycleMapContext {#CycleMapContext} +> Context passed to 'cycle:map` functions. + +--- +## Properties +### playback : [`PlaybackState`](#PlaybackState) {#playback} +> Specifies how the cycle currently is running. + +### trigger_note : [`integer`](../API/builtins/integer.md)[`?`](../API/builtins/nil.md) {#trigger_note} +> Note value that triggered, started the rhythm, if any. + +### channel : [`integer`](../API/builtins/integer.md) {#channel} +> channel/voice index within the cycle. each channel in the cycle gets emitted and thus mapped +> separately, starting with the first channel index 1. + +### trigger_volume : [`number`](../API/builtins/number.md)[`?`](../API/builtins/nil.md) {#trigger_volume} +> Note volume that triggered, started the rhythm, if any. + +### step : [`integer`](../API/builtins/integer.md) {#step} +> Continues step counter for each channel, incrementing with each new mapped value in the cycle. +> Starts from 1 when the cycle starts running or after it got reset. + +### trigger_offset : [`integer`](../API/builtins/integer.md)[`?`](../API/builtins/nil.md) {#trigger_offset} +> Note slice offset value that triggered, started the rhythm, if any. + +### step_length : [`number`](../API/builtins/number.md) {#step_length} +> step length fraction within the cycle, where 1 is the total duration of a single cycle run. + +### inputs : table<[`string`](../API/builtins/string.md), [`boolean`](../API/builtins/boolean.md) | [`string`](../API/builtins/string.md) | [`number`](../API/builtins/number.md)> {#inputs} +> Current input parameter values, using parameter ids as keys +> and the actual parameter value as value. + +### beats_per_min : [`number`](../API/builtins/number.md) {#beats_per_min} +> Project's tempo in beats per minutes. + +### beats_per_bar : [`integer`](../API/builtins/integer.md) {#beats_per_bar} +> Project's beats per bar setting. + +### samples_per_sec : [`integer`](../API/builtins/integer.md) {#samples_per_sec} +> Project's sample rate in samples per second. + + + + + +--- +## Aliases +### PlaybackState {#PlaybackState} +`"running"` | `"seeking"` +> ```lua +> -- - *seeking*: The emitter is auto-seeked to a target time. All results are discarded. Avoid +> -- unnecessary computations while seeking, and only maintain your generator's internal state. +> -- - *running*: The emitter is played back regularly. Results are audible. +> PlaybackState: +> | "seeking" +> | "running" +> ``` + + + + diff --git a/docs/src/API/input.md b/docs/src/API/input.md new file mode 100644 index 0000000..4eb6619 --- /dev/null +++ b/docs/src/API/input.md @@ -0,0 +1,79 @@ + + + +# InputParameter {#InputParameter} +> Opaque input parameter user data. Construct new input parameters via the `parameter.XXX(...)` +> functions. + + + +# Parameter {#Parameter} +> Contains functions to construct new input parameters. Input parameter values can be accessed +> via functionn `contexts` in pattern, gate and emitter functions or generators. + +--- +## Functions +### boolean(id : [`InputParameterId`](#InputParameterId), default : [`InputParameterBooleanDefault`](#InputParameterBooleanDefault), name : [`InputParameterName`](#InputParameterName)[`?`](../API/builtins/nil.md), description : [`InputParameterDescription`](#InputParameterDescription)[`?`](../API/builtins/nil.md)) {#boolean} +`->`[`InputParameter`](../API/input.md#InputParameter) + +> Creates an InputParameter with "boolean" Lua type with the given default value +> and other optional properties. +### integer(id : [`InputParameterId`](#InputParameterId), default : [`InputParameterIntegerDefault`](#InputParameterIntegerDefault), range : [`InputParameterIntegerRange`](#InputParameterIntegerRange)[`?`](../API/builtins/nil.md), name : [`InputParameterName`](#InputParameterName)[`?`](../API/builtins/nil.md), description : [`InputParameterDescription`](#InputParameterDescription)[`?`](../API/builtins/nil.md)) {#integer} +`->`[`InputParameter`](../API/input.md#InputParameter) + +> Creates an InputParameter with "integer" Lua type with the given default value +> and other optional properties. +### number(id : [`InputParameterId`](#InputParameterId), default : [`InputParameterNumberDefault`](#InputParameterNumberDefault), range : [`InputParameterNumberRange`](#InputParameterNumberRange)[`?`](../API/builtins/nil.md), name : [`InputParameterName`](#InputParameterName)[`?`](../API/builtins/nil.md), description : [`InputParameterDescription`](#InputParameterDescription)[`?`](../API/builtins/nil.md)) {#number} +`->`[`InputParameter`](../API/input.md#InputParameter) + +> Creates an InputParameter with "number" Lua type with the given default value +> and other optional properties. +### enum(id : [`InputParameterId`](#InputParameterId), default : [`InputParameterEnumDefault`](#InputParameterEnumDefault), values : [`string`](../API/builtins/string.md)[], name : [`InputParameterName`](#InputParameterName)[`?`](../API/builtins/nil.md), description : [`InputParameterDescription`](#InputParameterDescription)[`?`](../API/builtins/nil.md)) {#enum} +`->`[`InputParameter`](../API/input.md#InputParameter) + +> Creates an InputParameter with a "string" Lua type with the given default value, +> set of valid values to choose from and other optional properties. + + + +--- +## Aliases +### InputParameterBooleanDefault {#InputParameterBooleanDefault} +[`boolean`](../API/builtins/boolean.md) +> Default boolean value. + +### InputParameterDescription {#InputParameterDescription} +[`string`](../API/builtins/string.md) +> Optional long description of the parameter describing what the parameter does. + +### InputParameterEnumDefault {#InputParameterEnumDefault} +[`string`](../API/builtins/string.md) +> Default string value. Must be a valid string within the specified value set. + +### InputParameterId {#InputParameterId} +[`string`](../API/builtins/string.md) +> Unique id of the parameter. The id will be used in the `input` context table as key. + +### InputParameterIntegerDefault {#InputParameterIntegerDefault} +[`integer`](../API/builtins/integer.md) +> Default integer value. Must be in the specified value range. + +### InputParameterIntegerRange {#InputParameterIntegerRange} +{ 1 : [`integer`](../API/builtins/integer.md), 2 : [`integer`](../API/builtins/integer.md) } +> Optional value range. When undefined (0.0 - 1.0) + +### InputParameterName {#InputParameterName} +[`string`](../API/builtins/string.md) +> Optional name of the parameter as displayed to the user. When undefined, the id is used. + +### InputParameterNumberDefault {#InputParameterNumberDefault} +[`number`](../API/builtins/number.md) +> Default number value. Must be in the specified value range. + +### InputParameterNumberRange {#InputParameterNumberRange} +{ 1 : [`number`](../API/builtins/number.md), 2 : [`number`](../API/builtins/number.md) } +> Optional value range. When undefined (0 - 100) + + + + diff --git a/docs/src/API/modules.md b/docs/src/API/modules.md new file mode 100644 index 0000000..5e4c973 --- /dev/null +++ b/docs/src/API/modules.md @@ -0,0 +1 @@ +# Lua Module Extensions \ No newline at end of file diff --git a/docs/src/API/modules/math.md b/docs/src/API/modules/math.md new file mode 100644 index 0000000..0f1fb24 --- /dev/null +++ b/docs/src/API/modules/math.md @@ -0,0 +1,59 @@ +# math {#math} + +--- +## Functions +### imod(index : [`integer`](../../API/builtins/integer.md), length : [`integer`](../../API/builtins/integer.md)) {#imod} +`->`[`integer`](../../API/builtins/integer.md) + +> Wrap a lua 1 based integer index into the given array/table length. +> +> -> `(index - 1) % length + 1` +### random(m : [`integer`](../../API/builtins/integer.md), n : [`integer`](../../API/builtins/integer.md)) {#random} +`->`[`integer`](../../API/builtins/integer.md) + +> * `math.random()`: Returns a float in the range [0,1). +> * `math.random(n)`: Returns a integer in the range [1, n]. +> * `math.random(m, n)`: Returns a integer in the range [m, n]. +> +> Overridden to use a `Xoshiro256PlusPlus` random number generator to ensure that +> seeded random operations behave the same on all platforms and architectures. +> +> [View documents](command:extension.lua.doc?["en-us/51/manual.html/pdf-math.random"]) +### randomseed(x : [`integer`](../../API/builtins/integer.md)) {#randomseed} +> Sets `x` as the "seed" for the pseudo-random generator. +> +> Overridden to seed the internally used `Xoshiro256PlusPlus` random number generator. +> +> [View documents](command:extension.lua.doc?["en-us/51/manual.html/pdf-math.randomseed"]) +### randomstate(seed : [`integer`](../../API/builtins/integer.md)[`?`](../../API/builtins/nil.md)) {#randomstate} +`->`(m : [`integer`](../../API/builtins/integer.md)[`?`](../../API/builtins/nil.md), n : [`integer`](../../API/builtins/integer.md)[`?`](../../API/builtins/nil.md)) `->` [`number`](../../API/builtins/number.md) + +> Create a new local random number state with the given optional seed value. +> +> When no seed value is specified, the global `math.randomseed` value is used. +> When no global seed value is available, a new unique random seed is created. +> +> Random states can be useful to create multiple, separate seeded random number +> generators, e.g. in pattern, gate or emit generators, which get reset with the +> generator functions. +> +> #### examples: +> +> ```lua +> return rhythm { +> emit = function(context) +> -- use a unique random sequence every time the rhythm gets (re)triggered +> local rand = math.randomstate(12345) +> return function(context) +> if rand(1, 10) > 5 then +> return "c5" +> else +> return "g4" +> end +> end +> } +> ``` +> See: +> * [math.random](file:///c%3A/Users/emuell/Development/Crates/afseq/types/nerdo/library/math.lua#36#9) function arguments +> * [math.randomseed](file:///c%3A/Users/emuell/Development/Crates/afseq/types/nerdo/library/math.lua#46#9) seeding + diff --git a/docs/src/API/modules/table.md b/docs/src/API/modules/table.md new file mode 100644 index 0000000..a9c6f0a --- /dev/null +++ b/docs/src/API/modules/table.md @@ -0,0 +1,92 @@ +# table {#table} + +--- +## Functions +### `new()` {#new} +`->`[`table`](../../API/builtins/table.md) | tablelib + +> Create a new empty table that uses the global 'table.XXX' functions as methods. +> +> #### examples: +> ```lua +> t = table.new(); t:insert("a"); rprint(t) -> [1] = a; +> ``` +### create(t : [`table`](../../API/builtins/table.md)[`?`](../../API/builtins/nil.md)) {#create} +`->`[`table`](../../API/builtins/table.md) | tablelib + +> Create a new, or convert an exiting table to an object that uses the global +> 'table.XXX' functions as methods, just like strings in Lua do. +> +> #### examples: +> ```lua +> t = table.create(); t:insert("a"); rprint(t) -> [1] = a; +> t = table.create{1,2,3}; print(t:concat("|")); -> "1|2|3"; +> ``` +### clear(t : [`table`](../../API/builtins/table.md), cleared : [`unknown`](../../API/builtins/unknown.md)[`?`](../../API/builtins/nil.md)) {#clear} +> Recursively clears and removes all table elements. +### is_empty(t : [`table`](../../API/builtins/table.md)) {#is_empty} +`->`[`boolean`](../../API/builtins/boolean.md) + +> Returns true when the table is empty, else false and will also work +> for non indexed tables +> +> #### examples: +> ```lua +> t = {}; print(table.is_empty(t)); -> true; +> t = {66}; print(table.is_empty(t)); -> false; +> t = {["a"] = 1}; print(table.is_empty(t)); -> false; +### find(t : [`table`](../../API/builtins/table.md), value : [`any`](../../API/builtins/any.md), start_index : [`integer`](../../API/builtins/integer.md)[`?`](../../API/builtins/nil.md)) {#find} +`->`key_or_nil : [`any`](../../API/builtins/any.md) + +> Find first match of *value* in the given table, starting from element +> number *start_index*.
+> Returns the first *key* that matches the value or nil +> +> #### examples: +> ```lua +> t = {"a", "b"}; table.find(t, "a") --> 1 +> t = {a=1, b=2}; table.find(t, 2) --> "b" +> t = {"a", "b", "a"}; table.find(t, "a", 2) --> "3" +> t = {"a", "b"}; table.find(t, "c") --> nil +> ``` +### keys(t : [`table`](../../API/builtins/table.md)) {#keys} +`->`[`table`](../../API/builtins/table.md) + +> Return an indexed table of all keys that are used in the table. +> +> #### examples: +> ```lua +> t = {a="aa", b="bb"}; rprint(table.keys(t)); --> "a", "b" +> t = {"a", "b"}; rprint(table.keys(t)); --> 1, 2 +> ``` +### values(t : [`table`](../../API/builtins/table.md)) {#values} +`->`[`table`](../../API/builtins/table.md) + +> Return an indexed table of all values that are used in the table +> +> #### examples: +> ```lua +> t = {a="aa", b="bb"}; rprint(table.values(t)); --> "aa", "bb" +> t = {"a", "b"}; rprint(table.values(t)); --> "a", "b" +> ``` +### rcopy(t : [`table`](../../API/builtins/table.md)) {#rcopy} +`->`[`table`](../../API/builtins/table.md) + +> Deeply copy the metatable and all elements of the given table recursively +> into a new table - create a clone with unique references. +### copy(t : [`table`](../../API/builtins/table.md)) {#copy} +`->`[`table`](../../API/builtins/table.md) + +> Deeply copy the metatable and all elements of the given table recursively +> into a new table - create a clone with unique references. +### count(t : [`table`](../../API/builtins/table.md)) {#count} +`->`[`unknown`](../../API/builtins/unknown.md) + +> Count the number of items of a table, also works for non index +> based tables (using pairs). +> +> #### examples: +> ```lua +> t = {["a"]=1, ["b"]=1}; print(table.count(t)) --> 2 +> ``` + diff --git a/docs/src/API/note.md b/docs/src/API/note.md new file mode 100644 index 0000000..985ba26 --- /dev/null +++ b/docs/src/API/note.md @@ -0,0 +1,126 @@ + + + +# Global {#Global} + +--- +## Functions +### note(...[`NoteValue`](#NoteValue)) {#note} +`->`[`Note`](../API/note.md#Note) + +> Create a new monophonic or polyphonic note (a chord) from a number value, +> a note string, chord string or array of note values. +> +> In note strings the following prefixes are used to specify optional note +> attributes: +> ```md +> -'#' -> instrument (integer > 0) +> -'v' -> volume (float in range [0-1]) +> -'p' -> panning (float in range [-1-1]) +> -'d' -> delay (float in range [0-1]) +> ``` +> +> #### examples: +> ```lua +> note(60) -- middle C +> note("c4") -- middle C +> note("c4 #2 v0.5 d0.3") -- middle C with additional properties +> note({key="c4", volume=0.5}) -- middle C with volume 0.5 +> note("c4'maj v0.7") -- C4 major chord with volume 0.7 +> note("c4", "e4 v0.5", "off") -- custom chord with a c4, e4 and 'off' note +> ``` + + + +--- +## Aliases +### NoteValue {#NoteValue} +[`string`](../API/builtins/string.md) | [`number`](../API/builtins/number.md) | [`NoteTable`](../API/note.md#NoteTable) | [`nil`](../API/builtins/nil.md) + + + + + +# Note {#Note} + +--- +## Properties +### notes : [`NoteTable`](../API/note.md#NoteTable)[] {#notes} + + +--- +## Functions +### transpose([*self*](../API/builtins/self.md), step : [`integer`](../API/builtins/integer.md) | [`integer`](../API/builtins/integer.md)[]) {#transpose} +`->`[`Note`](../API/note.md#Note) + +> Transpose the notes key with the specified step or steps. +> +> Values outside of the valid key range (0 - 127) will be clamped. +> +> #### examples: +> ```lua +> note("c4"):transpose(12) +> note("c'maj"):transpose(5) +> note("c'maj"):transpose({0, 0, -12}) +> ``` +### amplify([*self*](../API/builtins/self.md), factor : [`number`](../API/builtins/number.md) | [`number`](../API/builtins/number.md)[]) {#amplify} +`->`[`Note`](../API/note.md#Note) + +> Multiply the note's volume attribute with the specified factor or factors. +> +> Values outside of the valid volume range (0 - 1) will be clamped. +> +> #### examples: +> ```lua +> note({"c4 0.5", "g4"}):amplify(0.5) +> note("c'maj 0.5"):amplify({2.0, 1.0, 0.3}) +> ``` +### volume([*self*](../API/builtins/self.md), volume : [`number`](../API/builtins/number.md) | [`number`](../API/builtins/number.md)[]) {#volume} +`->`[`Note`](../API/note.md#Note) + +> Set the note's volume attribute to the specified value or values. +> +> #### examples: +> ```lua +> note({"c4", "g4"}):volume(0.5) +> note("c'maj"):volume(0.5) +> note("c'maj"):volume({0.1, 0.2, 0.3}) +> ``` +### instrument([*self*](../API/builtins/self.md), instrument : [`number`](../API/builtins/number.md) | [`number`](../API/builtins/number.md)[]) {#instrument} +`->`[`Note`](../API/note.md#Note) + +> Set the note's instrument attribute to the specified value or values. +### panning([*self*](../API/builtins/self.md), panning : [`number`](../API/builtins/number.md) | [`number`](../API/builtins/number.md)[]) {#panning} +`->`[`Note`](../API/note.md#Note) + +> Set the note's panning attribute to the specified value or values. +### delay([*self*](../API/builtins/self.md), delay : [`number`](../API/builtins/number.md) | [`number`](../API/builtins/number.md)[]) {#delay} +`->`[`Note`](../API/note.md#Note) + +> Set the note's delay attribute to the specified value or values. + + + +# NoteTable {#NoteTable} + +--- +## Properties +### key : [`string`](../API/builtins/string.md) | [`number`](../API/builtins/number.md) {#key} +> Note Key + +### instrument : [`number`](../API/builtins/number.md)[`?`](../API/builtins/nil.md) {#instrument} +> Instrument/Sample/Patch >= 0 + +### volume : [`number`](../API/builtins/number.md)[`?`](../API/builtins/nil.md) {#volume} +> Volume in range [0.0 - 1.0] + +### panning : [`number`](../API/builtins/number.md)[`?`](../API/builtins/nil.md) {#panning} +> Panning factor in range [-1.0 - 1.0] where 0 is center + +### delay : [`number`](../API/builtins/number.md)[`?`](../API/builtins/nil.md) {#delay} +> Delay factor in range [0.0 - 1.0] + + + + + diff --git a/docs/src/API/pattern.md b/docs/src/API/pattern.md new file mode 100644 index 0000000..a9d030d --- /dev/null +++ b/docs/src/API/pattern.md @@ -0,0 +1,430 @@ + + + +# Pattern {#Pattern} +> Array alike table with helper functions to ease creating rhythmic patterns. +> +> #### examples: +> ```lua +> -- using + and * operators to combine patterns +> pattern.from{ 0, 1 } * 3 + { 1, 0 } +> -- repeating, spreading and subsets +> pattern.from{ 0, 1, { 1, 1 } }:repeat_n(4):spread(1.25):take(16) +> -- euclidean patterns +> pattern.euclidean(12, 16) +> pattern.from{ 1, 0.5, 1, 1 }:euclidean(12) +> -- generate/init from functions +> pattern.new(12):init(function() return math.random(0.5, 1.0) end ) +> pattern.new(16):init(scale("c", "minor").notes_iter()) +> -- generate note patterns +> pattern.from{ "c4", "g4", "a4" } * 7 + { "a4", "g4", "c4" } +> -- generate chord patterns +> pattern.from{ 1, 5, 6, 4 }:map(function(index, degree) +> return scale("c", "minor"):chord(degree) +> end) +> ``` + +--- +## Functions +### new(length : [`integer`](../API/builtins/integer.md)[`?`](../API/builtins/nil.md), value : [`PulseValue`](#PulseValue) | (index : [`integer`](../API/builtins/integer.md)) `->` [`PulseValue`](#PulseValue)[`?`](../API/builtins/nil.md)) {#new} +`->`[`Pattern`](../API/pattern.md#Pattern) + +> Create a new empty pattern or pattern with the given length. +### from(...[`PulseValue`](#PulseValue) | [`PulseValue`](#PulseValue)[]) {#from} +`->`[`Pattern`](../API/pattern.md#Pattern) + +> Create a new pattern from a set of values or tables. +> When passing tables, those will be flattened. +> +> #### examples: +> ```lua +> local p = pattern.from(1,0,1,0) -- {1,0,1,0} +> p = pattern.from({1,0},{1,0}) -- {1,0,1,0} +> ``` +### copy(self : [`Pattern`](../API/pattern.md#Pattern)) {#copy} +`->`[`Pattern`](../API/pattern.md#Pattern) + +> create a shallow-copy of the given pattern (or self) +> +> #### examples: +> ```lua +> local p = pattern.from(1, 0) +> local p2 = p:copy() --- {1,0} +> ``` +### distributed(steps : [`integer`](../API/builtins/integer.md) | [`table`](../API/builtins/table.md), length : [`integer`](../API/builtins/integer.md), offset : [`integer`](../API/builtins/integer.md)[`?`](../API/builtins/nil.md), empty_value : [`PulseValue`](#PulseValue)[`?`](../API/builtins/nil.md)) {#distributed} +`->`[`Pattern`](../API/pattern.md#Pattern) + +> Create an new pattern or spread and existing pattern evenly within the given length. +> Similar, but not exactly like `euclidean`. +> +> Shortcut for `pattern.from{1,1,1}:spread(length / #self):rotate(offset)` +> +> #### examples: +> ```lua +> local p = pattern.distributed(3, 8) --- {1,0,0,1,0,1,0} +> p = pattern.from{1,1}:distributed(4, 1) --- {0,1,0,1} +> ``` +### euclidean(steps : [`integer`](../API/builtins/integer.md) | [`table`](../API/builtins/table.md), length : [`integer`](../API/builtins/integer.md), offset : [`integer`](../API/builtins/integer.md)[`?`](../API/builtins/nil.md), empty_value : [`PulseValue`](#PulseValue)[`?`](../API/builtins/nil.md)) {#euclidean} +`->`[`Pattern`](../API/pattern.md#Pattern) + +> Create a new euclidean rhythm pattern with the given pulses or number of new pulses +> in the given length and optionally rotate the contents. +> [Euclidean Rhythm](https://en.wikipedia.org/wiki/Euclidean_rhythm) +> +> #### examples: +> ```lua +> local p = pattern.euclidean(3, 8) -- {1,0,0,1,0,0,1,0} +> p = pattern.from{"x", "x", "x"}:euclidean(8, 0, "-") -- {"x","-","-","x","-","-","x","-"} +> ``` +### unpack(self : [`Pattern`](../API/pattern.md#Pattern)) {#unpack} +`->`... : [`PulseValue`](#PulseValue) + +> Shortcut for table.unpack(pattern): returns elements from this pattern as var args. +> +> #### examples: +> ```lua +> local p = pattern.from{1,2,3,4} +> local v1, v2, v3, v4 = p:unpack() +> ``` +### subrange(self : [`Pattern`](../API/pattern.md#Pattern), i : [`integer`](../API/builtins/integer.md), j : [`integer`](../API/builtins/integer.md)[`?`](../API/builtins/nil.md), empty_value : [`PulseValue`](#PulseValue)[`?`](../API/builtins/nil.md)) {#subrange} +`->`[`Pattern`](../API/pattern.md#Pattern) + +> Get sub range from the pattern as new pattern. +> When the given length is past end of this pattern its filled up with empty values. +> +> #### examples: +> ```lua +> local p = pattern.from{1,2,3,4} +> p = p:subrange(2,3) -- {2,3} +> p = p:subrange(1,4,"X") -- {2,3,"X","X"} +> ``` +### take(self : [`Pattern`](../API/pattern.md#Pattern), length : [`integer`](../API/builtins/integer.md), empty_value : [`PulseValue`](#PulseValue)[`?`](../API/builtins/nil.md)) {#take} +`->`[`Pattern`](../API/pattern.md#Pattern) + +> Get first n items from the pattern as new pattern. +> When the given length is past end of this pattern its filled up with empty values. +> +> #### examples: +> ```lua +> local p = pattern.from{1,2,3,4} +> p = p:take(2) -- {1,2} +> p = p:take(4, "") -- {1,2,"",""} +> ``` +### clear(self : [`Pattern`](../API/pattern.md#Pattern)) {#clear} +`->`[`Pattern`](../API/pattern.md#Pattern) + +> Clear a pattern, remove all its contents. +> +> #### examples: +> ```lua +> local p = pattern.from{0,0} +> p:clear() -- {} +> ``` +### init(self : [`Pattern`](../API/pattern.md#Pattern), value : [`PulseValue`](#PulseValue) | (index : [`integer`](../API/builtins/integer.md)) `->` [`PulseValue`](#PulseValue), length : [`integer`](../API/builtins/integer.md)[`?`](../API/builtins/nil.md)) {#init} +`->`[`Pattern`](../API/pattern.md#Pattern) + +> Fill pattern with the given value or generator function in length. +> +> #### examples: +> ```lua +> local p = pattern.from{0,0} +> p:init(1) -- {1,1} +> p:init("X", 3) -- {"X","X", "X"} +> ``` +### map(self : [`Pattern`](../API/pattern.md#Pattern), fun : (index : [`integer`](../API/builtins/integer.md), value : [`PulseValue`](#PulseValue)) `->` [`PulseValue`](#PulseValue)) {#map} +`->`[`Pattern`](../API/pattern.md#Pattern) + +> Apply the given function to every item in the pattern. +> +> #### examples: +> ```lua +> local p = pattern.from{1,3,5} +> p:map(function(k, v) +> return scale("c", "minor"):degree(v) +> end) -- {48, 51, 55} +> ``` +### reverse(self : [`Pattern`](../API/pattern.md#Pattern)) {#reverse} +`->`[`Pattern`](../API/pattern.md#Pattern) + +> Invert the order of items. +> +> #### examples: +> ```lua +> local p = pattern.from{1,2,3} +> p:reverse() -- {3,2,1} +> ``` +### rotate(self : [`Pattern`](../API/pattern.md#Pattern), amount : [`integer`](../API/builtins/integer.md)) {#rotate} +`->`[`Pattern`](../API/pattern.md#Pattern) + +> Shift contents by the given amount to the left (negative amount) or right. +> +> #### examples: +> ```lua +> local p = pattern.from{1,0,0} +> p:rotate(1) -- {0,1,0} +> p:rotate(-2) -- {0,0,1} +> ``` +### push_back(self : [`Pattern`](../API/pattern.md#Pattern), ...[`PulseValue`](#PulseValue)[] | [`PulseValue`](#PulseValue)) {#push_back} +`->`[`Pattern`](../API/pattern.md#Pattern) + +> Push a single or multiple number of items or other pattern contents to the end of the pattern. +> Note: When passing array alike tables or patterns, they will be *unpacked*. +> +> #### examples: +> ```lua +> local p = pattern.new() +> p:push_back(1) -- {1} +> p:push_back(2,3) -- {1,2,3} +> p:push_back{4} -- {1,2,3,4} +> p:push_back({5,{6,7}) -- {1,2,3,4,5,6,7} +> ``` +### pop_back(self : [`Pattern`](../API/pattern.md#Pattern)) {#pop_back} +`->`[`PulseValue`](#PulseValue) + +> Remove an entry from the back of the pattern. returns the popped item. +> +> #### examples: +> ```lua +> local p = pattern.from({1,2}) +> p:pop_back() -- {1} +> p:pop_back() -- {} +> p:pop_back() -- {} +> ``` +### repeat_n(self : [`Pattern`](../API/pattern.md#Pattern), count : [`integer`](../API/builtins/integer.md)) {#repeat_n} +`->`[`Pattern`](../API/pattern.md#Pattern) + +> Duplicate the pattern n times. +> +> #### examples: +> ```lua +> local p = pattern.from{1,2,3} +> patterns:repeat_n(2) -- {1,2,3,1,2,3} +> ``` +### spread(self : [`Pattern`](../API/pattern.md#Pattern), amount : [`number`](../API/builtins/number.md), empty_value : [`PulseValue`](#PulseValue)[`?`](../API/builtins/nil.md)) {#spread} +`->`[`Pattern`](../API/pattern.md#Pattern) + +> Expand (with amount > 1) or shrink (amount < 1) the length of the pattern by the +> given factor, spreading allowed content evenly and filling gaps with 0 or the +> given empty value. +> +> #### examples: +> ```lua +> local p = pattern.from{1,1} +> p:spread(2) -- {1,0,1,0} +> p:spread(0.5) -- {1,1} +> ``` + + + +--- +## Aliases +### PulseValue {#PulseValue} +[`boolean`](../API/builtins/boolean.md) | [`string`](../API/builtins/string.md) | [`number`](../API/builtins/number.md) | [`table`](../API/builtins/table.md) +> Valid pulse value in a pattern + + + + +# pattern {#pattern} + +--- +## Functions +### new(length : [`integer`](../API/builtins/integer.md)[`?`](../API/builtins/nil.md), value : [`PulseValue`](#PulseValue) | (index : [`integer`](../API/builtins/integer.md)) `->` [`PulseValue`](#PulseValue)[`?`](../API/builtins/nil.md)) {#new} +`->`[`Pattern`](../API/pattern.md#Pattern) + +> Create a new empty pattern or pattern with the given length. +### from(...[`PulseValue`](#PulseValue) | [`PulseValue`](#PulseValue)[]) {#from} +`->`[`Pattern`](../API/pattern.md#Pattern) + +> Create a new pattern from a set of values or tables. +> When passing tables, those will be flattened. +> +> #### examples: +> ```lua +> local p = pattern.from(1,0,1,0) -- {1,0,1,0} +> p = pattern.from({1,0},{1,0}) -- {1,0,1,0} +> ``` +### copy(self : [`Pattern`](../API/pattern.md#Pattern)) {#copy} +`->`[`Pattern`](../API/pattern.md#Pattern) + +> create a shallow-copy of the given pattern (or self) +> +> #### examples: +> ```lua +> local p = pattern.from(1, 0) +> local p2 = p:copy() --- {1,0} +> ``` +### distributed(steps : [`integer`](../API/builtins/integer.md) | [`table`](../API/builtins/table.md), length : [`integer`](../API/builtins/integer.md), offset : [`integer`](../API/builtins/integer.md)[`?`](../API/builtins/nil.md), empty_value : [`PulseValue`](#PulseValue)[`?`](../API/builtins/nil.md)) {#distributed} +`->`[`Pattern`](../API/pattern.md#Pattern) + +> Create an new pattern or spread and existing pattern evenly within the given length. +> Similar, but not exactly like `euclidean`. +> +> Shortcut for `pattern.from{1,1,1}:spread(length / #self):rotate(offset)` +> +> #### examples: +> ```lua +> local p = pattern.distributed(3, 8) --- {1,0,0,1,0,1,0} +> p = pattern.from{1,1}:distributed(4, 1) --- {0,1,0,1} +> ``` +### euclidean(steps : [`integer`](../API/builtins/integer.md) | [`table`](../API/builtins/table.md), length : [`integer`](../API/builtins/integer.md), offset : [`integer`](../API/builtins/integer.md)[`?`](../API/builtins/nil.md), empty_value : [`PulseValue`](#PulseValue)[`?`](../API/builtins/nil.md)) {#euclidean} +`->`[`Pattern`](../API/pattern.md#Pattern) + +> Create a new euclidean rhythm pattern with the given pulses or number of new pulses +> in the given length and optionally rotate the contents. +> [Euclidean Rhythm](https://en.wikipedia.org/wiki/Euclidean_rhythm) +> +> #### examples: +> ```lua +> local p = pattern.euclidean(3, 8) -- {1,0,0,1,0,0,1,0} +> p = pattern.from{"x", "x", "x"}:euclidean(8, 0, "-") -- {"x","-","-","x","-","-","x","-"} +> ``` +### unpack(self : [`Pattern`](../API/pattern.md#Pattern)) {#unpack} +`->`... : [`PulseValue`](#PulseValue) + +> Shortcut for table.unpack(pattern): returns elements from this pattern as var args. +> +> #### examples: +> ```lua +> local p = pattern.from{1,2,3,4} +> local v1, v2, v3, v4 = p:unpack() +> ``` +### subrange(self : [`Pattern`](../API/pattern.md#Pattern), i : [`integer`](../API/builtins/integer.md), j : [`integer`](../API/builtins/integer.md)[`?`](../API/builtins/nil.md), empty_value : [`PulseValue`](#PulseValue)[`?`](../API/builtins/nil.md)) {#subrange} +`->`[`Pattern`](../API/pattern.md#Pattern) + +> Get sub range from the pattern as new pattern. +> When the given length is past end of this pattern its filled up with empty values. +> +> #### examples: +> ```lua +> local p = pattern.from{1,2,3,4} +> p = p:subrange(2,3) -- {2,3} +> p = p:subrange(1,4,"X") -- {2,3,"X","X"} +> ``` +### take(self : [`Pattern`](../API/pattern.md#Pattern), length : [`integer`](../API/builtins/integer.md), empty_value : [`PulseValue`](#PulseValue)[`?`](../API/builtins/nil.md)) {#take} +`->`[`Pattern`](../API/pattern.md#Pattern) + +> Get first n items from the pattern as new pattern. +> When the given length is past end of this pattern its filled up with empty values. +> +> #### examples: +> ```lua +> local p = pattern.from{1,2,3,4} +> p = p:take(2) -- {1,2} +> p = p:take(4, "") -- {1,2,"",""} +> ``` +### clear(self : [`Pattern`](../API/pattern.md#Pattern)) {#clear} +`->`[`Pattern`](../API/pattern.md#Pattern) + +> Clear a pattern, remove all its contents. +> +> #### examples: +> ```lua +> local p = pattern.from{0,0} +> p:clear() -- {} +> ``` +### init(self : [`Pattern`](../API/pattern.md#Pattern), value : [`PulseValue`](#PulseValue) | (index : [`integer`](../API/builtins/integer.md)) `->` [`PulseValue`](#PulseValue), length : [`integer`](../API/builtins/integer.md)[`?`](../API/builtins/nil.md)) {#init} +`->`[`Pattern`](../API/pattern.md#Pattern) + +> Fill pattern with the given value or generator function in length. +> +> #### examples: +> ```lua +> local p = pattern.from{0,0} +> p:init(1) -- {1,1} +> p:init("X", 3) -- {"X","X", "X"} +> ``` +### map(self : [`Pattern`](../API/pattern.md#Pattern), fun : (index : [`integer`](../API/builtins/integer.md), value : [`PulseValue`](#PulseValue)) `->` [`PulseValue`](#PulseValue)) {#map} +`->`[`Pattern`](../API/pattern.md#Pattern) + +> Apply the given function to every item in the pattern. +> +> #### examples: +> ```lua +> local p = pattern.from{1,3,5} +> p:map(function(k, v) +> return scale("c", "minor"):degree(v) +> end) -- {48, 51, 55} +> ``` +### reverse(self : [`Pattern`](../API/pattern.md#Pattern)) {#reverse} +`->`[`Pattern`](../API/pattern.md#Pattern) + +> Invert the order of items. +> +> #### examples: +> ```lua +> local p = pattern.from{1,2,3} +> p:reverse() -- {3,2,1} +> ``` +### rotate(self : [`Pattern`](../API/pattern.md#Pattern), amount : [`integer`](../API/builtins/integer.md)) {#rotate} +`->`[`Pattern`](../API/pattern.md#Pattern) + +> Shift contents by the given amount to the left (negative amount) or right. +> +> #### examples: +> ```lua +> local p = pattern.from{1,0,0} +> p:rotate(1) -- {0,1,0} +> p:rotate(-2) -- {0,0,1} +> ``` +### push_back(self : [`Pattern`](../API/pattern.md#Pattern), ...[`PulseValue`](#PulseValue)[] | [`PulseValue`](#PulseValue)) {#push_back} +`->`[`Pattern`](../API/pattern.md#Pattern) + +> Push a single or multiple number of items or other pattern contents to the end of the pattern. +> Note: When passing array alike tables or patterns, they will be *unpacked*. +> +> #### examples: +> ```lua +> local p = pattern.new() +> p:push_back(1) -- {1} +> p:push_back(2,3) -- {1,2,3} +> p:push_back{4} -- {1,2,3,4} +> p:push_back({5,{6,7}) -- {1,2,3,4,5,6,7} +> ``` +### pop_back(self : [`Pattern`](../API/pattern.md#Pattern)) {#pop_back} +`->`[`PulseValue`](#PulseValue) + +> Remove an entry from the back of the pattern. returns the popped item. +> +> #### examples: +> ```lua +> local p = pattern.from({1,2}) +> p:pop_back() -- {1} +> p:pop_back() -- {} +> p:pop_back() -- {} +> ``` +### repeat_n(self : [`Pattern`](../API/pattern.md#Pattern), count : [`integer`](../API/builtins/integer.md)) {#repeat_n} +`->`[`Pattern`](../API/pattern.md#Pattern) + +> Duplicate the pattern n times. +> +> #### examples: +> ```lua +> local p = pattern.from{1,2,3} +> patterns:repeat_n(2) -- {1,2,3,1,2,3} +> ``` +### spread(self : [`Pattern`](../API/pattern.md#Pattern), amount : [`number`](../API/builtins/number.md), empty_value : [`PulseValue`](#PulseValue)[`?`](../API/builtins/nil.md)) {#spread} +`->`[`Pattern`](../API/pattern.md#Pattern) + +> Expand (with amount > 1) or shrink (amount < 1) the length of the pattern by the +> given factor, spreading allowed content evenly and filling gaps with 0 or the +> given empty value. +> +> #### examples: +> ```lua +> local p = pattern.from{1,1} +> p:spread(2) -- {1,0,1,0} +> p:spread(0.5) -- {1,1} +> ``` + + + +--- +## Aliases +### PulseValue {#PulseValue} +[`boolean`](../API/builtins/boolean.md) | [`string`](../API/builtins/string.md) | [`number`](../API/builtins/number.md) | [`table`](../API/builtins/table.md) +> Valid pulse value in a pattern + + + + diff --git a/docs/src/API/rhythm.md b/docs/src/API/rhythm.md new file mode 100644 index 0000000..e79898d --- /dev/null +++ b/docs/src/API/rhythm.md @@ -0,0 +1,457 @@ + + + +# Global {#Global} + +--- +## Functions +### rhythm(options : [`RhythmOptions`](../API/rhythm.md#RhythmOptions)) {#rhythm} +`->`[`userdata`](../API/builtins/userdata.md) + +> Create a new rhythm with the given configuration. +> +> #### examples: +> ```lua +> -- trigger a chord sequence every 4 bars after 4 bars +> return rhythm { +> unit = "bars", +> resolution = 4, +> offset = 1, +> emit = sequence("c4'm", note("g3'm7"):transpose({0, 12, 0, 0})) +> } +> +> --trigger notes in an euclidean triplet pattern +> return rhythm { +> unit = "1/8", +> resolution = 3/2, +> pattern = pattern.euclidean(6, 16, 2), +> emit = sequence("c3", "c3", note{ "c4", "a4" }:volume(0.75)) +> } +> +> --trigger notes in a seeded, random subdivision pattern +> math.randomseed(23498) +> return rhythm { +> unit = "1/8", +> pattern = { 1, { 0, 1 }, 0, 0.3, 0.2, 1, { 0.5, 0.1, 1 }, 0.5 }, +> emit = { "c4" }, +> } +> +> --trigger random notes in a random pattern from a pentatonic scale +> return rhythm { +> unit = "1/16", +> pattern = function(context) +> return (context.pulse_step % 4 == 1) or (math.random() > 0.8) +> end, +> emit = function(context) +> local cmin = scale("c5", "pentatonic minor").notes +> return function(context) +> return { key = cmin[math.random(#cmin)], volume = 0.7 } +> end +> end +> } +> +> --play a seeded tidal cycle +> math.randomseed(9347565) +> return rhythm { +> unit = "bars", -- emit one cycle per bar +> emit = cycle("[c4 [f5 f4]*2]|[c4 [g5 g4]*3]") +> } +> -- +> ``` + + + +--- +## Aliases +### NoteValue {#NoteValue} +[`string`](../API/builtins/string.md) | [`number`](../API/builtins/number.md) | [`NoteTable`](../API/note.md#NoteTable) | [`nil`](../API/builtins/nil.md) + + +### Pulse {#Pulse} +[`boolean`](../API/builtins/boolean.md) | [`number`](../API/builtins/number.md) | [`boolean`](../API/builtins/boolean.md) | [`number`](../API/builtins/number.md) | `0` | `1` | [`Pulse`](#Pulse) | [`nil`](../API/builtins/nil.md)[] | `0` | `1` | [`nil`](../API/builtins/nil.md) +> ```lua +> -- Single pulse value or a nested subdivision of pulses within a pattern. +> Pulse: +> | 0 +> | 1 +> ``` + + + + +# RhythmOptions {#RhythmOptions} +> Construction options for a new rhythm. + +--- +## Properties +### unit : `"ms"` | `"seconds"` | `"bars"` | `"beats"` | `"1/1"` | `"1/2"` | `"1/4"` | `"1/8"` | `"1/16"` | `"1/32"` | `"1/64"` {#unit} +> Base time unit of the emitter. Use `resolution` to apply an additional factor, in order to +> create other less common rhythm bases. +> #### examples: +> ```lua +> unit = "beats", resolution = 1.01 --> slightly off beat pulse +> unit = "1/16", resolution = 4/3 --> triplet +> ``` + +### resolution : [`number`](../API/builtins/number.md)[`?`](../API/builtins/nil.md) {#resolution} +> Factor which is applied on `unit` to specify the final time resolution of the emitter. +> #### examples: +> ```lua +> unit = "beats", resolution = 1.01 --> slightly off beat pulse +> unit = "1/16", resolution = 4/3 --> triplet +> ``` + +### offset : [`number`](../API/builtins/number.md)[`?`](../API/builtins/nil.md) {#offset} +> Optional offset in `unit * resolution` time units. By default 0. +> When set, the rhythm's event output will be delayed by the given offset value. +> #### examples: +> ```lua +> unit = "1/4", +> resolution = 4, +> offset = 4 -- start emitting after 4*4 beats +> ``` + +### inputs : [`InputParameter`](../API/input.md#InputParameter)[] {#inputs} +> Define optional input parameters for the rhythm. Input parameters can dynamically +> change a rhythms behavior everywhere where `context`s are passed, e.g. in pattern, +> gate, emitter or cycle map generator functions. +> +> #### examples: +> ```lua +> -- trigger a single note as specified by input parameter 'note' +> -- when input parameter 'enabled' is true, else triggers nothing. +> inputs = { +> parameter.boolean("enabled", true), +> parameter.integer("note", 48, { 0, 127 }) +> }, +> -- [...] +> emit = function(context) +> if context.inputs.enabled then -- boolean value +> return note(context.inputs.note) -- integer value +> else +> return nil +> end +> end +> ``` + +### pattern : [`boolean`](../API/builtins/boolean.md) | [`number`](../API/builtins/number.md) | `0` | `1` | [`Pulse`](#Pulse) | [`nil`](../API/builtins/nil.md)[] | (context : [`PatternContext`](../API/rhythm.md#PatternContext)) `->` [`boolean`](../API/builtins/boolean.md) | [`number`](../API/builtins/number.md) | `0` | `1` | [`Pulse`](#Pulse) | [`nil`](../API/builtins/nil.md) | (context : [`PatternContext`](../API/rhythm.md#PatternContext)) `->` (context : [`PatternContext`](../API/rhythm.md#PatternContext)) `->` [`boolean`](../API/builtins/boolean.md) | [`number`](../API/builtins/number.md) | `0` | `1` | [`Pulse`](#Pulse) | [`nil`](../API/builtins/nil.md) {#pattern} +> Specify the rhythmical pattern of the emitter. Each pulse with a value of 1 or true +> will cause an event from the `emitter` property to be triggered in the emitters +> time unit. 0 or nil values never trigger, and values in-between do *maybe* trigger. +> +> To create deterministic random patterns, seed the random number generator before +> creating the rhythm via `math.randomseed(some_seed)` +> +> Patterns can contains subdivisions, sub tables of pulses, to "cram" multiple pulses +> into a single pulse's time interval. This way more complex rhythmical patterns can +> be created. +> +> When no pattern is defined, a constant pulse of `1` is triggered by the rhythm. +> +> Just like the `emitter` property, patterns can either be a fixed array of values or a +> function or iterator which produces values dynamically. +> +> #### examples: +> ```lua +> -- a fixed pattern +> pattern = { 1, 0, 0, 1 } +> -- "cram" pulses into a single pulse slot via subdivisions +> pattern = { 1, { 1, 1, 1 } } +> +> -- fixed patterns created via the "patterns" lib +> pattern = pattern.from{ 1, 0 } * 3 + { 1, 1 } +> pattern = pattern.euclidean(7, 16, 2) +> +> -- stateless generator function +> pattern = function(context) +> return math.random(0, 1) +> end +> +> -- stateful generator function +> pattern = function(context) +> local triggers = table.create({0, 6, 10}) +> ---@param context PatternContext +> return function(context) +> return triggers:find((context.step - 1) % 16) ~= nil +> end +> end +> +> ``` + +### repeats : [`boolean`](../API/builtins/boolean.md) | [`integer`](../API/builtins/integer.md) {#repeats} +> If and how many times a pattern should repeat. When 0 or false, the pattern does not repeat +> and plays back only once. When true, the pattern repeats endlessly, which is the default. +> When a number > 0, this specifies the number of times the pattern repeats until it stops. +> +> Note: When `pattern` is a function or iterator, the repeat count is the number of +> *function calls or iteration steps*. When the pattern is a pulse array, this is the number of +> times the whole pattern gets repeated. +> +> #### examples: +> ```lua +> repeat = 0 -- one-shot +> repeat = false -- also a one-shot +> repeat = 3 -- play the pattern 4 times +> repeat = true -- play & repeat forever +> ``` + +### gate : (context : [`GateContext`](../API/rhythm.md#GateContext)) `->` [`boolean`](../API/builtins/boolean.md) | (context : [`GateContext`](../API/rhythm.md#GateContext)) `->` (context : [`GateContext`](../API/rhythm.md#GateContext)) `->` [`boolean`](../API/builtins/boolean.md) {#gate} +> Optional pulse train filter function or generator function which filters events between +> the pattern and emitter. By default a threshold gate, which passes all pulse values +> greater than zero. +> +> Custom function should returns true when a pattern pulse value should be passed, +> and false when the emitter should be skipped. +> +> #### examples: +> ```lua +> -- probability gate: skips all 0s, passes all 1s. pulse alues in range (0, 1) are +> -- maybe passed, using the pulse value as probablility. +> gate = function(context) +> return context.pulse_value > math.random() +> end +> ``` + +### emit : [`Cycle`](../API/cycle.md#Cycle) | [`Sequence`](../API/sequence.md#Sequence) | [`Note`](../API/note.md#Note) | [`NoteValue`](#NoteValue) | [`Note`](../API/note.md#Note) | [`NoteValue`](#NoteValue)[] | (context : [`EmitterContext`](../API/rhythm.md#EmitterContext)) `->` [`NoteValue`](#NoteValue) | (context : [`EmitterContext`](../API/rhythm.md#EmitterContext)) `->` (context : [`EmitterContext`](../API/rhythm.md#EmitterContext)) `->` [`NoteValue`](#NoteValue) {#emit} +> Specify the melodic pattern of the rhythm. For every pulse in the rhythmical pattern, the event +> from the specified emit sequence. When the end of the sequence is reached, it starts again from +> the beginning.
+> +> To generate notes dynamically, you can pass a function or a function iterator, instead of a +> fixed array or sequence of notes.
+> +> Events can also be generated using the tidal cycle mini-notation. Cycles are repeated endlessly +> by default, and have the duration of a single pulse in the pattern. Patterns can be used to +> sequence cycles too. +> +> #### examples: +> ```lua +> -- a sequence of c4, g4 +> emit = {"c4", "g4"} +> -- a chord of c4, d#4, g4 +> emit = {{"c4", "d#4", "g4"}} -- or {"c4'min"} +> -- a sequence of c4, g4 with volume 0.5 +> emit = sequence{"c4", "g4"}:volume(0.5) +> +> -- stateless generator function +> emit = function(context) +> return 48 + math.random(1, 4) * 5 +> end +> +> -- stateful generator function +> emit = function(initial_context) +> local count, step, notes = 1, 2, scale("c5", "minor").notes +> ---@param context EmitterContext +> return function(context) +> local key = notes[count] +> count = (count + step - 1) % #notes + 1 +> return { key = key, volume = 0.5 } +> end +> end +> +> -- a note pattern +> local tritone = scale("c5", "tritone") +> ... +> emit = pattern.from(tritone:chord(1, 4)):euclidean(6) + +> pattern.from(tritone:chord(5, 4)):euclidean(6) +> +> -- a tidal cycle +> emit = cycle("<[a3 c4 e4 a4]*3 [d4 g3 g4 c4]>") +> -- +> ``` + + + + + +--- +## Aliases +### NoteValue {#NoteValue} +[`string`](../API/builtins/string.md) | [`number`](../API/builtins/number.md) | [`NoteTable`](../API/note.md#NoteTable) | [`nil`](../API/builtins/nil.md) + + +### Pulse {#Pulse} +[`boolean`](../API/builtins/boolean.md) | [`number`](../API/builtins/number.md) | [`boolean`](../API/builtins/boolean.md) | [`number`](../API/builtins/number.md) | `0` | `1` | [`Pulse`](#Pulse) | [`nil`](../API/builtins/nil.md)[] | `0` | `1` | [`nil`](../API/builtins/nil.md) +> ```lua +> -- Single pulse value or a nested subdivision of pulses within a pattern. +> Pulse: +> | 0 +> | 1 +> ``` + + + + +# EmitterContext {#EmitterContext} +> Context passed to 'emit' functions. + +--- +## Properties +### trigger_note : [`integer`](../API/builtins/integer.md)[`?`](../API/builtins/nil.md) {#trigger_note} +> Note value that triggered, started the rhythm, if any. + +### trigger_volume : [`number`](../API/builtins/number.md)[`?`](../API/builtins/nil.md) {#trigger_volume} +> Note volume that triggered, started the rhythm, if any. + +### trigger_offset : [`integer`](../API/builtins/integer.md)[`?`](../API/builtins/nil.md) {#trigger_offset} +> Note slice offset value that triggered, started the rhythm, if any. + +### inputs : table<[`string`](../API/builtins/string.md), [`boolean`](../API/builtins/boolean.md) | [`string`](../API/builtins/string.md) | [`number`](../API/builtins/number.md)> {#inputs} +> Current input parameter values, using parameter ids as keys +> and the actual parameter value as value. + +### beats_per_min : [`number`](../API/builtins/number.md) {#beats_per_min} +> Project's tempo in beats per minutes. + +### beats_per_bar : [`integer`](../API/builtins/integer.md) {#beats_per_bar} +> Project's beats per bar setting. + +### samples_per_sec : [`integer`](../API/builtins/integer.md) {#samples_per_sec} +> Project's sample rate in samples per second. + +### pulse_step : [`integer`](../API/builtins/integer.md) {#pulse_step} +> Continues pulse counter, incrementing with each new **skipped or emitted pulse**. +> Unlike `step` in emitter this includes all pulses, so it also counts pulses which do +> not emit events. Starts from 1 when the rhythm starts running or is reset. + +### pulse_time_step : [`number`](../API/builtins/number.md) {#pulse_time_step} +> Continues pulse time counter, incrementing with each new **skipped or emitted pulse**. +> Starts from 0 and increases with each new pulse by the pulse's step time duration. + +### pulse_time : [`number`](../API/builtins/number.md) {#pulse_time} +> Current pulse's step time as fraction of a full step in the pattern. For simple pulses this +> will be 1, for pulses in subdivisions this will be the reciprocal of the number of steps in the +> subdivision, relative to the parent subdivisions pulse step time. +> #### examples: +> ```lua +> {1, {1, 1}} --> step times: {1, {0.5, 0.5}} +> ``` + +### pulse_value : [`number`](../API/builtins/number.md) {#pulse_value} +> Current pulse value. For binary pulses this will be 1, 0 pulse values will not cause the emitter +> to be called, so they never end up here. +> Values between 0 and 1 will be used as probabilities and thus are maybe emitted or skipped. + +### playback : [`PlaybackState`](#PlaybackState) {#playback} +> Specifies how the emitter currently is running. + +### step : [`integer`](../API/builtins/integer.md) {#step} +> Continues step counter, incrementing with each new *emitted* pulse. +> Unlike `pulse_step` this does not include skipped, zero values pulses so it basically counts +> how often the emit function already got called. +> Starts from 1 when the rhythm starts running or is reset. + + + + + +--- +## Aliases +### PlaybackState {#PlaybackState} +`"running"` | `"seeking"` +> ```lua +> -- - *seeking*: The emitter is auto-seeked to a target time. All results are discarded. Avoid +> -- unnecessary computations while seeking, and only maintain your generator's internal state. +> -- - *running*: The emitter is played back regularly. Results are audible. +> PlaybackState: +> | "seeking" +> | "running" +> ``` + + + + +# GateContext {#GateContext} +> Context passed to `gate` functions. + +--- +## Properties +### trigger_note : [`integer`](../API/builtins/integer.md)[`?`](../API/builtins/nil.md) {#trigger_note} +> Note value that triggered, started the rhythm, if any. + +### trigger_volume : [`number`](../API/builtins/number.md)[`?`](../API/builtins/nil.md) {#trigger_volume} +> Note volume that triggered, started the rhythm, if any. + +### trigger_offset : [`integer`](../API/builtins/integer.md)[`?`](../API/builtins/nil.md) {#trigger_offset} +> Note slice offset value that triggered, started the rhythm, if any. + +### inputs : table<[`string`](../API/builtins/string.md), [`boolean`](../API/builtins/boolean.md) | [`string`](../API/builtins/string.md) | [`number`](../API/builtins/number.md)> {#inputs} +> Current input parameter values, using parameter ids as keys +> and the actual parameter value as value. + +### beats_per_min : [`number`](../API/builtins/number.md) {#beats_per_min} +> Project's tempo in beats per minutes. + +### beats_per_bar : [`integer`](../API/builtins/integer.md) {#beats_per_bar} +> Project's beats per bar setting. + +### samples_per_sec : [`integer`](../API/builtins/integer.md) {#samples_per_sec} +> Project's sample rate in samples per second. + +### pulse_step : [`integer`](../API/builtins/integer.md) {#pulse_step} +> Continues pulse counter, incrementing with each new **skipped or emitted pulse**. +> Unlike `step` in emitter this includes all pulses, so it also counts pulses which do +> not emit events. Starts from 1 when the rhythm starts running or is reset. + +### pulse_time_step : [`number`](../API/builtins/number.md) {#pulse_time_step} +> Continues pulse time counter, incrementing with each new **skipped or emitted pulse**. +> Starts from 0 and increases with each new pulse by the pulse's step time duration. + +### pulse_time : [`number`](../API/builtins/number.md) {#pulse_time} +> Current pulse's step time as fraction of a full step in the pattern. For simple pulses this +> will be 1, for pulses in subdivisions this will be the reciprocal of the number of steps in the +> subdivision, relative to the parent subdivisions pulse step time. +> #### examples: +> ```lua +> {1, {1, 1}} --> step times: {1, {0.5, 0.5}} +> ``` + +### pulse_value : [`number`](../API/builtins/number.md) {#pulse_value} +> Current pulse value. For binary pulses this will be 1, 0 pulse values will not cause the emitter +> to be called, so they never end up here. +> Values between 0 and 1 will be used as probabilities and thus are maybe emitted or skipped. + + + + + +# PatternContext {#PatternContext} +> Context passed to `pattern` and `gate` functions. + +--- +## Properties +### trigger_note : [`integer`](../API/builtins/integer.md)[`?`](../API/builtins/nil.md) {#trigger_note} +> Note value that triggered, started the rhythm, if any. + +### trigger_volume : [`number`](../API/builtins/number.md)[`?`](../API/builtins/nil.md) {#trigger_volume} +> Note volume that triggered, started the rhythm, if any. + +### trigger_offset : [`integer`](../API/builtins/integer.md)[`?`](../API/builtins/nil.md) {#trigger_offset} +> Note slice offset value that triggered, started the rhythm, if any. + +### inputs : table<[`string`](../API/builtins/string.md), [`boolean`](../API/builtins/boolean.md) | [`string`](../API/builtins/string.md) | [`number`](../API/builtins/number.md)> {#inputs} +> Current input parameter values, using parameter ids as keys +> and the actual parameter value as value. + +### beats_per_min : [`number`](../API/builtins/number.md) {#beats_per_min} +> Project's tempo in beats per minutes. + +### beats_per_bar : [`integer`](../API/builtins/integer.md) {#beats_per_bar} +> Project's beats per bar setting. + +### samples_per_sec : [`integer`](../API/builtins/integer.md) {#samples_per_sec} +> Project's sample rate in samples per second. + +### pulse_step : [`integer`](../API/builtins/integer.md) {#pulse_step} +> Continues pulse counter, incrementing with each new **skipped or emitted pulse**. +> Unlike `step` in emitter this includes all pulses, so it also counts pulses which do +> not emit events. Starts from 1 when the rhythm starts running or is reset. + +### pulse_time_step : [`number`](../API/builtins/number.md) {#pulse_time_step} +> Continues pulse time counter, incrementing with each new **skipped or emitted pulse**. +> Starts from 0 and increases with each new pulse by the pulse's step time duration. + + + + + diff --git a/docs/src/API/scale.md b/docs/src/API/scale.md new file mode 100644 index 0000000..461339e --- /dev/null +++ b/docs/src/API/scale.md @@ -0,0 +1,266 @@ + + + +# Global {#Global} + +--- +## Functions +### scale(key : [`string`](../API/builtins/string.md) | [`number`](../API/builtins/number.md), mode : [`ScaleMode`](#ScaleMode)) {#scale} +`->`[`Scale`](../API/scale.md#Scale) + +> Create a new scale from the given key notes and a mode name. +> +> Scale names can also be shortened by using the following synonyms: +> - "8-tone" -> "eight-tone" +> - "9-tone" -> "nine-tone" +> - "aug" -> "augmented" +> - "dim" -> "diminished" +> - "dom" -> "Dominant" +> - "egypt" -> "egyptian" +> - "harm" -> "harmonic" +> - "hungary" -> "hungarian" +> - "roman" -> "romanian" +> - "min" -> "minor" +> - "maj" -> "major" +> - "nat" -> "natural" +> - "penta" -> "pentatonic" +> - "span" -> "spanish", +> +> #### examples: +> ```lua +> scale("c4", "minor").notes -> {"c4", "d4", "d#4", "f4", "g4", "g#4", "a#4"} +> ``` +> +> ```lua +> -- Available scales. +> mode: +> | "chromatic" +> | "major" +> | "minor" +> | "natural major" +> | "natural minor" +> | "pentatonic major" +> | "pentatonic minor" +> | "pentatonic egyptian" +> | "blues major" +> | "blues minor" +> | "whole tone" +> | "augmented" +> | "prometheus" +> | "tritone" +> | "harmonic major" +> | "harmonic minor" +> | "melodic minor" +> | "all minor" +> | "dorian" +> | "phrygian" +> | "phrygian dominant" +> | "lydian" +> | "lydian augmented" +> | "mixolydian" +> | "locrian" +> | "locrian major" +> | "super locrian" +> | "neapolitan major" +> | "neapolitan minor" +> | "romanian minor" +> | "spanish gypsy" +> | "hungarian gypsy" +> | "enigmatic" +> | "overtone" +> | "diminished half" +> | "diminished whole" +> | "spanish eight-tone" +> | "nine-tone" +> ``` + + + +--- +## Aliases +### DegreeValue {#DegreeValue} +[`integer`](../API/builtins/integer.md) | `"i"` | `"ii"` | `"iii"` | `"iv"` | `"v"` | `"vi"` | `"vii"` +> ```lua +> -- Roman number or plain number as degree in range [1 - 7] +> DegreeValue: +> | "i" +> | "ii" +> | "iii" +> | "iv" +> | "v" +> | "vi" +> | "vii" +> ``` + +### NoteValue {#NoteValue} +[`string`](../API/builtins/string.md) | [`number`](../API/builtins/number.md) | [`NoteTable`](../API/note.md#NoteTable) | [`nil`](../API/builtins/nil.md) + + +### ScaleMode {#ScaleMode} +[`string`](../API/builtins/string.md) | `"all minor"` | `"augmented"` | `"blues major"` | `"blues minor"` | `"chromatic"` | `"diminished half"` | `"diminished whole"` | `"dorian"` | `"enigmatic"` | `"harmonic major"` | `"harmonic minor"` | `"hungarian gypsy"` | `"locrian major"` | `"locrian"` | `"lydian augmented"` | `"lydian"` | `"major"` | `"melodic minor"` | `"minor"` | `"mixolydian"` | `"natural major"` | `"natural minor"` | `"neapolitan major"` | `"neapolitan minor"` | `"nine-tone"` | `"overtone"` | `"pentatonic egyptian"` | `"pentatonic major"` | `"pentatonic minor"` | `"phrygian dominant"` | `"phrygian"` | `"prometheus"` | `"romanian minor"` | `"spanish eight-tone"` | `"spanish gypsy"` | `"super locrian"` | `"tritone"` | `"whole tone"` +> ```lua +> -- Available scales. +> ScaleMode: +> | "chromatic" +> | "major" +> | "minor" +> | "natural major" +> | "natural minor" +> | "pentatonic major" +> | "pentatonic minor" +> | "pentatonic egyptian" +> | "blues major" +> | "blues minor" +> | "whole tone" +> | "augmented" +> | "prometheus" +> | "tritone" +> | "harmonic major" +> | "harmonic minor" +> | "melodic minor" +> | "all minor" +> | "dorian" +> | "phrygian" +> | "phrygian dominant" +> | "lydian" +> | "lydian augmented" +> | "mixolydian" +> | "locrian" +> | "locrian major" +> | "super locrian" +> | "neapolitan major" +> | "neapolitan minor" +> | "romanian minor" +> | "spanish gypsy" +> | "hungarian gypsy" +> | "enigmatic" +> | "overtone" +> | "diminished half" +> | "diminished whole" +> | "spanish eight-tone" +> | "nine-tone" +> ``` + + + + +# Scale {#Scale} + +--- +## Properties +### notes : [`integer`](../API/builtins/integer.md)[] {#notes} +> Scale note values as integers, in ascending order of the mode, starting from the scale's key note. + + + +--- +## Functions +### chord([*self*](../API/builtins/self.md), degree : [`DegreeValue`](#DegreeValue), note_count : [`integer`](../API/builtins/integer.md)[`?`](../API/builtins/nil.md)) {#chord} +`->`notes : [`integer`](../API/builtins/integer.md)[] + +> Create a chord from the given degree, built from the scale's intervals. +> Skips nth notes from the root as degree, then takes every second note +> from the remaining scale to create a chord. By default a triad is created. +> +> #### examples: +> ```lua +> local cmin = scale("c4", "minor") +> cmin:chord("i", 4) --> {48, 51, 55, 58} +> note(cmin:chord(5)):transpose({12, 0, 0}) --> Gm 1st inversion +> ``` +> +> ```lua +> -- Roman number or plain number as degree in range [1 - 7] +> degree: +> | "i" +> | "ii" +> | "iii" +> | "iv" +> | "v" +> | "vi" +> | "vii" +> ``` +### degree([*self*](../API/builtins/self.md), ...[`DegreeValue`](#DegreeValue)) {#degree} +`->`... : [`integer`](../API/builtins/integer.md) + +> Get a single or multiple notes by its degree from the scale, using the given roman +> number string or a plain number as interval index. +> Allows picking intervals from the scale to e.g. create chords with roman number +> notation. +> +> #### examples: +> ```lua +> local cmin = scale("c4", "minor") +> cmin:degree(1) --> 48 ("c4") +> cmin:degree(5) --> 55 +> cmin:degree("i", "iii", "v") --> 48, 50, 55 +> ``` +> +> ```lua +> -- Roman number or plain number as degree in range [1 - 7] +> ...(param): +> | "i" +> | "ii" +> | "iii" +> | "iv" +> | "v" +> | "vi" +> | "vii" +> ``` +### notes_iter([*self*](../API/builtins/self.md), count : [`integer`](../API/builtins/integer.md)[`?`](../API/builtins/nil.md)) {#notes_iter} +`->`() `->` [`integer`](../API/builtins/integer.md) | [`nil`](../API/builtins/nil.md) + +> Create an iterator function that returns up to `count` notes from the scale. +> If the count exceeds the number of notes in the scale, then notes from the next +> octave are taken. +> +> The iterator function returns nil when the maximum number of MIDI notes has been +> reached, or when the given optional `count` parameter has been exceeded. +> +> #### examples: +> ```lua +> --collect 16 notes of a c major scale +> local cmaj = scale("c4", "major") +> local notes = {} +> for note in cmin:notes_iter(16) do +> table.insert(notes, note) +> end +> -- same using the `pattern` library +> local notes = pattern.new(16):init(cmaj.notes_iter()) +> ``` +### fit([*self*](../API/builtins/self.md), ...[`NoteValue`](#NoteValue)) {#fit} +`->`[`integer`](../API/builtins/integer.md)[] + +> Fit given note value(s) into scale by moving them to the nearest note in the scale. +> +> #### examples: +> ```lua +> local cmin = scale("c4", "minor") +> cmin:fit("c4", "d4", "f4") -> 48, 50, 53 (cmaj -> cmin) +> ``` + + + +--- +## Aliases +### DegreeValue {#DegreeValue} +[`integer`](../API/builtins/integer.md) | `"i"` | `"ii"` | `"iii"` | `"iv"` | `"v"` | `"vi"` | `"vii"` +> ```lua +> -- Roman number or plain number as degree in range [1 - 7] +> DegreeValue: +> | "i" +> | "ii" +> | "iii" +> | "iv" +> | "v" +> | "vi" +> | "vii" +> ``` + +### NoteValue {#NoteValue} +[`string`](../API/builtins/string.md) | [`number`](../API/builtins/number.md) | [`NoteTable`](../API/note.md#NoteTable) | [`nil`](../API/builtins/nil.md) + + + + + diff --git a/docs/src/API/sequence.md b/docs/src/API/sequence.md new file mode 100644 index 0000000..e9f4409 --- /dev/null +++ b/docs/src/API/sequence.md @@ -0,0 +1,93 @@ + + + +# Global {#Global} + +--- +## Functions +### sequence(...[`Note`](../API/note.md#Note) | [`NoteValue`](#NoteValue)) {#sequence} +`->`[`Sequence`](../API/sequence.md#Sequence) + +> Create a sequence from an array of note values or note value varargs. +> +> Using `sequence` instead of a raw `{}` table can be useful to ease transforming the note +> content and to explicitly pass a sequence of e.g. single notes to the emitter. +> +> #### examples: +> ```lua +> -- sequence of C4, C5 and an empty note +> sequence(48, "c5", {}) +> -- sequence of a +5 transposed C4 and G4 major chord +> sequence("c4'maj", "g4'maj"):transpose(5) +> ``` + + + +--- +## Aliases +### NoteValue {#NoteValue} +[`string`](../API/builtins/string.md) | [`number`](../API/builtins/number.md) | [`NoteTable`](../API/note.md#NoteTable) | [`nil`](../API/builtins/nil.md) + + + + + +# Sequence {#Sequence} + +--- +## Properties +### notes : [`NoteTable`](../API/note.md#NoteTable)[][] {#notes} + + +--- +## Functions +### transpose([*self*](../API/builtins/self.md), step : [`integer`](../API/builtins/integer.md) | [`integer`](../API/builtins/integer.md)[]) {#transpose} +`->`[`Sequence`](../API/sequence.md#Sequence) + +> Transpose all notes key values with the specified step value or values. +> +> Values outside of the valid key range (0 - 127) will be clamped. +> +> #### examples: +> ```lua +> sequence("c4", "d#5"):transpose(12) +> sequence(note("c'maj"), note("c'maj")):transpose({0, 5}) +> ``` +### amplify([*self*](../API/builtins/self.md), factor : [`number`](../API/builtins/number.md) | [`number`](../API/builtins/number.md)[]) {#amplify} +`->`[`Sequence`](../API/sequence.md#Sequence) + +> Multiply all notes volume values with the specified factor or factors. +> +> Values outside of the valid volume range (0 - 1) will be clamped. +> +> #### examples: +> ```lua +> sequence({"c4 0.5", "g4"}):amplify(0.5) +> sequence("c'maj 0.5"):amplify({2.0, 1.0, 0.3}) +> ``` +### instrument([*self*](../API/builtins/self.md), instrument : [`number`](../API/builtins/number.md) | [`number`](../API/builtins/number.md)[]) {#instrument} +`->`[`Note`](../API/note.md#Note) + +> Set the instrument attribute of all notes to the specified value or values. +### volume([*self*](../API/builtins/self.md), volume : [`number`](../API/builtins/number.md) | [`number`](../API/builtins/number.md)[]) {#volume} +`->`[`Sequence`](../API/sequence.md#Sequence) + +> Set the volume attribute of all notes to the specified value or values. +> +> #### examples: +> ```lua +> sequence({"c4", "g4"}):volume(0.5) +> sequence("c'maj"):volume(0.5) +> sequence("c'maj"):volume({0.1, 0.2, 0.3}) +> ``` +### panning([*self*](../API/builtins/self.md), panning : [`number`](../API/builtins/number.md) | [`number`](../API/builtins/number.md)[]) {#panning} +`->`[`Note`](../API/note.md#Note) + +> Set the panning attribute of all notes to the specified value or values. +### delay([*self*](../API/builtins/self.md), delay : [`number`](../API/builtins/number.md) | [`number`](../API/builtins/number.md)[]) {#delay} +`->`[`Sequence`](../API/sequence.md#Sequence) + +> Set the delay attribute of all notes to the specified value or values. + + + diff --git a/docs/src/README.md b/docs/src/README.md new file mode 100644 index 0000000..096e478 --- /dev/null +++ b/docs/src/README.md @@ -0,0 +1,35 @@ +# Welcome + +... to the afseq scripting guide! + +## Introduction + +***afseq***, aka **NerdoRhythm**, is an experimental imperative-styled music sequence generator engine. + +In addition to the custom imperative event generator approach, afseq also supports creating events using the Tidal Cycle mini-notation. + + +It allows you to programmatically create music sequences either in plain Rust (*-> static, compiled*) or in Lua (*-> dynamic, interpreted*). So it's also suitable for [live coding](https://github.com/pjagielski/awesome-live-coding-music ) music. + + +## Installation + +afseq is a Rust *library* that deals with raw musical event generation only. It does not generate any audio. You must use an application with built-in support for afseq to use it. + +You can also use `play-script.rs` from the [examples](https://github.com/emuell/afseq/tree/master/examples) in the git repository to test out afseq scripts using a basic sample player that plays a sample from the example assets folder using the script which has the same name as the audio file. + + +## Scripting + +afseq uses [Lua](https://www.lua.org/) as a scripting language to dynamically generate content. + +If you're not familiar with Lua, don't worry. Lua is very easy to pick up if you have used another imperative programming language before, and fortunately there are great tutorials out there, such as [this one](https://www.lua.org/pil/1.html). + + +## Creating Rhythms + +Ready to program some music? Then let's dive into the next chapter which will give you an overview of the overall architecture of a **rhythm**, the main building block in afseq. + +--- + +*Note: This guide covers the afseq Lua scripting API. For instructions on creating rhythms in plain Rust, see the [afseq crate docs](https://github.com/emuell/afseq).* diff --git a/docs/src/SUMMARY.md b/docs/src/SUMMARY.md new file mode 100644 index 0000000..cde6e5e --- /dev/null +++ b/docs/src/SUMMARY.md @@ -0,0 +1,40 @@ +- [Introduction](README.md) +- [Rhythm](guide/README.md) + - [Timebase](guide/timebase.md) + - [Pattern](guide/pattern.md) + - [Gate](guide/gate.md) + - [Emitter](guide/emitter.md) + - [Notes & Scales](guide/notes&scales.md) + - [Cycles](guide/cycles.md) + - [Parameters](guide/parameters.md) +- [Advanced Topics](extras/README.md) + - [Generators](extras/generators.md) + - [Randomization](extras/randomization.md) +- [Examples](examples/README.md) +- [API Reference](API/README.md) + + - [chord](API/chord.md) + - [cycle](API/cycle.md) + - [input](API/input.md) + - [note](API/note.md) + - [pattern](API/pattern.md) + - [rhythm](API/rhythm.md) + - [scale](API/scale.md) + - [sequence](API/sequence.md) + - [Module Extensions](API/modules.md) + - [math](API/modules/math.md) + - [table](API/modules/table.md) + - [Builtin Types](API/builtins.md) + - [any](API/builtins/any.md) + - [boolean](API/builtins/boolean.md) + - [function](API/builtins/function.md) + - [integer](API/builtins/integer.md) + - [lightuserdata](API/builtins/lightuserdata.md) + - [nil](API/builtins/nil.md) + - [number](API/builtins/number.md) + - [self](API/builtins/self.md) + - [string](API/builtins/string.md) + - [table](API/builtins/table.md) + - [unknown](API/builtins/unknown.md) + - [userdata](API/builtins/userdata.md) + \ No newline at end of file diff --git a/docs/src/examples/README.md b/docs/src/examples/README.md new file mode 100644 index 0000000..adfbd89 --- /dev/null +++ b/docs/src/examples/README.md @@ -0,0 +1,5 @@ +# Examples + +TODO + +here will be a few guided example scripts... diff --git a/docs/src/extras/README.md b/docs/src/extras/README.md new file mode 100644 index 0000000..1130326 --- /dev/null +++ b/docs/src/extras/README.md @@ -0,0 +1,5 @@ +# Advanced Topics + +This chapter contains more advanced topics about rhythms. + +Please read the basic concepts from the [guide](../guide/) first to get you started... diff --git a/docs/src/extras/generators.md b/docs/src/extras/generators.md new file mode 100644 index 0000000..34a4928 --- /dev/null +++ b/docs/src/extras/generators.md @@ -0,0 +1,94 @@ +# Generators + +[Patterns](../guide/pattern.md), [Gates](../guide/gate.md) and [Emitters](../guide/emitter.md) can use Lua functions to dynamically generate or evaluate content. + +Annonymous Lua functions, as used in rhythms, are actually [closures](https://www.lua.org/pil/6.1.html). They keep a record of their environment, so all (up)values which are declared outside of the annonymous function are accessible from within the function itself. + +We can use this in afseq scripts to keep track of a rhythm's *global* or *local* state. + +### Runtime + +To better understand how local and global states are relevant here, we first need to understand how rhythms are evaluated. + +Let's say we're in a DAW that supports afseq. This DAW triggers your rhythm script when a single note is triggered. If we now want to allow polyphonic playback of scripts, only *one script instance* is actually created *per instrument or track*. So a *single script* will be *triggered multiple times* with multiple notes. + +This means that all notes triggered by the DAW will share the same global state within a rhythm script. But this also means that in order to create local states for each individual note trigger, you'll need to keep track of a local state somehow. + + +### Functions + +In the following example, an emitter function keeps track of its state by referencing a globally defined `counter` variable. + +```lua +local counter = 0 +return rhythm { + emit = function(context) + local midi_note = counter + counter = (counter + 1) % 128 + return note(midi_note) + end, +} +``` + +When playing a single instance of this rhythm, you'll get an event stream of increasing note values. As expected. But when triggering this script multiple times polyphonically, each triggerd script instance increases the counter on its own, so you'll get multiple streams with note values increased by multiple note steps. + +### Contexts + +The easiest way to deal with this, is using the function's passed context. Apart from global playback information such as the BPM or sample rate, the context also keeps track of the rhythm's internal playback state. + +A `context` passed to *pattern* functions only contains the global playback status. A `context` passed to *gate and emitter* functions contains the global playback status and status of the pattern. + +See [pattern context API](../API/rhythm.md#PatternContext), [gate context API](../API/rhythm.md#GateContext), [emitter context API](../API/rhythm.md#EmitterContext) for details. + +Contexts also may contain user defined input variables. See [parameters](../guide/parameters.md) for more info about this. + +By making use of the context we can now rewrite the example above to: + +```lua +return rhythm { + emit = function(context) + -- NB: pulse_step is an 1 based index, midi notes start with 0 + local midi_note = (context.pulse_step - 1) % 128 + return note(midi_note) + end +} +``` + +Because the context is unique for each newly triggered rhythm instance, we now get multiple continously increasing note event streams again. + + +### Generators + +Generators in afseq are pattern, gate or emit **functions**, that do **return another function**. This is similar to how iterators work in Lua. By returning a function from a function you can create a new local state that is valid for the returned function only. + +Let's use our counter example again with such a *generator*: + +```lua +return rhythm { + emit = function(_initial_context) + local counter = 0 -- local state! + return function(_context) + local midi_note = counter + counter = (counter + 1) % 128 + return note(midi_note) + end + end, +} +``` + +Here the outer function is called *once* when the rhythm is started - just to create the local state and to return the actual emit function. The returned function is then called repeatedly while the rhythm instance is running, operating on the local state it was initialised with. + + +### When to use what? + +- If you have a function that does not depend on an (external) state, simply use a global or anonymous function. + +- If you have a function which only depends on the rhythm playback context, use a global or anonymous function too and only make use of the passed context. + +- If you need to keep track of local states separately for each new rhythm run, use a generator. + +- If you need a mix of local and global state, use a generator which also reaches out to global and local variables. + +--- + +See also advanced topic about [randomization](./randomization.md), which makes use the the generator concept to keep track of local random states. \ No newline at end of file diff --git a/docs/src/extras/randomization.md b/docs/src/extras/randomization.md new file mode 100644 index 0000000..1305a31 --- /dev/null +++ b/docs/src/extras/randomization.md @@ -0,0 +1,66 @@ +# Randomization + +Controlled randomness can be a lot of fun when creating music algorithmically, so afseq supports a number of randomisation techniques to deal with *pseudo* randomness. + +### Random Number Generation + +You can use the standard Lua [`math.random()`](https://www.lua.org/pil/18.html) to create pseudo-random numbers in afseq, and can use [`math.randomseed()`](https://www.lua.org/pil/18.html) to seed them. + +Note that the standard Lua random implementation is overridden by afseq, to use a [Xoshiro256PlusPlus](https://docs.rs/rand_xoshiro/latest/rand_xoshiro/struct.Xoshiro256PlusPlus.html) random number generator. This ensures that seeded random operations behave the same on all platforms and architectures. + +Here's a simple example which creates a random melody line based on a scale. + +```lua +-- create a scale to pick notes from +local cmin = scale("c", "minor") + +-- pick 10 random notes from the scale +local random_notes = pattern.new(10, function() + return cmin.notes[math.random(#cmin.notes)] +end) + +-- play notes +return rhythm { + emit = random_notes +} +``` + +### Random Number Seeding + +You can use `math.randomseed()` to seed the global random number generator. + +```lua +-- create a scale to pick notes from +local cmin = scale("c", "minor") +-- pick **the same** random 10 notes from the scale every time +math.randomseed(1234) +local random_notes = pattern.new(10, function() + return cmin.notes[math.random(#cmin.notes)] +end) + +return rhythm { + emit = random_notes +} +``` + +### Local Random Number Generators + +When seeding the RNG, each time a rhythm is (re)started, an existing rhythm instance will continue to run. The global state of a rhythm script is not recreated each time the rhythm is played again. + +See [generators](./generators.md) for details of how afseq handles global and local states in general. + +To create multiple separate local random states, use the non standard [`math.randomstate(seed)`](../API/modules/math.md#randomstate) function to create local, possibly seeded random number generators. + +```lua +local cmin = scale("c", "minor") +return rhythm { + emit = function(context) + local rand = math.randomstate(1234) -- a local random number generator + return function(context) + return note(cmin.notes[rand(#cmin.notes)]) + end + end +} +``` + +In the example above, each newly triggered rhythm instance will result in the same sequence of *random* notes, and multiple running instances will not interfere with each other. diff --git a/docs/src/guide/README.md b/docs/src/guide/README.md new file mode 100644 index 0000000..4b3d581 --- /dev/null +++ b/docs/src/guide/README.md @@ -0,0 +1,127 @@ +# Rhythm + +afseq consumes [Lua script](https://www.lua.org/) files that define rhythms, the main building block in afseq. + +A rhythm programatically generates musical events. + +The Lua API uses configuration tables to define the rhythm and their sub-components, so the main building blocks of a script are defined via Lua tables and functions as specified in the [API documentation](../API/). + + +## Components + +- [TimeBase](./timebase.md) defines the time unit of a rhythm. +- [Pattern](./pattern.md) -> [Gate](./gate.md) -> [Emitter](./emitter.md) do perform the basic event generation in 3 stages. +- [Parameters](./parameters.md) change behaviour of all components during runtime. + +```md + *Inputs* + Optional user controlled parameters. + ↓ ↓ ↓ +┌────────────────────────────────────────────────────────┐ +│ *Time base* │ +│ Basic unit of time and the increment of a pulse. │ +│ ┌┄┄┄┄┄┄┄┄┄┄┄┄┄┐ │ +│ │ *Pattern* │ e.g. { 0, 1, 0, 1, 1 } │ +│ └┄┄┄┄┄┄┄┄┄┄┄┄┄┘ │ +│ Defines the basic rhythmic pattern as a pulse train. │ +│ ↓ │ +│ ┌┄┄┄┄┄┄┄┄┄┄┄┄┄┐ │ +│ │ *Gate* │ e.g. { return pulse > 0.5 } │ +│ └┄┄┄┄┄┄┄┄┄┄┄┄┄┘ │ +│ Passes or suppresses pattern pulses. │ +│ ↓ │ +│ ┌┄┄┄┄┄┄┄┄┄┄┄┄┄┐ │ +│ │ *Emitter* │ e.g. sequence{ "c4", "d#4", "g#" } │ +│ └┄┄┄┄┄┄┄┄┄┄┄┄┄┘ │ +│ Generates events for each incoming filtered pulse. │ +└────────────────────────────────────────────────────────┘ + ↓ ↓ ↓ + [o] [x] [o] + *Event Stream* +``` + +By separating the **rhythmic** from the **tonal** or parameter value part of a musical sequence, each part of the sequence can be freely modified, composed and (re)combined. We're basically treating music in two dimensions here: the *rhythmic* part as one dimension, and the *tonal* part as another. + +All content in rhythms can be either fixed -> e.g. a Lua table of events, or dynamic -> a Lua function that [generates](../extras/generators.md) events on the fly. + +All dynamic functions or generators can also be controlled, automated via [parameters](./parameters.md) to change their behaviour at runtime in response to user input (e.g. MIDI controllers, DAW parameter automation). This also allows creating more flexible rhythm templates. + + +## Examples + +A simple rhythm with a static pattern and emitter, using the default gate implementation. + +```lua +-- sequence of 1/4th c4 and two 1/8 c5 notes. +rhythm { + unit = "1/4", + pattern = { 1, { 1, 1 } }, + emit = { "c4", "c5", "c5" } +} +``` + +A rhythm with default pattern and gate, emitting a Tidal Cycle. + +```lua +-- emit a tidal cycle every bar +rhythm { + unit = "1/1", + emit = cycle("a4 e4@2 ") +} +``` + +A rhythm, using a Lua function as dynamic pattern generator. + +```lua +-- maybe trigger a c4 on every 2nd 1/4. +rhythm { + unit = "1/4", + pattern = function (context) + if (context.pulse_step % 2 == 1) then + return math.random() > 0.5 and 1 or 0 + else + return 1 + end + end, + emit = "c4" +} +``` + +A rhythm with a static pattern, dynamic seeded probablility gate and a dynamic emitter. + +```lua +-- change for other variations or set to nil to get *really* random behavior +local seed = 2234 + +-- maybe emits events, using pulse values as probability +return rhythm { + unit = "1/8", + + pattern = { 1, { 1, 0.1, 0.5 }, 1, { 1, 0, 0, 0.5 } }, + + gate = function (context) + -- create a local random number generator for the probability + local rand = math.randomstate(seed) + return function (context) + -- use pulse value as trigger probability + return context.pulse_value >= rand() + end + end, + + emit = function (context) + -- create a local random number generator for the humanizing delay + local rand = math.randomstate(seed) + return function (context) + local volume, delay = 1.0, 0.0 + if context.pulse_time < 1 then + -- lower volume and add a delay for events in sub patterns + volume = context.pulse_time + delay = rand() * 0.05 + end + return { key = "c4", volume = volume, delay = delay } + end + end, +} +``` + +See [Examples](../examples/README.md) in this guide for more advanced and guided examples. diff --git a/docs/src/guide/cycles.md b/docs/src/guide/cycles.md new file mode 100644 index 0000000..755d9b8 --- /dev/null +++ b/docs/src/guide/cycles.md @@ -0,0 +1,150 @@ +# Cycles + +In addition to static arrays of [notes](./notes&scales.md) or dynamic [generator functions](../extras/generators.md), emitters in afseq can also emit cycles using the tidal cycles [mini-notation](https://tidalcycles.org/docs/reference/mini_notation/). + + +[Tidal Cycles](https://tidalcycles.org/) allows you to make patterns with code using a custom functional approach. It includes a language for describing flexible (e.g. polyphonic, polyrhythmic, generative) sequences of sounds, notes, parameters, and all kind of information. + +## Usage + +To create cycles in afseq, use the [`cycle`](../API/cycle.md#cycle) function in the emitter and pass it a mini-notation as string. + +\> `emit = cycle("c4 d#4 _")` + +> [!NOTE] +> Please see [Tidal Cycles Mini-Notation Reference](https://tidalcycles.org/docs/reference/mini_notation/) for a complete overview of the cycle notation. + +### Limitations + +There's no exact specification for how tidal cycles work, and it's constantly evolving, but at the moment we support the mini notation as it works in Tidal, with the following limitations and changes: + +* Stacks and random choices are valid without brackets (`a | b` is parsed as `[a | b]`) + +* Operators currently only accept numbers on the right side (`a3*2` is valid, `a3*<1 2>` is not) + +* `:` - Sets the instrument or remappable target instead of selecting samples + +### Timing + +The base time of a pattern in tidal is specified as *cycles per second*. In afseq, the time of a cycle instead is given in *cycles per pattern pulse units*. + +```lua +-- emits an entire cycle every *bar* +rhythm { + unit = "bars", + emit = cycle("c d e f") +} +``` + +### Sequencing + +An emitter in afseq gets triggered for each incoming non-gated pattern pulse. This is true for cycles are well and allows you to sequence entire cycles too. + +```lua +-- emit an entire cycle's every *bar*, then pause for a bar, then repeat +rhythm { + unit = "bars", + pattern = {1, 0}, + emit = cycle("c d e f") +} +``` + +You can also use the mini notation to emit single notes only, making use of tidal's note alternating and randomization features only: + +```lua +-- emit a single note from a cycle in an euclidean pattern +rhythm { + unit = "beats", + pattern = pattern.euclidean(5, 8), + emit = cycle("") +} +``` + +### Seeding + +afseq's general random number generator is also used in cycles. So when you seed the global number generator, you can also seed the cycle's random operations with `math.randomseed(12345)`. + + +### Mapping + +Notes and chords in cycles are expressed as [note strings](./notes&scales.md#note-strings) in afseq. But you can also dynamically evaluate and map cycle identifiers using the cycle [`map`](../API/cycle.md#map) function. + +This allows you, for example, to inject [parameters](./parameters.md) into cycles or to use custom identifiers. + +Using custom identifiers with a static map (a Lua table): + +```lua +rhythm { + unit = "bars", + emit = cycle("[bd*4], [_ sn]*2"):map({ + ["bd"] = note("c4 #0"), + ["sn"] = note("g4 #1") + }) +} +``` + +Using custom identifiers with a dynamic map function (a Lua function): + +```lua +rhythm { + unit = "bars", + emit = cycle("[bd*4], [_ sn]*2"):map(function(context, value) + if value == "bd" then + return note("c4 #0") + elseif value == "sn" then + return note("g4 #1") + end + end) +} +``` + +## Examples + +A simple polyrhythm + +```lua +return rhythm { + unit = "1/1", + emit = cycle("[C3 D#4 F3 G#4], [[D#3?0.2 G4 F4]/64]*63") +} +``` + +Mapped multi channel beats + +```lua +return rhythm { + unit = "1/1", + emit = cycle([[ + [

*12], + [kd ~]*2 ~ [~ kd] ~, + [~ s1]*2, + [~ s2]*8 + ]]):map({ + kd = "c4 #0", -- Kick + s1 = "c4 #1", -- Snare + s2 = "c4 #1 v0.1", -- Ghost snare + h1 = "c4 #2", -- Hat + h2 = "c4 #2 v0.2", -- Hat + }) +} +``` + +Dynamically mapped roman chord numbers with user defined scale + +```lua +return rhythm { + unit = "1/1", + resolution = 4, + inputs = { + parameter.enum("mode", "major", {"major", "minor"}) + }, + emit = cycle("I V III VI"):map( + function(context, value) + local s = scale("c4", context.inputs.mode) + return function(context, value) + return value ~= "_" and s:chord(value) or value + end + end + ) +} +``` diff --git a/docs/src/guide/emitter.md b/docs/src/guide/emitter.md new file mode 100644 index 0000000..870fa3d --- /dev/null +++ b/docs/src/guide/emitter.md @@ -0,0 +1,138 @@ +# Emitter + +A rhythm's [`emitter`](../API/rhythm.md#emit) generates events for incoming pulse values. Just like the pattern, it can be made up of a static list of events or it can be a dynamic generator - a Lua function. + +In addition to dynamic Lua functions, you can also use a tidal [cycle](./cycles.md) as an emitter. + +The default emitter generates a single middle C note value for each incoming pulse. + + +## Event Types + +Currently afseq only supports monophonic or polyphonic *note events* as emitter output. This is likely to change in the future to allow other musically interesting events to be emitted. + +Note values can be expressed as: +- Raw integer values like `48`, which are interpreted as MIDI note numbers. +- Raw note strings such as `"c4"` (single notes) or `"d#4'maj"` (chords). +- Lua note tables like `{ key = 48, volume = 0.1 }`. +- Lua API note objects such as `note(48):volume(0.1)` or `note("c4", "g4")` or `note("c4'min"):transpose({-12, 0, 0})`. +- Lua `nil` values, empty table `{}` or `"-"` strings are interpreted as rests. +- The string `"off"` or `"~"` is interpreted as note off. + +See [notes & scales](./notes&scales.md) for more information about the different ways to create and manipulate notes and chords. + + +## Static Emitters + +The simplest form of a emitter is a Lua table (array) of note or nil values (a rest). + +Static emitter arrays define **note event sequences**. Each incoming, possibly filtered, [gated](./gate.md) pulse from the [pattern](./pattern.md) picks the next event in the sequence, then moves on in the sequence. Sequences are repeated as long as the pattern is running. + +» `emit = { "c4", "d#4", "g4" }` *arpeggio - sequence* + +» `emit = { { "c4", "d#4", "g4" } }` *single chord - single event* + +To ease distinguishing polyponic contents, use [`sequence`](../API/sequence.md) and [`note`](../API/note.md): + +» `emit = sequence("c4", "d#4", "g4")` *arpeggio - sequence* + +» `emit = note("c4", "d#4", "g4")` *single chord - single event* + + +## Dynamic Emitters + +Dynamic emitter functions return **single note events**. Each incoming, possibly filtered, [gated](./gate.md) impulse from the [pattern](./pattern.md) will trigger the emit function to create the next event as long as the pattern is running. + +» `emit = function(context) return math.random() > 0.5 and "c4" or "c5" end` *randomly emit c4 or c5 notes* + +» `emit = function(context) return context.pulse_count % 2 == 1 and "c4" or "c5" end` *alternate c4 and c5 notes* + +See API docs for [context](../API/rhythm.md#EmitterContext) for more info about the context passed to dynamic functions. + + +## Cycle Emitters + +Cycle emitters emit **a whole cycle** for a single pulse. So any incoming, possibly filtered, [gated](./gate.md) pulse from the [pattern](./pattern.md) will trigger a full cycle as long as the pattern is running. + +» `emit = cycle("[c4, d#4, g4]")` *a single chord* + +You probably won't use custom patterns or gate functions with cycles, but it's possible to sequence entire cycles with them, or use cycles as single note generators too: + +» `emit = cycle("[c4 d#4 g4]")` *arpeggio* + +» `emit = cycle("[c4 g4|g5]")` *arpeggio with variations* + +See [cycles](./cycles.md) for more info about Tidal Cycles support in afseq. + +## Examples + +Sequence of c4, g4 notes. + +```lua +rhythm { + emit = { "c4", "g4" } +} +``` + +Chord of c4, d#4, g4 notes. +```lua +rhythm { + emit = sequence( + { "c4", "d#4", "g4" }, -- or "c4'min" + { "---", "off", "off" } + ) +} +``` + +Sequence of c4, g4 with volume 0.5. +```lua +rhythm { + emit = sequence{"c4", "g4"}:volume(0.5) +} +``` + + +Stateless function. +```lua +rhythm { + emit = function(context) + return 48 + math.random(1, 4) * 5 + end +} +``` + +Stateful generator. +```lua +rhythm { + emit = function(initial_context) + local count, step, notes = 1, 2, scale("c5", "minor").notes + ---@param context EmitterContext + return function(context) + local key = notes[count] + count = (count + step - 1) % #notes + 1 + return { key = key, volume = 0.5 } + end + end +} +``` + +Note pattern using the "pattern" lib. +```lua +local tritone = scale("c5", "tritone") +return rhythm { + emit = pattern.from(tritone:chord(1, 4)):euclidean(6) + + pattern.from(tritone:chord(5, 4)):euclidean(6) +} +``` + +Tidal cycle. +```lua +rhythm { + emit = cycle("<[a3 c4 e4 a4]*3 [d4 g3 g4 c4]>") +} +``` + + + +See [generators](../extras/generators.md) for more info about stateful generators. + diff --git a/docs/src/guide/gate.md b/docs/src/guide/gate.md new file mode 100644 index 0000000..edc90fc --- /dev/null +++ b/docs/src/guide/gate.md @@ -0,0 +1,45 @@ +# Gate + +A rhythm's [`gate`](../API/rhythm.md#gate) is an optional filter unit that determines whether or not an event should be passed from the [pattern](./pattern.md) to the [emitter](./emitter.md). It can be used to dynamically filter out pulse events. + +The default gate is a *threshold gate*, which passes all pulse values > 0. + + +## Examples + +Seeded probability gate, using the pattern pulse values as probability. + +```lua +rhythm { + pattern = { 0, { 0.5, 1 }, 1, { 1, 0.8 } }, + gate = function(context) + local rand = math.randomstate(12366) + ---@param context PatternContext + return function(context) + return context.pulse_value > rand() + end + end, + emit = { "c4" } +} +``` + +A gate which filters out pulse values with on a configurable threshold. + +```lua +rhythm { + inputs = { + parameter.number("threshold", 0.5, {0, 1}) + }, + pattern = { + 0.2, { 0.5, 1 }, 0.9, { 1, 0.8 } + }, + gate = function(context) + return context.pulse_value >= context.inputs.threshold + end, + emit = { "c4" } +} +``` + +--- + +See [generators](../extras/generators.md) for more info about stateful generators and [parameters](./parameters.md) about rhythm input parameters. \ No newline at end of file diff --git a/docs/src/guide/notes&scales.md b/docs/src/guide/notes&scales.md new file mode 100644 index 0000000..9468084 --- /dev/null +++ b/docs/src/guide/notes&scales.md @@ -0,0 +1,123 @@ +# Notes, Chords & Scales + +Note values, such as those specified in the [emitter](./emitter.md), can be expressed and modified in various ways. Sometimes it's easier to generate notes programmatically using note numbers. Other times you may want to write down a chord in a more expressible form. + +## Notes + +### Note Numbers + +Raw integer values like `48`, are interpreted as MIDI note numbers in the `note()` function and emitter. Valid MIDI notes are `0-127`. + +» `emit = { 48 }` *emit a single c4 note* + +### Note Strings + +Note strings such as `"c4"` are interpreted as `{KEY}{MOD}{OCT}` where MOD and OCT are optional. +Valid keys are `c,d,e,f,g,a,b`. Valid modifiers are `#` and `b`. Valid octaves are values `0-10` + +» `emit = { "c4" }` *emit a single c4 note* + +Other note properties can be specified in the string notation as well. + +- `'#'` instrument +- `'v'` volume +- `'p'` panning +- `'d'` delay + +» `emit = { "f#4 #1 v0.2" }` *emit a f sharp with instrument 1 and volume 0.2* + +### Chord Strings + +To create a chords from a note string, append a `'` character to the key and specify a chord mode. + +» `emit = "d#4'maj"` *d#4 major chord* + +See [chord Lua API](../API/chord.md#ChordName) for a list of all supported modes. + +Just like regular notes, additional note properties can be added to the chord string as well. + +» `emit = "c4'69 #1 v0.5"` *patch 1, volume 0.5* + + +### Note Tables + +Instead of using a string, you can also specify notes via a Lua table with the following properties. + +- `"key"` - REQUIRED - MIDI Note number such as `48` or a string, such as `"c4"` +- `"instrument"` OPTIONAL - Instrument/Sample/Patch number >= 0 +- `"volume"` - OPTIONAL - Volume number in range [0.0 - 1.0] +- `"panning"` - OPTIONAL - Panning factor in range [-1.0 - 1.0] where 0 is center +- `"delay"` - OPTIONAL - Delay factor in range [0.0 - 1.0] + +» `emit = { key = 48, volume = 0.1 }` *a c4 with volume 0.1* + + +### Note Objects + +Note numbers, strings and tables, as described above can be fed into a note object in the LuaAPI, which allows further transformation of the note. + +This is especially handy for chords, but also can be more verbose than using note string attributes. + +» `emit = note(48):volume(0.1)` *c4 note with volume of 0.1* + +» `emit = note({key = "c4"}):volume(0.2)` *c4 note with volume of 0.2* + +» `emit = note("c4'min"):transpose({-12, 0, 0})` *1st chord inversion* + +See [note Lua API](../API/note.md) for details. + +The [sequence Lua API](../API/note.md) has a similar interface to modify notes within a sequence. + +### Note Offs and Rests + +To create rest values use a Lua `nil` value, an empty tables `{}` or `"-"` strings. + +To create off notes use the string `"off"` or `"~"`. + +--- + +See [note Lua API](../API/note.md) and [chord Lua API](../API/chord.md) for more information about notes and chords. + + +## Scales + +To make working with chords and chord progressions, and programming music in general, easier, afseq also has a simple scale API to create chords and notes from scales. + + +### Scale objects + +Scale objects can be created from a note key and mode name, or custom intervals. + +» `scale("c4", "minor").notes` *"c4", "d", "d#4", "f4", "g4", "g#4" "a#4"* + +» `scale("c4", {0,2,3,5,7,8,10}).notes` *same as above* + +#### Common Scales + +See [scale Lua API](../API/scale.md#ScaleMode) for a list of all supported modes. + +#### Custom Scales + +Custom scales can be created by using an interval table with numbers from `0-11` in ascending order. + +» `scale("c4", {0,3,5,7}).notes` *"c4", "d#4", "f4", "g4", "a4"* + + +### Scale Chords + +The scale's `chord` function allows to generate chords from the scale's intervals. + +```lua +local cmin = scale("c4", "minor") +return rhythm { + emit = sequence( + note(cmin:chord("i", 4)), --> note{48, 51, 55, 58} + note(cmin:chord(5)):transpose({12, 0, 0}), --> Gm 1st inversion + ) +} +``` + +--- + +See [scale Lua API](../API/scale.md) for more information about scale objects. + diff --git a/docs/src/guide/parameters.md b/docs/src/guide/parameters.md new file mode 100644 index 0000000..3fd3188 --- /dev/null +++ b/docs/src/guide/parameters.md @@ -0,0 +1,104 @@ +# Parameters + +Rhythm [`inputs`](../API/rhythm.md#inputs) allow user-defined parameter values to be injected into a rhythm. This allows you to write more flexible rhythms that can be used as templates or to automate functions within the rhythm. + +Parameters can be accessed in dynamic pattern, gate, emitter or cycle function [`contexts`](../API/rhythm.md#EmitterContext). + +## Parameter Types + +Currenty available parameter types are: + +- boolean - on/off switches - [`parameter.boolean`](../API/input.md#boolean) +- integer - continues quantized values - [`parameter.integer`](../API/input.md#integer) +- number - continues values -[`parameter.number`](../API/input.md#number) +- string - enumeration value sets - [`parameter.enum`](../API/input.md#enum) + +## Parameter access + +When defining a parameter, each parameter has a unique string id set. This id can then be used to access the *actual* paramter value in the function contexts. + +Definition: + +» `inputs = { parameter.boolean("enabled", true) }` + +Usage: + +» `emit = function(context) context.enabled and "c5" or nil }` + +## Examples + + +Euclidean pattern generator with user configurable steps, pulses, offset value. + +```lua +return rhythm { + inputs = { + parameter.integer('steps', 12, {1, 64}, "Steps", + "Number of on steps in the pattern"), + parameter.integer('pulses', 16, {1, 64}, "Pulses", + "Total number of on & off pulses"), + parameter.integer('offset', 0, {-16, 16}, "Offset", + "Rotates on pattern left (values > 0) or right (values < 0)"), + }, + unit = "1/1", + pattern = function(context) + return pattern.euclidean( + math.min(context.inputs.steps, context.inputs.pulses), + context.inputs.pulses, + context.inputs.offset) + end, + emit = "c4" +} +``` + + +Random bass line generator with user defined custom scales and variations (seeds). +```lua +local scales = {"Chromatic", "Minor", "Major"} +return rhythm { + inputs = { + parameter.enum('scale', scales[1], scales, "Scale"), + parameter.integer('notes', 7, {1, 12}, "#Notes"), + parameter.integer('variation', 0, {0, 0xff}, "Variation"), + }, + unit = "1/1", + pattern = function (context) + local rand = math.randomstate(2345 + context.inputs.variation) + return pattern.euclidean(rand(3, 16), 16, 0) + end, + emit = function(context) + local notes = scale("c4", context.inputs.scale).notes + local rand = math.randomstate(127364 + context.inputs.variation) + local notes = pattern.new(context.inputs.notes):map(function(_) + return notes[rand(#notes)] + end) + return notes[math.imod(context.step, #notes)] + end +} +``` + +Drum pattern cycle with configurable note values for each drumkit instrument. +```lua +return rhythm { + unit = "1/1", + inputs = { + parameter.integer("bd_note", 48, {0, 127}), + parameter.integer("sn_note", 70, {0, 127}), + parameter.integer("hh_note", 98, {0, 127}) + }, + emit = cycle([[ + [*12], + [bd1 ~]*2 ~ [~ bd2] ~, + [~ sn1]*2, + [~ sn2]*8 + ]]):map(function (context, value) + for _, id in pairs{"bd", "sn", "hh"} do + local number = value:match(id.."(%d+)") + if number then + return note(context.inputs[id.."_note"]):volume( + number == "2" and 0.2 or 1.0) + end + end + end) +} +``` \ No newline at end of file diff --git a/docs/src/guide/pattern.md b/docs/src/guide/pattern.md new file mode 100644 index 0000000..61cc550 --- /dev/null +++ b/docs/src/guide/pattern.md @@ -0,0 +1,129 @@ +# Pattern + +A rhythm's [`pattern`](../API/rhythm.md#pattern) is a sequence of pulse values that defines the temporal occurence of events. It feeds the emitter with pulse values that are optionally filtered out by a gate. It is created from a list of pulses with possible subdivisions, an optional number of repeats and an optional time offset. + +A pattern can be generated by algorithms such as [Euclidean rhythms](https://en.wikipedia.org/wiki/Euclidean_rhythm), or it can be expressed as a static Lua table, or it can be generated by a dynamic generator - a Lua function. + +The default pattern is an endless repeated pulse train of 1's. + +## Offset + +By default, the pattern starts running immediately when the rhythm is triggered. Using the [`offset`](../API/rhythm.md#offset) property you can delay the start by the amount specified in the rhythm's **unit**. + +» `offset = 4` *delay start by 4 * rhythm's unit*. + +## Repeat + +By default, a pattern repeats endlessly. To create one-shot patterns, or patterns that repeat only a few times, you can use the [`repeats`](../API/rhythm.md#repeats) property. + +» `repeats = false` *one-shot* + +» `repeats = 1` *play pattern twice, then stop* + +## Static Patterns + +The simplest form of a pattern is a Lua table (array) of numbers or boolean values: + +» `pattern = {0, 1, 1}` *skip, trigger, trigger - repeat* + +» `pattern = {1, 0, 1, 1}` *1/2th followed by two 1/4th triggers in a 1/4th unit rhythm* + +» `pattern = {0.8, 0, 1, 0.2}` *pulse values are passed as numbers to gates and emitters in function contexts* + +> [!TIP] +> The default [gate](./gate.md) implementation skips all 0 pulses and passes all other pulse values to the emitter. When using a custom gate function, you can e.g. use the pulse value as a probability or you can use the pulse value as a volume value in a custom emitter function to create accents. + +### Sub-Divisions + +Each number value in the specified Lua table represents a single pulse in the rhythm's specified time unit. By using *tables* instead of pulse numbers as values, you can *cram* sub-patterns into a pattern to create more complex rhythms. + +» `pattern = {{1, 1, 1}, {1, 1}}` *triplet followed by two quarter notes with unit 1/2* + +» `pattern = {{1, 0}, {0, 1}}` *basic bossanova rhythm with unit 1/4* + + +## Dynamic Patterns + +When using a Lua function instead of a table as a pattern generator, you can dynamically generate pulse values. + +» `pattern = function(context) return math.random() end` *randomly emit pulse values between 0 and 1* + +Use functions in order to create dynamic patterns that can interact with user interaction, or to create probability based pattern rules. To connect the functions to its runtime use the passed [`context`](../API/rhythm.md#PatternContext) argument. + +See [generators](../extras/generators.md) for more info about using functions as generators. + +## Pattern Library + +afseq comes with a built-in pattern library, which contains a bunch of helper functions and generators to ease creating patterns. + +» `pattern = pattern.from{0, 1} * 3 + {1, 0}` *combine sub patterns* + +» `pattern = pattern.new(12, function (k, v) return k % 3 == 1 end),` *functionally create patterns* + +» `pattern = pattern.euclidean{3, 8, -1}` *create euclidean patterns* + +See [Pattern API Lua reference](../API/pattern.md) for more info and examples. + +## Examples + +Static pattern. +```lua +rhythm { + pattern = { 1, 0, 0, 1 }, + -- ... +} +``` + +*Cram* pulses into a single pulse slot via subdivisions in static patterns. +```lua +rhythm { + pattern = { 1, { 1, 1, 1 } }, + -- ... +} +``` + +Static pattern created using the "pattern" lib +```lua +rhythm { + pattern = pattern.from{1, 0} * 5 + {1, 1} + -- ... +} +``` + +Euclidean pattern created using the "patterns" lib. +```lua +rhythm { + pattern = pattern.euclidean(7, 16, 2), + -- ... +} +``` + +Stateless function. +```lua +rhythm { + pattern = function(context) + return math.random(0, 1) + end + -- ... +} +``` + +Stateful generator. +```lua +rhythm { + pattern = function(context) + local rand = math.randomstate(12345) + local triggers = table.create({0, 6, 10}) + ---@param context PatternContext + return function(context) + return rand() > 0.8 and + triggers:find((context.pulse_step - 1) % 16) ~= nil + end + end + -- ... +} +``` + +--- + +See [generators](../extras/generators.md) for more info about stateful generators. diff --git a/docs/src/guide/timebase.md b/docs/src/guide/timebase.md new file mode 100644 index 0000000..c088298 --- /dev/null +++ b/docs/src/guide/timebase.md @@ -0,0 +1,60 @@ +# Timebase + +A rhythm's [`timebase`](../API/rhythm.md#unit) represents the unit of time for the rhythm, either in musical beats or wall-clock time (seconds, ms). It defines the unit and duration of a single step in patterns. The time base is static and thus can't be changed during runtime. + +The default time unit of rhythm is a beat. + +The BPM and signature (beats per bar) settings are configured by the application which is running the rhythm. + +## Supported Time Units + +### Beat-Time + +- `"bars"` *using the host's beats per bar setting* +- `"beats"` *alias for 1/4* +- `"1/1"` *4 * 1/4* +- `"1/2"` *2 * 1/4* +- `"1/4"` *a beat* +- `"1/8"` *0.5 * 1/4* +- `"1/16"` *0.25 * 1/4* +- `"1/32"` *0.125 * 1/4* +- `"1/64"` *0.0625 * 1/4* + +### Wallclock-Time + + - `"ms"` *millisecond* + - `"seconds"` *whole seconds* + +## Resolution + +The [`resolution`](../API/rhythm.md#resolution) parameter acts as an additional multiplier to the time unit and can be any positive real number. You can use it to scale the unit or to create odd time signatures. + +## Examples + +A slightly off beat time unit. +```lua +rhythm { + unit = "beats", + resolution = 1.01, + emit = "c4" +} +``` + +Sixteenth tripplets +```lua +rhythm { + unit = "1/16", + resolution = 4/3, + emit = "c4" +} +``` + + +2 Seconds +```lua +rhythm { + unit = "seconds", + resolution = 2, + emit = "c4" +} +``` \ No newline at end of file diff --git a/docs/src/styles.css b/docs/src/styles.css new file mode 100644 index 0000000..75d157a --- /dev/null +++ b/docs/src/styles.css @@ -0,0 +1,20 @@ +blockquote { + margin: 10px 0; + padding-left: 10px; + padding-right: 10px; + background-color: transparent; + border-block-start: .1em solid transparent; + border-block-end: .1em solid transparent; +} + +blockquote > p { + color: var(--icons); + padding: 0 0 10px 0; + margin: 0 0 0 10px; +} + +hr { + height: 2px; + background-color: var(--quote-border); + border: none; +} \ No newline at end of file diff --git a/examples/README.md b/examples/README.md new file mode 100644 index 0000000..c5a737b Binary files /dev/null and b/examples/README.md differ diff --git a/justfile b/justfile new file mode 100644 index 0000000..f9725f6 --- /dev/null +++ b/justfile @@ -0,0 +1,18 @@ +# see https://github.com/casey/just + +default: build + +build: + cargo build --release + +run: + cargo run --release --example=play-script --features=player + +docs-generate-api: + cd docs/generate && cargo run -- "../../types/nerdo/library" "../src" + +docs-build: docs-generate-api + cd docs && mdbook build + +docs-serve: docs-generate-api + cd docs && mdbook serve diff --git a/types/nerdo/library/cycle.lua b/types/nerdo/library/cycle.lua index 4d11b0c..2e3a16e 100644 --- a/types/nerdo/library/cycle.lua +++ b/types/nerdo/library/cycle.lua @@ -71,10 +71,9 @@ local Cycle = {} --- end ---end) ---``` ----@param map { [string]: CycleMapNoteValue } +---@param map { [string]: CycleMapNoteValue }|CycleMapFunction|CycleMapGenerator ---@return Cycle ---@nodiscard ----@overload fun(self, function: CycleMapFunction|CycleMapGenerator): Cycle function Cycle:map(map) end ---------------------------------------------------------------------------------------------------- diff --git a/types/nerdo/library/input.lua b/types/nerdo/library/input.lua index 82b4d84..c311a4e 100644 --- a/types/nerdo/library/input.lua +++ b/types/nerdo/library/input.lua @@ -32,53 +32,54 @@ error("Do not try to execute this file. It's just a type definition file.") ---------------------------------------------------------------------------------------------------- ----Opaque input parameter user data. Construct new input parameters via the `XXX_input(...)` ----functions. Input parameter values can then be accessed via function contexts in pattern, ----gate and emitter functions or generators. +---Opaque input parameter user data. Construct new input parameters via the `parameter.XXX(...)` +---functions. ---@see TriggerContext.inputs ---@class InputParameter : userdata local InputParameter = {} ---------------------------------------------------------------------------------------------------- ----Functions to create InputParamters. -parameter = { - ---Creates an InputParameter with "boolean" Lua type with the given default value - ---and other optional properties. - ---@param id InputParameterId - ---@param default InputParameterBooleanDefault - ---@param name InputParameterName? - ---@param description InputParameterDescription? - ---@return InputParameter - boolean = function(id, default, name, description) end, +---Contains functions to construct new input parameters. Input parameter values can be accessed +---via functionn `contexts` in pattern, gate and emitter functions or generators. +---@class Parameter +local parameter = {} - ---Creates an InputParameter with "integer" Lua type with the given default value - ---and other optional properties. - ---@param id InputParameterId - ---@param default InputParameterIntegerDefault - ---@param range InputParameterIntegerRange? - ---@param name InputParameterName? - ---@param description InputParameterDescription? - ---@return InputParameter - integer = function(id, default, range, name, description) end, +---Creates an InputParameter with "boolean" Lua type with the given default value +---and other optional properties. +---@param id InputParameterId +---@param default InputParameterBooleanDefault +---@param name InputParameterName? +---@param description InputParameterDescription? +---@return InputParameter +function parameter.boolean(id, default, name, description) end - ---Creates an InputParameter with "number" Lua type with the given default value - ---and other optional properties. - ---@param id InputParameterId - ---@param default InputParameterNumberDefault - ---@param range InputParameterNumberRange? - ---@param name InputParameterName? - ---@param description InputParameterDescription? - ---@return InputParameter - number = function(id, default, range, name, description) end, +---Creates an InputParameter with "integer" Lua type with the given default value +---and other optional properties. +---@param id InputParameterId +---@param default InputParameterIntegerDefault +---@param range InputParameterIntegerRange? +---@param name InputParameterName? +---@param description InputParameterDescription? +---@return InputParameter +function parameter.integer(id, default, range, name, description) end - ---Creates an InputParameter with a "string" Lua type with the given default value, - ---set of valid values to choose from and other optional properties. - ---@param id InputParameterId - ---@param default InputParameterEnumDefault - ---@param values string[] - ---@param name InputParameterName? - ---@param description InputParameterDescription? - ---@return InputParameter - enum = function(id, default, values, name, description) end, -} +---Creates an InputParameter with "number" Lua type with the given default value +---and other optional properties. +---@param id InputParameterId +---@param default InputParameterNumberDefault +---@param range InputParameterNumberRange? +---@param name InputParameterName? +---@param description InputParameterDescription? +---@return InputParameter +function parameter.number(id, default, range, name, description) end + +---Creates an InputParameter with a "string" Lua type with the given default value, +---set of valid values to choose from and other optional properties. +---@param id InputParameterId +---@param default InputParameterEnumDefault +---@param values string[] +---@param name InputParameterName? +---@param description InputParameterDescription? +---@return InputParameter +function parameter.enum(id, default, values, name, description) end diff --git a/types/nerdo/library/pattern.lua b/types/nerdo/library/pattern.lua index 646611b..b0e0a3e 100644 --- a/types/nerdo/library/pattern.lua +++ b/types/nerdo/library/pattern.lua @@ -250,7 +250,7 @@ end ---local p = pattern.from{1,2,3,4} ---local v1, v2, v3, v4 = p:unpack() ---``` ----@return (PulseValue)[] +---@return PulseValue ... ---@nodiscard function pattern.unpack(self) return table.unpack(self) diff --git a/types/nerdo/library/rhythm.lua b/types/nerdo/library/rhythm.lua index d3e0351..e6f4d08 100644 --- a/types/nerdo/library/rhythm.lua +++ b/types/nerdo/library/rhythm.lua @@ -28,11 +28,11 @@ error("Do not try to execute this file. It's just a type definition file.") ---Transport & playback time context passed to `pattern`, `gate` and `emit` functions. ---@class TimeContext : TriggerContext --- ------Project's tempo in beats per minutes. +---Project's tempo in beats per minutes. ---@field beats_per_min number ------Project's beats per bar setting. +---Project's beats per bar setting. ---@field beats_per_bar integer ------Project's sample rate in samples per second. +---Project's sample rate in samples per second. ---@field samples_per_sec integer ---------------------------------------------------------------------------------------------------- @@ -124,7 +124,7 @@ error("Do not try to execute this file. It's just a type definition file.") ---change a rhythms behavior everywhere where `context`s are passed, e.g. in pattern, ---gate, emitter or cycle map generator functions. --- ----## examples: +---### examples: ---```lua ----- trigger a single note as specified by input parameter 'note' ----- when input parameter 'enabled' is true, else triggers nothing. @@ -163,12 +163,10 @@ error("Do not try to execute this file. It's just a type definition file.") ---```lua ----- a fixed pattern ---pattern = { 1, 0, 0, 1 } ------ maybe trigger with probabilities ----pattern = { 1, 0, 0.5, 0.9 } ----- "cram" pulses into a single pulse slot via subdivisions ---pattern = { 1, { 1, 1, 1 } } --- ------ fixed patterns created via "patterns" +----- fixed patterns created via the "patterns" lib ---pattern = pattern.from{ 1, 0 } * 3 + { 1, 1 } ---pattern = pattern.euclidean(7, 16, 2) --- @@ -179,10 +177,10 @@ error("Do not try to execute this file. It's just a type definition file.") --- ----- stateful generator function ---pattern = function(context) ---- local my_pattern = table.create({0, 6, 10}) ---- ---@param context EmitterContext +--- local triggers = table.create({0, 6, 10}) +--- ---@param context PatternContext --- return function(context) ---- return my_pattern:find((context.step - 1) % 16) ~= nil +--- return triggers:find((context.step - 1) % 16) ~= nil --- end ---end --- @@ -221,7 +219,7 @@ error("Do not try to execute this file. It's just a type definition file.") --- return context.pulse_value > math.random() ---end ---``` ----@field gate Pulse[]|(fun(context: GateContext):boolean)|(fun(context: GateContext):fun(context: GateContext):boolean)? +---@field gate (fun(context: GateContext):boolean)|(fun(context: GateContext):fun(context: GateContext):boolean)? --- ---Specify the melodic pattern of the rhythm. For every pulse in the rhythmical pattern, the event ---from the specified emit sequence. When the end of the sequence is reached, it starts again from @@ -244,7 +242,6 @@ error("Do not try to execute this file. It's just a type definition file.") ---emit = sequence{"c4", "g4"}:volume(0.5) --- ----- stateless generator function ------ a function ---emit = function(context) --- return 48 + math.random(1, 4) * 5 ---end diff --git a/types/nerdo/library/sequence.lua b/types/nerdo/library/sequence.lua index 2f9f8cb..365bb51 100644 --- a/types/nerdo/library/sequence.lua +++ b/types/nerdo/library/sequence.lua @@ -78,8 +78,10 @@ function Sequence:delay(delay) end --- ---### examples: ---```lua ----sequence(48, "c5", {}) -- sequence of C4, C5 and an empty note ----sequence("c4'maj", "g4'maj"):transpose(5) -- sequence of a +5 transposed C4 and G4 major chord +----- sequence of C4, C5 and an empty note +---sequence(48, "c5", {}) +----- sequence of a +5 transposed C4 and G4 major chord +---sequence("c4'maj", "g4'maj"):transpose(5) --- ``` ---@param ... NoteValue|Note ---@return Sequence