add gain change live updates

This commit is contained in:
Rokas Puzonas 2025-05-26 14:50:24 +03:00
parent eafafbab89
commit 0edeaefbf0
8 changed files with 753 additions and 701 deletions

View File

@ -194,93 +194,125 @@ const SumRingBuffer = struct {
}
};
pub const Transform = union(enum) {
sliding_window: f64,
multiply: f64,
addition: f64,
pub const State = struct {
sum_ring_buffer: ?SumRingBuffer = null,
pub fn init(arena: Allocator, transform: Transform) !State {
var sum_ring_buffer: ?SumRingBuffer = null;
if (transform == .sliding_window) {
const window_width: usize = @intFromFloat(@ceil(transform.sliding_window));
sum_ring_buffer = SumRingBuffer.init(try arena.alloc(f64, window_width));
}
return State{
.sum_ring_buffer = sum_ring_buffer
};
}
pub fn prepare(self: *State, transform: Transform, source: *SampleList, from_block_index: SampleList.Block.Id) void {
switch (transform) {
.sliding_window => |window_width_f64| {
const sum_ring_buffer = &(self.sum_ring_buffer.?);
const window_width: usize = @intFromFloat(@ceil(window_width_f64));
const window_width_blocks: usize = @intFromFloat(@ceil(@as(f64, @floatFromInt(window_width)) / SampleList.Block.capacity));
for (0..window_width_blocks) |block_offset| {
const block_id = @as(isize, @intCast(from_block_index)) - @as(isize, @intCast(window_width_blocks - block_offset));
if (block_id < 0) {
continue;
}
const source_block = source.getBlock(@intCast(block_id)).?;
for (source_block.samplesSlice()) |sample| {
sum_ring_buffer.append(sample);
}
}
},
else => {}
}
}
pub const Transform = struct {
const max_gain_changes = 32;
pub const GainChanges = std.BoundedArray(GainChange, max_gain_changes);
pub const GainChange = struct {
gain: f64,
sample: f64
};
pub fn apply(self: Transform, state: *State, destination: []f64, source: []f64) void {
switch (self) {
.sliding_window => {
const sum_ring_buffer = &(state.sum_ring_buffer.?);
gain_changes: GainChanges = .{},
if (sum_ring_buffer.len == 0) {
@memcpy(destination[0..source.len], source);
} else {
for (0..source.len) |i| {
const sample = source[i];
sum_ring_buffer.append(sample);
pub fn applySlice(self: Transform, index: usize, source: []f64, destination: []f64) void {
assert(source.len == destination.len);
assert(source.len <= SampleList.Block.capacity);
destination[i] = sum_ring_buffer.sum / @as(f64, @floatFromInt(sum_ring_buffer.len));
}
}
const gain_changes = self.gain_changes.constSlice();
},
.multiply => |scalar| {
for (0..source.len) |i| {
destination[i] = scalar * source[i];
}
},
.addition => |offset| {
for (0..source.len) |i| {
destination[i] = offset + source[i];
}
const gain_segment = Transform.findGainSegment(gain_changes, index);
if (gain_segment.to > @as(f64, @floatFromInt(index+source.len))) {
for (0..source.len) |i| {
destination[i] = source[i] * (100 / gain_segment.gain);
}
} else {
const gain_per_sample = Transform.createGainLookup(gain_changes, index, source.len);
for (0..source.len) |i| {
destination[i] = source[i] * (100 / gain_per_sample[i]);
}
}
}
pub fn eqlSlice(slice1: []const Transform, slice2: []const Transform) bool {
if (slice1.len != slice2.len) {
return false;
pub fn applyBlock(self: Transform, index: usize, source: *SampleList.Block, destination: *SampleList.Block) void {
destination.clear();
destination.len = source.len;
self.applySlice(index, source.samplesSlice(), destination.samplesSlice());
destination.recomputeMinMax();
}
pub fn applySampleList(self: Transform, from_block_index: SampleList.Block.Id, block_count: usize, source: *SampleList, destination: *SampleList) void {
for (0..block_count) |block_offset| {
const block_id = from_block_index + block_offset;
const source_block = source.getBlock(block_id).?;
const destination_block = destination.getBlock(block_id).?;
self.applyBlock(block_id * SampleList.Block.capacity, source_block, destination_block);
}
for (0.., slice1) |i, transform1| {
if (!std.meta.eql(transform1, slice2[i])) {
return false;
}
const GainSegment = struct {
index: usize,
from: f64,
to: f64,
gain: f64
};
pub fn findGainSegment(changes: []const GainChange, index: usize) GainSegment {
// TODO:
// Assumes that `changes` is sorted
assert(changes.len > 0);
assert(changes[0].sample == 0);
const index_f64: f64 = @floatFromInt(index);
for (0..(changes.len-1)) |i| {
if (changes[i].sample <= index_f64 and index_f64 <= changes[i+1].sample) {
return GainSegment{
.index = i,
.from = changes[i].sample,
.to = changes[i+1].sample,
.gain = changes[i].gain
};
}
}
return true;
return GainSegment{
.index = changes.len-1,
.from = changes[changes.len-1].sample,
.to = std.math.inf(f64),
.gain = changes[changes.len-1].gain
};
}
pub fn createGainLookup(changes: []const GainChange, index: usize, size: usize) [SampleList.Block.capacity]f64 {
assert(size <= SampleList.Block.capacity);
var result: [SampleList.Block.capacity]f64 = undefined;
var last_result_index: usize = 0;
const from_index = findGainSegment(changes, index).index;
for (from_index..changes.len-1) |i| {
const next_segment_index = @min(@as(usize, @intFromFloat(changes[i+1].sample)) - index, result.len);
@memset(
result[last_result_index..next_segment_index],
changes[i].gain
);
last_result_index = next_segment_index;
if (next_segment_index == result.len) {
break;
}
}
if (last_result_index != result.len) {
@memset(
result[last_result_index..],
changes[changes.len-1].gain
);
}
return result;
}
pub fn eql(self: Transform, other: Transform) bool {
_ = self;
_ = other;
return false;
}
};
@ -461,6 +493,10 @@ pub const SampleList = struct {
block.clear();
}
try self.ensureTotalBlocks(total_block_count);
}
pub fn ensureTotalBlocks(self: *SampleList, total_block_count: usize) !void {
while (self.blocks.items.len < total_block_count) {
try self.appendEmptyBlock();
}
@ -541,60 +577,8 @@ pub const SampleList = struct {
}
}
pub fn applySlidingWindow(self: *SampleList, source: *SampleList, from_block_index: Block.Id, block_count: Block.Len, window_width: usize) !void {
try self.applyTransformations(source, from_block_index, block_count, &[_]Transform{ .{ .sliding_window = window_width } });
}
pub fn applyTransformations(self: *SampleList, source: *SampleList, from_block_index: Block.Id, block_count: Block.Len, transforms: []const Transform) !void {
var arena = std.heap.ArenaAllocator.init(self.arena.child_allocator);
defer arena.deinit();
const allocator = arena.allocator();
var states = try allocator.alloc(Transform.State, transforms.len);
for (0..transforms.len) |i| {
states[i] = try Transform.State.init(allocator, transforms[i]);
}
for (0..transforms.len) |i| {
states[i].prepare(transforms[i], source, from_block_index);
}
for (0..block_count) |block_offset| {
const block_id = from_block_index + block_offset;
const source_block = source.getBlock(block_id).?;
const self_block = self.getBlock(block_id).?;
self_block.clear();
var temp_buffer1: SampleList.Block.Buffer = undefined;
var temp_buffer2: SampleList.Block.Buffer = undefined;
var front_buffer: []f64 = temp_buffer1[0..source_block.len];
var back_buffer: []f64 = temp_buffer2[0..source_block.len];
for (0..transforms.len) |i| {
var source_samples: []f64 = undefined;
if (i == 0) {
source_samples = source_block.samplesSlice();
} else {
source_samples = back_buffer;
}
var destination_samples: []f64 = undefined;
if (i == transforms.len - 1) {
destination_samples = self_block.buffer[0..];
} else {
destination_samples = front_buffer;
}
transforms[i].apply(&states[i], destination_samples, source_samples);
std.mem.swap([]f64, &front_buffer, &back_buffer);
}
self_block.len = source_block.len;
self_block.recomputeMinMax();
}
pub fn apply(self: *SampleList, source: *SampleList, from_block_index: Block.Id, block_count: usize, transform: Transform) void {
transform.applySampleList(from_block_index, block_count, source, self);
}
test {
@ -750,99 +734,6 @@ pub const File = struct {
};
pub const View = struct {
pub const max_markers = 32;
pub const max_transforms = 16;
pub const BoundedTransformsArray = std.BoundedArray(Transform, max_transforms);
pub const MarkedRange = struct {
// Persistent
axis: UI.Axis,
range: RangeF64,
// Runtime
min: ?f64 = null,
max: ?f64 = null,
average: ?f64 = null,
standard_deviation: ?f64 = null,
pub fn clear(self: *MarkedRange) void {
self.min = null;
self.max = null;
self.average = null;
self.standard_deviation = null;
}
pub fn refresh(self: *MarkedRange, sample_list: *SampleList) void {
self.clear();
if (self.axis == .X) {
var sample_count: f64 = 0;
var sum: f64 = 0;
var min = std.math.inf(f64);
var max = -std.math.inf(f64);
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 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;
self.max = max;
self.average = average;
if (sample_count > 1) {
var standard_deviation: f64 = 0;
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);
self.standard_deviation = standard_deviation;
}
}
}
}
};
const MarkedRangeIterator = struct {
view: *View,
axis: UI.Axis,
index: usize = 0,
next_index: usize = 0,
pub fn next(self: *MarkedRangeIterator) ?RangeF64 {
const marked_ranges = self.view.marked_ranges.constSlice();
while (self.next_index < marked_ranges.len) {
self.index = self.next_index;
const marked_range = marked_ranges[self.index];
self.next_index += 1;
if (marked_range.axis == self.axis) {
return marked_range.range;
}
}
return null;
}
};
pub const Reference = union(enum) {
file: Id,
channel: Id
@ -854,9 +745,6 @@ pub const View = struct {
// TODO: Implement different styles of following: Look ahead, sliding, sliding window
follow: bool = false,
graph_opts: Graph.ViewOptions = .{},
marked_ranges: std.BoundedArray(MarkedRange, 32) = .{},
markers: std.BoundedArray(f64, max_markers) = .{},
transforms: BoundedTransformsArray = .{},
name: std.BoundedArray(u8, 128) = .{},
// Runtime
@ -865,8 +753,9 @@ pub const View = struct {
available_y_range: RangeF64 = RangeF64.init(0, 0),
unit: ?NIDaq.Unit = .Voltage,
transformed_samples: ?Id = null,
computed_transforms: BoundedTransformsArray = .{},
transformed_samples: Id,
computed_transform: Transform = .{},
invalidated_transform_ranges: std.BoundedArray(RangeF64, 16) = .{},
pub fn clear(self: *View) void {
self.graph_cache.clear();
@ -887,13 +776,6 @@ pub const View = struct {
.Y => self.available_y_range
};
}
pub fn iterMarkedRanges(self: *View, axis: UI.Axis) MarkedRangeIterator {
return MarkedRangeIterator{
.view = self,
.axis = axis
};
}
};
pub const Project = struct {
@ -913,6 +795,7 @@ pub const Project = struct {
export_location: ?[]u8 = null,
solutions: std.ArrayListUnmanaged(Solution) = .{},
gain_changes: Transform.GainChanges = .{},
sample_lists: GenerationalArray(SampleList) = .{},
channels: GenerationalArray(Channel) = .{},
@ -951,7 +834,7 @@ pub const Project = struct {
return result_range;
}
pub fn getViewSampleListId(self: *Project, view_id: Id) Id {
pub fn getViewReferenceSampleListId(self: *Project, view_id: Id) Id {
const view = self.views.get(view_id).?;
switch (view.reference) {
@ -976,10 +859,9 @@ pub const Project = struct {
@floatFromInt(sample_list.getLength()),
@floatFromInt(sample_list.getLength() + samples.len)
);
_ = affected_range;
try sample_list.append(samples);
self.refreshMarkedRanges(sample_list_id, affected_range);
}
pub fn clearSamples(self: *Project, allocator: Allocator, sample_list_id: Id) !void {
@ -989,10 +871,10 @@ pub const Project = struct {
0,
@floatFromInt(sample_list.getLength())
);
_ = affected_range;
sample_list.clear(allocator);
self.refreshMarkedRanges(sample_list_id, affected_range);
}
pub fn addSampleList(self: *Project, allocator: Allocator) !Id {
@ -1005,32 +887,6 @@ pub const Project = struct {
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).?;
_ = 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);
// }
// }
// }
}
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;
@ -1045,27 +901,6 @@ pub const Project = struct {
}
}
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 getSampleTimestamp(self: *Project) ?u64 {
var channel_iter = self.channels.iterator();
while (channel_iter.next()) |channel| {
@ -1108,9 +943,7 @@ pub const Project = struct {
pub fn removeView(self: *Project, allocator: std.mem.Allocator, view_id: Id) void {
const view = self.views.get(view_id) orelse return;
if (view.transformed_samples) |sample_list_id| {
self.removeSampleList(sample_list_id);
}
self.removeSampleList(view.transformed_samples);
switch (view.reference) {
.file => |file_id| {
@ -1125,6 +958,13 @@ pub const Project = struct {
view.* = undefined;
}
pub fn init(self: *Project, allocator: std.mem.Allocator) !void {
self.* = Project{};
_ = allocator;
try self.gain_changes.append(.{ .gain = 100, .sample = 0 });
}
pub fn deinit(self: *Project, allocator: Allocator) void {
var file_iter = self.files.iterator();
while (file_iter.next()) |file| {
@ -1146,37 +986,50 @@ pub const Project = struct {
}
self.sample_lists.clear();
if (self.solutions.capacity > 0) {
for (self.solutions.items) |solution| {
allocator.free(solution.description);
}
self.solutions.deinit(allocator);
for (self.solutions.items) |solution| {
allocator.free(solution.description);
}
self.solutions.deinit(allocator);
if (self.pipete_solution.capacity > 0) {
self.pipete_solution.deinit(allocator);
}
if (self.notes.capacity > 0) {
self.notes.deinit(allocator);
}
self.pipete_solution.deinit(allocator);
self.notes.deinit(allocator);
if (self.export_location) |export_location| {
allocator.free(export_location);
}
if (self.statistic_points.items.len > 0) {
self.statistic_points.deinit(allocator);
self.statistic_points.deinit(allocator);
self.experiment_name.deinit(allocator);
}
pub fn getViewTransform(self: *Project, view_id: Id) Transform {
_ = view_id;
return Transform{
.gain_changes = self.gain_changes
};
}
pub fn removeGainChange(self: *Project, index: usize) void {
const gain_change = self.gain_changes.orderedRemove(index);
const invalidate_lower_bound = gain_change.sample;
var invalidate_upper_bound = std.math.inf(f64);
if (index < self.gain_changes.len) {
invalidate_upper_bound = self.gain_changes.get(index).sample;
}
self.experiment_name.deinit(allocator);
const invalidate_range = RangeF64.init(invalidate_lower_bound, invalidate_upper_bound);
var view_iter = self.views.iterator();
while (view_iter.next()) |view| {
view.invalidated_transform_ranges.append(invalidate_range) catch continue;
}
}
// ------------------- Serialization ------------------ //
pub fn initFromFile(self: *Project, allocator: Allocator, f: std.fs.File) !void {
self.* = .{};
try Project.init(self, allocator);
errdefer self.deinit(allocator);
const reader = f.reader();
@ -1269,9 +1122,13 @@ pub const Project = struct {
return error.InvalidReferenceTag;
}
const sample_list_id = try self.addSampleList(allocator);
errdefer self.removeSampleList(sample_list_id);
const view = self.views.get(id).?;
view.* = View{
.reference = reference,
.transformed_samples = sample_list_id
};
}
}
@ -1439,7 +1296,7 @@ const WorkJob = struct {
view_id: Id,
stage: Stage = .init,
transforms: View.BoundedTransformsArray = .{},
transform: Transform = .{},
mutex: std.Thread.Mutex = .{},
running_thread_jobs: std.ArrayListUnmanaged(SampleList.Block.Id) = .{},
@ -1488,33 +1345,20 @@ const WorkJob = struct {
const project = &app.project;
const view = project.views.get(self.view_id) orelse return true;
const sample_list_id = project.getViewSampleListId(self.view_id);
const sample_list_id = project.getViewReferenceSampleListId(self.view_id);
const sample_list = project.sample_lists.get(sample_list_id).?;
const transforms = self.transforms.constSlice();
if (!Transform.eqlSlice(view.transforms.constSlice(), transforms)) {
if (!Transform.eql(project.getViewTransform(self.view_id), self.transform)) {
// Transforms changed, job needs to be cancelled.
return true;
}
switch (self.stage) {
.init => {
if (transforms.len == 0) {
if (view.transformed_samples) |transformed_samples_id| {
project.removeSampleList(transformed_samples_id);
}
view.transformed_samples = null;
const transformed_samples = project.sample_lists.get(view.transformed_samples).?;
try transformed_samples.reserveEmptyBlocks(sample_list.blocks.items.len);
return true;
} else {
if (view.transformed_samples == null) {
view.transformed_samples = try project.addSampleList(app.allocator);
}
const transformed_samples = project.sample_lists.get(view.transformed_samples.?).?;
try transformed_samples.reserveEmptyBlocks(sample_list.blocks.items.len);
self.stage = .launch_threads;
}
self.stage = .launch_threads;
},
.launch_threads => {
const max_block_to_process = 256;
@ -1548,15 +1392,12 @@ const WorkJob = struct {
// }
const view = project.views.get(self.view_id) orelse return;
const transformed_samples_id = view.transformed_samples orelse return;
const transformed_samples = project.sample_lists.get(transformed_samples_id) orelse return;
const transformed_samples = project.sample_lists.get(view.transformed_samples) orelse return;
const sample_list_id = project.getViewSampleListId(self.view_id);
const sample_list_id = project.getViewReferenceSampleListId(self.view_id);
const sample_list = project.sample_lists.get(sample_list_id) orelse return;
transformed_samples.applyTransformations(sample_list, from_block_id, block_count, self.transforms.constSlice()) catch |e| {
log.err("Failed to compute sliding window: {}", .{e});
};
transformed_samples.apply(sample_list, from_block_id, block_count, self.transform);
}
};
@ -1572,7 +1413,7 @@ screen: enum {
} = .main,
main_screen: MainScreen,
channel_from_device: ChannelFromDeviceScreen,
project: Project = .{},
project: Project,
collection_mutex: std.Thread.Mutex = .{},
collection_samples_mutex: std.Thread.Mutex = .{},
@ -1594,8 +1435,10 @@ pub fn init(self: *App, allocator: Allocator) !void {
.main_screen = undefined,
.collection_thread = undefined,
.channel_from_device = undefined,
.work_thread_pool = undefined
.work_thread_pool = undefined,
.project = undefined
};
try Project.init(&self.project, allocator);
try self.initUI();
try self.work_thread_pool.init(.{
.allocator = allocator
@ -1689,9 +1532,8 @@ pub fn loadProject(self: *App, file: std.fs.File) !void {
var loaded = try self.allocator.create(Project);
defer self.allocator.destroy(loaded);
loaded.* = .{};
errdefer loaded.deinit(self.allocator);
try loaded.initFromFile(self.allocator, file);
errdefer loaded.deinit(self.allocator);
self.deinitUI();
self.deinitProject();
@ -1889,6 +1731,40 @@ pub fn tick(self: *App) !void {
}
try self.showUI();
{
const block_size = SampleList.Block.capacity;
var view_iter = self.project.views.idIterator();
while (view_iter.next()) |view_id| {
const view = self.getView(view_id).?;
const referenced_samples_id = self.project.getViewReferenceSampleListId(view_id);
const referenced_samples = self.project.sample_lists.get(referenced_samples_id).?;
const computed_samples_id = view.transformed_samples;
const computed_samples = self.project.sample_lists.get(computed_samples_id).?;
const transform = self.project.getViewTransform(view_id);
const computed_len = computed_samples.getLength();
const reference_len = referenced_samples.getLength();
if (reference_len > computed_len) {
view.invalidated_transform_ranges.append(RangeF64.init(@floatFromInt(computed_len), @floatFromInt(reference_len))) catch {};
}
for (view.invalidated_transform_ranges.constSlice()) |range| {
const from_block: usize = @intFromFloat(@divFloor(range.lower, block_size));
const to_block: usize = @intFromFloat(@divFloor(@min(range.upper, @as(f64, @floatFromInt(reference_len)) - 1), block_size));
const block_count = to_block - from_block + 1;
try computed_samples.ensureTotalBlocks(from_block + block_count);
transform.applySampleList(@intCast(from_block), block_count, referenced_samples, computed_samples);
view.graph_cache.invalidateRange(range);
}
view.invalidated_transform_ranges.len = 0;
}
}
}
rl.clearBackground(srcery.black);
@ -1957,18 +1833,20 @@ pub fn tick(self: *App) !void {
var view_iter = self.project.views.idIterator();
while (view_iter.next()) |view_id| {
const view = self.project.views.get(view_id).?;
_ = view;
if (Transform.eqlSlice(view.computed_transforms.constSlice(), view.transforms.constSlice())) {
continue;
}
// TODO:
// if (Transform.eqlSlice(view.computed_transforms.constSlice(), view.transforms.constSlice())) {
// continue;
// }
_ = try self.work_jobs.insert(WorkJob{
.transforms = view.transforms,
.view_id = view_id
});
// _ = try self.work_jobs.insert(WorkJob{
// .transforms = view.transforms,
// .view_id = view_id
// });
view.computed_transforms.len = 0;
view.computed_transforms.appendSliceAssumeCapacity(view.transforms.constSlice());
// view.computed_transforms.len = 0;
// view.computed_transforms.appendSliceAssumeCapacity(view.transforms.constSlice());
}
}
@ -2087,7 +1965,7 @@ fn startCollection(self: *App) !void {
self.collection_condition.signal();
}
fn stopCollection(self: *App) void {
pub fn stopCollection(self: *App) void {
if (!self.isCollectionInProgress()) {
return;
}
@ -2140,7 +2018,7 @@ fn startOutput(self: *App, channel_id: Id) !void {
channel.output_task = task;
}
fn stopOutput(self: *App, channel_id: Id) void {
pub fn stopOutput(self: *App, channel_id: Id) void {
const channel = self.getChannel(channel_id) orelse return;
if (channel.output_task == null) {
@ -2167,6 +2045,13 @@ pub fn isOutputingInProgress(self: *App) bool {
return false;
}
pub fn stopAllOutput(self: *App) void {
var channel_iter = self.project.channels.idIterator();
while (channel_iter.next()) |channel_id| {
self.stopOutput(channel_id);
}
}
fn isNiDaqInUse(self: *App) bool {
return self.isCollectionInProgress() or self.isOutputingInProgress();
}
@ -2371,9 +2256,13 @@ pub fn addView(self: *App, reference: View.Reference) !Id {
const id = try self.project.views.insertUndefined();
errdefer self.project.views.remove(id);
const sample_list_id = try self.project.addSampleList(self.allocator);
errdefer self.project.removeSampleList(sample_list_id);
const view = self.project.views.get(id).?;
view.* = View{
.reference = reference
.reference = reference,
.transformed_samples = sample_list_id
};
self.loadView(id) catch |e| {
@ -2385,7 +2274,7 @@ pub fn addView(self: *App, reference: View.Reference) !Id {
fn refreshViewAvailableXYRanges(self: *App, id: Id) void {
const view = self.getView(id) orelse return;
const sample_list_id = self.project.getViewSampleListId(id);
const sample_list_id = self.project.getViewReferenceSampleListId(id);
const sample_list = self.project.sample_lists.get(sample_list_id).?;
view.available_x_range = RangeF64.init(0, @floatFromInt(sample_list.getLength()));

View File

@ -153,6 +153,7 @@ const MarkedRangeIterator = struct {
}
};
app: *App,
project: *App.Project,
// TODO: Redo
@ -160,21 +161,13 @@ undo_stack: CommandFrameArray = .{},
last_applied_command: usize = 0,
zoom_start: ?ViewAxisPosition = null,
cursor: ?ViewAxisPosition = null,
view_settings: ?Id = null, // View id
view_protocol_modal: ?Id = null, // View id
selected_tool: enum { move, select, marker } = .move,
show_marked_range: ?struct {
view_id: Id,
index: usize,
} = null,
show_marker: ?struct {
view_id: Id,
index: usize
} = null,
// selected_tool: enum { move, select, marker } = .move,
pub fn init(project: *App.Project) System {
pub fn init(app: *App) System {
return System{
.project = project
.app = app,
.project = &app.project
};
}
@ -293,14 +286,15 @@ pub fn applyCommands(self: *System) void {
pub fn toggleViewSettings(self: *System, view_id: Id) void {
if (self.isViewSettingsOpen(view_id)) {
self.view_settings = null;
self.app.main_screen.side_panel = .project;
} else {
self.view_settings = view_id;
self.app.main_screen.side_panel = .{ .view_settings = view_id };
}
}
pub fn isViewSettingsOpen(self: *System, view_id: Id) bool {
return self.view_settings != null and self.view_settings.?.eql(view_id);
const side_panel = self.app.main_screen.side_panel;
return side_panel == .view_settings and side_panel.view_settings.eql(view_id);
}
pub fn setCursor(self: *System, view_id: Id, axis: UI.Axis, position: ?f64) void {
@ -320,29 +314,36 @@ pub fn getCursorHoldStart(self: *System, view_id: Id, axis: UI.Axis) ?f64 {
}
pub fn toggleShownMarkedRange(self: *System, view_id: Id, index: usize) void {
if (self.show_marked_range) |show_marked_range| {
const side_panel = &self.app.main_screen.side_panel;
if (side_panel.* == .marked_range) {
const show_marked_range = side_panel.marked_range;
if (show_marked_range.view_id.eql(view_id) and show_marked_range.index == index) {
self.show_marked_range = null;
side_panel.* = .project;
return;
}
}
self.show_marked_range = .{
.view_id = view_id,
.index = index,
side_panel.* = .{
.marked_range = .{ .index = index, .view_id = view_id }
};
}
pub fn toggleShownMarker(self: *System, view_id: Id, index: usize) void {
if (self.show_marker) |show_marker| {
const side_panel = &self.app.main_screen.side_panel;
if (side_panel.* == .marker) {
const show_marker = side_panel.marker;
if (show_marker.view_id.eql(view_id) and show_marker.index == index) {
self.show_marker = null;
side_panel.* = .project;
return;
}
}
self.show_marker = .{
.view_id = view_id,
.index = index,
side_panel.* = .{
.marker = .{
.view_id = view_id,
.index = index,
}
};
}

View File

@ -73,7 +73,7 @@ fn showGraph(ctx: Context, graph_box: *UI.Box, view_id: Id) void {
sample_value_under_mouse = mouse_y_range.remapTo(view_opts.y_range, signal.relative_mouse.y);
}
if (ctx.view_controls.selected_tool == .move) {
// if (ctx.view_controls.selected_tool == .move) {
if (signal.dragged()) {
const x_offset = mouse_x_range.remapTo(RangeF64.init(0, view_opts.x_range.size()), signal.drag.x);
const y_offset = mouse_y_range.remapTo(RangeF64.init(0, view_opts.y_range.size()), signal.drag.y);
@ -107,21 +107,16 @@ fn showGraph(ctx: Context, graph_box: *UI.Box, view_id: Id) void {
if (signal.flags.contains(.left_released)) {
ctx.view_controls.pushBreakpoint();
}
} else if (ctx.view_controls.selected_tool == .select) {
// TODO:
// } else if (ctx.view_controls.selected_tool == .select) {
// // TODO:
} else if (ctx.view_controls.selected_tool == .marker) {
// TODO:
}
// } else if (ctx.view_controls.selected_tool == .marker) {
// // TODO:
// }
{ // Render graph
var sample_list_id = app.project.getViewSampleListId(view_id);
if (view.transformed_samples) |transformed_samples_id| {
sample_list_id = transformed_samples_id;
}
const sample_list = app.project.sample_lists.get(sample_list_id).?;
const sample_list = app.project.sample_lists.get(view.transformed_samples).?;
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

@ -329,54 +329,87 @@ pub fn show(ctx: Context, box: *UI.Box, graph_box: *UI.Box, view_id: Id, axis: U
_ = showMarkerRect(ui, ruler, RangeF64.init(hold_start.?, cursor.?), marker_color.alpha(0.5), null);
}
{
var selected_range_iter = view.iterMarkedRanges(axis);
while (selected_range_iter.next()) |selected_range| {
var color = srcery.blue;
const index = selected_range_iter.index;
// {
// var selected_range_iter = view.iterMarkedRanges(axis);
// while (selected_range_iter.next()) |selected_range| {
// var color = srcery.blue;
// const index = selected_range_iter.index;
if (ctx.view_controls.show_marked_range) |show_marked_range| {
if (show_marked_range.view_id.eql(view_id) and show_marked_range.index == index) {
if (@mod(rl.getTime(), 0.5) < 0.25) {
color = utils.shiftColorInHSV(color, 0.8);
}
}
}
// const side_panel = ctx.view_controls.app.main_screen.side_panel;
// if (side_panel == .marked_range) {
// const show_marked_range = side_panel.marked_range;
// if (show_marked_range.view_id.eql(view_id) and show_marked_range.index == index) {
// if (@mod(rl.getTime(), 0.5) < 0.25) {
// color = utils.shiftColorInHSV(color, 0.8);
// }
// }
// }
showMarkerLine(ui, ruler, selected_range.lower, color);
showMarkerLine(ui, ruler, selected_range.upper, color);
// showMarkerLine(ui, ruler, selected_range.lower, color);
// showMarkerLine(ui, ruler, selected_range.upper, color);
var hasher = UI.Key.CombineHasher.init();
hasher.update(std.mem.asBytes("Marked ranges"));
hasher.update(std.mem.asBytes(&view_id));
hasher.update(std.mem.asBytes(&axis));
hasher.update(std.mem.asBytes(&index));
const range_box_key = UI.Key.init(hasher.final());
// var hasher = UI.Key.CombineHasher.init();
// hasher.update(std.mem.asBytes("Marked ranges"));
// hasher.update(std.mem.asBytes(&view_id));
// hasher.update(std.mem.asBytes(&axis));
// hasher.update(std.mem.asBytes(&index));
// const range_box_key = UI.Key.init(hasher.final());
var range_box = showMarkerRect(ui, ruler, selected_range, color.alpha(0.5), range_box_key);
range_box.flags.insert(.clickable);
range_box.flags.insert(.draw_hot);
range_box.flags.insert(.draw_active);
// var range_box = showMarkerRect(ui, ruler, selected_range, color.alpha(0.5), range_box_key);
// range_box.flags.insert(.clickable);
// range_box.flags.insert(.draw_hot);
// range_box.flags.insert(.draw_active);
range_box.hot_cursor = .mouse_cursor_pointing_hand;
if (ctx.view_controls.selected_tool == .select) {
const signal = ui.signal(range_box);
if (signal.clicked()) {
ctx.view_controls.toggleShownMarkedRange(view_id, index);
}
}
}
}
// range_box.hot_cursor = .mouse_cursor_pointing_hand;
// if (ctx.view_controls.selected_tool == .select) {
// const signal = ui.signal(range_box);
// if (signal.clicked()) {
// ctx.view_controls.toggleShownMarkedRange(view_id, index);
// }
// }
// }
// }
if (axis == .X) {
for (0.., view.markers.constSlice()) |i, marker| {
const color = srcery.cyan;
// for (0.., view.markers.constSlice()) |i, marker| {
// const color = srcery.cyan;
showMarkerLine(ui, ruler, marker, color);
showMarkerLine(ui, ruler, marker, color);
// showMarkerLine(ui, ruler, marker, color);
// showMarkerLine(ui, ruler, marker, color);
// var hasher = UI.Key.CombineHasher.init();
// hasher.update(std.mem.asBytes("Markers"));
// hasher.update(std.mem.asBytes(&view_id));
// hasher.update(std.mem.asBytes(&axis));
// hasher.update(std.mem.asBytes(&i));
// const view_size = view.graph_opts.x_range.size();
// const clickable_width = view_size * 0.01;
// const clickable = ui.createBox(.{
// .key = UI.Key.init(hasher.final()),
// .float_rect = ruler.getGraphDrawContext().getRect(marker - clickable_width/2, clickable_width, 0, 1),
// .float_relative_to = ruler.graph_box,
// .parent = ruler.graph_box,
// .flags = &.{ .draw_hot, .draw_active, .clickable },
// .hot_cursor = .mouse_cursor_pointing_hand,
// });
// if (ui.signal(clickable).clicked()) {
// ctx.view_controls.toggleShownMarker(view_id, i);
// }
// }
for (0.., project.gain_changes.slice()) |i, gain_change| {
const color = srcery.bright_orange;
const sample = gain_change.sample;
showMarkerLine(ui, ruler, sample, color);
showMarkerLine(ui, ruler, sample, color);
var hasher = UI.Key.CombineHasher.init();
hasher.update(std.mem.asBytes("Markers"));
hasher.update(std.mem.asBytes("Gain change"));
hasher.update(std.mem.asBytes(&view_id));
hasher.update(std.mem.asBytes(&axis));
hasher.update(std.mem.asBytes(&i));
@ -386,15 +419,20 @@ pub fn show(ctx: Context, box: *UI.Box, graph_box: *UI.Box, view_id: Id, axis: U
const clickable = ui.createBox(.{
.key = UI.Key.init(hasher.final()),
.float_rect = ruler.getGraphDrawContext().getRect(marker - clickable_width/2, clickable_width, 0, 1),
.float_rect = ruler.getGraphDrawContext().getRect(sample - clickable_width/2, clickable_width, 0, 1),
.float_relative_to = ruler.graph_box,
.parent = ruler.graph_box,
.flags = &.{ .draw_hot, .draw_active, .clickable },
.hot_cursor = .mouse_cursor_pointing_hand,
});
if (ui.signal(clickable).clicked()) {
ctx.view_controls.toggleShownMarker(view_id, i);
const signal = ui.signal(clickable);
if (signal.hot) {
const mouse_tooltip = ui.mouseTooltip();
mouse_tooltip.beginChildren();
defer mouse_tooltip.endChildren();
_ = ui.label("Gain: {d:.3}", .{ gain_change.gain });
}
}
}
@ -427,7 +465,7 @@ pub fn show(ctx: Context, box: *UI.Box, graph_box: *UI.Box, view_id: Id, axis: U
ctx.view_controls.setCursorHoldStart(view_id, axis, cursor);
}
if (ctx.view_controls.selected_tool == .move) {
// if (ctx.view_controls.selected_tool == .move) {
if (signal.scrolled() and cursor != null) {
var scale_factor: f64 = 1;
if (signal.scroll.y > 0) {
@ -462,27 +500,27 @@ 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) {
// } else if (ctx.view_controls.selected_tool == .select) {
if (cursor != null) {
if (ctx.view_controls.getCursorHoldStart(view_id, axis)) |hold_start| {
const range = RangeF64.init(
@min(hold_start, cursor.?),
@max(hold_start, cursor.?),
);
const hold_start_mouse = view_range.remapTo(mouse_range, range.lower);
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) {
_ = ctx.project.appendMarkedRange(view_id, axis, range);
}
}
}
} else if (ctx.view_controls.selected_tool == .marker) {
if (cursor != null and signal.flags.contains(.left_released) and axis == .X) {
view.markers.append(cursor.?) catch {};
}
}
// if (cursor != null) {
// if (ctx.view_controls.getCursorHoldStart(view_id, axis)) |hold_start| {
// const range = RangeF64.init(
// @min(hold_start, cursor.?),
// @max(hold_start, cursor.?),
// );
// const hold_start_mouse = view_range.remapTo(mouse_range, range.lower);
// 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) {
// _ = ctx.project.appendMarkedRange(view_id, axis, range);
// }
// }
// }
// } else if (ctx.view_controls.selected_tool == .marker) {
// if (cursor != null and signal.flags.contains(.left_released) and axis == .X) {
// view.markers.append(cursor.?) catch {};
// }
// }
if (signal.flags.contains(.left_released)) {
ctx.view_controls.setCursorHoldStart(view_id, axis, null);

View File

@ -30,7 +30,7 @@ pub const ViewOptions = struct {
pub const RenderCache = struct {
const Key = struct {
options: ViewOptions,
drawn_x_range: RangeF64
drawn_x_range: RangeF64,
};
texture: ?rl.RenderTexture2D = null,
@ -48,6 +48,14 @@ pub const RenderCache = struct {
self.key = null;
}
pub fn invalidateRange(self: *RenderCache, x_range: RangeF64) void {
if (self.key) |key| {
if (key.drawn_x_range.intersectPositive(x_range).isPositive()) {
self.invalidate();
}
}
}
pub fn draw(self: RenderCache, rect: rl.Rectangle) void {
if (self.texture) |texture| {
const source = rl.Rectangle{
@ -323,7 +331,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(sample_list.getLength())) - 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)) {

View File

@ -119,12 +119,12 @@ pub fn main() !void {
if (app_config_dir.openFile("config.bin", .{})) |save_file| {
defer save_file.close();
_ = try app.addView(.{
.file = try app.addFile("./samples/HeLa Cx37_ 40nM GFX + 35uM Propofol_18-Sep-2024_0003_I.bin")
});
// app.loadProject(save_file) catch |e| {
// log.err("Failed to load project: {}", .{e});
// };
app.loadProject(save_file) catch |e| {
log.err("Failed to load project: {}", .{e});
if (@errorReturnTrace()) |stack_trace| {
std.debug.dumpStackTrace(stack_trace.*);
}
};
} else |e| switch (e) {
error.FileNotFound => {},
else => return e
@ -178,6 +178,13 @@ pub fn main() !void {
}
{
if (app.isCollectionInProgress()) {
app.stopCollection();
}
if (app.isOutputingInProgress()) {
app.stopAllOutput();
}
const save_file = try app_config_dir.createFile("config.bin", .{});
defer save_file.close();
try app.saveProject(save_file);

View File

@ -22,8 +22,22 @@ const Id = App.Id;
app: *App,
view_controls: ViewControlsSystem,
view_solutions: bool = false,
view_statistics_points: bool = false,
side_panel: union(enum) {
project,
solutions,
gain_changes,
statistics_points,
view_settings: Id,
// marker: struct {
// view_id: Id,
// index: usize
// },
// marked_range: struct {
// view_id: Id,
// index: usize,
// }
} = .project,
modal: ?union(enum){
view_protocol: Id, // View id
notes
@ -42,6 +56,7 @@ notes_storage: UI.TextInputStorage,
// Project settings
solution_input: UI.TextInputStorage,
gain_input: UI.TextInputStorage,
sample_rate_input: UI.TextInputStorage,
experiment_name: UI.TextInputStorage,
pipete_solution: UI.TextInputStorage,
@ -50,7 +65,6 @@ parsed_sample_rate: ?f64 = null,
// View settings
prev_view_settings: ?Id = null, // View ID
transform_inputs: [App.View.max_transforms]UI.TextInputStorage,
view_name_input: UI.TextInputStorage,
channel_save_file_picker: ?Platform.FilePickerId = null,
file_save_file_picker: ?Platform.FilePickerId = null,
@ -58,11 +72,6 @@ file_save_file_picker: ?Platform.FilePickerId = null,
pub fn init(app: *App) !MainScreen {
const allocator = app.allocator;
var transform_inputs: [App.View.max_transforms]UI.TextInputStorage = undefined;
for (&transform_inputs) |*input| {
input.* = UI.TextInputStorage.init(allocator);
}
var self = MainScreen{
.app = app,
.frequency_input = UI.TextInputStorage.init(allocator),
@ -73,8 +82,8 @@ pub fn init(app: *App) !MainScreen {
.experiment_name = UI.TextInputStorage.init(allocator),
.pipete_solution = UI.TextInputStorage.init(allocator),
.view_name_input = UI.TextInputStorage.init(allocator),
.view_controls = ViewControlsSystem.init(&app.project),
.transform_inputs = transform_inputs,
.gain_input = UI.TextInputStorage.init(allocator),
.view_controls = ViewControlsSystem.init(app),
.preview_sample_list_id = try app.project.addSampleList(allocator)
};
@ -93,9 +102,7 @@ pub fn deinit(self: *MainScreen) void {
self.experiment_name.deinit();
self.pipete_solution.deinit();
self.view_name_input.deinit();
for (self.transform_inputs) |input| {
input.deinit();
}
self.gain_input.deinit();
self.app.project.removeSampleList(self.preview_sample_list_id);
self.clearProtocolErrorMessage();
@ -376,6 +383,7 @@ fn showProjectSettings(self: *MainScreen) !void {
{
_ = ui.label("Solution:", .{});
try ui.textInput(.{
.key = ui.keyFromString("Solution input"),
.storage = &self.solution_input,
@ -407,7 +415,7 @@ fn showProjectSettings(self: *MainScreen) !void {
const btn = ui.textButton("View solutions");
btn.borders = UI.Borders.all(.{ .color = srcery.blue, .size = 4 });
if (ui.signal(btn).clicked()) {
self.view_solutions = true;
self.side_panel = .solutions;
}
}
}
@ -416,7 +424,36 @@ fn showProjectSettings(self: *MainScreen) !void {
_ = ui.createBox(.{ .size_y = UI.Sizing.initFixedPixels(ui.rem(0.5)) });
{
{ // Gain
_ = ui.label("Gain:", .{});
const last_gain_change = project.gain_changes.buffer[project.gain_changes.len - 1];
const current_gain = try ui.numberInput(f64, .{
.key = ui.keyFromString("Gain input"),
.storage = &self.gain_input,
.initial = last_gain_change.gain,
.width = 400
});
if (!self.gain_input.editing and current_gain != null and current_gain.? != last_gain_change.gain) {
try project.gain_changes.append(.{
.gain = current_gain.?,
.sample = @floatFromInt(project.getSampleTimestamp() orelse 0)
});
}
{
const btn = ui.textButton("Show gain changes");
btn.borders = UI.Borders.all(.{ .color = srcery.blue, .size = 4 });
if (ui.signal(btn).clicked()) {
self.side_panel = .gain_changes;
}
}
}
_ = ui.createBox(.{ .size_y = UI.Sizing.initFixedPixels(ui.rem(0.5)) });
{ // Statistic points
const row = ui.createBox(.{
.size_x = UI.Sizing.initFitChildren(),
.size_y = UI.Sizing.initFitChildren(),
@ -439,7 +476,7 @@ fn showProjectSettings(self: *MainScreen) !void {
const btn = ui.textButton("View statistic points");
btn.borders = UI.Borders.all(.{ .color = srcery.blue, .size = 4 });
if (ui.signal(btn).clicked()) {
self.view_statistics_points = true;
self.side_panel = .statistics_points;
}
}
}
@ -475,7 +512,7 @@ fn showProjectSettings(self: *MainScreen) !void {
_ = ui.createBox(.{ .size_y = UI.Sizing.initFixedPixels(ui.rem(0.5)) });
{
{ // Open notes
const btn = ui.textButton("Open notes");
btn.borders = UI.Borders.all(.{ .color = srcery.blue, .size = 4 });
if (ui.signal(btn).clicked()) {
@ -485,7 +522,7 @@ fn showProjectSettings(self: *MainScreen) !void {
_ = ui.createBox(.{ .size_y = UI.Sizing.initFixedPixels(ui.rem(0.5)) });
{
{ // Export folder
_ = ui.label("Export folder:", .{});
if (ui.fileInput(.{
.allocator = self.app.allocator,
@ -557,6 +594,8 @@ fn showViewSettings(self: *MainScreen, view_id: Id) !void {
}
}
_ = ui.label("Statistics:", .{});
switch (view.reference) {
.channel => |channel_id| {
const channel = project.channels.get(channel_id).?;
@ -588,7 +627,7 @@ fn showViewSettings(self: *MainScreen, view_id: Id) !void {
}
}
const sample_list_id = project.getViewSampleListId(view_id);
const sample_list_id = project.getViewReferenceSampleListId(view_id);
const sample_list = project.sample_lists.get(sample_list_id).?;
const sample_count = sample_list.getLength();
@ -607,122 +646,122 @@ fn showViewSettings(self: *MainScreen, view_id: Id) !void {
_ = ui.label("Duration: {s}", .{ duration_str orelse "-" });
var deferred_remove: std.BoundedArray(usize, App.View.max_transforms) = .{};
// var deferred_remove: std.BoundedArray(usize, App.View.max_transforms) = .{};
for (0.., view.transforms.slice()) |i, *_transform| {
const transform: *App.Transform = _transform;
// for (0.., view.transforms.slice()) |i, *_transform| {
// const transform: *App.Transform = _transform;
const row = ui.createBox(.{
.key = UI.Key.initPtr(transform),
.size_x = UI.Sizing.initGrowFull(),
.size_y = UI.Sizing.initFixedPixels(ui.rem(1.5)),
.layout_direction = .left_to_right
});
row.beginChildren();
defer row.endChildren();
// const row = ui.createBox(.{
// .key = UI.Key.initPtr(transform),
// .size_x = UI.Sizing.initGrowFull(),
// .size_y = UI.Sizing.initFixedPixels(ui.rem(1.5)),
// .layout_direction = .left_to_right
// });
// row.beginChildren();
// defer row.endChildren();
if (ui.signal(ui.textButton("Remove")).clicked()) {
deferred_remove.appendAssumeCapacity(i);
}
// if (ui.signal(ui.textButton("Remove")).clicked()) {
// deferred_remove.appendAssumeCapacity(i);
// }
{
const options = .{
.{ .multiply, "Multiply" },
.{ .addition, "Addition" },
.{ .sliding_window, "Sliding window" },
};
// {
// const options = .{
// .{ .multiply, "Multiply" },
// .{ .addition, "Addition" },
// .{ .sliding_window, "Sliding window" },
// };
const select = ui.button(ui.keyFromString("Transform select"));
select.setFmtText("{s}", .{switch (transform.*) {
.sliding_window => "Sliding window",
.addition => "Addition",
.multiply => "Multiply"
}});
// const select = ui.button(ui.keyFromString("Transform select"));
// select.setFmtText("{s}", .{switch (transform.*) {
// .sliding_window => "Sliding window",
// .addition => "Addition",
// .multiply => "Multiply"
// }});
select.size.y = UI.Sizing.initGrowFull();
select.alignment.y = .center;
if (ui.signal(select).clicked()) {
select.persistent.open = !select.persistent.open;
}
// select.size.y = UI.Sizing.initGrowFull();
// select.alignment.y = .center;
// if (ui.signal(select).clicked()) {
// select.persistent.open = !select.persistent.open;
// }
if (select.persistent.open) {
const popup = ui.createBox(.{
.key = ui.keyFromString("Select popup"),
.size_x = UI.Sizing.initFixedPixels(ui.rem(10)),
.size_y = UI.Sizing.initFitChildren(),
.flags = &.{ .clickable, .scrollable },
.layout_direction = .top_to_bottom,
.float_relative_to = select,
.background = srcery.black,
.borders = UI.Borders.all(.{ .color = srcery.bright_black, .size = 4 }),
.draw_on_top = true
});
popup.setFloatPosition(0, select.persistent.size.y);
popup.beginChildren();
defer popup.endChildren();
// if (select.persistent.open) {
// const popup = ui.createBox(.{
// .key = ui.keyFromString("Select popup"),
// .size_x = UI.Sizing.initFixedPixels(ui.rem(10)),
// .size_y = UI.Sizing.initFitChildren(),
// .flags = &.{ .clickable, .scrollable },
// .layout_direction = .top_to_bottom,
// .float_relative_to = select,
// .background = srcery.black,
// .borders = UI.Borders.all(.{ .color = srcery.bright_black, .size = 4 }),
// .draw_on_top = true
// });
// popup.setFloatPosition(0, select.persistent.size.y);
// popup.beginChildren();
// defer popup.endChildren();
inline for (options) |option| {
const select_option = ui.textButton(option[1]);
select_option.alignment.x = .start;
select_option.size.x = UI.Sizing.initGrowFull();
select_option.borders = UI.Borders.all(.{ .color = srcery.hard_black, .size = 2 });
select_option.background = srcery.black;
// inline for (options) |option| {
// const select_option = ui.textButton(option[1]);
// select_option.alignment.x = .start;
// select_option.size.x = UI.Sizing.initGrowFull();
// select_option.borders = UI.Borders.all(.{ .color = srcery.hard_black, .size = 2 });
// select_option.background = srcery.black;
const signal = ui.signal(select_option);
if (signal.clicked()) {
select.persistent.open = false;
transform.* = switch (option[0]) {
.sliding_window => App.Transform{ .sliding_window = sample_rate orelse 0 },
.addition => App.Transform{ .addition = 0 },
.multiply => App.Transform{ .multiply = 1 },
else => unreachable
};
}
}
// const signal = ui.signal(select_option);
// if (signal.clicked()) {
// select.persistent.open = false;
// transform.* = switch (option[0]) {
// .sliding_window => App.Transform{ .sliding_window = sample_rate orelse 0 },
// .addition => App.Transform{ .addition = 0 },
// .multiply => App.Transform{ .multiply = 1 },
// else => unreachable
// };
// }
// }
_ = ui.signal(popup);
}
}
// _ = ui.signal(popup);
// }
// }
var input_opts = UI.NumberInputOptions{
.key = ui.keyFromString("Sliding window"),
.storage = &self.transform_inputs[i],
.width = ui.rem(4)
// .postfix = if (sample_rate != null) " s" else null,
// .display_scalar = sample_rate,
};
// var input_opts = UI.NumberInputOptions{
// .key = ui.keyFromString("Sliding window"),
// .storage = &self.transform_inputs[i],
// .width = ui.rem(4)
// // .postfix = if (sample_rate != null) " s" else null,
// // .display_scalar = sample_rate,
// };
_ = ui.createBox(.{ .size_x = UI.Sizing.initGrowFull() });
// _ = ui.createBox(.{ .size_x = UI.Sizing.initGrowFull() });
if (transform.* == .sliding_window and sample_rate != null) {
input_opts.postfix = " s";
input_opts.display_scalar = sample_rate;
}
// if (transform.* == .sliding_window and sample_rate != null) {
// input_opts.postfix = " s";
// input_opts.display_scalar = sample_rate;
// }
const current_value = switch (transform.*) {
.sliding_window => |*v| v,
.addition => |*v| v,
.multiply => |*v| v
};
input_opts.initial = current_value.*;
// const current_value = switch (transform.*) {
// .sliding_window => |*v| v,
// .addition => |*v| v,
// .multiply => |*v| v
// };
// input_opts.initial = current_value.*;
const new_value = try ui.numberInput(f64, input_opts);
if (new_value != null) {
current_value.* = new_value.?;
}
}
// const new_value = try ui.numberInput(f64, input_opts);
// if (new_value != null) {
// current_value.* = new_value.?;
// }
// }
for (0..deferred_remove.len) |i| {
const transform_index = deferred_remove.get(deferred_remove.len - 1 - i);
_ = view.transforms.orderedRemove(transform_index);
}
// for (0..deferred_remove.len) |i| {
// const transform_index = deferred_remove.get(deferred_remove.len - 1 - i);
// _ = view.transforms.orderedRemove(transform_index);
// }
if (view.transforms.unusedCapacitySlice().len > 0) {
const btn = ui.textButton("Add transform");
if (ui.signal(btn).clicked()) {
view.transforms.appendAssumeCapacity(.{ .addition = 0 });
}
}
// if (view.transforms.unusedCapacitySlice().len > 0) {
// const btn = ui.textButton("Add transform");
// if (ui.signal(btn).clicked()) {
// view.transforms.appendAssumeCapacity(.{ .addition = 0 });
// }
// }
_ = ui.createBox(.{ .size_y = UI.Sizing.initGrowFull() });
@ -735,103 +774,103 @@ fn showViewSettings(self: *MainScreen, view_id: Id) !void {
}
}
fn showMarkedRange(self: *MainScreen, view_id: Id, index: usize) void {
var ui = &self.app.ui;
// fn showMarkedRange(self: *MainScreen, view_id: Id, index: usize) void {
// var ui = &self.app.ui;
const view = self.app.getView(view_id) orelse return;
// const view = self.app.getView(view_id) orelse return;
const marked_range = view.marked_ranges.get(index);
// const marked_range = view.marked_ranges.get(index);
{
const label = ui.label("Selected range", .{});
label.borders.bottom = .{
.color = srcery.blue,
.size = 1
};
// {
// const label = ui.label("Selected range", .{});
// label.borders.bottom = .{
// .color = srcery.blue,
// .size = 1
// };
_ = ui.createBox(.{ .size_y = UI.Sizing.initFixedPixels(ui.rem(1)) });
}
// _ = ui.createBox(.{ .size_y = UI.Sizing.initFixedPixels(ui.rem(1)) });
// }
const sample_rate = self.app.project.getSampleRate();
if (marked_range.axis == .X and sample_rate != null) {
_ = ui.label("From: {d:.3}s", .{ marked_range.range.lower / sample_rate.? });
_ = ui.label("To: {d:.3}s", .{ marked_range.range.upper / sample_rate.? });
_ = ui.label("Size: {d:.3}s", .{ marked_range.range.size() / sample_rate.? });
} else {
_ = ui.label("From: {d:.2}", .{ marked_range.range.lower });
_ = ui.label("To: {d:.2}", .{ marked_range.range.upper });
_ = ui.label("Size: {d:.2}", .{ marked_range.range.size() });
}
// const sample_rate = self.app.project.getSampleRate();
// if (marked_range.axis == .X and sample_rate != null) {
// _ = ui.label("From: {d:.3}s", .{ marked_range.range.lower / sample_rate.? });
// _ = ui.label("To: {d:.3}s", .{ marked_range.range.upper / sample_rate.? });
// _ = ui.label("Size: {d:.3}s", .{ marked_range.range.size() / sample_rate.? });
// } else {
// _ = ui.label("From: {d:.2}", .{ marked_range.range.lower });
// _ = ui.label("To: {d:.2}", .{ marked_range.range.upper });
// _ = ui.label("Size: {d:.2}", .{ marked_range.range.size() });
// }
_ = ui.label("Samples: {d:.2}", .{ marked_range.range.size() });
// _ = ui.label("Samples: {d:.2}", .{ marked_range.range.size() });
if (marked_range.axis == .X) {
if (marked_range.min) |min| {
_ = ui.label("Minimum: {d:.3}", .{ min });
} else{
_ = ui.label("Minimum: <unknown>", .{});
}
// if (marked_range.axis == .X) {
// if (marked_range.min) |min| {
// _ = ui.label("Minimum: {d:.3}", .{ min });
// } else{
// _ = ui.label("Minimum: <unknown>", .{});
// }
if (marked_range.max) |max| {
_ = ui.label("Maximum: {d:.3}", .{ max });
} else{
_ = ui.label("Maximum: <unknown>", .{});
}
// if (marked_range.max) |max| {
// _ = ui.label("Maximum: {d:.3}", .{ max });
// } else{
// _ = ui.label("Maximum: <unknown>", .{});
// }
if (marked_range.average) |average| {
_ = ui.label("Average: {d:.3}", .{ average });
} else{
_ = ui.label("Average: <unknown>", .{});
}
// if (marked_range.average) |average| {
// _ = ui.label("Average: {d:.3}", .{ average });
// } else{
// _ = ui.label("Average: <unknown>", .{});
// }
if (marked_range.standard_deviation) |standard_deviation| {
_ = ui.label("Standard deviation: {d:.3}", .{ standard_deviation });
} else{
_ = ui.label("Standard deviation: <unknown>", .{});
}
}
// if (marked_range.standard_deviation) |standard_deviation| {
// _ = ui.label("Standard deviation: {d:.3}", .{ standard_deviation });
// } else{
// _ = ui.label("Standard deviation: <unknown>", .{});
// }
// }
_ = ui.createBox(.{ .size_y = UI.Sizing.initGrowFull() });
// _ = ui.createBox(.{ .size_y = UI.Sizing.initGrowFull() });
{
const btn = ui.textButton("Remove");
btn.borders = UI.Borders.all(.{ .color = srcery.red, .size = 4 });
const signal = ui.signal(btn);
if (signal.clicked() or ui.isKeyboardPressed(.key_backspace)) {
self.view_controls.show_marked_range = null;
_ = view.marked_ranges.swapRemove(index);
}
}
}
// {
// const btn = ui.textButton("Remove");
// btn.borders = UI.Borders.all(.{ .color = srcery.red, .size = 4 });
// const signal = ui.signal(btn);
// if (signal.clicked() or ui.isKeyboardPressed(.key_backspace)) {
// self.side_panel = .project;
// _ = view.marked_ranges.swapRemove(index);
// }
// }
// }
fn showMarker(self: *MainScreen, view_id: Id, index: usize) void {
var ui = &self.app.ui;
// fn showMarker(self: *MainScreen, view_id: Id, index: usize) void {
// var ui = &self.app.ui;
const view = self.app.getView(view_id) orelse return;
// const view = self.app.getView(view_id) orelse return;
const marker = view.markers.get(index);
// const marker = view.markers.get(index);
{
const label = ui.label("Selected range", .{});
label.borders.bottom = .{
.color = srcery.blue,
.size = 1
};
// {
// const label = ui.label("Selected range", .{});
// label.borders.bottom = .{
// .color = srcery.blue,
// .size = 1
// };
_ = ui.createBox(.{ .size_y = UI.Sizing.initFixedPixels(ui.rem(1)) });
}
// _ = ui.createBox(.{ .size_y = UI.Sizing.initFixedPixels(ui.rem(1)) });
// }
const sample_rate = self.app.project.getSampleRate();
// const sample_rate = self.app.project.getSampleRate();
if (sample_rate != null) {
const duration = utils.formatDuration(ui.frameAllocator(), marker / sample_rate.?) catch "";
_ = ui.label("Position: {s}", .{ duration });
} else {
_ = ui.label("Position: {d:.2}", .{ marker });
}
// if (sample_rate != null) {
// const duration = utils.formatDuration(ui.frameAllocator(), marker / sample_rate.?) catch "";
// _ = ui.label("Position: {s}", .{ duration });
// } else {
// _ = ui.label("Position: {d:.2}", .{ marker });
// }
}
// }
fn showToolbar(self: *MainScreen) void {
var ui = &self.app.ui;
@ -945,38 +984,38 @@ fn showToolbar(self: *MainScreen) void {
_ = ui.createBox(.{ .size_x = UI.Sizing.initFixedPixels(ui.rem(1)) });
{
var btn = ui.textButton("Move");
if (self.view_controls.selected_tool == .move) {
btn.borders = UI.Borders.bottom(.{ .size = 4, .color = srcery.green });
}
// {
// var btn = ui.textButton("Move");
// if (self.view_controls.selected_tool == .move) {
// btn.borders = UI.Borders.bottom(.{ .size = 4, .color = srcery.green });
// }
if (ui.signal(btn).clicked()) {
self.view_controls.selected_tool = .move;
}
}
// if (ui.signal(btn).clicked()) {
// self.view_controls.selected_tool = .move;
// }
// }
{
var btn = ui.textButton("Select");
if (self.view_controls.selected_tool == .select) {
btn.borders = UI.Borders.bottom(.{ .size = 4, .color = srcery.green });
}
// {
// var btn = ui.textButton("Select");
// if (self.view_controls.selected_tool == .select) {
// btn.borders = UI.Borders.bottom(.{ .size = 4, .color = srcery.green });
// }
if (ui.signal(btn).clicked()) {
self.view_controls.selected_tool = .select;
}
}
// if (ui.signal(btn).clicked()) {
// self.view_controls.selected_tool = .select;
// }
// }
{
var btn = ui.textButton("Marker");
if (self.view_controls.selected_tool == .marker) {
btn.borders = UI.Borders.bottom(.{ .size = 4, .color = srcery.green });
}
// {
// var btn = ui.textButton("Marker");
// if (self.view_controls.selected_tool == .marker) {
// btn.borders = UI.Borders.bottom(.{ .size = 4, .color = srcery.green });
// }
if (ui.signal(btn).clicked()) {
self.view_controls.selected_tool = .marker;
}
}
// if (ui.signal(btn).clicked()) {
// self.view_controls.selected_tool = .marker;
// }
// }
}
fn showSolutions(self: *MainScreen) !void {
@ -1024,15 +1063,14 @@ fn showSolutions(self: *MainScreen) !void {
const btn = ui.textButton("Close");
btn.borders = UI.Borders.all(.{ .color = srcery.red, .size = 4 });
if (ui.signal(btn).clicked()) {
self.view_solutions = false;
self.side_panel = .project;
}
}
}
fn showStatisticsPoints(self: *MainScreen) !void {
var ui = &self.app.ui;
const ui = &self.app.ui;
const project = &self.app.project;
_ = &ui;
{
const label = ui.label("Statistics points", .{});
@ -1070,7 +1108,72 @@ fn showStatisticsPoints(self: *MainScreen) !void {
const btn = ui.textButton("Close");
btn.borders = UI.Borders.all(.{ .color = srcery.red, .size = 4 });
if (ui.signal(btn).clicked()) {
self.view_statistics_points = false;
self.side_panel = .project;
}
}
}
fn showGainChanges(self: *MainScreen) !void {
const project = &self.app.project;
var ui = &self.app.ui;
{
const label = ui.label("Gain changes", .{});
label.borders.bottom = .{
.color = srcery.white,
.size = 1
};
_ = ui.createBox(.{ .size_y = UI.Sizing.initFixedPixels(ui.rem(1)) });
}
const sample_rate = project.getSampleRate();
var removed_index: ?usize = null;
for (0.., project.gain_changes.slice()) |i, *gain_change| {
const row = ui.createBox(.{
.key = UI.Key.initPtr(gain_change),
.size_x = UI.Sizing.initGrowFull(),
.size_y = UI.Sizing.initFitChildren(),
.layout_direction = .left_to_right,
.align_y = .center,
.layout_gap = ui.rem(1)
});
row.beginChildren();
defer row.endChildren();
if (sample_rate != null) {
const seconds = gain_change.sample / sample_rate.?;
_ = ui.label("{s}", .{ try utils.formatDuration(ui.frameAllocator(), seconds) });
} else {
_ = ui.label("{d}", .{ gain_change.sample });
}
var gain_label = ui.label("{d:.3}", .{ gain_change.gain });
gain_label.size.x = UI.Sizing.initGrowFull();
if (i > 0) {
const btn = ui.textButton("Remove");
btn.background = srcery.bright_red;
btn.padding = UI.Padding.all(ui.rem(0.2));
if (ui.signal(btn).clicked()) {
removed_index = i;
}
}
}
if (removed_index) |index| {
project.removeGainChange(index);
}
_ = ui.createBox(.{ .size_y = UI.Sizing.initFixedPixels(ui.rem(1)) });
{
const btn = ui.textButton("Close");
btn.borders = UI.Borders.all(.{ .color = srcery.red, .size = 4 });
if (ui.signal(btn).clicked()) {
self.side_panel = .project;
}
}
}
@ -1093,18 +1196,28 @@ pub fn showSidePanel(self: *MainScreen) !void {
_ = ui.createBox(.{ .size_x = UI.Sizing.initFixedPixels(ui.rem(18)) });
if (self.view_solutions) {
try self.showSolutions();
} else if (self.view_controls.show_marker) |marker| {
self.showMarker(marker.view_id, marker.index);
} else if (self.view_controls.show_marked_range) |show_marked_range| {
self.showMarkedRange(show_marked_range.view_id, show_marked_range.index);
} else if (self.view_controls.view_settings) |view_id| {
try self.showViewSettings(view_id);
} else if (self.view_statistics_points) {
try self.showStatisticsPoints();
} else {
try self.showProjectSettings();
switch (self.side_panel) {
.project => {
try self.showProjectSettings();
},
.solutions => {
try self.showSolutions();
},
.statistics_points => {
try self.showStatisticsPoints();
},
.gain_changes => {
try self.showGainChanges();
},
.view_settings => |view_id| {
try self.showViewSettings(view_id);
},
// .marker => |args| {
// self.showMarker(args.view_id, args.index);
// },
// .marked_range => |args| {
// self.showMarkedRange(args.view_id, args.index);
// }
}
}
@ -1199,14 +1312,8 @@ pub fn tick(self: *MainScreen) !void {
if (ui.isKeyboardPressed(.key_escape)) {
if (self.modal != null) {
self.modal = null;
} else if (self.view_controls.view_settings != null) {
self.view_controls.view_settings = null;
} else if (self.view_controls.show_marked_range != null) {
self.view_controls.show_marked_range = null;
} else if (self.view_solutions) {
self.view_solutions = false;
} else if (self.view_statistics_points) {
self.view_statistics_points = false;
} else if (self.side_panel != .project) {
self.side_panel = .project;
} else {
self.app.should_close = true;
}

View File

@ -2779,8 +2779,9 @@ pub fn textInput(self: *UI, opts: TextInputOptions) !void {
storage.editing = true;
}
var stop_editing = false;
if (self.isKeyActiveAny() and !container_signal.active) {
storage.editing = false;
stop_editing = true;
}
// Text input controls
@ -2952,7 +2953,7 @@ pub fn textInput(self: *UI, opts: TextInputOptions) !void {
}
if (self.isKeyboardPressed(.key_escape)) {
storage.editing = false;
stop_editing = true;
}
if (self.isKeyboardPressed(.key_enter)) {
@ -2975,9 +2976,15 @@ pub fn textInput(self: *UI, opts: TextInputOptions) !void {
}
if (!opts.editable) {
storage.editing = false;
stop_editing = true;
}
}
if (stop_editing) {
storage.editing = false;
storage.cursor_start = 0;
storage.cursor_stop = 0;
}
}
pub fn numberInput(self: *UI, T: type, opts: NumberInputOptions) !?T {