package main import "core:fmt" import "core:os" import "core:os/os2" import "core:path/filepath" import "core:sys/windows" import "core:strings" import "core:math" import "wav" /* 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) HEADER_TEMPLATE :: #load("header_template.txt", string) HEADER_FIELDS_PATH :: "info.txt" header_fields_file : 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 { // Content title : string, info_lines : []Info_Line, header : []string, table : [][]string, column_count : int, row_count : int, info_line_count : int, tc_column_index : int, // Meta path : string, } CSV :: string Directory :: [dynamic]string Job :: union {CSV, Directory} job_list : [dynamic]Job // TODO: Changing file_list to job_list, so the Directory jobs can contain a list of all the relevant .wav files before being sent to parse_folder() main :: proc() { when ODIN_OS == .Windows { windows.SetConsoleOutputCP(windows.CODEPAGE.UTF8) } input_file_name : string if len(os.args) < 2 { fmt.println("No paths submitted.") if os.is_file(HEADER_FIELDS_PATH) { fmt.printfln("\"%v\" already exists.", HEADER_FIELDS_PATH) } else { os.write_entire_file(HEADER_FIELDS_PATH, transmute([]u8)HEADER_TEMPLATE) fmt.printfln("Created \"%v\".", HEADER_FIELDS_PATH) } return } 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 { 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, 1) if len(job_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, 1) } } else { fmt.println("File submitted! Processing file...") append(&job_list, CSV(strings.clone(path_info.fullpath))) } for job, i in job_list { parsed : Report parse_ok : bool switch file in job { case CSV: file_info, _ := os.stat(file) fmt.printf("\nšŸ“„ File {}: {}\n", i+1, file_info.name) parsed, parse_ok = parse_file(file_info.fullpath) if !parse_ok { fmt.printf("Parse failed: {}\n", file_info.fullpath) continue } case Directory: fmt.printf("\nšŸ“ Folder {}: ", i+1) parsed, parse_ok = parse_folder(file) fmt.printf("{}", parsed.title) if parse_ok { fmt.printf("\nParsed %d WAV(s).\n", parsed.row_count) } else { file_info, _ := os.stat(file[0]) fmt.printf("\nParse failed: {}\n", file_info.fullpath) continue } } render(parsed) free_all(context.temp_allocator) files_done += 1 } fmt.printf("\nCompleted {}/{} job(s).\n\n", files_done, len(job_list)) } else { fmt.printf("ERROR could not get path info for: {}\n", input_file_name) } } parse_folder :: proc(paths : Directory) -> (Report, bool) { // 888 888 888 8888b. 888 888 // 888 888 888 "88b 888 888 // 888 888 888 .d888888 Y88 88P // d8b Y88b 888 d88P 888 888 Y8bd8P // Y8P "Y8888888P" "Y888888 Y88P output : Report = {} wavs : [dynamic]wav.Wav max_channels := 0 for path, i in paths { w, ok := wav.read(path) if ok { append(&wavs, w) max_channels = max(max_channels, w.channels) } } header_build : [dynamic]string append(&header_build, "Circled") append(&header_build, "File Name") append(&header_build, "Scene") append(&header_build, "Take") append(&header_build, "Timecode") append(&header_build, "TC FPS") append(&header_build, "User Bits") append(&header_build, "Tape") append(&header_build, "Date") append(&header_build, "Project") append(&header_build, "Sample Rate") append(&header_build, "Format") // Bit depth and int vs float first_channel_index := len(header_build) last_channel_index := -1 for i in 0..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 // Setting title for report output.title = strings.trim(filepath.base(filepath.dir(paths[0])), "/\\") for item in output.info_lines { if item.field == "Tape" { output.title = item.entry } } // Setting column to sort by for title, i in output.header { if title == "Timecode" { output.tc_column_index = i break } } when VERBOSE do fmt.printf("Struct before output:\n%#v\n", output) output.path = fmt.tprintf("{}/{}_Knekt_Lydrapport.html", filepath.dir(paths[0]), output.title) return output, true } parse_file :: proc(path : CSV, 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 when VERBOSE do fmt.printf("Detected ZOOM from quotes and comma on line index {}\n", line_number) } if line == "\"ZOOM F8\"," { device = .ZOOM when VERBOSE do fmt.printf("Detected ZOOM from \"ZOOM F8\" on line index {}\n", line_number) } if line == "SOUND REPORT" { device = .SD6 when 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 when 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 when 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, ",") when 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, ",") when VERBOSE do fmt.printf(".INFO {}: {}\n", line_index, line_elements) if line_elements[0] == "Date" { when 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 { when VERBOSE do fmt.printf(".HEADER {}:", line_index) // No trailing comma in the header?? for element, e in strings.split(line, ",") { when 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 } } when VERBOSE do fmt.printf("\n") when VERBOSE do fmt.printf("first_channel_index: {}\n", first_channel_index) when VERBOSE do fmt.printf("last_channel_index: {}\n", last_channel_index) stage = .BODY } case .BODY: if len(line) > 2 { when VERBOSE do fmt.printf(".BODY {}:", line_index) for element, e in strings.split(line, ",") { when VERBOSE do fmt.printf(" {}", element) entry : string = element output.table[body_line_index][e] = entry } when VERBOSE do fmt.printf("\n") body_line_index += 1 } } } 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, ",") when 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 { when VERBOSE do fmt.printf(".HEADER {}:", line_index) // No trailing comma in the header?? for element, e in strings.split(line, ",") { when 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 } } when VERBOSE do fmt.printf("\n") } else if line == "" { stage = .BODY when VERBOSE do fmt.printf("first_channel_index: {}\n", first_channel_index) when VERBOSE do fmt.printf("last_channel_index: {}\n", last_channel_index) } case .BODY: if len(line) > 2 { when 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] { when 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 } when 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, ",") when 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: when 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] { when 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 } } when VERBOSE do fmt.printf("\n") stage = .BODY when VERBOSE do fmt.printf("first_channel_index: {}\n", first_channel_index) when VERBOSE do fmt.printf("last_channel_index: {}\n", last_channel_index) case .BODY: if line == "" do break when 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] { when VERBOSE do fmt.printf(" {}", element) output.table[body_line_index][e] = element[1:len(element)-1] } when VERBOSE do fmt.printf("\n") body_line_index += 1 } } } // STAGE 4 -------------------------------------------------------------- // Cleanup! when 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 when VERBOSE do fmt.printf("Struct before output:\n%#v\n", output) output.path = fmt.tprintf("{}/{}_Knekt_Lydrapport.html", filepath.dir(path), output.title) return output, true } render :: proc(report : Report) { // 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(report.path, transmute([]u8)output_text) fmt.printf("Output: {}\n", report.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 } wav_files : [dynamic]string has_csv := false 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, 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(&job_list, strings.clone(file.fullpath)) file_number^ += 1 has_csv = true } if extension == ".wav" { append(&wav_files, strings.clone(full_path)) } } } wav_count := len(wav_files) if wav_count>0 && !has_csv { indent_by(depth) if wav_count == 1 { fmt.printf("šŸ’½ [#%d] A WAV file.\n", file_number^) } else { fmt.printf("šŸ’½ [#%d] %d WAV files.\n", file_number^, wav_count) } append(&job_list, wav_files) file_number^ += 1 } return false } walk_directory_os2 :: proc(path : string, file_number : ^int, 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 } wav_files : [dynamic]string has_csv := false 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, 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(&job_list, strings.clone(file.fullpath)) file_number^ += 1 has_csv = true } if extension == ".wav" { append(&wav_files, strings.clone(full_path)) } } } wav_count := len(wav_files) if wav_count>0 && !has_csv { indent_by(depth+1) if wav_count == 1 { fmt.printf("šŸ’½ [#%d] A WAV file.\n", file_number^) } else { fmt.printf("šŸ’½ [#%d] %d WAV files.\n", file_number^, wav_count) } append(&job_list, wav_files) file_number^ += 1 } }
") } else { strings.write_string(&builder, " ") } strings.write_string(&builder, field) strings.write_string(&builder, "
") strings.write_string(&builder, field) strings.write_string(&builder, "