remove trailing whitespaces
[nihav.git] / nihav-core / src / soundcvt / mod.rs
1 pub use crate::formats::{NASoniton,NAChannelMap};
2 pub use crate::frame::{NAAudioBuffer,NAAudioInfo,NABufferType};
3 use crate::formats::NAChannelType;
4 use crate::frame::alloc_audio_buffer;
5 use crate::io::byteio::*;
6 use std::f32::consts::SQRT_2;
7
8 #[derive(Clone,Copy,Debug,PartialEq)]
9 pub enum SoundConvertError {
10 InvalidInput,
11 AllocError,
12 Unsupported,
13 }
14
15 enum ChannelOp {
16 Passthrough,
17 Reorder(Vec<usize>),
18 Remix(Vec<f32>),
19 DupMono(Vec<bool>),
20 }
21
22 impl ChannelOp {
23 fn is_remix(&self) -> bool {
24 match *self {
25 ChannelOp::Remix(_) => true,
26 ChannelOp::DupMono(_) => true,
27 _ => false,
28 }
29 }
30 }
31
32 fn apply_channel_op<T:Copy>(ch_op: &ChannelOp, src: &Vec<T>, dst: &mut Vec<T>) {
33 match *ch_op {
34 ChannelOp::Passthrough => {
35 dst.copy_from_slice(src.as_slice());
36 },
37 ChannelOp::Reorder(ref reorder) => {
38 for (out, idx) in dst.iter_mut().zip(reorder.iter()) {
39 *out = src[*idx];
40 }
41 },
42 _ => {},
43 };
44 }
45
46 fn remix_i32(ch_op: &ChannelOp, src: &Vec<i32>, dst: &mut Vec<i32>) {
47 if let ChannelOp::Remix(ref remix_mat) = ch_op {
48 let sch = src.len();
49 for (out, coeffs) in dst.iter_mut().zip(remix_mat.chunks(sch)) {
50 let mut sum = 0.0;
51 for (inval, coef) in src.iter().zip(coeffs.iter()) {
52 sum += (*inval as f32) * *coef;
53 }
54 *out = sum as i32;
55 }
56 }
57 if let ChannelOp::DupMono(ref dup_mat) = ch_op {
58 let src = src[0];
59 for (out, copy) in dst.iter_mut().zip(dup_mat.iter()) {
60 *out = if *copy { src } else { 0 };
61 }
62 }
63 }
64
65 fn remix_f32(ch_op: &ChannelOp, src: &Vec<f32>, dst: &mut Vec<f32>) {
66 if let ChannelOp::Remix(ref remix_mat) = ch_op {
67 let sch = src.len();
68 for (out, coeffs) in dst.iter_mut().zip(remix_mat.chunks(sch)) {
69 let mut sum = 0.0;
70 for (inval, coef) in src.iter().zip(coeffs.iter()) {
71 sum += *inval * *coef;
72 }
73 *out = sum;
74 }
75 }
76 if let ChannelOp::DupMono(ref dup_mat) = ch_op {
77 let src = src[0];
78 for (out, copy) in dst.iter_mut().zip(dup_mat.iter()) {
79 *out = if *copy { src } else { 0.0 };
80 }
81 }
82 }
83
84 trait FromFmt<T:Copy> {
85 fn cvt_from(val: T) -> Self;
86 }
87
88 impl FromFmt<u8> for u8 {
89 fn cvt_from(val: u8) -> u8 { val }
90 }
91 impl FromFmt<u8> for i16 {
92 fn cvt_from(val: u8) -> i16 { ((val as i16) - 128).wrapping_mul(0x101) }
93 }
94 impl FromFmt<u8> for i32 {
95 fn cvt_from(val: u8) -> i32 { ((val as i32) - 128).wrapping_mul(0x01010101) }
96 }
97 impl FromFmt<u8> for f32 {
98 fn cvt_from(val: u8) -> f32 { ((val as f32) - 128.0) / 128.0 }
99 }
100
101 impl FromFmt<i16> for u8 {
102 fn cvt_from(val: i16) -> u8 { ((val >> 8) + 128).min(255).max(0) as u8 }
103 }
104 impl FromFmt<i16> for i16 {
105 fn cvt_from(val: i16) -> i16 { val }
106 }
107 impl FromFmt<i16> for i32 {
108 fn cvt_from(val: i16) -> i32 { (val as i32).wrapping_mul(0x10001) }
109 }
110 impl FromFmt<i16> for f32 {
111 fn cvt_from(val: i16) -> f32 { (val as f32) / 32768.0 }
112 }
113
114 impl FromFmt<i32> for u8 {
115 fn cvt_from(val: i32) -> u8 { ((val >> 24) + 128).min(255).max(0) as u8 }
116 }
117 impl FromFmt<i32> for i16 {
118 fn cvt_from(val: i32) -> i16 { (val >> 16) as i16 }
119 }
120 impl FromFmt<i32> for i32 {
121 fn cvt_from(val: i32) -> i32 { val }
122 }
123 impl FromFmt<i32> for f32 {
124 fn cvt_from(val: i32) -> f32 { (val as f32) / 31.0f32.exp2() }
125 }
126
127 impl FromFmt<f32> for u8 {
128 fn cvt_from(val: f32) -> u8 { ((val * 128.0) + 128.0).min(255.0).max(0.0) as u8 }
129 }
130 impl FromFmt<f32> for i16 {
131 fn cvt_from(val: f32) -> i16 { (val * 32768.0).min(16383.0).max(-16384.0) as i16 }
132 }
133 impl FromFmt<f32> for i32 {
134 fn cvt_from(val: f32) -> i32 { (val * 31.0f32.exp2()) as i32 }
135 }
136 impl FromFmt<f32> for f32 {
137 fn cvt_from(val: f32) -> f32 { val }
138 }
139
140 trait IntoFmt<T:Copy> {
141 fn cvt_into(self) -> T;
142 }
143
144 impl<T:Copy, U:Copy> IntoFmt<U> for T where U: FromFmt<T> {
145 fn cvt_into(self) -> U { U::cvt_from(self) }
146 }
147
148
149 trait SampleReader {
150 fn get_samples_i32(&self, pos: usize, dst: &mut Vec<i32>);
151 fn get_samples_f32(&self, pos: usize, dst: &mut Vec<f32>);
152 }
153
154 struct GenericSampleReader<'a, T:Copy> {
155 data: &'a [T],
156 stride: usize,
157 }
158
159 impl<'a, T:Copy+IntoFmt<i32>+IntoFmt<f32>> SampleReader for GenericSampleReader<'a, T> {
160 fn get_samples_i32(&self, pos: usize, dst: &mut Vec<i32>) {
161 let mut off = pos;
162 for el in dst.iter_mut() {
163 *el = self.data[off].cvt_into();
164 off += self.stride;
165 }
166 }
167 fn get_samples_f32(&self, pos: usize, dst: &mut Vec<f32>) {
168 let mut off = pos;
169 for el in dst.iter_mut() {
170 *el = self.data[off].cvt_into();
171 off += self.stride;
172 }
173 }
174 }
175
176 struct PackedSampleReader<'a> {
177 data: &'a [u8],
178 fmt: NASoniton,
179 bpp: usize,
180 }
181
182 impl<'a> PackedSampleReader<'a> {
183 fn new(data: &'a [u8], fmt: NASoniton) -> Self {
184 if (fmt.bits & 7) != 0 { unimplemented!(); }
185 let bpp = (fmt.bits >> 3) as usize;
186 Self { data, fmt, bpp }
187 }
188 fn get_samples<T:Copy>(&self, pos: usize, dst: &mut Vec<T>) where u8: IntoFmt<T>, i16: IntoFmt<T>, i32: IntoFmt<T>, f32: IntoFmt<T> {
189 let mut offset = pos * self.bpp * dst.len();
190
191 for el in dst.iter_mut() {
192 let src = &self.data[offset..];
193 *el = if !self.fmt.float {
194 match (self.bpp, self.fmt.be) {
195 (1, _) => src[0].cvt_into(),
196 (2, true) => (read_u16be(src).unwrap() as i16).cvt_into(),
197 (2, false) => (read_u16le(src).unwrap() as i16).cvt_into(),
198 (3, true) => ((read_u24be(src).unwrap() << 8) as i32).cvt_into(),
199 (3, false) => ((read_u24be(src).unwrap() << 8) as i32).cvt_into(),
200 (4, true) => (read_u32be(src).unwrap() as i32).cvt_into(),
201 (4, false) => (read_u32be(src).unwrap() as i32).cvt_into(),
202 _ => unreachable!(),
203 }
204 } else {
205 match (self.bpp, self.fmt.be) {
206 (4, true) => read_f32be(src).unwrap().cvt_into(),
207 (4, false) => read_f32le(src).unwrap().cvt_into(),
208 (8, true) => (read_f64be(src).unwrap() as f32).cvt_into(),
209 (8, false) => (read_f64le(src).unwrap() as f32).cvt_into(),
210 (_, _) => unreachable!(),
211 }
212 };
213 offset += self.bpp;
214 }
215 }
216 }
217
218 impl SampleReader for PackedSampleReader<'_> {
219 fn get_samples_i32(&self, pos: usize, dst: &mut Vec<i32>) {
220 self.get_samples(pos, dst);
221 }
222 fn get_samples_f32(&self, pos: usize, dst: &mut Vec<f32>) {
223 self.get_samples(pos, dst);
224 }
225 }
226
227 trait SampleWriter {
228 fn store_samples_i32(&mut self, pos: usize, src: &Vec<i32>);
229 fn store_samples_f32(&mut self, pos: usize, src: &Vec<f32>);
230 }
231
232 struct GenericSampleWriter<'a, T:Copy> {
233 data: &'a mut [T],
234 stride: usize,
235 }
236
237 impl<'a, T:Copy+FromFmt<i32>+FromFmt<f32>> SampleWriter for GenericSampleWriter<'a, T> {
238 fn store_samples_i32(&mut self, pos: usize, src: &Vec<i32>) {
239 let mut off = pos;
240 for el in src.iter() {
241 self.data[off] = (*el).cvt_into();
242 off += self.stride;
243 }
244 }
245 fn store_samples_f32(&mut self, pos: usize, src: &Vec<f32>) {
246 let mut off = pos;
247 for el in src.iter() {
248 self.data[off] = (*el).cvt_into();
249 off += self.stride;
250 }
251 }
252 }
253
254 struct PackedSampleWriter<'a> {
255 data: &'a mut [u8],
256 fmt: NASoniton,
257 bpp: usize,
258 }
259
260 impl<'a> PackedSampleWriter<'a> {
261 fn new(data: &'a mut [u8], fmt: NASoniton) -> Self {
262 if (fmt.bits & 7) != 0 { unimplemented!(); }
263 let bpp = (fmt.bits >> 3) as usize;
264 Self { data, fmt, bpp }
265 }
266
267 fn store_samples<T:Copy>(&mut self, pos: usize, src: &Vec<T>) where u8: FromFmt<T>, i16: FromFmt<T>, i32: FromFmt<T>, f32: FromFmt<T> {
268 let mut offset = pos * self.bpp * src.len();
269 for el in src.iter() {
270 let dst = &mut self.data[offset..];
271 if !self.fmt.float {
272 match (self.bpp, self.fmt.be) {
273 (1, _) => {
274 dst[0] = u8::cvt_from(*el);
275 },
276 (2, true) => write_u16be(dst, i16::cvt_from(*el) as u16).unwrap(),
277 (2, false) => write_u16le(dst, i16::cvt_from(*el) as u16).unwrap(),
278 (3, true) => write_u24be(dst, (i32::cvt_from(*el) >> 8) as u32).unwrap(),
279 (3, false) => write_u24le(dst, (i32::cvt_from(*el) >> 8) as u32).unwrap(),
280 (4, true) => write_u32be(dst, i32::cvt_from(*el) as u32).unwrap(),
281 (4, false) => write_u32le(dst, i32::cvt_from(*el) as u32).unwrap(),
282 _ => unreachable!(),
283 };
284 } else {
285 match (self.bpp, self.fmt.be) {
286 (4, true) => write_f32be(dst, f32::cvt_from(*el)).unwrap(),
287 (4, false) => write_f32le(dst, f32::cvt_from(*el)).unwrap(),
288 (8, true) => write_f64be(dst, f32::cvt_from(*el) as f64).unwrap(),
289 (8, false) => write_f64le(dst, f32::cvt_from(*el) as f64).unwrap(),
290 (_, _) => unreachable!(),
291 };
292 }
293 offset += self.bpp;
294 }
295 }
296 }
297
298 impl SampleWriter for PackedSampleWriter<'_> {
299 fn store_samples_i32(&mut self, pos: usize, src: &Vec<i32>) {
300 self.store_samples(pos, src);
301 }
302 fn store_samples_f32(&mut self, pos: usize, src: &Vec<f32>) {
303 self.store_samples(pos, src);
304 }
305 }
306
307 pub fn convert_audio_frame(src: &NABufferType, dst_info: &NAAudioInfo, dst_chmap: &NAChannelMap) ->
308 Result<NABufferType, SoundConvertError> {
309 let mut nsamples = src.get_audio_length();
310 if nsamples == 0 {
311 return Err(SoundConvertError::InvalidInput);
312 }
313 let src_chmap = src.get_chmap().unwrap();
314 let src_info = src.get_audio_info().unwrap();
315 if (src_chmap.num_channels() == 0) || (dst_chmap.num_channels() == 0) {
316 return Err(SoundConvertError::InvalidInput);
317 }
318
319 if let NABufferType::AudioPacked(_) = src {
320 nsamples = nsamples * 8 / (src_info.get_format().get_bits() as usize) / src_chmap.num_channels();
321 }
322
323 let needs_remix = src_chmap.num_channels() != dst_chmap.num_channels();
324 let no_channel_needs = !needs_remix && channel_maps_equal(src_chmap, dst_chmap);
325 let needs_reorder = !needs_remix && !no_channel_needs && channel_maps_reordered(src_chmap, dst_chmap);
326
327 let channel_op = if no_channel_needs {
328 ChannelOp::Passthrough
329 } else if needs_reorder {
330 let reorder_mat = calculate_reorder_matrix(src_chmap, dst_chmap);
331 ChannelOp::Reorder(reorder_mat)
332 } else if src_chmap.num_channels() > 1 {
333 let remix_mat = calculate_remix_matrix(src_chmap, dst_chmap);
334 ChannelOp::Remix(remix_mat)
335 } else {
336 let mut dup_mat: Vec<bool> = Vec::with_capacity(dst_chmap.num_channels());
337 for i in 0..dst_chmap.num_channels() {
338 let ch = dst_chmap.get_channel(i);
339 if ch.is_left() || ch.is_right() || ch == NAChannelType::C {
340 dup_mat.push(true);
341 } else {
342 dup_mat.push(false);
343 }
344 }
345 ChannelOp::DupMono(dup_mat)
346 };
347
348 let src_fmt = src_info.get_format();
349 let dst_fmt = dst_info.get_format();
350 let no_conversion = src_fmt == dst_fmt;
351
352 if no_conversion && no_channel_needs {
353 return Ok(src.clone());
354 }
355
356 let ret = alloc_audio_buffer(dst_info.clone(), nsamples, dst_chmap.clone());
357 if ret.is_err() {
358 return Err(SoundConvertError::AllocError);
359 }
360 let mut dst_buf = ret.unwrap();
361
362 let sr: Box<dyn SampleReader> = match src {
363 NABufferType::AudioU8(ref ab) => {
364 let stride = ab.get_stride();
365 let data = ab.get_data();
366 Box::new(GenericSampleReader { data, stride })
367 },
368 NABufferType::AudioI16(ref ab) => {
369 let data = ab.get_data();
370 let stride = ab.get_stride();
371 Box::new(GenericSampleReader { data, stride })
372 },
373 NABufferType::AudioI32(ref ab) => {
374 let data = ab.get_data();
375 let stride = ab.get_stride();
376 Box::new(GenericSampleReader { data, stride })
377 },
378 NABufferType::AudioF32(ref ab) => {
379 let data = ab.get_data();
380 let stride = ab.get_stride();
381 Box::new(GenericSampleReader { data, stride })
382 },
383 NABufferType::AudioPacked(ref ab) => {
384 let data = ab.get_data();
385 Box::new(PackedSampleReader::new(data, src_fmt))
386 },
387 _ => unimplemented!(),
388 };
389 let mut sw: Box<dyn SampleWriter> = match dst_buf {
390 NABufferType::AudioU8(ref mut ab) => {
391 let stride = ab.get_stride();
392 let data = ab.get_data_mut().unwrap();
393 Box::new(GenericSampleWriter { data, stride })
394 },
395 NABufferType::AudioI16(ref mut ab) => {
396 let stride = ab.get_stride();
397 let data = ab.get_data_mut().unwrap();
398 Box::new(GenericSampleWriter { data, stride })
399 },
400 NABufferType::AudioI32(ref mut ab) => {
401 let stride = ab.get_stride();
402 let data = ab.get_data_mut().unwrap();
403 Box::new(GenericSampleWriter { data, stride })
404 },
405 NABufferType::AudioF32(ref mut ab) => {
406 let stride = ab.get_stride();
407 let data = ab.get_data_mut().unwrap();
408 Box::new(GenericSampleWriter { data, stride })
409 },
410 NABufferType::AudioPacked(ref mut ab) => {
411 let data = ab.get_data_mut().unwrap();
412 Box::new(PackedSampleWriter::new(data, dst_fmt))
413 },
414 _ => unimplemented!(),
415 };
416
417 let into_float = dst_fmt.float;
418 if !into_float {
419 let mut svec = vec![0; src_chmap.num_channels()];
420 let mut dvec = vec![0; dst_chmap.num_channels()];
421 for i in 0..nsamples {
422 sr.get_samples_i32(i, &mut svec);
423 if !channel_op.is_remix() {
424 apply_channel_op(&channel_op, &svec, &mut dvec);
425 } else {
426 remix_i32(&channel_op, &svec, &mut dvec);
427 }
428 sw.store_samples_i32(i, &dvec);
429 }
430 } else {
431 let mut svec = vec![0.0; src_chmap.num_channels()];
432 let mut dvec = vec![0.0; dst_chmap.num_channels()];
433 for i in 0..nsamples {
434 sr.get_samples_f32(i, &mut svec);
435 if !channel_op.is_remix() {
436 apply_channel_op(&channel_op, &svec, &mut dvec);
437 } else {
438 remix_f32(&channel_op, &svec, &mut dvec);
439 }
440 sw.store_samples_f32(i, &dvec);
441 }
442 }
443 drop(sw);
444
445 Ok(dst_buf)
446 }
447
448 pub fn channel_maps_equal(a: &NAChannelMap, b: &NAChannelMap) -> bool {
449 if a.num_channels() != b.num_channels() { return false; }
450 for i in 0..a.num_channels() {
451 if a.get_channel(i) != b.get_channel(i) {
452 return false;
453 }
454 }
455 true
456 }
457
458 pub fn channel_maps_reordered(a: &NAChannelMap, b: &NAChannelMap) -> bool {
459 if a.num_channels() != b.num_channels() { return false; }
460 let mut count_a = [0u8; 32];
461 let mut count_b = [0u8; 32];
462 for i in 0..a.num_channels() {
463 count_a[a.get_channel(i) as usize] += 1;
464 count_b[b.get_channel(i) as usize] += 1;
465 }
466 for (c0, c1) in count_a.iter().zip(count_b.iter()) {
467 if *c0 != *c1 {
468 return false;
469 }
470 }
471 true
472 }
473
474 pub fn calculate_reorder_matrix(src: &NAChannelMap, dst: &NAChannelMap) -> Vec<usize> {
475 if src.num_channels() != dst.num_channels() { return Vec::new(); }
476 let num_channels = src.num_channels();
477 let mut reorder: Vec<usize> = Vec::with_capacity(num_channels);
478 for i in 0..num_channels {
479 let dst_ch = dst.get_channel(i);
480 for j in 0..num_channels {
481 if src.get_channel(j) == dst_ch {
482 reorder.push(j);
483 break;
484 }
485 }
486 }
487 if reorder.len() != num_channels { reorder.clear(); }
488 reorder
489 }
490
491 fn is_stereo(chmap: &NAChannelMap) -> bool {
492 (chmap.num_channels() == 2) &&
493 (chmap.get_channel(0) == NAChannelType::L) &&
494 (chmap.get_channel(1) == NAChannelType::R)
495 }
496
497 pub fn calculate_remix_matrix(src: &NAChannelMap, dst: &NAChannelMap) -> Vec<f32> {
498 if is_stereo(src) && dst.num_channels() == 1 &&
499 (dst.get_channel(0) == NAChannelType::L || dst.get_channel(0) == NAChannelType::C) {
500 return vec![0.5, 0.5];
501 }
502 if src.num_channels() >= 5 && is_stereo(dst) {
503 let src_nch = src.num_channels();
504 let mut mat = vec![0.0f32; src_nch * 2];
505 let (l_mat, r_mat) = mat.split_at_mut(src_nch);
506 for ch in 0..src_nch {
507 match src.get_channel(ch) {
508 NAChannelType::L => l_mat[ch] = 1.0,
509 NAChannelType::R => r_mat[ch] = 1.0,
510 NAChannelType::C => { l_mat[ch] = SQRT_2 / 2.0; r_mat[ch] = SQRT_2 / 2.0; },
511 NAChannelType::Ls => l_mat[ch] = SQRT_2 / 2.0,
512 NAChannelType::Rs => r_mat[ch] = SQRT_2 / 2.0,
513 _ => {},
514 };
515 }
516 return mat;
517 }
518 unimplemented!();
519 }
520
521 #[cfg(test)]
522 mod test {
523 use super::*;
524 use std::str::FromStr;
525 use crate::formats::*;
526
527 #[test]
528 fn test_matrices() {
529 let chcfg51 = NAChannelMap::from_str("L,R,C,LFE,Ls,Rs").unwrap();
530 let chcfg52 = NAChannelMap::from_str("C,L,R,Ls,Rs,LFE").unwrap();
531 let stereo = NAChannelMap::from_str("L,R").unwrap();
532 let reorder = calculate_reorder_matrix(&chcfg51, &chcfg52);
533 assert_eq!(reorder.as_slice(), [ 2, 0, 1, 4, 5, 3]);
534 let remix = calculate_remix_matrix(&chcfg51, &stereo);
535 assert_eq!(remix.as_slice(), [ 1.0, 0.0, SQRT_2 / 2.0, 0.0, SQRT_2 / 2.0, 0.0,
536 0.0, 1.0, SQRT_2 / 2.0, 0.0, 0.0, SQRT_2 / 2.0 ]);
537 }
538 #[test]
539 fn test_conversion() {
540 const CHANNEL_VALUES: [u8; 6] = [ 140, 90, 130, 128, 150, 70 ];
541 let chcfg51 = NAChannelMap::from_str("L,R,C,LFE,Ls,Rs").unwrap();
542 let stereo = NAChannelMap::from_str("L,R").unwrap();
543 let src_ainfo = NAAudioInfo {
544 sample_rate: 44100,
545 channels: chcfg51.num_channels() as u8,
546 format: SND_U8_FORMAT,
547 block_len: 512,
548 };
549 let mut dst_ainfo = NAAudioInfo {
550 sample_rate: 44100,
551 channels: stereo.num_channels() as u8,
552 format: SND_S16P_FORMAT,
553 block_len: 512,
554 };
555 let mut src_frm = alloc_audio_buffer(src_ainfo, 42, chcfg51.clone()).unwrap();
556 if let NABufferType::AudioPacked(ref mut abuf) = src_frm {
557 let data = abuf.get_data_mut().unwrap();
558 let mut idx = 0;
559 for _ in 0..42 {
560 for ch in 0..chcfg51.num_channels() {
561 data[idx] = CHANNEL_VALUES[ch];
562 idx += 1;
563 }
564 }
565 } else {
566 panic!("wrong buffer type");
567 }
568
569 let out_frm = convert_audio_frame(&src_frm, &dst_ainfo, &stereo).unwrap();
570 if let NABufferType::AudioI16(ref abuf) = out_frm {
571 let off0 = abuf.get_offset(0);
572 let off1 = abuf.get_offset(1);
573 let data = abuf.get_data();
574 let l = data[off0];
575 let r = data[off1];
576 assert_eq!(l, 7445);
577 assert_eq!(r, -19943);
578 } else {
579 panic!("wrong buffer type");
580 }
581
582 dst_ainfo.format = SND_F32P_FORMAT;
583 let out_frm = convert_audio_frame(&src_frm, &dst_ainfo, &stereo).unwrap();
584 if let NABufferType::AudioF32(ref abuf) = out_frm {
585 let off0 = abuf.get_offset(0);
586 let off1 = abuf.get_offset(1);
587 let data = abuf.get_data();
588 let l = data[off0];
589 let r = data[off1];
590 assert_eq!(l, 0.22633252);
591 assert_eq!(r, -0.6062342);
592 } else {
593 panic!("wrong buffer type");
594 }
595 }
596 }