From 09ec5f18a0583c94286f1f1f1b462f675d93d94d Mon Sep 17 00:00:00 2001 From: San Jacobs Date: Wed, 12 Nov 2025 05:21:32 +0100 Subject: Starting to read wav metadata directly --- build.bat | 8 +- main.odin | 758 ------------------------------------------------------ parts/end.html | 46 ---- parts/start.html | 9 - parts/start2.html | 103 -------- src/main.odin | 758 ++++++++++++++++++++++++++++++++++++++++++++++++++++++ src/wav/wav.odin | 140 ++++++++++ 7 files changed, 903 insertions(+), 919 deletions(-) delete mode 100755 main.odin delete mode 100755 parts/end.html delete mode 100755 parts/start.html delete mode 100755 parts/start2.html create mode 100644 src/main.odin create mode 100644 src/wav/wav.odin diff --git a/build.bat b/build.bat index f847c6e..f8662d4 100755 --- a/build.bat +++ b/build.bat @@ -1,3 +1,5 @@ -odin build . -debug -pdb-name:bin/better-report.pdb -out:bin/better-report.exe -bin\better-report.exe test\ -odin build . -o:speed -out:C:/tools/better-report.exe \ No newline at end of file +@echo off +rem odin build src/ -debug -pdb-name:bin/better-report.pdb -out:bin/better-report.exe +rem bin\better-report.exe test\ +rem odin build src/ -o:speed -out:C:/tools/better-report.exe +odin run src/wav \ No newline at end of file diff --git a/main.odin b/main.odin deleted file mode 100755 index cd3d3c5..0000000 --- a/main.odin +++ /dev/null @@ -1,758 +0,0 @@ -package main - -import "core:fmt" -import "core:os" -import "core:os/os2" -import "core:path/filepath" -import "core:sys/windows" -import "core:strings" - -/* -TODO: Simplify pre-allocation. Just allocate a bunch. It's probably fine. - Maybe do it by just counting how many lines are longer than 2 characters. -TODO: Drag-n-drop window if no files are specified -*/ - -VERBOSE :: false -INCLUDE_DATE :: false // By default I delete the retarded date field that says what day the report was generated. - -PART_ONE :: #load("parts/start.html", string) -PART_TWO :: #load("parts/start2.html", string) -PART_END :: #load("parts/end.html", string) - -Device :: enum { - UNSET, - ZOOM, - SD6, - SD8, // Tested with 888 -} - -Stages :: enum { - TITLE, - INFO, - HEADER, - BODY, -} - -Info_Line :: struct { - field : string, - entry : string, -} - -Report :: struct { - title : string, - info_lines : []Info_Line, - header : []string, - table : [][]string, - column_count : int, - row_count : int, - info_line_count : int, - tc_column_index : int, -} - -main :: proc() { - when ODIN_OS == .Windows { - windows.SetConsoleOutputCP(windows.CODEPAGE.UTF8) - } - - input_file_name : string - if len(os.args) < 2 { - fmt.println("ERROR: No file submitted.") - return - } else { - fmt.printf("Input path: {}\n", os.args[1]) - input_file_name = os.args[1] - } - - - path_info, error := os.stat(input_file_name) - - file_count := 1 - files_done := 0 - if error == os.ERROR_NONE { - - file_list : [dynamic]string - if(path_info.is_dir) { - fmt.printf("Directory submitted! Walking directory...\n\n") - fmt.printf("šŸ“ {}\n", path_info.name) - try_os2 := walk_directory(path_info.fullpath, &file_count, &file_list, 1) - if len(file_list) == 0 && try_os2 { - fmt.printf("\nNot_Dir error encountered. Trying os2 version...\n\n") - fmt.printf("šŸ“ {}\n", path_info.name) - walk_directory_os2(path_info.fullpath, &file_count, &file_list, 1) - } - } else { - fmt.println("File submitted! Processing file...") - append(&file_list, strings.clone(path_info.fullpath)) - } - - for file, f in file_list { - - file_info, _ := os.stat(file) - fmt.printf("\nšŸ“„ File {}: {}\n", f+1, file_info.name) - parsed, ok_parse := parse(file_info.fullpath) - if !ok_parse { - fmt.printf("Parse failed: {}\n", file_info.fullpath) - continue - } - output_name := fmt.aprintf("{}/{}_Knekt_Lydrapport.html", filepath.dir(file_info.fullpath), parsed.title, allocator=context.temp_allocator) - render(parsed, output_name) - free_all(context.temp_allocator) - files_done += 1 - } - fmt.printf("\nCompleted {}/{} files.\n\n", files_done, len(file_list)) - } else { - fmt.printf("ERROR could not get path info for: {}\n", input_file_name) - } - -} - - - - -parse :: proc(path : string, device : Device = .UNSET) -> (Report, bool) { - device := device - output : Report = {} - data, ok := os.read_entire_file(path, context.temp_allocator) - if !ok { - fmt.printf("ERROR: Could not read file: {}\n", path) - return {}, false - } - file_info, _ := os.lstat(path, context.temp_allocator) - - - lines := strings.split_lines(string(data), allocator=context.temp_allocator) - - - // STAGE 1 -------------------------------------------------------------- - // First, we detect what kind of sound report this is - for line, line_number in lines[:3] { - if (device!=.UNSET) { break } - if line == "\"SOUND REPORT\"," { - device = .ZOOM - if VERBOSE do fmt.printf("Detected ZOOM from quotes and comma on line index {}\n", line_number) - } - if line == "\"ZOOM F8\"," { - device = .ZOOM - if VERBOSE do fmt.printf("Detected ZOOM from \"ZOOM F8\" on line index {}\n", line_number) - } - if line == "SOUND REPORT" { - device = .SD6 - if VERBOSE do fmt.printf("Detected SOUND_DEVICES from unquoted SOUND REPORT line index {}\n", line_number) - } - if len(line)<15 do continue - if line[:13] == "SOUND REPORT," { - device = .SD8 - if VERBOSE do fmt.printf("Detected SOUND_DEVICES 8-series from SOUND REPORT with missing newline on line index {}\n", line_number) - } - } - - if device == .UNSET { - fmt.printf("ERROR: Unable to detect sound report type!\n") - return {}, false - } - - - - // STAGE 2 -------------------------------------------------------------- - // Measuring content for allocation - - switch device { - case .ZOOM: - output.column_count = 21 // Ugly magic number, could be fucked by firmware update - - // Padded for expanding info lines from unchanging columns - output.info_lines = make([]Info_Line, 2+output.column_count, context.temp_allocator) - output.info_line_count = 2 - - output.row_count = strings.count(string(data), "\n") - 7 // Ugly magic number, could be fucked by firmware update - output.table = make([][]string, output.row_count, context.temp_allocator) - output.header = make([]string, output.column_count, context.temp_allocator) - for &row in output.table { - row = make([]string, output.column_count, context.temp_allocator) - } - case .SD6: - second_to_last_line := lines[len(lines)-2] - output.column_count = strings.count(second_to_last_line, ",") - count_stage : Stages = .TITLE - for line, l in lines { - switch count_stage { - case .TITLE: - if l == 1 { // Ugly magic number, could be fucked by firmware update - count_stage = .INFO - } - case .INFO: - if line == "," { - count_stage = .HEADER - continue - } else if len(line) > 2 { - output.info_line_count += 1 - } - case .HEADER: - if line == "" { - count_stage = .BODY - } - case .BODY: - if len(line)>2 { - output.row_count += 1 - } - } - } - output.info_lines = make([]Info_Line, output.info_line_count+output.column_count, context.temp_allocator) - output.header = make([]string, output.column_count, context.temp_allocator) - output.table = make([][]string, output.row_count, context.temp_allocator) - for &row in output.table { - row = make([]string, output.column_count, context.temp_allocator) - } - - case .SD8: - count_stage : Stages = .INFO - for line, l in lines { - #partial switch count_stage { - case .INFO: - if line == "," { - count_stage = .HEADER - continue - } else if len(line) > 2 { - output.info_line_count += 1 - } - case .HEADER: - if len(line) > 2 { - // Missing comma at the en in 8-series report, v therefore + 1 - output.column_count = strings.count(line, ",") + 1 - count_stage = .BODY - } - case .BODY: - if len(line)>2 { - output.row_count += 1 - } - } - } - output.info_lines = make([]Info_Line, output.info_line_count+output.column_count, context.temp_allocator) - output.header = make([]string, output.column_count, context.temp_allocator) - output.table = make([][]string, output.row_count, context.temp_allocator) - for &row in output.table { - row = make([]string, output.column_count, context.temp_allocator) - } - - case .UNSET: - unreachable() - } - - - // STAGE 3 -------------------------------------------------------------- - // Filling with data - - if VERBOSE do fmt.printf("Struct before main parse:\n%#v\n", output) - - first_channel_index := -1 - last_channel_index := -1 - - stage : Stages = .TITLE - #partial switch device { - case .SD8: - // .d8888b. 8888888b. .d8888b. - // d88P Y88b 888 "Y88b d88P Y88b - // Y88b. 888 888 Y88b. d88P - // "Y888b. 888 888 "Y88888" - // "Y88b. 888 888 .d8P""Y8b. - // "888 888 888 888 888 - // Y88b d88P 888 .d88P Y88b d88P - // "Y8888P" 8888888P" "Y8888P" - fmt.printf("Parsing [{}] as Sound Devices 8XX report, ", file_info.name) - if strings.contains(file_info.name, "_1.CSV") || strings.contains(file_info.name, "_2.CSV") { - output.title = file_info.name[7:len(file_info.name)-6] - } else { - output.title = file_info.name - } - fmt.printf("titled \"{}\".\n", output.title) - - info_line_index := 0 - body_line_index := 0 - for line, line_index in lines { - switch stage { - case .TITLE: - // Missing newline on 8-series means we get info on the title line - stage = .INFO - - line_elements := strings.split(line, ",") - if VERBOSE do fmt.printf(".INFO {}: {}\n", line_index, line_elements) - field := fmt.aprintf("{}:", line_elements[1], allocator=context.temp_allocator) - entry := line_elements[2] - output.info_lines[info_line_index].field = field - output.info_lines[info_line_index].entry = entry - info_line_index += 1 - - - case .INFO: - if line == "," { - stage = .HEADER - continue - } - line_elements := strings.split(line, ",") - if VERBOSE do fmt.printf(".INFO {}: {}\n", line_index, line_elements) - if line_elements[0] == "Date" { - if VERBOSE do fmt.printf("Skipping line {}, because it's the retarded date field on an 8-series\n", line_index) - output.info_line_count -= 1 - continue - } - field := fmt.aprintf("{}:", line_elements[0], allocator=context.temp_allocator) - entry := line_elements[1] - output.info_lines[info_line_index].field = field - output.info_lines[info_line_index].entry = entry - info_line_index += 1 - - - case .HEADER: - if line == "," { - continue // This is here because there are a bunch of lines that are just commas before the header - } else if len(line)>3 { - if VERBOSE do fmt.printf(".HEADER {}:", line_index) - // No trailing comma in the header?? - for element, e in strings.split(line, ",") { - if VERBOSE do fmt.printf(" {}", element) - - output.header[e] = element - if element[:3] == "Trk" { - if first_channel_index == -1 do first_channel_index = e - last_channel_index = e - output.header[e] = fmt.aprintf("Trk {}", e-first_channel_index+1, allocator=context.temp_allocator) - } - if element == "Start TC" { - output.tc_column_index = e - } - } - - if VERBOSE do fmt.printf("\n") - if VERBOSE do fmt.printf("first_channel_index: {}\n", first_channel_index) - if VERBOSE do fmt.printf("last_channel_index: {}\n", last_channel_index) - stage = .BODY - } - - - case .BODY: - if len(line) > 2 { - if VERBOSE do fmt.printf(".BODY {}:", line_index) - for element, e in strings.split(line, ",") { - if VERBOSE do fmt.printf(" {}", element) - entry : string = element - output.table[body_line_index][e] = entry - } - if VERBOSE do fmt.printf("\n") - body_line_index += 1 - } - - - } - } - - fmt.printf("\n") - - - - - case .SD6: - // .d8888b. 8888888b. .d8888b. - // d88P Y88b 888 "Y88b d88P Y88b - // Y88b. 888 888 888 - // "Y888b. 888 888 888d888b. - // "Y88b. 888 888 888P "Y88b - // "888 888 888 888 888 - // Y88b d88P 888 .d88P Y88b d88P - // "Y8888P" 8888888P" "Y8888P" - fmt.printf("Parsing [{}] as Sound Devices 6XX report, ", file_info.name) - if file_info.name[len(file_info.name)-11:len(file_info.name)-3] == "_Report." { - output.title = file_info.name[:len(file_info.name)-11] - } else { - output.title = file_info.name - } - fmt.printf("titled \"{}\".\n", output.title) - - info_line_index := 0 - body_line_index := 0 - for line, line_index in lines { - switch stage { - case .TITLE: - if line_index == 1 { // Ugly magic number, could be fucked by firmware update - stage = .INFO - } - - case .INFO: - if line == "," { - stage = .HEADER - continue - } - line_elements := strings.split(line, ",") - if VERBOSE do fmt.printf(".INFO {}: {}\n", line_index, line_elements) - field := line_elements[0] - entry_raw := line_elements[1] - entry := line_elements[1][1:len(entry_raw)-1] - output.info_lines[info_line_index].field = field - output.info_lines[info_line_index].entry = entry - info_line_index += 1 - - - case .HEADER: - if line == "," { - // This is here because there are a bunch of lines that are just commas before the header - } else if len(line)>3 { - if VERBOSE do fmt.printf(".HEADER {}:", line_index) - // No trailing comma in the header?? - for element, e in strings.split(line, ",") { - if VERBOSE do fmt.printf(" {}", element) - - output.header[e] = element - 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 - } - } - - if VERBOSE do fmt.printf("\n") - } else if line == "" { - stage = .BODY - if VERBOSE do fmt.printf("first_channel_index: {}\n", first_channel_index) - if VERBOSE do fmt.printf("last_channel_index: {}\n", last_channel_index) - } - - - case .BODY: - if len(line) > 2 { - if VERBOSE do fmt.printf(".BODY {}:", line_index) - // to skip empty entry after trailing comma we do a silly slice - for element, e in strings.split(line, ",")[:output.column_count] { - if VERBOSE do fmt.printf(" {}", element) - entry : string = element - // Stripping quotes if after tracks begin - if e >= first_channel_index && (len(element)>0) { - entry = element[1:len(element)-1] - } - output.table[body_line_index][e] = entry - } - if VERBOSE do fmt.printf("\n") - body_line_index += 1 - } - - - } - } - - - case .ZOOM: - // 8888888888 .d8888b. - // 888 d88P Y88b - // 888 Y88b. d88P - // 8888888 "Y88888" - // 888 .d8P""Y8b. - // 888 888 888 - // 888 Y88b d88P - // 888 "Y8888P" - fmt.printf("Parsing [{}] as ZOOM report, ", file_info.name) - - // Getting title - if file_info.name[:8] == "F8n Pro_" { - output.title = file_info.name[8:len(file_info.name)-4] - } else if file_info.name[:4] == "F8n_" { // I don't own one, so I don't know if this is what the F8n does - output.title = file_info.name[4:len(file_info.name)-4] - } else if file_info.name[:3] == "F8_" { // TODO: Verify this is what the original F8 does - output.title = file_info.name[4:len(file_info.name)-4] - } else { - output.title = file_info.name - } - fmt.printf("titled \"{}\".\n", output.title) - - info_line_index := 0 - body_line_index := 0 - for line, line_index in lines { - switch stage { - case .TITLE: - if line_index == 1 { // Ugly magic number, could be fucked by firmware update - stage = .INFO - } - - case .INFO: - if line == "" { - stage = .HEADER - continue - } - line_elements := strings.split(line, ",") - if VERBOSE do fmt.printf(".INFO {}: {}\n", line_index, line_elements) - field_raw := line_elements[0] - entry_raw := line_elements[1] - field := line_elements[0][1:len(field_raw)-1] - entry := line_elements[1][1:len(entry_raw)-1] - output.info_lines[info_line_index].field = field - output.info_lines[info_line_index].entry = entry - info_line_index += 1 - - case .HEADER: - if VERBOSE do fmt.printf(".HEADER {}:", line_index) - // to skip empty entry after trailing comma we do a silly slice - for element, e in strings.split(line, ",")[:output.column_count] { - if VERBOSE do fmt.printf(" {}", element) - output.header[e] = element[1:len(element)-1] - - if element[:4] == "\"Tr " { - if first_channel_index==-1 do first_channel_index = e - last_channel_index = e - output.header[e] = fmt.aprintf("Trk {}", e-first_channel_index+1, allocator=context.temp_allocator) - } - if element == "\"Start TC\"" { - output.tc_column_index = e - } - } - if VERBOSE do fmt.printf("\n") - stage = .BODY - if VERBOSE do fmt.printf("first_channel_index: {}\n", first_channel_index) - if VERBOSE do fmt.printf("last_channel_index: {}\n", last_channel_index) - - case .BODY: - if line == "" do break - if VERBOSE do fmt.printf(".BODY {}:", line_index) - // to skip empty entry after trailing comma we do a silly slice - for element, e in strings.split(line, ",")[:output.column_count] { - if VERBOSE do fmt.printf(" {}", element) - output.table[body_line_index][e] = element[1:len(element)-1] - } - if VERBOSE do fmt.printf("\n") - body_line_index += 1 - } - } - } - - - - - // STAGE 4 -------------------------------------------------------------- - // Cleanup! - if VERBOSE do fmt.printf("Struct before cleanup:\n%#v\n", output) - - // Stacking tracks to the left - for &line, l in output.table { - stacking_index := first_channel_index - for &field, f in line[first_channel_index:last_channel_index+1] { - if field != "" { - line[stacking_index] = field - stacking_index += 1 - } - } - for &field, f in line[stacking_index:last_channel_index+1] { - field = "" - } - } - - - // Cleaning out unused columns - touched := make([]bool, output.column_count, context.temp_allocator) - // Finding them - for line, l in output.table { - for field, f in line { - if touched[f] do continue - if field != "" { - touched[f] = true - } - } - } - - // Turning unchanging columns into info line - changed := make([]bool, output.column_count, context.temp_allocator) - prev_line : []string = nil - for line, l in output.table { - if l>0 { - prev_line = output.table[l - 1] - for field, f in line { - if (prev_line[f] != field) || - (first_channel_index <= f && f <= last_channel_index) || - (f == output.tc_column_index) { - changed[f] = true - } - } - } - } - for did_change, i in changed { - if (!did_change) && touched[i] { - field := fmt.aprintf("{}: ", output.header[i], allocator=context.temp_allocator) - entry := prev_line[i] - output.info_lines[output.info_line_count] = {field=field, entry=entry} - output.info_line_count += 1 - } - } - - - // Removing unused and static - for &line, l in output.table { - stacking_index := 0 - for &field, f in line { - if touched[f] && changed[f] { - line[stacking_index] = field - stacking_index += 1 - } - } - for &field, f in line[stacking_index:] { - field = "" - } - } - stacking_index := 0 - for &field, f in output.header { - if touched[f] && changed[f] { - output.header[stacking_index] = field - stacking_index += 1 - } - } - for &field, f in output.header[stacking_index:] { - field = "" - } - - output.column_count = stacking_index - - if VERBOSE do fmt.printf("Struct before output:\n%#v\n", output) - - return output, true -} - - -render :: proc(report : Report, path : string) { - // Now we output the HTML. - - builder := strings.builder_make(context.temp_allocator) - - strings.write_string(&builder, PART_ONE) - strings.write_string(&builder, report.title) - strings.write_string(&builder, " - Lydrapport") - strings.write_string(&builder, PART_TWO) - - - for line, l in report.info_lines[:report.info_line_count] { - strings.write_string(&builder, "

