Merge #388
388: Feature/scan mode r=jordens a=ryan-summers This PR is to add visibility on design decisions for the scan mode implementation for #86 This PR: * Adds a signal generator for sinusoids, triangular waves, and square waves to both channels of `dual-iir` Testing: The new signal generator was scanned across 0-100% symmetry for all waveform types using frequencies of 500-1KHz. It was observed on an oscilloscope to contain nominal, well-formed outputs. Co-authored-by: Ryan Summers <ryan.summers@vertigo-designs.com>
This commit is contained in:
commit
929a7611d6
|
@ -29,7 +29,10 @@
|
|||
#![no_std]
|
||||
#![no_main]
|
||||
|
||||
use core::sync::atomic::{fence, Ordering};
|
||||
use core::{
|
||||
convert::TryInto,
|
||||
sync::atomic::{fence, Ordering},
|
||||
};
|
||||
|
||||
use mutex_trait::prelude::*;
|
||||
|
||||
|
@ -42,6 +45,7 @@ use stabilizer::{
|
|||
dac::{Dac0Output, Dac1Output, DacCode},
|
||||
embedded_hal::digital::v2::InputPin,
|
||||
hal,
|
||||
signal_generator::{self, SignalGenerator},
|
||||
system_timer::SystemTimer,
|
||||
DigitalInput0, DigitalInput1, AFE0, AFE1,
|
||||
},
|
||||
|
@ -119,6 +123,17 @@ pub struct Settings {
|
|||
/// # Value
|
||||
/// See [StreamTarget#miniconf]
|
||||
stream_target: StreamTarget,
|
||||
|
||||
/// Specifies the config for signal generators to add on to DAC0/DAC1 outputs.
|
||||
///
|
||||
/// # Path
|
||||
/// `signal_generator/<n>`
|
||||
///
|
||||
/// * <n> specifies which channel to configure. <n> := [0, 1]
|
||||
///
|
||||
/// # Value
|
||||
/// See [signal_generator::BasicConfig#miniconf]
|
||||
signal_generator: [signal_generator::BasicConfig; 2],
|
||||
}
|
||||
|
||||
impl Default for Settings {
|
||||
|
@ -139,6 +154,8 @@ impl Default for Settings {
|
|||
// The default telemetry period in seconds.
|
||||
telemetry_period: 10,
|
||||
|
||||
signal_generator: [signal_generator::BasicConfig::default(); 2],
|
||||
|
||||
stream_target: StreamTarget::default(),
|
||||
}
|
||||
}
|
||||
|
@ -153,6 +170,7 @@ const APP: () = {
|
|||
dacs: (Dac0Output, Dac1Output),
|
||||
network: NetworkUsers<Settings, Telemetry>,
|
||||
generator: BlockGenerator,
|
||||
signal_generator: [SignalGenerator; 2],
|
||||
|
||||
settings: Settings,
|
||||
telemetry: TelemetryBuffer,
|
||||
|
@ -193,6 +211,8 @@ const APP: () = {
|
|||
// Start sampling ADCs.
|
||||
stabilizer.adc_dac_timer.start();
|
||||
|
||||
let settings = Settings::default();
|
||||
|
||||
init::LateResources {
|
||||
afes: stabilizer.afes,
|
||||
adcs: stabilizer.adcs,
|
||||
|
@ -201,7 +221,15 @@ const APP: () = {
|
|||
network,
|
||||
digital_inputs: stabilizer.digital_inputs,
|
||||
telemetry: TelemetryBuffer::default(),
|
||||
settings: Settings::default(),
|
||||
settings,
|
||||
signal_generator: [
|
||||
SignalGenerator::new(
|
||||
settings.signal_generator[0].try_into().unwrap(),
|
||||
),
|
||||
SignalGenerator::new(
|
||||
settings.signal_generator[1].try_into().unwrap(),
|
||||
),
|
||||
],
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -221,7 +249,7 @@ const APP: () = {
|
|||
///
|
||||
/// Because the ADC and DAC operate at the same rate, these two constraints actually implement
|
||||
/// the same time bounds, meeting one also means the other is also met.
|
||||
#[task(binds=DMA1_STR4, resources=[adcs, digital_inputs, dacs, iir_state, settings, telemetry, generator], priority=2)]
|
||||
#[task(binds=DMA1_STR4, resources=[adcs, digital_inputs, dacs, iir_state, settings, signal_generator, telemetry, generator], priority=2)]
|
||||
#[inline(never)]
|
||||
#[link_section = ".itcm.process"]
|
||||
fn process(mut c: process::Context) {
|
||||
|
@ -233,6 +261,7 @@ const APP: () = {
|
|||
ref mut iir_state,
|
||||
ref mut telemetry,
|
||||
ref mut generator,
|
||||
ref mut signal_generator,
|
||||
} = c.resources;
|
||||
|
||||
let digital_inputs = [
|
||||
|
@ -255,7 +284,8 @@ const APP: () = {
|
|||
adc_samples[channel]
|
||||
.iter()
|
||||
.zip(dac_samples[channel].iter_mut())
|
||||
.map(|(ai, di)| {
|
||||
.zip(&mut signal_generator[channel])
|
||||
.map(|((ai, di), signal)| {
|
||||
let x = f32::from(*ai as i16);
|
||||
let y = settings.iir_ch[channel]
|
||||
.iter()
|
||||
|
@ -263,9 +293,13 @@ const APP: () = {
|
|||
.fold(x, |yi, (ch, state)| {
|
||||
ch.update(state, yi, hold)
|
||||
});
|
||||
|
||||
// Note(unsafe): The filter limits must ensure that the value is in range.
|
||||
// The truncation introduces 1/2 LSB distortion.
|
||||
let y: i16 = unsafe { y.to_int_unchecked() };
|
||||
|
||||
let y = y.saturating_add(signal);
|
||||
|
||||
// Convert to DAC code
|
||||
*di = DacCode::from(y).0;
|
||||
})
|
||||
|
@ -300,7 +334,7 @@ const APP: () = {
|
|||
}
|
||||
}
|
||||
|
||||
#[task(priority = 1, resources=[network, afes, settings])]
|
||||
#[task(priority = 1, resources=[network, afes, settings, signal_generator])]
|
||||
fn settings_update(mut c: settings_update::Context) {
|
||||
// Update the IIR channels.
|
||||
let settings = c.resources.network.miniconf.settings();
|
||||
|
@ -310,6 +344,22 @@ const APP: () = {
|
|||
c.resources.afes.0.set_gain(settings.afe[0]);
|
||||
c.resources.afes.1.set_gain(settings.afe[1]);
|
||||
|
||||
// Update the signal generators
|
||||
for (i, &config) in settings.signal_generator.iter().enumerate() {
|
||||
match config.try_into() {
|
||||
Ok(config) => {
|
||||
c.resources
|
||||
.signal_generator
|
||||
.lock(|generator| generator[i].update_waveform(config));
|
||||
}
|
||||
Err(err) => log::error!(
|
||||
"Failed to update signal generation on DAC{}: {:?}",
|
||||
i,
|
||||
err
|
||||
),
|
||||
}
|
||||
}
|
||||
|
||||
let target = settings.stream_target.into();
|
||||
c.resources.network.direct_stream(target);
|
||||
}
|
||||
|
|
|
@ -28,7 +28,10 @@
|
|||
#![no_std]
|
||||
#![no_main]
|
||||
|
||||
use core::sync::atomic::{fence, Ordering};
|
||||
use core::{
|
||||
convert::TryFrom,
|
||||
sync::atomic::{fence, Ordering},
|
||||
};
|
||||
|
||||
use mutex_trait::prelude::*;
|
||||
|
||||
|
@ -40,10 +43,10 @@ use stabilizer::{
|
|||
adc::{Adc0Input, Adc1Input, AdcCode},
|
||||
afe::Gain,
|
||||
dac::{Dac0Output, Dac1Output, DacCode},
|
||||
design_parameters,
|
||||
embedded_hal::digital::v2::InputPin,
|
||||
hal,
|
||||
input_stamper::InputStamper,
|
||||
signal_generator,
|
||||
system_timer::SystemTimer,
|
||||
DigitalInput0, DigitalInput1, AFE0, AFE1,
|
||||
},
|
||||
|
@ -56,13 +59,6 @@ use stabilizer::{
|
|||
},
|
||||
};
|
||||
|
||||
// A constant sinusoid to send on the DAC output.
|
||||
// Full-scale gives a +/- 10.24V amplitude waveform. Scale it down to give +/- 1V.
|
||||
const ONE: i16 = ((1.0 / 10.24) * i16::MAX as f32) as _;
|
||||
const SQRT2: i16 = (ONE as f32 * 0.707) as _;
|
||||
const DAC_SEQUENCE: [i16; design_parameters::SAMPLE_BUFFER_SIZE] =
|
||||
[ONE, SQRT2, 0, -SQRT2, -ONE, -SQRT2, 0, SQRT2];
|
||||
|
||||
#[derive(Copy, Clone, Debug, Deserialize, Miniconf)]
|
||||
enum Conf {
|
||||
/// Output the lockin magnitude.
|
||||
|
@ -213,6 +209,7 @@ const APP: () = {
|
|||
telemetry: TelemetryBuffer,
|
||||
digital_inputs: (DigitalInput0, DigitalInput1),
|
||||
generator: BlockGenerator,
|
||||
signal_generator: signal_generator::SignalGenerator,
|
||||
|
||||
timestamper: InputStamper,
|
||||
pll: RPLL,
|
||||
|
@ -264,6 +261,23 @@ const APP: () = {
|
|||
// Enable the timestamper.
|
||||
stabilizer.timestamper.start();
|
||||
|
||||
let signal_config = {
|
||||
let frequency_tuning_word =
|
||||
(1u64 << (32 - configuration::SAMPLE_BUFFER_SIZE_LOG2)) as u32;
|
||||
|
||||
signal_generator::Config {
|
||||
// Same frequency as batch size.
|
||||
frequency_tuning_word: [
|
||||
frequency_tuning_word,
|
||||
frequency_tuning_word,
|
||||
],
|
||||
// 1V Amplitude
|
||||
amplitude: DacCode::try_from(1.0).unwrap().into(),
|
||||
|
||||
signal: signal_generator::Signal::Cosine,
|
||||
}
|
||||
};
|
||||
|
||||
init::LateResources {
|
||||
afes: stabilizer.afes,
|
||||
adcs: stabilizer.adcs,
|
||||
|
@ -272,6 +286,9 @@ const APP: () = {
|
|||
digital_inputs: stabilizer.digital_inputs,
|
||||
timestamper: stabilizer.timestamper,
|
||||
telemetry: TelemetryBuffer::default(),
|
||||
signal_generator: signal_generator::SignalGenerator::new(
|
||||
signal_config,
|
||||
),
|
||||
|
||||
settings,
|
||||
generator,
|
||||
|
@ -288,7 +305,7 @@ const APP: () = {
|
|||
/// This is an implementation of a externally (DI0) referenced PLL lockin on the ADC0 signal.
|
||||
/// It outputs either I/Q or power/phase on DAC0/DAC1. Data is normalized to full scale.
|
||||
/// PLL bandwidth, filter bandwidth, slope, and x/y or power/phase post-filters are available.
|
||||
#[task(binds=DMA1_STR4, resources=[adcs, dacs, lockin, timestamper, pll, settings, telemetry, generator], priority=2)]
|
||||
#[task(binds=DMA1_STR4, resources=[adcs, dacs, lockin, timestamper, pll, settings, telemetry, generator, signal_generator], priority=2)]
|
||||
#[inline(never)]
|
||||
#[link_section = ".itcm.process"]
|
||||
fn process(mut c: process::Context) {
|
||||
|
@ -301,6 +318,7 @@ const APP: () = {
|
|||
ref mut pll,
|
||||
ref mut timestamper,
|
||||
ref mut generator,
|
||||
ref mut signal_generator,
|
||||
} = c.resources;
|
||||
|
||||
let (reference_phase, reference_frequency) = match settings.lockin_mode
|
||||
|
@ -356,7 +374,7 @@ const APP: () = {
|
|||
|
||||
// Convert to DAC data.
|
||||
for (channel, samples) in dac_samples.iter_mut().enumerate() {
|
||||
for (i, sample) in samples.iter_mut().enumerate() {
|
||||
for sample in samples.iter_mut() {
|
||||
let value = match settings.output_conf[channel] {
|
||||
Conf::Magnitude => output.abs_sqr() as i32 >> 16,
|
||||
Conf::Phase => output.arg() >> 16,
|
||||
|
@ -366,7 +384,10 @@ const APP: () = {
|
|||
}
|
||||
Conf::InPhase => output.re >> 16,
|
||||
Conf::Quadrature => output.im >> 16,
|
||||
Conf::Modulation => DAC_SEQUENCE[i] as i32,
|
||||
|
||||
Conf::Modulation => {
|
||||
signal_generator.next().unwrap() as i32
|
||||
}
|
||||
};
|
||||
|
||||
*sample = DacCode::from(value as i16).0;
|
||||
|
|
|
@ -83,18 +83,45 @@ use hal::dma::{
|
|||
#[derive(Copy, Clone)]
|
||||
pub struct AdcCode(pub u16);
|
||||
|
||||
#[allow(clippy::from_over_into)]
|
||||
impl Into<f32> for AdcCode {
|
||||
impl From<u16> for AdcCode {
|
||||
/// Construct an ADC code from a provided binary (ADC-formatted) code.
|
||||
fn from(value: u16) -> Self {
|
||||
Self(value)
|
||||
}
|
||||
}
|
||||
|
||||
impl From<i16> for AdcCode {
|
||||
/// Construct an ADC code from the stabilizer-defined code (i16 full range).
|
||||
fn from(value: i16) -> Self {
|
||||
Self(value as u16)
|
||||
}
|
||||
}
|
||||
|
||||
impl From<AdcCode> for i16 {
|
||||
/// Get a stabilizer-defined code from the ADC code.
|
||||
fn from(code: AdcCode) -> i16 {
|
||||
code.0 as i16
|
||||
}
|
||||
}
|
||||
|
||||
impl From<AdcCode> for u16 {
|
||||
/// Get an ADC-frmatted binary value from the code.
|
||||
fn from(code: AdcCode) -> u16 {
|
||||
code.0
|
||||
}
|
||||
}
|
||||
|
||||
impl From<AdcCode> for f32 {
|
||||
/// Convert raw ADC codes to/from voltage levels.
|
||||
///
|
||||
/// # Note
|
||||
/// This does not account for the programmable gain amplifier at the signal input.
|
||||
fn into(self) -> f32 {
|
||||
fn from(code: AdcCode) -> f32 {
|
||||
// The ADC has a differential input with a range of +/- 4.096 V and 16-bit resolution.
|
||||
// The gain into the two inputs is 1/5.
|
||||
let adc_volts_per_lsb = 5.0 / 2.0 * 4.096 / (1u16 << 15) as f32;
|
||||
|
||||
(self.0 as i16) as f32 * adc_volts_per_lsb
|
||||
i16::from(code) as f32 * adc_volts_per_lsb
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -57,6 +57,8 @@ use mutex_trait::Mutex;
|
|||
use super::design_parameters::{SampleBuffer, SAMPLE_BUFFER_SIZE};
|
||||
use super::timers;
|
||||
|
||||
use core::convert::TryFrom;
|
||||
|
||||
use hal::dma::{
|
||||
dma::{DMAReq, DmaConfig},
|
||||
traits::TargetAddress,
|
||||
|
@ -75,14 +77,37 @@ static mut DAC_BUF: [[SampleBuffer; 2]; 2] = [[[0; SAMPLE_BUFFER_SIZE]; 2]; 2];
|
|||
#[derive(Copy, Clone)]
|
||||
pub struct DacCode(pub u16);
|
||||
|
||||
#[allow(clippy::from_over_into)]
|
||||
impl Into<f32> for DacCode {
|
||||
fn into(self) -> f32 {
|
||||
impl TryFrom<f32> for DacCode {
|
||||
type Error = ();
|
||||
|
||||
fn try_from(voltage: f32) -> Result<DacCode, ()> {
|
||||
// The DAC output range in bipolar mode (including the external output op-amp) is +/- 4.096
|
||||
// V with 16-bit resolution. The anti-aliasing filter has an additional gain of 2.5.
|
||||
let dac_range = 4.096 * 2.5;
|
||||
|
||||
if voltage > dac_range || voltage < -1. * dac_range {
|
||||
Err(())
|
||||
} else {
|
||||
Ok(DacCode::from(
|
||||
(voltage * (i16::MAX as f32 / dac_range)) as i16,
|
||||
))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl From<DacCode> for f32 {
|
||||
fn from(code: DacCode) -> f32 {
|
||||
// The DAC output range in bipolar mode (including the external output op-amp) is +/- 4.096
|
||||
// V with 16-bit resolution. The anti-aliasing filter has an additional gain of 2.5.
|
||||
let dac_volts_per_lsb = 4.096 * 2.5 / (1u16 << 15) as f32;
|
||||
|
||||
(self.0 as i16).wrapping_add(i16::MIN) as f32 * dac_volts_per_lsb
|
||||
(code.0 as i16).wrapping_add(i16::MIN) as f32 * dac_volts_per_lsb
|
||||
}
|
||||
}
|
||||
|
||||
impl From<DacCode> for i16 {
|
||||
fn from(code: DacCode) -> i16 {
|
||||
(code.0 as i16).wrapping_sub(i16::MIN)
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -93,6 +118,13 @@ impl From<i16> for DacCode {
|
|||
}
|
||||
}
|
||||
|
||||
impl From<u16> for DacCode {
|
||||
/// Create a dac code from the provided DAC output code.
|
||||
fn from(value: u16) -> Self {
|
||||
Self(value)
|
||||
}
|
||||
}
|
||||
|
||||
macro_rules! dac_output {
|
||||
($name:ident, $index:literal, $data_stream:ident,
|
||||
$spi:ident, $trigger_channel:ident, $dma_req:ident) => {
|
||||
|
|
|
@ -10,6 +10,7 @@ pub mod design_parameters;
|
|||
pub mod input_stamper;
|
||||
pub mod pounder;
|
||||
pub mod setup;
|
||||
pub mod signal_generator;
|
||||
pub mod system_timer;
|
||||
|
||||
mod eeprom;
|
||||
|
|
|
@ -0,0 +1,170 @@
|
|||
use crate::{configuration::ADC_SAMPLE_TICKS_LOG2, hardware::dac::DacCode};
|
||||
use core::convert::{TryFrom, TryInto};
|
||||
use miniconf::Miniconf;
|
||||
use serde::Deserialize;
|
||||
|
||||
/// Types of signals that can be generated.
|
||||
#[derive(Copy, Clone, Debug, Deserialize, Miniconf)]
|
||||
pub enum Signal {
|
||||
Cosine,
|
||||
Square,
|
||||
Triangle,
|
||||
}
|
||||
|
||||
/// Basic configuration for a generated signal.
|
||||
///
|
||||
/// # Miniconf
|
||||
/// `{"signal": <signal>, "frequency", 1000.0, "symmetry": 0.5, "amplitude": 1.0}`
|
||||
///
|
||||
/// Where `<signal>` may be any of [Signal] variants, `frequency` specifies the signal frequency
|
||||
/// in Hertz, `symmetry` specifies the normalized signal symmetry which ranges from 0 - 1.0, and
|
||||
/// `amplitude` specifies the signal amplitude in Volts.
|
||||
#[derive(Copy, Clone, Debug, Miniconf, Deserialize)]
|
||||
pub struct BasicConfig {
|
||||
/// The signal type that should be generated. See [Signal] variants.
|
||||
pub signal: Signal,
|
||||
|
||||
/// The frequency of the generated signal in Hertz.
|
||||
pub frequency: f32,
|
||||
|
||||
/// The normalized symmetry of the signal. At 0% symmetry, the duration of the first half oscillation is minimal.
|
||||
/// At 25% symmetry, the first half oscillation lasts for 25% of the signal period. For square wave output this
|
||||
//// symmetry is the duty cycle.
|
||||
pub symmetry: f32,
|
||||
|
||||
/// The amplitude of the output signal in volts.
|
||||
pub amplitude: f32,
|
||||
}
|
||||
|
||||
impl Default for BasicConfig {
|
||||
fn default() -> Self {
|
||||
Self {
|
||||
frequency: 1.0e3,
|
||||
symmetry: 0.5,
|
||||
signal: Signal::Cosine,
|
||||
amplitude: 0.0,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Represents the errors that can occur when attempting to configure the signal generator.
|
||||
#[derive(Copy, Clone, Debug)]
|
||||
pub enum Error {
|
||||
/// The provided amplitude is out-of-range.
|
||||
InvalidAmplitude,
|
||||
}
|
||||
|
||||
impl TryFrom<BasicConfig> for Config {
|
||||
type Error = Error;
|
||||
|
||||
fn try_from(config: BasicConfig) -> Result<Config, Error> {
|
||||
// Calculate the frequency tuning words
|
||||
let frequency_tuning_word: [u32; 2] = {
|
||||
const LSB_PER_HERTZ: f32 =
|
||||
(1u64 << (31 + ADC_SAMPLE_TICKS_LOG2)) as f32 / 100.0e6;
|
||||
let ftw = config.frequency * LSB_PER_HERTZ;
|
||||
|
||||
if config.symmetry <= 0.0 {
|
||||
[1u32 << 31, ftw as u32]
|
||||
} else if config.symmetry >= 1.0 {
|
||||
[ftw as u32, 1u32 << 31]
|
||||
} else {
|
||||
[
|
||||
(ftw / config.symmetry) as u32,
|
||||
(ftw / (1.0 - config.symmetry)) as u32,
|
||||
]
|
||||
}
|
||||
};
|
||||
|
||||
Ok(Config {
|
||||
amplitude: DacCode::try_from(config.amplitude)
|
||||
.or(Err(Error::InvalidAmplitude))?
|
||||
.into(),
|
||||
signal: config.signal,
|
||||
frequency_tuning_word,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Copy, Clone, Debug)]
|
||||
pub struct Config {
|
||||
/// The type of signal being generated
|
||||
pub signal: Signal,
|
||||
|
||||
/// The full-scale output code of the signal
|
||||
pub amplitude: i16,
|
||||
|
||||
/// The frequency tuning word of the signal. Phase is incremented by this amount
|
||||
pub frequency_tuning_word: [u32; 2],
|
||||
}
|
||||
|
||||
#[derive(Debug)]
|
||||
pub struct SignalGenerator {
|
||||
phase_accumulator: u32,
|
||||
config: Config,
|
||||
}
|
||||
|
||||
impl Default for SignalGenerator {
|
||||
fn default() -> Self {
|
||||
Self {
|
||||
config: BasicConfig::default().try_into().unwrap(),
|
||||
phase_accumulator: 0,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl SignalGenerator {
|
||||
/// Construct a new signal generator with some specific config.
|
||||
///
|
||||
/// # Args
|
||||
/// * `config` - The config to use for generating signals.
|
||||
///
|
||||
/// # Returns
|
||||
/// The generator
|
||||
pub fn new(config: Config) -> Self {
|
||||
Self {
|
||||
config,
|
||||
phase_accumulator: 0,
|
||||
}
|
||||
}
|
||||
|
||||
/// Update waveform generation settings.
|
||||
pub fn update_waveform(&mut self, new_config: Config) {
|
||||
self.config = new_config;
|
||||
}
|
||||
}
|
||||
|
||||
impl core::iter::Iterator for SignalGenerator {
|
||||
type Item = i16;
|
||||
|
||||
/// Get the next value in the generator sequence.
|
||||
fn next(&mut self) -> Option<i16> {
|
||||
self.phase_accumulator = self.phase_accumulator.wrapping_add(
|
||||
if (self.phase_accumulator as i32).is_negative() {
|
||||
self.config.frequency_tuning_word[0]
|
||||
} else {
|
||||
self.config.frequency_tuning_word[1]
|
||||
},
|
||||
);
|
||||
|
||||
let phase = self.phase_accumulator as i32;
|
||||
|
||||
let amplitude: i16 = match self.config.signal {
|
||||
Signal::Cosine => (dsp::cossin(phase).0 >> 16) as i16,
|
||||
Signal::Square => {
|
||||
if phase.is_negative() {
|
||||
i16::MAX
|
||||
} else {
|
||||
-i16::MAX
|
||||
}
|
||||
}
|
||||
Signal::Triangle => i16::MAX - (phase.abs() >> 15) as i16,
|
||||
};
|
||||
|
||||
// Calculate the final output result as an i16.
|
||||
let result = amplitude as i32 * self.config.amplitude as i32;
|
||||
|
||||
// Note: We downshift by 15-bits to preserve only one of the sign bits.
|
||||
Some(((result + (1 << 14)) >> 15) as i16)
|
||||
}
|
||||
}
|
Loading…
Reference in New Issue