1 /* FAudio - XAudio Reimplementation for FNA
2  *
3  * Copyright (c) 2011-2021 Ethan Lee, Luigi Auriemma, and the MonoGame Team
4  *
5  * This software is provided 'as-is', without any express or implied warranty.
6  * In no event will the authors be held liable for any damages arising from
7  * the use of this software.
8  *
9  * Permission is granted to anyone to use this software for any purpose,
10  * including commercial applications, and to alter it and redistribute it
11  * freely, subject to the following restrictions:
12  *
13  * 1. The origin of this software must not be misrepresented; you must not
14  * claim that you wrote the original software. If you use this software in a
15  * product, an acknowledgment in the product documentation would be
16  * appreciated but is not required.
17  *
18  * 2. Altered source versions must be plainly marked as such, and must not be
19  * misrepresented as being the original software.
20  *
21  * 3. This notice may not be removed or altered from any source distribution.
22  *
23  * Ethan "flibitijibibo" Lee <flibitijibibo@flibitijibibo.com>
24  *
25  */
26 
27 #include "FAudio_internal.h"
28 
29 #include <SDL.h>
30 
31 #if !SDL_VERSION_ATLEAST(2, 0, 9)
32 #error "SDL version older than 2.0.9"
33 #endif /* !SDL_VERSION_ATLEAST */
34 
35 /* Mixer Thread */
36 
FAudio_INTERNAL_MixCallback(void * userdata,Uint8 * stream,int len)37 static void FAudio_INTERNAL_MixCallback(void *userdata, Uint8 *stream, int len)
38 {
39 	FAudio *audio = (FAudio*) userdata;
40 
41 	FAudio_zero(stream, len);
42 	if (audio->active)
43 	{
44 		FAudio_INTERNAL_UpdateEngine(
45 			audio,
46 			(float*) stream
47 		);
48 	}
49 }
50 
51 /* Platform Functions */
52 
FAudio_PlatformAddRef()53 void FAudio_PlatformAddRef()
54 {
55 	/* SDL tracks ref counts for each subsystem */
56 	if (SDL_InitSubSystem(SDL_INIT_AUDIO) < 0)
57 	{
58 		SDL_Log("SDL_INIT_AUDIO failed: %s", SDL_GetError());
59 	}
60 	FAudio_INTERNAL_InitSIMDFunctions(
61 		SDL_HasSSE2(),
62 		SDL_HasNEON()
63 	);
64 }
65 
FAudio_PlatformRelease()66 void FAudio_PlatformRelease()
67 {
68 	/* SDL tracks ref counts for each subsystem */
69 	SDL_QuitSubSystem(SDL_INIT_AUDIO);
70 }
71 
FAudio_PlatformInit(FAudio * audio,uint32_t flags,uint32_t deviceIndex,FAudioWaveFormatExtensible * mixFormat,uint32_t * updateSize,void ** platformDevice)72 void FAudio_PlatformInit(
73 	FAudio *audio,
74 	uint32_t flags,
75 	uint32_t deviceIndex,
76 	FAudioWaveFormatExtensible *mixFormat,
77 	uint32_t *updateSize,
78 	void** platformDevice
79 ) {
80 	SDL_AudioDeviceID device;
81 	SDL_AudioSpec want, have;
82 	const char *driver;
83 	int changes = 0;
84 
85 	FAudio_assert(mixFormat != NULL);
86 	FAudio_assert(updateSize != NULL);
87 
88 	/* Build the device spec */
89 	want.freq = mixFormat->Format.nSamplesPerSec;
90 	want.format = AUDIO_F32;
91 	want.channels = mixFormat->Format.nChannels;
92 	want.silence = 0;
93 	want.callback = FAudio_INTERNAL_MixCallback;
94 	want.userdata = audio;
95 	if (flags & FAUDIO_1024_QUANTUM)
96 	{
97 		/* Get the sample count for a 21.33ms frame.
98 		 * For 48KHz this should be 1024.
99 		 */
100 		want.samples = (int) (
101 			want.freq / (1000.0 / (64.0 / 3.0))
102 		);
103 	}
104 	else
105 	{
106 		want.samples = want.freq / 100;
107 	}
108 
109 	/* FIXME: SDL bug!
110 	 * The PulseAudio backend does this annoying thing where it halves the
111 	 * buffer size to prevent latency issues:
112 	 *
113 	 * https://hg.libsdl.org/SDL/file/df343364c6c5/src/audio/pulseaudio/SDL_pulseaudio.c#l577
114 	 *
115 	 * To get the _actual_ quantum size we want, we just double the buffer
116 	 * size and allow SDL to set the quantum size back to normal.
117 	 * -flibit
118 	 */
119 	driver = SDL_GetCurrentAudioDriver();
120 	if (SDL_strcmp(driver, "pulseaudio") == 0)
121 	{
122 		want.samples *= 2;
123 		changes = SDL_AUDIO_ALLOW_SAMPLES_CHANGE;
124 	}
125 
126 	/* FIXME: SDL bug!
127 	 * The most common backends support varying samples values, but many
128 	 * require a power-of-two value, which XAudio2 is not a fan of.
129 	 * Normally SDL creates an intermediary stream to handle this, but this
130 	 * has not been written yet:
131 	 * https://bugzilla.libsdl.org/show_bug.cgi?id=5136
132 	 * -flibit
133 	 */
134 	else if (	SDL_strcmp(driver, "emscripten") == 0 ||
135 			SDL_strcmp(driver, "dsp") == 0	)
136 	{
137 		want.samples -= 1;
138 		want.samples |= want.samples >> 1;
139 		want.samples |= want.samples >> 2;
140 		want.samples |= want.samples >> 4;
141 		want.samples |= want.samples >> 8;
142 		want.samples |= want.samples >> 16;
143 		want.samples += 1;
144 		SDL_Log(
145 			"Forcing FAudio quantum to a power-of-two.\n"
146 			"You don't actually want this, it's technically a bug:\n"
147 			"https://bugzilla.libsdl.org/show_bug.cgi?id=5136"
148 		);
149 	}
150 
151 	/* SDL audio with arts/dsp backends expect ^2, so round up.
152 	 * For 48KHz this should be 512.
153 	 * https://graphics.stanford.edu/~seander/bithacks.html#RoundUpPowerOf2
154         */
155 	want.samples--;
156 	want.samples |= want.samples >> 1;
157 	want.samples |= want.samples >> 2;
158 	want.samples |= want.samples >> 4;
159 	want.samples |= want.samples >> 8;
160 	want.samples |= want.samples >> 16;
161 	want.samples++;
162 
163 	/* Open the device (or at least try to) */
164 iosretry:
165 	device = SDL_OpenAudioDevice(
166 		deviceIndex > 0 ? SDL_GetAudioDeviceName(deviceIndex - 1, 0) : NULL,
167 		0,
168 		&want,
169 		&have,
170 		changes
171 	);
172 	if (device == 0)
173 	{
174 		const char *err = SDL_GetError();
175 		SDL_Log("OpenAudioDevice failed: %s", err);
176 
177 		/* iOS has a weird thing where you can't open a stream when the
178 		 * app is in the background, even though the program is meant
179 		 * to be suspended and thus not trip this in the first place.
180 		 *
181 		 * Startup suspend behavior when an app is opened then closed
182 		 * is a big pile of crap, basically.
183 		 *
184 		 * Google the error code and you'll find that this has been a
185 		 * long-standing issue that nobody seems to care about.
186 		 * -flibit
187 		 */
188 		if (SDL_strstr(err, "Code=561015905") != NULL)
189 		{
190 			goto iosretry;
191 		}
192 
193 		FAudio_assert(0 && "Failed to open audio device!");
194 		return;
195 	}
196 
197 	/* Write up the received format for the engine */
198 	WriteWaveFormatExtensible(
199 		mixFormat,
200 		have.channels,
201 		have.freq,
202 		&DATAFORMAT_SUBTYPE_IEEE_FLOAT
203 	);
204 	*updateSize = have.samples;
205 
206 	/* SDL_AudioDeviceID is a Uint32, anybody using a 16-bit PC still? */
207 	*platformDevice = (void*) ((size_t) device);
208 
209 	/* Start the thread! */
210 	SDL_PauseAudioDevice(device, 0);
211 }
212 
FAudio_PlatformQuit(void * platformDevice)213 void FAudio_PlatformQuit(void* platformDevice)
214 {
215 	SDL_CloseAudioDevice((SDL_AudioDeviceID) ((size_t) platformDevice));
216 }
217 
FAudio_PlatformGetDeviceCount()218 uint32_t FAudio_PlatformGetDeviceCount()
219 {
220 	uint32_t devCount = SDL_GetNumAudioDevices(0);
221 	if (devCount == 0)
222 	{
223 		return 0;
224 	}
225 	return devCount + 1; /* Add one for "Default Device" */
226 }
227 
228 void FAudio_UTF8_To_UTF16(const char *src, uint16_t *dst, size_t len);
229 
FAudio_PlatformGetDeviceDetails(uint32_t index,FAudioDeviceDetails * details)230 uint32_t FAudio_PlatformGetDeviceDetails(
231 	uint32_t index,
232 	FAudioDeviceDetails *details
233 ) {
234 	const char *name, *envvar;
235 	int channels, rate;
236 
237 	FAudio_zero(details, sizeof(FAudioDeviceDetails));
238 	if (index >= FAudio_PlatformGetDeviceCount())
239 	{
240 		return FAUDIO_E_INVALID_CALL;
241 	}
242 
243 	details->DeviceID[0] = L'0' + index;
244 	if (index == 0)
245 	{
246 		name = "Default Device";
247 		details->Role = FAudioGlobalDefaultDevice;
248 
249 		/* This variable will look like a DSound GUID or WASAPI ID, i.e.
250 		 * "{0.0.0.00000000}.{FD47D9CC-4218-4135-9CE2-0C195C87405B}"
251 		 */
252 		envvar = SDL_getenv("FAUDIO_FORCE_DEFAULT_DEVICEID");
253 		if (envvar != NULL)
254 		{
255 			FAudio_UTF8_To_UTF16(
256 				envvar,
257 				(uint16_t*) details->DeviceID,
258 				sizeof(details->DeviceID)
259 			);
260 		}
261 	}
262 	else
263 	{
264 		name = SDL_GetAudioDeviceName(index - 1, 0);
265 		details->Role = FAudioNotDefaultDevice;
266 	}
267 	FAudio_UTF8_To_UTF16(
268 		name,
269 		(uint16_t*) details->DisplayName,
270 		sizeof(details->DisplayName)
271 	);
272 
273 	/* TODO: SDL_GetAudioDeviceSpec! */
274 	envvar = SDL_getenv("SDL_AUDIO_FREQUENCY");
275 	if (!envvar || ((rate = SDL_atoi(envvar)) == 0))
276 	{
277 		rate = 48000;
278 	}
279 	envvar = SDL_getenv("SDL_AUDIO_CHANNELS");
280 	if (!envvar || ((channels = SDL_atoi(envvar)) == 0))
281 	{
282 		channels = 2;
283 	}
284 	WriteWaveFormatExtensible(
285 		&details->OutputFormat,
286 		channels,
287 		rate,
288 		&DATAFORMAT_SUBTYPE_PCM
289 	);
290 	return 0;
291 }
292 
293 /* Threading */
294 
FAudio_PlatformCreateThread(FAudioThreadFunc func,const char * name,void * data)295 FAudioThread FAudio_PlatformCreateThread(
296 	FAudioThreadFunc func,
297 	const char *name,
298 	void* data
299 ) {
300 	return (FAudioThread) SDL_CreateThread(
301 		(SDL_ThreadFunction) func,
302 		name,
303 		data
304 	);
305 }
306 
FAudio_PlatformWaitThread(FAudioThread thread,int32_t * retval)307 void FAudio_PlatformWaitThread(FAudioThread thread, int32_t *retval)
308 {
309 	SDL_WaitThread((SDL_Thread*) thread, retval);
310 }
311 
FAudio_PlatformThreadPriority(FAudioThreadPriority priority)312 void FAudio_PlatformThreadPriority(FAudioThreadPriority priority)
313 {
314 	SDL_SetThreadPriority((SDL_ThreadPriority) priority);
315 }
316 
FAudio_PlatformGetThreadID(void)317 uint64_t FAudio_PlatformGetThreadID(void)
318 {
319 	return (uint64_t) SDL_ThreadID();
320 }
321 
FAudio_PlatformCreateMutex()322 FAudioMutex FAudio_PlatformCreateMutex()
323 {
324 	return (FAudioMutex) SDL_CreateMutex();
325 }
326 
FAudio_PlatformDestroyMutex(FAudioMutex mutex)327 void FAudio_PlatformDestroyMutex(FAudioMutex mutex)
328 {
329 	SDL_DestroyMutex((SDL_mutex*) mutex);
330 }
331 
FAudio_PlatformLockMutex(FAudioMutex mutex)332 void FAudio_PlatformLockMutex(FAudioMutex mutex)
333 {
334 	SDL_LockMutex((SDL_mutex*) mutex);
335 }
336 
FAudio_PlatformUnlockMutex(FAudioMutex mutex)337 void FAudio_PlatformUnlockMutex(FAudioMutex mutex)
338 {
339 	SDL_UnlockMutex((SDL_mutex*) mutex);
340 }
341 
FAudio_sleep(uint32_t ms)342 void FAudio_sleep(uint32_t ms)
343 {
344 	SDL_Delay(ms);
345 }
346 
347 /* Time */
348 
FAudio_timems()349 uint32_t FAudio_timems()
350 {
351 	return SDL_GetTicks();
352 }
353 
354 /* FAudio I/O */
355 
FAudio_fopen(const char * path)356 FAudioIOStream* FAudio_fopen(const char *path)
357 {
358 	FAudioIOStream *io = (FAudioIOStream*) FAudio_malloc(
359 		sizeof(FAudioIOStream)
360 	);
361 	SDL_RWops *rwops = SDL_RWFromFile(path, "rb");
362 	io->data = rwops;
363 	io->read = (FAudio_readfunc) rwops->read;
364 	io->seek = (FAudio_seekfunc) rwops->seek;
365 	io->close = (FAudio_closefunc) rwops->close;
366 	io->lock = FAudio_PlatformCreateMutex();
367 	return io;
368 }
369 
FAudio_memopen(void * mem,int len)370 FAudioIOStream* FAudio_memopen(void *mem, int len)
371 {
372 	FAudioIOStream *io = (FAudioIOStream*) FAudio_malloc(
373 		sizeof(FAudioIOStream)
374 	);
375 	SDL_RWops *rwops = SDL_RWFromMem(mem, len);
376 	io->data = rwops;
377 	io->read = (FAudio_readfunc) rwops->read;
378 	io->seek = (FAudio_seekfunc) rwops->seek;
379 	io->close = (FAudio_closefunc) rwops->close;
380 	io->lock = FAudio_PlatformCreateMutex();
381 	return io;
382 }
383 
FAudio_memptr(FAudioIOStream * io,size_t offset)384 uint8_t* FAudio_memptr(FAudioIOStream *io, size_t offset)
385 {
386 	SDL_RWops *rwops = (SDL_RWops*) io->data;
387 	FAudio_assert(rwops->type == SDL_RWOPS_MEMORY);
388 	return rwops->hidden.mem.base + offset;
389 }
390 
FAudio_close(FAudioIOStream * io)391 void FAudio_close(FAudioIOStream *io)
392 {
393 	io->close(io->data);
394 	FAudio_PlatformDestroyMutex((FAudioMutex) io->lock);
395 	FAudio_free(io);
396 }
397 
398 #ifdef FAUDIO_DUMP_VOICES
FAudio_fopen_out(const char * path,const char * mode)399 FAudioIOStreamOut* FAudio_fopen_out(const char *path, const char *mode)
400 {
401 	FAudioIOStreamOut *io = (FAudioIOStreamOut*) FAudio_malloc(
402 		sizeof(FAudioIOStreamOut)
403 	);
404 	SDL_RWops *rwops = SDL_RWFromFile(path, mode);
405 	io->data = rwops;
406 	io->read = (FAudio_readfunc) rwops->read;
407 	io->write = (FAudio_writefunc) rwops->write;
408 	io->seek = (FAudio_seekfunc) rwops->seek;
409 	io->size = (FAudio_sizefunc) rwops->size;
410 	io->close = (FAudio_closefunc) rwops->close;
411 	io->lock = FAudio_PlatformCreateMutex();
412 	return io;
413 }
414 
FAudio_close_out(FAudioIOStreamOut * io)415 void FAudio_close_out(FAudioIOStreamOut *io)
416 {
417 	io->close(io->data);
418 	FAudio_PlatformDestroyMutex((FAudioMutex) io->lock);
419 	FAudio_free(io);
420 }
421 #endif /* FAUDIO_DUMP_VOICES */
422 
423 /* UTF8->UTF16 Conversion, taken from PhysicsFS */
424 
425 #define UNICODE_BOGUS_CHAR_VALUE 0xFFFFFFFF
426 #define UNICODE_BOGUS_CHAR_CODEPOINT '?'
427 
FAudio_UTF8_CodePoint(const char ** _str)428 static uint32_t FAudio_UTF8_CodePoint(const char **_str)
429 {
430     const char *str = *_str;
431     uint32_t retval = 0;
432     uint32_t octet = (uint32_t) ((uint8_t) *str);
433     uint32_t octet2, octet3, octet4;
434 
435     if (octet == 0)  /* null terminator, end of string. */
436         return 0;
437 
438     else if (octet < 128)  /* one octet char: 0 to 127 */
439     {
440         (*_str)++;  /* skip to next possible start of codepoint. */
441         return octet;
442     } /* else if */
443 
444     else if ((octet > 127) && (octet < 192))  /* bad (starts with 10xxxxxx). */
445     {
446         /*
447          * Apparently each of these is supposed to be flagged as a bogus
448          *  char, instead of just resyncing to the next valid codepoint.
449          */
450         (*_str)++;  /* skip to next possible start of codepoint. */
451         return UNICODE_BOGUS_CHAR_VALUE;
452     } /* else if */
453 
454     else if (octet < 224)  /* two octets */
455     {
456         (*_str)++;  /* advance at least one byte in case of an error */
457         octet -= (128+64);
458         octet2 = (uint32_t) ((uint8_t) *(++str));
459         if ((octet2 & (128+64)) != 128)  /* Format isn't 10xxxxxx? */
460             return UNICODE_BOGUS_CHAR_VALUE;
461 
462         *_str += 1;  /* skip to next possible start of codepoint. */
463         retval = ((octet << 6) | (octet2 - 128));
464         if ((retval >= 0x80) && (retval <= 0x7FF))
465             return retval;
466     } /* else if */
467 
468     else if (octet < 240)  /* three octets */
469     {
470         (*_str)++;  /* advance at least one byte in case of an error */
471         octet -= (128+64+32);
472         octet2 = (uint32_t) ((uint8_t) *(++str));
473         if ((octet2 & (128+64)) != 128)  /* Format isn't 10xxxxxx? */
474             return UNICODE_BOGUS_CHAR_VALUE;
475 
476         octet3 = (uint32_t) ((uint8_t) *(++str));
477         if ((octet3 & (128+64)) != 128)  /* Format isn't 10xxxxxx? */
478             return UNICODE_BOGUS_CHAR_VALUE;
479 
480         *_str += 2;  /* skip to next possible start of codepoint. */
481         retval = ( ((octet << 12)) | ((octet2-128) << 6) | ((octet3-128)) );
482 
483         /* There are seven "UTF-16 surrogates" that are illegal in UTF-8. */
484         switch (retval)
485         {
486             case 0xD800:
487             case 0xDB7F:
488             case 0xDB80:
489             case 0xDBFF:
490             case 0xDC00:
491             case 0xDF80:
492             case 0xDFFF:
493                 return UNICODE_BOGUS_CHAR_VALUE;
494         } /* switch */
495 
496         /* 0xFFFE and 0xFFFF are illegal, too, so we check them at the edge. */
497         if ((retval >= 0x800) && (retval <= 0xFFFD))
498             return retval;
499     } /* else if */
500 
501     else if (octet < 248)  /* four octets */
502     {
503         (*_str)++;  /* advance at least one byte in case of an error */
504         octet -= (128+64+32+16);
505         octet2 = (uint32_t) ((uint8_t) *(++str));
506         if ((octet2 & (128+64)) != 128)  /* Format isn't 10xxxxxx? */
507             return UNICODE_BOGUS_CHAR_VALUE;
508 
509         octet3 = (uint32_t) ((uint8_t) *(++str));
510         if ((octet3 & (128+64)) != 128)  /* Format isn't 10xxxxxx? */
511             return UNICODE_BOGUS_CHAR_VALUE;
512 
513         octet4 = (uint32_t) ((uint8_t) *(++str));
514         if ((octet4 & (128+64)) != 128)  /* Format isn't 10xxxxxx? */
515             return UNICODE_BOGUS_CHAR_VALUE;
516 
517         *_str += 3;  /* skip to next possible start of codepoint. */
518         retval = ( ((octet << 18)) | ((octet2 - 128) << 12) |
519                    ((octet3 - 128) << 6) | ((octet4 - 128)) );
520         if ((retval >= 0x10000) && (retval <= 0x10FFFF))
521             return retval;
522     } /* else if */
523 
524     /*
525      * Five and six octet sequences became illegal in rfc3629.
526      *  We throw the codepoint away, but parse them to make sure we move
527      *  ahead the right number of bytes and don't overflow the buffer.
528      */
529 
530     else if (octet < 252)  /* five octets */
531     {
532         (*_str)++;  /* advance at least one byte in case of an error */
533         octet = (uint32_t) ((uint8_t) *(++str));
534         if ((octet & (128+64)) != 128)  /* Format isn't 10xxxxxx? */
535             return UNICODE_BOGUS_CHAR_VALUE;
536 
537         octet = (uint32_t) ((uint8_t) *(++str));
538         if ((octet & (128+64)) != 128)  /* Format isn't 10xxxxxx? */
539             return UNICODE_BOGUS_CHAR_VALUE;
540 
541         octet = (uint32_t) ((uint8_t) *(++str));
542         if ((octet & (128+64)) != 128)  /* Format isn't 10xxxxxx? */
543             return UNICODE_BOGUS_CHAR_VALUE;
544 
545         octet = (uint32_t) ((uint8_t) *(++str));
546         if ((octet & (128+64)) != 128)  /* Format isn't 10xxxxxx? */
547             return UNICODE_BOGUS_CHAR_VALUE;
548 
549         *_str += 4;  /* skip to next possible start of codepoint. */
550         return UNICODE_BOGUS_CHAR_VALUE;
551     } /* else if */
552 
553     else  /* six octets */
554     {
555         (*_str)++;  /* advance at least one byte in case of an error */
556         octet = (uint32_t) ((uint8_t) *(++str));
557         if ((octet & (128+64)) != 128)  /* Format isn't 10xxxxxx? */
558             return UNICODE_BOGUS_CHAR_VALUE;
559 
560         octet = (uint32_t) ((uint8_t) *(++str));
561         if ((octet & (128+64)) != 128)  /* Format isn't 10xxxxxx? */
562             return UNICODE_BOGUS_CHAR_VALUE;
563 
564         octet = (uint32_t) ((uint8_t) *(++str));
565         if ((octet & (128+64)) != 128)  /* Format isn't 10xxxxxx? */
566             return UNICODE_BOGUS_CHAR_VALUE;
567 
568         octet = (uint32_t) ((uint8_t) *(++str));
569         if ((octet & (128+64)) != 128)  /* Format isn't 10xxxxxx? */
570             return UNICODE_BOGUS_CHAR_VALUE;
571 
572         octet = (uint32_t) ((uint8_t) *(++str));
573         if ((octet & (128+64)) != 128)  /* Format isn't 10xxxxxx? */
574             return UNICODE_BOGUS_CHAR_VALUE;
575 
576         *_str += 6;  /* skip to next possible start of codepoint. */
577         return UNICODE_BOGUS_CHAR_VALUE;
578     } /* else if */
579 
580     return UNICODE_BOGUS_CHAR_VALUE;
581 }
582 
FAudio_UTF8_To_UTF16(const char * src,uint16_t * dst,size_t len)583 void FAudio_UTF8_To_UTF16(const char *src, uint16_t *dst, size_t len)
584 {
585     len -= sizeof (uint16_t);   /* save room for null char. */
586     while (len >= sizeof (uint16_t))
587     {
588         uint32_t cp = FAudio_UTF8_CodePoint(&src);
589         if (cp == 0)
590             break;
591         else if (cp == UNICODE_BOGUS_CHAR_VALUE)
592             cp = UNICODE_BOGUS_CHAR_CODEPOINT;
593 
594         if (cp > 0xFFFF)  /* encode as surrogate pair */
595         {
596             if (len < (sizeof (uint16_t) * 2))
597                 break;  /* not enough room for the pair, stop now. */
598 
599             cp -= 0x10000;  /* Make this a 20-bit value */
600 
601             *(dst++) = 0xD800 + ((cp >> 10) & 0x3FF);
602             len -= sizeof (uint16_t);
603 
604             cp = 0xDC00 + (cp & 0x3FF);
605         } /* if */
606 
607         *(dst++) = cp;
608         len -= sizeof (uint16_t);
609     } /* while */
610 
611     *dst = 0;
612 }
613 
614 /* vim: set noexpandtab shiftwidth=8 tabstop=8: */
615