Dvd Audio Extractor Question or 2?

QuadraphonicQuad

Help Support QuadraphonicQuad:

This site may earn a commission from merchant affiliate links, including eBay, Amazon, and others.
According to Wikipedia, Ogg is only a container format; so it can contain either lossy or lossless audio and the two lossless formats it supports are flac and pcm. Given the extreme file size difference between your new flac rips and the previous ogg rips; I would guess that your Ogg rips are lossy. Whether this is real bad or not depends on your end goal. If you are archiving, then it is probably worth redoing. If you are doing it just for convenience of listening, then it is probably fine if your ear can't tell a difference.
 
Thanks, Bill. Here is what MediaInfo reported for one of the Benefit tracks...clearly successfully ripped to 96/24 FLAC as shown in the lower box under "Streams":

Based on the file size, I think you may be decoding the DTS to PCM. That won't hurt anything sonically, but it will take up more space. It may also be exactly what you need and intended, in which case please ignore me!

For comparison, my file of the same track, which is just the undecoded DTS stream in a FLAC "wrapper" is only 56,840,305 bytes.
 
According to Wikipedia, Ogg is only a container format; so it can contain either lossy or lossless audio and the two lossless formats it supports are flac and pcm. Given the extreme file size difference between your new flac rips and the previous ogg rips; I would guess that your Ogg rips are lossy. Whether this is real bad or not depends on your end goal. If you are archiving, then it is probably worth redoing. If you are doing it just for convenience of listening, then it is probably fine if your ear can't tell a difference.

I know, what you mean. Oh brother, what I might do is redo the ones that are what are my real good listens. I have been ripping so much the last two months, I am starting to get burned out, but I am the kinda person I have to stay focused and finish what I started.
I have about, 30 more DVD's to rip and then I need to start on my BD collection, more learning curves, just trying to eliminate the rookie mistakes.
 
Well I switched gears, and now ripping in FLAC instead of ogg. I did some reading and my biggest fear is that I spent a week ripping DVD-A's 5.1 in ogg but it seems like it is OK, still remains lossless, albeit the file size (bitrate) on ogg is in the 100's and the FLAC file size is in the 1000's. In hindsite I wish I had ripped all in FLAC and the tought of ripping them all over again makes me sick, about 70 DVD-A's but the drop down bar started at ogg and I never really paid attention. The good news as I am very successful with the ripping and tagging, so far I only have 5 that I couldn't rip for various reasons.
Please tell me that ogg is not real bad.

Sadly, if the file size is that different then it is lossy.

I redid all my .ape stereo files to .flac and it was a simple one step process using foobar. But .ape is lossless.

I don't have any experience with.ogg files but a quick search shows that .ogg is a container and usually contains lossy files and with a file size that different it is going to be lossy.

Edit...post #42 beat me to the punch...
 
I know, what you mean. Oh brother, what I might do is redo the ones that are what are my real good listens. I have been ripping so much the last two months, I am starting to get burned out, but I am the kinda person I have to stay focused and finish what I started.
I have about, 30 more DVD's to rip and then I need to start on my BD collection, more learning curves, just trying to eliminate the rookie mistakes.

Yeah Marpow - just go back and redo the ones that are goning to be in heavy rotation - then get on with the 30 dvds.............the the rest of the oggs you can do at a snail pace. :geek::LB
 
Based on the file size, I think you may be decoding the DTS to PCM. That won't hurt anything sonically, but it will take up more space. It may also be exactly what you need and intended, in which case please ignore me!

For comparison, my file of the same track, which is just the undecoded DTS stream in a FLAC "wrapper" is only 56,840,305 bytes.

Thanks Atrocity. IIRC I mentioned in an earlier post that I ripped a song to undecoded DTS using DVD Audio Extractor. It had a .dts extension but the Oppo 205 did not "see" the file on the USB drive. Do you or anyone else know how to rip the undecoded DTS stream in a FLAC "wrapper" using DVD Audio Extractor without affecting sampling/bit rate?
 
Yeah Marpow - just go back and redo the ones that are goning to be in heavy rotation - then get on with the 30 dvds.............the the rest of the oggs you can do at a snail pace. :geek::LB

No banana prize for me, oh well, note to self, don't do ogg again. Just to finish this I knew about FLAC of course but I have never used DVD AE before and when I noticed the low bit rate and the ogg file type, and the ogg selection from DVDAE, well not an excuse, but I just assumed that was the correct file for ripping DVD-A. Yes, I think I will make two piles, a pile that I want to re-rip now and one that I can do on my leisure.
My next rippping learning will be the Blu Rays with the MKV software, I'll ask more questions in the beginning, less rookie mistakes.
I want to re-rip 40 discs now, 21 later, 61 total in ogg :sick:. Found the problem and recorrected to FLAC with the last 45 for a total of 106 DVD rips, could have been worse, lets say I never even new, now that would be bad.
 
Last edited:
:sick:
Just a FYI in case it helps, above is showing the audio that DVDAE was able to find on the disc:
- (lossless) 96kHz 24bit 2 channel (stereo)
- (lossy) dts surround
- (lossy) ac3 / dolby surround.

Above agrees with I see is listed for the dvd as shown on the back of the package, seen here. Since it is a DVD and not a DVD-Audio or Blu-Ray, the only possible surround options for DTS are lossy: either 'normal' 48kHz or 96kHz (the latter lossy compresses more in the lower frequencies since the bit rate is the same as for 48kHz DTS) - this particular disc appears to only have the 48kHz DTS.


Actually above shows the available 'output' options that will be used for saving the extracted file. Choosing 96000 Hz for a 48000 Hz source means you are telling DVDAE to 'upmix' something that is 48kHz to be 96kHz (this results in saving the same information in twice the space, which has no benefit:) in my opinion).

I don't have this disc (yet) but given what I see in your screenshots I personally would choose DTS 48kHz in the first screen, 'Same as input' on the 2nd screen, and in the 3rd screen I use 'FLAC' as my output format since almost everything can play that format.

I am ripping Benefit now, in surround only in DTS 48Khz. The above info is all correct. I think I will rip again for the stereo 24/96Khz as an experiment in listening choices to see if the surround is so weak that the stereo is a better listening experience. That happens sometimes and a lot of these discs have these high stereo's and low surrounds.
 
