Initial working commit

This commit is contained in:
Leonora Tindall 2021-10-17 14:22:44 -05:00
commit 7d74d542db
Signed by: nora
GPG Key ID: 7A8B52EC67E09AAF
15 changed files with 2275 additions and 0 deletions

3
.gitignore vendored Normal file
View File

@ -0,0 +1,3 @@
libsig/target/
sigweb/target/
target/

1703
Cargo.lock generated Normal file

File diff suppressed because it is too large Load Diff

5
Cargo.toml Normal file
View File

@ -0,0 +1,5 @@
[workspace]
members = [
"libsig",
"sigweb"
]

2
libsig/.gitignore vendored Normal file
View File

@ -0,0 +1,2 @@
/target
Cargo.lock

14
libsig/Cargo.toml Normal file
View File

@ -0,0 +1,14 @@
[package]
name = "libsig"
version = "0.1.0"
edition = "2018"
[dependencies]
rand = "0.8"
rand_distr = "0.4"
derive_more = "0.99"
structopt = "0.3"
[dependencies.serde]
version = "1"
features = ["derive"]

26
libsig/examples/cli.rs Normal file
View File

@ -0,0 +1,26 @@
use libsig;
use structopt::StructOpt;
#[derive(StructOpt)]
#[structopt(name="sigcli", about="generate ideas for songs, tracks, and sounds")]
struct CliOptions {
/// Don't suggest percussion elements
#[structopt(short, long)]
ambient: bool,
/// The number of suggestions to generate
#[structopt(default_value="1")]
suggestions: usize
}
fn main() {
let opt = CliOptions::from_args();
for _ in 0..opt.suggestions {
let idea = if opt.ambient {
libsig::SongIdea::generate_ambient()
} else {
libsig::SongIdea::generate()
};
println!("{}", idea)
}
}

48
libsig/src/displayutil.rs Normal file
View File

@ -0,0 +1,48 @@
use std::fmt::Display;
pub fn display_list(list: &[impl Display]) -> String {
match list.len() {
0 => "".into(),
1 => format!("{}", list[0]),
2 => format!("{} and {}", list[0], list[1]),
_ => format!("{}and {}", display_list_commas(&list[0..list.len()-1]), list[list.len()-1])
}
}
fn display_list_commas(list: &[impl Display]) -> String {
let mut string = String::new();
for item in list {
string.push_str(&format!("{}, ", item));
}
string
}
#[test]
fn single_item() {
let list = ["red"];
assert_eq!(&display_list(&list), "red");
}
#[test]
fn two_items() {
let list = ["red", "green"];
assert_eq!(&display_list(&list), "red and green");
}
#[test]
fn three_items() {
let list = ["red", "green", "blue"];
assert_eq!(&display_list(&list), "red, green, and blue");
}
#[test]
fn four_items() {
let list = ["red", "yellow", "green", "blue"];
assert_eq!(&display_list(&list), "red, yellow, green, and blue");
}
#[test]
fn many_items() {
let list = ["red", "orange" , "yellow", "green", "blue", "purple"];
assert_eq!(&display_list(&list), "red, orange, yellow, green, blue, and purple");
}

82
libsig/src/effects.rs Normal file
View File

@ -0,0 +1,82 @@
use serde::{Deserialize, Serialize};
use derive_more::Display;
use rand::distributions::{Distribution, Standard};
use rand::Rng;
use crate::displayutil::display_list;
#[derive(Debug, PartialEq, Copy, Clone, Display, Serialize, Deserialize)]
pub enum Effect {
#[display(fmt = "delay")]
Delay,
#[display(fmt = "reverb")]
Reverb,
#[display(fmt = "lofi")]
Lofi,
#[display(fmt = "drive")]
Drive,
#[display(fmt = "chorus")]
Chorus,
#[display(fmt = "tremolo")]
Tremolo,
#[display(fmt = "distortion")]
Distortion,
#[display(fmt = "flanger")]
Flanger,
#[display(fmt = "phaser")]
Phaser,
#[display(fmt = "compressor")]
Compressor,
#[display(fmt = "sustain")]
Sustain,
#[display(fmt = "bitcrusher")]
Bitcrusher,
#[display(fmt = "wavefolder")]
Wavefolder,
#[display(fmt = "ring modulator")]
RingMod,
#[display(fmt = "wah")]
Wah,
}
impl Distribution<Effect> for Standard {
fn sample<R: Rng + ?Sized>(&self, rng: &mut R) -> Effect {
let index: u8 = rng.gen_range(0..15);
use Effect::*;
match index {
0 => Delay,
1 => Reverb,
2 => Lofi,
3 => Drive,
4 => Chorus,
5 => Tremolo,
6 => Distortion,
7 => Flanger,
8 => Phaser,
9 => Compressor,
10 => Sustain,
11 => Bitcrusher,
12 => Wavefolder,
13 => RingMod,
14 => Wah,
_ => unreachable!(),
}
}
}
pub fn generate_effects() -> Vec<Effect> {
let mut vec = Vec::<Effect>::new();
while rand::random() {
vec.push(rand::random())
}
vec
}
pub fn display_effects(effects: &[Effect]) -> String {
if effects.is_empty() {
"clean".into()
} else {
format!("through {}", display_list(effects))
}
}

