#!/usr/bin/env ruby
# $Id: cdio.rb,v 1.21 2008/05/02 13:05:40 karl Exp $
#
#  Copyright (C) 2006, 2007, 2008, 2009 Rocky Bernstein <rocky@gnu.org>
#
#  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 3 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, see <http://www.gnu.org/licenses/>.

# Author::    Rocky Bernstein  (mailto:rocky@gnu.org)
#
# = cdio
# Module for CD Input and Control library.
# == Version
# :include:VERSION
#
# == SYNOPSIS
#
# The CD Input and Control library (pycdio) encapsulates CD-ROM
# reading and control. Applications wishing to be oblivious of the OS-
# and device-dependent properties of a CD-ROM can use this library.
#
#     require "cdio'
# 
#     cd_drives = Cdio::devices(Rubycdio::DRIVER_DEVICE)
#     for drive in cd_drives
#        puts "Drive %s" % drive
#     end
#     drivers.each_pair { |driver_name, driver_id|
#     if Cdio::driver?(driver_id):
#         puts "Driver %s is installed." % driver_name
#     end
# 
# 
# == DESCRIPTION
# 
#
# This is an Ruby interface to the GNU CD Input and Control library,
# <tt>libcdio</tt>, written in C. The library encapsulates CD-ROM reading
# and control. Ruby programs wishing to be oblivious of the OS- and
# device-dependent properties of a CD-ROM can use this library.
#
# 
# The encapsulation is done in two parts. The lower-level Ruby
# module is called rubycdio and is generated by SWIG.
# 
# This module uses rubycdio. Actually, there are no objects in
# module, but there are in classes Device and Track.
# 
# Although <tt>rubycdio</tt> is perfectly usable on its own, it is expected
# that cdio is what most people will use. As rubycdio more closely
# models the C interface <tt>libcdio</tt>, it is conceivable (if unlikely)
# that die-hard libcdio C users who are very familiar with that
# interface could prefer that.
# 
# require 'rubygems'; require 'ruby-debug'; Debugger.start
require "rubycdio"