Thanks Atrocity. IIRC I mentioned in an earlier post that I ripped a song to undecoded DTS using DVD Audio Extractor. It had a .dts extension but the Oppo 205 did not "see" the file on the USB drive. Do you or anyone else know how to rip the undecoded DTS stream in a FLAC "wrapper" using DVD Audio Extractor without affecting sampling/bit rate?

You have to use Audiomuxer for that. It has that specific feature of taking a .DTS file and wrapping it within a FLAC. Oppo has no trouble outputting it as DTS.
 
You have to use Audiomuxer for that. It has that specific feature of taking a .DTS file and wrapping it within a FLAC. Oppo has no trouble outputting it as DTS.

Thank you for pointing that out. I just processed a bunch using an old Python script because I had forgotten about that!
 
You have to use Audiomuxer for that. It has that specific feature of taking a .DTS file and wrapping it within a FLAC. Oppo has no trouble outputting it as DTS.[/QUOTE]

Unfortunately, Audiomuxer is a Windows based program only. Anyone know of a similar program that runs on Mac OS that will perform this fancy trick?
 
I've run into a snag trying to rip Chicago II DVD-A. I want the seven tracks of Ballet for a Girl in Buchannon combined into one file so that they play without gaps. Here are the screen shots. Any ideas as to why the program won't let me uncheck the box that says to put all chapters into individual files? I've tried reloading everything, changing the output format, etc. I've had the same issue with other DVD-A's but not DVD-V, so I suspect it has something ot do with MLP/DVD-A's. Thanks.

DVDAE1.png


DVDAE2.png


DVDAE3.png
 
I've run into a snag trying to rip Chicago II DVD-A. I want the seven tracks of Ballet for a Girl in Buchannon combined into one file so that they play without gaps. Here are the screen shots. Any ideas as to why the program won't let me uncheck the box that says to put all chapters into individual files? I've tried reloading everything, changing the output format, etc. I've had the same issue with other DVD-A's but not DVD-V, so I suspect it has something ot do with MLP/DVD-A's. Thanks.

View attachment 36157

View attachment 36158

View attachment 36159

I just ripped that a couple of days ago, let me play it later today and I'll get back to you, I always have my JRiver set to gapless.
 
Unfortunately, Audiomuxer is a Windows based program only. Anyone know of a similar program that runs on Mac OS that will perform this fancy trick?

Do you know if Mac OS has Python installed by default? There's a Python script a Squeezebox fan wrote years and years ago that will put undecoded .DTS bits into a .WAV file which can then be converted to FLAC (or whatever). If you're comfortable with the command line, usage is pretty simple. You just need to save the below script as spdifconvert.py, put your .DTS files into a directory and then run the script against them, e.g.:
Code:
spdifconvert.py *.DTS
It may be necessary to change the first line of the script to properly reflect the location of the python executable on your system.

Unfortunately, I didn't write this script, am not at all conversant in Python and am very unlikely to be able to offer any meaningful support. I have run the same script under both Linux and Windows without any trouble, but I'd be lying if I said I understood what I was doing!

This same script will also process AC3 files, just execute it with "*.AC3" instead of ".DTS".

Here's the script:

Code:
#!/usr/bin/python
# Copyright 2005-2007 Steve Tregidgo -- [email protected]
#
# Utility to prepare DTS/AC3 files for SPDIF output.
#
# Contains a table derived from the Xine project.  The table is copyright 2001
# James Courtier-Dutton, and copyright 2000-2005 the Xine project.
#
#   This program is free software; you can redistribute it and/or modify
#   it under the terms of the GNU General Public License as published by
#   the Free Software Foundation; either version 2 of the License, or
#   (at your option) any later version.
#
#   This program is distributed in the hope that it will be useful,
#   but WITHOUT ANY WARRANTY; without even the implied warranty of
#   MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
#   GNU General Public License for more details.
#
#   You should have received a copy of the GNU General Public License
#   along with this program; if not, write to the Free Software
#   Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA  02110-1301  USA
#
#
# References:
# [1]: "Digital Audio Compression Standard (AC-3)"
#      - Document A/52, 2005-12-20
#      - Advanced Television Systems Committee
#
# [2]: "DTS Coherent Acoustics; Core And Extensions"
#      - ETSI TS 102 114 v1.2.1, 2002-12
#
# [3]: "xine_decoder.c"
#      - revision 1.63, 2005-05-28 11:22:05
#
# XXX TODO:
#
# - When printing file length, also print estimate of time...
#
# - Better justification for DTS output frame size than currently... I'm
#   hard-coding it to 512x4 because that's what works, but it's different to
#   AC3's 1536x4.  It may even differ for DTS files of a different source
#   bitrate, so ideally we should choose the output rate based on the input
#   rate somehow...
#
# - We only support the 16-bit packing of DTS streams, not the 14-bit version,
#   and only one endianness.  That's the one I've encountered on DVDs!  I can
#   add support for the others... we can tell what sort we've got by the magic
#   number.

VERSION = '0.4'

