def write_block(writer, context, channels, block_index, first_block, last_block, parameters): """writer is a BitstreamWriter-compatible object context is an EncoderContext object channels[c][s] is sample "s" in channel "c" block_index is an integer of the block's offset in PCM frames first_block and last_block are flags indicating the block's sequence parameters is an EncodingParameters object """ assert (len(channels) == 1) or (len(channels) == 2) if (len(channels) == 1) or (channels[0] == channels[1]): # 1 channel block or equivalent if len(channels) == 1: false_stereo = 0 else: false_stereo = 1 # calculate maximum magnitude of channel_0 magnitude = max(map(bits, channels[0])) # determine wasted bits wasted = min(map(wasted_bps, channels[0])) if wasted == INFINITY: # all samples are 0 wasted = 0 # if wasted bits, remove them from channel_0 if (wasted > 0) and (wasted != INFINITY): shifted = [[s >> wasted for s in channels[0]]] else: shifted = [channels[0]] # calculate CRC of shifted_0 crc = calculate_crc(shifted) else: # 2 channel block false_stereo = 0 # calculate maximum magnitude of channel_0/channel_1 magnitude = max(max(map(bits, channels[0])), max(map(bits, channels[1]))) # determine wasted bits wasted = min(min(map(wasted_bps, channels[0])), min(map(wasted_bps, channels[1]))) if wasted == INFINITY: # all samples are 0 wasted = 0 # if wasted bits, remove them from channel_0/channel_1 if wasted > 0: shifted = [[s >> wasted for s in channels[0]], [s >> wasted for s in channels[1]]] else: shifted = channels # calculate CRC of shifted_0/shifted_1 crc = calculate_crc(shifted) # joint stereo conversion of shifted_0/shifted_1 to mid/side channels mid_side = joint_stereo(shifted[0], shifted[1]) sub_blocks = BitstreamRecorder(1) sub_block = BitstreamRecorder(1) # if first block in file, write Wave header if not context.first_block_written: sub_block.reset() if context.wave_header is None: if context.wave_footer is None: write_wave_header(sub_block, context.pcmreader, 0, 0) else: write_wave_header(sub_block, context.pcmreader, 0, len(context.wave_footer)) else: sub_block.write_bytes(context.wave_header) write_sub_block(sub_blocks, WV_WAVE_HEADER, 1, sub_block) context.first_block_written = True # if correlation passes, write three sub blocks of pass data if parameters.correlation_passes > 0: sub_block.reset() write_correlation_terms( sub_block, [p.term for p in parameters.correlation_parameters(false_stereo)], [p.delta for p in parameters.correlation_parameters(false_stereo)], ) write_sub_block(sub_blocks, WV_TERMS, 0, sub_block) sub_block.reset() write_correlation_weights(sub_block, [p.weights for p in parameters.correlation_parameters(false_stereo)]) write_sub_block(sub_blocks, WV_WEIGHTS, 0, sub_block) sub_block.reset() write_correlation_samples( sub_block, [p.term for p in parameters.correlation_parameters(false_stereo)], [p.samples for p in parameters.correlation_parameters(false_stereo)], 2 if ((len(channels) == 2) and (not false_stereo)) else 1, ) write_sub_block(sub_blocks, WV_SAMPLES, 0, sub_block) # if wasted bits, write extended integers sub block if wasted > 0: sub_block.reset() write_extended_integers(sub_block, 0, wasted, 0, 0) write_sub_block(sub_blocks, WV_INT32_INFO, 0, sub_block) # if channel count > 2, write channel info sub block if context.pcmreader.channels > 2: sub_block.reset() sub_block.write(8, context.pcmreader.channels) sub_block.write(32, context.pcmreader.channel_mask) write_sub_block(sub_blocks, WV_CHANNEL_INFO, 0, sub_block) # if nonstandard sample rate, write sample rate sub block if context.pcmreader.sample_rate not in ( 6000, 8000, 9600, 11025, 12000, 16000, 22050, 24000, 32000, 44100, 48000, 64000, 88200, 96000, 192000, ): sub_block.reset() sub_block.write(32, context.pcmreader.sample_rate) write_sub_block(sub_blocks, WV_SAMPLE_RATE, 1, sub_block) if (len(channels) == 1) or (false_stereo): # 1 channel block # correlate shifted_0 with terms/deltas/weights/samples if parameters.correlation_passes > 0: assert len(shifted) == 1 correlated = correlate_channels(shifted, parameters.correlation_parameters(false_stereo), 1) else: correlated = shifted else: # 2 channel block # correlate shifted_0/shifted_1 with terms/deltas/weights/samples if parameters.correlation_passes > 0: assert len(mid_side) == 2 correlated = correlate_channels(mid_side, parameters.correlation_parameters(false_stereo), 2) else: correlated = mid_side # write entropy variables sub block sub_block.reset() write_entropy_variables(sub_block, correlated, parameters.entropy_variables) write_sub_block(sub_blocks, WV_ENTROPY, 0, sub_block) # write bitstream sub block sub_block.reset() write_bitstream(sub_block, correlated, parameters.entropy_variables) write_sub_block(sub_blocks, WV_BITSTREAM, 0, sub_block) # write block header with size of all sub blocks write_block_header( writer, sub_blocks.bytes(), block_index, len(channels[0]), context.pcmreader.bits_per_sample, len(channels), (len(channels) == 2) and (false_stereo == 0), len(set([-1, -2, -3]) & set([p.term for p in parameters.correlation_parameters(false_stereo)])) > 0, wasted, first_block, last_block, magnitude, context.pcmreader.sample_rate, false_stereo, crc, ) # write sub block data to stream sub_blocks.copy(writer) # round-trip entropy variables parameters.entropy_variables = [ [wv_exp2(wv_log2(p)) for p in parameters.entropy_variables[0]], [wv_exp2(wv_log2(p)) for p in parameters.entropy_variables[1]], ]
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 from audiotools.ogg import (PageReader, PacketReader, PageWriter, packet_to_pages, packets_to_pages) from audiotools.bitstream import BitstreamRecorder if metadata is None: return elif not isinstance(metadata, VorbisComment): 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) original_ogg = PacketReader(PageReader(open(self.filename, "rb"))) new_ogg = PageWriter(TemporaryFile(self.filename)) sequence_number = 0 # transfer current file's identification packet in its own page identification_packet = original_ogg.read_packet() for (i, page) in enumerate(packet_to_pages( identification_packet, self.__serial_number__, starting_sequence_number=sequence_number)): page.stream_beginning = (i == 0) new_ogg.write(page) sequence_number += 1 # discard the current file's comment packet comment_packet = original_ogg.read_packet() # generate new comment packet comment_writer = BitstreamRecorder(True) comment_writer.write_bytes(b"OpusTags") vendor_string = metadata.vendor_string.encode('utf-8') comment_writer.build("32u %db" % (len(vendor_string)), (len(vendor_string), vendor_string)) comment_writer.write(32, len(metadata.comment_strings)) for comment_string in metadata.comment_strings: comment_string = comment_string.encode('utf-8') comment_writer.build("32u %db" % (len(comment_string)), (len(comment_string), comment_string)) for page in packet_to_pages( comment_writer.data(), self.__serial_number__, starting_sequence_number=sequence_number): new_ogg.write(page) sequence_number += 1 # transfer remaining pages after re-sequencing page = original_ogg.read_page() page.sequence_number = sequence_number sequence_number += 1 new_ogg.write(page) while not page.stream_end: page = original_ogg.read_page() page.sequence_number = sequence_number page.bitstream_serial_number = self.__serial_number__ sequence_number += 1 new_ogg.write(page) original_ogg.close() new_ogg.close()
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 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 from audiotools.ogg import (PageReader, PacketReader, PageWriter, packet_to_pages, packets_to_pages) from audiotools.bitstream import BitstreamRecorder if (metadata is None): return elif (not isinstance(metadata, VorbisComment)): 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) original_ogg = PacketReader(PageReader(open(self.filename, "rb"))) new_ogg = PageWriter(TemporaryFile(self.filename)) sequence_number = 0 # transfer current file's identification packet in its own page identification_packet = original_ogg.read_packet() for (i, page) in enumerate( packet_to_pages(identification_packet, self.__serial_number__, starting_sequence_number=sequence_number)): page.stream_beginning = (i == 0) new_ogg.write(page) sequence_number += 1 # discard the current file's comment packet comment_packet = original_ogg.read_packet() # generate new comment packet comment_writer = BitstreamRecorder(True) comment_writer.write_bytes("OpusTags") vendor_string = metadata.vendor_string.encode('utf-8') comment_writer.build("32u %db" % (len(vendor_string)), (len(vendor_string), vendor_string)) comment_writer.write(32, len(metadata.comment_strings)) for comment_string in metadata.comment_strings: comment_string = comment_string.encode('utf-8') comment_writer.build("32u %db" % (len(comment_string)), (len(comment_string), comment_string)) for page in packet_to_pages(comment_writer.data(), self.__serial_number__, starting_sequence_number=sequence_number): new_ogg.write(page) sequence_number += 1 # transfer remaining pages after re-sequencing page = original_ogg.read_page() page.sequence_number = sequence_number sequence_number += 1 new_ogg.write(page) while (not page.stream_end): page = original_ogg.read_page() page.sequence_number = sequence_number page.bitstream_serial_number = self.__serial_number__ sequence_number += 1 new_ogg.write(page) original_ogg.close() new_ogg.close()