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
8 changes: 8 additions & 0 deletions neothesia-core/src/config/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -150,6 +150,14 @@ impl Config {
self.appearance.glow = glow;
}

pub fn chord_identifier(&self) -> bool {
self.appearance.chord_identifier
}

pub fn set_chord_identifier(&mut self, chord_identifier: bool) {
self.appearance.chord_identifier = chord_identifier;
}

pub fn last_opened_song(&self) -> Option<&PathBuf> {
self.history.last_opened_song.as_ref()
}
Expand Down
4 changes: 4 additions & 0 deletions neothesia-core/src/config/model.rs
Original file line number Diff line number Diff line change
Expand Up @@ -172,6 +172,9 @@ pub struct AppearanceConfigV1 {

#[serde(default = "default_glow")]
pub glow: bool,

#[serde(default)]
pub chord_identifier: bool,
}

#[derive(Serialize, Deserialize)]
Expand All @@ -187,6 +190,7 @@ impl Default for AppearanceConfig {
vertical_guidelines: default_vertical_guidelines(),
horizontal_guidelines: default_horizontal_guidelines(),
glow: default_glow(),
chord_identifier: false,
})
}
}
Expand Down
44 changes: 22 additions & 22 deletions neothesia/src/scene/freeplay/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -275,20 +275,20 @@ impl Scene for FreeplayScene {
.map(|(id, _)| id as u8 + start)
.collect();

self.deduced_chord_name = chords::deduce_name(&notes);
self.deduced_chord_name = chords::deduce_name(&notes).unwrap_or_default();
}
}
}