class SPDIFConverter:

    # Ref [1]
    ac3_sample_rate_table = [
        48000, # '00' = 0
        44100, # '01' = 1
        32000, # '10' = 2
        None,  # '11' = 3 (reserved)
    ]

    # Ref [1]
    ac3_frame_size_table = [
        ( 32, (  64,   69,   96)), # '000000' =  0
        ( 32, (  64,   70,   96)), # '000001' =  1
        ( 40, (  80,   87,  120)), # '000010' =  2
        ( 40, (  80,   88,  120)), # '000011' =  3
        ( 48, (  96,  104,  144)), # '000100' =  4
        ( 48, (  96,  105,  144)), # '000101' =  5
        ( 56, ( 112,  121,  168)), # '000110' =  6
        ( 56, ( 112,  122,  168)), # '000111' =  7
        ( 64, ( 128,  139,  192)), # '001000' =  8
        ( 64, ( 128,  140,  192)), # '001001' =  9
        ( 80, ( 160,  174,  240)), # '001010' = 10
        ( 80, ( 160,  175,  240)), # '001011' = 11
        ( 96, ( 192,  208,  288)), # '001100' = 12
        ( 96, ( 192,  209,  288)), # '001101' = 13
        (112, ( 224,  243,  336)), # '001110' = 14
        (112, ( 224,  244,  336)), # '001111' = 15
        (128, ( 256,  278,  384)), # '010000' = 16
        (128, ( 256,  279,  384)), # '010001' = 17
        (160, ( 320,  348,  480)), # '010010' = 18
        (160, ( 320,  349,  480)), # '010011' = 19
        (192, ( 384,  417,  576)), # '010100' = 20
        (192, ( 384,  418,  576)), # '010101' = 21
        (224, ( 448,  487,  672)), # '010110' = 22
        (224, ( 448,  488,  672)), # '010111' = 23
        (256, ( 512,  557,  768)), # '011000' = 24
        (256, ( 512,  558,  768)), # '011001' = 25
        (320, ( 640,  696,  960)), # '011010' = 26
        (320, ( 640,  697,  960)), # '011011' = 27
        (384, ( 768,  835, 1152)), # '011100' = 28
        (384, ( 768,  836, 1152)), # '011101' = 29
        (448, ( 896,  975, 1344)), # '011110' = 30
        (448, ( 896,  976, 1344)), # '011111' = 31
        (512, (1024, 1114, 1536)), # '100000' = 32
        (512, (1024, 1115, 1536)), # '100001' = 33
        (576, (1152, 1253, 1728)), # '100010' = 34
        (576, (1152, 1254, 1728)), # '100011' = 35
        (640, (1280, 1393, 1920)), # '100100' = 36
        (640, (1280, 1394, 1920)), # '100101' = 37
    ]

    # Ref [2]
    dts_sample_rate_table = [
        None,  # '0000' =  0
        8000,  # '0001' =  1
        16000, # '0010' =  2
        32000, # '0011' =  3
        None,  # '0100' =  4
        None,  # '0101' =  5
        11025, # '0110' =  6
        22050, # '0111' =  7
        44100, # '1000' =  8
        None,  # '1001' =  9
        None,  # '1010' = 10
        12000, # '1011' = 11
        24000, # '1100' = 12
        48000, # '1101' = 13
        None,  # '1110' = 14
        None,  # '1111' = 15
    ]

    # The stream type codes are taken from the Xine project, and were (as far
    # as I know) determined by James Courtier-Dutton.
    #
    # Ref [3], 'dts_decode_frame'.
    dts_stream_types = {
        512: 0x0b,
        1024: 0x0c,
        2048: 0x0d,
    }

    magic_numbers = {
        'ac3': '\x0b\x77',
        'dts': '\x7f\xfe\x80\x01',
    }
    frame_readers = {}

    spdif_magic = '\x72\xf8\x1f\x4e'

    # Have we written a WAV header?
    written_wav_header = False

    # Initialised by 'read_frame' (when it's seen at least one frame so
    # know the WAV details).  This will be a dict of various WAV
    # parameters.
    wav_header_info = None

    # Position in the WAV file of the actual data (after the header).
    wav_data_start_pos = None

    # Endianness of WAV file and SPDIF data.
    bigendian_wrapper = False
    bigendian_spdif_data = False

    # Number of frames we've read from the source file.
    num_frames_read = 0

    # Running total of output frames with no padding.
    running_unpadded = 0

    # Internal identifier for the incoming stream type.  This must be a key in
    # 'magic_numbers'.
    stream_type = None

    partial_frame = None

    def __init__(self, options, input_file=None, output_file=None):
        import sys

        self.options = options
        self.initialised = False
        self.message_writer = MessageWriter(sys.stderr, self.options['quiet'])

        if self.options['ac3'] and self.options['dts']:
            self.message(1, "Both 'ac3' and 'dts' options specified!")
            self.message(1, "Aborting.")
            return

        # 'stream_type' will either be set by a user option, or auto-detected
        # with magic numbers in 'read_frame'.
        if self.options['ac3']:
            self.stream_type = 'ac3'
        elif self.options['dts']:
            self.stream_type = 'dts'

        self.spdif_metadata = {
            # Size of each SPDIF frame; we'll pad to this.  'read_frame' will
            # set it based on the incoming stream, or it can be set as a user
            # option.
            'frame_size': None,

            # 5-bit value which is put into an SPDIF frame.  May change for
            # each frame; can be overwritten in 'read_frame'.  XXX This does
            # imply a limitation of only being able to read/write one frame at
            # a time.
            'preamble_data_dependent': 0,

            # 5-bit value indicating the type of data in the SPDIF stream.  Set
            # in 'read_frame' later.
            'data_type': None,
        }

        if self.options['frame_size'] is not None:
            self.spdif_metadata['frame_size'] = self.options['frame_size']

        self._eof = {}
        self._seekable = {}

        if input_file is None:
            input_file = sys.stdin
        self.fin = input_file
        if output_file is None:
            output_file = sys.stdout
        self.fout = output_file

        if self.options['wave'] \
        and not self.is_seekable(self.fin) \
        and not self.is_seekable(self.fout):
            self.message(2, "Require either input or output file to be seekable for WAV")
            return

        self.initialised = True


    def is_eof(self, file):
        return self._eof.get(file, False)


    def is_seekable(self, file):
        if not self._seekable.has_key(file):
            if hasattr(file, 'is_seekable'):
                self._seekable[file] = file.is_seekable()
            else:
                try:
                    file.seek(0, 1) # does nothing!
                except IOError:
                    self._seekable[file] = False
                else:
                    self._seekable[file] = True
        return self._seekable[file]


    def read_raw(self, file, num_bytes):
        if self.is_eof(file):
            return ''
        chunks = []
        eof = False
        to_read = num_bytes
        max_pos = self.options.get('truncate_input')
        tell = file.tell
        while to_read > 0 and \
        (max_pos is None or tell() <= max_pos):
            data = file.read(to_read)
            if not data: # EOF
                eof = True
                break
            chunks.append(data)
            to_read = to_read - len(data)
        if eof or \
        (max_pos is not None and tell() > max_pos):
            self._eof[file] = True
        return ''.join(chunks)


    # Return data string, or None if it failed.
    def read_frame(self):
        import struct

        current_fin_pos = self.fin.tell()
        self.partial_frame = None
        is_first_frame = self.num_frames_read == 0

        magic = ''
        discarded = ''

        # No stream type has been set, so we detect one by looking for any one
        # of the known magic numbers.
        if self.stream_type is None:
            checks = []
            for stream_type, want_magic in self.magic_numbers.items():
                checks.append( (len(want_magic), want_magic, stream_type) )
            checks.sort()
            checks.reverse() # longest magic numbers first
            if not checks:
                return None # No magic numbers defined!

            slice_magic = 1 - checks[0][0]

            while self.stream_type is None:
                for length, want_magic, stream_type in checks:
                    if magic[:length] == want_magic:
                        self.stream_type = stream_type
                        break # inner loop break
                else: # didn't break
                    next_byte = self.read_raw(self.fin, 1)
                    if self.is_eof(self.fin):
                        self.partial_frame = discarded + magic
                        return None
                    if checks[0][0] > 1:
                        discarded = discarded + magic[:1 - checks[0][0]]
                        magic = magic[1 - checks[0][0]:] + next_byte
                    else:
                        discarded = discarded + magic
                        magic = next_byte

            self.message(0, 'Detected stream type: ', self.stream_type)

        # Known stream type: look for next magic number to find start of frame.
        else:
            want_magic = self.magic_numbers[self.stream_type]
            slice_magic = 1 - len(want_magic)
            if slice_magic == 0:
                slice_magic = 1
            while 1:
                if magic == want_magic:
                    break
                next_byte = self.read_raw(self.fin, 1)
                if self.is_eof(self.fin):
                    self.partial_frame = discarded + magic
                    return None
                if len(want_magic) > 1:
                    discarded = discarded + magic[:1 - len(want_magic)]
                    magic = magic[1 - len(want_magic):] + next_byte
                else:
                    discarded = discarded + magic
                    magic = next_byte

        # Did we have to skip some data to get here?
        if discarded:
            self.message(-1, 'Discarded ', len(discarded), ' bytes before magic number, starting at pos ', current_fin_pos)
            self.message(-1, 'Bytes were: ', ('\\x%.2x'*len(discarded)) % tuple(map(ord, discarded)))

        # If we've never filled WAV header info before (so self doesn't know
        # anything about it) we'll do so now.  Otherwise, we set the local
        # 'wav_header_info' to None and don't record any of the relevant
        # values.
        if self.wav_header_info is None and self.options['wave']:
            wav_header_info = {}
        else:
            wav_header_info = None

        # Find the relevant method to dispatch to.
        frame_reader = getattr(self, self.frame_readers.get(self.stream_type), None)
        if frame_reader is None:
            self.message(1, 'Unknown stream type ', self.stream_type)
            return None

        # Read the complete frame.  We pass on the magic number data we've
        # already read; 'frame_data' will probably include this at its start.
        frame_data = frame_reader(magic, self.spdif_metadata, wav_header_info, is_first_frame)
        if frame_data is not None:
            self.num_frames_read = self.num_frames_read + 1
            if self.wav_header_info is None and self.options['wave']:
                self.wav_header_info = wav_header_info
        return frame_data


    # Ref [1] contains all the AC-3 info we could possible want.
    def read_ac3_frame(self, magic, spdif_metadata, wav_metadata, is_first_frame):
        import struct

        data = magic + self.read_raw(self.fin, 3)
        if self.is_eof(self.fin):
            self.partial_frame = data
            return None

        # Didn't get enough header data!
        if len(data) < 5:
            self.message(1, 'Couldn\'t read 5 bytes of header data; skipping frame.')
            self.partial_frame = data
            return None

        sync_code = ord(data[4])
        fscod = (sync_code >> 6) & 0x03
        sample_rate = self.ac3_sample_rate_table[fscod]
        if sample_rate is None:
            self.message(1, 'Unsupported sample rate; skipping frame')
            return None
        if is_first_frame:
            self.message(-1, 'Sample rate: ', sample_rate)

        # wav_metadata is None => caller doesn't need it to be filled in.
        if wav_metadata is not None:
            wav_metadata['sample_rate'] = sample_rate
            wav_metadata['bytes_per_sample'] = 2
            wav_metadata['nchannels'] = 2

        try:
            frame_size_row = self.ac3_frame_size_table[sync_code & 0x3f]
        except IndexError:
            self.message(0, 'Unexpected AC3 frame size code ', hex(sync_code & 0x3f), ' (sync_code: ', hex(sync_code), ', fscod: ', hex(fscod), '); skipping frame.')
            return None

        frame_size = frame_size_row[1][fscod]

        got_num_bytes = len(data) # got magic, CRC, sync
        to_read = (frame_size * 2) - got_num_bytes
        data = data + self.read_raw(self.fin, to_read)
        if len(data) != to_read + got_num_bytes:
            self.message(-1, 'Read short frame of ', len(data), ', expected ', to_read + got_num_bytes)
            self.partial_frame = data
            return None

        spdif_metadata['preamble_data_dependent'] = ord(data[5]) & 0x07
        spdif_metadata['data_type'] = 0x01
        if spdif_metadata.get('frame_size') is None:
            spdif_metadata['frame_size'] = 1536 * 4
            self.message(-1, 'Padding output file to frame size ', spdif_metadata['frame_size'])

        # If we can seek in the input file, determine its length and estimate
        # the size of the output file.  We can use this to write an accurate
        # WAV header without seeking back later.
        if self.is_seekable(self.fin):
            current_position = self.fin.tell()
            self.fin.seek(0, 2)
            file_length = self.fin.tell()
            if is_first_frame:
                self.message(-1, 'AC3 length: ', file_length, '; frame length: ', len(data))

            self.fin.seek(current_position, 0)
            if wav_metadata is not None:
                nframes, remainder = divmod(file_length, len(data))
                if remainder:
                    nframes = nframes + 1
                # Guess output size: nframes worth of padded frames
                wav_metadata['data_length'] = spdif_metadata['frame_size'] * nframes
                self.message(-1, 'Guessing ', nframes, ' AC3 frames, WAV length ', wav_metadata['data_length'], ' bytes')

        return data

    frame_readers['ac3'] = 'read_ac3_frame'


    def read_dts_frame(self, magic, spdif_metadata, wav_metadata, is_first_frame):
        import struct

        # Ref [2] for DTS decoding info...
        data = magic + self.read_raw(self.fin, 5)
        if self.is_eof(self.fin) or len(data) < 9:
            self.partial_frame = data
            return None

        # Ref [2], section 5.4.1, FSIZE.
        # 5 bytes after magic number are:
        #   abbbbbcd ddddddee eeeeeeee eeeeffff ffggggxx
        # a: FTYPE (Frame Type)
        # b: SHORT (Deficit Sample Count)
        # c: CPF (CRC Present Flag)
        # d: NBLKS (Number Of PCM Sample Blocks)
        # e: FSIZE: Primary Frame Byte Size
        # f: AMODE (Audio Channel Arrangement)
        # g: SFREQ (Core Audio Sampling Frequency)
        #
        # FSIZE+1 is the byte size of the current frame (including sync).
        # NBLKS+1 is the number of blocks, each containing 32 PCM samples,
        # in the current frame.
        fsize = int((struct.unpack('>L', data[4:8])[0] >> 4) & 0x3fff)
        nblks = int((struct.unpack('>H', data[4:6])[0] >> 2) & 0x7f)

        if is_first_frame:
            self.message(-1, 'DTS frame data: FSIZE ', fsize)
            self.message(-1, 'DTS frame data: NBLKS ', nblks)

        got_num_bytes = len(data) # got magic and a bit more
        to_read = fsize + 1 - got_num_bytes
        data = data + self.read_raw(self.fin, to_read)
        if len(data) != to_read + got_num_bytes:
            self.message(-1, 'Read short frame of ', len(data), ', expected ', to_read + got_num_bytes)
            self.partial_frame = data
            return None

        sfreq = (ord(data[8]) >> 2) & 0x0f
        sample_rate = self.dts_sample_rate_table[sfreq]
        if is_first_frame:
            self.message(-1, 'DTS frame data: SFREQ ', sfreq, '; sample rate is ', sample_rate)
        if sample_rate is None:
            self.message(1, 'Unsupported sample rate; skipping frame')
            return None

        num_samples = 32 * (nblks + 1)
        if not self.dts_stream_types.has_key(num_samples):
            self.message(1, 'Unsupported number of samples ', num_samples)
            return None
        spdif_metadata['data_type'] = self.dts_stream_types[num_samples]
        spdif_metadata['preamble_data_dependent'] = 0

        if spdif_metadata.get('frame_size') is None:
            # XXX This next bit should be justified somehow, probably by
            # considering sample rate etc.
            spdif_metadata['frame_size'] = 512 * 4
            self.message(-1, 'Padding output file to frame size ', spdif_metadata['frame_size'])

        if wav_metadata is not None:
            wav_metadata['sample_rate'] = sample_rate
            wav_metadata['bytes_per_sample'] = 2
            wav_metadata['nchannels'] = 2

        if self.is_seekable(self.fin):
            current_position = self.fin.tell()
            self.fin.seek(0, 2)
            file_length = self.fin.tell()
            if is_first_frame:
                self.message(-1, 'DTS length: ', file_length, '; frame length: ', len(data))
            self.fin.seek(current_position, 0)
            if wav_metadata is not None:
                nframes, remainder = divmod(file_length, fsize + 1)
                if remainder:
                    nframes = nframes + 1
                # Guess output size: nframes worth of padded frames
                wav_metadata['data_length'] = spdif_metadata['frame_size'] * nframes
                self.message(-1, 'Guessing ', nframes, ' DTS frames, WAV length ', wav_metadata['data_length'], ' bytes')

        return data

    frame_readers['dts'] = 'read_dts_frame'


    def write_wav_header_if_not_written(self):
        if not self.written_wav_header:
            self.write_wav_header()


    def write_wav_header(self):
        import struct

        data_length = self.wav_header_info.get('data_length', 0)
        endian_char = self.bigendian_wrapper and '>' or '<'

        if self.written_wav_header:
            return
        if self.wav_header_info is None:
            raise RuntimeError("Asked to write WAV header, but no info available.")

        current_position = self.fout.tell()
        if current_position:
            raise RuntimeError("WAV header not at start of output file.", current_position)

        self.fout.write('RIFF')
        self.fout.write(struct.pack(endian_char + 'L', data_length + 36))
        self.fout.write('WAVEfmt ')

        nchannels = self.wav_header_info['nchannels'] = 2
        block_size = nchannels * self.wav_header_info['bytes_per_sample']
        bytes_per_second = block_size * self.wav_header_info['sample_rate']
        self.fout.write(struct.pack(endian_char + 'LHHLLHH',
            16, # minimal fmt header size
            1, # compression type
            nchannels,
            self.wav_header_info['sample_rate'], # eg 48000
            bytes_per_second,
            block_size,
            self.wav_header_info['bytes_per_sample'] * 8, # bits per sample
        ))
        self.fout.write('data')
        self.fout.write(struct.pack(endian_char + 'L', data_length))

        self.wav_data_start_pos = self.fout.tell()
        self.written_wav_header = True


    def fix_wav_header(self):
        import struct

        if not self.is_seekable(self.fout):
            return

        # XXX The 'is_seekable' test doesn't always seem to be sufficient...
        if self.options['stdout']:
            return

        if self.wav_header_info is None:
            raise RuntimeError("Asked to fix WAV header, but no info available.")

        current_position = self.fout.tell()
        self.fout.seek(0, 2)
        data_length = self.fout.tell() - self.wav_data_start_pos
        if data_length == self.wav_header_info['data_length']:
            # No fixing needed!
            self.message(-1, 'WAV header does not need fixing (good estimate!)')
            self.fout.seek(current_position, 0)
            return

        self.message(-1, 'Fixing WAV header. Data length ', data_length, ' was marked as ', self.wav_header_info.get('data_length', 0))

        endian_char = self.bigendian_wrapper and '>' or '<'
        self.fout.seek(self.wav_data_start_pos - 4, 0)
        self.fout.write(struct.pack(endian_char + 'L', data_length))

        self.fout.seek(4, 0)
        self.fout.write(struct.pack(endian_char + 'L', data_length + self.wav_data_start_pos - 8))

        self.fout.seek(current_position, 0)


    def write_spdif_frame(self, data):
        import array
        import struct

        endian_char = self.bigendian_spdif_data and '>' or '<'

        # Ref [1], Annex B, section 4.5: if we've had a run of data with
        # absolutely no padding, we need to insert a pair of zeroes to
        # facilitate auto-detection of stream type.
        if self.running_unpadded >= (4096 * 32):
            self.fout.write('\000\000')
            self.running_unpadded = 0

        # First 32 bits of preamble = sync word
        header = self.spdif_magic

        # Next 16 bits of preamble = burst info.
        #   nnnddddd errttttt
        #   nnn: data stream number (0b000; main stream)
        # ddddd: data type dependent...
        #     e: error flag, set to 0b0
        #    rr: reserved, equal to 0b00
        # ttttt: data type (eg 0b00001 for AC3)
        header = header + struct.pack('B', self.spdif_metadata['data_type'] & 0x1f)
        header = header + struct.pack('B', self.spdif_metadata['preamble_data_dependent'] & 0x1f)

        # Next 16 bits of preamble = burst bits
        header = header + struct.pack(endian_char + 'H', len(data) * 8)

        padding_length = self.spdif_metadata['frame_size'] - len(data) - len(header)
        if padding_length == 0:
            self.running_unpadded = self.running_unpadded + len(data) + len(header)

        data_array = array.array('H', data + ('\000' * padding_length))
        data_array.byteswap()

        self.fout.write(header)
        data_array.tofile(self.fout)


    def go(self):

        import time
        start_time = time.time()

        while 1:
            frame = self.read_frame()
            if frame is None:
                if not self.options['persevere'] or self.is_eof(self.fin):
                    break
                self.message(0, "Encountered bad frame; looking for next good frame.")
                continue
            if self.options['wave']:
                self.write_wav_header_if_not_written()
            self.write_spdif_frame(frame)

        if self.options['wave']:
            self.fix_wav_header()

        end_time = time.time()

        spdif.message(-1, "Completed transformation.  Took %dm%.2ds." % divmod(end_time-start_time, 60))


    def message(self, msg_level, *message_items):
        self.message_writer.write(msg_level, *message_items)


