From 16575fd1c6b523fe91e7a882267645bd64debdda Mon Sep 17 00:00:00 2001 From: Ryan Summers Date: Mon, 1 Nov 2021 12:30:31 +0100 Subject: [PATCH 01/39] Initial addition of web server --- Cargo.toml | 2 + src/bin/stream_test.rs | 12 ++- src/bin/webserver.rs | 177 +++++++++++++++++++++++++++++++++++++++++ src/de/data/mod.rs | 71 +++++++++++++++++ src/de/deserializer.rs | 61 +++++--------- src/de/mod.rs | 25 ++---- src/lib.rs | 2 +- 7 files changed, 285 insertions(+), 65 deletions(-) create mode 100644 src/bin/webserver.rs create mode 100644 src/de/data/mod.rs diff --git a/Cargo.toml b/Cargo.toml index 5b4f78b..c72809f 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -11,3 +11,5 @@ num_enum = "0.5" log = "0.4" env_logger = "0.9" async-std = { version = "1", features = ["attributes"] } +tide = "0.16" +serde = {version = "1", features = ["derive"]} diff --git a/src/bin/stream_test.rs b/src/bin/stream_test.rs index 15aba7c..8cefd25 100644 --- a/src/bin/stream_test.rs +++ b/src/bin/stream_test.rs @@ -40,10 +40,10 @@ async fn main() { log::info!("Reading frames"); while Instant::now() < stop { let frame = stream_receiver.next_frame().await.unwrap(); - total_batches += frame.batch_count() as u64; + total_batches += frame.data.batch_count() as u64; if let Some(expect) = expect_sequence { - let num_dropped = frame.sequence_number.wrapping_sub(expect) as u64; + let num_dropped = frame.sequence_number().wrapping_sub(expect) as u64; dropped_batches += num_dropped; total_batches += num_dropped; @@ -52,12 +52,16 @@ async fn main() { "Lost {} batches: {:#08X} -> {:#08X}", num_dropped, expect, - frame.sequence_number, + frame.sequence_number(), ); } } - expect_sequence = Some(frame.sequence_number.wrapping_add(frame.batch_count() as _)); + expect_sequence = Some( + frame + .sequence_number() + .wrapping_add(frame.data.batch_count() as _), + ); } assert!(total_batches > 0); diff --git a/src/bin/webserver.rs b/src/bin/webserver.rs new file mode 100644 index 0000000..1b4047b --- /dev/null +++ b/src/bin/webserver.rs @@ -0,0 +1,177 @@ +use clap::Parser; +use serde::Serialize; +use stabilizer_streaming::{de::deserializer::StreamFrame, de::StreamFormat, StreamReceiver}; +use std::collections::VecDeque; +use tide::{Body, Response}; + +/// Execute stabilizer stream throughput testing. +/// Use `RUST_LOG=info cargo run` to increase logging verbosity. +#[derive(Parser)] +struct Opts { + /// The local IP to receive streaming data on. + #[clap(short, long, default_value = "0.0.0.0")] + ip: String, + + /// The UDP port to receive streaming data on. + #[clap(long, default_value = "9293")] + port: u16, +} + +struct StreamData { + current_format: Option, + + max_size: usize, + timebase: VecDeque, + data: Vec>, +} + +#[derive(Serialize, Debug)] +struct TraceData { + time: Vec, + data: Vec>, +} + +impl StreamData { + fn new() -> Self { + Self { + current_format: None, + timebase: VecDeque::new(), + data: Vec::new(), + + // TODO: Base this on the sample frequency. + max_size: 1024, + } + } + + pub fn add_frame(&mut self, frame: StreamFrame) { + // If the stream format has changed, clear all data buffers. + if let Some(format) = self.current_format { + if frame.format() != format { + self.timebase.clear(); + self.data.clear(); + self.current_format.replace(frame.format()); + } + } else { + self.current_format.replace(frame.format()); + } + + // TODO: Determine whether or not we actually want to accept the current frame (e.g. + // trigger state). We may just want to silently drop it at this point if we aren't armed. + + // Next, extract all of the data traces + for i in 0..frame.data.trace_count() { + if self.data.len() < frame.data.trace_count() { + self.data.push(VecDeque::new()); + } + + // TODO: Decimate the data as requested. + let trace = frame.data.get_trace(i); + self.data[i].extend(trace); + + // For the first trace, also extend the timebase. + if i == 0 { + let base = (frame.sequence_number() as u64) + .wrapping_mul(frame.data.samples_per_batch() as u64); + for sample_index in 0..trace.len() { + self.timebase + .push_back(base.wrapping_add(sample_index as u64)) + } + } + } + + // Drain the data/timebase queues to remain within our maximum size. + if self.timebase.len() > self.max_size { + let drain_size = self.timebase.len() - self.max_size; + self.timebase.drain(0..drain_size); + for trace in &mut self.data { + trace.drain(0..drain_size); + } + } + } + + pub fn get_data(&self) -> TraceData { + let mut times: Vec = Vec::new(); + let time_offset = if self.timebase.len() > 0 { + self.timebase[0] + } else { + 0 + }; + + for time in self.timebase.iter() { + times.push(time.wrapping_sub(time_offset) as f32) + } + + let mut data = Vec::new(); + for trace in self.data.iter() { + let mut vec = Vec::new(); + let (front, back) = trace.as_slices(); + vec.extend_from_slice(front); + vec.extend_from_slice(back); + data.push(vec); + } + + TraceData { time: times, data } + } +} + +struct TriggerState; + +struct ServerState { + trigger: TriggerState, + + // StreamData cannot implement a const-fn constructor, so we wrap it in an option instead. + pub data: async_std::sync::Mutex>, +} + +static STATE: ServerState = ServerState { + trigger: TriggerState {}, + data: async_std::sync::Mutex::new(None), +}; + +async fn receive(state: &ServerState, mut receiver: StreamReceiver) { + loop { + // Get a stream frame from Stabilizer. + let frame = receiver.next_frame().await.unwrap(); + + // TODO: Loop until we acquire mutex. + // Add the frame data to the traces. + let mut data = state.data.lock().await; + + data.as_mut().unwrap().add_frame(frame); + drop(data); + } +} + +async fn get_traces(request: tide::Request<&ServerState>) -> tide::Result> { + log::info!("Got data request"); + let state = request.state(); + let data = state.data.lock().await; + let response = data.as_ref().unwrap().get_data(); + log::debug!("Response: {:?}", response); + Ok(Response::builder(200).body(Body::from_json(&response)?)) +} + +#[async_std::main] +async fn main() -> tide::Result<()> { + env_logger::init(); + + let opts = Opts::parse(); + let ip: std::net::Ipv4Addr = opts.ip.parse().unwrap(); + let stream_receiver = StreamReceiver::new(ip, opts.port).await; + + // Populate the initial receiver data. + { + let mut stream_data = STATE.data.lock().await; + stream_data.replace(StreamData::new()); + } + + let child = async_std::task::spawn(receive(&STATE, stream_receiver)); + + let mut webapp = tide::with_state(&STATE); + webapp.at("/data").get(get_traces); + webapp.at("/").get(|_| async { Ok("Hello World!") }); + + webapp.listen("127.0.0.1:8080").await?; + + Ok(()) +} diff --git a/src/de/data/mod.rs b/src/de/data/mod.rs new file mode 100644 index 0000000..ec79072 --- /dev/null +++ b/src/de/data/mod.rs @@ -0,0 +1,71 @@ +#[derive(Debug, Copy, Clone)] +pub enum FormatError { + InvalidSize, +} + +pub trait FrameData { + fn trace_count(&self) -> usize; + fn get_trace(&self, index: usize) -> &Vec; + fn samples_per_batch(&self) -> usize; + fn batch_count(&self) -> usize { + self.get_trace(0).len() / self.samples_per_batch() + } +} + +pub struct AdcDacData { + traces: [Vec; 4], + batch_size: usize, +} + +impl FrameData for AdcDacData { + fn trace_count(&self) -> usize { + self.traces.len() + } + + fn get_trace(&self, index: usize) -> &Vec { + &self.traces[index] + } + + fn samples_per_batch(&self) -> usize { + // Each element of the batch is 4 samples, each of which are u16s. + self.batch_size + } +} + +impl AdcDacData { + /// Extract AdcDacData from a binary data block in the stream. + /// + /// # Args + /// * `batch_size` - The size of each batch in samples. + /// * `data` - The binary data composing the stream frame. + pub fn new(batch_size: usize, data: &[u8]) -> Result { + let batch_size_bytes: usize = batch_size * 8; + let num_batches = data.len() / batch_size_bytes; + if num_batches * batch_size_bytes != data.len() { + return Err(FormatError::InvalidSize); + } + + let mut traces: [Vec; 4] = [Vec::new(), Vec::new(), Vec::new(), Vec::new()]; + + for batch in 0..num_batches { + let batch_index = batch * batch_size_bytes; + + // Deserialize the batch + for sample in 0..batch_size { + let sample_index = batch_index + sample * 8; + for (i, trace) in traces.iter_mut().enumerate() { + let trace_index = sample_index + i * 2; + let value = { + let code = u16::from_le_bytes([data[trace_index], data[trace_index + 1]]); + // TODO: Convert code from u16 to floating point voltage. + code as f32 + }; + + trace.push(value); + } + } + } + + Ok(Self { batch_size, traces }) + } +} diff --git a/src/de/deserializer.rs b/src/de/deserializer.rs index 71b19c5..1e98dec 100644 --- a/src/de/deserializer.rs +++ b/src/de/deserializer.rs @@ -1,4 +1,5 @@ -use super::{AdcDacData, Error, FormatError, StreamData, StreamFormat}; +use super::data::{AdcDacData, FrameData}; +use super::{Error, StreamFormat}; use std::convert::TryFrom; @@ -9,9 +10,9 @@ const MAGIC_WORD: u16 = 0x057B; const HEADER_SIZE: usize = 8; /// A single stream frame contains multiple batches of data. -pub struct StreamFrame<'a> { - pub sequence_number: u32, - pub data: StreamData<'a>, +pub struct StreamFrame { + header: FrameHeader, + pub data: Box, } struct FrameHeader { @@ -55,55 +56,33 @@ impl FrameHeader { } } -impl<'a> StreamFrame<'a> { +impl StreamFrame { + /// Get the format code of the current frame. + pub fn format(&self) -> StreamFormat { + self.header.format_code + } + + /// Get the sequence number of the first batch in the frame. + pub fn sequence_number(&self) -> u32 { + self.header.sequence_number + } + /// Parse a stream frame from a single UDP packet. - pub fn from_bytes(input: &'a [u8]) -> Result, Error> { + pub fn from_bytes(input: &[u8]) -> Result { let (header, data) = input.split_at(HEADER_SIZE); let header = FrameHeader::parse(header)?; - if data.len() % header.batch_size as usize != 0 { - return Err(FormatError::InvalidSize.into()); - } - let data = match header.format_code { StreamFormat::AdcDacData => { - let data = AdcDacData::new(header.batch_size, data)?; - StreamData::AdcDacData(data) + let data = AdcDacData::new(header.batch_size as usize, data)?; + Box::new(data) } }; Ok(StreamFrame { - sequence_number: header.sequence_number, + header: header, data, }) } - - /// Get the number of batches contained within the frame. - pub fn batch_count(&self) -> usize { - match &self.data { - StreamData::AdcDacData(data) => data.batch_count(), - } - } -} - -impl<'a> AdcDacData<'a> { - /// Extract AdcDacData from a binary data block in the stream. - /// - /// # Args - /// * `batch_size` - The size of each batch in samples. - /// * `data` - The binary data composing the stream frame. - fn new(batch_size: u8, data: &'a [u8]) -> Result, FormatError> { - // Each element of the batch is 4 samples, each of which are u16s. - let batch_size_bytes: usize = (batch_size * 8) as usize; - if data.len() % batch_size_bytes != 0 { - return Err(FormatError::InvalidSize); - } - - Ok(Self { batch_size, data }) - } - - fn batch_count(&self) -> usize { - self.data.len() / (self.batch_size * 8) as usize - } } diff --git a/src/de/mod.rs b/src/de/mod.rs index 6916e06..aeacfee 100644 --- a/src/de/mod.rs +++ b/src/de/mod.rs @@ -1,36 +1,23 @@ use num_enum::TryFromPrimitive; +pub mod data; pub mod deserializer; -pub struct AdcDacData<'a> { - data: &'a [u8], - batch_size: u8, -} - -pub enum StreamData<'a> { - AdcDacData(AdcDacData<'a>), -} - -#[derive(TryFromPrimitive, Debug)] +#[derive(TryFromPrimitive, Debug, Copy, Clone, PartialEq)] #[repr(u8)] -enum StreamFormat { +pub enum StreamFormat { AdcDacData = 1, } #[derive(Debug, Copy, Clone)] pub enum Error { - DataFormat(FormatError), + DataFormat(data::FormatError), InvalidHeader, UnknownFormat, } -#[derive(Debug, Copy, Clone)] -pub enum FormatError { - InvalidSize, -} - -impl From for Error { - fn from(e: FormatError) -> Error { +impl From for Error { + fn from(e: data::FormatError) -> Error { Error::DataFormat(e) } } diff --git a/src/lib.rs b/src/lib.rs index 1c99274..4a19e19 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -27,7 +27,7 @@ impl StreamReceiver { } /// Receive a stream frame from Stabilizer. - pub async fn next_frame(&mut self) -> Option> { + pub async fn next_frame(&mut self) -> Option { // Read a single UDP packet. let len = async_std::io::timeout(Duration::from_secs(1), self.socket.recv(&mut self.buf)) .await From d22f740d1fe4496d893d7acd0624b227c983979d Mon Sep 17 00:00:00 2001 From: Ryan Summers Date: Mon, 1 Nov 2021 19:24:27 +0100 Subject: [PATCH 02/39] Refactoring server --- Cargo.toml | 3 + src/bin/webserver.rs | 239 ++++++++++++++++++++++++++++++++----------- src/de/data/mod.rs | 13 +++ 3 files changed, 194 insertions(+), 61 deletions(-) diff --git a/Cargo.toml b/Cargo.toml index c72809f..0a90d6b 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -13,3 +13,6 @@ env_logger = "0.9" async-std = { version = "1", features = ["attributes"] } tide = "0.16" serde = {version = "1", features = ["derive"]} + +[build-dependencies] +npm_rs = "0.2.1" diff --git a/src/bin/webserver.rs b/src/bin/webserver.rs index 1b4047b..9cff180 100644 --- a/src/bin/webserver.rs +++ b/src/bin/webserver.rs @@ -1,9 +1,13 @@ use clap::Parser; -use serde::Serialize; +use serde::{Deserialize, Serialize}; use stabilizer_streaming::{de::deserializer::StreamFrame, de::StreamFormat, StreamReceiver}; -use std::collections::VecDeque; use tide::{Body, Response}; +// TODO: Expose this as a configurable parameter and/or add it to the stream frame. +const SAMPLE_RATE_HZ: f32 = 100e6 / 128.0; + +const SAMPLE_PERIOD: f32 = 1.0 / SAMPLE_RATE_HZ; + /// Execute stabilizer stream throughput testing. /// Use `RUST_LOG=info cargo run` to increase logging verbosity. #[derive(Parser)] @@ -17,78 +21,120 @@ struct Opts { port: u16, } +// TODO: Perhaps refactor this to be a state machine to simplify transitional logic. +#[derive(Serialize, Copy, Clone, PartialEq)] +enum TriggerState { + /// The trigger is idle. + Idle, + + /// The trigger is armed and waiting for trigger conditions. + Armed, + + /// The trigger has occurred and data is actively being captured. + Triggered, + + /// The trigger is complete and data is available for query. + Stopped, +} + +#[derive(Deserialize)] +struct CaptureSettings { + /// The duration to capture data for in seconds. + capture_duration_secs: f32, +} + +struct ServerState { + // StreamData cannot implement a const-fn constructor, so we wrap it in an option instead. + pub data: async_std::sync::Mutex, +} + struct StreamData { current_format: Option, + trigger: TriggerState, max_size: usize, - timebase: VecDeque, - data: Vec>, + timebase: Vec, + traces: Vec, +} + +#[derive(Serialize, Clone, Debug)] +struct Trace { + label: String, + data: Vec, } #[derive(Serialize, Debug)] struct TraceData { time: Vec, - data: Vec>, + traces: Vec, } impl StreamData { - fn new() -> Self { + const fn new() -> Self { Self { current_format: None, - timebase: VecDeque::new(), - data: Vec::new(), + timebase: Vec::new(), + traces: Vec::new(), - // TODO: Base this on the sample frequency. - max_size: 1024, + max_size: SAMPLE_RATE_HZ as usize, + + trigger: TriggerState::Idle, } } + /// Ingest an incoming stream frame. pub fn add_frame(&mut self, frame: StreamFrame) { // If the stream format has changed, clear all data buffers. if let Some(format) = self.current_format { if frame.format() != format { - self.timebase.clear(); - self.data.clear(); - self.current_format.replace(frame.format()); + self.flush() } - } else { - self.current_format.replace(frame.format()); } - // TODO: Determine whether or not we actually want to accept the current frame (e.g. - // trigger state). We may just want to silently drop it at this point if we aren't armed. + self.current_format.replace(frame.format()); + + // If we aren't triggered, there's nothing more we want to do. + if self.trigger != TriggerState::Triggered { + return; + } // Next, extract all of the data traces for i in 0..frame.data.trace_count() { - if self.data.len() < frame.data.trace_count() { - self.data.push(VecDeque::new()); + if self.traces.len() < frame.data.trace_count() { + self.traces.push(Trace { + data: Vec::new(), + label: frame.data.trace_label(i), + }); } // TODO: Decimate the data as requested. let trace = frame.data.get_trace(i); - self.data[i].extend(trace); + self.traces[i].data.extend(trace); // For the first trace, also extend the timebase. if i == 0 { let base = (frame.sequence_number() as u64) .wrapping_mul(frame.data.samples_per_batch() as u64); for sample_index in 0..trace.len() { - self.timebase - .push_back(base.wrapping_add(sample_index as u64)) + self.timebase.push(base.wrapping_add(sample_index as u64)) } } } // Drain the data/timebase queues to remain within our maximum size. if self.timebase.len() > self.max_size { - let drain_size = self.timebase.len() - self.max_size; - self.timebase.drain(0..drain_size); - for trace in &mut self.data { - trace.drain(0..drain_size); + self.timebase.drain(self.max_size..); + + for trace in &mut self.traces { + trace.data.drain(self.max_size..); } + + // Stop the capture now that we've filled up our buffers. + self.trigger = TriggerState::Stopped; } } + /// Get the current trace data. pub fn get_data(&self) -> TraceData { let mut times: Vec = Vec::new(); let time_offset = if self.timebase.len() > 0 { @@ -98,59 +144,119 @@ impl StreamData { }; for time in self.timebase.iter() { - times.push(time.wrapping_sub(time_offset) as f32) + times.push(time.wrapping_sub(time_offset) as f32 * SAMPLE_PERIOD) } - let mut data = Vec::new(); - for trace in self.data.iter() { - let mut vec = Vec::new(); - let (front, back) = trace.as_slices(); - vec.extend_from_slice(front); - vec.extend_from_slice(back); - data.push(vec); + TraceData { + time: times, + traces: self.traces.clone(), } - - TraceData { time: times, data } } -} -struct TriggerState; - -struct ServerState { - trigger: TriggerState, - - // StreamData cannot implement a const-fn constructor, so we wrap it in an option instead. - pub data: async_std::sync::Mutex>, + /// Remove all data from buffers. + pub fn flush(&mut self) { + self.timebase.clear(); + self.traces.clear(); + self.current_format.take(); + } } -static STATE: ServerState = ServerState { - trigger: TriggerState {}, - data: async_std::sync::Mutex::new(None), -}; - +/// Stabilizer stream frame reception thread. +/// +/// # Note +/// This task executes forever, continuously receiving stabilizer stream frames for processing. +/// +/// # Args +/// * `state` - The server state +/// * `state` - A receiver for reading stabilizer stream frames. async fn receive(state: &ServerState, mut receiver: StreamReceiver) { loop { // Get a stream frame from Stabilizer. let frame = receiver.next_frame().await.unwrap(); - // TODO: Loop until we acquire mutex. // Add the frame data to the traces. let mut data = state.data.lock().await; - - data.as_mut().unwrap().add_frame(frame); - drop(data); + data.add_frame(frame); } } +/// Get all available data traces. +/// +/// # Note +/// There is no guarantee that the data will be complete. Poll the current trigger state to ensure +/// all data is available. +/// +/// # Args +/// `request` - Unused +/// +/// # Returns +/// All of the data as a json-serialized `TraceData`. async fn get_traces(request: tide::Request<&ServerState>) -> tide::Result> { log::info!("Got data request"); let state = request.state(); let data = state.data.lock().await; - let response = data.as_ref().unwrap().get_data(); + let response = data.get_data(); log::debug!("Response: {:?}", response); Ok(Response::builder(200).body(Body::from_json(&response)?)) } +/// Configure the current capture settings +/// +/// # Args +/// * `request` - An HTTP request containing json-serialized `CaptureSettings`. +async fn configure_capture( + mut request: tide::Request<&ServerState>, +) -> tide::Result> { + let config: CaptureSettings = request.body_json().await?; + let state = request.state(); + + // Clear any pre-existing data in the buffers. + let mut data = state.data.lock().await; + data.flush(); + + log::info!("Arming trigger"); + data.trigger = TriggerState::Armed; + + if config.capture_duration_secs < 0. { + return Ok(Response::builder(400).body("Negative capture duration not supported")); + } + + let samples: f32 = SAMPLE_RATE_HZ * config.capture_duration_secs; + if samples > usize::MAX as f32 { + return Ok(Response::builder(400).body("Too many samples requested")); + } + + // TODO: Configure decimation + data.max_size = samples as usize; + + Ok(Response::builder(200)) +} + +/// Get the current trigger state. +/// +/// # Args +/// * `request` - Unused. +/// +/// # Returns +/// JSON containing the current trigger state as a string. +async fn get_trigger(request: tide::Request<&ServerState>) -> tide::Result> { + let state = request.state(); + let data = state.data.lock().await; + Ok(Response::builder(200).body(Body::from_json(&data.trigger)?)) +} + +/// Force a trigger condition. +/// +/// # Args +/// * `request` - Unused. +async fn force_trigger(request: tide::Request<&ServerState>) -> tide::Result> { + let state = request.state(); + let mut data = state.data.lock().await; + log::info!("Forcing trigger"); + data.trigger = TriggerState::Triggered; + Ok(Response::new(200)) +} + #[async_std::main] async fn main() -> tide::Result<()> { env_logger::init(); @@ -160,17 +266,28 @@ async fn main() -> tide::Result<()> { let stream_receiver = StreamReceiver::new(ip, opts.port).await; // Populate the initial receiver data. - { - let mut stream_data = STATE.data.lock().await; - stream_data.replace(StreamData::new()); - } + static STATE: ServerState = ServerState { + data: async_std::sync::Mutex::new(StreamData::new()), + }; - let child = async_std::task::spawn(receive(&STATE, stream_receiver)); + async_std::task::spawn(receive(&STATE, stream_receiver)); let mut webapp = tide::with_state(&STATE); - webapp.at("/data").get(get_traces); - webapp.at("/").get(|_| async { Ok("Hello World!") }); + // Route configuration and queries. + webapp.at("/traces").get(get_traces); + webapp.at("/trigger").get(get_trigger).post(force_trigger); + webapp.at("/capture").post(configure_capture); + + // Serve front-end files. + webapp.at("/").get(|_| async { + Ok(Response::builder(200).body(Body::from_file("frontend/dist/index.html").await?)) + }); + webapp.at("/main.js").get(|_| async { + Ok(Response::builder(200).body(Body::from_file("frontend/dist/main.js").await?)) + }); + + // Start up the webapp. webapp.listen("127.0.0.1:8080").await?; Ok(()) diff --git a/src/de/data/mod.rs b/src/de/data/mod.rs index ec79072..27c1d05 100644 --- a/src/de/data/mod.rs +++ b/src/de/data/mod.rs @@ -10,6 +10,9 @@ pub trait FrameData { fn batch_count(&self) -> usize { self.get_trace(0).len() / self.samples_per_batch() } + fn trace_label(&self, index: usize) -> String { + format!("{}", index) + } } pub struct AdcDacData { @@ -30,6 +33,16 @@ impl FrameData for AdcDacData { // Each element of the batch is 4 samples, each of which are u16s. self.batch_size } + + fn trace_label(&self, index: usize) -> String { + match index { + 0 => "ADC0".to_string(), + 1 => "ADC1".to_string(), + 2 => "DAC0".to_string(), + 3 => "DAC1".to_string(), + _ => panic!("Invalid trace"), + } + } } impl AdcDacData { From 4e5de1f681a93cd10f0729a9251d6735b98a321f Mon Sep 17 00:00:00 2001 From: Ryan Summers Date: Mon, 1 Nov 2021 21:23:26 +0100 Subject: [PATCH 03/39] Adding WIP frontend --- build.rs | 16 + frontend/dist/index.html | 11 + frontend/package-lock.json | 2003 ++++++++++++++++++++++++++++++++++++ frontend/package.json | 19 + frontend/src/react.jsx | 145 +++ 5 files changed, 2194 insertions(+) create mode 100644 build.rs create mode 100644 frontend/dist/index.html create mode 100644 frontend/package-lock.json create mode 100644 frontend/package.json create mode 100644 frontend/src/react.jsx diff --git a/build.rs b/build.rs new file mode 100644 index 0000000..d75de48 --- /dev/null +++ b/build.rs @@ -0,0 +1,16 @@ +fn main() { + println!("cargo-rerun-if-changed=frontend"); + + let mut working_directory = std::env::current_dir().unwrap(); + working_directory.push("frontend"); + assert!( + npm_rs::NpmEnv::default() + .set_path(working_directory) + .init_env() + .run("build") + .exec() + .unwrap() + .success(), + "Failed to build front-end resources" + ); +} diff --git a/frontend/dist/index.html b/frontend/dist/index.html new file mode 100644 index 0000000..955c7bf --- /dev/null +++ b/frontend/dist/index.html @@ -0,0 +1,11 @@ + + + + + Stabilizer Livestream Viewer + + +
+ + + diff --git a/frontend/package-lock.json b/frontend/package-lock.json new file mode 100644 index 0000000..19411bd --- /dev/null +++ b/frontend/package-lock.json @@ -0,0 +1,2003 @@ +{ + "name": "stabilizer-stream-viewer", + "version": "0.1.0", + "lockfileVersion": 1, + "requires": true, + "dependencies": { + "JSONStream": { + "version": "1.3.5", + "resolved": "https://registry.npmjs.org/JSONStream/-/JSONStream-1.3.5.tgz", + "integrity": "sha512-E+iruNOY8VV9s4JEbe1aNEm6MiszPRr/UfcHMz0TQh1BXSxHK+ASV1R6W4HpjBhSeS+54PIsAMCBmwD06LLsqQ==", + "requires": { + "jsonparse": "^1.2.0", + "through": ">=2.2.7 <3" + } + }, + "acorn": { + "version": "7.4.1", + "resolved": "https://registry.npmjs.org/acorn/-/acorn-7.4.1.tgz", + "integrity": "sha512-nQyp0o1/mNdbTO1PO6kHkwSrmgZ0MT/jCCpNiwbUjGoRN4dlBhqJtoQuCnEOKzgTVwg0ZWiCoQy6SxMebQVh8A==" + }, + "acorn-node": { + "version": "1.8.2", + "resolved": "https://registry.npmjs.org/acorn-node/-/acorn-node-1.8.2.tgz", + "integrity": "sha512-8mt+fslDufLYntIoPAaIMUe/lrbrehIiwmR3t2k9LljIzoigEPF27eLk2hy8zSGzmR/ogr7zbRKINMo1u0yh5A==", + "requires": { + "acorn": "^7.0.0", + "acorn-walk": "^7.0.0", + "xtend": "^4.0.2" + } + }, + "acorn-walk": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/acorn-walk/-/acorn-walk-7.2.0.tgz", + "integrity": "sha512-OPdCF6GsMIP+Az+aWfAAOEt2/+iVDKE7oy6lJ098aoe59oAmK76qV6Gw60SbZ8jHuG2wH058GF4pLFbYamYrVA==" + }, + "ajv": { + "version": "6.12.6", + "resolved": "https://registry.npmjs.org/ajv/-/ajv-6.12.6.tgz", + "integrity": "sha512-j3fVLgvTo527anyYyJOGTYJbG+vnnQYvE0m5mmkc1TK+nxAppkCLMIL0aZ4dblVCNoGShhm+kzE4ZUykBoMg4g==", + "requires": { + "fast-deep-equal": "^3.1.1", + "fast-json-stable-stringify": "^2.0.0", + "json-schema-traverse": "^0.4.1", + "uri-js": "^4.2.2" + } + }, + "asn1": { + "version": "0.2.4", + "resolved": "https://registry.npmjs.org/asn1/-/asn1-0.2.4.tgz", + "integrity": "sha512-jxwzQpLQjSmWXgwaCZE9Nz+glAG01yF1QnWgbhGwHI5A6FRIEY6IVqtHhIepHqI7/kyEyQEagBC5mBEFlIYvdg==", + "requires": { + "safer-buffer": "~2.1.0" + } + }, + "asn1.js": { + "version": "5.4.1", + "resolved": "https://registry.npmjs.org/asn1.js/-/asn1.js-5.4.1.tgz", + "integrity": "sha512-+I//4cYPccV8LdmBLiX8CYvf9Sp3vQsrqu2QNXRcrbiWvcx/UdlFiqUJJzxRQxgsZmvhXhn4cSKeSmoFjVdupA==", + "requires": { + "bn.js": "^4.0.0", + "inherits": "^2.0.1", + "minimalistic-assert": "^1.0.0", + "safer-buffer": "^2.1.0" + }, + "dependencies": { + "bn.js": { + "version": "4.12.0", + "resolved": "https://registry.npmjs.org/bn.js/-/bn.js-4.12.0.tgz", + "integrity": "sha512-c98Bf3tPniI+scsdk237ku1Dc3ujXQTSgyiPUDEOe7tRkhrqridvh8klBv0HCEso1OLOYcHuCv/cS6DNxKH+ZA==" + } + } + }, + "assert": { + "version": "1.5.0", + "resolved": "https://registry.npmjs.org/assert/-/assert-1.5.0.tgz", + "integrity": "sha512-EDsgawzwoun2CZkCgtxJbv392v4nbk9XDD06zI+kQYoBM/3RBWLlEyJARDOmhAAosBjWACEkKL6S+lIZtcAubA==", + "requires": { + "object-assign": "^4.1.1", + "util": "0.10.3" + }, + "dependencies": { + "inherits": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.1.tgz", + "integrity": "sha1-sX0I0ya0Qj5Wjv9xn5GwscvfafE=" + }, + "util": { + "version": "0.10.3", + "resolved": "https://registry.npmjs.org/util/-/util-0.10.3.tgz", + "integrity": "sha1-evsa/lCAUkZInj23/g7TeTNqwPk=", + "requires": { + "inherits": "2.0.1" + } + } + } + }, + "assert-plus": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/assert-plus/-/assert-plus-1.0.0.tgz", + "integrity": "sha1-8S4PPF13sLHN2RRpQuTpbB5N1SU=" + }, + "asynckit": { + "version": "0.4.0", + "resolved": "https://registry.npmjs.org/asynckit/-/asynckit-0.4.0.tgz", + "integrity": "sha1-x57Zf380y48robyXkLzDZkdLS3k=" + }, + "available-typed-arrays": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/available-typed-arrays/-/available-typed-arrays-1.0.5.tgz", + "integrity": "sha512-DMD0KiN46eipeziST1LPP/STfDU0sufISXmjSgvVsoU2tqxctQeASejWcfNtxYKqETM1UxQ8sp2OrSBWpHY6sw==" + }, + "aws-sign2": { + "version": "0.7.0", + "resolved": "https://registry.npmjs.org/aws-sign2/-/aws-sign2-0.7.0.tgz", + "integrity": "sha1-tG6JCTSpWR8tL2+G1+ap8bP+dqg=" + }, + "aws4": { + "version": "1.11.0", + "resolved": "https://registry.npmjs.org/aws4/-/aws4-1.11.0.tgz", + "integrity": "sha512-xh1Rl34h6Fi1DC2WWKfxUTVqRsNnr6LsKz2+hfwDxQJWmrx8+c7ylaqBMcHfl1U1r2dsifOvKX3LQuLNZ+XSvA==" + }, + "balanced-match": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz", + "integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==" + }, + "base64-js": { + "version": "1.5.1", + "resolved": "https://registry.npmjs.org/base64-js/-/base64-js-1.5.1.tgz", + "integrity": "sha512-AKpaYlHn8t4SVbOHCy+b5+KKgvR4vrsD8vbvrbiQJps7fKDTkjkDry6ji0rUJjC0kzbNePLwzxq8iypo41qeWA==" + }, + "bcrypt-pbkdf": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/bcrypt-pbkdf/-/bcrypt-pbkdf-1.0.2.tgz", + "integrity": "sha1-pDAdOJtqQ/m2f/PKEaP2Y342Dp4=", + "requires": { + "tweetnacl": "^0.14.3" + } + }, + "bn.js": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/bn.js/-/bn.js-5.2.0.tgz", + "integrity": "sha512-D7iWRBvnZE8ecXiLj/9wbxH7Tk79fAh8IHaTNq1RWRixsS02W+5qS+iE9yq6RYl0asXx5tw0bLhmT5pIfbSquw==" + }, + "brace-expansion": { + "version": "1.1.11", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.11.tgz", + "integrity": "sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA==", + "requires": { + "balanced-match": "^1.0.0", + "concat-map": "0.0.1" + } + }, + "brorand": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/brorand/-/brorand-1.1.0.tgz", + "integrity": "sha1-EsJe/kCkXjwyPrhnWgoM5XsiNx8=" + }, + "browser-pack": { + "version": "6.1.0", + "resolved": "https://registry.npmjs.org/browser-pack/-/browser-pack-6.1.0.tgz", + "integrity": "sha512-erYug8XoqzU3IfcU8fUgyHqyOXqIE4tUTTQ+7mqUjQlvnXkOO6OlT9c/ZoJVHYoAaqGxr09CN53G7XIsO4KtWA==", + "requires": { + "JSONStream": "^1.0.3", + "combine-source-map": "~0.8.0", + "defined": "^1.0.0", + "safe-buffer": "^5.1.1", + "through2": "^2.0.0", + "umd": "^3.0.0" + } + }, + "browser-resolve": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/browser-resolve/-/browser-resolve-2.0.0.tgz", + "integrity": "sha512-7sWsQlYL2rGLy2IWm8WL8DCTJvYLc/qlOnsakDac87SOoCd16WLsaAMdCiAqsTNHIe+SXfaqyxyo6THoWqs8WQ==", + "requires": { + "resolve": "^1.17.0" + } + }, + "browserify": { + "version": "17.0.0", + "resolved": "https://registry.npmjs.org/browserify/-/browserify-17.0.0.tgz", + "integrity": "sha512-SaHqzhku9v/j6XsQMRxPyBrSP3gnwmE27gLJYZgMT2GeK3J0+0toN+MnuNYDfHwVGQfLiMZ7KSNSIXHemy905w==", + "requires": { + "JSONStream": "^1.0.3", + "assert": "^1.4.0", + "browser-pack": "^6.0.1", + "browser-resolve": "^2.0.0", + "browserify-zlib": "~0.2.0", + "buffer": "~5.2.1", + "cached-path-relative": "^1.0.0", + "concat-stream": "^1.6.0", + "console-browserify": "^1.1.0", + "constants-browserify": "~1.0.0", + "crypto-browserify": "^3.0.0", + "defined": "^1.0.0", + "deps-sort": "^2.0.1", + "domain-browser": "^1.2.0", + "duplexer2": "~0.1.2", + "events": "^3.0.0", + "glob": "^7.1.0", + "has": "^1.0.0", + "htmlescape": "^1.1.0", + "https-browserify": "^1.0.0", + "inherits": "~2.0.1", + "insert-module-globals": "^7.2.1", + "labeled-stream-splicer": "^2.0.0", + "mkdirp-classic": "^0.5.2", + "module-deps": "^6.2.3", + "os-browserify": "~0.3.0", + "parents": "^1.0.1", + "path-browserify": "^1.0.0", + "process": "~0.11.0", + "punycode": "^1.3.2", + "querystring-es3": "~0.2.0", + "read-only-stream": "^2.0.0", + "readable-stream": "^2.0.2", + "resolve": "^1.1.4", + "shasum-object": "^1.0.0", + "shell-quote": "^1.6.1", + "stream-browserify": "^3.0.0", + "stream-http": "^3.0.0", + "string_decoder": "^1.1.1", + "subarg": "^1.0.0", + "syntax-error": "^1.1.1", + "through2": "^2.0.0", + "timers-browserify": "^1.0.1", + "tty-browserify": "0.0.1", + "url": "~0.11.0", + "util": "~0.12.0", + "vm-browserify": "^1.0.0", + "xtend": "^4.0.0" + } + }, + "browserify-aes": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/browserify-aes/-/browserify-aes-1.2.0.tgz", + "integrity": "sha512-+7CHXqGuspUn/Sl5aO7Ea0xWGAtETPXNSAjHo48JfLdPWcMng33Xe4znFvQweqc/uzk5zSOI3H52CYnjCfb5hA==", + "requires": { + "buffer-xor": "^1.0.3", + "cipher-base": "^1.0.0", + "create-hash": "^1.1.0", + "evp_bytestokey": "^1.0.3", + "inherits": "^2.0.1", + "safe-buffer": "^5.0.1" + } + }, + "browserify-cipher": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/browserify-cipher/-/browserify-cipher-1.0.1.tgz", + "integrity": "sha512-sPhkz0ARKbf4rRQt2hTpAHqn47X3llLkUGn+xEJzLjwY8LRs2p0v7ljvI5EyoRO/mexrNunNECisZs+gw2zz1w==", + "requires": { + "browserify-aes": "^1.0.4", + "browserify-des": "^1.0.0", + "evp_bytestokey": "^1.0.0" + } + }, + "browserify-des": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/browserify-des/-/browserify-des-1.0.2.tgz", + "integrity": "sha512-BioO1xf3hFwz4kc6iBhI3ieDFompMhrMlnDFC4/0/vd5MokpuAc3R+LYbwTA9A5Yc9pq9UYPqffKpW2ObuwX5A==", + "requires": { + "cipher-base": "^1.0.1", + "des.js": "^1.0.0", + "inherits": "^2.0.1", + "safe-buffer": "^5.1.2" + } + }, + "browserify-rsa": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/browserify-rsa/-/browserify-rsa-4.1.0.tgz", + "integrity": "sha512-AdEER0Hkspgno2aR97SAf6vi0y0k8NuOpGnVH3O99rcA5Q6sh8QxcngtHuJ6uXwnfAXNM4Gn1Gb7/MV1+Ymbog==", + "requires": { + "bn.js": "^5.0.0", + "randombytes": "^2.0.1" + } + }, + "browserify-sign": { + "version": "4.2.1", + "resolved": "https://registry.npmjs.org/browserify-sign/-/browserify-sign-4.2.1.tgz", + "integrity": "sha512-/vrA5fguVAKKAVTNJjgSm1tRQDHUU6DbwO9IROu/0WAzC8PKhucDSh18J0RMvVeHAn5puMd+QHC2erPRNf8lmg==", + "requires": { + "bn.js": "^5.1.1", + "browserify-rsa": "^4.0.1", + "create-hash": "^1.2.0", + "create-hmac": "^1.1.7", + "elliptic": "^6.5.3", + "inherits": "^2.0.4", + "parse-asn1": "^5.1.5", + "readable-stream": "^3.6.0", + "safe-buffer": "^5.2.0" + }, + "dependencies": { + "readable-stream": { + "version": "3.6.0", + "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-3.6.0.tgz", + "integrity": "sha512-BViHy7LKeTz4oNnkcLJ+lVSL6vpiFeX6/d3oSH8zCW7UxP2onchk+vTGB143xuFjHS3deTgkKoXXymXqymiIdA==", + "requires": { + "inherits": "^2.0.3", + "string_decoder": "^1.1.1", + "util-deprecate": "^1.0.1" + } + } + } + }, + "browserify-zlib": { + "version": "0.2.0", + "resolved": "https://registry.npmjs.org/browserify-zlib/-/browserify-zlib-0.2.0.tgz", + "integrity": "sha512-Z942RysHXmJrhqk88FmKBVq/v5tqmSkDz7p54G/MGyjMnCFFnC79XWNbg+Vta8W6Wb2qtSZTSxIGkJrRpCFEiA==", + "requires": { + "pako": "~1.0.5" + } + }, + "buffer": { + "version": "5.2.1", + "resolved": "https://registry.npmjs.org/buffer/-/buffer-5.2.1.tgz", + "integrity": "sha512-c+Ko0loDaFfuPWiL02ls9Xd3GO3cPVmUobQ6t3rXNUk304u6hGq+8N/kFi+QEIKhzK3uwolVhLzszmfLmMLnqg==", + "requires": { + "base64-js": "^1.0.2", + "ieee754": "^1.1.4" + } + }, + "buffer-from": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/buffer-from/-/buffer-from-1.1.2.tgz", + "integrity": "sha512-E+XQCRwSbaaiChtv6k6Dwgc+bx+Bs6vuKJHHl5kox/BaKbhiXzqQOwK4cO22yElGp2OCmjwVhT3HmxgyPGnJfQ==" + }, + "buffer-xor": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/buffer-xor/-/buffer-xor-1.0.3.tgz", + "integrity": "sha1-JuYe0UIvtw3ULm42cp7VHYVf6Nk=" + }, + "builtin-status-codes": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/builtin-status-codes/-/builtin-status-codes-3.0.0.tgz", + "integrity": "sha1-hZgoeOIbmOHGZCXgPQF0eI9Wnug=" + }, + "cached-path-relative": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/cached-path-relative/-/cached-path-relative-1.0.2.tgz", + "integrity": "sha512-5r2GqsoEb4qMTTN9J+WzXfjov+hjxT+j3u5K+kIVNIwAd99DLCJE9pBIMP1qVeybV6JiijL385Oz0DcYxfbOIg==" + }, + "call-bind": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/call-bind/-/call-bind-1.0.2.tgz", + "integrity": "sha512-7O+FbCihrB5WGbFYesctwmTKae6rOiIzmz1icreWJ+0aA7LJfuqhEso2T9ncpcFtzMQtzXf2QGGueWJGTYsqrA==", + "requires": { + "function-bind": "^1.1.1", + "get-intrinsic": "^1.0.2" + } + }, + "candygraph": { + "version": "0.4.2", + "resolved": "https://registry.npmjs.org/candygraph/-/candygraph-0.4.2.tgz", + "integrity": "sha512-m6dguNPgTyRGlIDFW6ggDccJQvOAmsRwZDulP5T9rzxxLBQ+FrRrt3HxWTdXfmvw9jlwtUqeLsf32mlfTrDOFw==", + "requires": { + "gl-matrix": "^3.3.0", + "regl": "^1.7.0" + } + }, + "caseless": { + "version": "0.12.0", + "resolved": "https://registry.npmjs.org/caseless/-/caseless-0.12.0.tgz", + "integrity": "sha1-G2gcIf+EAzyCZUMJBolCDRhxUdw=" + }, + "cipher-base": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/cipher-base/-/cipher-base-1.0.4.tgz", + "integrity": "sha512-Kkht5ye6ZGmwv40uUDZztayT2ThLQGfnj/T71N/XzeZeo3nf8foyW7zGTsPYkEya3m5f3cAypH+qe7YOrM1U2Q==", + "requires": { + "inherits": "^2.0.1", + "safe-buffer": "^5.0.1" + } + }, + "combine-source-map": { + "version": "0.8.0", + "resolved": "https://registry.npmjs.org/combine-source-map/-/combine-source-map-0.8.0.tgz", + "integrity": "sha1-pY0N8ELBhvz4IqjoAV9UUNLXmos=", + "requires": { + "convert-source-map": "~1.1.0", + "inline-source-map": "~0.6.0", + "lodash.memoize": "~3.0.3", + "source-map": "~0.5.3" + } + }, + "combined-stream": { + "version": "1.0.8", + "resolved": "https://registry.npmjs.org/combined-stream/-/combined-stream-1.0.8.tgz", + "integrity": "sha512-FQN4MRfuJeHf7cBbBMJFXhKSDq+2kAArBlmRBvcvFE5BB1HZKXtSFASDhdlz9zOYwxh8lDdnvmMOe/+5cdoEdg==", + "requires": { + "delayed-stream": "~1.0.0" + } + }, + "concat-map": { + "version": "0.0.1", + "resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz", + "integrity": "sha1-2Klr13/Wjfd5OnMDajug1UBdR3s=" + }, + "concat-stream": { + "version": "1.6.2", + "resolved": "https://registry.npmjs.org/concat-stream/-/concat-stream-1.6.2.tgz", + "integrity": "sha512-27HBghJxjiZtIk3Ycvn/4kbJk/1uZuJFfuPEns6LaEvpvG1f0hTea8lilrouyo9mVc2GWdcEZ8OLoGmSADlrCw==", + "requires": { + "buffer-from": "^1.0.0", + "inherits": "^2.0.3", + "readable-stream": "^2.2.2", + "typedarray": "^0.0.6" + } + }, + "console-browserify": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/console-browserify/-/console-browserify-1.2.0.tgz", + "integrity": "sha512-ZMkYO/LkF17QvCPqM0gxw8yUzigAOZOSWSHg91FH6orS7vcEj5dVZTidN2fQ14yBSdg97RqhSNwLUXInd52OTA==" + }, + "constants-browserify": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/constants-browserify/-/constants-browserify-1.0.0.tgz", + "integrity": "sha1-wguW2MYXdIqvHBYCF2DNJ/y4y3U=" + }, + "convert-source-map": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/convert-source-map/-/convert-source-map-1.1.3.tgz", + "integrity": "sha1-SCnId+n+SbMWHzvzZziI4gRpmGA=" + }, + "core-util-is": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/core-util-is/-/core-util-is-1.0.3.tgz", + "integrity": "sha512-ZQBvi1DcpJ4GDqanjucZ2Hj3wEO5pZDS89BWbkcrvdxksJorwUDDZamX9ldFkp9aw2lmBDLgkObEA4DWNJ9FYQ==" + }, + "create-ecdh": { + "version": "4.0.4", + "resolved": "https://registry.npmjs.org/create-ecdh/-/create-ecdh-4.0.4.tgz", + "integrity": "sha512-mf+TCx8wWc9VpuxfP2ht0iSISLZnt0JgWlrOKZiNqyUZWnjIaCIVNQArMHnCZKfEYRg6IM7A+NeJoN8gf/Ws0A==", + "requires": { + "bn.js": "^4.1.0", + "elliptic": "^6.5.3" + }, + "dependencies": { + "bn.js": { + "version": "4.12.0", + "resolved": "https://registry.npmjs.org/bn.js/-/bn.js-4.12.0.tgz", + "integrity": "sha512-c98Bf3tPniI+scsdk237ku1Dc3ujXQTSgyiPUDEOe7tRkhrqridvh8klBv0HCEso1OLOYcHuCv/cS6DNxKH+ZA==" + } + } + }, + "create-hash": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/create-hash/-/create-hash-1.2.0.tgz", + "integrity": "sha512-z00bCGNHDG8mHAkP7CtT1qVu+bFQUPjYq/4Iv3C3kWjTFV10zIjfSoeqXo9Asws8gwSHDGj/hl2u4OGIjapeCg==", + "requires": { + "cipher-base": "^1.0.1", + "inherits": "^2.0.1", + "md5.js": "^1.3.4", + "ripemd160": "^2.0.1", + "sha.js": "^2.4.0" + } + }, + "create-hmac": { + "version": "1.1.7", + "resolved": "https://registry.npmjs.org/create-hmac/-/create-hmac-1.1.7.tgz", + "integrity": "sha512-MJG9liiZ+ogc4TzUwuvbER1JRdgvUFSB5+VR/g5h82fGaIRWMWddtKBHi7/sVhfjQZ6SehlyhvQYrcYkaUIpLg==", + "requires": { + "cipher-base": "^1.0.3", + "create-hash": "^1.1.0", + "inherits": "^2.0.1", + "ripemd160": "^2.0.0", + "safe-buffer": "^5.0.1", + "sha.js": "^2.4.8" + } + }, + "crypto-browserify": { + "version": "3.12.0", + "resolved": "https://registry.npmjs.org/crypto-browserify/-/crypto-browserify-3.12.0.tgz", + "integrity": "sha512-fz4spIh+znjO2VjL+IdhEpRJ3YN6sMzITSBijk6FK2UvTqruSQW+/cCZTSNsMiZNvUeq0CqurF+dAbyiGOY6Wg==", + "requires": { + "browserify-cipher": "^1.0.0", + "browserify-sign": "^4.0.0", + "create-ecdh": "^4.0.0", + "create-hash": "^1.1.0", + "create-hmac": "^1.1.0", + "diffie-hellman": "^5.0.0", + "inherits": "^2.0.1", + "pbkdf2": "^3.0.3", + "public-encrypt": "^4.0.0", + "randombytes": "^2.0.0", + "randomfill": "^1.0.3" + } + }, + "dash-ast": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/dash-ast/-/dash-ast-1.0.0.tgz", + "integrity": "sha512-Vy4dx7gquTeMcQR/hDkYLGUnwVil6vk4FOOct+djUnHOUWt+zJPJAaRIXaAFkPXtJjvlY7o3rfRu0/3hpnwoUA==" + }, + "dashdash": { + "version": "1.14.1", + "resolved": "https://registry.npmjs.org/dashdash/-/dashdash-1.14.1.tgz", + "integrity": "sha1-hTz6D3y+L+1d4gMmuN1YEDX24vA=", + "requires": { + "assert-plus": "^1.0.0" + } + }, + "define-properties": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/define-properties/-/define-properties-1.1.3.tgz", + "integrity": "sha512-3MqfYKj2lLzdMSf8ZIZE/V+Zuy+BgD6f164e8K2w7dgnpKArBDerGYpM46IYYcjnkdPNMjPk9A6VFB8+3SKlXQ==", + "requires": { + "object-keys": "^1.0.12" + } + }, + "defined": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/defined/-/defined-1.0.0.tgz", + "integrity": "sha1-yY2bzvdWdBiOEQlpFRGZ45sfppM=" + }, + "delayed-stream": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/delayed-stream/-/delayed-stream-1.0.0.tgz", + "integrity": "sha1-3zrhmayt+31ECqrgsp4icrJOxhk=" + }, + "deps-sort": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/deps-sort/-/deps-sort-2.0.1.tgz", + "integrity": "sha512-1orqXQr5po+3KI6kQb9A4jnXT1PBwggGl2d7Sq2xsnOeI9GPcE/tGcF9UiSZtZBM7MukY4cAh7MemS6tZYipfw==", + "requires": { + "JSONStream": "^1.0.3", + "shasum-object": "^1.0.0", + "subarg": "^1.0.0", + "through2": "^2.0.0" + } + }, + "des.js": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/des.js/-/des.js-1.0.1.tgz", + "integrity": "sha512-Q0I4pfFrv2VPd34/vfLrFOoRmlYj3OV50i7fskps1jZWK1kApMWWT9G6RRUeYedLcBDIhnSDaUvJMb3AhUlaEA==", + "requires": { + "inherits": "^2.0.1", + "minimalistic-assert": "^1.0.0" + } + }, + "detective": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/detective/-/detective-5.2.0.tgz", + "integrity": "sha512-6SsIx+nUUbuK0EthKjv0zrdnajCCXVYGmbYYiYjFVpzcjwEs/JMDZ8tPRG29J/HhN56t3GJp2cGSWDRjjot8Pg==", + "requires": { + "acorn-node": "^1.6.1", + "defined": "^1.0.0", + "minimist": "^1.1.1" + } + }, + "diffie-hellman": { + "version": "5.0.3", + "resolved": "https://registry.npmjs.org/diffie-hellman/-/diffie-hellman-5.0.3.tgz", + "integrity": "sha512-kqag/Nl+f3GwyK25fhUMYj81BUOrZ9IuJsjIcDE5icNM9FJHAVm3VcUDxdLPoQtTuUylWm6ZIknYJwwaPxsUzg==", + "requires": { + "bn.js": "^4.1.0", + "miller-rabin": "^4.0.0", + "randombytes": "^2.0.0" + }, + "dependencies": { + "bn.js": { + "version": "4.12.0", + "resolved": "https://registry.npmjs.org/bn.js/-/bn.js-4.12.0.tgz", + "integrity": "sha512-c98Bf3tPniI+scsdk237ku1Dc3ujXQTSgyiPUDEOe7tRkhrqridvh8klBv0HCEso1OLOYcHuCv/cS6DNxKH+ZA==" + } + } + }, + "domain-browser": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/domain-browser/-/domain-browser-1.2.0.tgz", + "integrity": "sha512-jnjyiM6eRyZl2H+W8Q/zLMA481hzi0eszAaBUzIVnmYVDBbnLxVNnfu1HgEBvCbL+71FrxMl3E6lpKH7Ge3OXA==" + }, + "duplexer2": { + "version": "0.1.4", + "resolved": "https://registry.npmjs.org/duplexer2/-/duplexer2-0.1.4.tgz", + "integrity": "sha1-ixLauHjA1p4+eJEFFmKjL8a93ME=", + "requires": { + "readable-stream": "^2.0.2" + } + }, + "ecc-jsbn": { + "version": "0.1.2", + "resolved": "https://registry.npmjs.org/ecc-jsbn/-/ecc-jsbn-0.1.2.tgz", + "integrity": "sha1-OoOpBOVDUyh4dMVkt1SThoSamMk=", + "requires": { + "jsbn": "~0.1.0", + "safer-buffer": "^2.1.0" + } + }, + "elliptic": { + "version": "6.5.4", + "resolved": "https://registry.npmjs.org/elliptic/-/elliptic-6.5.4.tgz", + "integrity": "sha512-iLhC6ULemrljPZb+QutR5TQGB+pdW6KGD5RSegS+8sorOZT+rdQFbsQFJgvN3eRqNALqJer4oQ16YvJHlU8hzQ==", + "requires": { + "bn.js": "^4.11.9", + "brorand": "^1.1.0", + "hash.js": "^1.0.0", + "hmac-drbg": "^1.0.1", + "inherits": "^2.0.4", + "minimalistic-assert": "^1.0.1", + "minimalistic-crypto-utils": "^1.0.1" + }, + "dependencies": { + "bn.js": { + "version": "4.12.0", + "resolved": "https://registry.npmjs.org/bn.js/-/bn.js-4.12.0.tgz", + "integrity": "sha512-c98Bf3tPniI+scsdk237ku1Dc3ujXQTSgyiPUDEOe7tRkhrqridvh8klBv0HCEso1OLOYcHuCv/cS6DNxKH+ZA==" + } + } + }, + "es-abstract": { + "version": "1.19.1", + "resolved": "https://registry.npmjs.org/es-abstract/-/es-abstract-1.19.1.tgz", + "integrity": "sha512-2vJ6tjA/UfqLm2MPs7jxVybLoB8i1t1Jd9R3kISld20sIxPcTbLuggQOUxeWeAvIUkduv/CfMjuh4WmiXr2v9w==", + "requires": { + "call-bind": "^1.0.2", + "es-to-primitive": "^1.2.1", + "function-bind": "^1.1.1", + "get-intrinsic": "^1.1.1", + "get-symbol-description": "^1.0.0", + "has": "^1.0.3", + "has-symbols": "^1.0.2", + "internal-slot": "^1.0.3", + "is-callable": "^1.2.4", + "is-negative-zero": "^2.0.1", + "is-regex": "^1.1.4", + "is-shared-array-buffer": "^1.0.1", + "is-string": "^1.0.7", + "is-weakref": "^1.0.1", + "object-inspect": "^1.11.0", + "object-keys": "^1.1.1", + "object.assign": "^4.1.2", + "string.prototype.trimend": "^1.0.4", + "string.prototype.trimstart": "^1.0.4", + "unbox-primitive": "^1.0.1" + } + }, + "es-to-primitive": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/es-to-primitive/-/es-to-primitive-1.2.1.tgz", + "integrity": "sha512-QCOllgZJtaUo9miYBcLChTUaHNjJF3PYs1VidD7AwiEj1kYxKeQTctLAezAOH5ZKRH0g2IgPn6KwB4IT8iRpvA==", + "requires": { + "is-callable": "^1.1.4", + "is-date-object": "^1.0.1", + "is-symbol": "^1.0.2" + } + }, + "esbuild": { + "version": "0.13.12", + "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.13.12.tgz", + "integrity": "sha512-vTKKUt+yoz61U/BbrnmlG9XIjwpdIxmHB8DlPR0AAW6OdS+nBQBci6LUHU2q9WbBobMEIQxxDpKbkmOGYvxsow==", + "requires": { + "esbuild-android-arm64": "0.13.12", + "esbuild-darwin-64": "0.13.12", + "esbuild-darwin-arm64": "0.13.12", + "esbuild-freebsd-64": "0.13.12", + "esbuild-freebsd-arm64": "0.13.12", + "esbuild-linux-32": "0.13.12", + "esbuild-linux-64": "0.13.12", + "esbuild-linux-arm": "0.13.12", + "esbuild-linux-arm64": "0.13.12", + "esbuild-linux-mips64le": "0.13.12", + "esbuild-linux-ppc64le": "0.13.12", + "esbuild-netbsd-64": "0.13.12", + "esbuild-openbsd-64": "0.13.12", + "esbuild-sunos-64": "0.13.12", + "esbuild-windows-32": "0.13.12", + "esbuild-windows-64": "0.13.12", + "esbuild-windows-arm64": "0.13.12" + } + }, + "esbuild-android-arm64": { + "version": "0.13.12", + "resolved": "https://registry.npmjs.org/esbuild-android-arm64/-/esbuild-android-arm64-0.13.12.tgz", + "integrity": "sha512-TSVZVrb4EIXz6KaYjXfTzPyyRpXV5zgYIADXtQsIenjZ78myvDGaPi11o4ZSaHIwFHsuwkB6ne5SZRBwAQ7maw==", + "optional": true + }, + "esbuild-darwin-64": { + "version": "0.13.12", + "resolved": "https://registry.npmjs.org/esbuild-darwin-64/-/esbuild-darwin-64-0.13.12.tgz", + "integrity": "sha512-c51C+N+UHySoV2lgfWSwwmlnLnL0JWj/LzuZt9Ltk9ub1s2Y8cr6SQV5W3mqVH1egUceew6KZ8GyI4nwu+fhsw==", + "optional": true + }, + "esbuild-darwin-arm64": { + "version": "0.13.12", + "resolved": "https://registry.npmjs.org/esbuild-darwin-arm64/-/esbuild-darwin-arm64-0.13.12.tgz", + "integrity": "sha512-JvAMtshP45Hd8A8wOzjkY1xAnTKTYuP/QUaKp5eUQGX+76GIie3fCdUUr2ZEKdvpSImNqxiZSIMziEiGB5oUmQ==", + "optional": true + }, + "esbuild-freebsd-64": { + "version": "0.13.12", + "resolved": "https://registry.npmjs.org/esbuild-freebsd-64/-/esbuild-freebsd-64-0.13.12.tgz", + "integrity": "sha512-r6On/Skv9f0ZjTu6PW5o7pdXr8aOgtFOEURJZYf1XAJs0IQ+gW+o1DzXjVkIoT+n1cm3N/t1KRJfX71MPg/ZUA==", + "optional": true + }, + "esbuild-freebsd-arm64": { + "version": "0.13.12", + "resolved": "https://registry.npmjs.org/esbuild-freebsd-arm64/-/esbuild-freebsd-arm64-0.13.12.tgz", + "integrity": "sha512-F6LmI2Q1gii073kmBE3NOTt/6zLL5zvZsxNLF8PMAwdHc+iBhD1vzfI8uQZMJA1IgXa3ocr3L3DJH9fLGXy6Yw==", + "optional": true + }, + "esbuild-linux-32": { + "version": "0.13.12", + "resolved": "https://registry.npmjs.org/esbuild-linux-32/-/esbuild-linux-32-0.13.12.tgz", + "integrity": "sha512-U1UZwG3UIwF7/V4tCVAo/nkBV9ag5KJiJTt+gaCmLVWH3bPLX7y+fNlhIWZy8raTMnXhMKfaTvWZ9TtmXzvkuQ==", + "optional": true + }, + "esbuild-linux-64": { + "version": "0.13.12", + "resolved": "https://registry.npmjs.org/esbuild-linux-64/-/esbuild-linux-64-0.13.12.tgz", + "integrity": "sha512-YpXSwtu2NxN3N4ifJxEdsgd6Q5d8LYqskrAwjmoCT6yQnEHJSF5uWcxv783HWN7lnGpJi9KUtDvYsnMdyGw71Q==", + "optional": true + }, + "esbuild-linux-arm": { + "version": "0.13.12", + "resolved": "https://registry.npmjs.org/esbuild-linux-arm/-/esbuild-linux-arm-0.13.12.tgz", + "integrity": "sha512-SyiT/JKxU6J+DY2qUiSLZJqCAftIt3uoGejZ0HDnUM2MGJqEGSGh7p1ecVL2gna3PxS4P+j6WAehCwgkBPXNIw==", + "optional": true + }, + "esbuild-linux-arm64": { + "version": "0.13.12", + "resolved": "https://registry.npmjs.org/esbuild-linux-arm64/-/esbuild-linux-arm64-0.13.12.tgz", + "integrity": "sha512-sgDNb8kb3BVodtAlcFGgwk+43KFCYjnFOaOfJibXnnIojNWuJHpL6aQJ4mumzNWw8Rt1xEtDQyuGK9f+Y24jGA==", + "optional": true + }, + "esbuild-linux-mips64le": { + "version": "0.13.12", + "resolved": "https://registry.npmjs.org/esbuild-linux-mips64le/-/esbuild-linux-mips64le-0.13.12.tgz", + "integrity": "sha512-qQJHlZBG+QwVIA8AbTEtbvF084QgDi4DaUsUnA+EolY1bxrG+UyOuGflM2ZritGhfS/k7THFjJbjH2wIeoKA2g==", + "optional": true + }, + "esbuild-linux-ppc64le": { + "version": "0.13.12", + "resolved": "https://registry.npmjs.org/esbuild-linux-ppc64le/-/esbuild-linux-ppc64le-0.13.12.tgz", + "integrity": "sha512-2dSnm1ldL7Lppwlo04CGQUpwNn5hGqXI38OzaoPOkRsBRWFBozyGxTFSee/zHFS+Pdh3b28JJbRK3owrrRgWNw==", + "optional": true + }, + "esbuild-netbsd-64": { + "version": "0.13.12", + "resolved": "https://registry.npmjs.org/esbuild-netbsd-64/-/esbuild-netbsd-64-0.13.12.tgz", + "integrity": "sha512-D4raxr02dcRiQNbxOLzpqBzcJNFAdsDNxjUbKkDMZBkL54Z0vZh4LRndycdZAMcIdizC/l/Yp/ZsBdAFxc5nbA==", + "optional": true + }, + "esbuild-openbsd-64": { + "version": "0.13.12", + "resolved": "https://registry.npmjs.org/esbuild-openbsd-64/-/esbuild-openbsd-64-0.13.12.tgz", + "integrity": "sha512-KuLCmYMb2kh05QuPJ+va60bKIH5wHL8ypDkmpy47lzwmdxNsuySeCMHuTv5o2Af1RUn5KLO5ZxaZeq4GEY7DaQ==", + "optional": true + }, + "esbuild-sunos-64": { + "version": "0.13.12", + "resolved": "https://registry.npmjs.org/esbuild-sunos-64/-/esbuild-sunos-64-0.13.12.tgz", + "integrity": "sha512-jBsF+e0woK3miKI8ufGWKG3o3rY9DpHvCVRn5eburMIIE+2c+y3IZ1srsthKyKI6kkXLvV4Cf/E7w56kLipMXw==", + "optional": true + }, + "esbuild-windows-32": { + "version": "0.13.12", + "resolved": "https://registry.npmjs.org/esbuild-windows-32/-/esbuild-windows-32-0.13.12.tgz", + "integrity": "sha512-L9m4lLFQrFeR7F+eLZXG82SbXZfUhyfu6CexZEil6vm+lc7GDCE0Q8DiNutkpzjv1+RAbIGVva9muItQ7HVTkQ==", + "optional": true + }, + "esbuild-windows-64": { + "version": "0.13.12", + "resolved": "https://registry.npmjs.org/esbuild-windows-64/-/esbuild-windows-64-0.13.12.tgz", + "integrity": "sha512-k4tX4uJlSbSkfs78W5d9+I9gpd+7N95W7H2bgOMFPsYREVJs31+Q2gLLHlsnlY95zBoPQMIzHooUIsixQIBjaQ==", + "optional": true + }, + "esbuild-windows-arm64": { + "version": "0.13.12", + "resolved": "https://registry.npmjs.org/esbuild-windows-arm64/-/esbuild-windows-arm64-0.13.12.tgz", + "integrity": "sha512-2tTv/BpYRIvuwHpp2M960nG7uvL+d78LFW/ikPItO+2GfK51CswIKSetSpDii+cjz8e9iSPgs+BU4o8nWICBwQ==", + "optional": true + }, + "events": { + "version": "3.3.0", + "resolved": "https://registry.npmjs.org/events/-/events-3.3.0.tgz", + "integrity": "sha512-mQw+2fkQbALzQ7V0MY0IqdnXNOeTtP4r0lN9z7AAawCXgqea7bDii20AYrIBrFd/Hx0M2Ocz6S111CaFkUcb0Q==" + }, + "evp_bytestokey": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/evp_bytestokey/-/evp_bytestokey-1.0.3.tgz", + "integrity": "sha512-/f2Go4TognH/KvCISP7OUsHn85hT9nUkxxA9BEWxFn+Oj9o8ZNLm/40hdlgSLyuOimsrTKLUMEorQexp/aPQeA==", + "requires": { + "md5.js": "^1.3.4", + "safe-buffer": "^5.1.1" + } + }, + "extend": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/extend/-/extend-3.0.2.tgz", + "integrity": "sha512-fjquC59cD7CyW6urNXK0FBufkZcoiGG80wTuPujX590cB5Ttln20E2UB4S/WARVqhXffZl2LNgS+gQdPIIim/g==" + }, + "extsprintf": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/extsprintf/-/extsprintf-1.3.0.tgz", + "integrity": "sha1-lpGEQOMEGnpBT4xS48V06zw+HgU=" + }, + "fast-deep-equal": { + "version": "3.1.3", + "resolved": "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz", + "integrity": "sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==" + }, + "fast-json-stable-stringify": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/fast-json-stable-stringify/-/fast-json-stable-stringify-2.1.0.tgz", + "integrity": "sha512-lhd/wF+Lk98HZoTCtlVraHtfh5XYijIjalXck7saUtuanSDyLMxnHhSXEDJqHxD7msR8D0uCmqlkwjCV8xvwHw==" + }, + "fast-safe-stringify": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/fast-safe-stringify/-/fast-safe-stringify-2.1.1.tgz", + "integrity": "sha512-W+KJc2dmILlPplD/H4K9l9LcAHAfPtP6BY84uVLXQ6Evcz9Lcg33Y2z1IVblT6xdY54PXYVHEv+0Wpq8Io6zkA==" + }, + "foreach": { + "version": "2.0.5", + "resolved": "https://registry.npmjs.org/foreach/-/foreach-2.0.5.tgz", + "integrity": "sha1-C+4AUBiusmDQo6865ljdATbsG5k=" + }, + "forever-agent": { + "version": "0.6.1", + "resolved": "https://registry.npmjs.org/forever-agent/-/forever-agent-0.6.1.tgz", + "integrity": "sha1-+8cfDEGt6zf5bFd60e1C2P2sypE=" + }, + "form-data": { + "version": "2.3.3", + "resolved": "https://registry.npmjs.org/form-data/-/form-data-2.3.3.tgz", + "integrity": "sha512-1lLKB2Mu3aGP1Q/2eCOx0fNbRMe7XdwktwOruhfqqd0rIJWwN4Dh+E3hrPSlDCXnSR7UtZ1N38rVXm+6+MEhJQ==", + "requires": { + "asynckit": "^0.4.0", + "combined-stream": "^1.0.6", + "mime-types": "^2.1.12" + } + }, + "fs.realpath": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/fs.realpath/-/fs.realpath-1.0.0.tgz", + "integrity": "sha1-FQStJSMVjKpA20onh8sBQRmU6k8=" + }, + "function-bind": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.1.tgz", + "integrity": "sha512-yIovAzMX49sF8Yl58fSCWJ5svSLuaibPxXQJFLmBObTuCr0Mf1KiPopGM9NiFjiYBCbfaa2Fh6breQ6ANVTI0A==" + }, + "get-assigned-identifiers": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/get-assigned-identifiers/-/get-assigned-identifiers-1.2.0.tgz", + "integrity": "sha512-mBBwmeGTrxEMO4pMaaf/uUEFHnYtwr8FTe8Y/mer4rcV/bye0qGm6pw1bGZFGStxC5O76c5ZAVBGnqHmOaJpdQ==" + }, + "get-intrinsic": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/get-intrinsic/-/get-intrinsic-1.1.1.tgz", + "integrity": "sha512-kWZrnVM42QCiEA2Ig1bG8zjoIMOgxWwYCEeNdwY6Tv/cOSeGpcoX4pXHfKUxNKVoArnrEr2e9srnAxxGIraS9Q==", + "requires": { + "function-bind": "^1.1.1", + "has": "^1.0.3", + "has-symbols": "^1.0.1" + } + }, + "get-symbol-description": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/get-symbol-description/-/get-symbol-description-1.0.0.tgz", + "integrity": "sha512-2EmdH1YvIQiZpltCNgkuiUnyukzxM/R6NDJX31Ke3BG1Nq5b0S2PhX59UKi9vZpPDQVdqn+1IcaAwnzTT5vCjw==", + "requires": { + "call-bind": "^1.0.2", + "get-intrinsic": "^1.1.1" + } + }, + "getpass": { + "version": "0.1.7", + "resolved": "https://registry.npmjs.org/getpass/-/getpass-0.1.7.tgz", + "integrity": "sha1-Xv+OPmhNVprkyysSgmBOi6YhSfo=", + "requires": { + "assert-plus": "^1.0.0" + } + }, + "gl-matrix": { + "version": "3.4.3", + "resolved": "https://registry.npmjs.org/gl-matrix/-/gl-matrix-3.4.3.tgz", + "integrity": "sha512-wcCp8vu8FT22BnvKVPjXa/ICBWRq/zjFfdofZy1WSpQZpphblv12/bOQLBC1rMM7SGOFS9ltVmKOHil5+Ml7gA==" + }, + "glob": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/glob/-/glob-7.2.0.tgz", + "integrity": "sha512-lmLf6gtyrPq8tTjSmrO94wBeQbFR3HbLHbuyD69wuyQkImp2hWqMGB47OX65FBkPffO641IP9jWa1z4ivqG26Q==", + "requires": { + "fs.realpath": "^1.0.0", + "inflight": "^1.0.4", + "inherits": "2", + "minimatch": "^3.0.4", + "once": "^1.3.0", + "path-is-absolute": "^1.0.0" + } + }, + "har-schema": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/har-schema/-/har-schema-2.0.0.tgz", + "integrity": "sha1-qUwiJOvKwEeCoNkDVSHyRzW37JI=" + }, + "har-validator": { + "version": "5.1.5", + "resolved": "https://registry.npmjs.org/har-validator/-/har-validator-5.1.5.tgz", + "integrity": "sha512-nmT2T0lljbxdQZfspsno9hgrG3Uir6Ks5afism62poxqBM6sDnMEuPmzTq8XN0OEwqKLLdh1jQI3qyE66Nzb3w==", + "requires": { + "ajv": "^6.12.3", + "har-schema": "^2.0.0" + } + }, + "has": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/has/-/has-1.0.3.tgz", + "integrity": "sha512-f2dvO0VU6Oej7RkWJGrehjbzMAjFp5/VKPp5tTpWIV4JHHZK1/BxbFRtf/siA2SWTe09caDmVtYYzWEIbBS4zw==", + "requires": { + "function-bind": "^1.1.1" + } + }, + "has-bigints": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/has-bigints/-/has-bigints-1.0.1.tgz", + "integrity": "sha512-LSBS2LjbNBTf6287JEbEzvJgftkF5qFkmCo9hDRpAzKhUOlJ+hx8dd4USs00SgsUNwc4617J9ki5YtEClM2ffA==" + }, + "has-symbols": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/has-symbols/-/has-symbols-1.0.2.tgz", + "integrity": "sha512-chXa79rL/UC2KlX17jo3vRGz0azaWEx5tGqZg5pO3NUyEJVB17dMruQlzCCOfUvElghKcm5194+BCRvi2Rv/Gw==" + }, + "has-tostringtag": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/has-tostringtag/-/has-tostringtag-1.0.0.tgz", + "integrity": "sha512-kFjcSNhnlGV1kyoGk7OXKSawH5JOb/LzUc5w9B02hOTO0dfFRjbHQKvg1d6cf3HbeUmtU9VbbV3qzZ2Teh97WQ==", + "requires": { + "has-symbols": "^1.0.2" + } + }, + "hash-base": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/hash-base/-/hash-base-3.1.0.tgz", + "integrity": "sha512-1nmYp/rhMDiE7AYkDw+lLwlAzz0AntGIe51F3RfFfEqyQ3feY2eI/NcwC6umIQVOASPMsWJLJScWKSSvzL9IVA==", + "requires": { + "inherits": "^2.0.4", + "readable-stream": "^3.6.0", + "safe-buffer": "^5.2.0" + }, + "dependencies": { + "readable-stream": { + "version": "3.6.0", + "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-3.6.0.tgz", + "integrity": "sha512-BViHy7LKeTz4oNnkcLJ+lVSL6vpiFeX6/d3oSH8zCW7UxP2onchk+vTGB143xuFjHS3deTgkKoXXymXqymiIdA==", + "requires": { + "inherits": "^2.0.3", + "string_decoder": "^1.1.1", + "util-deprecate": "^1.0.1" + } + } + } + }, + "hash.js": { + "version": "1.1.7", + "resolved": "https://registry.npmjs.org/hash.js/-/hash.js-1.1.7.tgz", + "integrity": "sha512-taOaskGt4z4SOANNseOviYDvjEJinIkRgmp7LbKP2YTTmVxWBl87s/uzK9r+44BclBSp2X7K1hqeNfz9JbBeXA==", + "requires": { + "inherits": "^2.0.3", + "minimalistic-assert": "^1.0.1" + } + }, + "hmac-drbg": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/hmac-drbg/-/hmac-drbg-1.0.1.tgz", + "integrity": "sha1-0nRXAQJabHdabFRXk+1QL8DGSaE=", + "requires": { + "hash.js": "^1.0.3", + "minimalistic-assert": "^1.0.0", + "minimalistic-crypto-utils": "^1.0.1" + } + }, + "htmlescape": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/htmlescape/-/htmlescape-1.1.1.tgz", + "integrity": "sha1-OgPtwiFLyjtmQko+eVk0lQnLA1E=" + }, + "http-signature": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/http-signature/-/http-signature-1.2.0.tgz", + "integrity": "sha1-muzZJRFHcvPZW2WmCruPfBj7rOE=", + "requires": { + "assert-plus": "^1.0.0", + "jsprim": "^1.2.2", + "sshpk": "^1.7.0" + } + }, + "https-browserify": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/https-browserify/-/https-browserify-1.0.0.tgz", + "integrity": "sha1-7AbBDgo0wPL68Zn3/X/Hj//QPHM=" + }, + "ieee754": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/ieee754/-/ieee754-1.2.1.tgz", + "integrity": "sha512-dcyqhDvX1C46lXZcVqCpK+FtMRQVdIMN6/Df5js2zouUsqG7I6sFxitIC+7KYK29KdXOLHdu9zL4sFnoVQnqaA==" + }, + "inflight": { + "version": "1.0.6", + "resolved": "https://registry.npmjs.org/inflight/-/inflight-1.0.6.tgz", + "integrity": "sha1-Sb1jMdfQLQwJvJEKEHW6gWW1bfk=", + "requires": { + "once": "^1.3.0", + "wrappy": "1" + } + }, + "inherits": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.4.tgz", + "integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==" + }, + "inline-source-map": { + "version": "0.6.2", + "resolved": "https://registry.npmjs.org/inline-source-map/-/inline-source-map-0.6.2.tgz", + "integrity": "sha1-+Tk0ccGKedFyT4Y/o4tYY3Ct4qU=", + "requires": { + "source-map": "~0.5.3" + } + }, + "insert-module-globals": { + "version": "7.2.1", + "resolved": "https://registry.npmjs.org/insert-module-globals/-/insert-module-globals-7.2.1.tgz", + "integrity": "sha512-ufS5Qq9RZN+Bu899eA9QCAYThY+gGW7oRkmb0vC93Vlyu/CFGcH0OYPEjVkDXA5FEbTt1+VWzdoOD3Ny9N+8tg==", + "requires": { + "JSONStream": "^1.0.3", + "acorn-node": "^1.5.2", + "combine-source-map": "^0.8.0", + "concat-stream": "^1.6.1", + "is-buffer": "^1.1.0", + "path-is-absolute": "^1.0.1", + "process": "~0.11.0", + "through2": "^2.0.0", + "undeclared-identifiers": "^1.1.2", + "xtend": "^4.0.0" + } + }, + "internal-slot": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/internal-slot/-/internal-slot-1.0.3.tgz", + "integrity": "sha512-O0DB1JC/sPyZl7cIo78n5dR7eUSwwpYPiXRhTzNxZVAMUuB8vlnRFyLxdrVToks6XPLVnFfbzaVd5WLjhgg+vA==", + "requires": { + "get-intrinsic": "^1.1.0", + "has": "^1.0.3", + "side-channel": "^1.0.4" + } + }, + "is-arguments": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/is-arguments/-/is-arguments-1.1.1.tgz", + "integrity": "sha512-8Q7EARjzEnKpt/PCD7e1cgUS0a6X8u5tdSiMqXhojOdoV9TsMsiO+9VLC5vAmO8N7/GmXn7yjR8qnA6bVAEzfA==", + "requires": { + "call-bind": "^1.0.2", + "has-tostringtag": "^1.0.0" + } + }, + "is-bigint": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/is-bigint/-/is-bigint-1.0.4.tgz", + "integrity": "sha512-zB9CruMamjym81i2JZ3UMn54PKGsQzsJeo6xvN3HJJ4CAsQNB6iRutp2To77OfCNuoxspsIhzaPoO1zyCEhFOg==", + "requires": { + "has-bigints": "^1.0.1" + } + }, + "is-boolean-object": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/is-boolean-object/-/is-boolean-object-1.1.2.tgz", + "integrity": "sha512-gDYaKHJmnj4aWxyj6YHyXVpdQawtVLHU5cb+eztPGczf6cjuTdwve5ZIEfgXqH4e57An1D1AKf8CZ3kYrQRqYA==", + "requires": { + "call-bind": "^1.0.2", + "has-tostringtag": "^1.0.0" + } + }, + "is-buffer": { + "version": "1.1.6", + "resolved": "https://registry.npmjs.org/is-buffer/-/is-buffer-1.1.6.tgz", + "integrity": "sha512-NcdALwpXkTm5Zvvbk7owOUSvVvBKDgKP5/ewfXEznmQFfs4ZRmanOeKBTjRVjka3QFoN6XJ+9F3USqfHqTaU5w==" + }, + "is-callable": { + "version": "1.2.4", + "resolved": "https://registry.npmjs.org/is-callable/-/is-callable-1.2.4.tgz", + "integrity": "sha512-nsuwtxZfMX67Oryl9LCQ+upnC0Z0BgpwntpS89m1H/TLF0zNfzfLMV/9Wa/6MZsj0acpEjAO0KF1xT6ZdLl95w==" + }, + "is-core-module": { + "version": "2.8.0", + "resolved": "https://registry.npmjs.org/is-core-module/-/is-core-module-2.8.0.tgz", + "integrity": "sha512-vd15qHsaqrRL7dtH6QNuy0ndJmRDrS9HAM1CAiSifNUFv4x1a0CCVsj18hJ1mShxIG6T2i1sO78MkP56r0nYRw==", + "requires": { + "has": "^1.0.3" + } + }, + "is-date-object": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/is-date-object/-/is-date-object-1.0.5.tgz", + "integrity": "sha512-9YQaSxsAiSwcvS33MBk3wTCVnWK+HhF8VZR2jRxehM16QcVOdHqPn4VPHmRK4lSr38n9JriurInLcP90xsYNfQ==", + "requires": { + "has-tostringtag": "^1.0.0" + } + }, + "is-generator-function": { + "version": "1.0.10", + "resolved": "https://registry.npmjs.org/is-generator-function/-/is-generator-function-1.0.10.tgz", + "integrity": "sha512-jsEjy9l3yiXEQ+PsXdmBwEPcOxaXWLspKdplFUVI9vq1iZgIekeC0L167qeu86czQaxed3q/Uzuw0swL0irL8A==", + "requires": { + "has-tostringtag": "^1.0.0" + } + }, + "is-negative-zero": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/is-negative-zero/-/is-negative-zero-2.0.1.tgz", + "integrity": "sha512-2z6JzQvZRa9A2Y7xC6dQQm4FSTSTNWjKIYYTt4246eMTJmIo0Q+ZyOsU66X8lxK1AbB92dFeglPLrhwpeRKO6w==" + }, + "is-number-object": { + "version": "1.0.6", + "resolved": "https://registry.npmjs.org/is-number-object/-/is-number-object-1.0.6.tgz", + "integrity": "sha512-bEVOqiRcvo3zO1+G2lVMy+gkkEm9Yh7cDMRusKKu5ZJKPUYSJwICTKZrNKHA2EbSP0Tu0+6B/emsYNHZyn6K8g==", + "requires": { + "has-tostringtag": "^1.0.0" + } + }, + "is-regex": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/is-regex/-/is-regex-1.1.4.tgz", + "integrity": "sha512-kvRdxDsxZjhzUX07ZnLydzS1TU/TJlTUHHY4YLL87e37oUA49DfkLqgy+VjFocowy29cKvcSiu+kIv728jTTVg==", + "requires": { + "call-bind": "^1.0.2", + "has-tostringtag": "^1.0.0" + } + }, + "is-shared-array-buffer": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/is-shared-array-buffer/-/is-shared-array-buffer-1.0.1.tgz", + "integrity": "sha512-IU0NmyknYZN0rChcKhRO1X8LYz5Isj/Fsqh8NJOSf+N/hCOTwy29F32Ik7a+QszE63IdvmwdTPDd6cZ5pg4cwA==" + }, + "is-string": { + "version": "1.0.7", + "resolved": "https://registry.npmjs.org/is-string/-/is-string-1.0.7.tgz", + "integrity": "sha512-tE2UXzivje6ofPW7l23cjDOMa09gb7xlAqG6jG5ej6uPV32TlWP3NKPigtaGeHNu9fohccRYvIiZMfOOnOYUtg==", + "requires": { + "has-tostringtag": "^1.0.0" + } + }, + "is-symbol": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/is-symbol/-/is-symbol-1.0.4.tgz", + "integrity": "sha512-C/CPBqKWnvdcxqIARxyOh4v1UUEOCHpgDa0WYgpKDFMszcrPcffg5uhwSgPCLD2WWxmq6isisz87tzT01tuGhg==", + "requires": { + "has-symbols": "^1.0.2" + } + }, + "is-typed-array": { + "version": "1.1.8", + "resolved": "https://registry.npmjs.org/is-typed-array/-/is-typed-array-1.1.8.tgz", + "integrity": "sha512-HqH41TNZq2fgtGT8WHVFVJhBVGuY3AnP3Q36K8JKXUxSxRgk/d+7NjmwG2vo2mYmXK8UYZKu0qH8bVP5gEisjA==", + "requires": { + "available-typed-arrays": "^1.0.5", + "call-bind": "^1.0.2", + "es-abstract": "^1.18.5", + "foreach": "^2.0.5", + "has-tostringtag": "^1.0.0" + } + }, + "is-typedarray": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/is-typedarray/-/is-typedarray-1.0.0.tgz", + "integrity": "sha1-5HnICFjfDBsR3dppQPlgEfzaSpo=" + }, + "is-weakref": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/is-weakref/-/is-weakref-1.0.1.tgz", + "integrity": "sha512-b2jKc2pQZjaeFYWEf7ScFj+Be1I+PXmlu572Q8coTXZ+LD/QQZ7ShPMst8h16riVgyXTQwUsFEl74mDvc/3MHQ==", + "requires": { + "call-bind": "^1.0.0" + } + }, + "isarray": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/isarray/-/isarray-1.0.0.tgz", + "integrity": "sha1-u5NdSFgsuhaMBoNJV6VKPgcSTxE=" + }, + "isstream": { + "version": "0.1.2", + "resolved": "https://registry.npmjs.org/isstream/-/isstream-0.1.2.tgz", + "integrity": "sha1-R+Y/evVa+m+S4VAOaQ64uFKcCZo=" + }, + "js-tokens": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-4.0.0.tgz", + "integrity": "sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ==" + }, + "jsbn": { + "version": "0.1.1", + "resolved": "https://registry.npmjs.org/jsbn/-/jsbn-0.1.1.tgz", + "integrity": "sha1-peZUwuWi3rXyAdls77yoDA7y9RM=" + }, + "json-schema": { + "version": "0.2.3", + "resolved": "https://registry.npmjs.org/json-schema/-/json-schema-0.2.3.tgz", + "integrity": "sha1-tIDIkuWaLwWVTOcnvT8qTogvnhM=" + }, + "json-schema-traverse": { + "version": "0.4.1", + "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-0.4.1.tgz", + "integrity": "sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg==" + }, + "json-stringify-safe": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/json-stringify-safe/-/json-stringify-safe-5.0.1.tgz", + "integrity": "sha1-Epai1Y/UXxmg9s4B1lcB4sc1tus=" + }, + "jsonparse": { + "version": "1.3.1", + "resolved": "https://registry.npmjs.org/jsonparse/-/jsonparse-1.3.1.tgz", + "integrity": "sha1-P02uSpH6wxX3EGL4UhzCOfE2YoA=" + }, + "jsprim": { + "version": "1.4.1", + "resolved": "https://registry.npmjs.org/jsprim/-/jsprim-1.4.1.tgz", + "integrity": "sha1-MT5mvB5cwG5Di8G3SZwuXFastqI=", + "requires": { + "assert-plus": "1.0.0", + "extsprintf": "1.3.0", + "json-schema": "0.2.3", + "verror": "1.10.0" + } + }, + "labeled-stream-splicer": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/labeled-stream-splicer/-/labeled-stream-splicer-2.0.2.tgz", + "integrity": "sha512-Ca4LSXFFZUjPScRaqOcFxneA0VpKZr4MMYCljyQr4LIewTLb3Y0IUTIsnBBsVubIeEfxeSZpSjSsRM8APEQaAw==", + "requires": { + "inherits": "^2.0.1", + "stream-splicer": "^2.0.0" + } + }, + "lodash.memoize": { + "version": "3.0.4", + "resolved": "https://registry.npmjs.org/lodash.memoize/-/lodash.memoize-3.0.4.tgz", + "integrity": "sha1-LcvSwofLwKVcxCMovQxzYVDVPj8=" + }, + "loose-envify": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/loose-envify/-/loose-envify-1.4.0.tgz", + "integrity": "sha512-lyuxPGr/Wfhrlem2CL/UcnUc1zcqKAImBDzukY7Y5F/yQiNdko6+fRLevlw1HgMySw7f611UIY408EtxRSoK3Q==", + "requires": { + "js-tokens": "^3.0.0 || ^4.0.0" + } + }, + "md5.js": { + "version": "1.3.5", + "resolved": "https://registry.npmjs.org/md5.js/-/md5.js-1.3.5.tgz", + "integrity": "sha512-xitP+WxNPcTTOgnTJcrhM0xvdPepipPSf3I8EIpGKeFLjt3PlJLIDG3u8EX53ZIubkb+5U2+3rELYpEhHhzdkg==", + "requires": { + "hash-base": "^3.0.0", + "inherits": "^2.0.1", + "safe-buffer": "^5.1.2" + } + }, + "miller-rabin": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/miller-rabin/-/miller-rabin-4.0.1.tgz", + "integrity": "sha512-115fLhvZVqWwHPbClyntxEVfVDfl9DLLTuJvq3g2O/Oxi8AiNouAHvDSzHS0viUJc+V5vm3eq91Xwqn9dp4jRA==", + "requires": { + "bn.js": "^4.0.0", + "brorand": "^1.0.1" + }, + "dependencies": { + "bn.js": { + "version": "4.12.0", + "resolved": "https://registry.npmjs.org/bn.js/-/bn.js-4.12.0.tgz", + "integrity": "sha512-c98Bf3tPniI+scsdk237ku1Dc3ujXQTSgyiPUDEOe7tRkhrqridvh8klBv0HCEso1OLOYcHuCv/cS6DNxKH+ZA==" + } + } + }, + "mime-db": { + "version": "1.50.0", + "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.50.0.tgz", + "integrity": "sha512-9tMZCDlYHqeERXEHO9f/hKfNXhre5dK2eE/krIvUjZbS2KPcqGDfNShIWS1uW9XOTKQKqK6qbeOci18rbfW77A==" + }, + "mime-types": { + "version": "2.1.33", + "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-2.1.33.tgz", + "integrity": "sha512-plLElXp7pRDd0bNZHw+nMd52vRYjLwQjygaNg7ddJ2uJtTlmnTCjWuPKxVu6//AdaRuME84SvLW91sIkBqGT0g==", + "requires": { + "mime-db": "1.50.0" + } + }, + "minimalistic-assert": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/minimalistic-assert/-/minimalistic-assert-1.0.1.tgz", + "integrity": "sha512-UtJcAD4yEaGtjPezWuO9wC4nwUnVH/8/Im3yEHQP4b67cXlD/Qr9hdITCU1xDbSEXg2XKNaP8jsReV7vQd00/A==" + }, + "minimalistic-crypto-utils": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/minimalistic-crypto-utils/-/minimalistic-crypto-utils-1.0.1.tgz", + "integrity": "sha1-9sAMHAsIIkblxNmd+4x8CDsrWCo=" + }, + "minimatch": { + "version": "3.0.4", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.0.4.tgz", + "integrity": "sha512-yJHVQEhyqPLUTgt9B83PXu6W3rx4MvvHvSUvToogpwoGDOUQ+yDrR0HRot+yOCdCO7u4hX3pWft6kWBBcqh0UA==", + "requires": { + "brace-expansion": "^1.1.7" + } + }, + "minimist": { + "version": "1.2.5", + "resolved": "https://registry.npmjs.org/minimist/-/minimist-1.2.5.tgz", + "integrity": "sha512-FM9nNUYrRBAELZQT3xeZQ7fmMOBg6nWNmJKTcgsJeaLstP/UODVpGsr5OhXhhXg6f+qtJ8uiZ+PUxkDWcgIXLw==" + }, + "mkdirp-classic": { + "version": "0.5.3", + "resolved": "https://registry.npmjs.org/mkdirp-classic/-/mkdirp-classic-0.5.3.tgz", + "integrity": "sha512-gKLcREMhtuZRwRAfqP3RFW+TK4JqApVBtOIftVgjuABpAtpxhPGaDcfvbhNvD0B8iD1oUr/txX35NjcaY6Ns/A==" + }, + "module-deps": { + "version": "6.2.3", + "resolved": "https://registry.npmjs.org/module-deps/-/module-deps-6.2.3.tgz", + "integrity": "sha512-fg7OZaQBcL4/L+AK5f4iVqf9OMbCclXfy/znXRxTVhJSeW5AIlS9AwheYwDaXM3lVW7OBeaeUEY3gbaC6cLlSA==", + "requires": { + "JSONStream": "^1.0.3", + "browser-resolve": "^2.0.0", + "cached-path-relative": "^1.0.2", + "concat-stream": "~1.6.0", + "defined": "^1.0.0", + "detective": "^5.2.0", + "duplexer2": "^0.1.2", + "inherits": "^2.0.1", + "parents": "^1.0.0", + "readable-stream": "^2.0.2", + "resolve": "^1.4.0", + "stream-combiner2": "^1.1.1", + "subarg": "^1.0.0", + "through2": "^2.0.0", + "xtend": "^4.0.0" + } + }, + "oauth-sign": { + "version": "0.9.0", + "resolved": "https://registry.npmjs.org/oauth-sign/-/oauth-sign-0.9.0.tgz", + "integrity": "sha512-fexhUFFPTGV8ybAtSIGbV6gOkSv8UtRbDBnAyLQw4QPKkgNlsH2ByPGtMUqdWkos6YCRmAqViwgZrJc/mRDzZQ==" + }, + "object-assign": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/object-assign/-/object-assign-4.1.1.tgz", + "integrity": "sha1-IQmtx5ZYh8/AXLvUQsrIv7s2CGM=" + }, + "object-inspect": { + "version": "1.11.0", + "resolved": "https://registry.npmjs.org/object-inspect/-/object-inspect-1.11.0.tgz", + "integrity": "sha512-jp7ikS6Sd3GxQfZJPyH3cjcbJF6GZPClgdV+EFygjFLQ5FmW/dRUnTd9PQ9k0JhoNDabWFbpF1yCdSWCC6gexg==" + }, + "object-keys": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/object-keys/-/object-keys-1.1.1.tgz", + "integrity": "sha512-NuAESUOUMrlIXOfHKzD6bpPu3tYt3xvjNdRIQ+FeT0lNb4K8WR70CaDxhuNguS2XG+GjkyMwOzsN5ZktImfhLA==" + }, + "object.assign": { + "version": "4.1.2", + "resolved": "https://registry.npmjs.org/object.assign/-/object.assign-4.1.2.tgz", + "integrity": "sha512-ixT2L5THXsApyiUPYKmW+2EHpXXe5Ii3M+f4e+aJFAHao5amFRW6J0OO6c/LU8Be47utCx2GL89hxGB6XSmKuQ==", + "requires": { + "call-bind": "^1.0.0", + "define-properties": "^1.1.3", + "has-symbols": "^1.0.1", + "object-keys": "^1.1.1" + } + }, + "once": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/once/-/once-1.4.0.tgz", + "integrity": "sha1-WDsap3WWHUsROsF9nFC6753Xa9E=", + "requires": { + "wrappy": "1" + } + }, + "os-browserify": { + "version": "0.3.0", + "resolved": "https://registry.npmjs.org/os-browserify/-/os-browserify-0.3.0.tgz", + "integrity": "sha1-hUNzx/XCMVkU/Jv8a9gjj92h7Cc=" + }, + "pako": { + "version": "1.0.11", + "resolved": "https://registry.npmjs.org/pako/-/pako-1.0.11.tgz", + "integrity": "sha512-4hLB8Py4zZce5s4yd9XzopqwVv/yGNhV1Bl8NTmCq1763HeK2+EwVTv+leGeL13Dnh2wfbqowVPXCIO0z4taYw==" + }, + "parents": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/parents/-/parents-1.0.1.tgz", + "integrity": "sha1-/t1NK/GTp3dF/nHjcdc8MwfZx1E=", + "requires": { + "path-platform": "~0.11.15" + } + }, + "parse-asn1": { + "version": "5.1.6", + "resolved": "https://registry.npmjs.org/parse-asn1/-/parse-asn1-5.1.6.tgz", + "integrity": "sha512-RnZRo1EPU6JBnra2vGHj0yhp6ebyjBZpmUCLHWiFhxlzvBCCpAuZ7elsBp1PVAbQN0/04VD/19rfzlBSwLstMw==", + "requires": { + "asn1.js": "^5.2.0", + "browserify-aes": "^1.0.0", + "evp_bytestokey": "^1.0.0", + "pbkdf2": "^3.0.3", + "safe-buffer": "^5.1.1" + } + }, + "path-browserify": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/path-browserify/-/path-browserify-1.0.1.tgz", + "integrity": "sha512-b7uo2UCUOYZcnF/3ID0lulOJi/bafxa1xPe7ZPsammBSpjSWQkjNxlt635YGS2MiR9GjvuXCtz2emr3jbsz98g==" + }, + "path-is-absolute": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/path-is-absolute/-/path-is-absolute-1.0.1.tgz", + "integrity": "sha1-F0uSaHNVNP+8es5r9TpanhtcX18=" + }, + "path-parse": { + "version": "1.0.7", + "resolved": "https://registry.npmjs.org/path-parse/-/path-parse-1.0.7.tgz", + "integrity": "sha512-LDJzPVEEEPR+y48z93A0Ed0yXb8pAByGWo/k5YYdYgpY2/2EsOsksJrq7lOHxryrVOn1ejG6oAp8ahvOIQD8sw==" + }, + "path-platform": { + "version": "0.11.15", + "resolved": "https://registry.npmjs.org/path-platform/-/path-platform-0.11.15.tgz", + "integrity": "sha1-6GQhf3TDaFDwhSt43Hv31KVyG/I=" + }, + "pbkdf2": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/pbkdf2/-/pbkdf2-3.1.2.tgz", + "integrity": "sha512-iuh7L6jA7JEGu2WxDwtQP1ddOpaJNC4KlDEFfdQajSGgGPNi4OyDc2R7QnbY2bR9QjBVGwgvTdNJZoE7RaxUMA==", + "requires": { + "create-hash": "^1.1.2", + "create-hmac": "^1.1.4", + "ripemd160": "^2.0.1", + "safe-buffer": "^5.0.1", + "sha.js": "^2.4.8" + } + }, + "performance-now": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/performance-now/-/performance-now-2.1.0.tgz", + "integrity": "sha1-Ywn04OX6kT7BxpMHrjZLSzd8nns=" + }, + "process": { + "version": "0.11.10", + "resolved": "https://registry.npmjs.org/process/-/process-0.11.10.tgz", + "integrity": "sha1-czIwDoQBYb2j5podHZGn1LwW8YI=" + }, + "process-nextick-args": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/process-nextick-args/-/process-nextick-args-2.0.1.tgz", + "integrity": "sha512-3ouUOpQhtgrbOa17J7+uxOTpITYWaGP7/AhoR3+A+/1e9skrzelGi/dXzEYyvbxubEF6Wn2ypscTKiKJFFn1ag==" + }, + "psl": { + "version": "1.8.0", + "resolved": "https://registry.npmjs.org/psl/-/psl-1.8.0.tgz", + "integrity": "sha512-RIdOzyoavK+hA18OGGWDqUTsCLhtA7IcZ/6NCs4fFJaHBDab+pDDmDIByWFRQJq2Cd7r1OoQxBGKOaztq+hjIQ==" + }, + "public-encrypt": { + "version": "4.0.3", + "resolved": "https://registry.npmjs.org/public-encrypt/-/public-encrypt-4.0.3.tgz", + "integrity": "sha512-zVpa8oKZSz5bTMTFClc1fQOnyyEzpl5ozpi1B5YcvBrdohMjH2rfsBtyXcuNuwjsDIXmBYlF2N5FlJYhR29t8Q==", + "requires": { + "bn.js": "^4.1.0", + "browserify-rsa": "^4.0.0", + "create-hash": "^1.1.0", + "parse-asn1": "^5.0.0", + "randombytes": "^2.0.1", + "safe-buffer": "^5.1.2" + }, + "dependencies": { + "bn.js": { + "version": "4.12.0", + "resolved": "https://registry.npmjs.org/bn.js/-/bn.js-4.12.0.tgz", + "integrity": "sha512-c98Bf3tPniI+scsdk237ku1Dc3ujXQTSgyiPUDEOe7tRkhrqridvh8klBv0HCEso1OLOYcHuCv/cS6DNxKH+ZA==" + } + } + }, + "punycode": { + "version": "1.4.1", + "resolved": "https://registry.npmjs.org/punycode/-/punycode-1.4.1.tgz", + "integrity": "sha1-wNWmOycYgArY4esPpSachN1BhF4=" + }, + "qs": { + "version": "6.5.2", + "resolved": "https://registry.npmjs.org/qs/-/qs-6.5.2.tgz", + "integrity": "sha512-N5ZAX4/LxJmF+7wN74pUD6qAh9/wnvdQcjq9TZjevvXzSUo7bfmw91saqMjzGS2xq91/odN2dW/WOl7qQHNDGA==" + }, + "querystring": { + "version": "0.2.0", + "resolved": "https://registry.npmjs.org/querystring/-/querystring-0.2.0.tgz", + "integrity": "sha1-sgmEkgO7Jd+CDadW50cAWHhSFiA=" + }, + "querystring-es3": { + "version": "0.2.1", + "resolved": "https://registry.npmjs.org/querystring-es3/-/querystring-es3-0.2.1.tgz", + "integrity": "sha1-nsYfeQSYdXB9aUFFlv2Qek1xHnM=" + }, + "randombytes": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/randombytes/-/randombytes-2.1.0.tgz", + "integrity": "sha512-vYl3iOX+4CKUWuxGi9Ukhie6fsqXqS9FE2Zaic4tNFD2N2QQaXOMFbuKK4QmDHC0JO6B1Zp41J0LpT0oR68amQ==", + "requires": { + "safe-buffer": "^5.1.0" + } + }, + "randomfill": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/randomfill/-/randomfill-1.0.4.tgz", + "integrity": "sha512-87lcbR8+MhcWcUiQ+9e+Rwx8MyR2P7qnt15ynUlbm3TU/fjbgz4GsvfSUDTemtCCtVCqb4ZcEFlyPNTh9bBTLw==", + "requires": { + "randombytes": "^2.0.5", + "safe-buffer": "^5.1.0" + } + }, + "react": { + "version": "17.0.2", + "resolved": "https://registry.npmjs.org/react/-/react-17.0.2.tgz", + "integrity": "sha512-gnhPt75i/dq/z3/6q/0asP78D0u592D5L1pd7M8P+dck6Fu/jJeL6iVVK23fptSUZj8Vjf++7wXA8UNclGQcbA==", + "requires": { + "loose-envify": "^1.1.0", + "object-assign": "^4.1.1" + } + }, + "react-dom": { + "version": "17.0.2", + "resolved": "https://registry.npmjs.org/react-dom/-/react-dom-17.0.2.tgz", + "integrity": "sha512-s4h96KtLDUQlsENhMn1ar8t2bEa+q/YAtj8pPPdIjPDGBDIVNsrD9aXNWqspUe6AzKCIG0C1HZZLqLV7qpOBGA==", + "requires": { + "loose-envify": "^1.1.0", + "object-assign": "^4.1.1", + "scheduler": "^0.20.2" + } + }, + "read-only-stream": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/read-only-stream/-/read-only-stream-2.0.0.tgz", + "integrity": "sha1-JyT9aoET1zdkrCiNQ4YnDB2/F/A=", + "requires": { + "readable-stream": "^2.0.2" + } + }, + "readable-stream": { + "version": "2.3.7", + "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-2.3.7.tgz", + "integrity": "sha512-Ebho8K4jIbHAxnuxi7o42OrZgF/ZTNcsZj6nRKyUmkhLFq8CHItp/fy6hQZuZmP/n3yZ9VBUbp4zz/mX8hmYPw==", + "requires": { + "core-util-is": "~1.0.0", + "inherits": "~2.0.3", + "isarray": "~1.0.0", + "process-nextick-args": "~2.0.0", + "safe-buffer": "~5.1.1", + "string_decoder": "~1.1.1", + "util-deprecate": "~1.0.1" + }, + "dependencies": { + "safe-buffer": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.1.2.tgz", + "integrity": "sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g==" + }, + "string_decoder": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.1.1.tgz", + "integrity": "sha512-n/ShnvDi6FHbbVfviro+WojiFzv+s8MPMHBczVePfUpDJLwoLT0ht1l4YwBCbi8pJAveEEdnkHyPyTP/mzRfwg==", + "requires": { + "safe-buffer": "~5.1.0" + } + } + } + }, + "regl": { + "version": "1.7.0", + "resolved": "https://registry.npmjs.org/regl/-/regl-1.7.0.tgz", + "integrity": "sha512-bEAtp/qrtKucxXSJkD4ebopFZYP0q1+3Vb2WECWv/T8yQEgKxDxJ7ztO285tAMaYZVR6mM1GgI6CCn8FROtL1w==" + }, + "request": { + "version": "2.88.2", + "resolved": "https://registry.npmjs.org/request/-/request-2.88.2.tgz", + "integrity": "sha512-MsvtOrfG9ZcrOwAW+Qi+F6HbD0CWXEh9ou77uOb7FM2WPhwT7smM833PzanhJLsgXjN89Ir6V2PczXNnMpwKhw==", + "requires": { + "aws-sign2": "~0.7.0", + "aws4": "^1.8.0", + "caseless": "~0.12.0", + "combined-stream": "~1.0.6", + "extend": "~3.0.2", + "forever-agent": "~0.6.1", + "form-data": "~2.3.2", + "har-validator": "~5.1.3", + "http-signature": "~1.2.0", + "is-typedarray": "~1.0.0", + "isstream": "~0.1.2", + "json-stringify-safe": "~5.0.1", + "mime-types": "~2.1.19", + "oauth-sign": "~0.9.0", + "performance-now": "^2.1.0", + "qs": "~6.5.2", + "safe-buffer": "^5.1.2", + "tough-cookie": "~2.5.0", + "tunnel-agent": "^0.6.0", + "uuid": "^3.3.2" + } + }, + "resolve": { + "version": "1.20.0", + "resolved": "https://registry.npmjs.org/resolve/-/resolve-1.20.0.tgz", + "integrity": "sha512-wENBPt4ySzg4ybFQW2TT1zMQucPK95HSh/nq2CFTZVOGut2+pQvSsgtda4d26YrYcr067wjbmzOG8byDPBX63A==", + "requires": { + "is-core-module": "^2.2.0", + "path-parse": "^1.0.6" + } + }, + "ripemd160": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/ripemd160/-/ripemd160-2.0.2.tgz", + "integrity": "sha512-ii4iagi25WusVoiC4B4lq7pbXfAp3D9v5CwfkY33vffw2+pkDjY1D8GaN7spsxvCSx8dkPqOZCEZyfxcmJG2IA==", + "requires": { + "hash-base": "^3.0.0", + "inherits": "^2.0.1" + } + }, + "safe-buffer": { + "version": "5.2.1", + "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.2.1.tgz", + "integrity": "sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ==" + }, + "safer-buffer": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/safer-buffer/-/safer-buffer-2.1.2.tgz", + "integrity": "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==" + }, + "scheduler": { + "version": "0.20.2", + "resolved": "https://registry.npmjs.org/scheduler/-/scheduler-0.20.2.tgz", + "integrity": "sha512-2eWfGgAqqWFGqtdMmcL5zCMK1U8KlXv8SQFGglL3CEtd0aDVDWgeF/YoCmvln55m5zSk3J/20hTaSBeSObsQDQ==", + "requires": { + "loose-envify": "^1.1.0", + "object-assign": "^4.1.1" + } + }, + "sha.js": { + "version": "2.4.11", + "resolved": "https://registry.npmjs.org/sha.js/-/sha.js-2.4.11.tgz", + "integrity": "sha512-QMEp5B7cftE7APOjk5Y6xgrbWu+WkLVQwk8JNjZ8nKRciZaByEW6MubieAiToS7+dwvrjGhH8jRXz3MVd0AYqQ==", + "requires": { + "inherits": "^2.0.1", + "safe-buffer": "^5.0.1" + } + }, + "shasum-object": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/shasum-object/-/shasum-object-1.0.0.tgz", + "integrity": "sha512-Iqo5rp/3xVi6M4YheapzZhhGPVs0yZwHj7wvwQ1B9z8H6zk+FEnI7y3Teq7qwnekfEhu8WmG2z0z4iWZaxLWVg==", + "requires": { + "fast-safe-stringify": "^2.0.7" + } + }, + "shell-quote": { + "version": "1.7.3", + "resolved": "https://registry.npmjs.org/shell-quote/-/shell-quote-1.7.3.tgz", + "integrity": "sha512-Vpfqwm4EnqGdlsBFNmHhxhElJYrdfcxPThu+ryKS5J8L/fhAwLazFZtq+S+TWZ9ANj2piSQLGj6NQg+lKPmxrw==" + }, + "side-channel": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/side-channel/-/side-channel-1.0.4.tgz", + "integrity": "sha512-q5XPytqFEIKHkGdiMIrY10mvLRvnQh42/+GoBlFW3b2LXLE2xxJpZFdm94we0BaoV3RwJyGqg5wS7epxTv0Zvw==", + "requires": { + "call-bind": "^1.0.0", + "get-intrinsic": "^1.0.2", + "object-inspect": "^1.9.0" + } + }, + "simple-concat": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/simple-concat/-/simple-concat-1.0.1.tgz", + "integrity": "sha512-cSFtAPtRhljv69IK0hTVZQ+OfE9nePi/rtJmw5UjHeVyVroEqJXP1sFztKUy1qU+xvz3u/sfYJLa947b7nAN2Q==" + }, + "source-map": { + "version": "0.5.7", + "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.5.7.tgz", + "integrity": "sha1-igOdLRAh0i0eoUyA2OpGi6LvP8w=" + }, + "sshpk": { + "version": "1.16.1", + "resolved": "https://registry.npmjs.org/sshpk/-/sshpk-1.16.1.tgz", + "integrity": "sha512-HXXqVUq7+pcKeLqqZj6mHFUMvXtOJt1uoUx09pFW6011inTMxqI8BA8PM95myrIyyKwdnzjdFjLiE6KBPVtJIg==", + "requires": { + "asn1": "~0.2.3", + "assert-plus": "^1.0.0", + "bcrypt-pbkdf": "^1.0.0", + "dashdash": "^1.12.0", + "ecc-jsbn": "~0.1.1", + "getpass": "^0.1.1", + "jsbn": "~0.1.0", + "safer-buffer": "^2.0.2", + "tweetnacl": "~0.14.0" + } + }, + "stream-browserify": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/stream-browserify/-/stream-browserify-3.0.0.tgz", + "integrity": "sha512-H73RAHsVBapbim0tU2JwwOiXUj+fikfiaoYAKHF3VJfA0pe2BCzkhAHBlLG6REzE+2WNZcxOXjK7lkso+9euLA==", + "requires": { + "inherits": "~2.0.4", + "readable-stream": "^3.5.0" + }, + "dependencies": { + "readable-stream": { + "version": "3.6.0", + "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-3.6.0.tgz", + "integrity": "sha512-BViHy7LKeTz4oNnkcLJ+lVSL6vpiFeX6/d3oSH8zCW7UxP2onchk+vTGB143xuFjHS3deTgkKoXXymXqymiIdA==", + "requires": { + "inherits": "^2.0.3", + "string_decoder": "^1.1.1", + "util-deprecate": "^1.0.1" + } + } + } + }, + "stream-combiner2": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/stream-combiner2/-/stream-combiner2-1.1.1.tgz", + "integrity": "sha1-+02KFCDqNidk4hrUeAOXvry0HL4=", + "requires": { + "duplexer2": "~0.1.0", + "readable-stream": "^2.0.2" + } + }, + "stream-http": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/stream-http/-/stream-http-3.2.0.tgz", + "integrity": "sha512-Oq1bLqisTyK3TSCXpPbT4sdeYNdmyZJv1LxpEm2vu1ZhK89kSE5YXwZc3cWk0MagGaKriBh9mCFbVGtO+vY29A==", + "requires": { + "builtin-status-codes": "^3.0.0", + "inherits": "^2.0.4", + "readable-stream": "^3.6.0", + "xtend": "^4.0.2" + }, + "dependencies": { + "readable-stream": { + "version": "3.6.0", + "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-3.6.0.tgz", + "integrity": "sha512-BViHy7LKeTz4oNnkcLJ+lVSL6vpiFeX6/d3oSH8zCW7UxP2onchk+vTGB143xuFjHS3deTgkKoXXymXqymiIdA==", + "requires": { + "inherits": "^2.0.3", + "string_decoder": "^1.1.1", + "util-deprecate": "^1.0.1" + } + } + } + }, + "stream-splicer": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/stream-splicer/-/stream-splicer-2.0.1.tgz", + "integrity": "sha512-Xizh4/NPuYSyAXyT7g8IvdJ9HJpxIGL9PjyhtywCZvvP0OPIdqyrr4dMikeuvY8xahpdKEBlBTySe583totajg==", + "requires": { + "inherits": "^2.0.1", + "readable-stream": "^2.0.2" + } + }, + "string.prototype.trimend": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/string.prototype.trimend/-/string.prototype.trimend-1.0.4.tgz", + "integrity": "sha512-y9xCjw1P23Awk8EvTpcyL2NIr1j7wJ39f+k6lvRnSMz+mz9CGz9NYPelDk42kOz6+ql8xjfK8oYzy3jAP5QU5A==", + "requires": { + "call-bind": "^1.0.2", + "define-properties": "^1.1.3" + } + }, + "string.prototype.trimstart": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/string.prototype.trimstart/-/string.prototype.trimstart-1.0.4.tgz", + "integrity": "sha512-jh6e984OBfvxS50tdY2nRZnoC5/mLFKOREQfw8t5yytkoUsJRNxvI/E39qu1sD0OtWI3OC0XgKSmcWwziwYuZw==", + "requires": { + "call-bind": "^1.0.2", + "define-properties": "^1.1.3" + } + }, + "string_decoder": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.3.0.tgz", + "integrity": "sha512-hkRX8U1WjJFd8LsDJ2yQ/wWWxaopEsABU1XfkM8A+j0+85JAGppt16cr1Whg6KIbb4okU6Mql6BOj+uup/wKeA==", + "requires": { + "safe-buffer": "~5.2.0" + } + }, + "subarg": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/subarg/-/subarg-1.0.0.tgz", + "integrity": "sha1-9izxdYHplrSPyWVpn1TAauJouNI=", + "requires": { + "minimist": "^1.1.0" + } + }, + "syntax-error": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/syntax-error/-/syntax-error-1.4.0.tgz", + "integrity": "sha512-YPPlu67mdnHGTup2A8ff7BC2Pjq0e0Yp/IyTFN03zWO0RcK07uLcbi7C2KpGR2FvWbaB0+bfE27a+sBKebSo7w==", + "requires": { + "acorn-node": "^1.2.0" + } + }, + "through": { + "version": "2.3.8", + "resolved": "https://registry.npmjs.org/through/-/through-2.3.8.tgz", + "integrity": "sha1-DdTJ/6q8NXlgsbckEV1+Doai4fU=" + }, + "through2": { + "version": "2.0.5", + "resolved": "https://registry.npmjs.org/through2/-/through2-2.0.5.tgz", + "integrity": "sha512-/mrRod8xqpA+IHSLyGCQ2s8SPHiCDEeQJSep1jqLYeEUClOFG2Qsh+4FU6G9VeqpZnGW/Su8LQGc4YKni5rYSQ==", + "requires": { + "readable-stream": "~2.3.6", + "xtend": "~4.0.1" + } + }, + "timers-browserify": { + "version": "1.4.2", + "resolved": "https://registry.npmjs.org/timers-browserify/-/timers-browserify-1.4.2.tgz", + "integrity": "sha1-ycWLV1voQHN1y14kYtrO50NZ9B0=", + "requires": { + "process": "~0.11.0" + } + }, + "tough-cookie": { + "version": "2.5.0", + "resolved": "https://registry.npmjs.org/tough-cookie/-/tough-cookie-2.5.0.tgz", + "integrity": "sha512-nlLsUzgm1kfLXSXfRZMc1KLAugd4hqJHDTvc2hDIwS3mZAfMEuMbc03SujMF+GEcpaX/qboeycw6iO8JwVv2+g==", + "requires": { + "psl": "^1.1.28", + "punycode": "^2.1.1" + }, + "dependencies": { + "punycode": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/punycode/-/punycode-2.1.1.tgz", + "integrity": "sha512-XRsRjdf+j5ml+y/6GKHPZbrF/8p2Yga0JPtdqTIY2Xe5ohJPD9saDJJLPvp9+NSBprVvevdXZybnj2cv8OEd0A==" + } + } + }, + "tty-browserify": { + "version": "0.0.1", + "resolved": "https://registry.npmjs.org/tty-browserify/-/tty-browserify-0.0.1.tgz", + "integrity": "sha512-C3TaO7K81YvjCgQH9Q1S3R3P3BtN3RIM8n+OvX4il1K1zgE8ZhI0op7kClgkxtutIE8hQrcrHBXvIheqKUUCxw==" + }, + "tunnel-agent": { + "version": "0.6.0", + "resolved": "https://registry.npmjs.org/tunnel-agent/-/tunnel-agent-0.6.0.tgz", + "integrity": "sha1-J6XeoGs2sEoKmWZ3SykIaPD8QP0=", + "requires": { + "safe-buffer": "^5.0.1" + } + }, + "tweetnacl": { + "version": "0.14.5", + "resolved": "https://registry.npmjs.org/tweetnacl/-/tweetnacl-0.14.5.tgz", + "integrity": "sha1-WuaBd/GS1EViadEIr6k/+HQ/T2Q=" + }, + "typedarray": { + "version": "0.0.6", + "resolved": "https://registry.npmjs.org/typedarray/-/typedarray-0.0.6.tgz", + "integrity": "sha1-hnrHTjhkGHsdPUfZlqeOxciDB3c=" + }, + "umd": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/umd/-/umd-3.0.3.tgz", + "integrity": "sha512-4IcGSufhFshvLNcMCV80UnQVlZ5pMOC8mvNPForqwA4+lzYQuetTESLDQkeLmihq8bRcnpbQa48Wb8Lh16/xow==" + }, + "unbox-primitive": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/unbox-primitive/-/unbox-primitive-1.0.1.tgz", + "integrity": "sha512-tZU/3NqK3dA5gpE1KtyiJUrEB0lxnGkMFHptJ7q6ewdZ8s12QrODwNbhIJStmJkd1QDXa1NRA8aF2A1zk/Ypyw==", + "requires": { + "function-bind": "^1.1.1", + "has-bigints": "^1.0.1", + "has-symbols": "^1.0.2", + "which-boxed-primitive": "^1.0.2" + } + }, + "undeclared-identifiers": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/undeclared-identifiers/-/undeclared-identifiers-1.1.3.tgz", + "integrity": "sha512-pJOW4nxjlmfwKApE4zvxLScM/njmwj/DiUBv7EabwE4O8kRUy+HIwxQtZLBPll/jx1LJyBcqNfB3/cpv9EZwOw==", + "requires": { + "acorn-node": "^1.3.0", + "dash-ast": "^1.0.0", + "get-assigned-identifiers": "^1.2.0", + "simple-concat": "^1.0.0", + "xtend": "^4.0.1" + } + }, + "uri-js": { + "version": "4.4.1", + "resolved": "https://registry.npmjs.org/uri-js/-/uri-js-4.4.1.tgz", + "integrity": "sha512-7rKUyy33Q1yc98pQ1DAmLtwX109F7TIfWlW1Ydo8Wl1ii1SeHieeh0HHfPeL2fMXK6z0s8ecKs9frCuLJvndBg==", + "requires": { + "punycode": "^2.1.0" + }, + "dependencies": { + "punycode": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/punycode/-/punycode-2.1.1.tgz", + "integrity": "sha512-XRsRjdf+j5ml+y/6GKHPZbrF/8p2Yga0JPtdqTIY2Xe5ohJPD9saDJJLPvp9+NSBprVvevdXZybnj2cv8OEd0A==" + } + } + }, + "url": { + "version": "0.11.0", + "resolved": "https://registry.npmjs.org/url/-/url-0.11.0.tgz", + "integrity": "sha1-ODjpfPxgUh63PFJajlW/3Z4uKPE=", + "requires": { + "punycode": "1.3.2", + "querystring": "0.2.0" + }, + "dependencies": { + "punycode": { + "version": "1.3.2", + "resolved": "https://registry.npmjs.org/punycode/-/punycode-1.3.2.tgz", + "integrity": "sha1-llOgNvt8HuQjQvIyXM7v6jkmxI0=" + } + } + }, + "util": { + "version": "0.12.4", + "resolved": "https://registry.npmjs.org/util/-/util-0.12.4.tgz", + "integrity": "sha512-bxZ9qtSlGUWSOy9Qa9Xgk11kSslpuZwaxCg4sNIDj6FLucDab2JxnHwyNTCpHMtK1MjoQiWQ6DiUMZYbSrO+Sw==", + "requires": { + "inherits": "^2.0.3", + "is-arguments": "^1.0.4", + "is-generator-function": "^1.0.7", + "is-typed-array": "^1.1.3", + "safe-buffer": "^5.1.2", + "which-typed-array": "^1.1.2" + } + }, + "util-deprecate": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/util-deprecate/-/util-deprecate-1.0.2.tgz", + "integrity": "sha1-RQ1Nyfpw3nMnYvvS1KKJgUGaDM8=" + }, + "uuid": { + "version": "3.4.0", + "resolved": "https://registry.npmjs.org/uuid/-/uuid-3.4.0.tgz", + "integrity": "sha512-HjSDRw6gZE5JMggctHBcjVak08+KEVhSIiDzFnT9S9aegmp85S/bReBVTb4QTFaRNptJ9kuYaNhnbNEOkbKb/A==" + }, + "verror": { + "version": "1.10.0", + "resolved": "https://registry.npmjs.org/verror/-/verror-1.10.0.tgz", + "integrity": "sha1-OhBcoXBTr1XW4nDB+CiGguGNpAA=", + "requires": { + "assert-plus": "^1.0.0", + "core-util-is": "1.0.2", + "extsprintf": "^1.2.0" + }, + "dependencies": { + "core-util-is": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/core-util-is/-/core-util-is-1.0.2.tgz", + "integrity": "sha1-tf1UIgqivFq1eqtxQMlAdUUDwac=" + } + } + }, + "vm-browserify": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/vm-browserify/-/vm-browserify-1.1.2.tgz", + "integrity": "sha512-2ham8XPWTONajOR0ohOKOHXkm3+gaBmGut3SRuu75xLd/RRaY6vqgh8NBYYk7+RW3u5AtzPQZG8F10LHkl0lAQ==" + }, + "which-boxed-primitive": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/which-boxed-primitive/-/which-boxed-primitive-1.0.2.tgz", + "integrity": "sha512-bwZdv0AKLpplFY2KZRX6TvyuN7ojjr7lwkg6ml0roIy9YeuSr7JS372qlNW18UQYzgYK9ziGcerWqZOmEn9VNg==", + "requires": { + "is-bigint": "^1.0.1", + "is-boolean-object": "^1.1.0", + "is-number-object": "^1.0.4", + "is-string": "^1.0.5", + "is-symbol": "^1.0.3" + } + }, + "which-typed-array": { + "version": "1.1.7", + "resolved": "https://registry.npmjs.org/which-typed-array/-/which-typed-array-1.1.7.tgz", + "integrity": "sha512-vjxaB4nfDqwKI0ws7wZpxIlde1XrLX5uB0ZjpfshgmapJMD7jJWhZI+yToJTqaFByF0eNBcYxbjmCzoRP7CfEw==", + "requires": { + "available-typed-arrays": "^1.0.5", + "call-bind": "^1.0.2", + "es-abstract": "^1.18.5", + "foreach": "^2.0.5", + "has-tostringtag": "^1.0.0", + "is-typed-array": "^1.1.7" + } + }, + "wrappy": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz", + "integrity": "sha1-tSQ9jz7BqjXxNkYFvA0QNuMKtp8=" + }, + "xtend": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/xtend/-/xtend-4.0.2.tgz", + "integrity": "sha512-LKYU1iAXJXUgAXn9URjiu+MWhyUXHsvfp7mcuYm9dSUKK0/CjtrUwFAxD82/mCWbtLsGjFIad0wIsod4zrTAEQ==" + } + } +} diff --git a/frontend/package.json b/frontend/package.json new file mode 100644 index 0000000..4e245d9 --- /dev/null +++ b/frontend/package.json @@ -0,0 +1,19 @@ +{ + "name": "stabilizer-stream-viewer", + "version": "0.1.0", + "description": "Visualize Stabilizer livestreamed data", + "scripts": { + "test": "echo \"Error: no test specified\" && exit 1", + "build": "./node_modules/.bin/esbuild src/react.jsx --define:global=window --bundle --outfile=dist/main.js" + }, + "author": "Ryan Summers", + "license": "MIT", + "dependencies": { + "browserify": "^17.0.0", + "candygraph": "^0.4.2", + "esbuild": "^0.13.12", + "react": "^17.0.2", + "react-dom": "^17.0.2", + "request": "^2.88.2" + } +} diff --git a/frontend/src/react.jsx b/frontend/src/react.jsx new file mode 100644 index 0000000..4878271 --- /dev/null +++ b/frontend/src/react.jsx @@ -0,0 +1,145 @@ +import CandyGraph, { + createCartesianCoordinateSystem, + createLinearScale, + createLineStrip, +} from "candygraph"; + +import ReactDOM from 'react-dom'; +import React from "react"; +import http from 'stream-http'; + +class Display extends React.Component { + render() { + const viewport = { + x: 0, + y: 0, + width: this.props.width, + height: this.props.height, + } + + const cg = new CandyGraph() + + const coords = createCartesianCoordinateSystem( + createLinearScale([0, this.props.times[-1]], [32, viewport.width - 16]), + createLinearScale([-10.24, 10.24], [32, viewport.width - 16]), + ); + + // Create the various traces for the display + var lines = [] + console.log(this.props) + for (var i = 0; i < this.props.traces.length; i += 1) { + lines += createLineStrip(cg, this.props.times, this.props.traces[i]) + } + + // Render the display to an HTML element. + cg.render(coords, viewport, lines) + cg.copyTo(viewport, document.getElementById("oscilloscope-display")) + + return ( +
+ +
+ ); + } +} + +class Trigger extends React.Component { + constructor(props) { + super(props) + this.state = { + trigger: 'Idle', + timer: null, + capture_duration: 1, + } + } + + pollTrigger() { + http.get('http://localhost:8080/trigger', res => { + res.on('data', data => { + const body = JSON.parse(data) + console.log("Trigger state: ${body}") + + this.setState({trigger: body}) + if (body == "Triggered") { + clearInterval(this.state.timer) + this.setState({timer: null}) + this.props.onTrigger() + } + }) + }) + } + + startCapture() { + const postData = JSON.stringify({ + capture_duration_secs: this.state.capture_duration + }); + + const req = http.put('http://localhost:8080', {path: '/capture', method: 'POST'}, (res) => { + res.on('end', _ => { + // Begin polling the trigger state. + self.setState({timer: setInterval(pollTrigger, 100)}) + }) + + res.on('error', error => { + console.log('Capture error: ${error}') + }) + }) + + req.write(postData) + req.end() + } + + render() { + return ( +
+ + +
{this.state.trigger}
+ + +
+ ); + } +} + +class Oscilloscope extends React.Component { + constructor(props) { + super(props); + this.state = { + width: props.width, + height: props.height, + times: [], + traces: [] + } + } + + getTraces() { + http.get('http://localhost:8080/data', (res) => { + res.on('data', data => { + const body = JSON.parse(data) + this.setState({times: body.times, traces: body.traces}) + }) + }) + } + + render() { + return ( +
+ + this.getTraces()} + /> +
+ ); + } +} + +ReactDOM.render(, document.getElementById("root")) From fc7c365aff92eebdd8662b0c7d1596a4272979a8 Mon Sep 17 00:00:00 2001 From: Ryan Summers Date: Tue, 2 Nov 2021 14:06:04 +0100 Subject: [PATCH 04/39] Adding WIP react changes --- build.rs | 16 --- frontend/dist/index.html | 3 + frontend/src/react.jsx | 264 ++++++++++++++++++++++++++++----------- 3 files changed, 192 insertions(+), 91 deletions(-) delete mode 100644 build.rs diff --git a/build.rs b/build.rs deleted file mode 100644 index d75de48..0000000 --- a/build.rs +++ /dev/null @@ -1,16 +0,0 @@ -fn main() { - println!("cargo-rerun-if-changed=frontend"); - - let mut working_directory = std::env::current_dir().unwrap(); - working_directory.push("frontend"); - assert!( - npm_rs::NpmEnv::default() - .set_path(working_directory) - .init_env() - .run("build") - .exec() - .unwrap() - .success(), - "Failed to build front-end resources" - ); -} diff --git a/frontend/dist/index.html b/frontend/dist/index.html index 955c7bf..a513728 100644 --- a/frontend/dist/index.html +++ b/frontend/dist/index.html @@ -5,6 +5,9 @@ Stabilizer Livestream Viewer +
+
+
diff --git a/frontend/src/react.jsx b/frontend/src/react.jsx index 4878271..023a308 100644 --- a/frontend/src/react.jsx +++ b/frontend/src/react.jsx @@ -2,50 +2,17 @@ import CandyGraph, { createCartesianCoordinateSystem, createLinearScale, createLineStrip, + createDefaultFont, + createOrthoAxis, } from "candygraph"; import ReactDOM from 'react-dom'; import React from "react"; -import http from 'stream-http'; - -class Display extends React.Component { - render() { - const viewport = { - x: 0, - y: 0, - width: this.props.width, - height: this.props.height, - } - - const cg = new CandyGraph() - - const coords = createCartesianCoordinateSystem( - createLinearScale([0, this.props.times[-1]], [32, viewport.width - 16]), - createLinearScale([-10.24, 10.24], [32, viewport.width - 16]), - ); - - // Create the various traces for the display - var lines = [] - console.log(this.props) - for (var i = 0; i < this.props.traces.length; i += 1) { - lines += createLineStrip(cg, this.props.times, this.props.traces[i]) - } - - // Render the display to an HTML element. - cg.render(coords, viewport, lines) - cg.copyTo(viewport, document.getElementById("oscilloscope-display")) - - return ( -
- -
- ); - } -} class Trigger extends React.Component { constructor(props) { super(props) + this.onChange = this.onChange.bind(this) this.state = { trigger: 'Idle', timer: null, @@ -54,18 +21,22 @@ class Trigger extends React.Component { } pollTrigger() { - http.get('http://localhost:8080/trigger', res => { - res.on('data', data => { - const body = JSON.parse(data) - console.log("Trigger state: ${body}") - - this.setState({trigger: body}) - if (body == "Triggered") { - clearInterval(this.state.timer) - this.setState({timer: null}) - this.props.onTrigger() - } - }) + + fetch('http://localhost:8080/trigger').then(response => { + if (response.ok) { + return response.json(); + } else { + return Promise.reject(`Trigger rerequest failed: ${response.text()}`) + } + }).then(body => { + console.log(`Trigger state: ${body}`) + + this.setState({trigger: body}) + if (body == "Triggered") { + clearInterval(this.state.timer) + this.setState({timer: null}) + this.props.onTrigger() + } }) } @@ -74,19 +45,24 @@ class Trigger extends React.Component { capture_duration_secs: this.state.capture_duration }); - const req = http.put('http://localhost:8080', {path: '/capture', method: 'POST'}, (res) => { - res.on('end', _ => { - // Begin polling the trigger state. - self.setState({timer: setInterval(pollTrigger, 100)}) + fetch('http://localhost:8080/capture', {method: 'POST', body: postData}) + .then(response => { + if (response.ok) { + // Begin polling the trigger state. + this.setState({timer: setInterval(() => this.pollTrigger(), 100)}) + } else { + console.log(`Capture error: ${error}`) + } }) + } - res.on('error', error => { - console.log('Capture error: ${error}') - }) - }) + forceTrigger() { + fetch('http://localhost:8080/trigger', {method: 'POST'}) + } - req.write(postData) - req.end() + onChange(evt) { + // TODO: Validate this is a float first + this.setState({duration: evt.target.value}) } render() { @@ -94,12 +70,13 @@ class Trigger extends React.Component {
{this.state.trigger}
+
); } @@ -108,32 +85,169 @@ class Trigger extends React.Component { class Oscilloscope extends React.Component { constructor(props) { super(props); - this.state = { - width: props.width, - height: props.height, - times: [], - traces: [] - } + this.times = [1, 2, 3] + this.traces = [[0, 0.5, 1]] + this.cg = new CandyGraph() + this.font = null + + createDefaultFont(this.cg).then(font => { + this.font = font + this.drawGraph() + }) } getTraces() { - http.get('http://localhost:8080/data', (res) => { - res.on('data', data => { - const body = JSON.parse(data) - this.setState({times: body.times, traces: body.traces}) + fetch('http://localhost:8080/traces').then(response => { + if (response.ok) { + return response.json(); + } else { + console.log(response) + return Promise.reject(`Data request failed: ${response.text()}`) + } + }).then(data => { + this.times = data.time + this.traces = data.traces + this.drawGraph() + }) + } + + drawGraph() { + if (this.font == null) { + return; + } + + this.cg.canvas.width = this.cg.canvas.height = 384; + + const viewport = { + x: 0, + y: 0, + width: 384, + height: 384, + } + + this.cg.clear([1, 1, 1, 1]) + + const coords = createCartesianCoordinateSystem( + createLinearScale([0, this.times[this.times.length - 1]], [32, viewport.width - 16]), + createLinearScale([-10.24, 10.24], [32, viewport.width - 16]), + ); + + // Create the various traces for the display + var lines = [] + for (var i = 0; i < this.traces.length; i += 1) { + const line = createLineStrip(this.cg, this.times, this.traces[i], { + colors: [1, 0.5, 0.0, 1.0], + widths: 3, }) + + lines.push(line) + } + + + const xAxis = createOrthoAxis(this.cg, coords, "x", this.font, { + labelSide: 1, + tickOffset: -2.5, + tickLength: 6, + tickStep: 0.2, + labelFormatter: (n) => n.toFixed(1), }) + + const yAxis = createOrthoAxis(this.cg, coords, "y", this.font, { + tickOffset: -2.5, + tickLength: 6, + tickStep: 2.0, + labelFormatter: (n) => n.toFixed(1), + }) + + // Render the display to an HTML element. + lines.push(xAxis) + lines.push(yAxis) + + this.cg.render(coords, viewport, lines) + + // Copy the plot to a new canvas and add it to the document. + if (this.canvas == null) { + this.canvas = this.cg.copyTo(viewport) + const element = document.getElementById("oscilloscope-display") + element.parentNode.replaceChild(this.canvas, element) + } else { + this.cg.copyTo(viewport, this.canvas) + } + } + + example() { + if (this.font == null) { + return; + } + + this.cg.canvas.width = this.cg.canvas.height = 384; + + // Generate some x & y data. + const xs = []; + const ys = []; + for (let x = 0; x <= 1; x += 0.001) { + xs.push(x); + ys.push(0.5 + 0.25 * Math.sin(x * 2 * Math.PI)); + } + + // Create a viewport. Units are in pixels. + const viewport = { + x: 0, + y: 0, + width: this.cg.canvas.width, + height: this.cg.canvas.height, + }; + + // Create a coordinate system from two linear scales. Note + // that we add 32 pixels of padding to the left and bottom + // of the viewport, and 16 pixels to the top and right. + const coords = createCartesianCoordinateSystem( + createLinearScale([0, 1], [32, viewport.width - 16]), + createLinearScale([0, 1], [32, viewport.height - 16]) + ); + + // Load the default Lato font + //const font = await createDefaultFont(cg); + + // Clear the viewport. + this.cg.clear([1, 1, 1, 1]); + + // Render the a line strip representing the x & y data, and axes. + this.cg.render(coords, viewport, [ + createLineStrip(this.cg, xs, ys, { + colors: [1, 0.5, 0.0, 1.0], + widths: 3, + }), + createOrthoAxis(this.cg, coords, "x", this.font, { + labelSide: 1, + tickOffset: -2.5, + tickLength: 6, + tickStep: 0.2, + labelFormatter: (n) => n.toFixed(1), + }), + createOrthoAxis(this.cg, coords, "y", this.font, { + tickOffset: 2.5, + tickLength: 6, + tickStep: 0.2, + labelFormatter: (n) => n.toFixed(1), + }), + ]); + + // Copy the plot to a new canvas and add it to the document. + if (this.canvas == null) { + this.canvas = this.cg.copyTo(viewport) + const element = document.getElementById("oscilloscope-display") + element.parentNode.replaceChild(this.canvas, element) + } else { + this.cg.copyTo(viewport, this.canvas) + } } render() { + this.drawGraph() + return (
- this.getTraces()} /> From 43e42e2fedc6198f233caeab8449958b8536dbf3 Mon Sep 17 00:00:00 2001 From: Ryan Summers Date: Tue, 2 Nov 2021 14:06:49 +0100 Subject: [PATCH 05/39] Adding WIP frontend --- frontend/app.py | 125 ++++++++++++++++++++++++++++++++++++++++++++++++ src/lib.rs | 15 ++++-- 2 files changed, 137 insertions(+), 3 deletions(-) create mode 100644 frontend/app.py diff --git a/frontend/app.py b/frontend/app.py new file mode 100644 index 0000000..1b7ef94 --- /dev/null +++ b/frontend/app.py @@ -0,0 +1,125 @@ +#!/usr/bin/python3 +""" +Author: Vertigo Designs, Ryan Summers + +Description: Bokeh application for serving Stabilizer stream visuals. +""" +import bokeh.plotting +import bokeh.layouts +import bokeh.document +import bokeh.io +import bokeh.models +import requests +import logging +import json +from typing import List + +DEFAULT_SERVER = 'http://127.0.0.1:8080' + + +class ReceiverApi: + + def __init__(self, server: str): + self.server = server + + + def get_json(self, path: str) -> dict: + response = requests.get(self.server + path) + assert response.ok, f'GET {path} failed: {response.text}' + return response.json() + + + def post_json(self, path: str, payload: dict = None): + if payload is None: + payload = dict() + + response = requests.post(self.server + path, data=json.dumps(payload)) + assert response.ok, f'POST {path} ({payload}) failed: {response.text}' + + + def get_trigger(self) -> str: + return self.get_json('/trigger') + + + def start_capture(self, duration: float): + request = { + 'capture_duration_secs': duration, + } + + self.post_json('/capture', request) + + +class StreamVisualizer: + + def __init__(self, document: bokeh.document.Document, server: str = DEFAULT_SERVER): + self.figure = bokeh.plotting.figure() + + # Add a trigger button + self._trigger_button = bokeh.models.Button(label='Capture', button_type='primary') + self._trigger_button.on_click(self.capture) + + document.add_root(bokeh.layouts.row(self.figure, self._trigger_button)) + + # TODO: Add a capture duration input + + self.doc = document + self._callback = None + + self.api = ReceiverApi(server) + self._data_store = bokeh.models.ColumnDataSource(data={ + 'time': [] + }) + + + def update(self): + if self.api.get_json('/trigger') == "Stopped": + trace_data = self.api.get_json('/traces') + self._redraw(**trace_data) + + # Disable the document callback now that we have redrawn the plot. + if self._callback is not None: + self.doc.remove_periodic_callback(self._callback) + self._callback = None + + + def _redraw(self, time: List[float], traces: List[dict]): + # Remove any existing trace data from the store. + for key in self._data_store.data: + # TODO: Do we need to remove the plot from the figure? + if key != 'time': + del self._data_store.data[key] + + # Update the timebase + self._data_store.data['time'] = time + + # Update traces + for trace in traces: + self._data_store.data[trace['label']] = trace['data'] + self.figure.circle(x='time', y=trace['label'], source=self._data_store) + + + def capture(self, duration: float = None): + if duration is None: + # TODO: If the duration is not explicitly provided, get it from the UI. + duration = 1 + + self.api.start_capture(duration) + + # Start periodicly reading trigger state. + self._callback = self.doc.add_periodic_callback(self.update, + period_milliseconds=100) + + +def main(): + logging.info('Startup') + + document = bokeh.io.curdoc() + + visualizer = StreamVisualizer(document) + + # Debug: Force a trigger + visualizer.capture(1.0) + visualizer.api.post_json('/trigger') + + +main() diff --git a/src/lib.rs b/src/lib.rs index 4a19e19..f3f5c75 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -8,6 +8,7 @@ use std::time::Duration; pub struct StreamReceiver { socket: UdpSocket, buf: [u8; 2048], + timeout: Option } impl StreamReceiver { @@ -18,20 +19,28 @@ impl StreamReceiver { /// communciate with Stabilizer. /// * `port` - The port that livestream data is being sent to. pub async fn new(ip: std::net::Ipv4Addr, port: u16) -> Self { + log::info!("Binding to {}:{}", ip, port); let socket = UdpSocket::bind((ip, port)).await.unwrap(); Self { socket, + timeout: None, buf: [0; 2048], } } + pub fn set_timeout(&mut self, duration: Duration) { + self.timeout.replace(duration); + } + /// Receive a stream frame from Stabilizer. pub async fn next_frame(&mut self) -> Option { // Read a single UDP packet. - let len = async_std::io::timeout(Duration::from_secs(1), self.socket.recv(&mut self.buf)) - .await - .unwrap(); + let len = if let Some(timeout) = self.timeout { + async_std::io::timeout(timeout, self.socket.recv(&mut self.buf)).await.unwrap() + } else { + self.socket.recv(&mut self.buf).await.unwrap() + }; // Deserialize the stream frame. StreamFrame::from_bytes(&self.buf[..len]) From 3d25d51bc44acaab54f0b949667ed455191476e8 Mon Sep 17 00:00:00 2001 From: Ryan Summers Date: Tue, 2 Nov 2021 16:50:26 +0100 Subject: [PATCH 06/39] Converting to a Bokeh frontend --- frontend/app.py | 68 ++++++++++++++++++++++++++++++----------- src/bin/stream_test.rs | 1 + src/bin/webserver.rs | 8 ----- src/de/data/mod.rs | 69 +++++++++++++++++++++++++++++++++++++----- 4 files changed, 112 insertions(+), 34 deletions(-) diff --git a/frontend/app.py b/frontend/app.py index 1b7ef94..c7c9ab8 100644 --- a/frontend/app.py +++ b/frontend/app.py @@ -7,6 +7,7 @@ import bokeh.plotting import bokeh.layouts import bokeh.document +import bokeh.palettes import bokeh.io import bokeh.models import requests @@ -52,13 +53,25 @@ def start_capture(self, duration: float): class StreamVisualizer: def __init__(self, document: bokeh.document.Document, server: str = DEFAULT_SERVER): - self.figure = bokeh.plotting.figure() + figure = bokeh.plotting.figure(sizing_mode='stretch_both') # Add a trigger button - self._trigger_button = bokeh.models.Button(label='Capture', button_type='primary') - self._trigger_button.on_click(self.capture) + trigger_button = bokeh.models.Button(label='Single', button_type='primary') + trigger_button.on_click(self.capture) - document.add_root(bokeh.layouts.row(self.figure, self._trigger_button)) + force_button = bokeh.models.Button(label='Force', button_type='primary') + force_button.on_click(lambda: self.api.post_json('/trigger')) + + self._capture_duration_input = bokeh.models.TextInput(title='Capture Duration', + value='0.001', width=100, sizing_mode='fixed') + self._capture_duration_input.on_change('value', self.handle_duration) + self._trigger_state = bokeh.models.Div(text='Trigger State: IDLE') + + control_layout = bokeh.layouts.column(self._trigger_state, trigger_button, force_button, + self._capture_duration_input) + + self.layout = bokeh.layouts.row(figure, control_layout, sizing_mode='stretch_height') + document.add_root(self.layout) # TODO: Add a capture duration input @@ -71,8 +84,18 @@ def __init__(self, document: bokeh.document.Document, server: str = DEFAULT_SERV }) + def handle_duration(self, _attr, old_value, new_value): + try: + self._capture_duration_input.value = str(float(new_value)) + except ValueError: + self._capture_duration_input.value = str(float(old_value)) + pass + + def update(self): - if self.api.get_json('/trigger') == "Stopped": + trigger = self.api.get_json('/trigger') + self._trigger_state.text = f'Trigger State: {trigger.upper()}' + if trigger == "Stopped": trace_data = self.api.get_json('/traces') self._redraw(**trace_data) @@ -83,25 +106,33 @@ def update(self): def _redraw(self, time: List[float], traces: List[dict]): - # Remove any existing trace data from the store. - for key in self._data_store.data: - # TODO: Do we need to remove the plot from the figure? - if key != 'time': - del self._data_store.data[key] + # Update the data store atomically + new_datastore = { + 'time': time, + } - # Update the timebase - self._data_store.data['time'] = time + for trace in traces: + new_datastore[trace['label']] = trace['data'] + + self._data_store = new_datastore + + figure = bokeh.plotting.figure(sizing_mode="stretch_both") # Update traces - for trace in traces: - self._data_store.data[trace['label']] = trace['data'] - self.figure.circle(x='time', y=trace['label'], source=self._data_store) + palette = bokeh.palettes.d3['Category10'][len(traces)] + for (trace, color) in zip(traces, palette): + figure.circle(x='time', y=trace['label'], source=self._data_store, color=color, + legend_label=trace['label']) + + figure.legend.location = 'top_left' + figure.legend.click_policy = 'hide' + + self.layout.children[0] = figure def capture(self, duration: float = None): if duration is None: - # TODO: If the duration is not explicitly provided, get it from the UI. - duration = 1 + duration = float(self._capture_duration_input.value) self.api.start_capture(duration) @@ -114,11 +145,12 @@ def main(): logging.info('Startup') document = bokeh.io.curdoc() + document.theme = 'dark_minimal' visualizer = StreamVisualizer(document) # Debug: Force a trigger - visualizer.capture(1.0) + visualizer.capture(0.001) visualizer.api.post_json('/trigger') diff --git a/src/bin/stream_test.rs b/src/bin/stream_test.rs index 8cefd25..7cb3b6f 100644 --- a/src/bin/stream_test.rs +++ b/src/bin/stream_test.rs @@ -30,6 +30,7 @@ async fn main() { log::info!("Binding to socket"); let mut stream_receiver = StreamReceiver::new(ip, opts.port).await; + stream_receiver.set_timeout(Duration::from_secs(1)); let mut total_batches = 0u64; let mut dropped_batches = 0u64; diff --git a/src/bin/webserver.rs b/src/bin/webserver.rs index 9cff180..3283a5c 100644 --- a/src/bin/webserver.rs +++ b/src/bin/webserver.rs @@ -279,14 +279,6 @@ async fn main() -> tide::Result<()> { webapp.at("/trigger").get(get_trigger).post(force_trigger); webapp.at("/capture").post(configure_capture); - // Serve front-end files. - webapp.at("/").get(|_| async { - Ok(Response::builder(200).body(Body::from_file("frontend/dist/index.html").await?)) - }); - webapp.at("/main.js").get(|_| async { - Ok(Response::builder(200).body(Body::from_file("frontend/dist/main.js").await?)) - }); - // Start up the webapp. webapp.listen("127.0.0.1:8080").await?; diff --git a/src/de/data/mod.rs b/src/de/data/mod.rs index 27c1d05..5af1132 100644 --- a/src/de/data/mod.rs +++ b/src/de/data/mod.rs @@ -3,6 +3,53 @@ pub enum FormatError { InvalidSize, } +/// Custom type for referencing DAC output codes. +/// The internal integer is the raw code written to the DAC output register. +#[derive(Copy, Clone)] +pub struct DacCode(pub u16); + +impl From 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; + + (code.0 as i16).wrapping_add(i16::MIN) as f32 * dac_volts_per_lsb + } +} + +/// A type representing an ADC sample. +#[derive(Copy, Clone)] +pub struct AdcCode(pub u16); + +impl From 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 for i16 { + /// Get a stabilizer-defined code from the ADC code. + fn from(code: AdcCode) -> i16 { + code.0 as i16 + } +} + +impl From 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 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; + + i16::from(code) as f32 * adc_volts_per_lsb + } +} + pub trait FrameData { fn trace_count(&self) -> usize; fn get_trace(&self, index: usize) -> &Vec; @@ -63,15 +110,21 @@ impl AdcDacData { for batch in 0..num_batches { let batch_index = batch * batch_size_bytes; - // Deserialize the batch - for sample in 0..batch_size { - let sample_index = batch_index + sample * 8; - for (i, trace) in traces.iter_mut().enumerate() { - let trace_index = sample_index + i * 2; + // Batches are serialized as , where the number of samples in + // ` is equal to that batch_size. + for (i, trace) in traces.iter_mut().enumerate() { + let trace_index = batch_index + 2 * batch_size * i; + + for sample in 0..batch_size { + let sample_index = trace_index + sample * 2; + let value = { - let code = u16::from_le_bytes([data[trace_index], data[trace_index + 1]]); - // TODO: Convert code from u16 to floating point voltage. - code as f32 + let code = u16::from_le_bytes([data[sample_index], data[sample_index+ 1]]); + if i < 2 { + f32::from(AdcCode(code)) + } else { + f32::from(DacCode(code)) + } }; trace.push(value); From 38af4692ff659e3f27157a32981c3b3b0052f328 Mon Sep 17 00:00:00 2001 From: Ryan Summers Date: Tue, 2 Nov 2021 17:00:00 +0100 Subject: [PATCH 07/39] Enabling webgl --- frontend/app.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/frontend/app.py b/frontend/app.py index c7c9ab8..6d3d6a6 100644 --- a/frontend/app.py +++ b/frontend/app.py @@ -53,7 +53,7 @@ def start_capture(self, duration: float): class StreamVisualizer: def __init__(self, document: bokeh.document.Document, server: str = DEFAULT_SERVER): - figure = bokeh.plotting.figure(sizing_mode='stretch_both') + figure = bokeh.plotting.figure(output_backend="webgl", sizing_mode='stretch_both') # Add a trigger button trigger_button = bokeh.models.Button(label='Single', button_type='primary') @@ -116,7 +116,7 @@ def _redraw(self, time: List[float], traces: List[dict]): self._data_store = new_datastore - figure = bokeh.plotting.figure(sizing_mode="stretch_both") + figure = bokeh.plotting.figure(output_backend="webgl", sizing_mode="stretch_both") # Update traces palette = bokeh.palettes.d3['Category10'][len(traces)] From 96c92399b914fca6ffaac3e52a3408efb50ee8b2 Mon Sep 17 00:00:00 2001 From: Ryan Summers Date: Fri, 19 Nov 2021 14:57:45 +0100 Subject: [PATCH 08/39] Refactoring to display traces --- build.rs | 6 ++ frontend/app.py | 157 ----------------------------------------- frontend/src/react.jsx | 108 ++++++++-------------------- src/bin/webserver.rs | 2 + 4 files changed, 36 insertions(+), 237 deletions(-) create mode 100644 build.rs delete mode 100644 frontend/app.py diff --git a/build.rs b/build.rs new file mode 100644 index 0000000..4124696 --- /dev/null +++ b/build.rs @@ -0,0 +1,6 @@ +use npm_rs::NpmEnv; + +fn main() { + let status = NpmEnv::default().set_path("frontend").init_env().install(None).run("build").exec().unwrap(); + assert!(status.success()); +} diff --git a/frontend/app.py b/frontend/app.py deleted file mode 100644 index 6d3d6a6..0000000 --- a/frontend/app.py +++ /dev/null @@ -1,157 +0,0 @@ -#!/usr/bin/python3 -""" -Author: Vertigo Designs, Ryan Summers - -Description: Bokeh application for serving Stabilizer stream visuals. -""" -import bokeh.plotting -import bokeh.layouts -import bokeh.document -import bokeh.palettes -import bokeh.io -import bokeh.models -import requests -import logging -import json -from typing import List - -DEFAULT_SERVER = 'http://127.0.0.1:8080' - - -class ReceiverApi: - - def __init__(self, server: str): - self.server = server - - - def get_json(self, path: str) -> dict: - response = requests.get(self.server + path) - assert response.ok, f'GET {path} failed: {response.text}' - return response.json() - - - def post_json(self, path: str, payload: dict = None): - if payload is None: - payload = dict() - - response = requests.post(self.server + path, data=json.dumps(payload)) - assert response.ok, f'POST {path} ({payload}) failed: {response.text}' - - - def get_trigger(self) -> str: - return self.get_json('/trigger') - - - def start_capture(self, duration: float): - request = { - 'capture_duration_secs': duration, - } - - self.post_json('/capture', request) - - -class StreamVisualizer: - - def __init__(self, document: bokeh.document.Document, server: str = DEFAULT_SERVER): - figure = bokeh.plotting.figure(output_backend="webgl", sizing_mode='stretch_both') - - # Add a trigger button - trigger_button = bokeh.models.Button(label='Single', button_type='primary') - trigger_button.on_click(self.capture) - - force_button = bokeh.models.Button(label='Force', button_type='primary') - force_button.on_click(lambda: self.api.post_json('/trigger')) - - self._capture_duration_input = bokeh.models.TextInput(title='Capture Duration', - value='0.001', width=100, sizing_mode='fixed') - self._capture_duration_input.on_change('value', self.handle_duration) - self._trigger_state = bokeh.models.Div(text='Trigger State: IDLE') - - control_layout = bokeh.layouts.column(self._trigger_state, trigger_button, force_button, - self._capture_duration_input) - - self.layout = bokeh.layouts.row(figure, control_layout, sizing_mode='stretch_height') - document.add_root(self.layout) - - # TODO: Add a capture duration input - - self.doc = document - self._callback = None - - self.api = ReceiverApi(server) - self._data_store = bokeh.models.ColumnDataSource(data={ - 'time': [] - }) - - - def handle_duration(self, _attr, old_value, new_value): - try: - self._capture_duration_input.value = str(float(new_value)) - except ValueError: - self._capture_duration_input.value = str(float(old_value)) - pass - - - def update(self): - trigger = self.api.get_json('/trigger') - self._trigger_state.text = f'Trigger State: {trigger.upper()}' - if trigger == "Stopped": - trace_data = self.api.get_json('/traces') - self._redraw(**trace_data) - - # Disable the document callback now that we have redrawn the plot. - if self._callback is not None: - self.doc.remove_periodic_callback(self._callback) - self._callback = None - - - def _redraw(self, time: List[float], traces: List[dict]): - # Update the data store atomically - new_datastore = { - 'time': time, - } - - for trace in traces: - new_datastore[trace['label']] = trace['data'] - - self._data_store = new_datastore - - figure = bokeh.plotting.figure(output_backend="webgl", sizing_mode="stretch_both") - - # Update traces - palette = bokeh.palettes.d3['Category10'][len(traces)] - for (trace, color) in zip(traces, palette): - figure.circle(x='time', y=trace['label'], source=self._data_store, color=color, - legend_label=trace['label']) - - figure.legend.location = 'top_left' - figure.legend.click_policy = 'hide' - - self.layout.children[0] = figure - - - def capture(self, duration: float = None): - if duration is None: - duration = float(self._capture_duration_input.value) - - self.api.start_capture(duration) - - # Start periodicly reading trigger state. - self._callback = self.doc.add_periodic_callback(self.update, - period_milliseconds=100) - - -def main(): - logging.info('Startup') - - document = bokeh.io.curdoc() - document.theme = 'dark_minimal' - - visualizer = StreamVisualizer(document) - - # Debug: Force a trigger - visualizer.capture(0.001) - visualizer.api.post_json('/trigger') - - -main() diff --git a/frontend/src/react.jsx b/frontend/src/react.jsx index 023a308..c9c2a68 100644 --- a/frontend/src/react.jsx +++ b/frontend/src/react.jsx @@ -4,6 +4,7 @@ import CandyGraph, { createLineStrip, createDefaultFont, createOrthoAxis, + createText, } from "candygraph"; import ReactDOM from 'react-dom'; @@ -16,7 +17,7 @@ class Trigger extends React.Component { this.state = { trigger: 'Idle', timer: null, - capture_duration: 1, + capture_duration: 0.001, } } @@ -32,7 +33,7 @@ class Trigger extends React.Component { console.log(`Trigger state: ${body}`) this.setState({trigger: body}) - if (body == "Triggered") { + if (body == "Stopped") { clearInterval(this.state.timer) this.setState({timer: null}) this.props.onTrigger() @@ -61,8 +62,7 @@ class Trigger extends React.Component { } onChange(evt) { - // TODO: Validate this is a float first - this.setState({duration: evt.target.value}) + this.setState({capture_duration: Number(evt.target.value)}) } render() { @@ -70,7 +70,7 @@ class Trigger extends React.Component {
{this.state.trigger}
@@ -86,7 +86,7 @@ class Oscilloscope extends React.Component { constructor(props) { super(props); this.times = [1, 2, 3] - this.traces = [[0, 0.5, 1]] + this.traces = [{'label': '', 'data': [0, 0.5, 1]}] this.cg = new CandyGraph() this.font = null @@ -127,29 +127,44 @@ class Oscilloscope extends React.Component { this.cg.clear([1, 1, 1, 1]) + const max_time = Math.max(...this.times) + const coords = createCartesianCoordinateSystem( - createLinearScale([0, this.times[this.times.length - 1]], [32, viewport.width - 16]), + createLinearScale([0, max_time], [32, viewport.width - 16]), createLinearScale([-10.24, 10.24], [32, viewport.width - 16]), ); // Create the various traces for the display + const colors = [ + [1, 0, 0, 1.0], + [0, 1, 0, 1.0], + [0, 0, 1, 1.0], + [1, 0, 1, 1.0], + [1, 1, 0, 1.0], + [1, 1, 1, 1.0], + ] var lines = [] for (var i = 0; i < this.traces.length; i += 1) { - const line = createLineStrip(this.cg, this.times, this.traces[i], { - colors: [1, 0.5, 0.0, 1.0], + const line = createLineStrip(this.cg, this.times, this.traces[i].data, { + colors: colors[i], widths: 3, }) lines.push(line) - } + const label = createText(this.cg, this.font, this.traces[i].label, [max_time / 10, 8 - i * 0.7], { + color: colors[i], + }) + + lines.push(label) + } const xAxis = createOrthoAxis(this.cg, coords, "x", this.font, { labelSide: 1, tickOffset: -2.5, tickLength: 6, - tickStep: 0.2, - labelFormatter: (n) => n.toFixed(1), + tickStep: max_time / 5, + labelFormatter: (n) => n.toExponential(2), }) const yAxis = createOrthoAxis(this.cg, coords, "y", this.font, { @@ -163,6 +178,7 @@ class Oscilloscope extends React.Component { lines.push(xAxis) lines.push(yAxis) + console.log('Redrawing plot') this.cg.render(coords, viewport, lines) // Copy the plot to a new canvas and add it to the document. @@ -175,74 +191,6 @@ class Oscilloscope extends React.Component { } } - example() { - if (this.font == null) { - return; - } - - this.cg.canvas.width = this.cg.canvas.height = 384; - - // Generate some x & y data. - const xs = []; - const ys = []; - for (let x = 0; x <= 1; x += 0.001) { - xs.push(x); - ys.push(0.5 + 0.25 * Math.sin(x * 2 * Math.PI)); - } - - // Create a viewport. Units are in pixels. - const viewport = { - x: 0, - y: 0, - width: this.cg.canvas.width, - height: this.cg.canvas.height, - }; - - // Create a coordinate system from two linear scales. Note - // that we add 32 pixels of padding to the left and bottom - // of the viewport, and 16 pixels to the top and right. - const coords = createCartesianCoordinateSystem( - createLinearScale([0, 1], [32, viewport.width - 16]), - createLinearScale([0, 1], [32, viewport.height - 16]) - ); - - // Load the default Lato font - //const font = await createDefaultFont(cg); - - // Clear the viewport. - this.cg.clear([1, 1, 1, 1]); - - // Render the a line strip representing the x & y data, and axes. - this.cg.render(coords, viewport, [ - createLineStrip(this.cg, xs, ys, { - colors: [1, 0.5, 0.0, 1.0], - widths: 3, - }), - createOrthoAxis(this.cg, coords, "x", this.font, { - labelSide: 1, - tickOffset: -2.5, - tickLength: 6, - tickStep: 0.2, - labelFormatter: (n) => n.toFixed(1), - }), - createOrthoAxis(this.cg, coords, "y", this.font, { - tickOffset: 2.5, - tickLength: 6, - tickStep: 0.2, - labelFormatter: (n) => n.toFixed(1), - }), - ]); - - // Copy the plot to a new canvas and add it to the document. - if (this.canvas == null) { - this.canvas = this.cg.copyTo(viewport) - const element = document.getElementById("oscilloscope-display") - element.parentNode.replaceChild(this.canvas, element) - } else { - this.cg.copyTo(viewport, this.canvas) - } - } - render() { this.drawGraph() diff --git a/src/bin/webserver.rs b/src/bin/webserver.rs index 3283a5c..b0ec353 100644 --- a/src/bin/webserver.rs +++ b/src/bin/webserver.rs @@ -278,6 +278,8 @@ async fn main() -> tide::Result<()> { webapp.at("/traces").get(get_traces); webapp.at("/trigger").get(get_trigger).post(force_trigger); webapp.at("/capture").post(configure_capture); + webapp.at("/").serve_file("frontend/dist/index.html").unwrap(); + webapp.at("/").serve_dir("frontend/dist").unwrap(); // Start up the webapp. webapp.listen("127.0.0.1:8080").await?; From 148df5a77c5d00b911a731b73d9befd5b38f3d16 Mon Sep 17 00:00:00 2001 From: Ryan Summers Date: Fri, 19 Nov 2021 16:10:30 +0100 Subject: [PATCH 09/39] Updating JS --- frontend/dist/index.html | 8 +++-- frontend/package-lock.json | 10 +++++++ frontend/package.json | 2 ++ frontend/src/react.jsx | 60 +++++++++++++++++++++++++------------- 4 files changed, 57 insertions(+), 23 deletions(-) diff --git a/frontend/dist/index.html b/frontend/dist/index.html index a513728..248497f 100644 --- a/frontend/dist/index.html +++ b/frontend/dist/index.html @@ -1,13 +1,15 @@ + + + + + Stabilizer Livestream Viewer -
-
-
diff --git a/frontend/package-lock.json b/frontend/package-lock.json index 19411bd..64db730 100644 --- a/frontend/package-lock.json +++ b/frontend/package-lock.json @@ -4,6 +4,11 @@ "lockfileVersion": 1, "requires": true, "dependencies": { + "@popperjs/core": { + "version": "2.10.2", + "resolved": "https://registry.npmjs.org/@popperjs/core/-/core-2.10.2.tgz", + "integrity": "sha512-IXf3XA7+XyN7CP9gGh/XB0UxVMlvARGEgGXLubFICsUMGz6Q+DU+i4gGlpOxTjKvXjkJDJC8YdqdKkDj9qZHEQ==" + }, "JSONStream": { "version": "1.3.5", "resolved": "https://registry.npmjs.org/JSONStream/-/JSONStream-1.3.5.tgz", @@ -142,6 +147,11 @@ "resolved": "https://registry.npmjs.org/bn.js/-/bn.js-5.2.0.tgz", "integrity": "sha512-D7iWRBvnZE8ecXiLj/9wbxH7Tk79fAh8IHaTNq1RWRixsS02W+5qS+iE9yq6RYl0asXx5tw0bLhmT5pIfbSquw==" }, + "bootstrap": { + "version": "5.1.3", + "resolved": "https://registry.npmjs.org/bootstrap/-/bootstrap-5.1.3.tgz", + "integrity": "sha512-fcQztozJ8jToQWXxVuEyXWW+dSo8AiXWKwiSSrKWsRB/Qt+Ewwza+JWoLKiTuQLaEPhdNAJ7+Dosc9DOIqNy7Q==" + }, "brace-expansion": { "version": "1.1.11", "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.11.tgz", diff --git a/frontend/package.json b/frontend/package.json index 4e245d9..9441d9f 100644 --- a/frontend/package.json +++ b/frontend/package.json @@ -9,6 +9,8 @@ "author": "Ryan Summers", "license": "MIT", "dependencies": { + "@popperjs/core": "^2.10.2", + "bootstrap": "^5.1.3", "browserify": "^17.0.0", "candygraph": "^0.4.2", "esbuild": "^0.13.12", diff --git a/frontend/src/react.jsx b/frontend/src/react.jsx index c9c2a68..84a28b3 100644 --- a/frontend/src/react.jsx +++ b/frontend/src/react.jsx @@ -1,3 +1,6 @@ +import "bootstrap"; +import "bootstrap/dist/css/bootstrap.css"; + import CandyGraph, { createCartesianCoordinateSystem, createLinearScale, @@ -68,15 +71,15 @@ class Trigger extends React.Component { render() { return (
- +
+ Duration + +
-
{this.state.trigger}
+
{this.state.trigger}
- - + +
); } @@ -89,6 +92,10 @@ class Oscilloscope extends React.Component { this.traces = [{'label': '', 'data': [0, 0.5, 1]}] this.cg = new CandyGraph() this.font = null + this.state = { + width: 384, + height: 384, + } createDefaultFont(this.cg).then(font => { this.font = font @@ -96,6 +103,13 @@ class Oscilloscope extends React.Component { }) } + componentDidMount() { + const height = this.divElement.clientHeight + const width = this.divElement.clientWidth + console.log(width, height) + this.setState({height, width: width}) + } + getTraces() { fetch('http://localhost:8080/traces').then(response => { if (response.ok) { @@ -112,26 +126,34 @@ class Oscilloscope extends React.Component { } drawGraph() { - if (this.font == null) { + if (this.font == null || this.state.width == 0 || this.state.height == 0) { return; } - this.cg.canvas.width = this.cg.canvas.height = 384; + if (this.canvas == null) { + this.canvas = document.getElementById("oscilloscope-display") + } + + this.canvas.width = this.state.width + this.cg.canvas.width = this.state.width + this.cg.canvas.height = this.canvas.height const viewport = { x: 0, y: 0, - width: 384, - height: 384, + width: this.state.width, + height: this.canvas.height, } + console.log(viewport) + this.cg.clear([1, 1, 1, 1]) const max_time = Math.max(...this.times) const coords = createCartesianCoordinateSystem( createLinearScale([0, max_time], [32, viewport.width - 16]), - createLinearScale([-10.24, 10.24], [32, viewport.width - 16]), + createLinearScale([-10.24, 10.24], [32, viewport.height - 16]), ); // Create the various traces for the display @@ -182,20 +204,18 @@ class Oscilloscope extends React.Component { this.cg.render(coords, viewport, lines) // Copy the plot to a new canvas and add it to the document. - if (this.canvas == null) { - this.canvas = this.cg.copyTo(viewport) - const element = document.getElementById("oscilloscope-display") - element.parentNode.replaceChild(this.canvas, element) - } else { - this.cg.copyTo(viewport, this.canvas) - } + this.cg.copyTo(viewport, this.canvas) } render() { this.drawGraph() return ( -
+
+
{ this.divElement = divElement } } > + +
this.getTraces()} /> From d46aaf01ff9d48586c0b486a109baf3f80d10375 Mon Sep 17 00:00:00 2001 From: Ryan Summers Date: Fri, 19 Nov 2021 17:21:10 +0100 Subject: [PATCH 10/39] Adding WIP updates --- frontend/src/react.jsx | 103 +++++++++++++++++++---------------------- src/bin/webserver.rs | 3 ++ 2 files changed, 50 insertions(+), 56 deletions(-) diff --git a/frontend/src/react.jsx b/frontend/src/react.jsx index 84a28b3..dbb9291 100644 --- a/frontend/src/react.jsx +++ b/frontend/src/react.jsx @@ -21,6 +21,7 @@ class Trigger extends React.Component { trigger: 'Idle', timer: null, capture_duration: 0.001, + running: false, } } @@ -37,9 +38,29 @@ class Trigger extends React.Component { this.setState({trigger: body}) if (body == "Stopped") { + console.log('Pulling traces') clearInterval(this.state.timer) - this.setState({timer: null}) - this.props.onTrigger() + if (this.state.timer) { + this.getTraces() + this.setState({timer: null}) + } + } + }) + } + + getTraces() { + fetch('http://localhost:8080/traces').then(response => { + if (response.ok) { + return response.json(); + } else { + return Promise.reject(`Data request failed: ${response.text()}`) + } + }).then(data => { + console.log('Redrawing') + this.props.onData(data.time, data.traces) + if (this.state.running) { + console.log('Recapturing') + this.startCapture() } }) } @@ -48,26 +69,30 @@ class Trigger extends React.Component { const postData = JSON.stringify({ capture_duration_secs: this.state.capture_duration }); + console.log('Starting capture') fetch('http://localhost:8080/capture', {method: 'POST', body: postData}) .then(response => { if (response.ok) { // Begin polling the trigger state. - this.setState({timer: setInterval(() => this.pollTrigger(), 100)}) + this.setState({ timer: setInterval(() => this.pollTrigger(), 10), }) } else { console.log(`Capture error: ${error}`) } }) } - forceTrigger() { - fetch('http://localhost:8080/trigger', {method: 'POST'}) - } - onChange(evt) { this.setState({capture_duration: Number(evt.target.value)}) } + toggleCapture() { + this.setState({running: !this.state.running}) + if (!this.state.running) { + this.startCapture(); + } + } + render() { return (
@@ -78,8 +103,8 @@ class Trigger extends React.Component {
{this.state.trigger}
+ -
); } @@ -88,68 +113,40 @@ class Trigger extends React.Component { class Oscilloscope extends React.Component { constructor(props) { super(props); - this.times = [1, 2, 3] - this.traces = [{'label': '', 'data': [0, 0.5, 1]}] this.cg = new CandyGraph() this.font = null - this.state = { - width: 384, - height: 384, - } + this.canvas = null createDefaultFont(this.cg).then(font => { this.font = font - this.drawGraph() + this.drawGraph([1, 2, 3], [{'label': '', 'data': [0, 0.5, 1]}]) }) } componentDidMount() { - const height = this.divElement.clientHeight const width = this.divElement.clientWidth - console.log(width, height) - this.setState({height, width: width}) + this.canvas = document.getElementById("oscilloscope-display") + this.canvas.width = width } - getTraces() { - fetch('http://localhost:8080/traces').then(response => { - if (response.ok) { - return response.json(); - } else { - console.log(response) - return Promise.reject(`Data request failed: ${response.text()}`) - } - }).then(data => { - this.times = data.time - this.traces = data.traces - this.drawGraph() - }) - } - - drawGraph() { - if (this.font == null || this.state.width == 0 || this.state.height == 0) { + drawGraph(times, traces) { + if (this.font == null || this.canvas == null) { return; } - if (this.canvas == null) { - this.canvas = document.getElementById("oscilloscope-display") - } - - this.canvas.width = this.state.width - this.cg.canvas.width = this.state.width + this.cg.canvas.width = this.canvas.width this.cg.canvas.height = this.canvas.height const viewport = { x: 0, y: 0, - width: this.state.width, + width: this.canvas.width, height: this.canvas.height, } - console.log(viewport) - this.cg.clear([1, 1, 1, 1]) - const max_time = Math.max(...this.times) + const max_time = Math.max(...times) const coords = createCartesianCoordinateSystem( createLinearScale([0, max_time], [32, viewport.width - 16]), @@ -166,15 +163,15 @@ class Oscilloscope extends React.Component { [1, 1, 1, 1.0], ] var lines = [] - for (var i = 0; i < this.traces.length; i += 1) { - const line = createLineStrip(this.cg, this.times, this.traces[i].data, { + for (var i = 0; i < traces.length; i += 1) { + const line = createLineStrip(this.cg, times, traces[i].data, { colors: colors[i], widths: 3, }) lines.push(line) - const label = createText(this.cg, this.font, this.traces[i].label, [max_time / 10, 8 - i * 0.7], { + const label = createText(this.cg, this.font, traces[i].label, [max_time / 10, 8 - i * 0.7], { color: colors[i], }) @@ -200,7 +197,6 @@ class Oscilloscope extends React.Component { lines.push(xAxis) lines.push(yAxis) - console.log('Redrawing plot') this.cg.render(coords, viewport, lines) // Copy the plot to a new canvas and add it to the document. @@ -208,17 +204,12 @@ class Oscilloscope extends React.Component { } render() { - this.drawGraph() - return (
-
{ this.divElement = divElement } } > +
{ this.divElement = divElement } } >
- this.getTraces()} - /> + this.drawGraph(times, traces)} />
); } diff --git a/src/bin/webserver.rs b/src/bin/webserver.rs index b0ec353..4354a78 100644 --- a/src/bin/webserver.rs +++ b/src/bin/webserver.rs @@ -217,6 +217,9 @@ async fn configure_capture( log::info!("Arming trigger"); data.trigger = TriggerState::Armed; + log::info!("Forcing trigger"); + data.trigger = TriggerState::Triggered; + if config.capture_duration_secs < 0. { return Ok(Response::builder(400).body("Negative capture duration not supported")); } From b0b6de8878575b8399eba5520d8f0be59240d972 Mon Sep 17 00:00:00 2001 From: Ryan Summers Date: Fri, 19 Nov 2021 19:22:54 +0100 Subject: [PATCH 11/39] Adding WIP updates --- README.md | 5 + build.rs | 8 +- frontend/src/react.jsx | 56 +++--------- src/bin/webserver.rs | 202 +++++++++++++++++++++++------------------ src/de/data/mod.rs | 2 +- src/lib.rs | 6 +- 6 files changed, 145 insertions(+), 134 deletions(-) diff --git a/README.md b/README.md index 1bc976c..a881045 100644 --- a/README.md +++ b/README.md @@ -1,2 +1,7 @@ # stabilizer-streaming Host-side stream utilities for interacting with Stabilizer's livestream + +## Usage +1. Install npm +2. `cargo run --bin webserver -- --ip ` +3. Navigate to `localhost:8080` diff --git a/build.rs b/build.rs index 4124696..7893f02 100644 --- a/build.rs +++ b/build.rs @@ -1,6 +1,12 @@ use npm_rs::NpmEnv; fn main() { - let status = NpmEnv::default().set_path("frontend").init_env().install(None).run("build").exec().unwrap(); + let status = NpmEnv::default() + .set_path("frontend") + .init_env() + .install(None) + .run("build") + .exec() + .unwrap(); assert!(status.success()); } diff --git a/frontend/src/react.jsx b/frontend/src/react.jsx index dbb9291..daf013f 100644 --- a/frontend/src/react.jsx +++ b/frontend/src/react.jsx @@ -18,34 +18,11 @@ class Trigger extends React.Component { super(props) this.onChange = this.onChange.bind(this) this.state = { - trigger: 'Idle', - timer: null, capture_duration: 0.001, running: false, } - } - - pollTrigger() { - fetch('http://localhost:8080/trigger').then(response => { - if (response.ok) { - return response.json(); - } else { - return Promise.reject(`Trigger rerequest failed: ${response.text()}`) - } - }).then(body => { - console.log(`Trigger state: ${body}`) - - this.setState({trigger: body}) - if (body == "Stopped") { - console.log('Pulling traces') - clearInterval(this.state.timer) - if (this.state.timer) { - this.getTraces() - this.setState({timer: null}) - } - } - }) + this.setCaptureDuration(this.state.capture_duration) } getTraces() { @@ -56,40 +33,37 @@ class Trigger extends React.Component { return Promise.reject(`Data request failed: ${response.text()}`) } }).then(data => { - console.log('Redrawing') this.props.onData(data.time, data.traces) if (this.state.running) { console.log('Recapturing') - this.startCapture() + this.getTraces() } }) } - startCapture() { + onChange(evt) { + const duration = Number(evt.target.value) + this.setState({capture_duration: duration}) + this.setCaptureDuration(duration) + } + + setCaptureDuration(duration) { const postData = JSON.stringify({ - capture_duration_secs: this.state.capture_duration + capture_duration_secs: duration }); - console.log('Starting capture') fetch('http://localhost:8080/capture', {method: 'POST', body: postData}) .then(response => { - if (response.ok) { - // Begin polling the trigger state. - this.setState({ timer: setInterval(() => this.pollTrigger(), 10), }) - } else { - console.log(`Capture error: ${error}`) + if (!response.ok) { + console.log(`Config error: ${error}`) } }) } - onChange(evt) { - this.setState({capture_duration: Number(evt.target.value)}) - } - toggleCapture() { this.setState({running: !this.state.running}) if (!this.state.running) { - this.startCapture(); + this.getTraces(); } } @@ -101,10 +75,8 @@ class Trigger extends React.Component {
-
{this.state.trigger}
- - +
); } diff --git a/src/bin/webserver.rs b/src/bin/webserver.rs index 4354a78..5c58d17 100644 --- a/src/bin/webserver.rs +++ b/src/bin/webserver.rs @@ -21,22 +21,6 @@ struct Opts { port: u16, } -// TODO: Perhaps refactor this to be a state machine to simplify transitional logic. -#[derive(Serialize, Copy, Clone, PartialEq)] -enum TriggerState { - /// The trigger is idle. - Idle, - - /// The trigger is armed and waiting for trigger conditions. - Armed, - - /// The trigger has occurred and data is actively being captured. - Triggered, - - /// The trigger is complete and data is available for query. - Stopped, -} - #[derive(Deserialize)] struct CaptureSettings { /// The duration to capture data for in seconds. @@ -50,11 +34,62 @@ struct ServerState { struct StreamData { current_format: Option, - trigger: TriggerState, max_size: usize, - timebase: Vec, - traces: Vec, + timebase: BufferedData, + traces: Vec, +} + +struct BufferedData { + index: usize, + data: Vec, +} + +impl BufferedData { + pub fn new(size: usize) -> Self { + let mut storage = Vec::with_capacity(size); + storage.resize(size, T::default()); + Self { + data: storage, + index: 0, + } + } + + pub fn overflowing_write(&mut self, mut data: &[T]) { + // Continuously append data into the buffer in an overflowing manner (old data is + // overwritten). + while data.len() > 0 { + let write_length = if data.len() > self.data.len() - self.index { + self.data.len() - self.index + } else { + data.len() + }; + + self.data[self.index..][..write_length].copy_from_slice(&data[..write_length]); + self.index = (self.index + write_length) % self.data.len(); + data = &data[write_length..]; + } + } + + pub fn push(&mut self, data: T) { + self.data[self.index] = data; + self.index = (self.index + 1) % self.data.len(); + } + + pub fn oldest_data(&self) -> T { + let index = if self.index == self.data.len() { + 0 + } else { + self.index + 1 + }; + + self.data[index] + } + + pub fn resize(&mut self, size: usize) { + self.index = 0; + self.data.resize(size, T::default()); + } } #[derive(Serialize, Clone, Debug)] @@ -63,6 +98,20 @@ struct Trace { data: Vec, } +struct BufferedTrace { + label: String, + data: BufferedData, +} + +impl From<&BufferedTrace> for Trace { + fn from(bt: &BufferedTrace) -> Trace { + Trace { + label: bt.label.clone(), + data: bt.data.data.clone(), + } + } +} + #[derive(Serialize, Debug)] struct TraceData { time: Vec, @@ -73,12 +122,13 @@ impl StreamData { const fn new() -> Self { Self { current_format: None, - timebase: Vec::new(), + timebase: BufferedData { + data: Vec::new(), + index: 0, + }, traces: Vec::new(), max_size: SAMPLE_RATE_HZ as usize, - - trigger: TriggerState::Idle, } } @@ -93,23 +143,18 @@ impl StreamData { self.current_format.replace(frame.format()); - // If we aren't triggered, there's nothing more we want to do. - if self.trigger != TriggerState::Triggered { - return; - } - // Next, extract all of the data traces for i in 0..frame.data.trace_count() { if self.traces.len() < frame.data.trace_count() { - self.traces.push(Trace { - data: Vec::new(), + self.traces.push(BufferedTrace { + data: BufferedData::new(self.max_size), label: frame.data.trace_label(i), }); } // TODO: Decimate the data as requested. let trace = frame.data.get_trace(i); - self.traces[i].data.extend(trace); + self.traces[i].data.overflowing_write(trace); // For the first trace, also extend the timebase. if i == 0 { @@ -120,44 +165,53 @@ impl StreamData { } } } + } - // Drain the data/timebase queues to remain within our maximum size. - if self.timebase.len() > self.max_size { - self.timebase.drain(self.max_size..); + /// Get the current trace data. + pub fn get_data(&self) -> TraceData { + let mut times: Vec = Vec::new(); - for trace in &mut self.traces { - trace.data.drain(self.max_size..); + // Find the smallest sequence number in the timebase. + let mut earliest_timestamp = self.timebase.oldest_data(); + for (i, time) in self.timebase.data.iter().enumerate() { + let delta = earliest_timestamp.wrapping_sub(*time); + if delta < u64::MAX / 4 { + earliest_timestamp = *time; } + } - // Stop the capture now that we've filled up our buffers. - self.trigger = TriggerState::Stopped; + for time in self.timebase.data.iter() { + times.push(time.wrapping_sub(earliest_timestamp) as f32 * SAMPLE_PERIOD) } - } - /// Get the current trace data. - pub fn get_data(&self) -> TraceData { - let mut times: Vec = Vec::new(); - let time_offset = if self.timebase.len() > 0 { - self.timebase[0] - } else { - 0 - }; + let offset = self.timebase.index; - for time in self.timebase.iter() { - times.push(time.wrapping_sub(time_offset) as f32 * SAMPLE_PERIOD) + let mut traces: Vec = Vec::new(); + for trace in self.traces.iter() { + let mut trace: Trace = trace.into(); + trace.data.rotate_left(offset); + traces.push(trace) } + times.rotate_left(offset); + TraceData { time: times, - traces: self.traces.clone(), + traces, } } /// Remove all data from buffers. pub fn flush(&mut self) { - self.timebase.clear(); + log::info!("Flushing"); self.traces.clear(); self.current_format.take(); + self.timebase.resize(self.max_size) + } + + pub fn resize(&mut self, max_samples: usize) { + self.max_size = max_samples; + self.flush(); } } @@ -210,16 +264,6 @@ async fn configure_capture( let config: CaptureSettings = request.body_json().await?; let state = request.state(); - // Clear any pre-existing data in the buffers. - let mut data = state.data.lock().await; - data.flush(); - - log::info!("Arming trigger"); - data.trigger = TriggerState::Armed; - - log::info!("Forcing trigger"); - data.trigger = TriggerState::Triggered; - if config.capture_duration_secs < 0. { return Ok(Response::builder(400).body("Negative capture duration not supported")); } @@ -229,37 +273,15 @@ async fn configure_capture( return Ok(Response::builder(400).body("Too many samples requested")); } + // Clear any pre-existing data in the buffers. + let mut data = state.data.lock().await; + // TODO: Configure decimation - data.max_size = samples as usize; + data.resize(samples as usize); Ok(Response::builder(200)) } -/// Get the current trigger state. -/// -/// # Args -/// * `request` - Unused. -/// -/// # Returns -/// JSON containing the current trigger state as a string. -async fn get_trigger(request: tide::Request<&ServerState>) -> tide::Result> { - let state = request.state(); - let data = state.data.lock().await; - Ok(Response::builder(200).body(Body::from_json(&data.trigger)?)) -} - -/// Force a trigger condition. -/// -/// # Args -/// * `request` - Unused. -async fn force_trigger(request: tide::Request<&ServerState>) -> tide::Result> { - let state = request.state(); - let mut data = state.data.lock().await; - log::info!("Forcing trigger"); - data.trigger = TriggerState::Triggered; - Ok(Response::new(200)) -} - #[async_std::main] async fn main() -> tide::Result<()> { env_logger::init(); @@ -273,15 +295,19 @@ async fn main() -> tide::Result<()> { data: async_std::sync::Mutex::new(StreamData::new()), }; + STATE.data.lock().await.flush(); + async_std::task::spawn(receive(&STATE, stream_receiver)); let mut webapp = tide::with_state(&STATE); // Route configuration and queries. webapp.at("/traces").get(get_traces); - webapp.at("/trigger").get(get_trigger).post(force_trigger); webapp.at("/capture").post(configure_capture); - webapp.at("/").serve_file("frontend/dist/index.html").unwrap(); + webapp + .at("/") + .serve_file("frontend/dist/index.html") + .unwrap(); webapp.at("/").serve_dir("frontend/dist").unwrap(); // Start up the webapp. diff --git a/src/de/data/mod.rs b/src/de/data/mod.rs index 5af1132..2aa1a86 100644 --- a/src/de/data/mod.rs +++ b/src/de/data/mod.rs @@ -119,7 +119,7 @@ impl AdcDacData { let sample_index = trace_index + sample * 2; let value = { - let code = u16::from_le_bytes([data[sample_index], data[sample_index+ 1]]); + let code = u16::from_le_bytes([data[sample_index], data[sample_index + 1]]); if i < 2 { f32::from(AdcCode(code)) } else { diff --git a/src/lib.rs b/src/lib.rs index f3f5c75..d6b906c 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -8,7 +8,7 @@ use std::time::Duration; pub struct StreamReceiver { socket: UdpSocket, buf: [u8; 2048], - timeout: Option + timeout: Option, } impl StreamReceiver { @@ -37,7 +37,9 @@ impl StreamReceiver { pub async fn next_frame(&mut self) -> Option { // Read a single UDP packet. let len = if let Some(timeout) = self.timeout { - async_std::io::timeout(timeout, self.socket.recv(&mut self.buf)).await.unwrap() + async_std::io::timeout(timeout, self.socket.recv(&mut self.buf)) + .await + .unwrap() } else { self.socket.recv(&mut self.buf).await.unwrap() }; From cd54671dd4bb2c4bf5d590baa7ae4e6e5971891c Mon Sep 17 00:00:00 2001 From: Ryan Summers Date: Tue, 23 Nov 2021 12:37:00 +0100 Subject: [PATCH 12/39] Adding updates --- frontend/package.json | 2 +- frontend/src/react.jsx | 25 ++++++++++++++++--------- src/bin/webserver.rs | 18 ++++++++++-------- 3 files changed, 27 insertions(+), 18 deletions(-) diff --git a/frontend/package.json b/frontend/package.json index 9441d9f..2196e6b 100644 --- a/frontend/package.json +++ b/frontend/package.json @@ -4,7 +4,7 @@ "description": "Visualize Stabilizer livestreamed data", "scripts": { "test": "echo \"Error: no test specified\" && exit 1", - "build": "./node_modules/.bin/esbuild src/react.jsx --define:global=window --bundle --outfile=dist/main.js" + "build": "./node_modules/.bin/esbuild src/react.jsx --define:global=window --bundle --outfile=dist/main.js --minify" }, "author": "Ryan Summers", "license": "MIT", diff --git a/frontend/src/react.jsx b/frontend/src/react.jsx index daf013f..a5baa13 100644 --- a/frontend/src/react.jsx +++ b/frontend/src/react.jsx @@ -19,13 +19,20 @@ class Trigger extends React.Component { this.onChange = this.onChange.bind(this) this.state = { capture_duration: 0.001, - running: false, + timer: null, } + this.capturing = false this.setCaptureDuration(this.state.capture_duration) } getTraces() { + if (this.capturing) { + return; + } + + this.capturing = true; + fetch('http://localhost:8080/traces').then(response => { if (response.ok) { return response.json(); @@ -34,10 +41,8 @@ class Trigger extends React.Component { } }).then(data => { this.props.onData(data.time, data.traces) - if (this.state.running) { - console.log('Recapturing') - this.getTraces() - } + data = null + this.capturing = false; }) } @@ -61,9 +66,11 @@ class Trigger extends React.Component { } toggleCapture() { - this.setState({running: !this.state.running}) - if (!this.state.running) { - this.getTraces(); + if (this.state.timer == null) { + this.setState({timer: setInterval(() => {this.getTraces()}, 1000/30)}) + } else { + clearInterval(this.state.timer) + this.setState({timer: null}) } } @@ -75,7 +82,7 @@ class Trigger extends React.Component {
- +
); diff --git a/src/bin/webserver.rs b/src/bin/webserver.rs index 5c58d17..a4c0299 100644 --- a/src/bin/webserver.rs +++ b/src/bin/webserver.rs @@ -3,6 +3,8 @@ use serde::{Deserialize, Serialize}; use stabilizer_streaming::{de::deserializer::StreamFrame, de::StreamFormat, StreamReceiver}; use tide::{Body, Response}; +use std::time::Instant; + // TODO: Expose this as a configurable parameter and/or add it to the stream frame. const SAMPLE_RATE_HZ: f32 = 100e6 / 128.0; @@ -77,12 +79,7 @@ impl BufferedData { } pub fn oldest_data(&self) -> T { - let index = if self.index == self.data.len() { - 0 - } else { - self.index + 1 - }; - + let index = self.index + 1 % (self.data.len()); self.data[index] } @@ -173,7 +170,7 @@ impl StreamData { // Find the smallest sequence number in the timebase. let mut earliest_timestamp = self.timebase.oldest_data(); - for (i, time) in self.timebase.data.iter().enumerate() { + for time in self.timebase.data.iter() { let delta = earliest_timestamp.wrapping_sub(*time); if delta < u64::MAX / 4 { earliest_timestamp = *time; @@ -249,9 +246,14 @@ async fn get_traces(request: tide::Request<&ServerState>) -> tide::Result Date: Tue, 23 Nov 2021 13:18:32 +0100 Subject: [PATCH 13/39] Finalizing changes --- README.md | 5 +- frontend/src/react.jsx | 2 +- src/bin/webserver.rs | 119 +++++++++++++++++++++++++++-------------- 3 files changed, 84 insertions(+), 42 deletions(-) diff --git a/README.md b/README.md index a881045..0f77a6a 100644 --- a/README.md +++ b/README.md @@ -2,6 +2,7 @@ Host-side stream utilities for interacting with Stabilizer's livestream ## Usage -1. Install npm -2. `cargo run --bin webserver -- --ip ` +1. Install [npm](https://docs.npmjs.com/downloading-and-installing-node-js-and-npm) +2. Configure streaming to device +2. `cargo run --bin webserver --release -- --ip ` 3. Navigate to `localhost:8080` diff --git a/frontend/src/react.jsx b/frontend/src/react.jsx index a5baa13..5e95384 100644 --- a/frontend/src/react.jsx +++ b/frontend/src/react.jsx @@ -98,7 +98,7 @@ class Oscilloscope extends React.Component { createDefaultFont(this.cg).then(font => { this.font = font - this.drawGraph([1, 2, 3], [{'label': '', 'data': [0, 0.5, 1]}]) + this.drawGraph([0, 1], []) }) } diff --git a/src/bin/webserver.rs b/src/bin/webserver.rs index a4c0299..4e2da42 100644 --- a/src/bin/webserver.rs +++ b/src/bin/webserver.rs @@ -29,72 +29,111 @@ struct CaptureSettings { capture_duration_secs: f32, } +/// The global state of the backend server. struct ServerState { - // StreamData cannot implement a const-fn constructor, so we wrap it in an option instead. pub data: async_std::sync::Mutex, } struct StreamData { + // The current format of the received stream. current_format: Option, + // The maximum buffer size of received stream data in samples. max_size: usize, + + // The buffer for maintaining trace timestamps. timebase: BufferedData, + + // The buffer for maintaining trace data points. traces: Vec, } +/// A trace containing a label and associated data. +#[derive(Serialize, Clone, Debug)] +struct Trace { + label: String, + data: Vec, +} + +/// All relavent data needed to display information. +#[derive(Serialize, Debug)] +struct TraceData { + time: Vec, + traces: Vec, +} + +// A ringbuffer-like vector for maintaining received data. struct BufferedData { + // The next write index index: usize, + + // The stored data. data: Vec, + + // The maximum number of data points stored. Once this level is hit, data will begin + // overwriting from the beginning. + max_size: usize, } -impl BufferedData { +impl BufferedData { pub fn new(size: usize) -> Self { - let mut storage = Vec::with_capacity(size); - storage.resize(size, T::default()); Self { - data: storage, + data: Vec::new(), index: 0, + max_size: size, } } + /// Append data to the buffer in an overflowing manner. + /// + /// # Note + /// If the amount of data provided overflows the buffer size, it will still be accepted. pub fn overflowing_write(&mut self, mut data: &[T]) { // Continuously append data into the buffer in an overflowing manner (old data is // overwritten). while data.len() > 0 { - let write_length = if data.len() > self.data.len() - self.index { - self.data.len() - self.index + let write_length = if data.len() > self.max_size - self.index { + self.max_size - self.index } else { data.len() }; - self.data[self.index..][..write_length].copy_from_slice(&data[..write_length]); - self.index = (self.index + write_length) % self.data.len(); + self.add(&data[..write_length]); data = &data[write_length..]; } } - pub fn push(&mut self, data: T) { - self.data[self.index] = data; - self.index = (self.index + 1) % self.data.len(); + // Add data to the buffer + fn add(&mut self, data: &[T]) { + if self.data.len() < self.max_size { + assert!(data.len() + self.data.len() <= self.max_size); + self.data.extend_from_slice(data) + } else { + self.data[self.index..][..data.len()].copy_from_slice(data); + } + + self.index = (self.index + data.len()) % self.max_size; } - pub fn oldest_data(&self) -> T { - let index = self.index + 1 % (self.data.len()); - self.data[index] + /// Get the earliest element in the buffer along with its location. + pub fn get_earliest_element(&self) -> (usize, T) { + if self.data.len() != self.max_size { + (0, self.data[0]) + } else { + let index = (self.index + 1) % self.max_size; + (index, self.data[index]) + } } + /// Resize the buffer, clearing any previous data. pub fn resize(&mut self, size: usize) { self.index = 0; - self.data.resize(size, T::default()); + self.data.clear(); + self.max_size = size; } } -#[derive(Serialize, Clone, Debug)] -struct Trace { - label: String, - data: Vec, -} - +// A trace, where data is not yet contiguous in memory with respect to the timebase. struct BufferedTrace { label: String, data: BufferedData, @@ -109,17 +148,12 @@ impl From<&BufferedTrace> for Trace { } } -#[derive(Serialize, Debug)] -struct TraceData { - time: Vec, - traces: Vec, -} - impl StreamData { const fn new() -> Self { Self { current_format: None, timebase: BufferedData { + max_size: SAMPLE_RATE_HZ as usize, data: Vec::new(), index: 0, }, @@ -158,7 +192,8 @@ impl StreamData { let base = (frame.sequence_number() as u64) .wrapping_mul(frame.data.samples_per_batch() as u64); for sample_index in 0..trace.len() { - self.timebase.push(base.wrapping_add(sample_index as u64)) + self.timebase + .overflowing_write(&[base.wrapping_add(sample_index as u64)]) } } } @@ -166,31 +201,36 @@ impl StreamData { /// Get the current trace data. pub fn get_data(&self) -> TraceData { - let mut times: Vec = Vec::new(); + // Find the smallest sequence number in the timebase. This will be our time reference t = 0 + let (mut earliest_timestamp_offset, mut earliest_timestamp) = + self.timebase.get_earliest_element(); + + for offset in 0..self.timebase.data.len() { + let index = (self.timebase.index + offset) % self.timebase.data.len(); + let delta = earliest_timestamp.wrapping_sub(self.timebase.data[index]); - // Find the smallest sequence number in the timebase. - let mut earliest_timestamp = self.timebase.oldest_data(); - for time in self.timebase.data.iter() { - let delta = earliest_timestamp.wrapping_sub(*time); if delta < u64::MAX / 4 { - earliest_timestamp = *time; + earliest_timestamp_offset = index; + earliest_timestamp = self.timebase.data[index] } } + // Now, create an array of times relative from the earliest timestamp in the timebase. + let mut times: Vec = Vec::new(); for time in self.timebase.data.iter() { times.push(time.wrapping_sub(earliest_timestamp) as f32 * SAMPLE_PERIOD) } - let offset = self.timebase.index; - + // Rotate all of the arrays so that they are sequential in time from the earliest + // timestamp. This is necessary because the vectors are being used as ringbuffers. let mut traces: Vec = Vec::new(); for trace in self.traces.iter() { let mut trace: Trace = trace.into(); - trace.data.rotate_left(offset); + trace.data.rotate_left(earliest_timestamp_offset); traces.push(trace) } - times.rotate_left(offset); + times.rotate_left(earliest_timestamp_offset); TraceData { time: times, @@ -206,6 +246,7 @@ impl StreamData { self.timebase.resize(self.max_size) } + /// Resize the receiver to the provided maximum sample size. pub fn resize(&mut self, max_samples: usize) { self.max_size = max_samples; self.flush(); From f3a5c59df359de593ee15971aef80ff445bfbed5 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Robert=20J=C3=B6rdens?= Date: Tue, 14 Dec 2021 13:10:47 +0100 Subject: [PATCH 14/39] parameters/naming tweaks --- frontend/src/react.jsx | 2 +- src/bin/webserver.rs | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/frontend/src/react.jsx b/frontend/src/react.jsx index 5e95384..2e81302 100644 --- a/frontend/src/react.jsx +++ b/frontend/src/react.jsx @@ -57,7 +57,7 @@ class Trigger extends React.Component { capture_duration_secs: duration }); - fetch('http://localhost:8080/capture', {method: 'POST', body: postData}) + fetch('http://localhost:8080/configure', {method: 'POST', body: postData}) .then(response => { if (!response.ok) { console.log(`Config error: ${error}`) diff --git a/src/bin/webserver.rs b/src/bin/webserver.rs index 4e2da42..4fd55c5 100644 --- a/src/bin/webserver.rs +++ b/src/bin/webserver.rs @@ -346,7 +346,7 @@ async fn main() -> tide::Result<()> { // Route configuration and queries. webapp.at("/traces").get(get_traces); - webapp.at("/capture").post(configure_capture); + webapp.at("/configure").post(configure_capture); webapp .at("/") .serve_file("frontend/dist/index.html") @@ -354,7 +354,7 @@ async fn main() -> tide::Result<()> { webapp.at("/").serve_dir("frontend/dist").unwrap(); // Start up the webapp. - webapp.listen("127.0.0.1:8080").await?; + webapp.listen("tcp://0.0.0.0:8080").await?; Ok(()) } From e8d73f77da6498bcc1253ef7cbde481cafd7e2df Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Robert=20J=C3=B6rdens?= Date: Thu, 31 Aug 2023 23:33:54 +0200 Subject: [PATCH 15/39] egui app with psd and hbf dsp tools --- .cargo/config.toml | 2 + .gitignore | 2 + Cargo.toml | 36 +- build.rs | 12 - frontend/dist/index.html | 16 - frontend/package-lock.json | 2013 ------------------------------------ frontend/package.json | 21 - frontend/src/react.jsx | 197 ---- src/bin/main.rs | 210 ++++ src/bin/stream_test.rs | 98 +- src/bin/webserver.rs | 360 ------- src/de/data.rs | 123 +++ src/de/data/mod.rs | 137 --- src/de/deserializer.rs | 88 -- src/de/frame.rs | 74 ++ src/de/mod.rs | 28 +- src/hbf.rs | 484 +++++++++ src/lib.rs | 67 +- src/loss.rs | 39 + src/psd.rs | 294 ++++++ 20 files changed, 1329 insertions(+), 2972 deletions(-) create mode 100644 .cargo/config.toml delete mode 100644 build.rs delete mode 100644 frontend/dist/index.html delete mode 100644 frontend/package-lock.json delete mode 100644 frontend/package.json delete mode 100644 frontend/src/react.jsx create mode 100644 src/bin/main.rs delete mode 100644 src/bin/webserver.rs create mode 100644 src/de/data.rs delete mode 100644 src/de/data/mod.rs delete mode 100644 src/de/deserializer.rs create mode 100644 src/de/frame.rs create mode 100644 src/hbf.rs create mode 100644 src/loss.rs create mode 100644 src/psd.rs diff --git a/.cargo/config.toml b/.cargo/config.toml new file mode 100644 index 0000000..ee0584d --- /dev/null +++ b/.cargo/config.toml @@ -0,0 +1,2 @@ +[target.x86_64-unknown-linux-gnu] +rustflags = ["-C", "target-cpu=native"] diff --git a/.gitignore b/.gitignore index 088ba6b..cd20cd2 100644 --- a/.gitignore +++ b/.gitignore @@ -8,3 +8,5 @@ Cargo.lock # These are backup files generated by rustfmt **/*.rs.bk + +/perf.data* diff --git a/Cargo.toml b/Cargo.toml index 0a90d6b..ac61160 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,18 +1,36 @@ [package] name = "stabilizer-streaming" version = "0.1.0" -edition = "2018" - -# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html +authors = [ + "Robert Jördens ", + "Ryan Summers ", +] +license = "MIT OR Apache-2.0" +edition = "2021" +rust-version = "1.67" [dependencies] -clap = "3.0.0-beta.5" +clap = { version = "4.3", features = ["derive"] } num_enum = "0.5" log = "0.4" -env_logger = "0.9" async-std = { version = "1", features = ["attributes"] } -tide = "0.16" -serde = {version = "1", features = ["derive"]} +#tide = "0.16" +serde = { version = "1", features = ["derive"] } +eframe = { version = "0.22", default-features = false, features = ["glow", "default_fonts"] } +# egui = "0.22" +# image = { version = "0.24", default-features = false, features = ["png"] } +# rfd = "0.11.0" +env_logger = "0.10" +ndarray = "0.15.6" +bytemuck = "1.13.1" +thiserror = "1.0.47" +anyhow = "1.0.75" +socket2 = "0.5.3" +idsp = "0.10.0" +rustfft = "6.1.0" + +[dev-dependencies] +rand = "0.8.5" -[build-dependencies] -npm_rs = "0.2.1" +#[build-dependencies] +#npm_rs = "0.2.1" diff --git a/build.rs b/build.rs deleted file mode 100644 index 7893f02..0000000 --- a/build.rs +++ /dev/null @@ -1,12 +0,0 @@ -use npm_rs::NpmEnv; - -fn main() { - let status = NpmEnv::default() - .set_path("frontend") - .init_env() - .install(None) - .run("build") - .exec() - .unwrap(); - assert!(status.success()); -} diff --git a/frontend/dist/index.html b/frontend/dist/index.html deleted file mode 100644 index 248497f..0000000 --- a/frontend/dist/index.html +++ /dev/null @@ -1,16 +0,0 @@ - - - - - - - - - - Stabilizer Livestream Viewer - - -
- - - diff --git a/frontend/package-lock.json b/frontend/package-lock.json deleted file mode 100644 index 64db730..0000000 --- a/frontend/package-lock.json +++ /dev/null @@ -1,2013 +0,0 @@ -{ - "name": "stabilizer-stream-viewer", - "version": "0.1.0", - "lockfileVersion": 1, - "requires": true, - "dependencies": { - "@popperjs/core": { - "version": "2.10.2", - "resolved": "https://registry.npmjs.org/@popperjs/core/-/core-2.10.2.tgz", - "integrity": "sha512-IXf3XA7+XyN7CP9gGh/XB0UxVMlvARGEgGXLubFICsUMGz6Q+DU+i4gGlpOxTjKvXjkJDJC8YdqdKkDj9qZHEQ==" - }, - "JSONStream": { - "version": "1.3.5", - "resolved": "https://registry.npmjs.org/JSONStream/-/JSONStream-1.3.5.tgz", - "integrity": "sha512-E+iruNOY8VV9s4JEbe1aNEm6MiszPRr/UfcHMz0TQh1BXSxHK+ASV1R6W4HpjBhSeS+54PIsAMCBmwD06LLsqQ==", - "requires": { - "jsonparse": "^1.2.0", - "through": ">=2.2.7 <3" - } - }, - "acorn": { - "version": "7.4.1", - "resolved": "https://registry.npmjs.org/acorn/-/acorn-7.4.1.tgz", - "integrity": "sha512-nQyp0o1/mNdbTO1PO6kHkwSrmgZ0MT/jCCpNiwbUjGoRN4dlBhqJtoQuCnEOKzgTVwg0ZWiCoQy6SxMebQVh8A==" - }, - "acorn-node": { - "version": "1.8.2", - "resolved": "https://registry.npmjs.org/acorn-node/-/acorn-node-1.8.2.tgz", - "integrity": "sha512-8mt+fslDufLYntIoPAaIMUe/lrbrehIiwmR3t2k9LljIzoigEPF27eLk2hy8zSGzmR/ogr7zbRKINMo1u0yh5A==", - "requires": { - "acorn": "^7.0.0", - "acorn-walk": "^7.0.0", - "xtend": "^4.0.2" - } - }, - "acorn-walk": { - "version": "7.2.0", - "resolved": "https://registry.npmjs.org/acorn-walk/-/acorn-walk-7.2.0.tgz", - "integrity": "sha512-OPdCF6GsMIP+Az+aWfAAOEt2/+iVDKE7oy6lJ098aoe59oAmK76qV6Gw60SbZ8jHuG2wH058GF4pLFbYamYrVA==" - }, - "ajv": { - "version": "6.12.6", - "resolved": "https://registry.npmjs.org/ajv/-/ajv-6.12.6.tgz", - "integrity": "sha512-j3fVLgvTo527anyYyJOGTYJbG+vnnQYvE0m5mmkc1TK+nxAppkCLMIL0aZ4dblVCNoGShhm+kzE4ZUykBoMg4g==", - "requires": { - "fast-deep-equal": "^3.1.1", - "fast-json-stable-stringify": "^2.0.0", - "json-schema-traverse": "^0.4.1", - "uri-js": "^4.2.2" - } - }, - "asn1": { - "version": "0.2.4", - "resolved": "https://registry.npmjs.org/asn1/-/asn1-0.2.4.tgz", - "integrity": "sha512-jxwzQpLQjSmWXgwaCZE9Nz+glAG01yF1QnWgbhGwHI5A6FRIEY6IVqtHhIepHqI7/kyEyQEagBC5mBEFlIYvdg==", - "requires": { - "safer-buffer": "~2.1.0" - } - }, - "asn1.js": { - "version": "5.4.1", - "resolved": "https://registry.npmjs.org/asn1.js/-/asn1.js-5.4.1.tgz", - "integrity": "sha512-+I//4cYPccV8LdmBLiX8CYvf9Sp3vQsrqu2QNXRcrbiWvcx/UdlFiqUJJzxRQxgsZmvhXhn4cSKeSmoFjVdupA==", - "requires": { - "bn.js": "^4.0.0", - "inherits": "^2.0.1", - "minimalistic-assert": "^1.0.0", - "safer-buffer": "^2.1.0" - }, - "dependencies": { - "bn.js": { - "version": "4.12.0", - "resolved": "https://registry.npmjs.org/bn.js/-/bn.js-4.12.0.tgz", - "integrity": "sha512-c98Bf3tPniI+scsdk237ku1Dc3ujXQTSgyiPUDEOe7tRkhrqridvh8klBv0HCEso1OLOYcHuCv/cS6DNxKH+ZA==" - } - } - }, - "assert": { - "version": "1.5.0", - "resolved": "https://registry.npmjs.org/assert/-/assert-1.5.0.tgz", - "integrity": "sha512-EDsgawzwoun2CZkCgtxJbv392v4nbk9XDD06zI+kQYoBM/3RBWLlEyJARDOmhAAosBjWACEkKL6S+lIZtcAubA==", - "requires": { - "object-assign": "^4.1.1", - "util": "0.10.3" - }, - "dependencies": { - "inherits": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.1.tgz", - "integrity": "sha1-sX0I0ya0Qj5Wjv9xn5GwscvfafE=" - }, - "util": { - "version": "0.10.3", - "resolved": "https://registry.npmjs.org/util/-/util-0.10.3.tgz", - "integrity": "sha1-evsa/lCAUkZInj23/g7TeTNqwPk=", - "requires": { - "inherits": "2.0.1" - } - } - } - }, - "assert-plus": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/assert-plus/-/assert-plus-1.0.0.tgz", - "integrity": "sha1-8S4PPF13sLHN2RRpQuTpbB5N1SU=" - }, - "asynckit": { - "version": "0.4.0", - "resolved": "https://registry.npmjs.org/asynckit/-/asynckit-0.4.0.tgz", - "integrity": "sha1-x57Zf380y48robyXkLzDZkdLS3k=" - }, - "available-typed-arrays": { - "version": "1.0.5", - "resolved": "https://registry.npmjs.org/available-typed-arrays/-/available-typed-arrays-1.0.5.tgz", - "integrity": "sha512-DMD0KiN46eipeziST1LPP/STfDU0sufISXmjSgvVsoU2tqxctQeASejWcfNtxYKqETM1UxQ8sp2OrSBWpHY6sw==" - }, - "aws-sign2": { - "version": "0.7.0", - "resolved": "https://registry.npmjs.org/aws-sign2/-/aws-sign2-0.7.0.tgz", - "integrity": "sha1-tG6JCTSpWR8tL2+G1+ap8bP+dqg=" - }, - "aws4": { - "version": "1.11.0", - "resolved": "https://registry.npmjs.org/aws4/-/aws4-1.11.0.tgz", - "integrity": "sha512-xh1Rl34h6Fi1DC2WWKfxUTVqRsNnr6LsKz2+hfwDxQJWmrx8+c7ylaqBMcHfl1U1r2dsifOvKX3LQuLNZ+XSvA==" - }, - "balanced-match": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz", - "integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==" - }, - "base64-js": { - "version": "1.5.1", - "resolved": "https://registry.npmjs.org/base64-js/-/base64-js-1.5.1.tgz", - "integrity": "sha512-AKpaYlHn8t4SVbOHCy+b5+KKgvR4vrsD8vbvrbiQJps7fKDTkjkDry6ji0rUJjC0kzbNePLwzxq8iypo41qeWA==" - }, - "bcrypt-pbkdf": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/bcrypt-pbkdf/-/bcrypt-pbkdf-1.0.2.tgz", - "integrity": "sha1-pDAdOJtqQ/m2f/PKEaP2Y342Dp4=", - "requires": { - "tweetnacl": "^0.14.3" - } - }, - "bn.js": { - "version": "5.2.0", - "resolved": "https://registry.npmjs.org/bn.js/-/bn.js-5.2.0.tgz", - "integrity": "sha512-D7iWRBvnZE8ecXiLj/9wbxH7Tk79fAh8IHaTNq1RWRixsS02W+5qS+iE9yq6RYl0asXx5tw0bLhmT5pIfbSquw==" - }, - "bootstrap": { - "version": "5.1.3", - "resolved": "https://registry.npmjs.org/bootstrap/-/bootstrap-5.1.3.tgz", - "integrity": "sha512-fcQztozJ8jToQWXxVuEyXWW+dSo8AiXWKwiSSrKWsRB/Qt+Ewwza+JWoLKiTuQLaEPhdNAJ7+Dosc9DOIqNy7Q==" - }, - "brace-expansion": { - "version": "1.1.11", - "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.11.tgz", - "integrity": "sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA==", - "requires": { - "balanced-match": "^1.0.0", - "concat-map": "0.0.1" - } - }, - "brorand": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/brorand/-/brorand-1.1.0.tgz", - "integrity": "sha1-EsJe/kCkXjwyPrhnWgoM5XsiNx8=" - }, - "browser-pack": { - "version": "6.1.0", - "resolved": "https://registry.npmjs.org/browser-pack/-/browser-pack-6.1.0.tgz", - "integrity": "sha512-erYug8XoqzU3IfcU8fUgyHqyOXqIE4tUTTQ+7mqUjQlvnXkOO6OlT9c/ZoJVHYoAaqGxr09CN53G7XIsO4KtWA==", - "requires": { - "JSONStream": "^1.0.3", - "combine-source-map": "~0.8.0", - "defined": "^1.0.0", - "safe-buffer": "^5.1.1", - "through2": "^2.0.0", - "umd": "^3.0.0" - } - }, - "browser-resolve": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/browser-resolve/-/browser-resolve-2.0.0.tgz", - "integrity": "sha512-7sWsQlYL2rGLy2IWm8WL8DCTJvYLc/qlOnsakDac87SOoCd16WLsaAMdCiAqsTNHIe+SXfaqyxyo6THoWqs8WQ==", - "requires": { - "resolve": "^1.17.0" - } - }, - "browserify": { - "version": "17.0.0", - "resolved": "https://registry.npmjs.org/browserify/-/browserify-17.0.0.tgz", - "integrity": "sha512-SaHqzhku9v/j6XsQMRxPyBrSP3gnwmE27gLJYZgMT2GeK3J0+0toN+MnuNYDfHwVGQfLiMZ7KSNSIXHemy905w==", - "requires": { - "JSONStream": "^1.0.3", - "assert": "^1.4.0", - "browser-pack": "^6.0.1", - "browser-resolve": "^2.0.0", - "browserify-zlib": "~0.2.0", - "buffer": "~5.2.1", - "cached-path-relative": "^1.0.0", - "concat-stream": "^1.6.0", - "console-browserify": "^1.1.0", - "constants-browserify": "~1.0.0", - "crypto-browserify": "^3.0.0", - "defined": "^1.0.0", - "deps-sort": "^2.0.1", - "domain-browser": "^1.2.0", - "duplexer2": "~0.1.2", - "events": "^3.0.0", - "glob": "^7.1.0", - "has": "^1.0.0", - "htmlescape": "^1.1.0", - "https-browserify": "^1.0.0", - "inherits": "~2.0.1", - "insert-module-globals": "^7.2.1", - "labeled-stream-splicer": "^2.0.0", - "mkdirp-classic": "^0.5.2", - "module-deps": "^6.2.3", - "os-browserify": "~0.3.0", - "parents": "^1.0.1", - "path-browserify": "^1.0.0", - "process": "~0.11.0", - "punycode": "^1.3.2", - "querystring-es3": "~0.2.0", - "read-only-stream": "^2.0.0", - "readable-stream": "^2.0.2", - "resolve": "^1.1.4", - "shasum-object": "^1.0.0", - "shell-quote": "^1.6.1", - "stream-browserify": "^3.0.0", - "stream-http": "^3.0.0", - "string_decoder": "^1.1.1", - "subarg": "^1.0.0", - "syntax-error": "^1.1.1", - "through2": "^2.0.0", - "timers-browserify": "^1.0.1", - "tty-browserify": "0.0.1", - "url": "~0.11.0", - "util": "~0.12.0", - "vm-browserify": "^1.0.0", - "xtend": "^4.0.0" - } - }, - "browserify-aes": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/browserify-aes/-/browserify-aes-1.2.0.tgz", - "integrity": "sha512-+7CHXqGuspUn/Sl5aO7Ea0xWGAtETPXNSAjHo48JfLdPWcMng33Xe4znFvQweqc/uzk5zSOI3H52CYnjCfb5hA==", - "requires": { - "buffer-xor": "^1.0.3", - "cipher-base": "^1.0.0", - "create-hash": "^1.1.0", - "evp_bytestokey": "^1.0.3", - "inherits": "^2.0.1", - "safe-buffer": "^5.0.1" - } - }, - "browserify-cipher": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/browserify-cipher/-/browserify-cipher-1.0.1.tgz", - "integrity": "sha512-sPhkz0ARKbf4rRQt2hTpAHqn47X3llLkUGn+xEJzLjwY8LRs2p0v7ljvI5EyoRO/mexrNunNECisZs+gw2zz1w==", - "requires": { - "browserify-aes": "^1.0.4", - "browserify-des": "^1.0.0", - "evp_bytestokey": "^1.0.0" - } - }, - "browserify-des": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/browserify-des/-/browserify-des-1.0.2.tgz", - "integrity": "sha512-BioO1xf3hFwz4kc6iBhI3ieDFompMhrMlnDFC4/0/vd5MokpuAc3R+LYbwTA9A5Yc9pq9UYPqffKpW2ObuwX5A==", - "requires": { - "cipher-base": "^1.0.1", - "des.js": "^1.0.0", - "inherits": "^2.0.1", - "safe-buffer": "^5.1.2" - } - }, - "browserify-rsa": { - "version": "4.1.0", - "resolved": "https://registry.npmjs.org/browserify-rsa/-/browserify-rsa-4.1.0.tgz", - "integrity": "sha512-AdEER0Hkspgno2aR97SAf6vi0y0k8NuOpGnVH3O99rcA5Q6sh8QxcngtHuJ6uXwnfAXNM4Gn1Gb7/MV1+Ymbog==", - "requires": { - "bn.js": "^5.0.0", - "randombytes": "^2.0.1" - } - }, - "browserify-sign": { - "version": "4.2.1", - "resolved": "https://registry.npmjs.org/browserify-sign/-/browserify-sign-4.2.1.tgz", - "integrity": "sha512-/vrA5fguVAKKAVTNJjgSm1tRQDHUU6DbwO9IROu/0WAzC8PKhucDSh18J0RMvVeHAn5puMd+QHC2erPRNf8lmg==", - "requires": { - "bn.js": "^5.1.1", - "browserify-rsa": "^4.0.1", - "create-hash": "^1.2.0", - "create-hmac": "^1.1.7", - "elliptic": "^6.5.3", - "inherits": "^2.0.4", - "parse-asn1": "^5.1.5", - "readable-stream": "^3.6.0", - "safe-buffer": "^5.2.0" - }, - "dependencies": { - "readable-stream": { - "version": "3.6.0", - "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-3.6.0.tgz", - "integrity": "sha512-BViHy7LKeTz4oNnkcLJ+lVSL6vpiFeX6/d3oSH8zCW7UxP2onchk+vTGB143xuFjHS3deTgkKoXXymXqymiIdA==", - "requires": { - "inherits": "^2.0.3", - "string_decoder": "^1.1.1", - "util-deprecate": "^1.0.1" - } - } - } - }, - "browserify-zlib": { - "version": "0.2.0", - "resolved": "https://registry.npmjs.org/browserify-zlib/-/browserify-zlib-0.2.0.tgz", - "integrity": "sha512-Z942RysHXmJrhqk88FmKBVq/v5tqmSkDz7p54G/MGyjMnCFFnC79XWNbg+Vta8W6Wb2qtSZTSxIGkJrRpCFEiA==", - "requires": { - "pako": "~1.0.5" - } - }, - "buffer": { - "version": "5.2.1", - "resolved": "https://registry.npmjs.org/buffer/-/buffer-5.2.1.tgz", - "integrity": "sha512-c+Ko0loDaFfuPWiL02ls9Xd3GO3cPVmUobQ6t3rXNUk304u6hGq+8N/kFi+QEIKhzK3uwolVhLzszmfLmMLnqg==", - "requires": { - "base64-js": "^1.0.2", - "ieee754": "^1.1.4" - } - }, - "buffer-from": { - "version": "1.1.2", - "resolved": "https://registry.npmjs.org/buffer-from/-/buffer-from-1.1.2.tgz", - "integrity": "sha512-E+XQCRwSbaaiChtv6k6Dwgc+bx+Bs6vuKJHHl5kox/BaKbhiXzqQOwK4cO22yElGp2OCmjwVhT3HmxgyPGnJfQ==" - }, - "buffer-xor": { - "version": "1.0.3", - "resolved": "https://registry.npmjs.org/buffer-xor/-/buffer-xor-1.0.3.tgz", - "integrity": "sha1-JuYe0UIvtw3ULm42cp7VHYVf6Nk=" - }, - "builtin-status-codes": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/builtin-status-codes/-/builtin-status-codes-3.0.0.tgz", - "integrity": "sha1-hZgoeOIbmOHGZCXgPQF0eI9Wnug=" - }, - "cached-path-relative": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/cached-path-relative/-/cached-path-relative-1.0.2.tgz", - "integrity": "sha512-5r2GqsoEb4qMTTN9J+WzXfjov+hjxT+j3u5K+kIVNIwAd99DLCJE9pBIMP1qVeybV6JiijL385Oz0DcYxfbOIg==" - }, - "call-bind": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/call-bind/-/call-bind-1.0.2.tgz", - "integrity": "sha512-7O+FbCihrB5WGbFYesctwmTKae6rOiIzmz1icreWJ+0aA7LJfuqhEso2T9ncpcFtzMQtzXf2QGGueWJGTYsqrA==", - "requires": { - "function-bind": "^1.1.1", - "get-intrinsic": "^1.0.2" - } - }, - "candygraph": { - "version": "0.4.2", - "resolved": "https://registry.npmjs.org/candygraph/-/candygraph-0.4.2.tgz", - "integrity": "sha512-m6dguNPgTyRGlIDFW6ggDccJQvOAmsRwZDulP5T9rzxxLBQ+FrRrt3HxWTdXfmvw9jlwtUqeLsf32mlfTrDOFw==", - "requires": { - "gl-matrix": "^3.3.0", - "regl": "^1.7.0" - } - }, - "caseless": { - "version": "0.12.0", - "resolved": "https://registry.npmjs.org/caseless/-/caseless-0.12.0.tgz", - "integrity": "sha1-G2gcIf+EAzyCZUMJBolCDRhxUdw=" - }, - "cipher-base": { - "version": "1.0.4", - "resolved": "https://registry.npmjs.org/cipher-base/-/cipher-base-1.0.4.tgz", - "integrity": "sha512-Kkht5ye6ZGmwv40uUDZztayT2ThLQGfnj/T71N/XzeZeo3nf8foyW7zGTsPYkEya3m5f3cAypH+qe7YOrM1U2Q==", - "requires": { - "inherits": "^2.0.1", - "safe-buffer": "^5.0.1" - } - }, - "combine-source-map": { - "version": "0.8.0", - "resolved": "https://registry.npmjs.org/combine-source-map/-/combine-source-map-0.8.0.tgz", - "integrity": "sha1-pY0N8ELBhvz4IqjoAV9UUNLXmos=", - "requires": { - "convert-source-map": "~1.1.0", - "inline-source-map": "~0.6.0", - "lodash.memoize": "~3.0.3", - "source-map": "~0.5.3" - } - }, - "combined-stream": { - "version": "1.0.8", - "resolved": "https://registry.npmjs.org/combined-stream/-/combined-stream-1.0.8.tgz", - "integrity": "sha512-FQN4MRfuJeHf7cBbBMJFXhKSDq+2kAArBlmRBvcvFE5BB1HZKXtSFASDhdlz9zOYwxh8lDdnvmMOe/+5cdoEdg==", - "requires": { - "delayed-stream": "~1.0.0" - } - }, - "concat-map": { - "version": "0.0.1", - "resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz", - "integrity": "sha1-2Klr13/Wjfd5OnMDajug1UBdR3s=" - }, - "concat-stream": { - "version": "1.6.2", - "resolved": "https://registry.npmjs.org/concat-stream/-/concat-stream-1.6.2.tgz", - "integrity": "sha512-27HBghJxjiZtIk3Ycvn/4kbJk/1uZuJFfuPEns6LaEvpvG1f0hTea8lilrouyo9mVc2GWdcEZ8OLoGmSADlrCw==", - "requires": { - "buffer-from": "^1.0.0", - "inherits": "^2.0.3", - "readable-stream": "^2.2.2", - "typedarray": "^0.0.6" - } - }, - "console-browserify": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/console-browserify/-/console-browserify-1.2.0.tgz", - "integrity": "sha512-ZMkYO/LkF17QvCPqM0gxw8yUzigAOZOSWSHg91FH6orS7vcEj5dVZTidN2fQ14yBSdg97RqhSNwLUXInd52OTA==" - }, - "constants-browserify": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/constants-browserify/-/constants-browserify-1.0.0.tgz", - "integrity": "sha1-wguW2MYXdIqvHBYCF2DNJ/y4y3U=" - }, - "convert-source-map": { - "version": "1.1.3", - "resolved": "https://registry.npmjs.org/convert-source-map/-/convert-source-map-1.1.3.tgz", - "integrity": "sha1-SCnId+n+SbMWHzvzZziI4gRpmGA=" - }, - "core-util-is": { - "version": "1.0.3", - "resolved": "https://registry.npmjs.org/core-util-is/-/core-util-is-1.0.3.tgz", - "integrity": "sha512-ZQBvi1DcpJ4GDqanjucZ2Hj3wEO5pZDS89BWbkcrvdxksJorwUDDZamX9ldFkp9aw2lmBDLgkObEA4DWNJ9FYQ==" - }, - "create-ecdh": { - "version": "4.0.4", - "resolved": "https://registry.npmjs.org/create-ecdh/-/create-ecdh-4.0.4.tgz", - "integrity": "sha512-mf+TCx8wWc9VpuxfP2ht0iSISLZnt0JgWlrOKZiNqyUZWnjIaCIVNQArMHnCZKfEYRg6IM7A+NeJoN8gf/Ws0A==", - "requires": { - "bn.js": "^4.1.0", - "elliptic": "^6.5.3" - }, - "dependencies": { - "bn.js": { - "version": "4.12.0", - "resolved": "https://registry.npmjs.org/bn.js/-/bn.js-4.12.0.tgz", - "integrity": "sha512-c98Bf3tPniI+scsdk237ku1Dc3ujXQTSgyiPUDEOe7tRkhrqridvh8klBv0HCEso1OLOYcHuCv/cS6DNxKH+ZA==" - } - } - }, - "create-hash": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/create-hash/-/create-hash-1.2.0.tgz", - "integrity": "sha512-z00bCGNHDG8mHAkP7CtT1qVu+bFQUPjYq/4Iv3C3kWjTFV10zIjfSoeqXo9Asws8gwSHDGj/hl2u4OGIjapeCg==", - "requires": { - "cipher-base": "^1.0.1", - "inherits": "^2.0.1", - "md5.js": "^1.3.4", - "ripemd160": "^2.0.1", - "sha.js": "^2.4.0" - } - }, - "create-hmac": { - "version": "1.1.7", - "resolved": "https://registry.npmjs.org/create-hmac/-/create-hmac-1.1.7.tgz", - "integrity": "sha512-MJG9liiZ+ogc4TzUwuvbER1JRdgvUFSB5+VR/g5h82fGaIRWMWddtKBHi7/sVhfjQZ6SehlyhvQYrcYkaUIpLg==", - "requires": { - "cipher-base": "^1.0.3", - "create-hash": "^1.1.0", - "inherits": "^2.0.1", - "ripemd160": "^2.0.0", - "safe-buffer": "^5.0.1", - "sha.js": "^2.4.8" - } - }, - "crypto-browserify": { - "version": "3.12.0", - "resolved": "https://registry.npmjs.org/crypto-browserify/-/crypto-browserify-3.12.0.tgz", - "integrity": "sha512-fz4spIh+znjO2VjL+IdhEpRJ3YN6sMzITSBijk6FK2UvTqruSQW+/cCZTSNsMiZNvUeq0CqurF+dAbyiGOY6Wg==", - "requires": { - "browserify-cipher": "^1.0.0", - "browserify-sign": "^4.0.0", - "create-ecdh": "^4.0.0", - "create-hash": "^1.1.0", - "create-hmac": "^1.1.0", - "diffie-hellman": "^5.0.0", - "inherits": "^2.0.1", - "pbkdf2": "^3.0.3", - "public-encrypt": "^4.0.0", - "randombytes": "^2.0.0", - "randomfill": "^1.0.3" - } - }, - "dash-ast": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/dash-ast/-/dash-ast-1.0.0.tgz", - "integrity": "sha512-Vy4dx7gquTeMcQR/hDkYLGUnwVil6vk4FOOct+djUnHOUWt+zJPJAaRIXaAFkPXtJjvlY7o3rfRu0/3hpnwoUA==" - }, - "dashdash": { - "version": "1.14.1", - "resolved": "https://registry.npmjs.org/dashdash/-/dashdash-1.14.1.tgz", - "integrity": "sha1-hTz6D3y+L+1d4gMmuN1YEDX24vA=", - "requires": { - "assert-plus": "^1.0.0" - } - }, - "define-properties": { - "version": "1.1.3", - "resolved": "https://registry.npmjs.org/define-properties/-/define-properties-1.1.3.tgz", - "integrity": "sha512-3MqfYKj2lLzdMSf8ZIZE/V+Zuy+BgD6f164e8K2w7dgnpKArBDerGYpM46IYYcjnkdPNMjPk9A6VFB8+3SKlXQ==", - "requires": { - "object-keys": "^1.0.12" - } - }, - "defined": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/defined/-/defined-1.0.0.tgz", - "integrity": "sha1-yY2bzvdWdBiOEQlpFRGZ45sfppM=" - }, - "delayed-stream": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/delayed-stream/-/delayed-stream-1.0.0.tgz", - "integrity": "sha1-3zrhmayt+31ECqrgsp4icrJOxhk=" - }, - "deps-sort": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/deps-sort/-/deps-sort-2.0.1.tgz", - "integrity": "sha512-1orqXQr5po+3KI6kQb9A4jnXT1PBwggGl2d7Sq2xsnOeI9GPcE/tGcF9UiSZtZBM7MukY4cAh7MemS6tZYipfw==", - "requires": { - "JSONStream": "^1.0.3", - "shasum-object": "^1.0.0", - "subarg": "^1.0.0", - "through2": "^2.0.0" - } - }, - "des.js": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/des.js/-/des.js-1.0.1.tgz", - "integrity": "sha512-Q0I4pfFrv2VPd34/vfLrFOoRmlYj3OV50i7fskps1jZWK1kApMWWT9G6RRUeYedLcBDIhnSDaUvJMb3AhUlaEA==", - "requires": { - "inherits": "^2.0.1", - "minimalistic-assert": "^1.0.0" - } - }, - "detective": { - "version": "5.2.0", - "resolved": "https://registry.npmjs.org/detective/-/detective-5.2.0.tgz", - "integrity": "sha512-6SsIx+nUUbuK0EthKjv0zrdnajCCXVYGmbYYiYjFVpzcjwEs/JMDZ8tPRG29J/HhN56t3GJp2cGSWDRjjot8Pg==", - "requires": { - "acorn-node": "^1.6.1", - "defined": "^1.0.0", - "minimist": "^1.1.1" - } - }, - "diffie-hellman": { - "version": "5.0.3", - "resolved": "https://registry.npmjs.org/diffie-hellman/-/diffie-hellman-5.0.3.tgz", - "integrity": "sha512-kqag/Nl+f3GwyK25fhUMYj81BUOrZ9IuJsjIcDE5icNM9FJHAVm3VcUDxdLPoQtTuUylWm6ZIknYJwwaPxsUzg==", - "requires": { - "bn.js": "^4.1.0", - "miller-rabin": "^4.0.0", - "randombytes": "^2.0.0" - }, - "dependencies": { - "bn.js": { - "version": "4.12.0", - "resolved": "https://registry.npmjs.org/bn.js/-/bn.js-4.12.0.tgz", - "integrity": "sha512-c98Bf3tPniI+scsdk237ku1Dc3ujXQTSgyiPUDEOe7tRkhrqridvh8klBv0HCEso1OLOYcHuCv/cS6DNxKH+ZA==" - } - } - }, - "domain-browser": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/domain-browser/-/domain-browser-1.2.0.tgz", - "integrity": "sha512-jnjyiM6eRyZl2H+W8Q/zLMA481hzi0eszAaBUzIVnmYVDBbnLxVNnfu1HgEBvCbL+71FrxMl3E6lpKH7Ge3OXA==" - }, - "duplexer2": { - "version": "0.1.4", - "resolved": "https://registry.npmjs.org/duplexer2/-/duplexer2-0.1.4.tgz", - "integrity": "sha1-ixLauHjA1p4+eJEFFmKjL8a93ME=", - "requires": { - "readable-stream": "^2.0.2" - } - }, - "ecc-jsbn": { - "version": "0.1.2", - "resolved": "https://registry.npmjs.org/ecc-jsbn/-/ecc-jsbn-0.1.2.tgz", - "integrity": "sha1-OoOpBOVDUyh4dMVkt1SThoSamMk=", - "requires": { - "jsbn": "~0.1.0", - "safer-buffer": "^2.1.0" - } - }, - "elliptic": { - "version": "6.5.4", - "resolved": "https://registry.npmjs.org/elliptic/-/elliptic-6.5.4.tgz", - "integrity": "sha512-iLhC6ULemrljPZb+QutR5TQGB+pdW6KGD5RSegS+8sorOZT+rdQFbsQFJgvN3eRqNALqJer4oQ16YvJHlU8hzQ==", - "requires": { - "bn.js": "^4.11.9", - "brorand": "^1.1.0", - "hash.js": "^1.0.0", - "hmac-drbg": "^1.0.1", - "inherits": "^2.0.4", - "minimalistic-assert": "^1.0.1", - "minimalistic-crypto-utils": "^1.0.1" - }, - "dependencies": { - "bn.js": { - "version": "4.12.0", - "resolved": "https://registry.npmjs.org/bn.js/-/bn.js-4.12.0.tgz", - "integrity": "sha512-c98Bf3tPniI+scsdk237ku1Dc3ujXQTSgyiPUDEOe7tRkhrqridvh8klBv0HCEso1OLOYcHuCv/cS6DNxKH+ZA==" - } - } - }, - "es-abstract": { - "version": "1.19.1", - "resolved": "https://registry.npmjs.org/es-abstract/-/es-abstract-1.19.1.tgz", - "integrity": "sha512-2vJ6tjA/UfqLm2MPs7jxVybLoB8i1t1Jd9R3kISld20sIxPcTbLuggQOUxeWeAvIUkduv/CfMjuh4WmiXr2v9w==", - "requires": { - "call-bind": "^1.0.2", - "es-to-primitive": "^1.2.1", - "function-bind": "^1.1.1", - "get-intrinsic": "^1.1.1", - "get-symbol-description": "^1.0.0", - "has": "^1.0.3", - "has-symbols": "^1.0.2", - "internal-slot": "^1.0.3", - "is-callable": "^1.2.4", - "is-negative-zero": "^2.0.1", - "is-regex": "^1.1.4", - "is-shared-array-buffer": "^1.0.1", - "is-string": "^1.0.7", - "is-weakref": "^1.0.1", - "object-inspect": "^1.11.0", - "object-keys": "^1.1.1", - "object.assign": "^4.1.2", - "string.prototype.trimend": "^1.0.4", - "string.prototype.trimstart": "^1.0.4", - "unbox-primitive": "^1.0.1" - } - }, - "es-to-primitive": { - "version": "1.2.1", - "resolved": "https://registry.npmjs.org/es-to-primitive/-/es-to-primitive-1.2.1.tgz", - "integrity": "sha512-QCOllgZJtaUo9miYBcLChTUaHNjJF3PYs1VidD7AwiEj1kYxKeQTctLAezAOH5ZKRH0g2IgPn6KwB4IT8iRpvA==", - "requires": { - "is-callable": "^1.1.4", - "is-date-object": "^1.0.1", - "is-symbol": "^1.0.2" - } - }, - "esbuild": { - "version": "0.13.12", - "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.13.12.tgz", - "integrity": "sha512-vTKKUt+yoz61U/BbrnmlG9XIjwpdIxmHB8DlPR0AAW6OdS+nBQBci6LUHU2q9WbBobMEIQxxDpKbkmOGYvxsow==", - "requires": { - "esbuild-android-arm64": "0.13.12", - "esbuild-darwin-64": "0.13.12", - "esbuild-darwin-arm64": "0.13.12", - "esbuild-freebsd-64": "0.13.12", - "esbuild-freebsd-arm64": "0.13.12", - "esbuild-linux-32": "0.13.12", - "esbuild-linux-64": "0.13.12", - "esbuild-linux-arm": "0.13.12", - "esbuild-linux-arm64": "0.13.12", - "esbuild-linux-mips64le": "0.13.12", - "esbuild-linux-ppc64le": "0.13.12", - "esbuild-netbsd-64": "0.13.12", - "esbuild-openbsd-64": "0.13.12", - "esbuild-sunos-64": "0.13.12", - "esbuild-windows-32": "0.13.12", - "esbuild-windows-64": "0.13.12", - "esbuild-windows-arm64": "0.13.12" - } - }, - "esbuild-android-arm64": { - "version": "0.13.12", - "resolved": "https://registry.npmjs.org/esbuild-android-arm64/-/esbuild-android-arm64-0.13.12.tgz", - "integrity": "sha512-TSVZVrb4EIXz6KaYjXfTzPyyRpXV5zgYIADXtQsIenjZ78myvDGaPi11o4ZSaHIwFHsuwkB6ne5SZRBwAQ7maw==", - "optional": true - }, - "esbuild-darwin-64": { - "version": "0.13.12", - "resolved": "https://registry.npmjs.org/esbuild-darwin-64/-/esbuild-darwin-64-0.13.12.tgz", - "integrity": "sha512-c51C+N+UHySoV2lgfWSwwmlnLnL0JWj/LzuZt9Ltk9ub1s2Y8cr6SQV5W3mqVH1egUceew6KZ8GyI4nwu+fhsw==", - "optional": true - }, - "esbuild-darwin-arm64": { - "version": "0.13.12", - "resolved": "https://registry.npmjs.org/esbuild-darwin-arm64/-/esbuild-darwin-arm64-0.13.12.tgz", - "integrity": "sha512-JvAMtshP45Hd8A8wOzjkY1xAnTKTYuP/QUaKp5eUQGX+76GIie3fCdUUr2ZEKdvpSImNqxiZSIMziEiGB5oUmQ==", - "optional": true - }, - "esbuild-freebsd-64": { - "version": "0.13.12", - "resolved": "https://registry.npmjs.org/esbuild-freebsd-64/-/esbuild-freebsd-64-0.13.12.tgz", - "integrity": "sha512-r6On/Skv9f0ZjTu6PW5o7pdXr8aOgtFOEURJZYf1XAJs0IQ+gW+o1DzXjVkIoT+n1cm3N/t1KRJfX71MPg/ZUA==", - "optional": true - }, - "esbuild-freebsd-arm64": { - "version": "0.13.12", - "resolved": "https://registry.npmjs.org/esbuild-freebsd-arm64/-/esbuild-freebsd-arm64-0.13.12.tgz", - "integrity": "sha512-F6LmI2Q1gii073kmBE3NOTt/6zLL5zvZsxNLF8PMAwdHc+iBhD1vzfI8uQZMJA1IgXa3ocr3L3DJH9fLGXy6Yw==", - "optional": true - }, - "esbuild-linux-32": { - "version": "0.13.12", - "resolved": "https://registry.npmjs.org/esbuild-linux-32/-/esbuild-linux-32-0.13.12.tgz", - "integrity": "sha512-U1UZwG3UIwF7/V4tCVAo/nkBV9ag5KJiJTt+gaCmLVWH3bPLX7y+fNlhIWZy8raTMnXhMKfaTvWZ9TtmXzvkuQ==", - "optional": true - }, - "esbuild-linux-64": { - "version": "0.13.12", - "resolved": "https://registry.npmjs.org/esbuild-linux-64/-/esbuild-linux-64-0.13.12.tgz", - "integrity": "sha512-YpXSwtu2NxN3N4ifJxEdsgd6Q5d8LYqskrAwjmoCT6yQnEHJSF5uWcxv783HWN7lnGpJi9KUtDvYsnMdyGw71Q==", - "optional": true - }, - "esbuild-linux-arm": { - "version": "0.13.12", - "resolved": "https://registry.npmjs.org/esbuild-linux-arm/-/esbuild-linux-arm-0.13.12.tgz", - "integrity": "sha512-SyiT/JKxU6J+DY2qUiSLZJqCAftIt3uoGejZ0HDnUM2MGJqEGSGh7p1ecVL2gna3PxS4P+j6WAehCwgkBPXNIw==", - "optional": true - }, - "esbuild-linux-arm64": { - "version": "0.13.12", - "resolved": "https://registry.npmjs.org/esbuild-linux-arm64/-/esbuild-linux-arm64-0.13.12.tgz", - "integrity": "sha512-sgDNb8kb3BVodtAlcFGgwk+43KFCYjnFOaOfJibXnnIojNWuJHpL6aQJ4mumzNWw8Rt1xEtDQyuGK9f+Y24jGA==", - "optional": true - }, - "esbuild-linux-mips64le": { - "version": "0.13.12", - "resolved": "https://registry.npmjs.org/esbuild-linux-mips64le/-/esbuild-linux-mips64le-0.13.12.tgz", - "integrity": "sha512-qQJHlZBG+QwVIA8AbTEtbvF084QgDi4DaUsUnA+EolY1bxrG+UyOuGflM2ZritGhfS/k7THFjJbjH2wIeoKA2g==", - "optional": true - }, - "esbuild-linux-ppc64le": { - "version": "0.13.12", - "resolved": "https://registry.npmjs.org/esbuild-linux-ppc64le/-/esbuild-linux-ppc64le-0.13.12.tgz", - "integrity": "sha512-2dSnm1ldL7Lppwlo04CGQUpwNn5hGqXI38OzaoPOkRsBRWFBozyGxTFSee/zHFS+Pdh3b28JJbRK3owrrRgWNw==", - "optional": true - }, - "esbuild-netbsd-64": { - "version": "0.13.12", - "resolved": "https://registry.npmjs.org/esbuild-netbsd-64/-/esbuild-netbsd-64-0.13.12.tgz", - "integrity": "sha512-D4raxr02dcRiQNbxOLzpqBzcJNFAdsDNxjUbKkDMZBkL54Z0vZh4LRndycdZAMcIdizC/l/Yp/ZsBdAFxc5nbA==", - "optional": true - }, - "esbuild-openbsd-64": { - "version": "0.13.12", - "resolved": "https://registry.npmjs.org/esbuild-openbsd-64/-/esbuild-openbsd-64-0.13.12.tgz", - "integrity": "sha512-KuLCmYMb2kh05QuPJ+va60bKIH5wHL8ypDkmpy47lzwmdxNsuySeCMHuTv5o2Af1RUn5KLO5ZxaZeq4GEY7DaQ==", - "optional": true - }, - "esbuild-sunos-64": { - "version": "0.13.12", - "resolved": "https://registry.npmjs.org/esbuild-sunos-64/-/esbuild-sunos-64-0.13.12.tgz", - "integrity": "sha512-jBsF+e0woK3miKI8ufGWKG3o3rY9DpHvCVRn5eburMIIE+2c+y3IZ1srsthKyKI6kkXLvV4Cf/E7w56kLipMXw==", - "optional": true - }, - "esbuild-windows-32": { - "version": "0.13.12", - "resolved": "https://registry.npmjs.org/esbuild-windows-32/-/esbuild-windows-32-0.13.12.tgz", - "integrity": "sha512-L9m4lLFQrFeR7F+eLZXG82SbXZfUhyfu6CexZEil6vm+lc7GDCE0Q8DiNutkpzjv1+RAbIGVva9muItQ7HVTkQ==", - "optional": true - }, - "esbuild-windows-64": { - "version": "0.13.12", - "resolved": "https://registry.npmjs.org/esbuild-windows-64/-/esbuild-windows-64-0.13.12.tgz", - "integrity": "sha512-k4tX4uJlSbSkfs78W5d9+I9gpd+7N95W7H2bgOMFPsYREVJs31+Q2gLLHlsnlY95zBoPQMIzHooUIsixQIBjaQ==", - "optional": true - }, - "esbuild-windows-arm64": { - "version": "0.13.12", - "resolved": "https://registry.npmjs.org/esbuild-windows-arm64/-/esbuild-windows-arm64-0.13.12.tgz", - "integrity": "sha512-2tTv/BpYRIvuwHpp2M960nG7uvL+d78LFW/ikPItO+2GfK51CswIKSetSpDii+cjz8e9iSPgs+BU4o8nWICBwQ==", - "optional": true - }, - "events": { - "version": "3.3.0", - "resolved": "https://registry.npmjs.org/events/-/events-3.3.0.tgz", - "integrity": "sha512-mQw+2fkQbALzQ7V0MY0IqdnXNOeTtP4r0lN9z7AAawCXgqea7bDii20AYrIBrFd/Hx0M2Ocz6S111CaFkUcb0Q==" - }, - "evp_bytestokey": { - "version": "1.0.3", - "resolved": "https://registry.npmjs.org/evp_bytestokey/-/evp_bytestokey-1.0.3.tgz", - "integrity": "sha512-/f2Go4TognH/KvCISP7OUsHn85hT9nUkxxA9BEWxFn+Oj9o8ZNLm/40hdlgSLyuOimsrTKLUMEorQexp/aPQeA==", - "requires": { - "md5.js": "^1.3.4", - "safe-buffer": "^5.1.1" - } - }, - "extend": { - "version": "3.0.2", - "resolved": "https://registry.npmjs.org/extend/-/extend-3.0.2.tgz", - "integrity": "sha512-fjquC59cD7CyW6urNXK0FBufkZcoiGG80wTuPujX590cB5Ttln20E2UB4S/WARVqhXffZl2LNgS+gQdPIIim/g==" - }, - "extsprintf": { - "version": "1.3.0", - "resolved": "https://registry.npmjs.org/extsprintf/-/extsprintf-1.3.0.tgz", - "integrity": "sha1-lpGEQOMEGnpBT4xS48V06zw+HgU=" - }, - "fast-deep-equal": { - "version": "3.1.3", - "resolved": "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz", - "integrity": "sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==" - }, - "fast-json-stable-stringify": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/fast-json-stable-stringify/-/fast-json-stable-stringify-2.1.0.tgz", - "integrity": "sha512-lhd/wF+Lk98HZoTCtlVraHtfh5XYijIjalXck7saUtuanSDyLMxnHhSXEDJqHxD7msR8D0uCmqlkwjCV8xvwHw==" - }, - "fast-safe-stringify": { - "version": "2.1.1", - "resolved": "https://registry.npmjs.org/fast-safe-stringify/-/fast-safe-stringify-2.1.1.tgz", - "integrity": "sha512-W+KJc2dmILlPplD/H4K9l9LcAHAfPtP6BY84uVLXQ6Evcz9Lcg33Y2z1IVblT6xdY54PXYVHEv+0Wpq8Io6zkA==" - }, - "foreach": { - "version": "2.0.5", - "resolved": "https://registry.npmjs.org/foreach/-/foreach-2.0.5.tgz", - "integrity": "sha1-C+4AUBiusmDQo6865ljdATbsG5k=" - }, - "forever-agent": { - "version": "0.6.1", - "resolved": "https://registry.npmjs.org/forever-agent/-/forever-agent-0.6.1.tgz", - "integrity": "sha1-+8cfDEGt6zf5bFd60e1C2P2sypE=" - }, - "form-data": { - "version": "2.3.3", - "resolved": "https://registry.npmjs.org/form-data/-/form-data-2.3.3.tgz", - "integrity": "sha512-1lLKB2Mu3aGP1Q/2eCOx0fNbRMe7XdwktwOruhfqqd0rIJWwN4Dh+E3hrPSlDCXnSR7UtZ1N38rVXm+6+MEhJQ==", - "requires": { - "asynckit": "^0.4.0", - "combined-stream": "^1.0.6", - "mime-types": "^2.1.12" - } - }, - "fs.realpath": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/fs.realpath/-/fs.realpath-1.0.0.tgz", - "integrity": "sha1-FQStJSMVjKpA20onh8sBQRmU6k8=" - }, - "function-bind": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.1.tgz", - "integrity": "sha512-yIovAzMX49sF8Yl58fSCWJ5svSLuaibPxXQJFLmBObTuCr0Mf1KiPopGM9NiFjiYBCbfaa2Fh6breQ6ANVTI0A==" - }, - "get-assigned-identifiers": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/get-assigned-identifiers/-/get-assigned-identifiers-1.2.0.tgz", - "integrity": "sha512-mBBwmeGTrxEMO4pMaaf/uUEFHnYtwr8FTe8Y/mer4rcV/bye0qGm6pw1bGZFGStxC5O76c5ZAVBGnqHmOaJpdQ==" - }, - "get-intrinsic": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/get-intrinsic/-/get-intrinsic-1.1.1.tgz", - "integrity": "sha512-kWZrnVM42QCiEA2Ig1bG8zjoIMOgxWwYCEeNdwY6Tv/cOSeGpcoX4pXHfKUxNKVoArnrEr2e9srnAxxGIraS9Q==", - "requires": { - "function-bind": "^1.1.1", - "has": "^1.0.3", - "has-symbols": "^1.0.1" - } - }, - "get-symbol-description": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/get-symbol-description/-/get-symbol-description-1.0.0.tgz", - "integrity": "sha512-2EmdH1YvIQiZpltCNgkuiUnyukzxM/R6NDJX31Ke3BG1Nq5b0S2PhX59UKi9vZpPDQVdqn+1IcaAwnzTT5vCjw==", - "requires": { - "call-bind": "^1.0.2", - "get-intrinsic": "^1.1.1" - } - }, - "getpass": { - "version": "0.1.7", - "resolved": "https://registry.npmjs.org/getpass/-/getpass-0.1.7.tgz", - "integrity": "sha1-Xv+OPmhNVprkyysSgmBOi6YhSfo=", - "requires": { - "assert-plus": "^1.0.0" - } - }, - "gl-matrix": { - "version": "3.4.3", - "resolved": "https://registry.npmjs.org/gl-matrix/-/gl-matrix-3.4.3.tgz", - "integrity": "sha512-wcCp8vu8FT22BnvKVPjXa/ICBWRq/zjFfdofZy1WSpQZpphblv12/bOQLBC1rMM7SGOFS9ltVmKOHil5+Ml7gA==" - }, - "glob": { - "version": "7.2.0", - "resolved": "https://registry.npmjs.org/glob/-/glob-7.2.0.tgz", - "integrity": "sha512-lmLf6gtyrPq8tTjSmrO94wBeQbFR3HbLHbuyD69wuyQkImp2hWqMGB47OX65FBkPffO641IP9jWa1z4ivqG26Q==", - "requires": { - "fs.realpath": "^1.0.0", - "inflight": "^1.0.4", - "inherits": "2", - "minimatch": "^3.0.4", - "once": "^1.3.0", - "path-is-absolute": "^1.0.0" - } - }, - "har-schema": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/har-schema/-/har-schema-2.0.0.tgz", - "integrity": "sha1-qUwiJOvKwEeCoNkDVSHyRzW37JI=" - }, - "har-validator": { - "version": "5.1.5", - "resolved": "https://registry.npmjs.org/har-validator/-/har-validator-5.1.5.tgz", - "integrity": "sha512-nmT2T0lljbxdQZfspsno9hgrG3Uir6Ks5afism62poxqBM6sDnMEuPmzTq8XN0OEwqKLLdh1jQI3qyE66Nzb3w==", - "requires": { - "ajv": "^6.12.3", - "har-schema": "^2.0.0" - } - }, - "has": { - "version": "1.0.3", - "resolved": "https://registry.npmjs.org/has/-/has-1.0.3.tgz", - "integrity": "sha512-f2dvO0VU6Oej7RkWJGrehjbzMAjFp5/VKPp5tTpWIV4JHHZK1/BxbFRtf/siA2SWTe09caDmVtYYzWEIbBS4zw==", - "requires": { - "function-bind": "^1.1.1" - } - }, - "has-bigints": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/has-bigints/-/has-bigints-1.0.1.tgz", - "integrity": "sha512-LSBS2LjbNBTf6287JEbEzvJgftkF5qFkmCo9hDRpAzKhUOlJ+hx8dd4USs00SgsUNwc4617J9ki5YtEClM2ffA==" - }, - "has-symbols": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/has-symbols/-/has-symbols-1.0.2.tgz", - "integrity": "sha512-chXa79rL/UC2KlX17jo3vRGz0azaWEx5tGqZg5pO3NUyEJVB17dMruQlzCCOfUvElghKcm5194+BCRvi2Rv/Gw==" - }, - "has-tostringtag": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/has-tostringtag/-/has-tostringtag-1.0.0.tgz", - "integrity": "sha512-kFjcSNhnlGV1kyoGk7OXKSawH5JOb/LzUc5w9B02hOTO0dfFRjbHQKvg1d6cf3HbeUmtU9VbbV3qzZ2Teh97WQ==", - "requires": { - "has-symbols": "^1.0.2" - } - }, - "hash-base": { - "version": "3.1.0", - "resolved": "https://registry.npmjs.org/hash-base/-/hash-base-3.1.0.tgz", - "integrity": "sha512-1nmYp/rhMDiE7AYkDw+lLwlAzz0AntGIe51F3RfFfEqyQ3feY2eI/NcwC6umIQVOASPMsWJLJScWKSSvzL9IVA==", - "requires": { - "inherits": "^2.0.4", - "readable-stream": "^3.6.0", - "safe-buffer": "^5.2.0" - }, - "dependencies": { - "readable-stream": { - "version": "3.6.0", - "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-3.6.0.tgz", - "integrity": "sha512-BViHy7LKeTz4oNnkcLJ+lVSL6vpiFeX6/d3oSH8zCW7UxP2onchk+vTGB143xuFjHS3deTgkKoXXymXqymiIdA==", - "requires": { - "inherits": "^2.0.3", - "string_decoder": "^1.1.1", - "util-deprecate": "^1.0.1" - } - } - } - }, - "hash.js": { - "version": "1.1.7", - "resolved": "https://registry.npmjs.org/hash.js/-/hash.js-1.1.7.tgz", - "integrity": "sha512-taOaskGt4z4SOANNseOviYDvjEJinIkRgmp7LbKP2YTTmVxWBl87s/uzK9r+44BclBSp2X7K1hqeNfz9JbBeXA==", - "requires": { - "inherits": "^2.0.3", - "minimalistic-assert": "^1.0.1" - } - }, - "hmac-drbg": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/hmac-drbg/-/hmac-drbg-1.0.1.tgz", - "integrity": "sha1-0nRXAQJabHdabFRXk+1QL8DGSaE=", - "requires": { - "hash.js": "^1.0.3", - "minimalistic-assert": "^1.0.0", - "minimalistic-crypto-utils": "^1.0.1" - } - }, - "htmlescape": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/htmlescape/-/htmlescape-1.1.1.tgz", - "integrity": "sha1-OgPtwiFLyjtmQko+eVk0lQnLA1E=" - }, - "http-signature": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/http-signature/-/http-signature-1.2.0.tgz", - "integrity": "sha1-muzZJRFHcvPZW2WmCruPfBj7rOE=", - "requires": { - "assert-plus": "^1.0.0", - "jsprim": "^1.2.2", - "sshpk": "^1.7.0" - } - }, - "https-browserify": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/https-browserify/-/https-browserify-1.0.0.tgz", - "integrity": "sha1-7AbBDgo0wPL68Zn3/X/Hj//QPHM=" - }, - "ieee754": { - "version": "1.2.1", - "resolved": "https://registry.npmjs.org/ieee754/-/ieee754-1.2.1.tgz", - "integrity": "sha512-dcyqhDvX1C46lXZcVqCpK+FtMRQVdIMN6/Df5js2zouUsqG7I6sFxitIC+7KYK29KdXOLHdu9zL4sFnoVQnqaA==" - }, - "inflight": { - "version": "1.0.6", - "resolved": "https://registry.npmjs.org/inflight/-/inflight-1.0.6.tgz", - "integrity": "sha1-Sb1jMdfQLQwJvJEKEHW6gWW1bfk=", - "requires": { - "once": "^1.3.0", - "wrappy": "1" - } - }, - "inherits": { - "version": "2.0.4", - "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.4.tgz", - "integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==" - }, - "inline-source-map": { - "version": "0.6.2", - "resolved": "https://registry.npmjs.org/inline-source-map/-/inline-source-map-0.6.2.tgz", - "integrity": "sha1-+Tk0ccGKedFyT4Y/o4tYY3Ct4qU=", - "requires": { - "source-map": "~0.5.3" - } - }, - "insert-module-globals": { - "version": "7.2.1", - "resolved": "https://registry.npmjs.org/insert-module-globals/-/insert-module-globals-7.2.1.tgz", - "integrity": "sha512-ufS5Qq9RZN+Bu899eA9QCAYThY+gGW7oRkmb0vC93Vlyu/CFGcH0OYPEjVkDXA5FEbTt1+VWzdoOD3Ny9N+8tg==", - "requires": { - "JSONStream": "^1.0.3", - "acorn-node": "^1.5.2", - "combine-source-map": "^0.8.0", - "concat-stream": "^1.6.1", - "is-buffer": "^1.1.0", - "path-is-absolute": "^1.0.1", - "process": "~0.11.0", - "through2": "^2.0.0", - "undeclared-identifiers": "^1.1.2", - "xtend": "^4.0.0" - } - }, - "internal-slot": { - "version": "1.0.3", - "resolved": "https://registry.npmjs.org/internal-slot/-/internal-slot-1.0.3.tgz", - "integrity": "sha512-O0DB1JC/sPyZl7cIo78n5dR7eUSwwpYPiXRhTzNxZVAMUuB8vlnRFyLxdrVToks6XPLVnFfbzaVd5WLjhgg+vA==", - "requires": { - "get-intrinsic": "^1.1.0", - "has": "^1.0.3", - "side-channel": "^1.0.4" - } - }, - "is-arguments": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/is-arguments/-/is-arguments-1.1.1.tgz", - "integrity": "sha512-8Q7EARjzEnKpt/PCD7e1cgUS0a6X8u5tdSiMqXhojOdoV9TsMsiO+9VLC5vAmO8N7/GmXn7yjR8qnA6bVAEzfA==", - "requires": { - "call-bind": "^1.0.2", - "has-tostringtag": "^1.0.0" - } - }, - "is-bigint": { - "version": "1.0.4", - "resolved": "https://registry.npmjs.org/is-bigint/-/is-bigint-1.0.4.tgz", - "integrity": "sha512-zB9CruMamjym81i2JZ3UMn54PKGsQzsJeo6xvN3HJJ4CAsQNB6iRutp2To77OfCNuoxspsIhzaPoO1zyCEhFOg==", - "requires": { - "has-bigints": "^1.0.1" - } - }, - "is-boolean-object": { - "version": "1.1.2", - "resolved": "https://registry.npmjs.org/is-boolean-object/-/is-boolean-object-1.1.2.tgz", - "integrity": "sha512-gDYaKHJmnj4aWxyj6YHyXVpdQawtVLHU5cb+eztPGczf6cjuTdwve5ZIEfgXqH4e57An1D1AKf8CZ3kYrQRqYA==", - "requires": { - "call-bind": "^1.0.2", - "has-tostringtag": "^1.0.0" - } - }, - "is-buffer": { - "version": "1.1.6", - "resolved": "https://registry.npmjs.org/is-buffer/-/is-buffer-1.1.6.tgz", - "integrity": "sha512-NcdALwpXkTm5Zvvbk7owOUSvVvBKDgKP5/ewfXEznmQFfs4ZRmanOeKBTjRVjka3QFoN6XJ+9F3USqfHqTaU5w==" - }, - "is-callable": { - "version": "1.2.4", - "resolved": "https://registry.npmjs.org/is-callable/-/is-callable-1.2.4.tgz", - "integrity": "sha512-nsuwtxZfMX67Oryl9LCQ+upnC0Z0BgpwntpS89m1H/TLF0zNfzfLMV/9Wa/6MZsj0acpEjAO0KF1xT6ZdLl95w==" - }, - "is-core-module": { - "version": "2.8.0", - "resolved": "https://registry.npmjs.org/is-core-module/-/is-core-module-2.8.0.tgz", - "integrity": "sha512-vd15qHsaqrRL7dtH6QNuy0ndJmRDrS9HAM1CAiSifNUFv4x1a0CCVsj18hJ1mShxIG6T2i1sO78MkP56r0nYRw==", - "requires": { - "has": "^1.0.3" - } - }, - "is-date-object": { - "version": "1.0.5", - "resolved": "https://registry.npmjs.org/is-date-object/-/is-date-object-1.0.5.tgz", - "integrity": "sha512-9YQaSxsAiSwcvS33MBk3wTCVnWK+HhF8VZR2jRxehM16QcVOdHqPn4VPHmRK4lSr38n9JriurInLcP90xsYNfQ==", - "requires": { - "has-tostringtag": "^1.0.0" - } - }, - "is-generator-function": { - "version": "1.0.10", - "resolved": "https://registry.npmjs.org/is-generator-function/-/is-generator-function-1.0.10.tgz", - "integrity": "sha512-jsEjy9l3yiXEQ+PsXdmBwEPcOxaXWLspKdplFUVI9vq1iZgIekeC0L167qeu86czQaxed3q/Uzuw0swL0irL8A==", - "requires": { - "has-tostringtag": "^1.0.0" - } - }, - "is-negative-zero": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/is-negative-zero/-/is-negative-zero-2.0.1.tgz", - "integrity": "sha512-2z6JzQvZRa9A2Y7xC6dQQm4FSTSTNWjKIYYTt4246eMTJmIo0Q+ZyOsU66X8lxK1AbB92dFeglPLrhwpeRKO6w==" - }, - "is-number-object": { - "version": "1.0.6", - "resolved": "https://registry.npmjs.org/is-number-object/-/is-number-object-1.0.6.tgz", - "integrity": "sha512-bEVOqiRcvo3zO1+G2lVMy+gkkEm9Yh7cDMRusKKu5ZJKPUYSJwICTKZrNKHA2EbSP0Tu0+6B/emsYNHZyn6K8g==", - "requires": { - "has-tostringtag": "^1.0.0" - } - }, - "is-regex": { - "version": "1.1.4", - "resolved": "https://registry.npmjs.org/is-regex/-/is-regex-1.1.4.tgz", - "integrity": "sha512-kvRdxDsxZjhzUX07ZnLydzS1TU/TJlTUHHY4YLL87e37oUA49DfkLqgy+VjFocowy29cKvcSiu+kIv728jTTVg==", - "requires": { - "call-bind": "^1.0.2", - "has-tostringtag": "^1.0.0" - } - }, - "is-shared-array-buffer": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/is-shared-array-buffer/-/is-shared-array-buffer-1.0.1.tgz", - "integrity": "sha512-IU0NmyknYZN0rChcKhRO1X8LYz5Isj/Fsqh8NJOSf+N/hCOTwy29F32Ik7a+QszE63IdvmwdTPDd6cZ5pg4cwA==" - }, - "is-string": { - "version": "1.0.7", - "resolved": "https://registry.npmjs.org/is-string/-/is-string-1.0.7.tgz", - "integrity": "sha512-tE2UXzivje6ofPW7l23cjDOMa09gb7xlAqG6jG5ej6uPV32TlWP3NKPigtaGeHNu9fohccRYvIiZMfOOnOYUtg==", - "requires": { - "has-tostringtag": "^1.0.0" - } - }, - "is-symbol": { - "version": "1.0.4", - "resolved": "https://registry.npmjs.org/is-symbol/-/is-symbol-1.0.4.tgz", - "integrity": "sha512-C/CPBqKWnvdcxqIARxyOh4v1UUEOCHpgDa0WYgpKDFMszcrPcffg5uhwSgPCLD2WWxmq6isisz87tzT01tuGhg==", - "requires": { - "has-symbols": "^1.0.2" - } - }, - "is-typed-array": { - "version": "1.1.8", - "resolved": "https://registry.npmjs.org/is-typed-array/-/is-typed-array-1.1.8.tgz", - "integrity": "sha512-HqH41TNZq2fgtGT8WHVFVJhBVGuY3AnP3Q36K8JKXUxSxRgk/d+7NjmwG2vo2mYmXK8UYZKu0qH8bVP5gEisjA==", - "requires": { - "available-typed-arrays": "^1.0.5", - "call-bind": "^1.0.2", - "es-abstract": "^1.18.5", - "foreach": "^2.0.5", - "has-tostringtag": "^1.0.0" - } - }, - "is-typedarray": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/is-typedarray/-/is-typedarray-1.0.0.tgz", - "integrity": "sha1-5HnICFjfDBsR3dppQPlgEfzaSpo=" - }, - "is-weakref": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/is-weakref/-/is-weakref-1.0.1.tgz", - "integrity": "sha512-b2jKc2pQZjaeFYWEf7ScFj+Be1I+PXmlu572Q8coTXZ+LD/QQZ7ShPMst8h16riVgyXTQwUsFEl74mDvc/3MHQ==", - "requires": { - "call-bind": "^1.0.0" - } - }, - "isarray": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/isarray/-/isarray-1.0.0.tgz", - "integrity": "sha1-u5NdSFgsuhaMBoNJV6VKPgcSTxE=" - }, - "isstream": { - "version": "0.1.2", - "resolved": "https://registry.npmjs.org/isstream/-/isstream-0.1.2.tgz", - "integrity": "sha1-R+Y/evVa+m+S4VAOaQ64uFKcCZo=" - }, - "js-tokens": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-4.0.0.tgz", - "integrity": "sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ==" - }, - "jsbn": { - "version": "0.1.1", - "resolved": "https://registry.npmjs.org/jsbn/-/jsbn-0.1.1.tgz", - "integrity": "sha1-peZUwuWi3rXyAdls77yoDA7y9RM=" - }, - "json-schema": { - "version": "0.2.3", - "resolved": "https://registry.npmjs.org/json-schema/-/json-schema-0.2.3.tgz", - "integrity": "sha1-tIDIkuWaLwWVTOcnvT8qTogvnhM=" - }, - "json-schema-traverse": { - "version": "0.4.1", - "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-0.4.1.tgz", - "integrity": "sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg==" - }, - "json-stringify-safe": { - "version": "5.0.1", - "resolved": "https://registry.npmjs.org/json-stringify-safe/-/json-stringify-safe-5.0.1.tgz", - "integrity": "sha1-Epai1Y/UXxmg9s4B1lcB4sc1tus=" - }, - "jsonparse": { - "version": "1.3.1", - "resolved": "https://registry.npmjs.org/jsonparse/-/jsonparse-1.3.1.tgz", - "integrity": "sha1-P02uSpH6wxX3EGL4UhzCOfE2YoA=" - }, - "jsprim": { - "version": "1.4.1", - "resolved": "https://registry.npmjs.org/jsprim/-/jsprim-1.4.1.tgz", - "integrity": "sha1-MT5mvB5cwG5Di8G3SZwuXFastqI=", - "requires": { - "assert-plus": "1.0.0", - "extsprintf": "1.3.0", - "json-schema": "0.2.3", - "verror": "1.10.0" - } - }, - "labeled-stream-splicer": { - "version": "2.0.2", - "resolved": "https://registry.npmjs.org/labeled-stream-splicer/-/labeled-stream-splicer-2.0.2.tgz", - "integrity": "sha512-Ca4LSXFFZUjPScRaqOcFxneA0VpKZr4MMYCljyQr4LIewTLb3Y0IUTIsnBBsVubIeEfxeSZpSjSsRM8APEQaAw==", - "requires": { - "inherits": "^2.0.1", - "stream-splicer": "^2.0.0" - } - }, - "lodash.memoize": { - "version": "3.0.4", - "resolved": "https://registry.npmjs.org/lodash.memoize/-/lodash.memoize-3.0.4.tgz", - "integrity": "sha1-LcvSwofLwKVcxCMovQxzYVDVPj8=" - }, - "loose-envify": { - "version": "1.4.0", - "resolved": "https://registry.npmjs.org/loose-envify/-/loose-envify-1.4.0.tgz", - "integrity": "sha512-lyuxPGr/Wfhrlem2CL/UcnUc1zcqKAImBDzukY7Y5F/yQiNdko6+fRLevlw1HgMySw7f611UIY408EtxRSoK3Q==", - "requires": { - "js-tokens": "^3.0.0 || ^4.0.0" - } - }, - "md5.js": { - "version": "1.3.5", - "resolved": "https://registry.npmjs.org/md5.js/-/md5.js-1.3.5.tgz", - "integrity": "sha512-xitP+WxNPcTTOgnTJcrhM0xvdPepipPSf3I8EIpGKeFLjt3PlJLIDG3u8EX53ZIubkb+5U2+3rELYpEhHhzdkg==", - "requires": { - "hash-base": "^3.0.0", - "inherits": "^2.0.1", - "safe-buffer": "^5.1.2" - } - }, - "miller-rabin": { - "version": "4.0.1", - "resolved": "https://registry.npmjs.org/miller-rabin/-/miller-rabin-4.0.1.tgz", - "integrity": "sha512-115fLhvZVqWwHPbClyntxEVfVDfl9DLLTuJvq3g2O/Oxi8AiNouAHvDSzHS0viUJc+V5vm3eq91Xwqn9dp4jRA==", - "requires": { - "bn.js": "^4.0.0", - "brorand": "^1.0.1" - }, - "dependencies": { - "bn.js": { - "version": "4.12.0", - "resolved": "https://registry.npmjs.org/bn.js/-/bn.js-4.12.0.tgz", - "integrity": "sha512-c98Bf3tPniI+scsdk237ku1Dc3ujXQTSgyiPUDEOe7tRkhrqridvh8klBv0HCEso1OLOYcHuCv/cS6DNxKH+ZA==" - } - } - }, - "mime-db": { - "version": "1.50.0", - "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.50.0.tgz", - "integrity": "sha512-9tMZCDlYHqeERXEHO9f/hKfNXhre5dK2eE/krIvUjZbS2KPcqGDfNShIWS1uW9XOTKQKqK6qbeOci18rbfW77A==" - }, - "mime-types": { - "version": "2.1.33", - "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-2.1.33.tgz", - "integrity": "sha512-plLElXp7pRDd0bNZHw+nMd52vRYjLwQjygaNg7ddJ2uJtTlmnTCjWuPKxVu6//AdaRuME84SvLW91sIkBqGT0g==", - "requires": { - "mime-db": "1.50.0" - } - }, - "minimalistic-assert": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/minimalistic-assert/-/minimalistic-assert-1.0.1.tgz", - "integrity": "sha512-UtJcAD4yEaGtjPezWuO9wC4nwUnVH/8/Im3yEHQP4b67cXlD/Qr9hdITCU1xDbSEXg2XKNaP8jsReV7vQd00/A==" - }, - "minimalistic-crypto-utils": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/minimalistic-crypto-utils/-/minimalistic-crypto-utils-1.0.1.tgz", - "integrity": "sha1-9sAMHAsIIkblxNmd+4x8CDsrWCo=" - }, - "minimatch": { - "version": "3.0.4", - "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.0.4.tgz", - "integrity": "sha512-yJHVQEhyqPLUTgt9B83PXu6W3rx4MvvHvSUvToogpwoGDOUQ+yDrR0HRot+yOCdCO7u4hX3pWft6kWBBcqh0UA==", - "requires": { - "brace-expansion": "^1.1.7" - } - }, - "minimist": { - "version": "1.2.5", - "resolved": "https://registry.npmjs.org/minimist/-/minimist-1.2.5.tgz", - "integrity": "sha512-FM9nNUYrRBAELZQT3xeZQ7fmMOBg6nWNmJKTcgsJeaLstP/UODVpGsr5OhXhhXg6f+qtJ8uiZ+PUxkDWcgIXLw==" - }, - "mkdirp-classic": { - "version": "0.5.3", - "resolved": "https://registry.npmjs.org/mkdirp-classic/-/mkdirp-classic-0.5.3.tgz", - "integrity": "sha512-gKLcREMhtuZRwRAfqP3RFW+TK4JqApVBtOIftVgjuABpAtpxhPGaDcfvbhNvD0B8iD1oUr/txX35NjcaY6Ns/A==" - }, - "module-deps": { - "version": "6.2.3", - "resolved": "https://registry.npmjs.org/module-deps/-/module-deps-6.2.3.tgz", - "integrity": "sha512-fg7OZaQBcL4/L+AK5f4iVqf9OMbCclXfy/znXRxTVhJSeW5AIlS9AwheYwDaXM3lVW7OBeaeUEY3gbaC6cLlSA==", - "requires": { - "JSONStream": "^1.0.3", - "browser-resolve": "^2.0.0", - "cached-path-relative": "^1.0.2", - "concat-stream": "~1.6.0", - "defined": "^1.0.0", - "detective": "^5.2.0", - "duplexer2": "^0.1.2", - "inherits": "^2.0.1", - "parents": "^1.0.0", - "readable-stream": "^2.0.2", - "resolve": "^1.4.0", - "stream-combiner2": "^1.1.1", - "subarg": "^1.0.0", - "through2": "^2.0.0", - "xtend": "^4.0.0" - } - }, - "oauth-sign": { - "version": "0.9.0", - "resolved": "https://registry.npmjs.org/oauth-sign/-/oauth-sign-0.9.0.tgz", - "integrity": "sha512-fexhUFFPTGV8ybAtSIGbV6gOkSv8UtRbDBnAyLQw4QPKkgNlsH2ByPGtMUqdWkos6YCRmAqViwgZrJc/mRDzZQ==" - }, - "object-assign": { - "version": "4.1.1", - "resolved": "https://registry.npmjs.org/object-assign/-/object-assign-4.1.1.tgz", - "integrity": "sha1-IQmtx5ZYh8/AXLvUQsrIv7s2CGM=" - }, - "object-inspect": { - "version": "1.11.0", - "resolved": "https://registry.npmjs.org/object-inspect/-/object-inspect-1.11.0.tgz", - "integrity": "sha512-jp7ikS6Sd3GxQfZJPyH3cjcbJF6GZPClgdV+EFygjFLQ5FmW/dRUnTd9PQ9k0JhoNDabWFbpF1yCdSWCC6gexg==" - }, - "object-keys": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/object-keys/-/object-keys-1.1.1.tgz", - "integrity": "sha512-NuAESUOUMrlIXOfHKzD6bpPu3tYt3xvjNdRIQ+FeT0lNb4K8WR70CaDxhuNguS2XG+GjkyMwOzsN5ZktImfhLA==" - }, - "object.assign": { - "version": "4.1.2", - "resolved": "https://registry.npmjs.org/object.assign/-/object.assign-4.1.2.tgz", - "integrity": "sha512-ixT2L5THXsApyiUPYKmW+2EHpXXe5Ii3M+f4e+aJFAHao5amFRW6J0OO6c/LU8Be47utCx2GL89hxGB6XSmKuQ==", - "requires": { - "call-bind": "^1.0.0", - "define-properties": "^1.1.3", - "has-symbols": "^1.0.1", - "object-keys": "^1.1.1" - } - }, - "once": { - "version": "1.4.0", - "resolved": "https://registry.npmjs.org/once/-/once-1.4.0.tgz", - "integrity": "sha1-WDsap3WWHUsROsF9nFC6753Xa9E=", - "requires": { - "wrappy": "1" - } - }, - "os-browserify": { - "version": "0.3.0", - "resolved": "https://registry.npmjs.org/os-browserify/-/os-browserify-0.3.0.tgz", - "integrity": "sha1-hUNzx/XCMVkU/Jv8a9gjj92h7Cc=" - }, - "pako": { - "version": "1.0.11", - "resolved": "https://registry.npmjs.org/pako/-/pako-1.0.11.tgz", - "integrity": "sha512-4hLB8Py4zZce5s4yd9XzopqwVv/yGNhV1Bl8NTmCq1763HeK2+EwVTv+leGeL13Dnh2wfbqowVPXCIO0z4taYw==" - }, - "parents": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/parents/-/parents-1.0.1.tgz", - "integrity": "sha1-/t1NK/GTp3dF/nHjcdc8MwfZx1E=", - "requires": { - "path-platform": "~0.11.15" - } - }, - "parse-asn1": { - "version": "5.1.6", - "resolved": "https://registry.npmjs.org/parse-asn1/-/parse-asn1-5.1.6.tgz", - "integrity": "sha512-RnZRo1EPU6JBnra2vGHj0yhp6ebyjBZpmUCLHWiFhxlzvBCCpAuZ7elsBp1PVAbQN0/04VD/19rfzlBSwLstMw==", - "requires": { - "asn1.js": "^5.2.0", - "browserify-aes": "^1.0.0", - "evp_bytestokey": "^1.0.0", - "pbkdf2": "^3.0.3", - "safe-buffer": "^5.1.1" - } - }, - "path-browserify": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/path-browserify/-/path-browserify-1.0.1.tgz", - "integrity": "sha512-b7uo2UCUOYZcnF/3ID0lulOJi/bafxa1xPe7ZPsammBSpjSWQkjNxlt635YGS2MiR9GjvuXCtz2emr3jbsz98g==" - }, - "path-is-absolute": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/path-is-absolute/-/path-is-absolute-1.0.1.tgz", - "integrity": "sha1-F0uSaHNVNP+8es5r9TpanhtcX18=" - }, - "path-parse": { - "version": "1.0.7", - "resolved": "https://registry.npmjs.org/path-parse/-/path-parse-1.0.7.tgz", - "integrity": "sha512-LDJzPVEEEPR+y48z93A0Ed0yXb8pAByGWo/k5YYdYgpY2/2EsOsksJrq7lOHxryrVOn1ejG6oAp8ahvOIQD8sw==" - }, - "path-platform": { - "version": "0.11.15", - "resolved": "https://registry.npmjs.org/path-platform/-/path-platform-0.11.15.tgz", - "integrity": "sha1-6GQhf3TDaFDwhSt43Hv31KVyG/I=" - }, - "pbkdf2": { - "version": "3.1.2", - "resolved": "https://registry.npmjs.org/pbkdf2/-/pbkdf2-3.1.2.tgz", - "integrity": "sha512-iuh7L6jA7JEGu2WxDwtQP1ddOpaJNC4KlDEFfdQajSGgGPNi4OyDc2R7QnbY2bR9QjBVGwgvTdNJZoE7RaxUMA==", - "requires": { - "create-hash": "^1.1.2", - "create-hmac": "^1.1.4", - "ripemd160": "^2.0.1", - "safe-buffer": "^5.0.1", - "sha.js": "^2.4.8" - } - }, - "performance-now": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/performance-now/-/performance-now-2.1.0.tgz", - "integrity": "sha1-Ywn04OX6kT7BxpMHrjZLSzd8nns=" - }, - "process": { - "version": "0.11.10", - "resolved": "https://registry.npmjs.org/process/-/process-0.11.10.tgz", - "integrity": "sha1-czIwDoQBYb2j5podHZGn1LwW8YI=" - }, - "process-nextick-args": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/process-nextick-args/-/process-nextick-args-2.0.1.tgz", - "integrity": "sha512-3ouUOpQhtgrbOa17J7+uxOTpITYWaGP7/AhoR3+A+/1e9skrzelGi/dXzEYyvbxubEF6Wn2ypscTKiKJFFn1ag==" - }, - "psl": { - "version": "1.8.0", - "resolved": "https://registry.npmjs.org/psl/-/psl-1.8.0.tgz", - "integrity": "sha512-RIdOzyoavK+hA18OGGWDqUTsCLhtA7IcZ/6NCs4fFJaHBDab+pDDmDIByWFRQJq2Cd7r1OoQxBGKOaztq+hjIQ==" - }, - "public-encrypt": { - "version": "4.0.3", - "resolved": "https://registry.npmjs.org/public-encrypt/-/public-encrypt-4.0.3.tgz", - "integrity": "sha512-zVpa8oKZSz5bTMTFClc1fQOnyyEzpl5ozpi1B5YcvBrdohMjH2rfsBtyXcuNuwjsDIXmBYlF2N5FlJYhR29t8Q==", - "requires": { - "bn.js": "^4.1.0", - "browserify-rsa": "^4.0.0", - "create-hash": "^1.1.0", - "parse-asn1": "^5.0.0", - "randombytes": "^2.0.1", - "safe-buffer": "^5.1.2" - }, - "dependencies": { - "bn.js": { - "version": "4.12.0", - "resolved": "https://registry.npmjs.org/bn.js/-/bn.js-4.12.0.tgz", - "integrity": "sha512-c98Bf3tPniI+scsdk237ku1Dc3ujXQTSgyiPUDEOe7tRkhrqridvh8klBv0HCEso1OLOYcHuCv/cS6DNxKH+ZA==" - } - } - }, - "punycode": { - "version": "1.4.1", - "resolved": "https://registry.npmjs.org/punycode/-/punycode-1.4.1.tgz", - "integrity": "sha1-wNWmOycYgArY4esPpSachN1BhF4=" - }, - "qs": { - "version": "6.5.2", - "resolved": "https://registry.npmjs.org/qs/-/qs-6.5.2.tgz", - "integrity": "sha512-N5ZAX4/LxJmF+7wN74pUD6qAh9/wnvdQcjq9TZjevvXzSUo7bfmw91saqMjzGS2xq91/odN2dW/WOl7qQHNDGA==" - }, - "querystring": { - "version": "0.2.0", - "resolved": "https://registry.npmjs.org/querystring/-/querystring-0.2.0.tgz", - "integrity": "sha1-sgmEkgO7Jd+CDadW50cAWHhSFiA=" - }, - "querystring-es3": { - "version": "0.2.1", - "resolved": "https://registry.npmjs.org/querystring-es3/-/querystring-es3-0.2.1.tgz", - "integrity": "sha1-nsYfeQSYdXB9aUFFlv2Qek1xHnM=" - }, - "randombytes": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/randombytes/-/randombytes-2.1.0.tgz", - "integrity": "sha512-vYl3iOX+4CKUWuxGi9Ukhie6fsqXqS9FE2Zaic4tNFD2N2QQaXOMFbuKK4QmDHC0JO6B1Zp41J0LpT0oR68amQ==", - "requires": { - "safe-buffer": "^5.1.0" - } - }, - "randomfill": { - "version": "1.0.4", - "resolved": "https://registry.npmjs.org/randomfill/-/randomfill-1.0.4.tgz", - "integrity": "sha512-87lcbR8+MhcWcUiQ+9e+Rwx8MyR2P7qnt15ynUlbm3TU/fjbgz4GsvfSUDTemtCCtVCqb4ZcEFlyPNTh9bBTLw==", - "requires": { - "randombytes": "^2.0.5", - "safe-buffer": "^5.1.0" - } - }, - "react": { - "version": "17.0.2", - "resolved": "https://registry.npmjs.org/react/-/react-17.0.2.tgz", - "integrity": "sha512-gnhPt75i/dq/z3/6q/0asP78D0u592D5L1pd7M8P+dck6Fu/jJeL6iVVK23fptSUZj8Vjf++7wXA8UNclGQcbA==", - "requires": { - "loose-envify": "^1.1.0", - "object-assign": "^4.1.1" - } - }, - "react-dom": { - "version": "17.0.2", - "resolved": "https://registry.npmjs.org/react-dom/-/react-dom-17.0.2.tgz", - "integrity": "sha512-s4h96KtLDUQlsENhMn1ar8t2bEa+q/YAtj8pPPdIjPDGBDIVNsrD9aXNWqspUe6AzKCIG0C1HZZLqLV7qpOBGA==", - "requires": { - "loose-envify": "^1.1.0", - "object-assign": "^4.1.1", - "scheduler": "^0.20.2" - } - }, - "read-only-stream": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/read-only-stream/-/read-only-stream-2.0.0.tgz", - "integrity": "sha1-JyT9aoET1zdkrCiNQ4YnDB2/F/A=", - "requires": { - "readable-stream": "^2.0.2" - } - }, - "readable-stream": { - "version": "2.3.7", - "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-2.3.7.tgz", - "integrity": "sha512-Ebho8K4jIbHAxnuxi7o42OrZgF/ZTNcsZj6nRKyUmkhLFq8CHItp/fy6hQZuZmP/n3yZ9VBUbp4zz/mX8hmYPw==", - "requires": { - "core-util-is": "~1.0.0", - "inherits": "~2.0.3", - "isarray": "~1.0.0", - "process-nextick-args": "~2.0.0", - "safe-buffer": "~5.1.1", - "string_decoder": "~1.1.1", - "util-deprecate": "~1.0.1" - }, - "dependencies": { - "safe-buffer": { - "version": "5.1.2", - "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.1.2.tgz", - "integrity": "sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g==" - }, - "string_decoder": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.1.1.tgz", - "integrity": "sha512-n/ShnvDi6FHbbVfviro+WojiFzv+s8MPMHBczVePfUpDJLwoLT0ht1l4YwBCbi8pJAveEEdnkHyPyTP/mzRfwg==", - "requires": { - "safe-buffer": "~5.1.0" - } - } - } - }, - "regl": { - "version": "1.7.0", - "resolved": "https://registry.npmjs.org/regl/-/regl-1.7.0.tgz", - "integrity": "sha512-bEAtp/qrtKucxXSJkD4ebopFZYP0q1+3Vb2WECWv/T8yQEgKxDxJ7ztO285tAMaYZVR6mM1GgI6CCn8FROtL1w==" - }, - "request": { - "version": "2.88.2", - "resolved": "https://registry.npmjs.org/request/-/request-2.88.2.tgz", - "integrity": "sha512-MsvtOrfG9ZcrOwAW+Qi+F6HbD0CWXEh9ou77uOb7FM2WPhwT7smM833PzanhJLsgXjN89Ir6V2PczXNnMpwKhw==", - "requires": { - "aws-sign2": "~0.7.0", - "aws4": "^1.8.0", - "caseless": "~0.12.0", - "combined-stream": "~1.0.6", - "extend": "~3.0.2", - "forever-agent": "~0.6.1", - "form-data": "~2.3.2", - "har-validator": "~5.1.3", - "http-signature": "~1.2.0", - "is-typedarray": "~1.0.0", - "isstream": "~0.1.2", - "json-stringify-safe": "~5.0.1", - "mime-types": "~2.1.19", - "oauth-sign": "~0.9.0", - "performance-now": "^2.1.0", - "qs": "~6.5.2", - "safe-buffer": "^5.1.2", - "tough-cookie": "~2.5.0", - "tunnel-agent": "^0.6.0", - "uuid": "^3.3.2" - } - }, - "resolve": { - "version": "1.20.0", - "resolved": "https://registry.npmjs.org/resolve/-/resolve-1.20.0.tgz", - "integrity": "sha512-wENBPt4ySzg4ybFQW2TT1zMQucPK95HSh/nq2CFTZVOGut2+pQvSsgtda4d26YrYcr067wjbmzOG8byDPBX63A==", - "requires": { - "is-core-module": "^2.2.0", - "path-parse": "^1.0.6" - } - }, - "ripemd160": { - "version": "2.0.2", - "resolved": "https://registry.npmjs.org/ripemd160/-/ripemd160-2.0.2.tgz", - "integrity": "sha512-ii4iagi25WusVoiC4B4lq7pbXfAp3D9v5CwfkY33vffw2+pkDjY1D8GaN7spsxvCSx8dkPqOZCEZyfxcmJG2IA==", - "requires": { - "hash-base": "^3.0.0", - "inherits": "^2.0.1" - } - }, - "safe-buffer": { - "version": "5.2.1", - "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.2.1.tgz", - "integrity": "sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ==" - }, - "safer-buffer": { - "version": "2.1.2", - "resolved": "https://registry.npmjs.org/safer-buffer/-/safer-buffer-2.1.2.tgz", - "integrity": "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==" - }, - "scheduler": { - "version": "0.20.2", - "resolved": "https://registry.npmjs.org/scheduler/-/scheduler-0.20.2.tgz", - "integrity": "sha512-2eWfGgAqqWFGqtdMmcL5zCMK1U8KlXv8SQFGglL3CEtd0aDVDWgeF/YoCmvln55m5zSk3J/20hTaSBeSObsQDQ==", - "requires": { - "loose-envify": "^1.1.0", - "object-assign": "^4.1.1" - } - }, - "sha.js": { - "version": "2.4.11", - "resolved": "https://registry.npmjs.org/sha.js/-/sha.js-2.4.11.tgz", - "integrity": "sha512-QMEp5B7cftE7APOjk5Y6xgrbWu+WkLVQwk8JNjZ8nKRciZaByEW6MubieAiToS7+dwvrjGhH8jRXz3MVd0AYqQ==", - "requires": { - "inherits": "^2.0.1", - "safe-buffer": "^5.0.1" - } - }, - "shasum-object": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/shasum-object/-/shasum-object-1.0.0.tgz", - "integrity": "sha512-Iqo5rp/3xVi6M4YheapzZhhGPVs0yZwHj7wvwQ1B9z8H6zk+FEnI7y3Teq7qwnekfEhu8WmG2z0z4iWZaxLWVg==", - "requires": { - "fast-safe-stringify": "^2.0.7" - } - }, - "shell-quote": { - "version": "1.7.3", - "resolved": "https://registry.npmjs.org/shell-quote/-/shell-quote-1.7.3.tgz", - "integrity": "sha512-Vpfqwm4EnqGdlsBFNmHhxhElJYrdfcxPThu+ryKS5J8L/fhAwLazFZtq+S+TWZ9ANj2piSQLGj6NQg+lKPmxrw==" - }, - "side-channel": { - "version": "1.0.4", - "resolved": "https://registry.npmjs.org/side-channel/-/side-channel-1.0.4.tgz", - "integrity": "sha512-q5XPytqFEIKHkGdiMIrY10mvLRvnQh42/+GoBlFW3b2LXLE2xxJpZFdm94we0BaoV3RwJyGqg5wS7epxTv0Zvw==", - "requires": { - "call-bind": "^1.0.0", - "get-intrinsic": "^1.0.2", - "object-inspect": "^1.9.0" - } - }, - "simple-concat": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/simple-concat/-/simple-concat-1.0.1.tgz", - "integrity": "sha512-cSFtAPtRhljv69IK0hTVZQ+OfE9nePi/rtJmw5UjHeVyVroEqJXP1sFztKUy1qU+xvz3u/sfYJLa947b7nAN2Q==" - }, - "source-map": { - "version": "0.5.7", - "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.5.7.tgz", - "integrity": "sha1-igOdLRAh0i0eoUyA2OpGi6LvP8w=" - }, - "sshpk": { - "version": "1.16.1", - "resolved": "https://registry.npmjs.org/sshpk/-/sshpk-1.16.1.tgz", - "integrity": "sha512-HXXqVUq7+pcKeLqqZj6mHFUMvXtOJt1uoUx09pFW6011inTMxqI8BA8PM95myrIyyKwdnzjdFjLiE6KBPVtJIg==", - "requires": { - "asn1": "~0.2.3", - "assert-plus": "^1.0.0", - "bcrypt-pbkdf": "^1.0.0", - "dashdash": "^1.12.0", - "ecc-jsbn": "~0.1.1", - "getpass": "^0.1.1", - "jsbn": "~0.1.0", - "safer-buffer": "^2.0.2", - "tweetnacl": "~0.14.0" - } - }, - "stream-browserify": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/stream-browserify/-/stream-browserify-3.0.0.tgz", - "integrity": "sha512-H73RAHsVBapbim0tU2JwwOiXUj+fikfiaoYAKHF3VJfA0pe2BCzkhAHBlLG6REzE+2WNZcxOXjK7lkso+9euLA==", - "requires": { - "inherits": "~2.0.4", - "readable-stream": "^3.5.0" - }, - "dependencies": { - "readable-stream": { - "version": "3.6.0", - "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-3.6.0.tgz", - "integrity": "sha512-BViHy7LKeTz4oNnkcLJ+lVSL6vpiFeX6/d3oSH8zCW7UxP2onchk+vTGB143xuFjHS3deTgkKoXXymXqymiIdA==", - "requires": { - "inherits": "^2.0.3", - "string_decoder": "^1.1.1", - "util-deprecate": "^1.0.1" - } - } - } - }, - "stream-combiner2": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/stream-combiner2/-/stream-combiner2-1.1.1.tgz", - "integrity": "sha1-+02KFCDqNidk4hrUeAOXvry0HL4=", - "requires": { - "duplexer2": "~0.1.0", - "readable-stream": "^2.0.2" - } - }, - "stream-http": { - "version": "3.2.0", - "resolved": "https://registry.npmjs.org/stream-http/-/stream-http-3.2.0.tgz", - "integrity": "sha512-Oq1bLqisTyK3TSCXpPbT4sdeYNdmyZJv1LxpEm2vu1ZhK89kSE5YXwZc3cWk0MagGaKriBh9mCFbVGtO+vY29A==", - "requires": { - "builtin-status-codes": "^3.0.0", - "inherits": "^2.0.4", - "readable-stream": "^3.6.0", - "xtend": "^4.0.2" - }, - "dependencies": { - "readable-stream": { - "version": "3.6.0", - "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-3.6.0.tgz", - "integrity": "sha512-BViHy7LKeTz4oNnkcLJ+lVSL6vpiFeX6/d3oSH8zCW7UxP2onchk+vTGB143xuFjHS3deTgkKoXXymXqymiIdA==", - "requires": { - "inherits": "^2.0.3", - "string_decoder": "^1.1.1", - "util-deprecate": "^1.0.1" - } - } - } - }, - "stream-splicer": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/stream-splicer/-/stream-splicer-2.0.1.tgz", - "integrity": "sha512-Xizh4/NPuYSyAXyT7g8IvdJ9HJpxIGL9PjyhtywCZvvP0OPIdqyrr4dMikeuvY8xahpdKEBlBTySe583totajg==", - "requires": { - "inherits": "^2.0.1", - "readable-stream": "^2.0.2" - } - }, - "string.prototype.trimend": { - "version": "1.0.4", - "resolved": "https://registry.npmjs.org/string.prototype.trimend/-/string.prototype.trimend-1.0.4.tgz", - "integrity": "sha512-y9xCjw1P23Awk8EvTpcyL2NIr1j7wJ39f+k6lvRnSMz+mz9CGz9NYPelDk42kOz6+ql8xjfK8oYzy3jAP5QU5A==", - "requires": { - "call-bind": "^1.0.2", - "define-properties": "^1.1.3" - } - }, - "string.prototype.trimstart": { - "version": "1.0.4", - "resolved": "https://registry.npmjs.org/string.prototype.trimstart/-/string.prototype.trimstart-1.0.4.tgz", - "integrity": "sha512-jh6e984OBfvxS50tdY2nRZnoC5/mLFKOREQfw8t5yytkoUsJRNxvI/E39qu1sD0OtWI3OC0XgKSmcWwziwYuZw==", - "requires": { - "call-bind": "^1.0.2", - "define-properties": "^1.1.3" - } - }, - "string_decoder": { - "version": "1.3.0", - "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.3.0.tgz", - "integrity": "sha512-hkRX8U1WjJFd8LsDJ2yQ/wWWxaopEsABU1XfkM8A+j0+85JAGppt16cr1Whg6KIbb4okU6Mql6BOj+uup/wKeA==", - "requires": { - "safe-buffer": "~5.2.0" - } - }, - "subarg": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/subarg/-/subarg-1.0.0.tgz", - "integrity": "sha1-9izxdYHplrSPyWVpn1TAauJouNI=", - "requires": { - "minimist": "^1.1.0" - } - }, - "syntax-error": { - "version": "1.4.0", - "resolved": "https://registry.npmjs.org/syntax-error/-/syntax-error-1.4.0.tgz", - "integrity": "sha512-YPPlu67mdnHGTup2A8ff7BC2Pjq0e0Yp/IyTFN03zWO0RcK07uLcbi7C2KpGR2FvWbaB0+bfE27a+sBKebSo7w==", - "requires": { - "acorn-node": "^1.2.0" - } - }, - "through": { - "version": "2.3.8", - "resolved": "https://registry.npmjs.org/through/-/through-2.3.8.tgz", - "integrity": "sha1-DdTJ/6q8NXlgsbckEV1+Doai4fU=" - }, - "through2": { - "version": "2.0.5", - "resolved": "https://registry.npmjs.org/through2/-/through2-2.0.5.tgz", - "integrity": "sha512-/mrRod8xqpA+IHSLyGCQ2s8SPHiCDEeQJSep1jqLYeEUClOFG2Qsh+4FU6G9VeqpZnGW/Su8LQGc4YKni5rYSQ==", - "requires": { - "readable-stream": "~2.3.6", - "xtend": "~4.0.1" - } - }, - "timers-browserify": { - "version": "1.4.2", - "resolved": "https://registry.npmjs.org/timers-browserify/-/timers-browserify-1.4.2.tgz", - "integrity": "sha1-ycWLV1voQHN1y14kYtrO50NZ9B0=", - "requires": { - "process": "~0.11.0" - } - }, - "tough-cookie": { - "version": "2.5.0", - "resolved": "https://registry.npmjs.org/tough-cookie/-/tough-cookie-2.5.0.tgz", - "integrity": "sha512-nlLsUzgm1kfLXSXfRZMc1KLAugd4hqJHDTvc2hDIwS3mZAfMEuMbc03SujMF+GEcpaX/qboeycw6iO8JwVv2+g==", - "requires": { - "psl": "^1.1.28", - "punycode": "^2.1.1" - }, - "dependencies": { - "punycode": { - "version": "2.1.1", - "resolved": "https://registry.npmjs.org/punycode/-/punycode-2.1.1.tgz", - "integrity": "sha512-XRsRjdf+j5ml+y/6GKHPZbrF/8p2Yga0JPtdqTIY2Xe5ohJPD9saDJJLPvp9+NSBprVvevdXZybnj2cv8OEd0A==" - } - } - }, - "tty-browserify": { - "version": "0.0.1", - "resolved": "https://registry.npmjs.org/tty-browserify/-/tty-browserify-0.0.1.tgz", - "integrity": "sha512-C3TaO7K81YvjCgQH9Q1S3R3P3BtN3RIM8n+OvX4il1K1zgE8ZhI0op7kClgkxtutIE8hQrcrHBXvIheqKUUCxw==" - }, - "tunnel-agent": { - "version": "0.6.0", - "resolved": "https://registry.npmjs.org/tunnel-agent/-/tunnel-agent-0.6.0.tgz", - "integrity": "sha1-J6XeoGs2sEoKmWZ3SykIaPD8QP0=", - "requires": { - "safe-buffer": "^5.0.1" - } - }, - "tweetnacl": { - "version": "0.14.5", - "resolved": "https://registry.npmjs.org/tweetnacl/-/tweetnacl-0.14.5.tgz", - "integrity": "sha1-WuaBd/GS1EViadEIr6k/+HQ/T2Q=" - }, - "typedarray": { - "version": "0.0.6", - "resolved": "https://registry.npmjs.org/typedarray/-/typedarray-0.0.6.tgz", - "integrity": "sha1-hnrHTjhkGHsdPUfZlqeOxciDB3c=" - }, - "umd": { - "version": "3.0.3", - "resolved": "https://registry.npmjs.org/umd/-/umd-3.0.3.tgz", - "integrity": "sha512-4IcGSufhFshvLNcMCV80UnQVlZ5pMOC8mvNPForqwA4+lzYQuetTESLDQkeLmihq8bRcnpbQa48Wb8Lh16/xow==" - }, - "unbox-primitive": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/unbox-primitive/-/unbox-primitive-1.0.1.tgz", - "integrity": "sha512-tZU/3NqK3dA5gpE1KtyiJUrEB0lxnGkMFHptJ7q6ewdZ8s12QrODwNbhIJStmJkd1QDXa1NRA8aF2A1zk/Ypyw==", - "requires": { - "function-bind": "^1.1.1", - "has-bigints": "^1.0.1", - "has-symbols": "^1.0.2", - "which-boxed-primitive": "^1.0.2" - } - }, - "undeclared-identifiers": { - "version": "1.1.3", - "resolved": "https://registry.npmjs.org/undeclared-identifiers/-/undeclared-identifiers-1.1.3.tgz", - "integrity": "sha512-pJOW4nxjlmfwKApE4zvxLScM/njmwj/DiUBv7EabwE4O8kRUy+HIwxQtZLBPll/jx1LJyBcqNfB3/cpv9EZwOw==", - "requires": { - "acorn-node": "^1.3.0", - "dash-ast": "^1.0.0", - "get-assigned-identifiers": "^1.2.0", - "simple-concat": "^1.0.0", - "xtend": "^4.0.1" - } - }, - "uri-js": { - "version": "4.4.1", - "resolved": "https://registry.npmjs.org/uri-js/-/uri-js-4.4.1.tgz", - "integrity": "sha512-7rKUyy33Q1yc98pQ1DAmLtwX109F7TIfWlW1Ydo8Wl1ii1SeHieeh0HHfPeL2fMXK6z0s8ecKs9frCuLJvndBg==", - "requires": { - "punycode": "^2.1.0" - }, - "dependencies": { - "punycode": { - "version": "2.1.1", - "resolved": "https://registry.npmjs.org/punycode/-/punycode-2.1.1.tgz", - "integrity": "sha512-XRsRjdf+j5ml+y/6GKHPZbrF/8p2Yga0JPtdqTIY2Xe5ohJPD9saDJJLPvp9+NSBprVvevdXZybnj2cv8OEd0A==" - } - } - }, - "url": { - "version": "0.11.0", - "resolved": "https://registry.npmjs.org/url/-/url-0.11.0.tgz", - "integrity": "sha1-ODjpfPxgUh63PFJajlW/3Z4uKPE=", - "requires": { - "punycode": "1.3.2", - "querystring": "0.2.0" - }, - "dependencies": { - "punycode": { - "version": "1.3.2", - "resolved": "https://registry.npmjs.org/punycode/-/punycode-1.3.2.tgz", - "integrity": "sha1-llOgNvt8HuQjQvIyXM7v6jkmxI0=" - } - } - }, - "util": { - "version": "0.12.4", - "resolved": "https://registry.npmjs.org/util/-/util-0.12.4.tgz", - "integrity": "sha512-bxZ9qtSlGUWSOy9Qa9Xgk11kSslpuZwaxCg4sNIDj6FLucDab2JxnHwyNTCpHMtK1MjoQiWQ6DiUMZYbSrO+Sw==", - "requires": { - "inherits": "^2.0.3", - "is-arguments": "^1.0.4", - "is-generator-function": "^1.0.7", - "is-typed-array": "^1.1.3", - "safe-buffer": "^5.1.2", - "which-typed-array": "^1.1.2" - } - }, - "util-deprecate": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/util-deprecate/-/util-deprecate-1.0.2.tgz", - "integrity": "sha1-RQ1Nyfpw3nMnYvvS1KKJgUGaDM8=" - }, - "uuid": { - "version": "3.4.0", - "resolved": "https://registry.npmjs.org/uuid/-/uuid-3.4.0.tgz", - "integrity": "sha512-HjSDRw6gZE5JMggctHBcjVak08+KEVhSIiDzFnT9S9aegmp85S/bReBVTb4QTFaRNptJ9kuYaNhnbNEOkbKb/A==" - }, - "verror": { - "version": "1.10.0", - "resolved": "https://registry.npmjs.org/verror/-/verror-1.10.0.tgz", - "integrity": "sha1-OhBcoXBTr1XW4nDB+CiGguGNpAA=", - "requires": { - "assert-plus": "^1.0.0", - "core-util-is": "1.0.2", - "extsprintf": "^1.2.0" - }, - "dependencies": { - "core-util-is": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/core-util-is/-/core-util-is-1.0.2.tgz", - "integrity": "sha1-tf1UIgqivFq1eqtxQMlAdUUDwac=" - } - } - }, - "vm-browserify": { - "version": "1.1.2", - "resolved": "https://registry.npmjs.org/vm-browserify/-/vm-browserify-1.1.2.tgz", - "integrity": "sha512-2ham8XPWTONajOR0ohOKOHXkm3+gaBmGut3SRuu75xLd/RRaY6vqgh8NBYYk7+RW3u5AtzPQZG8F10LHkl0lAQ==" - }, - "which-boxed-primitive": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/which-boxed-primitive/-/which-boxed-primitive-1.0.2.tgz", - "integrity": "sha512-bwZdv0AKLpplFY2KZRX6TvyuN7ojjr7lwkg6ml0roIy9YeuSr7JS372qlNW18UQYzgYK9ziGcerWqZOmEn9VNg==", - "requires": { - "is-bigint": "^1.0.1", - "is-boolean-object": "^1.1.0", - "is-number-object": "^1.0.4", - "is-string": "^1.0.5", - "is-symbol": "^1.0.3" - } - }, - "which-typed-array": { - "version": "1.1.7", - "resolved": "https://registry.npmjs.org/which-typed-array/-/which-typed-array-1.1.7.tgz", - "integrity": "sha512-vjxaB4nfDqwKI0ws7wZpxIlde1XrLX5uB0ZjpfshgmapJMD7jJWhZI+yToJTqaFByF0eNBcYxbjmCzoRP7CfEw==", - "requires": { - "available-typed-arrays": "^1.0.5", - "call-bind": "^1.0.2", - "es-abstract": "^1.18.5", - "foreach": "^2.0.5", - "has-tostringtag": "^1.0.0", - "is-typed-array": "^1.1.7" - } - }, - "wrappy": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz", - "integrity": "sha1-tSQ9jz7BqjXxNkYFvA0QNuMKtp8=" - }, - "xtend": { - "version": "4.0.2", - "resolved": "https://registry.npmjs.org/xtend/-/xtend-4.0.2.tgz", - "integrity": "sha512-LKYU1iAXJXUgAXn9URjiu+MWhyUXHsvfp7mcuYm9dSUKK0/CjtrUwFAxD82/mCWbtLsGjFIad0wIsod4zrTAEQ==" - } - } -} diff --git a/frontend/package.json b/frontend/package.json deleted file mode 100644 index 2196e6b..0000000 --- a/frontend/package.json +++ /dev/null @@ -1,21 +0,0 @@ -{ - "name": "stabilizer-stream-viewer", - "version": "0.1.0", - "description": "Visualize Stabilizer livestreamed data", - "scripts": { - "test": "echo \"Error: no test specified\" && exit 1", - "build": "./node_modules/.bin/esbuild src/react.jsx --define:global=window --bundle --outfile=dist/main.js --minify" - }, - "author": "Ryan Summers", - "license": "MIT", - "dependencies": { - "@popperjs/core": "^2.10.2", - "bootstrap": "^5.1.3", - "browserify": "^17.0.0", - "candygraph": "^0.4.2", - "esbuild": "^0.13.12", - "react": "^17.0.2", - "react-dom": "^17.0.2", - "request": "^2.88.2" - } -} diff --git a/frontend/src/react.jsx b/frontend/src/react.jsx deleted file mode 100644 index 2e81302..0000000 --- a/frontend/src/react.jsx +++ /dev/null @@ -1,197 +0,0 @@ -import "bootstrap"; -import "bootstrap/dist/css/bootstrap.css"; - -import CandyGraph, { - createCartesianCoordinateSystem, - createLinearScale, - createLineStrip, - createDefaultFont, - createOrthoAxis, - createText, -} from "candygraph"; - -import ReactDOM from 'react-dom'; -import React from "react"; - -class Trigger extends React.Component { - constructor(props) { - super(props) - this.onChange = this.onChange.bind(this) - this.state = { - capture_duration: 0.001, - timer: null, - } - this.capturing = false - - this.setCaptureDuration(this.state.capture_duration) - } - - getTraces() { - if (this.capturing) { - return; - } - - this.capturing = true; - - fetch('http://localhost:8080/traces').then(response => { - if (response.ok) { - return response.json(); - } else { - return Promise.reject(`Data request failed: ${response.text()}`) - } - }).then(data => { - this.props.onData(data.time, data.traces) - data = null - this.capturing = false; - }) - } - - onChange(evt) { - const duration = Number(evt.target.value) - this.setState({capture_duration: duration}) - this.setCaptureDuration(duration) - } - - setCaptureDuration(duration) { - const postData = JSON.stringify({ - capture_duration_secs: duration - }); - - fetch('http://localhost:8080/configure', {method: 'POST', body: postData}) - .then(response => { - if (!response.ok) { - console.log(`Config error: ${error}`) - } - }) - } - - toggleCapture() { - if (this.state.timer == null) { - this.setState({timer: setInterval(() => {this.getTraces()}, 1000/30)}) - } else { - clearInterval(this.state.timer) - this.setState({timer: null}) - } - } - - render() { - return ( -
-
- Duration - -
- - - -
- ); - } -} - -class Oscilloscope extends React.Component { - constructor(props) { - super(props); - this.cg = new CandyGraph() - this.font = null - this.canvas = null - - createDefaultFont(this.cg).then(font => { - this.font = font - this.drawGraph([0, 1], []) - }) - } - - componentDidMount() { - const width = this.divElement.clientWidth - this.canvas = document.getElementById("oscilloscope-display") - this.canvas.width = width - } - - drawGraph(times, traces) { - if (this.font == null || this.canvas == null) { - return; - } - - this.cg.canvas.width = this.canvas.width - this.cg.canvas.height = this.canvas.height - - const viewport = { - x: 0, - y: 0, - width: this.canvas.width, - height: this.canvas.height, - } - - this.cg.clear([1, 1, 1, 1]) - - const max_time = Math.max(...times) - - const coords = createCartesianCoordinateSystem( - createLinearScale([0, max_time], [32, viewport.width - 16]), - createLinearScale([-10.24, 10.24], [32, viewport.height - 16]), - ); - - // Create the various traces for the display - const colors = [ - [1, 0, 0, 1.0], - [0, 1, 0, 1.0], - [0, 0, 1, 1.0], - [1, 0, 1, 1.0], - [1, 1, 0, 1.0], - [1, 1, 1, 1.0], - ] - var lines = [] - for (var i = 0; i < traces.length; i += 1) { - const line = createLineStrip(this.cg, times, traces[i].data, { - colors: colors[i], - widths: 3, - }) - - lines.push(line) - - const label = createText(this.cg, this.font, traces[i].label, [max_time / 10, 8 - i * 0.7], { - color: colors[i], - }) - - lines.push(label) - } - - const xAxis = createOrthoAxis(this.cg, coords, "x", this.font, { - labelSide: 1, - tickOffset: -2.5, - tickLength: 6, - tickStep: max_time / 5, - labelFormatter: (n) => n.toExponential(2), - }) - - const yAxis = createOrthoAxis(this.cg, coords, "y", this.font, { - tickOffset: -2.5, - tickLength: 6, - tickStep: 2.0, - labelFormatter: (n) => n.toFixed(1), - }) - - // Render the display to an HTML element. - lines.push(xAxis) - lines.push(yAxis) - - this.cg.render(coords, viewport, lines) - - // Copy the plot to a new canvas and add it to the document. - this.cg.copyTo(viewport, this.canvas) - } - - render() { - return ( -
-
{ this.divElement = divElement } } > - -
- this.drawGraph(times, traces)} /> -
- ); - } -} - -ReactDOM.render(, document.getElementById("root")) diff --git a/src/bin/main.rs b/src/bin/main.rs new file mode 100644 index 0000000..6c50164 --- /dev/null +++ b/src/bin/main.rs @@ -0,0 +1,210 @@ +#![cfg_attr(not(debug_assertions), windows_subsystem = "windows")] // hide console window on Windows in release + +use anyhow::Result; +use clap::Parser; +use eframe::egui; +use eframe::egui::plot::{Legend, Line, Plot, PlotPoints}; +// use std::io::{Read, Seek}; +use std::sync::mpsc; +use std::time::Duration; + +use stabilizer_streaming::{Detrend, Frame, Loss, PsdCascade}; + +/// Execute stabilizer stream throughput testing. +/// Use `RUST_LOG=info cargo run` to increase logging verbosity. +#[derive(Parser)] +struct Opts { + /// The local IP to receive streaming data on. + #[clap(short, long, default_value = "0.0.0.0")] + ip: std::net::Ipv4Addr, + + /// The UDP port to receive streaming data on. + #[clap(long, long, default_value = "9293")] + port: u16, +} + +#[derive(Clone, Copy, Debug)] +enum Cmd { + Exit, + Reset, +} + +struct Trace { + psd: Vec<[f64; 2]>, +} + +impl Trace { + fn new(psd: Vec<[f64; 2]>) -> Self { + Self { psd } + } +} + +fn main() -> Result<()> { + env_logger::init(); // Log to stderr (if you run with `RUST_LOG=debug`). + let opts = Opts::parse(); + + let (cmd_send, cmd_recv) = mpsc::channel(); + let (trace_send, trace_recv) = mpsc::sync_channel(1); + let receiver = std::thread::spawn(move || { + log::info!("Binding to {}:{}", opts.ip, opts.port); + let socket = std::net::UdpSocket::bind((opts.ip, opts.port))?; + socket2::SockRef::from(&socket).set_recv_buffer_size(1 << 20)?; + socket.set_read_timeout(Some(Duration::from_millis(1000)))?; + log::info!("Receiving frames"); + + let mut loss = Loss::default(); + let mut dec: Vec<_> = (0..4) + .map(|_| PsdCascade::new(1 << 9, 3, Detrend::Mean)) + .collect(); + + // let mut fil = std::fs::File::open("/tmp/fls2x.raw")?; + + let mut buf = vec![0; 2048]; + let mut i = 0; + loop { + match cmd_recv.try_recv() { + Err(mpsc::TryRecvError::Disconnected) | Ok(Cmd::Exit) => break, + Ok(Cmd::Reset) => { + dec = (0..4) + .map(|_| PsdCascade::new(1 << 9, 3, Detrend::Mean)) + .collect(); + } + Err(mpsc::TryRecvError::Empty) => {} + }; + + // let len = fil.read(&mut buf[..1400])?; + // if len == 0 { + // fil.seek(std::io::SeekFrom::Start(0))?; + // continue; + // } + + let len = socket.recv(&mut buf)?; + match Frame::from_bytes(&buf[..len]) { + Ok(frame) => { + loss.update(&frame); + for (dec, x) in dec.iter_mut().zip(frame.data.traces()) { + dec.process(x); + } + i += 1; + } + Err(e) => log::warn!("{e} {:?}", &buf[..8]), + }; + if i >= 50 { + i = 0; + let trace = dec + .iter() + .map(|dec| { + let (p, b) = dec.get(4); + let mut f = vec![]; + for bi in b.iter() { + f.truncate(bi[0]); + let df = 1.0 / bi[3] as f32; + f.extend((0..bi[2]).rev().map(|f| f as f32 * df)); + } + Trace::new( + f.iter() + .zip(p.iter()) + .rev() + .skip(1) + .map(|(f, p)| [f.log10() as f64, 10.0 * p.log10() as f64]) + .collect(), + ) + }) + .collect(); + match trace_send.try_send(trace) { + Ok(()) => {} + Err(mpsc::TrySendError::Full(_)) => { + // log::warn!("full"); + } + Err(e) => { + log::error!("{:?}", e); + } + } + } + } + + loss.analyze(); + + Result::<()>::Ok(()) + }); + + let options = eframe::NativeOptions { + initial_window_size: Some(egui::vec2(640.0, 500.0)), + ..Default::default() + }; + eframe::run_native( + "FLS", + options, + Box::new(|cc| Box::new(FLS::new(cc, trace_recv, cmd_send))), + ) + .unwrap(); + + receiver.join().unwrap()?; + + Ok(()) +} + +pub struct FLS { + trace_recv: mpsc::Receiver>, + cmd_send: mpsc::Sender, + current: Option>, +} + +impl FLS { + fn new( + cc: &eframe::CreationContext<'_>, + trace_recv: mpsc::Receiver>, + cmd_send: mpsc::Sender, + ) -> Self { + cc.egui_ctx.set_visuals(egui::Visuals::light()); + + Self { + trace_recv, + cmd_send, + current: None, + } + } +} + +impl eframe::App for FLS { + fn on_exit(&mut self, _gl: Option<&eframe::glow::Context>) { + self.cmd_send.send(Cmd::Exit).ok(); + } + + fn update(&mut self, ctx: &egui::Context, _frame: &mut eframe::Frame) { + egui::CentralPanel::default().show(ctx, |ui| { + match self.trace_recv.try_recv() { + Err(mpsc::TryRecvError::Empty) => {} + Ok(new) => { + self.current = Some(new); + ctx.request_repaint_after(Duration::from_millis(100)); + } + Err(mpsc::TryRecvError::Disconnected) => { + panic!("lost data processing thread") + } + }; + ui.heading("FLS"); + ui.add_space(20.0); + ui.horizontal(|ui| { + ui.add_space(20.0); + let plot = Plot::new("") + .width(600.0) + .height(400.0) + // .x_grid_spacer(log_grid_spacer(10)) + .legend(Legend::default()); + plot.show(ui, |plot_ui| { + if let Some(traces) = &mut self.current { + for (trace, name) in traces.iter().zip(["AI", "AQ", "BI", "BQ"].into_iter()) + { + plot_ui.line(Line::new(PlotPoints::from(trace.psd.clone())).name(name)); + } + } + }); + }); + ui.add_space(20.0); + if ui.button("Reset").clicked() { + self.cmd_send.send(Cmd::Reset).unwrap(); + } + }); + } +} diff --git a/src/bin/stream_test.rs b/src/bin/stream_test.rs index 7cb3b6f..b9c111a 100644 --- a/src/bin/stream_test.rs +++ b/src/bin/stream_test.rs @@ -1,8 +1,8 @@ +use anyhow::Result; use clap::Parser; -use stabilizer_streaming::StreamReceiver; -use std::time::{Duration, Instant}; - -const MAX_LOSS: f32 = 0.05; +use stabilizer_streaming::{Detrend, Frame, Loss, PsdCascade}; +use std::sync::mpsc; +use std::time::Duration; /// Execute stabilizer stream throughput testing. /// Use `RUST_LOG=info cargo run` to increase logging verbosity. @@ -10,70 +10,60 @@ const MAX_LOSS: f32 = 0.05; struct Opts { /// The local IP to receive streaming data on. #[clap(short, long, default_value = "0.0.0.0")] - ip: String, + ip: std::net::Ipv4Addr, /// The UDP port to receive streaming data on. - #[clap(long, default_value = "9293")] + #[clap(long, long, default_value = "9293")] port: u16, /// The test duration in seconds. - #[clap(long, default_value = "5")] + #[clap(long, long, default_value = "5")] duration: f32, } -#[async_std::main] -async fn main() { +fn main() -> Result<()> { env_logger::init(); - let opts = Opts::parse(); - let ip: std::net::Ipv4Addr = opts.ip.parse().unwrap(); - - log::info!("Binding to socket"); - let mut stream_receiver = StreamReceiver::new(ip, opts.port).await; - stream_receiver.set_timeout(Duration::from_secs(1)); - - let mut total_batches = 0u64; - let mut dropped_batches = 0u64; - let mut expect_sequence = None; - let stop = Instant::now() + Duration::from_millis((opts.duration * 1000.) as _); - - log::info!("Reading frames"); - while Instant::now() < stop { - let frame = stream_receiver.next_frame().await.unwrap(); - total_batches += frame.data.batch_count() as u64; - - if let Some(expect) = expect_sequence { - let num_dropped = frame.sequence_number().wrapping_sub(expect) as u64; - dropped_batches += num_dropped; - total_batches += num_dropped; - - if num_dropped > 0 { - log::warn!( - "Lost {} batches: {:#08X} -> {:#08X}", - num_dropped, - expect, - frame.sequence_number(), - ); - } + let (cmd_send, cmd_recv) = mpsc::channel(); + let receiver = std::thread::spawn(move || { + log::info!("Binding to {}:{}", opts.ip, opts.port); + let socket = std::net::UdpSocket::bind((opts.ip, opts.port))?; + socket2::SockRef::from(&socket).set_recv_buffer_size(1 << 20)?; + socket.set_read_timeout(Some(Duration::from_millis(100)))?; + log::info!("Receiving frames"); + let mut buf = vec![0u8; 2048]; + + let mut loss = Loss::default(); + + let mut dec: Vec<_> = (0..4) + .map(|_| PsdCascade::new(1 << 9, 3, Detrend::Mean)) + .collect(); + + while cmd_recv.try_recv() == Err(mpsc::TryRecvError::Empty) { + let len = socket.recv(&mut buf)?; + match Frame::from_bytes(&buf[..len]) { + Ok(frame) => { + loss.update(&frame); + for (dec, x) in dec.iter_mut().zip(frame.data.traces()) { + dec.process(x); + } + } + Err(e) => log::warn!("{e}"), + }; } - expect_sequence = Some( - frame - .sequence_number() - .wrapping_add(frame.data.batch_count() as _), - ); - } + loss.analyze(); + + let (y, b) = dec[1].get(4); + println!("{:?}, {:?}", b, y); - assert!(total_batches > 0); - let loss = dropped_batches as f32 / total_batches as f32; + Result::<()>::Ok(()) + }); - log::info!( - "Loss: {} % ({}/{} batches)", - loss * 100.0, - dropped_batches, - total_batches - ); + std::thread::sleep(Duration::from_millis((opts.duration * 1000.) as _)); + cmd_send.send(())?; + receiver.join().unwrap()?; - assert!(loss < MAX_LOSS); + Ok(()) } diff --git a/src/bin/webserver.rs b/src/bin/webserver.rs deleted file mode 100644 index 4fd55c5..0000000 --- a/src/bin/webserver.rs +++ /dev/null @@ -1,360 +0,0 @@ -use clap::Parser; -use serde::{Deserialize, Serialize}; -use stabilizer_streaming::{de::deserializer::StreamFrame, de::StreamFormat, StreamReceiver}; -use tide::{Body, Response}; - -use std::time::Instant; - -// TODO: Expose this as a configurable parameter and/or add it to the stream frame. -const SAMPLE_RATE_HZ: f32 = 100e6 / 128.0; - -const SAMPLE_PERIOD: f32 = 1.0 / SAMPLE_RATE_HZ; - -/// Execute stabilizer stream throughput testing. -/// Use `RUST_LOG=info cargo run` to increase logging verbosity. -#[derive(Parser)] -struct Opts { - /// The local IP to receive streaming data on. - #[clap(short, long, default_value = "0.0.0.0")] - ip: String, - - /// The UDP port to receive streaming data on. - #[clap(long, default_value = "9293")] - port: u16, -} - -#[derive(Deserialize)] -struct CaptureSettings { - /// The duration to capture data for in seconds. - capture_duration_secs: f32, -} - -/// The global state of the backend server. -struct ServerState { - pub data: async_std::sync::Mutex, -} - -struct StreamData { - // The current format of the received stream. - current_format: Option, - - // The maximum buffer size of received stream data in samples. - max_size: usize, - - // The buffer for maintaining trace timestamps. - timebase: BufferedData, - - // The buffer for maintaining trace data points. - traces: Vec, -} - -/// A trace containing a label and associated data. -#[derive(Serialize, Clone, Debug)] -struct Trace { - label: String, - data: Vec, -} - -/// All relavent data needed to display information. -#[derive(Serialize, Debug)] -struct TraceData { - time: Vec, - traces: Vec, -} - -// A ringbuffer-like vector for maintaining received data. -struct BufferedData { - // The next write index - index: usize, - - // The stored data. - data: Vec, - - // The maximum number of data points stored. Once this level is hit, data will begin - // overwriting from the beginning. - max_size: usize, -} - -impl BufferedData { - pub fn new(size: usize) -> Self { - Self { - data: Vec::new(), - index: 0, - max_size: size, - } - } - - /// Append data to the buffer in an overflowing manner. - /// - /// # Note - /// If the amount of data provided overflows the buffer size, it will still be accepted. - pub fn overflowing_write(&mut self, mut data: &[T]) { - // Continuously append data into the buffer in an overflowing manner (old data is - // overwritten). - while data.len() > 0 { - let write_length = if data.len() > self.max_size - self.index { - self.max_size - self.index - } else { - data.len() - }; - - self.add(&data[..write_length]); - data = &data[write_length..]; - } - } - - // Add data to the buffer - fn add(&mut self, data: &[T]) { - if self.data.len() < self.max_size { - assert!(data.len() + self.data.len() <= self.max_size); - self.data.extend_from_slice(data) - } else { - self.data[self.index..][..data.len()].copy_from_slice(data); - } - - self.index = (self.index + data.len()) % self.max_size; - } - - /// Get the earliest element in the buffer along with its location. - pub fn get_earliest_element(&self) -> (usize, T) { - if self.data.len() != self.max_size { - (0, self.data[0]) - } else { - let index = (self.index + 1) % self.max_size; - (index, self.data[index]) - } - } - - /// Resize the buffer, clearing any previous data. - pub fn resize(&mut self, size: usize) { - self.index = 0; - self.data.clear(); - self.max_size = size; - } -} - -// A trace, where data is not yet contiguous in memory with respect to the timebase. -struct BufferedTrace { - label: String, - data: BufferedData, -} - -impl From<&BufferedTrace> for Trace { - fn from(bt: &BufferedTrace) -> Trace { - Trace { - label: bt.label.clone(), - data: bt.data.data.clone(), - } - } -} - -impl StreamData { - const fn new() -> Self { - Self { - current_format: None, - timebase: BufferedData { - max_size: SAMPLE_RATE_HZ as usize, - data: Vec::new(), - index: 0, - }, - traces: Vec::new(), - - max_size: SAMPLE_RATE_HZ as usize, - } - } - - /// Ingest an incoming stream frame. - pub fn add_frame(&mut self, frame: StreamFrame) { - // If the stream format has changed, clear all data buffers. - if let Some(format) = self.current_format { - if frame.format() != format { - self.flush() - } - } - - self.current_format.replace(frame.format()); - - // Next, extract all of the data traces - for i in 0..frame.data.trace_count() { - if self.traces.len() < frame.data.trace_count() { - self.traces.push(BufferedTrace { - data: BufferedData::new(self.max_size), - label: frame.data.trace_label(i), - }); - } - - // TODO: Decimate the data as requested. - let trace = frame.data.get_trace(i); - self.traces[i].data.overflowing_write(trace); - - // For the first trace, also extend the timebase. - if i == 0 { - let base = (frame.sequence_number() as u64) - .wrapping_mul(frame.data.samples_per_batch() as u64); - for sample_index in 0..trace.len() { - self.timebase - .overflowing_write(&[base.wrapping_add(sample_index as u64)]) - } - } - } - } - - /// Get the current trace data. - pub fn get_data(&self) -> TraceData { - // Find the smallest sequence number in the timebase. This will be our time reference t = 0 - let (mut earliest_timestamp_offset, mut earliest_timestamp) = - self.timebase.get_earliest_element(); - - for offset in 0..self.timebase.data.len() { - let index = (self.timebase.index + offset) % self.timebase.data.len(); - let delta = earliest_timestamp.wrapping_sub(self.timebase.data[index]); - - if delta < u64::MAX / 4 { - earliest_timestamp_offset = index; - earliest_timestamp = self.timebase.data[index] - } - } - - // Now, create an array of times relative from the earliest timestamp in the timebase. - let mut times: Vec = Vec::new(); - for time in self.timebase.data.iter() { - times.push(time.wrapping_sub(earliest_timestamp) as f32 * SAMPLE_PERIOD) - } - - // Rotate all of the arrays so that they are sequential in time from the earliest - // timestamp. This is necessary because the vectors are being used as ringbuffers. - let mut traces: Vec = Vec::new(); - for trace in self.traces.iter() { - let mut trace: Trace = trace.into(); - trace.data.rotate_left(earliest_timestamp_offset); - traces.push(trace) - } - - times.rotate_left(earliest_timestamp_offset); - - TraceData { - time: times, - traces, - } - } - - /// Remove all data from buffers. - pub fn flush(&mut self) { - log::info!("Flushing"); - self.traces.clear(); - self.current_format.take(); - self.timebase.resize(self.max_size) - } - - /// Resize the receiver to the provided maximum sample size. - pub fn resize(&mut self, max_samples: usize) { - self.max_size = max_samples; - self.flush(); - } -} - -/// Stabilizer stream frame reception thread. -/// -/// # Note -/// This task executes forever, continuously receiving stabilizer stream frames for processing. -/// -/// # Args -/// * `state` - The server state -/// * `state` - A receiver for reading stabilizer stream frames. -async fn receive(state: &ServerState, mut receiver: StreamReceiver) { - loop { - // Get a stream frame from Stabilizer. - let frame = receiver.next_frame().await.unwrap(); - - // Add the frame data to the traces. - let mut data = state.data.lock().await; - data.add_frame(frame); - } -} - -/// Get all available data traces. -/// -/// # Note -/// There is no guarantee that the data will be complete. Poll the current trigger state to ensure -/// all data is available. -/// -/// # Args -/// `request` - Unused -/// -/// # Returns -/// All of the data as a json-serialized `TraceData`. -async fn get_traces(request: tide::Request<&ServerState>) -> tide::Result> { - log::info!("Got data request"); - let state = request.state(); - let data = state.data.lock().await; - let start = Instant::now(); - let response = data.get_data(); - log::info!("Copying: {:?}", start.elapsed()); - log::debug!("Response: {:?}", response); - let body = Body::from_json(&response)?; - log::info!("Trace serialization: {:?}", start.elapsed()); - - Ok(Response::builder(200).body(body)) -} - -/// Configure the current capture settings -/// -/// # Args -/// * `request` - An HTTP request containing json-serialized `CaptureSettings`. -async fn configure_capture( - mut request: tide::Request<&ServerState>, -) -> tide::Result> { - let config: CaptureSettings = request.body_json().await?; - let state = request.state(); - - if config.capture_duration_secs < 0. { - return Ok(Response::builder(400).body("Negative capture duration not supported")); - } - - let samples: f32 = SAMPLE_RATE_HZ * config.capture_duration_secs; - if samples > usize::MAX as f32 { - return Ok(Response::builder(400).body("Too many samples requested")); - } - - // Clear any pre-existing data in the buffers. - let mut data = state.data.lock().await; - - // TODO: Configure decimation - data.resize(samples as usize); - - Ok(Response::builder(200)) -} - -#[async_std::main] -async fn main() -> tide::Result<()> { - env_logger::init(); - - let opts = Opts::parse(); - let ip: std::net::Ipv4Addr = opts.ip.parse().unwrap(); - let stream_receiver = StreamReceiver::new(ip, opts.port).await; - - // Populate the initial receiver data. - static STATE: ServerState = ServerState { - data: async_std::sync::Mutex::new(StreamData::new()), - }; - - STATE.data.lock().await.flush(); - - async_std::task::spawn(receive(&STATE, stream_receiver)); - - let mut webapp = tide::with_state(&STATE); - - // Route configuration and queries. - webapp.at("/traces").get(get_traces); - webapp.at("/configure").post(configure_capture); - webapp - .at("/") - .serve_file("frontend/dist/index.html") - .unwrap(); - webapp.at("/").serve_dir("frontend/dist").unwrap(); - - // Start up the webapp. - webapp.listen("tcp://0.0.0.0:8080").await?; - - Ok(()) -} diff --git a/src/de/data.rs b/src/de/data.rs new file mode 100644 index 0000000..4bf3ad1 --- /dev/null +++ b/src/de/data.rs @@ -0,0 +1,123 @@ +use ndarray::{ArrayView, Axis, ShapeError}; +use thiserror::Error; + +#[derive(Error, Debug, Clone)] +pub enum FormatError { + #[error("Invalid frame payload size")] + InvalidSize(#[from] ShapeError), +} + +pub trait Payload { + fn new(batches: usize, data: &[u8]) -> Result + where + Self: Sized; + fn traces(&self) -> &[Vec]; + fn traces_mut(&mut self) -> &mut [Vec]; + fn labels(&self) -> &[&str]; +} + +pub struct AdcDac { + traces: [Vec; 4], +} + +impl Payload for AdcDac { + /// Extract AdcDacData from a binary data block in the stream. + /// + /// # Args + /// * `batch_size` - The size of each batch in samples. + /// * `data` - The binary data composing the stream frame. + fn new(batches: usize, data: &[u8]) -> Result { + let channels = 4; + let samples = data.len() / batches / channels / core::mem::size_of::(); + let mut data = ArrayView::from_shape( + (batches, channels, samples, core::mem::size_of::()), + data, + )?; + data.swap_axes(0, 1); // FIXME: non-contig + let data = data.into_shape((channels, samples * batches, core::mem::size_of::()))?; + + // 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. + const DAC_VOLT_PER_LSB: f32 = 4.096 * 2.5 / (1u16 << 15) as 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. + const ADC_VOLT_PER_LSB: f32 = 5.0 / 2.0 * 4.096 / (1u16 << 15) as f32; + assert_eq!(DAC_VOLT_PER_LSB, ADC_VOLT_PER_LSB); + + let traces: [Vec; 4] = [ + data.index_axis(Axis(0), 0) + .axis_iter(Axis(0)) + .map(|x| { + i16::from_le_bytes([x[0], x[1]]).wrapping_add(i16::MIN) as f32 + * DAC_VOLT_PER_LSB + }) + .collect(), + data.index_axis(Axis(0), 1) + .axis_iter(Axis(0)) + .map(|x| { + i16::from_le_bytes([x[0], x[1]]).wrapping_add(i16::MIN) as f32 + * DAC_VOLT_PER_LSB + }) + .collect(), + data.index_axis(Axis(0), 2) + .axis_iter(Axis(0)) + .map(|x| i16::from_le_bytes([x[0], x[1]]) as f32 * ADC_VOLT_PER_LSB) + .collect(), + data.index_axis(Axis(0), 3) + .axis_iter(Axis(0)) + .map(|x| i16::from_le_bytes([x[0], x[1]]) as f32 * ADC_VOLT_PER_LSB) + .collect(), + ]; + + Ok(Self { traces }) + } + + fn traces(&self) -> &[Vec] { + &self.traces[..] + } + + fn traces_mut(&mut self) -> &mut [Vec] { + &mut self.traces + } + fn labels(&self) -> &[&str] { + &["ADC0", "ADC1", "DAC0", "DAC1"] + } +} + +pub struct Fls { + traces: [Vec; 4], +} + +impl Payload for Fls { + fn new(batches: usize, data: &[u8]) -> Result { + let data: &[[[i32; 6]; 2]] = bytemuck::cast_slice(data); + // demod_re, demod_im, wraps, ftw, pow_amp, pll + assert_eq!(batches, data.len()); + let traces: [Vec; 4] = [ + data.iter() + .map(|b| b[0][0] as f32 / i32::MAX as f32) + .collect(), + data.iter() + .map(|b| b[0][1] as f32 / i32::MAX as f32) + .collect(), + data.iter() + .map(|b| b[1][0] as f32 / i32::MAX as f32) + .collect(), + data.iter() + .map(|b| b[1][1] as f32 / i32::MAX as f32) + .collect(), + ]; + Ok(Self { traces }) + } + + fn labels(&self) -> &[&str] { + &["AI", "AQ", "BI", "BQ"] + } + + fn traces(&self) -> &[Vec] { + &self.traces + } + fn traces_mut(&mut self) -> &mut [Vec] { + &mut self.traces + } +} diff --git a/src/de/data/mod.rs b/src/de/data/mod.rs deleted file mode 100644 index 2aa1a86..0000000 --- a/src/de/data/mod.rs +++ /dev/null @@ -1,137 +0,0 @@ -#[derive(Debug, Copy, Clone)] -pub enum FormatError { - InvalidSize, -} - -/// Custom type for referencing DAC output codes. -/// The internal integer is the raw code written to the DAC output register. -#[derive(Copy, Clone)] -pub struct DacCode(pub u16); - -impl From 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; - - (code.0 as i16).wrapping_add(i16::MIN) as f32 * dac_volts_per_lsb - } -} - -/// A type representing an ADC sample. -#[derive(Copy, Clone)] -pub struct AdcCode(pub u16); - -impl From 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 for i16 { - /// Get a stabilizer-defined code from the ADC code. - fn from(code: AdcCode) -> i16 { - code.0 as i16 - } -} - -impl From 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 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; - - i16::from(code) as f32 * adc_volts_per_lsb - } -} - -pub trait FrameData { - fn trace_count(&self) -> usize; - fn get_trace(&self, index: usize) -> &Vec; - fn samples_per_batch(&self) -> usize; - fn batch_count(&self) -> usize { - self.get_trace(0).len() / self.samples_per_batch() - } - fn trace_label(&self, index: usize) -> String { - format!("{}", index) - } -} - -pub struct AdcDacData { - traces: [Vec; 4], - batch_size: usize, -} - -impl FrameData for AdcDacData { - fn trace_count(&self) -> usize { - self.traces.len() - } - - fn get_trace(&self, index: usize) -> &Vec { - &self.traces[index] - } - - fn samples_per_batch(&self) -> usize { - // Each element of the batch is 4 samples, each of which are u16s. - self.batch_size - } - - fn trace_label(&self, index: usize) -> String { - match index { - 0 => "ADC0".to_string(), - 1 => "ADC1".to_string(), - 2 => "DAC0".to_string(), - 3 => "DAC1".to_string(), - _ => panic!("Invalid trace"), - } - } -} - -impl AdcDacData { - /// Extract AdcDacData from a binary data block in the stream. - /// - /// # Args - /// * `batch_size` - The size of each batch in samples. - /// * `data` - The binary data composing the stream frame. - pub fn new(batch_size: usize, data: &[u8]) -> Result { - let batch_size_bytes: usize = batch_size * 8; - let num_batches = data.len() / batch_size_bytes; - if num_batches * batch_size_bytes != data.len() { - return Err(FormatError::InvalidSize); - } - - let mut traces: [Vec; 4] = [Vec::new(), Vec::new(), Vec::new(), Vec::new()]; - - for batch in 0..num_batches { - let batch_index = batch * batch_size_bytes; - - // Batches are serialized as , where the number of samples in - // ` is equal to that batch_size. - for (i, trace) in traces.iter_mut().enumerate() { - let trace_index = batch_index + 2 * batch_size * i; - - for sample in 0..batch_size { - let sample_index = trace_index + sample * 2; - - let value = { - let code = u16::from_le_bytes([data[sample_index], data[sample_index + 1]]); - if i < 2 { - f32::from(AdcCode(code)) - } else { - f32::from(DacCode(code)) - } - }; - - trace.push(value); - } - } - } - - Ok(Self { batch_size, traces }) - } -} diff --git a/src/de/deserializer.rs b/src/de/deserializer.rs deleted file mode 100644 index 1e98dec..0000000 --- a/src/de/deserializer.rs +++ /dev/null @@ -1,88 +0,0 @@ -use super::data::{AdcDacData, FrameData}; -use super::{Error, StreamFormat}; - -use std::convert::TryFrom; - -// The magic word at the start of each stream frame. -const MAGIC_WORD: u16 = 0x057B; - -// The size of the frame header in bytes. -const HEADER_SIZE: usize = 8; - -/// A single stream frame contains multiple batches of data. -pub struct StreamFrame { - header: FrameHeader, - pub data: Box, -} - -struct FrameHeader { - // The format code associated with the stream binary data. - pub format_code: StreamFormat, - - // The size of each batch contained within the binary data. - pub batch_size: u8, - - // The sequence number of the first batch in the binary data. The sequence number increments - // monotonically for each batch. All batches the binary data are sequential. - pub sequence_number: u32, -} - -impl FrameHeader { - /// Parse the header of a stream frame. - pub fn parse(header: &[u8]) -> Result { - assert_eq!(header.len(), HEADER_SIZE); - - let magic_word = u16::from_le_bytes([header[0], header[1]]); - - if magic_word != MAGIC_WORD { - return Err(Error::InvalidHeader); - } - - let format_code = StreamFormat::try_from(header[2]).map_err(|_| Error::UnknownFormat)?; - let batch_size = header[3]; - let sequence_number = u32::from_le_bytes([header[4], header[5], header[6], header[7]]); - log::debug!( - "Header: {:?}, {}, {:X}", - format_code, - batch_size, - sequence_number - ); - - Ok(Self { - format_code, - batch_size, - sequence_number, - }) - } -} - -impl StreamFrame { - /// Get the format code of the current frame. - pub fn format(&self) -> StreamFormat { - self.header.format_code - } - - /// Get the sequence number of the first batch in the frame. - pub fn sequence_number(&self) -> u32 { - self.header.sequence_number - } - - /// Parse a stream frame from a single UDP packet. - pub fn from_bytes(input: &[u8]) -> Result { - let (header, data) = input.split_at(HEADER_SIZE); - - let header = FrameHeader::parse(header)?; - - let data = match header.format_code { - StreamFormat::AdcDacData => { - let data = AdcDacData::new(header.batch_size as usize, data)?; - Box::new(data) - } - }; - - Ok(StreamFrame { - header: header, - data, - }) - } -} diff --git a/src/de/frame.rs b/src/de/frame.rs new file mode 100644 index 0000000..99ec5a0 --- /dev/null +++ b/src/de/frame.rs @@ -0,0 +1,74 @@ +use super::data::{self, Payload}; +use super::{Error, Format}; + +use std::convert::TryFrom; + +// The magic word at the start of each stream frame. +const MAGIC_WORD: [u8; 2] = [0x7b, 0x05]; + +// The size of the frame header in bytes. +const HEADER_SIZE: usize = 8; + +#[derive(Debug, Copy, Clone, PartialEq, Eq, PartialOrd, Ord)] +struct Header { + // The format code associated with the stream binary data. + pub format: Format, + + // The number of batches in the payload. + pub batches: u8, + + // The sequence number of the first batch in the binary data. The sequence number increments + // monotonically for each batch. All batches the binary data are sequential. + pub seq: u32, +} + +impl Header { + /// Parse the header of a stream frame. + fn parse(header: &[u8]) -> Result { + if header[..2] != MAGIC_WORD { + return Err(Error::InvalidHeader); + } + let format = Format::try_from(header[2]).or(Err(Error::UnknownFormat))?; + let batches = header[3]; + let seq = u32::from_le_bytes(header[4..8].try_into().unwrap()); + Ok(Self { + format, + batches, + seq, + }) + } +} + +/// A single stream frame contains multiple batches of data. +// #[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord)] +pub struct Frame { + header: Header, + pub data: Box, +} + +impl Frame { + /// Get the format code of the current frame. + pub fn format(&self) -> Format { + self.header.format + } + + /// Get the sequence number of the first batch in the frame. + pub fn seq(&self) -> u32 { + self.header.seq + } + + pub fn batches(&self) -> usize { + self.header.batches as _ + } + + /// Parse a stream frame from a single UDP packet. + pub fn from_bytes(input: &[u8]) -> Result { + let header = Header::parse(input)?; + let data = &input[HEADER_SIZE..]; + let data: Box = match header.format { + Format::AdcDac => Box::new(data::AdcDac::new(header.batches as _, data)?), + Format::Fls => Box::new(data::Fls::new(header.batches as _, data)?), + }; + Ok(Self { header, data }) + } +} diff --git a/src/de/mod.rs b/src/de/mod.rs index aeacfee..d36a52e 100644 --- a/src/de/mod.rs +++ b/src/de/mod.rs @@ -1,23 +1,25 @@ use num_enum::TryFromPrimitive; +use thiserror::Error; -pub mod data; -pub mod deserializer; +mod data; +pub use data::*; +mod frame; +pub use frame::*; -#[derive(TryFromPrimitive, Debug, Copy, Clone, PartialEq)] +#[derive(TryFromPrimitive, Debug, Copy, Clone, PartialEq, Eq, PartialOrd, Ord)] #[repr(u8)] -pub enum StreamFormat { - AdcDacData = 1, +#[non_exhaustive] +pub enum Format { + AdcDac = 1, + Fls = 2, } -#[derive(Debug, Copy, Clone)] +#[derive(Debug, Clone, Error)] pub enum Error { - DataFormat(data::FormatError), + #[error("Could not parse the frame payload")] + DataFormat(#[from] data::FormatError), + #[error("Invalid frame header")] InvalidHeader, + #[error("Unknown format ID")] UnknownFormat, } - -impl From for Error { - fn from(e: data::FormatError) -> Error { - Error::DataFormat(e) - } -} diff --git a/src/hbf.rs b/src/hbf.rs new file mode 100644 index 0000000..4cf3780 --- /dev/null +++ b/src/hbf.rs @@ -0,0 +1,484 @@ +/// Decimation/interpolation filters +/// +/// These focus on half-band filters (rate change of 2) and cascades of HBF. +/// The half-band filter has unique properties that make it preferrable in many cases: +/// +/// * only needs N multiplications (fused multiply accumulate) for 4*N taps +/// * stores less state compared with with a straight FIR +/// * as a FIR filter has linear phase/flat group delay +/// * very small passband ripple and excellent stopband attenuation +/// * as a cascade of decimation/interpolation filters, the higher-rate filters +/// need successively fewer taps, allowing the filtering to be dominated by +/// only the highest rate filter with the fewest taps +/// * high dynamic range (compared with a biquad IIR) +/// * can be combined with a CIC filter for non-power-of-two or even higher rate changes +/// +/// The implementations here are all `no_std` and `no-alloc`. +/// They support (but don't require) in-place filtering to reduce memory usage. +/// They unroll and optimmize extremely well targetting current architectures, +/// e.g. requiring less than 4 instructions per input item for the full `HbfDecCascade` on Skylake. +/// The filters are optimized for decent block sizes and perform best (i.e. with negligible +/// overhead) for blocks of 32 high-rate items or more, depending very much on architecture. + +/// Filter input items into output items. +pub trait Filter { + /// Input/output item type. + type Item; + + /// Process a block of items. + /// + /// Input items can be either in `x` or in `y`. + /// In the latter case the filtering operation is done in-place. + /// Output is always written into `y`. + /// The number of items written into `y` is returned. + /// Input and output size relations must match the filter requirements + /// (decimation/interpolation and maximum block size). + /// When using in-place operation, `y` needs to contain the input items + /// (fewer than `y.len()` in the case of interpolation) and must be able to + /// contain the output items. + fn process_block(&mut self, x: Option<&[Self::Item]>, y: &mut [Self::Item]) -> usize; + + /// Return the block size granularity and the maximum block size. + /// + /// For in-place processing, this refers to constraints on `y`. + /// Otherwise this refers to the larger of `x` and `y` (`x` for decimation and `y` for interpolation). + /// The granularity is also the rate change in the case of interpolation/decimation filters. + fn block_size(&self) -> (usize, usize); + + // TODO: process items with automatic blocks + // fn process(&mut self, x: Option<&[Self::Item]>, y: &mut [Self::Item]) -> usize {} +} + +/// Symmetric FIR filter prototype. +/// +/// Center tap assumed to be 0.5 +/// DSP taps 2*M + 1 +/// +/// M: number of taps +/// N: state size: N = 2*M - 1 + {input/output}.len() +#[derive(Clone, Debug, Copy)] +struct SymFir<'a, const M: usize, const N: usize> { + x: [f32; N], + taps: &'a [f32; M], +} + +impl<'a, const M: usize, const N: usize> SymFir<'a, M, N> { + /// taps: one-sided, expluding center tap, oldest to one-before-center + fn new(taps: &'a [f32; M]) -> Self { + debug_assert!(N >= M * 2); + Self { x: [0.0; N], taps } + } + + /// Perform the FIR convolution and yield results iteratively. + #[inline] + fn get(&self) -> impl Iterator + '_ { + self.x.windows(2 * M).map(|x| { + let (old, new) = x.split_at(M); + old.iter() + .zip(new.iter().rev()) + .zip(self.taps.iter()) + .map(|((xo, xn), tap)| (xo + xn) * tap) + .sum() + }) + } +} + +// TODO: pub struct SymFirInt, SymFirDec + +/// Half band decimator (decimate by two) +/// +/// The effective number of DSP taps is 4*M - 1. +/// +/// M: number of taps +/// N: state size: N = 2*M - 1 + output.len() +#[derive(Clone, Debug, Copy)] +pub struct HbfDec<'a, const M: usize, const N: usize> { + even: [f32; N], // This is an upper bound to N - M (unstable const expr) + odd: SymFir<'a, M, N>, +} + +impl<'a, const M: usize, const N: usize> HbfDec<'a, M, N> { + /// Non-zero (odd) taps from oldest to one-before-center. + pub fn new(taps: &'a [f32; M]) -> Self { + Self { + even: [0.0; N], + odd: SymFir::new(taps), + } + } +} + +impl<'a, const M: usize, const N: usize> Filter for HbfDec<'a, M, N> { + type Item = f32; + + fn block_size(&self) -> (usize, usize) { + (2, 2 * (N - (2 * M - 1))) + } + + fn process_block(&mut self, x: Option<&[Self::Item]>, y: &mut [Self::Item]) -> usize { + let x = x.unwrap_or(y); + debug_assert_eq!(x.len() & 1, 0); + let k = x.len() / 2; + // load input + for (xi, (even, odd)) in x.chunks_exact(2).zip( + self.even[M - 1..][..k] + .iter_mut() + .zip(self.odd.x[2 * M - 1..][..k].iter_mut()), + ) { + *even = xi[0]; + *odd = xi[1]; + } + // compute output + for (yi, (even, odd)) in y[..k] + .iter_mut() + .zip(self.even[..k].iter().zip(self.odd.get())) + { + *yi = 0.5 * even + odd; + } + // keep state + self.even.copy_within(k..k + M - 1, 0); + self.odd.x.copy_within(k..k + 2 * M - 1, 0); + k + } +} + +/// Half band interpolator (interpolation rate 2) +/// +/// The effective number of DSP taps is 4*M - 1. +/// +/// M: number of taps +/// N: state size: N = 2*M - 1 + input.len() +#[derive(Clone, Debug, Copy)] +pub struct HbfInt<'a, const M: usize, const N: usize> { + fir: SymFir<'a, M, N>, +} + +impl<'a, const M: usize, const N: usize> HbfInt<'a, M, N> { + /// Non-zero (odd) taps from oldest to one-before-center. + pub fn new(taps: &'a [f32; M]) -> Self { + Self { + fir: SymFir::new(taps), + } + } +} + +impl<'a, const M: usize, const N: usize> Filter for HbfInt<'a, M, N> { + type Item = f32; + + fn block_size(&self) -> (usize, usize) { + (2, 2 * (N - (2 * M - 1))) + } + + fn process_block(&mut self, x: Option<&[Self::Item]>, y: &mut [Self::Item]) -> usize { + debug_assert_eq!(y.len() & 1, 0); + let k = y.len() / 2; + let x = x.unwrap_or(&y[..k]); + // load input + self.fir.x[2 * M - 1..][..k].copy_from_slice(x); + // compute output + for (yi, (even, odd)) in y + .chunks_exact_mut(2) + .zip(self.fir.get().zip(self.fir.x[M..][..k].iter())) + { + yi[0] = 2.0 * even; + yi[1] = *odd; + } + // keep state + self.fir.x.copy_within(k..k + 2 * M - 1, 0); + y.len() + } +} + +/// Standard/optimal half-band filter cascade taps +/// +/// * more than 98 dB stop band attenuation +/// * 0.4 pass band (relative to lowest sample rate) +/// * less than 0.001 dB ripple +/// * linear phase/flat group delay +/// * rate change up to 2**5 = 32 +/// * lowest rate filter is at 0 index +/// * use taps 0..n for 2**n interpolation/decimation +#[allow(clippy::excessive_precision, clippy::type_complexity)] +pub const HBF_TAPS: ([f32; 15], [f32; 6], [f32; 3], [f32; 3], [f32; 2]) = ( + // 15 coefficients (effective number of DSP taps 4*15-1 = 59), transition band width .2 fs + [ + 3.51072006e-05, + -1.21639791e-04, + 3.17513468e-04, + -6.98912706e-04, + 1.37306791e-03, + -2.48201920e-03, + 4.20903456e-03, + -6.79138003e-03, + 1.05502027e-02, + -1.59633823e-02, + 2.38512144e-02, + -3.59007172e-02, + 5.64710020e-02, + -1.01639797e-01, + 3.16796462e-01, + ], + // 6, .47 + [ + -0.00043471, + 0.00288919, + -0.01100837, + 0.03178935, + -0.08313839, + 0.30989656, + ], + // 3, .754 + [0.00707325, -0.0521982, 0.29513371], + // 3, .877 + [0.00613987, -0.04965391, 0.29351417], + // 2, .94 + [-0.03145898, 0.28145805], +); + +/// Passband width in units of lowest sample rate +pub const HBF_PASSBAND: f32 = 0.4; + +/// Max low-rate block size (HbfIntCascade input, HbfDecCascade output) +pub const HBF_CASCADE_BLOCK: usize = 1 << 8; + +/// Half-band decimation filter cascade with optimal taps +/// +/// See [HBF_TAPS]. +/// Only in-place processing is implemented. +/// Supports rate changes of 1, 2, 4, 8, and 16. +pub struct HbfDecCascade { + n: usize, + stages: ( + HbfDec<'static, { HBF_TAPS.3.len() }, { 2 * HBF_TAPS.3.len() - 1 + HBF_CASCADE_BLOCK * 8 }>, + HbfDec<'static, { HBF_TAPS.2.len() }, { 2 * HBF_TAPS.2.len() - 1 + HBF_CASCADE_BLOCK * 4 }>, + HbfDec<'static, { HBF_TAPS.1.len() }, { 2 * HBF_TAPS.1.len() - 1 + HBF_CASCADE_BLOCK * 2 }>, + HbfDec<'static, { HBF_TAPS.0.len() }, { 2 * HBF_TAPS.0.len() - 1 + HBF_CASCADE_BLOCK }>, + ), +} + +impl Default for HbfDecCascade { + fn default() -> Self { + Self { + n: 4, + stages: ( + HbfDec::new(&HBF_TAPS.3), + HbfDec::new(&HBF_TAPS.2), + HbfDec::new(&HBF_TAPS.1), + HbfDec::new(&HBF_TAPS.0), + ), + } + } +} + +impl HbfDecCascade { + pub fn set_n(&mut self, n: usize) { + assert!(n <= 4); + self.n = n; + } + + pub fn n(&self) -> usize { + self.n + } +} + +impl Filter for HbfDecCascade { + type Item = f32; + + fn block_size(&self) -> (usize, usize) { + ( + 1 << self.n, + match self.n { + 0 => usize::MAX, + 1 => self.stages.3.block_size().1, + 2 => self.stages.2.block_size().1, + 3 => self.stages.1.block_size().1, + _ => self.stages.0.block_size().1, + }, + ) + } + + fn process_block(&mut self, x: Option<&[f32]>, y: &mut [f32]) -> usize { + if x.is_some() { + unimplemented!(); // TODO: pair of intermediate buffers + } + + let mut n = y.len(); + if self.n > 3 { + n = self.stages.0.process_block(None, &mut y[..n]); + } + if self.n > 2 { + n = self.stages.1.process_block(None, &mut y[..n]); + } + if self.n > 1 { + n = self.stages.2.process_block(None, &mut y[..n]); + } + if self.n > 0 { + n = self.stages.3.process_block(None, &mut y[..n]); + } + debug_assert_eq!(n, y.len() >> self.n); + n + } +} + +/// Half-band interpolation filter cascade with optimal taps. +/// +/// See [HBF_TAPS]. +/// Only in-place processing is implemented. +/// Supports rate changes of 1, 2, 4, 8, and 16. +pub struct HbfIntCascade { + n: usize, + pub stages: ( + HbfInt<'static, { HBF_TAPS.0.len() }, { 2 * HBF_TAPS.0.len() - 1 + HBF_CASCADE_BLOCK }>, + HbfInt<'static, { HBF_TAPS.1.len() }, { 2 * HBF_TAPS.1.len() - 1 + HBF_CASCADE_BLOCK * 2 }>, + HbfInt<'static, { HBF_TAPS.2.len() }, { 2 * HBF_TAPS.2.len() - 1 + HBF_CASCADE_BLOCK * 4 }>, + HbfInt<'static, { HBF_TAPS.3.len() }, { 2 * HBF_TAPS.3.len() - 1 + HBF_CASCADE_BLOCK * 8 }>, + ), +} + +impl Default for HbfIntCascade { + fn default() -> Self { + Self { + n: 4, + stages: ( + HbfInt::new(&HBF_TAPS.0), + HbfInt::new(&HBF_TAPS.1), + HbfInt::new(&HBF_TAPS.2), + HbfInt::new(&HBF_TAPS.3), + ), + } + } +} + +impl HbfIntCascade { + pub fn set_n(&mut self, n: usize) { + assert!(n <= 4); + self.n = n; + } + + pub fn n(&self) -> usize { + self.n + } +} + +impl Filter for HbfIntCascade { + type Item = f32; + + fn block_size(&self) -> (usize, usize) { + ( + 1 << self.n, + match self.n { + 0 => usize::MAX, + 1 => self.stages.0.block_size().1, + 2 => self.stages.1.block_size().1, + 3 => self.stages.2.block_size().1, + _ => self.stages.3.block_size().1, + }, + ) + } + + fn process_block(&mut self, x: Option<&[f32]>, y: &mut [f32]) -> usize { + if x.is_some() { + unimplemented!(); // TODO: one intermediate buffer and `y` + } + + let mut n = y.len() >> self.n; + if self.n > 0 { + n = self.stages.0.process_block(None, &mut y[..2 * n]); + } + if self.n > 1 { + n = self.stages.1.process_block(None, &mut y[..2 * n]); + } + if self.n > 2 { + n = self.stages.2.process_block(None, &mut y[..2 * n]); + } + if self.n > 3 { + n = self.stages.3.process_block(None, &mut y[..2 * n]); + } + debug_assert_eq!(n, y.len()); + n + } +} + +#[cfg(test)] +mod test { + use super::*; + use rustfft::{num_complex::Complex, FftPlanner}; + + #[test] + fn test() { + let mut h = HbfDec::<1, 5>::new(&[0.25]); + assert_eq!(h.process_block(None, &mut []), 0); + + let mut x = [1.0; 8]; + assert_eq!((2, x.len()), h.block_size()); + let n = h.process_block(None, &mut x); + assert_eq!(x[..n], [0.75, 1.0, 1.0, 1.0]); + + let mut h = HbfDec::<3, 9>::new(&HBF_TAPS.3); + let mut x: Vec<_> = (0..8).map(|i| i as f32).collect(); + assert_eq!((2, x.len()), h.block_size()); + let n = h.process_block(None, &mut x); + println!("{:?}", &x[..n]); + } + + #[test] + fn decim() { + let mut h = HbfDecCascade::default(); + assert_eq!(h.block_size(), (1 << h.n(), HBF_CASCADE_BLOCK << h.n())); + let mut x: Vec<_> = (0..2 << h.n()).map(|i| i as f32).collect(); + let n = h.process_block(None, &mut x); + println!("{:?}", &x[..n]); + } + + #[test] + fn interp() { + let mut h = HbfIntCascade::default(); + h.set_n(4); + assert_eq!(h.block_size(), (1 << h.n(), HBF_CASCADE_BLOCK << h.n())); + let mut x = [0.0; 37 << 4]; + x[0] = 1.0; + let n = h.process_block(None, &mut x); + println!("{:?}", &x[..n]); // interpolator impulse response + + let g = (1 << h.n()) as f32; + let mut y = Vec::from_iter(x[..n].iter().map(|&x| Complex { re: x / g, im: 0.0 })); + // pad + y.resize(5 << 10, Complex::default()); + FftPlanner::new().plan_fft_forward(y.len()).process(&mut y); + // transfer function + let p = Vec::from_iter(y.iter().map(|y| 10.0 * y.norm_sqr().log10())); + let f = p.len() as f32 / g; + // pass band ripple + let p_pass = p[..(f * HBF_PASSBAND).floor() as _] + .iter() + .fold(0.0, |m, p| p.abs().max(m)); + assert!(p_pass < 0.00035); + // stop band attenuation + let p_stop = p[(f * (1.0 - HBF_PASSBAND)).ceil() as _..p.len() / 2] + .iter() + .fold(-200.0, |m, p| p.max(m)); + assert!(p_stop < -98.4); + } + + /// small batch size single 3 mul (11 tap) decimator + #[test] + #[ignore] + fn insn_dec() { + let mut h = HbfDec::<3, { 2 * 3 - 1 + (1 << 4) }>::new(&HBF_TAPS.3); + let mut x = [9.0; 1 << 5]; + for _ in 0..1 << 26 { + h.process_block(None, &mut x); + } + } + + // large batch size full decimator cascade + #[test] + #[ignore] + fn insn_casc() { + let mut x = [9.0; 1 << 8]; + let mut h = HbfDecCascade::default(); + h.set_n(3); + for _ in 0..1 << 22 { + h.process_block(None, &mut x); + } + } +} diff --git a/src/lib.rs b/src/lib.rs index d6b906c..7545241 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -1,55 +1,18 @@ -pub mod de; +use thiserror::Error; -use async_std::net::UdpSocket; -use de::deserializer::StreamFrame; -use std::time::Duration; +mod de; +pub use de::*; +mod hbf; +pub use hbf::*; +mod psd; +pub use psd::*; +mod loss; +pub use loss::*; -/// Receives stream frames from Stabilizer over UDP. -pub struct StreamReceiver { - socket: UdpSocket, - buf: [u8; 2048], - timeout: Option, -} - -impl StreamReceiver { - /// Construct a new receiver. - /// - /// # Args - /// * `ip` - The IP address to bind to. Should be associated with the interface that is used to - /// communciate with Stabilizer. - /// * `port` - The port that livestream data is being sent to. - pub async fn new(ip: std::net::Ipv4Addr, port: u16) -> Self { - log::info!("Binding to {}:{}", ip, port); - let socket = UdpSocket::bind((ip, port)).await.unwrap(); - - Self { - socket, - timeout: None, - buf: [0; 2048], - } - } - - pub fn set_timeout(&mut self, duration: Duration) { - self.timeout.replace(duration); - } - - /// Receive a stream frame from Stabilizer. - pub async fn next_frame(&mut self) -> Option { - // Read a single UDP packet. - let len = if let Some(timeout) = self.timeout { - async_std::io::timeout(timeout, self.socket.recv(&mut self.buf)) - .await - .unwrap() - } else { - self.socket.recv(&mut self.buf).await.unwrap() - }; - - // Deserialize the stream frame. - StreamFrame::from_bytes(&self.buf[..len]) - .map_err(|err| { - log::warn!("Frame deserialization error: {:?}", err); - err - }) - .ok() - } +#[derive(Debug, Error)] +pub enum Error { + #[error("Frame deserialization error")] + Frame(#[from] de::Error), + #[error("IO/Networt error")] + Network(#[from] std::io::Error), } diff --git a/src/loss.rs b/src/loss.rs new file mode 100644 index 0000000..974b015 --- /dev/null +++ b/src/loss.rs @@ -0,0 +1,39 @@ +use crate::Frame; + +#[derive(Clone, Copy, Default)] +pub struct Loss { + received: u64, + dropped: u64, + seq: Option, +} + +impl Loss { + pub fn update(&mut self, frame: &Frame) { + self.received += frame.batches() as u64; + if let Some(seq) = self.seq { + let missing = frame.seq().wrapping_sub(seq) as u64; + self.dropped += missing; + if missing > 0 { + log::warn!( + "Lost {} batches: {:#08X} -> {:#08X}", + missing, + seq, + frame.seq(), + ); + } + } + self.seq = Some(frame.seq().wrapping_add(frame.batches() as _)); + } + + pub fn analyze(&self) { + assert!(self.received > 0); + let loss = self.dropped as f32 / (self.received + self.dropped) as f32; + log::info!( + "Loss: {} % ({} of {})", + loss * 100.0, + self.dropped, + self.received + self.dropped + ); + assert!(loss < 0.05); + } +} diff --git a/src/psd.rs b/src/psd.rs new file mode 100644 index 0000000..4a41992 --- /dev/null +++ b/src/psd.rs @@ -0,0 +1,294 @@ +use rustfft::{num_complex::Complex, Fft, FftPlanner}; +use std::sync::Arc; + +use crate::{Filter, HbfDecCascade}; + +/// Window kernel +#[allow(clippy::len_without_is_empty)] +pub trait Window { + fn len(&self) -> usize; + fn get(&self) -> &[f32]; + /// Normalized effective noise bandwidth (in bins) + fn nenbw(&self) -> f32; + fn power(&self) -> f32; +} + +/// Hann window +pub struct Hann { + win: Vec, +} + +impl Hann { + pub fn new(len: usize) -> Self { + assert!(len > 0); + let df = core::f32::consts::PI / len as f32; + Self { + win: Vec::from_iter((0..len).map(|i| (df * i as f32).sin().powi(2))), + } + } +} + +impl Window for Hann { + fn get(&self) -> &[f32] { + &self.win + } + fn power(&self) -> f32 { + 4.0 + } + fn nenbw(&self) -> f32 { + 1.5 + } + fn len(&self) -> usize { + self.win.len() + } +} + +#[derive(Clone, Copy, Debug, PartialEq, Eq)] +pub enum Detrend { + /// No detrending + None, + /// Remove the mean of first and last item per segment + Mean, + /// Remove linear interpolation between first and last item for each segment + Linear, +} + +/// Power spectral density accumulator and decimator +/// +/// One stage in [PsdCascade]. +pub struct Psd { + hbf: HbfDecCascade, + buf: Vec, + psd: Vec, + count: usize, + fft: Arc>, + win: Arc, +} + +impl Psd { + pub fn new(fft: Arc>, win: Arc, stage_length: usize) -> Self { + let mut hbf = HbfDecCascade::default(); + hbf.set_n(stage_length); + // check fft and decimation block size compatibility + assert!(hbf.block_size().0 <= fft.len() / 2); + assert!(hbf.block_size().1 >= fft.len() / 2); + Self { + hbf, + buf: vec![], + psd: vec![], + count: 0, + fft, + win, + } + } + + /// Process items + /// + /// Unusde items are buffered. + /// Full FFT blocks are processed. + /// Overlap is kept. + /// Decimation is performed on fully processed input items. + /// + /// # Args + /// * `x`: input items + /// * `detrend`: [Detrend] method + /// + /// # Returns + /// decimated output + pub fn process(&mut self, x: &[f32], detrend: Detrend) -> Vec { + self.buf.extend_from_slice(x); + let n = self.fft.len() / 2; + + // fft with n/2 overlap + let m = self + .buf + .windows(2 * n) + .step_by(n) + .map(|block| { + let (mean, slope) = match detrend { + Detrend::None => (0.0, 0.0), + Detrend::Mean => (0.5 * (block[0] + block[2 * n - 1]), 0.0), + Detrend::Linear => { + (block[0], (block[2 * n - 1] - block[0]) / (2 * n - 1) as f32) + } + }; + // apply detrending, window, make complex + let mut p: Vec<_> = block + .iter() + .zip(self.win.get().iter()) + .enumerate() + .map(|(i, (x, w))| Complex::new((x - mean - i as f32 * slope) * w, 0.0)) + .collect(); + // fft in-place + self.fft.process(&mut p); + // convert positive frequency spectrum to power + let p = p[..n].iter().map(|y| y.norm_sqr()); + // accumulate + if self.psd.is_empty() { + self.psd.extend(p); + } else { + // TODO note that this looses accuracy for very large averages + for (psd, p) in self.psd.iter_mut().zip(p) { + *psd += p; + } + } + }) + .count(); + self.count += m; + + let mut y = vec![]; + // decimate chunks + for block in self.buf[..m * n].chunks_mut(self.hbf.block_size().1) { + assert!(block.len() >= self.hbf.block_size().0); + let k = self.hbf.process_block(None, block); + y.extend_from_slice(&block[..k]); + } + // drop the overlapped and processed chunks + self.buf.drain(..m * n); + y + } + + /// PSD normalization factor + pub fn gain(&self) -> f32 { + // 2 for one-sided + // 0.5 for overlap + self.win.power() / ((self.count * self.fft.len()) as f32 * self.win.nenbw()) + } +} + +/// Online PSD calculator +/// +/// Infinite averaging +/// Incremental updates +/// Automatic FFT stage extension +pub struct PsdCascade { + stages: Vec, + fft: Arc>, + win: Arc, + stage_length: usize, + detrend: Detrend, +} + +impl PsdCascade { + /// Create a new Psd instance + /// + /// fft_size: size of the FFT blocks and the window + /// stage_length: number of decimation stages. rate change per stage is 1 << stage_length + /// detrend: [Detrend] method + pub fn new(fft_size: usize, stage_length: usize, detrend: Detrend) -> Self { + let fft = FftPlanner::::new().plan_fft_forward(fft_size); + let win = Arc::new(Hann::new(fft_size)); + Self { + stages: vec![], + fft, + win, + stage_length, + detrend, + } + } + + /// Process input items + pub fn process(&mut self, x: &[f32]) { + let mut x = x; + let mut y: Vec<_>; + let mut i = 0; + while !x.is_empty() { + if i + 1 > self.stages.len() { + self.stages.push(Psd::new( + self.fft.clone(), + self.win.clone(), + self.stage_length, + )); + } + y = self.stages[i].process(x, self.detrend); + x = &y; + i += 1; + } + } + + /// Return the PSD and a Vec of segement information + /// + /// # Args + /// * `min_count`: minimum number of averages to include in output + /// + /// # Returns + /// * `Vec` of `[end index, average count, highest bin, effective fft size]` + /// * PSD `Vec` normalized + pub fn get(&self, min_count: usize) -> (Vec, Vec<[usize; 4]>) { + let mut p = vec![]; + let mut b = vec![]; + let mut n = 0; + for stage in self.stages.iter().take_while(|s| s.count >= min_count) { + let mut pi = &stage.psd[..]; + let f = stage.fft.len(); + // a stage yields frequency bins 0..N/2 ty its nyquist + // 0..floor(0.4*N) is its passband if it was preceeded by a decimator + // 0..floor(0.4*N/R) is next lower stage + // hence take bins ceil(0.4*N/R)..floor(0.4*N) from a stage + if !p.is_empty() { + // not the first stage + // remove transition band of previous stage's decimator, floor + let f_pass = 4 * f / 10; + pi = &pi[..f_pass]; + // remove low f bins from previous stage, ceil + let f_low = (4 * f + (10 << stage.hbf.n()) - 1) / (10 << stage.hbf.n()); + p.truncate(p.len() - f_low); + } + // stage start index, number of averages, highest bin freq, 1/bin width (effective fft size) + let g = stage.gain() * (1 << n) as f32; + b.push([p.len(), stage.count, pi.len(), f << n]); + p.extend(pi.iter().rev().map(|pi| pi * g)); + n += stage.hbf.n(); + } + (p, b) + } +} + +#[cfg(test)] +mod test { + use super::*; + + #[test] + fn test() { + assert_eq!(crate::HBF_PASSBAND, 0.4); + + // make uniform noise [-1, 1), ignore the epsilon. + let mut x: Vec = (0..1 << 20) + .map(|_| rand::random::() * 2.0 - 1.0) + .collect(); + let xm = x.iter().map(|x| *x as f64).sum::() as f32 / x.len() as f32; + // mean is 0, take 10 sigma here and elsewhere + assert!(xm.abs() < 10.0 / (x.len() as f32).sqrt()); + let xv = x.iter().map(|x| (x * x) as f64).sum::() as f32 / x.len() as f32; + // variance is 1/3 + assert!((xv * 3.0 - 1.0).abs() < 10.0 / (x.len() as f32).sqrt()); + + let f = 1 << 9; + let n = 3; + let mut s = Psd::new( + FftPlanner::new().plan_fft_forward(f), + Arc::new(Hann::new(f)), + n, + ); + let y = s.process(&mut x, Detrend::None); + assert_eq!(y.len(), (x.len() - f / 2) >> n); + let p: Vec<_> = s.psd.iter().map(|p| p * s.gain()).collect(); + // psd of a stage + assert!(p + .iter() + .all(|p| (p * 3.0 - 1.0).abs() < 10.0 * (f as f32 / x.len() as f32).sqrt())); + + let mut d = PsdCascade::new(f, n, Detrend::None); + d.process(&x); + let (y, b) = d.get(1); + for (i, bi) in b.iter().enumerate() { + // let (start, count, high, size) = bi.into(); + let end = b.get(i + 1).map(|bi| bi[0]).unwrap_or(y.len()); + let yi = &y[bi[0]..end]; + // psd of the cascade + assert!(yi + .iter() + .all(|yi| (yi * 3.0 - 1.0).abs() < 10.0 / (bi[1] as f32).sqrt())); + } + } +} From 678c97d430e4cbc23c5e1701cc1c7abc2bd3ca5b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Robert=20J=C3=B6rdens?= Date: Fri, 1 Sep 2023 10:43:47 +0200 Subject: [PATCH 16/39] tweak --- Cargo.toml | 2 -- src/bin/main.rs | 7 ++++--- src/de/data.rs | 8 +++++--- src/psd.rs | 48 ++++++++++++++++++++++++++++++++---------------- 4 files changed, 41 insertions(+), 24 deletions(-) diff --git a/Cargo.toml b/Cargo.toml index ac61160..52072df 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -28,8 +28,6 @@ anyhow = "1.0.75" socket2 = "0.5.3" idsp = "0.10.0" rustfft = "6.1.0" - -[dev-dependencies] rand = "0.8.5" #[build-dependencies] diff --git a/src/bin/main.rs b/src/bin/main.rs index 6c50164..bb3d52b 100644 --- a/src/bin/main.rs +++ b/src/bin/main.rs @@ -54,7 +54,7 @@ fn main() -> Result<()> { let mut loss = Loss::default(); let mut dec: Vec<_> = (0..4) - .map(|_| PsdCascade::new(1 << 9, 3, Detrend::Mean)) + .map(|_| PsdCascade::new(1 << 8, 3, Detrend::Mean)) .collect(); // let mut fil = std::fs::File::open("/tmp/fls2x.raw")?; @@ -66,7 +66,7 @@ fn main() -> Result<()> { Err(mpsc::TryRecvError::Disconnected) | Ok(Cmd::Exit) => break, Ok(Cmd::Reset) => { dec = (0..4) - .map(|_| PsdCascade::new(1 << 9, 3, Detrend::Mean)) + .map(|_| PsdCascade::new(1 << 8, 3, Detrend::Mean)) .collect(); } Err(mpsc::TryRecvError::Empty) => {} @@ -83,6 +83,7 @@ fn main() -> Result<()> { Ok(frame) => { loss.update(&frame); for (dec, x) in dec.iter_mut().zip(frame.data.traces()) { + // let x = (0..1<<10).map(|_| (rand::random::()*2.0 - 1.0)).collect::>(); dec.process(x); } i += 1; @@ -94,7 +95,7 @@ fn main() -> Result<()> { let trace = dec .iter() .map(|dec| { - let (p, b) = dec.get(4); + let (p, b) = dec.get(1); let mut f = vec![]; for bi in b.iter() { f.truncate(bi[0]); diff --git a/src/de/data.rs b/src/de/data.rs index 4bf3ad1..b743054 100644 --- a/src/de/data.rs +++ b/src/de/data.rs @@ -95,10 +95,12 @@ impl Payload for Fls { assert_eq!(batches, data.len()); let traces: [Vec; 4] = [ data.iter() - .map(|b| b[0][0] as f32 / i32::MAX as f32) + .map(|b| { + ((b[0][0] as f32).powi(2) + (b[0][1] as f32).powi(2)).sqrt() / (i32::MAX as f32) + }) .collect(), data.iter() - .map(|b| b[0][1] as f32 / i32::MAX as f32) + .map(|b| (b[0][1] as f32).atan2(b[0][0] as f32)) .collect(), data.iter() .map(|b| b[1][0] as f32 / i32::MAX as f32) @@ -111,7 +113,7 @@ impl Payload for Fls { } fn labels(&self) -> &[&str] { - &["AI", "AQ", "BI", "BQ"] + &["AR", "AP", "BI", "BQ"] } fn traces(&self) -> &[Vec] { diff --git a/src/psd.rs b/src/psd.rs index 4a41992..dd3527f 100644 --- a/src/psd.rs +++ b/src/psd.rs @@ -70,8 +70,8 @@ impl Psd { let mut hbf = HbfDecCascade::default(); hbf.set_n(stage_length); // check fft and decimation block size compatibility - assert!(hbf.block_size().0 <= fft.len() / 2); - assert!(hbf.block_size().1 >= fft.len() / 2); + assert!(hbf.block_size().0 <= fft.len() / 2); // needed for processing and dropping blocks + assert!(fft.len() >= 2); // Nyquist and DC distinction Self { hbf, buf: vec![], @@ -149,10 +149,12 @@ impl Psd { } /// PSD normalization factor + /// + /// one-sided pub fn gain(&self) -> f32 { // 2 for one-sided - // 0.5 for overlap - self.win.power() / ((self.count * self.fft.len()) as f32 * self.win.nenbw()) + // overlap compensated by count + 2.0 * self.win.power() / ((self.count * self.fft.len()) as f32 * self.win.nenbw()) } } @@ -206,14 +208,14 @@ impl PsdCascade { } } - /// Return the PSD and a Vec of segement information + /// Return the PSD and a Vec of segement break information /// /// # Args /// * `min_count`: minimum number of averages to include in output /// /// # Returns - /// * `Vec` of `[end index, average count, highest bin, effective fft size]` - /// * PSD `Vec` normalized + /// * `psd`: `Vec` normalized reversed (Nyquist first, DC last) + /// * `breaks`: `Vec` of stage breaks `[start index in psd, average count, highest bin index, effective fft size]` pub fn get(&self, min_count: usize) -> (Vec, Vec<[usize; 4]>) { let mut p = vec![]; let mut b = vec![]; @@ -234,12 +236,17 @@ impl PsdCascade { let f_low = (4 * f + (10 << stage.hbf.n()) - 1) / (10 << stage.hbf.n()); p.truncate(p.len() - f_low); } - // stage start index, number of averages, highest bin freq, 1/bin width (effective fft size) let g = stage.gain() * (1 << n) as f32; b.push([p.len(), stage.count, pi.len(), f << n]); p.extend(pi.iter().rev().map(|pi| pi * g)); n += stage.hbf.n(); } + // correct DC and Nyquist bins as both only contribute once to the one-sided spectrum + // this matches matplotlib and matlab but is certainly a questionable step + // need special care when interpreting and integrating the PSD + p[0] *= 0.5; + let n = p.len(); + p[n - 1] *= 0.5; (p, b) } } @@ -274,21 +281,30 @@ mod test { assert_eq!(y.len(), (x.len() - f / 2) >> n); let p: Vec<_> = s.psd.iter().map(|p| p * s.gain()).collect(); // psd of a stage - assert!(p - .iter() - .all(|p| (p * 3.0 - 1.0).abs() < 10.0 * (f as f32 / x.len() as f32).sqrt())); + assert!( + p.iter() + // 0.5 for one-sided spectrum + .all(|p| (p * 0.5 * 3.0 - 1.0).abs() < 10.0 * (f as f32 / x.len() as f32).sqrt()), + "{:?}", + &p[..3] + ); let mut d = PsdCascade::new(f, n, Detrend::None); d.process(&x); - let (y, b) = d.get(1); + let (mut p, b) = d.get(1); + // tweak DC and Nyquist to make checks less code + let n = p.len(); + p[0] *= 2.0; + p[n - 1] *= 2.0; for (i, bi) in b.iter().enumerate() { // let (start, count, high, size) = bi.into(); - let end = b.get(i + 1).map(|bi| bi[0]).unwrap_or(y.len()); - let yi = &y[bi[0]..end]; + let end = b.get(i + 1).map(|bi| bi[0]).unwrap_or(n); + let pi = &p[bi[0]..end]; // psd of the cascade - assert!(yi + assert!(pi .iter() - .all(|yi| (yi * 3.0 - 1.0).abs() < 10.0 / (bi[1] as f32).sqrt())); + // 0.5 for one-sided spectrum + .all(|p| (p * 0.5 * 3.0 - 1.0).abs() < 10.0 / (bi[1] as f32).sqrt())); } } } From cd4f45fdb239157eb26f85685b2328a98532c8c1 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Robert=20J=C3=B6rdens?= Date: Fri, 1 Sep 2023 15:21:01 +0200 Subject: [PATCH 17/39] static psd stages --- Cargo.toml | 1 + src/bin/main.rs | 14 +- src/bin/stream_test.rs | 6 +- src/psd.rs | 296 ++++++++++++++++++++++------------------- 4 files changed, 179 insertions(+), 138 deletions(-) diff --git a/Cargo.toml b/Cargo.toml index 52072df..bbdabdb 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -29,6 +29,7 @@ socket2 = "0.5.3" idsp = "0.10.0" rustfft = "6.1.0" rand = "0.8.5" +heapless = "0.7.16" #[build-dependencies] #npm_rs = "0.2.1" diff --git a/src/bin/main.rs b/src/bin/main.rs index bb3d52b..9b10dec 100644 --- a/src/bin/main.rs +++ b/src/bin/main.rs @@ -54,7 +54,11 @@ fn main() -> Result<()> { let mut loss = Loss::default(); let mut dec: Vec<_> = (0..4) - .map(|_| PsdCascade::new(1 << 8, 3, Detrend::Mean)) + .map(|_| { + PsdCascade::<{ 1 << 8 }>::default() + .stage_length(3) + .detrend(Detrend::Mean) + }) .collect(); // let mut fil = std::fs::File::open("/tmp/fls2x.raw")?; @@ -66,7 +70,11 @@ fn main() -> Result<()> { Err(mpsc::TryRecvError::Disconnected) | Ok(Cmd::Exit) => break, Ok(Cmd::Reset) => { dec = (0..4) - .map(|_| PsdCascade::new(1 << 8, 3, Detrend::Mean)) + .map(|_| { + PsdCascade::<{ 1 << 8 }>::default() + .stage_length(3) + .detrend(Detrend::None) + }) .collect(); } Err(mpsc::TryRecvError::Empty) => {} @@ -106,7 +114,7 @@ fn main() -> Result<()> { f.iter() .zip(p.iter()) .rev() - .skip(1) + .skip(2) // DC and first bin .map(|(f, p)| [f.log10() as f64, 10.0 * p.log10() as f64]) .collect(), ) diff --git a/src/bin/stream_test.rs b/src/bin/stream_test.rs index b9c111a..1d0de89 100644 --- a/src/bin/stream_test.rs +++ b/src/bin/stream_test.rs @@ -37,7 +37,11 @@ fn main() -> Result<()> { let mut loss = Loss::default(); let mut dec: Vec<_> = (0..4) - .map(|_| PsdCascade::new(1 << 9, 3, Detrend::Mean)) + .map(|_| { + PsdCascade::<{ 1 << 8 }>::default() + .stage_length(3) + .detrend(Detrend::Mean) + }) .collect(); while cmd_recv.try_recv() == Err(mpsc::TryRecvError::Empty) { diff --git a/src/psd.rs b/src/psd.rs index dd3527f..5898492 100644 --- a/src/psd.rs +++ b/src/psd.rs @@ -1,48 +1,34 @@ +use crate::{Filter, HbfDecCascade}; use rustfft::{num_complex::Complex, Fft, FftPlanner}; use std::sync::Arc; -use crate::{Filter, HbfDecCascade}; - /// Window kernel -#[allow(clippy::len_without_is_empty)] -pub trait Window { - fn len(&self) -> usize; - fn get(&self) -> &[f32]; +#[derive(Copy, Clone, Debug, PartialEq)] +pub struct Window { + pub win: [f32; N], + pub power: f32, /// Normalized effective noise bandwidth (in bins) - fn nenbw(&self) -> f32; - fn power(&self) -> f32; + pub nenbw: f32, } /// Hann window -pub struct Hann { - win: Vec, -} -impl Hann { - pub fn new(len: usize) -> Self { - assert!(len > 0); - let df = core::f32::consts::PI / len as f32; +impl Window { + pub fn hann() -> Self { + assert!(N > 0); + let df = core::f32::consts::PI / N as f32; + let mut win = [0.0; N]; + for (i, w) in win.iter_mut().enumerate() { + *w = (df * i as f32).sin().powi(2); + } Self { - win: Vec::from_iter((0..len).map(|i| (df * i as f32).sin().powi(2))), + win, + power: 4.0, + nenbw: 1.5, } } } -impl Window for Hann { - fn get(&self) -> &[f32] { - &self.win - } - fn power(&self) -> f32 { - 4.0 - } - fn nenbw(&self) -> f32 { - 1.5 - } - fn len(&self) -> usize { - self.win.len() - } -} - #[derive(Clone, Copy, Debug, PartialEq, Eq)] pub enum Detrend { /// No detrending @@ -56,32 +42,50 @@ pub enum Detrend { /// Power spectral density accumulator and decimator /// /// One stage in [PsdCascade]. -pub struct Psd { +pub struct Psd { hbf: HbfDecCascade, - buf: Vec, - psd: Vec, + buf: heapless::Vec, + out: heapless::Vec, + spectrum: [f32; N], // using only the positive half count: usize, fft: Arc>, - win: Arc, + win: Window, + detrend: Detrend, } -impl Psd { - pub fn new(fft: Arc>, win: Arc, stage_length: usize) -> Self { - let mut hbf = HbfDecCascade::default(); - hbf.set_n(stage_length); +impl Psd { + pub fn new(fft: Arc>, win: Window) -> Self { + let hbf = HbfDecCascade::default(); + assert_eq!(N & 1, 0); + assert_eq!(N, fft.len()); // check fft and decimation block size compatibility - assert!(hbf.block_size().0 <= fft.len() / 2); // needed for processing and dropping blocks - assert!(fft.len() >= 2); // Nyquist and DC distinction + assert!(hbf.block_size().0 <= N / 2); // needed for processing and dropping blocks + assert!(hbf.block_size().1 >= N / 2); // needed for processing and dropping blocks + assert!(N >= 2); // Nyquist and DC distinction Self { hbf, - buf: vec![], - psd: vec![], + buf: heapless::Vec::new(), + out: heapless::Vec::new(), + spectrum: [0.0; N], count: 0, fft, win, + detrend: Detrend::None, } } + pub fn detrend(mut self, d: Detrend) -> Self { + self.detrend = d; + self + } + + pub fn stage_length(mut self, n: usize) -> Self { + self.hbf.set_n(n); + self + } +} + +pub trait Stage { /// Process items /// /// Unusde items are buffered. @@ -95,66 +99,80 @@ impl Psd { /// /// # Returns /// decimated output - pub fn process(&mut self, x: &[f32], detrend: Detrend) -> Vec { - self.buf.extend_from_slice(x); - let n = self.fft.len() / 2; + fn process(&mut self, x: &[f32]) -> &[f32]; + /// Return the positive frequency half of the spectrum + fn spectrum(&self) -> &[f32]; + /// PSD normalization factor + /// + /// one-sided + fn gain(&self) -> f32; + fn count(&self) -> usize; +} - // fft with n/2 overlap - let m = self - .buf - .windows(2 * n) - .step_by(n) - .map(|block| { - let (mean, slope) = match detrend { - Detrend::None => (0.0, 0.0), - Detrend::Mean => (0.5 * (block[0] + block[2 * n - 1]), 0.0), - Detrend::Linear => { - (block[0], (block[2 * n - 1] - block[0]) / (2 * n - 1) as f32) - } - }; - // apply detrending, window, make complex - let mut p: Vec<_> = block - .iter() - .zip(self.win.get().iter()) - .enumerate() - .map(|(i, (x, w))| Complex::new((x - mean - i as f32 * slope) * w, 0.0)) - .collect(); - // fft in-place - self.fft.process(&mut p); - // convert positive frequency spectrum to power - let p = p[..n].iter().map(|y| y.norm_sqr()); - // accumulate - if self.psd.is_empty() { - self.psd.extend(p); - } else { - // TODO note that this looses accuracy for very large averages - for (psd, p) in self.psd.iter_mut().zip(p) { - *psd += p; - } - } - }) - .count(); - self.count += m; +impl Stage for Psd { + fn process(&mut self, mut x: &[f32]) -> &[f32] { + // assert!(x.len() <= (N / 2) << self.hbf.n()); + self.out.clear(); + let mut c = [Complex::default(); N]; + while !x.is_empty() { + // load + let take = x.len().min(self.buf.capacity() - self.buf.len()); + let (chunk, rest) = x.split_at(take); + x = rest; + self.buf.extend_from_slice(chunk).unwrap(); + if self.buf.len() < N { + break; + } + // compute detrend + let (mean, slope) = match self.detrend { + Detrend::None => (0.0, 0.0), + Detrend::Mean => (0.5 * (self.buf[0] + self.buf[N - 1]), 0.0), + Detrend::Linear => ( + self.buf[0], + (self.buf[N - 1] - self.buf[0]) / (N - 1) as f32, + ), + }; + // apply detrending, window, make complex + for (i, (c, (x, w))) in c + .iter_mut() + .zip(self.buf.iter().zip(self.win.win.iter())) + .enumerate() + { + c.re = (x - mean - i as f32 * slope) * w; + c.im = 0.0; + } + // fft in-place + self.fft.process(&mut c); + // convert positive frequency spectrum to power + // and accumulate + for (c, p) in c[..N / 2].iter().zip(self.spectrum.iter_mut()) { + *p += c.norm_sqr(); + } + self.count += 1; - let mut y = vec![]; - // decimate chunks - for block in self.buf[..m * n].chunks_mut(self.hbf.block_size().1) { - assert!(block.len() >= self.hbf.block_size().0); - let k = self.hbf.process_block(None, block); - y.extend_from_slice(&block[..k]); + // decimate non-overlapping chunks + let (left, right) = self.buf.split_at_mut(N / 2); + let k = self.hbf.process_block(None, left); + self.out.extend_from_slice(&left[..k]).unwrap(); + // drop the overlapped and processed chunks + left.copy_from_slice(right); + self.buf.truncate(N / 2); } - // drop the overlapped and processed chunks - self.buf.drain(..m * n); - y + &self.out } - /// PSD normalization factor - /// - /// one-sided - pub fn gain(&self) -> f32 { + fn spectrum(&self) -> &[f32] { + &self.spectrum[..N / 2] + } + + fn count(&self) -> usize { + self.count + } + + fn gain(&self) -> f32 { // 2 for one-sided // overlap compensated by count - 2.0 * self.win.power() / ((self.count * self.fft.len()) as f32 * self.win.nenbw()) + 2.0 * self.win.power / ((self.count * N) as f32 * self.win.nenbw) } } @@ -163,48 +181,54 @@ impl Psd { /// Infinite averaging /// Incremental updates /// Automatic FFT stage extension -pub struct PsdCascade { - stages: Vec, +pub struct PsdCascade { + stages: Vec>, fft: Arc>, - win: Arc, stage_length: usize, detrend: Detrend, + win: Arc>, } -impl PsdCascade { +impl Default for PsdCascade { /// Create a new Psd instance /// /// fft_size: size of the FFT blocks and the window /// stage_length: number of decimation stages. rate change per stage is 1 << stage_length /// detrend: [Detrend] method - pub fn new(fft_size: usize, stage_length: usize, detrend: Detrend) -> Self { - let fft = FftPlanner::::new().plan_fft_forward(fft_size); - let win = Arc::new(Hann::new(fft_size)); + fn default() -> Self { + let fft = FftPlanner::::new().plan_fft_forward(N); + let win = Arc::new(Window::hann()); Self { stages: vec![], fft, + stage_length: 4, + detrend: Detrend::None, win, - stage_length, - detrend, } } +} + +impl PsdCascade { + pub fn stage_length(mut self, n: usize) -> Self { + self.stage_length = n; + self + } + + pub fn detrend(mut self, d: Detrend) -> Self { + self.detrend = d; + self + } /// Process input items pub fn process(&mut self, x: &[f32]) { let mut x = x; - let mut y: Vec<_>; - let mut i = 0; - while !x.is_empty() { - if i + 1 > self.stages.len() { - self.stages.push(Psd::new( - self.fft.clone(), - self.win.clone(), - self.stage_length, - )); - } - y = self.stages[i].process(x, self.detrend); - x = &y; - i += 1; + x = self.stages.iter_mut().fold(x, |x, stage| stage.process(x)); + if !x.is_empty() { + let mut stage = Psd::new(self.fft.clone(), *self.win) + .stage_length(self.stage_length) + .detrend(self.detrend); + stage.process(x); + self.stages.push(stage); } } @@ -221,7 +245,7 @@ impl PsdCascade { let mut b = vec![]; let mut n = 0; for stage in self.stages.iter().take_while(|s| s.count >= min_count) { - let mut pi = &stage.psd[..]; + let mut pi = stage.spectrum(); let f = stage.fft.len(); // a stage yields frequency bins 0..N/2 ty its nyquist // 0..floor(0.4*N) is its passband if it was preceeded by a decimator @@ -237,7 +261,7 @@ impl PsdCascade { p.truncate(p.len() - f_low); } let g = stage.gain() * (1 << n) as f32; - b.push([p.len(), stage.count, pi.len(), f << n]); + b.push([p.len(), stage.count(), pi.len(), f << n]); p.extend(pi.iter().rev().map(|pi| pi * g)); n += stage.hbf.n(); } @@ -260,7 +284,7 @@ mod test { assert_eq!(crate::HBF_PASSBAND, 0.4); // make uniform noise [-1, 1), ignore the epsilon. - let mut x: Vec = (0..1 << 20) + let x: Vec = (0..1 << 20) .map(|_| rand::random::() * 2.0 - 1.0) .collect(); let xm = x.iter().map(|x| *x as f64).sum::() as f32 / x.len() as f32; @@ -270,27 +294,31 @@ mod test { // variance is 1/3 assert!((xv * 3.0 - 1.0).abs() < 10.0 / (x.len() as f32).sqrt()); - let f = 1 << 9; + const F: usize = 1 << 9; let n = 3; - let mut s = Psd::new( - FftPlanner::new().plan_fft_forward(f), - Arc::new(Hann::new(f)), - n, - ); - let y = s.process(&mut x, Detrend::None); - assert_eq!(y.len(), (x.len() - f / 2) >> n); - let p: Vec<_> = s.psd.iter().map(|p| p * s.gain()).collect(); + let mut s = Psd::<{ 1 << 9 }>::new(FftPlanner::new().plan_fft_forward(F), Window::hann()) + .stage_length(n); + let mut y = vec![]; + for x in x.chunks(F << n) { + y.extend_from_slice(s.process(x)); + } + assert_eq!(y.len(), (x.len() - F / 2) >> n); + let p: Vec<_> = s.spectrum().iter().map(|p| p * s.gain()).collect(); // psd of a stage assert!( p.iter() // 0.5 for one-sided spectrum - .all(|p| (p * 0.5 * 3.0 - 1.0).abs() < 10.0 * (f as f32 / x.len() as f32).sqrt()), + .all(|p| (p * 0.5 * 3.0 - 1.0).abs() < 10.0 / (s.count() as f32).sqrt()), "{:?}", - &p[..3] + &p[..] ); - let mut d = PsdCascade::new(f, n, Detrend::None); - d.process(&x); + let mut d = PsdCascade::::default() + .stage_length(n) + .detrend(Detrend::None); + for x in x.chunks(F << n) { + d.process(x); + } let (mut p, b) = d.get(1); // tweak DC and Nyquist to make checks less code let n = p.len(); From 77162ec59bde8a2c629d7e9c6e38f471b07ddc0a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Robert=20J=C3=B6rdens?= Date: Fri, 1 Sep 2023 15:26:12 +0200 Subject: [PATCH 18/39] header rework --- src/de/frame.rs | 18 ++---------------- src/loss.rs | 8 ++++---- 2 files changed, 6 insertions(+), 20 deletions(-) diff --git a/src/de/frame.rs b/src/de/frame.rs index 99ec5a0..6eaf0ac 100644 --- a/src/de/frame.rs +++ b/src/de/frame.rs @@ -10,7 +10,7 @@ const MAGIC_WORD: [u8; 2] = [0x7b, 0x05]; const HEADER_SIZE: usize = 8; #[derive(Debug, Copy, Clone, PartialEq, Eq, PartialOrd, Ord)] -struct Header { +pub struct Header { // The format code associated with the stream binary data. pub format: Format, @@ -42,25 +42,11 @@ impl Header { /// A single stream frame contains multiple batches of data. // #[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord)] pub struct Frame { - header: Header, + pub header: Header, pub data: Box, } impl Frame { - /// Get the format code of the current frame. - pub fn format(&self) -> Format { - self.header.format - } - - /// Get the sequence number of the first batch in the frame. - pub fn seq(&self) -> u32 { - self.header.seq - } - - pub fn batches(&self) -> usize { - self.header.batches as _ - } - /// Parse a stream frame from a single UDP packet. pub fn from_bytes(input: &[u8]) -> Result { let header = Header::parse(input)?; diff --git a/src/loss.rs b/src/loss.rs index 974b015..dce3914 100644 --- a/src/loss.rs +++ b/src/loss.rs @@ -9,20 +9,20 @@ pub struct Loss { impl Loss { pub fn update(&mut self, frame: &Frame) { - self.received += frame.batches() as u64; + self.received += frame.header.batches as u64; if let Some(seq) = self.seq { - let missing = frame.seq().wrapping_sub(seq) as u64; + let missing = frame.header.seq.wrapping_sub(seq) as u64; self.dropped += missing; if missing > 0 { log::warn!( "Lost {} batches: {:#08X} -> {:#08X}", missing, seq, - frame.seq(), + frame.header.seq, ); } } - self.seq = Some(frame.seq().wrapping_add(frame.batches() as _)); + self.seq = Some(frame.header.seq.wrapping_add(frame.header.batches as _)); } pub fn analyze(&self) { From a74bf3955d89179587cc90cac6f5b8c52851840e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Robert=20J=C3=B6rdens?= Date: Fri, 1 Sep 2023 16:12:14 +0200 Subject: [PATCH 19/39] drain initial impulse response from decimator --- src/bin/main.rs | 6 +-- src/bin/stream_test.rs | 2 +- src/hbf.rs | 99 ++++++++++++++++++++++++++++++++++-------- src/psd.rs | 12 +++-- 4 files changed, 94 insertions(+), 25 deletions(-) diff --git a/src/bin/main.rs b/src/bin/main.rs index 9b10dec..84263af 100644 --- a/src/bin/main.rs +++ b/src/bin/main.rs @@ -55,7 +55,7 @@ fn main() -> Result<()> { let mut loss = Loss::default(); let mut dec: Vec<_> = (0..4) .map(|_| { - PsdCascade::<{ 1 << 8 }>::default() + PsdCascade::<{ 1 << 9 }>::default() .stage_length(3) .detrend(Detrend::Mean) }) @@ -71,7 +71,7 @@ fn main() -> Result<()> { Ok(Cmd::Reset) => { dec = (0..4) .map(|_| { - PsdCascade::<{ 1 << 8 }>::default() + PsdCascade::<{ 1 << 9 }>::default() .stage_length(3) .detrend(Detrend::None) }) @@ -114,7 +114,7 @@ fn main() -> Result<()> { f.iter() .zip(p.iter()) .rev() - .skip(2) // DC and first bin + .skip(1) // DC .map(|(f, p)| [f.log10() as f64, 10.0 * p.log10() as f64]) .collect(), ) diff --git a/src/bin/stream_test.rs b/src/bin/stream_test.rs index 1d0de89..45910cc 100644 --- a/src/bin/stream_test.rs +++ b/src/bin/stream_test.rs @@ -38,7 +38,7 @@ fn main() -> Result<()> { let mut dec: Vec<_> = (0..4) .map(|_| { - PsdCascade::<{ 1 << 8 }>::default() + PsdCascade::<{ 1 << 9 }>::default() .stage_length(3) .detrend(Detrend::Mean) }) diff --git a/src/hbf.rs b/src/hbf.rs index 4cf3780..537cbda 100644 --- a/src/hbf.rs +++ b/src/hbf.rs @@ -45,14 +45,17 @@ pub trait Filter { /// The granularity is also the rate change in the case of interpolation/decimation filters. fn block_size(&self) -> (usize, usize); + /// Finite impulse response length in numer of output items + /// Get this many to drain all previous memory + fn response_length(&self) -> usize; + // TODO: process items with automatic blocks // fn process(&mut self, x: Option<&[Self::Item]>, y: &mut [Self::Item]) -> usize {} } /// Symmetric FIR filter prototype. /// -/// Center tap assumed to be 0.5 -/// DSP taps 2*M + 1 +/// DSP taps 2*M /// /// M: number of taps /// N: state size: N = 2*M - 1 + {input/output}.len() @@ -110,10 +113,16 @@ impl<'a, const M: usize, const N: usize> HbfDec<'a, M, N> { impl<'a, const M: usize, const N: usize> Filter for HbfDec<'a, M, N> { type Item = f32; + #[inline] fn block_size(&self) -> (usize, usize) { (2, 2 * (N - (2 * M - 1))) } + #[inline] + fn response_length(&self) -> usize { + 2 * M - 1 + } + fn process_block(&mut self, x: Option<&[Self::Item]>, y: &mut [Self::Item]) -> usize { let x = x.unwrap_or(y); debug_assert_eq!(x.len() & 1, 0); @@ -164,10 +173,16 @@ impl<'a, const M: usize, const N: usize> HbfInt<'a, M, N> { impl<'a, const M: usize, const N: usize> Filter for HbfInt<'a, M, N> { type Item = f32; + #[inline] fn block_size(&self) -> (usize, usize) { (2, 2 * (N - (2 * M - 1))) } + #[inline] + fn response_length(&self) -> usize { + 4 * M - 1 + } + fn process_block(&mut self, x: Option<&[Self::Item]>, y: &mut [Self::Item]) -> usize { debug_assert_eq!(y.len() & 1, 0); let k = y.len() / 2; @@ -248,22 +263,22 @@ pub const HBF_CASCADE_BLOCK: usize = 1 << 8; pub struct HbfDecCascade { n: usize, stages: ( - HbfDec<'static, { HBF_TAPS.3.len() }, { 2 * HBF_TAPS.3.len() - 1 + HBF_CASCADE_BLOCK * 8 }>, - HbfDec<'static, { HBF_TAPS.2.len() }, { 2 * HBF_TAPS.2.len() - 1 + HBF_CASCADE_BLOCK * 4 }>, - HbfDec<'static, { HBF_TAPS.1.len() }, { 2 * HBF_TAPS.1.len() - 1 + HBF_CASCADE_BLOCK * 2 }>, HbfDec<'static, { HBF_TAPS.0.len() }, { 2 * HBF_TAPS.0.len() - 1 + HBF_CASCADE_BLOCK }>, + HbfDec<'static, { HBF_TAPS.1.len() }, { 2 * HBF_TAPS.1.len() - 1 + HBF_CASCADE_BLOCK * 2 }>, + HbfDec<'static, { HBF_TAPS.2.len() }, { 2 * HBF_TAPS.2.len() - 1 + HBF_CASCADE_BLOCK * 4 }>, + HbfDec<'static, { HBF_TAPS.3.len() }, { 2 * HBF_TAPS.3.len() - 1 + HBF_CASCADE_BLOCK * 8 }>, ), } impl Default for HbfDecCascade { fn default() -> Self { Self { - n: 4, + n: 0, stages: ( - HbfDec::new(&HBF_TAPS.3), - HbfDec::new(&HBF_TAPS.2), - HbfDec::new(&HBF_TAPS.1), HbfDec::new(&HBF_TAPS.0), + HbfDec::new(&HBF_TAPS.1), + HbfDec::new(&HBF_TAPS.2), + HbfDec::new(&HBF_TAPS.3), ), } } @@ -283,19 +298,42 @@ impl HbfDecCascade { impl Filter for HbfDecCascade { type Item = f32; + #[inline] fn block_size(&self) -> (usize, usize) { ( 1 << self.n, match self.n { 0 => usize::MAX, - 1 => self.stages.3.block_size().1, - 2 => self.stages.2.block_size().1, - 3 => self.stages.1.block_size().1, - _ => self.stages.0.block_size().1, + 1 => self.stages.0.block_size().1, + 2 => self.stages.1.block_size().1, + 3 => self.stages.2.block_size().1, + _ => self.stages.3.block_size().1, }, ) } + #[inline] + fn response_length(&self) -> usize { + let mut n = 0; + if self.n > 0 { + n *= 2; + n += self.stages.0.response_length(); + } + if self.n > 1 { + n *= 2; + n += self.stages.1.response_length(); + } + if self.n > 2 { + n *= 2; + n += self.stages.2.response_length(); + } + if self.n > 3 { + n *= 2; + n += self.stages.3.response_length(); + } + n + } + fn process_block(&mut self, x: Option<&[f32]>, y: &mut [f32]) -> usize { if x.is_some() { unimplemented!(); // TODO: pair of intermediate buffers @@ -303,16 +341,16 @@ impl Filter for HbfDecCascade { let mut n = y.len(); if self.n > 3 { - n = self.stages.0.process_block(None, &mut y[..n]); + n = self.stages.3.process_block(None, &mut y[..n]); } if self.n > 2 { - n = self.stages.1.process_block(None, &mut y[..n]); + n = self.stages.2.process_block(None, &mut y[..n]); } if self.n > 1 { - n = self.stages.2.process_block(None, &mut y[..n]); + n = self.stages.1.process_block(None, &mut y[..n]); } if self.n > 0 { - n = self.stages.3.process_block(None, &mut y[..n]); + n = self.stages.0.process_block(None, &mut y[..n]); } debug_assert_eq!(n, y.len() >> self.n); n @@ -362,6 +400,7 @@ impl HbfIntCascade { impl Filter for HbfIntCascade { type Item = f32; + #[inline] fn block_size(&self) -> (usize, usize) { ( 1 << self.n, @@ -375,6 +414,28 @@ impl Filter for HbfIntCascade { ) } + #[inline] + fn response_length(&self) -> usize { + let mut n = 0; + if self.n > 0 { + n *= 2; + n += self.stages.0.response_length(); + } + if self.n > 1 { + n *= 2; + n += self.stages.1.response_length(); + } + if self.n > 2 { + n *= 2; + n += self.stages.2.response_length(); + } + if self.n > 3 { + n *= 2; + n += self.stages.3.response_length(); + } + n + } + fn process_block(&mut self, x: Option<&[f32]>, y: &mut [f32]) -> usize { if x.is_some() { unimplemented!(); // TODO: one intermediate buffer and `y` @@ -423,6 +484,7 @@ mod test { #[test] fn decim() { let mut h = HbfDecCascade::default(); + h.set_n(4); assert_eq!(h.block_size(), (1 << h.n(), HBF_CASCADE_BLOCK << h.n())); let mut x: Vec<_> = (0..2 << h.n()).map(|i| i as f32).collect(); let n = h.process_block(None, &mut x); @@ -434,10 +496,11 @@ mod test { let mut h = HbfIntCascade::default(); h.set_n(4); assert_eq!(h.block_size(), (1 << h.n(), HBF_CASCADE_BLOCK << h.n())); - let mut x = [0.0; 37 << 4]; + let mut x = vec![0.0; h.response_length() + 1]; x[0] = 1.0; let n = h.process_block(None, &mut x); println!("{:?}", &x[..n]); // interpolator impulse response + assert_eq!(x[x.len() - 1], 0.0); let g = (1 << h.n()) as f32; let mut y = Vec::from_iter(x[..n].iter().map(|&x| Complex { re: x / g, im: 0.0 })); diff --git a/src/psd.rs b/src/psd.rs index 5898492..9456583 100644 --- a/src/psd.rs +++ b/src/psd.rs @@ -51,6 +51,7 @@ pub struct Psd { fft: Arc>, win: Window, detrend: Detrend, + drain: usize, } impl Psd { @@ -71,6 +72,7 @@ impl Psd { fft, win, detrend: Detrend::None, + drain: 0, } } @@ -81,6 +83,7 @@ impl Psd { pub fn stage_length(mut self, n: usize) -> Self { self.hbf.set_n(n); + self.drain = self.hbf.response_length(); self } } @@ -111,7 +114,6 @@ pub trait Stage { impl Stage for Psd { fn process(&mut self, mut x: &[f32]) -> &[f32] { - // assert!(x.len() <= (N / 2) << self.hbf.n()); self.out.clear(); let mut c = [Complex::default(); N]; while !x.is_empty() { @@ -152,7 +154,9 @@ impl Stage for Psd { // decimate non-overlapping chunks let (left, right) = self.buf.split_at_mut(N / 2); - let k = self.hbf.process_block(None, left); + let mut k = self.hbf.process_block(None, left); + // drain decimator impulse response to initial state (zeros) + (k, self.drain) = (k.saturating_sub(self.drain), self.drain.saturating_sub(k)); self.out.extend_from_slice(&left[..k]).unwrap(); // drop the overlapped and processed chunks left.copy_from_slice(right); @@ -302,7 +306,9 @@ mod test { for x in x.chunks(F << n) { y.extend_from_slice(s.process(x)); } - assert_eq!(y.len(), (x.len() - F / 2) >> n); + let mut hbf = HbfDecCascade::default(); + hbf.set_n(n); + assert_eq!(y.len(), ((x.len() - F / 2) >> n) - hbf.response_length()); let p: Vec<_> = s.spectrum().iter().map(|p| p * s.gain()).collect(); // psd of a stage assert!( From f18db2f2658525515e87a9ddd8a69d9676cae5c4 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Robert=20J=C3=B6rdens?= Date: Fri, 1 Sep 2023 16:19:26 +0200 Subject: [PATCH 20/39] rm heapless --- Cargo.toml | 1 - src/psd.rs | 26 +++++++++++++++----------- 2 files changed, 15 insertions(+), 12 deletions(-) diff --git a/Cargo.toml b/Cargo.toml index bbdabdb..52072df 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -29,7 +29,6 @@ socket2 = "0.5.3" idsp = "0.10.0" rustfft = "6.1.0" rand = "0.8.5" -heapless = "0.7.16" #[build-dependencies] #npm_rs = "0.2.1" diff --git a/src/psd.rs b/src/psd.rs index 9456583..7fbe5e0 100644 --- a/src/psd.rs +++ b/src/psd.rs @@ -44,8 +44,9 @@ pub enum Detrend { /// One stage in [PsdCascade]. pub struct Psd { hbf: HbfDecCascade, - buf: heapless::Vec, - out: heapless::Vec, + buf: [f32; N], + idx: usize, + out: [f32; N], spectrum: [f32; N], // using only the positive half count: usize, fft: Arc>, @@ -65,8 +66,9 @@ impl Psd { assert!(N >= 2); // Nyquist and DC distinction Self { hbf, - buf: heapless::Vec::new(), - out: heapless::Vec::new(), + buf: [0.0; N], + idx: 0, + out: [0.0; N], spectrum: [0.0; N], count: 0, fft, @@ -114,15 +116,16 @@ pub trait Stage { impl Stage for Psd { fn process(&mut self, mut x: &[f32]) -> &[f32] { - self.out.clear(); let mut c = [Complex::default(); N]; + let mut n = 0; while !x.is_empty() { // load - let take = x.len().min(self.buf.capacity() - self.buf.len()); + let take = x.len().min(self.buf.len() - self.idx); let (chunk, rest) = x.split_at(take); x = rest; - self.buf.extend_from_slice(chunk).unwrap(); - if self.buf.len() < N { + self.buf[self.idx..][..take].copy_from_slice(chunk); + self.idx += take; + if self.idx < N { break; } // compute detrend @@ -157,12 +160,13 @@ impl Stage for Psd { let mut k = self.hbf.process_block(None, left); // drain decimator impulse response to initial state (zeros) (k, self.drain) = (k.saturating_sub(self.drain), self.drain.saturating_sub(k)); - self.out.extend_from_slice(&left[..k]).unwrap(); + self.out[n..][..k].copy_from_slice(&left[..k]); + n += k; // drop the overlapped and processed chunks left.copy_from_slice(right); - self.buf.truncate(N / 2); + self.idx = N / 2; } - &self.out + &self.out[..n] } fn spectrum(&self) -> &[f32] { From 09a2c39a2da02413d7f357a20195ef762feb13a7 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Robert=20J=C3=B6rdens?= Date: Fri, 1 Sep 2023 16:19:54 +0200 Subject: [PATCH 21/39] add Cargo.lock --- .gitignore | 2 +- Cargo.lock | 2606 ++++++++++++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 2607 insertions(+), 1 deletion(-) create mode 100644 Cargo.lock diff --git a/.gitignore b/.gitignore index cd20cd2..8b8a38d 100644 --- a/.gitignore +++ b/.gitignore @@ -4,7 +4,7 @@ # Remove Cargo.lock from gitignore if creating an executable, leave it for libraries # More information here https://doc.rust-lang.org/cargo/guide/cargo-toml-vs-cargo-lock.html -Cargo.lock +# Cargo.lock # These are backup files generated by rustfmt **/*.rs.bk diff --git a/Cargo.lock b/Cargo.lock new file mode 100644 index 0000000..7e9306e --- /dev/null +++ b/Cargo.lock @@ -0,0 +1,2606 @@ +# This file is automatically @generated by Cargo. +# It is not intended for manual editing. +version = 3 + +[[package]] +name = "ab_glyph" +version = "0.2.21" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5110f1c78cf582855d895ecd0746b653db010cec6d9f5575293f27934d980a39" +dependencies = [ + "ab_glyph_rasterizer", + "owned_ttf_parser", +] + +[[package]] +name = "ab_glyph_rasterizer" +version = "0.1.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c71b1793ee61086797f5c80b6efa2b8ffa6d5dd703f118545808a7f2e27f7046" + +[[package]] +name = "adler" +version = "1.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f26201604c87b1e01bd3d98f8d5d9a8fcbb815e8cedb41ffccbeb4bf593a35fe" + +[[package]] +name = "ahash" +version = "0.8.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2c99f64d1e06488f620f932677e24bc6e2897582980441ae90a671415bd7ec2f" +dependencies = [ + "cfg-if", + "once_cell", + "version_check", +] + +[[package]] +name = "aho-corasick" +version = "1.0.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0c378d78423fdad8089616f827526ee33c19f2fddbd5de1629152c9593ba4783" +dependencies = [ + "memchr", +] + +[[package]] +name = "android-activity" +version = "0.4.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "64529721f27c2314ced0890ce45e469574a73e5e6fdd6e9da1860eb29285f5e0" +dependencies = [ + "android-properties", + "bitflags 1.3.2", + "cc", + "jni-sys", + "libc", + "log", + "ndk", + "ndk-context", + "ndk-sys", + "num_enum 0.6.1", +] + +[[package]] +name = "android-properties" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fc7eb209b1518d6bb87b283c20095f5228ecda460da70b44f0802523dea6da04" + +[[package]] +name = "anstream" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b1f58811cfac344940f1a400b6e6231ce35171f614f26439e80f8c1465c5cc0c" +dependencies = [ + "anstyle", + "anstyle-parse", + "anstyle-query", + "anstyle-wincon", + "colorchoice", + "utf8parse", +] + +[[package]] +name = "anstyle" +version = "1.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "15c4c2c83f81532e5845a733998b6971faca23490340a418e9b72a3ec9de12ea" + +[[package]] +name = "anstyle-parse" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "938874ff5980b03a87c5524b3ae5b59cf99b1d6bc836848df7bc5ada9643c333" +dependencies = [ + "utf8parse", +] + +[[package]] +name = "anstyle-query" +version = "1.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5ca11d4be1bab0c8bc8734a9aa7bf4ee8316d462a08c6ac5052f888fef5b494b" +dependencies = [ + "windows-sys 0.48.0", +] + +[[package]] +name = "anstyle-wincon" +version = "2.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "58f54d10c6dfa51283a066ceab3ec1ab78d13fae00aa49243a45e4571fb79dfd" +dependencies = [ + "anstyle", + "windows-sys 0.48.0", +] + +[[package]] +name = "anyhow" +version = "1.0.75" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a4668cab20f66d8d020e1fbc0ebe47217433c1b6c8f2040faf858554e394ace6" + +[[package]] +name = "arboard" +version = "3.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ac57f2b058a76363e357c056e4f74f1945bf734d37b8b3ef49066c4787dde0fc" +dependencies = [ + "clipboard-win", + "log", + "objc", + "objc-foundation", + "objc_id", + "parking_lot", + "thiserror", + "winapi", + "x11rb", +] + +[[package]] +name = "arrayref" +version = "0.3.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6b4930d2cb77ce62f89ee5d5289b4ac049559b1c45539271f5ed4fdc7db34545" + +[[package]] +name = "arrayvec" +version = "0.7.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "96d30a06541fbafbc7f82ed10c06164cfbd2c401138f6addd8404629c4b16711" + +[[package]] +name = "async-attributes" +version = "1.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a3203e79f4dd9bdda415ed03cf14dae5a2bf775c683a00f94e9cd1faf0f596e5" +dependencies = [ + "quote", + "syn 1.0.109", +] + +[[package]] +name = "async-channel" +version = "1.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "81953c529336010edd6d8e358f886d9581267795c61b19475b71314bffa46d35" +dependencies = [ + "concurrent-queue", + "event-listener", + "futures-core", +] + +[[package]] +name = "async-executor" +version = "1.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6fa3dc5f2a8564f07759c008b9109dc0d39de92a88d5588b8a5036d286383afb" +dependencies = [ + "async-lock", + "async-task", + "concurrent-queue", + "fastrand", + "futures-lite", + "slab", +] + +[[package]] +name = "async-global-executor" +version = "2.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f1b6f5d7df27bd294849f8eec66ecfc63d11814df7a4f5d74168a2394467b776" +dependencies = [ + "async-channel", + "async-executor", + "async-io", + "async-lock", + "blocking", + "futures-lite", + "once_cell", +] + +[[package]] +name = "async-io" +version = "1.13.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0fc5b45d93ef0529756f812ca52e44c221b35341892d3dcc34132ac02f3dd2af" +dependencies = [ + "async-lock", + "autocfg", + "cfg-if", + "concurrent-queue", + "futures-lite", + "log", + "parking", + "polling", + "rustix 0.37.23", + "slab", + "socket2 0.4.9", + "waker-fn", +] + +[[package]] +name = "async-lock" +version = "2.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "287272293e9d8c41773cec55e365490fe034813a2f172f502d6ddcf75b2f582b" +dependencies = [ + "event-listener", +] + +[[package]] +name = "async-std" +version = "1.12.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "62565bb4402e926b29953c785397c6dc0391b7b446e45008b0049eb43cec6f5d" +dependencies = [ + "async-attributes", + "async-channel", + "async-global-executor", + "async-io", + "async-lock", + "crossbeam-utils", + "futures-channel", + "futures-core", + "futures-io", + "futures-lite", + "gloo-timers", + "kv-log-macro", + "log", + "memchr", + "once_cell", + "pin-project-lite", + "pin-utils", + "slab", + "wasm-bindgen-futures", +] + +[[package]] +name = "async-task" +version = "4.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ecc7ab41815b3c653ccd2978ec3255c81349336702dfdf62ee6f7069b12a3aae" + +[[package]] +name = "atomic-waker" +version = "1.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1181e1e0d1fce796a03db1ae795d67167da795f9cf4a39c37589e85ef57f26d3" + +[[package]] +name = "atomic_refcell" +version = "0.1.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "112ef6b3f6cb3cb6fc5b6b494ef7a848492cff1ab0ef4de10b0f7d572861c905" + +[[package]] +name = "autocfg" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d468802bab17cbc0cc575e9b053f41e72aa36bfa6b7f55e3529ffa43161b97fa" + +[[package]] +name = "bitflags" +version = "1.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bef38d45163c2f1dde094a7dfd33ccf595c92905c8f8f4fdc18d06fb1037718a" + +[[package]] +name = "bitflags" +version = "2.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b4682ae6287fcf752ecaabbfcc7b6f9b72aa33933dc23a554d853aea8eea8635" + +[[package]] +name = "block" +version = "0.1.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0d8c1fef690941d3e7788d328517591fecc684c084084702d6ff1641e993699a" + +[[package]] +name = "block-sys" +version = "0.1.0-beta.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0fa55741ee90902547802152aaf3f8e5248aab7e21468089560d4c8840561146" +dependencies = [ + "objc-sys", +] + +[[package]] +name = "block2" +version = "0.2.0-alpha.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8dd9e63c1744f755c2f60332b88de39d341e5e86239014ad839bd71c106dec42" +dependencies = [ + "block-sys", + "objc2-encode", +] + +[[package]] +name = "blocking" +version = "1.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "77231a1c8f801696fc0123ec6150ce92cffb8e164a02afb9c8ddee0e9b65ad65" +dependencies = [ + "async-channel", + "async-lock", + "async-task", + "atomic-waker", + "fastrand", + "futures-lite", + "log", +] + +[[package]] +name = "bumpalo" +version = "3.13.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a3e2c3daef883ecc1b5d58c15adae93470a91d425f3532ba1695849656af3fc1" + +[[package]] +name = "bytemuck" +version = "1.13.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "17febce684fd15d89027105661fec94afb475cb995fbc59d2865198446ba2eea" +dependencies = [ + "bytemuck_derive", +] + +[[package]] +name = "bytemuck_derive" +version = "1.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fdde5c9cd29ebd706ce1b35600920a33550e402fc998a2e53ad3b42c3c47a192" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.29", +] + +[[package]] +name = "byteorder" +version = "1.4.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "14c189c53d098945499cdfa7ecc63567cf3886b3332b312a5b4585d8d3a6a610" + +[[package]] +name = "bytes" +version = "1.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "89b2fd2a0dcf38d7971e2194b6b6eebab45ae01067456a7fd93d5547a61b70be" + +[[package]] +name = "calloop" +version = "0.10.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "52e0d00eb1ea24371a97d2da6201c6747a633dc6dc1988ef503403b4c59504a8" +dependencies = [ + "bitflags 1.3.2", + "log", + "nix 0.25.1", + "slotmap", + "thiserror", + "vec_map", +] + +[[package]] +name = "cc" +version = "1.0.83" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f1174fb0b6ec23863f8b971027804a42614e347eafb0a95bf0b12cdae21fc4d0" +dependencies = [ + "jobserver", + "libc", +] + +[[package]] +name = "cesu8" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6d43a04d8753f35258c91f8ec639f792891f748a1edbd759cf1dcea3382ad83c" + +[[package]] +name = "cfg-if" +version = "1.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "baf1de4339761588bc0619e3cbc0120ee582ebb74b53b4efbf79117bd2da40fd" + +[[package]] +name = "cfg_aliases" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fd16c4719339c4530435d38e511904438d07cce7950afa3718a84ac36c10e89e" + +[[package]] +name = "cgl" +version = "0.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0ced0551234e87afee12411d535648dd89d2e7f34c78b753395567aff3d447ff" +dependencies = [ + "libc", +] + +[[package]] +name = "clap" +version = "4.4.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6a13b88d2c62ff462f88e4a121f17a82c1af05693a2f192b5c38d14de73c19f6" +dependencies = [ + "clap_builder", + "clap_derive", +] + +[[package]] +name = "clap_builder" +version = "4.4.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2bb9faaa7c2ef94b2743a21f5a29e6f0010dff4caa69ac8e9d6cf8b6fa74da08" +dependencies = [ + "anstream", + "anstyle", + "clap_lex", + "strsim", +] + +[[package]] +name = "clap_derive" +version = "4.4.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0862016ff20d69b84ef8247369fabf5c008a7417002411897d40ee1f4532b873" +dependencies = [ + "heck", + "proc-macro2", + "quote", + "syn 2.0.29", +] + +[[package]] +name = "clap_lex" +version = "0.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cd7cc57abe963c6d3b9d8be5b06ba7c8957a930305ca90304f24ef040aa6f961" + +[[package]] +name = "clipboard-win" +version = "4.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7191c27c2357d9b7ef96baac1773290d4ca63b24205b82a3fd8a0637afcf0362" +dependencies = [ + "error-code", + "str-buf", + "winapi", +] + +[[package]] +name = "cocoa" +version = "0.24.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f425db7937052c684daec3bd6375c8abe2d146dca4b8b143d6db777c39138f3a" +dependencies = [ + "bitflags 1.3.2", + "block", + "cocoa-foundation", + "core-foundation", + "core-graphics", + "foreign-types", + "libc", + "objc", +] + +[[package]] +name = "cocoa-foundation" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "931d3837c286f56e3c58423ce4eba12d08db2374461a785c86f672b08b5650d6" +dependencies = [ + "bitflags 1.3.2", + "block", + "core-foundation", + "core-graphics-types", + "foreign-types", + "libc", + "objc", +] + +[[package]] +name = "color_quant" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3d7b894f5411737b7867f4827955924d7c254fc9f4d91a6aad6b097804b1018b" + +[[package]] +name = "colorchoice" +version = "1.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "acbf1af155f9b9ef647e42cdc158db4b64a1b61f743629225fde6f3e0be2a7c7" + +[[package]] +name = "combine" +version = "4.6.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "35ed6e9d84f0b51a7f52daf1c7d71dd136fd7a3f41a8462b8cdb8c78d920fad4" +dependencies = [ + "bytes", + "memchr", +] + +[[package]] +name = "concurrent-queue" +version = "2.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "62ec6771ecfa0762d24683ee5a32ad78487a3d3afdc0fb8cae19d2c5deb50b7c" +dependencies = [ + "crossbeam-utils", +] + +[[package]] +name = "core-foundation" +version = "0.9.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "194a7a9e6de53fa55116934067c844d9d749312f75c6f6d0980e8c252f8c2146" +dependencies = [ + "core-foundation-sys", + "libc", +] + +[[package]] +name = "core-foundation-sys" +version = "0.8.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e496a50fda8aacccc86d7529e2c1e0892dbd0f898a6b5645b5561b89c3210efa" + +[[package]] +name = "core-graphics" +version = "0.22.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2581bbab3b8ffc6fcbd550bf46c355135d16e9ff2a6ea032ad6b9bf1d7efe4fb" +dependencies = [ + "bitflags 1.3.2", + "core-foundation", + "core-graphics-types", + "foreign-types", + "libc", +] + +[[package]] +name = "core-graphics-types" +version = "0.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2bb142d41022986c1d8ff29103a1411c8a3dfad3552f87a4f8dc50d61d4f4e33" +dependencies = [ + "bitflags 1.3.2", + "core-foundation", + "libc", +] + +[[package]] +name = "crc32fast" +version = "1.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b540bd8bc810d3885c6ea91e2018302f68baba2129ab3e88f32389ee9370880d" +dependencies = [ + "cfg-if", +] + +[[package]] +name = "crossbeam-utils" +version = "0.8.16" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5a22b2d63d4d1dc0b7f1b6b2747dd0088008a9be28b6ddf0b1e7d335e3037294" +dependencies = [ + "cfg-if", +] + +[[package]] +name = "dispatch" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bd0c93bb4b0c6d9b77f4435b0ae98c24d17f1c45b2ff844c6151a07256ca923b" + +[[package]] +name = "dlib" +version = "0.5.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "330c60081dcc4c72131f8eb70510f1ac07223e5d4163db481a04a0befcffa412" +dependencies = [ + "libloading 0.8.0", +] + +[[package]] +name = "downcast-rs" +version = "1.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9ea835d29036a4087793836fa931b08837ad5e957da9e23886b29586fb9b6650" + +[[package]] +name = "ecolor" +version = "0.22.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2e479a7fa3f23d4e794f8b2f8b3568dd4e47886ad1b12c9c095e141cb591eb63" +dependencies = [ + "bytemuck", +] + +[[package]] +name = "eframe" +version = "0.22.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bf4596583a2c680c55b6feaa748f74890c4f9cb9c7cb69d6117110444cb65b2f" +dependencies = [ + "bytemuck", + "cocoa", + "egui", + "egui-winit", + "egui_glow", + "glow", + "glutin", + "glutin-winit", + "image", + "js-sys", + "log", + "objc", + "percent-encoding", + "raw-window-handle", + "thiserror", + "wasm-bindgen", + "wasm-bindgen-futures", + "web-sys", + "winapi", + "winit", +] + +[[package]] +name = "egui" +version = "0.22.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a3aef8ec3ae1b772f340170c65bf27d5b8c28f543a0116c844d2ac08d01123e7" +dependencies = [ + "ahash", + "epaint", + "log", + "nohash-hasher", +] + +[[package]] +name = "egui-winit" +version = "0.22.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4a49155fd4a0a4fb21224407a91de0030847972ef90fc64edb63621caea61cb2" +dependencies = [ + "arboard", + "egui", + "instant", + "log", + "raw-window-handle", + "smithay-clipboard", + "webbrowser", + "winit", +] + +[[package]] +name = "egui_glow" +version = "0.22.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1f8c2752cdf1b0ef5fcda59a898cacabad974d4f5880e92a420b2c917022da64" +dependencies = [ + "bytemuck", + "egui", + "glow", + "log", + "memoffset", + "wasm-bindgen", + "web-sys", +] + +[[package]] +name = "emath" +version = "0.22.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3857d743a6e0741cdd60b622a74c7a36ea75f5f8f11b793b41d905d2c9721a4b" +dependencies = [ + "bytemuck", +] + +[[package]] +name = "env_logger" +version = "0.10.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "85cdab6a89accf66733ad5a1693a4dcced6aeff64602b634530dd73c1f3ee9f0" +dependencies = [ + "humantime", + "is-terminal", + "log", + "regex", + "termcolor", +] + +[[package]] +name = "epaint" +version = "0.22.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "09333964d4d57f40a85338ba3ca5ed4716070ab184dcfed966b35491c5c64f3b" +dependencies = [ + "ab_glyph", + "ahash", + "atomic_refcell", + "bytemuck", + "ecolor", + "emath", + "log", + "nohash-hasher", + "parking_lot", +] + +[[package]] +name = "equivalent" +version = "1.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5443807d6dff69373d433ab9ef5378ad8df50ca6298caf15de6e52e24aaf54d5" + +[[package]] +name = "errno" +version = "0.3.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "136526188508e25c6fef639d7927dfb3e0e3084488bf202267829cf7fc23dbdd" +dependencies = [ + "errno-dragonfly", + "libc", + "windows-sys 0.48.0", +] + +[[package]] +name = "errno-dragonfly" +version = "0.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "aa68f1b12764fab894d2755d2518754e71b4fd80ecfb822714a1206c2aab39bf" +dependencies = [ + "cc", + "libc", +] + +[[package]] +name = "error-code" +version = "2.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "64f18991e7bf11e7ffee451b5318b5c1a73c52d0d0ada6e5a3017c8c1ced6a21" +dependencies = [ + "libc", + "str-buf", +] + +[[package]] +name = "event-listener" +version = "2.5.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0206175f82b8d6bf6652ff7d71a1e27fd2e4efde587fd368662814d6ec1d9ce0" + +[[package]] +name = "fastrand" +version = "1.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e51093e27b0797c359783294ca4f0a911c270184cb10f85783b118614a1501be" +dependencies = [ + "instant", +] + +[[package]] +name = "fdeflate" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d329bdeac514ee06249dabc27877490f17f5d371ec693360768b838e19f3ae10" +dependencies = [ + "simd-adler32", +] + +[[package]] +name = "flate2" +version = "1.0.27" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c6c98ee8095e9d1dcbf2fcc6d95acccb90d1c81db1e44725c6a984b1dbdfb010" +dependencies = [ + "crc32fast", + "miniz_oxide", +] + +[[package]] +name = "foreign-types" +version = "0.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f6f339eb8adc052cd2ca78910fda869aefa38d22d5cb648e6485e4d3fc06f3b1" +dependencies = [ + "foreign-types-shared", +] + +[[package]] +name = "foreign-types-shared" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "00b0228411908ca8685dba7fc2cdd70ec9990a6e753e89b6ac91a84c40fbaf4b" + +[[package]] +name = "form_urlencoded" +version = "1.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a62bc1cf6f830c2ec14a513a9fb124d0a213a629668a4186f329db21fe045652" +dependencies = [ + "percent-encoding", +] + +[[package]] +name = "futures-channel" +version = "0.3.28" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "955518d47e09b25bbebc7a18df10b81f0c766eaf4c4f1cccef2fca5f2a4fb5f2" +dependencies = [ + "futures-core", +] + +[[package]] +name = "futures-core" +version = "0.3.28" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4bca583b7e26f571124fe5b7561d49cb2868d79116cfa0eefce955557c6fee8c" + +[[package]] +name = "futures-io" +version = "0.3.28" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4fff74096e71ed47f8e023204cfd0aa1289cd54ae5430a9523be060cdb849964" + +[[package]] +name = "futures-lite" +version = "1.13.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "49a9d51ce47660b1e808d3c990b4709f2f415d928835a17dfd16991515c46bce" +dependencies = [ + "fastrand", + "futures-core", + "futures-io", + "memchr", + "parking", + "pin-project-lite", + "waker-fn", +] + +[[package]] +name = "gethostname" +version = "0.2.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c1ebd34e35c46e00bb73e81363248d627782724609fe1b6396f553f68fe3862e" +dependencies = [ + "libc", + "winapi", +] + +[[package]] +name = "getrandom" +version = "0.2.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "be4136b2a15dd319360be1c07d9933517ccf0be8f16bf62a3bee4f0d618df427" +dependencies = [ + "cfg-if", + "libc", + "wasi", +] + +[[package]] +name = "gl_generator" +version = "0.14.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1a95dfc23a2b4a9a2f5ab41d194f8bfda3cabec42af4e39f08c339eb2a0c124d" +dependencies = [ + "khronos_api", + "log", + "xml-rs", +] + +[[package]] +name = "gloo-timers" +version = "0.2.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9b995a66bb87bebce9a0f4a95aed01daca4872c050bfcb21653361c03bc35e5c" +dependencies = [ + "futures-channel", + "futures-core", + "js-sys", + "wasm-bindgen", +] + +[[package]] +name = "glow" +version = "0.12.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ca0fe580e4b60a8ab24a868bc08e2f03cbcb20d3d676601fa909386713333728" +dependencies = [ + "js-sys", + "slotmap", + "wasm-bindgen", + "web-sys", +] + +[[package]] +name = "glutin" +version = "0.30.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8fc93b03242719b8ad39fb26ed2b01737144ce7bd4bfc7adadcef806596760fe" +dependencies = [ + "bitflags 1.3.2", + "cfg_aliases", + "cgl", + "core-foundation", + "dispatch", + "glutin_egl_sys", + "glutin_glx_sys", + "glutin_wgl_sys", + "libloading 0.7.4", + "objc2", + "once_cell", + "raw-window-handle", + "wayland-sys 0.30.1", + "windows-sys 0.45.0", + "x11-dl", +] + +[[package]] +name = "glutin-winit" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "629a873fc04062830bfe8f97c03773bcd7b371e23bcc465d0a61448cd1588fa4" +dependencies = [ + "cfg_aliases", + "glutin", + "raw-window-handle", + "winit", +] + +[[package]] +name = "glutin_egl_sys" +version = "0.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "af784eb26c5a68ec85391268e074f0aa618c096eadb5d6330b0911cf34fe57c5" +dependencies = [ + "gl_generator", + "windows-sys 0.45.0", +] + +[[package]] +name = "glutin_glx_sys" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1b53cb5fe568964aa066a3ba91eac5ecbac869fb0842cd0dc9e412434f1a1494" +dependencies = [ + "gl_generator", + "x11-dl", +] + +[[package]] +name = "glutin_wgl_sys" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ef89398e90033fc6bc65e9bd42fd29bbbfd483bda5b56dc5562f455550618165" +dependencies = [ + "gl_generator", +] + +[[package]] +name = "hashbrown" +version = "0.14.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2c6201b9ff9fd90a5a3bac2e56a830d0caa509576f0e503818ee82c181b3437a" + +[[package]] +name = "heck" +version = "0.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "95505c38b4572b2d910cecb0281560f54b440a19336cbbcb27bf6ce6adc6f5a8" + +[[package]] +name = "hermit-abi" +version = "0.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "443144c8cdadd93ebf52ddb4056d257f5b52c04d3c804e657d19eb73fc33668b" + +[[package]] +name = "home" +version = "0.5.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5444c27eef6923071f7ebcc33e3444508466a76f7a2b93da00ed6e19f30c1ddb" +dependencies = [ + "windows-sys 0.48.0", +] + +[[package]] +name = "humantime" +version = "2.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9a3a5bfb195931eeb336b2a7b4d761daec841b97f947d34394601737a7bba5e4" + +[[package]] +name = "idna" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7d20d6b07bfbc108882d88ed8e37d39636dcc260e15e30c45e6ba089610b917c" +dependencies = [ + "unicode-bidi", + "unicode-normalization", +] + +[[package]] +name = "idsp" +version = "0.10.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d86b122ef447608ce36932016201caddb871691596578e3518c69f57acbed006" +dependencies = [ + "num-complex", + "num-traits", + "serde", +] + +[[package]] +name = "image" +version = "0.24.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6f3dfdbdd72063086ff443e297b61695500514b1e41095b6fb9a5ab48a70a711" +dependencies = [ + "bytemuck", + "byteorder", + "color_quant", + "num-rational", + "num-traits", + "png", +] + +[[package]] +name = "indexmap" +version = "2.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d5477fe2230a79769d8dc68e0eabf5437907c0457a5614a9e8dddb67f65eb65d" +dependencies = [ + "equivalent", + "hashbrown", +] + +[[package]] +name = "instant" +version = "0.1.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7a5bbe824c507c5da5956355e86a746d82e0e1464f65d862cc5e71da70e94b2c" +dependencies = [ + "cfg-if", + "js-sys", + "wasm-bindgen", + "web-sys", +] + +[[package]] +name = "io-lifetimes" +version = "1.0.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "eae7b9aee968036d54dce06cebaefd919e4472e753296daccd6d344e3e2df0c2" +dependencies = [ + "hermit-abi", + "libc", + "windows-sys 0.48.0", +] + +[[package]] +name = "is-terminal" +version = "0.4.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cb0889898416213fab133e1d33a0e5858a48177452750691bde3666d0fdbaf8b" +dependencies = [ + "hermit-abi", + "rustix 0.38.11", + "windows-sys 0.48.0", +] + +[[package]] +name = "jni" +version = "0.21.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1a87aa2bb7d2af34197c04845522473242e1aa17c12f4935d5856491a7fb8c97" +dependencies = [ + "cesu8", + "cfg-if", + "combine", + "jni-sys", + "log", + "thiserror", + "walkdir", + "windows-sys 0.45.0", +] + +[[package]] +name = "jni-sys" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8eaf4bc02d17cbdd7ff4c7438cafcdf7fb9a4613313ad11b4f8fefe7d3fa0130" + +[[package]] +name = "jobserver" +version = "0.1.26" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "936cfd212a0155903bcbc060e316fb6cc7cbf2e1907329391ebadc1fe0ce77c2" +dependencies = [ + "libc", +] + +[[package]] +name = "js-sys" +version = "0.3.64" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c5f195fe497f702db0f318b07fdd68edb16955aed830df8363d837542f8f935a" +dependencies = [ + "wasm-bindgen", +] + +[[package]] +name = "khronos_api" +version = "3.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e2db585e1d738fc771bf08a151420d3ed193d9d895a36df7f6f8a9456b911ddc" + +[[package]] +name = "kv-log-macro" +version = "1.0.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0de8b303297635ad57c9f5059fd9cee7a47f8e8daa09df0fcd07dd39fb22977f" +dependencies = [ + "log", +] + +[[package]] +name = "lazy_static" +version = "1.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e2abad23fbc42b3700f2f279844dc832adb2b2eb069b2df918f455c4e18cc646" + +[[package]] +name = "libc" +version = "0.2.147" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b4668fb0ea861c1df094127ac5f1da3409a82116a4ba74fca2e58ef927159bb3" + +[[package]] +name = "libloading" +version = "0.7.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b67380fd3b2fbe7527a606e18729d21c6f3951633d0500574c4dc22d2d638b9f" +dependencies = [ + "cfg-if", + "winapi", +] + +[[package]] +name = "libloading" +version = "0.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d580318f95776505201b28cf98eb1fa5e4be3b689633ba6a3e6cd880ff22d8cb" +dependencies = [ + "cfg-if", + "windows-sys 0.48.0", +] + +[[package]] +name = "libm" +version = "0.2.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f7012b1bbb0719e1097c47611d3898568c546d597c2e74d66f6087edd5233ff4" + +[[package]] +name = "linux-raw-sys" +version = "0.3.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ef53942eb7bf7ff43a617b3e2c1c4a5ecf5944a7c1bc12d7ee39bbb15e5c1519" + +[[package]] +name = "linux-raw-sys" +version = "0.4.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "57bcfdad1b858c2db7c38303a6d2ad4dfaf5eb53dfeb0910128b2c26d6158503" + +[[package]] +name = "lock_api" +version = "0.4.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c1cc9717a20b1bb222f333e6a92fd32f7d8a18ddc5a3191a11af45dcbf4dcd16" +dependencies = [ + "autocfg", + "scopeguard", +] + +[[package]] +name = "log" +version = "0.4.20" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b5e6163cb8c49088c2c36f57875e58ccd8c87c7427f7fbd50ea6710b2f3f2e8f" +dependencies = [ + "value-bag", +] + +[[package]] +name = "malloc_buf" +version = "0.0.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "62bb907fe88d54d8d9ce32a3cceab4218ed2f6b7d35617cafe9adf84e43919cb" +dependencies = [ + "libc", +] + +[[package]] +name = "matrixmultiply" +version = "0.3.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "090126dc04f95dc0d1c1c91f61bdd474b3930ca064c1edc8a849da2c6cbe1e77" +dependencies = [ + "autocfg", + "rawpointer", +] + +[[package]] +name = "memchr" +version = "2.6.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5486aed0026218e61b8a01d5fbd5a0a134649abb71a0e53b7bc088529dced86e" + +[[package]] +name = "memmap2" +version = "0.5.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "83faa42c0a078c393f6b29d5db232d8be22776a891f8f56e5284faee4a20b327" +dependencies = [ + "libc", +] + +[[package]] +name = "memoffset" +version = "0.6.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5aa361d4faea93603064a027415f07bd8e1d5c88c9fbf68bf56a285428fd79ce" +dependencies = [ + "autocfg", +] + +[[package]] +name = "minimal-lexical" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "68354c5c6bd36d73ff3feceb05efa59b6acb7626617f4962be322a825e61f79a" + +[[package]] +name = "miniz_oxide" +version = "0.7.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e7810e0be55b428ada41041c41f32c9f1a42817901b4ccf45fa3d4b6561e74c7" +dependencies = [ + "adler", + "simd-adler32", +] + +[[package]] +name = "mio" +version = "0.8.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "927a765cd3fc26206e66b296465fa9d3e5ab003e651c1b3c060e7956d96b19d2" +dependencies = [ + "libc", + "log", + "wasi", + "windows-sys 0.48.0", +] + +[[package]] +name = "ndarray" +version = "0.15.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "adb12d4e967ec485a5f71c6311fe28158e9d6f4bc4a447b474184d0f91a8fa32" +dependencies = [ + "matrixmultiply", + "num-complex", + "num-integer", + "num-traits", + "rawpointer", +] + +[[package]] +name = "ndk" +version = "0.7.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "451422b7e4718271c8b5b3aadf5adedba43dc76312454b387e98fae0fc951aa0" +dependencies = [ + "bitflags 1.3.2", + "jni-sys", + "ndk-sys", + "num_enum 0.5.11", + "raw-window-handle", + "thiserror", +] + +[[package]] +name = "ndk-context" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "27b02d87554356db9e9a873add8782d4ea6e3e58ea071a9adb9a2e8ddb884a8b" + +[[package]] +name = "ndk-sys" +version = "0.4.1+23.1.7779620" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3cf2aae958bd232cac5069850591667ad422d263686d75b52a065f9badeee5a3" +dependencies = [ + "jni-sys", +] + +[[package]] +name = "nix" +version = "0.24.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fa52e972a9a719cecb6864fb88568781eb706bac2cd1d4f04a648542dbf78069" +dependencies = [ + "bitflags 1.3.2", + "cfg-if", + "libc", + "memoffset", +] + +[[package]] +name = "nix" +version = "0.25.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f346ff70e7dbfd675fe90590b92d59ef2de15a8779ae305ebcbfd3f0caf59be4" +dependencies = [ + "autocfg", + "bitflags 1.3.2", + "cfg-if", + "libc", + "memoffset", +] + +[[package]] +name = "nohash-hasher" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2bf50223579dc7cdcfb3bfcacf7069ff68243f8c363f62ffa99cf000a6b9c451" + +[[package]] +name = "nom" +version = "7.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d273983c5a657a70a3e8f2a01329822f3b8c8172b73826411a55751e404a0a4a" +dependencies = [ + "memchr", + "minimal-lexical", +] + +[[package]] +name = "num-complex" +version = "0.4.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1ba157ca0885411de85d6ca030ba7e2a83a28636056c7c699b07c8b6f7383214" +dependencies = [ + "num-traits", + "serde", +] + +[[package]] +name = "num-integer" +version = "0.1.45" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "225d3389fb3509a24c93f5c29eb6bde2586b98d9f016636dff58d7c6f7569cd9" +dependencies = [ + "autocfg", + "num-traits", +] + +[[package]] +name = "num-rational" +version = "0.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0638a1c9d0a3c0914158145bc76cff373a75a627e6ecbfb71cbe6f453a5a19b0" +dependencies = [ + "autocfg", + "num-integer", + "num-traits", +] + +[[package]] +name = "num-traits" +version = "0.2.16" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f30b0abd723be7e2ffca1272140fac1a2f084c77ec3e123c192b66af1ee9e6c2" +dependencies = [ + "autocfg", + "libm", +] + +[[package]] +name = "num_enum" +version = "0.5.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1f646caf906c20226733ed5b1374287eb97e3c2a5c227ce668c1f2ce20ae57c9" +dependencies = [ + "num_enum_derive 0.5.11", +] + +[[package]] +name = "num_enum" +version = "0.6.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7a015b430d3c108a207fd776d2e2196aaf8b1cf8cf93253e3a097ff3085076a1" +dependencies = [ + "num_enum_derive 0.6.1", +] + +[[package]] +name = "num_enum_derive" +version = "0.5.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dcbff9bc912032c62bf65ef1d5aea88983b420f4f839db1e9b0c281a25c9c799" +dependencies = [ + "proc-macro-crate", + "proc-macro2", + "quote", + "syn 1.0.109", +] + +[[package]] +name = "num_enum_derive" +version = "0.6.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "96667db765a921f7b295ffee8b60472b686a51d4f21c2ee4ffdb94c7013b65a6" +dependencies = [ + "proc-macro-crate", + "proc-macro2", + "quote", + "syn 2.0.29", +] + +[[package]] +name = "objc" +version = "0.2.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "915b1b472bc21c53464d6c8461c9d3af805ba1ef837e1cac254428f4a77177b1" +dependencies = [ + "malloc_buf", +] + +[[package]] +name = "objc-foundation" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1add1b659e36c9607c7aab864a76c7a4c2760cd0cd2e120f3fb8b952c7e22bf9" +dependencies = [ + "block", + "objc", + "objc_id", +] + +[[package]] +name = "objc-sys" +version = "0.2.0-beta.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "df3b9834c1e95694a05a828b59f55fa2afec6288359cda67146126b3f90a55d7" + +[[package]] +name = "objc2" +version = "0.3.0-beta.3.patch-leaks.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7e01640f9f2cb1220bbe80325e179e532cb3379ebcd1bf2279d703c19fe3a468" +dependencies = [ + "block2", + "objc-sys", + "objc2-encode", +] + +[[package]] +name = "objc2-encode" +version = "2.0.0-pre.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "abfcac41015b00a120608fdaa6938c44cb983fee294351cc4bac7638b4e50512" +dependencies = [ + "objc-sys", +] + +[[package]] +name = "objc_id" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c92d4ddb4bd7b50d730c215ff871754d0da6b2178849f8a2a2ab69712d0c073b" +dependencies = [ + "objc", +] + +[[package]] +name = "once_cell" +version = "1.18.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dd8b5dd2ae5ed71462c540258bedcb51965123ad7e7ccf4b9a8cafaa4a63576d" + +[[package]] +name = "orbclient" +version = "0.3.46" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8378ac0dfbd4e7895f2d2c1f1345cab3836910baf3a300b000d04250f0c8428f" +dependencies = [ + "redox_syscall", +] + +[[package]] +name = "owned_ttf_parser" +version = "0.19.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "706de7e2214113d63a8238d1910463cfce781129a6f263d13fdb09ff64355ba4" +dependencies = [ + "ttf-parser", +] + +[[package]] +name = "parking" +version = "2.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "14f2252c834a40ed9bb5422029649578e63aa341ac401f74e719dd1afda8394e" + +[[package]] +name = "parking_lot" +version = "0.12.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3742b2c103b9f06bc9fff0a37ff4912935851bee6d36f3c02bcc755bcfec228f" +dependencies = [ + "lock_api", + "parking_lot_core", +] + +[[package]] +name = "parking_lot_core" +version = "0.9.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "93f00c865fe7cabf650081affecd3871070f26767e7b2070a3ffae14c654b447" +dependencies = [ + "cfg-if", + "libc", + "redox_syscall", + "smallvec", + "windows-targets 0.48.5", +] + +[[package]] +name = "percent-encoding" +version = "2.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9b2a4787296e9989611394c33f193f676704af1686e70b8f8033ab5ba9a35a94" + +[[package]] +name = "pin-project-lite" +version = "0.2.13" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8afb450f006bf6385ca15ef45d71d2288452bc3683ce2e2cacc0d18e4be60b58" + +[[package]] +name = "pin-utils" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8b870d8c151b6f2fb93e84a13146138f05d02ed11c7e7c54f8826aaaf7c9f184" + +[[package]] +name = "pkg-config" +version = "0.3.27" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "26072860ba924cbfa98ea39c8c19b4dd6a4a25423dbdf219c1eca91aa0cf6964" + +[[package]] +name = "png" +version = "0.17.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dd75bf2d8dd3702b9707cdbc56a5b9ef42cec752eb8b3bafc01234558442aa64" +dependencies = [ + "bitflags 1.3.2", + "crc32fast", + "fdeflate", + "flate2", + "miniz_oxide", +] + +[[package]] +name = "polling" +version = "2.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4b2d323e8ca7996b3e23126511a523f7e62924d93ecd5ae73b333815b0eb3dce" +dependencies = [ + "autocfg", + "bitflags 1.3.2", + "cfg-if", + "concurrent-queue", + "libc", + "log", + "pin-project-lite", + "windows-sys 0.48.0", +] + +[[package]] +name = "ppv-lite86" +version = "0.2.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5b40af805b3121feab8a3c29f04d8ad262fa8e0561883e7653e024ae4479e6de" + +[[package]] +name = "primal-check" +version = "0.3.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9df7f93fd637f083201473dab4fee2db4c429d32e55e3299980ab3957ab916a0" +dependencies = [ + "num-integer", +] + +[[package]] +name = "proc-macro-crate" +version = "1.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7f4c021e1093a56626774e81216a4ce732a735e5bad4868a03f3ed65ca0c3919" +dependencies = [ + "once_cell", + "toml_edit", +] + +[[package]] +name = "proc-macro2" +version = "1.0.66" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "18fb31db3f9bddb2ea821cde30a9f70117e3f119938b5ee630b7403aa6e2ead9" +dependencies = [ + "unicode-ident", +] + +[[package]] +name = "quote" +version = "1.0.33" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5267fca4496028628a95160fc423a33e8b2e6af8a5302579e322e4b520293cae" +dependencies = [ + "proc-macro2", +] + +[[package]] +name = "rand" +version = "0.8.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "34af8d1a0e25924bc5b7c43c079c942339d8f0a8b57c39049bef581b46327404" +dependencies = [ + "libc", + "rand_chacha", + "rand_core", +] + +[[package]] +name = "rand_chacha" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e6c10a63a0fa32252be49d21e7709d4d4baf8d231c2dbce1eaa8141b9b127d88" +dependencies = [ + "ppv-lite86", + "rand_core", +] + +[[package]] +name = "rand_core" +version = "0.6.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ec0be4795e2f6a28069bec0b5ff3e2ac9bafc99e6a9a7dc3547996c5c816922c" +dependencies = [ + "getrandom", +] + +[[package]] +name = "raw-window-handle" +version = "0.5.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f2ff9a1f06a88b01621b7ae906ef0211290d1c8a168a15542486a8f61c0833b9" + +[[package]] +name = "rawpointer" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "60a357793950651c4ed0f3f52338f53b2f809f32d83a07f72909fa13e4c6c1e3" + +[[package]] +name = "redox_syscall" +version = "0.3.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "567664f262709473930a4bf9e51bf2ebf3348f2e748ccc50dea20646858f8f29" +dependencies = [ + "bitflags 1.3.2", +] + +[[package]] +name = "regex" +version = "1.9.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "12de2eff854e5fa4b1295edd650e227e9d8fb0c9e90b12e7f36d6a6811791a29" +dependencies = [ + "aho-corasick", + "memchr", + "regex-automata", + "regex-syntax", +] + +[[package]] +name = "regex-automata" +version = "0.3.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "49530408a136e16e5b486e883fbb6ba058e8e4e8ae6621a77b048b314336e629" +dependencies = [ + "aho-corasick", + "memchr", + "regex-syntax", +] + +[[package]] +name = "regex-syntax" +version = "0.7.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dbb5fb1acd8a1a18b3dd5be62d25485eb770e05afb408a9627d14d451bae12da" + +[[package]] +name = "rustfft" +version = "6.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e17d4f6cbdb180c9f4b2a26bbf01c4e647f1e1dea22fe8eb9db54198b32f9434" +dependencies = [ + "num-complex", + "num-integer", + "num-traits", + "primal-check", + "strength_reduce", + "transpose", + "version_check", +] + +[[package]] +name = "rustix" +version = "0.37.23" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4d69718bf81c6127a49dc64e44a742e8bb9213c0ff8869a22c308f84c1d4ab06" +dependencies = [ + "bitflags 1.3.2", + "errno", + "io-lifetimes", + "libc", + "linux-raw-sys 0.3.8", + "windows-sys 0.48.0", +] + +[[package]] +name = "rustix" +version = "0.38.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c0c3dde1fc030af041adc40e79c0e7fbcf431dd24870053d187d7c66e4b87453" +dependencies = [ + "bitflags 2.4.0", + "errno", + "libc", + "linux-raw-sys 0.4.5", + "windows-sys 0.48.0", +] + +[[package]] +name = "same-file" +version = "1.0.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "93fc1dc3aaa9bfed95e02e6eadabb4baf7e3078b0bd1b4d7b6b0b68378900502" +dependencies = [ + "winapi-util", +] + +[[package]] +name = "scoped-tls" +version = "1.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e1cf6437eb19a8f4a6cc0f7dca544973b0b78843adbfeb3683d1a94a0024a294" + +[[package]] +name = "scopeguard" +version = "1.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "94143f37725109f92c262ed2cf5e59bce7498c01bcc1502d7b9afe439a4e9f49" + +[[package]] +name = "sctk-adwaita" +version = "0.5.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cda4e97be1fd174ccc2aae81c8b694e803fa99b34e8fd0f057a9d70698e3ed09" +dependencies = [ + "ab_glyph", + "log", + "memmap2", + "smithay-client-toolkit", + "tiny-skia", +] + +[[package]] +name = "serde" +version = "1.0.188" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cf9e0fcba69a370eed61bcf2b728575f726b50b55cba78064753d708ddc7549e" +dependencies = [ + "serde_derive", +] + +[[package]] +name = "serde_derive" +version = "1.0.188" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4eca7ac642d82aa35b60049a6eccb4be6be75e599bd2e9adb5f875a737654af2" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.29", +] + +[[package]] +name = "simd-adler32" +version = "0.3.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d66dc143e6b11c1eddc06d5c423cfc97062865baf299914ab64caa38182078fe" + +[[package]] +name = "slab" +version = "0.4.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8f92a496fb766b417c996b9c5e57daf2f7ad3b0bebe1ccfca4856390e3d3bb67" +dependencies = [ + "autocfg", +] + +[[package]] +name = "slotmap" +version = "1.0.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e1e08e261d0e8f5c43123b7adf3e4ca1690d655377ac93a03b2c9d3e98de1342" +dependencies = [ + "version_check", +] + +[[package]] +name = "smallvec" +version = "1.11.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "62bb4feee49fdd9f707ef802e22365a35de4b7b299de4763d44bfea899442ff9" + +[[package]] +name = "smithay-client-toolkit" +version = "0.16.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f307c47d32d2715eb2e0ece5589057820e0e5e70d07c247d1063e844e107f454" +dependencies = [ + "bitflags 1.3.2", + "calloop", + "dlib", + "lazy_static", + "log", + "memmap2", + "nix 0.24.3", + "pkg-config", + "wayland-client", + "wayland-cursor", + "wayland-protocols", +] + +[[package]] +name = "smithay-clipboard" +version = "0.6.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0a345c870a1fae0b1b779085e81b51e614767c239e93503588e54c5b17f4b0e8" +dependencies = [ + "smithay-client-toolkit", + "wayland-client", +] + +[[package]] +name = "socket2" +version = "0.4.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "64a4a911eed85daf18834cfaa86a79b7d266ff93ff5ba14005426219480ed662" +dependencies = [ + "libc", + "winapi", +] + +[[package]] +name = "socket2" +version = "0.5.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2538b18701741680e0322a2302176d3253a35388e2e62f172f64f4f16605f877" +dependencies = [ + "libc", + "windows-sys 0.48.0", +] + +[[package]] +name = "stabilizer-streaming" +version = "0.1.0" +dependencies = [ + "anyhow", + "async-std", + "bytemuck", + "clap", + "eframe", + "env_logger", + "idsp", + "log", + "ndarray", + "num_enum 0.5.11", + "rand", + "rustfft", + "serde", + "socket2 0.5.3", + "thiserror", +] + +[[package]] +name = "str-buf" +version = "1.0.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9e08d8363704e6c71fc928674353e6b7c23dcea9d82d7012c8faf2a3a025f8d0" + +[[package]] +name = "strength_reduce" +version = "0.2.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fe895eb47f22e2ddd4dabc02bce419d2e643c8e3b585c78158b349195bc24d82" + +[[package]] +name = "strict-num" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6637bab7722d379c8b41ba849228d680cc12d0a45ba1fa2b48f2a30577a06731" + +[[package]] +name = "strsim" +version = "0.10.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "73473c0e59e6d5812c5dfe2a064a6444949f089e20eec9a2e5506596494e4623" + +[[package]] +name = "syn" +version = "1.0.109" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "72b64191b275b66ffe2469e8af2c1cfe3bafa67b529ead792a6d0160888b4237" +dependencies = [ + "proc-macro2", + "quote", + "unicode-ident", +] + +[[package]] +name = "syn" +version = "2.0.29" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c324c494eba9d92503e6f1ef2e6df781e78f6a7705a0202d9801b198807d518a" +dependencies = [ + "proc-macro2", + "quote", + "unicode-ident", +] + +[[package]] +name = "termcolor" +version = "1.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "be55cf8942feac5c765c2c993422806843c9a9a45d4d5c407ad6dd2ea95eb9b6" +dependencies = [ + "winapi-util", +] + +[[package]] +name = "thiserror" +version = "1.0.47" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "97a802ec30afc17eee47b2855fc72e0c4cd62be9b4efe6591edde0ec5bd68d8f" +dependencies = [ + "thiserror-impl", +] + +[[package]] +name = "thiserror-impl" +version = "1.0.47" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6bb623b56e39ab7dcd4b1b98bb6c8f8d907ed255b18de254088016b27a8ee19b" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.29", +] + +[[package]] +name = "tiny-skia" +version = "0.8.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "df8493a203431061e901613751931f047d1971337153f96d0e5e363d6dbf6a67" +dependencies = [ + "arrayref", + "arrayvec", + "bytemuck", + "cfg-if", + "png", + "tiny-skia-path", +] + +[[package]] +name = "tiny-skia-path" +version = "0.8.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "adbfb5d3f3dd57a0e11d12f4f13d4ebbbc1b5c15b7ab0a156d030b21da5f677c" +dependencies = [ + "arrayref", + "bytemuck", + "strict-num", +] + +[[package]] +name = "tinyvec" +version = "1.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "87cc5ceb3875bb20c2890005a4e226a4651264a5c75edb2421b52861a0a0cb50" +dependencies = [ + "tinyvec_macros", +] + +[[package]] +name = "tinyvec_macros" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1f3ccbac311fea05f86f61904b462b55fb3df8837a366dfc601a0161d0532f20" + +[[package]] +name = "toml_datetime" +version = "0.6.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7cda73e2f1397b1262d6dfdcef8aafae14d1de7748d66822d3bfeeb6d03e5e4b" + +[[package]] +name = "toml_edit" +version = "0.19.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f8123f27e969974a3dfba720fdb560be359f57b44302d280ba72e76a74480e8a" +dependencies = [ + "indexmap", + "toml_datetime", + "winnow", +] + +[[package]] +name = "transpose" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e6522d49d03727ffb138ae4cbc1283d3774f0d10aa7f9bf52e6784c45daf9b23" +dependencies = [ + "num-integer", + "strength_reduce", +] + +[[package]] +name = "ttf-parser" +version = "0.19.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a464a4b34948a5f67fddd2b823c62d9d92e44be75058b99939eae6c5b6960b33" + +[[package]] +name = "unicode-bidi" +version = "0.3.13" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "92888ba5573ff080736b3648696b70cafad7d250551175acbaa4e0385b3e1460" + +[[package]] +name = "unicode-ident" +version = "1.0.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "301abaae475aa91687eb82514b328ab47a211a533026cb25fc3e519b86adfc3c" + +[[package]] +name = "unicode-normalization" +version = "0.1.22" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5c5713f0fc4b5db668a2ac63cdb7bb4469d8c9fed047b1d0292cc7b0ce2ba921" +dependencies = [ + "tinyvec", +] + +[[package]] +name = "url" +version = "2.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "143b538f18257fac9cad154828a57c6bf5157e1aa604d4816b5995bf6de87ae5" +dependencies = [ + "form_urlencoded", + "idna", + "percent-encoding", +] + +[[package]] +name = "utf8parse" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "711b9620af191e0cdc7468a8d14e709c3dcdb115b36f838e601583af800a370a" + +[[package]] +name = "value-bag" +version = "1.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d92ccd67fb88503048c01b59152a04effd0782d035a83a6d256ce6085f08f4a3" + +[[package]] +name = "vec_map" +version = "0.8.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f1bddf1187be692e79c5ffeab891132dfb0f236ed36a43c7ed39f1165ee20191" + +[[package]] +name = "version_check" +version = "0.9.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "49874b5167b65d7193b8aba1567f5c7d93d001cafc34600cee003eda787e483f" + +[[package]] +name = "waker-fn" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9d5b2c62b4012a3e1eca5a7e077d13b3bf498c4073e33ccd58626607748ceeca" + +[[package]] +name = "walkdir" +version = "2.3.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "36df944cda56c7d8d8b7496af378e6b16de9284591917d307c9b4d313c44e698" +dependencies = [ + "same-file", + "winapi-util", +] + +[[package]] +name = "wasi" +version = "0.11.0+wasi-snapshot-preview1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9c8d87e72b64a3b4db28d11ce29237c246188f4f51057d65a7eab63b7987e423" + +[[package]] +name = "wasm-bindgen" +version = "0.2.87" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7706a72ab36d8cb1f80ffbf0e071533974a60d0a308d01a5d0375bf60499a342" +dependencies = [ + "cfg-if", + "wasm-bindgen-macro", +] + +[[package]] +name = "wasm-bindgen-backend" +version = "0.2.87" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5ef2b6d3c510e9625e5fe6f509ab07d66a760f0885d858736483c32ed7809abd" +dependencies = [ + "bumpalo", + "log", + "once_cell", + "proc-macro2", + "quote", + "syn 2.0.29", + "wasm-bindgen-shared", +] + +[[package]] +name = "wasm-bindgen-futures" +version = "0.4.37" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c02dbc21516f9f1f04f187958890d7e6026df8d16540b7ad9492bc34a67cea03" +dependencies = [ + "cfg-if", + "js-sys", + "wasm-bindgen", + "web-sys", +] + +[[package]] +name = "wasm-bindgen-macro" +version = "0.2.87" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dee495e55982a3bd48105a7b947fd2a9b4a8ae3010041b9e0faab3f9cd028f1d" +dependencies = [ + "quote", + "wasm-bindgen-macro-support", +] + +[[package]] +name = "wasm-bindgen-macro-support" +version = "0.2.87" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "54681b18a46765f095758388f2d0cf16eb8d4169b639ab575a8f5693af210c7b" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.29", + "wasm-bindgen-backend", + "wasm-bindgen-shared", +] + +[[package]] +name = "wasm-bindgen-shared" +version = "0.2.87" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ca6ad05a4870b2bf5fe995117d3728437bd27d7cd5f06f13c17443ef369775a1" + +[[package]] +name = "wayland-client" +version = "0.29.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3f3b068c05a039c9f755f881dc50f01732214f5685e379829759088967c46715" +dependencies = [ + "bitflags 1.3.2", + "downcast-rs", + "libc", + "nix 0.24.3", + "scoped-tls", + "wayland-commons", + "wayland-scanner", + "wayland-sys 0.29.5", +] + +[[package]] +name = "wayland-commons" +version = "0.29.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8691f134d584a33a6606d9d717b95c4fa20065605f798a3f350d78dced02a902" +dependencies = [ + "nix 0.24.3", + "once_cell", + "smallvec", + "wayland-sys 0.29.5", +] + +[[package]] +name = "wayland-cursor" +version = "0.29.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6865c6b66f13d6257bef1cd40cbfe8ef2f150fb8ebbdb1e8e873455931377661" +dependencies = [ + "nix 0.24.3", + "wayland-client", + "xcursor", +] + +[[package]] +name = "wayland-protocols" +version = "0.29.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b950621f9354b322ee817a23474e479b34be96c2e909c14f7bc0100e9a970bc6" +dependencies = [ + "bitflags 1.3.2", + "wayland-client", + "wayland-commons", + "wayland-scanner", +] + +[[package]] +name = "wayland-scanner" +version = "0.29.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8f4303d8fa22ab852f789e75a967f0a2cdc430a607751c0499bada3e451cbd53" +dependencies = [ + "proc-macro2", + "quote", + "xml-rs", +] + +[[package]] +name = "wayland-sys" +version = "0.29.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "be12ce1a3c39ec7dba25594b97b42cb3195d54953ddb9d3d95a7c3902bc6e9d4" +dependencies = [ + "dlib", + "lazy_static", + "pkg-config", +] + +[[package]] +name = "wayland-sys" +version = "0.30.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "96b2a02ac608e07132978689a6f9bf4214949c85998c247abadd4f4129b1aa06" +dependencies = [ + "dlib", + "lazy_static", + "log", + "pkg-config", +] + +[[package]] +name = "web-sys" +version = "0.3.64" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9b85cbef8c220a6abc02aefd892dfc0fc23afb1c6a426316ec33253a3877249b" +dependencies = [ + "js-sys", + "wasm-bindgen", +] + +[[package]] +name = "webbrowser" +version = "0.8.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b2c79b77f525a2d670cb40619d7d9c673d09e0666f72c591ebd7861f84a87e57" +dependencies = [ + "core-foundation", + "home", + "jni", + "log", + "ndk-context", + "objc", + "raw-window-handle", + "url", + "web-sys", +] + +[[package]] +name = "winapi" +version = "0.3.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5c839a674fcd7a98952e593242ea400abe93992746761e38641405d28b00f419" +dependencies = [ + "winapi-i686-pc-windows-gnu", + "winapi-x86_64-pc-windows-gnu", +] + +[[package]] +name = "winapi-i686-pc-windows-gnu" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ac3b87c63620426dd9b991e5ce0329eff545bccbbb34f3be09ff6fb6ab51b7b6" + +[[package]] +name = "winapi-util" +version = "0.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "70ec6ce85bb158151cae5e5c87f95a8e97d2c0c4b001223f33a334e3ce5de178" +dependencies = [ + "winapi", +] + +[[package]] +name = "winapi-wsapoll" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "44c17110f57155602a80dca10be03852116403c9ff3cd25b079d666f2aa3df6e" +dependencies = [ + "winapi", +] + +[[package]] +name = "winapi-x86_64-pc-windows-gnu" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "712e227841d057c1ee1cd2fb22fa7e5a5461ae8e48fa2ca79ec42cfc1931183f" + +[[package]] +name = "windows-sys" +version = "0.45.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "75283be5efb2831d37ea142365f009c02ec203cd29a3ebecbc093d52315b66d0" +dependencies = [ + "windows-targets 0.42.2", +] + +[[package]] +name = "windows-sys" +version = "0.48.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "677d2418bec65e3338edb076e806bc1ec15693c5d0104683f2efe857f61056a9" +dependencies = [ + "windows-targets 0.48.5", +] + +[[package]] +name = "windows-targets" +version = "0.42.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8e5180c00cd44c9b1c88adb3693291f1cd93605ded80c250a75d472756b4d071" +dependencies = [ + "windows_aarch64_gnullvm 0.42.2", + "windows_aarch64_msvc 0.42.2", + "windows_i686_gnu 0.42.2", + "windows_i686_msvc 0.42.2", + "windows_x86_64_gnu 0.42.2", + "windows_x86_64_gnullvm 0.42.2", + "windows_x86_64_msvc 0.42.2", +] + +[[package]] +name = "windows-targets" +version = "0.48.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9a2fa6e2155d7247be68c096456083145c183cbbbc2764150dda45a87197940c" +dependencies = [ + "windows_aarch64_gnullvm 0.48.5", + "windows_aarch64_msvc 0.48.5", + "windows_i686_gnu 0.48.5", + "windows_i686_msvc 0.48.5", + "windows_x86_64_gnu 0.48.5", + "windows_x86_64_gnullvm 0.48.5", + "windows_x86_64_msvc 0.48.5", +] + +[[package]] +name = "windows_aarch64_gnullvm" +version = "0.42.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "597a5118570b68bc08d8d59125332c54f1ba9d9adeedeef5b99b02ba2b0698f8" + +[[package]] +name = "windows_aarch64_gnullvm" +version = "0.48.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2b38e32f0abccf9987a4e3079dfb67dcd799fb61361e53e2882c3cbaf0d905d8" + +[[package]] +name = "windows_aarch64_msvc" +version = "0.42.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e08e8864a60f06ef0d0ff4ba04124db8b0fb3be5776a5cd47641e942e58c4d43" + +[[package]] +name = "windows_aarch64_msvc" +version = "0.48.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dc35310971f3b2dbbf3f0690a219f40e2d9afcf64f9ab7cc1be722937c26b4bc" + +[[package]] +name = "windows_i686_gnu" +version = "0.42.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c61d927d8da41da96a81f029489353e68739737d3beca43145c8afec9a31a84f" + +[[package]] +name = "windows_i686_gnu" +version = "0.48.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a75915e7def60c94dcef72200b9a8e58e5091744960da64ec734a6c6e9b3743e" + +[[package]] +name = "windows_i686_msvc" +version = "0.42.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "44d840b6ec649f480a41c8d80f9c65108b92d89345dd94027bfe06ac444d1060" + +[[package]] +name = "windows_i686_msvc" +version = "0.48.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8f55c233f70c4b27f66c523580f78f1004e8b5a8b659e05a4eb49d4166cca406" + +[[package]] +name = "windows_x86_64_gnu" +version = "0.42.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8de912b8b8feb55c064867cf047dda097f92d51efad5b491dfb98f6bbb70cb36" + +[[package]] +name = "windows_x86_64_gnu" +version = "0.48.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "53d40abd2583d23e4718fddf1ebec84dbff8381c07cae67ff7768bbf19c6718e" + +[[package]] +name = "windows_x86_64_gnullvm" +version = "0.42.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "26d41b46a36d453748aedef1486d5c7a85db22e56aff34643984ea85514e94a3" + +[[package]] +name = "windows_x86_64_gnullvm" +version = "0.48.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0b7b52767868a23d5bab768e390dc5f5c55825b6d30b86c844ff2dc7414044cc" + +[[package]] +name = "windows_x86_64_msvc" +version = "0.42.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9aec5da331524158c6d1a4ac0ab1541149c0b9505fde06423b02f5ef0106b9f0" + +[[package]] +name = "windows_x86_64_msvc" +version = "0.48.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ed94fce61571a4006852b7389a063ab983c02eb1bb37b47f8272ce92d06d9538" + +[[package]] +name = "winit" +version = "0.28.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "866db3f712fffba75d31bf0cdecf357c8aeafd158c5b7ab51dba2a2b2d47f196" +dependencies = [ + "android-activity", + "bitflags 1.3.2", + "cfg_aliases", + "core-foundation", + "core-graphics", + "dispatch", + "instant", + "libc", + "log", + "mio", + "ndk", + "objc2", + "once_cell", + "orbclient", + "percent-encoding", + "raw-window-handle", + "redox_syscall", + "sctk-adwaita", + "smithay-client-toolkit", + "wasm-bindgen", + "wayland-client", + "wayland-commons", + "wayland-protocols", + "wayland-scanner", + "web-sys", + "windows-sys 0.45.0", + "x11-dl", +] + +[[package]] +name = "winnow" +version = "0.5.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7c2e3184b9c4e92ad5167ca73039d0c42476302ab603e2fec4487511f38ccefc" +dependencies = [ + "memchr", +] + +[[package]] +name = "x11-dl" +version = "2.21.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "38735924fedd5314a6e548792904ed8c6de6636285cb9fec04d5b1db85c1516f" +dependencies = [ + "libc", + "once_cell", + "pkg-config", +] + +[[package]] +name = "x11rb" +version = "0.10.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "592b4883219f345e712b3209c62654ebda0bb50887f330cbd018d0f654bfd507" +dependencies = [ + "gethostname", + "nix 0.24.3", + "winapi", + "winapi-wsapoll", + "x11rb-protocol", +] + +[[package]] +name = "x11rb-protocol" +version = "0.10.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "56b245751c0ac9db0e006dc812031482784e434630205a93c73cfefcaabeac67" +dependencies = [ + "nix 0.24.3", +] + +[[package]] +name = "xcursor" +version = "0.3.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "463705a63313cd4301184381c5e8042f0a7e9b4bb63653f216311d4ae74690b7" +dependencies = [ + "nom", +] + +[[package]] +name = "xml-rs" +version = "0.8.16" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "47430998a7b5d499ccee752b41567bc3afc57e1327dc855b1a2aa44ce29b5fa1" From 77efcb30f6d6b221c6e3b7dd4fba571524b1c0a8 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Robert=20J=C3=B6rdens?= Date: Fri, 1 Sep 2023 17:02:50 +0200 Subject: [PATCH 22/39] fix stage depth --- src/bin/main.rs | 31 ++++++----- src/bin/stream_test.rs | 7 ++- src/hbf.rs | 2 + src/psd.rs | 124 ++++++++++++++++++++++++----------------- 4 files changed, 95 insertions(+), 69 deletions(-) diff --git a/src/bin/main.rs b/src/bin/main.rs index 84263af..6d5db66 100644 --- a/src/bin/main.rs +++ b/src/bin/main.rs @@ -55,9 +55,10 @@ fn main() -> Result<()> { let mut loss = Loss::default(); let mut dec: Vec<_> = (0..4) .map(|_| { - PsdCascade::<{ 1 << 9 }>::default() - .stage_length(3) - .detrend(Detrend::Mean) + let mut c = PsdCascade::<{ 1 << 9 }>::default(); + c.set_stage_length(3); + c.set_detrend(Detrend::Mean); + c }) .collect(); @@ -71,9 +72,10 @@ fn main() -> Result<()> { Ok(Cmd::Reset) => { dec = (0..4) .map(|_| { - PsdCascade::<{ 1 << 9 }>::default() - .stage_length(3) - .detrend(Detrend::None) + let mut c = PsdCascade::<{ 1 << 9 }>::default(); + c.set_stage_length(3); + c.set_detrend(Detrend::Mean); + c }) .collect(); } @@ -104,12 +106,7 @@ fn main() -> Result<()> { .iter() .map(|dec| { let (p, b) = dec.get(1); - let mut f = vec![]; - for bi in b.iter() { - f.truncate(bi[0]); - let df = 1.0 / bi[3] as f32; - f.extend((0..bi[2]).rev().map(|f| f as f32 * df)); - } + let f = dec.f(&b); Trace::new( f.iter() .zip(p.iter()) @@ -211,9 +208,13 @@ impl eframe::App for FLS { }); }); ui.add_space(20.0); - if ui.button("Reset").clicked() { - self.cmd_send.send(Cmd::Reset).unwrap(); - } + ui.horizontal(|ui| { + ui.add_space(20.0); + if ui.button("Reset").clicked() { + self.cmd_send.send(Cmd::Reset).unwrap(); + } + ui.label("Every"); + }); }); } } diff --git a/src/bin/stream_test.rs b/src/bin/stream_test.rs index 45910cc..6f96192 100644 --- a/src/bin/stream_test.rs +++ b/src/bin/stream_test.rs @@ -38,9 +38,10 @@ fn main() -> Result<()> { let mut dec: Vec<_> = (0..4) .map(|_| { - PsdCascade::<{ 1 << 9 }>::default() - .stage_length(3) - .detrend(Detrend::Mean) + let mut c = PsdCascade::<{ 1 << 9 }>::default(); + c.set_stage_length(3); + c.set_detrend(Detrend::Mean); + c }) .collect(); diff --git a/src/hbf.rs b/src/hbf.rs index 537cbda..386c839 100644 --- a/src/hbf.rs +++ b/src/hbf.rs @@ -260,6 +260,7 @@ pub const HBF_CASCADE_BLOCK: usize = 1 << 8; /// See [HBF_TAPS]. /// Only in-place processing is implemented. /// Supports rate changes of 1, 2, 4, 8, and 16. +#[derive(Copy, Clone, Debug)] pub struct HbfDecCascade { n: usize, stages: ( @@ -362,6 +363,7 @@ impl Filter for HbfDecCascade { /// See [HBF_TAPS]. /// Only in-place processing is implemented. /// Supports rate changes of 1, 2, 4, 8, and 16. +#[derive(Copy, Clone, Debug)] pub struct HbfIntCascade { n: usize, pub stages: ( diff --git a/src/psd.rs b/src/psd.rs index 7fbe5e0..ec5717a 100644 --- a/src/psd.rs +++ b/src/psd.rs @@ -12,7 +12,6 @@ pub struct Window { } /// Hann window - impl Window { pub fn hann() -> Self { assert!(N > 0); @@ -29,7 +28,8 @@ impl Window { } } -#[derive(Clone, Copy, Debug, PartialEq, Eq)] +/// Detrend method +#[derive(Clone, Copy, Debug, PartialEq, Eq, PartialOrd, Ord)] pub enum Detrend { /// No detrending None, @@ -42,21 +42,21 @@ pub enum Detrend { /// Power spectral density accumulator and decimator /// /// One stage in [PsdCascade]. +#[derive(Clone)] pub struct Psd { hbf: HbfDecCascade, buf: [f32; N], idx: usize, - out: [f32; N], spectrum: [f32; N], // using only the positive half count: usize, fft: Arc>, - win: Window, + win: Arc>, detrend: Detrend, drain: usize, } impl Psd { - pub fn new(fft: Arc>, win: Window) -> Self { + pub fn new(fft: Arc>, win: Arc>) -> Self { let hbf = HbfDecCascade::default(); assert_eq!(N & 1, 0); assert_eq!(N, fft.len()); @@ -68,7 +68,6 @@ impl Psd { hbf, buf: [0.0; N], idx: 0, - out: [0.0; N], spectrum: [0.0; N], count: 0, fft, @@ -78,33 +77,31 @@ impl Psd { } } - pub fn detrend(mut self, d: Detrend) -> Self { + pub fn set_detrend(&mut self, d: Detrend) { self.detrend = d; - self } - pub fn stage_length(mut self, n: usize) -> Self { + pub fn set_stage_length(&mut self, n: usize) { self.hbf.set_n(n); self.drain = self.hbf.response_length(); - self } } pub trait Stage { /// Process items /// - /// Unusde items are buffered. + /// Unused items are buffered. /// Full FFT blocks are processed. /// Overlap is kept. /// Decimation is performed on fully processed input items. /// /// # Args /// * `x`: input items - /// * `detrend`: [Detrend] method + /// * `y`: output items /// /// # Returns - /// decimated output - fn process(&mut self, x: &[f32]) -> &[f32]; + /// number if items written to `y` + fn process(&mut self, x: &[f32], y: &mut [f32]) -> usize; /// Return the positive frequency half of the spectrum fn spectrum(&self) -> &[f32]; /// PSD normalization factor @@ -115,7 +112,7 @@ pub trait Stage { } impl Stage for Psd { - fn process(&mut self, mut x: &[f32]) -> &[f32] { + fn process(&mut self, mut x: &[f32], y: &mut [f32]) -> usize { let mut c = [Complex::default(); N]; let mut n = 0; while !x.is_empty() { @@ -155,18 +152,18 @@ impl Stage for Psd { } self.count += 1; - // decimate non-overlapping chunks + // decimate left half let (left, right) = self.buf.split_at_mut(N / 2); let mut k = self.hbf.process_block(None, left); // drain decimator impulse response to initial state (zeros) (k, self.drain) = (k.saturating_sub(self.drain), self.drain.saturating_sub(k)); - self.out[n..][..k].copy_from_slice(&left[..k]); + y[n..][..k].copy_from_slice(&left[..k]); n += k; - // drop the overlapped and processed chunks + // drop the left keep the right as overlap left.copy_from_slice(right); self.idx = N / 2; } - &self.out[..n] + n } fn spectrum(&self) -> &[f32] { @@ -179,8 +176,8 @@ impl Stage for Psd { fn gain(&self) -> f32 { // 2 for one-sided - // overlap compensated by count - 2.0 * self.win.power / ((self.count * N) as f32 * self.win.nenbw) + // overlap compensated by counting + self.win.power / ((self.count * N / 2) as f32 * self.win.nenbw) } } @@ -189,6 +186,7 @@ impl Stage for Psd { /// Infinite averaging /// Incremental updates /// Automatic FFT stage extension +#[derive(Clone)] pub struct PsdCascade { stages: Vec>, fft: Arc>, @@ -217,26 +215,35 @@ impl Default for PsdCascade { } impl PsdCascade { - pub fn stage_length(mut self, n: usize) -> Self { + pub fn set_stage_length(&mut self, n: usize) { self.stage_length = n; - self + for stage in self.stages.iter_mut() { + stage.set_stage_length(n); + } } - pub fn detrend(mut self, d: Detrend) -> Self { + pub fn set_detrend(&mut self, d: Detrend) { self.detrend = d; - self } /// Process input items pub fn process(&mut self, x: &[f32]) { - let mut x = x; - x = self.stages.iter_mut().fold(x, |x, stage| stage.process(x)); - if !x.is_empty() { - let mut stage = Psd::new(self.fft.clone(), *self.win) - .stage_length(self.stage_length) - .detrend(self.detrend); - stage.process(x); - self.stages.push(stage); + let mut a = ([0f32; N], [0f32; N]); + let (mut y, mut z) = (&mut a.0[..], &mut a.1[..]); + for mut x in x.chunks(N << self.stage_length) { + let mut i = 0; + while !x.is_empty() { + while i >= self.stages.len() { + let mut stage = Psd::new(self.fft.clone(), self.win.clone()); + stage.set_stage_length(self.stage_length); + stage.set_detrend(self.detrend); + self.stages.push(stage); + } + let n = self.stages[i].process(x, &mut y[..]); + core::mem::swap(&mut z, &mut y); + x = &z[..n]; + i += 1; + } } } @@ -249,8 +256,8 @@ impl PsdCascade { /// * `psd`: `Vec` normalized reversed (Nyquist first, DC last) /// * `breaks`: `Vec` of stage breaks `[start index in psd, average count, highest bin index, effective fft size]` pub fn get(&self, min_count: usize) -> (Vec, Vec<[usize; 4]>) { - let mut p = vec![]; - let mut b = vec![]; + let mut p = Vec::with_capacity(self.stages.len() * N / 2); + let mut b = Vec::with_capacity(self.stages.len()); let mut n = 0; for stage in self.stages.iter().take_while(|s| s.count >= min_count) { let mut pi = stage.spectrum(); @@ -273,14 +280,29 @@ impl PsdCascade { p.extend(pi.iter().rev().map(|pi| pi * g)); n += stage.hbf.n(); } - // correct DC and Nyquist bins as both only contribute once to the one-sided spectrum - // this matches matplotlib and matlab but is certainly a questionable step - // need special care when interpreting and integrating the PSD - p[0] *= 0.5; - let n = p.len(); - p[n - 1] *= 0.5; + if !p.is_empty() { + // correct DC and Nyquist bins as both only contribute once to the one-sided spectrum + // this matches matplotlib and matlab but is certainly a questionable step + // need special care when interpreting and integrating the PSD + p[0] *= 0.5; + let n = p.len(); + p[n - 1] *= 0.5; + } (p, b) } + + /// Compute PSD bin center frequencies from stage breaks. + pub fn f(&self, b: &[[usize; 4]]) -> Vec { + let Some(bi) = b.last() else { return vec![] }; + let mut f = Vec::with_capacity(bi[0] + bi[2]); + for bi in b.iter() { + f.truncate(bi[0]); + let df = 1.0 / bi[3] as f32; + f.extend((0..bi[2]).rev().map(|f| f as f32 * df)); + } + assert_eq!(f.len(), bi[0] + bi[2]); + f + } } #[cfg(test)] @@ -292,7 +314,7 @@ mod test { assert_eq!(crate::HBF_PASSBAND, 0.4); // make uniform noise [-1, 1), ignore the epsilon. - let x: Vec = (0..1 << 20) + let x: Vec<_> = (0..1 << 20) .map(|_| rand::random::() * 2.0 - 1.0) .collect(); let xm = x.iter().map(|x| *x as f64).sum::() as f32 / x.len() as f32; @@ -304,11 +326,13 @@ mod test { const F: usize = 1 << 9; let n = 3; - let mut s = Psd::<{ 1 << 9 }>::new(FftPlanner::new().plan_fft_forward(F), Window::hann()) - .stage_length(n); + let mut s = Psd::::new(FftPlanner::new().plan_fft_forward(F), Window::hann()); + s.set_stage_length(n); let mut y = vec![]; for x in x.chunks(F << n) { - y.extend_from_slice(s.process(x)); + let mut yi = [0.0; F]; + let k = s.process(x, &mut yi[..]); + y.extend_from_slice(&yi[..k]); } let mut hbf = HbfDecCascade::default(); hbf.set_n(n); @@ -323,12 +347,10 @@ mod test { &p[..] ); - let mut d = PsdCascade::::default() - .stage_length(n) - .detrend(Detrend::None); - for x in x.chunks(F << n) { - d.process(x); - } + let mut d = PsdCascade::::default(); + d.set_stage_length(n); + d.set_detrend(Detrend::None); + d.process(&x); let (mut p, b) = d.get(1); // tweak DC and Nyquist to make checks less code let n = p.len(); From 77cc5bd8e524b7bae9f8fb5d85a87ab7140cbddc Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Robert=20J=C3=B6rdens?= Date: Fri, 1 Sep 2023 18:39:36 +0200 Subject: [PATCH 23/39] docs --- src/bin/main.rs | 29 +++++++++++----------------- src/hbf.rs | 51 +++++++++++++++++++++++++------------------------ src/psd.rs | 17 +++++++++++++++-- 3 files changed, 52 insertions(+), 45 deletions(-) diff --git a/src/bin/main.rs b/src/bin/main.rs index 6d5db66..5b0a0c4 100644 --- a/src/bin/main.rs +++ b/src/bin/main.rs @@ -53,14 +53,7 @@ fn main() -> Result<()> { log::info!("Receiving frames"); let mut loss = Loss::default(); - let mut dec: Vec<_> = (0..4) - .map(|_| { - let mut c = PsdCascade::<{ 1 << 9 }>::default(); - c.set_stage_length(3); - c.set_detrend(Detrend::Mean); - c - }) - .collect(); + let mut dec = Vec::with_capacity(4); // let mut fil = std::fs::File::open("/tmp/fls2x.raw")?; @@ -69,19 +62,19 @@ fn main() -> Result<()> { loop { match cmd_recv.try_recv() { Err(mpsc::TryRecvError::Disconnected) | Ok(Cmd::Exit) => break, - Ok(Cmd::Reset) => { - dec = (0..4) - .map(|_| { - let mut c = PsdCascade::<{ 1 << 9 }>::default(); - c.set_stage_length(3); - c.set_detrend(Detrend::Mean); - c - }) - .collect(); - } + Ok(Cmd::Reset) => dec.clear(), Err(mpsc::TryRecvError::Empty) => {} }; + if dec.is_empty() { + dec.extend((0..4).map(|_| { + let mut c = PsdCascade::<{ 1 << 9 }>::default(); + c.set_stage_length(3); + c.set_detrend(Detrend::Mean); + c + })); + } + // let len = fil.read(&mut buf[..1400])?; // if len == 0 { // fil.seek(std::io::SeekFrom::Start(0))?; diff --git a/src/hbf.rs b/src/hbf.rs index 386c839..cbc947a 100644 --- a/src/hbf.rs +++ b/src/hbf.rs @@ -1,25 +1,3 @@ -/// Decimation/interpolation filters -/// -/// These focus on half-band filters (rate change of 2) and cascades of HBF. -/// The half-band filter has unique properties that make it preferrable in many cases: -/// -/// * only needs N multiplications (fused multiply accumulate) for 4*N taps -/// * stores less state compared with with a straight FIR -/// * as a FIR filter has linear phase/flat group delay -/// * very small passband ripple and excellent stopband attenuation -/// * as a cascade of decimation/interpolation filters, the higher-rate filters -/// need successively fewer taps, allowing the filtering to be dominated by -/// only the highest rate filter with the fewest taps -/// * high dynamic range (compared with a biquad IIR) -/// * can be combined with a CIC filter for non-power-of-two or even higher rate changes -/// -/// The implementations here are all `no_std` and `no-alloc`. -/// They support (but don't require) in-place filtering to reduce memory usage. -/// They unroll and optimmize extremely well targetting current architectures, -/// e.g. requiring less than 4 instructions per input item for the full `HbfDecCascade` on Skylake. -/// The filters are optimized for decent block sizes and perform best (i.e. with negligible -/// overhead) for blocks of 32 high-rate items or more, depending very much on architecture. - /// Filter input items into output items. pub trait Filter { /// Input/output item type. @@ -59,22 +37,45 @@ pub trait Filter { /// /// M: number of taps /// N: state size: N = 2*M - 1 + {input/output}.len() +/// +/// Decimation/interpolation filters +/// +/// These focus on half-band filters (rate change of 2) and cascades of HBF. +/// The half-band filter has unique properties that make it preferrable in many cases: +/// +/// * only needs N multiplications (fused multiply accumulate) for 4*N taps +/// * stores less state compared with with a straight FIR +/// * as a FIR filter has linear phase/flat group delay +/// * very small passband ripple and excellent stopband attenuation +/// * as a cascade of decimation/interpolation filters, the higher-rate filters +/// need successively fewer taps, allowing the filtering to be dominated by +/// only the highest rate filter with the fewest taps +/// * high dynamic range (compared with a biquad IIR) +/// * can be combined with a CIC filter for non-power-of-two or even higher rate changes +/// +/// The implementations here are all `no_std` and `no-alloc`. +/// They support (but don't require) in-place filtering to reduce memory usage. +/// They unroll and optimmize extremely well targetting current architectures, +/// e.g. requiring less than 4 instructions per input item for the full `HbfDecCascade` on Skylake. +/// The filters are optimized for decent block sizes and perform best (i.e. with negligible +/// overhead) for blocks of 32 high-rate items or more, depending very much on architecture. + #[derive(Clone, Debug, Copy)] -struct SymFir<'a, const M: usize, const N: usize> { +pub struct SymFir<'a, const M: usize, const N: usize> { x: [f32; N], taps: &'a [f32; M], } impl<'a, const M: usize, const N: usize> SymFir<'a, M, N> { /// taps: one-sided, expluding center tap, oldest to one-before-center - fn new(taps: &'a [f32; M]) -> Self { + pub fn new(taps: &'a [f32; M]) -> Self { debug_assert!(N >= M * 2); Self { x: [0.0; N], taps } } /// Perform the FIR convolution and yield results iteratively. #[inline] - fn get(&self) -> impl Iterator + '_ { + pub fn get(&self) -> impl Iterator + '_ { self.x.windows(2 * M).map(|x| { let (old, new) = x.split_at(M); old.iter() diff --git a/src/psd.rs b/src/psd.rs index ec5717a..983698a 100644 --- a/src/psd.rs +++ b/src/psd.rs @@ -181,7 +181,20 @@ impl Stage for Psd { } } -/// Online PSD calculator +/// Online power spectral density calculation +/// +/// This performs efficient long term power spectral density monitoring in real time. +/// The idea is to make short FFTs and decimate in stages, then +/// stitch together the FFT bins from the different stages. +/// This allows arbitrarily large effective FFTs sizes in practice with only +/// logarithmically increasing memory consumption. And it gets rid of the delay in +/// recording and computing the large FFTs. The effective full FFT size grows in real-time +/// and does not need to be fixed before recording and computing. +/// This is well defined with the caveat that spur power depends on the changing bin width. +/// It's also typically what some modern signal analyzers or noise metrology instruments do. +/// +/// See also [`csdl`](https://github.com/jordens/csdl) or +/// [LPSD](https://doi.org/10.1016/j.measurement.2005.10.010). /// /// Infinite averaging /// Incremental updates @@ -205,7 +218,7 @@ impl Default for PsdCascade { let fft = FftPlanner::::new().plan_fft_forward(N); let win = Arc::new(Window::hann()); Self { - stages: vec![], + stages: Vec::with_capacity(4), fft, stage_length: 4, detrend: Detrend::None, From a914fe25d3a589e5247af91229c2690565ea559c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Robert=20J=C3=B6rdens?= Date: Fri, 1 Sep 2023 23:03:49 +0200 Subject: [PATCH 24/39] options --- src/bin/main.rs | 57 +++++++++++++----------------------------- src/bin/stream_test.rs | 29 ++++++++------------- src/de/frame.rs | 3 +++ src/lib.rs | 1 + src/psd.rs | 38 ++++++++++++++++------------ src/source.rs | 57 ++++++++++++++++++++++++++++++++++++++++++ 6 files changed, 112 insertions(+), 73 deletions(-) create mode 100644 src/source.rs diff --git a/src/bin/main.rs b/src/bin/main.rs index 5b0a0c4..f25828b 100644 --- a/src/bin/main.rs +++ b/src/bin/main.rs @@ -4,24 +4,13 @@ use anyhow::Result; use clap::Parser; use eframe::egui; use eframe::egui::plot::{Legend, Line, Plot, PlotPoints}; -// use std::io::{Read, Seek}; use std::sync::mpsc; use std::time::Duration; -use stabilizer_streaming::{Detrend, Frame, Loss, PsdCascade}; - -/// Execute stabilizer stream throughput testing. -/// Use `RUST_LOG=info cargo run` to increase logging verbosity. -#[derive(Parser)] -struct Opts { - /// The local IP to receive streaming data on. - #[clap(short, long, default_value = "0.0.0.0")] - ip: std::net::Ipv4Addr, - - /// The UDP port to receive streaming data on. - #[clap(long, long, default_value = "9293")] - port: u16, -} +use stabilizer_streaming::{ + source::{Source, SourceOpts}, + Detrend, Frame, Loss, PsdCascade, +}; #[derive(Clone, Copy, Debug)] enum Cmd { @@ -40,25 +29,19 @@ impl Trace { } fn main() -> Result<()> { - env_logger::init(); // Log to stderr (if you run with `RUST_LOG=debug`). - let opts = Opts::parse(); + env_logger::init(); + let opts = SourceOpts::parse(); let (cmd_send, cmd_recv) = mpsc::channel(); let (trace_send, trace_recv) = mpsc::sync_channel(1); let receiver = std::thread::spawn(move || { - log::info!("Binding to {}:{}", opts.ip, opts.port); - let socket = std::net::UdpSocket::bind((opts.ip, opts.port))?; - socket2::SockRef::from(&socket).set_recv_buffer_size(1 << 20)?; - socket.set_read_timeout(Some(Duration::from_millis(1000)))?; - log::info!("Receiving frames"); + let mut source = Source::new(&opts)?; let mut loss = Loss::default(); let mut dec = Vec::with_capacity(4); - // let mut fil = std::fs::File::open("/tmp/fls2x.raw")?; - let mut buf = vec![0; 2048]; - let mut i = 0; + let mut i = 0usize; loop { match cmd_recv.try_recv() { Err(mpsc::TryRecvError::Disconnected) | Ok(Cmd::Exit) => break, @@ -73,15 +56,10 @@ fn main() -> Result<()> { c.set_detrend(Detrend::Mean); c })); + i = 0; } - // let len = fil.read(&mut buf[..1400])?; - // if len == 0 { - // fil.seek(std::io::SeekFrom::Start(0))?; - // continue; - // } - - let len = socket.recv(&mut buf)?; + let len = source.get(&mut buf)?; match Frame::from_bytes(&buf[..len]) { Ok(frame) => { loss.update(&frame); @@ -93,21 +71,22 @@ fn main() -> Result<()> { } Err(e) => log::warn!("{e} {:?}", &buf[..8]), }; - if i >= 50 { + if i > 50 { i = 0; let trace = dec .iter() .map(|dec| { let (p, b) = dec.get(1); - let f = dec.f(&b); - Trace::new( + let f = dec.frequencies(&b); + let mut t = Vec::with_capacity(f.len()); + t.extend( f.iter() .zip(p.iter()) .rev() .skip(1) // DC - .map(|(f, p)| [f.log10() as f64, 10.0 * p.log10() as f64]) - .collect(), - ) + .map(|(f, p)| [f.log10() as f64, 10.0 * p.log10() as f64]), + ); + Trace::new(t) }) .collect(); match trace_send.try_send(trace) { @@ -193,7 +172,7 @@ impl eframe::App for FLS { .legend(Legend::default()); plot.show(ui, |plot_ui| { if let Some(traces) = &mut self.current { - for (trace, name) in traces.iter().zip(["AI", "AQ", "BI", "BQ"].into_iter()) + for (trace, name) in traces.iter().zip(["AR", "AT", "BI", "BQ"].into_iter()) { plot_ui.line(Line::new(PlotPoints::from(trace.psd.clone())).name(name)); } diff --git a/src/bin/stream_test.rs b/src/bin/stream_test.rs index 6f96192..c1cc5b4 100644 --- a/src/bin/stream_test.rs +++ b/src/bin/stream_test.rs @@ -1,23 +1,20 @@ use anyhow::Result; use clap::Parser; -use stabilizer_streaming::{Detrend, Frame, Loss, PsdCascade}; +use stabilizer_streaming::{ + source::{Source, SourceOpts}, + Detrend, Frame, Loss, PsdCascade, +}; use std::sync::mpsc; use std::time::Duration; /// Execute stabilizer stream throughput testing. /// Use `RUST_LOG=info cargo run` to increase logging verbosity. -#[derive(Parser)] -struct Opts { - /// The local IP to receive streaming data on. - #[clap(short, long, default_value = "0.0.0.0")] - ip: std::net::Ipv4Addr, +#[derive(Parser, Debug)] +pub struct Opts { + #[command(flatten)] + source: SourceOpts, - /// The UDP port to receive streaming data on. - #[clap(long, long, default_value = "9293")] - port: u16, - - /// The test duration in seconds. - #[clap(long, long, default_value = "5")] + #[arg(short, long, default_value_t = 10.0)] duration: f32, } @@ -27,11 +24,7 @@ fn main() -> Result<()> { let (cmd_send, cmd_recv) = mpsc::channel(); let receiver = std::thread::spawn(move || { - log::info!("Binding to {}:{}", opts.ip, opts.port); - let socket = std::net::UdpSocket::bind((opts.ip, opts.port))?; - socket2::SockRef::from(&socket).set_recv_buffer_size(1 << 20)?; - socket.set_read_timeout(Some(Duration::from_millis(100)))?; - log::info!("Receiving frames"); + let mut source = Source::new(&opts.source)?; let mut buf = vec![0u8; 2048]; let mut loss = Loss::default(); @@ -46,7 +39,7 @@ fn main() -> Result<()> { .collect(); while cmd_recv.try_recv() == Err(mpsc::TryRecvError::Empty) { - let len = socket.recv(&mut buf)?; + let len = source.get(&mut buf)?; match Frame::from_bytes(&buf[..len]) { Ok(frame) => { loss.update(&frame); diff --git a/src/de/frame.rs b/src/de/frame.rs index 6eaf0ac..8d3a148 100644 --- a/src/de/frame.rs +++ b/src/de/frame.rs @@ -25,6 +25,9 @@ pub struct Header { impl Header { /// Parse the header of a stream frame. fn parse(header: &[u8]) -> Result { + if header.len() < HEADER_SIZE { + return Err(Error::InvalidHeader); + } if header[..2] != MAGIC_WORD { return Err(Error::InvalidHeader); } diff --git a/src/lib.rs b/src/lib.rs index 7545241..599ef20 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -8,6 +8,7 @@ mod psd; pub use psd::*; mod loss; pub use loss::*; +pub mod source; #[derive(Debug, Error)] pub enum Error { diff --git a/src/psd.rs b/src/psd.rs index 983698a..22594ac 100644 --- a/src/psd.rs +++ b/src/psd.rs @@ -47,7 +47,7 @@ pub struct Psd { hbf: HbfDecCascade, buf: [f32; N], idx: usize, - spectrum: [f32; N], // using only the positive half + spectrum: [f32; N], // using only the positive half N/2 + 1 count: usize, fft: Arc>, win: Arc>, @@ -58,11 +58,10 @@ pub struct Psd { impl Psd { pub fn new(fft: Arc>, win: Arc>) -> Self { let hbf = HbfDecCascade::default(); - assert_eq!(N & 1, 0); assert_eq!(N, fft.len()); // check fft and decimation block size compatibility - assert!(hbf.block_size().0 <= N / 2); // needed for processing and dropping blocks - assert!(hbf.block_size().1 >= N / 2); // needed for processing and dropping blocks + assert_eq!((N / 2) % hbf.block_size().0, 0); + assert!(hbf.block_size().1 >= N / 2); assert!(N >= 2); // Nyquist and DC distinction Self { hbf, @@ -112,6 +111,7 @@ pub trait Stage { } impl Stage for Psd { + #[inline(never)] fn process(&mut self, mut x: &[f32], y: &mut [f32]) -> usize { let mut c = [Complex::default(); N]; let mut n = 0; @@ -147,7 +147,7 @@ impl Stage for Psd { self.fft.process(&mut c); // convert positive frequency spectrum to power // and accumulate - for (c, p) in c[..N / 2].iter().zip(self.spectrum.iter_mut()) { + for (c, p) in c[..N / 2 + 1].iter().zip(self.spectrum.iter_mut()) { *p += c.norm_sqr(); } self.count += 1; @@ -167,7 +167,7 @@ impl Stage for Psd { } fn spectrum(&self) -> &[f32] { - &self.spectrum[..N / 2] + &self.spectrum[..N / 2 + 1] } fn count(&self) -> usize { @@ -181,15 +181,17 @@ impl Stage for Psd { } } -/// Online power spectral density calculation +/// Online power spectral density estimation /// /// This performs efficient long term power spectral density monitoring in real time. -/// The idea is to make short FFTs and decimate in stages, then +/// The idea is to perform FFTs over relatively short windows and simultaneously decimate +/// the time domain data, everything in multiple stages, then /// stitch together the FFT bins from the different stages. /// This allows arbitrarily large effective FFTs sizes in practice with only -/// logarithmically increasing memory consumption. And it gets rid of the delay in +/// logarithmically increasing memory and cpu consumption. And it makes available PSD data +/// from higher frequency stages early to get rid of the delay in /// recording and computing the large FFTs. The effective full FFT size grows in real-time -/// and does not need to be fixed before recording and computing. +/// and does not need to be fixed. /// This is well defined with the caveat that spur power depends on the changing bin width. /// It's also typically what some modern signal analyzers or noise metrology instruments do. /// @@ -239,6 +241,10 @@ impl PsdCascade { self.detrend = d; } + pub fn count(&self) -> usize { + self.stages.get(0).map(|s| s.count).unwrap_or_default() * N + } + /// Process input items pub fn process(&mut self, x: &[f32]) { let mut a = ([0f32; N], [0f32; N]); @@ -269,12 +275,11 @@ impl PsdCascade { /// * `psd`: `Vec` normalized reversed (Nyquist first, DC last) /// * `breaks`: `Vec` of stage breaks `[start index in psd, average count, highest bin index, effective fft size]` pub fn get(&self, min_count: usize) -> (Vec, Vec<[usize; 4]>) { - let mut p = Vec::with_capacity(self.stages.len() * N / 2); + let mut p = Vec::with_capacity(self.stages.len() * (N / 2 + 1)); let mut b = Vec::with_capacity(self.stages.len()); let mut n = 0; for stage in self.stages.iter().take_while(|s| s.count >= min_count) { let mut pi = stage.spectrum(); - let f = stage.fft.len(); // a stage yields frequency bins 0..N/2 ty its nyquist // 0..floor(0.4*N) is its passband if it was preceeded by a decimator // 0..floor(0.4*N/R) is next lower stage @@ -282,14 +287,14 @@ impl PsdCascade { if !p.is_empty() { // not the first stage // remove transition band of previous stage's decimator, floor - let f_pass = 4 * f / 10; + let f_pass = 4 * N / 10; pi = &pi[..f_pass]; // remove low f bins from previous stage, ceil - let f_low = (4 * f + (10 << stage.hbf.n()) - 1) / (10 << stage.hbf.n()); + let f_low = (4 * N + (10 << stage.hbf.n()) - 1) / (10 << stage.hbf.n()); p.truncate(p.len() - f_low); } let g = stage.gain() * (1 << n) as f32; - b.push([p.len(), stage.count(), pi.len(), f << n]); + b.push([p.len(), stage.count(), pi.len(), N << n]); p.extend(pi.iter().rev().map(|pi| pi * g)); n += stage.hbf.n(); } @@ -305,7 +310,8 @@ impl PsdCascade { } /// Compute PSD bin center frequencies from stage breaks. - pub fn f(&self, b: &[[usize; 4]]) -> Vec { + #[inline(never)] + pub fn frequencies(&self, b: &[[usize; 4]]) -> Vec { let Some(bi) = b.last() else { return vec![] }; let mut f = Vec::with_capacity(bi[0] + bi[2]); for bi in b.iter() { diff --git a/src/source.rs b/src/source.rs new file mode 100644 index 0000000..abb5356 --- /dev/null +++ b/src/source.rs @@ -0,0 +1,57 @@ +use anyhow::Result; +use clap::Parser; +use std::io::{Read, Seek}; +use std::time::Duration; + +/// Stabilizer stream source options +#[derive(Parser, Debug, Clone)] +pub struct SourceOpts { + /// The local IP to receive streaming data on. + #[arg(short, long, default_value = "0.0.0.0")] + ip: std::net::Ipv4Addr, + + /// The UDP port to receive streaming data on. + #[arg(short, long, default_value_t = 9293)] + port: u16, + + /// Use frames from the given file + #[arg(short, long)] + file: Option, + + /// Frame size in file (8 + n_batches*n_channel*batch_size) + #[arg(short, long, default_value_t = 1400)] + frame_size: usize, +} + +#[derive(Debug)] +pub enum Source { + Udp(std::net::UdpSocket), + File(std::fs::File, usize), +} + +impl Source { + pub fn new(opts: &SourceOpts) -> Result { + Ok(if let Some(file) = &opts.file { + Self::File(std::fs::File::open(file)?, opts.frame_size) + } else { + log::info!("Binding to {}:{}", opts.ip, opts.port); + let socket = std::net::UdpSocket::bind((opts.ip, opts.port))?; + socket2::SockRef::from(&socket).set_recv_buffer_size(1 << 20)?; + socket.set_read_timeout(Some(Duration::from_millis(1000)))?; + Self::Udp(socket) + }) + } + + pub fn get(&mut self, buf: &mut [u8]) -> Result { + Ok(match self { + Self::File(fil, n) => loop { + let len = fil.read(&mut buf[..*n])?; + if len == *n { + break len; + } + fil.seek(std::io::SeekFrom::Start(0))?; + }, + Self::Udp(socket) => socket.recv(buf)?, + }) + } +} From 1b3d776921cef9ede16cc5bb6f2b1bf8f8c689dc Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Robert=20J=C3=B6rdens?= Date: Sun, 3 Sep 2023 00:02:52 +0200 Subject: [PATCH 25/39] tweaks --- src/bin/main.rs | 45 +++++---- src/bin/stream_test.rs | 4 +- src/hbf.rs | 77 +++++++++------ src/psd.rs | 215 +++++++++++++++++++++++++++++------------ 4 files changed, 228 insertions(+), 113 deletions(-) diff --git a/src/bin/main.rs b/src/bin/main.rs index f25828b..4a8deb1 100644 --- a/src/bin/main.rs +++ b/src/bin/main.rs @@ -9,7 +9,7 @@ use std::time::Duration; use stabilizer_streaming::{ source::{Source, SourceOpts}, - Detrend, Frame, Loss, PsdCascade, + Break, Detrend, Frame, Loss, PsdCascade, }; #[derive(Clone, Copy, Debug)] @@ -19,15 +19,10 @@ enum Cmd { } struct Trace { + breaks: Vec, psd: Vec<[f64; 2]>, } -impl Trace { - fn new(psd: Vec<[f64; 2]>) -> Self { - Self { psd } - } -} - fn main() -> Result<()> { env_logger::init(); let opts = SourceOpts::parse(); @@ -53,7 +48,7 @@ fn main() -> Result<()> { dec.extend((0..4).map(|_| { let mut c = PsdCascade::<{ 1 << 9 }>::default(); c.set_stage_length(3); - c.set_detrend(Detrend::Mean); + c.set_detrend(Detrend::Mid); c })); i = 0; @@ -71,22 +66,23 @@ fn main() -> Result<()> { } Err(e) => log::warn!("{e} {:?}", &buf[..8]), }; - if i > 50 { + if i > 100 { i = 0; let trace = dec .iter() .map(|dec| { - let (p, b) = dec.get(1); + let (p, b) = dec.psd(1); let f = dec.frequencies(&b); - let mut t = Vec::with_capacity(f.len()); - t.extend( - f.iter() - .zip(p.iter()) - .rev() - .skip(1) // DC - .map(|(f, p)| [f.log10() as f64, 10.0 * p.log10() as f64]), - ); - Trace::new(t) + Trace { + breaks: b, + psd: Vec::from_iter( + f.iter() + .zip(p.iter()) + .rev() + .skip(1) // DC + .map(|(f, p)| [f.log10() as f64, 10.0 * p.log10() as f64]), + ), + } }) .collect(); match trace_send.try_send(trace) { @@ -185,7 +181,16 @@ impl eframe::App for FLS { if ui.button("Reset").clicked() { self.cmd_send.send(Cmd::Reset).unwrap(); } - ui.label("Every"); + self.current + .as_ref() + .and_then(|ts| ts.get(0)) + .and_then(|t| t.breaks.get(0)) + .map(|bi| { + ui.label(format!( + "{:.2e} samples", // includes overlap + (bi.count * bi.effective_fft_size) as f32 + )) + }); }); }); } diff --git a/src/bin/stream_test.rs b/src/bin/stream_test.rs index c1cc5b4..d0e0277 100644 --- a/src/bin/stream_test.rs +++ b/src/bin/stream_test.rs @@ -33,7 +33,7 @@ fn main() -> Result<()> { .map(|_| { let mut c = PsdCascade::<{ 1 << 9 }>::default(); c.set_stage_length(3); - c.set_detrend(Detrend::Mean); + c.set_detrend(Detrend::Mid); c }) .collect(); @@ -53,7 +53,7 @@ fn main() -> Result<()> { loss.analyze(); - let (y, b) = dec[1].get(4); + let (y, b) = dec[1].psd(4); println!("{:?}, {:?}", b, y); Result::<()>::Ok(()) diff --git a/src/hbf.rs b/src/hbf.rs index cbc947a..926eb7f 100644 --- a/src/hbf.rs +++ b/src/hbf.rs @@ -23,7 +23,7 @@ pub trait Filter { /// The granularity is also the rate change in the case of interpolation/decimation filters. fn block_size(&self) -> (usize, usize); - /// Finite impulse response length in numer of output items + /// Finite impulse response length in numer of output items minus one /// Get this many to drain all previous memory fn response_length(&self) -> usize; @@ -181,7 +181,7 @@ impl<'a, const M: usize, const N: usize> Filter for HbfInt<'a, M, N> { #[inline] fn response_length(&self) -> usize { - 4 * M - 1 + 4 * M - 2 } fn process_block(&mut self, x: Option<&[Self::Item]>, y: &mut [Self::Item]) -> usize { @@ -254,7 +254,7 @@ pub const HBF_TAPS: ([f32; 15], [f32; 6], [f32; 3], [f32; 3], [f32; 2]) = ( pub const HBF_PASSBAND: f32 = 0.4; /// Max low-rate block size (HbfIntCascade input, HbfDecCascade output) -pub const HBF_CASCADE_BLOCK: usize = 1 << 8; +pub const HBF_CASCADE_BLOCK: usize = 1 << 6; /// Half-band decimation filter cascade with optimal taps /// @@ -287,11 +287,13 @@ impl Default for HbfDecCascade { } impl HbfDecCascade { + #[inline] pub fn set_n(&mut self, n: usize) { assert!(n <= 4); self.n = n; } + #[inline] pub fn n(&self) -> usize { self.n } @@ -317,21 +319,17 @@ impl Filter for HbfDecCascade { #[inline] fn response_length(&self) -> usize { let mut n = 0; - if self.n > 0 { - n *= 2; - n += self.stages.0.response_length(); - } - if self.n > 1 { - n *= 2; - n += self.stages.1.response_length(); + if self.n > 3 { + n = n / 2 + self.stages.3.response_length(); } if self.n > 2 { - n *= 2; - n += self.stages.2.response_length(); + n = n / 2 + self.stages.2.response_length(); } - if self.n > 3 { - n *= 2; - n += self.stages.3.response_length(); + if self.n > 1 { + n = n / 2 + self.stages.1.response_length(); + } + if self.n > 0 { + n = n / 2 + self.stages.0.response_length(); } n } @@ -421,20 +419,16 @@ impl Filter for HbfIntCascade { fn response_length(&self) -> usize { let mut n = 0; if self.n > 0 { - n *= 2; - n += self.stages.0.response_length(); + n = 2 * n + self.stages.0.response_length(); } if self.n > 1 { - n *= 2; - n += self.stages.1.response_length(); + n = 2 * n + self.stages.1.response_length(); } if self.n > 2 { - n *= 2; - n += self.stages.2.response_length(); + n = 2 * n + self.stages.2.response_length(); } if self.n > 3 { - n *= 2; - n += self.stages.3.response_length(); + n = 2 * n + self.stages.3.response_length(); } n } @@ -499,11 +493,14 @@ mod test { let mut h = HbfIntCascade::default(); h.set_n(4); assert_eq!(h.block_size(), (1 << h.n(), HBF_CASCADE_BLOCK << h.n())); - let mut x = vec![0.0; h.response_length() + 1]; + let k = h.block_size().0; + let r = h.response_length(); + let mut x = vec![0.0; (r + 1 + k - 1) / k * k]; x[0] = 1.0; let n = h.process_block(None, &mut x); println!("{:?}", &x[..n]); // interpolator impulse response - assert_eq!(x[x.len() - 1], 0.0); + assert!(x[r] != 0.0); + assert_eq!(x[r + 1..], vec![0.0; x.len() - r - 1]); let g = (1 << h.n()) as f32; let mut y = Vec::from_iter(x[..n].iter().map(|&x| Complex { re: x / g, im: 0.0 })); @@ -525,25 +522,43 @@ mod test { assert!(p_stop < -98.4); } - /// small batch size single 3 mul (11 tap) decimator + /// small 32 batch size, single stage, 3 mul (11 tap) decimator + /// 3.5 insn per input sample, > 1 GS/s on Skylake #[test] #[ignore] fn insn_dec() { - let mut h = HbfDec::<3, { 2 * 3 - 1 + (1 << 4) }>::new(&HBF_TAPS.3); + const N: usize = HBF_TAPS.3.len(); + let mut h = HbfDec::::new(&HBF_TAPS.3); let mut x = [9.0; 1 << 5]; for _ in 0..1 << 26 { h.process_block(None, &mut x); } } - // large batch size full decimator cascade + /// 1k batch size, single stage, 15 mul (59 tap) decimator + /// 5 insn per input sample, > 1 GS/s on Skylake + #[test] + #[ignore] + fn insn_dec2() { + const N: usize = HBF_TAPS.0.len(); + assert_eq!(N, 15); + const M: usize = 1 << 10; + let mut h = HbfDec::::new(&HBF_TAPS.0); + let mut x = [9.0; M]; + for _ in 0..1 << 20 { + h.process_block(None, &mut x); + } + } + + /// large batch size full decimator cascade (depth 4, 1024 sampled per batch) + /// 4.1 insns per input sample, > 1 GS/s per core #[test] #[ignore] fn insn_casc() { - let mut x = [9.0; 1 << 8]; + let mut x = [9.0; 1 << 10]; let mut h = HbfDecCascade::default(); - h.set_n(3); - for _ in 0..1 << 22 { + h.set_n(4); + for _ in 0..1 << 20 { h.process_block(None, &mut x); } } diff --git a/src/psd.rs b/src/psd.rs index 22594ac..6c6fa16 100644 --- a/src/psd.rs +++ b/src/psd.rs @@ -6,6 +6,7 @@ use std::sync::Arc; #[derive(Copy, Clone, Debug, PartialEq)] pub struct Window { pub win: [f32; N], + /// Mean squared pub power: f32, /// Normalized effective noise bandwidth (in bins) pub nenbw: f32, @@ -13,6 +14,21 @@ pub struct Window { /// Hann window impl Window { + pub fn rectangular() -> Self { + assert!(N > 0); + Self { + win: [1.0; N], + power: 1.0, + nenbw: 1.0, + } + } + + /// Hann window + /// + /// This is the "numerical" version of the window with period N, win[0] = win[N] + /// (conceptually), specifically win[0] != win[win.len() - 1]. + /// Matplotlib uses the symetric one of period N-1, with win[0] = win[N - 1] = 0 + /// which looses a lot of useful properties (exact nenbw and power independent of N) pub fn hann() -> Self { assert!(N > 0); let df = core::f32::consts::PI / N as f32; @@ -22,7 +38,7 @@ impl Window { } Self { win, - power: 4.0, + power: 0.25, nenbw: 1.5, } } @@ -33,10 +49,10 @@ impl Window { pub enum Detrend { /// No detrending None, - /// Remove the mean of first and last item per segment - Mean, + /// Subtract the midpoint of segment + Mid, /// Remove linear interpolation between first and last item for each segment - Linear, + Span, } /// Power spectral density accumulator and decimator @@ -51,6 +67,7 @@ pub struct Psd { count: usize, fft: Arc>, win: Arc>, + overlap: usize, detrend: Detrend, drain: usize, } @@ -60,10 +77,8 @@ impl Psd { let hbf = HbfDecCascade::default(); assert_eq!(N, fft.len()); // check fft and decimation block size compatibility - assert_eq!((N / 2) % hbf.block_size().0, 0); - assert!(hbf.block_size().1 >= N / 2); assert!(N >= 2); // Nyquist and DC distinction - Self { + let mut s = Self { hbf, buf: [0.0; N], idx: 0, @@ -71,9 +86,20 @@ impl Psd { count: 0, fft, win, + overlap: 0, detrend: Detrend::None, drain: 0, - } + }; + s.set_overlap(N / 2); + s.set_stage_length(0); + s + } + + pub fn set_overlap(&mut self, o: usize) { + assert_eq!(o % self.hbf.block_size().0, 0); + assert!(self.hbf.block_size().1 >= o); + assert!(o < N); + self.overlap = o; } pub fn set_detrend(&mut self, d: Detrend) { @@ -107,13 +133,12 @@ pub trait Stage { /// /// one-sided fn gain(&self) -> f32; + /// Number of averages fn count(&self) -> usize; } impl Stage for Psd { - #[inline(never)] fn process(&mut self, mut x: &[f32], y: &mut [f32]) -> usize { - let mut c = [Complex::default(); N]; let mut n = 0; while !x.is_empty() { // load @@ -126,42 +151,43 @@ impl Stage for Psd { break; } // compute detrend - let (mean, slope) = match self.detrend { + let (mut mean, slope) = match self.detrend { Detrend::None => (0.0, 0.0), - Detrend::Mean => (0.5 * (self.buf[0] + self.buf[N - 1]), 0.0), - Detrend::Linear => ( + Detrend::Mid => (self.buf[N / 2], 0.0), + Detrend::Span => ( self.buf[0], (self.buf[N - 1] - self.buf[0]) / (N - 1) as f32, ), }; // apply detrending, window, make complex - for (i, (c, (x, w))) in c - .iter_mut() - .zip(self.buf.iter().zip(self.win.win.iter())) - .enumerate() - { - c.re = (x - mean - i as f32 * slope) * w; - c.im = 0.0; + let mut c = [Complex::default(); N]; + for (c, (x, w)) in c.iter_mut().zip(self.buf.iter().zip(self.win.win.iter())) { + c.re = (x - mean) * w; + mean += slope; } // fft in-place self.fft.process(&mut c); // convert positive frequency spectrum to power // and accumulate - for (c, p) in c[..N / 2 + 1].iter().zip(self.spectrum.iter_mut()) { + // TODO: accuracy for large counts + for (c, p) in c[..N / 2 + 1] + .iter() + .zip(self.spectrum[..N / 2 + 1].iter_mut()) + { *p += c.norm_sqr(); } self.count += 1; - // decimate left half - let (left, right) = self.buf.split_at_mut(N / 2); - let mut k = self.hbf.process_block(None, left); + // decimate overlap + let mut k = self.hbf.process_block(None, &mut self.buf[..self.overlap]); // drain decimator impulse response to initial state (zeros) (k, self.drain) = (k.saturating_sub(self.drain), self.drain.saturating_sub(k)); - y[n..][..k].copy_from_slice(&left[..k]); + // yield k + y[n..][..k].copy_from_slice(&self.buf[..k]); n += k; // drop the left keep the right as overlap - left.copy_from_slice(right); - self.idx = N / 2; + self.buf.copy_within(self.overlap..N, 0); + self.idx = N - self.overlap; } n } @@ -176,11 +202,24 @@ impl Stage for Psd { fn gain(&self) -> f32 { // 2 for one-sided - // overlap compensated by counting - self.win.power / ((self.count * N / 2) as f32 * self.win.nenbw) + // overlap is compensated by counting + 1.0 / ((self.count * N / 2) as f32 * self.win.nenbw * self.win.power) } } +/// Stage break information +#[derive(Copy, Clone, Debug, PartialEq, Eq, PartialOrd, Ord, Hash)] +pub struct Break { + /// Start index in PSD and frequencies + pub start: usize, + /// Number of averages + pub count: usize, + /// Highes FFT bin (at `start`) + pub highest_bin: usize, + /// The effective FFT size + pub effective_fft_size: usize, +} + /// Online power spectral density estimation /// /// This performs efficient long term power spectral density monitoring in real time. @@ -207,6 +246,7 @@ pub struct PsdCascade { fft: Arc>, stage_length: usize, detrend: Detrend, + overlap: usize, win: Arc>, } @@ -222,15 +262,21 @@ impl Default for PsdCascade { Self { stages: Vec::with_capacity(4), fft, - stage_length: 4, + stage_length: 1, detrend: Detrend::None, + overlap: N / 2, win, } } } impl PsdCascade { + pub fn set_window(&mut self, win: Window) { + self.win = Arc::new(win); + } + pub fn set_stage_length(&mut self, n: usize) { + assert!(n > 0); self.stage_length = n; for stage in self.stages.iter_mut() { stage.set_stage_length(n); @@ -241,10 +287,6 @@ impl PsdCascade { self.detrend = d; } - pub fn count(&self) -> usize { - self.stages.get(0).map(|s| s.count).unwrap_or_default() * N - } - /// Process input items pub fn process(&mut self, x: &[f32]) { let mut a = ([0f32; N], [0f32; N]); @@ -256,9 +298,10 @@ impl PsdCascade { let mut stage = Psd::new(self.fft.clone(), self.win.clone()); stage.set_stage_length(self.stage_length); stage.set_detrend(self.detrend); + stage.set_overlap(self.overlap); self.stages.push(stage); } - let n = self.stages[i].process(x, &mut y[..]); + let n = self.stages[i].process(x, y); core::mem::swap(&mut z, &mut y); x = &z[..n]; i += 1; @@ -273,8 +316,8 @@ impl PsdCascade { /// /// # Returns /// * `psd`: `Vec` normalized reversed (Nyquist first, DC last) - /// * `breaks`: `Vec` of stage breaks `[start index in psd, average count, highest bin index, effective fft size]` - pub fn get(&self, min_count: usize) -> (Vec, Vec<[usize; 4]>) { + /// * `breaks`: `Vec` of stage breaks + pub fn psd(&self, min_count: usize) -> (Vec, Vec) { let mut p = Vec::with_capacity(self.stages.len() * (N / 2 + 1)); let mut b = Vec::with_capacity(self.stages.len()); let mut n = 0; @@ -294,14 +337,20 @@ impl PsdCascade { p.truncate(p.len() - f_low); } let g = stage.gain() * (1 << n) as f32; - b.push([p.len(), stage.count(), pi.len(), N << n]); + b.push(Break { + start: p.len(), + count: stage.count(), + highest_bin: pi.len(), + effective_fft_size: N << n, + }); p.extend(pi.iter().rev().map(|pi| pi * g)); n += stage.hbf.n(); } if !p.is_empty() { // correct DC and Nyquist bins as both only contribute once to the one-sided spectrum // this matches matplotlib and matlab but is certainly a questionable step - // need special care when interpreting and integrating the PSD + // need special care when interpreting and integrating the PSD: DC and nyquist bins + // must be counted as only half the width as the "usual" bins 0 < i < N/2 p[0] *= 0.5; let n = p.len(); p[n - 1] *= 0.5; @@ -310,16 +359,15 @@ impl PsdCascade { } /// Compute PSD bin center frequencies from stage breaks. - #[inline(never)] - pub fn frequencies(&self, b: &[[usize; 4]]) -> Vec { + pub fn frequencies(&self, b: &[Break]) -> Vec { let Some(bi) = b.last() else { return vec![] }; - let mut f = Vec::with_capacity(bi[0] + bi[2]); + let mut f = Vec::with_capacity(bi.start + bi.highest_bin); for bi in b.iter() { - f.truncate(bi[0]); - let df = 1.0 / bi[3] as f32; - f.extend((0..bi[2]).rev().map(|f| f as f32 * df)); + f.truncate(bi.start); + let df = 1.0 / bi.effective_fft_size as f32; + f.extend((0..bi.highest_bin).rev().map(|f| f as f32 * df)); } - assert_eq!(f.len(), bi[0] + bi[2]); + assert_eq!(f.len(), bi.start + bi.highest_bin); f } } @@ -328,6 +376,52 @@ impl PsdCascade { mod test { use super::*; + /// 44 insns per input sample: > 100 MS/s per core + #[test] + #[ignore] + fn insn() { + let mut s = PsdCascade::<{ 1 << 9 }>::default(); + s.set_stage_length(3); + s.set_detrend(Detrend::Mid); + let x: Vec<_> = (0..1 << 16) + .map(|_| rand::random::() * 2.0 - 1.0) + .collect(); + for _ in 0..1 << 11 { + s.process(&x); + } + } + + /// full accuracy tests + #[test] + fn exact() { + const N: usize = 4; + let mut s = Psd::::new( + FftPlanner::new().plan_fft_forward(N), + Arc::new(Window::rectangular()), + ); + let x = vec![1.0; N]; + let mut y = vec![0.0; N]; + let k = s.process(&x, &mut y[..]); + y.truncate(k); + assert_eq!(&y, &x[..N / 2]); + println!("{:?}, {}", s.spectrum(), s.gain()); + + let mut s = PsdCascade::::default(); + s.set_window(Window::hann()); + s.process(&x); + let (p, b) = s.psd(0); + let f = s.frequencies(&b); + println!("{:?}, {:?}", p, f); + assert!(p + .iter() + .zip([0.0, 4.0 / 3.0, 8.0 / 3.0].iter()) + .all(|(p, p0)| (p - p0).abs() < 1e-7)); + assert!(f + .iter() + .zip([0.5, 0.25, 0.0].iter()) + .all(|(p, p0)| (p - p0).abs() < 1e-7)); + } + #[test] fn test() { assert_eq!(crate::HBF_PASSBAND, 0.4); @@ -343,19 +437,20 @@ mod test { // variance is 1/3 assert!((xv * 3.0 - 1.0).abs() < 10.0 / (x.len() as f32).sqrt()); - const F: usize = 1 << 9; + const N: usize = 1 << 9; let n = 3; - let mut s = Psd::::new(FftPlanner::new().plan_fft_forward(F), Window::hann()); + let mut s = Psd::::new( + FftPlanner::new().plan_fft_forward(N), + Arc::new(Window::hann()), + ); s.set_stage_length(n); - let mut y = vec![]; - for x in x.chunks(F << n) { - let mut yi = [0.0; F]; - let k = s.process(x, &mut yi[..]); - y.extend_from_slice(&yi[..k]); - } + let mut y = vec![0.0; x.len() >> n]; + let k = s.process(&x, &mut y[..]); + y.truncate(k); + let mut hbf = HbfDecCascade::default(); hbf.set_n(n); - assert_eq!(y.len(), ((x.len() - F / 2) >> n) - hbf.response_length()); + assert_eq!(y.len(), ((x.len() - N / 2) >> n) - hbf.response_length()); let p: Vec<_> = s.spectrum().iter().map(|p| p * s.gain()).collect(); // psd of a stage assert!( @@ -366,24 +461,24 @@ mod test { &p[..] ); - let mut d = PsdCascade::::default(); + let mut d = PsdCascade::::default(); d.set_stage_length(n); d.set_detrend(Detrend::None); d.process(&x); - let (mut p, b) = d.get(1); + let (mut p, b) = d.psd(1); // tweak DC and Nyquist to make checks less code let n = p.len(); p[0] *= 2.0; p[n - 1] *= 2.0; for (i, bi) in b.iter().enumerate() { // let (start, count, high, size) = bi.into(); - let end = b.get(i + 1).map(|bi| bi[0]).unwrap_or(n); - let pi = &p[bi[0]..end]; + let end = b.get(i + 1).map(|bi| bi.start).unwrap_or(n); + let pi = &p[bi.start..end]; // psd of the cascade assert!(pi .iter() // 0.5 for one-sided spectrum - .all(|p| (p * 0.5 * 3.0 - 1.0).abs() < 10.0 / (bi[1] as f32).sqrt())); + .all(|p| (p * 0.5 * 3.0 - 1.0).abs() < 10.0 / (bi.count as f32).sqrt())); } } } From a646f6391309a3776e68fb27cbb7a9dd37e5e6dd Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Robert=20J=C3=B6rdens?= Date: Wed, 6 Sep 2023 20:49:28 +0200 Subject: [PATCH 26/39] rm perf stuff from gitignore --- .gitignore | 2 -- 1 file changed, 2 deletions(-) diff --git a/.gitignore b/.gitignore index 8b8a38d..676c9ca 100644 --- a/.gitignore +++ b/.gitignore @@ -8,5 +8,3 @@ # These are backup files generated by rustfmt **/*.rs.bk - -/perf.data* From 0ea36325067ac153806f4cc18c06e8b5556553dc Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Robert=20J=C3=B6rdens?= Date: Sun, 10 Sep 2023 19:07:42 +0200 Subject: [PATCH 27/39] fix drain --- src/psd.rs | 11 ++++++----- 1 file changed, 6 insertions(+), 5 deletions(-) diff --git a/src/psd.rs b/src/psd.rs index 6c6fa16..671fd30 100644 --- a/src/psd.rs +++ b/src/psd.rs @@ -179,12 +179,13 @@ impl Stage for Psd { self.count += 1; // decimate overlap - let mut k = self.hbf.process_block(None, &mut self.buf[..self.overlap]); + let k = self.hbf.process_block(None, &mut self.buf[..self.overlap]); // drain decimator impulse response to initial state (zeros) - (k, self.drain) = (k.saturating_sub(self.drain), self.drain.saturating_sub(k)); - // yield k - y[n..][..k].copy_from_slice(&self.buf[..k]); - n += k; + let l = k.saturating_sub(self.drain); + self.drain = self.drain.saturating_sub(k); + // yield l + y[n..][..l].copy_from_slice(&self.buf[k - l..k]); + n += l; // drop the left keep the right as overlap self.buf.copy_within(self.overlap..N, 0); self.idx = N - self.overlap; From 736b96ffab91b7ce0e10202d3353bc1c3a0359df Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Robert=20J=C3=B6rdens?= Date: Sun, 10 Sep 2023 19:31:02 +0200 Subject: [PATCH 28/39] doc --- src/bin/main.rs | 15 ++++++++-- src/hbf.rs | 80 ++++++++++++++++++++++++++++++------------------- src/lib.rs | 1 + src/psd.rs | 79 +++++++++++++++++++++++++++--------------------- 4 files changed, 108 insertions(+), 67 deletions(-) diff --git a/src/bin/main.rs b/src/bin/main.rs index 4a8deb1..248f98c 100644 --- a/src/bin/main.rs +++ b/src/bin/main.rs @@ -23,14 +23,23 @@ struct Trace { psd: Vec<[f64; 2]>, } +#[derive(Parser, Debug)] +pub struct Opts { + #[command(flatten)] + source: SourceOpts, + + #[arg(short, long, default_value_t = 4)] + min_avg: usize, +} + fn main() -> Result<()> { env_logger::init(); - let opts = SourceOpts::parse(); + let opts = Opts::parse(); let (cmd_send, cmd_recv) = mpsc::channel(); let (trace_send, trace_recv) = mpsc::sync_channel(1); let receiver = std::thread::spawn(move || { - let mut source = Source::new(&opts)?; + let mut source = Source::new(&opts.source)?; let mut loss = Loss::default(); let mut dec = Vec::with_capacity(4); @@ -71,7 +80,7 @@ fn main() -> Result<()> { let trace = dec .iter() .map(|dec| { - let (p, b) = dec.psd(1); + let (p, b) = dec.psd(opts.min_avg); let f = dec.frequencies(&b); Trace { breaks: b, diff --git a/src/hbf.rs b/src/hbf.rs index 926eb7f..9c6db40 100644 --- a/src/hbf.rs +++ b/src/hbf.rs @@ -8,13 +8,17 @@ pub trait Filter { /// Input items can be either in `x` or in `y`. /// In the latter case the filtering operation is done in-place. /// Output is always written into `y`. - /// The number of items written into `y` is returned. + /// The slice of items written into `y` is returned. /// Input and output size relations must match the filter requirements /// (decimation/interpolation and maximum block size). /// When using in-place operation, `y` needs to contain the input items /// (fewer than `y.len()` in the case of interpolation) and must be able to /// contain the output items. - fn process_block(&mut self, x: Option<&[Self::Item]>, y: &mut [Self::Item]) -> usize; + fn process_block<'a>( + &mut self, + x: Option<&[Self::Item]>, + y: &'a mut [Self::Item], + ) -> &'a mut [Self::Item]; /// Return the block size granularity and the maximum block size. /// @@ -124,7 +128,11 @@ impl<'a, const M: usize, const N: usize> Filter for HbfDec<'a, M, N> { 2 * M - 1 } - fn process_block(&mut self, x: Option<&[Self::Item]>, y: &mut [Self::Item]) -> usize { + fn process_block<'b>( + &mut self, + x: Option<&[Self::Item]>, + y: &'b mut [Self::Item], + ) -> &'b mut [Self::Item] { let x = x.unwrap_or(y); debug_assert_eq!(x.len() & 1, 0); let k = x.len() / 2; @@ -147,7 +155,7 @@ impl<'a, const M: usize, const N: usize> Filter for HbfDec<'a, M, N> { // keep state self.even.copy_within(k..k + M - 1, 0); self.odd.x.copy_within(k..k + 2 * M - 1, 0); - k + &mut y[..k] } } @@ -184,7 +192,11 @@ impl<'a, const M: usize, const N: usize> Filter for HbfInt<'a, M, N> { 4 * M - 2 } - fn process_block(&mut self, x: Option<&[Self::Item]>, y: &mut [Self::Item]) -> usize { + fn process_block<'b>( + &mut self, + x: Option<&[Self::Item]>, + y: &'b mut [Self::Item], + ) -> &'b mut [Self::Item] { debug_assert_eq!(y.len() & 1, 0); let k = y.len() / 2; let x = x.unwrap_or(&y[..k]); @@ -200,7 +212,7 @@ impl<'a, const M: usize, const N: usize> Filter for HbfInt<'a, M, N> { } // keep state self.fir.x.copy_within(k..k + 2 * M - 1, 0); - y.len() + y } } @@ -334,26 +346,30 @@ impl Filter for HbfDecCascade { n } - fn process_block(&mut self, x: Option<&[f32]>, y: &mut [f32]) -> usize { + fn process_block<'a>( + &mut self, + x: Option<&[Self::Item]>, + mut y: &'a mut [Self::Item], + ) -> &'a mut [Self::Item] { if x.is_some() { unimplemented!(); // TODO: pair of intermediate buffers } + let n = y.len(); - let mut n = y.len(); if self.n > 3 { - n = self.stages.3.process_block(None, &mut y[..n]); + y = self.stages.3.process_block(None, y); } if self.n > 2 { - n = self.stages.2.process_block(None, &mut y[..n]); + y = self.stages.2.process_block(None, y); } if self.n > 1 { - n = self.stages.1.process_block(None, &mut y[..n]); + y = self.stages.1.process_block(None, y); } if self.n > 0 { - n = self.stages.0.process_block(None, &mut y[..n]); + y = self.stages.0.process_block(None, y); } - debug_assert_eq!(n, y.len() >> self.n); - n + debug_assert_eq!(y.len(), n >> self.n); + y } } @@ -433,26 +449,30 @@ impl Filter for HbfIntCascade { n } - fn process_block(&mut self, x: Option<&[f32]>, y: &mut [f32]) -> usize { + fn process_block<'a>( + &mut self, + x: Option<&[Self::Item]>, + y: &'a mut [Self::Item], + ) -> &'a mut [Self::Item] { if x.is_some() { unimplemented!(); // TODO: one intermediate buffer and `y` } let mut n = y.len() >> self.n; if self.n > 0 { - n = self.stages.0.process_block(None, &mut y[..2 * n]); + n = self.stages.0.process_block(None, &mut y[..2 * n]).len(); } if self.n > 1 { - n = self.stages.1.process_block(None, &mut y[..2 * n]); + n = self.stages.1.process_block(None, &mut y[..2 * n]).len(); } if self.n > 2 { - n = self.stages.2.process_block(None, &mut y[..2 * n]); + n = self.stages.2.process_block(None, &mut y[..2 * n]).len(); } if self.n > 3 { - n = self.stages.3.process_block(None, &mut y[..2 * n]); + n = self.stages.3.process_block(None, &mut y[..2 * n]).len(); } debug_assert_eq!(n, y.len()); - n + &mut y[..n] } } @@ -464,18 +484,18 @@ mod test { #[test] fn test() { let mut h = HbfDec::<1, 5>::new(&[0.25]); - assert_eq!(h.process_block(None, &mut []), 0); + assert_eq!(h.process_block(None, &mut []), &[]); let mut x = [1.0; 8]; assert_eq!((2, x.len()), h.block_size()); - let n = h.process_block(None, &mut x); - assert_eq!(x[..n], [0.75, 1.0, 1.0, 1.0]); + let x = h.process_block(None, &mut x); + assert_eq!(x, [0.75, 1.0, 1.0, 1.0]); let mut h = HbfDec::<3, 9>::new(&HBF_TAPS.3); let mut x: Vec<_> = (0..8).map(|i| i as f32).collect(); assert_eq!((2, x.len()), h.block_size()); - let n = h.process_block(None, &mut x); - println!("{:?}", &x[..n]); + let x = h.process_block(None, &mut x); + println!("{:?}", x); } #[test] @@ -484,8 +504,8 @@ mod test { h.set_n(4); assert_eq!(h.block_size(), (1 << h.n(), HBF_CASCADE_BLOCK << h.n())); let mut x: Vec<_> = (0..2 << h.n()).map(|i| i as f32).collect(); - let n = h.process_block(None, &mut x); - println!("{:?}", &x[..n]); + let x = h.process_block(None, &mut x); + println!("{:?}", x); } #[test] @@ -497,13 +517,13 @@ mod test { let r = h.response_length(); let mut x = vec![0.0; (r + 1 + k - 1) / k * k]; x[0] = 1.0; - let n = h.process_block(None, &mut x); - println!("{:?}", &x[..n]); // interpolator impulse response + let x = h.process_block(None, &mut x); + println!("{:?}", x); // interpolator impulse response assert!(x[r] != 0.0); assert_eq!(x[r + 1..], vec![0.0; x.len() - r - 1]); let g = (1 << h.n()) as f32; - let mut y = Vec::from_iter(x[..n].iter().map(|&x| Complex { re: x / g, im: 0.0 })); + let mut y = Vec::from_iter(x.iter().map(|&x| Complex { re: x / g, im: 0.0 })); // pad y.resize(5 << 10, Complex::default()); FftPlanner::new().plan_fft_forward(y.len()).process(&mut y); diff --git a/src/lib.rs b/src/lib.rs index 599ef20..fda8d6d 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -8,6 +8,7 @@ mod psd; pub use psd::*; mod loss; pub use loss::*; + pub mod source; #[derive(Debug, Error)] diff --git a/src/psd.rs b/src/psd.rs index 671fd30..6ae252a 100644 --- a/src/psd.rs +++ b/src/psd.rs @@ -3,6 +3,10 @@ use rustfft::{num_complex::Complex, Fft, FftPlanner}; use std::sync::Arc; /// Window kernel +/// +/// https://holometer.fnal.gov/GH_FFT.pdf +/// https://gist.github.com/endolith/c4b8e1e3c630a260424123b4e9d964c4 +/// https://docs.google.com/spreadsheets/d/1glvo-y1tqCiYwK0QQWhB4AAcDFiK_C_0M4SeA0Uyqdc/edit #[derive(Copy, Clone, Debug, PartialEq)] pub struct Window { pub win: [f32; N], @@ -12,8 +16,8 @@ pub struct Window { pub nenbw: f32, } -/// Hann window impl Window { + /// Rectangular window pub fn rectangular() -> Self { assert!(N > 0); Self { @@ -28,7 +32,8 @@ impl Window { /// This is the "numerical" version of the window with period N, win[0] = win[N] /// (conceptually), specifically win[0] != win[win.len() - 1]. /// Matplotlib uses the symetric one of period N-1, with win[0] = win[N - 1] = 0 - /// which looses a lot of useful properties (exact nenbw and power independent of N) + /// which looses a lot of useful properties (exact nenbw and power independent of N, + /// exact optimal overlap etc) pub fn hann() -> Self { assert!(N > 0); let df = core::f32::consts::PI / N as f32; @@ -112,7 +117,7 @@ impl Psd { } } -pub trait Stage { +pub trait PsdStage { /// Process items /// /// Unused items are buffered. @@ -126,7 +131,7 @@ pub trait Stage { /// /// # Returns /// number if items written to `y` - fn process(&mut self, x: &[f32], y: &mut [f32]) -> usize; + fn process<'a>(&mut self, x: &[f32], y: &'a mut [f32]) -> &'a mut [f32]; /// Return the positive frequency half of the spectrum fn spectrum(&self) -> &[f32]; /// PSD normalization factor @@ -137,8 +142,8 @@ pub trait Stage { fn count(&self) -> usize; } -impl Stage for Psd { - fn process(&mut self, mut x: &[f32], y: &mut [f32]) -> usize { +impl PsdStage for Psd { + fn process<'a>(&mut self, mut x: &[f32], y: &'a mut [f32]) -> &'a mut [f32] { let mut n = 0; while !x.is_empty() { // load @@ -179,18 +184,19 @@ impl Stage for Psd { self.count += 1; // decimate overlap - let k = self.hbf.process_block(None, &mut self.buf[..self.overlap]); + let mut yi = self.hbf.process_block(None, &mut self.buf[..self.overlap]); // drain decimator impulse response to initial state (zeros) - let l = k.saturating_sub(self.drain); - self.drain = self.drain.saturating_sub(k); + let skip = self.drain.min(yi.len()); + self.drain -= skip; + yi = &mut yi[skip..]; // yield l - y[n..][..l].copy_from_slice(&self.buf[k - l..k]); - n += l; + y[n..][..yi.len()].copy_from_slice(yi); + n += yi.len(); // drop the left keep the right as overlap self.buf.copy_within(self.overlap..N, 0); self.idx = N - self.overlap; } - n + &mut y[..n] } fn spectrum(&self) -> &[f32] { @@ -288,6 +294,17 @@ impl PsdCascade { self.detrend = d; } + fn get_or_add(&mut self, i: usize) -> &mut Psd { + while i >= self.stages.len() { + let mut stage = Psd::new(self.fft.clone(), self.win.clone()); + stage.set_stage_length(self.stage_length); + stage.set_detrend(self.detrend); + stage.set_overlap(self.overlap); + self.stages.push(stage); + } + &mut self.stages[i] + } + /// Process input items pub fn process(&mut self, x: &[f32]) { let mut a = ([0f32; N], [0f32; N]); @@ -295,14 +312,7 @@ impl PsdCascade { for mut x in x.chunks(N << self.stage_length) { let mut i = 0; while !x.is_empty() { - while i >= self.stages.len() { - let mut stage = Psd::new(self.fft.clone(), self.win.clone()); - stage.set_stage_length(self.stage_length); - stage.set_detrend(self.detrend); - stage.set_overlap(self.overlap); - self.stages.push(stage); - } - let n = self.stages[i].process(x, y); + let n = self.get_or_add(i).process(x, y).len(); core::mem::swap(&mut z, &mut y); x = &z[..n]; i += 1; @@ -347,14 +357,15 @@ impl PsdCascade { p.extend(pi.iter().rev().map(|pi| pi * g)); n += stage.hbf.n(); } - if !p.is_empty() { - // correct DC and Nyquist bins as both only contribute once to the one-sided spectrum - // this matches matplotlib and matlab but is certainly a questionable step - // need special care when interpreting and integrating the PSD: DC and nyquist bins - // must be counted as only half the width as the "usual" bins 0 < i < N/2 - p[0] *= 0.5; - let n = p.len(); - p[n - 1] *= 0.5; + // correct DC and Nyquist bins as both only contribute once to the one-sided spectrum + // this matches matplotlib and matlab but is certainly a questionable step + // need special care when interpreting and integrating the PSD: DC and nyquist bins + // must be counted as only half the width as the "usual" bins 0 < i < N/2 + if let Some(p) = p.first_mut() { + *p *= 0.5; + } + if let Some(p) = p.last_mut() { + *p *= 0.5; } (p, b) } @@ -369,6 +380,8 @@ impl PsdCascade { f.extend((0..bi.highest_bin).rev().map(|f| f as f32 * df)); } assert_eq!(f.len(), bi.start + bi.highest_bin); + debug_assert_eq!(f.first(), Some(&0.5)); + debug_assert_eq!(f.last(), Some(&0.0)); f } } @@ -402,9 +415,8 @@ mod test { ); let x = vec![1.0; N]; let mut y = vec![0.0; N]; - let k = s.process(&x, &mut y[..]); - y.truncate(k); - assert_eq!(&y, &x[..N / 2]); + let y = s.process(&x, &mut y[..]); + assert_eq!(y, &x[..N / 2]); println!("{:?}, {}", s.spectrum(), s.gain()); let mut s = PsdCascade::::default(); @@ -428,7 +440,7 @@ mod test { assert_eq!(crate::HBF_PASSBAND, 0.4); // make uniform noise [-1, 1), ignore the epsilon. - let x: Vec<_> = (0..1 << 20) + let x: Vec<_> = (0..1 << 16) .map(|_| rand::random::() * 2.0 - 1.0) .collect(); let xm = x.iter().map(|x| *x as f64).sum::() as f32 / x.len() as f32; @@ -446,8 +458,7 @@ mod test { ); s.set_stage_length(n); let mut y = vec![0.0; x.len() >> n]; - let k = s.process(&x, &mut y[..]); - y.truncate(k); + let y = s.process(&x, &mut y[..]); let mut hbf = HbfDecCascade::default(); hbf.set_n(n); From 216736115c0dadfa15a114b858a003905adcffea Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Robert=20J=C3=B6rdens?= Date: Mon, 11 Sep 2023 18:39:13 +0200 Subject: [PATCH 29/39] better hbf tap normalization --- src/bin/main.rs | 5 ++--- src/hbf.rs | 60 +++++++++++++++++++++++++------------------------ 2 files changed, 33 insertions(+), 32 deletions(-) diff --git a/src/bin/main.rs b/src/bin/main.rs index 248f98c..722d1fd 100644 --- a/src/bin/main.rs +++ b/src/bin/main.rs @@ -85,10 +85,9 @@ fn main() -> Result<()> { Trace { breaks: b, psd: Vec::from_iter( - f.iter() + f[..f.len() - 1] // DC + .iter() .zip(p.iter()) - .rev() - .skip(1) // DC .map(|(f, p)| [f.log10() as f64, 10.0 * p.log10() as f64]), ), } diff --git a/src/hbf.rs b/src/hbf.rs index 9c6db40..ef6ad9f 100644 --- a/src/hbf.rs +++ b/src/hbf.rs @@ -107,6 +107,7 @@ pub struct HbfDec<'a, const M: usize, const N: usize> { impl<'a, const M: usize, const N: usize> HbfDec<'a, M, N> { /// Non-zero (odd) taps from oldest to one-before-center. + /// Normalized such that center tap is 1. pub fn new(taps: &'a [f32; M]) -> Self { Self { even: [0.0; N], @@ -150,7 +151,7 @@ impl<'a, const M: usize, const N: usize> Filter for HbfDec<'a, M, N> { .iter_mut() .zip(self.even[..k].iter().zip(self.odd.get())) { - *yi = 0.5 * even + odd; + *yi = 0.5 * (even + odd); } // keep state self.even.copy_within(k..k + M - 1, 0); @@ -172,6 +173,7 @@ pub struct HbfInt<'a, const M: usize, const N: usize> { impl<'a, const M: usize, const N: usize> HbfInt<'a, M, N> { /// Non-zero (odd) taps from oldest to one-before-center. + /// Normalized such that center tap is 1. pub fn new(taps: &'a [f32; M]) -> Self { Self { fir: SymFir::new(taps), @@ -203,12 +205,12 @@ impl<'a, const M: usize, const N: usize> Filter for HbfInt<'a, M, N> { // load input self.fir.x[2 * M - 1..][..k].copy_from_slice(x); // compute output - for (yi, (even, odd)) in y + for (yi, (even, &odd)) in y .chunks_exact_mut(2) .zip(self.fir.get().zip(self.fir.x[M..][..k].iter())) { - yi[0] = 2.0 * even; - yi[1] = *odd; + yi[0] = even; + yi[1] = odd; } // keep state self.fir.x.copy_within(k..k + 2 * M - 1, 0); @@ -229,37 +231,37 @@ impl<'a, const M: usize, const N: usize> Filter for HbfInt<'a, M, N> { pub const HBF_TAPS: ([f32; 15], [f32; 6], [f32; 3], [f32; 3], [f32; 2]) = ( // 15 coefficients (effective number of DSP taps 4*15-1 = 59), transition band width .2 fs [ - 3.51072006e-05, - -1.21639791e-04, - 3.17513468e-04, - -6.98912706e-04, - 1.37306791e-03, - -2.48201920e-03, - 4.20903456e-03, - -6.79138003e-03, - 1.05502027e-02, - -1.59633823e-02, - 2.38512144e-02, - -3.59007172e-02, - 5.64710020e-02, - -1.01639797e-01, - 3.16796462e-01, + 7.02144012e-05, + -2.43279582e-04, + 6.35026936e-04, + -1.39782541e-03, + 2.74613582e-03, + -4.96403839e-03, + 8.41806912e-03, + -1.35827601e-02, + 2.11004053e-02, + -3.19267647e-02, + 4.77024289e-02, + -7.18014345e-02, + 1.12942004e-01, + -2.03279594e-01, + 6.33592923e-01, ], // 6, .47 [ - -0.00043471, - 0.00288919, - -0.01100837, - 0.03178935, - -0.08313839, - 0.30989656, + -0.00086943, + 0.00577837, + -0.02201674, + 0.06357869, + -0.16627679, + 0.61979312, ], // 3, .754 - [0.00707325, -0.0521982, 0.29513371], + [0.01414651, -0.10439639, 0.59026742], // 3, .877 - [0.00613987, -0.04965391, 0.29351417], + [0.01227974, -0.09930782, 0.58702834], // 2, .94 - [-0.03145898, 0.28145805], + [-0.06291796, 0.5629161], ); /// Passband width in units of lowest sample rate @@ -483,7 +485,7 @@ mod test { #[test] fn test() { - let mut h = HbfDec::<1, 5>::new(&[0.25]); + let mut h = HbfDec::<1, 5>::new(&[0.5]); assert_eq!(h.process_block(None, &mut []), &[]); let mut x = [1.0; 8]; From 396de93915b4a58b03fd1a54a48b244af727b95b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Robert=20J=C3=B6rdens?= Date: Wed, 13 Sep 2023 22:37:01 +0200 Subject: [PATCH 30/39] clean up README remnants --- README.md | 6 ------ 1 file changed, 6 deletions(-) diff --git a/README.md b/README.md index 0f77a6a..1bc976c 100644 --- a/README.md +++ b/README.md @@ -1,8 +1,2 @@ # stabilizer-streaming Host-side stream utilities for interacting with Stabilizer's livestream - -## Usage -1. Install [npm](https://docs.npmjs.com/downloading-and-installing-node-js-and-npm) -2. Configure streaming to device -2. `cargo run --bin webserver --release -- --ip ` -3. Navigate to `localhost:8080` From 4de58352a50027947e2dffb73e57d079f37afda5 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Robert=20J=C3=B6rdens?= Date: Fri, 15 Sep 2023 15:48:34 +0200 Subject: [PATCH 31/39] refactor hbf --- src/hbf.rs | 162 ++++++++++++++++++++++++++++++++++------------------- src/psd.rs | 29 ++++++---- 2 files changed, 120 insertions(+), 71 deletions(-) diff --git a/src/hbf.rs b/src/hbf.rs index ef6ad9f..e2e20e7 100644 --- a/src/hbf.rs +++ b/src/hbf.rs @@ -1,6 +1,7 @@ /// Filter input items into output items. pub trait Filter { /// Input/output item type. + // TODO: impl with generic item type type Item; /// Process a block of items. @@ -37,24 +38,27 @@ pub trait Filter { /// Symmetric FIR filter prototype. /// -/// DSP taps 2*M +/// # Generics +/// * `M`: number of taps, one-sided. The filter has effectively 2*M DSP taps +/// * `N`: state size: N = 2*M - 1 + {input/output}.len() /// -/// M: number of taps -/// N: state size: N = 2*M - 1 + {input/output}.len() -/// -/// Decimation/interpolation filters +/// # Half band decimation/interpolation filters /// -/// These focus on half-band filters (rate change of 2) and cascades of HBF. +/// Half-band filters (rate change of 2) and cascades of HBFs are implemented in +/// [`HbfDec`] and [`HbfInt`] etc. /// The half-band filter has unique properties that make it preferrable in many cases: /// -/// * only needs N multiplications (fused multiply accumulate) for 4*N taps -/// * stores less state compared with with a straight FIR +/// * only needs M multiplications (fused multiply accumulate) for 4*M taps +/// * HBF decimator stores less state than a generic FIR filter /// * as a FIR filter has linear phase/flat group delay /// * very small passband ripple and excellent stopband attenuation /// * as a cascade of decimation/interpolation filters, the higher-rate filters /// need successively fewer taps, allowing the filtering to be dominated by /// only the highest rate filter with the fewest taps -/// * high dynamic range (compared with a biquad IIR) +/// * In a cascade of HBF the overall latency, group delay, and impulse response +/// length are dominated by the lowest-rate filter which, due to its manageable transition +/// band width (compared to single-stage filters) can be smaller, shorter, and faster. +/// * high dynamic range and inherent stability compared with an IIR filter /// * can be combined with a CIC filter for non-power-of-two or even higher rate changes /// /// The implementations here are all `no_std` and `no-alloc`. @@ -71,12 +75,21 @@ pub struct SymFir<'a, const M: usize, const N: usize> { } impl<'a, const M: usize, const N: usize> SymFir<'a, M, N> { - /// taps: one-sided, expluding center tap, oldest to one-before-center + /// Create a new `SymFir`. + /// + /// # Args + /// * `taps`: one-sided FIR coefficients, expluding center tap, oldest to one-before-center pub fn new(taps: &'a [f32; M]) -> Self { debug_assert!(N >= M * 2); Self { x: [0.0; N], taps } } + /// Obtain a mutable reference to the input items buffer space. + #[inline] + pub fn buf_mut(&mut self) -> &mut [f32] { + &mut self.x[2 * M - 1..] + } + /// Perform the FIR convolution and yield results iteratively. #[inline] pub fn get(&self) -> impl Iterator + '_ { @@ -89,6 +102,15 @@ impl<'a, const M: usize, const N: usize> SymFir<'a, M, N> { .sum() }) } + + /// Move items as new filter state. + /// + /// # Args + /// * `offset`: Keep the `2*M-1` items at `offset` as the new filter state. + #[inline] + pub fn keep_state(&mut self, offset: usize) { + self.x.copy_within(offset..offset + 2 * M - 1, 0); + } } // TODO: pub struct SymFirInt, SymFirDec @@ -106,8 +128,11 @@ pub struct HbfDec<'a, const M: usize, const N: usize> { } impl<'a, const M: usize, const N: usize> HbfDec<'a, M, N> { - /// Non-zero (odd) taps from oldest to one-before-center. - /// Normalized such that center tap is 1. + /// Create a new `HbfDec`. + /// + /// # Args + /// * `taps`: The FIR filter coefficients. Only the non-zero (odd) taps + /// from oldest to one-before-center. Normalized such that center tap is 1. pub fn new(taps: &'a [f32; M]) -> Self { Self { even: [0.0; N], @@ -141,7 +166,7 @@ impl<'a, const M: usize, const N: usize> Filter for HbfDec<'a, M, N> { for (xi, (even, odd)) in x.chunks_exact(2).zip( self.even[M - 1..][..k] .iter_mut() - .zip(self.odd.x[2 * M - 1..][..k].iter_mut()), + .zip(self.odd.buf_mut()[..k].iter_mut()), ) { *even = xi[0]; *odd = xi[1]; @@ -155,7 +180,7 @@ impl<'a, const M: usize, const N: usize> Filter for HbfDec<'a, M, N> { } // keep state self.even.copy_within(k..k + M - 1, 0); - self.odd.x.copy_within(k..k + 2 * M - 1, 0); + self.odd.keep_state(k); &mut y[..k] } } @@ -179,6 +204,11 @@ impl<'a, const M: usize, const N: usize> HbfInt<'a, M, N> { fir: SymFir::new(taps), } } + + /// Obtain a mutable reference to the input items buffer space + pub fn buf_mut(&mut self) -> &mut [f32] { + self.fir.buf_mut() + } } impl<'a, const M: usize, const N: usize> Filter for HbfInt<'a, M, N> { @@ -203,17 +233,20 @@ impl<'a, const M: usize, const N: usize> Filter for HbfInt<'a, M, N> { let k = y.len() / 2; let x = x.unwrap_or(&y[..k]); // load input - self.fir.x[2 * M - 1..][..k].copy_from_slice(x); + self.fir.buf_mut()[..k].copy_from_slice(x); // compute output for (yi, (even, &odd)) in y .chunks_exact_mut(2) .zip(self.fir.get().zip(self.fir.x[M..][..k].iter())) { - yi[0] = even; - yi[1] = odd; + // Choose the even item to be the interpolated one. + // The alternative would have the same response length + // but larger latency. + yi[0] = even; // interpolated + yi[1] = odd; // center tap: identity } // keep state - self.fir.x.copy_within(k..k + 2 * M - 1, 0); + self.fir.keep_state(k); y } } @@ -277,7 +310,7 @@ pub const HBF_CASCADE_BLOCK: usize = 1 << 6; /// Supports rate changes of 1, 2, 4, 8, and 16. #[derive(Copy, Clone, Debug)] pub struct HbfDecCascade { - n: usize, + depth: usize, stages: ( HbfDec<'static, { HBF_TAPS.0.len() }, { 2 * HBF_TAPS.0.len() - 1 + HBF_CASCADE_BLOCK }>, HbfDec<'static, { HBF_TAPS.1.len() }, { 2 * HBF_TAPS.1.len() - 1 + HBF_CASCADE_BLOCK * 2 }>, @@ -289,7 +322,7 @@ pub struct HbfDecCascade { impl Default for HbfDecCascade { fn default() -> Self { Self { - n: 0, + depth: 0, stages: ( HbfDec::new(&HBF_TAPS.0), HbfDec::new(&HBF_TAPS.1), @@ -302,14 +335,14 @@ impl Default for HbfDecCascade { impl HbfDecCascade { #[inline] - pub fn set_n(&mut self, n: usize) { + pub fn set_depth(&mut self, n: usize) { assert!(n <= 4); - self.n = n; + self.depth = n; } #[inline] - pub fn n(&self) -> usize { - self.n + pub fn depth(&self) -> usize { + self.depth } } @@ -319,8 +352,8 @@ impl Filter for HbfDecCascade { #[inline] fn block_size(&self) -> (usize, usize) { ( - 1 << self.n, - match self.n { + 1 << self.depth, + match self.depth { 0 => usize::MAX, 1 => self.stages.0.block_size().1, 2 => self.stages.1.block_size().1, @@ -333,16 +366,16 @@ impl Filter for HbfDecCascade { #[inline] fn response_length(&self) -> usize { let mut n = 0; - if self.n > 3 { + if self.depth > 3 { n = n / 2 + self.stages.3.response_length(); } - if self.n > 2 { + if self.depth > 2 { n = n / 2 + self.stages.2.response_length(); } - if self.n > 1 { + if self.depth > 1 { n = n / 2 + self.stages.1.response_length(); } - if self.n > 0 { + if self.depth > 0 { n = n / 2 + self.stages.0.response_length(); } n @@ -358,31 +391,35 @@ impl Filter for HbfDecCascade { } let n = y.len(); - if self.n > 3 { + if self.depth > 3 { y = self.stages.3.process_block(None, y); } - if self.n > 2 { + if self.depth > 2 { y = self.stages.2.process_block(None, y); } - if self.n > 1 { + if self.depth > 1 { y = self.stages.1.process_block(None, y); } - if self.n > 0 { + if self.depth > 0 { y = self.stages.0.process_block(None, y); } - debug_assert_eq!(y.len(), n >> self.n); + debug_assert_eq!(y.len(), n >> self.depth); y } } /// Half-band interpolation filter cascade with optimal taps. /// +/// This is a no_alloc version without trait objects. +/// The price to pay is fixed and maximal memory usage independent +/// of block size and cascade length. +/// /// See [HBF_TAPS]. /// Only in-place processing is implemented. /// Supports rate changes of 1, 2, 4, 8, and 16. #[derive(Copy, Clone, Debug)] pub struct HbfIntCascade { - n: usize, + depth: usize, pub stages: ( HbfInt<'static, { HBF_TAPS.0.len() }, { 2 * HBF_TAPS.0.len() - 1 + HBF_CASCADE_BLOCK }>, HbfInt<'static, { HBF_TAPS.1.len() }, { 2 * HBF_TAPS.1.len() - 1 + HBF_CASCADE_BLOCK * 2 }>, @@ -394,7 +431,7 @@ pub struct HbfIntCascade { impl Default for HbfIntCascade { fn default() -> Self { Self { - n: 4, + depth: 4, stages: ( HbfInt::new(&HBF_TAPS.0), HbfInt::new(&HBF_TAPS.1), @@ -406,13 +443,13 @@ impl Default for HbfIntCascade { } impl HbfIntCascade { - pub fn set_n(&mut self, n: usize) { + pub fn set_depth(&mut self, n: usize) { assert!(n <= 4); - self.n = n; + self.depth = n; } - pub fn n(&self) -> usize { - self.n + pub fn depth(&self) -> usize { + self.depth } } @@ -422,8 +459,8 @@ impl Filter for HbfIntCascade { #[inline] fn block_size(&self) -> (usize, usize) { ( - 1 << self.n, - match self.n { + 1 << self.depth, + match self.depth { 0 => usize::MAX, 1 => self.stages.0.block_size().1, 2 => self.stages.1.block_size().1, @@ -436,16 +473,16 @@ impl Filter for HbfIntCascade { #[inline] fn response_length(&self) -> usize { let mut n = 0; - if self.n > 0 { + if self.depth > 0 { n = 2 * n + self.stages.0.response_length(); } - if self.n > 1 { + if self.depth > 1 { n = 2 * n + self.stages.1.response_length(); } - if self.n > 2 { + if self.depth > 2 { n = 2 * n + self.stages.2.response_length(); } - if self.n > 3 { + if self.depth > 3 { n = 2 * n + self.stages.3.response_length(); } n @@ -459,18 +496,19 @@ impl Filter for HbfIntCascade { if x.is_some() { unimplemented!(); // TODO: one intermediate buffer and `y` } + // TODO: use buf_mut() and write directly into next filters' input buffer - let mut n = y.len() >> self.n; - if self.n > 0 { + let mut n = y.len() >> self.depth; + if self.depth > 0 { n = self.stages.0.process_block(None, &mut y[..2 * n]).len(); } - if self.n > 1 { + if self.depth > 1 { n = self.stages.1.process_block(None, &mut y[..2 * n]).len(); } - if self.n > 2 { + if self.depth > 2 { n = self.stages.2.process_block(None, &mut y[..2 * n]).len(); } - if self.n > 3 { + if self.depth > 3 { n = self.stages.3.process_block(None, &mut y[..2 * n]).len(); } debug_assert_eq!(n, y.len()); @@ -503,9 +541,12 @@ mod test { #[test] fn decim() { let mut h = HbfDecCascade::default(); - h.set_n(4); - assert_eq!(h.block_size(), (1 << h.n(), HBF_CASCADE_BLOCK << h.n())); - let mut x: Vec<_> = (0..2 << h.n()).map(|i| i as f32).collect(); + h.set_depth(4); + assert_eq!( + h.block_size(), + (1 << h.depth(), HBF_CASCADE_BLOCK << h.depth()) + ); + let mut x: Vec<_> = (0..2 << h.depth()).map(|i| i as f32).collect(); let x = h.process_block(None, &mut x); println!("{:?}", x); } @@ -513,8 +554,11 @@ mod test { #[test] fn interp() { let mut h = HbfIntCascade::default(); - h.set_n(4); - assert_eq!(h.block_size(), (1 << h.n(), HBF_CASCADE_BLOCK << h.n())); + h.set_depth(4); + assert_eq!( + h.block_size(), + (1 << h.depth(), HBF_CASCADE_BLOCK << h.depth()) + ); let k = h.block_size().0; let r = h.response_length(); let mut x = vec![0.0; (r + 1 + k - 1) / k * k]; @@ -524,7 +568,7 @@ mod test { assert!(x[r] != 0.0); assert_eq!(x[r + 1..], vec![0.0; x.len() - r - 1]); - let g = (1 << h.n()) as f32; + let g = (1 << h.depth()) as f32; let mut y = Vec::from_iter(x.iter().map(|&x| Complex { re: x / g, im: 0.0 })); // pad y.resize(5 << 10, Complex::default()); @@ -579,7 +623,7 @@ mod test { fn insn_casc() { let mut x = [9.0; 1 << 10]; let mut h = HbfDecCascade::default(); - h.set_n(4); + h.set_depth(4); for _ in 0..1 << 20 { h.process_block(None, &mut x); } diff --git a/src/psd.rs b/src/psd.rs index 6ae252a..bec7c61 100644 --- a/src/psd.rs +++ b/src/psd.rs @@ -4,9 +4,9 @@ use std::sync::Arc; /// Window kernel /// -/// https://holometer.fnal.gov/GH_FFT.pdf -/// https://gist.github.com/endolith/c4b8e1e3c630a260424123b4e9d964c4 -/// https://docs.google.com/spreadsheets/d/1glvo-y1tqCiYwK0QQWhB4AAcDFiK_C_0M4SeA0Uyqdc/edit +/// +/// +/// #[derive(Copy, Clone, Debug, PartialEq)] pub struct Window { pub win: [f32; N], @@ -29,10 +29,11 @@ impl Window { /// Hann window /// - /// This is the "numerical" version of the window with period N, win[0] = win[N] - /// (conceptually), specifically win[0] != win[win.len() - 1]. - /// Matplotlib uses the symetric one of period N-1, with win[0] = win[N - 1] = 0 - /// which looses a lot of useful properties (exact nenbw and power independent of N, + /// This is the "numerical" version of the window with period `N`, `win[0] = win[N]` + /// (conceptually), specifically `win[0] != win[win.len() - 1]`. + /// Matplotlib's `matplotlib.mlab.window_hanning()` (but not scipy.signal.get_window()) + /// uses the symetric one of period `N-1`, with `win[0] = win[N - 1] = 0` + /// which looses a lot of useful properties (exact nenbw and power independent of `N`, /// exact optimal overlap etc) pub fn hann() -> Self { assert!(N > 0); @@ -54,10 +55,14 @@ impl Window { pub enum Detrend { /// No detrending None, - /// Subtract the midpoint of segment + /// Subtract the midpoint of each segment Mid, /// Remove linear interpolation between first and last item for each segment Span, + // TODO: real mean + // Mean, + // TODO: linear regression + // Linear } /// Power spectral density accumulator and decimator @@ -112,7 +117,7 @@ impl Psd { } pub fn set_stage_length(&mut self, n: usize) { - self.hbf.set_n(n); + self.hbf.set_depth(n); self.drain = self.hbf.response_length(); } } @@ -344,7 +349,7 @@ impl PsdCascade { let f_pass = 4 * N / 10; pi = &pi[..f_pass]; // remove low f bins from previous stage, ceil - let f_low = (4 * N + (10 << stage.hbf.n()) - 1) / (10 << stage.hbf.n()); + let f_low = (4 * N + (10 << stage.hbf.depth()) - 1) / (10 << stage.hbf.depth()); p.truncate(p.len() - f_low); } let g = stage.gain() * (1 << n) as f32; @@ -355,7 +360,7 @@ impl PsdCascade { effective_fft_size: N << n, }); p.extend(pi.iter().rev().map(|pi| pi * g)); - n += stage.hbf.n(); + n += stage.hbf.depth(); } // correct DC and Nyquist bins as both only contribute once to the one-sided spectrum // this matches matplotlib and matlab but is certainly a questionable step @@ -461,7 +466,7 @@ mod test { let y = s.process(&x, &mut y[..]); let mut hbf = HbfDecCascade::default(); - hbf.set_n(n); + hbf.set_depth(n); assert_eq!(y.len(), ((x.len() - N / 2) >> n) - hbf.response_length()); let p: Vec<_> = s.spectrum().iter().map(|p| p * s.gain()).collect(); // psd of a stage From f3d85d4479fd85d55b988d4a125fa2839675973d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Robert=20J=C3=B6rdens?= Date: Fri, 15 Sep 2023 22:57:40 +0200 Subject: [PATCH 32/39] move hbf to idsp --- Cargo.lock | 3 +- Cargo.toml | 2 +- src/hbf.rs | 631 ----------------------------------------------------- src/lib.rs | 2 - src/psd.rs | 4 +- 5 files changed, 4 insertions(+), 638 deletions(-) delete mode 100644 src/hbf.rs diff --git a/Cargo.lock b/Cargo.lock index 7e9306e..13bd8fc 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1030,8 +1030,7 @@ dependencies = [ [[package]] name = "idsp" version = "0.10.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d86b122ef447608ce36932016201caddb871691596578e3518c69f57acbed006" +source = "git+https://github.com/quartiq/idsp.git?branch=hbf#55051df0fbefde6256e1f3d562835f29c17e50bd" dependencies = [ "num-complex", "num-traits", diff --git a/Cargo.toml b/Cargo.toml index 52072df..ecff30e 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -26,7 +26,7 @@ bytemuck = "1.13.1" thiserror = "1.0.47" anyhow = "1.0.75" socket2 = "0.5.3" -idsp = "0.10.0" +idsp = {git = "https://github.com/quartiq/idsp.git", branch = "hbf"} rustfft = "6.1.0" rand = "0.8.5" diff --git a/src/hbf.rs b/src/hbf.rs deleted file mode 100644 index e2e20e7..0000000 --- a/src/hbf.rs +++ /dev/null @@ -1,631 +0,0 @@ -/// Filter input items into output items. -pub trait Filter { - /// Input/output item type. - // TODO: impl with generic item type - type Item; - - /// Process a block of items. - /// - /// Input items can be either in `x` or in `y`. - /// In the latter case the filtering operation is done in-place. - /// Output is always written into `y`. - /// The slice of items written into `y` is returned. - /// Input and output size relations must match the filter requirements - /// (decimation/interpolation and maximum block size). - /// When using in-place operation, `y` needs to contain the input items - /// (fewer than `y.len()` in the case of interpolation) and must be able to - /// contain the output items. - fn process_block<'a>( - &mut self, - x: Option<&[Self::Item]>, - y: &'a mut [Self::Item], - ) -> &'a mut [Self::Item]; - - /// Return the block size granularity and the maximum block size. - /// - /// For in-place processing, this refers to constraints on `y`. - /// Otherwise this refers to the larger of `x` and `y` (`x` for decimation and `y` for interpolation). - /// The granularity is also the rate change in the case of interpolation/decimation filters. - fn block_size(&self) -> (usize, usize); - - /// Finite impulse response length in numer of output items minus one - /// Get this many to drain all previous memory - fn response_length(&self) -> usize; - - // TODO: process items with automatic blocks - // fn process(&mut self, x: Option<&[Self::Item]>, y: &mut [Self::Item]) -> usize {} -} - -/// Symmetric FIR filter prototype. -/// -/// # Generics -/// * `M`: number of taps, one-sided. The filter has effectively 2*M DSP taps -/// * `N`: state size: N = 2*M - 1 + {input/output}.len() -/// -/// # Half band decimation/interpolation filters -/// -/// Half-band filters (rate change of 2) and cascades of HBFs are implemented in -/// [`HbfDec`] and [`HbfInt`] etc. -/// The half-band filter has unique properties that make it preferrable in many cases: -/// -/// * only needs M multiplications (fused multiply accumulate) for 4*M taps -/// * HBF decimator stores less state than a generic FIR filter -/// * as a FIR filter has linear phase/flat group delay -/// * very small passband ripple and excellent stopband attenuation -/// * as a cascade of decimation/interpolation filters, the higher-rate filters -/// need successively fewer taps, allowing the filtering to be dominated by -/// only the highest rate filter with the fewest taps -/// * In a cascade of HBF the overall latency, group delay, and impulse response -/// length are dominated by the lowest-rate filter which, due to its manageable transition -/// band width (compared to single-stage filters) can be smaller, shorter, and faster. -/// * high dynamic range and inherent stability compared with an IIR filter -/// * can be combined with a CIC filter for non-power-of-two or even higher rate changes -/// -/// The implementations here are all `no_std` and `no-alloc`. -/// They support (but don't require) in-place filtering to reduce memory usage. -/// They unroll and optimmize extremely well targetting current architectures, -/// e.g. requiring less than 4 instructions per input item for the full `HbfDecCascade` on Skylake. -/// The filters are optimized for decent block sizes and perform best (i.e. with negligible -/// overhead) for blocks of 32 high-rate items or more, depending very much on architecture. - -#[derive(Clone, Debug, Copy)] -pub struct SymFir<'a, const M: usize, const N: usize> { - x: [f32; N], - taps: &'a [f32; M], -} - -impl<'a, const M: usize, const N: usize> SymFir<'a, M, N> { - /// Create a new `SymFir`. - /// - /// # Args - /// * `taps`: one-sided FIR coefficients, expluding center tap, oldest to one-before-center - pub fn new(taps: &'a [f32; M]) -> Self { - debug_assert!(N >= M * 2); - Self { x: [0.0; N], taps } - } - - /// Obtain a mutable reference to the input items buffer space. - #[inline] - pub fn buf_mut(&mut self) -> &mut [f32] { - &mut self.x[2 * M - 1..] - } - - /// Perform the FIR convolution and yield results iteratively. - #[inline] - pub fn get(&self) -> impl Iterator + '_ { - self.x.windows(2 * M).map(|x| { - let (old, new) = x.split_at(M); - old.iter() - .zip(new.iter().rev()) - .zip(self.taps.iter()) - .map(|((xo, xn), tap)| (xo + xn) * tap) - .sum() - }) - } - - /// Move items as new filter state. - /// - /// # Args - /// * `offset`: Keep the `2*M-1` items at `offset` as the new filter state. - #[inline] - pub fn keep_state(&mut self, offset: usize) { - self.x.copy_within(offset..offset + 2 * M - 1, 0); - } -} - -// TODO: pub struct SymFirInt, SymFirDec - -/// Half band decimator (decimate by two) -/// -/// The effective number of DSP taps is 4*M - 1. -/// -/// M: number of taps -/// N: state size: N = 2*M - 1 + output.len() -#[derive(Clone, Debug, Copy)] -pub struct HbfDec<'a, const M: usize, const N: usize> { - even: [f32; N], // This is an upper bound to N - M (unstable const expr) - odd: SymFir<'a, M, N>, -} - -impl<'a, const M: usize, const N: usize> HbfDec<'a, M, N> { - /// Create a new `HbfDec`. - /// - /// # Args - /// * `taps`: The FIR filter coefficients. Only the non-zero (odd) taps - /// from oldest to one-before-center. Normalized such that center tap is 1. - pub fn new(taps: &'a [f32; M]) -> Self { - Self { - even: [0.0; N], - odd: SymFir::new(taps), - } - } -} - -impl<'a, const M: usize, const N: usize> Filter for HbfDec<'a, M, N> { - type Item = f32; - - #[inline] - fn block_size(&self) -> (usize, usize) { - (2, 2 * (N - (2 * M - 1))) - } - - #[inline] - fn response_length(&self) -> usize { - 2 * M - 1 - } - - fn process_block<'b>( - &mut self, - x: Option<&[Self::Item]>, - y: &'b mut [Self::Item], - ) -> &'b mut [Self::Item] { - let x = x.unwrap_or(y); - debug_assert_eq!(x.len() & 1, 0); - let k = x.len() / 2; - // load input - for (xi, (even, odd)) in x.chunks_exact(2).zip( - self.even[M - 1..][..k] - .iter_mut() - .zip(self.odd.buf_mut()[..k].iter_mut()), - ) { - *even = xi[0]; - *odd = xi[1]; - } - // compute output - for (yi, (even, odd)) in y[..k] - .iter_mut() - .zip(self.even[..k].iter().zip(self.odd.get())) - { - *yi = 0.5 * (even + odd); - } - // keep state - self.even.copy_within(k..k + M - 1, 0); - self.odd.keep_state(k); - &mut y[..k] - } -} - -/// Half band interpolator (interpolation rate 2) -/// -/// The effective number of DSP taps is 4*M - 1. -/// -/// M: number of taps -/// N: state size: N = 2*M - 1 + input.len() -#[derive(Clone, Debug, Copy)] -pub struct HbfInt<'a, const M: usize, const N: usize> { - fir: SymFir<'a, M, N>, -} - -impl<'a, const M: usize, const N: usize> HbfInt<'a, M, N> { - /// Non-zero (odd) taps from oldest to one-before-center. - /// Normalized such that center tap is 1. - pub fn new(taps: &'a [f32; M]) -> Self { - Self { - fir: SymFir::new(taps), - } - } - - /// Obtain a mutable reference to the input items buffer space - pub fn buf_mut(&mut self) -> &mut [f32] { - self.fir.buf_mut() - } -} - -impl<'a, const M: usize, const N: usize> Filter for HbfInt<'a, M, N> { - type Item = f32; - - #[inline] - fn block_size(&self) -> (usize, usize) { - (2, 2 * (N - (2 * M - 1))) - } - - #[inline] - fn response_length(&self) -> usize { - 4 * M - 2 - } - - fn process_block<'b>( - &mut self, - x: Option<&[Self::Item]>, - y: &'b mut [Self::Item], - ) -> &'b mut [Self::Item] { - debug_assert_eq!(y.len() & 1, 0); - let k = y.len() / 2; - let x = x.unwrap_or(&y[..k]); - // load input - self.fir.buf_mut()[..k].copy_from_slice(x); - // compute output - for (yi, (even, &odd)) in y - .chunks_exact_mut(2) - .zip(self.fir.get().zip(self.fir.x[M..][..k].iter())) - { - // Choose the even item to be the interpolated one. - // The alternative would have the same response length - // but larger latency. - yi[0] = even; // interpolated - yi[1] = odd; // center tap: identity - } - // keep state - self.fir.keep_state(k); - y - } -} - -/// Standard/optimal half-band filter cascade taps -/// -/// * more than 98 dB stop band attenuation -/// * 0.4 pass band (relative to lowest sample rate) -/// * less than 0.001 dB ripple -/// * linear phase/flat group delay -/// * rate change up to 2**5 = 32 -/// * lowest rate filter is at 0 index -/// * use taps 0..n for 2**n interpolation/decimation -#[allow(clippy::excessive_precision, clippy::type_complexity)] -pub const HBF_TAPS: ([f32; 15], [f32; 6], [f32; 3], [f32; 3], [f32; 2]) = ( - // 15 coefficients (effective number of DSP taps 4*15-1 = 59), transition band width .2 fs - [ - 7.02144012e-05, - -2.43279582e-04, - 6.35026936e-04, - -1.39782541e-03, - 2.74613582e-03, - -4.96403839e-03, - 8.41806912e-03, - -1.35827601e-02, - 2.11004053e-02, - -3.19267647e-02, - 4.77024289e-02, - -7.18014345e-02, - 1.12942004e-01, - -2.03279594e-01, - 6.33592923e-01, - ], - // 6, .47 - [ - -0.00086943, - 0.00577837, - -0.02201674, - 0.06357869, - -0.16627679, - 0.61979312, - ], - // 3, .754 - [0.01414651, -0.10439639, 0.59026742], - // 3, .877 - [0.01227974, -0.09930782, 0.58702834], - // 2, .94 - [-0.06291796, 0.5629161], -); - -/// Passband width in units of lowest sample rate -pub const HBF_PASSBAND: f32 = 0.4; - -/// Max low-rate block size (HbfIntCascade input, HbfDecCascade output) -pub const HBF_CASCADE_BLOCK: usize = 1 << 6; - -/// Half-band decimation filter cascade with optimal taps -/// -/// See [HBF_TAPS]. -/// Only in-place processing is implemented. -/// Supports rate changes of 1, 2, 4, 8, and 16. -#[derive(Copy, Clone, Debug)] -pub struct HbfDecCascade { - depth: usize, - stages: ( - HbfDec<'static, { HBF_TAPS.0.len() }, { 2 * HBF_TAPS.0.len() - 1 + HBF_CASCADE_BLOCK }>, - HbfDec<'static, { HBF_TAPS.1.len() }, { 2 * HBF_TAPS.1.len() - 1 + HBF_CASCADE_BLOCK * 2 }>, - HbfDec<'static, { HBF_TAPS.2.len() }, { 2 * HBF_TAPS.2.len() - 1 + HBF_CASCADE_BLOCK * 4 }>, - HbfDec<'static, { HBF_TAPS.3.len() }, { 2 * HBF_TAPS.3.len() - 1 + HBF_CASCADE_BLOCK * 8 }>, - ), -} - -impl Default for HbfDecCascade { - fn default() -> Self { - Self { - depth: 0, - stages: ( - HbfDec::new(&HBF_TAPS.0), - HbfDec::new(&HBF_TAPS.1), - HbfDec::new(&HBF_TAPS.2), - HbfDec::new(&HBF_TAPS.3), - ), - } - } -} - -impl HbfDecCascade { - #[inline] - pub fn set_depth(&mut self, n: usize) { - assert!(n <= 4); - self.depth = n; - } - - #[inline] - pub fn depth(&self) -> usize { - self.depth - } -} - -impl Filter for HbfDecCascade { - type Item = f32; - - #[inline] - fn block_size(&self) -> (usize, usize) { - ( - 1 << self.depth, - match self.depth { - 0 => usize::MAX, - 1 => self.stages.0.block_size().1, - 2 => self.stages.1.block_size().1, - 3 => self.stages.2.block_size().1, - _ => self.stages.3.block_size().1, - }, - ) - } - - #[inline] - fn response_length(&self) -> usize { - let mut n = 0; - if self.depth > 3 { - n = n / 2 + self.stages.3.response_length(); - } - if self.depth > 2 { - n = n / 2 + self.stages.2.response_length(); - } - if self.depth > 1 { - n = n / 2 + self.stages.1.response_length(); - } - if self.depth > 0 { - n = n / 2 + self.stages.0.response_length(); - } - n - } - - fn process_block<'a>( - &mut self, - x: Option<&[Self::Item]>, - mut y: &'a mut [Self::Item], - ) -> &'a mut [Self::Item] { - if x.is_some() { - unimplemented!(); // TODO: pair of intermediate buffers - } - let n = y.len(); - - if self.depth > 3 { - y = self.stages.3.process_block(None, y); - } - if self.depth > 2 { - y = self.stages.2.process_block(None, y); - } - if self.depth > 1 { - y = self.stages.1.process_block(None, y); - } - if self.depth > 0 { - y = self.stages.0.process_block(None, y); - } - debug_assert_eq!(y.len(), n >> self.depth); - y - } -} - -/// Half-band interpolation filter cascade with optimal taps. -/// -/// This is a no_alloc version without trait objects. -/// The price to pay is fixed and maximal memory usage independent -/// of block size and cascade length. -/// -/// See [HBF_TAPS]. -/// Only in-place processing is implemented. -/// Supports rate changes of 1, 2, 4, 8, and 16. -#[derive(Copy, Clone, Debug)] -pub struct HbfIntCascade { - depth: usize, - pub stages: ( - HbfInt<'static, { HBF_TAPS.0.len() }, { 2 * HBF_TAPS.0.len() - 1 + HBF_CASCADE_BLOCK }>, - HbfInt<'static, { HBF_TAPS.1.len() }, { 2 * HBF_TAPS.1.len() - 1 + HBF_CASCADE_BLOCK * 2 }>, - HbfInt<'static, { HBF_TAPS.2.len() }, { 2 * HBF_TAPS.2.len() - 1 + HBF_CASCADE_BLOCK * 4 }>, - HbfInt<'static, { HBF_TAPS.3.len() }, { 2 * HBF_TAPS.3.len() - 1 + HBF_CASCADE_BLOCK * 8 }>, - ), -} - -impl Default for HbfIntCascade { - fn default() -> Self { - Self { - depth: 4, - stages: ( - HbfInt::new(&HBF_TAPS.0), - HbfInt::new(&HBF_TAPS.1), - HbfInt::new(&HBF_TAPS.2), - HbfInt::new(&HBF_TAPS.3), - ), - } - } -} - -impl HbfIntCascade { - pub fn set_depth(&mut self, n: usize) { - assert!(n <= 4); - self.depth = n; - } - - pub fn depth(&self) -> usize { - self.depth - } -} - -impl Filter for HbfIntCascade { - type Item = f32; - - #[inline] - fn block_size(&self) -> (usize, usize) { - ( - 1 << self.depth, - match self.depth { - 0 => usize::MAX, - 1 => self.stages.0.block_size().1, - 2 => self.stages.1.block_size().1, - 3 => self.stages.2.block_size().1, - _ => self.stages.3.block_size().1, - }, - ) - } - - #[inline] - fn response_length(&self) -> usize { - let mut n = 0; - if self.depth > 0 { - n = 2 * n + self.stages.0.response_length(); - } - if self.depth > 1 { - n = 2 * n + self.stages.1.response_length(); - } - if self.depth > 2 { - n = 2 * n + self.stages.2.response_length(); - } - if self.depth > 3 { - n = 2 * n + self.stages.3.response_length(); - } - n - } - - fn process_block<'a>( - &mut self, - x: Option<&[Self::Item]>, - y: &'a mut [Self::Item], - ) -> &'a mut [Self::Item] { - if x.is_some() { - unimplemented!(); // TODO: one intermediate buffer and `y` - } - // TODO: use buf_mut() and write directly into next filters' input buffer - - let mut n = y.len() >> self.depth; - if self.depth > 0 { - n = self.stages.0.process_block(None, &mut y[..2 * n]).len(); - } - if self.depth > 1 { - n = self.stages.1.process_block(None, &mut y[..2 * n]).len(); - } - if self.depth > 2 { - n = self.stages.2.process_block(None, &mut y[..2 * n]).len(); - } - if self.depth > 3 { - n = self.stages.3.process_block(None, &mut y[..2 * n]).len(); - } - debug_assert_eq!(n, y.len()); - &mut y[..n] - } -} - -#[cfg(test)] -mod test { - use super::*; - use rustfft::{num_complex::Complex, FftPlanner}; - - #[test] - fn test() { - let mut h = HbfDec::<1, 5>::new(&[0.5]); - assert_eq!(h.process_block(None, &mut []), &[]); - - let mut x = [1.0; 8]; - assert_eq!((2, x.len()), h.block_size()); - let x = h.process_block(None, &mut x); - assert_eq!(x, [0.75, 1.0, 1.0, 1.0]); - - let mut h = HbfDec::<3, 9>::new(&HBF_TAPS.3); - let mut x: Vec<_> = (0..8).map(|i| i as f32).collect(); - assert_eq!((2, x.len()), h.block_size()); - let x = h.process_block(None, &mut x); - println!("{:?}", x); - } - - #[test] - fn decim() { - let mut h = HbfDecCascade::default(); - h.set_depth(4); - assert_eq!( - h.block_size(), - (1 << h.depth(), HBF_CASCADE_BLOCK << h.depth()) - ); - let mut x: Vec<_> = (0..2 << h.depth()).map(|i| i as f32).collect(); - let x = h.process_block(None, &mut x); - println!("{:?}", x); - } - - #[test] - fn interp() { - let mut h = HbfIntCascade::default(); - h.set_depth(4); - assert_eq!( - h.block_size(), - (1 << h.depth(), HBF_CASCADE_BLOCK << h.depth()) - ); - let k = h.block_size().0; - let r = h.response_length(); - let mut x = vec![0.0; (r + 1 + k - 1) / k * k]; - x[0] = 1.0; - let x = h.process_block(None, &mut x); - println!("{:?}", x); // interpolator impulse response - assert!(x[r] != 0.0); - assert_eq!(x[r + 1..], vec![0.0; x.len() - r - 1]); - - let g = (1 << h.depth()) as f32; - let mut y = Vec::from_iter(x.iter().map(|&x| Complex { re: x / g, im: 0.0 })); - // pad - y.resize(5 << 10, Complex::default()); - FftPlanner::new().plan_fft_forward(y.len()).process(&mut y); - // transfer function - let p = Vec::from_iter(y.iter().map(|y| 10.0 * y.norm_sqr().log10())); - let f = p.len() as f32 / g; - // pass band ripple - let p_pass = p[..(f * HBF_PASSBAND).floor() as _] - .iter() - .fold(0.0, |m, p| p.abs().max(m)); - assert!(p_pass < 0.00035); - // stop band attenuation - let p_stop = p[(f * (1.0 - HBF_PASSBAND)).ceil() as _..p.len() / 2] - .iter() - .fold(-200.0, |m, p| p.max(m)); - assert!(p_stop < -98.4); - } - - /// small 32 batch size, single stage, 3 mul (11 tap) decimator - /// 3.5 insn per input sample, > 1 GS/s on Skylake - #[test] - #[ignore] - fn insn_dec() { - const N: usize = HBF_TAPS.3.len(); - let mut h = HbfDec::::new(&HBF_TAPS.3); - let mut x = [9.0; 1 << 5]; - for _ in 0..1 << 26 { - h.process_block(None, &mut x); - } - } - - /// 1k batch size, single stage, 15 mul (59 tap) decimator - /// 5 insn per input sample, > 1 GS/s on Skylake - #[test] - #[ignore] - fn insn_dec2() { - const N: usize = HBF_TAPS.0.len(); - assert_eq!(N, 15); - const M: usize = 1 << 10; - let mut h = HbfDec::::new(&HBF_TAPS.0); - let mut x = [9.0; M]; - for _ in 0..1 << 20 { - h.process_block(None, &mut x); - } - } - - /// large batch size full decimator cascade (depth 4, 1024 sampled per batch) - /// 4.1 insns per input sample, > 1 GS/s per core - #[test] - #[ignore] - fn insn_casc() { - let mut x = [9.0; 1 << 10]; - let mut h = HbfDecCascade::default(); - h.set_depth(4); - for _ in 0..1 << 20 { - h.process_block(None, &mut x); - } - } -} diff --git a/src/lib.rs b/src/lib.rs index fda8d6d..8d36f4d 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -2,8 +2,6 @@ use thiserror::Error; mod de; pub use de::*; -mod hbf; -pub use hbf::*; mod psd; pub use psd::*; mod loss; diff --git a/src/psd.rs b/src/psd.rs index bec7c61..c667c98 100644 --- a/src/psd.rs +++ b/src/psd.rs @@ -1,4 +1,4 @@ -use crate::{Filter, HbfDecCascade}; +use idsp::hbf::{Filter, HbfDecCascade}; use rustfft::{num_complex::Complex, Fft, FftPlanner}; use std::sync::Arc; @@ -442,7 +442,7 @@ mod test { #[test] fn test() { - assert_eq!(crate::HBF_PASSBAND, 0.4); + assert_eq!(idsp::hbf::HBF_PASSBAND, 0.4); // make uniform noise [-1, 1), ignore the epsilon. let x: Vec<_> = (0..1 << 16) From c263f7aab339aedf8f3a5b4fb08665da8105501f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Robert=20J=C3=B6rdens?= Date: Fri, 15 Sep 2023 23:00:31 +0200 Subject: [PATCH 33/39] psd: move frequencies to Break --- Cargo.lock | 5 +++-- Cargo.toml | 2 +- src/bin/main.rs | 2 +- src/psd.rs | 34 ++++++++++++++++++---------------- 4 files changed, 23 insertions(+), 20 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 13bd8fc..f9ad1a6 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1029,8 +1029,9 @@ dependencies = [ [[package]] name = "idsp" -version = "0.10.0" -source = "git+https://github.com/quartiq/idsp.git?branch=hbf#55051df0fbefde6256e1f3d562835f29c17e50bd" +version = "0.11.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "43632fb78a5a307efb78ae8c3c11961ec1528de34bc034d94801b1ec3465e9e5" dependencies = [ "num-complex", "num-traits", diff --git a/Cargo.toml b/Cargo.toml index ecff30e..bf53863 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -26,7 +26,7 @@ bytemuck = "1.13.1" thiserror = "1.0.47" anyhow = "1.0.75" socket2 = "0.5.3" -idsp = {git = "https://github.com/quartiq/idsp.git", branch = "hbf"} +idsp = "0.11.0" rustfft = "6.1.0" rand = "0.8.5" diff --git a/src/bin/main.rs b/src/bin/main.rs index 722d1fd..8768fc2 100644 --- a/src/bin/main.rs +++ b/src/bin/main.rs @@ -81,7 +81,7 @@ fn main() -> Result<()> { .iter() .map(|dec| { let (p, b) = dec.psd(opts.min_avg); - let f = dec.frequencies(&b); + let f = Break::frequencies(&b); Trace { breaks: b, psd: Vec::from_iter( diff --git a/src/psd.rs b/src/psd.rs index c667c98..770f331 100644 --- a/src/psd.rs +++ b/src/psd.rs @@ -232,6 +232,23 @@ pub struct Break { pub effective_fft_size: usize, } +impl Break { + /// Compute PSD bin center frequencies from stage breaks. + pub fn frequencies(b: &[Break]) -> Vec { + let Some(bi) = b.last() else { return vec![] }; + let mut f = Vec::with_capacity(bi.start + bi.highest_bin); + for bi in b.iter() { + f.truncate(bi.start); + let df = 1.0 / bi.effective_fft_size as f32; + f.extend((0..bi.highest_bin).rev().map(|f| f as f32 * df)); + } + assert_eq!(f.len(), bi.start + bi.highest_bin); + debug_assert_eq!(f.first(), Some(&0.5)); + debug_assert_eq!(f.last(), Some(&0.0)); + f + } +} + /// Online power spectral density estimation /// /// This performs efficient long term power spectral density monitoring in real time. @@ -374,21 +391,6 @@ impl PsdCascade { } (p, b) } - - /// Compute PSD bin center frequencies from stage breaks. - pub fn frequencies(&self, b: &[Break]) -> Vec { - let Some(bi) = b.last() else { return vec![] }; - let mut f = Vec::with_capacity(bi.start + bi.highest_bin); - for bi in b.iter() { - f.truncate(bi.start); - let df = 1.0 / bi.effective_fft_size as f32; - f.extend((0..bi.highest_bin).rev().map(|f| f as f32 * df)); - } - assert_eq!(f.len(), bi.start + bi.highest_bin); - debug_assert_eq!(f.first(), Some(&0.5)); - debug_assert_eq!(f.last(), Some(&0.0)); - f - } } #[cfg(test)] @@ -428,7 +430,7 @@ mod test { s.set_window(Window::hann()); s.process(&x); let (p, b) = s.psd(0); - let f = s.frequencies(&b); + let f = Break::frequencies(&b); println!("{:?}, {:?}", p, f); assert!(p .iter() From 65502751a300e33c931e841802d9e0537a85a2f0 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Robert=20J=C3=B6rdens?= Date: Sat, 16 Sep 2023 22:15:12 +0200 Subject: [PATCH 34/39] add var calculator --- Cargo.lock | 79 ++++++++++++++++++++++++++++++++++++++++++ Cargo.toml | 1 + src/bin/stream_test.rs | 19 ++++++++-- src/lib.rs | 2 ++ src/var.rs | 66 +++++++++++++++++++++++++++++++++++ 5 files changed, 164 insertions(+), 3 deletions(-) create mode 100644 src/var.rs diff --git a/Cargo.lock b/Cargo.lock index f9ad1a6..dec4330 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -593,6 +593,72 @@ dependencies = [ "cfg-if", ] +[[package]] +name = "darling" +version = "0.14.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7b750cb3417fd1b327431a470f388520309479ab0bf5e323505daf0290cd3850" +dependencies = [ + "darling_core", + "darling_macro", +] + +[[package]] +name = "darling_core" +version = "0.14.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "109c1ca6e6b7f82cc233a97004ea8ed7ca123a9af07a8230878fcfda9b158bf0" +dependencies = [ + "fnv", + "ident_case", + "proc-macro2", + "quote", + "strsim", + "syn 1.0.109", +] + +[[package]] +name = "darling_macro" +version = "0.14.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a4aab4dbc9f7611d8b55048a3a16d2d010c2c8334e46304b40ac1cc14bf3b48e" +dependencies = [ + "darling_core", + "quote", + "syn 1.0.109", +] + +[[package]] +name = "derive_builder" +version = "0.12.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8d67778784b508018359cbc8696edb3db78160bab2c2a28ba7f56ef6932997f8" +dependencies = [ + "derive_builder_macro", +] + +[[package]] +name = "derive_builder_core" +version = "0.12.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c11bdc11a0c47bc7d37d582b5285da6849c96681023680b906673c5707af7b0f" +dependencies = [ + "darling", + "proc-macro2", + "quote", + "syn 1.0.109", +] + +[[package]] +name = "derive_builder_macro" +version = "0.12.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ebcda35c7a396850a55ffeac740804b40ffec779b98fffbb1738f4033f0ee79e" +dependencies = [ + "derive_builder_core", + "syn 1.0.109", +] + [[package]] name = "dispatch" version = "0.2.0" @@ -804,6 +870,12 @@ dependencies = [ "miniz_oxide", ] +[[package]] +name = "fnv" +version = "1.0.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3f9eec918d3f24069decb9af1554cad7c880e2da24a9afd88aca000531ab82c1" + [[package]] name = "foreign-types" version = "0.3.2" @@ -1017,6 +1089,12 @@ version = "2.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9a3a5bfb195931eeb336b2a7b4d761daec841b97f947d34394601737a7bba5e4" +[[package]] +name = "ident_case" +version = "1.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b9e0384b61958566e926dc50660321d12159025e767c18e043daf26b70104c39" + [[package]] name = "idna" version = "0.4.0" @@ -1924,6 +2002,7 @@ dependencies = [ "async-std", "bytemuck", "clap", + "derive_builder", "eframe", "env_logger", "idsp", diff --git a/Cargo.toml b/Cargo.toml index bf53863..80e6f5a 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -29,6 +29,7 @@ socket2 = "0.5.3" idsp = "0.11.0" rustfft = "6.1.0" rand = "0.8.5" +derive_builder = "0.12.0" #[build-dependencies] #npm_rs = "0.2.1" diff --git a/src/bin/stream_test.rs b/src/bin/stream_test.rs index d0e0277..33d67c4 100644 --- a/src/bin/stream_test.rs +++ b/src/bin/stream_test.rs @@ -2,7 +2,7 @@ use anyhow::Result; use clap::Parser; use stabilizer_streaming::{ source::{Source, SourceOpts}, - Detrend, Frame, Loss, PsdCascade, + Break, Detrend, Frame, Loss, PsdCascade, VarBuilder, }; use std::sync::mpsc; use std::time::Duration; @@ -53,8 +53,21 @@ fn main() -> Result<()> { loss.analyze(); - let (y, b) = dec[1].psd(4); - println!("{:?}, {:?}", b, y); + let (y, b) = dec[1].psd(1); + log::info!("breaks: {:?}", b); + log::info!("psd: {:?}", y); + + if let Some(b0) = b.last() { + let var = VarBuilder::default().dc_cut(1).clip(1.0).build().unwrap(); + let mut fdev = vec![]; + let mut tau = 1.0; + let f = Break::frequencies(&b); + while tau <= (b0.effective_fft_size / 2) as f32 { + fdev.push((tau, var.eval(&y, &f, tau).sqrt())); + tau *= 2.0; + } + log::info!("fdev: {:?}", fdev); + } Result::<()>::Ok(()) }); diff --git a/src/lib.rs b/src/lib.rs index 8d36f4d..3c81fc8 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -6,6 +6,8 @@ mod psd; pub use psd::*; mod loss; pub use loss::*; +mod var; +pub use var::*; pub mod source; diff --git a/src/var.rs b/src/var.rs new file mode 100644 index 0000000..391bb0e --- /dev/null +++ b/src/var.rs @@ -0,0 +1,66 @@ +use core::f32::consts::PI; +use derive_builder::Builder; + +#[derive(Debug, Builder, Clone, Copy, PartialEq)] +pub struct Var { + /// exponent of `pi*f*tau` in the variance frequency response (-2 for AVAR, -4 for MVAR) + #[builder(default = "-2")] + x_exp: i32, + /// Exponent of `sin(pi*f*tau)` in the variance frequency response (4 for AVAR, 6 for MVAR) + #[builder(default = "4")] + sinx_exp: i32, + /// Response clip (infinite for AVAR and MVAR, 1 for the main lobe: FVAR) + #[builder(default = "f32::MAX")] + clip: f32, + /// skip the fix `dc_cut` bins to suppress DC window leakage + #[builder(default = "2")] + dc_cut: usize, +} + +impl Var { + /// Compute statistical variance estimator (AVAR, MVAR, FVAR...) from Phase PSD + /// + /// # Args + /// * `phase_psd`: Phase noise PSD vector from Nyquist down + /// * `frequencies`: PSD bin frequencies, Nyquist first + pub fn eval(&self, phase_psd: &[f32], frequencies: &[f32], tau: f32) -> f32 { + phase_psd + .iter() + .rev() + .zip( + frequencies + .iter() + .rev() + .take_while(|&f| f * tau <= self.clip), + ) + .skip(self.dc_cut) + // force DC bin to 0 + .fold((0.0, (0.0, 0.0)), |(accu, (a0, f0)), (&sp, &f)| { + // frequency PSD + let sy = sp * f * f; + let pft = PI * (f * tau); + // Allan variance transfer function (rectangular window: sinc**2 and differencing: 2*sin**2) + // Cancel the 2 here with the 0.5 in the trapezoidal rule + let hahd = pft.sin().powi(self.sinx_exp) * pft.powi(self.x_exp); + let a = sy * hahd; + // trapezoidal integration + (accu + (a + a0) * (f - f0), (a, f)) + }) + .0 + } +} + +#[cfg(test)] +mod test { + use super::*; + + #[test] + fn basic() { + let p = [1000.0, 100.0, 1.2, 3.4, 5.6]; + let f = [0.0, 1.0, 3.0, 6.0, 9.0]; + let var = VarBuilder::default().build().unwrap(); + let v = var.eval(&p, &f, 2.7); + println!("{}", v); + assert!((0.13478442 - v).abs() < 1e-6); + } +} From 2badebd58db65bcc2d73c88386bb02a60b34f70e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Robert=20J=C3=B6rdens?= Date: Sat, 16 Sep 2023 22:18:57 +0200 Subject: [PATCH 35/39] loss: don't assert --- src/bin/stream_test.rs | 4 ++-- src/loss.rs | 18 +++++++++--------- 2 files changed, 11 insertions(+), 11 deletions(-) diff --git a/src/bin/stream_test.rs b/src/bin/stream_test.rs index 33d67c4..44fc9b9 100644 --- a/src/bin/stream_test.rs +++ b/src/bin/stream_test.rs @@ -51,8 +51,6 @@ fn main() -> Result<()> { }; } - loss.analyze(); - let (y, b) = dec[1].psd(1); log::info!("breaks: {:?}", b); log::info!("psd: {:?}", y); @@ -69,6 +67,8 @@ fn main() -> Result<()> { log::info!("fdev: {:?}", fdev); } + loss.analyze(); + Result::<()>::Ok(()) }); diff --git a/src/loss.rs b/src/loss.rs index dce3914..e8638f8 100644 --- a/src/loss.rs +++ b/src/loss.rs @@ -26,14 +26,14 @@ impl Loss { } pub fn analyze(&self) { - assert!(self.received > 0); - let loss = self.dropped as f32 / (self.received + self.dropped) as f32; - log::info!( - "Loss: {} % ({} of {})", - loss * 100.0, - self.dropped, - self.received + self.dropped - ); - assert!(loss < 0.05); + if self.received > 0 { + let loss = self.dropped as f32 / (self.received + self.dropped) as f32; + log::info!( + "Loss: {} % ({} of {})", + loss * 100.0, + self.dropped, + self.received + self.dropped + ); + } } } From c7fdc147f6c89784524f7e5d40cb703ac1132850 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Robert=20J=C3=B6rdens?= Date: Sun, 17 Sep 2023 16:13:11 +0200 Subject: [PATCH 36/39] tweak Source --- src/de/frame.rs | 7 ++----- src/source.rs | 25 ++++++++++++++++++------- 2 files changed, 20 insertions(+), 12 deletions(-) diff --git a/src/de/frame.rs b/src/de/frame.rs index 8d3a148..2c0718e 100644 --- a/src/de/frame.rs +++ b/src/de/frame.rs @@ -24,10 +24,7 @@ pub struct Header { impl Header { /// Parse the header of a stream frame. - fn parse(header: &[u8]) -> Result { - if header.len() < HEADER_SIZE { - return Err(Error::InvalidHeader); - } + fn parse(header: &[u8; HEADER_SIZE]) -> Result { if header[..2] != MAGIC_WORD { return Err(Error::InvalidHeader); } @@ -52,7 +49,7 @@ pub struct Frame { impl Frame { /// Parse a stream frame from a single UDP packet. pub fn from_bytes(input: &[u8]) -> Result { - let header = Header::parse(input)?; + let header = Header::parse(&input[..HEADER_SIZE].try_into().unwrap())?; let data = &input[HEADER_SIZE..]; let data: Box = match header.format { Format::AdcDac => Box::new(data::AdcDac::new(header.batches as _, data)?), diff --git a/src/source.rs b/src/source.rs index abb5356..a6dc710 100644 --- a/src/source.rs +++ b/src/source.rs @@ -1,7 +1,11 @@ use anyhow::Result; use clap::Parser; -use std::io::{Read, Seek}; +use std::io::ErrorKind; use std::time::Duration; +use std::{ + fs::File, + io::{BufReader, Read, Seek}, +}; /// Stabilizer stream source options #[derive(Parser, Debug, Clone)] @@ -26,13 +30,16 @@ pub struct SourceOpts { #[derive(Debug)] pub enum Source { Udp(std::net::UdpSocket), - File(std::fs::File, usize), + File(BufReader, usize), } impl Source { pub fn new(opts: &SourceOpts) -> Result { Ok(if let Some(file) = &opts.file { - Self::File(std::fs::File::open(file)?, opts.frame_size) + Self::File( + BufReader::with_capacity(1 << 20, File::open(file)?), + opts.frame_size, + ) } else { log::info!("Binding to {}:{}", opts.ip, opts.port); let socket = std::net::UdpSocket::bind((opts.ip, opts.port))?; @@ -45,11 +52,15 @@ impl Source { pub fn get(&mut self, buf: &mut [u8]) -> Result { Ok(match self { Self::File(fil, n) => loop { - let len = fil.read(&mut buf[..*n])?; - if len == *n { - break len; + match fil.read_exact(&mut buf[..*n]) { + Ok(()) => { + break *n; + } + Err(e) if e.kind() == ErrorKind::UnexpectedEof => { + fil.seek(std::io::SeekFrom::Start(0))?; + } + Err(e) => Err(e)?, } - fil.seek(std::io::SeekFrom::Start(0))?; }, Self::Udp(socket) => socket.recv(buf)?, }) From 033196cda44ee01edc15b554c9838539d2203b56 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Robert=20J=C3=B6rdens?= Date: Sun, 17 Sep 2023 17:54:07 +0200 Subject: [PATCH 37/39] move loss/frame parse to source, add raw single --- src/bin/main.rs | 55 +++++++++++++--------------- src/bin/stream_test.rs | 34 +++++++++-------- src/bin/stream_to_raw.rs | 28 ++++++++++++++ src/psd.rs | 36 ++++++++++-------- src/source.rs | 79 +++++++++++++++++++++++++++++++--------- src/var.rs | 6 ++- 6 files changed, 159 insertions(+), 79 deletions(-) create mode 100644 src/bin/stream_to_raw.rs diff --git a/src/bin/main.rs b/src/bin/main.rs index 8768fc2..95c2e64 100644 --- a/src/bin/main.rs +++ b/src/bin/main.rs @@ -9,7 +9,7 @@ use std::time::Duration; use stabilizer_streaming::{ source::{Source, SourceOpts}, - Break, Detrend, Frame, Loss, PsdCascade, + Break, Detrend, PsdCascade, }; #[derive(Clone, Copy, Debug)] @@ -34,17 +34,14 @@ pub struct Opts { fn main() -> Result<()> { env_logger::init(); - let opts = Opts::parse(); + let Opts { source, min_avg } = Opts::parse(); let (cmd_send, cmd_recv) = mpsc::channel(); let (trace_send, trace_recv) = mpsc::sync_channel(1); let receiver = std::thread::spawn(move || { - let mut source = Source::new(&opts.source)?; - - let mut loss = Loss::default(); + let mut source = Source::new(source)?; let mut dec = Vec::with_capacity(4); - let mut buf = vec![0; 2048]; let mut i = 0usize; loop { match cmd_recv.try_recv() { @@ -56,40 +53,41 @@ fn main() -> Result<()> { if dec.is_empty() { dec.extend((0..4).map(|_| { let mut c = PsdCascade::<{ 1 << 9 }>::default(); - c.set_stage_length(3); + c.set_stage_depth(3); c.set_detrend(Detrend::Mid); c })); i = 0; } - let len = source.get(&mut buf)?; - match Frame::from_bytes(&buf[..len]) { - Ok(frame) => { - loss.update(&frame); - for (dec, x) in dec.iter_mut().zip(frame.data.traces()) { - // let x = (0..1<<10).map(|_| (rand::random::()*2.0 - 1.0)).collect::>(); - dec.process(x); + match source.get() { + Ok(traces) => { + for (dec, x) in dec.iter_mut().zip(traces) { + dec.process(&x); } - i += 1; } - Err(e) => log::warn!("{e} {:?}", &buf[..8]), - }; + Err(e) => log::warn!("source: {}", e), + } + i += 1; + if i > 100 { i = 0; let trace = dec .iter() - .map(|dec| { - let (p, b) = dec.psd(opts.min_avg); - let f = Break::frequencies(&b); - Trace { - breaks: b, - psd: Vec::from_iter( - f[..f.len() - 1] // DC + .map_while(|dec| { + let (p, b) = dec.psd(min_avg); + if p.is_empty() { + None + } else { + let f = Break::frequencies(&b); + Some(Trace { + breaks: b, + psd: f[..f.len() - 1] // DC .iter() .zip(p.iter()) - .map(|(f, p)| [f.log10() as f64, 10.0 * p.log10() as f64]), - ), + .map(|(f, p)| [f.log10() as f64, 10.0 * p.log10() as f64]) + .collect(), + }) } }) .collect(); @@ -105,7 +103,7 @@ fn main() -> Result<()> { } } - loss.analyze(); + source.finish(); Result::<()>::Ok(()) }); @@ -176,8 +174,7 @@ impl eframe::App for FLS { .legend(Legend::default()); plot.show(ui, |plot_ui| { if let Some(traces) = &mut self.current { - for (trace, name) in traces.iter().zip(["AR", "AT", "BI", "BQ"].into_iter()) - { + for (trace, name) in traces.iter().zip("ABCD".chars()) { plot_ui.line(Line::new(PlotPoints::from(trace.psd.clone())).name(name)); } } diff --git a/src/bin/stream_test.rs b/src/bin/stream_test.rs index 44fc9b9..f87bb6c 100644 --- a/src/bin/stream_test.rs +++ b/src/bin/stream_test.rs @@ -2,7 +2,7 @@ use anyhow::Result; use clap::Parser; use stabilizer_streaming::{ source::{Source, SourceOpts}, - Break, Detrend, Frame, Loss, PsdCascade, VarBuilder, + Break, Detrend, PsdCascade, VarBuilder, }; use std::sync::mpsc; use std::time::Duration; @@ -16,42 +16,44 @@ pub struct Opts { #[arg(short, long, default_value_t = 10.0)] duration: f32, + + #[arg(short, long, default_value_t = 0)] + trace: usize, } fn main() -> Result<()> { env_logger::init(); - let opts = Opts::parse(); + let Opts { + source, + duration, + trace, + } = Opts::parse(); let (cmd_send, cmd_recv) = mpsc::channel(); let receiver = std::thread::spawn(move || { - let mut source = Source::new(&opts.source)?; - let mut buf = vec![0u8; 2048]; - - let mut loss = Loss::default(); + let mut source = Source::new(source)?; let mut dec: Vec<_> = (0..4) .map(|_| { let mut c = PsdCascade::<{ 1 << 9 }>::default(); - c.set_stage_length(3); + c.set_stage_depth(3); c.set_detrend(Detrend::Mid); c }) .collect(); while cmd_recv.try_recv() == Err(mpsc::TryRecvError::Empty) { - let len = source.get(&mut buf)?; - match Frame::from_bytes(&buf[..len]) { - Ok(frame) => { - loss.update(&frame); - for (dec, x) in dec.iter_mut().zip(frame.data.traces()) { - dec.process(x); + match source.get() { + Ok(traces) => { + for (dec, x) in dec.iter_mut().zip(traces) { + dec.process(&x); } } Err(e) => log::warn!("{e}"), }; } - let (y, b) = dec[1].psd(1); + let (y, b) = dec[trace].psd(1); log::info!("breaks: {:?}", b); log::info!("psd: {:?}", y); @@ -67,12 +69,12 @@ fn main() -> Result<()> { log::info!("fdev: {:?}", fdev); } - loss.analyze(); + source.finish(); Result::<()>::Ok(()) }); - std::thread::sleep(Duration::from_millis((opts.duration * 1000.) as _)); + std::thread::sleep(Duration::from_millis((duration * 1000.) as _)); cmd_send.send(())?; receiver.join().unwrap()?; diff --git a/src/bin/stream_to_raw.rs b/src/bin/stream_to_raw.rs new file mode 100644 index 0000000..3b790fb --- /dev/null +++ b/src/bin/stream_to_raw.rs @@ -0,0 +1,28 @@ +use anyhow::Result; +use clap::Parser; +use stabilizer_streaming::source::{Source, SourceOpts}; +use std::io::Write; + +#[derive(Parser, Debug)] +pub struct Opts { + #[command(flatten)] + source: SourceOpts, + + #[arg(short, long, default_value_t = 0)] + trace: usize, +} + +fn main() -> Result<()> { + env_logger::init(); + let Opts { trace, source } = Opts::parse(); + + let mut source = Source::new(source)?; + let mut stdout = std::io::BufWriter::new(std::io::stdout()); + + loop { + let t = &source.get()?[trace]; + stdout.write_all(bytemuck::cast_slice(&t[..]))?; + } + + // source.finish(); +} diff --git a/src/psd.rs b/src/psd.rs index 770f331..35397b9 100644 --- a/src/psd.rs +++ b/src/psd.rs @@ -33,7 +33,7 @@ impl Window { /// (conceptually), specifically `win[0] != win[win.len() - 1]`. /// Matplotlib's `matplotlib.mlab.window_hanning()` (but not scipy.signal.get_window()) /// uses the symetric one of period `N-1`, with `win[0] = win[N - 1] = 0` - /// which looses a lot of useful properties (exact nenbw and power independent of `N`, + /// which looses a lot of useful properties (exact nenbw() and power() independent of `N`, /// exact optimal overlap etc) pub fn hann() -> Self { assert!(N > 0); @@ -59,14 +59,14 @@ pub enum Detrend { Mid, /// Remove linear interpolation between first and last item for each segment Span, - // TODO: real mean - // Mean, - // TODO: linear regression - // Linear + // TODO: mean + // TODO: linear } /// Power spectral density accumulator and decimator /// +/// Note: Don't feed more than N*1e7 items without expecting loss of accuracy +/// /// One stage in [PsdCascade]. #[derive(Clone)] pub struct Psd { @@ -101,7 +101,7 @@ impl Psd { drain: 0, }; s.set_overlap(N / 2); - s.set_stage_length(0); + s.set_stage_depth(0); s } @@ -116,7 +116,7 @@ impl Psd { self.detrend = d; } - pub fn set_stage_length(&mut self, n: usize) { + pub fn set_stage_depth(&mut self, n: usize) { self.hbf.set_depth(n); self.drain = self.hbf.response_length(); } @@ -145,16 +145,18 @@ pub trait PsdStage { fn gain(&self) -> f32; /// Number of averages fn count(&self) -> usize; + /// Currently buffered items + fn buf(&self) -> &[f32]; } impl PsdStage for Psd { fn process<'a>(&mut self, mut x: &[f32], y: &'a mut [f32]) -> &'a mut [f32] { let mut n = 0; + let mut chunk; while !x.is_empty() { // load let take = x.len().min(self.buf.len() - self.idx); - let (chunk, rest) = x.split_at(take); - x = rest; + (chunk, x) = x.split_at(take); self.buf[self.idx..][..take].copy_from_slice(chunk); self.idx += take; if self.idx < N { @@ -217,6 +219,10 @@ impl PsdStage for Psd { // overlap is compensated by counting 1.0 / ((self.count * N / 2) as f32 * self.win.nenbw * self.win.power) } + + fn buf(&self) -> &[f32] { + &self.buf[..self.idx] + } } /// Stage break information @@ -304,11 +310,11 @@ impl PsdCascade { self.win = Arc::new(win); } - pub fn set_stage_length(&mut self, n: usize) { + pub fn set_stage_depth(&mut self, n: usize) { assert!(n > 0); self.stage_length = n; for stage in self.stages.iter_mut() { - stage.set_stage_length(n); + stage.set_stage_depth(n); } } @@ -319,7 +325,7 @@ impl PsdCascade { fn get_or_add(&mut self, i: usize) -> &mut Psd { while i >= self.stages.len() { let mut stage = Psd::new(self.fft.clone(), self.win.clone()); - stage.set_stage_length(self.stage_length); + stage.set_stage_depth(self.stage_length); stage.set_detrend(self.detrend); stage.set_overlap(self.overlap); self.stages.push(stage); @@ -402,7 +408,7 @@ mod test { #[ignore] fn insn() { let mut s = PsdCascade::<{ 1 << 9 }>::default(); - s.set_stage_length(3); + s.set_stage_depth(3); s.set_detrend(Detrend::Mid); let x: Vec<_> = (0..1 << 16) .map(|_| rand::random::() * 2.0 - 1.0) @@ -463,7 +469,7 @@ mod test { FftPlanner::new().plan_fft_forward(N), Arc::new(Window::hann()), ); - s.set_stage_length(n); + s.set_stage_depth(n); let mut y = vec![0.0; x.len() >> n]; let y = s.process(&x, &mut y[..]); @@ -481,7 +487,7 @@ mod test { ); let mut d = PsdCascade::::default(); - d.set_stage_length(n); + d.set_stage_depth(n); d.set_detrend(Detrend::None); d.process(&x); let (mut p, b) = d.psd(1); diff --git a/src/source.rs b/src/source.rs index a6dc710..a8c6f2a 100644 --- a/src/source.rs +++ b/src/source.rs @@ -1,3 +1,4 @@ +use crate::{Frame, Loss}; use anyhow::Result; use clap::Parser; use std::io::ErrorKind; @@ -23,46 +24,90 @@ pub struct SourceOpts { file: Option, /// Frame size in file (8 + n_batches*n_channel*batch_size) - #[arg(short, long, default_value_t = 1400)] + #[arg(short, long, default_value_t = 8 + 30 * 2 * 6 * 4)] frame_size: usize, + + /// On a file, wrap around and repeat + #[arg(short, long)] + repeat: bool, + + /// Single f32 raw trace in file, architecture dependent + #[arg(short, long)] + single: Option, } #[derive(Debug)] -pub enum Source { +enum Data { Udp(std::net::UdpSocket), - File(BufReader, usize), + File(BufReader), + Single(BufReader), +} + +pub struct Source { + opts: SourceOpts, + data: Data, + loss: Loss, } impl Source { - pub fn new(opts: &SourceOpts) -> Result { - Ok(if let Some(file) = &opts.file { - Self::File( - BufReader::with_capacity(1 << 20, File::open(file)?), - opts.frame_size, - ) + pub fn new(opts: SourceOpts) -> Result { + let data = if let Some(file) = &opts.file { + Data::File(BufReader::with_capacity(1 << 20, File::open(file)?)) + } else if let Some(single) = &opts.single { + Data::Single(BufReader::with_capacity(1 << 20, File::open(single)?)) } else { log::info!("Binding to {}:{}", opts.ip, opts.port); let socket = std::net::UdpSocket::bind((opts.ip, opts.port))?; socket2::SockRef::from(&socket).set_recv_buffer_size(1 << 20)?; socket.set_read_timeout(Some(Duration::from_millis(1000)))?; - Self::Udp(socket) + Data::Udp(socket) + }; + Ok(Self { + opts, + data, + loss: Loss::default(), }) } - pub fn get(&mut self, buf: &mut [u8]) -> Result { - Ok(match self { - Self::File(fil, n) => loop { - match fil.read_exact(&mut buf[..*n]) { + pub fn get(&mut self) -> Result>> { + let mut buf = [0u8; 2048]; + Ok(match &mut self.data { + Data::File(fil) => loop { + match fil.read_exact(&mut buf[..self.opts.frame_size]) { Ok(()) => { - break *n; + let frame = Frame::from_bytes(&buf[..self.opts.frame_size])?; + self.loss.update(&frame); + break frame.data.traces().into(); } - Err(e) if e.kind() == ErrorKind::UnexpectedEof => { + Err(e) if e.kind() == ErrorKind::UnexpectedEof && self.opts.repeat => { fil.seek(std::io::SeekFrom::Start(0))?; } Err(e) => Err(e)?, } }, - Self::Udp(socket) => socket.recv(buf)?, + Data::Single(fil) => loop { + match fil.read(&mut buf[..]) { + Ok(len) => { + if len == 0 && self.opts.repeat { + fil.seek(std::io::SeekFrom::Start(0))?; + continue; + } + let v: &[[u8; 4]] = bytemuck::cast_slice(&buf[..len / 4 * 4]); + break vec![v.iter().map(|b| f32::from_le_bytes(*b)).collect()]; + } + Err(e) => Err(e)?, + } + }, + Data::Udp(socket) => { + let len = socket.recv(&mut buf[..])?; + let frame = Frame::from_bytes(&buf[..len])?; + self.loss.update(&frame); + frame.data.traces().into() + } }) } + + pub fn finish(&self) { + self.loss.analyze() + } } diff --git a/src/var.rs b/src/var.rs index 391bb0e..4cf7bcc 100644 --- a/src/var.rs +++ b/src/var.rs @@ -56,8 +56,10 @@ mod test { #[test] fn basic() { - let p = [1000.0, 100.0, 1.2, 3.4, 5.6]; - let f = [0.0, 1.0, 3.0, 6.0, 9.0]; + let mut p = [1000.0, 100.0, 1.2, 3.4, 5.6]; + let mut f = [0.0, 1.0, 3.0, 6.0, 9.0]; + p.reverse(); + f.reverse(); let var = VarBuilder::default().build().unwrap(); let v = var.eval(&p, &f, 2.7); println!("{}", v); From fa2d1b6ab577d118c3bc65cb4984301d6c21de8c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Robert=20J=C3=B6rdens?= Date: Sun, 17 Sep 2023 22:22:03 +0200 Subject: [PATCH 38/39] psd: decimate entire first block --- src/psd.rs | 41 ++++++++++++++++++++++++++++------------- 1 file changed, 28 insertions(+), 13 deletions(-) diff --git a/src/psd.rs b/src/psd.rs index 35397b9..bf34000 100644 --- a/src/psd.rs +++ b/src/psd.rs @@ -108,7 +108,8 @@ impl Psd { pub fn set_overlap(&mut self, o: usize) { assert_eq!(o % self.hbf.block_size().0, 0); assert!(self.hbf.block_size().1 >= o); - assert!(o < N); + assert!(o <= N / 2); + // TODO assert w.r.t. decimation workspace self.overlap = o; } @@ -152,10 +153,10 @@ pub trait PsdStage { impl PsdStage for Psd { fn process<'a>(&mut self, mut x: &[f32], y: &'a mut [f32]) -> &'a mut [f32] { let mut n = 0; - let mut chunk; while !x.is_empty() { // load let take = x.len().min(self.buf.len() - self.idx); + let chunk; (chunk, x) = x.split_at(take); self.buf[self.idx..][..take].copy_from_slice(chunk); self.idx += take; @@ -163,7 +164,7 @@ impl PsdStage for Psd { break; } // compute detrend - let (mut mean, slope) = match self.detrend { + let (mut offset, slope) = match self.detrend { Detrend::None => (0.0, 0.0), Detrend::Mid => (self.buf[N / 2], 0.0), Detrend::Span => ( @@ -172,10 +173,11 @@ impl PsdStage for Psd { ), }; // apply detrending, window, make complex + // TODO; the loop doesn't appear to optimize well let mut c = [Complex::default(); N]; for (c, (x, w)) in c.iter_mut().zip(self.buf.iter().zip(self.win.win.iter())) { - c.re = (x - mean) * w; - mean += slope; + c.re = (x - offset) * w; + offset += slope; } // fft in-place self.fft.process(&mut c); @@ -188,10 +190,19 @@ impl PsdStage for Psd { { *p += c.norm_sqr(); } - self.count += 1; + + let start = if self.count == 0 { + // decimate all, keep overlap later + 0 + } else { + // keep overlap + self.buf.copy_within(N - self.overlap..N, 0); + // decimate only new + self.overlap + }; // decimate overlap - let mut yi = self.hbf.process_block(None, &mut self.buf[..self.overlap]); + let mut yi = self.hbf.process_block(None, &mut self.buf[start..]); // drain decimator impulse response to initial state (zeros) let skip = self.drain.min(yi.len()); self.drain -= skip; @@ -199,9 +210,13 @@ impl PsdStage for Psd { // yield l y[n..][..yi.len()].copy_from_slice(yi); n += yi.len(); - // drop the left keep the right as overlap - self.buf.copy_within(self.overlap..N, 0); - self.idx = N - self.overlap; + + if self.count == 0 { + self.buf.copy_within(N - self.overlap..N, 0); + } + + self.count += 1; + self.idx = self.overlap; } &mut y[..n] } @@ -428,8 +443,8 @@ mod test { ); let x = vec![1.0; N]; let mut y = vec![0.0; N]; - let y = s.process(&x, &mut y[..]); - assert_eq!(y, &x[..N / 2]); + let y = s.process(&x, &mut y); + assert_eq!(y, &x[..N]); println!("{:?}, {}", s.spectrum(), s.gain()); let mut s = PsdCascade::::default(); @@ -475,7 +490,7 @@ mod test { let mut hbf = HbfDecCascade::default(); hbf.set_depth(n); - assert_eq!(y.len(), ((x.len() - N / 2) >> n) - hbf.response_length()); + assert_eq!(y.len(), (x.len() >> n) - hbf.response_length()); let p: Vec<_> = s.spectrum().iter().map(|p| p * s.gain()).collect(); // psd of a stage assert!( From b0e893afedf9077d719138c2a23714dbbb6f4edc Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Robert=20J=C3=B6rdens?= Date: Mon, 18 Sep 2023 23:15:08 +0200 Subject: [PATCH 39/39] tune detrend --- src/bin/main.rs | 2 +- src/psd.rs | 51 ++++++++++++++++++++++++++++++++----------------- 2 files changed, 35 insertions(+), 18 deletions(-) diff --git a/src/bin/main.rs b/src/bin/main.rs index 95c2e64..0874da0 100644 --- a/src/bin/main.rs +++ b/src/bin/main.rs @@ -70,7 +70,7 @@ fn main() -> Result<()> { } i += 1; - if i > 100 { + if i > 200 { i = 0; let trace = dec .iter() diff --git a/src/psd.rs b/src/psd.rs index bf34000..af155c7 100644 --- a/src/psd.rs +++ b/src/psd.rs @@ -121,6 +121,37 @@ impl Psd { self.hbf.set_depth(n); self.drain = self.hbf.response_length(); } + + fn apply_window(&self) -> [Complex; N] { + // apply detrending, window, make complex + let mut c = [Complex::default(); N]; + + match self.detrend { + Detrend::None => { + for ((c, x), w) in c.iter_mut().zip(&self.buf).zip(&self.win.win) { + c.re = x * w; + c.im = 0.0; + } + } + Detrend::Mid => { + let offset = self.buf[N / 2]; + for ((c, x), w) in c.iter_mut().zip(&self.buf).zip(&self.win.win) { + c.re = (x - offset) * w; + c.im = 0.0; + } + } + Detrend::Span => { + let mut offset = self.buf[0]; + let slope = (self.buf[N - 1] - self.buf[0]) / (N - 1) as f32; + for ((c, x), w) in c.iter_mut().zip(&self.buf).zip(&self.win.win) { + c.re = (x - offset) * w; + c.im = 0.0; + offset += slope; + } + } + }; + c + } } pub trait PsdStage { @@ -163,22 +194,8 @@ impl PsdStage for Psd { if self.idx < N { break; } - // compute detrend - let (mut offset, slope) = match self.detrend { - Detrend::None => (0.0, 0.0), - Detrend::Mid => (self.buf[N / 2], 0.0), - Detrend::Span => ( - self.buf[0], - (self.buf[N - 1] - self.buf[0]) / (N - 1) as f32, - ), - }; - // apply detrending, window, make complex - // TODO; the loop doesn't appear to optimize well - let mut c = [Complex::default(); N]; - for (c, (x, w)) in c.iter_mut().zip(self.buf.iter().zip(self.win.win.iter())) { - c.re = (x - offset) * w; - offset += slope; - } + + let mut c = self.apply_window(); // fft in-place self.fft.process(&mut c); // convert positive frequency spectrum to power @@ -418,7 +435,7 @@ impl PsdCascade { mod test { use super::*; - /// 44 insns per input sample: > 100 MS/s per core + /// 36 insns per input sample: > 190 MS/s per skylake core #[test] #[ignore] fn insn() {