[ Index ]

PHP Cross Reference of WordPress Trunk (Updated Daily)

Search

title

Body

[close]

/wp-includes/ID3/ -> module.audio.flac.php (source)

   1  <?php
   2  
   3  /////////////////////////////////////////////////////////////////
   4  /// getID3() by James Heinrich <info@getid3.org>               //
   5  //  available at https://github.com/JamesHeinrich/getID3       //
   6  //            or https://www.getid3.org                        //
   7  //            or http://getid3.sourceforge.net                 //
   8  //  see readme.txt for more details                            //
   9  /////////////////////////////////////////////////////////////////
  10  //                                                             //
  11  // module.audio.flac.php                                       //
  12  // module for analyzing FLAC and OggFLAC audio files           //
  13  // dependencies: module.audio.ogg.php                          //
  14  //                                                            ///
  15  /////////////////////////////////////////////////////////////////
  16  
  17  if (!defined('GETID3_INCLUDEPATH')) { // prevent path-exposing attacks that access modules directly on public webservers
  18      exit;
  19  }
  20  getid3_lib::IncludeDependency(GETID3_INCLUDEPATH.'module.audio.ogg.php', __FILE__, true);
  21  
  22  /**
  23  * @tutorial http://flac.sourceforge.net/format.html
  24  */
  25  class getid3_flac extends getid3_handler
  26  {
  27      const syncword = 'fLaC';
  28  
  29      /**
  30       * @return bool
  31       */
  32  	public function Analyze() {
  33          $info = &$this->getid3->info;
  34  
  35          $this->fseek($info['avdataoffset']);
  36          $StreamMarker = $this->fread(4);
  37          if ($StreamMarker != self::syncword) {
  38              return $this->error('Expecting "'.getid3_lib::PrintHexBytes(self::syncword).'" at offset '.$info['avdataoffset'].', found "'.getid3_lib::PrintHexBytes($StreamMarker).'"');
  39          }
  40          $info['fileformat']            = 'flac';
  41          $info['audio']['dataformat']   = 'flac';
  42          $info['audio']['bitrate_mode'] = 'vbr';
  43          $info['audio']['lossless']     = true;
  44  
  45          // parse flac container
  46          return $this->parseMETAdata();
  47      }
  48  
  49      /**
  50       * @return bool
  51       */
  52  	public function parseMETAdata() {
  53          $info = &$this->getid3->info;
  54          do {
  55              $BlockOffset   = $this->ftell();
  56              $BlockHeader   = $this->fread(4);
  57              $LBFBT         = getid3_lib::BigEndian2Int(substr($BlockHeader, 0, 1));  // LBFBT = LastBlockFlag + BlockType
  58              $LastBlockFlag = (bool) ($LBFBT & 0x80);
  59              $BlockType     =        ($LBFBT & 0x7F);
  60              $BlockLength   = getid3_lib::BigEndian2Int(substr($BlockHeader, 1, 3));
  61              $BlockTypeText = self::metaBlockTypeLookup($BlockType);
  62  
  63              if (($BlockOffset + 4 + $BlockLength) > $info['avdataend']) {
  64                  $this->warning('METADATA_BLOCK_HEADER.BLOCK_TYPE ('.$BlockTypeText.') at offset '.$BlockOffset.' extends beyond end of file');
  65                  break;
  66              }
  67              if ($BlockLength < 1) {
  68                  if ($BlockTypeText != 'reserved') {
  69                      // probably supposed to be zero-length
  70                      $this->warning('METADATA_BLOCK_HEADER.BLOCK_LENGTH ('.$BlockTypeText.') at offset '.$BlockOffset.' is zero bytes');
  71                      continue;
  72                  }
  73                  $this->error('METADATA_BLOCK_HEADER.BLOCK_LENGTH ('.$BlockLength.') at offset '.$BlockOffset.' is invalid');
  74                  break;
  75              }
  76  
  77              $info['flac'][$BlockTypeText]['raw'] = array();
  78              $BlockTypeText_raw = &$info['flac'][$BlockTypeText]['raw'];
  79  
  80              $BlockTypeText_raw['offset']          = $BlockOffset;
  81              $BlockTypeText_raw['last_meta_block'] = $LastBlockFlag;
  82              $BlockTypeText_raw['block_type']      = $BlockType;
  83              $BlockTypeText_raw['block_type_text'] = $BlockTypeText;
  84              $BlockTypeText_raw['block_length']    = $BlockLength;
  85              if ($BlockTypeText_raw['block_type'] != 0x06) { // do not read attachment data automatically
  86                  $BlockTypeText_raw['block_data']  = $this->fread($BlockLength);
  87              }
  88  
  89              switch ($BlockTypeText) {
  90                  case 'STREAMINFO':     // 0x00
  91                      if (!$this->parseSTREAMINFO($BlockTypeText_raw['block_data'])) {
  92                          return false;
  93                      }
  94                      break;
  95  
  96                  case 'PADDING':        // 0x01
  97                      unset($info['flac']['PADDING']); // ignore
  98                      break;
  99  
 100                  case 'APPLICATION':    // 0x02
 101                      if (!$this->parseAPPLICATION($BlockTypeText_raw['block_data'])) {
 102                          return false;
 103                      }
 104                      break;
 105  
 106                  case 'SEEKTABLE':      // 0x03
 107                      if (!$this->parseSEEKTABLE($BlockTypeText_raw['block_data'])) {
 108                          return false;
 109                      }
 110                      break;
 111  
 112                  case 'VORBIS_COMMENT': // 0x04
 113                      if (!$this->parseVORBIS_COMMENT($BlockTypeText_raw['block_data'])) {
 114                          return false;
 115                      }
 116                      break;
 117  
 118                  case 'CUESHEET':       // 0x05
 119                      if (!$this->parseCUESHEET($BlockTypeText_raw['block_data'])) {
 120                          return false;
 121                      }
 122                      break;
 123  
 124                  case 'PICTURE':        // 0x06
 125                      if (!$this->parsePICTURE()) {
 126                          return false;
 127                      }
 128                      break;
 129  
 130                  default:
 131                      $this->warning('Unhandled METADATA_BLOCK_HEADER.BLOCK_TYPE ('.$BlockType.') at offset '.$BlockOffset);
 132              }
 133  
 134              unset($info['flac'][$BlockTypeText]['raw']);
 135              $info['avdataoffset'] = $this->ftell();
 136          }
 137          while ($LastBlockFlag === false);
 138  
 139          // handle tags
 140          if (!empty($info['flac']['VORBIS_COMMENT']['comments'])) {
 141              $info['flac']['comments'] = $info['flac']['VORBIS_COMMENT']['comments'];
 142          }
 143          if (!empty($info['flac']['VORBIS_COMMENT']['vendor'])) {
 144              $info['audio']['encoder'] = str_replace('reference ', '', $info['flac']['VORBIS_COMMENT']['vendor']);
 145          }
 146  
 147          // copy attachments to 'comments' array if nesesary
 148          if (isset($info['flac']['PICTURE']) && ($this->getid3->option_save_attachments !== getID3::ATTACHMENTS_NONE)) {
 149              foreach ($info['flac']['PICTURE'] as $entry) {
 150                  if (!empty($entry['data'])) {
 151                      if (!isset($info['flac']['comments']['picture'])) {
 152                          $info['flac']['comments']['picture'] = array();
 153                      }
 154                      $comments_picture_data = array();
 155                      foreach (array('data', 'image_mime', 'image_width', 'image_height', 'imagetype', 'picturetype', 'description', 'datalength') as $picture_key) {
 156                          if (isset($entry[$picture_key])) {
 157                              $comments_picture_data[$picture_key] = $entry[$picture_key];
 158                          }
 159                      }
 160                      $info['flac']['comments']['picture'][] = $comments_picture_data;
 161                      unset($comments_picture_data);
 162                  }
 163              }
 164          }
 165  
 166          if (isset($info['flac']['STREAMINFO'])) {
 167              if (!$this->isDependencyFor('matroska')) {
 168                  $info['flac']['compressed_audio_bytes'] = $info['avdataend'] - $info['avdataoffset'];
 169              }
 170              $info['flac']['uncompressed_audio_bytes'] = $info['flac']['STREAMINFO']['samples_stream'] * $info['flac']['STREAMINFO']['channels'] * ($info['flac']['STREAMINFO']['bits_per_sample'] / 8);
 171              if ($info['flac']['uncompressed_audio_bytes'] == 0) {
 172                  return $this->error('Corrupt FLAC file: uncompressed_audio_bytes == zero');
 173              }
 174              if (!empty($info['flac']['compressed_audio_bytes'])) {
 175                  $info['flac']['compression_ratio'] = $info['flac']['compressed_audio_bytes'] / $info['flac']['uncompressed_audio_bytes'];
 176              }
 177          }
 178  
 179          // set md5_data_source - built into flac 0.5+
 180          if (isset($info['flac']['STREAMINFO']['audio_signature'])) {
 181  
 182              if ($info['flac']['STREAMINFO']['audio_signature'] === str_repeat("\x00", 16)) {
 183                  $this->warning('FLAC STREAMINFO.audio_signature is null (known issue with libOggFLAC)');
 184              }
 185              else {
 186                  $info['md5_data_source'] = '';
 187                  $md5 = $info['flac']['STREAMINFO']['audio_signature'];
 188                  for ($i = 0; $i < strlen($md5); $i++) {
 189                      $info['md5_data_source'] .= str_pad(dechex(ord($md5[$i])), 2, '00', STR_PAD_LEFT);
 190                  }
 191                  if (!preg_match('/^[0-9a-f]{32}$/', $info['md5_data_source'])) {
 192                      unset($info['md5_data_source']);
 193                  }
 194              }
 195          }
 196  
 197          if (isset($info['flac']['STREAMINFO']['bits_per_sample'])) {
 198              $info['audio']['bits_per_sample'] = $info['flac']['STREAMINFO']['bits_per_sample'];
 199              if ($info['audio']['bits_per_sample'] == 8) {
 200                  // special case
 201                  // must invert sign bit on all data bytes before MD5'ing to match FLAC's calculated value
 202                  // MD5sum calculates on unsigned bytes, but FLAC calculated MD5 on 8-bit audio data as signed
 203                  $this->warning('FLAC calculates MD5 data strangely on 8-bit audio, so the stored md5_data_source value will not match the decoded WAV file');
 204              }
 205          }
 206  
 207          return true;
 208      }
 209  
 210  
 211      /**
 212       * @param string $BlockData
 213       *
 214       * @return array
 215       */
 216  	public static function parseSTREAMINFOdata($BlockData) {
 217          $streaminfo = array();
 218          $streaminfo['min_block_size']  = getid3_lib::BigEndian2Int(substr($BlockData, 0, 2));
 219          $streaminfo['max_block_size']  = getid3_lib::BigEndian2Int(substr($BlockData, 2, 2));
 220          $streaminfo['min_frame_size']  = getid3_lib::BigEndian2Int(substr($BlockData, 4, 3));
 221          $streaminfo['max_frame_size']  = getid3_lib::BigEndian2Int(substr($BlockData, 7, 3));
 222  
 223          $SRCSBSS                       = getid3_lib::BigEndian2Bin(substr($BlockData, 10, 8));
 224          $streaminfo['sample_rate']     = getid3_lib::Bin2Dec(substr($SRCSBSS,  0, 20));
 225          $streaminfo['channels']        = getid3_lib::Bin2Dec(substr($SRCSBSS, 20,  3)) + 1;
 226          $streaminfo['bits_per_sample'] = getid3_lib::Bin2Dec(substr($SRCSBSS, 23,  5)) + 1;
 227          $streaminfo['samples_stream']  = getid3_lib::Bin2Dec(substr($SRCSBSS, 28, 36));
 228  
 229          $streaminfo['audio_signature'] =                           substr($BlockData, 18, 16);
 230  
 231          return $streaminfo;
 232      }
 233  
 234      /**
 235       * @param string $BlockData
 236       *
 237       * @return bool
 238       */
 239  	private function parseSTREAMINFO($BlockData) {
 240          $info = &$this->getid3->info;
 241  
 242          $info['flac']['STREAMINFO'] = self::parseSTREAMINFOdata($BlockData);
 243  
 244          if (!empty($info['flac']['STREAMINFO']['sample_rate'])) {
 245  
 246              $info['audio']['bitrate_mode']    = 'vbr';
 247              $info['audio']['sample_rate']     = $info['flac']['STREAMINFO']['sample_rate'];
 248              $info['audio']['channels']        = $info['flac']['STREAMINFO']['channels'];
 249              $info['audio']['bits_per_sample'] = $info['flac']['STREAMINFO']['bits_per_sample'];
 250              $info['playtime_seconds']         = $info['flac']['STREAMINFO']['samples_stream'] / $info['flac']['STREAMINFO']['sample_rate'];
 251              if ($info['playtime_seconds'] > 0) {
 252                  if (!$this->isDependencyFor('matroska')) {
 253                      $info['audio']['bitrate'] = (($info['avdataend'] - $info['avdataoffset']) * 8) / $info['playtime_seconds'];
 254                  }
 255                  else {
 256                      $this->warning('Cannot determine audio bitrate because total stream size is unknown');
 257                  }
 258              }
 259  
 260          } else {
 261              return $this->error('Corrupt METAdata block: STREAMINFO');
 262          }
 263  
 264          return true;
 265      }
 266  
 267      /**
 268       * @param string $BlockData
 269       *
 270       * @return bool
 271       */
 272  	private function parseAPPLICATION($BlockData) {
 273          $info = &$this->getid3->info;
 274  
 275          $ApplicationID = getid3_lib::BigEndian2Int(substr($BlockData, 0, 4));
 276          $info['flac']['APPLICATION'][$ApplicationID]['name'] = self::applicationIDLookup($ApplicationID);
 277          $info['flac']['APPLICATION'][$ApplicationID]['data'] = substr($BlockData, 4);
 278  
 279          return true;
 280      }
 281  
 282      /**
 283       * @param string $BlockData
 284       *
 285       * @return bool
 286       */
 287  	private function parseSEEKTABLE($BlockData) {
 288          $info = &$this->getid3->info;
 289  
 290          $offset = 0;
 291          $BlockLength = strlen($BlockData);
 292          $placeholderpattern = str_repeat("\xFF", 8);
 293          while ($offset < $BlockLength) {
 294              $SampleNumberString = substr($BlockData, $offset, 8);
 295              $offset += 8;
 296              if ($SampleNumberString == $placeholderpattern) {
 297  
 298                  // placeholder point
 299                  getid3_lib::safe_inc($info['flac']['SEEKTABLE']['placeholders'], 1);
 300                  $offset += 10;
 301  
 302              } else {
 303  
 304                  $SampleNumber                                        = getid3_lib::BigEndian2Int($SampleNumberString);
 305                  $info['flac']['SEEKTABLE'][$SampleNumber]['offset']  = getid3_lib::BigEndian2Int(substr($BlockData, $offset, 8));
 306                  $offset += 8;
 307                  $info['flac']['SEEKTABLE'][$SampleNumber]['samples'] = getid3_lib::BigEndian2Int(substr($BlockData, $offset, 2));
 308                  $offset += 2;
 309  
 310              }
 311          }
 312  
 313          return true;
 314      }
 315  
 316      /**
 317       * @param string $BlockData
 318       *
 319       * @return bool
 320       */
 321  	private function parseVORBIS_COMMENT($BlockData) {
 322          $info = &$this->getid3->info;
 323  
 324          $getid3_ogg = new getid3_ogg($this->getid3);
 325          if ($this->isDependencyFor('matroska')) {
 326              $getid3_ogg->setStringMode($this->data_string);
 327          }
 328          $getid3_ogg->ParseVorbisComments();
 329          if (isset($info['ogg'])) {
 330              unset($info['ogg']['comments_raw']);
 331              $info['flac']['VORBIS_COMMENT'] = $info['ogg'];
 332              unset($info['ogg']);
 333          }
 334  
 335          unset($getid3_ogg);
 336  
 337          return true;
 338      }
 339  
 340      /**
 341       * @param string $BlockData
 342       *
 343       * @return bool
 344       */
 345  	private function parseCUESHEET($BlockData) {
 346          $info = &$this->getid3->info;
 347          $offset = 0;
 348          $info['flac']['CUESHEET']['media_catalog_number'] =                              trim(substr($BlockData, $offset, 128), "\0");
 349          $offset += 128;
 350          $info['flac']['CUESHEET']['lead_in_samples']      =         getid3_lib::BigEndian2Int(substr($BlockData, $offset, 8));
 351          $offset += 8;
 352          $info['flac']['CUESHEET']['flags']['is_cd']       = (bool) (getid3_lib::BigEndian2Int(substr($BlockData, $offset, 1)) & 0x80);
 353          $offset += 1;
 354  
 355          $offset += 258; // reserved
 356  
 357          $info['flac']['CUESHEET']['number_tracks']        =         getid3_lib::BigEndian2Int(substr($BlockData, $offset, 1));
 358          $offset += 1;
 359  
 360          for ($track = 0; $track < $info['flac']['CUESHEET']['number_tracks']; $track++) {
 361              $TrackSampleOffset = getid3_lib::BigEndian2Int(substr($BlockData, $offset, 8));
 362              $offset += 8;
 363              $TrackNumber       = getid3_lib::BigEndian2Int(substr($BlockData, $offset, 1));
 364              $offset += 1;
 365  
 366              $info['flac']['CUESHEET']['tracks'][$TrackNumber]['sample_offset']         = $TrackSampleOffset;
 367  
 368              $info['flac']['CUESHEET']['tracks'][$TrackNumber]['isrc']                  =                           substr($BlockData, $offset, 12);
 369              $offset += 12;
 370  
 371              $TrackFlagsRaw                                                             = getid3_lib::BigEndian2Int(substr($BlockData, $offset, 1));
 372              $offset += 1;
 373              $info['flac']['CUESHEET']['tracks'][$TrackNumber]['flags']['is_audio']     = (bool) ($TrackFlagsRaw & 0x80);
 374              $info['flac']['CUESHEET']['tracks'][$TrackNumber]['flags']['pre_emphasis'] = (bool) ($TrackFlagsRaw & 0x40);
 375  
 376              $offset += 13; // reserved
 377  
 378              $info['flac']['CUESHEET']['tracks'][$TrackNumber]['index_points']          = getid3_lib::BigEndian2Int(substr($BlockData, $offset, 1));
 379              $offset += 1;
 380  
 381              for ($index = 0; $index < $info['flac']['CUESHEET']['tracks'][$TrackNumber]['index_points']; $index++) {
 382                  $IndexSampleOffset = getid3_lib::BigEndian2Int(substr($BlockData, $offset, 8));
 383                  $offset += 8;
 384                  $IndexNumber       = getid3_lib::BigEndian2Int(substr($BlockData, $offset, 1));
 385                  $offset += 1;
 386  
 387                  $offset += 3; // reserved
 388  
 389                  $info['flac']['CUESHEET']['tracks'][$TrackNumber]['indexes'][$IndexNumber] = $IndexSampleOffset;
 390              }
 391          }
 392  
 393          return true;
 394      }
 395  
 396      /**
 397       * Parse METADATA_BLOCK_PICTURE flac structure and extract attachment
 398       * External usage: audio.ogg
 399       *
 400       * @return bool
 401       */
 402  	public function parsePICTURE() {
 403          $info = &$this->getid3->info;
 404  
 405          $picture = array();
 406          $picture['typeid']         = getid3_lib::BigEndian2Int($this->fread(4));
 407          $picture['picturetype']    = self::pictureTypeLookup($picture['typeid']);
 408          $picture['image_mime']     = $this->fread(getid3_lib::BigEndian2Int($this->fread(4)));
 409          $descr_length              = getid3_lib::BigEndian2Int($this->fread(4));
 410          if ($descr_length) {
 411              $picture['description'] = $this->fread($descr_length);
 412          }
 413          $picture['image_width']    = getid3_lib::BigEndian2Int($this->fread(4));
 414          $picture['image_height']   = getid3_lib::BigEndian2Int($this->fread(4));
 415          $picture['color_depth']    = getid3_lib::BigEndian2Int($this->fread(4));
 416          $picture['colors_indexed'] = getid3_lib::BigEndian2Int($this->fread(4));
 417          $picture['datalength']     = getid3_lib::BigEndian2Int($this->fread(4));
 418  
 419          if ($picture['image_mime'] == '-->') {
 420              $picture['data'] = $this->fread($picture['datalength']);
 421          } else {
 422              $picture['data'] = $this->saveAttachment(
 423                  str_replace('/', '_', $picture['picturetype']).'_'.$this->ftell(),
 424                  $this->ftell(),
 425                  $picture['datalength'],
 426                  $picture['image_mime']);
 427          }
 428  
 429          $info['flac']['PICTURE'][] = $picture;
 430  
 431          return true;
 432      }
 433  
 434      /**
 435       * @param int $blocktype
 436       *
 437       * @return string
 438       */
 439  	public static function metaBlockTypeLookup($blocktype) {
 440          static $lookup = array(
 441              0 => 'STREAMINFO',
 442              1 => 'PADDING',
 443              2 => 'APPLICATION',
 444              3 => 'SEEKTABLE',
 445              4 => 'VORBIS_COMMENT',
 446              5 => 'CUESHEET',
 447              6 => 'PICTURE',
 448          );
 449          return (isset($lookup[$blocktype]) ? $lookup[$blocktype] : 'reserved');
 450      }
 451  
 452      /**
 453       * @param int $applicationid
 454       *
 455       * @return string
 456       */
 457  	public static function applicationIDLookup($applicationid) {
 458          // http://flac.sourceforge.net/id.html
 459          static $lookup = array(
 460              0x41544348 => 'FlacFile',                                                                           // "ATCH"
 461              0x42534F4C => 'beSolo',                                                                             // "BSOL"
 462              0x42554753 => 'Bugs Player',                                                                        // "BUGS"
 463              0x43756573 => 'GoldWave cue points (specification)',                                                // "Cues"
 464              0x46696361 => 'CUE Splitter',                                                                       // "Fica"
 465              0x46746F6C => 'flac-tools',                                                                         // "Ftol"
 466              0x4D4F5442 => 'MOTB MetaCzar',                                                                      // "MOTB"
 467              0x4D505345 => 'MP3 Stream Editor',                                                                  // "MPSE"
 468              0x4D754D4C => 'MusicML: Music Metadata Language',                                                   // "MuML"
 469              0x52494646 => 'Sound Devices RIFF chunk storage',                                                   // "RIFF"
 470              0x5346464C => 'Sound Font FLAC',                                                                    // "SFFL"
 471              0x534F4E59 => 'Sony Creative Software',                                                             // "SONY"
 472              0x5351455A => 'flacsqueeze',                                                                        // "SQEZ"
 473              0x54745776 => 'TwistedWave',                                                                        // "TtWv"
 474              0x55495453 => 'UITS Embedding tools',                                                               // "UITS"
 475              0x61696666 => 'FLAC AIFF chunk storage',                                                            // "aiff"
 476              0x696D6167 => 'flac-image application for storing arbitrary files in APPLICATION metadata blocks',  // "imag"
 477              0x7065656D => 'Parseable Embedded Extensible Metadata (specification)',                             // "peem"
 478              0x71667374 => 'QFLAC Studio',                                                                       // "qfst"
 479              0x72696666 => 'FLAC RIFF chunk storage',                                                            // "riff"
 480              0x74756E65 => 'TagTuner',                                                                           // "tune"
 481              0x78626174 => 'XBAT',                                                                               // "xbat"
 482              0x786D6364 => 'xmcd',                                                                               // "xmcd"
 483          );
 484          return (isset($lookup[$applicationid]) ? $lookup[$applicationid] : 'reserved');
 485      }
 486  
 487      /**
 488       * @param int $type_id
 489       *
 490       * @return string
 491       */
 492  	public static function pictureTypeLookup($type_id) {
 493          static $lookup = array (
 494               0 => 'Other',
 495               1 => '32x32 pixels \'file icon\' (PNG only)',
 496               2 => 'Other file icon',
 497               3 => 'Cover (front)',
 498               4 => 'Cover (back)',
 499               5 => 'Leaflet page',
 500               6 => 'Media (e.g. label side of CD)',
 501               7 => 'Lead artist/lead performer/soloist',
 502               8 => 'Artist/performer',
 503               9 => 'Conductor',
 504              10 => 'Band/Orchestra',
 505              11 => 'Composer',
 506              12 => 'Lyricist/text writer',
 507              13 => 'Recording Location',
 508              14 => 'During recording',
 509              15 => 'During performance',
 510              16 => 'Movie/video screen capture',
 511              17 => 'A bright coloured fish',
 512              18 => 'Illustration',
 513              19 => 'Band/artist logotype',
 514              20 => 'Publisher/Studio logotype',
 515          );
 516          return (isset($lookup[$type_id]) ? $lookup[$type_id] : 'reserved');
 517      }
 518  
 519  }


Generated : Thu Nov 21 08:20:01 2024 Cross-referenced by PHPXref