class MessageWriter:
    def __init__(self, output_file, quiet_level):
        self.output_file = output_file
        self.quiet_level = quiet_level
    def write(self, message_level, *message_items):
        if message_level >= self.quiet_level:
            self.output_file.write(''.join(map(str, message_items)) + '\n')


class MultiFileReader:
    _is_seekable = None

    def __init__(self, file_list):
        if not file_list:
            raise RuntimeError('Cannot instantiate MultiFileReader without at least one file!')
        self._file_list = file_list
        self._file = file_list[0]
        self._file_list_index = 0
        self._offsets = [None] * len(file_list)
        self._offsets[0] = 0

    def _get_offset(self, file_index):
        if self._offsets[file_index] is None:
            prev_offset = self._get_offset(file_index - 1)
            prev_file = self._file_list[file_index - 1]
            old_pos = prev_file.tell()
            prev_file.seek(0, 2)
            prev_file_length = prev_file.tell()
            prev_file.seek(old_pos, 0)
            self._offsets[file_index] = prev_offset + prev_file_length
        return self._offsets[file_index]

    def is_seekable(self):
        if self._is_seekable is None:
            self._is_seekable = True
            for f in self._file_list:
                try:
                    f.seek(0, 1) # does nothing!
                except IOError:
                    self._is_seekable = False
                    break
        return self._is_seekable

    def tell(self):
        i = 0
        pos = 0
        while i <= self._file_list_index:
            pos = pos + self._get_offset(i)
            i = i + 1
        return pos + self._file.tell()

    def seek(self, pos, whence=0):
        if not self._is_seekable:
            raise IOError
        if whence == 1:
            target = pos + self.tell()
        elif whence == 2:
            self._file_list_index = len(self._file_list) - 1
            self._file = self._file_list[self._file_list_index]
            self._file.seek(0, 2)
            target = pos + self.tell()
        else:
            target = pos

        i = 0
        offsets = [None] * len(self._file_list)
        offsets[0] = 0
        while i < len(self._file_list):
            if i + 1 < len(self._file_list):
                offsets[i+1] = self._get_offset(i+1)
                if target > offsets[i+1]:
                    i = i + 1
                    continue
            self._file_list_index = i
            self._file = self._file_list[self._file_list_index]
            self._file.seek(target-offsets[i], 0)
            break

    def read(self, num=None):
        data = []
        while 1:
            if num is not None and num <= 0:
                break
            if num is None:
                chunk = self._file.read()
            else:
                chunk = self._file.read(num)
            if not chunk:
                if self._file_list_index == len(self._file_list) - 1:
                    break
                self._file_list_index = self._file_list_index + 1
                self._file = self._file_list[self._file_list_index]
                self._file.seek(0)
                continue
            data.append(chunk)
            if num is not None:
                num -= len(chunk)
        return ''.join(data)


