refactor sample list to store in blocks

This commit is contained in:
Rokas Puzonas 2025-05-01 19:09:32 +03:00
parent af8cf21f53
commit e48887cdee
6 changed files with 665 additions and 451 deletions

View File

@ -167,19 +167,293 @@ fn GenerationalArray(Item: type) type {
}
pub const SampleList = struct {
samples: std.ArrayListUnmanaged(f64) = .{},
graph_min_max_cache: Graph.MinMaxCache = .{},
min_sample: ?f64 = null,
max_sample: ?f64 = null,
pub const Block = struct {
pub const Id = usize;
pub const capacity = 512;
pub const Len = std.math.IntFittingRange(0, capacity);
const Buffer = [capacity]f64;
pub fn clear(self: *SampleList, allocator: Allocator) void {
self.deinit(allocator);
self.* = .{};
comptime {
assert(std.math.isPowerOfTwo(capacity));
}
buffer: *Buffer,
len: Len = 0,
min: ?f64 = null,
max: ?f64 = null,
pub fn samplesSlice(self: *Block) []f64 {
return self.buffer[0..self.len];
}
pub fn append(self: *Block, samples: []const f64) usize {
const unused_slice = self.buffer[self.len..];
const copy_amount = @min(unused_slice.len, samples.len);
const added_samples = samples[0..copy_amount];
@memcpy(unused_slice[0..copy_amount], added_samples);
self.len += @intCast(copy_amount);
assert(self.len <= capacity);
if (added_samples.len > 0) {
var min = self.min orelse added_samples[0];
var max = self.max orelse added_samples[0];
for (added_samples) |sample| {
min = @min(min, sample);
max = @max(max, sample);
}
self.min = min;
self.max = max;
}
return copy_amount;
}
pub fn clear(self: *Block) void {
self.* = Block{
.buffer = self.buffer
};
}
};
const Iterator = struct {
sample_list: *SampleList,
current: usize,
to: usize,
pub fn next(self: *Iterator) ?[]f64 {
while (self.current < self.to) {
const block = self.sample_list.getBlockAtSample(self.current) orelse return null;
const from_index_in_block = @mod(self.current, Block.capacity);
const to_index_in_block = @min(self.to - self.current + from_index_in_block, block.len);
self.current += Block.capacity - from_index_in_block;
if (from_index_in_block >= block.len) {
continue;
}
const segment = block.buffer[from_index_in_block..to_index_in_block];
if (segment.len == 0) {
continue;
}
return segment;
}
return null;
}
};
const BlockIdIterator = struct {
current: Block.Id,
to: Block.Id,
pub fn next(self: *BlockIdIterator) ?Block.Id {
if (self.current > self.to) {
return null;
}
defer self.current += 1;
return self.current;
}
};
arena: std.heap.ArenaAllocator,
blocks: std.ArrayListUnmanaged(Block) = .{},
min: ?f64 = null,
max: ?f64 = null,
pub fn init(allocator: Allocator) SampleList {
return SampleList{
.arena = std.heap.ArenaAllocator.init(allocator),
};
}
pub fn deinit(self: *SampleList, allocator: Allocator) void {
self.samples.clearAndFree(allocator);
self.graph_min_max_cache.deinit(allocator);
pub fn deinit(self: *SampleList) void {
const allocator = self.arena.child_allocator;
self.arena.deinit();
self.blocks.deinit(allocator);
}
pub fn clear(self: *SampleList, allocator: Allocator) void {
self.deinit();
self.* = SampleList.init(allocator);
}
fn getBlockAtSample(self: *SampleList, sample_index: usize) ?*Block {
return self.getBlock(@divFloor(sample_index, Block.capacity));
}
pub fn getBlock(self: *SampleList, block_id: Block.Id) ?*Block {
if (block_id < self.blocks.items.len) {
return &self.blocks.items[block_id];
}
return null;
}
pub fn append(self: *SampleList, samples: []const f64) !void {
if (samples.len == 0) return;
var appended_count: usize = 0;
while (appended_count < samples.len) {
if (self.blocks.items.len == 0 or self.blocks.getLast().len == Block.capacity) {
const allocator = self.arena.child_allocator;
const buffer_allocator = self.arena.allocator();
const block_buffer = try buffer_allocator.create(Block.Buffer);
errdefer buffer_allocator.destroy(block_buffer);
try self.blocks.append(allocator, Block{
.buffer = block_buffer
});
}
const last_block = &self.blocks.items[self.blocks.items.len - 1];
appended_count += last_block.append(samples[appended_count..]);
if (last_block.min) |block_min| {
self.min = @min(self.min orelse block_min, block_min);
}
if (last_block.max) |block_max| {
self.max = @max(self.max orelse block_max, block_max);
}
}
}
pub fn getLength(self: *const SampleList) usize {
var length: usize = 0;
if (self.blocks.items.len > 1) {
length += (self.blocks.items.len - 1) * Block.capacity;
}
if (self.blocks.items.len > 0) {
length += self.blocks.getLast().len;
}
return length;
}
pub fn iterator(self: *SampleList, from: usize, to: usize) Iterator {
return Iterator{
.sample_list = self,
.current = from,
.to = to
};
}
pub fn blockIdIterator(self: *SampleList, from_sample: usize, to_sample: usize) BlockIdIterator {
_ = self;
return BlockIdIterator{
.current = @divFloor(from_sample, Block.capacity),
.to = @divFloor(to_sample, Block.capacity),
};
}
pub fn writeToFile(self: *SampleList, file: std.fs.File) !void {
const writer = file.writer();
var last_sample: f64 = 0;
for (0.., self.blocks.items) |i, *block| {
const block_samples = block.samplesSlice();
if (block_samples.len > 0) {
try writer.writeAll(std.mem.sliceAsBytes(block_samples));
last_sample = block_samples[block_samples.len - 1];
}
if (i < self.blocks.items.len - 1) {
const unused_size = SampleList.Block.capacity - block_samples.len;
if (unused_size > 0) {
try writer.writeBytesNTimes(std.mem.asBytes(&last_sample), unused_size);
}
}
}
}
test {
var sample_list = SampleList.init(std.testing.allocator);
defer sample_list.deinit();
try sample_list.append(&[1]f64{ 0 } ** 256);
try std.testing.expect(sample_list.block_sizes.items[0] == 256);
try sample_list.append(&[1]f64{ 0 } ** 256);
try std.testing.expect(sample_list.block_sizes.items[0] == 512);
try sample_list.append(&[1]f64{ 0 } ** 100);
try std.testing.expect(sample_list.block_sizes.items[1] == 100);
}
test {
var sample_list = SampleList.init(std.testing.allocator);
defer sample_list.deinit();
try sample_list.append(&[1]f64{ 0 } ** (Block.capacity * 3));
try std.testing.expect(
sample_list.getBlockAtSample(Block.capacity - 1).?.ptr == sample_list.getBlock(0).ptr
);
try std.testing.expect(
sample_list.getBlockAtSample(Block.capacity).?.ptr == sample_list.getBlock(1).ptr
);
try std.testing.expect(
sample_list.getBlockAtSample(2*Block.capacity).?.ptr == sample_list.getBlock(2).ptr
);
}
test {
var sample_list = SampleList.init(std.testing.allocator);
defer sample_list.deinit();
try sample_list.append(&[1]f64{ 0 } ** (Block.capacity * 3));
var iter = sample_list.iterator(0, Block.capacity);
try std.testing.expectEqual(sample_list.getBlock(0), iter.next());
try std.testing.expectEqual(null, iter.next());
}
test {
var sample_list = SampleList.init(std.testing.allocator);
defer sample_list.deinit();
try sample_list.append(&[1]f64{ 0 } ** (Block.capacity * 3));
var iter = sample_list.iterator(256, Block.capacity);
try std.testing.expectEqual(sample_list.getBlock(0)[256..], iter.next());
try std.testing.expectEqual(null, iter.next());
}
test {
var sample_list = SampleList.init(std.testing.allocator);
defer sample_list.deinit();
try sample_list.append(&[1]f64{ 0 } ** (Block.capacity * 3));
var iter = sample_list.iterator(256, Block.capacity + 256);
try std.testing.expectEqual(sample_list.getBlock(0)[256..], iter.next());
try std.testing.expectEqual(sample_list.getBlock(1)[0..256], iter.next());
try std.testing.expectEqual(null, iter.next());
}
test {
var sample_list = SampleList.init(std.testing.allocator);
defer sample_list.deinit();
try sample_list.append(&[1]f64{ 0 } ** (Block.capacity * 3));
var iter = sample_list.iterator(400, 2*Block.capacity + 100);
try std.testing.expectEqual(sample_list.getBlock(0)[400..], iter.next());
try std.testing.expectEqual(sample_list.getBlock(1), iter.next());
try std.testing.expectEqual(sample_list.getBlock(2)[0..100], iter.next());
try std.testing.expectEqual(null, iter.next());
}
};
@ -297,26 +571,32 @@ pub const View = struct {
self.standard_deviation = null;
}
pub fn refresh(self: *MarkedRange, samples: []const f64) void {
pub fn refresh(self: *MarkedRange, sample_list: *SampleList) void {
self.clear();
if (self.axis == .X) {
const from = std.math.clamp(@as(usize, @intFromFloat(@floor(self.range.lower))), 0, samples.len);
const to = std.math.clamp(@as(usize, @intFromFloat(@ceil(self.range.upper))), 0, samples.len);
if (to - from == 0) return;
var sample_count: f64 = 0;
var sum: f64 = 0;
var min = std.math.inf(f64);
var max = -std.math.inf(f64);
const samples_in_range = samples[from..to];
if (samples_in_range.len > 0) {
const sample_count: f64 = @floatFromInt(samples_in_range.len);
const from_sample: usize = @intFromFloat(@floor(std.math.clamp(self.range.lower, 0, @as(f64, @floatFromInt(sample_list.getLength())))));
const to_sample: usize = @intFromFloat(@ceil(std.math.clamp(self.range.upper, 0, @as(f64, @floatFromInt(sample_list.getLength())))));
var sum: f64 = 0;
var min = samples_in_range[0];
var max = samples_in_range[0];
for (samples_in_range) |sample| {
min = @min(min, sample);
max = @max(max, sample);
sum += sample;
{
var iter = sample_list.iterator(from_sample, to_sample);
while (iter.next()) |segment| {
sample_count += @floatFromInt(segment.len);
for (segment) |sample| {
min = @min(min, sample);
max = @max(max, sample);
sum += sample;
}
}
}
if (sample_count > 0) {
const average = sum / sample_count;
self.min = min;
@ -325,8 +605,11 @@ pub const View = struct {
if (sample_count > 1) {
var standard_deviation: f64 = 0;
for (samples_in_range) |sample| {
standard_deviation += (sample - average) * (sample - average);
var iter = sample_list.iterator(from_sample, to_sample);
while (iter.next()) |segment| {
for (segment) |sample| {
standard_deviation += (sample - average) * (sample - average);
}
}
standard_deviation /= (sample_count - 1);
standard_deviation = @sqrt(standard_deviation);
@ -379,7 +662,8 @@ pub const View = struct {
available_x_range: RangeF64 = RangeF64.init(0, 0),
available_y_range: RangeF64 = RangeF64.init(0, 0),
unit: ?NIDaq.Unit = .Voltage,
transformed_samples: ?[]f64 = null,
transformed_samples: ?Id = null,
pub fn clear(self: *View) void {
self.graph_cache.clear();
@ -401,19 +685,6 @@ pub const View = struct {
};
}
pub fn appendMarkedRange(self: *View, axis: UI.Axis, range: RangeF64) ?*MarkedRange {
if (self.marked_ranges.unusedCapacitySlice().len == 0) {
return null;
}
const marked_range = self.marked_ranges.addOneAssumeCapacity();
marked_range.* = MarkedRange{
.axis = axis,
.range = range
};
return marked_range;
}
pub fn iterMarkedRanges(self: *View, axis: UI.Axis) MarkedRangeIterator {
return MarkedRangeIterator{
.view = self,
@ -469,7 +740,7 @@ pub const Project = struct {
return result_range;
}
pub fn getViewSampleList(self: *Project, view_id: Id) Id {
pub fn getViewSampleListId(self: *Project, view_id: Id) Id {
const view = self.views.get(view_id).?;
switch (view.reference) {
@ -484,75 +755,150 @@ pub const Project = struct {
}
}
pub fn getViewSamples(self: *Project, view_id: Id) []const f64 {
const samples_list_id = self.getViewSampleList(view_id);
const samples_list = self.sample_lists.get(samples_list_id).?;
return samples_list.samples.items;
}
pub fn appendSamples(self: *Project, allocator: Allocator, sample_list_id: Id, samples: []const f64) !void {
if (samples.len == 0) return;
const sample_list = self.sample_lists.get(sample_list_id).?;
const affected_range = RangeF64.init(
@floatFromInt(sample_list.samples.items.len),
@floatFromInt(sample_list.samples.items.len + samples.len)
@floatFromInt(sample_list.getLength()),
@floatFromInt(sample_list.getLength() + samples.len)
);
try sample_list.samples.appendSlice(allocator, samples);
var min_sample = sample_list.min_sample orelse samples[0];
var max_sample = sample_list.max_sample orelse samples[0];
for (samples) |sample| {
min_sample = @min(min_sample, sample);
max_sample = @max(max_sample, sample);
}
sample_list.min_sample = min_sample;
sample_list.max_sample = max_sample;
try sample_list.graph_min_max_cache.updateLast(allocator, sample_list.samples.items);
try sample_list.append(samples);
try self.refreshTransformedSamplesBySampleList(allocator, sample_list_id);
self.refreshMarkedRanges(sample_list_id, affected_range);
}
pub fn clearSamples(self: *Project, allocator: Allocator, sample_list_id: Id) void {
pub fn clearSamples(self: *Project, allocator: Allocator, sample_list_id: Id) !void {
const sample_list = self.sample_lists.get(sample_list_id).?;
const affected_range = RangeF64.init(
0,
@floatFromInt(sample_list.samples.items.len)
@floatFromInt(sample_list.getLength())
);
sample_list.clear(allocator);
try self.refreshTransformedSamplesBySampleList(allocator, sample_list_id);
self.refreshMarkedRanges(sample_list_id, affected_range);
}
pub fn addSampleList(self: *Project, allocator: Allocator) !Id {
return try self.sample_lists.insert(SampleList.init(allocator));
}
pub fn removeSampleList(self: *Project, sample_list_id: Id) void {
const sample_list = self.sample_lists.get(sample_list_id) orelse return;
sample_list.deinit();
self.sample_lists.remove(sample_list_id);
}
fn refreshMarkedRanges(self: *Project, sample_list_id: Id, affected_range: RangeF64) void {
const sample_list = self.sample_lists.get(sample_list_id).?;
const total_samples = sample_list.samples.items;
_ = sample_list;
_ = affected_range;
// const total_samples = sample_list.samples.items;
// var view_iter = self.views.idIterator();
// while (view_iter.next()) |view_id| {
// const view_sample_list_id = self.getViewSampleList(view_id);
// if (!view_sample_list_id.eql(sample_list_id)) {
// continue;
// }
// const view = self.views.get(view_id).?;
// const marked_ranges: []View.MarkedRange = view.marked_ranges.slice();
// for (marked_ranges) |*marked_range| {
// if (marked_range.axis != .X) continue;
// if (affected_range.intersectPositive(marked_range.range).isPositive()) {
// marked_range.refresh(total_samples);
// }
// }
// }
}
fn refreshTransformedSamplesBySampleList(self: *Project, allocator: Allocator, sample_list_id: Id) !void {
var view_iter = self.views.idIterator();
while (view_iter.next()) |view_id| {
const view_sample_list_id = self.getViewSampleList(view_id);
if (!view_sample_list_id.eql(sample_list_id)) {
continue;
}
const view = self.views.get(view_id).?;
const marked_ranges: []View.MarkedRange = view.marked_ranges.slice();
for (marked_ranges) |*marked_range| {
if (marked_range.axis != .X) continue;
if (affected_range.intersectPositive(marked_range.range).isPositive()) {
marked_range.refresh(total_samples);
}
const view_sample_list_id = self.getViewSampleListId(view_id);
if (view_sample_list_id.eql(sample_list_id)) {
try self.refreshTransformedSamplesByView(allocator, view_id);
}
}
}
fn refreshTransformedSamplesByView(self: *Project, allocator: Allocator, view_id: Id) !void {
const view = self.views.get(view_id).?;
if (view.sliding_window) |sliding_window| {
if (view.transformed_samples == null) {
view.transformed_samples = try self.addSampleList(allocator);
}
// var transformed_sample_list = self.sample_lists.get(view.transformed_samples.?).?;
// const samples = self.getViewSamples(view_id);
// try transformed_sample_list.append(samples);
_ = sliding_window;
} else {
if (view.transformed_samples) |sample_list_id| {
self.removeSampleList(sample_list_id);
view.transformed_samples = null;
}
}
}
pub fn readSamplesFromFile(self: *Project, allocator: Allocator, sample_list_id: Id, file: std.fs.File, sample_count: usize) !void {
var bytes_left: usize = sample_count * 8;
var buffer: [SampleList.Block.capacity * 8]u8 = undefined;
while (true) {
const count = try file.readAll(buffer[0..@min(bytes_left, buffer.len)]);
if (count == 0) break;
assert(count % 8 == 0);
bytes_left -= count;
const samples = std.mem.bytesAsSlice(f64, buffer[0..count]);
try self.appendSamples(allocator, sample_list_id, @alignCast(samples));
}
}
pub fn updateSlidingWindow(self: *Project, view_id: Id, allocator: Allocator, sliding_window: ?f64) !void {
const view = self.views.get(view_id).?;
if (view.sliding_window == sliding_window) {
return;
}
view.sliding_window = sliding_window;
try self.refreshTransformedSamplesByView(allocator, view_id);
}
pub fn appendMarkedRange(self: *Project, view_id: Id, axis: UI.Axis, range: RangeF64) ?*View.MarkedRange {
const view = self.views.get(view_id) orelse return null;
if (view.marked_ranges.unusedCapacitySlice().len == 0) {
return null;
}
const marked_range = view.marked_ranges.addOneAssumeCapacity();
marked_range.* = View.MarkedRange{
.axis = axis,
.range = range
};
const sample_list_id = self.getViewSampleListId(view_id);
if (self.sample_lists.get(sample_list_id)) |sample_list| {
marked_range.refresh(sample_list);
}
return marked_range;
}
pub fn deinit(self: *Project, allocator: Allocator) void {
var file_iter = self.files.iterator();
while (file_iter.next()) |file| {
@ -621,8 +967,8 @@ pub const Project = struct {
}
errdefer if (saved_collected_samples) |str| allocator.free(str);
const sample_list_id = try self.sample_lists.insert(.{});
errdefer self.sample_lists.remove(sample_list_id);
const sample_list_id = try self.addSampleList(allocator);
errdefer self.removeSampleList(sample_list_id);
const channel = self.channels.get(id).?;
channel.* = Channel{
@ -643,8 +989,8 @@ pub const Project = struct {
const path = try readString(reader, allocator);
errdefer allocator.free(path);
const sample_list_id = try self.sample_lists.insert(.{});
errdefer self.sample_lists.remove(sample_list_id);
const sample_list_id = try self.addSampleList(allocator);
errdefer self.removeSampleList(sample_list_id);
const file = self.files.get(id).?;
file.* = File{
@ -735,8 +1081,8 @@ pub const Project = struct {
const samples_file = try dir.createFile(path, .{});
defer samples_file.close();
const samples = self.sample_lists.get(channel.collected_samples_id).?.samples.items;
try writeFileF64(samples_file, samples);
const sample_list = self.sample_lists.get(channel.collected_samples_id).?;
try sample_list.writeToFile(samples_file);
}
const stat = try dir.statFile(path);
@ -838,6 +1184,7 @@ pub const Project = struct {
fn readString(reader: anytype, allocator: Allocator) ![]u8 {
// TODO: This could be risky. `str_len` can be a really large number and in turn request a really large allocation.
// Maybe some sane maximum should be implemented?
const str_len = try readInt(reader, u32);
const buff = try allocator.alloc(u8, str_len);
errdefer allocator.free(buff);
@ -1245,7 +1592,7 @@ fn startCollection(self: *App) !void {
for (channels_in_task.constSlice()) |id| {
const channel = self.getChannel(id).?;
self.project.clearSamples(self.allocator, channel.collected_samples_id);
try self.project.clearSamples(self.allocator, channel.collected_samples_id);
}
const read_array = try self.allocator.alloc(f64,
@ -1420,8 +1767,8 @@ pub fn addFile(self: *App, path: []const u8) !Id {
const path_dupe = try self.allocator.dupe(u8, path);
errdefer self.allocator.free(path_dupe);
const sample_list_id = try self.project.sample_lists.insert(.{});
errdefer self.project.sample_lists.remove(sample_list_id);
const sample_list_id = try self.project.addSampleList(self.allocator);
errdefer self.project.removeSampleList(sample_list_id);
const file = self.project.files.get(id).?;
file.* = File{
@ -1436,41 +1783,6 @@ pub fn addFile(self: *App, path: []const u8) !Id {
return id;
}
fn readFileF64(allocator: Allocator, file: std.fs.File) ![]f64 {
const byte_count = try file.getEndPos();
if (byte_count % 8 != 0) {
return error.NotMultipleOf8;
}
var samples = try allocator.alloc(f64, @divExact(byte_count, 8));
errdefer allocator.free(samples);
try file.seekTo(0);
var i: usize = 0;
var buffer: [4096]u8 = undefined;
while (true) {
const count = try file.readAll(&buffer);
if (count == 0) break;
for (0..@divExact(count, 8)) |j| {
samples[i] = std.mem.bytesToValue(f64, buffer[(j*8)..]);
i += 1;
}
}
return samples;
}
fn writeFileF64(file: std.fs.File, data: []const f64) !void {
try file.seekTo(0);
for (data) |number| {
const number_bytes = std.mem.asBytes(&number);
try file.writeAll(number_bytes);
}
}
pub fn loadFile(self: *App, id: Id) !void {
const file = self.getFile(id) orelse return;
const sample_list = self.project.sample_lists.get(file.samples_id) orelse return;
@ -1483,10 +1795,12 @@ pub fn loadFile(self: *App, id: Id) !void {
const samples_file = try cwd.openFile(file.path, .{ .mode = .read_only });
defer samples_file.close();
const samples = try readFileF64(self.allocator, samples_file);
defer self.allocator.free(samples);
const byte_count = try samples_file.getEndPos();
if (byte_count % 8 != 0) {
return error.NotMultipleOf8;
}
try self.project.appendSamples(self.allocator, file.samples_id, samples);
try self.project.readSamplesFromFile(self.allocator, file.samples_id, samples_file, @divExact(byte_count, 8));
}
// ---------------- Channels --------------------------------- //
@ -1499,8 +1813,8 @@ pub fn addChannel(self: *App, channel_name: []const u8) !Id {
const id = try self.project.channels.insertUndefined();
errdefer self.project.channels.remove(id);
const sample_list_id = try self.project.sample_lists.insert(.{});
errdefer self.project.sample_lists.remove(sample_list_id);
const sample_list_id = try self.project.addSampleList(self.allocator);
errdefer self.project.removeSampleList(sample_list_id);
const channel = self.project.channels.get(id).?;
channel.* = Channel{
@ -1574,10 +1888,12 @@ fn loadSavedSamples(self: *App, channel_id: Id) !void {
const samples_file = try dir.openFile(saved_samples_location, .{ });
defer samples_file.close();
const samples = try readFileF64(self.allocator, samples_file);
defer self.allocator.free(samples);
const byte_count = try samples_file.getEndPos();
if (byte_count % 8 != 0) {
return error.NotMultipleOf8;
}
try self.project.appendSamples(self.allocator, channel.collected_samples_id, samples);
try self.project.readSamplesFromFile(self.allocator, channel.collected_samples_id, samples_file, @divExact(byte_count, 8));
const stat = try dir.statFile(saved_samples_location);
channel.last_sample_save_at = stat.mtime;
@ -1646,23 +1962,12 @@ pub fn addView(self: *App, reference: View.Reference) !Id {
return id;
}
pub fn getViewSamples(self: *App, id: Id) []const f64 {
return self.project.getViewSamples(id);
}
pub fn getViewMinMaxCache(self: *App, id: Id) Graph.MinMaxCache {
const samples_list_id = self.project.getViewSampleList(id);
const sample_list = self.project.sample_lists.get(samples_list_id).?;
return sample_list.graph_min_max_cache;
}
fn refreshViewAvailableXYRanges(self: *App, id: Id) void {
const view = self.getView(id) orelse return;
const sample_list_id = self.project.getViewSampleList(id);
const sample_list_id = self.project.getViewSampleListId(id);
const sample_list = self.project.sample_lists.get(sample_list_id).?;
view.available_x_range = RangeF64.init(0, @floatFromInt(sample_list.samples.items.len));
view.available_x_range = RangeF64.init(0, @floatFromInt(sample_list.getLength()));
switch (view.reference) {
.channel => |channel_id| if (self.getChannel(channel_id)) |channel| {
@ -1671,8 +1976,8 @@ fn refreshViewAvailableXYRanges(self: *App, id: Id) void {
view.available_y_range = RangeF64.init(allowed_sample_values.upper, allowed_sample_values.lower);
},
.file => {
const min_sample = sample_list.min_sample orelse 0;
const max_sample = sample_list.max_sample orelse 1;
const min_sample = sample_list.min orelse 0;
const max_sample = sample_list.max orelse 1;
view.available_y_range = RangeF64.init(max_sample, min_sample);
}
}
@ -1692,3 +1997,7 @@ pub fn loadView(self: *App, id: Id) !void {
view.graph_opts.y_range = view.available_y_range;
}
}
test {
_ = SampleList;
}

View File

@ -37,7 +37,6 @@ fn showGraph(ctx: Context, view_id: Id) *UI.Box {
const app = ctx.app;
const view = app.getView(view_id).?;
const samples = app.getViewSamples(view_id);
const view_opts = &view.graph_opts;
const graph_box = ui.createBox(.{
@ -110,8 +109,9 @@ fn showGraph(ctx: Context, view_id: Id) *UI.Box {
{ // Render graph
view.graph_cache.min_max_cache = app.getViewMinMaxCache(view_id);
Graph.drawCached(&view.graph_cache, graph_box.persistent.size, view_opts.*, samples);
const sample_list_id = app.project.getViewSampleListId(view_id);
const sample_list = app.project.sample_lists.get(sample_list_id).?;
Graph.drawCached(&view.graph_cache, graph_box.persistent.size, view_opts.*, sample_list);
if (view.graph_cache.texture) |texture| {
graph_box.texture = texture.texture;
}

View File

@ -406,7 +406,6 @@ pub fn show(ctx: Context, box: *UI.Box, graph_box: *UI.Box, view_id: Id, axis: U
}
}
} else if (ctx.view_controls.selected_tool == .select) {
// TODO:
if (cursor != null) {
if (ctx.view_controls.getCursorHoldStart(view_id, axis)) |hold_start| {
@ -418,10 +417,7 @@ pub fn show(ctx: Context, box: *UI.Box, graph_box: *UI.Box, view_id: Id, axis: U
const hold_end_mouse = view_range.remapTo(mouse_range, range.upper);
const mouse_move_distance = @abs(hold_end_mouse - hold_start_mouse);
if (signal.flags.contains(.left_released) and mouse_move_distance > 5) {
if (view.appendMarkedRange(axis, range)) |marked_range| {
marked_range.refresh(ctx.project.getViewSamples(view_id));
}
_ = ctx.project.appendMarkedRange(view_id, axis, range);
}
}
}

View File

@ -3,6 +3,7 @@ const std = @import("std");
const rl = @import("raylib");
const srcery = @import("./srcery.zig");
const RangeF64 = @import("./range.zig").RangeF64;
const SampleList = @import("./app.zig").SampleList;
const remap = @import("./utils.zig").remap;
const assert = std.debug.assert;
@ -24,130 +25,6 @@ pub const ViewOptions = struct {
y_range: RangeF64 = RangeF64.init(0, 0),
color: rl.Color = srcery.red,
fn writeStruct(self: ViewOptions, writer: anytype) !void {
_ = self;
_ = writer;
// try writer.writeStructEndian(self.id, file_endian);
// try writer.writeStructEndian(self.channel_name.constSlice(), file_endian);
}
fn readStruct(reader: anytype) !ViewOptions {
_ = reader;
// const id = try reader.readStructEndian(Id, file_endian);
// const channel_name = try reader.readStructEndian([]const u8, file_endian);
return ViewOptions{
.x_range = RangeF64.init(0, 0),
.y_range = RangeF64.init(0, 0),
};
}
};
pub const MinMaxCache = struct {
const MinMaxPair = struct {
min: f64,
max: f64
};
chunk_size: usize = 256,
min_max_pairs: std.ArrayListUnmanaged(MinMaxPair) = .{},
sample_count: usize = 0,
pub fn deinit(self: *MinMaxCache, allocator: std.mem.Allocator) void {
self.min_max_pairs.clearAndFree(allocator);
self.sample_count = 0;
}
fn getMinMaxPair(self: *MinMaxCache, chunk: []const f64) MinMaxPair {
assert(chunk.len > 0);
assert(chunk.len <= self.chunk_size);
var min_sample = chunk[0];
var max_sample = chunk[0];
for (chunk) |sample| {
min_sample = @min(min_sample, sample);
max_sample = @max(max_sample, sample);
}
return MinMaxPair{
.min = min_sample,
.max = max_sample
};
}
pub fn updateAll(self: *MinMaxCache, allocator: std.mem.Allocator, samples: []const f64) !void {
self.min_max_pairs.clearRetainingCapacity();
self.sample_count = 0;
if (samples.len == 0) return;
var iter = std.mem.window(f64, samples, self.chunk_size, self.chunk_size);
while (iter.next()) |chunk| {
try self.min_max_pairs.append(allocator, self.getMinMaxPair(chunk));
}
self.sample_count = samples.len;
}
pub fn updateLast(self: *MinMaxCache, allocator: std.mem.Allocator, samples: []const f64) !void {
if (self.sample_count > samples.len) {
try self.updateAll(allocator, samples);
}
if (self.sample_count == samples.len) {
return;
}
const from_chunk = @divFloor(self.sample_count, self.chunk_size);
const to_chunk = @divFloor(samples.len - 1, self.chunk_size);
for (from_chunk..(to_chunk+1)) |i| {
const chunk = samples[
(self.chunk_size*i)..(@min(self.chunk_size*(i+1), samples.len))
];
const min_max_pair = self.getMinMaxPair(chunk);
if (i >= self.min_max_pairs.items.len) {
try self.min_max_pairs.append(allocator, min_max_pair);
} else {
self.min_max_pairs.items[i] = min_max_pair;
}
}
self.sample_count = samples.len;
}
test {
const allocator = std.testing.allocator;
var min_max_cache: MinMaxCache = .{ .chunk_size = 4 };
defer min_max_cache.deinit(allocator);
try min_max_cache.updateLast(allocator, &.{ 1, 2 });
try std.testing.expectEqualSlices(
MinMaxPair,
&.{
MinMaxPair{ .min = 1, .max = 2 }
},
min_max_cache.min_max_pairs.items
);
try min_max_cache.updateLast(allocator, &.{ 1, 2, 3 });
try std.testing.expectEqualSlices(
MinMaxPair,
&.{
MinMaxPair{ .min = 1, .max = 3 }
},
min_max_cache.min_max_pairs.items
);
try min_max_cache.updateLast(allocator, &.{ 1, 2, 3, -1 });
try std.testing.expectEqualSlices(
MinMaxPair,
&.{
MinMaxPair{ .min = -1, .max = 3 }
},
min_max_cache.min_max_pairs.items
);
}
};
pub const RenderCache = struct {
@ -156,7 +33,6 @@ pub const RenderCache = struct {
drawn_x_range: RangeF64
};
min_max_cache: ?MinMaxCache = null,
texture: ?rl.RenderTexture2D = null,
key: ?Key = null,
@ -235,7 +111,164 @@ pub const SampleIterator = struct {
}
};
fn drawSamplesExact(draw_rect: rl.Rectangle, options: ViewOptions, samples: []const f64) void {
fn drawSamplesExact(draw_rect: rl.Rectangle, options: ViewOptions, sample_list: *SampleList) void {
const draw_x_range, const draw_y_range = RangeF64.initRect(draw_rect);
// TODO: Wtf is this, I don't remember why I fiddled with the x range.
var view_x_range = options.x_range;
view_x_range.upper += 2;
view_x_range.lower -= 1;
const i_range = view_x_range.intersectPositive(
RangeF64.init(0, @max(@as(f64, @floatFromInt(sample_list.getLength())) - 1, 0))
);
if (i_range.lower > i_range.upper) {
return;
}
var block_id_iter = sample_list.blockIdIterator(
@intFromFloat(i_range.lower),
@intFromFloat(i_range.upper)
);
while (block_id_iter.next()) |block_id| {
const block = sample_list.getBlock(block_id) orelse continue;
const block_samples = block.samplesSlice();
if (block_samples.len == 0) continue;
if (block_samples.len > 1) {
for (0..(block_samples.len-1)) |i| {
const i_f64: f64 = @floatFromInt(block_id * SampleList.Block.capacity + i);
rl.drawLineV(
.{
.x = @floatCast(options.x_range.remapTo(draw_x_range, i_f64)),
.y = @floatCast(options.y_range.remapTo(draw_y_range, block_samples[i])),
},
.{
.x = @floatCast(options.x_range.remapTo(draw_x_range, i_f64 + 1)),
.y = @floatCast(options.y_range.remapTo(draw_y_range, block_samples[i + 1])),
},
options.color
);
}
}
if (block_samples.len == SampleList.Block.capacity) {
if (sample_list.getBlock(block_id + 1)) |next_block| {
const next_block_samples = next_block.samplesSlice();
if (next_block_samples.len > 0) {
const i_f64: f64 = @floatFromInt((block_id + 1) * SampleList.Block.capacity - 1);
rl.drawLineV(
.{
.x = @floatCast(options.x_range.remapTo(draw_x_range, i_f64)),
.y = @floatCast(options.y_range.remapTo(draw_y_range, block_samples[SampleList.Block.capacity - 1])),
},
.{
.x = @floatCast(options.x_range.remapTo(draw_x_range, i_f64 + 1)),
.y = @floatCast(options.y_range.remapTo(draw_y_range, next_block_samples[0])),
},
options.color
);
}
}
}
}
}
fn drawColumn(draw_rect: rl.Rectangle, options: ViewOptions, sample_index: f64, column_min: f64, column_max: f64) void {
const draw_x_range, const draw_y_range = RangeF64.initRect(draw_rect);
const x = options.x_range.remapTo(draw_x_range, sample_index);
const y_min = options.y_range.remapTo(draw_y_range, column_min);
const y_max = options.y_range.remapTo(draw_y_range, column_max);
if (@abs(y_max - y_min) < 1) {
const avg = (y_min + y_max) / 2;
rl.drawLineV(
.{ .x = @floatCast(x), .y = @floatCast(avg) },
.{ .x = @floatCast(x), .y = @floatCast(avg+1) },
options.color
);
} else {
rl.drawLineV(
.{ .x = @floatCast(x), .y = @floatCast(y_min) },
.{ .x = @floatCast(x), .y = @floatCast(y_max) },
options.color
);
}
}
fn drawSamplesApproximate(draw_rect: rl.Rectangle, options: ViewOptions, sample_list: *SampleList) void {
const i_range = options.x_range.intersectPositive(
RangeF64.init(0, @max(@as(f64, @floatFromInt(sample_list.getLength())) - 1, 0))
);
if (i_range.isNegative()) {
return;
}
const samples_per_column = options.x_range.size() / @as(f64, @floatCast(draw_rect.width));
assert(samples_per_column >= 1);
var sample_index = i_range.lower;
while (sample_index < i_range.upper) : (sample_index += samples_per_column) {
var column_min: f64 = std.math.inf(f64);
var column_max: f64 = -std.math.inf(f64);
var iter = sample_list.iterator(
@intFromFloat(sample_index),
@intFromFloat(sample_index + samples_per_column + 1)
);
while (iter.next()) |segment| {
for (segment) |sample| {
column_min = @min(column_min, sample);
column_max = @max(column_max, sample);
}
}
if (!std.math.isInf(column_min)) {
drawColumn(draw_rect, options, sample_index, column_min, column_max);
}
}
}
fn drawSamplesMinMax(draw_rect: rl.Rectangle, options: ViewOptions, sample_list: *SampleList) void {
const i_range = options.x_range.intersectPositive(
RangeF64.init(0, @max(@as(f64, @floatFromInt(sample_list.getLength())) - 1, 0))
);
if (i_range.isNegative()) {
return;
}
const samples_per_column = options.x_range.size() / @as(f64, @floatCast(draw_rect.width));
assert(samples_per_column >= @as(f64, @floatFromInt(SampleList.Block.capacity)));
var i = i_range.lower;
while (i < i_range.upper) : (i += samples_per_column) {
var column_min: f64 = std.math.inf(f64);
var column_max: f64 = -std.math.inf(f64);
var block_id_iter = sample_list.blockIdIterator(
@intFromFloat(i),
@intFromFloat(i + samples_per_column)
);
while (block_id_iter.next()) |block_id| {
const block = sample_list.getBlock(block_id) orelse continue;
if (block.min) |block_min| {
column_min = @min(column_min, block_min);
}
if (block.max) |block_max| {
column_max = @max(column_max, block_max);
}
}
if (!std.math.isInf(column_min)) {
drawColumn(draw_rect, options, i, column_min, column_max);
}
}
}
fn drawSamples(draw_rect: rl.Rectangle, options: ViewOptions, sample_list: *SampleList) void {
rl.beginScissorMode(
@intFromFloat(draw_rect.x),
@intFromFloat(draw_rect.y),
@ -244,157 +277,24 @@ fn drawSamplesExact(draw_rect: rl.Rectangle, options: ViewOptions, samples: []co
);
defer rl.endScissorMode();
const draw_x_range, const draw_y_range = RangeF64.initRect(draw_rect);
var view_x_range = options.x_range;
view_x_range.upper += 2;
view_x_range.lower -= 1;
const i_range = view_x_range.intersectPositive(
RangeF64.init(0, @max(@as(f64, @floatFromInt(samples.len)) - 1, 0))
);
if (i_range.lower > i_range.upper) {
return;
}
const from_i: usize = @intFromFloat(i_range.lower);
const to_i: usize = @intFromFloat(i_range.upper);
if (to_i == 0 or from_i == to_i) {
return;
}
for (from_i..(to_i-1)) |i| {
const i_f64: f64 = @floatFromInt(i);
rl.drawLineV(
.{
.x = @floatCast(options.x_range.remapTo(draw_x_range, i_f64)),
.y = @floatCast(options.y_range.remapTo(draw_y_range, samples[i])),
},
.{
.x = @floatCast(options.x_range.remapTo(draw_x_range, i_f64 + 1)),
.y = @floatCast(options.y_range.remapTo(draw_y_range, samples[i + 1])),
},
options.color
);
}
}
fn drawSamplesApproximate(draw_rect: rl.Rectangle, options: ViewOptions, samples: []const f64) void {
const draw_x_range, const draw_y_range = RangeF64.initRect(draw_rect);
const i_range = options.x_range.intersectPositive(
RangeF64.init(0, @max(@as(f64, @floatFromInt(samples.len)) - 1, 0))
);
if (i_range.isNegative()) {
return;
}
const samples_per_column = options.x_range.size() / draw_x_range.size();
assert(samples_per_column >= 1);
var iter = SampleIterator.init(samples, i_range.lower, i_range.upper, samples_per_column);
while (iter.next()) |column_samples| {
var column_min = column_samples[0];
var column_max = column_samples[0];
for (column_samples) |sample| {
column_min = @min(column_min, sample);
column_max = @max(column_max, sample);
}
const x = options.x_range.remapTo(draw_x_range, iter.i);
const y_min = options.y_range.remapTo(draw_y_range, column_min);
const y_max = options.y_range.remapTo(draw_y_range, column_max);
if (@abs(y_max - y_min) < 1) {
const avg = (y_min + y_max) / 2;
rl.drawLineV(
.{ .x = @floatCast(x), .y = @floatCast(avg) },
.{ .x = @floatCast(x), .y = @floatCast(avg+1) },
options.color
);
} else {
rl.drawLineV(
.{ .x = @floatCast(x), .y = @floatCast(y_min) },
.{ .x = @floatCast(x), .y = @floatCast(y_max) },
options.color
);
}
}
}
fn drawSamplesMinMax(draw_rect: rl.Rectangle, options: ViewOptions, min_max_cache: MinMaxCache) void {
const draw_x_range, const draw_y_range = RangeF64.initRect(draw_rect);
const i_range = options.x_range.intersectPositive(
RangeF64.init(0, @max(@as(f64, @floatFromInt(min_max_cache.sample_count)) - 1, 0))
);
if (i_range.isNegative()) {
return;
}
const samples_per_column = options.x_range.size() / draw_x_range.size();
assert(samples_per_column >= 1);
var i = i_range.lower;
while (i < i_range.upper - samples_per_column) : (i += samples_per_column) {
var column_start: usize = @intFromFloat(i);
var column_end: usize = @intFromFloat(i + samples_per_column);
column_start = @divFloor(column_start, min_max_cache.chunk_size);
column_end = @divFloor(column_end, min_max_cache.chunk_size);
const min_max_pairs = min_max_cache.min_max_pairs.items[column_start..column_end];
if (min_max_pairs.len == 0) continue;
var column_min = min_max_pairs[0].min;
var column_max = min_max_pairs[0].max;
for (min_max_pairs) |min_max_pair| {
column_min = @min(column_min, min_max_pair.min);
column_max = @max(column_max, min_max_pair.max);
}
const x = options.x_range.remapTo(draw_x_range, i);
const y_min = options.y_range.remapTo(draw_y_range, column_min);
const y_max = options.y_range.remapTo(draw_y_range, column_max);
if (@abs(y_max - y_min) < 1) {
const avg = (y_min + y_max) / 2;
rl.drawLineV(
.{ .x = @floatCast(x), .y = @floatCast(avg) },
.{ .x = @floatCast(x), .y = @floatCast(avg+1) },
options.color
);
} else {
rl.drawLineV(
.{ .x = @floatCast(x), .y = @floatCast(y_min) },
.{ .x = @floatCast(x), .y = @floatCast(y_max) },
options.color
);
}
}
}
fn drawSamples(draw_rect: rl.Rectangle, options: ViewOptions, samples: []const f64, min_max_cache: ?MinMaxCache) void {
const x_range = options.x_range;
if (x_range.lower >= @as(f64, @floatFromInt(samples.len))) return;
if (x_range.lower >= @as(f64, @floatFromInt(sample_list.getLength()))) return;
if (x_range.upper < 0) return;
const samples_per_column = x_range.size() / draw_rect.width;
if (samples_per_column >= 2) {
if (min_max_cache != null and samples_per_column > @as(f64, @floatFromInt(2*min_max_cache.?.chunk_size))) {
drawSamplesMinMax(draw_rect, options, min_max_cache.?);
if (samples_per_column >= @as(f64, @floatFromInt(2*SampleList.Block.capacity))) {
drawSamplesMinMax(draw_rect, options, sample_list);
} else {
drawSamplesApproximate(draw_rect, options, samples);
drawSamplesApproximate(draw_rect, options, sample_list);
}
} else {
drawSamplesExact(draw_rect, options, samples);
drawSamplesExact(draw_rect, options, sample_list);
}
}
pub fn drawCached(cache: *RenderCache, render_size: Vec2, options: ViewOptions, samples: []const f64) void {
pub fn drawCached(cache: *RenderCache, render_size: Vec2, options: ViewOptions, sample_list: *SampleList) void {
const render_width: i32 = @intFromFloat(@ceil(render_size.x));
const render_height: i32 = @intFromFloat(@ceil(render_size.y));
@ -423,7 +323,7 @@ pub fn drawCached(cache: *RenderCache, render_size: Vec2, options: ViewOptions,
const cache_key = RenderCache.Key{
.options = options,
.drawn_x_range = RangeF64.init(0, @max(@as(f64, @floatFromInt(samples.len)) - 1, 0)).intersectPositive(options.x_range)
.drawn_x_range = RangeF64.init(0, @max(@as(f64, @floatFromInt(sample_list.getLength())) - 1, 0)).intersectPositive(options.x_range)
};
if (cache.key != null and std.meta.eql(cache.key.?, cache_key)) {
@ -449,23 +349,19 @@ pub fn drawCached(cache: *RenderCache, render_size: Vec2, options: ViewOptions,
.width = render_size.x,
.height = render_size.y
};
drawSamples(draw_rect, options, samples, cache.min_max_cache);
drawSamples(draw_rect, options, sample_list);
}
pub fn draw(cache: ?*RenderCache, draw_rect: rl.Rectangle, options: ViewOptions, samples: []const f64) void {
pub fn draw(cache: ?*RenderCache, draw_rect: rl.Rectangle, options: ViewOptions, sample_list: *SampleList) void {
if (draw_rect.width < 0 or draw_rect.height < 0) {
return;
}
if (cache != null and !disable_caching) {
const c = cache.?;
drawCached(c, .{ .x = draw_rect.width, .y = draw_rect.height }, options, samples);
drawCached(c, .{ .x = draw_rect.width, .y = draw_rect.height }, options, sample_list);
c.draw(draw_rect);
} else {
drawSamples(draw_rect, options, samples, null);
drawSamples(draw_rect, options, sample_list, null);
}
}
test {
_ = MinMaxCache;
}

View File

@ -66,7 +66,7 @@ fn raylibTraceLogCallback(logType: c_int, text: [*c]const u8, args: raylib_h.va_
pub fn main() !void {
var gpa = std.heap.GeneralPurposeAllocator(.{
.thread_safe = true
.thread_safe = true,
}){};
const allocator = gpa.allocator();
defer _ = gpa.deinit();
@ -78,6 +78,8 @@ pub fn main() !void {
raylib_h.SetTraceLogCallback(raylibTraceLogCallback);
rl.setTraceLogLevel(toRaylibLogLevel(std.options.log_level));
// std.debug.print("{any}\n", .{ std.mem.bytesAsSlice(f64, std.mem.sliceAsBytes(&[_]f64{ 1, 2, 3}) ) });
const icon_png = @embedFile("./assets/icon.png");
var icon_image = rl.loadImageFromMemory(".png", icon_png);
defer icon_image.unload();
@ -183,11 +185,11 @@ pub fn main() !void {
rl.endDrawing();
}
}
test {
_ = @import("./ni-daq/root.zig");
_ = @import("./range.zig");
_ = @import("./graph.zig");
_ = Application;
}

View File

@ -28,7 +28,7 @@ frequency_input: UI.TextInputStorage,
amplitude_input: UI.TextInputStorage,
protocol_error_message: ?[]const u8 = null,
protocol_graph_cache: Graph.RenderCache = .{},
preview_samples: std.ArrayListUnmanaged(f64) = .{},
preview_sample_list_id: App.Id,
preview_samples_y_range: RangeF64 = RangeF64.init(0, 0),
// Project settings
@ -49,7 +49,8 @@ pub fn init(app: *App) !MainScreen {
.amplitude_input = UI.TextInputStorage.init(allocator),
.sample_rate_input = UI.TextInputStorage.init(allocator),
.view_controls = ViewControlsSystem.init(&app.project),
.sliding_window_input = UI.TextInputStorage.init(allocator)
.sliding_window_input = UI.TextInputStorage.init(allocator),
.preview_sample_list_id = try app.project.addSampleList(allocator)
};
try self.frequency_input.setText("10");
@ -59,12 +60,11 @@ pub fn init(app: *App) !MainScreen {
}
pub fn deinit(self: *MainScreen) void {
const allocator = self.app.allocator;
self.frequency_input.deinit();
self.amplitude_input.deinit();
self.sample_rate_input.deinit();
self.preview_samples.clearAndFree(allocator);
self.sliding_window_input.deinit();
self.app.project.removeSampleList(self.preview_sample_list_id);
self.clearProtocolErrorMessage();
}
@ -100,14 +100,15 @@ pub fn showProtocolModal(self: *MainScreen, view_id: Id) !void {
.borders = UI.Borders.all(.{ .color = srcery.hard_black, .size = 2 })
});
const samples = self.preview_samples.items;
const view_rect = Graph.ViewOptions{
.x_range = RangeF64.init(0, @floatFromInt(samples.len)),
.y_range = self.preview_samples_y_range
};
Graph.drawCached(&self.protocol_graph_cache, protocol_view.persistent.size, view_rect, samples);
if (self.protocol_graph_cache.texture) |texture| {
protocol_view.texture = texture.texture;
if (self.app.project.sample_lists.get(self.preview_sample_list_id)) |sample_list| {
const view_rect = Graph.ViewOptions{
.x_range = RangeF64.init(0, @floatFromInt(sample_list.getLength())),
.y_range = self.preview_samples_y_range
};
Graph.drawCached(&self.protocol_graph_cache, protocol_view.persistent.size, view_rect, sample_list);
if (self.protocol_graph_cache.texture) |texture| {
protocol_view.texture = texture.texture;
}
}
}
@ -182,9 +183,17 @@ pub fn showProtocolModal(self: *MainScreen, view_id: Id) !void {
}
if (self.protocol_error_message == null and any_input_modified) {
try App.Channel.generateSine(&self.preview_samples, allocator, sample_rate, frequency, amplitude);
self.preview_samples_y_range = RangeF64.init(-amplitude*1.1, amplitude*1.1);
self.protocol_graph_cache.invalidate();
if (self.app.project.sample_lists.get(self.preview_sample_list_id)) |sample_list| {
var preview_samples: std.ArrayListUnmanaged(f64) = .{};
defer preview_samples.deinit(allocator);
try App.Channel.generateSine(&preview_samples, allocator, sample_rate, frequency, amplitude);
sample_list.clear(allocator);
try sample_list.append(preview_samples.items);
self.preview_samples_y_range = RangeF64.init(-amplitude*1.1, amplitude*1.1);
self.protocol_graph_cache.invalidate();
}
}
if (self.protocol_error_message) |message| {
@ -319,7 +328,6 @@ fn showViewSettings(self: *MainScreen, view_id: Id) !void {
.label = "Sync controls"
});
const samples = project.getViewSamples(view_id);
switch (view.reference) {
.channel => |channel_id| {
@ -363,24 +371,27 @@ fn showViewSettings(self: *MainScreen, view_id: Id) !void {
self.app.pushCommand(.{ .reload_file = file_id });
}
}
}
_ = ui.label("Samples: {d}", .{ samples.len });
const sample_list_id = project.getViewSampleListId(view_id);
const sample_list = project.sample_lists.get(sample_list_id).?;
const sample_count = sample_list.getLength();
_ = ui.label("Samples: {d}", .{ sample_count });
var duration_str: []const u8 = "-";
if (sample_rate != null) {
const duration = @as(f64, @floatFromInt(samples.len)) / sample_rate.?;
const duration = @as(f64, @floatFromInt(sample_count)) / sample_rate.?;
if (utils.formatDuration(ui.frameAllocator(), duration)) |str| {
duration_str = str;
} else |_| {}
}
_ = ui.label("Duration: {s}", .{ duration_str });
_ = try ui.numberInput(f64, .{
try project.updateSlidingWindow(view_id, self.app.allocator, try ui.numberInput(f64, .{
.key = ui.keyFromString("Sliding window"),
.storage = &self.sliding_window_input
});
}));
}
fn showMarkedRange(self: *MainScreen, view_id: Id, index: usize) void {