package wav import "core:fmt" import "core:math" import "core:strings" import "core:strconv" import "core:os" import "xml" Wav :: struct { // Basic data path : string, format : Audio_Format, channels : int, sample_rate : int, bit_depth : int, reported_size : u32, // Internals handle : os.Handle, // Metadata date : Date, channel_names : []string, samples_since_midnight: u64, timecode : Timecode, // Derived from samples_since_midnight tc_framerate : f32, tc_dropframe : bool, ubits : [8]u8, take : int, project : string, scene : string, note : string, tape : string, circled : bool, } Audio_Format :: enum { INT = 1, FLOAT = 3, } Timecode :: struct { hour : u8, minute : u8, second : u8, frame : f32, } Date :: struct { year, month, day : int, } VERBOSE :: false BUFFER_SIZE :: 1<<15 main :: proc() { // Test 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) } /* 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 load_err : os.Error file.handle, load_err = os.open(path) defer os.close(file.handle) defer file.handle = 0 if load_err != os.General_Error.None { fmt.eprintln("ERROR %v: Unable to load file \"%v\"", load_err, path) return {}, false } temp_buf := new([BUFFER_SIZE]u8)[:] temp_bext : []u8 temp_ixml : string defer delete(temp_buf) os.read(file.handle, temp_buf) head : int = 0 // RIFF header when VERBOSE do fmt.println(string(temp_buf[0:4])) if string(temp_buf[0:4]) != "RIFF" do return {}, false head += 4 // Size file.reported_size = read_little_endian_u32(temp_buf[head:head+4]) when VERBOSE do fmt.println("Reported size:", file.reported_size) head += 4 // Confirming again that this is a wave file when VERBOSE do fmt.println(string(temp_buf[head:head+4])) if string(temp_buf[head:head+4]) != "WAVE" do return {}, false head += 4 when VERBOSE do fmt.println("\nChunks:\n") // Looping through chunks null_chunks := 0 for _ in 0.. 3 { when VERBOSE do fmt.println("Got more than 3 null chunks in a row. Quitting parse.") break } if data_reached { when VERBOSE do fmt.println("Data reached.") } } file.channel_names = make([]string, file.channels, allocator=allocator) // iXML Parsing if temp_ixml != "" { // Stripping null padding end := len(temp_ixml) - 1 for end >= 0 && temp_ixml[end] == 0 { end -= 1 } temp_ixml = temp_ixml[:end] naming_channel := 0 /* interleave_set is here because we don't want to overwrite naming_channel with a number from CHANNEL_INDEX, if we've already set it with INTERLEAVE_INDEX. INTERLEAVE_INDEX is the number that actually tells you which channel the name belongs to. CHANNEL_INDEX is to be treated as a backup. */ interleave_set := false xml_recurse :: proc(doc: ^xml.Document, element_id: xml.Element_ID, file: ^Wav, naming_channel: ^int, interleave_set: ^bool, allocator:=context.allocator, indent := 0) { naming_channel := naming_channel interleave_set := interleave_set tab :: proc(indent: int) { for _ in 0..=indent { fmt.printf("\t") } } when VERBOSE do fmt.printf("\n") when VERBOSE do tab(indent) element := doc.elements[element_id] if element.kind == .Element { when VERBOSE do fmt.printf("<%v>", element.ident) if len(element.value) > 0 { value := element.value[0] switch element.ident { case "TRACK": interleave_set^ = false case "CHANNEL_INDEX": if !interleave_set^ { switch v in value { case string: naming_channel^, _ = strconv.parse_int(v) case u32: naming_channel^ = int(v) } } naming_channel^ -= 1 case "INTERLEAVE_INDEX": interleave_set^ = true switch v in value { case string: naming_channel^, _ = strconv.parse_int(v) case u32: naming_channel^ = int(v) } naming_channel^ -= 1 case "NAME": #partial switch v in value { case string: file.channel_names[naming_channel^] = strings.clone(v, allocator=allocator) } case "UBITS": #partial switch v in value { case string: for r, i in v { file.ubits[i] = u8(r) - u8('0') } } case "TAKE": #partial switch v in value { case string: file.take, _ = strconv.parse_int(v) } case "SCENE": #partial switch v in value { case string: file.scene = strings.clone(v, allocator=allocator) } case "PROJECT": #partial switch v in value { case string: file.project = strings.clone(v, allocator=allocator) } case "TAPE": #partial switch v in value { case string: file.tape = strings.clone(v, allocator=allocator) } case "CIRCLED": #partial switch v in value { case string: file.circled = v == "TRUE" } case "TIMECODE_FLAG": #partial switch v in value { case string: file.tc_dropframe = v != "NDF" } case "TIMECODE_RATE": #partial switch v in value { case string: end : int for r, i in v { if r == '/' { end = i break } } file.tc_framerate, _ = strconv.parse_f32(v[:end]) } case "NOTE": #partial switch v in value { case string: if v != "" { file.note = strings.clone(v, allocator=allocator) } } } } for value in element.value { switch v in value { case string: when VERBOSE do fmt.printf(": %v", v) case xml.Element_ID: xml_recurse(doc, v, file, naming_channel, interleave_set, allocator, indent + 1) } } for attr in element.attribs { when VERBOSE do tab(indent + 1) when VERBOSE do fmt.printf("[Attr] %v: %v\n", attr.key, attr.val) } } else if element.kind == .Comment { when VERBOSE do fmt.printf("[COMMENT] %v\n", element.value) } return } parsed_ixml : ^xml.Document parsed_ixml, _ = xml.parse(temp_ixml, xml.Options{ flags={.Ignore_Unsupported}, expected_doctype = "", }) xml_recurse(parsed_ixml, 0, &file, &naming_channel, &interleave_set, allocator) } // bext parsing if len(temp_bext) > 0 { // Stripping null padding end := len(temp_bext) - 1 for end >= 0 && temp_bext[end] == 0 { end -= 1 } temp_bext = temp_bext[:end] naming_channel := 0 description := string(temp_bext[:256]) for line in strings.split_lines(description) { if file.channel_names[naming_channel] == "" && (strings.starts_with(line, "sTRK") || strings.starts_with(line, "zTRK")) { eq_index := strings.index(line, "=") file.channel_names[naming_channel] = strings.clone(line[eq_index+1:], allocator=allocator) naming_channel += 1 } if strings.starts_with(line[1:], "TAKE=") { file.take, _ = strconv.parse_int(line[6:]) } if strings.starts_with(line[1:], "CIRCLED=") { file.circled = line[9:] == "TRUE" } if strings.starts_with(line[1:], "SPEED=") { value := line[7:] num_end : int type_start : int for r, i in value { if !strings.contains_rune("1234567890.", r) && num_end == 0 { num_end = i } if r=='N' || r=='D' { type_start = i break } } file.tc_framerate, _ = strconv.parse_f32(value[:num_end]) file.tc_dropframe = value[type_start:] != "ND" } // Only if ixml doesn't exist, so we don't allocate the note string twice. if file.note == "" { if strings.starts_with(line[1:], "NOTE=") { v := line[6:] if v != "" { file.note = strings.clone(v, allocator=allocator) } } } } head := 0 when VERBOSE do fmt.printf("Description: \n%v\n", string(temp_bext[head:256])) head += 256 when VERBOSE do fmt.printf("Originator: %v\n", string(temp_bext[head:head+32])) head += 32 when VERBOSE do fmt.printf("Originator Reference: %v\n", string(temp_bext[head:head+32])) head += 32 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.month, _ = strconv.parse_int(date_splits[1]) 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 = read_little_endian_u64(temp_bext[head:head+8]) seconds_since_midnight := file.samples_since_midnight / u64(file.sample_rate) file.timecode.hour = u8( seconds_since_midnight / 3600) file.timecode.minute = u8((seconds_since_midnight % 3600) / 60) file.timecode.second = u8( seconds_since_midnight % 60) file.timecode.frame = f32( f64(file.samples_since_midnight % u64(file.sample_rate) ) * f64(file.tc_framerate) / f64(file.sample_rate)) when VERBOSE do fmt.printf("Time Reference: %v (Samples since midnight, source of timecode)\n", file.samples_since_midnight) when VERBOSE do fmt.printf(" %v seconds + %v samples\n", seconds_since_midnight, file.samples_since_midnight % u64(file.sample_rate)) head += 8 when VERBOSE do fmt.printf("Version: %v\n", read_little_endian_u16(temp_bext[head:head+2])) head += 2 when VERBOSE do fmt.printf("UMID Skipped.\n") head += 64 when VERBOSE do fmt.printf("Skipped reserved nothingness.\n") head += 190 when VERBOSE do fmt.printf("Coding history:\n%v\n", string(temp_bext[head:])) } when VERBOSE do fmt.println() // just here to make some printing prettier temp_bext = nil temp_buf = nil return file, true } read_little_endian_u64 :: proc(x : []u8) -> u64 { return u64(x[0]) | u64(x[1]) << 8 | u64(x[2]) << 16 | u64(x[3]) << 24 | u64(x[4]) << 32 | u64(x[5]) << 40 | u64(x[6]) << 48 | u64(x[7]) << 56 } read_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 { return u16(x[0]) | u16(x[1]) << 8 }