def print_help(program_name):
    assume_max_option_width = 18
    option_format = '%%%ds' % (assume_max_option_width,)

    print
    print "  Copyright 2005-2007 Steve Tregidgo -- [email protected]"
    print "  Version:", VERSION
    print
    print "  Usage: %s [options] [INPUT_FILE_1 [INPUT_FILE_2 [...]]]" % (program_name,)
    print
    print "  Given an AC3 or DTS file from a DVD-Video, this utility will write a WAV file"
    print "  encapsulating that digital data in an IEC61937 stream.  If that WAV is sent"
    print "  out over an S/PDIF link in the normal IEC60958 way, without modification for"
    print "  volume etc, a digital receiver should be able to play the multichannel audio"
    print "  therein."
    print
    print "  Start with a file taken from a DVD-Video; de-multiplex the AC3 or DTS file"
    print "  required, then run this utility on it to generate a WAV file.  It is"
    print "  recommended that the WAV be further transformed to FLAC, which supports the"
    print "  storage of metadata values, although the compression will likely be very poor."
    print "  This utility was written for the benefit of SqueezeBox2 owners; playback of"
    print "  such a WAV or FLAC on the SB2, with digital output, will result in the"
    print "  original multichannel audio being passed through to the receiver."
    print
    print "  Each INPUT_FILE may be a single file name or may contain wildcards (in which"
    print "  case the expanded list of files will be sorted alphabetically before being"
    print "  processed).  There may be multiple INPUT_FILEs.  If no output files are"
    print "  specified, output names will be derived from the input file names.  If just"
    print "  one output file is to be produced, you may specify '--stdout' to write that"
    print "  data to stdout.  You may specify '--stdin' to read from stdin instead of from"
    print "  named files."
    print
    print "  Options:"
    for option_string, text in [
        ('--stdin', 'Read input from stdin.  May not be used with INPUT_FILE.'),
        ('--stdout', 'Write output to stdout.  May not be used with \'--output\'.'),
        ('', 'NOTE: using both stdin and stdout with WAV output will give\na WAV file with invalid data length.'),
        ('--output=FILENAME', 'Specify an output filename.  If there are multiple input\nfiles, you must use this option once for each corresponding\noutput file (or omit the option altogether to derive names\nautomatically).'),
        ('-j, --join', 'Concatenate all input files before processing.  A single\noutput file will be produced; \'--output\' must be specified\nas the name cannot be guessed.'),
        ('', ''),
        ('-a, --ac3', 'The input stream is expected to be AC-3.',),
        ('-d, --dts', 'The input stream is expected to be 16-bit big-endian DTS\n(such as is obtained from ripping a DVD).',),
        ('', 'NOTE: in most cases \'--ac3\' and \'--dts\' can be omitted,\nand the stream type will be auto-detected.'),
        ('--persevere', 'Conversion is usually aborted if a bad frame is encountered,\nas the utility expects that it will have difficulty with the\nrest of the data.  Sometimes a file might be encountered\nwith a bad frame at the start, followed by good frames;\nsupplying this option causes the conversion to keep trying\nto find a good frame.  If the file is full of junk, the\nutility might take a long time to wade through it!'),
        ('', ''),
        ('-w, --wave', 'Prefix output with a WAV header (default).  If not using\nstdout for output, the WAV header will definitely contain an\naccurate data length; if using stdout, the recorded length\nwill be an estimate, which may be wrong if frame sizes vary\nin the source file.  If using both stdin and stdout, the WAV\nheader will definitely be wrong (the length will be zero).'),
        ('-r, --raw', 'Don\'t write a WAV header.  If both \'--raw\' and \'--wave\' are\nspecified, the last one supplied takes precedence.'),
        ('', ''),
        ('-q, --quiet', 'Print fewer messages to stderr.  Repeat this option to\nsilence all messages.'),
        ('-v, --verbose', 'Print more messages to stderr.'),
        ('--frame-size=SIZE', 'Advanced users\' setting: override the frame size in the\noutput file.  We normally pad to a frame size of 6144\n(=1536*4) or 2048 (=512*4); if the resultant WAV file\ndoesn\'t appear to be the correct length (as indicated by any\nnormal audio software which can read WAV files -- but don\'t\nplay the file!) then adjusting this value might help.  Use\n\'--verbose\' to discover the frame size used, and then try\nrunning again with the \'--frame-size\' option set to a\ndifferent value (multiplied up or down by an integer).  If\nyou discover something which works for your file, please let\nme know so I can try setting that size automatically\ndepending on the input file.'),
        ('--no-massage', 'When multiple input files are processed at once, a partial\nframe at the end of one file will be prepended to the next\nfile.  This is useful if the original AC3/DTS file was not\nsplit on a frame boundary.  To simply discard partial frames\nin each file, use this option.'),
        ('', ''),
        ('-h, -?, --help', 'Display this help text.'),

    ]:
        print option_format % (option_string,),
        for i, line in enumerate(text.split('\n')):
            if i:
                print (' ' * assume_max_option_width),
            print line

    print
    print "  If you have any comments or improvements, please email [email protected]"
    print

