Stream audio to spectrograph shader too
This commit is contained in:
parent
04a7f35b84
commit
408343094a
|
|
@ -272,24 +272,25 @@ impl Engine {
|
||||||
// Forward chunk generation events from background threads
|
// Forward chunk generation events from background threads
|
||||||
while let Ok(event) = self.chunk_generation_rx.try_recv() {
|
while let Ok(event) = self.chunk_generation_rx.try_recv() {
|
||||||
match event {
|
match event {
|
||||||
AudioEvent::WaveformDecodeComplete { pool_index, samples } => {
|
AudioEvent::WaveformDecodeComplete { pool_index, samples, decoded_frames: df, total_frames: _tf } => {
|
||||||
// Update pool entry with decoded waveform samples
|
// Update pool entry and forward samples directly to UI
|
||||||
if let Some(file) = self.audio_pool.get_file_mut(pool_index) {
|
if let Some(file) = self.audio_pool.get_file_mut(pool_index) {
|
||||||
let total = file.frames;
|
let sr = file.sample_rate;
|
||||||
|
let ch = file.channels;
|
||||||
if let crate::audio::pool::AudioStorage::Compressed {
|
if let crate::audio::pool::AudioStorage::Compressed {
|
||||||
ref mut decoded_for_waveform,
|
ref mut decoded_for_waveform,
|
||||||
ref mut decoded_frames,
|
ref mut decoded_frames,
|
||||||
..
|
..
|
||||||
} = file.storage {
|
} = file.storage {
|
||||||
eprintln!("[ENGINE] Waveform decode complete for pool {}: {} samples", pool_index, samples.len());
|
*decoded_for_waveform = samples.clone();
|
||||||
*decoded_for_waveform = samples;
|
*decoded_frames = df;
|
||||||
*decoded_frames = total;
|
|
||||||
}
|
}
|
||||||
// Notify frontend that waveform data is ready
|
// Send samples inline — UI won't need to query back
|
||||||
let _ = self.event_tx.push(AudioEvent::AudioDecodeProgress {
|
let _ = self.event_tx.push(AudioEvent::AudioDecodeProgress {
|
||||||
pool_index,
|
pool_index,
|
||||||
decoded_frames: total,
|
samples,
|
||||||
total_frames: total,
|
sample_rate: sr,
|
||||||
|
channels: ch,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -1789,26 +1790,25 @@ impl Engine {
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
// Spawn background thread to decode full file for waveform display
|
// Spawn background thread to decode file progressively for waveform display
|
||||||
let bg_tx = self.chunk_generation_tx.clone();
|
let bg_tx = self.chunk_generation_tx.clone();
|
||||||
let bg_path = path.to_path_buf();
|
let bg_path = path.to_path_buf();
|
||||||
|
let bg_total_frames = total_frames;
|
||||||
let _ = std::thread::Builder::new()
|
let _ = std::thread::Builder::new()
|
||||||
.name(format!("waveform-decode-{}", idx))
|
.name(format!("waveform-decode-{}", idx))
|
||||||
.spawn(move || {
|
.spawn(move || {
|
||||||
eprintln!("[WAVEFORM DECODE] Starting full decode of {:?}", bg_path);
|
crate::io::AudioFile::decode_progressive(
|
||||||
match crate::io::AudioFile::load(&bg_path) {
|
&bg_path,
|
||||||
Ok(loaded) => {
|
bg_total_frames,
|
||||||
eprintln!("[WAVEFORM DECODE] Complete: {} frames, {} channels",
|
|audio_data, decoded_frames, total| {
|
||||||
loaded.frames, loaded.channels);
|
|
||||||
let _ = bg_tx.send(AudioEvent::WaveformDecodeComplete {
|
let _ = bg_tx.send(AudioEvent::WaveformDecodeComplete {
|
||||||
pool_index: idx,
|
pool_index: idx,
|
||||||
samples: loaded.data,
|
samples: audio_data.to_vec(),
|
||||||
|
decoded_frames,
|
||||||
|
total_frames: total,
|
||||||
});
|
});
|
||||||
}
|
},
|
||||||
Err(e) => {
|
);
|
||||||
eprintln!("[WAVEFORM DECODE] Failed to decode {:?}: {}", bg_path, e);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
});
|
});
|
||||||
idx
|
idx
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -274,18 +274,22 @@ pub enum AudioEvent {
|
||||||
},
|
},
|
||||||
|
|
||||||
/// Progressive decode progress for a compressed audio file's waveform data.
|
/// Progressive decode progress for a compressed audio file's waveform data.
|
||||||
/// The UI can use this to update waveform display incrementally.
|
/// Carries the samples inline so the UI doesn't need to query back.
|
||||||
AudioDecodeProgress {
|
AudioDecodeProgress {
|
||||||
pool_index: usize,
|
pool_index: usize,
|
||||||
decoded_frames: u64,
|
samples: Vec<f32>,
|
||||||
total_frames: u64,
|
sample_rate: u32,
|
||||||
|
channels: u32,
|
||||||
},
|
},
|
||||||
|
|
||||||
/// Background waveform decode completed for a compressed audio file.
|
/// Background waveform decode progress/completion for a compressed audio file.
|
||||||
/// Internal event — consumed by the engine to update the pool, not forwarded to UI.
|
/// Internal event — consumed by the engine to update the pool, not forwarded to UI.
|
||||||
|
/// `decoded_frames` < `total_frames` means partial; equal means complete.
|
||||||
WaveformDecodeComplete {
|
WaveformDecodeComplete {
|
||||||
pool_index: usize,
|
pool_index: usize,
|
||||||
samples: Vec<f32>,
|
samples: Vec<f32>,
|
||||||
|
decoded_frames: u64,
|
||||||
|
total_frames: u64,
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -338,6 +338,122 @@ impl AudioFile {
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Decode a compressed audio file progressively, calling `on_progress` with
|
||||||
|
/// partial data snapshots so the UI can display waveforms as they decode.
|
||||||
|
/// Sends updates roughly every 2 seconds of decoded audio.
|
||||||
|
pub fn decode_progressive<P: AsRef<Path>, F>(path: P, total_frames: u64, on_progress: F)
|
||||||
|
where
|
||||||
|
F: Fn(&[f32], u64, u64),
|
||||||
|
{
|
||||||
|
let path = path.as_ref();
|
||||||
|
|
||||||
|
let file = match std::fs::File::open(path) {
|
||||||
|
Ok(f) => f,
|
||||||
|
Err(e) => {
|
||||||
|
eprintln!("[WAVEFORM DECODE] Failed to open {:?}: {}", path, e);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
let mss = MediaSourceStream::new(Box::new(file), Default::default());
|
||||||
|
|
||||||
|
let mut hint = Hint::new();
|
||||||
|
if let Some(extension) = path.extension() {
|
||||||
|
if let Some(ext_str) = extension.to_str() {
|
||||||
|
hint.with_extension(ext_str);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
let probed = match symphonia::default::get_probe()
|
||||||
|
.format(&hint, mss, &FormatOptions::default(), &MetadataOptions::default())
|
||||||
|
{
|
||||||
|
Ok(p) => p,
|
||||||
|
Err(e) => {
|
||||||
|
eprintln!("[WAVEFORM DECODE] Failed to probe {:?}: {}", path, e);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
let mut format = probed.format;
|
||||||
|
|
||||||
|
let track = match format.tracks().iter()
|
||||||
|
.find(|t| t.codec_params.codec != symphonia::core::codecs::CODEC_TYPE_NULL)
|
||||||
|
{
|
||||||
|
Some(t) => t,
|
||||||
|
None => {
|
||||||
|
eprintln!("[WAVEFORM DECODE] No audio tracks in {:?}", path);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
let track_id = track.id;
|
||||||
|
let channels = track.codec_params.channels
|
||||||
|
.map(|c| c.count() as u32)
|
||||||
|
.unwrap_or(2);
|
||||||
|
let sample_rate = track.codec_params.sample_rate.unwrap_or(44100);
|
||||||
|
|
||||||
|
let mut decoder = match symphonia::default::get_codecs()
|
||||||
|
.make(&track.codec_params, &DecoderOptions::default())
|
||||||
|
{
|
||||||
|
Ok(d) => d,
|
||||||
|
Err(e) => {
|
||||||
|
eprintln!("[WAVEFORM DECODE] Failed to create decoder for {:?}: {}", path, e);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
let mut audio_data = Vec::new();
|
||||||
|
let mut sample_buf = None;
|
||||||
|
// Send a progress update roughly every 2 seconds of audio
|
||||||
|
// Send first update quickly (0.25s), then every 2s of audio
|
||||||
|
let initial_interval = (sample_rate as usize * channels as usize) / 4;
|
||||||
|
let steady_interval = (sample_rate as usize * channels as usize) * 2;
|
||||||
|
let mut sent_first = false;
|
||||||
|
let mut last_update_len = 0usize;
|
||||||
|
|
||||||
|
loop {
|
||||||
|
let packet = match format.next_packet() {
|
||||||
|
Ok(packet) => packet,
|
||||||
|
Err(Error::IoError(e)) if e.kind() == std::io::ErrorKind::UnexpectedEof => break,
|
||||||
|
Err(Error::ResetRequired) => break,
|
||||||
|
Err(_) => break,
|
||||||
|
};
|
||||||
|
|
||||||
|
if packet.track_id() != track_id {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
match decoder.decode(&packet) {
|
||||||
|
Ok(decoded) => {
|
||||||
|
if sample_buf.is_none() {
|
||||||
|
let spec = *decoded.spec();
|
||||||
|
let duration = decoded.capacity() as u64;
|
||||||
|
sample_buf = Some(SampleBuffer::<f32>::new(duration, spec));
|
||||||
|
}
|
||||||
|
if let Some(ref mut buf) = sample_buf {
|
||||||
|
buf.copy_interleaved_ref(decoded);
|
||||||
|
audio_data.extend_from_slice(buf.samples());
|
||||||
|
}
|
||||||
|
|
||||||
|
// Send progressive update (fast initial, then periodic)
|
||||||
|
let interval = if sent_first { steady_interval } else { initial_interval };
|
||||||
|
if audio_data.len() - last_update_len >= interval {
|
||||||
|
let decoded_frames = audio_data.len() as u64 / channels as u64;
|
||||||
|
on_progress(&audio_data, decoded_frames, total_frames);
|
||||||
|
last_update_len = audio_data.len();
|
||||||
|
sent_first = true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Err(Error::DecodeError(_)) => continue,
|
||||||
|
Err(_) => break,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Final update with all data
|
||||||
|
let decoded_frames = audio_data.len() as u64 / channels as u64;
|
||||||
|
on_progress(&audio_data, decoded_frames, decoded_frames.max(total_frames));
|
||||||
|
}
|
||||||
|
|
||||||
/// Calculate the duration of the audio file in seconds
|
/// Calculate the duration of the audio file in seconds
|
||||||
pub fn duration(&self) -> f64 {
|
pub fn duration(&self) -> f64 {
|
||||||
self.frames as f64 / self.sample_rate as f64
|
self.frames as f64 / self.sample_rate as f64
|
||||||
|
|
|
||||||
|
|
@ -59,8 +59,6 @@ opt-level = 2
|
||||||
opt-level = 2
|
opt-level = 2
|
||||||
[profile.dev.package.symphonia-bundle-flac]
|
[profile.dev.package.symphonia-bundle-flac]
|
||||||
opt-level = 2
|
opt-level = 2
|
||||||
[profile.dev.package.symphonia-format-wav]
|
|
||||||
opt-level = 2
|
|
||||||
[profile.dev.package.symphonia-format-ogg]
|
[profile.dev.package.symphonia-format-ogg]
|
||||||
opt-level = 2
|
opt-level = 2
|
||||||
[profile.dev.package.symphonia-codec-vorbis]
|
[profile.dev.package.symphonia-codec-vorbis]
|
||||||
|
|
@ -71,5 +69,3 @@ opt-level = 2
|
||||||
opt-level = 2
|
opt-level = 2
|
||||||
[profile.dev.package.cpal]
|
[profile.dev.package.cpal]
|
||||||
opt-level = 2
|
opt-level = 2
|
||||||
[profile.dev.package.rubato]
|
|
||||||
opt-level = 2
|
|
||||||
|
|
|
||||||
|
|
@ -21,7 +21,7 @@ const BINS_PER_OCTAVE: u32 = 24;
|
||||||
const FREQ_BINS: u32 = 174; // ceil(log2(4186.0 / 27.5) * 24) = ceil(173.95)
|
const FREQ_BINS: u32 = 174; // ceil(log2(4186.0 / 27.5) * 24) = ceil(173.95)
|
||||||
const HOP_SIZE: u32 = 512;
|
const HOP_SIZE: u32 = 512;
|
||||||
const CACHE_CAPACITY: u32 = 4096;
|
const CACHE_CAPACITY: u32 = 4096;
|
||||||
const MAX_COLS_PER_FRAME: u32 = 256;
|
const MAX_COLS_PER_FRAME: u32 = 128;
|
||||||
const F_MIN: f64 = 27.5; // A0 = MIDI 21
|
const F_MIN: f64 = 27.5; // A0 = MIDI 21
|
||||||
const WAVEFORM_TEX_WIDTH: u32 = 2048;
|
const WAVEFORM_TEX_WIDTH: u32 = 2048;
|
||||||
|
|
||||||
|
|
@ -49,7 +49,7 @@ struct CqtComputeParams {
|
||||||
tex_width: u32,
|
tex_width: u32,
|
||||||
total_frames: u32,
|
total_frames: u32,
|
||||||
sample_rate: f32,
|
sample_rate: f32,
|
||||||
_pad0: u32,
|
column_stride: u32,
|
||||||
_pad1: u32,
|
_pad1: u32,
|
||||||
_pad2: u32,
|
_pad2: u32,
|
||||||
}
|
}
|
||||||
|
|
@ -78,7 +78,8 @@ pub struct CqtRenderParams {
|
||||||
pub cache_start_column: f32, // 4 @ 76
|
pub cache_start_column: f32, // 4 @ 76
|
||||||
pub cache_valid_start: f32, // 4 @ 80
|
pub cache_valid_start: f32, // 4 @ 80
|
||||||
pub cache_valid_end: f32, // 4 @ 84
|
pub cache_valid_end: f32, // 4 @ 84
|
||||||
pub _pad: [f32; 2], // 8 @ 88, total 96
|
pub column_stride: f32, // 4 @ 88
|
||||||
|
pub _pad: f32, // 4 @ 92, total 96
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Per-pool-index cache entry with ring buffer and GPU resources.
|
/// Per-pool-index cache entry with ring buffer and GPU resources.
|
||||||
|
|
@ -111,6 +112,7 @@ struct CqtCacheEntry {
|
||||||
|
|
||||||
// Metadata
|
// Metadata
|
||||||
sample_rate: u32,
|
sample_rate: u32,
|
||||||
|
current_stride: u32,
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Global GPU resources for CQT (stored in egui_wgpu::CallbackResources).
|
/// Global GPU resources for CQT (stored in egui_wgpu::CallbackResources).
|
||||||
|
|
@ -132,6 +134,8 @@ pub struct CqtCallback {
|
||||||
/// Visible column range (global CQT column indices)
|
/// Visible column range (global CQT column indices)
|
||||||
pub visible_col_start: i64,
|
pub visible_col_start: i64,
|
||||||
pub visible_col_end: i64,
|
pub visible_col_end: i64,
|
||||||
|
/// Column stride: 1 = full resolution, N = compute every Nth column
|
||||||
|
pub stride: u32,
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Precompute CQT bin parameters for a given sample rate.
|
/// Precompute CQT bin parameters for a given sample rate.
|
||||||
|
|
@ -341,7 +345,14 @@ impl CqtGpuResources {
|
||||||
total_frames: u64,
|
total_frames: u64,
|
||||||
sample_rate: u32,
|
sample_rate: u32,
|
||||||
) {
|
) {
|
||||||
if self.entries.contains_key(&pool_index) {
|
// If entry exists, check if waveform data has grown (progressive decode)
|
||||||
|
if let Some(entry) = self.entries.get_mut(&pool_index) {
|
||||||
|
if entry.waveform_total_frames != total_frames {
|
||||||
|
// Waveform texture updated in-place with more data.
|
||||||
|
// The texture view is still valid (no destroy/recreate),
|
||||||
|
// so just update total_frames to allow computing new columns.
|
||||||
|
entry.waveform_total_frames = total_frames;
|
||||||
|
}
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -458,6 +469,7 @@ impl CqtGpuResources {
|
||||||
render_bind_group,
|
render_bind_group,
|
||||||
render_uniform_buffer,
|
render_uniform_buffer,
|
||||||
sample_rate,
|
sample_rate,
|
||||||
|
current_stride: 1,
|
||||||
},
|
},
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
@ -473,18 +485,20 @@ fn dispatch_cqt_compute(
|
||||||
entry: &CqtCacheEntry,
|
entry: &CqtCacheEntry,
|
||||||
start_col: i64,
|
start_col: i64,
|
||||||
end_col: i64,
|
end_col: i64,
|
||||||
|
stride: u32,
|
||||||
) -> Vec<wgpu::CommandBuffer> {
|
) -> Vec<wgpu::CommandBuffer> {
|
||||||
let num_cols = (end_col - start_col) as u32;
|
// Number of cache slots needed (each slot covers `stride` global columns)
|
||||||
if num_cols == 0 {
|
let num_cols = ((end_col - start_col) as u32 / stride).max(1);
|
||||||
|
if end_col <= start_col {
|
||||||
return Vec::new();
|
return Vec::new();
|
||||||
}
|
}
|
||||||
|
|
||||||
// Clamp to max per frame
|
// Clamp to max per frame
|
||||||
let num_cols = num_cols.min(MAX_COLS_PER_FRAME);
|
let num_cols = num_cols.min(MAX_COLS_PER_FRAME);
|
||||||
|
|
||||||
// Calculate ring buffer write offset
|
// Calculate ring buffer write offset (in cache slots, not global columns)
|
||||||
let cache_write_offset =
|
let cache_write_offset =
|
||||||
((start_col - entry.cache_start_column) as u32) % entry.cache_capacity;
|
(((start_col - entry.cache_start_column) / stride as i64) as u32) % entry.cache_capacity;
|
||||||
|
|
||||||
let params = CqtComputeParams {
|
let params = CqtComputeParams {
|
||||||
hop_size: HOP_SIZE,
|
hop_size: HOP_SIZE,
|
||||||
|
|
@ -496,7 +510,7 @@ fn dispatch_cqt_compute(
|
||||||
tex_width: WAVEFORM_TEX_WIDTH,
|
tex_width: WAVEFORM_TEX_WIDTH,
|
||||||
total_frames: entry.waveform_total_frames as u32,
|
total_frames: entry.waveform_total_frames as u32,
|
||||||
sample_rate: entry.sample_rate as f32,
|
sample_rate: entry.sample_rate as f32,
|
||||||
_pad0: 0,
|
column_stride: stride,
|
||||||
_pad1: 0,
|
_pad1: 0,
|
||||||
_pad2: 0,
|
_pad2: 0,
|
||||||
};
|
};
|
||||||
|
|
@ -569,9 +583,26 @@ impl egui_wgpu::CallbackTrait for CqtCallback {
|
||||||
);
|
);
|
||||||
|
|
||||||
// Determine which columns need computing
|
// Determine which columns need computing
|
||||||
|
let stride = self.stride.max(1) as i64;
|
||||||
let vis_start = self.visible_col_start.max(0);
|
let vis_start = self.visible_col_start.max(0);
|
||||||
let max_col = (total_frames as i64) / HOP_SIZE as i64;
|
let max_col = (total_frames as i64) / HOP_SIZE as i64;
|
||||||
let vis_end = self.visible_col_end.min(max_col);
|
let vis_end_raw = self.visible_col_end.min(max_col);
|
||||||
|
// Clamp visible range to cache capacity (in global columns, accounting for stride)
|
||||||
|
let vis_end = vis_end_raw.min(vis_start + CACHE_CAPACITY as i64 * stride);
|
||||||
|
|
||||||
|
// If stride changed, invalidate cache
|
||||||
|
{
|
||||||
|
let entry = cqt_gpu.entries.get_mut(&self.pool_index).unwrap();
|
||||||
|
if entry.current_stride != self.stride {
|
||||||
|
entry.current_stride = self.stride;
|
||||||
|
entry.cache_start_column = vis_start;
|
||||||
|
entry.cache_valid_start = vis_start;
|
||||||
|
entry.cache_valid_end = vis_start;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Stride-aware max columns per frame (in global column units)
|
||||||
|
let max_cols_global = MAX_COLS_PER_FRAME as i64 * stride;
|
||||||
|
|
||||||
// Read current cache state, compute what's needed, then update state.
|
// Read current cache state, compute what's needed, then update state.
|
||||||
// We split borrows carefully: read entry state, compute, then write back.
|
// We split borrows carefully: read entry state, compute, then write back.
|
||||||
|
|
@ -590,17 +621,18 @@ impl egui_wgpu::CallbackTrait for CqtCallback {
|
||||||
&& vis_start < cache_valid_end
|
&& vis_start < cache_valid_end
|
||||||
&& vis_end > cache_valid_end
|
&& vis_end > cache_valid_end
|
||||||
{
|
{
|
||||||
// Scrolling right
|
// Scrolling right — align to stride boundary
|
||||||
let actual_end =
|
let actual_end =
|
||||||
cache_valid_end + (vis_end - cache_valid_end).min(MAX_COLS_PER_FRAME as i64);
|
cache_valid_end + (vis_end - cache_valid_end).min(max_cols_global);
|
||||||
cmds = dispatch_cqt_compute(
|
cmds = dispatch_cqt_compute(
|
||||||
device, queue, &cqt_gpu.compute_pipeline, entry,
|
device, queue, &cqt_gpu.compute_pipeline, entry,
|
||||||
cache_valid_end, actual_end,
|
cache_valid_end, actual_end, self.stride,
|
||||||
);
|
);
|
||||||
let entry = cqt_gpu.entries.get_mut(&self.pool_index).unwrap();
|
let entry = cqt_gpu.entries.get_mut(&self.pool_index).unwrap();
|
||||||
entry.cache_valid_end = actual_end;
|
entry.cache_valid_end = actual_end;
|
||||||
if entry.cache_valid_end - entry.cache_valid_start > entry.cache_capacity as i64 {
|
let cache_cap_global = entry.cache_capacity as i64 * stride;
|
||||||
entry.cache_valid_start = entry.cache_valid_end - entry.cache_capacity as i64;
|
if entry.cache_valid_end - entry.cache_valid_start > cache_cap_global {
|
||||||
|
entry.cache_valid_start = entry.cache_valid_end - cache_cap_global;
|
||||||
entry.cache_start_column = entry.cache_valid_start;
|
entry.cache_start_column = entry.cache_valid_start;
|
||||||
}
|
}
|
||||||
} else if vis_end <= cache_valid_end
|
} else if vis_end <= cache_valid_end
|
||||||
|
|
@ -609,16 +641,17 @@ impl egui_wgpu::CallbackTrait for CqtCallback {
|
||||||
{
|
{
|
||||||
// Scrolling left
|
// Scrolling left
|
||||||
let actual_start =
|
let actual_start =
|
||||||
cache_valid_start - (cache_valid_start - vis_start).min(MAX_COLS_PER_FRAME as i64);
|
cache_valid_start - (cache_valid_start - vis_start).min(max_cols_global);
|
||||||
cmds = dispatch_cqt_compute(
|
cmds = dispatch_cqt_compute(
|
||||||
device, queue, &cqt_gpu.compute_pipeline, entry,
|
device, queue, &cqt_gpu.compute_pipeline, entry,
|
||||||
actual_start, cache_valid_start,
|
actual_start, cache_valid_start, self.stride,
|
||||||
);
|
);
|
||||||
let entry = cqt_gpu.entries.get_mut(&self.pool_index).unwrap();
|
let entry = cqt_gpu.entries.get_mut(&self.pool_index).unwrap();
|
||||||
entry.cache_valid_start = actual_start;
|
entry.cache_valid_start = actual_start;
|
||||||
entry.cache_start_column = actual_start;
|
entry.cache_start_column = actual_start;
|
||||||
if entry.cache_valid_end - entry.cache_valid_start > entry.cache_capacity as i64 {
|
let cache_cap_global = entry.cache_capacity as i64 * stride;
|
||||||
entry.cache_valid_end = entry.cache_valid_start + entry.cache_capacity as i64;
|
if entry.cache_valid_end - entry.cache_valid_start > cache_cap_global {
|
||||||
|
entry.cache_valid_end = entry.cache_valid_start + cache_cap_global;
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
// No overlap or first compute — reset cache
|
// No overlap or first compute — reset cache
|
||||||
|
|
@ -627,11 +660,11 @@ impl egui_wgpu::CallbackTrait for CqtCallback {
|
||||||
entry.cache_valid_start = vis_start;
|
entry.cache_valid_start = vis_start;
|
||||||
entry.cache_valid_end = vis_start;
|
entry.cache_valid_end = vis_start;
|
||||||
|
|
||||||
let compute_end = vis_start + (vis_end - vis_start).min(MAX_COLS_PER_FRAME as i64);
|
let compute_end = vis_start + (vis_end - vis_start).min(max_cols_global);
|
||||||
let entry = cqt_gpu.entries.get(&self.pool_index).unwrap();
|
let entry = cqt_gpu.entries.get(&self.pool_index).unwrap();
|
||||||
cmds = dispatch_cqt_compute(
|
cmds = dispatch_cqt_compute(
|
||||||
device, queue, &cqt_gpu.compute_pipeline, entry,
|
device, queue, &cqt_gpu.compute_pipeline, entry,
|
||||||
vis_start, compute_end,
|
vis_start, compute_end, self.stride,
|
||||||
);
|
);
|
||||||
let entry = cqt_gpu.entries.get_mut(&self.pool_index).unwrap();
|
let entry = cqt_gpu.entries.get_mut(&self.pool_index).unwrap();
|
||||||
entry.cache_valid_end = compute_end;
|
entry.cache_valid_end = compute_end;
|
||||||
|
|
@ -645,6 +678,7 @@ impl egui_wgpu::CallbackTrait for CqtCallback {
|
||||||
params.cache_valid_start = entry.cache_valid_start as f32;
|
params.cache_valid_start = entry.cache_valid_start as f32;
|
||||||
params.cache_valid_end = entry.cache_valid_end as f32;
|
params.cache_valid_end = entry.cache_valid_end as f32;
|
||||||
params.cache_capacity = entry.cache_capacity as f32;
|
params.cache_capacity = entry.cache_capacity as f32;
|
||||||
|
params.column_stride = self.stride as f32;
|
||||||
|
|
||||||
queue.write_buffer(
|
queue.write_buffer(
|
||||||
&entry.render_uniform_buffer,
|
&entry.render_uniform_buffer,
|
||||||
|
|
|
||||||
|
|
@ -2353,8 +2353,6 @@ impl EditorApp {
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Import an audio file via daw-backend (async — non-blocking)
|
/// Import an audio file via daw-backend (async — non-blocking)
|
||||||
///
|
|
||||||
/// Reads only metadata from the file (sub-millisecond), then sends the path
|
|
||||||
/// to the engine for async import. The engine memory-maps WAV files or sets
|
/// to the engine for async import. The engine memory-maps WAV files or sets
|
||||||
/// up stream decoding for compressed formats. An `AudioFileReady` event is
|
/// up stream decoding for compressed formats. An `AudioFileReady` event is
|
||||||
/// emitted when the file is playback-ready; the event handler populates the
|
/// emitted when the file is playback-ready; the event handler populates the
|
||||||
|
|
@ -2749,10 +2747,37 @@ impl EditorApp {
|
||||||
// Get the newly created layer ID (it's the last child in the document)
|
// Get the newly created layer ID (it's the last child in the document)
|
||||||
let doc = self.action_executor.document();
|
let doc = self.action_executor.document();
|
||||||
if let Some(last_layer) = doc.root.children.last() {
|
if let Some(last_layer) = doc.root.children.last() {
|
||||||
target_layer_id = Some(last_layer.id());
|
let layer_id = last_layer.id();
|
||||||
|
target_layer_id = Some(layer_id);
|
||||||
|
|
||||||
// Update active layer to the new layer
|
// Update active layer to the new layer
|
||||||
self.active_layer_id = target_layer_id;
|
self.active_layer_id = target_layer_id;
|
||||||
|
|
||||||
|
// Create a backend audio/MIDI track and add the mapping
|
||||||
|
if let Some(ref controller_arc) = self.audio_controller {
|
||||||
|
let mut controller = controller_arc.lock().unwrap();
|
||||||
|
match asset_info.clip_type {
|
||||||
|
panes::DragClipType::AudioSampled => {
|
||||||
|
match controller.create_audio_track_sync(layer_name.clone()) {
|
||||||
|
Ok(track_id) => {
|
||||||
|
self.layer_to_track_map.insert(layer_id, track_id);
|
||||||
|
self.track_to_layer_map.insert(track_id, layer_id);
|
||||||
|
}
|
||||||
|
Err(e) => eprintln!("Failed to create audio track for auto-place: {}", e),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
panes::DragClipType::AudioMidi => {
|
||||||
|
match controller.create_midi_track_sync(layer_name.clone()) {
|
||||||
|
Ok(track_id) => {
|
||||||
|
self.layer_to_track_map.insert(layer_id, track_id);
|
||||||
|
self.track_to_layer_map.insert(track_id, layer_id);
|
||||||
|
}
|
||||||
|
Err(e) => eprintln!("Failed to create MIDI track for auto-place: {}", e),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
_ => {} // Other types don't need backend tracks
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -3613,23 +3638,12 @@ impl eframe::App for EditorApp {
|
||||||
// via AudioDecodeProgress events.
|
// via AudioDecodeProgress events.
|
||||||
ctx.request_repaint();
|
ctx.request_repaint();
|
||||||
}
|
}
|
||||||
AudioEvent::AudioDecodeProgress { pool_index, decoded_frames, total_frames } => {
|
AudioEvent::AudioDecodeProgress { pool_index, samples, sample_rate, channels } => {
|
||||||
// Waveform decode complete — fetch samples for GPU waveform
|
// Samples arrive inline — no query needed
|
||||||
if decoded_frames == total_frames {
|
self.raw_audio_cache.insert(pool_index, (samples, sample_rate, channels));
|
||||||
if let Some(ref controller_arc) = self.audio_controller {
|
|
||||||
let mut controller = controller_arc.lock().unwrap();
|
|
||||||
match controller.get_pool_audio_samples(pool_index) {
|
|
||||||
Ok((samples, sr, ch)) => {
|
|
||||||
println!("Waveform decode complete for pool {}: {} samples", pool_index, samples.len());
|
|
||||||
self.raw_audio_cache.insert(pool_index, (samples, sr, ch));
|
|
||||||
self.waveform_gpu_dirty.insert(pool_index);
|
self.waveform_gpu_dirty.insert(pool_index);
|
||||||
}
|
|
||||||
Err(e) => eprintln!("Failed to fetch decoded audio for pool {}: {}", pool_index, e),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
ctx.request_repaint();
|
ctx.request_repaint();
|
||||||
}
|
}
|
||||||
}
|
|
||||||
_ => {} // Ignore other events for now
|
_ => {} // Ignore other events for now
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -4057,6 +4071,19 @@ impl eframe::App for EditorApp {
|
||||||
self.split_clips_at_playhead();
|
self.split_clips_at_playhead();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Space bar toggles play/pause (only when no text input is focused)
|
||||||
|
if !wants_keyboard && ctx.input(|i| i.key_pressed(egui::Key::Space)) {
|
||||||
|
self.is_playing = !self.is_playing;
|
||||||
|
if let Some(ref controller_arc) = self.audio_controller {
|
||||||
|
let mut controller = controller_arc.lock().unwrap();
|
||||||
|
if self.is_playing {
|
||||||
|
controller.play();
|
||||||
|
} else {
|
||||||
|
controller.pause();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
ctx.input(|i| {
|
ctx.input(|i| {
|
||||||
// Check menu shortcuts that use modifiers (Cmd+S, etc.) - allow even when typing
|
// Check menu shortcuts that use modifiers (Cmd+S, etc.) - allow even when typing
|
||||||
// But skip shortcuts without modifiers when keyboard input is claimed (e.g., virtual piano)
|
// But skip shortcuts without modifiers when keyboard input is claimed (e.g., virtual piano)
|
||||||
|
|
|
||||||
|
|
@ -1296,6 +1296,11 @@ impl PianoRollPane {
|
||||||
let vis_col_start = (vis_audio_start * sample_rate as f64 / 512.0).floor() as i64;
|
let vis_col_start = (vis_audio_start * sample_rate as f64 / 512.0).floor() as i64;
|
||||||
let vis_col_end = (vis_audio_end * sample_rate as f64 / 512.0).ceil() as i64 + 1;
|
let vis_col_end = (vis_audio_end * sample_rate as f64 / 512.0).ceil() as i64 + 1;
|
||||||
|
|
||||||
|
// Calculate stride: how many CQT columns per pixel
|
||||||
|
// When zoomed out, multiple CQT columns map to one pixel — compute every Nth
|
||||||
|
let cols_per_pixel = sample_rate as f32 / (512.0 * self.pixels_per_second);
|
||||||
|
let cqt_stride = (cols_per_pixel.ceil() as u32).max(1);
|
||||||
|
|
||||||
let callback = crate::cqt_gpu::CqtCallback {
|
let callback = crate::cqt_gpu::CqtCallback {
|
||||||
pool_index,
|
pool_index,
|
||||||
params: crate::cqt_gpu::CqtRenderParams {
|
params: crate::cqt_gpu::CqtRenderParams {
|
||||||
|
|
@ -1318,12 +1323,14 @@ impl PianoRollPane {
|
||||||
cache_start_column: 0.0,
|
cache_start_column: 0.0,
|
||||||
cache_valid_start: 0.0,
|
cache_valid_start: 0.0,
|
||||||
cache_valid_end: 0.0,
|
cache_valid_end: 0.0,
|
||||||
_pad: [0.0; 2],
|
column_stride: 0.0, // filled by prepare()
|
||||||
|
_pad: 0.0,
|
||||||
},
|
},
|
||||||
target_format: shared.target_format,
|
target_format: shared.target_format,
|
||||||
sample_rate,
|
sample_rate,
|
||||||
visible_col_start: vis_col_start,
|
visible_col_start: vis_col_start,
|
||||||
visible_col_end: vis_col_end,
|
visible_col_end: vis_col_end,
|
||||||
|
stride: cqt_stride,
|
||||||
};
|
};
|
||||||
|
|
||||||
ui.painter().add(egui_wgpu::Callback::new_paint_callback(
|
ui.painter().add(egui_wgpu::Callback::new_paint_callback(
|
||||||
|
|
|
||||||
|
|
@ -18,7 +18,7 @@ struct CqtParams {
|
||||||
tex_width: u32, // waveform texture width (2048)
|
tex_width: u32, // waveform texture width (2048)
|
||||||
total_frames: u32, // total audio frames in waveform texture
|
total_frames: u32, // total audio frames in waveform texture
|
||||||
sample_rate: f32,
|
sample_rate: f32,
|
||||||
_pad0: u32,
|
column_stride: u32,
|
||||||
_pad1: u32,
|
_pad1: u32,
|
||||||
_pad2: u32,
|
_pad2: u32,
|
||||||
}
|
}
|
||||||
|
|
@ -46,7 +46,7 @@ fn main(@builtin(global_invocation_id) gid: vec3<u32>) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
let global_col = params.column_start + col_rel;
|
let global_col = params.column_start + col_rel * params.column_stride;
|
||||||
let sample_start = global_col * params.hop_size;
|
let sample_start = global_col * params.hop_size;
|
||||||
|
|
||||||
let info = bins[bin_k];
|
let info = bins[bin_k];
|
||||||
|
|
|
||||||
|
|
@ -27,7 +27,8 @@ struct Params {
|
||||||
cache_start_column: f32, // 4 @ 76
|
cache_start_column: f32, // 4 @ 76
|
||||||
cache_valid_start: f32, // 4 @ 80
|
cache_valid_start: f32, // 4 @ 80
|
||||||
cache_valid_end: f32, // 4 @ 84
|
cache_valid_end: f32, // 4 @ 84
|
||||||
_pad: vec2<f32>, // 8 @ 88, total 96
|
column_stride: f32, // 4 @ 88
|
||||||
|
_pad: f32, // 4 @ 92, total 96
|
||||||
}
|
}
|
||||||
|
|
||||||
@group(0) @binding(0) var cache_tex: texture_2d<f32>;
|
@group(0) @binding(0) var cache_tex: texture_2d<f32>;
|
||||||
|
|
@ -141,8 +142,8 @@ fn fs_main(in: VertexOutput) -> @location(0) vec4<f32> {
|
||||||
discard;
|
discard;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Map global column to ring buffer position
|
// Map global column to ring buffer position (accounting for stride)
|
||||||
let ring_pos = global_col - params.cache_start_column;
|
let ring_pos = (global_col - params.cache_start_column) / params.column_stride;
|
||||||
let cache_x = ring_pos % params.cache_capacity;
|
let cache_x = ring_pos % params.cache_capacity;
|
||||||
|
|
||||||
// Sample cache texture with bilinear filtering
|
// Sample cache texture with bilinear filtering
|
||||||
|
|
|
||||||
|
|
@ -63,8 +63,9 @@ fn fs_main(in: VertexOutput) -> @location(0) vec4<f32> {
|
||||||
}
|
}
|
||||||
|
|
||||||
// Fragment X position → audio time
|
// Fragment X position → audio time
|
||||||
let timeline_time = params.viewport_start_time + (frag_x - params.clip_rect.x) / params.pixels_per_second;
|
// clip_start_time is the screen X of the (unclamped) clip left edge.
|
||||||
let audio_time = timeline_time - params.clip_start_time + params.trim_start;
|
// (frag_x - clip_start_time) / pps gives the time offset from the clip's start.
|
||||||
|
let audio_time = (frag_x - params.clip_start_time) / params.pixels_per_second + params.trim_start;
|
||||||
|
|
||||||
// Audio time → frame index
|
// Audio time → frame index
|
||||||
let frame_f = audio_time * params.sample_rate - params.segment_start_frame;
|
let frame_f = audio_time * params.sample_rate - params.segment_start_frame;
|
||||||
|
|
|
||||||
|
|
@ -1255,7 +1255,7 @@ impl TimelinePane {
|
||||||
pixels_per_second: self.pixels_per_second as f32,
|
pixels_per_second: self.pixels_per_second as f32,
|
||||||
audio_duration: audio_file_duration as f32,
|
audio_duration: audio_file_duration as f32,
|
||||||
sample_rate: *sr as f32,
|
sample_rate: *sr as f32,
|
||||||
clip_start_time: instance_start as f32,
|
clip_start_time: clip_screen_start,
|
||||||
trim_start: preview_trim_start as f32,
|
trim_start: preview_trim_start as f32,
|
||||||
tex_width: crate::waveform_gpu::tex_width() as f32,
|
tex_width: crate::waveform_gpu::tex_width() as f32,
|
||||||
total_frames: total_frames as f32,
|
total_frames: total_frames as f32,
|
||||||
|
|
|
||||||
|
|
@ -43,8 +43,10 @@ pub struct WaveformGpuEntry {
|
||||||
pub uniform_buffers: Vec<wgpu::Buffer>,
|
pub uniform_buffers: Vec<wgpu::Buffer>,
|
||||||
/// Frames covered by each texture segment
|
/// Frames covered by each texture segment
|
||||||
pub frames_per_segment: u32,
|
pub frames_per_segment: u32,
|
||||||
/// Total frame count
|
/// Total frame count of data currently in the texture
|
||||||
pub total_frames: u64,
|
pub total_frames: u64,
|
||||||
|
/// Allocated texture height (may be larger than needed for current total_frames)
|
||||||
|
pub tex_height: u32,
|
||||||
/// Sample rate
|
/// Sample rate
|
||||||
pub sample_rate: u32,
|
pub sample_rate: u32,
|
||||||
/// Number of channels in source audio
|
/// Number of channels in source audio
|
||||||
|
|
@ -271,14 +273,100 @@ impl WaveformGpuResources {
|
||||||
sample_rate: u32,
|
sample_rate: u32,
|
||||||
channels: u32,
|
channels: u32,
|
||||||
) -> Vec<wgpu::CommandBuffer> {
|
) -> Vec<wgpu::CommandBuffer> {
|
||||||
// Remove old entry if exists
|
let new_total_frames = samples.len() / channels.max(1) as usize;
|
||||||
self.entries.remove(&pool_index);
|
if new_total_frames == 0 {
|
||||||
|
|
||||||
let total_frames = samples.len() / channels.max(1) as usize;
|
|
||||||
if total_frames == 0 {
|
|
||||||
return Vec::new();
|
return Vec::new();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// If entry exists and texture is large enough, do an incremental update
|
||||||
|
let incremental = if let Some(entry) = self.entries.get(&pool_index) {
|
||||||
|
let new_tex_height = (new_total_frames as u32 + TEX_WIDTH - 1) / TEX_WIDTH;
|
||||||
|
if new_tex_height <= entry.tex_height && new_total_frames > entry.total_frames as usize {
|
||||||
|
Some((entry.total_frames as usize, entry.tex_height))
|
||||||
|
} else if new_total_frames <= entry.total_frames as usize {
|
||||||
|
return Vec::new(); // No new data
|
||||||
|
} else {
|
||||||
|
None // Texture too small, need full recreate
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
None // No entry yet
|
||||||
|
};
|
||||||
|
|
||||||
|
if let Some((old_frames, tex_height)) = incremental {
|
||||||
|
// Write only the NEW rows into the existing texture
|
||||||
|
let start_row = old_frames as u32 / TEX_WIDTH;
|
||||||
|
let end_row = (new_total_frames as u32 + TEX_WIDTH - 1) / TEX_WIDTH;
|
||||||
|
let rows_to_write = end_row - start_row;
|
||||||
|
|
||||||
|
let row_texel_count = (TEX_WIDTH * rows_to_write) as usize;
|
||||||
|
let mut row_data: Vec<half::f16> = vec![half::f16::ZERO; row_texel_count * 4];
|
||||||
|
|
||||||
|
let row_start_frame = start_row as usize * TEX_WIDTH as usize;
|
||||||
|
for frame in 0..(rows_to_write as usize * TEX_WIDTH as usize) {
|
||||||
|
let global_frame = row_start_frame + frame;
|
||||||
|
if global_frame >= new_total_frames {
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
let sample_offset = global_frame * channels as usize;
|
||||||
|
let left = if sample_offset < samples.len() {
|
||||||
|
samples[sample_offset]
|
||||||
|
} else {
|
||||||
|
0.0
|
||||||
|
};
|
||||||
|
let right = if channels >= 2 && sample_offset + 1 < samples.len() {
|
||||||
|
samples[sample_offset + 1]
|
||||||
|
} else {
|
||||||
|
left
|
||||||
|
};
|
||||||
|
let texel_offset = frame * 4;
|
||||||
|
row_data[texel_offset] = half::f16::from_f32(left);
|
||||||
|
row_data[texel_offset + 1] = half::f16::from_f32(left);
|
||||||
|
row_data[texel_offset + 2] = half::f16::from_f32(right);
|
||||||
|
row_data[texel_offset + 3] = half::f16::from_f32(right);
|
||||||
|
}
|
||||||
|
|
||||||
|
let entry = self.entries.get(&pool_index).unwrap();
|
||||||
|
queue.write_texture(
|
||||||
|
wgpu::TexelCopyTextureInfo {
|
||||||
|
texture: &entry.textures[0],
|
||||||
|
mip_level: 0,
|
||||||
|
origin: wgpu::Origin3d { x: 0, y: start_row, z: 0 },
|
||||||
|
aspect: wgpu::TextureAspect::All,
|
||||||
|
},
|
||||||
|
bytemuck::cast_slice(&row_data),
|
||||||
|
wgpu::TexelCopyBufferLayout {
|
||||||
|
offset: 0,
|
||||||
|
bytes_per_row: Some(TEX_WIDTH * 8),
|
||||||
|
rows_per_image: Some(rows_to_write),
|
||||||
|
},
|
||||||
|
wgpu::Extent3d {
|
||||||
|
width: TEX_WIDTH,
|
||||||
|
height: rows_to_write,
|
||||||
|
depth_or_array_layers: 1,
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
|
// Regenerate mipmaps
|
||||||
|
let mip_count = compute_mip_count(TEX_WIDTH, tex_height);
|
||||||
|
let cmds = self.generate_mipmaps(
|
||||||
|
device,
|
||||||
|
&entry.textures[0],
|
||||||
|
TEX_WIDTH,
|
||||||
|
tex_height,
|
||||||
|
mip_count,
|
||||||
|
new_total_frames as u32,
|
||||||
|
);
|
||||||
|
|
||||||
|
// Update total_frames after borrow of entry is done
|
||||||
|
self.entries.get_mut(&pool_index).unwrap().total_frames = new_total_frames as u64;
|
||||||
|
return cmds;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Full create (first upload or texture needs to grow)
|
||||||
|
self.entries.remove(&pool_index);
|
||||||
|
|
||||||
|
let total_frames = new_total_frames;
|
||||||
|
|
||||||
let max_frames_per_segment = (TEX_WIDTH as u64)
|
let max_frames_per_segment = (TEX_WIDTH as u64)
|
||||||
* (device.limits().max_texture_dimension_2d as u64);
|
* (device.limits().max_texture_dimension_2d as u64);
|
||||||
let segment_count =
|
let segment_count =
|
||||||
|
|
@ -323,7 +411,6 @@ impl WaveformGpuResources {
|
||||||
});
|
});
|
||||||
|
|
||||||
// Pack raw samples into Rgba16Float data for mip 0
|
// Pack raw samples into Rgba16Float data for mip 0
|
||||||
// R=left_min=left_sample, G=left_max=left_sample, B=right_min, A=right_max
|
|
||||||
let texel_count = (TEX_WIDTH * tex_height) as usize;
|
let texel_count = (TEX_WIDTH * tex_height) as usize;
|
||||||
let mut mip0_data: Vec<half::f16> = vec![half::f16::ZERO; texel_count * 4];
|
let mut mip0_data: Vec<half::f16> = vec![half::f16::ZERO; texel_count * 4];
|
||||||
|
|
||||||
|
|
@ -339,14 +426,14 @@ impl WaveformGpuResources {
|
||||||
let right = if channels >= 2 && sample_offset + 1 < samples.len() {
|
let right = if channels >= 2 && sample_offset + 1 < samples.len() {
|
||||||
samples[sample_offset + 1]
|
samples[sample_offset + 1]
|
||||||
} else {
|
} else {
|
||||||
left // Mono: duplicate left to right
|
left
|
||||||
};
|
};
|
||||||
|
|
||||||
let texel_offset = frame * 4;
|
let texel_offset = frame * 4;
|
||||||
mip0_data[texel_offset] = half::f16::from_f32(left); // R = left_min
|
mip0_data[texel_offset] = half::f16::from_f32(left);
|
||||||
mip0_data[texel_offset + 1] = half::f16::from_f32(left); // G = left_max
|
mip0_data[texel_offset + 1] = half::f16::from_f32(left);
|
||||||
mip0_data[texel_offset + 2] = half::f16::from_f32(right); // B = right_min
|
mip0_data[texel_offset + 2] = half::f16::from_f32(right);
|
||||||
mip0_data[texel_offset + 3] = half::f16::from_f32(right); // A = right_max
|
mip0_data[texel_offset + 3] = half::f16::from_f32(right);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Upload mip 0
|
// Upload mip 0
|
||||||
|
|
@ -360,7 +447,7 @@ impl WaveformGpuResources {
|
||||||
bytemuck::cast_slice(&mip0_data),
|
bytemuck::cast_slice(&mip0_data),
|
||||||
wgpu::TexelCopyBufferLayout {
|
wgpu::TexelCopyBufferLayout {
|
||||||
offset: 0,
|
offset: 0,
|
||||||
bytes_per_row: Some(TEX_WIDTH * 8), // 4 channels × 2 bytes (f16)
|
bytes_per_row: Some(TEX_WIDTH * 8),
|
||||||
rows_per_image: Some(tex_height),
|
rows_per_image: Some(tex_height),
|
||||||
},
|
},
|
||||||
wgpu::Extent3d {
|
wgpu::Extent3d {
|
||||||
|
|
@ -387,7 +474,7 @@ impl WaveformGpuResources {
|
||||||
..Default::default()
|
..Default::default()
|
||||||
});
|
});
|
||||||
|
|
||||||
// Create uniform buffer placeholder (will be filled per-draw in paint)
|
// Create uniform buffer placeholder
|
||||||
let uniform_buffer = device.create_buffer(&wgpu::BufferDescriptor {
|
let uniform_buffer = device.create_buffer(&wgpu::BufferDescriptor {
|
||||||
label: Some(&format!("waveform_{}_seg{}_uniforms", pool_index, seg)),
|
label: Some(&format!("waveform_{}_seg{}_uniforms", pool_index, seg)),
|
||||||
size: std::mem::size_of::<WaveformParams>() as u64,
|
size: std::mem::size_of::<WaveformParams>() as u64,
|
||||||
|
|
@ -430,6 +517,7 @@ impl WaveformGpuResources {
|
||||||
uniform_buffers,
|
uniform_buffers,
|
||||||
frames_per_segment,
|
frames_per_segment,
|
||||||
total_frames: total_frames as u64,
|
total_frames: total_frames as u64,
|
||||||
|
tex_height: (total_frames as u32 + TEX_WIDTH - 1) / TEX_WIDTH,
|
||||||
sample_rate,
|
sample_rate,
|
||||||
channels,
|
channels,
|
||||||
},
|
},
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue