159 lines
5.6 KiB
WebGPU Shading Language
159 lines
5.6 KiB
WebGPU Shading Language
// Waveform rendering shader for audio data stored in 2D Rgba16Float textures.
|
|
// Audio samples are packed row-major into 2D: frame_index = y * tex_width + x
|
|
// Mip levels use min/max reduction (4 consecutive samples per level).
|
|
//
|
|
// At full zoom, min≈max → renders as a continuous wave.
|
|
// At zoom-out, min/max spread → renders as filled peak region.
|
|
//
|
|
// display_mode: 0 = combined (mono mix), 1 = split (left top, right bottom)
|
|
|
|
struct Params {
|
|
// Clip rectangle in screen pixels (min.x, min.y, max.x, max.y)
|
|
clip_rect: vec4<f32>,
|
|
// Timeline viewport parameters
|
|
viewport_start_time: f32,
|
|
pixels_per_second: f32,
|
|
// Audio file properties
|
|
audio_duration: f32,
|
|
sample_rate: f32,
|
|
// Clip placement
|
|
clip_start_time: f32,
|
|
trim_start: f32,
|
|
// Texture layout
|
|
tex_width: f32,
|
|
total_frames: f32, // total frame count in this texture segment
|
|
segment_start_frame: f32, // first frame this texture covers (for multi-texture)
|
|
display_mode: f32, // 0 = combined, 1 = split stereo
|
|
// Appearance
|
|
tint_color: vec4<f32>,
|
|
// Screen dimensions for coordinate conversion
|
|
screen_size: vec2<f32>,
|
|
_pad: vec2<f32>,
|
|
}
|
|
|
|
@group(0) @binding(0) var peak_tex: texture_2d<f32>;
|
|
@group(0) @binding(1) var peak_sampler: sampler;
|
|
@group(0) @binding(2) var<uniform> params: Params;
|
|
|
|
struct VertexOutput {
|
|
@builtin(position) position: vec4<f32>,
|
|
@location(0) uv: vec2<f32>,
|
|
}
|
|
|
|
// Fullscreen triangle (3 vertices, no vertex buffer)
|
|
@vertex
|
|
fn vs_main(@builtin(vertex_index) vi: u32) -> VertexOutput {
|
|
var out: VertexOutput;
|
|
let x = f32(i32(vi) / 2) * 4.0 - 1.0;
|
|
let y = f32(i32(vi) % 2) * 4.0 - 1.0;
|
|
out.position = vec4(x, y, 0.0, 1.0);
|
|
out.uv = vec2((x + 1.0) * 0.5, (1.0 - y) * 0.5);
|
|
return out;
|
|
}
|
|
|
|
@fragment
|
|
fn fs_main(in: VertexOutput) -> @location(0) vec4<f32> {
|
|
let frag_x = in.position.x;
|
|
let frag_y = in.position.y;
|
|
|
|
// Clip to the clip rectangle
|
|
if frag_x < params.clip_rect.x || frag_x > params.clip_rect.z ||
|
|
frag_y < params.clip_rect.y || frag_y > params.clip_rect.w {
|
|
discard;
|
|
}
|
|
|
|
// Fragment X position → audio time
|
|
// clip_start_time is the screen X of the (unclamped) clip left edge.
|
|
// (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
|
|
let frame_f = audio_time * params.sample_rate - params.segment_start_frame;
|
|
if frame_f < 0.0 || frame_f >= params.total_frames {
|
|
discard;
|
|
}
|
|
|
|
// Determine mip level based on how many audio frames map to one pixel
|
|
let frames_per_pixel = params.sample_rate / params.pixels_per_second;
|
|
// Each mip level reduces by 4x in sample count (2x in each texture dimension)
|
|
let mip_f = max(0.0, log2(frames_per_pixel) / 2.0);
|
|
let max_mip = f32(textureNumLevels(peak_tex) - 1u);
|
|
let mip = min(mip_f, max_mip);
|
|
|
|
// Frame index at the chosen mip level
|
|
let mip_floor = u32(mip);
|
|
let reduction = pow(4.0, f32(mip_floor));
|
|
let mip_frame = frame_f / reduction;
|
|
|
|
// Convert 1D mip-space index to 2D UV coordinates
|
|
let mip_tex_width = params.tex_width / pow(2.0, f32(mip_floor));
|
|
let mip_tex_height = ceil(params.total_frames / reduction / mip_tex_width);
|
|
let texel_x = mip_frame % mip_tex_width;
|
|
let texel_y = floor(mip_frame / mip_tex_width);
|
|
let uv = vec2((texel_x + 0.5) / mip_tex_width, (texel_y + 0.5) / mip_tex_height);
|
|
|
|
// Sample the peak texture at computed mip level
|
|
// R = left_min, G = left_max, B = right_min, A = right_max
|
|
let peak = textureSampleLevel(peak_tex, peak_sampler, uv, mip);
|
|
|
|
let clip_height = params.clip_rect.w - params.clip_rect.y;
|
|
let clip_top = params.clip_rect.y;
|
|
|
|
if params.display_mode < 0.5 {
|
|
// Combined mode: merge both channels
|
|
let wave_min = min(peak.r, peak.b);
|
|
let wave_max = max(peak.g, peak.a);
|
|
|
|
let center_y = clip_top + clip_height * 0.5;
|
|
let scale = clip_height * 0.45;
|
|
|
|
let y_top = center_y - wave_max * scale;
|
|
let y_bot = center_y - wave_min * scale;
|
|
|
|
// At least 1px tall for visibility
|
|
let y_top_adj = min(y_top, center_y - 0.5);
|
|
let y_bot_adj = max(y_bot, center_y + 0.5);
|
|
|
|
if frag_y >= y_top_adj && frag_y <= y_bot_adj {
|
|
return params.tint_color;
|
|
}
|
|
} else {
|
|
// Split stereo mode: left channel in top half, right channel in bottom half
|
|
let half_height = clip_height * 0.5;
|
|
let mid_y = clip_top + half_height;
|
|
|
|
// Determine which channel this fragment belongs to
|
|
if frag_y < mid_y {
|
|
// Top half: left channel
|
|
let center_y = clip_top + half_height * 0.5;
|
|
let scale = half_height * 0.45;
|
|
|
|
let y_top = center_y - peak.g * scale; // left_max
|
|
let y_bot = center_y - peak.r * scale; // left_min
|
|
|
|
let y_top_adj = min(y_top, center_y - 0.5);
|
|
let y_bot_adj = max(y_bot, center_y + 0.5);
|
|
|
|
if frag_y >= y_top_adj && frag_y <= y_bot_adj {
|
|
return params.tint_color;
|
|
}
|
|
} else {
|
|
// Bottom half: right channel
|
|
let center_y = mid_y + half_height * 0.5;
|
|
let scale = half_height * 0.45;
|
|
|
|
let y_top = center_y - peak.a * scale; // right_max
|
|
let y_bot = center_y - peak.b * scale; // right_min
|
|
|
|
let y_top_adj = min(y_top, center_y - 0.5);
|
|
let y_bot_adj = max(y_bot, center_y + 0.5);
|
|
|
|
if frag_y >= y_top_adj && frag_y <= y_bot_adj {
|
|
return params.tint_color;
|
|
}
|
|
}
|
|
}
|
|
|
|
discard;
|
|
}
|