") - strings.write_string(&builder, line.field) - strings.write_string(&builder, " ") - strings.write_string(&builder, line.entry) - strings.write_string(&builder, "

\n") - } - strings.write_string(&builder, " \n \n \n \n \n") - - for field, f in report.header[:report.column_count] { - if f != report.tc_column_index { - strings.write_string(&builder, " \n") - } - - strings.write_string(&builder, " \n \n \n") - - for line, l in report.table { - strings.write_string(&builder, " \n") - for field, f in line[:report.column_count] { - strings.write_string(&builder, " \n") - - } - strings.write_string(&builder, " \n") - } - - strings.write_string(&builder, PART_END) - - output_text := strings.to_string(builder) - os.write_entire_file(path, transmute([]u8)output_text) - - fmt.printf("Output: {}\n", path) -} - - - - -indent_by :: proc(i : int) { - for x in 0.. bool { - handle, ok := os.open(path) - if ok != os.ERROR_NONE { - indent_by(depth) - fmt.printf("ERROR opening dir: %s\n", path) - return false - } - defer os.close(handle) - - files, okr := os.read_dir(handle, -1, context.temp_allocator) - if okr != os.ERROR_NONE { - indent_by(depth) - fmt.printf("ERROR [{}] reading dir: %s\n", okr, path) - if okr == os.ERROR_FILE_IS_NOT_DIR do return true - return true - } - - - for file in files { - - full_path := file.fullpath - - if file.is_dir { - indent_by(depth) - fmt.printf("šŸ“ %s\n", file.name) - walk_directory(full_path, file_number, file_list, depth+1) // Recurse - - } else { // If file is actually a file - - extension := strings.to_lower(filepath.ext(file.name)) - defer delete(extension) - if(extension == ".csv"){ - indent_by(depth) - fmt.printf("šŸ“„ %d %s\n", file_number^, file.name) - append(file_list, strings.clone(file.fullpath)) - file_number^ += 1 - } - } - } - return false -} - -walk_directory_os2 :: proc(path : string, file_number : ^int, file_list : ^[dynamic]string, depth : int = 0) { - handle, ok := os2.open(path) - if ok != os2.ERROR_NONE { - indent_by(depth) - fmt.printf("ERROR opening dir: %s\n", path) - return - } - defer os2.close(handle) - - files, okr := os2.read_dir(handle, -1, context.temp_allocator) - if okr != os2.ERROR_NONE { - indent_by(depth) - fmt.printf("ERROR [{}] reading dir: %s\n", okr, path) - return - } - - - for file in files { - - full_path := file.fullpath - - if os.is_dir(full_path) { - indent_by(depth) - fmt.printf("šŸ“ %s\n", file.name) - walk_directory_os2(full_path, file_number, file_list, depth+1) // Recurse - - } else { // If file is actually a file - - extension := strings.to_lower(filepath.ext(file.name)) - defer delete(extension) - if(extension == ".csv"){ - indent_by(depth) - fmt.printf("šŸ“„ %d %s\n", file_number^, file.name) - append(file_list, strings.clone(file.fullpath)) - file_number^ += 1 - } - } - } -} \ No newline at end of file diff --git a/parts/end.html b/parts/end.html deleted file mode 100755 index cc51027..0000000 --- a/parts/end.html +++ /dev/null @@ -1,46 +0,0 @@ - -
") - } else { - strings.write_string(&builder, " ") - } - strings.write_string(&builder, field) - strings.write_string(&builder, "
") - strings.write_string(&builder, field) - strings.write_string(&builder, "
- - - - - - diff --git a/parts/start.html b/parts/start.html deleted file mode 100755 index a1e9809..0000000 --- a/parts/start.html +++ /dev/null @@ -1,9 +0,0 @@ - - - - - - - - - \ No newline at end of file diff --git a/parts/start2.html b/parts/start2.html deleted file mode 100755 index d7de36f..0000000 --- a/parts/start2.html +++ /dev/null @@ -1,103 +0,0 @@ - - - - - - - - - - - - - - - -
-
-
- - - - - - - - - - - -

