diff options
| -rwxr-xr-x | build.bat | 4 | ||||
| -rw-r--r-- | src/main.odin | 26 | ||||
| -rw-r--r-- | src/parts/end.html | 7 | ||||
| -rw-r--r-- | src/wav/wav.odin | 327 |
4 files changed, 323 insertions, 41 deletions
@@ -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 |