extends Node class_name MusicMeta class MusicMetadata: var error: Error var bpm: int var title: String var album: String var comments: String var year: int var artist: String var cover: ImageTexture func print_info(): print("bpm: ", bpm) print("title: ", title) print("album: ", album) print("comments: ", comments) print("year: ", year) print("cover: ", cover) print("artist: ", artist) func get_mp3_metadata(stream: AudioStreamMP3) -> MusicMetadata: var meta: MusicMetadata = MusicMetadata.new() var data: PackedByteArray = stream.data meta.error = OK if data.size() < 10: meta.error = FAILED return meta var header = data.slice(0, 10) var id = header.slice(0, 3).get_string_from_ascii() if id != "ID3": meta.error = FAILED push_error("Error: Stream data's header '%s' is not ID3."%id) return meta var v = "ID3v2.%d.%d" % [header[3], header[4]] if v == "ID3v2.4.0" or v == "ID3v2.3.0": var flags = header[5] var _unsync = flags & 0x80 > 0 var extended = flags & 0x40 > 0 var _experimental = flags & 0x20 > 0 var _has_footer = flags & 0x10 > 0 var idx = 10 var end = idx + bytes_to_int(header.slice(6, 10)) if extended: idx += bytes_to_int(data.slice(idx, idx + 4)) while idx < end: if not data: meta.error = FAILED push_error("Error: Stream data is null.") return meta var frame_id = data.slice(idx, idx + 4).get_string_from_ascii() var size = bytes_to_int(data.slice(idx + 4, idx + 8), frame_id != "APIC") # if greater than byte, not sync safe number (0b0111_1111 -> 0x7f) if size > 0x7f: size = bytes_to_int(data.slice(idx + 4, idx + 8), false) idx += 10 match frame_id: "TBPM", 'TBP': meta.bpm = int(get_string_from_data(data, idx, size)) "TIT2": print("a " + str(Array(data.slice(idx, idx + 3)).hash())) print([1, 0xff, 0xfe].hash()) meta.title = get_string_from_data(data, idx, size) "TALB", 'TAL': meta.album = get_string_from_data(data, idx, size) "COMM": meta.comments = get_string_from_data(data, idx, size) "TYER": meta.year = int(get_string_from_data(data, idx, size)) "TPE1", 'TP1': meta.artist = get_string_from_data(data, idx, size) "APIC","PIC": var pic_frame = data.slice(idx + 1, idx + size) var zero1 = pic_frame.find(0) if zero1 > 0: var mime_type = pic_frame.slice(0, zero1).get_string_from_ascii() zero1 += 1 # Picture type if zero1 < pic_frame.size(): zero1 += 1 if zero1 < pic_frame.size(): var zero2 = pic_frame.find(0, zero1) var image_bytes = pic_frame.slice(zero2 + 1, pic_frame.size()) var img = Image.new() match mime_type: "image/png": img.load_png_from_buffer(image_bytes) "image/jpeg", "image/jpg": img.load_jpg_from_buffer(image_bytes) _: meta.error = FAILED push_error("MusicMeta.get_metadata_mp3(): ERROR: mime type ", mime_type, " not yet supported...") return meta var t: ImageTexture = ImageTexture.new() t.set_image(img) meta.cover = t idx += size return meta else: meta.error = FAILED push_error("Error: Found version '%s' from streams data; must be 'ID3v2.4.0'."%v) return meta func get_string_from_data(data, idx, size): var ret if size > 3 and Array(data.slice(idx, idx + 3)).hash() == [1, 0xff, 0xfe].hash(): # Null-terminated string of ucs2 chars ret = get_string_from_ucs2(data.slice(idx + 3, idx + size)) if data[idx] == 0: # Simple utf8 string ret = data.slice(idx + 1, idx + size).get_string_from_utf8() if ret: return ret else: return "" # Syncsafe uses 0x80 multiplier otherwise use 0x100 multiplier func bytes_to_int(bytes: Array, is_syncsafe = true): var mult = 0x80 if is_syncsafe else 0x100 var n = 0 for byte in bytes: n *= mult n += byte return n func get_string_from_ucs2(bytes: Array): var s = "" var idx = 0 while idx < (bytes.size() - 1): s += char(bytes[idx] + 256 * bytes[idx + 1]) idx += 2 return s