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.nsv.php                                        //
12// module for analyzing Nullsoft NSV files                     //
13// dependencies: NONE                                          //
14//                                                            ///
15/////////////////////////////////////////////////////////////////
16
17if (!defined('GETID3_INCLUDEPATH')) { // prevent path-exposing attacks that access modules directly on public webservers
18	exit;
19}
20
21class getid3_nsv extends getid3_handler
22{
23	/**
24	 * @return bool
25	 */
26	public function Analyze() {
27		$info = &$this->getid3->info;
28
29		$this->fseek($info['avdataoffset']);
30		$NSVheader = $this->fread(4);
31
32		switch ($NSVheader) {
33			case 'NSVs':
34				if ($this->getNSVsHeaderFilepointer(0)) {
35					$info['fileformat']          = 'nsv';
36					$info['audio']['dataformat'] = 'nsv';
37					$info['video']['dataformat'] = 'nsv';
38					$info['audio']['lossless']   = false;
39					$info['video']['lossless']   = false;
40				}
41				break;
42
43			case 'NSVf':
44				if ($this->getNSVfHeaderFilepointer(0)) {
45					$info['fileformat']          = 'nsv';
46					$info['audio']['dataformat'] = 'nsv';
47					$info['video']['dataformat'] = 'nsv';
48					$info['audio']['lossless']   = false;
49					$info['video']['lossless']   = false;
50					$this->getNSVsHeaderFilepointer($info['nsv']['NSVf']['header_length']);
51				}
52				break;
53
54			default:
55				$this->error('Expecting "NSVs" or "NSVf" at offset '.$info['avdataoffset'].', found "'.getid3_lib::PrintHexBytes($NSVheader).'"');
56				return false;
57		}
58
59		if (!isset($info['nsv']['NSVf'])) {
60			$this->warning('NSVf header not present - cannot calculate playtime or bitrate');
61		}
62
63		return true;
64	}
65
66	/**
67	 * @param int $fileoffset
68	 *
69	 * @return bool
70	 */
71	public function getNSVsHeaderFilepointer($fileoffset) {
72		$info = &$this->getid3->info;
73		$this->fseek($fileoffset);
74		$NSVsheader = $this->fread(28);
75		$offset = 0;
76
77		$info['nsv']['NSVs']['identifier']      =                  substr($NSVsheader, $offset, 4);
78		$offset += 4;
79
80		if ($info['nsv']['NSVs']['identifier'] != 'NSVs') {
81			$this->error('expected "NSVs" at offset ('.$fileoffset.'), found "'.$info['nsv']['NSVs']['identifier'].'" instead');
82			unset($info['nsv']['NSVs']);
83			return false;
84		}
85
86		$info['nsv']['NSVs']['offset']          = $fileoffset;
87
88		$info['nsv']['NSVs']['video_codec']     =                              substr($NSVsheader, $offset, 4);
89		$offset += 4;
90		$info['nsv']['NSVs']['audio_codec']     =                              substr($NSVsheader, $offset, 4);
91		$offset += 4;
92		$info['nsv']['NSVs']['resolution_x']    = getid3_lib::LittleEndian2Int(substr($NSVsheader, $offset, 2));
93		$offset += 2;
94		$info['nsv']['NSVs']['resolution_y']    = getid3_lib::LittleEndian2Int(substr($NSVsheader, $offset, 2));
95		$offset += 2;
96
97		$info['nsv']['NSVs']['framerate_index'] = getid3_lib::LittleEndian2Int(substr($NSVsheader, $offset, 1));
98		$offset += 1;
99		//$info['nsv']['NSVs']['unknown1b']       = getid3_lib::LittleEndian2Int(substr($NSVsheader, $offset, 1));
100		$offset += 1;
101		//$info['nsv']['NSVs']['unknown1c']       = getid3_lib::LittleEndian2Int(substr($NSVsheader, $offset, 1));
102		$offset += 1;
103		//$info['nsv']['NSVs']['unknown1d']       = getid3_lib::LittleEndian2Int(substr($NSVsheader, $offset, 1));
104		$offset += 1;
105		//$info['nsv']['NSVs']['unknown2a']       = getid3_lib::LittleEndian2Int(substr($NSVsheader, $offset, 1));
106		$offset += 1;
107		//$info['nsv']['NSVs']['unknown2b']       = getid3_lib::LittleEndian2Int(substr($NSVsheader, $offset, 1));
108		$offset += 1;
109		//$info['nsv']['NSVs']['unknown2c']       = getid3_lib::LittleEndian2Int(substr($NSVsheader, $offset, 1));
110		$offset += 1;
111		//$info['nsv']['NSVs']['unknown2d']       = getid3_lib::LittleEndian2Int(substr($NSVsheader, $offset, 1));
112		$offset += 1;
113
114		switch ($info['nsv']['NSVs']['audio_codec']) {
115			case 'PCM ':
116				$info['nsv']['NSVs']['bits_channel'] = getid3_lib::LittleEndian2Int(substr($NSVsheader, $offset, 1));
117				$offset += 1;
118				$info['nsv']['NSVs']['channels']     = getid3_lib::LittleEndian2Int(substr($NSVsheader, $offset, 1));
119				$offset += 1;
120				$info['nsv']['NSVs']['sample_rate']  = getid3_lib::LittleEndian2Int(substr($NSVsheader, $offset, 2));
121				$offset += 2;
122
123				$info['audio']['sample_rate']        = $info['nsv']['NSVs']['sample_rate'];
124				break;
125
126			case 'MP3 ':
127			case 'NONE':
128			default:
129				//$info['nsv']['NSVs']['unknown3']     = getid3_lib::LittleEndian2Int(substr($NSVsheader, $offset, 4));
130				$offset += 4;
131				break;
132		}
133
134		$info['video']['resolution_x']       = $info['nsv']['NSVs']['resolution_x'];
135		$info['video']['resolution_y']       = $info['nsv']['NSVs']['resolution_y'];
136		$info['nsv']['NSVs']['frame_rate']   = $this->NSVframerateLookup($info['nsv']['NSVs']['framerate_index']);
137		$info['video']['frame_rate']         = $info['nsv']['NSVs']['frame_rate'];
138		$info['video']['bits_per_sample']    = 24;
139		$info['video']['pixel_aspect_ratio'] = (float) 1;
140
141		return true;
142	}
143
144	/**
145	 * @param int  $fileoffset
146	 * @param bool $getTOCoffsets
147	 *
148	 * @return bool
149	 */
150	public function getNSVfHeaderFilepointer($fileoffset, $getTOCoffsets=false) {
151		$info = &$this->getid3->info;
152		$this->fseek($fileoffset);
153		$NSVfheader = $this->fread(28);
154		$offset = 0;
155
156		$info['nsv']['NSVf']['identifier']    =                  substr($NSVfheader, $offset, 4);
157		$offset += 4;
158
159		if ($info['nsv']['NSVf']['identifier'] != 'NSVf') {
160			$this->error('expected "NSVf" at offset ('.$fileoffset.'), found "'.$info['nsv']['NSVf']['identifier'].'" instead');
161			unset($info['nsv']['NSVf']);
162			return false;
163		}
164
165		$info['nsv']['NSVs']['offset']        = $fileoffset;
166
167		$info['nsv']['NSVf']['header_length'] = getid3_lib::LittleEndian2Int(substr($NSVfheader, $offset, 4));
168		$offset += 4;
169		$info['nsv']['NSVf']['file_size']     = getid3_lib::LittleEndian2Int(substr($NSVfheader, $offset, 4));
170		$offset += 4;
171
172		if ($info['nsv']['NSVf']['file_size'] > $info['avdataend']) {
173			$this->warning('truncated file - NSVf header indicates '.$info['nsv']['NSVf']['file_size'].' bytes, file actually '.$info['avdataend'].' bytes');
174		}
175
176		$info['nsv']['NSVf']['playtime_ms']   = getid3_lib::LittleEndian2Int(substr($NSVfheader, $offset, 4));
177		$offset += 4;
178		$info['nsv']['NSVf']['meta_size']     = getid3_lib::LittleEndian2Int(substr($NSVfheader, $offset, 4));
179		$offset += 4;
180		$info['nsv']['NSVf']['TOC_entries_1'] = getid3_lib::LittleEndian2Int(substr($NSVfheader, $offset, 4));
181		$offset += 4;
182		$info['nsv']['NSVf']['TOC_entries_2'] = getid3_lib::LittleEndian2Int(substr($NSVfheader, $offset, 4));
183		$offset += 4;
184
185		if ($info['nsv']['NSVf']['playtime_ms'] == 0) {
186			$this->error('Corrupt NSV file: NSVf.playtime_ms == zero');
187			return false;
188		}
189
190		$NSVfheader .= $this->fread($info['nsv']['NSVf']['meta_size'] + (4 * $info['nsv']['NSVf']['TOC_entries_1']) + (4 * $info['nsv']['NSVf']['TOC_entries_2']));
191		$NSVfheaderlength = strlen($NSVfheader);
192		$info['nsv']['NSVf']['metadata']      =                  substr($NSVfheader, $offset, $info['nsv']['NSVf']['meta_size']);
193		$offset += $info['nsv']['NSVf']['meta_size'];
194
195		if ($getTOCoffsets) {
196			$TOCcounter = 0;
197			while ($TOCcounter < $info['nsv']['NSVf']['TOC_entries_1']) {
198				if ($TOCcounter < $info['nsv']['NSVf']['TOC_entries_1']) {
199					$info['nsv']['NSVf']['TOC_1'][$TOCcounter] = getid3_lib::LittleEndian2Int(substr($NSVfheader, $offset, 4));
200					$offset += 4;
201					$TOCcounter++;
202				}
203			}
204		}
205
206		if (trim($info['nsv']['NSVf']['metadata']) != '') {
207			$info['nsv']['NSVf']['metadata'] = str_replace('`', "\x01", $info['nsv']['NSVf']['metadata']);
208			$CommentPairArray = explode("\x01".' ', $info['nsv']['NSVf']['metadata']);
209			foreach ($CommentPairArray as $CommentPair) {
210				if (strstr($CommentPair, '='."\x01")) {
211					list($key, $value) = explode('='."\x01", $CommentPair, 2);
212					$info['nsv']['comments'][strtolower($key)][] = trim(str_replace("\x01", '', $value));
213				}
214			}
215		}
216
217		$info['playtime_seconds'] = $info['nsv']['NSVf']['playtime_ms'] / 1000;
218		$info['bitrate']          = ($info['nsv']['NSVf']['file_size'] * 8) / $info['playtime_seconds'];
219
220		return true;
221	}
222
223	/**
224	 * @param int $framerateindex
225	 *
226	 * @return float|false
227	 */
228	public static function NSVframerateLookup($framerateindex) {
229		if ($framerateindex <= 127) {
230			return (float) $framerateindex;
231		}
232		static $NSVframerateLookup = array();
233		if (empty($NSVframerateLookup)) {
234			$NSVframerateLookup[129] = 29.970;
235			$NSVframerateLookup[131] = 23.976;
236			$NSVframerateLookup[133] = 14.985;
237			$NSVframerateLookup[197] = 59.940;
238			$NSVframerateLookup[199] = 47.952;
239		}
240		return (isset($NSVframerateLookup[$framerateindex]) ? $NSVframerateLookup[$framerateindex] : false);
241	}
242
243}
244