mod chords {
pub(crate) mod chords {
/// Get chord name based on notes, eg. Cmaj7
pub fn deduce_name(midi_notes: &[u8]) -> String {
pub fn deduce_name(midi_notes: &[u8]) -> Option<String> {
if midi_notes.is_empty() {
return "No notes".to_string();
return None;
}

if midi_notes.len() == 1 {
return note_name(midi_notes[0]).to_string();
return Some(note_name(midi_notes[0]).to_string());
}

// Normalize notes to a single octave and sort
Expand All @@ -297,7 +297,7 @@ mod chords {
normalized.dedup();

if normalized.is_empty() {
return String::new();
return None;
}

// Try each note as potential root
Expand All @@ -306,11 +306,11 @@ mod chords {
let intervals = get_intervals(&normalized, root);

if let Some(chord_type) = match_chord_type(intervals) {
return format!("{}{}", note_name(root), chord_type);
return Some(format!("{}{}", note_name(root), chord_type));
}
}

String::new()
None
}

// TODO: This is clunky, we should change names based on the scale in use
Expand Down Expand Up @@ -376,37 +376,37 @@ mod chords {

#[test]
fn test_major_chords() {
assert_eq!(deduce_name(&[60, 64, 67]), "CM"); // C major
assert_eq!(deduce_name(&[62, 66, 69]), "DM"); // D major
assert_eq!(deduce_name(&[60, 64, 67]).unwrap(), "CM"); // C major
assert_eq!(deduce_name(&[62, 66, 69]).unwrap(), "DM"); // D major
}

#[test]
fn test_minor_chords() {
assert_eq!(deduce_name(&[60, 63, 67]), "Cm"); // C minor
assert_eq!(deduce_name(&[57, 60, 64]), "Am"); // A minor
assert_eq!(deduce_name(&[60, 63, 67]).unwrap(), "Cm"); // C minor
assert_eq!(deduce_name(&[57, 60, 64]).unwrap(), "Am"); // A minor
}

#[test]
fn test_seventh_chords() {
assert_eq!(deduce_name(&[60, 64, 67, 71]), "Cmaj7"); // C major 7
assert_eq!(deduce_name(&[60, 64, 67, 70]), "C7"); // C dominant 7
assert_eq!(deduce_name(&[60, 63, 67, 70]), "Cm7"); // C minor 7
assert_eq!(deduce_name(&[60, 64, 67, 71]).unwrap(), "Cmaj7"); // C major 7
assert_eq!(deduce_name(&[60, 64, 67, 70]).unwrap(), "C7"); // C dominant 7
assert_eq!(deduce_name(&[60, 63, 67, 70]).unwrap(), "Cm7"); // C minor 7
}

#[test]
fn test_other_chords() {
assert_eq!(deduce_name(&[60, 63, 66]), "Cdim"); // C diminished
assert_eq!(deduce_name(&[60, 64, 68]), "Caug"); // C augmented
assert_eq!(deduce_name(&[60, 65, 67]), "Csus4"); // C sus4
assert_eq!(deduce_name(&[60, 67]), "C5"); // C power chord
assert_eq!(deduce_name(&[60, 63, 66]).unwrap(), "Cdim"); // C diminished
assert_eq!(deduce_name(&[60, 64, 68]).unwrap(), "Caug"); // C augmented
assert_eq!(deduce_name(&[60, 65, 67]).unwrap(), "Csus4"); // C sus4
assert_eq!(deduce_name(&[60, 67]).unwrap(), "C5"); // C power chord
}

#[test]
fn test_edge_cases() {
assert_eq!(deduce_name(&[]), "No notes");
assert_eq!(deduce_name(&[60]), "C");
assert_eq!(deduce_name(&[]), None);
assert_eq!(deduce_name(&[60]).unwrap(), "C");
// Multiple octaves should normalize
assert_eq!(deduce_name(&[48, 64, 67, 72]), "CM");
assert_eq!(deduce_name(&[48, 64, 67, 72]).unwrap(), "CM");
}
}
}
32 changes: 32 additions & 0 deletions neothesia/src/scene/playing_scene/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -50,6 +50,8 @@ pub struct PlayingScene {
nuon: nuon::Ui,
mouse_to_midi_state: MouseToMidiEventState,

deduced_chord_name: String,

top_bar: TopBar,
}

Expand Down Expand Up @@ -126,6 +128,7 @@ impl PlayingScene {

nuon: nuon::Ui::new(),
mouse_to_midi_state: MouseToMidiEventState::default(),
deduced_chord_name: String::new(),

top_bar: TopBar::new(),
}
Expand Down Expand Up @@ -157,6 +160,24 @@ impl PlayingScene {
}
}

fn update_chord_identifier(&mut self, enabled: bool) {
if !enabled {
return;
}

let start = self.keyboard.layout().range.start();
let notes = self
.keyboard
.key_states()
.iter()
.enumerate()
.filter(|(_, state)| state.pressed_by_user().is_some())
.map(|(id, _)| id as u8 + start)
.collect::<Vec<_>>();

self.deduced_chord_name = super::freeplay::chords::deduce_name(&notes).unwrap_or_default();
}

#[profiling::function]
fn update_midi_player(&mut self, ctx: &Context, delta: Duration) -> f32 {
if self.top_bar.is_looper_active() && self.player.time() > self.top_bar.loop_end_timestamp()
Expand Down Expand Up @@ -209,6 +230,7 @@ impl Scene for PlayingScene {
);
self.keyboard
.update(&mut self.quad_renderer_fg, &mut self.text_renderer);
self.update_chord_identifier(ctx.config.chord_identifier());
if let Some(note_labels) = self.note_labels.as_mut() {
note_labels.update(
ctx.window_state.physical_size,
Expand All @@ -223,6 +245,16 @@ impl Scene for PlayingScene {

TopBar::update(self, ctx);

if ctx.config.chord_identifier() {
nuon::label()
.text(&self.deduced_chord_name)
.font_size(25.0)
.y(self.keyboard.pos().y - 35.0)
.height(25.0)
.width(ctx.window_state.logical_size.width)
.build(&mut self.nuon);
}

super::render_nuon(&mut self.nuon, &mut self.nuon_renderer, ctx);

self.quad_renderer_bg.prepare();
Expand Down
40 changes: 40 additions & 0 deletions neothesia/src/scene/playing_scene/top_bar/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -103,6 +103,7 @@ impl TopBar {
Self::panel_left(this, ctx, ui);
Self::panel_center(this, ctx, ui);
Self::panel_right(this, ctx, ui);
Self::settings_panel(this, ctx, ui);

// ProggressBar
nuon::translate().y(30.0).build(ui, |ui| {
Expand Down Expand Up @@ -217,6 +218,45 @@ impl TopBar {
});
}

fn settings_panel(this: &mut PlayingScene, ctx: &mut Context, ui: &mut nuon::Ui) {
let width = 280.0;
let offset = this
.top_bar
.settings_animation
.animate_bool(0.0, width, ctx.frame_timestamp);

nuon::translate()
.x(ctx.window_state.logical_size.width - offset)
.y(75.0)
.build(ui, |ui| {
// Gap
nuon::translate().y(5.0).add_to_current(ui);

nuon::quad()
.size(width, 100.0)
.color([37, 35, 42])
.border_radius([10.0, 0.0, 0.0, 10.0])
.build(ui);

nuon::translate().x(15.0).build(ui, |ui| {
nuon::settings_section("Display").width(width - 30.0).build(
ui,
|ui, rows, _| {
if nuon::settings_row_toggler()
.title("Chord Identifier")
.subtitle("Display chord above keyboard")
.value(ctx.config.chord_identifier())
.build(ui, rows)
{
ctx.config
.set_chord_identifier(!ctx.config.chord_identifier());
}
},
);
});
});
}

fn proggress_bar(this: &mut PlayingScene, ctx: &mut Context, ui: &mut nuon::Ui) {
let h = 45.0;
let w = ctx.window_state.logical_size.width;
Expand Down