if __name__ == '__main__':

    import getopt
    import glob
    import os
    import StringIO
    import struct
    import sys

    program_name = os.path.split(sys.argv[0])[-1]

    short_options = ''.join([
        'o:', # == 'output'
        'j', # == 'join'
        'a', # == 'ac3'
        'd', # == 'dts'
        'w', # == 'wave'
        'r', # == 'raw'
        'q', # == 'quiet'
        'v', # == 'verbose'
        'h', # == 'help'
        '?', # == 'help'
    ])
    long_options = [
        'ac3',
        'dts',
        'stdin',
        'stdout',
        'output=',
        'join',
        'no-massage',
        'wave',
        'raw',
        'quiet',
        'verbose',
        'frame-size=',
        'truncate-input=',
        'persevere',
        'help',
    ]

    try:
        cli_options, cli_arguments = getopt.getopt(sys.argv[1:], short_options, long_options)
    except getopt.error, err:
        sys.stderr.write("Usage error!\n" + str(err) + '\n\n')
        sys.stderr.write("For help, type: %s --help\n" % (program_name,))
        sys.exit(1)

    options = {
        'wave': True,
        'stdin': False,
        'stdout': False,
        'output': [],
        'join': False,
        'no-massage': False,
        'quiet': 0, # 0, 1, 2
        'ac3': False,
        'dts': False,
        'frame_size': None,
        'truncate_input': None,
        'persevere': False,
        'help': False,
    }

    for option, value in cli_options:
        if option in ['--ac3', '-a']:
            options['ac3'] = True
        elif option in ['--dts', '-d']:
            options['dts'] = True
        elif option in ['--wave', '-w']:
            options['wave'] = True
        elif option in ['--raw', '-r']:
            options['wave'] = False
        elif option in ['--quiet', '-q']:
            options['quiet'] = options['quiet'] + 1
        elif option in ['--verbose', '-v']:
            options['quiet'] = options['quiet'] - 1
        elif option in ['--stdin']:
            options['stdin'] = True
        elif option in ['--stdout']:
            options['stdout'] = True
        elif option in ['--frame-size']:
            try:
                options['frame_size'] = int(value)
            except ValueError:
                sys.stderr.write("'frame-size' option not an int: " + value)
                sys.exit(1)
        elif option in ['--truncate-input']:
            try:
                options['truncate_input'] = long(value)
            except ValueError:
                sys.stderr.write("'truncate-input' option not an int: " + value)
                sys.exit(1)

        elif option in ['-o', '--output']:
            options['output'].append(value)

        elif option in ['--persevere']:
            options['persevere'] = True

        elif option in ['-j', '--join']:
            options['join'] = True

        elif option in ['--no-massage']:
            options['no-massage'] = True

        elif option in ['--help', '-h', '-?']:
            options['help'] = True

    if options['help']:
        print_help(program_name)
        sys.exit(0)

    message = MessageWriter(sys.stderr, options['quiet']).write

    input_fname_list = []
    _seen = {}
    for argument in cli_arguments:
        fname_list = glob.glob(argument)
        if not fname_list:
            message(0, "Warning: no files matched by input argument: ", argument)
        fname_list.sort()
        for fname in fname_list:
            if _seen.has_key(fname):
                message(0, "Warning: processing this file more than once: ", fname)
            _seen[fname] = _seen.get(fname, 0) + 1
            input_fname_list.append(fname)

    if input_fname_list and options['stdin']:
        message(1, "Cannot mix stdin and named file inputs.")
        message(1, "Aborting.")
        sys.exit(1)

    if options['output'] and options['stdout']:
        message(1, "Cannot mix stdout and named file outputs.")
        message(1, "Aborting.")
        sys.exit(1)

    if options['stdin']:
        input_fname_list.append(None)
    elif not input_fname_list:
        message(1, "Not using stdin, and no input file name specified!")
        message(1, "Aborting.")
        sys.exit(1)

    if options['stdout']:
        options['output'].append(None)

    if options['join']:
        num_input_files = 1
    else:
        num_input_files = len(input_fname_list)

    if len(options['output']) > num_input_files:
        message(1, "More output files specified than input files.")
        message(1, "Aborting.")
        sys.exit(1)

    if options['output'] and len(options['output']) < num_input_files:
        message(1, "At least one output file specified, but fewer than input files.")
        message(1, "Aborting.")
        sys.exit(1)

    if input_fname_list and not options['output']:
        if options['join']:
            message(1, "Cannot derive automatic output filename when joining input files.")
            message(1, "Aborting.")
            sys.exit(1)
        for input_fname in input_fname_list:
            if input_fname is None:
                message(1, "Cannot derive automatic output filename when using stdin.")
                message(1, "Aborting.")
                sys.exit(1)
            if options['wave']:
                output_fname = input_fname + '.wav'
            else:
                output_fname = input_fname + '.spdif'
            options['output'].append(output_fname)

    input_file_list = []
    for input_fname in input_fname_list:
        if input_fname is None:
            input_file = sys.stdin
        else:
            input_file = open(input_fname, 'rb')
        input_file_list.append(input_file)
    if options['join']:
        input_file_list = [MultiFileReader(input_file_list)]
        input_fname_list = ['[%s]' % ('; '.join([
            input_fname is None and '<stdin>' or input_fname
            for input_fname in input_fname_list
        ]),)]

    remainder_file = None
    for input_fname, input_file, output_fname in zip(input_fname_list, input_file_list, options['output']):
        if input_fname is None:
            message(0, "Reading input from stdin")
        else:
            message(0, "Reading input from file ", input_fname)
        if remainder_file is not None:
            input_file = MultiFileReader( [remainder_file, input_file] )
            remainder_file = None
            message(0, "Including data remaining from previous input file.")

        if output_fname is None:
            output_file = sys.stdout
            message(0, "Writing output to stdout")
        else:
            output_file = open(output_fname, 'w+b')
            message(0, "Writing output to file ", output_fname)

        spdif = SPDIFConverter(options.copy(), input_file=input_file, output_file=output_file)
        if not spdif.initialised:
            spdif.message(0, "There was an error initialising.  Skipping file: ", input_fname)
            continue

        try:
            spdif.go()
        except RuntimeError, error:
            spdif.message(0, "Received RuntimeError during conversion of file: ", input_fname)
            spdif.message(0, error)

        except KeyboardInterrupt:
            print "Interrupted!  Read this many bytes:", spdif.fin.tell()
            print "Aborting."
            break

        else:
            if spdif.partial_frame and not options['no-massage']:
                remainder_file = StringIO.StringIO(spdif.partial_frame)
                remainder_file.seek(0, 0)
                message(-1, "Remainder of ", len(spdif.partial_frame), " bytes.  Prepending to next input file.")
            message(0, '') # blank line
 
