def update_metadata(self, metadata): """takes this track's current MetaData object as returned by get_metadata() and sets this track's metadata with any fields updated in that object raises IOError if unable to write the file """ if (metadata is None): return elif (not isinstance(metadata, ApeTag)): from audiotools.text import ERR_FOREIGN_METADATA raise ValueError(ERR_FOREIGN_METADATA) from audiotools.bitstream import BitstreamReader, BitstreamWriter from audiotools import transfer_data f = open(self.filename, "r+b") f.seek(-32, 2) (preamble, version, tag_size, item_count, read_only, item_encoding, is_header, no_footer, has_header) = BitstreamReader(f, 1).parse(ApeTag.HEADER_FORMAT) if ((preamble == 'APETAGEX') and (version == 2000)): if (has_header): old_tag_size = 32 + tag_size else: old_tag_size = tag_size if (metadata.total_size() >= old_tag_size): # metadata has grown # so append it to existing file f.seek(-old_tag_size, 2) metadata.build(BitstreamWriter(f, 1)) else: # metadata has shrunk # so rewrite file with smaller metadata from audiotools import TemporaryFile from os.path import getsize # copy everything but the last "old_tag_size" bytes # from existing file to rewritten file new_apev2 = TemporaryFile(self.filename) old_apev2 = open(self.filename, "rb") limited_transfer_data(old_apev2.read, new_apev2.write, getsize(self.filename) - old_tag_size) # append new tag to rewritten file metadata.build(BitstreamWriter(new_apev2, 1)) old_apev2.close() new_apev2.close() else: # no existing metadata, so simply append a fresh tag f = open(self.filename, "ab") metadata.build(BitstreamWriter(f, 1)) f.close()
def update_metadata(self, metadata): """takes this track's current MetaData object as returned by get_metadata() and sets this track's metadata with any fields updated in that object raises IOError if unable to write the file """ import os from audiotools import (TemporaryFile, LimitedFileReader, transfer_data) from audiotools.id3 import (ID3v2Comment, ID3CommentPair) from audiotools.id3v1 import ID3v1Comment from audiotools.bitstream import BitstreamWriter if metadata is None: return elif (not (isinstance(metadata, ID3v2Comment) or isinstance(metadata, ID3CommentPair) or isinstance(metadata, ID3v1Comment))): from audiotools.text import ERR_FOREIGN_METADATA raise ValueError(ERR_FOREIGN_METADATA) elif not os.access(self.filename, os.W_OK): raise IOError(self.filename) new_mp3 = TemporaryFile(self.filename) # get the original MP3 data old_mp3 = open(self.filename, "rb") MP3Audio.__find_last_mp3_frame__(old_mp3) data_end = old_mp3.tell() old_mp3.seek(0, 0) MP3Audio.__find_mp3_start__(old_mp3) data_start = old_mp3.tell() old_mp3 = LimitedFileReader(old_mp3, data_end - data_start) # write id3v2 + data + id3v1 to file if isinstance(metadata, ID3CommentPair): metadata.id3v2.build(BitstreamWriter(new_mp3, False)) transfer_data(old_mp3.read, new_mp3.write) metadata.id3v1.build(new_mp3) elif isinstance(metadata, ID3v2Comment): metadata.build(BitstreamWriter(new_mp3, False)) transfer_data(old_mp3.read, new_mp3.write) elif isinstance(metadata, ID3v1Comment): transfer_data(old_mp3.read, new_mp3.write) metadata.build(new_mp3) # commit change to disk old_mp3.close() new_mp3.close()
def encode_flac(filename, pcmreader, block_size=4096, max_lpc_order=8, adaptive_mid_side=False, mid_side=True, exhaustive_model_search=False, max_residual_partition_order=5): options = Encoding_Options(block_size, max_lpc_order, adaptive_mid_side, mid_side, exhaustive_model_search, max_residual_partition_order, 14 if pcmreader.bits_per_sample <= 16 else 30) streaminfo = STREAMINFO(block_size, block_size, 2 ** 32, 0, pcmreader.sample_rate, pcmreader.channels, pcmreader.bits_per_sample, 0, md5()) pcmreader = BufferedPCMReader(pcmreader) output_file = open(filename, "wb") writer = BitstreamWriter(output_file, 0) #write placeholder metadata blocks writer.write_bytes("fLaC") writer.build("1u 7u 24u", [1, 0, 34]) streaminfo.write(writer) #walk through PCM reader's FrameLists frame_number = 0 frame = pcmreader.read(block_size * (pcmreader.bits_per_sample / 8) * pcmreader.channels) flac_frame = BitstreamRecorder(0) while (len(frame) > 0): streaminfo.input_update(frame) flac_frame.reset() encode_flac_frame(flac_frame, pcmreader, options, frame_number, frame) streaminfo.output_update(flac_frame) flac_frame.copy(writer) frame_number += 1 frame = pcmreader.read(block_size * (pcmreader.bits_per_sample / 8) * pcmreader.channels) #return to beginning of file and rewrite STREAMINFO block output_file.seek(8, 0) streaminfo.write(writer) writer.close()
def set_metadata(self, metadata): """takes a MetaData object and sets this track's metadata raises IOError if unable to write the file""" from audiotools.bitstream import BitstreamWriter if metadata is None: return self.delete_metadata() new_metadata = ApeTag.converted(metadata) old_metadata = self.get_metadata() if old_metadata is not None: # transfer ReplayGain tags from old metadata to new metadata for tag in [b"replaygain_track_gain", b"replaygain_track_peak", b"replaygain_album_gain", b"replaygain_album_peak"]: try: # if old_metadata has tag, shift it over new_metadata[tag] = old_metadata[tag] except KeyError: try: # otherwise, if new_metadata has tag, delete it del(new_metadata[tag]) except KeyError: # if neither has tag, ignore it continue # transfer Cuesheet from old metadata to new metadata if b"Cuesheet" in old_metadata: new_metadata[b"Cuesheet"] = old_metadata[b"Cuesheet"] elif b"Cuesheet" in new_metadata: del(new_metadata[b"Cuesheet"]) self.update_metadata(new_metadata) else: # delete ReplayGain tags from new metadata for tag in [b"replaygain_track_gain", b"replaygain_track_peak", b"replaygain_album_gain", b"replaygain_album_peak"]: try: del(new_metadata[tag]) except KeyError: continue # delete Cuesheet from new metadata if b"Cuesheet" in new_metadata: del(new_metadata[b"Cuesheet"]) if len(new_metadata.keys()) > 0: # no existing metadata, so simply append a fresh tag with BitstreamWriter(open(self.filename, "ab"), True) as writer: new_metadata.build(writer)
def build(self, mp3_file): """given an MP3 file positioned at the file's end, generate a tag""" from audiotools.bitstream import BitstreamWriter BitstreamWriter(mp3_file, 0).build( "3b 30b 30b 30b 4b 28b 8p 1b 1b", ("TAG", self.__track_name__, self.__artist_name__, self.__album_name__, self.__year__, self.__comment__, self.__track_number__, self.__genre__))
def encode_mdat(file, pcmreader, block_size=4096, initial_history=10, history_multiplier=40, maximum_k=14, interlacing_shift=2, min_interlacing_leftweight=0, max_interlacing_leftweight=4): options = Encoding_Options(block_size, initial_history, history_multiplier, maximum_k, interlacing_shift, min_interlacing_leftweight, max_interlacing_leftweight) pcmreader = BufferedPCMReader(pcmreader) mdat = BitstreamWriter(file, 0) total_pcm_frames = 0 frame_byte_sizes = [] #write placeholder mdat header mdat_start = file.tell() mdat.write(32, 0) mdat.write_bytes("mdat") #read FrameList objects until stream is empty frame = pcmreader.read(block_size) while (len(frame) > 0): total_pcm_frames += frame.frames frame_start = file.tell() encode_frameset(mdat, pcmreader, options, frame) mdat.flush() frame_byte_sizes.append(file.tell() - frame_start) frame = pcmreader.read(block_size) #finally, return to start of mdat and write actual length file.seek(mdat_start) mdat.write(32, sum(frame_byte_sizes) + 8) return (frame_byte_sizes, total_pcm_frames)
def encode_tta(file, pcmreader): """given a file object and buffered PCMReader, writes TTA frames to the writer and returns a list of TTA frame lengths, in bytes""" writer = BitstreamWriter(file, True) block_size = (pcmreader.sample_rate * 256) // 245 frame_sizes = [] # encode FrameLists from PCMReader to temporary space framelist = pcmreader.read(block_size) while len(framelist) > 0: frame_sizes.append( encode_tta_frame(writer, pcmreader.bits_per_sample, framelist)) framelist = pcmreader.read(block_size) writer.flush() return frame_sizes
def encode_mdat( file, pcmreader, block_size=4096, initial_history=10, history_multiplier=40, maximum_k=14, interlacing_shift=2, min_interlacing_leftweight=0, max_interlacing_leftweight=4, ): options = Encoding_Options( block_size, initial_history, history_multiplier, maximum_k, interlacing_shift, min_interlacing_leftweight, max_interlacing_leftweight, ) pcmreader = BufferedPCMReader(pcmreader) mdat = BitstreamWriter(file, 0) total_pcm_frames = 0 frame_byte_sizes = [] # write placeholder mdat header mdat_start = file.tell() mdat.write(32, 0) mdat.write_bytes("mdat") # read FrameList objects until stream is empty frame = pcmreader.read(block_size) while len(frame) > 0: total_pcm_frames += frame.frames frame_start = file.tell() encode_frameset(mdat, pcmreader, options, frame) mdat.flush() frame_byte_sizes.append(file.tell() - frame_start) frame = pcmreader.read(block_size) # finally, return to start of mdat and write actual length file.seek(mdat_start) mdat.write(32, sum(frame_byte_sizes) + 8) return (frame_byte_sizes, total_pcm_frames)
def encode_tta(file, pcmreader): """given a file object and buffered PCMReader, writes TTA frames to the writer and returns a list of TTA frame lengths, in bytes""" writer = BitstreamWriter(file, True) block_size = (pcmreader.sample_rate * 256) / 245 frame_sizes = [] #encode FrameLists from PCMReader to temporary space framelist = pcmreader.read(block_size) while (len(framelist) > 0): frame_sizes.append(encode_tta_frame(writer, pcmreader.bits_per_sample, framelist)) framelist = pcmreader.read(block_size) writer.flush() return frame_sizes
def encode_shn(filename, pcmreader, is_big_endian, signed_samples, header_data, footer_data="", block_size=256): """filename is a string to the output file's path pcmreader is a PCMReader object header_data and footer_data are binary strings block_size is the default size of each Shorten audio command """ pcmreader = BufferedPCMReader(pcmreader) output_file = open(filename, "wb") writer = BitstreamWriter(output_file, 0) left_shift = 0 wrapped_channels = [[] for c in xrange(pcmreader.channels)] #write magic number and version writer.build("4b 8u", ["ajkg", 2]) bytes_written = __Counter__() writer.add_callback(bytes_written.byte) #write header from PCMReader info and encoding options if (pcmreader.bits_per_sample == 8): if (signed_samples): write_long(writer, 1) # signed, 8-bit sign_adjustment = 0 else: write_long(writer, 2) # unsigned, 8-bit sign_adjustment = 1 << (pcmreader.bits_per_sample - 1) #8-bit samples have no endianness elif (pcmreader.bits_per_sample == 16): if (signed_samples): if (is_big_endian): write_long(writer, 3) # signed, 16-bit, big-endian else: write_long(writer, 5) # signed, 16-bit, little-endian sign_adjustment = 0 else: if (is_big_endian): write_long(writer, 4) # unsigned, 16-bit, big-endian else: write_long(writer, 6) # unsigned, 16-bit, little-endian sign_adjustment = 1 << (pcmreader.bits_per_sample - 1) else: raise ValueError("unsupported bits_per_sample") write_long(writer, pcmreader.channels) write_long(writer, block_size) write_long(writer, 0) # max LPC write_long(writer, 0) # mean count write_long(writer, 0) # bytes to skip #write header as a VERBATIM block write_unsigned(writer, COMMAND_SIZE, FN_VERBATIM) write_unsigned(writer, VERBATIM_SIZE, len(header_data)) for b in header_data: write_unsigned(writer, VERBATIM_BYTE_SIZE, ord(b)) #split PCMReader into block_size chunks #and continue until the number of PCM frames is 0 frame = pcmreader.read(block_size) while (len(frame) > 0): #if the chunk isn't block_size frames long, #issue a command to change it if (frame.frames != block_size): block_size = frame.frames write_unsigned(writer, COMMAND_SIZE, FN_BLOCKSIZE) write_long(writer, block_size) #split chunk into individual channels for c in xrange(pcmreader.channels): #convert PCM data to unsigned, if necessary if (signed_samples): channel = list(frame.channel(c)) else: channel = [s + sign_adjustment for s in frame.channel(c)] #if all samples are 0, issue a ZERO command if (all_zeroes(channel)): write_unsigned(writer, COMMAND_SIZE, FN_ZERO) #wrap zeroes around for next set of channels wrapped_channels[c] = channel else: #if channel's shifted bits have changed #from the previous channel's shift #issue a new BITSHIFT command wasted_bits = wasted_bps(channel) if (wasted_bits != left_shift): write_unsigned(writer, COMMAND_SIZE, FN_BITSHIFT) write_unsigned(writer, BITSHIFT_SIZE, wasted_bits) left_shift = wasted_bits #and shift the channel's bits if the amount is still > 0 if (left_shift > 0): shifted = [s >> left_shift for s in channel] else: shifted = channel #determine the best DIFF command and residuals #to issue for shifted channel data (diff, residuals) = best_diff(wrapped_channels[c], shifted) #determine the best energy size for DIFF's residuals energy = best_energy(residuals) #write DIFF command, energy size and residuals write_unsigned(writer, COMMAND_SIZE, { 1: FN_DIFF1, 2: FN_DIFF2, 3: FN_DIFF3 }[diff]) write_unsigned(writer, ENERGY_SIZE, energy) for residual in residuals: write_signed(writer, energy, residual) #wrap shifted channels around for next set of channels wrapped_channels[c] = shifted #and get another set of channels to encode frame = pcmreader.read(block_size) #once all PCM data has been sent #if there's any footer data, write it as another VERBATIM block if (len(footer_data) > 0): write_unsigned(writer, COMMAND_SIZE, FN_VERBATIM) write_unsigned(writer, VERBATIM_SIZE, len(footer_data)) for b in footer_data: write_unsigned(writer, VERBATIM_BYTE_SIZE, ord(b)) #issue a QUIT command write_unsigned(writer, COMMAND_SIZE, FN_QUIT) #finally, due to Shorten's silly way of using bit buffers, #output (not counting the 5 bytes of magic + version) #must be padded to a multiple of 4 bytes #or its reference decoder explodes writer.byte_align() while ((int(bytes_written) % 4) != 0): writer.write(8, 0)
def encode_flac(filename, pcmreader, block_size=4096, max_lpc_order=8, min_residual_partition_order=0, max_residual_partition_order=5, mid_side=True, adaptive_mid_side=False, exhaustive_model_search=False, disable_verbatim_subframes=False, disable_constant_subframes=False, disable_fixed_subframes=False, disable_lpc_subframes=False, padding_size=4096): frame_sizes = [] options = Encoding_Options(block_size, max_lpc_order, adaptive_mid_side, mid_side, exhaustive_model_search, max_residual_partition_order, 14 if pcmreader.bits_per_sample <= 16 else 30) streaminfo = STREAMINFO(block_size, block_size, (2 ** 24) - 1, 0, pcmreader.sample_rate, pcmreader.channels, pcmreader.bits_per_sample, 0, md5()) pcmreader = BufferedPCMReader(pcmreader) output_file = open(filename, "wb") writer = BitstreamWriter(output_file, False) # write placeholder metadata blocks such as STREAMINFO and PADDING writer.write_bytes("fLaC") writer.build("1u 7u 24u", [0, 0, 34]) streaminfo_start = writer.getpos() streaminfo.write(writer) writer.build("1u 7u 24u", [1, 1, padding_size]) writer.write_bytes(b"\x00" * padding_size) # walk through PCM reader's FrameLists frame_number = 0 frame = pcmreader.read(block_size) flac_frame = BitstreamRecorder(0) while len(frame) > 0: streaminfo.input_update(frame) flac_frame.reset() encode_flac_frame(flac_frame, pcmreader, options, frame_number, frame) frame_sizes.append((flac_frame.bytes(), frame.frames)) streaminfo.output_update(flac_frame) flac_frame.copy(writer) frame_number += 1 frame = pcmreader.read(block_size) # return to beginning of file and rewrite STREAMINFO block writer.setpos(streaminfo_start) streaminfo.write(writer) writer.flush() writer.close() return frame_sizes
def encode_mdat(file, pcmreader, block_size=4096, initial_history=10, history_multiplier=40, maximum_K=14, interlacing_shift=2, min_interlacing_leftweight=0, max_interlacing_leftweight=4): options = Encoding_Options(block_size, initial_history, history_multiplier, maximum_K, interlacing_shift, min_interlacing_leftweight, max_interlacing_leftweight) pcmreader = BufferedPCMReader(pcmreader) mdat = BitstreamWriter(file, 0) mdat_length = ByteCounter() mdat.add_callback(mdat_length.update) frame_sample_sizes = [] frame_byte_sizes = [] frame_file_offsets = [] #write placeholder mdat header mdat.write(32, 0) mdat.write_bytes("mdat") #read FrameList objects until stream is empty frame = pcmreader.read(block_size * pcmreader.channels * (pcmreader.bits_per_sample / 8)) while (len(frame) > 0): frame_sample_sizes.append(frame.frames) frame_file_offsets.append(int(mdat_length)) encode_frameset(mdat, pcmreader, options, frame) frame_byte_sizes.append(int(mdat_length) - frame_file_offsets[-1]) frame = pcmreader.read(block_size * pcmreader.channels * (pcmreader.bits_per_sample / 8)) #finally, return to start of mdat and write actual length mdat.byte_align() mdat.pop_callback() file.seek(0, 0) mdat.write(32, int(mdat_length)) return (frame_sample_sizes, frame_byte_sizes, frame_file_offsets, int(mdat_length))
def set_replay_gain(self, replaygain): """given a ReplayGain object, sets the track's gain to those values may raise IOError if unable to modify the file""" from math import log10 from audiotools import TemporaryFile gain_title = int(round((64.82 - replaygain.track_gain) * 256)) if replaygain.track_peak > 0.0: peak_title = int(log10(replaygain.track_peak * 2**15) * 20 * 256) else: peak_title = 0 gain_album = int(round((64.82 - replaygain.album_gain) * 256)) if replaygain.album_peak > 0.0: peak_album = int(log10(replaygain.album_peak * 2**15) * 20 * 256) else: peak_album = 0 #FIXME - check for missing "RG" block and add one if not present metadata = self.get_metadata() writer = BitstreamWriter(TemporaryFile(self.filename), False) writer.write_bytes(b"MPCK") for key, size, block in self.blocks(): if key != b"RG": writer.write_bytes(key) size.build(writer) writer.write_bytes(block) else: writer.write_bytes(b"RG") MPC_Size(2 + 1 + 1 + 2 * 4, 1).build(writer) writer.write(8, 1) writer.write(16, gain_title) writer.write(16, peak_title) writer.write(16, gain_album) writer.write(16, peak_album) if metadata is not None: writer.set_endianness(True) metadata.build(writer) writer.close()
def set_replay_gain(self, replaygain): """given a ReplayGain object, sets the track's gain to those values may raise IOError if unable to modify the file""" from math import log10 from audiotools import TemporaryFile gain_title = int(round((64.82 - replaygain.track_gain) * 256)) if replaygain.track_peak > 0.0: peak_title = int(log10(replaygain.track_peak * 2 ** 15) * 20 * 256) else: peak_title = 0 gain_album = int(round((64.82 - replaygain.album_gain) * 256)) if replaygain.album_peak > 0.0: peak_album = int(log10(replaygain.album_peak * 2 ** 15) * 20 * 256) else: peak_album = 0 #FIXME - check for missing "RG" block and add one if not present metadata = self.get_metadata() writer = BitstreamWriter(TemporaryFile(self.filename), False) writer.write_bytes(b"MPCK") for key, size, block in self.blocks(): if key != b"RG": writer.write_bytes(key) size.build(writer) writer.write_bytes(block) else: writer.write_bytes(b"RG") MPC_Size(2 + 1 + 1 + 2 * 4, 1).build(writer) writer.write(8, 1) writer.write(16, gain_title) writer.write(16, peak_title) writer.write(16, gain_album) writer.write(16, peak_album) if metadata is not None: writer.set_endianness(True) metadata.build(writer) writer.close()
def delete_replay_gain(self): """removes ReplayGain values from file, if any may raise IOError if unable to modify the file""" from audiotools import TemporaryFile writer = BitstreamWriter(TemporaryFile(self.filename), False) writer.write_bytes(b"MPCK") for key, size, block in self.blocks(): if key != b"RG": writer.write_bytes(key) size.build(writer) writer.write_bytes(block) else: writer.write_bytes(b"RG") MPC_Size(2 + 1 + 1 + 2 * 4, 1).build(writer) writer.write(8, 1) writer.write(16, 0) writer.write(16, 0) writer.write(16, 0) writer.write(16, 0) writer.close()
def encode_flac(filename, pcmreader, block_size=4096, max_lpc_order=8, min_residual_partition_order=0, max_residual_partition_order=5, mid_side=True, adaptive_mid_side=False, exhaustive_model_search=False, disable_verbatim_subframes=False, disable_constant_subframes=False, disable_fixed_subframes=False, disable_lpc_subframes=False, padding_size=4096): frame_sizes = [] options = Encoding_Options(block_size, max_lpc_order, adaptive_mid_side, mid_side, exhaustive_model_search, max_residual_partition_order, 14 if pcmreader.bits_per_sample <= 16 else 30) streaminfo = STREAMINFO(block_size, block_size, (2**24) - 1, 0, pcmreader.sample_rate, pcmreader.channels, pcmreader.bits_per_sample, 0, md5()) pcmreader = BufferedPCMReader(pcmreader) output_file = open(filename, "wb") writer = BitstreamWriter(output_file, False) # write placeholder metadata blocks such as STREAMINFO and PADDING writer.write_bytes("fLaC") writer.build("1u 7u 24u", [0, 0, 34]) streaminfo_start = writer.getpos() streaminfo.write(writer) writer.build("1u 7u 24u", [1, 1, padding_size]) writer.write_bytes(b"\x00" * padding_size) # walk through PCM reader's FrameLists frame_number = 0 frame = pcmreader.read(block_size) flac_frame = BitstreamRecorder(0) while len(frame) > 0: streaminfo.input_update(frame) flac_frame.reset() encode_flac_frame(flac_frame, pcmreader, options, frame_number, frame) frame_sizes.append((flac_frame.bytes(), frame.frames)) streaminfo.output_update(flac_frame) flac_frame.copy(writer) frame_number += 1 frame = pcmreader.read(block_size) # return to beginning of file and rewrite STREAMINFO block writer.setpos(streaminfo_start) streaminfo.write(writer) writer.flush() writer.close() return frame_sizes
def update_metadata(self, metadata, old_metadata=None): """takes this track's updated MetaData object as returned by get_metadata() and sets this track's metadata with any fields updated in that object old_metadata is the unmodifed metadata returned by get_metadata() raises IOError if unable to write the file """ from audiotools.bitstream import BitstreamWriter from audiotools.bitstream import BitstreamReader import os.path if metadata is None: return if not isinstance(metadata, M4A_META_Atom): from audiotools.text import ERR_FOREIGN_METADATA raise ValueError(ERR_FOREIGN_METADATA) if old_metadata is None: # get_metadata() result may still be None, and that's okay old_metadata = self.get_metadata() # M4A streams often have *two* "free" atoms we can attempt to resize # first, attempt to resize the one inside the "meta" atom if ((old_metadata is not None) and metadata.has_child(b"free") and ((metadata.size() - metadata[b"free"].size()) <= old_metadata.size())): metadata.replace_child( M4A_FREE_Atom(old_metadata.size() - (metadata.size() - metadata[b"free"].size()))) f = open(self.filename, 'r+b') (meta_size, meta_offset) = get_m4a_atom_offset(BitstreamReader(f, False), b"moov", b"udta", b"meta") f.seek(meta_offset + 8, 0) with BitstreamWriter(f, False) as writer: metadata.build(writer) # writer will close "f" when finished else: from audiotools import TemporaryFile # if there's insufficient room, # attempt to resize the outermost "free" also # this is only possible if the file is laid out correctly, # with "free" coming after "moov" but before "mdat" # FIXME # if neither fix is possible, the whole file must be rewritten # which also requires adjusting the "stco" atom offsets with open(self.filename, "rb") as f: m4a_tree = M4A_Tree_Atom.parse( None, os.path.getsize(self.filename), BitstreamReader(f, False), { b"moov": M4A_Tree_Atom, b"trak": M4A_Tree_Atom, b"mdia": M4A_Tree_Atom, b"minf": M4A_Tree_Atom, b"stbl": M4A_Tree_Atom, b"stco": M4A_STCO_Atom, b"udta": M4A_Tree_Atom }) # find initial mdat offset initial_mdat_offset = m4a_tree.child_offset(b"mdat") # adjust moov -> udta -> meta atom # (generating sub-atoms as necessary) if not m4a_tree.has_child(b"moov"): return else: moov = m4a_tree[b"moov"] if not moov.has_child(b"udta"): moov.add_child(M4A_Tree_Atom(b"udta", [])) udta = moov[b"udta"] if not udta.has_child(b"meta"): udta.add_child(metadata) else: udta.replace_child(metadata) # find new mdat offset new_mdat_offset = m4a_tree.child_offset(b"mdat") # adjust moov -> trak -> mdia -> minf -> stbl -> stco offsets # based on the difference between the new mdat position and the old try: delta_offset = new_mdat_offset - initial_mdat_offset stco = m4a_tree[b"moov"][b"trak"][b"mdia"][b"minf"][b"stbl"][ b"stco"] stco.offsets = [ offset + delta_offset for offset in stco.offsets ] except KeyError: # if there is no stco atom, don't worry about it pass # then write entire tree back to disk with BitstreamWriter(TemporaryFile(self.filename), False) as writer: m4a_tree.build(writer)
def update_metadata(self, metadata): """takes this track's current MetaData object as returned by get_metadata() and sets this track's metadata with any fields updated in that object raises IOError if unable to write the file """ import os from audiotools.ape import ApeTag from audiotools.id3 import ID3v2Comment from audiotools.id3 import ID3CommentPair from audiotools.id3v1 import ID3v1Comment if (metadata is None): return elif (not os.access(self.filename, os.W_OK)): raise IOError(self.filename) # ensure metadata is APEv2, ID3v2, ID3v1, or ID3CommentPair if (((not isinstance(metadata, ApeTag)) and (not isinstance(metadata, ID3v2Comment)) and (not isinstance(metadata, ID3CommentPair)) and (not isinstance(metadata, ID3v1Comment)))): from audiotools.text import ERR_FOREIGN_METADATA raise ValueError(ERR_FOREIGN_METADATA) current_metadata = self.get_metadata() if (isinstance(metadata, ApeTag) and (current_metadata is None)): # if new metadata is APEv2 and no current metadata, # simply append APEv2 tag from audiotools.bitstream import BitstreamWriter with BitstreamWriter(open(self.filename, "ab"), True) as writer: metadata.build(writer) elif (isinstance(metadata, ApeTag) and isinstance(current_metadata, ApeTag) and (metadata.total_size() > current_metadata.total_size())): # if new metadata is APEv2, current metadata is APEv2 # and new metadata is larger, # overwrite old tag with new tag from audiotools.bitstream import BitstreamWriter with open(self.filename, "r+b") as f: f.seek(-current_metadata.total_size(), 2) metadata.build(BitstreamWriter(f, True)) else: from audiotools.bitstream import BitstreamWriter from audiotools import (transfer_data, LimitedFileReader, TemporaryFile) from audiotools.id3 import skip_id3v2_comment # otherwise, rebuild TTA with APEv2/ID3 tags in place old_tta = open(self.filename, "rb") skip_id3v2_comment(old_tta) old_tta = LimitedFileReader(old_tta, self.data_size()) new_tta = TemporaryFile(self.filename) if (isinstance(metadata, ApeTag)): transfer_data(old_tta.read, new_tta.write) metadata.build(BitstreamWriter(new_tta, True)) elif (isinstance(metadata, ID3CommentPair)): metadata.id3v2.build(BitstreamWriter(new_tta, False)) transfer_data(old_tta.read, new_tta.write) metadata.id3v1.build(new_tta) elif (isinstance(metadata, ID3v2Comment)): metadata.build(BitstreamWriter(new_tta, False)) transfer_data(old_tta.read, new_tta.write) else: # ID3v1Comment transfer_data(old_tta.read, new_tta.write) metadata.build(new_tta) old_tta.close() new_tta.close()
def set_metadata(self, metadata): """takes a MetaData object and sets this track's metadata this metadata includes track name, album name, and so on raises IOError if unable to write the file""" import os from audiotools.ape import ApeTag from audiotools.bitstream import BitstreamWriter if (metadata is None): return else: new_metadata = ApeTag.converted(metadata) if (not os.access(self.filename, os.W_OK)): raise IOError(self.filename) # if current metadata is present and in a particular format # set_metadata() should continue using that format old_metadata = ApeTag.converted(self.get_metadata()) if (old_metadata is not None): # transfer ReplayGain tags from old metadata to new metadata for tag in [ b"replaygain_track_gain", b"replaygain_track_peak", b"replaygain_album_gain", b"replaygain_album_peak" ]: try: # if old_metadata has tag, shift it over new_metadata[tag] = old_metadata[tag] except KeyError: try: # otherwise, if new_metadata has tag, delete it del (new_metadata[tag]) except KeyError: # if neither has tag, ignore it continue # transfer Cuesheet from old metadata to new metadata if (b"Cuesheet" in old_metadata): new_metadata[b"Cuesheet"] = old_metadata[b"Cuesheet"] elif (b"Cuesheet" in new_metadata): del (new_metadata[b"Cuesheet"]) self.update_metadata(new_metadata) else: # delete ReplayGain tags from new metadata for tag in [ b"replaygain_track_gain", b"replaygain_track_peak", b"replaygain_album_gain", b"replaygain_album_peak" ]: try: del (new_metadata[tag]) except KeyError: continue # delete Cuesheet from new metadata if (b"Cuesheet" in new_metadata): del (new_metadata[b"Cuesheet"]) # no current metadata, so append a fresh APEv2 tag with BitstreamWriter(open(self.filename, "ab"), True) as writer: new_metadata.build(writer)
def from_pcm(cls, filename, pcmreader, compression=None, total_pcm_frames=None, encoding_function=None): """encodes a new file from PCM data takes a filename string, PCMReader object, optional compression level string and optional total_pcm_frames integer encodes a new audio file from pcmreader's data at the given filename with the specified compression level and returns a new AudioFile-compatible object may raise EncodingError if some problem occurs when encoding the input file. This includes an error in the input stream, a problem writing the output file, or even an EncodingError subclass such as "UnsupportedBitsPerSample" if the input stream is formatted in a way this class is unable to support """ from audiotools import (BufferedPCMReader, CounterPCMReader, transfer_data, EncodingError) # from audiotools.py_encoders import encode_tta from audiotools.encoders import encode_tta from audiotools.bitstream import BitstreamWriter # open output file right away # so we can fail as soon as possible try: file = open(filename, "wb") except IOError as err: pcmreader.close() raise EncodingError(str(err)) writer = BitstreamWriter(file, True) counter = CounterPCMReader(pcmreader) try: if (total_pcm_frames is not None): # write header to disk write_header(writer, pcmreader.channels, pcmreader.bits_per_sample, pcmreader.sample_rate, total_pcm_frames) block_size = (pcmreader.sample_rate * 256) // 245 total_tta_frames = div_ceil(total_pcm_frames, block_size) # write temporary seektable to disk writer.mark() write_seektable(writer, [0] * total_tta_frames) writer.flush() # write frames to disk try: frame_sizes = \ (encode_tta if encoding_function is None else encoding_function)(file, BufferedPCMReader(counter)) except (IOError, ValueError) as err: cls.__unlink__(filename) raise EncodingError(str(err)) # ensure written number of PCM frames # matches total_pcm_frames if (counter.frames_written != total_pcm_frames): from audiotools.text import ERR_TOTAL_PCM_FRAMES_MISMATCH cls.__unlink__(filename) raise EncodingError(ERR_TOTAL_PCM_FRAMES_MISMATCH) assert (len(frame_sizes) == total_tta_frames) # go back and rewrite seektable with completed one writer.rewind() write_seektable(writer, frame_sizes) writer.unmark() else: import tempfile frames = tempfile.TemporaryFile() # encode TTA frames to temporary file try: frame_sizes = \ (encode_tta if encoding_function is None else encoding_function)(frames, BufferedPCMReader(counter)) except (IOError, ValueError) as err: frames.close() cls.__unlink__(filename) raise EncodingError(str(err)) # write header to disk write_header(writer, pcmreader.channels, pcmreader.bits_per_sample, pcmreader.sample_rate, counter.frames_written) # write seektable to disk write_seektable(writer, frame_sizes) # transfer TTA frames from temporary space to disk frames.seek(0, 0) transfer_data(frames.read, writer.write_bytes) frames.close() finally: counter.close() if (writer.has_mark()): writer.unmark() writer.close() return cls(filename)
def update_metadata(self, metadata): """takes this track's current MetaData object as returned by get_metadata() and sets this track's metadata with any fields updated in that object raises IOError if unable to write the file """ if metadata is None: return elif not isinstance(metadata, ApeTag): from audiotools.text import ERR_FOREIGN_METADATA raise ValueError(ERR_FOREIGN_METADATA) from audiotools.bitstream import parse, BitstreamWriter from audiotools import transfer_data f = open(self.filename, "r+b") f.seek(-32, 2) tag_footer = f.read(32) if len(tag_footer) < 32: # no existing ApeTag can fit, so append fresh tag f.close() with BitstreamWriter(open(self.filename, "ab"), True) as writer: metadata.build(writer) return (preamble, version, tag_size, item_count, read_only, item_encoding, is_header, no_footer, has_header) = parse(ApeTag.HEADER_FORMAT, True, tag_footer) if (preamble == b'APETAGEX') and (version == 2000): if has_header: old_tag_size = 32 + tag_size else: old_tag_size = tag_size if metadata.total_size() >= old_tag_size: # metadata has grown # so append it to existing file f.seek(-old_tag_size, 2) writer = BitstreamWriter(f, True) metadata.build(writer) writer.close() else: f.close() # metadata has shrunk # so rewrite file with smaller metadata from audiotools import TemporaryFile from os.path import getsize # copy everything but the last "old_tag_size" bytes # from existing file to rewritten file new_apev2 = TemporaryFile(self.filename) with open(self.filename, "rb") as old_apev2: limited_transfer_data( old_apev2.read, new_apev2.write, getsize(self.filename) - old_tag_size) # append new tag to rewritten file with BitstreamWriter(new_apev2, True) as writer: metadata.build(writer) # closing writer closes new_apev2 also else: # no existing metadata, so simply append a fresh tag f.close() with BitstreamWriter(open(self.filename, "ab"), True) as writer: metadata.build(writer)
def encode_wavpack(filename, pcmreader, block_size, correlation_passes=0, wave_header=None, wave_footer=None): pcmreader = BufferedPCMReader(pcmreader) output_file = open(filename, "wb") writer = BitstreamWriter(output_file, 1) context = EncoderContext( pcmreader, block_parameters(pcmreader.channels, pcmreader.channel_mask, correlation_passes), wave_header, wave_footer, ) block_index = 0 # walk through PCM reader's FrameLists frame = pcmreader.read(block_size * (pcmreader.bits_per_sample / 8) * pcmreader.channels) while len(frame) > 0: context.total_frames += frame.frames context.md5sum.update(frame.to_bytes(False, pcmreader.bits_per_sample >= 16)) c = 0 for parameters in context.block_parameters: if parameters.channel_count == 1: channel_data = [list(frame.channel(c))] else: channel_data = [list(frame.channel(c)), list(frame.channel(c + 1))] first_block = parameters is context.block_parameters[0] last_block = parameters is context.block_parameters[-1] context.block_offsets.append(output_file.tell()) write_block(writer, context, channel_data, block_index, first_block, last_block, parameters) c += parameters.channel_count block_index += frame.frames frame = pcmreader.read(block_size * (pcmreader.bits_per_sample / 8) * pcmreader.channels) # write MD5 sum and optional Wave footer in final block sub_blocks = BitstreamRecorder(1) sub_block = BitstreamRecorder(1) sub_block.reset() sub_block.write_bytes(context.md5sum.digest()) write_sub_block(sub_blocks, WV_MD5, 1, sub_block) # write Wave footer in final block, if present if context.wave_footer is not None: sub_block.reset() sub_block.write_bytes(context.wave_footer) write_sub_block(sub_blocks, WV_WAVE_FOOTER, 1, sub_block) write_block_header( writer, sub_blocks.bytes(), 0xFFFFFFFF, 0, pcmreader.bits_per_sample, 1, 0, 0, 0, 1, 1, 0, pcmreader.sample_rate, 0, 0xFFFFFFFF, ) sub_blocks.copy(writer) # update Wave header's "data" chunk size, if generated if context.wave_header is None: output_file.seek(32 + 2) if context.wave_footer is None: write_wave_header(writer, context.pcmreader, context.total_frames, 0) else: write_wave_header(writer, context.pcmreader, context.total_frames, len(context.wave_footer)) # go back and populate block headers with total samples for block_offset in context.block_offsets: output_file.seek(block_offset + 12, 0) writer.write(32, block_index) writer.close()
def encode_shn(filename, pcmreader, is_big_endian, signed_samples, header_data, footer_data="", block_size=256): """filename is a string to the output file's path pcmreader is a PCMReader object header_data and footer_data are binary strings block_size is the default size of each Shorten audio command """ pcmreader = BufferedPCMReader(pcmreader) output_file = open(filename, "wb") writer = BitstreamWriter(output_file, 0) left_shift = 0 wrapped_channels = [[] for c in xrange(pcmreader.channels)] # write magic number and version writer.build("4b 8u", ["ajkg", 2]) bytes_written = __Counter__() writer.add_callback(bytes_written.byte) # write header from PCMReader info and encoding options if pcmreader.bits_per_sample == 8: if signed_samples: write_long(writer, 1) # signed, 8-bit sign_adjustment = 0 else: write_long(writer, 2) # unsigned, 8-bit sign_adjustment = 1 << (pcmreader.bits_per_sample - 1) # 8-bit samples have no endianness elif pcmreader.bits_per_sample == 16: if signed_samples: if is_big_endian: write_long(writer, 3) # signed, 16-bit, big-endian else: write_long(writer, 5) # signed, 16-bit, little-endian sign_adjustment = 0 else: if is_big_endian: write_long(writer, 4) # unsigned, 16-bit, big-endian else: write_long(writer, 6) # unsigned, 16-bit, little-endian sign_adjustment = 1 << (pcmreader.bits_per_sample - 1) else: raise ValueError("unsupported bits_per_sample") write_long(writer, pcmreader.channels) write_long(writer, block_size) write_long(writer, 0) # max LPC write_long(writer, 0) # mean count write_long(writer, 0) # bytes to skip # write header as a VERBATIM block write_unsigned(writer, COMMAND_SIZE, FN_VERBATIM) write_unsigned(writer, VERBATIM_SIZE, len(header_data)) for b in header_data: write_unsigned(writer, VERBATIM_BYTE_SIZE, ord(b)) # split PCMReader into block_size chunks # and continue until the number of PCM frames is 0 frame = pcmreader.read(block_size) while len(frame) > 0: # if the chunk isn't block_size frames long, # issue a command to change it if frame.frames != block_size: block_size = frame.frames write_unsigned(writer, COMMAND_SIZE, FN_BLOCKSIZE) write_long(writer, block_size) # split chunk into individual channels for c in xrange(pcmreader.channels): # convert PCM data to unsigned, if necessary if signed_samples: channel = list(frame.channel(c)) else: channel = [s + sign_adjustment for s in frame.channel(c)] # if all samples are 0, issue a ZERO command if all_zeroes(channel): write_unsigned(writer, COMMAND_SIZE, FN_ZERO) # wrap zeroes around for next set of channels wrapped_channels[c] = channel else: # if channel's shifted bits have changed # from the previous channel's shift # issue a new BITSHIFT command wasted_bits = wasted_bps(channel) if wasted_bits != left_shift: write_unsigned(writer, COMMAND_SIZE, FN_BITSHIFT) write_unsigned(writer, BITSHIFT_SIZE, wasted_bits) left_shift = wasted_bits # and shift the channel's bits if the amount is still > 0 if left_shift > 0: shifted = [s >> left_shift for s in channel] else: shifted = channel # determine the best DIFF command and residuals # to issue for shifted channel data (diff, residuals) = best_diff(wrapped_channels[c], shifted) # determine the best energy size for DIFF's residuals energy = best_energy(residuals) # write DIFF command, energy size and residuals write_unsigned(writer, COMMAND_SIZE, {1: FN_DIFF1, 2: FN_DIFF2, 3: FN_DIFF3}[diff]) write_unsigned(writer, ENERGY_SIZE, energy) for residual in residuals: write_signed(writer, energy, residual) # wrap shifted channels around for next set of channels wrapped_channels[c] = shifted # and get another set of channels to encode frame = pcmreader.read(block_size) # once all PCM data has been sent # if there's any footer data, write it as another VERBATIM block if len(footer_data) > 0: write_unsigned(writer, COMMAND_SIZE, FN_VERBATIM) write_unsigned(writer, VERBATIM_SIZE, len(footer_data)) for b in footer_data: write_unsigned(writer, VERBATIM_BYTE_SIZE, ord(b)) # issue a QUIT command write_unsigned(writer, COMMAND_SIZE, FN_QUIT) # finally, due to Shorten's silly way of using bit buffers, # output (not counting the 5 bytes of magic + version) # must be padded to a multiple of 4 bytes # or its reference decoder explodes writer.byte_align() while (int(bytes_written) % 4) != 0: writer.write(8, 0)
def encode_mdat(file, pcmreader, block_size=4096, initial_history=10, history_multiplier=40, maximum_k=14, interlacing_shift=2, min_interlacing_leftweight=0, max_interlacing_leftweight=4): options = Encoding_Options(block_size, initial_history, history_multiplier, maximum_k, interlacing_shift, min_interlacing_leftweight, max_interlacing_leftweight) pcmreader = BufferedPCMReader(pcmreader) mdat = BitstreamWriter(file, False) total_pcm_frames = 0 frame_byte_sizes = [] # write placeholder mdat header mdat.mark() mdat.write(32, 0) mdat.write_bytes(b"mdat") # read FrameList objects until stream is empty frame = pcmreader.read(block_size) while (len(frame) > 0): total_pcm_frames += frame.frames frame_byte_size = Counter() mdat.add_callback(frame_byte_size.add) encode_frameset(mdat, pcmreader, options, frame) mdat.pop_callback() frame_byte_sizes.append(int(frame_byte_size)) frame = pcmreader.read(block_size) # finally, return to start of mdat and write actual length mdat.rewind() mdat.write(32, sum(frame_byte_sizes) + 8) mdat.unmark() return (frame_byte_sizes, total_pcm_frames)
def from_pcm(cls, filename, pcmreader, compression=None, total_pcm_frames=None, encoding_function=None): """encodes a new file from PCM data takes a filename string, PCMReader object, optional compression level string and optional total_pcm_frames integer encodes a new audio file from pcmreader's data at the given filename with the specified compression level and returns a new AudioFile-compatible object may raise EncodingError if some problem occurs when encoding the input file. This includes an error in the input stream, a problem writing the output file, or even an EncodingError subclass such as "UnsupportedBitsPerSample" if the input stream is formatted in a way this class is unable to support """ from audiotools import (BufferedPCMReader, CounterPCMReader, transfer_data, EncodingError) # from audiotools.py_encoders import encode_tta from audiotools.encoders import encode_tta from audiotools.bitstream import BitstreamWriter # open output file right away # so we can fail as soon as possible try: file = open(filename, "wb") except IOError as err: pcmreader.close() raise EncodingError(str(err)) writer = BitstreamWriter(file, True) counter = CounterPCMReader(pcmreader) try: if (total_pcm_frames is not None): # write header to disk write_header(writer, pcmreader.channels, pcmreader.bits_per_sample, pcmreader.sample_rate, total_pcm_frames) block_size = (pcmreader.sample_rate * 256) // 245 total_tta_frames = div_ceil(total_pcm_frames, block_size) # write temporary seektable to disk writer.mark() write_seektable(writer, [0] * total_tta_frames) writer.flush() # write frames to disk try: frame_sizes = \ (encode_tta if encoding_function is None else encoding_function)(file, BufferedPCMReader(counter)) except (IOError, ValueError) as err: cls.__unlink__(filename) raise EncodingError(str(err)) # ensure written number of PCM frames # matches total_pcm_frames if (counter.frames_written != total_pcm_frames): from audiotools.text import ERR_TOTAL_PCM_FRAMES_MISMATCH cls.__unlink__(filename) raise EncodingError(ERR_TOTAL_PCM_FRAMES_MISMATCH) assert(len(frame_sizes) == total_tta_frames) # go back and rewrite seektable with completed one writer.rewind() write_seektable(writer, frame_sizes) writer.unmark() else: import tempfile frames = tempfile.TemporaryFile() # encode TTA frames to temporary file try: frame_sizes = \ (encode_tta if encoding_function is None else encoding_function)(frames, BufferedPCMReader(counter)) except (IOError, ValueError) as err: frames.close() cls.__unlink__(filename) raise EncodingError(str(err)) # write header to disk write_header(writer, pcmreader.channels, pcmreader.bits_per_sample, pcmreader.sample_rate, counter.frames_written) # write seektable to disk write_seektable(writer, frame_sizes) # transfer TTA frames from temporary space to disk frames.seek(0, 0) transfer_data(frames.read, writer.write_bytes) frames.close() finally: counter.close() if (writer.has_mark()): writer.unmark() writer.close() return cls(filename)
def update_metadata(self, metadata): """takes this track's current MetaData object as returned by get_metadata() and sets this track's metadata with any fields updated in that object raises IOError if unable to write the file """ from audiotools.bitstream import (parse, BitstreamWriter, BitstreamReader) from audiotools import transfer_data if metadata is None: return elif not isinstance(metadata, ApeTag): from audiotools.text import ERR_FOREIGN_METADATA raise ValueError(ERR_FOREIGN_METADATA) elif len(metadata.keys()) == 0: # wipe out entire block of metadata from os import access, R_OK, W_OK if not access(self.filename, R_OK | W_OK): raise IOError(self.filename) with open(self.filename, "rb") as f: f.seek(-32, 2) (preamble, version, tag_size, item_count, read_only, item_encoding, is_header, no_footer, has_header) = BitstreamReader(f, True).parse( ApeTag.HEADER_FORMAT) if (preamble == b'APETAGEX') and (version == 2000): from audiotools import TemporaryFile, transfer_data from os.path import getsize # there's existing metadata to delete # so rewrite file without trailing metadata tag if has_header: old_tag_size = 32 + tag_size else: old_tag_size = tag_size # copy everything but the last "old_tag_size" bytes # from existing file to rewritten file new_apev2 = TemporaryFile(self.filename) old_apev2 = open(self.filename, "rb") limited_transfer_data( old_apev2.read, new_apev2.write, getsize(self.filename) - old_tag_size) old_apev2.close() new_apev2.close() else: # re-set metadata block at end of file f = open(self.filename, "r+b") f.seek(-32, 2) tag_footer = f.read(32) if len(tag_footer) < 32: # no existing ApeTag can fit, so append fresh tag f.close() with BitstreamWriter(open(self.filename, "ab"), True) as writer: metadata.build(writer) return (preamble, version, tag_size, item_count, read_only, item_encoding, is_header, no_footer, has_header) = parse(ApeTag.HEADER_FORMAT, True, tag_footer) if (preamble == b'APETAGEX') and (version == 2000): if has_header: old_tag_size = 32 + tag_size else: old_tag_size = tag_size if metadata.total_size() >= old_tag_size: # metadata has grown # so append it to existing file f.seek(-old_tag_size, 2) writer = BitstreamWriter(f, True) metadata.build(writer) writer.close() else: f.close() # metadata has shrunk # so rewrite file with smaller metadata from audiotools import TemporaryFile from os.path import getsize # copy everything but the last "old_tag_size" bytes # from existing file to rewritten file new_apev2 = TemporaryFile(self.filename) with open(self.filename, "rb") as old_apev2: limited_transfer_data( old_apev2.read, new_apev2.write, getsize(self.filename) - old_tag_size) # append new tag to rewritten file with BitstreamWriter(new_apev2, True) as writer: metadata.build(writer) # closing writer closes new_apev2 also else: # no existing metadata, so simply append a fresh tag f.close() with BitstreamWriter(open(self.filename, "ab"), True) as writer: metadata.build(writer)