srctree

Andrew Kelley parent 4223a56c b4abb8c2
initial implementation of waveforms drawn by the client

client/main.zig added: 118, removed: 6, total 112
@@ -228,6 +228,11 @@ export fn file_title(index: Db.File.Index) String {
return String.init(db.stringToSlice(db.file(index).title));
}
 
export fn playbackFileIndex() Db.File.OptionalIndex {
const head = currentPlayHead() orelse return .none;
return head.item.file.toOptional();
}
 
export fn playbackTitle() String {
const head = currentPlayHead() orelse return String.empty;
return String.init(db.stringToSlice(head.file.title));
@@ -243,6 +248,64 @@ export fn playbackDuration() f64 {
return @as(f64, @floatFromInt(head.duration)) / std.time.ns_per_s;
}
 
var waveform_result: [waveform_row_size * waveform_h]u8 = undefined;
const waveform_row_size = Db.waveform_width * 4;
const waveform_h = 256;
const peak_color = [4]u8{ 0x00, 0x73, 0xff, 0xff };
const transparent_color = [4]u8{ 0x00, 0x73, 0xff, 0x00 };
const rms_color = [4]u8{ 0xcc, 0xcc, 0xee, 0xff };
 
export fn fileWaveform(opt_file_index: Db.File.OptionalIndex) Slice(u8) {
const file_index = opt_file_index.unwrap() orelse {
@memset(&waveform_result, 0);
return Slice(u8).init(&waveform_result);
};
const channel_len = Db.waveform_width / Twi.slice_len; // number of u64 in the channel
const channel_count = @typeInfo(Twi.ColumnSlice).Struct.fields.len;
const waveform_size = channel_count * channel_len; // total number of u64 in the waveform
const start = waveform_size * @intFromEnum(file_index);
 
const max_array = db.waveforms.items[start + channel_len * 0 ..][0..channel_len];
const min_array = db.waveforms.items[start + channel_len * 1 ..][0..channel_len];
const rms_array = db.waveforms.items[start + channel_len * 2 ..][0..channel_len];
 
var decoder = Twi.Decoder.init;
var out_max: [Twi.slice_len]i8 = undefined;
var out_min: [Twi.slice_len]i8 = undefined;
var out_rms: [Twi.slice_len]i8 = undefined;
for (max_array, min_array, rms_array, 0..) |max_slice, min_slice, rms_slice, slice_index| {
decoder.decode(.{
.max = max_slice,
.min = min_slice,
.rms = rms_slice,
}, &out_max, &out_min, &out_rms);
 
for (out_max, out_min, out_rms, 0..) |max, decoded_min, rms, i| {
const min = @min(decoded_min, max);
const inv_max: usize = @intCast(waveform_h - (128 + @as(i32, max)));
const inv_min: usize = @intCast(waveform_h - (128 + @as(i32, min)));
const x = Twi.slice_len * slice_index + i;
for (0..inv_max) |y| {
waveform_result[y * waveform_row_size + x * 4 ..][0..4].* = transparent_color;
}
for (inv_max..inv_min) |y| {
waveform_result[y * waveform_row_size + x * 4 ..][0..4].* = peak_color;
}
for (inv_min..256) |y| {
waveform_result[y * waveform_row_size + x * 4 ..][0..4].* = transparent_color;
}
const rms_mag: usize = @intCast(128 + @as(i32, rms));
const half = rms_mag / 2;
log.debug("rms={d} rms_mag={d} half={d}", .{ rms, rms_mag, half });
for (127 - half..127 + half) |y| {
waveform_result[y * waveform_row_size + x * 4 ..][0..4].* = rms_color;
}
}
}
 
return Slice(u8).init(&waveform_result);
}
 
fn send(bytes: []const u8) void {
js.send(bytes.ptr, bytes.len);
}
 
shared/Db.zig added: 118, removed: 6, total 112
@@ -208,6 +208,20 @@ pub const File = extern struct {
 
pub const Index = enum(u32) {
_,
 
pub fn toOptional(i: Index) OptionalIndex {
return @enumFromInt(@intFromEnum(i));
}
};
 
pub const OptionalIndex = enum(u16) {
none = std.math.maxInt(u16),
_,
 
pub fn unwrap(i: OptionalIndex) ?Index {
if (i == .none) return null;
return @enumFromInt(@intFromEnum(i));
}
};
 
pub const Hasher = struct {
 
www/index.html added: 118, removed: 6, total 112
@@ -15,18 +15,33 @@ body {
}
#sectPlayback {
background-color: #272727;
margin: 0.3em;
margin: 0;
padding: 0;
position: absolute;
left: 0.3em;
right: 0.3em;
top: 0.3em;
height: 5em;
}
#playbackCanvas {
background: none;
position: absolute;
left: 0;
right: 0;
top: 0;
bottom: 0;
height: 100%;
width: 100%;
}
#seekBar {
background: grey;
background: rgb(255 255 255 / 50%);
width: 0%;
height: 100%;
margin: 0;
padding: 0;
overflow-x: visible;
white-space: nowrap;
position: absolute;
}
#sectLibrary {
width: 22em;
@@ -55,6 +70,7 @@ body {
<body>
<p id="status">Loading...</p>
<div id="sectPlayback" class="hidden">
<canvas id="playbackCanvas"></canvas>
<div id="seekBar">
<div id="title"></div>
<div id="seek"></div>
 
www/main.js added: 118, removed: 6, total 112
@@ -13,9 +13,11 @@
const domTitle = document.getElementById("title");
const domSeek = document.getElementById("seek");
const domSeekBar = document.getElementById("seekBar");
const domPlaybackCanvas = document.getElementById("playbackCanvas");
 
let ws = null;
let wasm_exports = null;
let prevFileIndex = null;
const text_decoder = new TextDecoder();
const text_encoder = new TextEncoder();
 
@@ -101,6 +103,13 @@
return decodeString(ptr, len);
}
 
function unwrapBytes(bigint) {
const ptr = Number(bigint & 0xffffffffn);
const len = Number(bigint >> 32n);
if (len === 0) return [];
return new Uint8ClampedArray(wasm_exports.memory.buffer, ptr, len);
}
 
function renderUi() {
domStatus.classList.add("hidden");
domSectLibrary.classList.add("hidden");
@@ -127,6 +136,16 @@
(unclampedPosition < 0) ? 0 : unclampedPosition;
domSeek.textContent = position + " / " + duration;
domSeekBar.style.width = (position / duration * 100) + "%";
 
const curFileIndex = wasm_exports.playbackFileIndex();
if (prevFileIndex !== curFileIndex) {
prevFileIndex = curFileIndex;
const bytes = unwrapBytes(wasm_exports.fileWaveform(curFileIndex));
domPlaybackCanvas.width = 1920;
domPlaybackCanvas.height = 256;
const canvasContext = domPlaybackCanvas.getContext('2d');
canvasContext.putImageData(new ImageData(bytes, domPlaybackCanvas.width), 0, 0);
}
}
 
function renderQueueUi() {