module Cdio

  # Raise a Driver Error exception on error as determined by drc
  def possibly_raise_exception__(drc, msg=nil)
    if drc==Rubycdio::DRIVER_OP_SUCCESS
      return
    end
    if drc==Rubycdio::DRIVER_OP_ERROR
      raise DriverError
    end
    if drc==Rubycdio::DRIVER_OP_UNINIT
      raise DriverUninitError
    end
    if drc==Rubycdio::DRIVER_OP_UNSUPPORTED
      raise DriverUnsupportedError
    end
    if drc==Rubycdio::DRIVER_OP_NOT_PERMITTED
      raise DriverUnsupportedError
    end
    if drc==Rubycdio::DRIVER_OP_BAD_PARAMETER
      raise DriverBadParameterError
    end
    if drc==Rubycdio::DRIVER_OP_BAD_POINTER
      raise DriverBadPointerError
    end
    if drc==Rubycdio::DRIVER_OP_NO_DRIVER
      raise NoDriverError
    end
    raise DeviceException('unknown exception %d' % drc)
  end
  
  # = class DeviceException
  # General device or driver exceptions
  
  class DeviceException < Exception
  end
  
  class DriverError < DeviceException; end
  class DriverUnsupportedError < DeviceException; end
  class DriverUninitError < DeviceException; end
  class DriverNotPermittedError < DeviceException; end
  class DriverBadParameterError < DeviceException; end
  class DriverBadPointerError < DeviceException; end
  class NoDriverError < DeviceException; end
  
  class TrackError < DeviceException; end


  # Note: the keys below match those the names returned by
  # cdio_get_driver_name(). 
  def drivers()
    return {
      :Unknown     => Rubycdio::DRIVER_UNKNOWN,
      :AIX         => Rubycdio::DRIVER_AIX,
      :BSDI        => Rubycdio::DRIVER_BSDI,
      :FreeBSD     => Rubycdio::DRIVER_FREEBSD,
      :"GNU/Linux" => Rubycdio::DRIVER_LINUX,
      :Solaris     => Rubycdio::DRIVER_SOLARIS,
      :"OS X"      => Rubycdio::DRIVER_OSX,
      :WIN32       => Rubycdio::DRIVER_WIN32,
      :CDRDAO      => Rubycdio::DRIVER_CDRDAO,
      :"BIN/CUE"   => Rubycdio::DRIVER_BINCUE,
      :NRG         => Rubycdio::DRIVER_NRG,
      :device      => Rubycdio::DRIVER_DEVICE
    }
  end
  
  def read_mode2blocksize()
    return {
      Rubycdio::READ_MODE_AUDIO =>  Rubycdio::CD_FRAMESIZE_RAW,
      Rubycdio::READ_MODE_M1F1  =>  Rubycdio::M2RAW_SECTOR_SIZE,
      Rubycdio::READ_MODE_M1F2  =>  Rubycdio::CD_FRAMESIZE,
      Rubycdio::READ_MODE_M2F1  =>  Rubycdio::M2RAW_SECTOR_SIZE,
      Rubycdio::READ_MODE_M2F2  =>  Rubycdio::CD_FRAMESIZE
    }
  end


  #    close media tray in CD drive if there is a routine to do so. 
  #    The +driver id +is returned. A DeviceException is thrown on error.
  def close_tray(drive=nil, driver_id=Rubycdio::DRIVER_UNKNOWN)
    drc, found_driver_id = Rubycdio::close_tray(drive, driver_id)
    possibly_raise_exception__(drc)
    return found_driver_id
  end
  
  #    Returns: [device, driver]
  #
  #    Return a string containing the default CD device if none is
  #    specified.  if +driver_id+ is DRIVER_UNKNOWN or DRIVER_DEVICE
  #    then one set the default device for that.
  #    
  #    nil is returned as the device if we couldn't get a default
  #    device.
  def default_device_driver(driver_id=Rubycdio::DRIVER_DEVICE)
    return Rubycdio::get_default_device_driver(driver_id)
  end

  #    Returns: [device1, device2, ...]
  #
  #    Get an list of device names.
  def devices(driver_id=Rubycdio::DRIVER_UNKNOWN)
    return Rubycdio::get_devices(driver_id)
  end
  
  #    Returns: [device1, device2, ... driver_id]
  #
  #    Like get_devices, but return the driver_id which may be different
  #    from the passed-in driver_id if it was Rubycdio::DRIVER_DEVICE or
  #    Rubycdio::DRIVER_UNKNOWN. The return +driver_id+ may be useful because
  #    often one wants to get a drive name and then *open* it
  #    afterwards. Giving the driver back facilitates this, and speeds things
  #    up for libcdio as well.
  def devices_ret(driver_id=Rubycdio::DRIVER_UNKNOWN)
    devices = Rubycdio::get_devices_ret(driver_id)
  end

  #    Get an array of device names in search_devices that have at least
  #    the capabilities listed by the capabities parameter.  
  #        
  #    If any is false then every capability listed in the
  #    extended portion of capabilities (i.e. not the basic filesystem)
  #    must be satisified. If any is true, then if any of the
  #    capabilities matches, we call that a success.
  #
  #    To find a CD-drive of any type, use the mask Rubycdio::CDIO_FS_MATCH_ALL.
  #
  #    The array of device names is returned or nil if we couldn't get a
  #    default device.  It is also possible to return a non nil but after
  #    dereferencing the the value is nil. This also means nothing was
  #    found.
  def devices_with_cap(capabilities, any=false)
    return Rubycdio::get_devices_with_cap(capabilities, any)
  end

  #    Returns: [device1, device2..., driver_id]
  #
  #    Like cdio_get_devices_with_cap but we return the driver we found
  #    as well. This is because often one wants to search for kind of drive
  #    and then *open* it afterwards. Giving the driver back facilitates this,
  #    and speeds things up for libcdio as well.
  def devices_with_cap_ret(capabilities, any=false)
    return Rubycdio::get_devices_with_cap_ret(capabilities, any)
  end
  
  #    return bool
  #    
  #    Return true if we have driver driver_id.
  def driver?(driver_id)
    if driver_id.class == Fixnum
      return Rubycdio::have_driver(driver_id) == 1
    elsif driver_id.class == Symbol and drivers.member?(driver_id)
      ret = Rubycdio::have_driver(drivers[driver_id])
      if ret == 0 then return false end
      if ret == 1 then return true end
      raise ArgumentError
    else
      raise ArgumentError
    end
  end
  
  #--
  #   FIXME ? is not quite right
  #    binfile?(binfile_name)->cue_name
  #++
  #    
  #    Determine if +binfile_name+ is the BIN file part of a CDRWIN CD
  #    disk image.
  #    
  #    Return the corresponding CUE file if bin_name is a BIN file or
  #    nil if not a BIN file.
  def binfile?(binfile_name)
    return Rubycdio::is_binfile(binfile_name)
  end

  #--
  #   FIXME ? is not quite right
  #++
  #    return bin_name for a corresponding CUE file
  #    
  #    Determine if cuefile_name is the CUE file part of a CDRWIN CD
  #    disk image.
  #    
  #    Return the corresponding BIN file if bin_name is a CUE file or
  #    nil if not a CUE file.
  def cuefile?(cuefile_name)
    return Rubycdio::is_cuefile(cuefile_name)
  end

  #    Returns: bool
  #
  #    Return true if source refers to a real hardware CD-ROM.
  def device?(source, driver_id=Rubycdio::DRIVER_UNKNOWN)
    if not driver_id then driver_id=Rubycdio::DRIVER_UNKNOWN end
    return Rubycdio::device?(source, driver_id)
  end

  #    Returns: bool
  #    
  #    Determine if nrgfile_name is a Nero CD disc image
  def nrg?(nrgfile_name)
    return Rubycdio::nrg?(nrgfile_name)
  end

  #    tocfile?(tocfile_name)->bool
  #    
  #    Determine if +tocfile_name+ is a cdrdao CD disc image
  def tocfile?(tocfile_name)
    return Rubycdio::tocfile?(tocfile_name)
  end

  #    Convert +bitmask+ for miscellaneous drive properties
  #    into a dictionary of drive capabilities
  def convert_drive_cap_misc(bitmask)
    result={}
    if bitmask & Rubycdio::DRIVE_CAP_ERROR
      result[:DRIVE_CAP_ERROR] = true
    end
    if bitmask & Rubycdio::DRIVE_CAP_UNKNOWN
      result[:DRIVE_CAP_UNKNOWN] = true
    end
    if bitmask & Rubycdio::DRIVE_CAP_MISC_CLOSE_TRAY
      result[:DRIVE_CAP_MISC_CLOSE_TRAY] = true
    end
    if bitmask & Rubycdio::DRIVE_CAP_MISC_EJECT
      result[:DRIVE_CAP_MISC_EJECT] = true
    end
    if bitmask & Rubycdio::DRIVE_CAP_MISC_LOCK
      result['DRIVE_CAP_MISC_LOCK'] = true
    end
    if bitmask & Rubycdio::DRIVE_CAP_MISC_SELECT_SPEED
      result[:DRIVE_CAP_MISC_SELECT_SPEED] = true
    end
    if bitmask & Rubycdio::DRIVE_CAP_MISC_SELECT_DISC
      result[:DRIVE_CAP_MISC_SELECT_DISC] = true
    end
    if bitmask & Rubycdio::DRIVE_CAP_MISC_MULTI_SESSION
      result[:DRIVE_CAP_MISC_MULTI_SESSION] = true
    end
    if bitmask & Rubycdio::DRIVE_CAP_MISC_MEDIA_CHANGED
      result[:DRIVE_CAP_MISC_MEDIA_CHANGED] = true
    end
    if bitmask & Rubycdio::DRIVE_CAP_MISC_RESET
      result[:DRIVE_CAP_MISC_RESET] = true
    end
    if bitmask & Rubycdio::DRIVE_CAP_MISC_FILE
      result[:DRIVE_CAP_MISC_FILE] = true
    end
    return result
  end
  
  #    Convert bit mask for drive read properties
  #    into a dictionary of drive capabilities
  def convert_drive_cap_read(bitmask)
    result={}
    if bitmask & Rubycdio::DRIVE_CAP_READ_AUDIO
      result[:DRIVE_CAP_READ_AUDIO] = true
    end
    if bitmask & Rubycdio::DRIVE_CAP_READ_CD_DA
      result[:DRIVE_CAP_READ_CD_DA] = true
    end
    if bitmask & Rubycdio::DRIVE_CAP_READ_CD_G
      result[:DRIVE_CAP_READ_CD_G] = true
    end
    if bitmask & Rubycdio::DRIVE_CAP_READ_CD_R
      result[:DRIVE_CAP_READ_CD_R] = true
    end
    if bitmask & Rubycdio::DRIVE_CAP_READ_CD_RW
      result[:DRIVE_CAP_READ_CD_RW] = true
    end
    if bitmask & Rubycdio::DRIVE_CAP_READ_DVD_R
      result[:DRIVE_CAP_READ_DVD_R] = true
    end
    if bitmask & Rubycdio::DRIVE_CAP_READ_DVD_PR
      result[:DRIVE_CAP_READ_DVD_PR] = true
    end
    if bitmask & Rubycdio::DRIVE_CAP_READ_DVD_RAM
      result[:DRIVE_CAP_READ_DVD_RAM] = true
    end
    if bitmask & Rubycdio::DRIVE_CAP_READ_DVD_ROM
      result[:DRIVE_CAP_READ_DVD_ROM] = true
    end
    if bitmask & Rubycdio::DRIVE_CAP_READ_DVD_RW
      result[:DRIVE_CAP_READ_DVD_RW] = true
    end
    if bitmask & Rubycdio::DRIVE_CAP_READ_DVD_RPW
      result[:DRIVE_CAP_READ_DVD_RPW] = true
    end
    if bitmask & Rubycdio::DRIVE_CAP_READ_C2_ERRS
      result[:DRIVE_CAP_READ_C2_ERRS] = true
    end
    if bitmask & Rubycdio::DRIVE_CAP_READ_MODE2_FORM1
      result[:DRIVE_CAP_READ_MODE2_FORM1] = true
    end
    if bitmask & Rubycdio::DRIVE_CAP_READ_MODE2_FORM2
      result[:DRIVE_CAP_READ_MODE2_FORM2] = true
    end
    if bitmask & Rubycdio::DRIVE_CAP_READ_MCN
      result[:DRIVE_CAP_READ_MCN] = true
    end
    if bitmask & Rubycdio::DRIVE_CAP_READ_ISRC
      result[:DRIVE_CAP_READ_ISRC] = true
    end
    return result
  end

  #    Convert +bitmask+ for drive write properties
  #    into a dictionary of drive capabilities
  def convert_drive_cap_write(bitmask)
    result={}
    if bitmask & Rubycdio::DRIVE_CAP_WRITE_CD_R
      result[:DRIVE_CAP_WRITE_CD_R] = true
    end
    if bitmask & Rubycdio::DRIVE_CAP_WRITE_CD_RW
      result[:DRIVE_CAP_WRITE_CD_RW] = true
    end
    if bitmask & Rubycdio::DRIVE_CAP_WRITE_DVD_R
      result[:DRIVE_CAP_WRITE_DVD_R] = true
    end
    if bitmask & Rubycdio::DRIVE_CAP_WRITE_DVD_PR
      result[:DRIVE_CAP_WRITE_DVD_PR] = true
    end
    if bitmask & Rubycdio::DRIVE_CAP_WRITE_DVD_RAM
      result[:DRIVE_CAP_WRITE_DVD_RAM] = true
    end
    if bitmask & Rubycdio::DRIVE_CAP_WRITE_DVD_RW
      result[:DRIVE_CAP_WRITE_DVD_RW] = true
    end
    if bitmask & Rubycdio::DRIVE_CAP_WRITE_DVD_RPW
      result[:DRIVE_CAP_WRITE_DVD_RPW] = true
    end
    if bitmask & Rubycdio::DRIVE_CAP_WRITE_MT_RAINIER
      result[:DRIVE_CAP_WRITE_MT_RAINIER] = true
    end
    if bitmask & Rubycdio::DRIVE_CAP_WRITE_BURN_PROOF
      result[:DRIVE_CAP_WRITE_BURN_PROOF] = true
    end
    return result
  end

  # = class Device
  #
  #  CD Input and control class for discs/devices
  #
  # == SYNOPSIS
  #
  #    require "cdio"
  #    d = Cdio::Device.new("", Rubycdio::DRIVER_UNKNOWN)
  #    drive_name = d.device()
  #    hw  = d.hwinfo()
  #    if hw then
  #       puts "drive: %s, vendor: %s, model: %s, revision: %s" %
  #         [drive_name, hw["vendor"], hw["model"], hw["revision"]]
  #    end
  #
  #    drivers.each_pair { |driver_name, driver_id|
  #    begin 
  #        if driver?(driver_id):
  #            puts "Driver %s is installed." % driver_name
  #        end
  #    rescue ValueError
  #    end
  
  class Device
    
    def initialize(source=nil, driver_id=nil,
                   access_mode=nil)
      @cd = nil
      if source or driver_id
        open(source, driver_id, access_mode)
      end
    end
    
    # Returns: bool
    #
    # return true if CD-ROM understand ATAPI commands.
    def ATAPI?()
      return Rubycdio::ATAPI?(@cd)
    end
    
    # Returns: status
    #
    # Pause playing CD through analog output.
    # A DeviceError exception may be raised.
    def audio_pause()
      drc=Rubycdio::audio_pause(@cd)
      possibly_raise_exception__(drc)
    end
    
    # Returns: status
    # 
    # Playing CD through analog output from +start_lsn+ to +ending_lsn+
    # A DeviceError exception may be raised.
    def audio_play_lsn(start_lsn, end_lsn)
      drc=Rubycdio::audio_play_lsn(@cd, start_lsn, end_lsn)
      possibly_raise_exception__(drc)
    end
    
    # Returns: status
    # 
    # Resume playing an audio CD through the analog interface.
    # A DeviceError exception may be raised.
    def audio_resume()
      drc=Rubycdio::audio_resume(@cd)
      possibly_raise_exception__(drc)
    end
  
    # Returns: status
    # 
    # Stop playing an audio CD through the analog interface.
    # A DeviceError exception may be raised.
    def audio_stop()
      drc=Rubycdio::audio_stop(@cd)
      possibly_raise_exception__(drc)
    end
    
    # Free (C memory) resources associated with the object.  Call this when
    # done using using CD reading/control operations for the current
    # device.
    def close()
      if @cd 
        Rubycdio::close(@cd)
      else
        puts "***No object to close"
      end
      @cd=nil
    end
    
    # Eject media in CD drive if there is a routine to do so.
    # A DeviceError exception may be raised.
    def eject_media()
      drc=Rubycdio::eject_media(@cd)
      @cd = nil
      possibly_raise_exception__(drc)
    end
    
    # Eject media in CD drive if there is a routine to do so.
    # An exception is thrown on error.
    def eject_media_drive(drive=nil)
      ### FIXME: combine into above by testing if drive is the string
      ### nil versus drive = Rubycdio::DRIVER_UNKNOWN
      Rubycdio::eject_media_drive(drive)
    end
    
    # Returns: String
    
    # Get the value associatied with key.
    def arg(key)
      return Rubycdio::get_arg(@cd, key)
    end
    
    # Returns: String
    # 
    # Get the default CD device.
    # If we haven't initialized a specific device driver), 
    # then find a suitable one and return the default device for that.
    # In some situations of drivers or OS's we can't find a CD device if
    # there is no media in it and it is possible for this routine to return
    # nil even though there may be a hardware CD-ROM.
    def device()
      if @cd 
        return Rubycdio::get_arg(@cd, "source")
      end
      return Rubycdio::get_device(@cd)
    end
    
    # Returns: Fixnum
    # 
    # Get the LSN of the end of the CD
    # 
    # DriverError and IOError may raised on error.
    def disc_last_lsn()
      lsn = Rubycdio::get_disc_last_lsn(@cd)
      if lsn == Rubycdio::INVALID_LSN
        raise DriverError 
      end
      return lsn
    end
    
    # Returns: String
    #   
    # Get disc mode - the kind of CD (CD-DA, CD-ROM mode 1, CD-MIXED, ...)
    # that we've got. The notion of 'CD' is extended a little to include
    # DVD's.
    def disc_mode()
      if not @cd then return "Uninitialized Device" end
      return Rubycdio::get_disc_mode(@cd)
    end
    
    # Get drive capabilities of device.
    #
    # In some situations of drivers or OS's we can't find a CD
    # device if there is no media in it. In this situation
    # capabilities will show up as empty even though there is a
    # hardware CD-ROM.  get_drive_cap_dev()->(read_cap, write_cap,
    # misc_cap)
    #
    # Get drive capabilities of device.
    # 
    # In some situations of drivers or OS's we can't find a CD
    # device if there is no media in it. In this situation
    # capabilities will show up as empty even though there is a
    # hardware CD-ROM.
    def drive_cap()
    b_read_cap, b_write_cap, b_misc_cap = Rubycdio::get_drive_cap(@cd)
    return [convert_drive_cap_read(b_read_cap),
            convert_drive_cap_write(b_write_cap),
            convert_drive_cap_misc(b_misc_cap)]
    end
    
    def drive_cap_dev(device=nil)
      #---
      ### FIXME: combine into above by testing on the type of device.
      #+++
      b_read_cap, b_write_cap, b_misc_cap = 
      Rubycdio::get_drive_cap_dev(device);
      return [convert_drive_cap_read(b_read_cap), 
              convert_drive_cap_write(b_write_cap), 
              convert_drive_cap_misc(b_misc_cap)]
    end
    
    # Returns: String
    #        
    # return a string containing the name of the driver in use.
    #        
    # An IOError exception is raised on error.
    def driver_name()
      if not @cd then raise DriverUninitError end
      return Rubycdio::get_driver_name(@cd)
    end
    
    # Returns: Fixnum
    #        
    # Return the driver id of the driver in use.
    # if object has not been initialized or is nil,
    # return Rubycdio::DRIVER_UNKNOWN.
    def driver_id()
      return Rubycdio::get_driver_id(@cd)
    end
    
    # Returns: Track
    #
    # return a Track object of the first track. nil is returned 
    # if there was a problem.
    def first_track()
      track = Rubycdio::get_first_track_num(@cd)
      if track == Rubycdio::INVALID_TRACK
      return nil
      end
      return Track.new(@cd, track)
    end
    
    # Returns: {"vendor"=>??, "model"=>??, "release"=>??}
    #
    # Get the CD-ROM hardware info via a SCSI MMC INQUIRY command.
    def hwinfo()
      return Rubycdio::get_hwinfo(@cd)
    end
    
    # Returns: FixNum
    
    # Return the Joliet level recognized for cdio.  This only makes
    # sense for something that has an ISO-9660 filesystem.
    
    def joliet_level()
      return Rubycdio::get_joliet_level(@cd)
    end
    
    # Returns: Fixnum
    
    # Get the LSN of the first track of the last session of on the CD.
    # An exception is thrown on error.
    
    def last_session()
      drc, session = Rubycdio::get_last_session(@cd)
      possibly_raise_exception__(drc)
      return session
    end
    
    # Returns: Track
    #
    # return a Track object of the first track. nil is returned 
    # if there was a problem.
    def last_track()
    track = Rubycdio::get_last_track_num(@cd)
      if track == Rubycdio::INVALID_TRACK
        return nil
      end
      return Track.new(@cd, track)
    end
    
    # Returns: String
    # 
    # Get the media catalog number (MCN) from the CD.
    def mcn()
      return Rubycdio::get_mcn(@cd)
    end
    
    # Find out if media has changed since the last call.
    # Return true if media has changed since last call. An exception
    # Error is given on error.
    def media_changed?()
      drc = Rubycdio::get_media_changed(@cd)
      if drc == 0 then return false end
      if drc == 1 then return true end
      possibly_raise_exception__(drc)
      raise DeviceException
    end
    
    # Returns: Fixnum
    #    
    # Return the number of tracks on the CD. 
    # A TrackError or IOError exception may be raised on error.
    def num_tracks()
      track = Rubycdio::get_num_tracks(@cd)
      if track == Rubycdio::INVALID_TRACK
        raise TrackError
      end
      return track
    end
    
    # Returns: track 
    #    
    # Return a track object for the given track number.
    def track(track_num)
      return Track.new(@cd, track_num)
    end
    
    # Returns: track
    #    
    # Find the track which contains +lsn+.
    # nil is returned if the lsn outside of the CD or
    # if there was some error. 
    #    
    # If +lsn+ is before the pregap of the first track,
    # A track object with a 0 track is returned.
    # Otherwise we return the track that spans the LSN.
    def track_for_lsn(lsn)
      track = Rubycdio::get_last_track_num(@cd)
      if track == Rubycdio::INVALID_TRACK
        return nil
      end
      return Track.new(@cd, track)
    end
    
    # Returns: Fixnum
    # Reposition read offset
    # Similar to (if not the same as) libc's fseek()
    #    
    # +offset+ is amount to seek and 
    # +whence+ is like corresponding parameter in libc's lseek, e.g. 
    # it should be SEEK_SET or SEEK_END.
    #    
    # the offset is returned or -1 on error.
    def lseek(offset, whence)
      return Rubycdio::lseek(@cd, offset, whence)
    end
    
    # Sets up to read from place specified by +source+, +driver_id+ and
    # +access_mode+. This should be called before using any other routine
    # except those that act on a CD-ROM drive by name. 
    #        
    # If nil is the value of +source+, we'll use the default driver device.
    # If nil is the value of +driver_id+, we'll find a suitable device driver.
    #
    # If device object was, previously opened it is closed first.
    #        
    # Device is opened so that subsequent operations can be performed.
    def open(source=nil, driver_id=Rubycdio::DRIVER_UNKNOWN,
             access_mode=nil)
      if not driver_id 
        driver_id=Rubycdio::DRIVER_UNKNOWN 
      end
      if not source
        source = '' 
      end
      if not access_mode 
        access_mode = '' 
      end
    if @cd
      close()
    end
      @cd = Rubycdio::open_cd(source, driver_id, access_mode)
    end
    
    # Returns: [size, data]
    #        
    # Reads the next +size+ bytes.
    # Similar to (if not the same as) libc's read()
    #        
    # The number of bytes read and the data is returned. 
    # A DeviceError exception may be raised.
    def read(size)
      size, data = Rubycdio::read_cd(@cd, size)
      possibly_raise_exception__(size)
      return [size, data]
    end
    
    # return [size, data]
    #        
    # Reads a number of data sectors (AKA blocks).
    #      
    # +lsn+ is the sector to read; +bytes+ is the number of bytes to read.
    # A DeviceError exception may be raised.
    def read_data_blocks(lsn, blocks=1)
      size = Rubycdio::ISO_BLOCKSIZE*blocks
      triple  = Rubycdio::read_data_bytes(@cd, lsn,
                                          Rubycdio::ISO_BLOCKSIZE, size) 
      if not triple
        return [-1, nil]
      end
    data, size, drc = triple
      possibly_raise_exception__(drc)
      return [size, data]
    end
    
    # return  [blocks, data]
    #
    # Reads a number of sectors (AKA blocks).
    # 
    # +lsn+ is the sector to read, +bytes+ is the number of bytes to read.
    #        
    # If +read_mode+ is Rubycdio::MODE_AUDIO, the return buffer size will be
    # truncated to multiple of Rubycdio::CDIO_FRAMESIZE_RAW i_blocks bytes.
    #        
    # If +read_mode+ is Rubycdio::MODE_DATA, buffer will be truncated to a
    # multiple of Rubycdio::ISO_BLOCKSIZE, Rubycdio::M1RAW_SECTOR_SIZE or
    # Rubycdio::M2F2_SECTOR_SIZE bytes depending on what mode the data is in.
    #
    # If +read_mode+ is Rubycdio::MODE_M2F1, buffer will be truncated to a 
    # multiple of Rubycdio::M2RAW_SECTOR_SIZE bytes.
    #        
    # If +read_mode+ is Rubycdio::MODE_M2F2, the return buffer size will be
    # truncated to a multiple of Rubycdio::CD_FRAMESIZE bytes.
    #        
    # The number of bytes read and the data is returned. 
    # A DeviceError exception may be raised.
    def read_sectors(lsn, read_mode, blocks=1)
      begin
        blocksize = read_mode2blocksize()[read_mode]
        size = blocks * blocksize
      rescue
        return [-1, nil]
      end
      triple = Rubycdio::read_sectors(@cd, lsn, read_mode, size)
      if not triple
        return [-1, nil]
      end
      data, size, drc = triple
      possibly_raise_exception__(drc)
      blocks = size / blocksize
      return [blocks, data]
    end

    # Set the blocksize for subsequent reads.
    # An exception is thrown on error.
    def blocksize=(blocksize)
      drc = Rubycdio::set_blocksize(@cd, blocksize)
      possibly_raise_exception__(drc)
    end
    
    # Set the drive speed. An exception is thrown on error.
    def speed=(speed)
      drc = Rubycdio::set_speed(@cd, speed)
      possibly_raise_exception__(drc)
    end
  end # Device

  # = CD Input and control track class
  #
  # == SYNOPSIS
  #
  #   require "cdio"
  #   d = Device.new("/dev/cdrom")
  #   t = d.first_track()
  #   first_track = t.track
  #   num_tracks  = d.num_tracks()
  #   last_track  = first_track+num_tracks-1
  #   for i in first_track .. last_track
  #      t = d.track(i)
  #      puts "%3d: %06u  %-6s %s"  % [t.track, t.lsn(), t.msf(), t.format()]
  #   end
  #   puts "%3X: %06u  leadout" % [Rubycdio::CDROM_LEADOUT_TRACK, d.disc_last_lsn()]
  #
  class Track
    
    attr_accessor :track
    
    def initialize(device, track_num)
      
      if track_num.class != Fixnum
        raise TrackError('track number parameter is not an integer')
      end
      @track = track_num
      
      # See if the device parameter is a string or
      # a device object.
      if device.class == String
        @device = Device.new(device)
      else
        @device = device
      end
    end
    
    # Returns: Fixnum
    #     
    # Return number of channels in track: 2 or 4.
    # Not meaningful if track is not an audio track.
    # An exception can be raised on error.
    def audio_channels()
      channels = Rubycdio::get_track_channels(@device, @track)
      if -2 == channels
        raise DriverUnsupportedError
      elsif -1 == channels
        raise TrackError
      else
        return channels
      end
    end
    
    def cdtext
      return CDText.new(Rubycdio::get_cdtext(@device, @track))
    end

    # Returns: bool
    #
    # Return copy protection status on a track. Is this meaningful 
    # not an audio track?
    def copy_permit?()
      return Rubycdio::track_copy_permit?(@device, @track)
    end
    
    # Returns: format (String)
    #
    # Get the format (e.g. 'audio', 'mode2', 'mode1') of track. 
    def format()
      return Rubycdio::get_track_format(@device, @track)
    end
    
    # Returns: Fixnum (lsn)
    #      
    # Return the ending LSN for a track.
    # A TrackError or IOError exception may be raised on error.
    def last_lsn()
      lsn = Rubycdio::get_track_last_lsn(@device, @track)
      if lsn == Rubycdio::INVALID_LSN
        raise TrackError('Invalid LSN returned')
      end
      return lsn
    end
    
    # Returns: Fixnum (lba)
    #
    # Return the starting LBA for a track
    # A TrackError exception is raised on error.
    def lba()
      lba = Rubycdio::get_track_lba(@device, @track)
      if lba == Rubycdio::INVALID_LBA
        raise TrackError('Invalid LBA returned')
      end
      return lba
    end
    
    # Returns: Fixnum (lsn)
    #
    # Return the starting LSN for a track
    # A TrackError exception is raised on error.
    def lsn()
      lsn = Rubycdio::get_track_lsn(@device, @track)
      if lsn == Rubycdio::INVALID_LSN
        raise TrackError('Invalid LSN returned')
      end
      return lsn
    end
    
    # Returns: String 
    # 
    # Return the starting MSF (minutes/secs/frames) for track number track.
    # Track numbers usually start at something greater than 0, usually 1.
    # 
    # Returns string of the form <i>mm:ss:ff</i> if all good, or string nil on
    # error.
    def msf()
      return Rubycdio::get_track_msf(@device, @track)
    end
    
    # Returns: String
    # 
    # Get linear preemphasis status on an audio track.
    # This is not meaningful if not an audio track?
    # A TrackError exception is raised on error.
    def preemphasis()
      rc = Rubycdio::get_track_preemphasis(@device, @track)
      if rc == Rubycdio::TRACK_FLAG_FALSE
        return 'none'
      elsif rc == Rubycdio::TRACK_FLAG_TRUE
        return 'preemphasis'
      elsif rc == Rubycdio::TRACK_FLAG_UNKNOWN
        return 'unknown'
      else
        raise TrackError('Invalid return value %d' % d)
      end
    end

    # Returns: Fixnum
    # 
    # Get the number of sectors between this track an the next.  This
    # includes any pregap sectors before the start of the next track.
    # Track numbers usually start at something 
    # greater than 0, usually 1.
    # 
    # A TrackError exception is raised on error.
    def sec_count()
      sec_count = Rubycdio::get_track_sec_count(@device, @track)
      if sec_count == 0
        raise TrackError
      end
      return sec_count
    end
    
    # Return true if we have XA data (green, mode2 form1) or
    # XA data (green, mode2 form2). That is track begins:
    # sync - header - subheader
    # 12     4      -  8
    def green?()
      return Rubycdio::track_green?(@device, @track)
    end

  end # class Track

  class CDText
    def initialize(opaque)
        @_cdtext = opaque
    end

    #
    #  get(self, key)->string
    #
    #  Get the value associatied with key.
    # 
    def get(key)
      return Rubycdio::cdtext_get(key, @_cdtext)
    end

    def is_keyword(key)
      return Rubycdio::cdtext_is_keyword(key, @_cdtext)
    end

    #
    #  set(self, key, string)->None
    #
    #  Set the value associatied with key.
    #
    def set( key, string)
        return Rubycdio::cdtext_set(key, string, @_cdtext)
    end


  end
end # module Cdio

include Cdio
#
# Local variables:
#  mode: Ruby
# End:
