generated from lks/eframe_template_android
Compare commits
26 Commits
7f6d5069f8
...
main
| Author | SHA1 | Date | |
|---|---|---|---|
| 03d076cb94 | |||
| ab8abd635b | |||
| a8340a3fb9 | |||
| edcf10f73c | |||
| d1271c1f7d | |||
| 4724d23459 | |||
| 7abb089c04 | |||
| d878f66bd8 | |||
| 9f18bb6920 | |||
| 8978be49bd | |||
| f3216e2d8d | |||
| 243fcdb31e | |||
| 02c0f54079 | |||
| 54826d50a6 | |||
| 6445949083 | |||
| dbbd6ab849 | |||
| 24d9fc7972 | |||
| 162220aa72 | |||
| fc9e04ffd2 | |||
| 63ec587d08 | |||
| e37eb169ce | |||
| e5dc145ada | |||
| e59f6ce896 | |||
| f6f6a5b1ef | |||
| 5cecec3f70 | |||
| 636f67c6de |
1
.gitignore
vendored
1
.gitignore
vendored
@@ -1,3 +1,4 @@
|
|||||||
target
|
target
|
||||||
.DS_Store
|
.DS_Store
|
||||||
dist
|
dist
|
||||||
|
heaptrack.*.zst
|
||||||
|
|||||||
3130
Cargo.lock
generated
3130
Cargo.lock
generated
File diff suppressed because it is too large
Load Diff
21
Cargo.toml
21
Cargo.toml
@@ -7,34 +7,39 @@ edition = "2021"
|
|||||||
|
|
||||||
# Common dependencies
|
# Common dependencies
|
||||||
[dependencies]
|
[dependencies]
|
||||||
egui = "0.27.0"
|
anyhow = "1.0.98"
|
||||||
log = "0.4.21"
|
cpal = "0.17.1"
|
||||||
|
egui = "0.31"
|
||||||
|
egui_plot = "0.32"
|
||||||
|
log = "0.4.27"
|
||||||
|
realfft = "3.4.0"
|
||||||
|
|
||||||
# eframe features for non android targets
|
# eframe features for non android targets
|
||||||
[target.'cfg(not(target_os = "android"))'.dependencies.eframe]
|
[target.'cfg(not(target_os = "android"))'.dependencies.eframe]
|
||||||
version = "0.27.0"
|
version = "0.31"
|
||||||
default-features = false
|
default-features = false
|
||||||
features = ["accesskit", "default_fonts", "glow"]
|
features = ["accesskit", "default_fonts", "glow", "wayland", "x11"]
|
||||||
|
|
||||||
# eframe features for android targets
|
# eframe features for android targets
|
||||||
[target.'cfg(target_os = "android")'.dependencies.eframe]
|
[target.'cfg(target_os = "android")'.dependencies.eframe]
|
||||||
version = "0.27.0"
|
version = "0.31"
|
||||||
default-features = false
|
default-features = false
|
||||||
features = ["accesskit", "default_fonts", "glow", "android-native-activity"]
|
features = ["accesskit", "default_fonts", "glow", "android-native-activity"]
|
||||||
|
|
||||||
# android only dependencies
|
# android only dependencies
|
||||||
[target.'cfg(target_os = "android")'.dependencies]
|
[target.'cfg(target_os = "android")'.dependencies]
|
||||||
android_logger = "0.13.3"
|
android_logger = "0.15.0"
|
||||||
#android-activity = { version = "0.5", features = ["native-activity"] }
|
#android-activity = { version = "0.5", features = ["native-activity"] }
|
||||||
winit = { version = "0.29.4", features = ["android-native-activity"] }
|
winit = { version = "0.30.11", features = ["android-native-activity"] }
|
||||||
|
|
||||||
# native only dependencies
|
# native only dependencies
|
||||||
[target.'cfg(all(not(target_arch = "wasm32"), not(target_os = "android")))'.dependencies]
|
[target.'cfg(all(not(target_arch = "wasm32"), not(target_os = "android")))'.dependencies]
|
||||||
env_logger = "0.10"
|
env_logger = "0.11"
|
||||||
|
|
||||||
# web only dependencies
|
# web only dependencies
|
||||||
[target.'cfg(target_arch = "wasm32")'.dependencies]
|
[target.'cfg(target_arch = "wasm32")'.dependencies]
|
||||||
wasm-bindgen-futures = "0.4"
|
wasm-bindgen-futures = "0.4"
|
||||||
|
web-sys = "0.3.70" # to access the DOM (to hide the loading text)
|
||||||
|
|
||||||
[profile.release]
|
[profile.release]
|
||||||
opt-level = 2
|
opt-level = 2
|
||||||
|
|||||||
@@ -3,18 +3,18 @@
|
|||||||
"short_name": "egui-template-pwa",
|
"short_name": "egui-template-pwa",
|
||||||
"icons": [
|
"icons": [
|
||||||
{
|
{
|
||||||
"src": "./icon-256.png",
|
"src": "./assets/icon-256.png",
|
||||||
"sizes": "256x256",
|
"sizes": "256x256",
|
||||||
"type": "image/png"
|
"type": "image/png"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"src": "./maskable_icon_x512.png",
|
"src": "./assets/maskable_icon_x512.png",
|
||||||
"sizes": "512x512",
|
"sizes": "512x512",
|
||||||
"type": "image/png",
|
"type": "image/png",
|
||||||
"purpose": "any maskable"
|
"purpose": "any maskable"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"src": "./icon-1024.png",
|
"src": "./assets/icon-1024.png",
|
||||||
"sizes": "1024x1024",
|
"sizes": "1024x1024",
|
||||||
"type": "image/png"
|
"type": "image/png"
|
||||||
}
|
}
|
||||||
|
|||||||
30
index.html
30
index.html
@@ -10,7 +10,7 @@
|
|||||||
<title>eframe template</title>
|
<title>eframe template</title>
|
||||||
|
|
||||||
<!-- config for our rust wasm binary. go to https://trunkrs.dev/assets/#rust for more customization -->
|
<!-- config for our rust wasm binary. go to https://trunkrs.dev/assets/#rust for more customization -->
|
||||||
<link data-trunk rel="rust" data-target-name="eframe-template" data-wasm-opt="2" />
|
<link data-trunk rel="rust" data-target-name="waterfall" data-wasm-opt="2" />
|
||||||
<!-- this is the base url relative to which other urls will be constructed. trunk will insert this from the public-url option -->
|
<!-- this is the base url relative to which other urls will be constructed. trunk will insert this from the public-url option -->
|
||||||
<base data-trunk-public-url />
|
<base data-trunk-public-url />
|
||||||
|
|
||||||
@@ -19,14 +19,14 @@
|
|||||||
|
|
||||||
<link data-trunk rel="copy-file" href="assets/sw.js"/>
|
<link data-trunk rel="copy-file" href="assets/sw.js"/>
|
||||||
<link data-trunk rel="copy-file" href="assets/manifest.json"/>
|
<link data-trunk rel="copy-file" href="assets/manifest.json"/>
|
||||||
<link data-trunk rel="copy-file" href="assets/icon-1024.png" />
|
<link data-trunk rel="copy-file" href="assets/icon-1024.png" data-target-path="assets"/>
|
||||||
<link data-trunk rel="copy-file" href="assets/icon-256.png" />
|
<link data-trunk rel="copy-file" href="assets/icon-256.png" data-target-path="assets"/>
|
||||||
<link data-trunk rel="copy-file" href="assets/icon_ios_touch_192.png" />
|
<link data-trunk rel="copy-file" href="assets/icon_ios_touch_192.png" data-target-path="assets"/>
|
||||||
<link data-trunk rel="copy-file" href="assets/maskable_icon_x512.png" />
|
<link data-trunk rel="copy-file" href="assets/maskable_icon_x512.png" data-target-path="assets"/>
|
||||||
|
|
||||||
|
|
||||||
<link rel="manifest" href="manifest.json">
|
<link rel="manifest" href="manifest.json">
|
||||||
<link rel="apple-touch-icon" href="icon_ios_touch_192.png">
|
<link rel="apple-touch-icon" href="assets/icon_ios_touch_192.png">
|
||||||
<meta name="theme-color" media="(prefers-color-scheme: light)" content="white">
|
<meta name="theme-color" media="(prefers-color-scheme: light)" content="white">
|
||||||
<meta name="theme-color" media="(prefers-color-scheme: dark)" content="#404040">
|
<meta name="theme-color" media="(prefers-color-scheme: dark)" content="#404040">
|
||||||
|
|
||||||
@@ -60,15 +60,16 @@
|
|||||||
width: 100%;
|
width: 100%;
|
||||||
}
|
}
|
||||||
|
|
||||||
/* Position canvas in center-top: */
|
/* Make canvas fill entire document: */
|
||||||
canvas {
|
canvas {
|
||||||
margin-right: auto;
|
margin-right: auto;
|
||||||
margin-left: auto;
|
margin-left: auto;
|
||||||
display: block;
|
display: block;
|
||||||
position: absolute;
|
position: absolute;
|
||||||
top: 0%;
|
top: 0;
|
||||||
left: 50%;
|
left: 0;
|
||||||
transform: translate(-50%, 0%);
|
width: 100%;
|
||||||
|
height: 100%;
|
||||||
}
|
}
|
||||||
|
|
||||||
.centered {
|
.centered {
|
||||||
@@ -114,7 +115,6 @@
|
|||||||
transform: rotate(360deg);
|
transform: rotate(360deg);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
</style>
|
</style>
|
||||||
</head>
|
</head>
|
||||||
|
|
||||||
@@ -123,6 +123,14 @@
|
|||||||
<!-- the id is hardcoded in main.rs . so, make sure both match. -->
|
<!-- the id is hardcoded in main.rs . so, make sure both match. -->
|
||||||
<canvas id="the_canvas_id"></canvas>
|
<canvas id="the_canvas_id"></canvas>
|
||||||
|
|
||||||
|
<!-- the loading spinner will be removed in main.rs -->
|
||||||
|
<div class="centered" id="loading_text">
|
||||||
|
<p style="font-size:16px">
|
||||||
|
Loading…
|
||||||
|
</p>
|
||||||
|
<div class="lds-dual-ring"></div>
|
||||||
|
</div>
|
||||||
|
|
||||||
<!--Register Service Worker. this will cache the wasm / js scripts for offline use (for PWA functionality). -->
|
<!--Register Service Worker. this will cache the wasm / js scripts for offline use (for PWA functionality). -->
|
||||||
<!-- Force refresh (Ctrl + F5) to load the latest files instead of cached files -->
|
<!-- Force refresh (Ctrl + F5) to load the latest files instead of cached files -->
|
||||||
<script>
|
<script>
|
||||||
|
|||||||
@@ -5,6 +5,6 @@
|
|||||||
# to the user in the error, instead of "error: invalid channel name '[toolchain]'".
|
# to the user in the error, instead of "error: invalid channel name '[toolchain]'".
|
||||||
|
|
||||||
[toolchain]
|
[toolchain]
|
||||||
channel = "1.76.0"
|
channel = "1.87.0"
|
||||||
components = [ "rustfmt", "clippy" ]
|
components = [ "rustfmt", "clippy" ]
|
||||||
targets = [ "wasm32-unknown-unknown", "aarch64-linux-android" ]
|
targets = [ "wasm32-unknown-unknown", "aarch64-linux-android" ]
|
||||||
|
|||||||
182
src/app.rs
182
src/app.rs
@@ -1,18 +1,29 @@
|
|||||||
use eframe::{egui_glow, glow};
|
use eframe::{egui_glow, glow};
|
||||||
use egui::mutex::Mutex;
|
use egui::{mutex::Mutex, ScrollArea};
|
||||||
use std::sync::Arc;
|
use std::sync::Arc;
|
||||||
|
|
||||||
|
use crate::backend::{self, Backends};
|
||||||
|
|
||||||
|
pub mod debug_plot;
|
||||||
|
use debug_plot::DebugPlots;
|
||||||
mod waterfall;
|
mod waterfall;
|
||||||
use waterfall::Waterfall;
|
use waterfall::Waterfall;
|
||||||
|
mod fft;
|
||||||
|
use fft::Fft;
|
||||||
pub mod turbo_colormap;
|
pub mod turbo_colormap;
|
||||||
|
|
||||||
/// We derive Deserialize/Serialize so we can persist app state on shutdown.
|
const FFT_SIZE: usize = 1024;
|
||||||
|
|
||||||
pub struct TemplateApp {
|
pub struct TemplateApp {
|
||||||
// Example stuff:
|
plots: DebugPlots,
|
||||||
label: String,
|
|
||||||
value: f32,
|
|
||||||
/// Behind an `Arc<Mutex<…>>` so we can pass it to [`egui::PaintCallback`] and paint later.
|
/// Behind an `Arc<Mutex<…>>` so we can pass it to [`egui::PaintCallback`] and paint later.
|
||||||
waterfall: Arc<Mutex<Waterfall>>,
|
waterfall: Arc<Mutex<Waterfall>>,
|
||||||
|
fft: Fft,
|
||||||
|
backends: backend::Backends,
|
||||||
|
selected_backend: usize,
|
||||||
|
open_device: Option<Box<dyn backend::Device>>,
|
||||||
|
device_window_open: bool,
|
||||||
|
side_panel_open: bool,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl TemplateApp {
|
impl TemplateApp {
|
||||||
@@ -24,16 +35,25 @@ impl TemplateApp {
|
|||||||
// Load previous app state (if any).
|
// Load previous app state (if any).
|
||||||
// Note that you must enable the `persistence` feature for this to work.
|
// Note that you must enable the `persistence` feature for this to work.
|
||||||
|
|
||||||
|
let plots = DebugPlots::new();
|
||||||
|
|
||||||
|
let (fft, rx) = Fft::new(FFT_SIZE, plots.get_sender()).unwrap();
|
||||||
|
|
||||||
|
let wf_size = fft.output_len;
|
||||||
let gl = cc
|
let gl = cc
|
||||||
.gl
|
.gl
|
||||||
.as_ref()
|
.as_ref()
|
||||||
.expect("Could not get gl context from glow backend");
|
.expect("Could not get gl context from glow backend");
|
||||||
|
|
||||||
Self {
|
Self {
|
||||||
// Example stuff:
|
plots,
|
||||||
label: "Hello World!".to_owned(),
|
waterfall: Arc::new(Mutex::new(Waterfall::new(gl, wf_size, wf_size, rx))),
|
||||||
value: 2.7,
|
fft,
|
||||||
waterfall: Arc::new(Mutex::new(Waterfall::new(gl, 300, 300))),
|
backends: Backends::default(),
|
||||||
|
selected_backend: 0,
|
||||||
|
open_device: None,
|
||||||
|
device_window_open: true,
|
||||||
|
side_panel_open: false,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -52,53 +72,65 @@ impl eframe::App for TemplateApp {
|
|||||||
|
|
||||||
/// Called each time the UI needs repainting, which may be many times per second.
|
/// Called each time the UI needs repainting, which may be many times per second.
|
||||||
fn update(&mut self, ctx: &egui::Context, _frame: &mut eframe::Frame) {
|
fn update(&mut self, ctx: &egui::Context, _frame: &mut eframe::Frame) {
|
||||||
// Put your widgets into a `SidePanel`, `TopBottomPanel`, `CentralPanel`, `Window` or `Area`.
|
ctx.request_repaint();
|
||||||
// For inspiration and more examples, go to https://emilk.github.io/egui
|
self.plots.update_plots();
|
||||||
|
|
||||||
|
// Menu bar panel
|
||||||
egui::TopBottomPanel::top("top_panel").show(ctx, |ui| {
|
egui::TopBottomPanel::top("top_panel").show(ctx, |ui| {
|
||||||
// The top panel is often a good place for a menu bar:
|
|
||||||
|
|
||||||
egui::menu::bar(ui, |ui| {
|
egui::menu::bar(ui, |ui| {
|
||||||
// NOTE: no File->Quit on web pages!
|
// NOTE: no File->Quit on web pages!
|
||||||
let is_web = cfg!(target_arch = "wasm32");
|
let is_web = cfg!(target_arch = "wasm32");
|
||||||
if !is_web {
|
|
||||||
ui.menu_button("File", |ui| {
|
ui.menu_button("File", |ui| {
|
||||||
|
if ui.button("Open Device").clicked() {
|
||||||
|
self.device_window_open = true;
|
||||||
|
}
|
||||||
|
if self.open_device.is_some() {
|
||||||
|
if ui.button("Close Device").clicked() {
|
||||||
|
if let Some(dev) = self.open_device.take() {
|
||||||
|
dev.close();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if !is_web {
|
||||||
if ui.button("Quit").clicked() {
|
if ui.button("Quit").clicked() {
|
||||||
ctx.send_viewport_cmd(egui::ViewportCommand::Close);
|
ctx.send_viewport_cmd(egui::ViewportCommand::Close);
|
||||||
}
|
}
|
||||||
|
}
|
||||||
});
|
});
|
||||||
|
ui.menu_button("View", |ui| {
|
||||||
|
ui.checkbox(&mut self.side_panel_open, "Side Panel");
|
||||||
|
});
|
||||||
|
|
||||||
|
self.plots.render_menu_buttons(ui);
|
||||||
ui.add_space(16.0);
|
ui.add_space(16.0);
|
||||||
|
|
||||||
|
egui::widgets::global_theme_preference_buttons(ui);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
// Side panel
|
||||||
|
egui::SidePanel::right("Sid panel").show_animated(ctx, self.side_panel_open, |ui| {
|
||||||
|
if let Some(d) = &mut self.open_device {
|
||||||
|
d.show_settings(ui)
|
||||||
}
|
}
|
||||||
|
|
||||||
egui::widgets::global_dark_light_mode_buttons(ui);
|
|
||||||
});
|
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// Central panel
|
||||||
egui::CentralPanel::default().show(ctx, |ui| {
|
egui::CentralPanel::default().show(ctx, |ui| {
|
||||||
// The central panel the region left after adding TopPanel's and SidePanel's
|
egui::TopBottomPanel::top("Plot")
|
||||||
ui.heading("eframe template");
|
.resizable(true)
|
||||||
|
.show_inside(ui, |_ui| {
|
||||||
ui.horizontal(|ui| {
|
// TODO: Add plot
|
||||||
ui.label("Write something: ");
|
|
||||||
ui.text_edit_singleline(&mut self.label);
|
|
||||||
});
|
|
||||||
|
|
||||||
ui.add(egui::Slider::new(&mut self.value, 0.0..=10.0).text("value"));
|
|
||||||
if ui.button("Increment").clicked() {
|
|
||||||
self.value += 1.0;
|
|
||||||
}
|
|
||||||
|
|
||||||
ui.separator();
|
|
||||||
ui.horizontal(|ui| {
|
|
||||||
ui.spacing_mut().item_spacing.x = 0.0;
|
|
||||||
ui.label("The texture is being painted using ");
|
|
||||||
ui.hyperlink_to("glow", "https://github.com/grovesNL/glow");
|
|
||||||
ui.label(" (OpenGL).");
|
|
||||||
});
|
});
|
||||||
|
|
||||||
|
egui::CentralPanel::default().show_inside(ui, |ui| {
|
||||||
|
ui.with_layout(egui::Layout::bottom_up(egui::Align::LEFT), |ui| {
|
||||||
|
powered_by_egui_and_eframe(ui);
|
||||||
|
egui::warn_if_debug_build(ui);
|
||||||
egui::Frame::canvas(ui.style()).show(ui, |ui| {
|
egui::Frame::canvas(ui.style()).show(ui, |ui| {
|
||||||
|
let available_space = ui.available_size();
|
||||||
let (rect, response) =
|
let (rect, response) =
|
||||||
ui.allocate_exact_size(egui::Vec2::splat(300.0), egui::Sense::drag());
|
ui.allocate_exact_size(available_space, egui::Sense::drag());
|
||||||
|
|
||||||
let _angle = response.drag_motion().x * 0.01;
|
let _angle = response.drag_motion().x * 0.01;
|
||||||
|
|
||||||
@@ -115,18 +147,70 @@ impl eframe::App for TemplateApp {
|
|||||||
};
|
};
|
||||||
ui.painter().add(callback);
|
ui.painter().add(callback);
|
||||||
});
|
});
|
||||||
ui.separator();
|
|
||||||
|
|
||||||
ui.add(egui::github_link_file!(
|
|
||||||
"https://github.com/emilk/eframe_template/blob/main/",
|
|
||||||
"Source code."
|
|
||||||
));
|
|
||||||
|
|
||||||
ui.with_layout(egui::Layout::bottom_up(egui::Align::LEFT), |ui| {
|
|
||||||
powered_by_egui_and_eframe(ui);
|
|
||||||
egui::warn_if_debug_build(ui);
|
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
// Update debug plot windows
|
||||||
|
self.plots.render_plot_windows(ctx);
|
||||||
|
|
||||||
|
// Update device selection window
|
||||||
|
let mut device_window = egui::Window::new("Select Device")
|
||||||
|
.default_width(600.0)
|
||||||
|
.default_height(400.0)
|
||||||
|
.vscroll(false)
|
||||||
|
.resizable(true)
|
||||||
|
.collapsible(false);
|
||||||
|
if self.open_device.is_some() {
|
||||||
|
device_window = device_window.open(&mut self.device_window_open);
|
||||||
|
} else {
|
||||||
|
device_window = device_window.anchor(egui::Align2::CENTER_CENTER, [0., 0.]);
|
||||||
|
}
|
||||||
|
let mut close_device_window = false;
|
||||||
|
device_window.show(ctx, |ui| {
|
||||||
|
egui::SidePanel::left("Select Driver")
|
||||||
|
.resizable(true)
|
||||||
|
.default_width(150.0)
|
||||||
|
.width_range(80.0..=200.0)
|
||||||
|
.show_inside(ui, |ui| {
|
||||||
|
ScrollArea::vertical().show(ui, |ui| {
|
||||||
|
ui.with_layout(egui::Layout::top_down_justified(egui::Align::LEFT), |ui| {
|
||||||
|
for (i, b) in self.backends.0.iter().enumerate() {
|
||||||
|
ui.selectable_value(
|
||||||
|
&mut self.selected_backend,
|
||||||
|
i,
|
||||||
|
b.display_text(),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
ui.vertical_centered(|ui| {
|
||||||
|
egui::ScrollArea::vertical().show(ui, |ui| {
|
||||||
|
//if self._selected_backend < self._backends.0.len() {
|
||||||
|
if let Some(b) = self.backends.0.get_mut(self.selected_backend) {
|
||||||
|
//let mut b = &self._backends.0[self._selected_backend];
|
||||||
|
b.show_device_selection(ui);
|
||||||
|
if ui.add(egui::Button::new("Apply")).clicked() {
|
||||||
|
if let Some(dev) = self.open_device.take() {
|
||||||
|
dev.close()
|
||||||
|
};
|
||||||
|
if let Ok(device) =
|
||||||
|
b.build_device(self.fft.tx.clone(), self.plots.get_sender())
|
||||||
|
{
|
||||||
|
self.open_device = Some(device);
|
||||||
|
close_device_window = true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
ui.add(egui::Label::new("Select a Device Driver"));
|
||||||
|
}
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
if close_device_window {
|
||||||
|
self.device_window_open = false;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -135,11 +219,13 @@ fn powered_by_egui_and_eframe(ui: &mut egui::Ui) {
|
|||||||
ui.spacing_mut().item_spacing.x = 0.0;
|
ui.spacing_mut().item_spacing.x = 0.0;
|
||||||
ui.label("Powered by ");
|
ui.label("Powered by ");
|
||||||
ui.hyperlink_to("egui", "https://github.com/emilk/egui");
|
ui.hyperlink_to("egui", "https://github.com/emilk/egui");
|
||||||
ui.label(" and ");
|
ui.label(", ");
|
||||||
ui.hyperlink_to(
|
ui.hyperlink_to(
|
||||||
"eframe",
|
"eframe",
|
||||||
"https://github.com/emilk/egui/tree/master/crates/eframe",
|
"https://github.com/emilk/egui/tree/master/crates/eframe",
|
||||||
);
|
);
|
||||||
|
ui.label(" and ");
|
||||||
|
ui.hyperlink_to("glow", "https://github.com/grovesNL/glow");
|
||||||
ui.label(".");
|
ui.label(".");
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|||||||
144
src/app/debug_plot.rs
Normal file
144
src/app/debug_plot.rs
Normal file
@@ -0,0 +1,144 @@
|
|||||||
|
use std::collections::HashMap;
|
||||||
|
use std::sync::mpsc;
|
||||||
|
|
||||||
|
use egui::{Context, Ui};
|
||||||
|
use egui_plot::{Line, Plot, PlotBounds, PlotPoints};
|
||||||
|
use realfft::num_complex::Complex32;
|
||||||
|
|
||||||
|
pub enum PlotData {
|
||||||
|
U8(Vec<u8>),
|
||||||
|
F32(Vec<f32>),
|
||||||
|
Bode32(Vec<Complex32>),
|
||||||
|
}
|
||||||
|
#[derive(Clone)]
|
||||||
|
pub struct DebugPlotSender {
|
||||||
|
tx: mpsc::SyncSender<(&'static str, PlotData)>,
|
||||||
|
}
|
||||||
|
impl DebugPlotSender {
|
||||||
|
pub fn send(
|
||||||
|
&self,
|
||||||
|
plot_name: &'static str,
|
||||||
|
plot_data: PlotData,
|
||||||
|
) -> Result<(), mpsc::SendError<PlotData>> {
|
||||||
|
match self.tx.try_send((plot_name, plot_data)) {
|
||||||
|
Err(mpsc::TrySendError::Full(_)) => {
|
||||||
|
log::warn!("Debug buffer is full!");
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
Err(mpsc::TrySendError::Disconnected((_, d))) => Err(mpsc::SendError(d)),
|
||||||
|
Ok(()) => Ok(()),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
pub struct DebugPlots {
|
||||||
|
plots: HashMap<&'static str, PlotData>,
|
||||||
|
plot_en: HashMap<&'static str, bool>,
|
||||||
|
rx: mpsc::Receiver<(&'static str, PlotData)>,
|
||||||
|
tx: DebugPlotSender,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl DebugPlots {
|
||||||
|
pub fn new() -> Self {
|
||||||
|
let (tx, rx) = mpsc::sync_channel(128);
|
||||||
|
DebugPlots {
|
||||||
|
plots: HashMap::new(),
|
||||||
|
plot_en: HashMap::new(),
|
||||||
|
rx,
|
||||||
|
tx: DebugPlotSender { tx },
|
||||||
|
}
|
||||||
|
}
|
||||||
|
pub fn get_sender(&self) -> DebugPlotSender {
|
||||||
|
self.tx.clone()
|
||||||
|
}
|
||||||
|
pub fn update_plots(&mut self) {
|
||||||
|
while let Ok((key, plot)) = self.rx.try_recv() {
|
||||||
|
if self.plots.insert(key, plot).is_none() {
|
||||||
|
self.plot_en.insert(key, false);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
pub fn render_menu_buttons(&mut self, ui: &mut Ui) {
|
||||||
|
ui.menu_button("Debug Plots", |ui| {
|
||||||
|
for &k in self.plots.keys() {
|
||||||
|
if !self.plot_en.contains_key(k) {
|
||||||
|
self.plot_en.insert(k, false);
|
||||||
|
}
|
||||||
|
let enabled = self.plot_en.get_mut(k).unwrap();
|
||||||
|
ui.checkbox(enabled, k);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
pub fn render_plot_windows(&mut self, ctx: &Context) {
|
||||||
|
for (key, plot) in self.plots.iter() {
|
||||||
|
let enabled = self.plot_en.get_mut(key).unwrap();
|
||||||
|
if *enabled {
|
||||||
|
DebugPlots::render_window(ctx, key, plot, enabled);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
fn render_window(ctx: &Context, title: &'static str, plot: &PlotData, open: &mut bool) {
|
||||||
|
egui::Window::new(title).open(open).show(ctx, |ui| {
|
||||||
|
ui.heading(title);
|
||||||
|
match plot {
|
||||||
|
PlotData::U8(v) => {
|
||||||
|
ui.heading("u8 Plot");
|
||||||
|
let line = Line::new(
|
||||||
|
"Data",
|
||||||
|
PlotPoints::from_iter(
|
||||||
|
v.iter().enumerate().map(|(i, y)| [i as f64, *y as f64]),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
let plot = Plot::new(title);
|
||||||
|
plot.show(ui, |plot_ui| {
|
||||||
|
plot_ui.line(line);
|
||||||
|
plot_ui.set_plot_bounds(PlotBounds::from_min_max(
|
||||||
|
[-1.0, -1.0],
|
||||||
|
[(v.len() + 1) as f64, core::u8::MAX as f64 + 1.0],
|
||||||
|
));
|
||||||
|
});
|
||||||
|
}
|
||||||
|
PlotData::F32(v) => {
|
||||||
|
ui.heading("f32 plot");
|
||||||
|
let line = Line::new("Data", PlotPoints::from_ys_f32(&v));
|
||||||
|
let plot = Plot::new(title);
|
||||||
|
plot.show(ui, |plot_ui| {
|
||||||
|
plot_ui.line(line);
|
||||||
|
plot_ui.set_plot_bounds(PlotBounds::from_min_max(
|
||||||
|
[-1.0, -2.0],
|
||||||
|
[(v.len() + 1) as f64, 2.0],
|
||||||
|
));
|
||||||
|
});
|
||||||
|
}
|
||||||
|
PlotData::Bode32(v) => {
|
||||||
|
ui.heading("Bode Plot");
|
||||||
|
let mag_line = Line::new(
|
||||||
|
"Magnitude",
|
||||||
|
PlotPoints::from_iter(v.iter().enumerate().map(|(i, c)| {
|
||||||
|
[
|
||||||
|
i as f64,
|
||||||
|
((c.re * c.re) + (c.im * c.im)).sqrt() as f64 / v.len() as f64,
|
||||||
|
]
|
||||||
|
})),
|
||||||
|
);
|
||||||
|
let phase_line = Line::new(
|
||||||
|
"Phase",
|
||||||
|
PlotPoints::from_iter(
|
||||||
|
v.iter()
|
||||||
|
.enumerate()
|
||||||
|
.map(|(i, c)| [i as f64, c.arg() as f64 / core::f64::consts::PI]),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
let plot = Plot::new(title);
|
||||||
|
plot.show(ui, |plot_ui| {
|
||||||
|
plot_ui.line(mag_line);
|
||||||
|
plot_ui.line(phase_line);
|
||||||
|
plot_ui.set_plot_bounds(PlotBounds::from_min_max(
|
||||||
|
[0.0, -1.0],
|
||||||
|
[(v.len() + 1) as f64, 1.0],
|
||||||
|
));
|
||||||
|
});
|
||||||
|
}
|
||||||
|
};
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
92
src/app/fft.rs
Normal file
92
src/app/fft.rs
Normal file
@@ -0,0 +1,92 @@
|
|||||||
|
use anyhow::{anyhow, Result};
|
||||||
|
use realfft::RealFftPlanner;
|
||||||
|
use std::sync::mpsc::{self, Receiver, SyncSender, TrySendError};
|
||||||
|
|
||||||
|
use super::debug_plot::{DebugPlotSender, PlotData};
|
||||||
|
|
||||||
|
pub struct Fft {
|
||||||
|
pub tx: SyncSender<Vec<f32>>,
|
||||||
|
pub output_len: usize,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Fft {
|
||||||
|
pub fn new(size: usize, plot_tx: DebugPlotSender) -> Result<(Self, mpsc::Receiver<Vec<u8>>)> {
|
||||||
|
let output_len = size / 2 + 1;
|
||||||
|
|
||||||
|
// Create mpsc queue
|
||||||
|
let (tx, rx) = mpsc::sync_channel(10);
|
||||||
|
let (in_tx, in_rx): (SyncSender<Vec<f32>>, Receiver<Vec<f32>>) = mpsc::sync_channel(10);
|
||||||
|
|
||||||
|
// Setup fft use f32 for now
|
||||||
|
let mut fft_planner = RealFftPlanner::<f32>::new();
|
||||||
|
let fft = fft_planner.plan_fft_forward(size);
|
||||||
|
|
||||||
|
let mut fft_in: Vec<f32> = Vec::with_capacity(size);
|
||||||
|
let mut fft_out = fft.make_output_vec();
|
||||||
|
let mut fft_scratch = fft.make_scratch_vec();
|
||||||
|
|
||||||
|
std::thread::spawn(move || {
|
||||||
|
while let Ok(samples) = in_rx.recv() {
|
||||||
|
let mut data = samples.as_slice();
|
||||||
|
while data.fill_vec(&mut fft_in, size).is_ok() {
|
||||||
|
assert_eq!(size, fft_in.len());
|
||||||
|
fft.process_with_scratch(&mut fft_in, &mut fft_out, &mut fft_scratch)
|
||||||
|
.unwrap();
|
||||||
|
plot_tx
|
||||||
|
.send("FFT Output", PlotData::Bode32(fft_out.clone()))
|
||||||
|
.unwrap();
|
||||||
|
fft_in.clear();
|
||||||
|
let output: Vec<u8> = fft_out
|
||||||
|
.iter()
|
||||||
|
.map(|c| {
|
||||||
|
(((c.re * c.re) + (c.im * c.im)).sqrt() / output_len as f32 * 255.0)
|
||||||
|
as u8
|
||||||
|
})
|
||||||
|
.collect();
|
||||||
|
assert_eq!(output_len, output.len());
|
||||||
|
plot_tx
|
||||||
|
.send("FFT Processed Output", PlotData::U8(output.clone()))
|
||||||
|
.unwrap();
|
||||||
|
match tx.try_send(output) {
|
||||||
|
Ok(_) => {}
|
||||||
|
Err(TrySendError::Full(_)) => log::warn!("Waterfall buffer full."),
|
||||||
|
Err(TrySendError::Disconnected(_)) => {
|
||||||
|
panic!("The fft thread has disconnected from the waterfall!")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
Ok((
|
||||||
|
Self {
|
||||||
|
tx: in_tx,
|
||||||
|
output_len,
|
||||||
|
},
|
||||||
|
rx,
|
||||||
|
))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
trait FillVec {
|
||||||
|
/// Takes elements from self and inserts them into out_vec
|
||||||
|
/// Returns Ok if out_vec is filled to size
|
||||||
|
/// Returns Err when out_vec is not fully filled (self will be empty)
|
||||||
|
fn fill_vec(&mut self, out_vec: &mut Vec<f32>, size: usize) -> Result<()>;
|
||||||
|
}
|
||||||
|
impl FillVec for &[f32] {
|
||||||
|
fn fill_vec(&mut self, out_vec: &mut Vec<f32>, size: usize) -> Result<()> {
|
||||||
|
let have = self.len();
|
||||||
|
if have == 0 {
|
||||||
|
anyhow::bail!("Self empty");
|
||||||
|
}
|
||||||
|
let need = size - out_vec.len();
|
||||||
|
let can_move = need.min(have);
|
||||||
|
out_vec.extend_from_slice(&self[..can_move]);
|
||||||
|
*self = &self[can_move..];
|
||||||
|
match out_vec.len() == size {
|
||||||
|
true => Ok(()),
|
||||||
|
false => Err(anyhow!("out_vec not full")),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,8 +1,11 @@
|
|||||||
use eframe::glow::{self, PixelUnpackData, TEXTURE0, TEXTURE1, UNSIGNED_BYTE};
|
use eframe::glow::{
|
||||||
|
self, PixelUnpackData, CLAMP_TO_EDGE, TEXTURE0, TEXTURE1, TEXTURE_WRAP_S, UNSIGNED_BYTE,
|
||||||
|
};
|
||||||
use glow::HasContext as _;
|
use glow::HasContext as _;
|
||||||
use glow::{NEAREST, TEXTURE_1D, TEXTURE_2D, TEXTURE_MAG_FILTER, TEXTURE_MIN_FILTER};
|
use glow::{NEAREST, TEXTURE_2D, TEXTURE_MAG_FILTER, TEXTURE_MIN_FILTER};
|
||||||
use log;
|
use log;
|
||||||
use std::mem::{size_of, transmute};
|
use std::mem::{size_of, transmute};
|
||||||
|
use std::sync::mpsc::Receiver;
|
||||||
|
|
||||||
const SIZE_OF_F32: i32 = size_of::<f32>() as i32;
|
const SIZE_OF_F32: i32 = size_of::<f32>() as i32;
|
||||||
|
|
||||||
@@ -14,19 +17,25 @@ unsafe fn check_for_gl_errors(gl: &glow::Context, msg: &str) {
|
|||||||
log::error!("Waterfall {}: GL ERROR {} ({:#X})", msg, err, err);
|
log::error!("Waterfall {}: GL ERROR {} ({:#X})", msg, err, err);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
unsafe fn clear_texture(gl: &glow::Context, bytes_per_row: usize, n_rows: usize) {
|
||||||
mod deadbeef_rand {
|
let blank_line = vec![0_u8; bytes_per_row];
|
||||||
static mut RNG_SEED: u32 = 0x3d2faba7;
|
for offset in 0..n_rows {
|
||||||
static mut RNG_BEEF: u32 = 0xdeadbeef;
|
|
||||||
pub fn rand() -> u8 {
|
|
||||||
unsafe {
|
unsafe {
|
||||||
RNG_SEED = (RNG_SEED << 7) ^ ((RNG_SEED >> 25) + RNG_BEEF);
|
gl.tex_sub_image_2d(
|
||||||
RNG_BEEF = (RNG_BEEF << 7) ^ ((RNG_BEEF >> 25) + 0xdeadbeef);
|
glow::TEXTURE_2D,
|
||||||
(RNG_SEED & 0xff) as u8
|
0,
|
||||||
|
0,
|
||||||
|
offset as i32,
|
||||||
|
bytes_per_row as i32,
|
||||||
|
1,
|
||||||
|
glow::RED,
|
||||||
|
glow::UNSIGNED_BYTE,
|
||||||
|
PixelUnpackData::Slice(Some(&blank_line)),
|
||||||
|
);
|
||||||
|
check_for_gl_errors(&gl, &format!("clear texture with offset {}", offset));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
use deadbeef_rand::rand;
|
|
||||||
|
|
||||||
use crate::app::turbo_colormap;
|
use crate::app::turbo_colormap;
|
||||||
|
|
||||||
@@ -38,6 +47,9 @@ pub struct Waterfall {
|
|||||||
vbo: glow::Buffer,
|
vbo: glow::Buffer,
|
||||||
ebo: glow::Buffer,
|
ebo: glow::Buffer,
|
||||||
offset: usize,
|
offset: usize,
|
||||||
|
width: usize,
|
||||||
|
height: usize,
|
||||||
|
fft_in: Receiver<Vec<u8>>,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Waterfall {
|
impl Waterfall {
|
||||||
@@ -54,16 +66,11 @@ impl Waterfall {
|
|||||||
pub fn paint(&mut self, gl: &glow::Context, _angle: f32) {
|
pub fn paint(&mut self, gl: &glow::Context, _angle: f32) {
|
||||||
use glow::HasContext as _;
|
use glow::HasContext as _;
|
||||||
|
|
||||||
let mut new_data: [u8; 300] = [0; 300];
|
|
||||||
for data in new_data.iter_mut() {
|
|
||||||
*data = rand();
|
|
||||||
}
|
|
||||||
|
|
||||||
unsafe {
|
unsafe {
|
||||||
// Bind our texturs
|
// Bind our texturs
|
||||||
gl.active_texture(TEXTURE1);
|
gl.active_texture(TEXTURE1);
|
||||||
check_for_gl_errors(&gl, "Active texture 1");
|
check_for_gl_errors(&gl, "Active texture 1");
|
||||||
gl.bind_texture(glow::TEXTURE_1D, Some(self.color_lut));
|
gl.bind_texture(glow::TEXTURE_2D, Some(self.color_lut));
|
||||||
check_for_gl_errors(&gl, "bind lut");
|
check_for_gl_errors(&gl, "bind lut");
|
||||||
|
|
||||||
gl.active_texture(TEXTURE0);
|
gl.active_texture(TEXTURE0);
|
||||||
@@ -80,22 +87,27 @@ impl Waterfall {
|
|||||||
check_for_gl_errors(&gl, "bind vao");
|
check_for_gl_errors(&gl, "bind vao");
|
||||||
|
|
||||||
// Update texture
|
// Update texture
|
||||||
|
while let Ok(fft) = self.fft_in.try_recv() {
|
||||||
|
if fft.len() != self.width {
|
||||||
|
todo!();
|
||||||
|
}
|
||||||
gl.tex_sub_image_2d(
|
gl.tex_sub_image_2d(
|
||||||
glow::TEXTURE_2D,
|
glow::TEXTURE_2D,
|
||||||
0,
|
0,
|
||||||
0,
|
0,
|
||||||
self.offset as i32,
|
self.offset as i32,
|
||||||
300,
|
self.width as i32,
|
||||||
1,
|
1,
|
||||||
glow::RED,
|
glow::RED,
|
||||||
glow::UNSIGNED_BYTE,
|
glow::UNSIGNED_BYTE,
|
||||||
PixelUnpackData::Slice(&new_data),
|
PixelUnpackData::Slice(Some(&fft)),
|
||||||
);
|
);
|
||||||
check_for_gl_errors(&gl, "update texture");
|
check_for_gl_errors(&gl, &format!("update texture with offset {}", self.offset));
|
||||||
self.offset = (self.offset + 1) % 300;
|
self.offset = (self.offset + 1) % self.height;
|
||||||
|
}
|
||||||
|
|
||||||
if let Some(uniform) = gl.get_uniform_location(self.program, "offset") {
|
if let Some(uniform) = gl.get_uniform_location(self.program, "offset") {
|
||||||
gl.uniform_1_f32(Some(&uniform), self.offset as f32 / 300.0);
|
gl.uniform_1_f32(Some(&uniform), self.offset as f32 / self.height as f32);
|
||||||
}
|
}
|
||||||
check_for_gl_errors(&gl, "update uniform");
|
check_for_gl_errors(&gl, "update uniform");
|
||||||
|
|
||||||
@@ -106,7 +118,7 @@ impl Waterfall {
|
|||||||
check_for_gl_errors(&gl, "APP PAINT");
|
check_for_gl_errors(&gl, "APP PAINT");
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
pub fn new(gl: &glow::Context, width: usize, height: usize) -> Self {
|
pub fn new(gl: &glow::Context, width: usize, height: usize, fft_in: Receiver<Vec<u8>>) -> Self {
|
||||||
let vertices: [f32; 32] = [
|
let vertices: [f32; 32] = [
|
||||||
// positions // colors // texture coords
|
// positions // colors // texture coords
|
||||||
1.0, 1.0, 0.0, /**/ 1.0, 0.0, 0.0, /**/ 1.0, 1.0, // top right
|
1.0, 1.0, 0.0, /**/ 1.0, 0.0, 0.0, /**/ 1.0, 1.0, // top right
|
||||||
@@ -127,8 +139,10 @@ impl Waterfall {
|
|||||||
// Generate something to put into the texture Buffer
|
// Generate something to put into the texture Buffer
|
||||||
let mut buffer = vec![0; width * height];
|
let mut buffer = vec![0; width * height];
|
||||||
// Add some stripes to the texture
|
// Add some stripes to the texture
|
||||||
|
let stripes = 8;
|
||||||
|
let slen = width / stripes;
|
||||||
for (i, val) in buffer.iter_mut().enumerate() {
|
for (i, val) in buffer.iter_mut().enumerate() {
|
||||||
*val = if i % 50 < 25 { 255 } else { 0 };
|
*val = if i % slen < slen / 2 { 255 } else { 0 };
|
||||||
//*val = 255;
|
//*val = 255;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -191,7 +205,6 @@ impl Waterfall {
|
|||||||
gl.tex_parameter_i32(TEXTURE_2D, TEXTURE_MAG_FILTER, NEAREST as i32);
|
gl.tex_parameter_i32(TEXTURE_2D, TEXTURE_MAG_FILTER, NEAREST as i32);
|
||||||
check_for_gl_errors(&gl, "Set texture params");
|
check_for_gl_errors(&gl, "Set texture params");
|
||||||
|
|
||||||
//gl.tex_storage_2d(glow::TEXTURE_2D, 1, glow::R8, 300, 300);
|
|
||||||
gl.tex_image_2d(
|
gl.tex_image_2d(
|
||||||
glow::TEXTURE_2D,
|
glow::TEXTURE_2D,
|
||||||
0,
|
0,
|
||||||
@@ -201,26 +214,39 @@ impl Waterfall {
|
|||||||
0,
|
0,
|
||||||
glow::RED,
|
glow::RED,
|
||||||
glow::UNSIGNED_BYTE,
|
glow::UNSIGNED_BYTE,
|
||||||
Some(&buffer),
|
//Some(&buffer), // This segfaults with large buffers
|
||||||
|
PixelUnpackData::Slice(None),
|
||||||
);
|
);
|
||||||
check_for_gl_errors(&gl, "Initializing Texture");
|
check_for_gl_errors(&gl, "Initializing Texture");
|
||||||
|
|
||||||
|
// Clear the texture
|
||||||
|
clear_texture(gl, width, height);
|
||||||
|
|
||||||
let color_lut = gl
|
let color_lut = gl
|
||||||
.create_texture()
|
.create_texture()
|
||||||
.expect("Waterfall: could not create LUT");
|
.expect("Waterfall: could not create LUT");
|
||||||
gl.bind_texture(TEXTURE_1D, Some(color_lut));
|
gl.bind_texture(TEXTURE_2D, Some(color_lut));
|
||||||
gl.tex_parameter_i32(TEXTURE_1D, TEXTURE_MIN_FILTER, NEAREST as i32);
|
check_for_gl_errors(&gl, "Setup Bind LUT");
|
||||||
gl.tex_parameter_i32(TEXTURE_1D, TEXTURE_MAG_FILTER, NEAREST as i32);
|
gl.tex_parameter_i32(TEXTURE_2D, TEXTURE_MIN_FILTER, NEAREST as i32);
|
||||||
check_for_gl_errors(&gl, "Set LUT params");
|
check_for_gl_errors(&gl, "Set LUT MIN_FILTER");
|
||||||
gl.tex_image_1d(
|
gl.tex_parameter_i32(TEXTURE_2D, TEXTURE_MAG_FILTER, NEAREST as i32);
|
||||||
TEXTURE_1D,
|
check_for_gl_errors(&gl, "Set LUT MAG_FILTER");
|
||||||
|
gl.tex_parameter_i32(TEXTURE_2D, TEXTURE_WRAP_S, CLAMP_TO_EDGE as i32);
|
||||||
|
check_for_gl_errors(&gl, "Set LUT wrap mode");
|
||||||
|
gl.tex_image_2d(
|
||||||
|
TEXTURE_2D,
|
||||||
0,
|
0,
|
||||||
glow::SRGB as i32,
|
if cfg!(target_os = "android") || cfg!(target_arch = "wasm32") {
|
||||||
|
glow::RGB
|
||||||
|
} else {
|
||||||
|
glow::SRGB
|
||||||
|
} as i32,
|
||||||
256,
|
256,
|
||||||
|
1,
|
||||||
0,
|
0,
|
||||||
glow::RGB,
|
glow::RGB,
|
||||||
UNSIGNED_BYTE,
|
UNSIGNED_BYTE,
|
||||||
Some(&turbo_colormap::TURBO_SRGB_BYTES),
|
PixelUnpackData::Slice(Some(&turbo_colormap::TURBO_SRGB_BYTES)),
|
||||||
);
|
);
|
||||||
check_for_gl_errors(&gl, "Initializing LUT");
|
check_for_gl_errors(&gl, "Initializing LUT");
|
||||||
|
|
||||||
@@ -243,6 +269,8 @@ impl Waterfall {
|
|||||||
}
|
}
|
||||||
"#,
|
"#,
|
||||||
r#"
|
r#"
|
||||||
|
precision mediump float;
|
||||||
|
|
||||||
out vec4 FragColor;
|
out vec4 FragColor;
|
||||||
|
|
||||||
in vec3 ourColor;
|
in vec3 ourColor;
|
||||||
@@ -250,13 +278,13 @@ impl Waterfall {
|
|||||||
|
|
||||||
// texture sampler
|
// texture sampler
|
||||||
uniform sampler2D texture1;
|
uniform sampler2D texture1;
|
||||||
uniform sampler1D LUT;
|
uniform sampler2D LUT;
|
||||||
uniform float offset;
|
uniform float offset;
|
||||||
|
|
||||||
void main()
|
void main()
|
||||||
{
|
{
|
||||||
float val = texture(texture1, vec2(TexCoord.x, TexCoord.y + offset)).x;
|
float val = texture(texture1, vec2(TexCoord.x, TexCoord.y + offset)).x;
|
||||||
FragColor = texture(LUT, val);
|
FragColor = texture(LUT, vec2(val, 0));
|
||||||
}
|
}
|
||||||
"#,
|
"#,
|
||||||
);
|
);
|
||||||
@@ -311,6 +339,9 @@ impl Waterfall {
|
|||||||
vbo,
|
vbo,
|
||||||
ebo,
|
ebo,
|
||||||
offset: 0_usize,
|
offset: 0_usize,
|
||||||
|
width,
|
||||||
|
height,
|
||||||
|
fft_in,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
124
src/backend/audio.rs
Normal file
124
src/backend/audio.rs
Normal file
@@ -0,0 +1,124 @@
|
|||||||
|
use anyhow::Result;
|
||||||
|
use cpal::{
|
||||||
|
self,
|
||||||
|
traits::{DeviceTrait, HostTrait},
|
||||||
|
BufferSize,
|
||||||
|
};
|
||||||
|
use std::sync::mpsc::{SyncSender, TrySendError};
|
||||||
|
|
||||||
|
use crate::app::debug_plot::DebugPlotSender;
|
||||||
|
|
||||||
|
pub struct Audio {
|
||||||
|
pub _stream: cpal::Stream,
|
||||||
|
}
|
||||||
|
impl Audio {
|
||||||
|
pub fn new(
|
||||||
|
device_id: cpal::DeviceId,
|
||||||
|
config: cpal::StreamConfig,
|
||||||
|
fft_input: SyncSender<Vec<f32>>,
|
||||||
|
_plot_tx: DebugPlotSender,
|
||||||
|
) -> Result<Self> {
|
||||||
|
let host = cpal::default_host();
|
||||||
|
let device = host
|
||||||
|
.device_by_id(&device_id)
|
||||||
|
.ok_or(anyhow::anyhow!("Can't open device."))?;
|
||||||
|
let _stream = device.build_input_stream(
|
||||||
|
&config,
|
||||||
|
move |data: &[f32], _: &cpal::InputCallbackInfo| {
|
||||||
|
match fft_input.try_send(data.to_vec()) {
|
||||||
|
Err(TrySendError::Disconnected(_)) => panic!(
|
||||||
|
"Error: Audio backend has lost connection to frontend! Can not continue!"
|
||||||
|
),
|
||||||
|
Err(TrySendError::Full(_)) => log::warn!("Audio Backend buffer full."),
|
||||||
|
Ok(()) => {}
|
||||||
|
};
|
||||||
|
},
|
||||||
|
move |err| log::error!("Audio Thread Error: {err}"),
|
||||||
|
None,
|
||||||
|
)?;
|
||||||
|
|
||||||
|
Ok(Self { _stream })
|
||||||
|
}
|
||||||
|
}
|
||||||
|
impl crate::backend::Device for Audio {
|
||||||
|
fn show_settings(&mut self, ui: &mut egui::Ui) {
|
||||||
|
ui.label("TODO");
|
||||||
|
}
|
||||||
|
|
||||||
|
fn can_tune(&self) -> bool {
|
||||||
|
false
|
||||||
|
}
|
||||||
|
|
||||||
|
fn tune(&mut self, _freq: usize) -> anyhow::Result<()> {
|
||||||
|
anyhow::bail!("Can't tune this device")
|
||||||
|
}
|
||||||
|
|
||||||
|
fn close(self: Box<Self>) {
|
||||||
|
drop(self);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub struct AudioBackend {
|
||||||
|
host: cpal::Host,
|
||||||
|
devices: Vec<cpal::Device>,
|
||||||
|
current_device: usize,
|
||||||
|
}
|
||||||
|
impl AudioBackend {
|
||||||
|
pub fn new() -> Self {
|
||||||
|
let host = cpal::default_host();
|
||||||
|
let devices = host.devices().unwrap().collect();
|
||||||
|
let current_device = 0;
|
||||||
|
Self {
|
||||||
|
host,
|
||||||
|
devices,
|
||||||
|
current_device,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
fn update_devices(&mut self) {
|
||||||
|
self.devices.clear();
|
||||||
|
self.devices = self.host.devices().unwrap().collect();
|
||||||
|
self.current_device = 0;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
impl super::Backend for AudioBackend {
|
||||||
|
fn display_text(&self) -> &'static str {
|
||||||
|
"Audio"
|
||||||
|
}
|
||||||
|
|
||||||
|
fn show_device_selection(&mut self, ui: &mut egui::Ui) {
|
||||||
|
egui::ComboBox::from_label("Device")
|
||||||
|
.selected_text(
|
||||||
|
self.devices[self.current_device]
|
||||||
|
.id()
|
||||||
|
.map(|id| id.1)
|
||||||
|
.unwrap_or("UNKNOWN DEVICE".into()),
|
||||||
|
)
|
||||||
|
.show_index(ui, &mut self.current_device, self.devices.len(), |i| {
|
||||||
|
self.devices[i]
|
||||||
|
.id()
|
||||||
|
.map(|id| id.1)
|
||||||
|
.unwrap_or("UNKNOWN DEVICE".into())
|
||||||
|
});
|
||||||
|
if ui.add(egui::Button::new("Refresh")).clicked() {
|
||||||
|
self.update_devices();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn build_device(
|
||||||
|
&mut self,
|
||||||
|
fft_input: SyncSender<Vec<f32>>,
|
||||||
|
_plot_tx: DebugPlotSender,
|
||||||
|
) -> anyhow::Result<Box<dyn super::Device>> {
|
||||||
|
let config = cpal::StreamConfig {
|
||||||
|
channels: 1,
|
||||||
|
sample_rate: 44100,
|
||||||
|
buffer_size: BufferSize::Default,
|
||||||
|
};
|
||||||
|
Ok(Box::new(Audio::new(
|
||||||
|
self.devices[self.current_device].id()?,
|
||||||
|
config,
|
||||||
|
fft_input,
|
||||||
|
_plot_tx,
|
||||||
|
)?))
|
||||||
|
}
|
||||||
|
}
|
||||||
109
src/backend/dummy.rs
Normal file
109
src/backend/dummy.rs
Normal file
@@ -0,0 +1,109 @@
|
|||||||
|
use anyhow::Result;
|
||||||
|
use core::panic;
|
||||||
|
use std::{
|
||||||
|
sync::mpsc::{self, RecvTimeoutError, SyncSender, TrySendError},
|
||||||
|
time::{Duration, Instant},
|
||||||
|
usize,
|
||||||
|
};
|
||||||
|
|
||||||
|
use crate::app::debug_plot::{DebugPlotSender, PlotData};
|
||||||
|
|
||||||
|
const LUT_LEN: usize = 4096;
|
||||||
|
|
||||||
|
pub struct DummyDevice {
|
||||||
|
close: SyncSender<()>,
|
||||||
|
}
|
||||||
|
impl DummyDevice {
|
||||||
|
pub fn new(
|
||||||
|
sample_rate: usize,
|
||||||
|
fft_input: SyncSender<Vec<f32>>,
|
||||||
|
_plot_tx: DebugPlotSender,
|
||||||
|
) -> Result<Self> {
|
||||||
|
let sin_lut: Vec<f32> = (0..LUT_LEN)
|
||||||
|
.map(|i| ((i as f32 / LUT_LEN as f32) * std::f32::consts::TAU).sin())
|
||||||
|
.collect();
|
||||||
|
let (close, close_rx) = mpsc::sync_channel(0);
|
||||||
|
let buffer_size: usize = 2048;
|
||||||
|
let loop_interval = Duration::from_secs_f32((1. / sample_rate as f32) * buffer_size as f32);
|
||||||
|
let freq = (sample_rate / 4) as f32;
|
||||||
|
let phase_delta = sin_lut.len() as f32 * (freq / sample_rate as f32);
|
||||||
|
std::thread::spawn(move || {
|
||||||
|
let mut phase = 0_f32;
|
||||||
|
loop {
|
||||||
|
let start = Instant::now();
|
||||||
|
let samples: Vec<f32> = (0..buffer_size)
|
||||||
|
.map(|_i| {
|
||||||
|
phase = (phase + phase_delta) % sin_lut.len() as f32;
|
||||||
|
sin_lut[phase as usize]
|
||||||
|
})
|
||||||
|
.collect();
|
||||||
|
_plot_tx
|
||||||
|
.send("Dummy output", PlotData::F32(samples.clone()))
|
||||||
|
.unwrap();
|
||||||
|
match fft_input.try_send(samples) {
|
||||||
|
Ok(_) => {}
|
||||||
|
Err(TrySendError::Full(_)) => log::warn!("Dummy Backend buffer full."),
|
||||||
|
Err(TrySendError::Disconnected(_)) => {
|
||||||
|
panic!("Dummy device lost connection to frontend!")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
match close_rx.recv_timeout(loop_interval - start.elapsed()) {
|
||||||
|
Ok(_) => break,
|
||||||
|
Err(RecvTimeoutError::Disconnected) => {
|
||||||
|
panic!("Dummy device lost connection to frontend!")
|
||||||
|
}
|
||||||
|
Err(RecvTimeoutError::Timeout) => {}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
Ok(Self { close })
|
||||||
|
}
|
||||||
|
}
|
||||||
|
impl crate::backend::Device for DummyDevice {
|
||||||
|
fn show_settings(&mut self, ui: &mut egui::Ui) {
|
||||||
|
ui.label("TODO");
|
||||||
|
}
|
||||||
|
|
||||||
|
fn can_tune(&self) -> bool {
|
||||||
|
false
|
||||||
|
}
|
||||||
|
|
||||||
|
fn tune(&mut self, _freq: usize) -> anyhow::Result<()> {
|
||||||
|
anyhow::bail!("Can't tune this device")
|
||||||
|
}
|
||||||
|
|
||||||
|
fn close(self: Box<Self>) {
|
||||||
|
self.close.send(()).unwrap();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub struct DummyBackend {
|
||||||
|
sample_rate: usize,
|
||||||
|
}
|
||||||
|
impl DummyBackend {
|
||||||
|
pub fn new() -> Self {
|
||||||
|
Self { sample_rate: 48000 }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
impl super::Backend for DummyBackend {
|
||||||
|
fn display_text(&self) -> &'static str {
|
||||||
|
"Dummy"
|
||||||
|
}
|
||||||
|
|
||||||
|
fn show_device_selection(&mut self, ui: &mut egui::Ui) {
|
||||||
|
ui.label("TODO");
|
||||||
|
}
|
||||||
|
|
||||||
|
fn build_device(
|
||||||
|
&mut self,
|
||||||
|
fft_input: SyncSender<Vec<f32>>,
|
||||||
|
_plot_tx: DebugPlotSender,
|
||||||
|
) -> anyhow::Result<Box<dyn super::Device>> {
|
||||||
|
Ok(Box::new(DummyDevice::new(
|
||||||
|
self.sample_rate,
|
||||||
|
fft_input,
|
||||||
|
_plot_tx,
|
||||||
|
)?))
|
||||||
|
}
|
||||||
|
}
|
||||||
47
src/backend/mod.rs
Normal file
47
src/backend/mod.rs
Normal file
@@ -0,0 +1,47 @@
|
|||||||
|
use std::sync::mpsc::SyncSender;
|
||||||
|
|
||||||
|
use egui::Ui;
|
||||||
|
|
||||||
|
use crate::app::debug_plot::DebugPlotSender;
|
||||||
|
mod audio;
|
||||||
|
mod dummy;
|
||||||
|
pub trait Device {
|
||||||
|
fn show_settings(&mut self, ui: &mut Ui);
|
||||||
|
fn can_tune(&self) -> bool;
|
||||||
|
fn tune(&mut self, freq: usize) -> anyhow::Result<()>;
|
||||||
|
fn close(self: Box<Self>);
|
||||||
|
}
|
||||||
|
pub trait Backend {
|
||||||
|
fn display_text(&self) -> &'static str;
|
||||||
|
fn show_device_selection(&mut self, ui: &mut Ui);
|
||||||
|
fn build_device(
|
||||||
|
&mut self,
|
||||||
|
fft_input: SyncSender<Vec<f32>>,
|
||||||
|
_plot_tx: DebugPlotSender,
|
||||||
|
) -> anyhow::Result<Box<dyn Device>>;
|
||||||
|
}
|
||||||
|
pub struct Backends(pub Vec<Box<dyn Backend>>);
|
||||||
|
|
||||||
|
#[cfg(all(not(target_arch = "wasm32"), not(target_os = "android")))]
|
||||||
|
impl Default for Backends {
|
||||||
|
fn default() -> Self {
|
||||||
|
Backends(vec![
|
||||||
|
Box::new(audio::AudioBackend::new()),
|
||||||
|
Box::new(dummy::DummyBackend::new()),
|
||||||
|
])
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(target_arch = "wasm32")]
|
||||||
|
impl Default for Backends {
|
||||||
|
fn default() -> Self {
|
||||||
|
Backends(vec![Box::new(dummy::DummyBackend::new())])
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(target_os = "android")]
|
||||||
|
impl Default for Backends {
|
||||||
|
fn default() -> Self {
|
||||||
|
Backends(vec![Box::new(dummy::DummyBackend::new())])
|
||||||
|
}
|
||||||
|
}
|
||||||
19
src/lib.rs
19
src/lib.rs
@@ -1,12 +1,16 @@
|
|||||||
#![warn(clippy::all, rust_2018_idioms)]
|
#![warn(clippy::all, rust_2018_idioms)]
|
||||||
|
|
||||||
pub mod app;
|
pub mod app;
|
||||||
|
mod backend;
|
||||||
|
|
||||||
#[cfg(target_os = "android")]
|
#[cfg(target_os = "android")]
|
||||||
#[no_mangle]
|
#[no_mangle]
|
||||||
fn android_main(app: winit::platform::android::activity::AndroidApp) {
|
fn android_main(app: winit::platform::android::activity::AndroidApp) {
|
||||||
use winit::platform::android::activity::WindowManagerFlags;
|
use winit::platform::android::activity::WindowManagerFlags;
|
||||||
use winit::platform::android::EventLoopBuilderExtAndroid;
|
|
||||||
|
android_logger::init_once(
|
||||||
|
android_logger::Config::default().with_max_level(log::LevelFilter::Debug),
|
||||||
|
);
|
||||||
|
|
||||||
// Disable LAYOUT_IN_SCREEN to keep app from drawing under the status bar
|
// Disable LAYOUT_IN_SCREEN to keep app from drawing under the status bar
|
||||||
// winit does not currently do anything with MainEvent::InsetsChanged events
|
// winit does not currently do anything with MainEvent::InsetsChanged events
|
||||||
@@ -17,18 +21,15 @@ fn android_main(app: winit::platform::android::activity::AndroidApp) {
|
|||||||
// Alternatively we can hide the system bars by setting the app to fullscreen
|
// Alternatively we can hide the system bars by setting the app to fullscreen
|
||||||
//app.set_window_flags(WindowManagerFlags::FULLSCREEN, WindowManagerFlags::empty());
|
//app.set_window_flags(WindowManagerFlags::FULLSCREEN, WindowManagerFlags::empty());
|
||||||
|
|
||||||
android_logger::init_once(
|
let options = eframe::NativeOptions {
|
||||||
android_logger::Config::default().with_max_level(log::LevelFilter::Debug),
|
android_app: Some(app),
|
||||||
);
|
..Default::default()
|
||||||
let mut options = eframe::NativeOptions::default();
|
};
|
||||||
options.event_loop_builder = Some(Box::new(move |builder| {
|
|
||||||
builder.with_android_app(app);
|
|
||||||
}));
|
|
||||||
|
|
||||||
let res = eframe::run_native(
|
let res = eframe::run_native(
|
||||||
"eframe template",
|
"eframe template",
|
||||||
options,
|
options,
|
||||||
Box::new(|cc| Box::new(app::TemplateApp::new(cc))),
|
Box::new(|cc| Ok(Box::new(app::TemplateApp::new(cc)))),
|
||||||
);
|
);
|
||||||
if let Err(e) = res {
|
if let Err(e) = res {
|
||||||
log::error!("{e:?}");
|
log::error!("{e:?}");
|
||||||
|
|||||||
40
src/main.rs
40
src/main.rs
@@ -1,5 +1,6 @@
|
|||||||
mod app;
|
mod app;
|
||||||
use app::TemplateApp;
|
use app::TemplateApp;
|
||||||
|
mod backend;
|
||||||
|
|
||||||
//#[cfg(target_os = "android")]
|
//#[cfg(target_os = "android")]
|
||||||
//fn main() {}
|
//fn main() {}
|
||||||
@@ -22,26 +23,53 @@ fn main() -> eframe::Result<()> {
|
|||||||
eframe::run_native(
|
eframe::run_native(
|
||||||
"eframe template",
|
"eframe template",
|
||||||
native_options,
|
native_options,
|
||||||
Box::new(|cc| Box::new(TemplateApp::new(cc))),
|
Box::new(|cc| Ok(Box::new(TemplateApp::new(cc)))),
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
// When compiling to web using trunk:
|
// When compiling to web using trunk:
|
||||||
#[cfg(target_arch = "wasm32")]
|
#[cfg(target_arch = "wasm32")]
|
||||||
fn main() {
|
fn main() {
|
||||||
|
use eframe::wasm_bindgen::JsCast as _;
|
||||||
|
|
||||||
// Redirect `log` message to `console.log` and friends:
|
// Redirect `log` message to `console.log` and friends:
|
||||||
eframe::WebLogger::init(log::LevelFilter::Debug).ok();
|
eframe::WebLogger::init(log::LevelFilter::Debug).ok();
|
||||||
|
|
||||||
let web_options = eframe::WebOptions::default();
|
let web_options = eframe::WebOptions::default();
|
||||||
|
|
||||||
wasm_bindgen_futures::spawn_local(async {
|
wasm_bindgen_futures::spawn_local(async {
|
||||||
eframe::WebRunner::new()
|
let document = web_sys::window()
|
||||||
|
.expect("No window")
|
||||||
|
.document()
|
||||||
|
.expect("No document");
|
||||||
|
|
||||||
|
let canvas = document
|
||||||
|
.get_element_by_id("the_canvas_id")
|
||||||
|
.expect("Failed to find the_canvas_id")
|
||||||
|
.dyn_into::<web_sys::HtmlCanvasElement>()
|
||||||
|
.expect("the_canvas_id was not a HtmlCanvasElement");
|
||||||
|
|
||||||
|
let start_result = eframe::WebRunner::new()
|
||||||
.start(
|
.start(
|
||||||
"the_canvas_id", // hardcode it
|
canvas,
|
||||||
web_options,
|
web_options,
|
||||||
Box::new(|cc| Box::new(TemplateApp::new(cc))),
|
Box::new(|cc| Ok(Box::new(TemplateApp::new(cc)))),
|
||||||
)
|
)
|
||||||
.await
|
.await;
|
||||||
.expect("failed to start eframe");
|
|
||||||
|
// Remove the loading text and spinner:
|
||||||
|
if let Some(loading_text) = document.get_element_by_id("loading_text") {
|
||||||
|
match start_result {
|
||||||
|
Ok(_) => {
|
||||||
|
loading_text.remove();
|
||||||
|
}
|
||||||
|
Err(e) => {
|
||||||
|
loading_text.set_inner_html(
|
||||||
|
"<p> The app has crashed. See the developer console for details. </p>",
|
||||||
|
);
|
||||||
|
panic!("Failed to start eframe: {e:?}");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user