67
libsig/src/lib.rs Normal file
View File

@ -0,0 +1,67 @@
use serde::{Deserialize, Serialize};
use std::fmt::Display;
mod displayutil;
mod effects;
mod tempo;
mod voices;
pub use effects::{display_effects, generate_effects, Effect};
pub use tempo::Tempo;
pub use voices::{generate_voice_adjective, Voice, display_voices};
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
pub struct SongIdea {
pub tempo: Tempo,
pub effects: Vec<Effect>,
pub lead: Option<Voice>,
pub bass: Option<Voice>,
pub pad: Option<Voice>,
pub vocals: Option<Voice>,
pub kick: Option<Voice>,
pub snare: Option<Voice>,
pub hat: Option<Voice>,
pub tom: Option<Voice>,
pub cymbal: Option<Voice>,
}
impl SongIdea {
pub fn generate() -> Self {
Self {
tempo: rand::random(),
effects: generate_effects(),
lead: generate_voice_adjective("lead"),
bass: generate_voice_adjective("bass"),
pad: generate_voice_adjective("pad"),
vocals: generate_voice_adjective("vocals"),
kick: generate_voice_adjective("kick"),
snare: generate_voice_adjective("snare"),
hat: generate_voice_adjective("hat"),
tom: generate_voice_adjective("tom"),
cymbal: generate_voice_adjective("cymbal")
}
}
pub fn generate_ambient() -> Self {
Self {
tempo: rand::random(),
effects: generate_effects(),
lead: generate_voice_adjective("lead"),
bass: generate_voice_adjective("bass"),
pad: generate_voice_adjective("pad"),
vocals: generate_voice_adjective("vocals"),
kick: None,
snare: None,
hat: None,
tom: None,
cymbal: None
}
}
}
impl Display for SongIdea {
fn fmt(&self, fmt: &mut std::fmt::Formatter<'_>) -> Result<(), std::fmt::Error> {
let voices: Vec<_> = [&self.lead, &self.bass, &self.pad, &self.vocals, &self.kick, &self.snare, &self.hat, &self.tom, &self.cymbal].iter().cloned().cloned().filter_map(|voice| voice).collect();
write!(fmt, "{} at {}, {}", display_voices(&voices), self.tempo, display_effects(&self.effects))
}
}

51
libsig/src/tempo.rs Normal file
View File

@ -0,0 +1,51 @@
use serde::{Serialize, Deserialize};
use derive_more::Display;
use rand::distributions::{Distribution, Standard};
use rand::Rng;
#[derive(Debug, PartialEq, Copy, Clone, Display, Serialize, Deserialize)]
#[display(fmt="{} bpm, with {} swing", bpm, swing)]
pub struct Tempo {
bpm: u16,
swing: Swing,
}
impl Distribution<Tempo> for Standard {
fn sample<R: Rng + ?Sized>(&self, rng: &mut R) -> Tempo {
// These parameters are static and known-good
let raw_bpm_distr: f32 = rand_distr::SkewNormal::new(25.0, 75.0, 5.0).unwrap().sample(rng);
let bpm: u16 =
if raw_bpm_distr < 33. { 33 } else if raw_bpm_distr > 300.0 { 300 } else { raw_bpm_distr as u16 };
let swing: Swing = Standard::sample(&self, rng);
Tempo {
bpm, swing
}
}
}
#[derive(Debug, PartialEq, Copy, Clone, Display, Serialize, Deserialize)]
pub enum Swing {
#[display(fmt = "no")]
None,
#[display(fmt = "light")]
Light,
#[display(fmt = "heavy")]
Heavy
}
impl Distribution<Swing> for Standard {
fn sample<R: Rng + ?Sized>(&self, rng: &mut R) -> Swing {
let index: u8 = rng.gen_range(0..6);
match index {
0 | 1 | 2=> Swing::None,
3 | 4 => Swing::Light,
5 => Swing::Heavy,
_ => unreachable!(),
}
}
}
#[test]
fn fmt_tempo_struct() {
assert_eq!(&format!("{}", Tempo { bpm: 80, swing: Swing::Light}), "80 bpm, with light swing");
}

204
libsig/src/voices.rs Normal file
View File

