PageRenderTime 48ms CodeModel.GetById 9ms RepoModel.GetById 0ms app.codeStats 1ms

/external/fieldtrip/trunk/fileio/private/read_plexon_plx.m

http://open-realtime-fmri.googlecode.com/
MATLAB | 409 lines | 250 code | 48 blank | 111 comment | 35 complexity | a7f91dc40ee54cdc3e4a2c14330bed5e MD5 | raw file
Possible License(s): GPL-2.0, GPL-3.0
  1. function [varargout] = read_plexon_plx(filename, varargin)
  2. % READ_PLEXON_PLX reads header or data from a Plexon *.plx file, which
  3. % is a file containing action-potential (spike) timestamps and waveforms
  4. % (spike channels), event timestamps (event channels), and continuous
  5. % variable data (continuous A/D channels).
  6. %
  7. % Use as
  8. % [hdr] = read_plexon_plx(filename)
  9. % [dat] = read_plexon_plx(filename, ...)
  10. % [dat1, dat2, dat3, hdr] = read_plexon_plx(filename, ...)
  11. %
  12. % Optional input arguments should be specified in key-value pairs
  13. % 'header' = structure with header information
  14. % 'memmap' = 0 or 1
  15. % 'feedback' = 0 or 1
  16. % 'ChannelIndex' = number, or list of numbers (that will result in multiple outputs)
  17. % 'SlowChannelIndex' = number, or list of numbers (that will result in multiple outputs)
  18. % Copyright (C) 2007, Robert Oostenveld
  19. %
  20. % This file is part of FieldTrip, see http://www.ru.nl/neuroimaging/fieldtrip
  21. % for the documentation and details.
  22. %
  23. % FieldTrip is free software: you can redistribute it and/or modify
  24. % it under the terms of the GNU General Public License as published by
  25. % the Free Software Foundation, either version 3 of the License, or
  26. % (at your option) any later version.
  27. %
  28. % FieldTrip is distributed in the hope that it will be useful,
  29. % but WITHOUT ANY WARRANTY; without even the implied warranty of
  30. % MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
  31. % GNU General Public License for more details.
  32. %
  33. % You should have received a copy of the GNU General Public License
  34. % along with FieldTrip. If not, see <http://www.gnu.org/licenses/>.
  35. %
  36. % $Id: read_plexon_plx.m 2528 2011-01-05 14:12:08Z eelspa $
  37. % parse the optional input arguments
  38. hdr = keyval('header', varargin);
  39. memmap = keyval('memmap', varargin);
  40. feedback = keyval('feedback', varargin);
  41. ChannelIndex = keyval('ChannelIndex', varargin);
  42. SlowChannelIndex = keyval('SlowChannelIndex', varargin);
  43. EventIndex = keyval('EventIndex', varargin); % not yet used
  44. % set the defaults
  45. if isempty(memmap)
  46. memmap=0;
  47. end
  48. if isempty(feedback)
  49. feedback=1;
  50. end
  51. needhdr = isempty(hdr);
  52. % start with empty return values
  53. varargout = {};
  54. % the datafile is little endian, hence it may be neccessary to swap bytes in
  55. % the memory mapped data stream depending on the CPU type of this computer
  56. if littleendian
  57. swapFcn = @(x) x;
  58. else
  59. swapFcn = @(x) swapbytes(x);
  60. end
  61. % read header info from file, use Matlabs for automatic byte-ordering
  62. fid = fopen(filename, 'r', 'ieee-le');
  63. fseek(fid, 0, 'eof');
  64. siz = ftell(fid);
  65. fseek(fid, 0, 'bof');
  66. if needhdr
  67. if feedback, fprintf('reading header from %s\n', filename); end
  68. % a PLX file consists of a file header, channel headers, and data blocks
  69. hdr = PL_FileHeader(fid);
  70. for i=1:hdr.NumDSPChannels
  71. hdr.ChannelHeader(i) = PL_ChannelHeader(fid);
  72. end
  73. for i=1:hdr.NumEventChannels
  74. hdr.EventHeader(i) = PL_EventHeader(fid);
  75. end
  76. for i=1:hdr.NumSlowChannels
  77. hdr.SlowChannelHeader(i) = PL_SlowChannelHeader(fid);
  78. end
  79. hdr.DataOffset = ftell(fid);
  80. if memmap
  81. % open the file as meory mapped object, note that byte swapping may be needed
  82. mm = memmapfile(filename, 'offset', hdr.DataOffset, 'format', 'int16');
  83. end
  84. dum = struct(...
  85. 'Type', [],...
  86. 'UpperByteOf5ByteTimestamp', [],...
  87. 'TimeStamp', [],...
  88. 'Channel', [],...
  89. 'Unit', [],...
  90. 'NumberOfWaveforms', [],...
  91. 'NumberOfWordsInWaveform', [] ...
  92. );
  93. % read the header of each data block and remember its data offset in bytes
  94. Nblocks = 0;
  95. offset = hdr.DataOffset; % only used when reading from memmapped file
  96. hdr.DataBlockOffset = [];
  97. hdr.DataBlockHeader = dum;
  98. while offset<siz
  99. if Nblocks>=length(hdr.DataBlockOffset);
  100. % allocate another 1000 elements, this prevents continuous reallocation
  101. hdr.DataBlockOffset(Nblocks+10000) = 0;
  102. hdr.DataBlockHeader(Nblocks+10000) = dum;
  103. if feedback, fprintf('reading DataBlockHeader %4.1f%%\n', 100*(offset-hdr.DataOffset)/(siz-hdr.DataOffset)); end
  104. end
  105. Nblocks = Nblocks+1;
  106. if memmap
  107. % get the header information from the memory mapped file
  108. hdr.DataBlockOffset(Nblocks) = offset;
  109. hdr.DataBlockHeader(Nblocks) = PL_DataBlockHeader(mm, offset-hdr.DataOffset, swapFcn);
  110. % skip the header (16 bytes) and the data (int16 words)
  111. offset = offset + 16 + 2 * double(hdr.DataBlockHeader(Nblocks).NumberOfWordsInWaveform * hdr.DataBlockHeader(Nblocks).NumberOfWaveforms);
  112. else
  113. % read the header information from the file the traditional way
  114. hdr.DataBlockOffset(Nblocks) = offset;
  115. hdr.DataBlockHeader(Nblocks) = PL_DataBlockHeader(fid, [], swapFcn);
  116. fseek(fid, 2 * double(hdr.DataBlockHeader(Nblocks).NumberOfWordsInWaveform * hdr.DataBlockHeader(Nblocks).NumberOfWaveforms), 'cof'); % data consists of short integers
  117. offset = ftell(fid);
  118. end % if memmap
  119. end
  120. % this prints the final 100%
  121. if feedback, fprintf('reading DataBlockHeader %4.1f%%\n', 100*(offset-hdr.DataOffset)/(siz-hdr.DataOffset)); end
  122. % remove the allocated space that was not needed
  123. hdr.DataBlockOffset = hdr.DataBlockOffset(1:Nblocks);
  124. hdr.DataBlockHeader = hdr.DataBlockHeader(1:Nblocks);
  125. end % if needhdr
  126. %%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%
  127. % read the spike channel data
  128. %%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%
  129. if ~isempty(ChannelIndex)
  130. if feedback, fprintf('reading spike data from %s\n', filename); end
  131. if memmap
  132. % open the file as meory mapped object, note that byte swapping may be needed
  133. mm = memmapfile(filename, 'offset', hdr.DataOffset, 'format', 'int16');
  134. end
  135. type = [hdr.DataBlockHeader.Type];
  136. chan = [hdr.DataBlockHeader.Channel];
  137. ts = [hdr.DataBlockHeader.TimeStamp];
  138. for i=1:length(ChannelIndex)
  139. % determine the data blocks with continuous data belonging to this channel
  140. sel = (type==1 & chan==hdr.ChannelHeader(ChannelIndex(i)).Channel);
  141. sel = find(sel);
  142. if isempty(sel)
  143. warning('spike channel %d contains no data', ChannelIndex(i));
  144. varargin{end+1} = [];
  145. continue;
  146. end
  147. % the number of samples can potentially be different in each block
  148. num = double([hdr.DataBlockHeader(sel).NumberOfWordsInWaveform]) .* double([hdr.DataBlockHeader(sel).NumberOfWaveforms]);
  149. % check whether the number of samples per block makes sense
  150. if any(num~=num(1))
  151. error('spike channel blocks with diffent number of samples');
  152. end
  153. % allocate memory to hold the data
  154. buf = zeros(num(1), length(sel), 'int16');
  155. if memmap
  156. % get the header information from the memory mapped file
  157. datbeg = double(hdr.DataBlockOffset(sel) - hdr.DataOffset)/2 + 8 + 1; % expressed in 2-byte words, minus the file header, skip the 16 byte block header
  158. datend = datbeg + num - 1;
  159. for j=1:length(sel)
  160. buf(:,j) = mm.Data(datbeg(j):datend(j));
  161. end
  162. % optionally swap the bytes to correct for the endianness
  163. buf = swapFcn(buf);
  164. else
  165. % read the data from the file in the traditional way
  166. offset = double(hdr.DataBlockOffset(sel)) + 16; % expressed in bytes, skip the 16 byte block header
  167. for j=1:length(sel)
  168. fseek(fid, offset(j), 'bof');
  169. buf(:,j) = fread(fid, num(j), 'int16');
  170. end
  171. end % if memmap
  172. % remember the data for this channel
  173. varargout{i} = buf;
  174. end %for ChannelIndex
  175. end % if ChannelIndex
  176. %%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%
  177. % read the continuous channel data
  178. %%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%
  179. if ~isempty(SlowChannelIndex)
  180. if feedback, fprintf('reading continuous data from %s\n', filename); end
  181. dat = {};
  182. if memmap
  183. % open the file as meory mapped object, note that byte swapping may be needed
  184. mm = memmapfile(filename, 'offset', hdr.DataOffset, 'format', 'int16');
  185. end
  186. type = [hdr.DataBlockHeader.Type];
  187. chan = [hdr.DataBlockHeader.Channel];
  188. ts = [hdr.DataBlockHeader.TimeStamp];
  189. for i=1:length(SlowChannelIndex)
  190. % determine the data blocks with continuous data belonging to this channel
  191. sel = (type==5 & chan==hdr.SlowChannelHeader(SlowChannelIndex(i)).Channel);
  192. sel = find(sel);
  193. if isempty(sel)
  194. error(sprintf('Continuous channel %d contains no data', SlowChannelIndex(i)));
  195. % warning('Continuous channel %d contains no data', SlowChannelIndex(i));
  196. % varargin{end+1} = [];
  197. % continue;
  198. end
  199. % the number of samples can be different in each block
  200. num = double([hdr.DataBlockHeader(sel).NumberOfWordsInWaveform]) .* double([hdr.DataBlockHeader(sel).NumberOfWaveforms]);
  201. cumnum = cumsum([0 num]);
  202. % allocate memory to hold the data
  203. buf = zeros(1, cumnum(end), 'int16');
  204. if memmap
  205. % get the header information from the memory mapped file
  206. datbeg = double(hdr.DataBlockOffset(sel) - hdr.DataOffset)/2 + 8 + 1; % expressed in 2-byte words, minus the file header, skip the 16 byte block header
  207. datend = datbeg + num - 1;
  208. for j=1:length(sel)
  209. bufbeg = cumnum(j)+1;
  210. bufend = cumnum(j+1);
  211. % copy the data from the memory mapped file into the continuous buffer
  212. buf(bufbeg:bufend) = mm.Data(datbeg(j):datend(j));
  213. end
  214. % optionally swap the bytes to correct for the endianness
  215. buf = swapFcn(buf);
  216. else
  217. % read the data from the file in the traditional way
  218. offset = double(hdr.DataBlockOffset(sel)) + 16; % expressed in bytes, skip the 16 byte block header
  219. for j=1:length(sel)
  220. bufbeg = cumnum(j)+1;
  221. bufend = cumnum(j+1);
  222. % copy the data from the file into the continuous buffer
  223. fseek(fid, offset(j), 'bof');
  224. buf(bufbeg:bufend) = fread(fid, num(j), 'int16');
  225. end
  226. end % if memmap
  227. % remember the data for this channel
  228. varargout{i} = buf;
  229. end %for SlowChannelIndex
  230. end % if SlowChannelIndex
  231. %%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%
  232. % read the event channel data
  233. %%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%
  234. if ~isempty(EventIndex)
  235. type = [hdr.DataBlockHeader.Type];
  236. chan = [hdr.DataBlockHeader.Channel];
  237. ts = [hdr.DataBlockHeader.TimeStamp];
  238. for i=1:length(EventIndex)
  239. % determine the data blocks with continuous data belonging to this channel
  240. sel = (type==0 & chan==hdr.EventHeader(EventIndex(i)).Channel);
  241. sel = find(sel);
  242. if isempty(sel)
  243. warning('event channel %d contains no data', EventIndex(i));
  244. varargin{end+1} = [];
  245. continue;
  246. end
  247. % this still has to be implemented
  248. keyboard
  249. end % for EventIndex
  250. end % if EventIndex
  251. fclose(fid);
  252. % always return the header as last
  253. varargout{end+1} = hdr;
  254. return
  255. %%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%
  256. % SUBFUNCTIONS for reading the different header elements
  257. %%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%
  258. function hdr = PL_FileHeader(fid)
  259. hdr.MagicNumber = fread(fid, 1, 'uint32=>uint32'); % = 0x58454c50;
  260. hdr.Version = fread(fid, 1, 'int32' ); % Version of the data format; determines which data items are valid
  261. hdr.Comment = fread(fid, [1 128], 'uint8=>char' ); % User-supplied comment
  262. hdr.ADFrequency = fread(fid, 1, 'int32' ); % Timestamp frequency in hertz
  263. hdr.NumDSPChannels = fread(fid, 1, 'int32' ); % Number of DSP channel headers in the file
  264. hdr.NumEventChannels = fread(fid, 1, 'int32' ); % Number of Event channel headers in the file
  265. hdr.NumSlowChannels = fread(fid, 1, 'int32' ); % Number of A/D channel headers in the file
  266. hdr.NumPointsWave = fread(fid, 1, 'int32' ); % Number of data points in waveform
  267. hdr.NumPointsPreThr = fread(fid, 1, 'int32' ); % Number of data points before crossing the threshold
  268. hdr.Year = fread(fid, 1, 'int32' ); % Time/date when the data was acquired
  269. hdr.Month = fread(fid, 1, 'int32' );
  270. hdr.Day = fread(fid, 1, 'int32' );
  271. hdr.Hour = fread(fid, 1, 'int32' );
  272. hdr.Minute = fread(fid, 1, 'int32' );
  273. hdr.Second = fread(fid, 1, 'int32' );
  274. hdr.FastRead = fread(fid, 1, 'int32' ); % reserved
  275. hdr.WaveformFreq = fread(fid, 1, 'int32' ); % waveform sampling rate; ADFrequency above is timestamp freq
  276. hdr.LastTimestamp = fread(fid, 1, 'double'); % duration of the experimental session, in ticks
  277. % The following 6 items are only valid if Version >= 103
  278. hdr.Trodalness = fread(fid, 1, 'char' ); % 1 for single, 2 for stereotrode, 4 for tetrode
  279. hdr.DataTrodalness = fread(fid, 1, 'char' ); % trodalness of the data representation
  280. hdr.BitsPerSpikeSample = fread(fid, 1, 'char' ); % ADC resolution for spike waveforms in bits (usually 12)
  281. hdr.BitsPerSlowSample = fread(fid, 1, 'char' ); % ADC resolution for slow-channel data in bits (usually 12)
  282. hdr.SpikeMaxMagnitudeMV = fread(fid, 1, 'uint16'); % the zero-to-peak voltage in mV for spike waveform adc values (usually 3000)
  283. hdr.SlowMaxMagnitudeMV = fread(fid, 1, 'uint16'); % the zero-to-peak voltage in mV for slow-channel waveform adc values (usually 5000); Only valid if Version >= 105 (usually either 1000 or 500)
  284. % The following item is only valid if Version >= 105
  285. hdr.SpikePreAmpGain = fread(fid, 1, 'uint16'); % so that this part of the header is 256 bytes
  286. hdr.Padding = fread(fid, 46, 'char' ); % so that this part of the header is 256 bytes
  287. % Counters for the number of timestamps and waveforms in each channel and unit.
  288. % Note that these only record the counts for the first 4 units in each channel.
  289. % channel numbers are 1-based - array entry at [0] is unused
  290. hdr.TSCounts = fread(fid, [5 130], 'int32' ); % number of timestamps[channel][unit]
  291. hdr.WFCounts = fread(fid, [5 130], 'int32' ); % number of waveforms[channel][unit]
  292. % Starting at index 300, the next array also records the number of samples for the
  293. % continuous channels. Note that since EVCounts has only 512 entries, continuous
  294. % channels above channel 211 do not have sample counts.
  295. hdr.EVCounts = fread(fid, 512, 'int32' ); % number of timestamps[event_number]
  296. %%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%
  297. function hdr = PL_ChannelHeader(fid)
  298. hdr.Name = fread(fid, [1 32], 'uint8=>char' ); % Name given to the DSP channel
  299. hdr.SIGName = fread(fid, [1 32], 'uint8=>char' ); % Name given to the corresponding SIG channel
  300. hdr.Channel = fread(fid, 1, 'int32' ); % DSP channel number, 1-based
  301. hdr.WFRate = fread(fid, 1, 'int32' ); % When MAP is doing waveform rate limiting, this is limit w/f per sec divided by 10
  302. hdr.SIG = fread(fid, 1, 'int32' ); % SIG channel associated with this DSP channel 1 - based
  303. hdr.Ref = fread(fid, 1, 'int32' ); % SIG channel used as a Reference signal, 1- based
  304. hdr.Gain = fread(fid, 1, 'int32' ); % actual gain divided by SpikePreAmpGain. For pre version 105, actual gain divided by 1000.
  305. hdr.Filter = fread(fid, 1, 'int32' ); % 0 or 1
  306. hdr.Threshold = fread(fid, 1, 'int32' ); % Threshold for spike detection in a/d values
  307. hdr.Method = fread(fid, 1, 'int32' ); % Method used for sorting units, 1 - boxes, 2 - templates
  308. hdr.NUnits = fread(fid, 1, 'int32' ); % number of sorted units
  309. hdr.Template = fread(fid, [64 5], 'int16' ); % Templates used for template sorting, in a/d values
  310. hdr.Fit = fread(fid, 5, 'int32' ); % Template fit
  311. hdr.SortWidth = fread(fid, 1, 'int32' ); % how many points to use in template sorting (template only)
  312. hdr.Boxes = reshape(fread(fid, 4*2*5, 'int16' ), [4 2 5]); % the boxes used in boxes sorting
  313. hdr.SortBeg = fread(fid, 1, 'int32' ); % beginning of the sorting window to use in template sorting (width defined by SortWidth)
  314. hdr.Comment = fread(fid, [1 128], 'uint8=>char' );
  315. hdr.Padding = fread(fid, 11, 'int32' );
  316. %%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%
  317. function hdr = PL_EventHeader(fid)
  318. hdr.Name = fread(fid, [1 32], 'uint8=>char' ); % name given to this event
  319. hdr.Channel = fread(fid, 1, 'int32' ); % event number, 1-based
  320. hdr.Comment = fread(fid, [1 128], 'uint8=>char' );
  321. hdr.Padding = fread(fid, 33, 'int32' );
  322. %%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%
  323. function hdr = PL_SlowChannelHeader(fid)
  324. hdr.Name = fread(fid, [1 32], 'uint8=>char' ); % name given to this channel
  325. hdr.Channel = fread(fid, 1, 'int32' ); % channel number, 0-based
  326. hdr.ADFreq = fread(fid, 1, 'int32' ); % digitization frequency
  327. hdr.Gain = fread(fid, 1, 'int32' ); % gain at the adc card
  328. hdr.Enabled = fread(fid, 1, 'int32' ); % whether this channel is enabled for taking data, 0 or 1
  329. hdr.PreAmpGain = fread(fid, 1, 'int32' ); % gain at the preamp
  330. % As of Version 104, this indicates the spike channel (PL_ChannelHeader.Channel) of
  331. % a spike channel corresponding to this continuous data channel.
  332. % <=0 means no associated spike channel.
  333. hdr.SpikeChannel = fread(fid, 1, 'int32' );
  334. hdr.Comment = fread(fid, [1 128], 'uint8=>char' );
  335. hdr.Padding = fread(fid, 28, 'int32' );
  336. %%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%
  337. function hdr = PL_DataBlockHeader(fid, offset, swapFcn)
  338. % % this is the conventional code, it has been replaced by code that works
  339. % % with both regular and memmapped files
  340. % hdr.Type = fread(fid, 1, 'int16=>int16' ); % Data type; 1=spike, 4=Event, 5=continuous
  341. % hdr.UpperByteOf5ByteTimestamp = fread(fid, 1, 'uint16=>uint16' ); % Upper 8 bits of the 40 bit timestamp
  342. % hdr.TimeStamp = fread(fid, 1, 'uint32=>uint32' ); % Lower 32 bits of the 40 bit timestamp
  343. % hdr.Channel = fread(fid, 1, 'int16=>int16' ); % Channel number
  344. % hdr.Unit = fread(fid, 1, 'int16=>int16' ); % Sorted unit number; 0=unsorted
  345. % hdr.NumberOfWaveforms = fread(fid, 1, 'int16=>int16' ); % Number of waveforms in the data to folow, usually 0 or 1
  346. % hdr.NumberOfWordsInWaveform = fread(fid, 1, 'int16=>int16' ); % Number of samples per waveform in the data to follow
  347. if isa(fid, 'memmapfile')
  348. mm = fid;
  349. datbeg = offset/2 + 1; % the offset is in bytes (minus the file header), the memory mapped file is indexed in int16 words
  350. datend = offset/2 + 8;
  351. buf = mm.Data(datbeg:datend);
  352. else
  353. buf = fread(fid, 8, 'int16=>int16');
  354. end
  355. hdr.Type = swapFcn(buf(1));
  356. hdr.UpperByteOf5ByteTimestamp = swapFcn(uint16(buf(2)));
  357. hdr.TimeStamp = swapFcn(typecast(buf([3 4]), 'uint32'));
  358. hdr.Channel = swapFcn(buf(5));
  359. hdr.Unit = swapFcn(buf(6));
  360. hdr.NumberOfWaveforms = swapFcn(buf(7));
  361. hdr.NumberOfWordsInWaveform = swapFcn(buf(8));