Skip to content
Open
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
5 changes: 5 additions & 0 deletions Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -59,6 +59,11 @@ codegen-units = 1 # better optimizations
debug = true # symbols are nice and they don't increase the size in flash
lto = true # better optimizations
opt-level = "s" # optimize for binary size
# This firmware is within ~1 KB of the STM32H750's 128 KB internal flash limit.
# Keep debug symbols, but omit runtime assertion/overflow machinery just like the
# release image so `cargo build` and the default probe-rs runner still fit.
debug-assertions = false
overflow-checks = false

[profile.release]
codegen-units = 1 # better optimizations
Expand Down
46 changes: 39 additions & 7 deletions src/handler.rs
Original file line number Diff line number Diff line change
Expand Up @@ -29,8 +29,15 @@ pub fn audio_handler(
let mut sr_factor = 1;
let mut wave_type = Waveform::Sine;
let mut bit_depth = 32;
let mut volume_gain = 1.0f32;
let mut waveform_compensation = 1.0f32;
// In Vocode / Harmony the melody channel (ch 1) drives the vocal effect only —
// its notes are silenced as raw synth so you hear the processed voice, not a
// synth playing over it. Every other mode plays the note sound normally.
let mut mute_melody_synth = false;
let mut voice_gain = 1.0f32;
let mut melody_gain = 1.0f32;
let mut drum_gain = 1.0f32;
let mut master_gain = 1.0f32;
shared.app_state_machine.lock(|msm| {
let snapshot = msm.snapshot();
sr_factor = snapshot.sample_reduction;
Expand All @@ -41,7 +48,6 @@ pub fn audio_handler(
_ => Waveform::Sine,
};
bit_depth = snapshot.bit_rate;
volume_gain = snapshot.volume as f32 / 10.0;
// Sine and triangle are perceptually quieter than saw/square at the same
// peak amplitude — they're spectrally pure, while saw/square spread energy
// across many harmonics, which the ear perceives as louder. Compensate so
Expand All @@ -53,6 +59,18 @@ pub fn audio_handler(
Waveform::Triangle => 1.2,
Waveform::Square | Waveform::Saw => 1.0,
};
let profile = match snapshot.current_state {
AppState::Processing(p) | AppState::EffectsProfile(p) | AppState::Menu(_, p) => p,
AppState::Splash => ProcessingProfile::PitchControl,
};
mute_melody_synth = matches!(
profile,
ProcessingProfile::Vocode | ProcessingProfile::Harmony
);
voice_gain = snapshot.voice_volume as f32 * 0.1;
melody_gain = snapshot.melody_volume as f32 * 0.1;
drum_gain = snapshot.drum_volume as f32 * 0.1;
master_gain = snapshot.volume as f32 * 0.1;
});

if audio.get_stereo(buffer) {
Expand Down Expand Up @@ -172,11 +190,14 @@ pub fn audio_handler(
// Get MIDI sample and mix it with the processed audio. Waveform
// compensation applies only to the synthesized note, not the live
// audio-in signal already folded into out_sample above.
let midi_sample = shared.voice_manager.lock(|vm| vm.get_mixed_sample());
out_sample = out_sample * 0.75 + (midi_sample * waveform_compensation) * 0.1;
let midi_sample = shared
.voice_manager
.lock(|vm| vm.get_mixed_sample(mute_melody_synth, melody_gain, drum_gain));
out_sample = (out_sample * voice_gain + (midi_sample * waveform_compensation) * 0.1)
* master_gain;

// Normalize final output
out_sample = normalize_sample(out_sample, 0.8) * volume_gain;
out_sample = normalize_sample(out_sample, 0.8);
// **********************************************

// Check and handle hop counter
Expand Down Expand Up @@ -218,7 +239,7 @@ pub fn audio_handler(
}

pub fn interface_handler(
local: crate::rtic_app::app::interface_handler::LocalResources,
local: &mut crate::rtic_app::app::interface_handler::LocalResources,
shared: &mut crate::rtic_app::app::interface_handler::SharedResources,
) {
local.timer2.clear_irq();
Expand Down Expand Up @@ -473,12 +494,23 @@ pub fn handle_vocal_effects(
}
}

// In Pitch Control the voice must always autotune to the musical scale, exactly
// as it does with no note held. MIDI notes still play the synth (that mixing
// happens elsewhere), but they must not become the pitch-correction target —
// otherwise singing while playing force-tunes your voice to the held note. Other
// modes keep the live frequencies (Vocode/Harmony are driven by them).
let effect_midi_frequencies = if mode == ProcessingMode::PitchControl {
[0.0; 8]
} else {
midi_frequencies
};

let musical_settings = MusicalSettings {
formant,
formant_male_ratio,
formant_female_ratio,
note,
midi_frequencies,
midi_frequencies: effect_midi_frequencies,
key,
octave_ratio: pitch_ratio,
mode,
Expand Down
1 change: 1 addition & 0 deletions src/lib.rs
Original file line number Diff line number Diff line change
@@ -1,2 +1,3 @@
#![no_std]
pub mod settings_storage;
pub mod state_machine;
37 changes: 35 additions & 2 deletions src/main.rs
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@ mod display;
mod handler;
mod input;
mod midi;
mod settings_storage;
mod state_machine;
mod types;

Expand Down Expand Up @@ -105,6 +106,7 @@ mod rtic_app {
#[local]
struct Local {
audio: audio::Audio,
flash: libdaisy::flash::Flash,
buffer: audio::AudioBuffer,
timer2: Timer<stm32::TIM2>,
knob_1: Knob,
Expand Down Expand Up @@ -141,6 +143,7 @@ mod rtic_app {
let device = ctx.device;
let ccdr = system::System::init_clocks(device.PWR, device.RCC, &device.SYSCFG);
let mut system = libdaisy::system_init!(core, device, ccdr, BLOCK_SIZE);
let saved_volumes = crate::settings_storage::load(&mut system.flash);

let buffer = [(0.0, 0.0); audio::BLOCK_SIZE_MAX];

Expand Down Expand Up @@ -321,20 +324,27 @@ mod rtic_app {
info!("Startup done!! yo!");
startup_complete_task::spawn().ok();

let mut app_state_machine = AppStateMachine::new();
if let Some(volumes) = saved_volumes {
app_state_machine.apply_volume_settings(volumes);
info!("restored mixer volumes");
}

(
Shared {
in_ring: RingBuffer::new(),
out_ring: RingBuffer::with_offset((FFT_SIZE + (2 * HOP_SIZE)) as u32),
previous_pitch_shift_ratio: 1.0,
in_pointer_cached: 0,
app_state_machine: AppStateMachine::new(),
app_state_machine,
old_matrix_state: [[false; 3]; 4],
display_needs_update: true,
midi_events: heapless::spsc::Queue::new(),
voice_manager: VoiceManager::new(SAMPLE_RATE),
},
Local {
audio: system.audio,
flash: system.flash,
buffer,
timer2,
knob_1,
Expand Down Expand Up @@ -515,6 +525,8 @@ mod rtic_app {
row_3_pin,
row_4_pin,
encoder_button,
flash,
save_countdown: u16 = 0,
],
shared = [
app_state_machine,
Expand All @@ -525,7 +537,28 @@ mod rtic_app {
priority = 3
)]
fn interface_handler(mut ctx: interface_handler::Context) {
crate::handler::interface_handler(ctx.local, &mut ctx.shared);
let before = ctx
.shared
.app_state_machine
.lock(|state| state.volume_settings());
crate::handler::interface_handler(&mut ctx.local, &mut ctx.shared);
let after = ctx
.shared
.app_state_machine
.lock(|state| state.volume_settings());
if after != before {
*ctx.local.save_countdown = 2000;
} else if *ctx.local.save_countdown > 0 {
*ctx.local.save_countdown -= 1;
if *ctx.local.save_countdown == 0 {
if crate::settings_storage::save(ctx.local.flash, after) {
info!("mixer volumes saved");
} else {
log::warn!("mixer volume save failed");
*ctx.local.save_countdown = 2000;
}
}
}
}

/// FFT TASK
Expand Down
48 changes: 42 additions & 6 deletions src/midi/voice_generator.rs
Original file line number Diff line number Diff line change
Expand Up @@ -114,7 +114,14 @@ impl HybridVoice {
self.note = None;
self.velocity = 0;
}
sample * vel_scale * 0.9
// Compress velocity into 0.5..1.0 so soft hits stay audible while
// hard hits still accent — a linear 0..1 curve made low-velocity
// drums vanish under the sustained synth.
let drum_vel = 0.5 + 0.5 * (self.velocity as f32 * (1.0 / 127.0));
// Drum makeup gain > 1.0: percussion is a short transient, so it
// needs a higher peak than a sustained synth note to feel equally
// loud. (Was 0.75, then 1.4.)
sample * drum_vel * 2.5
}
}
}
Expand All @@ -135,13 +142,18 @@ impl HybridVoice {
/// MIDI channel routing (0-indexed):
/// 0 (MIDI ch 1) — voice effects only, no audio
/// 1 (MIDI ch 2) — audio only, no voice effects
/// 2 (MIDI ch 3) — audio only, no voice effects
/// 2 (MIDI ch 3) — auto-switch: plays audio normally, but in Vocode/Harmony it
/// goes silent-as-synth and drives the vocal effect instead
/// 9 (MIDI ch 10) — drums, no voice effects
const VOICE_CTRL_CHANNEL: u8 = 0;

/// The auto-switch channel (MIDI ch 3). Feeds the vocal-effect frequency cache and
/// is silenced as raw synth in Vocode/Harmony (see `get_mixed_sample`).
const AUTO_SWITCH_CHANNEL: u8 = 2;

#[inline(always)]
fn is_sound_only_channel(channel: u8) -> bool {
channel == 1 || channel == 2 || channel == 9
channel == 1 || channel == 9
}

pub struct VoiceManager<const MAX_VOICES: usize> {
Expand Down Expand Up @@ -193,6 +205,8 @@ impl<const MAX_VOICES: usize> VoiceManager<MAX_VOICES> {
};

// Whether this channel feeds the vocal effects frequency cache.
// Sound-only channels and drum-map note numbers must not retune the
// frequency cache used by Pitch Control / Harmony / Vocode.
let update_cache = !is_sound_only_channel(channel);

// 1. Check for retrigger
Expand Down Expand Up @@ -266,16 +280,38 @@ impl<const MAX_VOICES: usize> VoiceManager<MAX_VOICES> {
}
}

/// Mix all active voices. When `mute_melody_synth` is set (Vocode / Harmony
/// profiles), pitched synth voices on the auto-switch channel (MIDI ch 3) stay
/// silent — they still track pitch for the vocal-effect frequency cache, so
/// you hear your processed voice instead of a raw synth on top. Drums and any
/// other channel still sound.
#[inline(always)]
pub fn get_mixed_sample(&mut self) -> f32 {
pub fn get_mixed_sample(
&mut self,
mute_melody_synth: bool,
melody_gain: f32,
drum_gain: f32,
) -> f32 {
let mut sum = 0.0f32;
let mut count = 0usize;

for voice in &mut self.voices {
let s = voice.get_sample();
if s != 0.0 {
sum += s;
count += 1;
if mute_melody_synth
&& voice.channel == AUTO_SWITCH_CHANNEL
&& voice.type_id() == VoiceTypeId::Synth
{
continue;
}
let scaled = match voice.type_id() {
VoiceTypeId::Synth => s * melody_gain,
VoiceTypeId::Drum => s * drum_gain,
};
if scaled != 0.0 {
sum += scaled;
count += 1;
}
}
}

Expand Down
Loading
Loading