///|
/// Export-time calculations for frame budgets, timecode, and deterministic
/// chunking. These helpers keep host-specific file writing outside the core.
pub struct FrameBudget2 {
frame_count : Int
frame_bytes : Int
total_bytes : Int
duration : Double
throughput : Double
} derive(Debug)
///|
pub fn frame_budget2(
schedule : FrameSchedule2,
bytes_per_frame : Int,
throughput : Double,
) -> FrameBudget2 {
let frames = schedule.frame_count()
{
frame_count: frames,
frame_bytes: bytes_per_frame.max(0),
total_bytes: frames * bytes_per_frame.max(0),
duration: schedule.duration(),
throughput: throughput.max(0.0),
}
}
///|
pub fn FrameBudget2::frame_count(self : FrameBudget2) -> Int {
self.frame_count
}
///|
pub fn FrameBudget2::frame_bytes(self : FrameBudget2) -> Int {
self.frame_bytes
}
///|
pub fn FrameBudget2::total_bytes(self : FrameBudget2) -> Int {
self.total_bytes
}
///|
pub fn FrameBudget2::duration(self : FrameBudget2) -> Double {
self.duration
}
///|
pub fn FrameBudget2::throughput(self : FrameBudget2) -> Double {
self.throughput
}
///|
pub fn FrameBudget2::estimated_seconds(self : FrameBudget2) -> Double {
if self.throughput == 0.0 {
0.0
} else {
self.total_bytes.to_double() / self.throughput
}
}
///|
pub fn FrameBudget2::estimated_megabytes(self : FrameBudget2) -> Double {
self.total_bytes.to_double() / 1000000.0
}
///|
pub struct Timecode2 {
hours : Int
minutes : Int
seconds : Int
frames : Int
frame_rate : Double
} derive(Debug)
///|
pub fn timecode2(seconds : Double, frame_rate : Double) -> Timecode2 {
let rate = frame_rate.max(0.001)
let total_frames = @math.floor(seconds.max(0.0) * rate + 0.5).to_int()
let frames_per_hour = (rate * 3600.0).to_int().max(1)
let frames_per_minute = (rate * 60.0).to_int().max(1)
let hours = total_frames / frames_per_hour
let minutes = total_frames % frames_per_hour / frames_per_minute
let whole_seconds = total_frames % frames_per_minute / rate.to_int().max(1)
let frames = total_frames % rate.to_int().max(1)
{ hours, minutes, seconds: whole_seconds, frames, frame_rate: rate }
}
///|
pub fn Timecode2::hours(self : Timecode2) -> Int {
self.hours
}
///|
pub fn Timecode2::minutes(self : Timecode2) -> Int {
self.minutes
}
///|
pub fn Timecode2::seconds(self : Timecode2) -> Int {
self.seconds
}
///|
pub fn Timecode2::frames(self : Timecode2) -> Int {
self.frames
}
///|
pub fn Timecode2::frame_rate(self : Timecode2) -> Double {
self.frame_rate
}
///|
pub fn Timecode2::to_seconds(self : Timecode2) -> Double {
self.hours.to_double() * 3600.0 +
self.minutes.to_double() * 60.0 +
self.seconds.to_double() +
self.frames.to_double() / self.frame_rate
}
///|
pub fn Timecode2::frame_index(self : Timecode2) -> Int {
@math.floor(self.to_seconds() * self.frame_rate + 0.5).to_int()
}
///|
pub struct ExportChunk2 {
start_frame : Int
end_frame : Int
start_time : Double
end_time : Double
bytes : Int
} derive(Debug)
///|
pub fn ExportChunk2::start_frame(self : ExportChunk2) -> Int {
self.start_frame
}
///|
pub fn ExportChunk2::end_frame(self : ExportChunk2) -> Int {
self.end_frame
}
///|
pub fn ExportChunk2::start_time(self : ExportChunk2) -> Double {
self.start_time
}
///|
pub fn ExportChunk2::end_time(self : ExportChunk2) -> Double {
self.end_time
}
///|
pub fn ExportChunk2::bytes(self : ExportChunk2) -> Int {
self.bytes
}
///|
pub fn split_export_chunks(
schedule : FrameSchedule2,
chunk_size : Int,
bytes_per_frame : Int,
) -> Array[ExportChunk2] {
let result : Array[ExportChunk2] = []
let size = chunk_size.max(1)
let total = schedule.frame_count()
let mut start = 0
while start < total {
let end = (start + size).min(total)
result.push({
start_frame: start,
end_frame: end - 1,
start_time: schedule.time_at(start),
end_time: schedule.time_at(end - 1),
bytes: (end - start) * bytes_per_frame.max(0),
})
start = end
}
result
}
///|
pub fn export_chunk_count(schedule : FrameSchedule2, chunk_size : Int) -> Int {
let size = chunk_size.max(1)
(schedule.frame_count() + size - 1) / size
}
///|
pub fn export_frame_index(schedule : FrameSchedule2, time : Double) -> Int {
let minimum = schedule.index_at(schedule.start())
let maximum = schedule.index_at(schedule.end())
schedule.index_at(time).max(minimum).min(maximum)
}
///|
pub fn export_time_for_frame(schedule : FrameSchedule2, frame : Int) -> Double {
schedule.time_at((frame - schedule.index_at(schedule.start())).max(0))
}
///|
pub fn export_duration_for_frames(
frame_count : Int,
frame_rate : Double,
) -> Double {
if frame_count <= 1 {
0.0
} else {
(frame_count - 1).to_double() / frame_rate.max(0.001)
}
}
///|
pub fn estimate_compressed_bytes(
frame_count : Int,
width : Int,
height : Int,
bits_per_pixel : Double,
compression : Double,
) -> Int {
let raw_bits = frame_count.max(0).to_double() *
width.max(1).to_double() *
height.max(1).to_double() *
bits_per_pixel.max(0.0)
(raw_bits / 8.0 * compression.max(0.000001)).to_int()
}
///|
pub fn validate_export_dimensions(
width : Int,
height : Int,
maximum : Int,
) -> Bool {
width > 0 && height > 0 && width <= maximum && height <= maximum
}
///|
pub fn aspect_ratio2(width : Int, height : Int) -> Double {
if height <= 0 {
0.0
} else {
width.to_double() / height.to_double()
}
}
///|
pub fn fit_dimensions2(width : Int, height : Int, maximum : Int) -> (Int, Int) {
let scale = maximum.to_double() / width.max(height).to_double()
(
(width.to_double() * scale).to_int().max(1),
(height.to_double() * scale).to_int().max(1),
)
}