Lydrapport

-
Version 0.9
-
-
diff --git a/src/main.odin b/src/main.odin new file mode 100644 index 0000000..71ae6ed --- /dev/null +++ b/src/main.odin @@ -0,0 +1,758 @@ +package main + +import "core:fmt" +import "core:os" +import "core:os/os2" +import "core:path/filepath" +import "core:sys/windows" +import "core:strings" + +/* +TODO: Simplify pre-allocation. Just allocate a bunch. It's probably fine. + Maybe do it by just counting how many lines are longer than 2 characters. +TODO: Drag-n-drop window if no files are specified +*/ + +VERBOSE :: false +INCLUDE_DATE :: false // By default I delete the retarded date field that says what day the report was generated. + +PART_ONE :: #load("parts/start.html", string) +PART_TWO :: #load("parts/start2.html", string) +PART_END :: #load("parts/end.html", string) + +Device :: enum { + UNSET, + ZOOM, + SD6, + SD8, // Tested with 888 +} + +Stages :: enum { + TITLE, + INFO, + HEADER, + BODY, +} + +Info_Line :: struct { + field : string, + entry : string, +} + +Report :: struct { + title : string, + info_lines : []Info_Line, + header : []string, + table : [][]string, + column_count : int, + row_count : int, + info_line_count : int, + tc_column_index : int, +} + +main :: proc() { + when ODIN_OS == .Windows { + windows.SetConsoleOutputCP(windows.CODEPAGE.UTF8) + } + + input_file_name : string + if len(os.args) < 2 { + fmt.println("ERROR: No file submitted.") + return + } else { + fmt.printf("Input path: {}\n", os.args[1]) + input_file_name = os.args[1] + } + + + path_info, error := os.stat(input_file_name) + + file_count := 1 + files_done := 0 + if error == os.ERROR_NONE { + + file_list : [dynamic]string + if(path_info.is_dir) { + fmt.printf("Directory submitted! Walking directory...\n\n") + fmt.printf("šŸ“ {}\n", path_info.name) + try_os2 := walk_directory(path_info.fullpath, &file_count, &file_list, 1) + if len(file_list) == 0 && try_os2 { + fmt.printf("\nNot_Dir error encountered. Trying os2 version...\n\n") + fmt.printf("šŸ“ {}\n", path_info.name) + walk_directory_os2(path_info.fullpath, &file_count, &file_list, 1) + } + } else { + fmt.println("File submitted! Processing file...") + append(&file_list, strings.clone(path_info.fullpath)) + } + + for file, f in file_list { + + file_info, _ := os.stat(file) + fmt.printf("\nšŸ“„ File {}: {}\n", f+1, file_info.name) + parsed, ok_parse := parse(file_info.fullpath) + if !ok_parse { + fmt.printf("Parse failed: {}\n", file_info.fullpath) + continue + } + output_name := fmt.aprintf("{}/{}_Knekt_Lydrapport.html", filepath.dir(file_info.fullpath), parsed.title, allocator=context.temp_allocator) + render(parsed, output_name) + free_all(context.temp_allocator) + files_done += 1 + } + fmt.printf("\nCompleted {}/{} files.\n\n", files_done, len(file_list)) + } else { + fmt.printf("ERROR could not get path info for: {}\n", input_file_name) + } + +} + + + + +parse :: proc(path : string, device : Device = .UNSET) -> (Report, bool) { + device := device + output : Report = {} + data, ok := os.read_entire_file(path, context.temp_allocator) + if !ok { + fmt.printf("ERROR: Could not read file: {}\n", path) + return {}, false + } + file_info, _ := os.lstat(path, context.temp_allocator) + + + lines := strings.split_lines(string(data), allocator=context.temp_allocator) + + + // STAGE 1 -------------------------------------------------------------- + // First, we detect what kind of sound report this is + for line, line_number in lines[:3] { + if (device!=.UNSET) { break } + if line == "\"SOUND REPORT\"," { + device = .ZOOM + if VERBOSE do fmt.printf("Detected ZOOM from quotes and comma on line index {}\n", line_number) + } + if line == "\"ZOOM F8\"," { + device = .ZOOM + if VERBOSE do fmt.printf("Detected ZOOM from \"ZOOM F8\" on line index {}\n", line_number) + } + if line == "SOUND REPORT" { + device = .SD6 + if VERBOSE do fmt.printf("Detected SOUND_DEVICES from unquoted SOUND REPORT line index {}\n", line_number) + } + if len(line)<15 do continue + if line[:13] == "SOUND REPORT," { + device = .SD8 + if VERBOSE do fmt.printf("Detected SOUND_DEVICES 8-series from SOUND REPORT with missing newline on line index {}\n", line_number) + } + } + + if device == .UNSET { + fmt.printf("ERROR: Unable to detect sound report type!\n") + return {}, false + } + + + + // STAGE 2 -------------------------------------------------------------- + // Measuring content for allocation + + switch device { + case .ZOOM: + output.column_count = 21 // Ugly magic number, could be fucked by firmware update + + // Padded for expanding info lines from unchanging columns + output.info_lines = make([]Info_Line, 2+output.column_count, context.temp_allocator) + output.info_line_count = 2 + + output.row_count = strings.count(string(data), "\n") - 7 // Ugly magic number, could be fucked by firmware update + output.table = make([][]string, output.row_count, context.temp_allocator) + output.header = make([]string, output.column_count, context.temp_allocator) + for &row in output.table { + row = make([]string, output.column_count, context.temp_allocator) + } + case .SD6: + second_to_last_line := lines[len(lines)-2] + output.column_count = strings.count(second_to_last_line, ",") + count_stage : Stages = .TITLE + for line, l in lines { + switch count_stage { + case .TITLE: + if l == 1 { // Ugly magic number, could be fucked by firmware update + count_stage = .INFO + } + case .INFO: + if line == "," { + count_stage = .HEADER + continue + } else if len(line) > 2 { + output.info_line_count += 1 + } + case .HEADER: + if line == "" { + count_stage = .BODY + } + case .BODY: + if len(line)>2 { + output.row_count += 1 + } + } + } + output.info_lines = make([]Info_Line, output.info_line_count+output.column_count, context.temp_allocator) + output.header = make([]string, output.column_count, context.temp_allocator) + output.table = make([][]string, output.row_count, context.temp_allocator) + for &row in output.table { + row = make([]string, output.column_count, context.temp_allocator) + } + + case .SD8: + count_stage : Stages = .INFO + for line, l in lines { + #partial switch count_stage { + case .INFO: + if line == "," { + count_stage = .HEADER + continue + } else if len(line) > 2 { + output.info_line_count += 1 + } + case .HEADER: + if len(line) > 2 { + // Missing comma at the en in 8-series report, v therefore + 1 + output.column_count = strings.count(line, ",") + 1 + count_stage = .BODY + } + case .BODY: + if len(line)>2 { + output.row_count += 1 + } + } + } + output.info_lines = make([]Info_Line, output.info_line_count+output.column_count, context.temp_allocator) + output.header = make([]string, output.column_count, context.temp_allocator) + output.table = make([][]string, output.row_count, context.temp_allocator) + for &row in output.table { + row = make([]string, output.column_count, context.temp_allocator) + } + + case .UNSET: + unreachable() + } + + + // STAGE 3 -------------------------------------------------------------- + // Filling with data + + if VERBOSE do fmt.printf("Struct before main parse:\n%#v\n", output) + + first_channel_index := -1 + last_channel_index := -1 + + stage : Stages = .TITLE + #partial switch device { + case .SD8: + // .d8888b. 8888888b. .d8888b. + // d88P Y88b 888 "Y88b d88P Y88b + // Y88b. 888 888 Y88b. d88P + // "Y888b. 888 888 "Y88888" + // "Y88b. 888 888 .d8P""Y8b. + // "888 888 888 888 888 + // Y88b d88P 888 .d88P Y88b d88P + // "Y8888P" 8888888P" "Y8888P" + fmt.printf("Parsing [{}] as Sound Devices 8XX report, ", file_info.name) + if strings.contains(file_info.name, "_1.CSV") || strings.contains(file_info.name, "_2.CSV") { + output.title = file_info.name[7:len(file_info.name)-6] + } else { + output.title = file_info.name + } + fmt.printf("titled \"{}\".\n", output.title) + + info_line_index := 0 + body_line_index := 0 + for line, line_index in lines { + switch stage { + case .TITLE: + // Missing newline on 8-series means we get info on the title line + stage = .INFO + + line_elements := strings.split(line, ",") + if VERBOSE do fmt.printf(".INFO {}: {}\n", line_index, line_elements) + field := fmt.aprintf("{}:", line_elements[1], allocator=context.temp_allocator) + entry := line_elements[2] + output.info_lines[info_line_index].field = field + output.info_lines[info_line_index].entry = entry + info_line_index += 1 + + + case .INFO: + if line == "," { + stage = .HEADER + continue + } + line_elements := strings.split(line, ",") + if VERBOSE do fmt.printf(".INFO {}: {}\n", line_index, line_elements) + if line_elements[0] == "Date" { + if VERBOSE do fmt.printf("Skipping line {}, because it's the retarded date field on an 8-series\n", line_index) + output.info_line_count -= 1 + continue + } + field := fmt.aprintf("{}:", line_elements[0], allocator=context.temp_allocator) + entry := line_elements[1] + output.info_lines[info_line_index].field = field + output.info_lines[info_line_index].entry = entry + info_line_index += 1 + + + case .HEADER: + if line == "," { + continue // This is here because there are a bunch of lines that are just commas before the header + } else if len(line)>3 { + if VERBOSE do fmt.printf(".HEADER {}:", line_index) + // No trailing comma in the header?? + for element, e in strings.split(line, ",") { + if VERBOSE do fmt.printf(" {}", element) + + output.header[e] = element + if element[:3] == "Trk" { + if first_channel_index == -1 do first_channel_index = e + last_channel_index = e + output.header[e] = fmt.aprintf("Trk {}", e-first_channel_index+1, allocator=context.temp_allocator) + } + if element == "Start TC" { + output.tc_column_index = e + } + } + + if VERBOSE do fmt.printf("\n") + if VERBOSE do fmt.printf("first_channel_index: {}\n", first_channel_index) + if VERBOSE do fmt.printf("last_channel_index: {}\n", last_channel_index) + stage = .BODY + } + + + case .BODY: + if len(line) > 2 { + if VERBOSE do fmt.printf(".BODY {}:", line_index) + for element, e in strings.split(line, ",") { + if VERBOSE do fmt.printf(" {}", element) + entry : string = element + output.table[body_line_index][e] = entry + } + if VERBOSE do fmt.printf("\n") + body_line_index += 1 + } + + + } + } + + fmt.printf("\n") + + + + + case .SD6: + // .d8888b. 8888888b. .d8888b. + // d88P Y88b 888 "Y88b d88P Y88b + // Y88b. 888 888 888 + // "Y888b. 888 888 888d888b. + // "Y88b. 888 888 888P "Y88b + // "888 888 888 888 888 + // Y88b d88P 888 .d88P Y88b d88P + // "Y8888P" 8888888P" "Y8888P" + fmt.printf("Parsing [{}] as Sound Devices 6XX report, ", file_info.name) + if file_info.name[len(file_info.name)-11:len(file_info.name)-3] == "_Report." { + output.title = file_info.name[:len(file_info.name)-11] + } else { + output.title = file_info.name + } + fmt.printf("titled \"{}\".\n", output.title) + + info_line_index := 0 + body_line_index := 0 + for line, line_index in lines { + switch stage { + case .TITLE: + if line_index == 1 { // Ugly magic number, could be fucked by firmware update + stage = .INFO + } + + case .INFO: + if line == "," { + stage = .HEADER + continue + } + line_elements := strings.split(line, ",") + if VERBOSE do fmt.printf(".INFO {}: {}\n", line_index, line_elements) + field := line_elements[0] + entry_raw := line_elements[1] + entry := line_elements[1][1:len(entry_raw)-1] + output.info_lines[info_line_index].field = field + output.info_lines[info_line_index].entry = entry + info_line_index += 1 + + + case .HEADER: + if line == "," { + // This is here because there are a bunch of lines that are just commas before the header + } else if len(line)>3 { + if VERBOSE do fmt.printf(".HEADER {}:", line_index) + // No trailing comma in the header?? + for element, e in strings.split(line, ",") { + if VERBOSE do fmt.printf(" {}", element) + + output.header[e] = element + 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 + } + } + + if VERBOSE do fmt.printf("\n") + } else if line == "" { + stage = .BODY + if VERBOSE do fmt.printf("first_channel_index: {}\n", first_channel_index) + if VERBOSE do fmt.printf("last_channel_index: {}\n", last_channel_index) + } + + + case .BODY: + if len(line) > 2 { + if VERBOSE do fmt.printf(".BODY {}:", line_index) + // to skip empty entry after trailing comma we do a silly slice + for element, e in strings.split(line, ",")[:output.column_count] { + if VERBOSE do fmt.printf(" {}", element) + entry : string = element + // Stripping quotes if after tracks begin + if e >= first_channel_index && (len(element)>0) { + entry = element[1:len(element)-1] + } + output.table[body_line_index][e] = entry + } + if VERBOSE do fmt.printf("\n") + body_line_index += 1 + } + + + } + } + + + case .ZOOM: + // 8888888888 .d8888b. + // 888 d88P Y88b + // 888 Y88b. d88P + // 8888888 "Y88888" + // 888 .d8P""Y8b. + // 888 888 888 + // 888 Y88b d88P + // 888 "Y8888P" + fmt.printf("Parsing [{}] as ZOOM report, ", file_info.name) + + // Getting title + if file_info.name[:8] == "F8n Pro_" { + output.title = file_info.name[8:len(file_info.name)-4] + } else if file_info.name[:4] == "F8n_" { // I don't own one, so I don't know if this is what the F8n does + output.title = file_info.name[4:len(file_info.name)-4] + } else if file_info.name[:3] == "F8_" { // TODO: Verify this is what the original F8 does + output.title = file_info.name[4:len(file_info.name)-4] + } else { + output.title = file_info.name + } + fmt.printf("titled \"{}\".\n", output.title) + + info_line_index := 0 + body_line_index := 0 + for line, line_index in lines { + switch stage { + case .TITLE: + if line_index == 1 { // Ugly magic number, could be fucked by firmware update + stage = .INFO + } + + case .INFO: + if line == "" { + stage = .HEADER + continue + } + line_elements := strings.split(line, ",") + if VERBOSE do fmt.printf(".INFO {}: {}\n", line_index, line_elements) + field_raw := line_elements[0] + entry_raw := line_elements[1] + field := line_elements[0][1:len(field_raw)-1] + entry := line_elements[1][1:len(entry_raw)-1] + output.info_lines[info_line_index].field = field + output.info_lines[info_line_index].entry = entry + info_line_index += 1 + + case .HEADER: + if VERBOSE do fmt.printf(".HEADER {}:", line_index) + // to skip empty entry after trailing comma we do a silly slice + for element, e in strings.split(line, ",")[:output.column_count] { + if VERBOSE do fmt.printf(" {}", element) + output.header[e] = element[1:len(element)-1] + + if element[:4] == "\"Tr " { + if first_channel_index==-1 do first_channel_index = e + last_channel_index = e + output.header[e] = fmt.aprintf("Trk {}", e-first_channel_index+1, allocator=context.temp_allocator) + } + if element == "\"Start TC\"" { + output.tc_column_index = e + } + } + if VERBOSE do fmt.printf("\n") + stage = .BODY + if VERBOSE do fmt.printf("first_channel_index: {}\n", first_channel_index) + if VERBOSE do fmt.printf("last_channel_index: {}\n", last_channel_index) + + case .BODY: + if line == "" do break + if VERBOSE do fmt.printf(".BODY {}:", line_index) + // to skip empty entry after trailing comma we do a silly slice + for element, e in strings.split(line, ",")[:output.column_count] { + if VERBOSE do fmt.printf(" {}", element) + output.table[body_line_index][e] = element[1:len(element)-1] + } + if VERBOSE do fmt.printf("\n") + body_line_index += 1 + } + } + } + + + + + // STAGE 4 -------------------------------------------------------------- + // Cleanup! + if VERBOSE do fmt.printf("Struct before cleanup:\n%#v\n", output) + + // Stacking tracks to the left + for &line, l in output.table { + stacking_index := first_channel_index + for &field, f in line[first_channel_index:last_channel_index+1] { + if field != "" { + line[stacking_index] = field + stacking_index += 1 + } + } + for &field, f in line[stacking_index:last_channel_index+1] { + field = "" + } + } + + + // Cleaning out unused columns + touched := make([]bool, output.column_count, context.temp_allocator) + // Finding them + for line, l in output.table { + for field, f in line { + if touched[f] do continue + if field != "" { + touched[f] = true + } + } + } + + // Turning unchanging columns into info line + changed := make([]bool, output.column_count, context.temp_allocator) + prev_line : []string = nil + for line, l in output.table { + if l>0 { + prev_line = output.table[l - 1] + for field, f in line { + if (prev_line[f] != field) || + (first_channel_index <= f && f <= last_channel_index) || + (f == output.tc_column_index) { + changed[f] = true + } + } + } + } + for did_change, i in changed { + if (!did_change) && touched[i] { + field := fmt.aprintf("{}: ", output.header[i], allocator=context.temp_allocator) + entry := prev_line[i] + output.info_lines[output.info_line_count] = {field=field, entry=entry} + output.info_line_count += 1 + } + } + + + // Removing unused and static + for &line, l in output.table { + stacking_index := 0 + for &field, f in line { + if touched[f] && changed[f] { + line[stacking_index] = field + stacking_index += 1 + } + } + for &field, f in line[stacking_index:] { + field = "" + } + } + stacking_index := 0 + for &field, f in output.header { + if touched[f] && changed[f] { + output.header[stacking_index] = field + stacking_index += 1 + } + } + for &field, f in output.header[stacking_index:] { + field = "" + } + + output.column_count = stacking_index + + if VERBOSE do fmt.printf("Struct before output:\n%#v\n", output) + + return output, true +} + + +render :: proc(report : Report, path : string) { + // Now we output the HTML. + + builder := strings.builder_make(context.temp_allocator) + + strings.write_string(&builder, PART_ONE) + strings.write_string(&builder, report.title) + strings.write_string(&builder, " - Lydrapport") + strings.write_string(&builder, PART_TWO) + + + for line, l in report.info_lines[:report.info_line_count] { + strings.write_string(&builder, "

