From 1aff34a06a372de4c8150d6034fcce27f56e046d Mon Sep 17 00:00:00 2001 From: Ivan Bushchik Date: Thu, 18 Jan 2024 19:51:45 +0300 Subject: [PATCH] 0.0.1: First alpha release Signed-off-by: Ivan Bushchik --- .gitignore | 5 + .rustfmt.toml | 9 + Cargo.lock | 464 ++++++++++++++++++++++ Cargo.toml | 3 + LICENSE | 3 + LICENSE-BSD | 28 ++ README.md | 92 +++++ examples/determination/main | 165 ++++++++ examples/determination/project.yml | 6 + examples/megalovania-no-delay/channel | 22 + examples/megalovania-no-delay/project.yml | 7 + examples/megalovania/channel | 24 ++ examples/megalovania/project.yml | 9 + rinth-midi/Cargo.toml | 12 + rinth-midi/src/main.rs | 73 ++++ rinth-synth/Cargo.toml | 15 + rinth-synth/src/fm.rs | 101 +++++ rinth-synth/src/main.rs | 127 ++++++ rinth-synth/src/shared.rs | 27 ++ rinth-synth/src/ssg.rs | 68 ++++ rinth-types/Cargo.toml | 11 + rinth-types/src/file.rs | 28 ++ rinth-types/src/lib.rs | 3 + rinth-types/src/note.rs | 72 ++++ rinth-types/src/traits.rs | 9 + 25 files changed, 1383 insertions(+) create mode 100644 .gitignore create mode 100644 .rustfmt.toml create mode 100644 Cargo.lock create mode 100644 Cargo.toml create mode 100644 LICENSE create mode 100644 LICENSE-BSD create mode 100644 README.md create mode 100644 examples/determination/main create mode 100644 examples/determination/project.yml create mode 100644 examples/megalovania-no-delay/channel create mode 100644 examples/megalovania-no-delay/project.yml create mode 100644 examples/megalovania/channel create mode 100644 examples/megalovania/project.yml create mode 100644 rinth-midi/Cargo.toml create mode 100644 rinth-midi/src/main.rs create mode 100644 rinth-synth/Cargo.toml create mode 100644 rinth-synth/src/fm.rs create mode 100644 rinth-synth/src/main.rs create mode 100644 rinth-synth/src/shared.rs create mode 100644 rinth-synth/src/ssg.rs create mode 100644 rinth-types/Cargo.toml create mode 100644 rinth-types/src/file.rs create mode 100644 rinth-types/src/lib.rs create mode 100644 rinth-types/src/note.rs create mode 100644 rinth-types/src/traits.rs diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..d99ae7d --- /dev/null +++ b/.gitignore @@ -0,0 +1,5 @@ +target/ +*.DS_Store +.idea +*.wav +*.m4a \ No newline at end of file diff --git a/.rustfmt.toml b/.rustfmt.toml new file mode 100644 index 0000000..32e806d --- /dev/null +++ b/.rustfmt.toml @@ -0,0 +1,9 @@ +edition = "2021" +hard_tabs = true +merge_derives = true +reorder_imports = true +reorder_modules = true +use_field_init_shorthand = true +use_small_heuristics = "Off" +wrap_comments = true +comment_width = 80 diff --git a/Cargo.lock b/Cargo.lock new file mode 100644 index 0000000..e4562c4 --- /dev/null +++ b/Cargo.lock @@ -0,0 +1,464 @@ +# This file is automatically @generated by Cargo. +# It is not intended for manual editing. +version = 3 + +[[package]] +name = "anstream" +version = "0.6.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "628a8f9bd1e24b4e0db2b4bc2d000b001e7dd032d54afa60a68836aeec5aa54a" +dependencies = [ + "anstyle", + "anstyle-parse", + "anstyle-query", + "anstyle-wincon", + "colorchoice", + "utf8parse", +] + +[[package]] +name = "anstyle" +version = "1.0.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7079075b41f533b8c61d2a4d073c4676e1f8b249ff94a393b0595db304e0dd87" + +[[package]] +name = "anstyle-parse" +version = "0.2.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c75ac65da39e5fe5ab759307499ddad880d724eed2f6ce5b5e8a26f4f387928c" +dependencies = [ + "utf8parse", +] + +[[package]] +name = "anstyle-query" +version = "1.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e28923312444cdd728e4738b3f9c9cac739500909bb3d3c94b43551b16517648" +dependencies = [ + "windows-sys", +] + +[[package]] +name = "anstyle-wincon" +version = "3.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1cd54b81ec8d6180e24654d0b371ad22fc3dd083b6ff8ba325b72e00c87660a7" +dependencies = [ + "anstyle", + "windows-sys", +] + +[[package]] +name = "clap" +version = "4.4.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1e578d6ec4194633722ccf9544794b71b1385c3c027efe0c55db226fc880865c" +dependencies = [ + "clap_builder", + "clap_derive", +] + +[[package]] +name = "clap_builder" +version = "4.4.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4df4df40ec50c46000231c914968278b1eb05098cf8f1b3a518a95030e71d1c7" +dependencies = [ + "anstream", + "anstyle", + "clap_lex", + "strsim", +] + +[[package]] +name = "clap_derive" +version = "4.4.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cf9804afaaf59a91e75b022a30fb7229a7901f60c755489cc61c9b423b836442" +dependencies = [ + "heck", + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "clap_lex" +version = "0.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "702fc72eb24e5a1e48ce58027a675bc24edd52096d5397d4aea7c6dd9eca0bd1" + +[[package]] +name = "colorchoice" +version = "1.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "acbf1af155f9b9ef647e42cdc158db4b64a1b61f743629225fde6f3e0be2a7c7" + +[[package]] +name = "crossbeam-deque" +version = "0.8.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "613f8cc01fe9cf1a3eb3d7f488fd2fa8388403e97039e2f73692932e291a770d" +dependencies = [ + "crossbeam-epoch", + "crossbeam-utils", +] + +[[package]] +name = "crossbeam-epoch" +version = "0.9.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5b82ac4a3c2ca9c3460964f020e1402edd5753411d7737aa39c3714ad1b5420e" +dependencies = [ + "crossbeam-utils", +] + +[[package]] +name = "crossbeam-utils" +version = "0.8.19" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "248e3bacc7dc6baa3b21e405ee045c3047101a49145e7e9eca583ab4c2ca5345" + +[[package]] +name = "either" +version = "1.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a26ae43d7bcc3b814de94796a5e736d4029efb0ee900c12e2d54c993ad1a1e07" + +[[package]] +name = "equivalent" +version = "1.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5443807d6dff69373d433ab9ef5378ad8df50ca6298caf15de6e52e24aaf54d5" + +[[package]] +name = "fnv" +version = "1.0.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3f9eec918d3f24069decb9af1554cad7c880e2da24a9afd88aca000531ab82c1" + +[[package]] +name = "hashbrown" +version = "0.14.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "290f1a1d9242c78d09ce40a5e87e7554ee637af1351968159f4952f028f75604" + +[[package]] +name = "heck" +version = "0.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "95505c38b4572b2d910cecb0281560f54b440a19336cbbcb27bf6ce6adc6f5a8" + +[[package]] +name = "hound" +version = "3.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "62adaabb884c94955b19907d60019f4e145d091c75345379e70d1ee696f7854f" + +[[package]] +name = "indexmap" +version = "2.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d530e1a18b1cb4c484e6e34556a0d948706958449fca0cab753d649f2bce3d1f" +dependencies = [ + "equivalent", + "hashbrown", +] + +[[package]] +name = "itoa" +version = "1.0.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b1a46d1a171d865aa5f83f92695765caa047a9b4cbae2cbf37dbd613a793fd4c" + +[[package]] +name = "memchr" +version = "2.7.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "523dc4f511e55ab87b694dc30d0f820d60906ef06413f93d4d7a1385599cc149" + +[[package]] +name = "meval" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f79496a5651c8d57cd033c5add8ca7ee4e3d5f7587a4777484640d9cb60392d9" +dependencies = [ + "fnv", + "nom", +] + +[[package]] +name = "midly" +version = "0.5.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "207d755f4cb882d20c4da58d707ca9130a0c9bc5061f657a4f299b8e36362b7a" +dependencies = [ + "rayon", +] + +[[package]] +name = "nom" +version = "1.2.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a5b8c256fd9471521bcb84c3cdba98921497f1a331cbc15b8030fc63b82050ce" + +[[package]] +name = "proc-macro2" +version = "1.0.76" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "95fc56cda0b5c3325f5fbbd7ff9fda9e02bb00bb3dac51252d2f1bfa1cb8cc8c" +dependencies = [ + "unicode-ident", +] + +[[package]] +name = "quote" +version = "1.0.35" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "291ec9ab5efd934aaf503a6466c5d5251535d108ee747472c3977cc5acc868ef" +dependencies = [ + "proc-macro2", +] + +[[package]] +name = "rayon" +version = "1.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9c27db03db7734835b3f53954b534c91069375ce6ccaa2e065441e07d9b6cdb1" +dependencies = [ + "either", + "rayon-core", +] + +[[package]] +name = "rayon-core" +version = "1.12.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5ce3fb6ad83f861aac485e76e1985cd109d9a3713802152be56c3b1f0e0658ed" +dependencies = [ + "crossbeam-deque", + "crossbeam-utils", +] + +[[package]] +name = "rinth-midi" +version = "0.0.1" +dependencies = [ + "clap", + "midly", + "rinth-types", +] + +[[package]] +name = "rinth-synth" +version = "0.0.1" +dependencies = [ + "clap", + "hound", + "meval", + "rinth-types", + "serde", + "serde_yaml", +] + +[[package]] +name = "rinth-types" +version = "0.0.1" +dependencies = [ + "serde", + "toml", +] + +[[package]] +name = "ryu" +version = "1.0.16" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f98d2aa92eebf49b69786be48e4477826b256916e84a57ff2a4f21923b48eb4c" + +[[package]] +name = "serde" +version = "1.0.195" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "63261df402c67811e9ac6def069e4786148c4563f4b50fd4bf30aa370d626b02" +dependencies = [ + "serde_derive", +] + +[[package]] +name = "serde_derive" +version = "1.0.195" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "46fe8f8603d81ba86327b23a2e9cdf49e1255fb94a4c5f297f6ee0547178ea2c" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "serde_spanned" +version = "0.6.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "eb3622f419d1296904700073ea6cc23ad690adbd66f13ea683df73298736f0c1" +dependencies = [ + "serde", +] + +[[package]] +name = "serde_yaml" +version = "0.9.30" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b1bf28c79a99f70ee1f1d83d10c875d2e70618417fda01ad1785e027579d9d38" +dependencies = [ + "indexmap", + "itoa", + "ryu", + "serde", + "unsafe-libyaml", +] + +[[package]] +name = "strsim" +version = "0.10.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "73473c0e59e6d5812c5dfe2a064a6444949f089e20eec9a2e5506596494e4623" + +[[package]] +name = "syn" +version = "2.0.48" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0f3531638e407dfc0814761abb7c00a5b54992b849452a0646b7f65c9f770f3f" +dependencies = [ + "proc-macro2", + "quote", + "unicode-ident", +] + +[[package]] +name = "toml" +version = "0.8.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a1a195ec8c9da26928f773888e0742ca3ca1040c6cd859c919c9f59c1954ab35" +dependencies = [ + "serde", + "serde_spanned", + "toml_datetime", + "toml_edit", +] + +[[package]] +name = "toml_datetime" +version = "0.6.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3550f4e9685620ac18a50ed434eb3aec30db8ba93b0287467bca5826ea25baf1" +dependencies = [ + "serde", +] + +[[package]] +name = "toml_edit" +version = "0.21.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d34d383cd00a163b4a5b85053df514d45bc330f6de7737edfe0a93311d1eaa03" +dependencies = [ + "indexmap", + "serde", + "serde_spanned", + "toml_datetime", + "winnow", +] + +[[package]] +name = "unicode-ident" +version = "1.0.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3354b9ac3fae1ff6755cb6db53683adb661634f67557942dea4facebec0fee4b" + +[[package]] +name = "unsafe-libyaml" +version = "0.2.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ab4c90930b95a82d00dc9e9ac071b4991924390d46cbd0dfe566148667605e4b" + +[[package]] +name = "utf8parse" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "711b9620af191e0cdc7468a8d14e709c3dcdb115b36f838e601583af800a370a" + +[[package]] +name = "windows-sys" +version = "0.52.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "282be5f36a8ce781fad8c8ae18fa3f9beff57ec1b52cb3de0789201425d9a33d" +dependencies = [ + "windows-targets", +] + +[[package]] +name = "windows-targets" +version = "0.52.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8a18201040b24831fbb9e4eb208f8892e1f50a37feb53cc7ff887feb8f50e7cd" +dependencies = [ + "windows_aarch64_gnullvm", + "windows_aarch64_msvc", + "windows_i686_gnu", + "windows_i686_msvc", + "windows_x86_64_gnu", + "windows_x86_64_gnullvm", + "windows_x86_64_msvc", +] + +[[package]] +name = "windows_aarch64_gnullvm" +version = "0.52.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cb7764e35d4db8a7921e09562a0304bf2f93e0a51bfccee0bd0bb0b666b015ea" + +[[package]] +name = "windows_aarch64_msvc" +version = "0.52.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bbaa0368d4f1d2aaefc55b6fcfee13f41544ddf36801e793edbbfd7d7df075ef" + +[[package]] +name = "windows_i686_gnu" +version = "0.52.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a28637cb1fa3560a16915793afb20081aba2c92ee8af57b4d5f28e4b3e7df313" + +[[package]] +name = "windows_i686_msvc" +version = "0.52.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ffe5e8e31046ce6230cc7215707b816e339ff4d4d67c65dffa206fd0f7aa7b9a" + +[[package]] +name = "windows_x86_64_gnu" +version = "0.52.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3d6fa32db2bc4a2f5abeacf2b69f7992cd09dca97498da74a151a3132c26befd" + +[[package]] +name = "windows_x86_64_gnullvm" +version = "0.52.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1a657e1e9d3f514745a572a6846d3c7aa7dbe1658c056ed9c3344c4109a6949e" + +[[package]] +name = "windows_x86_64_msvc" +version = "0.52.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dff9641d1cd4be8d1a070daf9e3773c5f67e78b4d9d42263020c057706765c04" + +[[package]] +name = "winnow" +version = "0.5.34" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b7cf47b659b318dccbd69cc4797a39ae128f533dce7902a1096044d1967b9c16" +dependencies = [ + "memchr", +] diff --git a/Cargo.toml b/Cargo.toml new file mode 100644 index 0000000..421082a --- /dev/null +++ b/Cargo.toml @@ -0,0 +1,3 @@ +[workspace] +members = [ "rinth-midi", "rinth-synth", "rinth-types" ] +resolver = "2" \ No newline at end of file diff --git a/LICENSE b/LICENSE new file mode 100644 index 0000000..93c23ec --- /dev/null +++ b/LICENSE @@ -0,0 +1,3 @@ +Rinth and all of it's subcrates are available for non-commercial (non-profit) personal use under the terms of BSD 3-Clause New (Revised) License license, see LICENSE-BSD + +At the moment, the commercial use of Rinth is completely prohibited at the moment. \ No newline at end of file diff --git a/LICENSE-BSD b/LICENSE-BSD new file mode 100644 index 0000000..ae6c6d0 --- /dev/null +++ b/LICENSE-BSD @@ -0,0 +1,28 @@ +BSD 3-Clause License + +Copyright (c) 2024, Ivan Bushchik + +Redistribution and use in source and binary forms, with or without +modification, are permitted provided that the following conditions are met: + +1. Redistributions of source code must retain the above copyright notice, this + list of conditions and the following disclaimer. + +2. Redistributions in binary form must reproduce the above copyright notice, + this list of conditions and the following disclaimer in the documentation + and/or other materials provided with the distribution. + +3. Neither the name of the copyright holder nor the names of its + contributors may be used to endorse or promote products derived from + this software without specific prior written permission. + +THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" +AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE +IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE +DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE +FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL +DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR +SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER +CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, +OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE +OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. \ No newline at end of file diff --git a/README.md b/README.md new file mode 100644 index 0000000..a8fd2eb --- /dev/null +++ b/README.md @@ -0,0 +1,92 @@ +# Rinth synthesiser + +FM and SSG synthesiser + +## Usage + +```shell +rinth-synth b[uild] # Will build separate tracks +rinth-synth m[aster] # Will "master" tracks into one track +``` + +See `--help` for more + +## File formats + +### \.yml + +```yaml +name: +bpm: +channels: + - type: + path: + volume: [Optional in [0;1]] +``` + +Example: + +```yaml +name: Megalovania +bpm: 210 +channels: + - type: SSG + path: _channel + - type: FM + path: _channel + volume: 0.7 +``` + +### _channel (FM) + +``` +/ Comment +# +@ + [Time from previous note] +``` + +Example: + +``` +#1 +@2000 +D4 0.0625 +Dd5 0.0625 0.0125 +Db5 1/32+1/32 1/80 +``` + +### _channel (SSG) + +``` +/ Comment +# Ignored +@ Ignored + [Time from previous note] +``` + +Example: + +``` +/ Comment +# Comment 2 +@ Comment 3 +D4 0.0625 +Dd5 0.0625 0.0125 +Db5 1/32+1/32 1/80 +``` +More examples in [examples/](./examples) + +## MIDI Converter + +WIP MIDI converter is available in the `rinth-midi` crate. + +### Usage + +```shell +rinth-midi +``` + +## License + +The project is available for non-commercial personal use under the terms of the [BSD 3-Clause New (Revised) License](./LICENSE) and fully unavailable for any other kind of use. diff --git a/examples/determination/main b/examples/determination/main new file mode 100644 index 0000000..15a998d --- /dev/null +++ b/examples/determination/main @@ -0,0 +1,165 @@ +#1 +@4 + +G5 1/8 +Fd5 1/8 +E5 1/8 +D5 1/8 +E5 1/8 +B4 1/8 + +Cd5 1/4 +A4 1/4 + +E5 1/8 +Fd5 1/8 + +G5 1/4 +A5 1/4 +D6 1/4 + +B5 1/2+1/4 + + +G5 1/8 +Fd5 1/8 +E5 1/8 +D5 1/8 +E5 1/8 +B4 1/8 + +Cd5 1/4 +A4 1/4 + +E4 1/8 +Fd4 1/8 + +G4 1/4 +Fd4 1/4 +D4 1/4 +E4 1/2+1/4 + +G5 1/8 +Fd5 1/8 +E5 1/8 +D5 1/8 +E5 1/8 +B4 1/8 + +Cd5 1/4 +A4 1/4 + +E5 1/8 +Fd5 1/8 + +G5 1/4 +A5 1/4 +D6 1/4 + +B5 1/2+1/4 + + +G5 1/8 +Fd5 1/8 +E5 1/8 +D5 1/8 +E5 1/8 +B4 1/8 + +Cd5 1/4 +A4 1/4 + +E4 1/8 +Fd4 1/8 + +G4 1/4 +Fd4 1/4 +D4 1/4 +E4 1/2+1/4 + + + + +A5 1/8 +G5 1/8 +F5 1/8 +E5 1/8 +D5 1/8 +F5 1/8 + +E5 1/4 +B4 1/4 + +B4 1/8 +E5 1/8 + +A5 1/8 +G5 1/8 +F5 1/8 +E5 1/8 +D5 1/8 +F5 1/8 + +E5 1/2 +E4 1/8 +A4 1/8 + +D5 1/8 +Cd5 1/8 +B4 1/8 +A4 1/8 +B4 1/8 +Cd5 1/8 + +B4 1/4 +E4 1/4 + +E4 1/8 +Fd4 1/8 + +G4 1/4 +A4 1/4 +C5 1/4 + +B4 1/2+1/4 + +A5 1/8 +G5 1/8 +F5 1/8 +E5 1/8 +D5 1/8 +F5 1/8 + +E5 1/4 +B4 1/4 + +B4 1/8 +E5 1/8 + +A5 1/8 +G5 1/8 +F5 1/8 +E5 1/8 +D5 1/8 +F5 1/8 + +E5 1/2 +E4 1/8 +A4 1/8 + +D5 1/8 +Cd5 1/8 +B4 1/8 +A4 1/8 +B4 1/8 +Cd5 1/8 + +B4 1/4 +E4 1/4 +E4 1/8 +Fd4 1/8 + +G4 1/4 +Fd4 1/4 +D4 1/4 +E4 1/2+1/4 diff --git a/examples/determination/project.yml b/examples/determination/project.yml new file mode 100644 index 0000000..0afd664 --- /dev/null +++ b/examples/determination/project.yml @@ -0,0 +1,6 @@ +name: Determination +bpm: 120 +channels: + - type: FM + path: main + volume: 0.7 \ No newline at end of file diff --git a/examples/megalovania-no-delay/channel b/examples/megalovania-no-delay/channel new file mode 100644 index 0000000..70167bd --- /dev/null +++ b/examples/megalovania-no-delay/channel @@ -0,0 +1,22 @@ +@2000 +D4 0.0625 0 +D4 0.0625 0 +D5 0.1875 0 +A4 0.1875 0 +Gd4 0.125 0 +G4 0.125 0 +F4 0.125 0 +D4 0.125 0.0 +F4 0.0625 0 +G4 0.0625 0 +@1000 +C4 0.0625 0.0 +C4 0.0625 0 +D5 0.1875 0 +A4 0.1875 0 +Gd4 0.125 0 +G4 0.125 0 +F4 0.125 0 +D4 0.0625 0.0 +F4 0.0625 0 +G4 0.0625 0.0 \ No newline at end of file diff --git a/examples/megalovania-no-delay/project.yml b/examples/megalovania-no-delay/project.yml new file mode 100644 index 0000000..4ec5470 --- /dev/null +++ b/examples/megalovania-no-delay/project.yml @@ -0,0 +1,7 @@ +name: Megalovania +bpm: 210 +channels: + - type: SSG + path: channel + - type: FM + path: channel diff --git a/examples/megalovania/channel b/examples/megalovania/channel new file mode 100644 index 0000000..a7dbb9e --- /dev/null +++ b/examples/megalovania/channel @@ -0,0 +1,24 @@ +#1 +@1 +D4 0.0625 0.05 +D4 0.0625 1/80 +D5 0.1875 0.0125 +A4 0.1875 0.0125 +Gd4 0.125 0.0625 +G4 0.125 0.0625 +F4 0.125 0.0625 +D4 0.125 0.025 +F4 0.0625 0.0125 +G4 0.0625 0.0125 +#-1 +@-1 +C4 0.0625 0.05 +C4 0.0625 0.0125 +D5 0.1875 0.0125 +A4 0.1875 0.0125 +Gd4 0.125 0.0625 +G4 0.125 0.0625 +F4 0.125 0.0625 +D4 0.0625 0.025 +F4 0.0625 0.0125 +G4 0.0625 0.0125 \ No newline at end of file diff --git a/examples/megalovania/project.yml b/examples/megalovania/project.yml new file mode 100644 index 0000000..436f065 --- /dev/null +++ b/examples/megalovania/project.yml @@ -0,0 +1,9 @@ +name: Megalovania +bpm: 210 +channels: + - type: SSG + path: channel + volume: 0.3 + - type: FM + path: channel + volume: 1 diff --git a/rinth-midi/Cargo.toml b/rinth-midi/Cargo.toml new file mode 100644 index 0000000..e9468ba --- /dev/null +++ b/rinth-midi/Cargo.toml @@ -0,0 +1,12 @@ +[package] +name = "rinth-midi" +version = "0.0.1" +edition = "2021" +publish = false +license-file = "../LICENSE" +authors = [ "Ivan Bushchik " ] + +[dependencies] +rinth-types = { path="../rinth-types" } +clap = { version = "4.4.18", features = ["derive"] } +midly = "0.5.3" diff --git a/rinth-midi/src/main.rs b/rinth-midi/src/main.rs new file mode 100644 index 0000000..f42a758 --- /dev/null +++ b/rinth-midi/src/main.rs @@ -0,0 +1,73 @@ +use clap::Parser; +use midly::num::{u28, u7}; +use midly::{MidiMessage, Smf, TrackEventKind}; +use rinth_types::note::{Note, Tone}; +use std::path::PathBuf; + +#[derive(Parser)] +#[command(version)] +struct Args { + midi: PathBuf, + + #[arg(short, default_value = "0")] + channel: u8, +} +fn main() { + let args = Args::parse(); + let binding = std::fs::read(args.midi).unwrap(); + let smf = Smf::parse(binding.as_slice()).unwrap(); + let mut is_pressed = false; + let mut delay = 0; + let mut note_pressed = u7::new(0); + let mut timer = u28::new(0); + let mut notes: Vec = vec![]; + let note_names = ["C", "C#", "D", "D#", "E", "F", "F#", "G", "G#", "A", "A#", "B"]; + for track in smf.tracks.iter() { + for event in track { + if let TrackEventKind::Midi { + channel: _channel, + message, + } = event.kind + { + match message { + MidiMessage::NoteOff { + key, + .. + } => { + if is_pressed && note_pressed == key { + let note = Note { + tone: Tone::from(&format!( + "{}{}", + note_names[(key.as_int() % 12) as usize], + (key.as_int() / 12) - 1 + )), + length: event.delta.as_int() as f32 / 1000.0, + delay: delay as f32 / 1000.0, + }; + notes.push(note); + note_pressed = u7::new(0); + is_pressed = false; + } + } + MidiMessage::NoteOn { + key, + .. + } => { + if !is_pressed { + eprintln!("pressing on {}", key); + is_pressed = true; + note_pressed = key; + timer = u28::new(0); + delay = event.delta.as_int(); + } + } + _ => {} + } + } + timer += event.delta; + } + } + for note in notes { + println!("{} {} {}", note.tone, note.length, note.delay); + } +} diff --git a/rinth-synth/Cargo.toml b/rinth-synth/Cargo.toml new file mode 100644 index 0000000..f8e920d --- /dev/null +++ b/rinth-synth/Cargo.toml @@ -0,0 +1,15 @@ +[package] +name = "rinth-synth" +version = "0.0.1" +edition = "2021" +publish = false +license-file = "../LICENSE" +authors = [ "Ivan Bushchik " ] + +[dependencies] +rinth-types = {path = "../rinth-types" } +hound = "3.5.1" +serde_yaml = "0.9.30" +clap = { version = "4.4", features = ["derive"] } +serde = { version = "1.0.195", features = ["derive"] } +meval = "0.2.0" diff --git a/rinth-synth/src/fm.rs b/rinth-synth/src/fm.rs new file mode 100644 index 0000000..4bb9545 --- /dev/null +++ b/rinth-synth/src/fm.rs @@ -0,0 +1,101 @@ +use crate::shared::{add_delay, get_note}; +use rinth_types::file::Channel; +use rinth_types::note::get_note_len; +use rinth_types::{file::ChannelType, note::Note, traits::Synth}; +use std::f32::consts::PI; +use std::fs; + +pub struct FM { + pub bpm: u16, + pub notes: Vec<(Note, ModulationFreq, FrequencyDeviation)>, +} + +#[derive(Copy, Clone)] +pub struct ModulationFreq(f32); + +#[derive(Copy, Clone)] +pub struct FrequencyDeviation(f32); + +const FIGHT_CLICKS: usize = 128; + +impl Synth for FM { + fn from_channel(channel: Channel, bpm: u16) -> Self + where + Self: Sized, + { + let channel = match channel { + Channel { + path, + channel_type: ChannelType::FM, + .. + } => path, + _ => { + unreachable!() + } + }; + + let contents = fs::read(channel).unwrap(); + + let mut notes: Vec<(Note, ModulationFreq, FrequencyDeviation)> = vec![]; + let mut current_tone = ModulationFreq(440.0); + let mut current_deviation = FrequencyDeviation(8.0); + + for line in contents.split(|&x| x == b'\n') { + if line.is_empty() { + continue; + } + let s = String::from_utf8_lossy(line); + let l = s.split_ascii_whitespace().collect::>(); + // Ignore comment + if l[0].as_bytes()[0] == b'/' { + continue; + } + // String in FM channel may be or carrier tone or note + if l[0].as_bytes()[0] == b'@' { + current_tone = ModulationFreq(l[0][1..].parse::().unwrap()); + continue; + } + if l[0].as_bytes()[0] == b'#' { + current_deviation = FrequencyDeviation(l[0][1..].parse::().unwrap()); + continue; + } + notes.push((get_note(l), current_tone, current_deviation)) + } + FM { + bpm, + notes, + } + } + + fn synthesise(&self, sample_rate: u32) -> Vec { + // t - time, m - modulating freq (carrier) + let y: fn(f32, f32, f32, f32) -> f32 = + |t: f32, f: f32, m: f32, d| (2_f32 * PI * m * t + d * (2_f32 * PI * f * t).sin()).cos(); + let mut stream: Vec = vec![]; + for (note, tone, deviation) in &self.notes { + if note.delay != 0_f32 { + add_delay(&mut stream, note.delay, self.bpm, sample_rate); + } + // Samples in _THIS_ note + let samples = (get_note_len(note.length, self.bpm) * sample_rate as f32) as usize; + for k in 0..samples { + stream.push( + y(k as f32 / sample_rate as f32, tone.0, note.tone.get_freq(), deviation.0) + // This makes a little linear fade-in-out so we don't get "clicks" + * if k <= FIGHT_CLICKS { + k as f32 / FIGHT_CLICKS as f32 + } else if k + >= samples + - FIGHT_CLICKS + { + (samples + - k) as f32 / FIGHT_CLICKS as f32 + } else { + 1.0 + }, + ) + } + } + stream + } +} diff --git a/rinth-synth/src/main.rs b/rinth-synth/src/main.rs new file mode 100644 index 0000000..2bda9a2 --- /dev/null +++ b/rinth-synth/src/main.rs @@ -0,0 +1,127 @@ +use clap::Parser; +use rinth_types::file::ChannelType; +use rinth_types::traits::Synth; +use std::cmp::max; +use std::io::Read; +use std::path::PathBuf; + +mod fm; +mod shared; +mod ssg; + +#[derive(Parser)] +enum Commands { + #[command(visible_alias = "b")] + Build(Build), + #[command(visible_alias = "m")] + Master(Build), +} + +#[derive(Parser)] +#[command(author, about, version)] +struct Build { + path: PathBuf, + #[arg(short, long, default_value = "44100")] + sample_rate: u32, +} + +fn truncate(sample: f32) -> f32 { + if sample.abs() > 1_f32 { + sample.signum() + } else { + sample + } +} + +fn merge(master: &mut Vec, slave: Vec) { + master.resize(max(master.len(), slave.len()), 0_f32); + let slave_len = slave.len(); + for (n, sample) in &mut master.iter_mut().enumerate() { + *sample = if n < slave_len { + truncate(*sample + slave[n]) + } else { + truncate(*sample) + } + } +} + +fn main() { + let args = Commands::parse(); + + let (args, mastering) = match args { + Commands::Build(args) => (args, false), + Commands::Master(args) => (args, true), + }; + let mut project_file = std::fs::File::open(&args.path).unwrap(); + let mut project = String::new(); + project_file.read_to_string(&mut project).unwrap(); + + let project: rinth_types::file::Header = serde_yaml::from_str(&project).unwrap(); + let spec = hound::WavSpec { + channels: 1, + sample_rate: args.sample_rate, + bits_per_sample: 32, + sample_format: hound::SampleFormat::Float, + }; + let mut master = vec![]; + for channel in project.channels { + let mut channel = channel; + let mut channel_path = args.path.parent().unwrap().to_path_buf(); + channel_path.push(channel.path); + channel.path = channel_path; + + let synth: Box = match channel.channel_type { + ChannelType::FM => Box::new(fm::FM::from_channel(channel.clone(), project.bpm)), + ChannelType::SSG => Box::new(ssg::SSG::from_channel(channel.clone(), project.bpm)), + }; + if !mastering { + let mut writer = hound::WavWriter::create( + format!( + "{}-{:?}.wav", + channel.path.as_os_str().to_str().unwrap(), + channel.channel_type + ), + spec, + ) + .unwrap(); + synth.synthesise(args.sample_rate).iter().for_each(|sample| { + writer + .write_sample(if let Some(volume) = channel.volume { + *sample * volume + } else { + *sample + }) + .unwrap() + }); + writer.finalize().unwrap(); + } else { + merge( + &mut master, + synth + .synthesise(args.sample_rate) + .iter() + .map(|sample| { + if let Some(volume) = channel.volume { + *sample * volume + } else { + *sample + } + }) + .collect(), + ); + } + } + if mastering { + let mut writer = hound::WavWriter::create( + format!( + "{}/{}-master.wav", + args.path.parent().unwrap().to_path_buf().to_str().unwrap(), + project.name + ), + spec, + ) + .unwrap(); + master.iter().for_each(|sample| writer.write_sample(*sample).unwrap()); + writer.finalize().unwrap(); + } +} diff --git a/rinth-synth/src/shared.rs b/rinth-synth/src/shared.rs new file mode 100644 index 0000000..36e1fb1 --- /dev/null +++ b/rinth-synth/src/shared.rs @@ -0,0 +1,27 @@ +use rinth_types::note::{get_note_len, Note, Tone}; + +pub const DEFAULT_DELAY: f32 = 0.0; + +#[inline(always)] +pub fn add_delay(stream: &mut Vec, delay: f32, bpm: u16, sample_rate: u32) { + // println!("Pushing delay of len {}s", get_note_len(delay, bpm)); + stream.append(&mut vec![0_f32; (get_note_len(delay, bpm) * sample_rate as f32) as usize]); +} + +pub fn get_note(l: Vec<&str>) -> Note { + Note { + tone: Tone::from(l[0]), + length: match l[1].parse::() { + Ok(f) => f, + Err(_) => meval::eval_str(l[1]).unwrap() as f32, + }, + delay: if l.len() > 2 { + match l[2].parse::() { + Ok(f) => f, + Err(_) => meval::eval_str(l[2]).unwrap() as f32, + } + } else { + DEFAULT_DELAY + }, + } +} diff --git a/rinth-synth/src/ssg.rs b/rinth-synth/src/ssg.rs new file mode 100644 index 0000000..6f111ae --- /dev/null +++ b/rinth-synth/src/ssg.rs @@ -0,0 +1,68 @@ +use crate::shared::{add_delay, get_note}; +use rinth_types::file::Channel; +use rinth_types::note::get_note_len; +use rinth_types::{file::ChannelType, note::Note, traits::Synth}; +use std::f32::consts::PI; +use std::fs; + +#[allow(clippy::upper_case_acronyms)] +pub struct SSG { + pub bpm: u16, + pub notes: Vec, +} + +impl Synth for SSG { + fn from_channel(channel: Channel, bpm: u16) -> Self + where + Self: Sized, + { + let channel = match channel { + Channel { + path, + channel_type: ChannelType::SSG, + .. + } => path, + _ => { + unreachable!() + } + }; + + let contents = fs::read(channel).unwrap(); + + let mut notes: Vec = vec![]; + for line in contents.split(|&x| x == b'\n') { + if line.is_empty() { + continue; + } + let s = String::from_utf8_lossy(line); + let l = s.split_ascii_whitespace().collect::>(); + // Ignoring FM-only lines and comments + if l[0].as_bytes()[0] == b'@' + || l[0].as_bytes()[0] == b'#' + || l[0].as_bytes()[0] == b'/' + { + continue; + } + notes.push(get_note(l)) + } + SSG { + bpm, + notes, + } + } + + fn synthesise(&self, sample_rate: u32) -> Vec { + // t - time, m - modulating freq (carrier) + let y: fn(f32, f32) -> f32 = |t: f32, f: f32| (2_f32 * PI * f * t).sin().signum(); + let mut stream: Vec = vec![]; + for note in &self.notes { + if note.delay != 0_f32 { + add_delay(&mut stream, note.delay, self.bpm, sample_rate); + } + for k in 0..(get_note_len(note.length, self.bpm) * sample_rate as f32) as usize { + stream.push(y(k as f32 / sample_rate as f32, note.tone.get_freq())) + } + } + stream + } +} diff --git a/rinth-types/Cargo.toml b/rinth-types/Cargo.toml new file mode 100644 index 0000000..bf92390 --- /dev/null +++ b/rinth-types/Cargo.toml @@ -0,0 +1,11 @@ +[package] +name = "rinth-types" +version = "0.0.1" +edition = "2021" +publish = false +license-file = "../LICENSE" +authors = [ "Ivan Bushchik " ] + +[dependencies] +toml = "0.8.8" +serde = { version = "1.0.195", features = ["derive"] } \ No newline at end of file diff --git a/rinth-types/src/file.rs b/rinth-types/src/file.rs new file mode 100644 index 0000000..7b6461a --- /dev/null +++ b/rinth-types/src/file.rs @@ -0,0 +1,28 @@ +use serde::{Deserialize, Serialize}; +use std::path::PathBuf; + +pub enum FileType { + Header(Header), + Channel(Channel), +} + +#[derive(Clone, Deserialize, Serialize, Debug)] +pub enum ChannelType { + FM, + SSG, +} + +#[derive(Deserialize, Serialize)] +pub struct Header { + pub name: String, + pub bpm: u16, + pub channels: Vec, +} + +#[derive(Clone, Deserialize, Serialize)] +pub struct Channel { + pub path: PathBuf, + #[serde(alias = "type")] + pub channel_type: ChannelType, + pub volume: Option, +} diff --git a/rinth-types/src/lib.rs b/rinth-types/src/lib.rs new file mode 100644 index 0000000..ed5cbad --- /dev/null +++ b/rinth-types/src/lib.rs @@ -0,0 +1,3 @@ +pub mod file; +pub mod note; +pub mod traits; diff --git a/rinth-types/src/note.rs b/rinth-types/src/note.rs new file mode 100644 index 0000000..a89ba19 --- /dev/null +++ b/rinth-types/src/note.rs @@ -0,0 +1,72 @@ +//use rinth_macros::*; +use std::fmt::{Display, Formatter}; + +use serde::{Deserialize, Serialize}; + +/// Get real length of note from musical length +pub fn get_note_len(len: f32, bpm: u16) -> f32 { + len * 240_f32 / bpm as f32 +} + +#[derive(Debug, Deserialize, Serialize)] +pub struct Note { + pub tone: Tone, + pub length: f32, + pub delay: f32, +} + +#[derive(Debug)] +pub enum ToneError { + InvalidTone(String), +} + +impl Display for ToneError { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + write!( + f, + "{}", + match self { + ToneError::InvalidTone(s) => format!("Invalid tone ({})", s), + } + ) + } +} + +#[derive(Clone, Debug, Deserialize, Serialize)] +pub struct Tone(String); + +impl Tone { + pub fn from(t: &str) -> Self { + Tone(t.to_string()) + } + + pub fn get_freq(&self) -> f32 { + const BASE_FREQ: f32 = 440.0; + let sr = self.to_string(); + let modifier: i8 = if sr.as_bytes()[1] == b'b' { + -1 + } else if sr.as_bytes()[1] == b'd' { + 1 + } else { + 0 + }; + let magic_num = (12 * sr.get(sr.len() - 1..).unwrap().parse::().unwrap() + modifier) + as u8 + match sr.as_bytes()[0] { + b'C' => 2, + b'D' => 4, + b'E' => 6, + b'F' => 7, + b'G' => 9, + b'A' => 11, + b'B' => 13, + _ => 0, + }; + BASE_FREQ * 2_f32.powf((magic_num as f32 - 59_f32) / 12_f32) + } +} + +impl Display for Tone { + fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result { + f.write_str(&self.0.to_string()) + } +} diff --git a/rinth-types/src/traits.rs b/rinth-types/src/traits.rs new file mode 100644 index 0000000..249e9b4 --- /dev/null +++ b/rinth-types/src/traits.rs @@ -0,0 +1,9 @@ +use crate::file::Channel; + +pub trait Synth { + fn from_channel(channel: Channel, bpm: u16) -> Self + where + Self: Sized; + + fn synthesise(&self, sample_rate: u32) -> Vec; +}