Split up audio.c into three separate .c files:
[wine] / dlls / winmm / driver.c
1 /* -*- tab-width: 8; c-basic-offset: 4 -*- */
2
3 /*
4  * WINE Drivers functions
5  *
6  * Copyright 1994 Martin Ayotte
7  * Copyright 1998 Marcus Meissner
8  * Copyright 1999 Eric Pouech
9  *
10  * This library is free software; you can redistribute it and/or
11  * modify it under the terms of the GNU Lesser General Public
12  * License as published by the Free Software Foundation; either
13  * version 2.1 of the License, or (at your option) any later version.
14  *
15  * This library is distributed in the hope that it will be useful,
16  * but WITHOUT ANY WARRANTY; without even the implied warranty of
17  * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
18  * Lesser General Public License for more details.
19  *
20  * You should have received a copy of the GNU Lesser General Public
21  * License along with this library; if not, write to the Free Software
22  * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA  02111-1307  USA
23  */
24
25 #include <string.h>
26 #include <stdarg.h>
27 #include "windef.h"
28 #include "winbase.h"
29 #include "wingdi.h"
30 #include "winuser.h"
31 #include "winnls.h"
32 #include "winreg.h"
33 #include "mmddk.h"
34 #include "winemm.h"
35 #include "wine/debug.h"
36
37 WINE_DEFAULT_DEBUG_CHANNEL(driver);
38
39 #define HKLM_BASE "Software\\Microsoft\\Windows NT\\CurrentVersion"
40
41 static LPWINE_DRIVER   lpDrvItemList  /* = NULL */;
42
43 WINE_MMTHREAD*  (*pFnGetMMThread16)(UINT16 h) /* = NULL */;
44 LPWINE_DRIVER   (*pFnOpenDriver16)(LPCSTR,LPCSTR,LPARAM) /* = NULL */;
45 LRESULT         (*pFnCloseDriver16)(UINT16,LPARAM,LPARAM) /* = NULL */;
46 LRESULT         (*pFnSendMessage16)(UINT16,UINT,LPARAM,LPARAM) /* = NULL */;
47
48 /**************************************************************************
49  *                      DRIVER_GetNumberOfModuleRefs            [internal]
50  *
51  * Returns the number of open drivers which share the same module.
52  */
53 static  unsigned DRIVER_GetNumberOfModuleRefs(HMODULE hModule, WINE_DRIVER** found)
54 {
55     LPWINE_DRIVER       lpDrv;
56     unsigned            count = 0;
57
58     if (found) *found = NULL;
59     for (lpDrv = lpDrvItemList; lpDrv; lpDrv = lpDrv->lpNextItem)
60     {
61         if (!(lpDrv->dwFlags & WINE_GDF_16BIT) && lpDrv->d.d32.hModule == hModule)
62         {
63             if (found && !*found) *found = lpDrv;
64             count++;
65         }
66     }
67     return count;
68 }
69
70 /**************************************************************************
71  *                              DRIVER_FindFromHDrvr            [internal]
72  *
73  * From a hDrvr being 32 bits, returns the WINE internal structure.
74  */
75 LPWINE_DRIVER   DRIVER_FindFromHDrvr(HDRVR hDrvr)
76 {
77     LPWINE_DRIVER       d = (LPWINE_DRIVER)hDrvr;
78
79     if (hDrvr && HeapValidate(GetProcessHeap(), 0, d) && d->dwMagic == WINE_DI_MAGIC) {
80         return d;
81     }
82     return NULL;
83 }
84
85 /**************************************************************************
86  *                              DRIVER_SendMessage              [internal]
87  */
88 static LRESULT inline DRIVER_SendMessage(LPWINE_DRIVER lpDrv, UINT msg,
89                                          LPARAM lParam1, LPARAM lParam2)
90 {
91     LRESULT             ret = 0;
92
93     if (lpDrv->dwFlags & WINE_GDF_16BIT) {
94         /* no need to check mmsystem presence: the driver must have been opened as a 16 bit one,
95          */
96         if (pFnSendMessage16)
97             ret = pFnSendMessage16(lpDrv->d.d16.hDriver16, msg, lParam1, lParam2);
98     } else {
99         TRACE("Before call32 proc=%p drvrID=%08lx hDrv=%p wMsg=%04x p1=%08lx p2=%08lx\n", 
100               lpDrv->d.d32.lpDrvProc, lpDrv->d.d32.dwDriverID, (HDRVR)lpDrv, msg, lParam1, lParam2);
101         ret = lpDrv->d.d32.lpDrvProc(lpDrv->d.d32.dwDriverID, (HDRVR)lpDrv, msg, lParam1, lParam2);
102         TRACE("After  call32 proc=%p drvrID=%08lx hDrv=%p wMsg=%04x p1=%08lx p2=%08lx => %08lx\n", 
103               lpDrv->d.d32.lpDrvProc, lpDrv->d.d32.dwDriverID, (HDRVR)lpDrv, msg, lParam1, lParam2, ret);
104     }
105     return ret;
106 }
107
108 /**************************************************************************
109  *                              SendDriverMessage               [WINMM.@]
110  *                              DrvSendMessage                  [WINMM.@]
111  */
112 LRESULT WINAPI SendDriverMessage(HDRVR hDriver, UINT msg, LPARAM lParam1,
113                                  LPARAM lParam2)
114 {
115     LPWINE_DRIVER       lpDrv;
116     LRESULT             retval = 0;
117
118     TRACE("(%p, %04X, %08lX, %08lX)\n", hDriver, msg, lParam1, lParam2);
119
120     if ((lpDrv = DRIVER_FindFromHDrvr(hDriver)) != NULL) {
121         retval = DRIVER_SendMessage(lpDrv, msg, lParam1, lParam2);
122     } else {
123         WARN("Bad driver handle %p\n", hDriver);
124     }
125     TRACE("retval = %ld\n", retval);
126
127     return retval;
128 }
129
130 /**************************************************************************
131  *                              DRIVER_RemoveFromList           [internal]
132  *
133  * Generates all the logic to handle driver closure / deletion
134  * Removes a driver struct to the list of open drivers.
135  */
136 static  BOOL    DRIVER_RemoveFromList(LPWINE_DRIVER lpDrv)
137 {
138     if (!(lpDrv->dwFlags & WINE_GDF_16BIT)) {
139         /* last of this driver in list ? */
140         if (DRIVER_GetNumberOfModuleRefs(lpDrv->d.d32.hModule, NULL) == 1) {
141             DRIVER_SendMessage(lpDrv, DRV_DISABLE, 0L, 0L);
142             DRIVER_SendMessage(lpDrv, DRV_FREE,    0L, 0L);
143         }
144     }
145
146     if (lpDrv->lpPrevItem)
147         lpDrv->lpPrevItem->lpNextItem = lpDrv->lpNextItem;
148     else
149         lpDrvItemList = lpDrv->lpNextItem;
150     if (lpDrv->lpNextItem)
151         lpDrv->lpNextItem->lpPrevItem = lpDrv->lpPrevItem;
152     /* trash magic number */
153     lpDrv->dwMagic ^= 0xa5a5a5a5;
154
155     return TRUE;
156 }
157
158 /**************************************************************************
159  *                              DRIVER_AddToList                [internal]
160  *
161  * Adds a driver struct to the list of open drivers.
162  * Generates all the logic to handle driver creation / open.
163  */
164 static  BOOL    DRIVER_AddToList(LPWINE_DRIVER lpNewDrv, LPARAM lParam1, LPARAM lParam2)
165 {
166     lpNewDrv->dwMagic = WINE_DI_MAGIC;
167     /* First driver to be loaded for this module, need to load correctly the module */
168     if (!(lpNewDrv->dwFlags & WINE_GDF_16BIT)) {
169         /* first of this driver in list ? */
170         if (DRIVER_GetNumberOfModuleRefs(lpNewDrv->d.d32.hModule, NULL) == 0) {
171             if (DRIVER_SendMessage(lpNewDrv, DRV_LOAD, 0L, 0L) != DRV_SUCCESS) {
172                 TRACE("DRV_LOAD failed on driver 0x%08lx\n", (DWORD)lpNewDrv);
173                 return FALSE;
174             }
175             /* returned value is not checked */
176             DRIVER_SendMessage(lpNewDrv, DRV_ENABLE, 0L, 0L);
177         }
178     }
179
180     lpNewDrv->lpNextItem = NULL;
181     if (lpDrvItemList == NULL) {
182         lpDrvItemList = lpNewDrv;
183         lpNewDrv->lpPrevItem = NULL;
184     } else {
185         LPWINE_DRIVER   lpDrv = lpDrvItemList;  /* find end of list */
186         while (lpDrv->lpNextItem != NULL)
187             lpDrv = lpDrv->lpNextItem;
188
189         lpDrv->lpNextItem = lpNewDrv;
190         lpNewDrv->lpPrevItem = lpDrv;
191     }
192
193     if (!(lpNewDrv->dwFlags & WINE_GDF_16BIT)) {
194         /* Now just open a new instance of a driver on this module */
195         lpNewDrv->d.d32.dwDriverID = DRIVER_SendMessage(lpNewDrv, DRV_OPEN, lParam1, lParam2);
196
197         if (lpNewDrv->d.d32.dwDriverID == 0) {
198             TRACE("DRV_OPEN failed on driver 0x%08lx\n", (DWORD)lpNewDrv);
199             DRIVER_RemoveFromList(lpNewDrv);
200             return FALSE;
201         }
202     }
203     return TRUE;
204 }
205
206 /**************************************************************************
207  *                              DRIVER_GetLibName               [internal]
208  *
209  */
210 BOOL    DRIVER_GetLibName(LPCSTR keyName, LPCSTR sectName, LPSTR buf, int sz)
211 {
212     HKEY        hKey, hSecKey;
213     DWORD       bufLen, lRet;
214
215     lRet = RegOpenKeyExA(HKEY_LOCAL_MACHINE, HKLM_BASE, 0, KEY_QUERY_VALUE, &hKey);
216     if (lRet == ERROR_SUCCESS) {
217         lRet = RegOpenKeyExA(hKey, sectName, 0, KEY_QUERY_VALUE, &hSecKey);
218         if (lRet == ERROR_SUCCESS) {
219             lRet = RegQueryValueExA(hSecKey, keyName, 0, 0, buf, &bufLen);
220             RegCloseKey( hSecKey );
221         }
222         RegCloseKey( hKey );
223     }
224     if (lRet == ERROR_SUCCESS) return TRUE;
225     /* default to system.ini if we can't find it in the registry,
226      * to support native installations where system.ini is still used */
227     return GetPrivateProfileStringA(sectName, keyName, "", buf, sz, "SYSTEM.INI");
228 }
229
230 /**************************************************************************
231  *                              DRIVER_TryOpenDriver32          [internal]
232  *
233  * Tries to load a 32 bit driver whose DLL's (module) name is fn
234  */
235 LPWINE_DRIVER   DRIVER_TryOpenDriver32(LPCSTR fn, LPARAM lParam2)
236 {
237     LPWINE_DRIVER       lpDrv = NULL;
238     HMODULE             hModule = 0;
239     LPSTR               ptr;
240     LPCSTR              cause = 0;
241
242     TRACE("(%s, %08lX);\n", debugstr_a(fn), lParam2);
243
244     if ((ptr = strchr(fn, ' ')) != NULL) {
245         *ptr++ = '\0';
246         while (*ptr == ' ') ptr++;
247         if (*ptr == '\0') ptr = NULL;
248     }
249
250     lpDrv = HeapAlloc(GetProcessHeap(), 0, sizeof(WINE_DRIVER));
251     if (lpDrv == NULL) {cause = "OOM"; goto exit;}
252
253     if ((hModule = LoadLibraryA(fn)) == 0) {cause = "Not a 32 bit lib"; goto exit;}
254
255     lpDrv->d.d32.lpDrvProc = (DRIVERPROC)GetProcAddress(hModule, "DriverProc");
256     if (lpDrv->d.d32.lpDrvProc == NULL) {cause = "no DriverProc"; goto exit;}
257
258     lpDrv->dwFlags          = 0;
259     lpDrv->d.d32.hModule    = hModule;
260     lpDrv->d.d32.dwDriverID = 0;
261
262     /* Win32 installable drivers must support a two phase opening scheme:
263      * + first open with NULL as lParam2 (session instance),
264      * + then do a second open with the real non null lParam2)
265      */
266     if (DRIVER_GetNumberOfModuleRefs(lpDrv->d.d32.hModule, NULL) == 0 && lParam2)
267     {
268         LPWINE_DRIVER   ret;
269
270         if (!DRIVER_AddToList(lpDrv, (LPARAM)ptr, 0L))
271         {
272             cause = "load0 failed";
273             goto exit;
274         }
275         ret = DRIVER_TryOpenDriver32(fn, lParam2);
276         if (!ret)
277         {
278             CloseDriver((HDRVR)lpDrv, 0L, 0L);
279             cause = "load1 failed";
280             goto exit;
281         }
282         return ret;
283     }
284
285     if (!DRIVER_AddToList(lpDrv, (LPARAM)ptr, lParam2))
286     {cause = "load failed"; goto exit;}
287
288     TRACE("=> %p\n", lpDrv);
289     return lpDrv;
290  exit:
291     FreeLibrary(hModule);
292     HeapFree(GetProcessHeap(), 0, lpDrv);
293     TRACE("Unable to load 32 bit module %s: %s\n", debugstr_a(fn), cause);
294     return NULL;
295 }
296
297 /**************************************************************************
298  *                              OpenDriverA                     [WINMM.@]
299  *                              DrvOpenA                        [WINMM.@]
300  * (0,1,DRV_LOAD  ,0       ,0)
301  * (0,1,DRV_ENABLE,0       ,0)
302  * (0,1,DRV_OPEN  ,buf[256],0)
303  */
304 HDRVR WINAPI OpenDriverA(LPCSTR lpDriverName, LPCSTR lpSectionName, LPARAM lParam2)
305 {
306     LPWINE_DRIVER       lpDrv = NULL;
307     char                libName[128];
308     LPCSTR              lsn = lpSectionName;
309
310     TRACE("(%s, %s, 0x%08lx);\n", debugstr_a(lpDriverName), debugstr_a(lpSectionName), lParam2);
311
312     if (lsn == NULL) {
313         lstrcpynA(libName, lpDriverName, sizeof(libName));
314
315         if ((lpDrv = DRIVER_TryOpenDriver32(libName, lParam2)))
316             goto the_end;
317         lsn = "Drivers32";
318     }
319     if (DRIVER_GetLibName(lpDriverName, lsn, libName, sizeof(libName)) &&
320         (lpDrv = DRIVER_TryOpenDriver32(libName, lParam2)))
321         goto the_end;
322
323     /* now we will try a 16 bit driver (and add all the glue to make it work... which
324      * is located in our mmsystem implementation)
325      * so ensure, we can load our mmsystem, otherwise just fail
326      */
327     WINMM_CheckForMMSystem();
328     if (pFnOpenDriver16 &&
329         (lpDrv = pFnOpenDriver16(lpDriverName, lpSectionName, lParam2)))
330     {
331         if (DRIVER_AddToList(lpDrv, 0, lParam2)) goto the_end;
332         HeapFree(GetProcessHeap(), 0, lpDrv);
333     }
334     TRACE("Failed to open driver %s from system.ini file, section %s\n", debugstr_a(lpDriverName), debugstr_a(lpSectionName));
335     return 0;
336
337  the_end:
338     if (lpDrv)  TRACE("=> %08lx\n", (DWORD)lpDrv);
339     return (HDRVR)lpDrv;
340 }
341
342 /**************************************************************************
343  *                              OpenDriver                      [WINMM.@]
344  *                              DrvOpen                         [WINMM.@]
345  */
346 HDRVR WINAPI OpenDriverW(LPCWSTR lpDriverName, LPCWSTR lpSectionName, LPARAM lParam)
347 {
348     INT                 len;
349     LPSTR               dn = NULL;
350     LPSTR               sn = NULL;
351     HDRVR               ret;
352
353     if (lpDriverName)
354     {
355         len = WideCharToMultiByte( CP_ACP, 0, lpDriverName, -1, NULL, 0, NULL, NULL );
356         dn = HeapAlloc( GetProcessHeap(), 0, len );
357         if (!dn) return 0;
358         WideCharToMultiByte( CP_ACP, 0, lpDriverName, -1, dn, len, NULL, NULL );
359     }
360
361     if (lpSectionName)
362     {
363         len = WideCharToMultiByte( CP_ACP, 0, lpSectionName, -1, NULL, 0, NULL, NULL );
364         sn = HeapAlloc( GetProcessHeap(), 0, len );
365         if (!sn) return 0;
366         WideCharToMultiByte( CP_ACP, 0, lpSectionName, -1, sn, len, NULL, NULL );
367     }
368
369     ret = OpenDriverA(dn, sn, lParam);
370
371     if (dn) HeapFree(GetProcessHeap(), 0, dn);
372     if (sn) HeapFree(GetProcessHeap(), 0, sn);
373     return ret;
374 }
375
376 /**************************************************************************
377  *                      CloseDriver                             [WINMM.@]
378  *                      DrvClose                                [WINMM.@]
379  */
380 LRESULT WINAPI CloseDriver(HDRVR hDrvr, LPARAM lParam1, LPARAM lParam2)
381 {
382     LPWINE_DRIVER       lpDrv;
383
384     TRACE("(%p, %08lX, %08lX);\n", hDrvr, lParam1, lParam2);
385
386     if ((lpDrv = DRIVER_FindFromHDrvr(hDrvr)) != NULL)
387     {
388         if (lpDrv->dwFlags & WINE_GDF_16BIT)
389         {
390             if (pFnCloseDriver16)
391                 pFnCloseDriver16(lpDrv->d.d16.hDriver16, lParam1, lParam2);
392         }
393         else
394         {
395             DRIVER_SendMessage(lpDrv, DRV_CLOSE, lParam1, lParam2);
396             lpDrv->d.d32.dwDriverID = 0;
397         }
398         if (DRIVER_RemoveFromList(lpDrv)) {
399             if (!(lpDrv->dwFlags & WINE_GDF_16BIT))
400             {
401                 LPWINE_DRIVER       lpDrv0;
402
403                 /* if driver has an opened session instance, we have to close it too */
404                 if (DRIVER_GetNumberOfModuleRefs(lpDrv->d.d32.hModule, &lpDrv0) == 1)
405                 {
406                     DRIVER_SendMessage(lpDrv0, DRV_CLOSE, 0L, 0L);
407                     lpDrv0->d.d32.dwDriverID = 0;
408                     DRIVER_RemoveFromList(lpDrv0);
409                     FreeLibrary(lpDrv0->d.d32.hModule);
410                     HeapFree(GetProcessHeap(), 0, lpDrv0);
411                 }
412                 FreeLibrary(lpDrv->d.d32.hModule);
413             }
414             HeapFree(GetProcessHeap(), 0, lpDrv);
415             return TRUE;
416         }
417     }
418     WARN("Failed to close driver\n");
419     return FALSE;
420 }
421
422 /**************************************************************************
423  *                              GetDriverFlags          [WINMM.@]
424  * [in] hDrvr handle to the driver
425  *
426  * Returns:
427  *      0x00000000 if hDrvr is an invalid handle
428  *      0x80000000 if hDrvr is a valid 32 bit driver
429  *      0x90000000 if hDrvr is a valid 16 bit driver
430  *
431  * native WINMM doesn't return those flags
432  *      0x80000000 for a valid 32 bit driver and that's it
433  *      (I may have mixed up the two flags :-(
434  */
435 DWORD   WINAPI GetDriverFlags(HDRVR hDrvr)
436 {
437     LPWINE_DRIVER       lpDrv;
438     DWORD               ret = 0;
439
440     TRACE("(%p)\n", hDrvr);
441
442     if ((lpDrv = DRIVER_FindFromHDrvr(hDrvr)) != NULL) {
443         ret = WINE_GDF_EXIST | lpDrv->dwFlags;
444     }
445     return ret;
446 }
447
448 /**************************************************************************
449  *                              GetDriverModuleHandle   [WINMM.@]
450  *                              DrvGetModuleHandle      [WINMM.@]
451  */
452 HMODULE WINAPI GetDriverModuleHandle(HDRVR hDrvr)
453 {
454     LPWINE_DRIVER       lpDrv;
455     HMODULE             hModule = 0;
456
457     TRACE("(%p);\n", hDrvr);
458
459     if ((lpDrv = DRIVER_FindFromHDrvr(hDrvr)) != NULL) {
460         if (!(lpDrv->dwFlags & WINE_GDF_16BIT))
461             hModule = lpDrv->d.d32.hModule;
462     }
463     TRACE("=> %p\n", hModule);
464     return hModule;
465 }
466
467 /**************************************************************************
468  *                              DefDriverProc                     [WINMM.@]
469  *                              DrvDefDriverProc                  [WINMM.@]
470  */
471 LRESULT WINAPI DefDriverProc(DWORD_PTR dwDriverIdentifier, HDRVR hDrv,
472                              UINT Msg, LPARAM lParam1, LPARAM lParam2)
473 {
474     switch (Msg) {
475     case DRV_LOAD:
476     case DRV_FREE:
477     case DRV_ENABLE:
478     case DRV_DISABLE:
479         return 1;
480     case DRV_INSTALL:
481     case DRV_REMOVE:
482         return DRV_SUCCESS;
483     default:
484         return 0;
485     }
486 }
487
488 /**************************************************************************
489  *                              DriverCallback                  [WINMM.@]
490  */
491 BOOL WINAPI DriverCallback(DWORD dwCallBack, UINT uFlags, HDRVR hDev,
492                            UINT wMsg, DWORD dwUser, DWORD dwParam1,
493                            DWORD dwParam2)
494 {
495     TRACE("(%08lX, %04X, %p, %04X, %08lX, %08lX, %08lX); !\n",
496           dwCallBack, uFlags, hDev, wMsg, dwUser, dwParam1, dwParam2);
497
498     switch (uFlags & DCB_TYPEMASK) {
499     case DCB_NULL:
500         TRACE("Null !\n");
501         if (dwCallBack)
502             WARN("uFlags=%04X has null DCB value, but dwCallBack=%08lX is not null !\n", uFlags, dwCallBack);
503         break;
504     case DCB_WINDOW:
505         TRACE("Window(%04lX) handle=%p!\n", dwCallBack, hDev);
506         PostMessageA((HWND)dwCallBack, wMsg, (WPARAM)hDev, dwParam1);
507         break;
508     case DCB_TASK: /* aka DCB_THREAD */
509         TRACE("Task(%04lx) !\n", dwCallBack);
510         PostThreadMessageA(dwCallBack, wMsg, (WPARAM)hDev, dwParam1);
511         break;
512     case DCB_FUNCTION:
513         TRACE("Function (32 bit) !\n");
514         ((LPDRVCALLBACK)dwCallBack)(hDev, wMsg, dwUser, dwParam1, dwParam2);
515         break;
516     case DCB_EVENT:
517         TRACE("Event(%08lx) !\n", dwCallBack);
518         SetEvent((HANDLE)dwCallBack);
519         break;
520     case 6: /* I would dub it DCB_MMTHREADSIGNAL */
521         /* this is an undocumented DCB_ value used for mmThreads
522          * loword of dwCallBack contains the handle of the lpMMThd block
523          * which dwSignalCount has to be incremented
524          */     
525         if (pFnGetMMThread16)
526         {
527             WINE_MMTHREAD*      lpMMThd = pFnGetMMThread16(LOWORD(dwCallBack));
528
529             TRACE("mmThread (%04x, %p) !\n", LOWORD(dwCallBack), lpMMThd);
530             /* same as mmThreadSignal16 */
531             InterlockedIncrement(&lpMMThd->dwSignalCount);
532             SetEvent(lpMMThd->hEvent);
533             /* some other stuff on lpMMThd->hVxD */
534         }
535         break;
536 #if 0
537     case 4:
538         /* this is an undocumented DCB_ value for... I don't know */
539         break;
540 #endif
541     default:
542         WARN("Unknown callback type %d\n", uFlags & DCB_TYPEMASK);
543         return FALSE;
544     }
545     TRACE("Done\n");
546     return TRUE;
547 }
548
549 /******************************************************************
550  *              DRIVER_UnloadAll
551  *
552  *
553  */
554 void    DRIVER_UnloadAll(void)
555 {
556     LPWINE_DRIVER       lpDrv;
557     LPWINE_DRIVER       lpNextDrv = NULL;
558     unsigned            count = 0;
559
560     for (lpDrv = lpDrvItemList; lpDrv != NULL; lpDrv = lpNextDrv)
561     {
562         lpNextDrv = lpDrv->lpNextItem;
563         CloseDriver((HDRVR)lpDrv, 0, 0);
564         count++;
565     }
566     TRACE("Unloaded %u drivers\n", count);
567 }