1 /*-------------- Telecommunications & Signal Processing Lab ---------------
2
3 Routine:
4 AFILE *AFrdWVhead (FILE *fp)
5
6 Purpose:
7 Get file format information from a WAVE file
8
9 Description:
10 This routine reads the header for a WAVE file. The header information
11 is used to set the file data format information in the audio file pointer
12 structure.
13
14 WAVE file:
15 Offset Length Type Contents
16 0 4 char "RIFF" file identifier
17 4 4 int Chunk length
18 8 4 char "WAVE" file identifier
19 ... ... ... ...
20 F 4 char "fmt " chunk identifier
21 +4 4 int Chunk length
22 +8 2 int Audio data type
23 +10 2 int Number of interleaved channels
24 +12 4 int Sample rate
25 +16 4 int Average bytes/sec
26 +20 2 int Block align
27 +22 2 int Data word length (bits)
28 +24 2 int Extra data count (for non-PCM or extensible format)
29 +26 2 int Valid bits per sample
30 +28 4 int Channel mask
31 +32 16 int Sub-format type
32 ... ... ...
33 C 4 char "fact" chunk identifier (for non-PCM)
34 +4 4 int Chunk length
35 +8 4 int Number of samples (per channel)
36 ... ... ... ...
37 D 4 char "data" chunk identifier
38 +4 4 int Chunk length
39 +8 ... ... Audio data
40
41 Parameters:
42 <- AFILE *AFrdWVhead
43 Audio file pointer for the audio file
44 -> FILE *fp
45 File pointer for the file
46
47 Author / revision:
48 P. Kabal Copyright (C) 2004
49 $Revision: 1.90 $ $Date: 2004/03/29 01:49:41 $
50
51 -------------------------------------------------------------------------*/
52
53 #include <assert.h>
54 #include <setjmp.h>
55 #include <string.h>
56
57 #include <libtsp.h>
58 #include <libtsp/AFdataio.h>
59 #include <libtsp/AFheader.h>
60 #include <libtsp/AFmsg.h>
61 #define AF_SPKR_MASKS
62 #include <libtsp/AFpar.h>
63 #define WV_CHANNEL_MAP
64 #define WAVEFORMATEX_SUBTYPE
65 #include <libtsp/WVpar.h>
66 #include <libtsp/nucleus.h>
67
68 #define ICEILV(n,m) (((n) + ((m) - 1)) / (m)) /* int n,m >= 0 */
69 #define NELEM(array) ((int) ((sizeof array) / (sizeof array[0])))
70 #define RNDUPV(n,m) ((m) * ICEILV (n, m)) /* Round up */
71
72 #define SAME_CSTR(str,ref) (memcmp (str, ref, sizeof (str)) == 0)
73
74 #define ALIGN 2 /* Chunks padded out to a multiple of ALIGN */
75
76 #define WV_FMT_MINSIZE 16
77 #define WV_LHMIN (8 + 4 + 8 + WV_FMT_MINSIZE + 8)
78
79 /* setjmp / longjmp environment */
80 extern jmp_buf AFR_JMPENV;
81
82 AF_READ_DEFAULT(AFr_default); /* Define the AF_read defaults */
83
84 static int
85 AF_decFMT (const struct WV_CKfmt *CKfmt, struct AF_read *AFr);
86 static int
87 AF_rdDISP_text (FILE *fp, int Size, struct AF_infoX *InfoX);
88 static int
89 AF_rdFMT (FILE *fp, struct WV_CKfmt *CKfmt);
90 static int
91 AF_rdFACT (FILE *fp, struct WV_CKfact *CKfact);
92 static int
93 AF_rdLIST_INFO (FILE *fp, int Size, struct AF_infoX *InfoX);
94 static int
95 AF_rdRIFF_WAVE (FILE *fp, struct WV_CKRIFF *CKRIFF);
96 static void
97 AF_UnsFormat (int FormatTag);
98 static void
99 AF_decChannelConfig (uint4_t ChannelMask, unsigned char *SpkrConfig);
100
101
102 AFILE *
AFrdWVhead(FILE * fp)103 AFrdWVhead (FILE *fp)
104
105 {
106 AFILE *AFp;
107 int AtData, FoundAFsp;
108 long int offs, LRIFF, Fact_Nsamp, Dstart, EoD;
109 char Info[AF_MAXINFO];
110 struct WV_CKRIFF CKRIFF;
111 struct WV_CKpreamb CkHead;
112 struct AF_read AFr;
113
114 /* Set the long jump environment; on error return a NULL */
115 if (setjmp (AFR_JMPENV))
116 return NULL; /* Return from a header read error */
117
118 /* Defaults and inital values */
119 AFr = AFr_default;
120 AFr.InfoX.Info = Info;
121 AFr.InfoX.Nmax = AF_MAXINFO;
122
123 Fact_Nsamp = AF_NSAMP_UNDEF;
124
125 /* Check the file magic for a RIFF/WAVE file */
126 if (AF_rdRIFF_WAVE (fp, &CKRIFF))
127 return NULL;
128 offs = 12L; /* Positioned after RIFF/WAVE preamble */
129 LRIFF = CKRIFF.ckSize + 8;
130
131 Dstart = 0L;
132 EoD = 0L;
133 AtData = 0;
134 FoundAFsp = 0;
135 while (offs < LRIFF-8) { /* Leave 8 bytes for the chunk preamble */
136
137 /* Read the chunk preamble */
138 offs += RHEAD_S (fp, CkHead.ckID);
139
140 /* fmt chunk */
141 if (SAME_CSTR (CkHead.ckID, "fmt ")) {
142 offs += AF_rdFMT (fp, &CKRIFF.CKfmt);
143 if (AF_decFMT (&CKRIFF.CKfmt, &AFr))
144 return NULL;
145 }
146
147 /* fact chunk */
148 else if (SAME_CSTR (CkHead.ckID, "fact")) {
149 offs += AF_rdFACT (fp, &CKRIFF.CKfact);
150 Fact_Nsamp = CKRIFF.CKfact.dwSampleLength;
151 }
152
153 /* data chunk */
154 else if (SAME_CSTR (CkHead.ckID, "data")) {
155 offs += RHEAD_V (fp, CKRIFF.CKdata.ckSize, DS_EL);
156 AFr.NData.Ldata = CKRIFF.CKdata.ckSize;
157 Dstart = offs;
158 EoD = RNDUPV (Dstart + AFr.NData.Ldata, ALIGN);
159 if (EoD >= LRIFF || ! FLseekable (fp)) {
160 AtData = 1;
161 break;
162 }
163 else {
164 AtData = 0;
165 offs += RSKIP (fp, EoD - Dstart);
166 }
167 }
168
169 /* Text chunks */
170 else if (SAME_CSTR (CkHead.ckID, "afsp")) {
171 offs += RHEAD_V (fp, CkHead.ckSize, DS_EL);
172 offs += AFrdTextAFsp (fp, (int) CkHead.ckSize, "AFsp: ", &AFr.InfoX,
173 ALIGN);
174 FoundAFsp = 1;
175 }
176 else if (SAME_CSTR (CkHead.ckID, "DISP") && ! FoundAFsp) {
177 offs += RHEAD_V (fp, CkHead.ckSize, DS_EL);
178 offs += AF_rdDISP_text (fp, (int) CkHead.ckSize, &AFr.InfoX);
179 }
180 else if (SAME_CSTR (CkHead.ckID, "LIST") && ! FoundAFsp) {
181 offs += RHEAD_V (fp, CkHead.ckSize, DS_EL);
182 offs += AF_rdLIST_INFO (fp, (int) CkHead.ckSize, &AFr.InfoX);
183 }
184 /* Miscellaneous chunks */
185 else {
186 offs += RHEAD_V (fp, CkHead.ckSize, DS_EL);
187 offs += RSKIP (fp, RNDUPV (CkHead.ckSize, ALIGN));
188 }
189 }
190 /* Error Checks */
191 /* Check that we found a fmt and a data chunk */
192 if (AFr.DFormat.Format == FD_UNDEF || AFr.NData.Ldata == AF_LDATA_UNDEF) {
193 UTwarn ("AFrdWVhead - %s", AFM_WV_BadHead);
194 return NULL;
195 }
196 if ((! AtData && offs != LRIFF) || (AtData && EoD != LRIFF))
197 UTwarn ("AFrdWVhead - %s", AFM_WV_BadSize);
198
199 /* Use the number of samples from the fact chunk only for non-PCM
200 or WAVE-EX files. Some PCM files use the fact chunk, but it does
201 NOT specify the number of samples */
202 if (CKRIFF.CKfmt.wFormatTag != WAVE_FORMAT_PCM
203 && Fact_Nsamp != AF_NSAMP_UNDEF)
204 AFr.NData.Nsamp = AFr.NData.Nchan * Fact_Nsamp;
205
206 /* Position at the start of data */
207 if (! AtData) {
208 if (AFseek (fp, Dstart, NULL))
209 return NULL;
210 }
211
212 /* Set the parameters for file access */
213 AFp = AFsetRead (fp, FT_WAVE, &AFr, AF_FIX_NSAMP_HIGH + AF_FIX_LDATA_HIGH);
214
215 return AFp;
216 }
217
218 /* Check the RIFF/WAVE file preamble */
219
220
221 static int
AF_rdRIFF_WAVE(FILE * fp,struct WV_CKRIFF * CKRIFF)222 AF_rdRIFF_WAVE (FILE *fp, struct WV_CKRIFF *CKRIFF)
223
224 {
225 long int Lfile, LRIFF;
226
227 RHEAD_S (fp, CKRIFF->ckID);
228 if (! SAME_CSTR (CKRIFF->ckID, "RIFF")) {
229 UTwarn ("AFrdWVhead - %s", AFM_WV_BadId);
230 return 1;
231 }
232
233 RHEAD_V (fp, CKRIFF->ckSize, DS_EL);
234 LRIFF = CKRIFF->ckSize + 8;
235 if (LRIFF < WV_LHMIN) {
236 UTwarn ("AFrdWVhead - %s", AFM_WV_BadRIFF);
237 return 1;
238 }
239
240 if (FLseekable (fp)) {
241 Lfile = FLfileSize (fp);
242 if (LRIFF > Lfile) {
243 CKRIFF->ckSize = Lfile - 8;
244 UTwarn ("AFrdWVhead - %s", AFM_WV_FixRIFF);
245 }
246 }
247
248 RHEAD_S (fp, CKRIFF->WAVEID);
249 if (! SAME_CSTR (CKRIFF->WAVEID, "WAVE")) {
250 UTwarn ("AFrdWVhead - %s", AFM_WV_BadId);
251 return 1;
252 }
253
254 return 0;
255 }
256
257 /* Read the fmt chunk, starting at ckSize */
258
259
260 static int
AF_rdFMT(FILE * fp,struct WV_CKfmt * CKfmt)261 AF_rdFMT (FILE *fp, struct WV_CKfmt *CKfmt)
262
263 {
264 int offs, NB;
265
266 offs = RHEAD_V (fp, CKfmt->ckSize, DS_EL);
267 if (CKfmt->ckSize < WV_FMT_MINSIZE) {
268 UTwarn ("AFrdWVhead - %s", AFM_WV_BadfmtSize);
269 longjmp (AFR_JMPENV, 1);
270 }
271
272 offs += RHEAD_V (fp, CKfmt->wFormatTag, DS_EL);
273 offs += RHEAD_V (fp, CKfmt->nChannels, DS_EL);
274 offs += RHEAD_V (fp, CKfmt->nSamplesPerSec, DS_EL);
275 offs += RHEAD_V (fp, CKfmt->nAvgBytesPerSec, DS_EL);
276 offs += RHEAD_V (fp, CKfmt->nBlockAlign, DS_EL);
277 offs += RHEAD_V (fp, CKfmt->wBitsPerSample, DS_EL);
278
279 NB = ((int) CKfmt->ckSize + 4) - offs;
280 if (NB >= 24) {
281 offs += RHEAD_V (fp, CKfmt->cbSize, DS_EL);
282 if (CKfmt->cbSize >= 22) {
283 offs += RHEAD_V (fp, CKfmt->wValidBitsPerSample, DS_EL);
284 offs += RHEAD_V (fp, CKfmt->dwChannelMask, DS_EL);
285 offs += RHEAD_V (fp, CKfmt->SubFormat.wFormatTag, DS_EL);
286 offs += RHEAD_S (fp, CKfmt->SubFormat.guidx);
287 }
288 else
289 CKfmt->cbSize = 0;
290 }
291
292 /* Skip over any extra data at the end of the fmt chunk */
293 offs += RSKIP (fp, RNDUPV (CKfmt->ckSize + 4, ALIGN) - offs);
294
295 return offs;
296 }
297
298 /* Decode the data format */
299
300
301 static int
AF_decFMT(const struct WV_CKfmt * CKfmt,struct AF_read * AFr)302 AF_decFMT (const struct WV_CKfmt *CKfmt, struct AF_read *AFr)
303
304 {
305 uint2_t FormatTag;
306 int NBytesS;
307
308 /*
309 Ordinary WAVE (not EXTENSIBLE format) files
310 - The data format is over-specified
311 - The number of bits per sample is wBitsPerSample.
312 - The number of bytes per sample is NByteS = nBlockAlign/nChannels.
313 - A reasonable approach is to get the container size from the block size
314 and the data size from wBitsPerSample.
315 - The CoolEdit float format specifies float data (flagged as 24 bits per
316 sample, in a 4-byte container)
317 - In calculating the container size, it is expected that nBlockAlign be a
318 multiple of the number of channels. Note, however, that some compressed
319 schemes set nBlockAlign to 1.
320 WAVE EXTENSIBLE files
321 - wBitsPerSample now explicitly is the container size in bits and hence
322 should agree with 8 * NByteS, and nBlockAlign must be a multiple of
323 nChannels.
324 */
325 if (CKfmt->nChannels == 0)
326 NBytesS = 0;
327 else
328 NBytesS = (int) (CKfmt->nBlockAlign / CKfmt->nChannels);
329
330 if (CKfmt->wFormatTag == WAVE_FORMAT_EXTENSIBLE) {
331 if (CKfmt->cbSize < 22 ||
332 ! IS_VALID_WAVEFORMATEX_GUID (CKfmt->SubFormat)) {
333 UTwarn ("AFrdWVhead - %s", AFM_WV_BadfmtEx);
334 return 1;
335 }
336 /* Extensible file: container size must match wBitsPerSample */
337 AFr->DFormat.NbS = 8 * NBytesS;
338 if (CKfmt->wBitsPerSample != 8 * NBytesS) {
339 UTwarn ("AFrdWVhead - %s: %d <-> %d", AFM_WV_BadSS,
340 (int) CKfmt->wBitsPerSample, 8 * NBytesS);
341 }
342 AFr->DFormat.NbS = (int) CKfmt->wValidBitsPerSample;
343 FormatTag = CKfmt->SubFormat.wFormatTag;
344 AF_decChannelConfig (CKfmt->dwChannelMask, AFr->NData.SpkrConfig);
345 }
346 else {
347 AFr->DFormat.NbS = (int) CKfmt->wBitsPerSample;
348 FormatTag = CKfmt->wFormatTag;
349 }
350
351 switch (FormatTag) {
352 case WAVE_FORMAT_PCM:
353
354 if (NBytesS == FDL_INT16)
355 AFr->DFormat.Format = FD_INT16;
356 /* Special case: IEEE float from CoolEdit (samples are flagged as 24-bit
357 integer, but are 32-bit IEEE float) */
358 else if (CKfmt->wFormatTag == WAVE_FORMAT_PCM &&
359 AFr->DFormat.NbS == 24 && NBytesS == 4) {
360 AFr->DFormat.Format = FD_FLOAT32;
361 AFr->DFormat.NbS = 8 * FDL_FLOAT32;
362 AFr->DFormat.ScaleF = AF_SF_INT24; /* Same scaling as INT24 */
363 if (! UTcheckIEEE ())
364 UTwarn ("AFrdAIhead - %s", AFM_NoIEEE);
365 }
366 else if (NBytesS == FDL_INT24)
367 AFr->DFormat.Format = FD_INT24;
368 else if (NBytesS == FDL_INT32)
369 AFr->DFormat.Format = FD_INT32;
370 else if (NBytesS == FDL_UINT8)
371 AFr->DFormat.Format = FD_UINT8;
372 else {
373 UTwarn ("AFrdWVhead - %s: \"%d\"", AFM_WV_UnsDSize, 8 * NBytesS);
374 return 1;
375 }
376 break;
377
378 case WAVE_FORMAT_MULAW:
379 if (NBytesS != 1) {
380 UTwarn ("AFrdWVhead - %s: \"%d\"", AFM_WV_BadMulaw, 8 * NBytesS);
381 return 1;
382 }
383 if (AFr->DFormat.NbS != 8) {
384 UTwarn ("AFrdWVhead - %s: \"%d\"", AFM_WV_BadMulaw, AFr->DFormat.NbS);
385 AFr->DFormat.NbS = 8;
386 }
387 AFr->DFormat.Format = FD_MULAW8;
388 break;
389
390 case WAVE_FORMAT_ALAW:
391 if (NBytesS != 1) {
392 UTwarn ("AFrdWVhead - %s: \"%d\"", AFM_WV_BadAlaw, 8 * NBytesS);
393 return 1;
394 }
395 if (AFr->DFormat.NbS != 8) {
396 UTwarn ("AFrdWVhead - %s: \"%d\"", AFM_WV_BadAlaw, AFr->DFormat.NbS);
397 AFr->DFormat.NbS = 8;
398 }
399 AFr->DFormat.Format = FD_ALAW8;
400 break;
401
402 case WAVE_FORMAT_IEEE_FLOAT:
403 if (NBytesS == FDL_FLOAT32)
404 AFr->DFormat.Format = FD_FLOAT32;
405 else if (NBytesS == FDL_FLOAT64)
406 AFr->DFormat.Format = FD_FLOAT64;
407 else
408 UTwarn ("AFrdWVhead - %s: \"%d\"", AFM_WV_BadFloat, 8 * NBytesS);
409 if (! UTcheckIEEE ())
410 UTwarn ("AFrdWVhead - %s", AFM_NoIEEE);
411 break;
412
413 /* Unsupported data formats */
414 default:
415 AF_UnsFormat ((int) CKfmt->wFormatTag);
416 return 1;
417 }
418
419 AFr->NData.Nchan = (long int) CKfmt->nChannels;
420
421 /* Error checks */
422 if (AFr->NData.Nchan <= 0) {
423 UTwarn ("AFrdWVhead - %s : %d", AFM_WV_BadNChan, CKfmt->nChannels);
424 return 1;
425 }
426 if ((CKfmt->nBlockAlign % CKfmt->nChannels) != 0) {
427 UTwarn ("AFrdWVhead - %s: %d", AFM_WV_BadBlock, CKfmt->nBlockAlign);
428 return 1;
429 }
430 if (AFr->DFormat.NbS > 8 * NBytesS) {
431 UTwarn ("AFrdWVhead - %s: %d", AFM_WV_BadNbS, AFr->DFormat.NbS);
432 AFr->DFormat.NbS = 8 * NBytesS;
433 }
434
435 /* Consistency check */
436 if (CKfmt->nAvgBytesPerSec != CKfmt->nChannels * CKfmt->nSamplesPerSec *
437 NBytesS)
438 UTwarn ("AFrdWVhead - %s", AFM_WV_BadBytesSec);
439
440 AFr->DFormat.Swapb = DS_EL;
441 AFr->Sfreq = (double) CKfmt->nSamplesPerSec;
442
443 return 0;
444 }
445
446 /* Read the fact chunk */
447
448 static int
AF_rdFACT(FILE * fp,struct WV_CKfact * CKfact)449 AF_rdFACT (FILE *fp, struct WV_CKfact *CKfact)
450
451 {
452 int offs;
453
454 offs = RHEAD_V (fp, CKfact->ckSize, DS_EL);
455 if (CKfact->ckSize < 4) {
456 UTwarn ("AFrdWVhead - %s", AFM_WV_BadFACT);
457 longjmp (AFR_JMPENV, 1);
458 }
459 offs += RHEAD_V (fp, CKfact->dwSampleLength, DS_EL);
460 offs += RSKIP (fp, RNDUPV (CKfact->ckSize + 4, ALIGN) - offs);
461
462 return offs;
463 }
464
465 /* Unsupported data format */
466
467 static void
AF_UnsFormat(int FormatTag)468 AF_UnsFormat (int FormatTag)
469
470 {
471 switch (FormatTag) {
472 case WAVE_FORMAT_ADPCM:
473 UTwarn ("AFrdWVhead - %s: \"%s\"", AFM_WV_UnsData, "MS ADPCM");
474 break;
475 case WAVE_FORMAT_MPEGLAYER3:
476 UTwarn ("AFrdWVhead - %s: \"%s\"", AFM_WV_UnsData, "MPEG-1 Layer 3");
477 break;
478 case WAVE_FORMAT_MSG723:
479 UTwarn ("AFrdWVhead - %s: \"%s\"", AFM_WV_UnsData, "G.723.1 coding");
480 break;
481 default:
482 UTwarn ("AFrdWVhead - %s: \"%d\"", AFM_WV_UnsData, FormatTag);
483 break;
484 }
485 }
486
487 /* Read a DISP chunk */
488
489
490 static int
AF_rdDISP_text(FILE * fp,int Size,struct AF_infoX * InfoX)491 AF_rdDISP_text (FILE *fp, int Size, struct AF_infoX *InfoX)
492
493 {
494 int offs;
495 uint4_t DISP_ID;
496
497 offs = RHEAD_V (fp, DISP_ID, DS_EL);
498 if (DISP_ID == CF_TEXT)
499 offs += AFrdTextAFsp (fp, Size - offs, "display_name: ", InfoX, ALIGN);
500 else
501 offs += RSKIP (fp, RNDUPV (Size, ALIGN) - offs);
502
503 return offs;
504 }
505
506 /* Read the LIST-INFO records from the header */
507
508
509 struct WV_LI{
510 char ckid[5];
511 char *key;
512 };
513 static const struct WV_LI IID[] = {
514 {"IARL", "archival_location: "},
515 {"IART", "artist: "},
516 {"ICMS", "commissioned: "},
517 {"ICMT", "comments: "},
518 {"ICOP", "copyright: "},
519 {"ICRD", "creation_date: "},
520 {"IENG", "engineer: "},
521 {"IGNR", "genre: "},
522 {"IKEY", "keywords: "},
523 {"IMED", "medium: "},
524 {"INAM", "name: "},
525 {"IPRD", "product: "},
526 {"ISBJ", "subject: "},
527 {"ISFT", "software: "},
528 {"ISRC", "source: "},
529 {"ISRF", "source_form: "},
530 {"ITCH", "technician: "}
531 };
532
533 #define NIID NELEM (IID)
534
535
536 static int
AF_rdLIST_INFO(FILE * fp,int Size,struct AF_infoX * InfoX)537 AF_rdLIST_INFO (FILE *fp, int Size, struct AF_infoX *InfoX)
538
539 {
540 int i, offs;
541 char ID[4], key[7];
542 struct WV_CKpreamb CkHead;
543
544 offs = RHEAD_S (fp, ID);
545 if (SAME_CSTR (ID, "INFO")) {
546
547 while (offs < Size) {
548 offs += RHEAD_S (fp, CkHead.ckID);
549 offs += RHEAD_V (fp, CkHead.ckSize, DS_EL);
550
551 /* Look for standard INFO ID values */
552 for (i = 0; i < (int) NIID; ++i) {
553 if (SAME_CSTR (CkHead.ckID, IID[i].ckid)) {
554 offs += AFrdTextAFsp (fp, (int) CkHead.ckSize, IID[i].key,
555 InfoX, ALIGN);
556 break;
557 }
558 }
559
560 /* No match, use the INFO ID field as the information record keyword */
561 if (i == NIID) {
562 strncpy (key, CkHead.ckID, 4);
563 strcpy (&key[4], ": ");
564 offs += AFrdTextAFsp (fp, (int) CkHead.ckSize, key, InfoX, ALIGN);
565 }
566 }
567 }
568 offs += RSKIP (fp, RNDUPV (Size, ALIGN) - offs);
569
570 return offs;
571 }
572
573 /* Decode channel/speaker information */
574
575
576 static void
AF_decChannelConfig(uint4_t ChannelMask,unsigned char * SpkrConfig)577 AF_decChannelConfig (uint4_t ChannelMask, unsigned char *SpkrConfig)
578
579 {
580 int i, k;
581
582 assert (AF_N_SPKR_NAMES - 1 == WV_N_CHANNEL_MAP);
583
584 k = 0;
585 if (ChannelMask & WV_SPEAKER_ALL)
586 ;
587 else if (ChannelMask & ~WV_SPEAKER_KNOWN)
588 UTwarn ("AFrdWVhead - %s", AFM_WV_UnkChannel);
589 else if (ChannelMask != 0) {
590 for (i = 0, k = 0; i < WV_N_CHANNEL_MAP; ++i) {
591 if (ChannelMask & WV_ChannelMap[i]) {
592 SpkrConfig[k] = i+1;
593 ++k;
594 }
595 }
596 }
597
598 SpkrConfig[k] = '\0';
599 return;
600 }
601