aboutsummaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorSan Jacobs2025-12-13 05:08:37 +0100
committerSan Jacobs2025-12-13 05:08:37 +0100
commitdae712ac1cbe9751c8da0e826b8111351cf9519d (patch)
treeffee6d293f2bc178b518e1d7e6156d763d5e1bbc
parente2c930c8b48c3974ce20e5ce459b257178189dc7 (diff)
downloadbetter-report-dae712ac1cbe9751c8da0e826b8111351cf9519d.tar.gz
better-report-dae712ac1cbe9751c8da0e826b8111351cf9519d.tar.bz2
better-report-dae712ac1cbe9751c8da0e826b8111351cf9519d.zip
The wav lib is loading the audio data!! Tested with 24-bit audioHEADmaster
Also it handles wave_format_extensible now.
-rwxr-xr-xbuild.bat4
-rw-r--r--src/main.odin26
-rw-r--r--src/parts/end.html7
-rw-r--r--src/wav/wav.odin327
4 files changed, 323 insertions, 41 deletions
diff --git a/build.bat b/build.bat
index 219d828..717b776 100755
--- a/build.bat
+++ b/build.bat
@@ -1,4 +1,4 @@
@echo off
-odin build src/ -debug -pdb-name:bin/better-report.pdb -out:bin/better-report.exe && bin\better-report.exe test\
+rem odin build src/ -debug -pdb-name:bin/better-report.pdb -out:bin/better-report.exe && bin\better-report.exe test\
rem odin build src/ -o:speed -out:C:/tools/better-report.exe
-rem odin run src/wav \ No newline at end of file
+odin run src/wav -debug -out:bin/wav.exe \ No newline at end of file
diff --git a/src/main.odin b/src/main.odin
index 20969c1..ec9d70e 100644
--- a/src/main.odin
+++ b/src/main.odin
@@ -244,16 +244,22 @@ parse_folder :: proc(paths : Directory) -> (Report, bool) {
case "Scene":
row[i] = w.scene
case "Take":
- row[i] = fmt.tprintf("T%03d", w.take)
+ if w.take >= 0 {
+ row[i] = fmt.tprintf("%02d", w.take)
+ }
case "Duration":
row[i] = wav.tprint_duration(w)
case "Timecode":
- row[i] = wav.tprint_timecode(w)
+ if w.tc_framerate > 0 {
+ row[i] = wav.tprint_timecode(w)
+ }
case "TC FPS":
- if w.tc_dropframe { // TC FPS
- row[i] = fmt.tprintf("%.03f DF", w.tc_framerate)
- } else {
- row[i] = fmt.tprintf("%.03f ND", w.tc_framerate)
+ if w.tc_framerate > 0 {
+ if w.tc_dropframe { // TC FPS
+ row[i] = fmt.tprintf("%.03f DF", w.tc_framerate)
+ } else {
+ row[i] = fmt.tprintf("%.03f ND", w.tc_framerate)
+ }
}
case "User Bits":
if w.ubits != {0,0,0,0,0,0,0,0,} {
@@ -273,6 +279,8 @@ parse_folder :: proc(paths : Directory) -> (Report, bool) {
row[i] = fmt.tprintf("%d-bit int", w.bit_depth)
case .FLOAT:
row[i] = fmt.tprintf("%d-bit float", w.bit_depth)
+ case .ADPCM:
+ row[i] = fmt.tprintf("%d-bit ADPCM", w.bit_depth)
}
case "Circled":
if w.circled do row[i] = "O"
@@ -530,7 +538,10 @@ parse_file :: proc(path : CSV, device : Device = .UNSET) -> (Report, bool) {
last_channel_index := -1
stage : Stages = .TITLE
- #partial switch device {
+ switch device {
+ case .UNSET:
+ fmt.eprintln("Uh-oh. This shouldn't happen. No device set when trying to fill in data.")
+ unreachable()
case .SD8:
// .d8888b. 8888888b. .d8888b.
// d88P Y88b 888 "Y88b d88P Y88b
@@ -684,7 +695,6 @@ parse_file :: proc(path : CSV, device : Device = .UNSET) -> (Report, bool) {
if element[:4] == "Trk " {
if first_channel_index == -1 do first_channel_index = e
last_channel_index = e
- // TODO: Rename track column with corrected numbers
}
if element == "Start TC" {
output.tc_column_index = e
diff --git a/src/parts/end.html b/src/parts/end.html
index 204188c..fb5a632 100644
--- a/src/parts/end.html
+++ b/src/parts/end.html
@@ -33,13 +33,16 @@
prevColumnSorted = th;
}
- // Event listeners and initial sort
+ // Event listener
document.querySelectorAll('th').forEach(th => th.addEventListener('click', () => {
document.querySelectorAll('.current-sort').forEach(el => el.classList.remove('current-sort'));
th.classList.add('current-sort');
doSort(th);
}));
-
+
+ // Sort by the default column on page load
+ doSort(prevColumnSorted);
+
// Column and row highlighting on hover
const table = document.querySelector('table');
const allCells = table.querySelectorAll('td, th');
diff --git a/src/wav/wav.odin b/src/wav/wav.odin
index af11604..14ed74a 100644
--- a/src/wav/wav.odin
+++ b/src/wav/wav.odin
@@ -14,15 +14,22 @@ Wav :: struct {
channels : int,
sample_rate : int,
bit_depth : int,
+ sample_size : int, // Samples can be bigger than bit_depth for alignment
reported_size : int,
data_size : int,
+ data_start : int,
+
+ // The actual audio data
+ audio : [][]f32,
// Internals
handle : os.Handle,
+ load_head : int,
// Metadata
date : Date,
channel_names : []string,
+ channel_mask : Speaker_Set,
samples_since_midnight : int, // Source of timecode
tc_framerate : f32,
tc_dropframe : bool,
@@ -36,23 +43,57 @@ Wav :: struct {
}
Audio_Format :: enum {
INT = 1,
+ ADPCM = 2, // Compressed format. Not yet handled.
FLOAT = 3,
}
Date :: struct {
year, month, day : int,
}
+Timecode :: struct {
+ hour, minute, second, frame : int
+}
+Speakers :: enum u32 {
+ Front_Left = 0,
+ Front_Right = 1,
+ Front_Center = 2,
+ LFE = 3,
+ Back_Left = 4,
+ Back_Right = 5,
+ Front_Left_of_Center = 6,
+ Front_Right_of_Center = 7,
+ Back_Center = 8,
+ Side_Left = 9,
+ Side_Right = 10,
+ Top_Center = 11,
+ Top_Front_Left = 12,
+ Top_Front_Center = 13,
+ Top_Front_Right = 14,
+ Top_Back_Left = 15,
+ Top_Back_Center = 16,
+ Top_Back_Right = 17,
+}
+Speaker_Set :: bit_set[Speakers; u32]
VERBOSE :: false
BUFFER_SIZE :: 1<<18
main :: proc() {
// Test
- enok, enok_ok := read("test/WAVs/ENOKS-BIRHTDAYT02.WAV", context.temp_allocator)
+ /*enok, enok_ok := read("test/WAVs/ENOKS-BIRHTDAYT02.WAV", context.temp_allocator)
when VERBOSE do fmt.printf("\n\nenok = %#v\n\n", enok)
prins, prins_ok := read("test/WAVs/KRONPRINS01T01.wav", context.temp_allocator)
when VERBOSE do fmt.printf("\n\nprins = %#v\n\n", prins)
f8, f8_ok := read("test/WAVs/F8-SL098-T001.WAV", context.temp_allocator)
when VERBOSE do fmt.printf("\n\nf8 = %#v\n\n", f8)
+ ski, ski_ok := read("test/WAVs/FOLEY, SKI, KLAEBO 01, LCR.wav", context.temp_allocator)
+ when VERBOSE do fmt.printf("\n\nSKI = %#v\n\n", ski)
+ load(&ski)
+ wave_print(ski.audio[0])
+ */
+ t, t_ok := read("test/WAVs/test_data.wav", context.temp_allocator)
+ when VERBOSE do fmt.printf("\n\nTEST = %#v\n\n", t)
+ load(&t)
+ wave_print(t.audio[1])
}
/*
@@ -61,6 +102,9 @@ Reads in the wav file metadata, without loading the sound data into ram.
read :: proc(path : string, allocator:=context.allocator) -> (Wav, bool) #optional_ok {
file : Wav
file.path = path
+ file.take = -1
+ file.tc_framerate = -1
+ file.take = -1
load_err : os.Error
file.handle, load_err = os.open(path)
@@ -87,7 +131,7 @@ read :: proc(path : string, allocator:=context.allocator) -> (Wav, bool) #option
head += 4
// Size
- file.reported_size = int(read_little_endian_u32(temp_buf[head:head+4]))
+ file.reported_size = int(little_endian_u32(temp_buf[head:head+4]))
when VERBOSE do fmt.println("Reported size:", file.reported_size)
head += 4
@@ -104,14 +148,16 @@ read :: proc(path : string, allocator:=context.allocator) -> (Wav, bool) #option
chunk_id_raw := temp_buf[head:head+4]
chunk_id := string(chunk_id_raw)
head += 4
- chunk_size := int(read_little_endian_u32(temp_buf[head:head+4]))
+ chunk_size := int(little_endian_u32(temp_buf[head:head+4]))
head += 4
when VERBOSE do fmt.println(chunk_id, chunk_size,"\n-------------------------------------")
+ current_chunk_start := head
next_chunk_start := head + chunk_size
data_reached := false
if chunk_id == "data" {
file.data_size = chunk_size
+ file.data_start = head
data_reached = true
}
if next_chunk_start < BUFFER_SIZE && !data_reached {
@@ -128,13 +174,21 @@ read :: proc(path : string, allocator:=context.allocator) -> (Wav, bool) #option
//print_data = string(temp_bext)
null_chunks = 0
case "fmt ":
- file.format = Audio_Format(read_little_endian_u16(temp_buf[head:]))
- when VERBOSE do fmt.println("Format:", file.format)
+ raw_format_field := little_endian_u16(temp_buf[head:])
+ wave_format_extensible := false
+ switch raw_format_field {
+ case 1..=3:
+ file.format = Audio_Format(raw_format_field)
+ when VERBOSE do fmt.println("Format:", file.format)
+ case 65534:
+ wave_format_extensible = true
+ when VERBOSE do fmt.println("Format: wave_format_extensible")
+ }
head += 2
- file.channels = int(read_little_endian_u16(temp_buf[head:]))
+ file.channels = int(little_endian_u16(temp_buf[head:]))
when VERBOSE do fmt.println("Channels:", file.channels)
head += 2
- file.sample_rate = int(read_little_endian_u32(temp_buf[head:]))
+ file.sample_rate = int(little_endian_u32(temp_buf[head:]))
when VERBOSE do fmt.println("Sample rate:", file.sample_rate)
head += 4
@@ -143,9 +197,67 @@ read :: proc(path : string, allocator:=context.allocator) -> (Wav, bool) #option
// and are implied by the other fields.
head += 4 + 2
- file.bit_depth = int(read_little_endian_u16(temp_buf[head:]))
- when VERBOSE do fmt.println("Bit depth:", file.bit_depth)
- head += 2
+ if !wave_format_extensible {
+ file.bit_depth = int(little_endian_u16(temp_buf[head:]))
+ file.sample_size = file.bit_depth
+ when VERBOSE do fmt.println("Bit depth:", file.bit_depth)
+ head += 2
+ } else if chunk_size>=40 {
+
+ // This is what would normally be the bit depth field
+ // But in the wave_format_extensible case, it essentailly tells you the
+ // stride between each sample, aka "sample size" in this lib's API.
+ bits_pr_sample := int(little_endian_u16(temp_buf[head:]))
+ head += 2
+
+ when VERBOSE do fmt.println(" - wave_format_extensible -")
+ when VERBOSE do fmt.println("Size of extension:", little_endian_u16(temp_buf[head:]))
+ head += 2
+
+ // Valid bits pr sample (aka is every 24-bit sample padded to be 32 bit aligned?)
+ // OR this field can be samples pr block, if this is a compressed format...
+ // ...sigh... yes, you can cram a compressed format into a wav file. I know.
+ valid_bits_pr_sample := int(little_endian_u16(temp_buf[head:]))
+ head += 2
+
+ // Channel mask (Which speaker channels are in the file)
+ file.channel_mask = transmute(Speaker_Set)little_endian_u32(temp_buf[head:])
+ when VERBOSE do fmt.println("Channel Mask (RAW):", temp_buf[head:head+4])
+ when VERBOSE do fmt.println("Channel Mask (Speakers):", file.channel_mask)
+ head += 4
+
+ // 16 bytes of SubFormat GUID, with the first two bytes being the format
+ when VERBOSE do fmt.println("GUID:", temp_buf[head:head+16])
+ file.format = Audio_Format(little_endian_u16(temp_buf[head:]))
+ when VERBOSE do fmt.println("Format:", file.format)
+ head += 2
+ // Other GUID stuff
+ head += 14
+
+
+ if file.format == .ADPCM {
+ // TODO: Support ADPCM
+ when VERBOSE do fmt.println("Samples pr chunk (In bit-depth field because it's unsupported):", file.bit_depth)
+ } else {
+ if valid_bits_pr_sample < bits_pr_sample {
+ when VERBOSE do fmt.printfln("Warning: Bad metadata. valid_bits_pr_sample ({}) < bits_pr_sample ({}). Ignoring valid_bits_pr_sample. Using bits_pr_sample for bit_depth and sample_size.", valid_bits_pr_sample, bits_pr_sample)
+ file.bit_depth = bits_pr_sample
+ file.sample_size = bits_pr_sample
+ when VERBOSE do fmt.println("Bit depth & sample size:", bits_pr_sample)
+ } else {
+ // This is what is supposed to happen with wave_format_extensible
+ file.bit_depth = valid_bits_pr_sample
+ file.sample_size = bits_pr_sample
+ when VERBOSE do fmt.println("Bit depth:", file.bit_depth)
+ when VERBOSE do fmt.println("Sample size/stride:", file.sample_size)
+ }
+ }
+ }
+
+ if file.format == .ADPCM {
+ fmt.eprintln("WARNING! ADPCM file submitted. This is NOT SUPPORTED.")
+ }
+
head = data_end
null_chunks = 0
case "\x00\x00\x00\x00":
@@ -172,7 +284,7 @@ read :: proc(path : string, allocator:=context.allocator) -> (Wav, bool) #option
}
file.channel_names = make([]string, file.channels, allocator=allocator)
-
+ file.audio = make([][]f32, file.channels, allocator=allocator)
// iXML Parsing
@@ -336,11 +448,11 @@ read :: proc(path : string, allocator:=context.allocator) -> (Wav, bool) #option
if len(temp_bext) > 0 {
// Stripping null padding
- end := len(temp_bext) - 1
+ /*end := len(temp_bext) - 1
for end >= 0 && temp_bext[end] == 0 {
end -= 1
}
- temp_bext = temp_bext[:end]
+ temp_bext = temp_bext[:end]*/
naming_channel := 0
@@ -396,19 +508,19 @@ read :: proc(path : string, allocator:=context.allocator) -> (Wav, bool) #option
date := string(temp_bext[head:head+10])
when VERBOSE do fmt.printf("Origination Date: %v\n", date)
date_splits := strings.split(date, "-")
- file.date.year, _ = strconv.parse_int(date_splits[0])
+ file.date.year, _ = strconv.parse_int(date_splits[0])
file.date.month, _ = strconv.parse_int(date_splits[1])
- file.date.day, _ = strconv.parse_int(date_splits[2])
+ file.date.day, _ = strconv.parse_int(date_splits[2])
delete(date_splits)
head += 10
when VERBOSE do fmt.printf("Origination Time: %v\n", string(temp_bext[head:head+8]))
head += 8
- file.samples_since_midnight = int(read_little_endian_u64(temp_bext[head:head+8]))
+ file.samples_since_midnight = int(little_endian_u64(temp_bext[head:head+8]))
when VERBOSE do fmt.printf("Time Reference: %v (Samples since midnight, source of timecode)\n", file.samples_since_midnight)
head += 8
- when VERBOSE do fmt.printf("Version: %v\n", read_little_endian_u16(temp_bext[head:head+2]))
+ when VERBOSE do fmt.printf("Version: %v\n", little_endian_u16(temp_bext[head:head+2]))
head += 2
when VERBOSE do fmt.printf("UMID Skipped.\n")
head += 64
@@ -416,6 +528,7 @@ read :: proc(path : string, allocator:=context.allocator) -> (Wav, bool) #option
head += 190
when VERBOSE do fmt.printf("Coding history:\n%v\n", string(temp_bext[head:]))
}
+ when VERBOSE do fmt.printfln("%#v", file)
when VERBOSE do fmt.println()
// just here to make some printing prettier
@@ -425,36 +538,192 @@ read :: proc(path : string, allocator:=context.allocator) -> (Wav, bool) #option
return file, true
}
+/*
+Loads the full-length audio of the wav file into memory.
+Pass in a slice of ints to select only specific channels to load.
+*/
+load :: proc(file : ^Wav, channels : []int = {}, allocator:=context.allocator) {
+ channels := channels
+ all_channels := false
+ if len(channels) == 0 {
+ all_channels = true
+ channels = make([]int, file.channels)
+ for c, i in channels {
+ channels[i] = i
+ }
+ }
+ err_quit := false
+ for c in channels {
+ if c >= file.channels {
+ fmt.eprintfln("ERROR: Requested higher channel ({}) to be loaded than exists in file ({})!", c, file.channels)
+ }
+ }
+ if err_quit do return
+
+
+ // The main work part
+ length := get_sample_count(file^)
+
+ for c in channels {
+ file.audio[c] = make([]f32, length, allocator=allocator)
+ }
+
+ samples_in_buffer :: 1024
+ bytes_pr_sample := file.sample_size/8
+ buffer_bytes : i64 = i64(bytes_pr_sample*file.channels*samples_in_buffer)
+ buffer := make([]u8, buffer_bytes)
+
+ decode := decode_24
+ switch file.format {
+ case .INT:
+ switch file.bit_depth {
+ case 16:
+ decode = decode_16
+ case 24:
+ decode = decode_24
+ case 32:
+ decode = decode_32
+ }
+ case .FLOAT:
+ switch file.bit_depth {
+ case 32:
+ decode = decode_f32
+ }
+ case .ADPCM:
+ fmt.eprintln("ERROR: ADPCM is not supported!")
+ return
+ }
+
+ head : i64 = i64(file.data_start)
+ absolute_sample : u64 = 0
+ file.handle, _ = os.open(file.path)
+ for {
+ os.read_at(file.handle, buffer, head)
+ //fmt.println(buffer)
+ for local_sample in 0..<samples_in_buffer {
+ for c in channels {
+ offset := (local_sample*file.channels+c)*bytes_pr_sample
+ file.audio[c][absolute_sample] = decode(buffer[offset:])
+ }
+ absolute_sample += 1
+ if absolute_sample >= u64(length) {
+ break
+ }
+ }
+ if absolute_sample >= u64(length) {
+ break
+ }
+ head += buffer_bytes
+ }
+ os.close(file.handle)
+ delete(buffer)
+
+
+ if all_channels do delete(channels)
+ return
+}
tprint_timecode :: proc(file : Wav) -> string {
return print_timecode(file, allocator=context.temp_allocator)
}
print_timecode :: proc(file : Wav, allocator := context.allocator) -> string {
- seconds_since_midnight := file.samples_since_midnight / file.sample_rate
- hour := int( seconds_since_midnight / 3600)
- minute := int((seconds_since_midnight % 3600) / 60)
- second := int( seconds_since_midnight % 60)
- frame := int( math.round(f64(file.samples_since_midnight % file.sample_rate ) * f64(file.tc_framerate) / f64(file.sample_rate)))
-
+ tc := get_timecode(file)
+ using tc
return fmt.aprintf("%02d:%02d:%02d:%02d",hour,minute,second,frame,
allocator=allocator)
}
+get_timecode :: proc(file : Wav) -> (output:Timecode) {
+ seconds_since_midnight := file.samples_since_midnight / file.sample_rate
+ output.hour = int( seconds_since_midnight / 3600)
+ output.minute = int((seconds_since_midnight % 3600) / 60)
+ output.second = int( seconds_since_midnight % 60)
+ output.frame = int( math.round(f64(file.samples_since_midnight % file.sample_rate ) * f64(file.tc_framerate) / f64(file.sample_rate)))
+ return
+}
tprint_duration :: proc(file : Wav) -> string {
return print_duration(file, allocator=context.temp_allocator)
}
print_duration :: proc(file : Wav, allocator := context.allocator) -> string {
- samples := file.data_size/file.channels/(file.bit_depth/8)
- total_seconds := samples/file.sample_rate
-
+ total_seconds := int(math.round(get_duration(file)))
hours := int( total_seconds / 3600)
minutes := int((total_seconds % 3600) / 60)
seconds := int( total_seconds % 60)
return fmt.aprintf("%02d:%02d:%02d", hours, minutes, seconds, allocator=allocator)
}
+/* Returns duration in seconds */
+get_duration :: proc(file : Wav) -> f64 {
+ return f64(get_sample_count(file))/f64(file.sample_rate)
+}
+get_sample_count :: proc(file : Wav) -> int {
+ return file.data_size/file.channels/(file.sample_size/8)
+}
+
+
+wave_print :: proc(wave : []f32) {
+ for sample, i in wave {
+ fmt.printf("[%012d] ", i)
+ bar_print(sample)
+ fmt.printf(" %+01.04f\n", sample)
+ }
+}
+bar_print :: proc(x : f32, one_side_width : int = 25) {
+ bar_length := int(math.round(abs(min(x, 1))*f32(one_side_width)))
+ spaces := one_side_width-bar_length
+ if x>0 {
+ for _ in 0..<one_side_width {
+ fmt.print(" ")
+ }
+ fmt.print("|")
+ for _ in 0..<bar_length {
+ fmt.print("#")
+ }
+ for _ in 0..<spaces {
+ fmt.print(" ")
+ }
+ } else {
+ for _ in 0..<spaces {
+ fmt.print(" ")
+ }
+ for _ in 0..<bar_length {
+ fmt.print("#")
+ }
+ fmt.print("|")
+ for _ in 0..<one_side_width {
+ fmt.print(" ")
+ }
+ }
+}
-read_little_endian_u64 :: proc(x : []u8) -> u64 {
+
+decode_f32 :: proc(x : []u8) -> f32 {
+ return (^f32)(&x[0])^
+}
+decode_32 :: proc(x : []u8) -> f32 {
+ integer := i32(x[0]) |
+ i32(x[1]) << 8 |
+ i32(x[2]) << 16 |
+ i32(x[3]) << 24
+ return f32(integer) / f32(1 << 31)
+}
+decode_24 :: proc(x : []u8) -> f32 {
+ integer := i32(x[0]) |
+ i32(x[1]) << 8 |
+ i32(x[2]) << 16
+
+ // Cheeky sign extension
+ integer = (integer << 8) >> 8
+
+ return f32(integer) / f32(1 << 23)
+}
+decode_16 :: proc(x : []u8) -> f32 {
+ integer := i16(x[0]) |
+ i16(x[1]) << 8
+ return f32(integer) / f32(1 << 15)
+}
+
+little_endian_u64 :: proc(x : []u8) -> u64 {
return u64(x[0]) |
u64(x[1]) << 8 |
u64(x[2]) << 16 |
@@ -465,14 +734,14 @@ read_little_endian_u64 :: proc(x : []u8) -> u64 {
u64(x[7]) << 56
}
-read_little_endian_u32 :: proc(x : []u8) -> u32 {
+little_endian_u32 :: proc(x : []u8) -> u32 {
return u32(x[0]) |
u32(x[1]) << 8 |
u32(x[2]) << 16 |
u32(x[3]) << 24
}
-read_little_endian_u16 :: proc(x : []u8) -> u16 {
+little_endian_u16 :: proc(x : []u8) -> u16 {
return u16(x[0]) |
u16(x[1]) << 8
} \ No newline at end of file