Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
26 changes: 26 additions & 0 deletions .gitattributes
Original file line number Diff line number Diff line change
@@ -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
18 changes: 18 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
@@ -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
122 changes: 122 additions & 0 deletions Cargo.toml
Original file line number Diff line number Diff line change
@@ -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"
15 changes: 15 additions & 0 deletions cli/Cargo.toml
Original file line number Diff line number Diff line change
@@ -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
3 changes: 3 additions & 0 deletions cli/src/main.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
fn main() {
println!("griff {}", env!("CARGO_PKG_VERSION"));
}
22 changes: 22 additions & 0 deletions clippy.toml
Original file line number Diff line number Diff line change
@@ -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" },
]
11 changes: 11 additions & 0 deletions core/Cargo.toml
Original file line number Diff line number Diff line change
@@ -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
174 changes: 174 additions & 0 deletions core/src/event.rs
Original file line number Diff line number Diff line change
@@ -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<Articulation>,
}

/// 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<Event>,
}

/// An ordered sequence of bars forming a musical phrase.
#[derive(Debug, Clone, PartialEq)]
pub struct Phrase {
/// Bars in order.
pub bars: Vec<Bar>,
}

#[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",
);
}
}
Loading