") + strings.write_string(&builder, line.field) + strings.write_string(&builder, " ") + strings.write_string(&builder, line.entry) + strings.write_string(&builder, "

\n") + } + strings.write_string(&builder, "
\n
\n \n \n \n") + + for field, f in report.header[:report.column_count] { + if f != report.tc_column_index { + strings.write_string(&builder, " \n") + } + + strings.write_string(&builder, " \n \n \n") + + for line, l in report.table { + strings.write_string(&builder, " \n") + for field, f in line[:report.column_count] { + strings.write_string(&builder, " \n") + + } + strings.write_string(&builder, " \n") + } + + strings.write_string(&builder, PART_END) + + output_text := strings.to_string(builder) + os.write_entire_file(path, transmute([]u8)output_text) + + fmt.printf("Output: {}\n", path) +} + + + + +indent_by :: proc(i : int) { + for x in 0.. bool { + handle, ok := os.open(path) + if ok != os.ERROR_NONE { + indent_by(depth) + fmt.printf("ERROR opening dir: %s\n", path) + return false + } + defer os.close(handle) + + files, okr := os.read_dir(handle, -1, context.temp_allocator) + if okr != os.ERROR_NONE { + indent_by(depth) + fmt.printf("ERROR [{}] reading dir: %s\n", okr, path) + if okr == os.ERROR_FILE_IS_NOT_DIR do return true + return true + } + + + for file in files { + + full_path := file.fullpath + + if file.is_dir { + indent_by(depth) + fmt.printf("šŸ“ %s\n", file.name) + walk_directory(full_path, file_number, file_list, depth+1) // Recurse + + } else { // If file is actually a file + + extension := strings.to_lower(filepath.ext(file.name)) + defer delete(extension) + if(extension == ".csv"){ + indent_by(depth) + fmt.printf("šŸ“„ %d %s\n", file_number^, file.name) + append(file_list, strings.clone(file.fullpath)) + file_number^ += 1 + } + } + } + return false +} + +walk_directory_os2 :: proc(path : string, file_number : ^int, file_list : ^[dynamic]string, depth : int = 0) { + handle, ok := os2.open(path) + if ok != os2.ERROR_NONE { + indent_by(depth) + fmt.printf("ERROR opening dir: %s\n", path) + return + } + defer os2.close(handle) + + files, okr := os2.read_dir(handle, -1, context.temp_allocator) + if okr != os2.ERROR_NONE { + indent_by(depth) + fmt.printf("ERROR [{}] reading dir: %s\n", okr, path) + return + } + + + for file in files { + + full_path := file.fullpath + + if os.is_dir(full_path) { + indent_by(depth) + fmt.printf("šŸ“ %s\n", file.name) + walk_directory_os2(full_path, file_number, file_list, depth+1) // Recurse + + } else { // If file is actually a file + + extension := strings.to_lower(filepath.ext(file.name)) + defer delete(extension) + if(extension == ".csv"){ + indent_by(depth) + fmt.printf("šŸ“„ %d %s\n", file_number^, file.name) + append(file_list, strings.clone(file.fullpath)) + file_number^ += 1 + } + } + } +} \ No newline at end of file diff --git a/src/wav/wav.odin b/src/wav/wav.odin new file mode 100644 index 0000000..04cb0cf --- /dev/null +++ b/src/wav/wav.odin @@ -0,0 +1,140 @@ +package wav + +import "core:fmt" +import "core:math" +import "core:strings" +import "core:os" + +Wav :: struct { + path : string, + handle : os.Handle, + buf : []u8, + reported_size : u32, + bext : string, + ixml : string, + format : Audio_Format, + channels : int, + channel_names : []string, + sample_rate : int, + bit_depth : int, +} +Audio_Format :: enum { + PCM = 1, + FLOAT = 3, +} + +BUFFER_SIZE :: 1<<15 + +main :: proc() { + sweden, sweden_ok := read_wav("test/sweden.wav", context.temp_allocator) + fmt.printf("\n\nsweden = %#v\n\n", sweden) + enok, enok_ok := read_wav("test/ENOKS-BIRHTDAYT02.WAV", context.temp_allocator) + fmt.printf("\n\nenok = %#v\n\n", enok) +} + + +read_wav :: proc(path : string, allocator:=context.allocator) -> (Wav, bool) { + file : Wav + + file_ok : os.Error + file.handle, file_ok = os.open(path) + defer os.close(file.handle) + assert(file_ok == os.General_Error.None) + + file.buf = new([BUFFER_SIZE]u8)[:] + defer delete(file.buf) + + os.read(file.handle, file.buf) + + + head : int = 0 + + // RIFF header + fmt.println(string(file.buf[0:4])) + if string(file.buf[0:4]) != "RIFF" do return {}, false + head += 4 + + // Size + file.reported_size = read_little_endian_u32(file.buf[head:head+4]) + fmt.println("Reported size:", file.reported_size) + head += 4 + + // Confirming again that this is a wave file + fmt.println(string(file.buf[head:head+4])) + if string(file.buf[head:head+4]) != "WAVE" do return {}, false + head += 4 + + fmt.println("\nChunks:\n") + + // Looping through chunks + for _ in 0..
") + } else { + strings.write_string(&builder, " ") + } + strings.write_string(&builder, field) + strings.write_string(&builder, "
") + strings.write_string(&builder, field) + strings.write_string(&builder, "