I've run into a snag trying to rip Chicago II DVD-A. I want the seven tracks of Ballet for a Girl in Buchannon combined into one file so that they play without gaps. Here are the screen shots. Any ideas as to why the program won't let me uncheck the box that says to put all chapters into individual files? I've tried reloading everything, changing the output format, etc. I've had the same issue with other DVD-A's but not DVD-V, so I suspect it has something ot do with MLP/DVD-A's. Thanks.

View attachment 36157

View attachment 36158

View attachment 36159
I Hope we get an answer to this one, because I've had to combine tracks after the fact using Audacity, and this would save a lot of time!
 
So . much . confusion. here.

1) If you are inputting DTS (or AC3) audio to DAE, and you select 'WAV' as output , it is *decoding* the DTS audio. And for DTS (or AC3), if you decode and also set output to anything other than 'same as input', you are resampling. Upsampling, in the case of a DTS core audio stream that is being decoded to 96kHz wav. Sure, the file will 'read' as 96kHz. But the precious ultrasonic content you need so badly from the original DTS 96/24 stream, is not really there.

In all my experience (I have a fairly recent version of DAE, though perhaps not the latest), DAE CANNOT decode a DTS 96/24 stream directly to its native 96/24 PCM audio. It is limited to decoding the DTS 'core' (48/24 adio). The only way I know of to rip a DTS 96/24 stream 'as it is' with DAE, is to use its 'Direct Stream Demux' output option. DAE should output a set of 96/24 .dts files, one per track. Then, if you need to tag them, you can do what himey suggests: use Audiomuxer's 'DTS or AC3 to S/PDIFWav' converter option, which puts the .dts files in a .wav wrapper. You can also choose to output the wav-wrapped files as FLAC, which can then be tagged. In this case, FLAC does not 'shrink' the files in any way. It's just adding another 'wrapper' layer on top of the .wav header. The files are *never decoded to PCM* during this process. The resulting FLAC or WAV files are still DTS. Your player must decode them. This means you must have a way to do bit-perfect output from your player to the decoder.

That is what I do. I don't use an Oppo, or any other standalone hardware player; I play the FLAC-wrapped .dts files from a hard drive using Foobar + WASAPI output, sending the bitstream to my AVR by HDMI. The AVR recognizes, displays, and decodes the audio correctly. I have no experience of what an Oppo can or can't do with files processed as above (though I would expect it could at least play the 'raw' 96/24 .dts files, either decoding them before output, or sending them as bitstream to the AVR for decoding, depending on your Oppo settings)


2) If you are inputting DVD-A (lossless MLP or PCM audio) to DAE, then it *CAN* rip that to .wav (and FLAC) at whatever their native PCM sample rate/bit depth is. There is no limitation AFAIK there. (However, IIRC DTS-MA and Dolby True Audio do not fall into this category, even though they are lossless.)
 
Last edited:
Back
Top