|
|
/* FAudio - XAudio Reimplementation for FNA
|
|
|
*
|
|
|
* Copyright (c) 2011-2020 Ethan Lee, Luigi Auriemma, and the MonoGame Team
|
|
|
*
|
|
|
* This software is provided 'as-is', without any express or implied warranty.
|
|
|
* In no event will the authors be held liable for any damages arising from
|
|
|
* the use of this software.
|
|
|
*
|
|
|
* Permission is granted to anyone to use this software for any purpose,
|
|
|
* including commercial applications, and to alter it and redistribute it
|
|
|
* freely, subject to the following restrictions:
|
|
|
*
|
|
|
* 1. The origin of this software must not be misrepresented; you must not
|
|
|
* claim that you wrote the original software. If you use this software in a
|
|
|
* product, an acknowledgment in the product documentation would be
|
|
|
* appreciated but is not required.
|
|
|
*
|
|
|
* 2. Altered source versions must be plainly marked as such, and must not be
|
|
|
* misrepresented as being the original software.
|
|
|
*
|
|
|
* 3. This notice may not be removed or altered from any source distribution.
|
|
|
*
|
|
|
* Ethan "flibitijibibo" Lee <flibitijibibo@flibitijibibo.com>
|
|
|
*
|
|
|
*/
|
|
|
|
|
|
#include "FAudio_internal.h"
|
|
|
|
|
|
#include <SDL.h>
|
|
|
|
|
|
/* Mixer Thread */
|
|
|
|
|
|
static void FAudio_INTERNAL_MixCallback(void *userdata, Uint8 *stream, int len)
|
|
|
{
|
|
|
FAudio *audio = (FAudio*) userdata;
|
|
|
|
|
|
FAudio_zero(stream, len);
|
|
|
if (audio->active)
|
|
|
{
|
|
|
FAudio_INTERNAL_UpdateEngine(
|
|
|
audio,
|
|
|
(float*) stream
|
|
|
);
|
|
|
}
|
|
|
}
|
|
|
|
|
|
/* Platform Functions */
|
|
|
|
|
|
void FAudio_PlatformAddRef()
|
|
|
{
|
|
|
/* SDL tracks ref counts for each subsystem */
|
|
|
if (SDL_InitSubSystem(SDL_INIT_AUDIO) < 0)
|
|
|
{
|
|
|
SDL_Log("SDL_INIT_AUDIO failed: %s", SDL_GetError());
|
|
|
}
|
|
|
FAudio_INTERNAL_InitSIMDFunctions(
|
|
|
SDL_HasSSE2(),
|
|
|
SDL_HasNEON()
|
|
|
);
|
|
|
}
|
|
|
|
|
|
void FAudio_PlatformRelease()
|
|
|
{
|
|
|
/* SDL tracks ref counts for each subsystem */
|
|
|
SDL_QuitSubSystem(SDL_INIT_AUDIO);
|
|
|
}
|
|
|
|
|
|
void FAudio_PlatformInit(
|
|
|
FAudio *audio,
|
|
|
uint32_t flags,
|
|
|
uint32_t deviceIndex,
|
|
|
FAudioWaveFormatExtensible *mixFormat,
|
|
|
uint32_t *updateSize,
|
|
|
void** platformDevice
|
|
|
) {
|
|
|
SDL_AudioDeviceID device;
|
|
|
SDL_AudioSpec want, have;
|
|
|
|
|
|
FAudio_assert(mixFormat != NULL);
|
|
|
FAudio_assert(updateSize != NULL);
|
|
|
|
|
|
/* Build the device spec */
|
|
|
want.freq = mixFormat->Format.nSamplesPerSec;
|
|
|
want.format = AUDIO_F32;
|
|
|
want.channels = mixFormat->Format.nChannels;
|
|
|
want.silence = 0;
|
|
|
want.callback = FAudio_INTERNAL_MixCallback;
|
|
|
want.userdata = audio;
|
|
|
if (flags & FAUDIO_1024_QUANTUM)
|
|
|
{
|
|
|
/* Get the sample count for a 21.33ms frame.
|
|
|
* For 48KHz this should be 1024.
|
|
|
*/
|
|
|
want.samples = (int) (
|
|
|
want.freq / (1000.0 / (64.0 / 3.0))
|
|
|
);
|
|
|
}
|
|
|
else
|
|
|
{
|
|
|
want.samples = want.freq / 100;
|
|
|
}
|
|
|
|
|
|
/* Open the device (or at least try to) */
|
|
|
iosretry:
|
|
|
device = SDL_OpenAudioDevice(
|
|
|
deviceIndex > 0 ? SDL_GetAudioDeviceName(deviceIndex - 1, 0) : NULL,
|
|
|
0,
|
|
|
&want,
|
|
|
&have,
|
|
|
0
|
|
|
);
|
|
|
if (device == 0)
|
|
|
{
|
|
|
const char *err = SDL_GetError();
|
|
|
SDL_Log("OpenAudioDevice failed: %s", err);
|
|
|
|
|
|
/* iOS has a weird thing where you can't open a stream when the
|
|
|
* app is in the background, even though the program is meant
|
|
|
* to be suspended and thus not trip this in the first place.
|
|
|
*
|
|
|
* Startup suspend behavior when an app is opened then closed
|
|
|
* is a big pile of crap, basically.
|
|
|
*
|
|
|
* Google the error code and you'll find that this has been a
|
|
|
* long-standing issue that nobody seems to care about.
|
|
|
* -flibit
|
|
|
*/
|
|
|
if (SDL_strstr(err, "Code=561015905") != NULL)
|
|
|
{
|
|
|
goto iosretry;
|
|
|
}
|
|
|
|
|
|
FAudio_assert(0 && "Failed to open audio device!");
|
|
|
return;
|
|
|
}
|
|
|
|
|
|
/* Write up the received format for the engine */
|
|
|
WriteWaveFormatExtensible(mixFormat, have.channels, have.freq);
|
|
|
*updateSize = have.samples;
|
|
|
|
|
|
/* SDL_AudioDeviceID is a Uint32, anybody using a 16-bit PC still? */
|
|
|
*platformDevice = (void*) ((size_t) device);
|
|
|
|
|
|
/* Start the thread! */
|
|
|
SDL_PauseAudioDevice(device, 0);
|
|
|
}
|
|
|
|
|
|
void FAudio_PlatformQuit(void* platformDevice)
|
|
|
{
|
|
|
SDL_CloseAudioDevice((SDL_AudioDeviceID) ((size_t) platformDevice));
|
|
|
}
|
|
|
|
|
|
uint32_t FAudio_PlatformGetDeviceCount()
|
|
|
{
|
|
|
uint32_t devCount = SDL_GetNumAudioDevices(0);
|
|
|
if (devCount == 0)
|
|
|
{
|
|
|
return 0;
|
|
|
}
|
|
|
return devCount + 1; /* Add one for "Default Device" */
|
|
|
}
|
|
|
|
|
|
void FAudio_UTF8_To_UTF16(const char *src, uint16_t *dst, size_t len);
|
|
|
|
|
|
uint32_t FAudio_PlatformGetDeviceDetails(
|
|
|
uint32_t index,
|
|
|
FAudioDeviceDetails *details
|
|
|
) {
|
|
|
const char *name, *envvar;
|
|
|
int channels, rate;
|
|
|
|
|
|
FAudio_zero(details, sizeof(FAudioDeviceDetails));
|
|
|
if (index >= FAudio_PlatformGetDeviceCount())
|
|
|
{
|
|
|
return FAUDIO_E_INVALID_CALL;
|
|
|
}
|
|
|
|
|
|
details->DeviceID[0] = L'0' + index;
|
|
|
if (index == 0)
|
|
|
{
|
|
|
name = "Default Device";
|
|
|
details->Role = FAudioGlobalDefaultDevice;
|
|
|
|
|
|
/* This variable will look like a DSound GUID or WASAPI ID, i.e.
|
|
|
* "{0.0.0.00000000}.{FD47D9CC-4218-4135-9CE2-0C195C87405B}"
|
|
|
*/
|
|
|
envvar = SDL_getenv("FAUDIO_FORCE_DEFAULT_DEVICEID");
|
|
|
if (envvar != NULL)
|
|
|
{
|
|
|
FAudio_UTF8_To_UTF16(
|
|
|
envvar,
|
|
|
(uint16_t*) details->DeviceID,
|
|
|
sizeof(details->DeviceID)
|
|
|
);
|
|
|
}
|
|
|
}
|
|
|
else
|
|
|
{
|
|
|
name = SDL_GetAudioDeviceName(index - 1, 0);
|
|
|
details->Role = FAudioNotDefaultDevice;
|
|
|
}
|
|
|
FAudio_UTF8_To_UTF16(
|
|
|
name,
|
|
|
(uint16_t*) details->DisplayName,
|
|
|
sizeof(details->DisplayName)
|
|
|
);
|
|
|
|
|
|
/* TODO: SDL_GetAudioDeviceSpec! */
|
|
|
envvar = SDL_getenv("SDL_AUDIO_FREQUENCY");
|
|
|
if (!envvar || ((rate = SDL_atoi(envvar)) == 0))
|
|
|
{
|
|
|
rate = 48000;
|
|
|
}
|
|
|
envvar = SDL_getenv("SDL_AUDIO_CHANNELS");
|
|
|
if (!envvar || ((channels = SDL_atoi(envvar)) == 0))
|
|
|
{
|
|
|
channels = 2;
|
|
|
}
|
|
|
WriteWaveFormatExtensible(&details->OutputFormat, channels, rate);
|
|
|
return 0;
|
|
|
}
|
|
|
|
|
|
/* Threading */
|
|
|
|
|
|
FAudioThread FAudio_PlatformCreateThread(
|
|
|
FAudioThreadFunc func,
|
|
|
const char *name,
|
|
|
void* data
|
|
|
) {
|
|
|
return (FAudioThread) SDL_CreateThread(
|
|
|
(SDL_ThreadFunction) func,
|
|
|
name,
|
|
|
data
|
|
|
);
|
|
|
}
|
|
|
|
|
|
void FAudio_PlatformWaitThread(FAudioThread thread, int32_t *retval)
|
|
|
{
|
|
|
SDL_WaitThread((SDL_Thread*) thread, retval);
|
|
|
}
|
|
|
|
|
|
void FAudio_PlatformThreadPriority(FAudioThreadPriority priority)
|
|
|
{
|
|
|
SDL_SetThreadPriority((SDL_ThreadPriority) priority);
|
|
|
}
|
|
|
|
|
|
uint64_t FAudio_PlatformGetThreadID(void)
|
|
|
{
|
|
|
return (uint64_t) SDL_ThreadID();
|
|
|
}
|
|
|
|
|
|
FAudioMutex FAudio_PlatformCreateMutex()
|
|
|
{
|
|
|
return (FAudioMutex) SDL_CreateMutex();
|
|
|
}
|
|
|
|
|
|
void FAudio_PlatformDestroyMutex(FAudioMutex mutex)
|
|
|
{
|
|
|
SDL_DestroyMutex((SDL_mutex*) mutex);
|
|
|
}
|
|
|
|
|
|
void FAudio_PlatformLockMutex(FAudioMutex mutex)
|
|
|
{
|
|
|
SDL_LockMutex((SDL_mutex*) mutex);
|
|
|
}
|
|
|
|
|
|
void FAudio_PlatformUnlockMutex(FAudioMutex mutex)
|
|
|
{
|
|
|
SDL_UnlockMutex((SDL_mutex*) mutex);
|
|
|
}
|
|
|
|
|
|
void FAudio_sleep(uint32_t ms)
|
|
|
{
|
|
|
SDL_Delay(ms);
|
|
|
}
|
|
|
|
|
|
/* Time */
|
|
|
|
|
|
uint32_t FAudio_timems()
|
|
|
{
|
|
|
return SDL_GetTicks();
|
|
|
}
|
|
|
|
|
|
/* FAudio I/O */
|
|
|
|
|
|
FAudioIOStream* FAudio_fopen(const char *path)
|
|
|
{
|
|
|
FAudioIOStream *io = (FAudioIOStream*) FAudio_malloc(
|
|
|
sizeof(FAudioIOStream)
|
|
|
);
|
|
|
SDL_RWops *rwops = SDL_RWFromFile(path, "rb");
|
|
|
io->data = rwops;
|
|
|
io->read = (FAudio_readfunc) rwops->read;
|
|
|
io->seek = (FAudio_seekfunc) rwops->seek;
|
|
|
io->close = (FAudio_closefunc) rwops->close;
|
|
|
io->lock = FAudio_PlatformCreateMutex();
|
|
|
return io;
|
|
|
}
|
|
|
|
|
|
FAudioIOStream* FAudio_memopen(void *mem, int len)
|
|
|
{
|
|
|
FAudioIOStream *io = (FAudioIOStream*) FAudio_malloc(
|
|
|
sizeof(FAudioIOStream)
|
|
|
);
|
|
|
SDL_RWops *rwops = SDL_RWFromMem(mem, len);
|
|
|
io->data = rwops;
|
|
|
io->read = (FAudio_readfunc) rwops->read;
|
|
|
io->seek = (FAudio_seekfunc) rwops->seek;
|
|
|
io->close = (FAudio_closefunc) rwops->close;
|
|
|
io->lock = FAudio_PlatformCreateMutex();
|
|
|
return io;
|
|
|
}
|
|
|
|
|
|
uint8_t* FAudio_memptr(FAudioIOStream *io, size_t offset)
|
|
|
{
|
|
|
SDL_RWops *rwops = (SDL_RWops*) io->data;
|
|
|
FAudio_assert(rwops->type == SDL_RWOPS_MEMORY);
|
|
|
return rwops->hidden.mem.base + offset;
|
|
|
}
|
|
|
|
|
|
void FAudio_close(FAudioIOStream *io)
|
|
|
{
|
|
|
io->close(io->data);
|
|
|
FAudio_PlatformDestroyMutex((FAudioMutex) io->lock);
|
|
|
FAudio_free(io);
|
|
|
}
|
|
|
|
|
|
#ifdef FAUDIO_DUMP_VOICES
|
|
|
FAudioIOStreamOut* FAudio_fopen_out(const char *path, const char *mode)
|
|
|
{
|
|
|
FAudioIOStreamOut *io = (FAudioIOStreamOut*) FAudio_malloc(
|
|
|
sizeof(FAudioIOStreamOut)
|
|
|
);
|
|
|
SDL_RWops *rwops = SDL_RWFromFile(path, mode);
|
|
|
io->data = rwops;
|
|
|
io->read = (FAudio_readfunc) rwops->read;
|
|
|
io->write = (FAudio_writefunc) rwops->write;
|
|
|
io->seek = (FAudio_seekfunc) rwops->seek;
|
|
|
io->size = (FAudio_sizefunc) rwops->size;
|
|
|
io->close = (FAudio_closefunc) rwops->close;
|
|
|
io->lock = FAudio_PlatformCreateMutex();
|
|
|
return io;
|
|
|
}
|
|
|
|
|
|
void FAudio_close_out(FAudioIOStreamOut *io)
|
|
|
{
|
|
|
io->close(io->data);
|
|
|
FAudio_PlatformDestroyMutex((FAudioMutex) io->lock);
|
|
|
FAudio_free(io);
|
|
|
}
|
|
|
#endif /* FAUDIO_DUMP_VOICES */
|
|
|
|
|
|
/* UTF8->UTF16 Conversion, taken from PhysicsFS */
|
|
|
|
|
|
#define UNICODE_BOGUS_CHAR_VALUE 0xFFFFFFFF
|
|
|
#define UNICODE_BOGUS_CHAR_CODEPOINT '?'
|
|
|
|
|
|
static uint32_t FAudio_UTF8_CodePoint(const char **_str)
|
|
|
{
|
|
|
const char *str = *_str;
|
|
|
uint32_t retval = 0;
|
|
|
uint32_t octet = (uint32_t) ((uint8_t) *str);
|
|
|
uint32_t octet2, octet3, octet4;
|
|
|
|
|
|
if (octet == 0) /* null terminator, end of string. */
|
|
|
return 0;
|
|
|
|
|
|
else if (octet < 128) /* one octet char: 0 to 127 */
|
|
|
{
|
|
|
(*_str)++; /* skip to next possible start of codepoint. */
|
|
|
return octet;
|
|
|
} /* else if */
|
|
|
|
|
|
else if ((octet > 127) && (octet < 192)) /* bad (starts with 10xxxxxx). */
|
|
|
{
|
|
|
/*
|
|
|
* Apparently each of these is supposed to be flagged as a bogus
|
|
|
* char, instead of just resyncing to the next valid codepoint.
|
|
|
*/
|
|
|
(*_str)++; /* skip to next possible start of codepoint. */
|
|
|
return UNICODE_BOGUS_CHAR_VALUE;
|
|
|
} /* else if */
|
|
|
|
|
|
else if (octet < 224) /* two octets */
|
|
|
{
|
|
|
(*_str)++; /* advance at least one byte in case of an error */
|
|
|
octet -= (128+64);
|
|
|
octet2 = (uint32_t) ((uint8_t) *(++str));
|
|
|
if ((octet2 & (128+64)) != 128) /* Format isn't 10xxxxxx? */
|
|
|
return UNICODE_BOGUS_CHAR_VALUE;
|
|
|
|
|
|
*_str += 1; /* skip to next possible start of codepoint. */
|
|
|
retval = ((octet << 6) | (octet2 - 128));
|
|
|
if ((retval >= 0x80) && (retval <= 0x7FF))
|
|
|
return retval;
|
|
|
} /* else if */
|
|
|
|
|
|
else if (octet < 240) /* three octets */
|
|
|
{
|
|
|
(*_str)++; /* advance at least one byte in case of an error */
|
|
|
octet -= (128+64+32);
|
|
|
octet2 = (uint32_t) ((uint8_t) *(++str));
|
|
|
if ((octet2 & (128+64)) != 128) /* Format isn't 10xxxxxx? */
|
|
|
return UNICODE_BOGUS_CHAR_VALUE;
|
|
|
|
|
|
octet3 = (uint32_t) ((uint8_t) *(++str));
|
|
|
if ((octet3 & (128+64)) != 128) /* Format isn't 10xxxxxx? */
|
|
|
return UNICODE_BOGUS_CHAR_VALUE;
|
|
|
|
|
|
*_str += 2; /* skip to next possible start of codepoint. */
|
|
|
retval = ( ((octet << 12)) | ((octet2-128) << 6) | ((octet3-128)) );
|
|
|
|
|
|
/* There are seven "UTF-16 surrogates" that are illegal in UTF-8. */
|
|
|
switch (retval)
|
|
|
{
|
|
|
case 0xD800:
|
|
|
case 0xDB7F:
|
|
|
case 0xDB80:
|
|
|
case 0xDBFF:
|
|
|
case 0xDC00:
|
|
|
case 0xDF80:
|
|
|
case 0xDFFF:
|
|
|
return UNICODE_BOGUS_CHAR_VALUE;
|
|
|
} /* switch */
|
|
|
|
|
|
/* 0xFFFE and 0xFFFF are illegal, too, so we check them at the edge. */
|
|
|
if ((retval >= 0x800) && (retval <= 0xFFFD))
|
|
|
return retval;
|
|
|
} /* else if */
|
|
|
|
|
|
else if (octet < 248) /* four octets */
|
|
|
{
|
|
|
(*_str)++; /* advance at least one byte in case of an error */
|
|
|
octet -= (128+64+32+16);
|
|
|
octet2 = (uint32_t) ((uint8_t) *(++str));
|
|
|
if ((octet2 & (128+64)) != 128) /* Format isn't 10xxxxxx? */
|
|
|
return UNICODE_BOGUS_CHAR_VALUE;
|
|
|
|
|
|
octet3 = (uint32_t) ((uint8_t) *(++str));
|
|
|
if ((octet3 & (128+64)) != 128) /* Format isn't 10xxxxxx? */
|
|
|
return UNICODE_BOGUS_CHAR_VALUE;
|
|
|
|
|
|
octet4 = (uint32_t) ((uint8_t) *(++str));
|
|
|
if ((octet4 & (128+64)) != 128) /* Format isn't 10xxxxxx? */
|
|
|
return UNICODE_BOGUS_CHAR_VALUE;
|
|
|
|
|
|
*_str += 3; /* skip to next possible start of codepoint. */
|
|
|
retval = ( ((octet << 18)) | ((octet2 - 128) << 12) |
|
|
|
((octet3 - 128) << 6) | ((octet4 - 128)) );
|
|
|
if ((retval >= 0x10000) && (retval <= 0x10FFFF))
|
|
|
return retval;
|
|
|
} /* else if */
|
|
|
|
|
|
/*
|
|
|
* Five and six octet sequences became illegal in rfc3629.
|
|
|
* We throw the codepoint away, but parse them to make sure we move
|
|
|
* ahead the right number of bytes and don't overflow the buffer.
|
|
|
*/
|
|
|
|
|
|
else if (octet < 252) /* five octets */
|
|
|
{
|
|
|
(*_str)++; /* advance at least one byte in case of an error */
|
|
|
octet = (uint32_t) ((uint8_t) *(++str));
|
|
|
if ((octet & (128+64)) != 128) /* Format isn't 10xxxxxx? */
|
|
|
return UNICODE_BOGUS_CHAR_VALUE;
|
|
|
|
|
|
octet = (uint32_t) ((uint8_t) *(++str));
|
|
|
if ((octet & (128+64)) != 128) /* Format isn't 10xxxxxx? */
|
|
|
return UNICODE_BOGUS_CHAR_VALUE;
|
|
|
|
|
|
octet = (uint32_t) ((uint8_t) *(++str));
|
|
|
if ((octet & (128+64)) != 128) /* Format isn't 10xxxxxx? */
|
|
|
return UNICODE_BOGUS_CHAR_VALUE;
|
|
|
|
|
|
octet = (uint32_t) ((uint8_t) *(++str));
|
|
|
if ((octet & (128+64)) != 128) /* Format isn't 10xxxxxx? */
|
|
|
return UNICODE_BOGUS_CHAR_VALUE;
|
|
|
|
|
|
*_str += 4; /* skip to next possible start of codepoint. */
|
|
|
return UNICODE_BOGUS_CHAR_VALUE;
|
|
|
} /* else if */
|
|
|
|
|
|
else /* six octets */
|
|
|
{
|
|
|
(*_str)++; /* advance at least one byte in case of an error */
|
|
|
octet = (uint32_t) ((uint8_t) *(++str));
|
|
|
if ((octet & (128+64)) != 128) /* Format isn't 10xxxxxx? */
|
|
|
return UNICODE_BOGUS_CHAR_VALUE;
|
|
|
|
|
|
octet = (uint32_t) ((uint8_t) *(++str));
|
|
|
if ((octet & (128+64)) != 128) /* Format isn't 10xxxxxx? */
|
|
|
return UNICODE_BOGUS_CHAR_VALUE;
|
|
|
|
|
|
octet = (uint32_t) ((uint8_t) *(++str));
|
|
|
if ((octet & (128+64)) != 128) /* Format isn't 10xxxxxx? */
|
|
|
return UNICODE_BOGUS_CHAR_VALUE;
|
|
|
|
|
|
octet = (uint32_t) ((uint8_t) *(++str));
|
|
|
if ((octet & (128+64)) != 128) /* Format isn't 10xxxxxx? */
|
|
|
return UNICODE_BOGUS_CHAR_VALUE;
|
|
|
|
|
|
octet = (uint32_t) ((uint8_t) *(++str));
|
|
|
if ((octet & (128+64)) != 128) /* Format isn't 10xxxxxx? */
|
|
|
return UNICODE_BOGUS_CHAR_VALUE;
|
|
|
|
|
|
*_str += 6; /* skip to next possible start of codepoint. */
|
|
|
return UNICODE_BOGUS_CHAR_VALUE;
|
|
|
} /* else if */
|
|
|
|
|
|
return UNICODE_BOGUS_CHAR_VALUE;
|
|
|
}
|
|
|
|
|
|
void FAudio_UTF8_To_UTF16(const char *src, uint16_t *dst, size_t len)
|
|
|
{
|
|
|
len -= sizeof (uint16_t); /* save room for null char. */
|
|
|
while (len >= sizeof (uint16_t))
|
|
|
{
|
|
|
uint32_t cp = FAudio_UTF8_CodePoint(&src);
|
|
|
if (cp == 0)
|
|
|
break;
|
|
|
else if (cp == UNICODE_BOGUS_CHAR_VALUE)
|
|
|
cp = UNICODE_BOGUS_CHAR_CODEPOINT;
|
|
|
|
|
|
if (cp > 0xFFFF) /* encode as surrogate pair */
|
|
|
{
|
|
|
if (len < (sizeof (uint16_t) * 2))
|
|
|
break; /* not enough room for the pair, stop now. */
|
|
|
|
|
|
cp -= 0x10000; /* Make this a 20-bit value */
|
|
|
|
|
|
*(dst++) = 0xD800 + ((cp >> 10) & 0x3FF);
|
|
|
len -= sizeof (uint16_t);
|
|
|
|
|
|
cp = 0xDC00 + (cp & 0x3FF);
|
|
|
} /* if */
|
|
|
|
|
|
*(dst++) = cp;
|
|
|
len -= sizeof (uint16_t);
|
|
|
} /* while */
|
|
|
|
|
|
*dst = 0;
|
|
|
}
|
|
|
|
|
|
/* vim: set noexpandtab shiftwidth=8 tabstop=8: */
|
|
|
|