0.0.1: First alpha release

Signed-off-by: Ivan Bushchik <ivabus@ivabus.dev>
This commit is contained in:
Ivan Bushchik 2024-01-18 19:51:45 +03:00
commit 1aff34a06a
No known key found for this signature in database
GPG key ID: 2F16FBF3262E090C
25 changed files with 1383 additions and 0 deletions

5
.gitignore vendored Normal file
View file

@ -0,0 +1,5 @@
target/
*.DS_Store
.idea
*.wav
*.m4a

9
.rustfmt.toml Normal file
View file

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

464
Cargo.lock generated Normal file
View file

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

3
Cargo.toml Normal file
View file

@ -0,0 +1,3 @@
[workspace]
members = [ "rinth-midi", "rinth-synth", "rinth-types" ]
resolver = "2"

3
LICENSE Normal file
View file

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

28
LICENSE-BSD Normal file
View file

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

92
README.md Normal file
View file

@ -0,0 +1,92 @@
# Rinth synthesiser
FM and SSG synthesiser
## Usage
```shell
rinth-synth b[uild] <path/to/project.yml> # Will build separate tracks
rinth-synth m[aster] <path/to/project.yml> # Will "master" tracks into one track
```
See `--help` for more
## File formats
### \<project\>.yml
```yaml
name: <NAME>
bpm: <BPM>
channels:
- type: <SSG|FM>
path: <RELATIVE_TO_YML_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
#<Frequency deviation>
@<Modulating frequency>
<Note in SPN> <Note value> [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
<Note in SPN> <Note value> [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 <path/to/midi.mid>
```
## 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.

165
examples/determination/main Normal file
View file

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

View file

@ -0,0 +1,6 @@
name: Determination
bpm: 120
channels:
- type: FM
path: main
volume: 0.7

View file

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

View file

@ -0,0 +1,7 @@
name: Megalovania
bpm: 210
channels:
- type: SSG
path: channel
- type: FM
path: channel

View file

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

View file

@ -0,0 +1,9 @@
name: Megalovania
bpm: 210
channels:
- type: SSG
path: channel
volume: 0.3
- type: FM
path: channel
volume: 1

12
rinth-midi/Cargo.toml Normal file
View file

@ -0,0 +1,12 @@
[package]
name = "rinth-midi"
version = "0.0.1"
edition = "2021"
publish = false
license-file = "../LICENSE"
authors = [ "Ivan Bushchik <ivabus@ivabus.dev>" ]
[dependencies]
rinth-types = { path="../rinth-types" }
clap = { version = "4.4.18", features = ["derive"] }
midly = "0.5.3"

73
rinth-midi/src/main.rs Normal file
View file

@ -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<Note> = 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);
}
}

15
rinth-synth/Cargo.toml Normal file
View file

@ -0,0 +1,15 @@
[package]
name = "rinth-synth"
version = "0.0.1"
edition = "2021"
publish = false
license-file = "../LICENSE"
authors = [ "Ivan Bushchik <ivabus@ivabus.dev>" ]
[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"

101
rinth-synth/src/fm.rs Normal file
View file

@ -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::<Vec<&str>>();
// 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::<f32>().unwrap());
continue;
}
if l[0].as_bytes()[0] == b'#' {
current_deviation = FrequencyDeviation(l[0][1..].parse::<f32>().unwrap());
continue;
}
notes.push((get_note(l), current_tone, current_deviation))
}
FM {
bpm,
notes,
}
}
fn synthesise(&self, sample_rate: u32) -> Vec<f32> {
// 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<f32> = 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
}
}

127
rinth-synth/src/main.rs Normal file
View file

@ -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<f32>, slave: Vec<f32>) {
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<dyn Synth> = 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();
}
}

27
rinth-synth/src/shared.rs Normal file
View file

@ -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<f32>, 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::<f32>() {
Ok(f) => f,
Err(_) => meval::eval_str(l[1]).unwrap() as f32,
},
delay: if l.len() > 2 {
match l[2].parse::<f32>() {
Ok(f) => f,
Err(_) => meval::eval_str(l[2]).unwrap() as f32,
}
} else {
DEFAULT_DELAY
},
}
}

68
rinth-synth/src/ssg.rs Normal file
View file

@ -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<Note>,
}
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<Note> = 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::<Vec<&str>>();
// 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<f32> {
// 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<f32> = 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
}
}

11
rinth-types/Cargo.toml Normal file
View file

@ -0,0 +1,11 @@
[package]
name = "rinth-types"
version = "0.0.1"
edition = "2021"
publish = false
license-file = "../LICENSE"
authors = [ "Ivan Bushchik <ivabus@ivabus.dev>" ]
[dependencies]
toml = "0.8.8"
serde = { version = "1.0.195", features = ["derive"] }

28
rinth-types/src/file.rs Normal file
View file

@ -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<Channel>,
}
#[derive(Clone, Deserialize, Serialize)]
pub struct Channel {
pub path: PathBuf,
#[serde(alias = "type")]
pub channel_type: ChannelType,
pub volume: Option<f32>,
}

3
rinth-types/src/lib.rs Normal file
View file

@ -0,0 +1,3 @@
pub mod file;
pub mod note;
pub mod traits;

72
rinth-types/src/note.rs Normal file
View file

@ -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::<i8>().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())
}
}

View file

@ -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<f32>;
}