commit
7d74d542db
15 changed files with 2275 additions and 0 deletions
@ -0,0 +1,3 @@ |
|||
libsig/target/ |
|||
sigweb/target/ |
|||
target/ |
File diff suppressed because it is too large
@ -0,0 +1,5 @@ |
|||
[workspace] |
|||
members = [ |
|||
"libsig", |
|||
"sigweb" |
|||
] |
@ -0,0 +1,2 @@ |
|||
/target |
|||
Cargo.lock |
@ -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"] |
@ -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) |
|||
} |
|||
} |
@ -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"); |
|||
} |
@ -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)) |
|||
} |
|||
} |
@ -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)) |
|||
} |
|||
} |
@ -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"); |
|||
} |
@ -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) |
|||
} |
@ -0,0 +1 @@ |
|||
/target |
@ -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" |
@ -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"] |
@ -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]) |
|||
} |
Loading…
Reference in new issue