mod chords;
use crate::error::{check_count, check_range};
use crate::midi::{note_period, velocity_gain, MidiEvent};
use crate::numeric::fixed::{Q12, Q14, Q6};
use crate::scale::PitchClass;
use crate::{Error, SampleRate};
use chords::SCALE_TONES;
pub const HARMONY_VOICES: usize = 4;
const GAIN_ATTACK: Q12 = Q12::from_bits(163);
const GAIN_RELEASE: Q12 = Q12::from_bits(81);
#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)]
#[cfg_attr(
all(feature = "wasm", target_arch = "wasm32"),
derive(serde::Serialize, serde::Deserialize, tsify::Tsify),
serde(rename_all = "camelCase")
)]
pub enum HarmonySource {
FixedInterval,
SmartInterval,
ChordByDegree,
ChordByName,
ChordViaMidi,
MidiOmni,
#[default]
MidiChannels,
}
#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)]
#[repr(u8)]
#[cfg_attr(
all(feature = "wasm", target_arch = "wasm32"),
derive(serde::Serialize, serde::Deserialize, tsify::Tsify),
serde(rename_all = "camelCase")
)]
pub enum HarmonyScale {
#[default]
Major,
MajorWithoutSeventh,
MajorFlatSeventh,
Minor,
HarmonicMinor,
MinorNaturalSixth,
MinorMajorSeventh,
}
#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)]
#[repr(u8)]
#[cfg_attr(
all(feature = "wasm", target_arch = "wasm32"),
derive(serde::Serialize, serde::Deserialize, tsify::Tsify),
serde(rename_all = "camelCase")
)]
pub enum ChordName {
#[default]
Major,
MajorSixth,
MajorSeventh,
MajorSeventhSuspendedFourth,
Minor,
MinorSixth,
MinorSeventh,
MinorSeventhFlatFifth,
Diminished,
DiminishedSeventh,
DominantSeventh,
DominantSeventhFlatFifth,
Augmented,
AugmentedSeventh,
SuspendedFourth,
SuspendedSecond,
SuspendedSeventh,
}
#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)]
#[repr(u8)]
#[cfg_attr(
all(feature = "wasm", target_arch = "wasm32"),
derive(serde::Serialize, serde::Deserialize, tsify::Tsify),
serde(rename_all = "camelCase")
)]
pub enum ChordDegree {
#[default]
I,
Ii,
IiSeventh,
Iii,
IiiSeventh,
Iv,
IvSeventh,
V,
VSeventh,
Vi,
ViSeventh,
Vii,
ViiSeventh,
}
#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)]
#[repr(u8)]
#[cfg_attr(
all(feature = "wasm", target_arch = "wasm32"),
derive(serde::Serialize, serde::Deserialize, tsify::Tsify),
serde(rename_all = "camelCase")
)]
pub enum Inversion {
#[default]
Root,
First,
Second,
}
#[derive(Clone, Copy, Debug, PartialEq)]
#[non_exhaustive]
#[cfg_attr(
all(feature = "wasm", target_arch = "wasm32"),
derive(serde::Serialize, serde::Deserialize, tsify::Tsify),
serde(rename_all = "camelCase")
)]
pub struct HarmonyConfig {
pub source: HarmonySource,
pub key: PitchClass,
pub scale: HarmonyScale,
#[cfg_attr(
all(feature = "wasm", target_arch = "wasm32"),
tsify(type = "[number, number, number, number]")
)]
pub chromatic_intervals: [i8; HARMONY_VOICES],
#[cfg_attr(
all(feature = "wasm", target_arch = "wasm32"),
tsify(type = "[number, number, number, number]")
)]
pub diatonic_intervals: [i8; HARMONY_VOICES],
pub chord: ChordName,
pub degree: ChordDegree,
pub inversion: Inversion,
pub register_percent: f32,
pub voicing_spread_percent: f32,
pub portamento_ms: f32,
#[cfg_attr(
all(feature = "wasm", target_arch = "wasm32"),
serde(default = "default_voices"),
tsify(optional)
)]
pub voices: u8,
}
#[cfg(all(feature = "wasm", target_arch = "wasm32"))]
fn default_voices() -> u8 {
HARMONY_VOICES as u8
}
impl Default for HarmonyConfig {
fn default() -> Self {
Self {
source: HarmonySource::MidiChannels,
key: PitchClass::C,
scale: HarmonyScale::Major,
chromatic_intervals: [0; HARMONY_VOICES],
diatonic_intervals: [0; HARMONY_VOICES],
chord: ChordName::Major,
degree: ChordDegree::I,
inversion: Inversion::Root,
register_percent: 50.0,
voicing_spread_percent: 0.0,
portamento_ms: 0.0,
voices: HARMONY_VOICES as u8,
}
}
}
impl HarmonyConfig {
pub fn validate(self) -> Result<(), Error> {
for interval in self.chromatic_intervals {
let field = "resynthesis.harmony.chromatic_intervals";
check_count(field, i64::from(interval), -24, 24)?;
}
for interval in self.diatonic_intervals {
let field = "resynthesis.harmony.diatonic_intervals";
check_count(field, i64::from(interval), -14, 14)?;
}
let field = "resynthesis.harmony.voices";
check_count(field, i64::from(self.voices), 0, HARMONY_VOICES as i64)?;
let field = "resynthesis.harmony.register_percent";
check_range(field, self.register_percent, 0.0, 100.0)?;
let field = "resynthesis.harmony.voicing_spread_percent";
check_range(field, self.voicing_spread_percent, 0.0, 100.0)?;
check_range(
"resynthesis.harmony.portamento_ms",
self.portamento_ms,
0.0,
10_000.0,
)
}
}
pub(crate) struct HarmonyState {
rate: f32,
config: HarmonyConfig,
midi_note: [i32; HARMONY_VOICES],
midi_period: [Q6; HARMONY_VOICES],
midi_gain: [Q12; HARMONY_VOICES],
midi_order: [i32; HARMONY_VOICES],
portamento_period: [Q6; HARMONY_VOICES],
current_ratio: [Q14; HARMONY_VOICES],
smoothed_gain: [Q12; HARMONY_VOICES],
period_output: [Q6; HARMONY_VOICES],
target_period: [Q6; HARMONY_VOICES],
chord_period: [Q6; HARMONY_VOICES],
chord_gain: [Q12; HARMONY_VOICES],
fixed_interval: [Q14; HARMONY_VOICES],
scale_periods: [[Q6; 79]; HARMONY_VOICES],
scale_targets: [[Q6; 78]; HARMONY_VOICES],
current_idx: [usize; HARMONY_VOICES],
scale_target: [Q6; HARMONY_VOICES],
scale_note: [i32; HARMONY_VOICES],
porta_decay: Q14,
pending_gain: [Q12; 128],
pending_order: [i32; 128],
event_counter: i32,
gains: [Q12; 128],
}
impl HarmonyState {
pub(crate) fn new(rate: SampleRate, config: HarmonyConfig) -> Self {
let mut result = Self {
rate: rate.hz(),
config,
midi_note: [0; HARMONY_VOICES],
midi_period: [Q6::ZERO; HARMONY_VOICES],
midi_gain: [Q12::ZERO; HARMONY_VOICES],
midi_order: [0; HARMONY_VOICES],
portamento_period: [Q6::ZERO; HARMONY_VOICES],
current_ratio: [Q14::ONE; HARMONY_VOICES],
smoothed_gain: [Q12::ZERO; HARMONY_VOICES],
period_output: [Q6::ZERO; HARMONY_VOICES],
target_period: [Q6::ZERO; HARMONY_VOICES],
chord_period: [Q6::ZERO; HARMONY_VOICES],
chord_gain: [Q12::from_bits(-16_384); HARMONY_VOICES],
fixed_interval: [Q14::ONE; HARMONY_VOICES],
scale_periods: [[Q6::ZERO; 79]; HARMONY_VOICES],
scale_targets: [[Q6::ZERO; 78]; HARMONY_VOICES],
current_idx: [39; HARMONY_VOICES],
scale_target: [Q6::ZERO; HARMONY_VOICES],
scale_note: [0; HARMONY_VOICES],
porta_decay: Q14::ZERO,
pending_gain: [Q12::ZERO; 128],
pending_order: [0; 128],
event_counter: 0,
gains: std::array::from_fn(|i| velocity_gain(i as u8)),
};
result.prepare_intervals();
result.setup_chord();
result
}
pub(crate) fn apply_config(&mut self, config: HarmonyConfig) {
let old = self.config;
self.config = config;
if old.source != config.source
&& matches!(
config.source,
HarmonySource::FixedInterval
| HarmonySource::SmartInterval
| HarmonySource::ChordByDegree
| HarmonySource::ChordByName
)
{
self.reset();
}
if old.key != config.key
|| old.scale != config.scale
|| old.diatonic_intervals != config.diatonic_intervals
{
for channel in 0..HARMONY_VOICES {
self.make_scale_periods(channel);
}
}
if old.chromatic_intervals != config.chromatic_intervals {
self.prepare_fixed_intervals();
}
if old.portamento_ms != config.portamento_ms {
self.prepare_portamento();
}
if old != config {
self.setup_chord();
}
}
pub(crate) fn reset(&mut self) {
self.midi_period.fill(Q6::ZERO);
self.portamento_period.fill(Q6::ZERO);
self.pending_gain.fill(Q12::ZERO);
self.pending_order.fill(0);
self.event_counter = 0;
if self.config.source == HarmonySource::ChordViaMidi {
self.setup_chord();
}
}
fn cents_period(&self, cents: f64) -> Q6 {
let period = ((f64::from(self.rate) / 440.0)
* ((900.0 - cents) * std::f64::consts::LN_2 / 1200.0).exp())
as f32;
Q6::from_f64(f64::from(period))
}
fn prepare_fixed_intervals(&mut self) {
self.fixed_interval = std::array::from_fn(|i| {
Q14::from_f64(
(f64::from(self.config.chromatic_intervals[i]) * std::f64::consts::LN_2 / 12.0)
.exp(),
)
});
}
fn prepare_portamento(&mut self) {
self.porta_decay = if self.config.portamento_ms == 0.0 {
Q14::ZERO
} else {
let param = f64::from(self.config.portamento_ms) * 5.0;
Q14::from_f64(
(-std::f64::consts::LN_2 / (f64::from(self.rate / 100.0) * (param / 5000.0))).exp(),
)
};
}
fn prepare_intervals(&mut self) {
self.prepare_fixed_intervals();
self.prepare_portamento();
for i in 0..HARMONY_VOICES {
self.make_scale_periods(i);
}
}
fn chromatic_to_degree(&self, note: i32) -> i32 {
let relative = note - i32::from(self.config.key.semitone());
let table = match self.config.scale {
HarmonyScale::Major
| HarmonyScale::MajorWithoutSeventh
| HarmonyScale::MajorFlatSeventh => [1, 1, 2, 2, 3, 4, 4, 5, 5, 6, 7, 7],
HarmonyScale::Minor
| HarmonyScale::HarmonicMinor
| HarmonyScale::MinorNaturalSixth
| HarmonyScale::MinorMajorSeventh => [1, 1, 2, 3, 3, 4, 4, 5, 6, 6, 7, 7],
};
relative.div_euclid(12) * 7 + table[relative.rem_euclid(12) as usize]
}
fn degree_to_chromatic(&self, degree: i32) -> i32 {
let zero = degree - 1;
zero.div_euclid(7) * 12
+ SCALE_TONES[self.config.scale as usize][zero.rem_euclid(7) as usize]
+ i32::from(self.config.key.semitone())
}
fn make_scale_periods(&mut self, channel: usize) {
let interval = i32::from(self.config.diatonic_intervals[channel]);
for i in 0..78 {
let cents = 3600 - i as i32 * 100;
let note = 36 - i as i32;
self.scale_periods[channel][i] = self.cents_period(f64::from(cents) + 50.0);
let diff = if interval % 7 == 0 {
interval / 7 * 1200
} else {
(self.degree_to_chromatic(self.chromatic_to_degree(note) + interval) - note) * 100
};
self.scale_targets[channel][i] = self.cents_period(f64::from(cents + diff));
}
self.scale_periods[channel][0] = Q6::ZERO;
self.scale_periods[channel][78] = Q6::from_bits(i32::MAX);
self.current_idx[channel] = 39;
}
#[inline(always)]
fn find_scale_period(&mut self, period: Q6, channel: usize) -> (Q6, i32) {
let mut index = self.current_idx[channel];
while index < 77 && self.scale_periods[channel][index + 1] < period {
index += 1;
}
while index > 0 && self.scale_periods[channel][index] > period {
index -= 1;
}
if self.scale_targets[channel][index] != Q6::ZERO {
self.scale_target[channel] = self.scale_targets[channel][index];
}
self.current_idx[channel] = index;
(
if self.scale_target[channel] == Q6::ZERO {
period
} else {
self.scale_target[channel]
},
index as i32,
)
}
pub(crate) fn apply_validated_midi(&mut self, event: MidiEvent) -> bool {
let (channel, note, velocity) = match event {
MidiEvent::AllNotesOff => {
self.reset();
return false;
}
MidiEvent::NoteOn {
channel,
note,
velocity,
} => (channel, usize::from(note), i32::from(velocity)),
MidiEvent::NoteOff { channel, note } => (channel, usize::from(note), 0),
};
self.event_counter = self.event_counter.wrapping_add(1);
let gain = self.gains[velocity as usize];
if velocity == 0 {
self.pending_gain[note] = Q12::ZERO;
self.pending_order[note] = 0;
}
if matches!(
self.config.source,
HarmonySource::MidiOmni | HarmonySource::ChordViaMidi
) {
if velocity == 0 {
let mut released = None;
for voice in 0..HARMONY_VOICES {
if self.midi_period[voice] != Q6::ZERO && self.midi_note[voice] == note as i32 {
self.midi_period[voice] = Q6::ZERO;
released = Some(voice);
}
}
if let Some(voice) = released {
let mut best_order = 0;
let mut best = None;
for pending in 0..128 {
if self.pending_order[pending] > best_order {
best_order = self.pending_order[pending];
best = Some(pending);
}
}
if let Some(pending) = best {
self.midi_period[voice] = note_period(self.rate, pending as i32);
self.midi_note[voice] = pending as i32;
self.midi_gain[voice] = self.pending_gain[pending];
self.midi_order[voice] = self.pending_order[pending];
self.pending_gain[pending] = Q12::ZERO;
self.pending_order[pending] = 0;
}
}
if self.config.source == HarmonySource::ChordViaMidi {
self.setup_chord();
}
return true;
}
for voice in 0..HARMONY_VOICES {
if self.midi_period[voice] != Q6::ZERO && self.midi_note[voice] == note as i32 {
self.midi_gain[voice] = gain;
self.midi_order[voice] = self.event_counter;
if self.config.source == HarmonySource::ChordViaMidi {
self.setup_chord();
}
return false;
}
}
let voice = if let Some(free) =
(0..HARMONY_VOICES).find(|&v| self.midi_period[v] == Q6::ZERO)
{
self.portamento_period[free] = Q6::ZERO;
free
} else {
let mut oldest = 0;
for i in 1..4 {
if self.midi_order[i] < self.midi_order[oldest] {
oldest = i;
}
}
let stolen = self.midi_note[oldest] as usize;
self.pending_gain[stolen] = self.midi_gain[oldest];
self.pending_order[stolen] = self.midi_order[oldest];
oldest
};
self.assign(voice, note, gain);
if self.config.source == HarmonySource::ChordViaMidi {
self.setup_chord();
}
return true;
}
if self.config.source == HarmonySource::MidiChannels && channel < 4 {
let voice = usize::from(channel);
if velocity == 0 {
if self.midi_period[voice] != Q6::ZERO && self.midi_note[voice] == note as i32 {
self.midi_period[voice] = Q6::ZERO;
}
} else {
if self.midi_period[voice] == Q6::ZERO {
self.portamento_period[voice] = Q6::ZERO;
}
self.assign(voice, note, gain);
return true;
}
}
false
}
fn assign(&mut self, voice: usize, note: usize, gain: Q12) {
self.midi_period[voice] = note_period(self.rate, note as i32);
self.midi_note[voice] = note as i32;
self.midi_gain[voice] = gain;
self.midi_order[voice] = self.event_counter;
}
pub(crate) fn advance_voice(&mut self, period: Q6, voice: usize) -> (Q14, Q12) {
if period == Q6::ZERO {
self.portamento_period[voice] = Q6::ZERO;
return (Q14::ONE, Q12::ZERO);
}
let target_gain = self.select_target(period, voice);
if self.smoothed_gain[voice] < target_gain {
self.smoothed_gain[voice] = (self.smoothed_gain[voice] + GAIN_ATTACK).min(target_gain);
} else if self.smoothed_gain[voice] > target_gain {
self.smoothed_gain[voice] = (self.smoothed_gain[voice] - GAIN_RELEASE).max(target_gain);
}
if self.target_period[voice] != Q6::ZERO {
self.period_output[voice] = self.target_period[voice];
} else if self.smoothed_gain[voice] == Q12::ZERO {
self.period_output[voice] = Q6::ZERO;
self.portamento_period[voice] = Q6::ZERO;
}
if self.period_output[voice] > Q6::ZERO {
self.portamento_period[voice] = if self.portamento_period[voice] == Q6::ZERO {
self.period_output[voice]
} else {
self.porta_decay.mul(self.portamento_period[voice])
+ (Q14::ONE - self.porta_decay).mul(self.period_output[voice])
};
if self.portamento_period[voice] > Q6::ZERO {
self.current_ratio[voice] = period.div(self.portamento_period[voice]);
}
}
(self.current_ratio[voice], self.smoothed_gain[voice])
}
fn select_target(&mut self, period: Q6, voice: usize) -> Q12 {
match self.config.source {
HarmonySource::FixedInterval | HarmonySource::SmartInterval
if voice >= self.config.voices as usize =>
{
self.target_period[voice] = Q6::ZERO;
Q12::ZERO
}
HarmonySource::FixedInterval | HarmonySource::SmartInterval => {
let previous = self.scale_note[voice];
let (target, note) = self.find_scale_period(period, voice);
self.scale_note[voice] = note;
self.target_period[voice] = if self.config.source == HarmonySource::FixedInterval {
period.div(self.fixed_interval[voice])
} else {
target
};
if (previous - self.scale_note[voice]).abs() > 1 {
self.portamento_period[voice] = Q6::ZERO;
}
Q12::ONE
}
HarmonySource::ChordByDegree | HarmonySource::ChordByName => {
self.target_period[voice] = self.chord_period[voice];
if self.target_period[voice] != Q6::ZERO {
Q12::ONE
} else {
Q12::ZERO
}
}
HarmonySource::ChordViaMidi => {
self.target_period[voice] = self.chord_period[voice];
if self.target_period[voice] != Q6::ZERO {
self.chord_gain[voice]
} else {
Q12::ZERO
}
}
HarmonySource::MidiOmni | HarmonySource::MidiChannels => {
self.target_period[voice] = self.midi_period[voice];
if self.target_period[voice] != Q6::ZERO {
self.midi_gain[voice]
} else {
Q12::ZERO
}
}
}
}
fn setup_chord(&mut self) {
let (tones, gains, count) = chords::voice_chord(
&self.config,
self.midi_note,
self.midi_period,
self.midi_gain,
);
if count == 0 {
self.chord_period.fill(Q6::ZERO);
return;
}
for i in 0..count {
self.chord_gain[3 - i] = gains[i];
self.chord_period[3 - i] = self.cents_period(f64::from(tones[i]) * 100.0);
}
for i in count..4 {
self.chord_period[3 - i] = Q6::ZERO;
self.chord_gain[3 - i] = -Q12::ONE;
}
}
}