diff --git a/.gitattributes b/.gitattributes new file mode 100644 index 00000000..c4adf026 --- /dev/null +++ b/.gitattributes @@ -0,0 +1,26 @@ +# Force LF for all text files, regardless of core.autocrlf. +* text=auto eol=lf + +# Source and config — LF +*.rs text eol=lf +*.toml text eol=lf +*.md text eol=lf +*.yml text eol=lf +*.yaml text eol=lf +*.json text eol=lf +*.sh text eol=lf + +# Windows-only scripts — keep CRLF +*.ps1 text eol=crlf +*.bat text eol=crlf +*.cmd text eol=crlf + +# Binaries — never touch +*.exe binary +*.dll binary +*.pdb binary +*.png binary +*.jpg binary +*.ico binary +*.zip binary +*.tar.gz binary diff --git a/.gitignore b/.gitignore new file mode 100644 index 00000000..6682bf4a --- /dev/null +++ b/.gitignore @@ -0,0 +1,18 @@ +# Generated by Cargo +debug +target + +# Backup files generated by rustfmt +**/*.rs.bk + +# MSVC debugging information +*.pdb + +# cargo mutants output +**/mutants.out*/ + +# rustc internal compiler error traces +rustc-ice-*.txt + +# Local development overrides (e.g. rust-analyzer) +*.local.toml diff --git a/Cargo.toml b/Cargo.toml new file mode 100644 index 00000000..53c391f1 --- /dev/null +++ b/Cargo.toml @@ -0,0 +1,122 @@ +[workspace] +members = ["core", "cli", "plugin"] +resolver = "2" + +[workspace.package] +version = "0.1.0" +edition = "2021" +rust-version = "1.74" +license = "MIT" +repository = "https://github.com/physshell/griff" +homepage = "https://github.com/physshell/griff" + +[profile.release] +lto = "thin" +codegen-units = 1 +strip = true + +# --------------------------------------------------------------------------- +# Workspace-wide lint policy. Every member opts in via `[lints] workspace = true`. +# Groups are denied wholesale; a curated allow-list covers legitimate patterns. +# Anything outside the allow-list must be fixed or annotated with a local +# `#[allow(...)]` accompanied by a reason comment. +# --------------------------------------------------------------------------- +[workspace.lints.rust] +unsafe_code = "forbid" +missing_debug_implementations = "warn" +missing_copy_implementations = "warn" +unreachable_pub = "warn" +trivial_casts = "warn" +trivial_numeric_casts = "warn" +unused_import_braces = "warn" +unused_lifetimes = "warn" +unused_qualifications = "warn" +keyword_idents = { level = "warn", priority = -1 } +let_underscore_drop = "warn" +macro_use_extern_crate = "warn" +meta_variable_misuse = "warn" +non_ascii_idents = "warn" +single_use_lifetimes = "warn" +variant_size_differences = "warn" +rust_2018_idioms = { level = "warn", priority = -1 } +future_incompatible = { level = "warn", priority = -1 } +nonstandard_style = { level = "warn", priority = -1 } + +[workspace.lints.rustdoc] +broken_intra_doc_links = "warn" +private_intra_doc_links = "warn" +invalid_codeblock_attributes = "warn" +bare_urls = "warn" + +[workspace.lints.clippy] +# Whole groups denied. `priority = -1` lets per-lint entries below override. +all = { level = "deny", priority = -1 } +pedantic = { level = "deny", priority = -1 } +nursery = { level = "deny", priority = -1 } +cargo = { level = "deny", priority = -1 } +complexity = { level = "deny", priority = -1 } +perf = { level = "deny", priority = -1 } +style = { level = "deny", priority = -1 } +suspicious = { level = "deny", priority = -1 } +correctness = { level = "deny", priority = -1 } + +# Restriction lints — explicitly opted in. +absolute_paths = "warn" +arithmetic_side_effects = "warn" +assertions_on_result_states = "warn" +as_underscore = "warn" +clone_on_ref_ptr = "warn" +dbg_macro = "warn" +empty_drop = "warn" +empty_structs_with_brackets = "warn" +exit = "warn" +expect_used = "warn" +filetype_is_file = "warn" +float_cmp_const = "warn" +fn_to_numeric_cast_any = "warn" +get_unwrap = "warn" +if_then_some_else_none = "warn" +indexing_slicing = "warn" +let_underscore_must_use = "warn" +lossy_float_literal = "warn" +mem_forget = "warn" +missing_assert_message = "warn" +mixed_read_write_in_expression = "warn" +mod_module_files = "warn" +multiple_inherent_impl = "warn" +mutex_atomic = "warn" +panic = "warn" +rc_buffer = "warn" +rc_mutex = "warn" +rest_pat_in_fully_bound_structs = "warn" +same_name_method = "warn" +shadow_unrelated = "warn" +str_to_string = "warn" +string_add = "warn" +string_slice = "warn" +suspicious_xor_used_as_pow = "warn" +todo = "warn" +try_err = "warn" +undocumented_unsafe_blocks = "warn" +unimplemented = "warn" +unnecessary_safety_doc = "warn" +unnecessary_self_imports = "warn" +unneeded_field_pattern = "warn" +unwrap_in_result = "warn" +unwrap_used = "warn" +verbose_file_reads = "warn" + +# Targeted opt-outs (with rationale). +# `module_name_repetitions` — idiomatic Rust often pairs module and type names. +module_name_repetitions = "allow" +# `multiple_crate_versions` — transitive duplicates we cannot control. +multiple_crate_versions = "allow" +# Doc-comment completeness — overkill for internal crates at this stage. +missing_errors_doc = "allow" +missing_panics_doc = "allow" +must_use_candidate = "allow" +# CLI legitimately writes to stdout/stderr. +print_stdout = "allow" +print_stderr = "allow" +# Explicit `pub(crate)` visibility is intentional throughout. +redundant_pub_crate = "allow" diff --git a/cli/Cargo.toml b/cli/Cargo.toml new file mode 100644 index 00000000..abe8d661 --- /dev/null +++ b/cli/Cargo.toml @@ -0,0 +1,15 @@ +[package] +name = "griff-cli" +description = "griff command-line interface" +version.workspace = true +edition.workspace = true +rust-version.workspace = true +license.workspace = true +repository.workspace = true + +[[bin]] +name = "griff" +path = "src/main.rs" + +[lints] +workspace = true diff --git a/cli/src/main.rs b/cli/src/main.rs new file mode 100644 index 00000000..b86c4f9d --- /dev/null +++ b/cli/src/main.rs @@ -0,0 +1,3 @@ +fn main() { + println!("griff {}", env!("CARGO_PKG_VERSION")); +} diff --git a/clippy.toml b/clippy.toml new file mode 100644 index 00000000..b0a6c786 --- /dev/null +++ b/clippy.toml @@ -0,0 +1,22 @@ +# Complexity / size thresholds. +cognitive-complexity-threshold = 15 +too-many-arguments-threshold = 5 +too-many-lines-threshold = 80 +type-complexity-threshold = 150 +pass-by-value-size-limit = 192 +array-size-threshold = 16384 + +doc-valid-idents = [ + "MIDI", "CLAP", "DAW", "BPM", "PPQN", "VST", + "JSON", "TOML", "YAML", "URL", "URI", "CLI", "API", "ID", + "OS", "UUID", "DGD", +] + +disallowed-methods = [ + { path = "std::env::set_var", reason = "racy in multithreaded code; pass values explicitly" }, + { path = "std::env::remove_var", reason = "racy in multithreaded code" }, +] + +disallowed-macros = [ + { path = "std::dbg", reason = "use tracing::debug! instead" }, +] diff --git a/core/Cargo.toml b/core/Cargo.toml new file mode 100644 index 00000000..c3895137 --- /dev/null +++ b/core/Cargo.toml @@ -0,0 +1,11 @@ +[package] +name = "griff-core" +description = "Musical model, event types, slicing, features, and generation engine" +version.workspace = true +edition.workspace = true +rust-version.workspace = true +license.workspace = true +repository.workspace = true + +[lints] +workspace = true diff --git a/core/src/event.rs b/core/src/event.rs new file mode 100644 index 00000000..897707b7 --- /dev/null +++ b/core/src/event.rs @@ -0,0 +1,174 @@ +//! Fundamental musical event types. + +/// MIDI pitch number (0–127; 60 = middle C). +#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash)] +pub struct Pitch(pub u8); + +/// Duration in ticks (PPQN-relative; track resolution is carried externally). +#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash)] +pub struct Ticks(pub u32); + +/// MIDI velocity (0–127). +#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash)] +pub struct Velocity(pub u8); + +/// Tempo in beats per minute. +#[derive(Debug, Clone, Copy, PartialEq, PartialOrd)] +pub struct Tempo(pub f64); + +/// Time signature, e.g. 4/4 or 7/8. +#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)] +pub struct TimeSignature { + /// Beats per measure. + pub numerator: u8, + /// Beat unit as a power of two (2 = half-note, 4 = quarter-note, …). + pub denominator: u8, +} + +/// Per-note guitar articulation carried as optional metadata. +#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)] +pub enum Articulation { + /// Slide into or out of the note. + Slide, + /// Pitch bend up or down. + Bend, + /// Legato (slur). + Legato, + /// Palm mute. + PalmMute, + /// Hammer-on. + HammerOn, + /// Pull-off. + PullOff, + /// Vibrato. + Vibrato, + /// Natural harmonic. + HarmonicNatural, + /// Pinch harmonic. + HarmonicPinch, +} + +/// A sounding note. +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub struct Note { + /// MIDI pitch. + pub pitch: Pitch, + /// Duration in ticks. + pub duration: Ticks, + /// MIDI velocity. + pub velocity: Velocity, + /// Optional playing technique. + pub articulation: Option, +} + +/// A silence of a given duration. +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub struct Rest { + /// Duration in ticks. + pub duration: Ticks, +} + +/// A musical event: a sounding note or a silence. +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub enum Event { + /// A sounding note. + Note(Note), + /// A silence. + Rest(Rest), +} + +impl Event { + /// Duration of this event regardless of its kind. + pub fn duration(self) -> Ticks { + match self { + Self::Note(n) => n.duration, + Self::Rest(r) => r.duration, + } + } +} + +/// One measure: its events plus the governing meter and tempo. +#[derive(Debug, Clone, PartialEq)] +pub struct Bar { + /// Meter of this bar. + pub time_signature: TimeSignature, + /// Tempo at bar start. + pub tempo: Tempo, + /// Ordered events that fill the bar. + pub events: Vec, +} + +/// An ordered sequence of bars forming a musical phrase. +#[derive(Debug, Clone, PartialEq)] +pub struct Phrase { + /// Bars in order. + pub bars: Vec, +} + +#[cfg(test)] +mod tests { + use super::{ + Articulation, Bar, Event, Note, Phrase, Pitch, Rest, Tempo, Ticks, TimeSignature, + Velocity, + }; + + #[test] + fn note_event_duration_matches() { + let note = Note { + pitch: Pitch(60), + duration: Ticks(480), + velocity: Velocity(100), + articulation: None, + }; + assert_eq!( + Event::Note(note).duration(), + Ticks(480), + "note event duration must equal the inner note duration", + ); + } + + #[test] + fn rest_event_duration_matches() { + let rest = Rest { duration: Ticks(240) }; + assert_eq!( + Event::Rest(rest).duration(), + Ticks(240), + "rest event duration must equal the inner rest duration", + ); + } + + #[test] + fn bar_holds_events() { + let note = Note { + pitch: Pitch(64), + duration: Ticks(480), + velocity: Velocity(80), + articulation: Some(Articulation::PalmMute), + }; + let bar = Bar { + time_signature: TimeSignature { numerator: 4, denominator: 4 }, + tempo: Tempo(120.0), + events: vec![Event::Note(note)], + }; + assert_eq!( + bar.events.len(), + 1, + "bar must contain the one event that was added", + ); + } + + #[test] + fn phrase_collects_bars() { + let bar = Bar { + time_signature: TimeSignature { numerator: 7, denominator: 8 }, + tempo: Tempo(140.0), + events: vec![Event::Rest(Rest { duration: Ticks(1920) })], + }; + let phrase = Phrase { bars: vec![bar] }; + assert_eq!( + phrase.bars.len(), + 1, + "phrase must contain the one bar that was added", + ); + } +} diff --git a/core/src/lib.rs b/core/src/lib.rs new file mode 100644 index 00000000..04680590 --- /dev/null +++ b/core/src/lib.rs @@ -0,0 +1,7 @@ +//! Core musical model for griff. +//! +//! All musical data flows through the types defined here. +//! MIDI bytes are produced and consumed only at the import/export boundary; +//! the rest of the codebase works exclusively with these structured types. + +pub mod event; diff --git a/deny.toml b/deny.toml new file mode 100644 index 00000000..1fa81ff9 --- /dev/null +++ b/deny.toml @@ -0,0 +1,33 @@ +# cargo-deny — licenses, security advisories, banned crates, sources. +# See https://embarkstudios.github.io/cargo-deny/ + +[graph] +all-features = true + +[advisories] +version = 2 +yanked = "warn" + +[licenses] +version = 2 +allow = [ + "Apache-2.0", + "Apache-2.0 WITH LLVM-exception", + "MIT", + "BSD-3-Clause", + "ISC", + "Unicode-3.0", + "Zlib", +] +confidence-threshold = 0.8 + +[bans] +multiple-versions = "warn" +wildcards = "warn" +highlight = "all" + +[sources] +unknown-registry = "deny" +unknown-git = "deny" +allow-registry = ["https://github.com/rust-lang/crates.io-index"] +allow-git = [] diff --git a/plugin/Cargo.toml b/plugin/Cargo.toml new file mode 100644 index 00000000..e4ef8e0a --- /dev/null +++ b/plugin/Cargo.toml @@ -0,0 +1,11 @@ +[package] +name = "griff-plugin" +description = "griff CLAP plugin — intentionally empty until S10" +version.workspace = true +edition.workspace = true +rust-version.workspace = true +license.workspace = true +repository.workspace = true + +[lints] +workspace = true diff --git a/plugin/src/lib.rs b/plugin/src/lib.rs new file mode 100644 index 00000000..a0a2f9a2 --- /dev/null +++ b/plugin/src/lib.rs @@ -0,0 +1 @@ +// CLAP plugin — intentionally empty until S10. diff --git a/rust-toolchain.toml b/rust-toolchain.toml new file mode 100644 index 00000000..833584bf --- /dev/null +++ b/rust-toolchain.toml @@ -0,0 +1,3 @@ +[toolchain] +channel = "stable" +components = ["rustfmt", "clippy"] diff --git a/rustfmt.toml b/rustfmt.toml new file mode 100644 index 00000000..7606f851 --- /dev/null +++ b/rustfmt.toml @@ -0,0 +1,11 @@ +edition = "2021" +max_width = 100 +hard_tabs = false +tab_spaces = 4 +newline_style = "Unix" +use_small_heuristics = "Default" +reorder_imports = true +reorder_modules = true +match_block_trailing_comma = false +use_field_init_shorthand = true +use_try_shorthand = true