@ -0,0 +1,204 @@
use rand::prelude::SliceRandom;
use serde::{Deserialize, Serialize};
use crate::displayutil::display_list;
use derive_more::Display;
use rand::distributions::{Distribution, Standard};
use rand::Rng;
#[derive(Clone, Copy, Debug, Serialize, Deserialize, PartialEq, Eq, Display)]
pub enum VoiceAdjective {
Glassy,
Wide,
Glitchy,
Aggressive,
Warm,
Thick,
Clean,
Hard,
Wet,
Dry,
Thin,
Tinny,
Splashy,
Heavy,
Light,
Dark,
Smooth,
Plucky,
Soft,
Droning,
Digital,
Squishy,
Analogue,
Round,
Sharp,
Crunchy,
Aliased,
Punchy,
Intense,
Mild,
Calm,
Sonorous,
Resonant,
Buzzy,
Dampened,
Subdued,
Rampant,
Hot,
Sweaty,
Flooded,
Plonky,
Yoinky,
Sploinky,
Boofy,
Middy,
Grimy,
Ugly,
Soothing,
Skittery,
Harsh,
Alien,
Bubbly,
Airy,
Subterranean,
Claustrophobic,
Spacious,
Ringing,
Clicky,
Acidic,
Solid,
Ethereal,
Massive,
Distant,
Muffled,
Garbled,
Degraded,
BlownOut,
Sizzling,
#[display(fmt = "In-your-face")]
Inyouface,
#[display(fmt = "Way Huge")]
Wayhuge,
ElectroHarmonic,
Surfy,
Nostalgic,
Ancient,
Angular,
Fried,
Ineffable,
Ramshackle,
Mercurial,
}
#[derive(Clone, Debug, Serialize, Deserialize, PartialEq, Eq)]
pub struct Voice {
adjective: VoiceAdjective,
voice: String,
}
impl Distribution<VoiceAdjective> for Standard {
fn sample<R: Rng + ?Sized>(&self, rng: &mut R) -> VoiceAdjective {
use VoiceAdjective::*;
let values = [
Glassy,
Wide,
Glitchy,
Aggressive,
Warm,
Thick,
Clean,
Hard,
Wet,
Dry,
Thin,
Tinny,
Splashy,
Heavy,
Light,
Dark,
Smooth,
Plucky,
Soft,
Droning,
Digital,
Squishy,
Analogue,
Round,
Sharp,
Crunchy,
Aliased,
Punchy,
Intense,
Mild,
Calm,
Sonorous,
Resonant,
Buzzy,
Dampened,
Subdued,
Rampant,
Hot,
Sweaty,
Flooded,
Plonky,
Yoinky,
Sploinky,
Boofy,
Middy,
Grimy,
Ugly,
Soothing,
Skittery,
Harsh,
Alien,
Bubbly,
Airy,
Subterranean,
Claustrophobic,
Spacious,
Ringing,
Clicky,
Acidic,
Solid,
Ethereal,
Massive,
Distant,
Muffled,
Garbled,
Degraded,
BlownOut,
Sizzling,
Inyouface,
Wayhuge,
ElectroHarmonic,
Surfy,
Nostalgic,
Ancient,
Angular,
Fried,
Ineffable,
Ramshackle,
Mercurial,
];
values.choose(rng).unwrap().clone()
}
}
pub fn generate_voice_adjective(voice: &'static str) -> Option<Voice> {
if rand::random() {
None
} else {
Some(Voice {
adjective: rand::random(),
voice: voice.into(),
})
}
}
pub fn display_voices(list: &[Voice]) -> String {
let voices_formatted: Vec<_> = list
.iter()
.map(|voice| format!("{} {}", voice.adjective, voice.voice).to_lowercase())
.collect();
display_list(&voices_formatted)
}

1
sigweb/.gitignore vendored Normal file
View File

@ -0,0 +1 @@
/target

7
sigweb/Cargo.lock generated Normal file
View File

@ -0,0 +1,7 @@
# This file is automatically @generated by Cargo.
# It is not intended for manual editing.
version = 3
[[package]]
name = "sigweb"
version = "0.1.0"

13
sigweb/Cargo.toml Normal file
View File

@ -0,0 +1,13 @@
[package]
name = "sigweb"
version = "0.1.0"
edition = "2018"
[dependencies]
serde = "1"
serde_json = "1"
libsig = { path = "../libsig" }
[dependencies.rocket]
version = "0.5.0-rc.1"
features = ["json"]

49
sigweb/src/main.rs Normal file
View File

@ -0,0 +1,49 @@
#[macro_use]
extern crate rocket;
use libsig;
use serde::{Serialize, Deserialize};
use rocket::serde::json::Json;
use serde_json::{json, Value};
#[get("/api")]
fn api_version_available() -> Json<Value> {
Json(json! ({
"versions": [ "v1" ]
}))
}
#[get("/api/v1")]
fn api_v1_index() -> Json<Value> {
Json(json! ({
"endpoints": [ "generate", "generate_ambient" ]
}))
}
#[derive(Debug, Serialize, Deserialize)]
struct Output {
data: libsig::SongIdea,
text: String,
}
#[get("/api/v1/generate")]
fn generate() -> Json<Output> {
let idea = libsig::SongIdea::generate();
Json(Output {
data: idea.clone(),
text: idea.to_string()
})
}
#[get("/api/v1/generate_ambient")]
fn generate_ambient() -> Json<Output> {
let idea = libsig::SongIdea::generate_ambient();
Json(Output {
data: idea.clone(),
text: idea.to_string()
})
}
#[launch]
fn rocket() -> _ {
rocket::build().mount("/", routes![api_version_available, api_v1_index, generate, generate_ambient])
}