summary refs log tree commit diff
path: root/src/Audio.c
diff options
context:
space:
mode:
authorWlodekM <[email protected]>2024-06-16 10:35:45 +0300
committerWlodekM <[email protected]>2024-06-16 10:35:45 +0300
commitabef6da56913f1c55528103e60a50451a39628b1 (patch)
treeb3c8092471ecbb73e568cd0d336efa0e7871ee8d /src/Audio.c
initial commit
Diffstat (limited to 'src/Audio.c')
-rw-r--r--src/Audio.c566
1 files changed, 566 insertions, 0 deletions
diff --git a/src/Audio.c b/src/Audio.c
new file mode 100644
index 0000000..74aa40b
--- /dev/null
+++ b/src/Audio.c
@@ -0,0 +1,566 @@
+#include "Audio.h"
+#include "String.h"
+#include "Logger.h"
+#include "Event.h"
+#include "Block.h"
+#include "ExtMath.h"
+#include "Funcs.h"
+#include "Game.h"
+#include "Errors.h"
+#include "Vorbis.h"
+#include "Chat.h"
+#include "Stream.h"
+#include "Utils.h"
+#include "Options.h"
+#include "Deflate.h"
+#ifdef CC_BUILD_ANDROID
+/* TODO: Refactor maybe to not rely on checking WinInfo.Handle != NULL */
+#include "Window.h"
+#endif
+
+int Audio_SoundsVolume, Audio_MusicVolume;
+const cc_string Sounds_ZipPathMC = String_FromConst("audio/default.zip");
+const cc_string Sounds_ZipPathCC = String_FromConst("audio/classicube.zip");
+static const cc_string audio_dir = String_FromConst("audio");
+
+struct Sound {
+	int channels, sampleRate;
+	struct AudioChunk chunk;
+};
+
+
+/*########################################################################################################################*
+*--------------------------------------------------------Sounds-----------------------------------------------------------*
+*#########################################################################################################################*/
+#ifdef CC_BUILD_NOSOUNDS
+/* Can't use mojang's sound assets, so just stub everything out */
+static void Sounds_Init(void) { }
+static void Sounds_Free(void) { }
+static void Sounds_Stop(void) { }
+static void Sounds_Start(void) {
+	Chat_AddRaw("&cSounds are not supported currently");
+	Audio_SoundsVolume = 0;
+}
+
+void Audio_PlayDigSound(cc_uint8 type)  { }
+void Audio_PlayStepSound(cc_uint8 type) { }
+#else
+#define AUDIO_MAX_SOUNDS 10
+
+struct SoundGroup {
+	int count;
+	struct Sound sounds[AUDIO_MAX_SOUNDS];
+};
+struct Soundboard { struct SoundGroup groups[SOUND_COUNT]; };
+
+static struct Soundboard digBoard, stepBoard;
+static RNGState sounds_rnd;
+
+#define WAV_FourCC(a, b, c, d) (((cc_uint32)a << 24) | ((cc_uint32)b << 16) | ((cc_uint32)c << 8) | (cc_uint32)d)
+#define WAV_FMT_SIZE 16
+
+static cc_result Sound_ReadWaveData(struct Stream* stream, struct Sound* snd) {
+	cc_uint32 fourCC, size;
+	cc_uint8 tmp[WAV_FMT_SIZE];
+	cc_result res;
+	int bitsPerSample;
+
+	if ((res = Stream_Read(stream, tmp, 12)))  return res;
+	fourCC = Stream_GetU32_BE(tmp + 0);
+	if (fourCC == WAV_FourCC('I','D','3', 2))  return AUDIO_ERR_MP3_SIG; /* ID3 v2.2 tags header */
+	if (fourCC == WAV_FourCC('I','D','3', 3))  return AUDIO_ERR_MP3_SIG; /* ID3 v2.3 tags header */
+	if (fourCC != WAV_FourCC('R','I','F','F')) return WAV_ERR_STREAM_HDR;
+
+	/* tmp[4] (4) file size */
+	fourCC = Stream_GetU32_BE(tmp + 8);
+	if (fourCC != WAV_FourCC('W','A','V','E')) return WAV_ERR_STREAM_TYPE;
+
+	for (;;) {
+		if ((res = Stream_Read(stream, tmp, 8))) return res;
+		fourCC = Stream_GetU32_BE(tmp + 0);
+		size   = Stream_GetU32_LE(tmp + 4);
+
+		if (fourCC == WAV_FourCC('f','m','t',' ')) {
+			if ((res = Stream_Read(stream, tmp, sizeof(tmp)))) return res;
+			if (Stream_GetU16_LE(tmp + 0) != 1) return WAV_ERR_DATA_TYPE;
+
+			snd->channels   = Stream_GetU16_LE(tmp + 2);
+			snd->sampleRate = Stream_GetU32_LE(tmp + 4);
+			/* tmp[8] (6) alignment data and stuff */
+
+			bitsPerSample = Stream_GetU16_LE(tmp + 14);
+			if (bitsPerSample != 16) return WAV_ERR_SAMPLE_BITS;
+			size -= WAV_FMT_SIZE;
+		} else if (fourCC == WAV_FourCC('d','a','t','a')) {
+			if ((res = Audio_AllocChunks(size, &snd->chunk, 1))) return res;
+			res = Stream_Read(stream, snd->chunk.data, size);
+
+			#ifdef CC_BUILD_BIGENDIAN
+			Utils_SwapEndian16((cc_int16*)snd->chunk.data, size / 2);
+			#endif
+			return res;
+		}
+
+		/* Skip over unhandled data */
+		if (size && (res = stream->Skip(stream, size))) return res;
+	}
+}
+
+static struct SoundGroup* Soundboard_FindGroup(struct Soundboard* board, const cc_string* name) {
+	struct SoundGroup* groups = board->groups;
+	int i;
+
+	for (i = 0; i < SOUND_COUNT; i++) 
+	{
+		if (String_CaselessEqualsConst(name, Sound_Names[i])) return &groups[i];
+	}
+	return NULL;
+}
+
+static void Soundboard_Load(struct Soundboard* board, const cc_string* boardName, const cc_string* file, struct Stream* stream) {
+	struct SoundGroup* group;
+	struct Sound* snd;
+	cc_string name = *file;
+	cc_result res;
+	int dotIndex;
+	Utils_UNSAFE_TrimFirstDirectory(&name);
+
+	/* dig_grass1.wav -> dig_grass1 */
+	dotIndex = String_LastIndexOf(&name, '.');
+	if (dotIndex >= 0) name.length = dotIndex;
+	if (!String_CaselessStarts(&name, boardName)) return;
+
+	/* Convert dig_grass1 to grass */
+	name = String_UNSAFE_SubstringAt(&name, boardName->length);
+	name = String_UNSAFE_Substring(&name, 0, name.length - 1);
+
+	group = Soundboard_FindGroup(board, &name);
+	if (!group) {
+		Chat_Add1("&cUnknown sound group '%s'", &name); return;
+	}
+	if (group->count == Array_Elems(group->sounds)) {
+		Chat_AddRaw("&cCannot have more than 10 sounds in a group"); return;
+	}
+
+	snd = &group->sounds[group->count];
+	res = Sound_ReadWaveData(stream, snd);
+
+	if (res) {
+		Logger_SysWarn2(res, "decoding", file);
+		Audio_FreeChunks(&snd->chunk, 1);
+		snd->chunk.data = NULL;
+		snd->chunk.size = 0;
+	} else { group->count++; }
+}
+
+static const struct Sound* Soundboard_PickRandom(struct Soundboard* board, cc_uint8 type) {
+	struct SoundGroup* group;
+	int idx;
+
+	if (type == SOUND_NONE || type >= SOUND_COUNT) return NULL;
+	if (type == SOUND_METAL) type = SOUND_STONE;
+
+	group = &board->groups[type];
+	if (!group->count) return NULL;
+
+	idx = Random_Next(&sounds_rnd, group->count);
+	return &group->sounds[idx];
+}
+
+
+CC_NOINLINE static void Sounds_Fail(cc_result res) {
+	Audio_Warn(res, "playing sounds");
+	Chat_AddRaw("&cDisabling sounds");
+	Audio_SetSounds(0);
+}
+
+static void Sounds_Play(cc_uint8 type, struct Soundboard* board) {
+	const struct Sound* snd;
+	struct AudioData data;
+	cc_result res;
+
+	if (type == SOUND_NONE || !Audio_SoundsVolume) return;
+	snd = Soundboard_PickRandom(board, type);
+	if (!snd) return;
+
+	data.chunk      = snd->chunk;
+	data.channels   = snd->channels;
+	data.sampleRate = snd->sampleRate;
+	data.rate       = 100;
+	data.volume     = Audio_SoundsVolume;
+
+	/* https://minecraft.wiki/w/Block_of_Gold#Sounds */
+	/* https://minecraft.wiki/w/Grass#Sounds */
+	if (board == &digBoard) {
+		if (type == SOUND_METAL) data.rate = 120;
+		else data.rate = 80;
+	} else {
+		data.volume /= 2;
+		if (type == SOUND_METAL) data.rate = 140;
+	}
+	
+	res = AudioPool_Play(&data);
+	if (res) Sounds_Fail(res);
+}
+
+static void Audio_PlayBlockSound(void* obj, IVec3 coords, BlockID old, BlockID now) {
+	if (now == BLOCK_AIR) {
+		Audio_PlayDigSound(Blocks.DigSounds[old]);
+	} else if (!Game_ClassicMode) {
+		/* use StepSounds instead when placing, as don't want */
+		/*  to play glass break sound when placing glass */
+		Audio_PlayDigSound(Blocks.StepSounds[now]);
+	}
+}
+
+static cc_bool SelectZipEntry(const cc_string* path) { return true; }
+static cc_result ProcessZipEntry(const cc_string* path, struct Stream* stream, struct ZipEntry* source) {
+	static const cc_string dig  = String_FromConst("dig_");
+	static const cc_string step = String_FromConst("step_");
+	
+	Soundboard_Load(&digBoard,  &dig,  path, stream);
+	Soundboard_Load(&stepBoard, &step, path, stream);
+	return 0;
+}
+
+static cc_result Sounds_ExtractZip(const cc_string* path) {
+	struct Stream stream;
+	cc_result res;
+
+	res = Stream_OpenFile(&stream, path);
+	if (res) { Logger_SysWarn2(res, "opening", path); return res; }
+
+	res = Zip_Extract(&stream, SelectZipEntry, ProcessZipEntry);
+	if (res) Logger_SysWarn2(res, "extracting", path);
+
+	/* No point logging error for closing readonly file */
+	(void)stream.Close(&stream);
+	return res;
+}
+
+/* TODO this is a pretty terrible solution */
+#ifdef CC_BUILD_WEBAUDIO
+static const struct SoundID { int group; const char* name; } sounds_list[] =
+{
+	{ SOUND_CLOTH,  "step_cloth1"  }, { SOUND_CLOTH,  "step_cloth2"  }, { SOUND_CLOTH,  "step_cloth3"  }, { SOUND_CLOTH,  "step_cloth4"  },
+	{ SOUND_GRASS,  "step_grass1"  }, { SOUND_GRASS,  "step_grass2"  }, { SOUND_GRASS,  "step_grass3"  }, { SOUND_GRASS,  "step_grass4"  },
+	{ SOUND_GRAVEL, "step_gravel1" }, { SOUND_GRAVEL, "step_gravel2" }, { SOUND_GRAVEL, "step_gravel3" }, { SOUND_GRAVEL, "step_gravel4" },
+	{ SOUND_SAND,   "step_sand1"   }, { SOUND_SAND,   "step_sand2"   }, { SOUND_SAND,   "step_sand3"   }, { SOUND_SAND,   "step_sand4"   },
+	{ SOUND_SNOW,   "step_snow1"   }, { SOUND_SNOW,   "step_snow2"   }, { SOUND_SNOW,   "step_snow3"   }, { SOUND_SNOW,   "step_snow4"   },
+	{ SOUND_STONE,  "step_stone1"  }, { SOUND_STONE,  "step_stone2"  }, { SOUND_STONE,  "step_stone3"  }, { SOUND_STONE,  "step_stone4"  },
+	{ SOUND_WOOD,   "step_wood1"   }, { SOUND_WOOD,   "step_wood2"   }, { SOUND_WOOD,   "step_wood3"   }, { SOUND_WOOD,   "step_wood4"   },
+	{ SOUND_NONE, NULL },
+
+	{ SOUND_CLOTH,  "dig_cloth1"   }, { SOUND_CLOTH,  "dig_cloth2"   }, { SOUND_CLOTH,  "dig_cloth3"   }, { SOUND_CLOTH,  "dig_cloth4"   },
+	{ SOUND_GRASS,  "dig_grass1"   }, { SOUND_GRASS,  "dig_grass2"   }, { SOUND_GRASS,  "dig_grass3"   }, { SOUND_GRASS,  "dig_grass4"   },
+	{ SOUND_GLASS,  "dig_glass1"   }, { SOUND_GLASS,  "dig_glass2"   }, { SOUND_GLASS,  "dig_glass3"   },
+	{ SOUND_GRAVEL, "dig_gravel1"  }, { SOUND_GRAVEL, "dig_gravel2"  }, { SOUND_GRAVEL, "dig_gravel3"  }, { SOUND_GRAVEL, "dig_gravel4"  },
+	{ SOUND_SAND,   "dig_sand1"    }, { SOUND_SAND,   "dig_sand2"    }, { SOUND_SAND,   "dig_sand3"    }, { SOUND_SAND,   "dig_sand4"    },
+	{ SOUND_SNOW,   "dig_snow1"    }, { SOUND_SNOW,   "dig_snow2"    }, { SOUND_SNOW,   "dig_snow3"    }, { SOUND_SNOW,   "dig_snow4"    },
+	{ SOUND_STONE,  "dig_stone1"   }, { SOUND_STONE,  "dig_stone2"   }, { SOUND_STONE,  "dig_stone3"   }, { SOUND_STONE,  "dig_stone4"   },
+	{ SOUND_WOOD,   "dig_wood1"    }, { SOUND_WOOD,   "dig_wood2"    }, { SOUND_WOOD,   "dig_wood3"    }, { SOUND_WOOD,   "dig_wood4"    },
+};
+
+/* TODO this is a terrible solution */
+static void InitWebSounds(void) {
+	struct Soundboard* board = &stepBoard;
+	struct SoundGroup* group;
+	int i;
+
+	for (i = 0; i < Array_Elems(sounds_list); i++) {
+		if (sounds_list[i].group == SOUND_NONE) {
+			board = &digBoard;
+		} else {
+			group = &board->groups[sounds_list[i].group];
+			group->sounds[group->count++].chunk.data = sounds_list[i].name;
+		}
+	}
+}
+#endif
+
+static cc_bool sounds_loaded;
+static void Sounds_Start(void) {
+	cc_result res;
+	if (!AudioBackend_Init()) { 
+		AudioBackend_Free(); 
+		Audio_SoundsVolume = 0; 
+		return; 
+	}
+
+	if (sounds_loaded) return;
+	sounds_loaded = true;
+#ifdef CC_BUILD_WEBAUDIO
+	InitWebSounds();
+#else
+	res = Sounds_ExtractZip(&Sounds_ZipPathMC);
+	if (res == ReturnCode_FileNotFound)
+		Sounds_ExtractZip(&Sounds_ZipPathCC);
+#endif
+}
+
+static void Sounds_Stop(void) { AudioPool_Close(); }
+
+static void Sounds_Init(void) {
+	int volume = Options_GetInt(OPT_SOUND_VOLUME, 0, 100, DEFAULT_SOUNDS_VOLUME);
+	Audio_SetSounds(volume);
+	Event_Register_(&UserEvents.BlockChanged, NULL, Audio_PlayBlockSound);
+}
+static void Sounds_Free(void) { Sounds_Stop(); }
+
+void Audio_PlayDigSound(cc_uint8 type)  { Sounds_Play(type, &digBoard); }
+void Audio_PlayStepSound(cc_uint8 type) { Sounds_Play(type, &stepBoard); }
+#endif
+
+
+/*########################################################################################################################*
+*--------------------------------------------------------Music------------------------------------------------------------*
+*#########################################################################################################################*/
+#ifdef CC_BUILD_NOMUSIC
+/* Can't use mojang's music assets, so just stub everything out */
+static void Music_Init(void) { }
+static void Music_Free(void) { }
+static void Music_Stop(void) { }
+static void Music_Start(void) {
+	Chat_AddRaw("&cMusic is not supported currently");
+	Audio_MusicVolume = 0;
+}
+#else
+static void* music_thread;
+static void* music_waitable;
+static volatile cc_bool music_stopping, music_joining;
+static int music_minDelay, music_maxDelay;
+
+static cc_result Music_Buffer(struct AudioChunk* chunk, int maxSamples, struct VorbisState* ctx) {
+	int samples = 0;
+	cc_int16* cur;
+	cc_result res = 0, res2;
+	cc_int16* data = chunk->data;
+
+	while (samples < maxSamples) {
+		if ((res = Vorbis_DecodeFrame(ctx))) break;
+
+		cur = &data[samples];
+		samples += Vorbis_OutputFrame(ctx, cur);
+	}
+
+	chunk->size = samples * 2;
+	res2 = Audio_QueueChunk(&music_ctx, chunk);
+	if (res2) { music_stopping = true; return res2; }
+	return res;
+}
+
+static cc_result Music_PlayOgg(struct Stream* source) {
+	struct OggState ogg;
+	struct VorbisState vorbis;
+	int channels, sampleRate, volume;
+
+	int chunkSize, samplesPerSecond;
+	struct AudioChunk chunks[AUDIO_MAX_BUFFERS] = { 0 };
+	int inUse, i, cur;
+	cc_result res;
+
+	Ogg_Init(&ogg, source);
+	Vorbis_Init(&vorbis);
+	vorbis.source = &ogg;
+	if ((res = Vorbis_DecodeHeaders(&vorbis))) goto cleanup;
+	
+	channels   = vorbis.channels;
+	sampleRate = vorbis.sampleRate;
+	if ((res = Audio_SetFormat(&music_ctx, channels, sampleRate, 100))) goto cleanup;
+
+	/* largest possible vorbis frame decodes to blocksize1 * channels samples, */
+	/*  so can end up decoding slightly over a second of audio */
+	chunkSize        = channels * (sampleRate + vorbis.blockSizes[1]);
+	samplesPerSecond = channels * sampleRate;
+
+	if ((res = Audio_AllocChunks(chunkSize * 2, chunks, AUDIO_MAX_BUFFERS))) goto cleanup;
+    volume = Audio_MusicVolume;
+    Audio_SetVolume(&music_ctx, volume);	
+
+	/* fill up with some samples before playing */
+	for (i = 0; i < AUDIO_MAX_BUFFERS && !res; i++) 
+	{
+		res = Music_Buffer(&chunks[i], samplesPerSecond, &vorbis);
+	}
+	if (music_stopping) goto cleanup;
+
+	res  = Audio_Play(&music_ctx);
+	if (res) goto cleanup;
+	cur  = 0;
+
+	while (!music_stopping) {
+#ifdef CC_BUILD_ANDROID
+		/* Don't play music while in the background on Android */
+    	/* TODO: Not use such a terrible approach */
+    	if (!Window_Main.Handle) {
+    		Audio_Pause(&music_ctx);
+    		while (!Window_Main.Handle && !music_stopping) {
+    			Thread_Sleep(10); continue;
+    		}
+    		Audio_Play(&music_ctx);
+    	}
+#endif
+        if (volume != Audio_MusicVolume) {
+            volume = Audio_MusicVolume;
+            Audio_SetVolume(&music_ctx, volume);
+        }
+
+		res = Audio_Poll(&music_ctx, &inUse);
+		if (res) { music_stopping = true; break; }
+
+		if (inUse >= AUDIO_MAX_BUFFERS) {
+			Thread_Sleep(10); continue;
+		}
+
+		res = Music_Buffer(&chunks[cur], samplesPerSecond, &vorbis);
+		cur = (cur + 1) % AUDIO_MAX_BUFFERS;
+
+		/* need to specially handle last bit of audio */
+		if (res) break;
+	}
+
+	if (music_stopping) {
+		/* must close audio context, as otherwise some of the audio */
+		/*  context's internal audio buffers may have a reference */
+		/*  to the `data` buffer which will be freed after this */
+		Audio_Close(&music_ctx);
+	} else {
+		/* Wait until the buffers finished playing */
+		for (;;) {
+			if (Audio_Poll(&music_ctx, &inUse) || inUse == 0) break;
+			Thread_Sleep(10);
+		}
+	}
+
+cleanup:
+	Audio_FreeChunks(chunks, AUDIO_MAX_BUFFERS);
+	Vorbis_Free(&vorbis);
+	return res == ERR_END_OF_STREAM ? 0 : res;
+}
+
+static void Music_AddFile(const cc_string* path, void* obj) {
+	struct StringsBuffer* files = (struct StringsBuffer*)obj;
+	static const cc_string ogg  = String_FromConst(".ogg");
+
+	if (!String_CaselessEnds(path, &ogg)) return;
+	StringsBuffer_Add(files, path);
+}
+
+static void Music_RunLoop(void) {
+	struct StringsBuffer files;
+	cc_string path;
+	RNGState rnd;
+	struct Stream stream;
+	int idx, delay;
+	cc_result res = 0;
+
+	StringsBuffer_SetLengthBits(&files, STRINGSBUFFER_DEF_LEN_SHIFT);
+	StringsBuffer_Init(&files);
+	Directory_Enum(&audio_dir, &files, Music_AddFile);
+
+	Random_SeedFromCurrentTime(&rnd);
+	res = Audio_Init(&music_ctx, AUDIO_MAX_BUFFERS);
+	if (res) music_stopping = true;
+
+	while (!music_stopping && files.count) {
+		idx  = Random_Next(&rnd, files.count);
+		path = StringsBuffer_UNSAFE_Get(&files, idx);
+		Platform_Log1("playing music file: %s", &path);
+
+		res = Stream_OpenFile(&stream, &path);
+		if (res) { Logger_SysWarn2(res, "opening", &path); break; }
+
+		res = Music_PlayOgg(&stream);
+		if (res) { Logger_SimpleWarn2(res, "playing", &path); }
+
+		/* No point logging error for closing readonly file */
+		(void)stream.Close(&stream);
+
+		if (music_stopping) break;
+		delay = Random_Range(&rnd, music_minDelay, music_maxDelay);
+		Waitable_WaitFor(music_waitable, delay);
+	}
+
+	if (res) {
+		Chat_AddRaw("&cDisabling music");
+		Audio_MusicVolume = 0;
+	}
+	Audio_Close(&music_ctx);
+	StringsBuffer_Clear(&files);
+
+	if (music_joining) return;
+	Thread_Detach(music_thread);
+	music_thread = NULL;
+}
+
+static void Music_Start(void) {
+	if (music_thread) return;
+	if (!AudioBackend_Init()) {
+		AudioBackend_Free(); 
+		Audio_MusicVolume = 0;
+		return; 
+	}
+
+	music_joining  = false;
+	music_stopping = false;
+
+	Thread_Run(&music_thread, Music_RunLoop, 256 * 1024, "Music");
+}
+
+static void Music_Stop(void) {
+	music_joining  = true;
+	music_stopping = true;
+	Waitable_Signal(music_waitable);
+	
+	if (music_thread) Thread_Join(music_thread);
+	music_thread = NULL;
+}
+
+static void Music_Init(void) {
+	int volume;
+	/* music is delayed between 2 - 7 minutes by default */
+	music_minDelay = Options_GetInt(OPT_MIN_MUSIC_DELAY, 0, 3600, 120) * MILLIS_PER_SEC;
+	music_maxDelay = Options_GetInt(OPT_MAX_MUSIC_DELAY, 0, 3600, 420) * MILLIS_PER_SEC;
+	music_waitable = Waitable_Create("Music sleep");
+
+	volume = Options_GetInt(OPT_MUSIC_VOLUME, 0, 100, DEFAULT_MUSIC_VOLUME);
+	Audio_SetMusic(volume);
+}
+
+static void Music_Free(void) {
+	Music_Stop();
+	Waitable_Free(music_waitable);
+}
+#endif
+
+
+/*########################################################################################################################*
+*--------------------------------------------------------General----------------------------------------------------------*
+*#########################################################################################################################*/
+void Audio_SetSounds(int volume) {
+	Audio_SoundsVolume = volume;
+	if (volume) Sounds_Start();
+	else        Sounds_Stop();
+}
+
+void Audio_SetMusic(int volume) {
+	Audio_MusicVolume = volume;
+	if (volume) Music_Start();
+	else        Music_Stop();
+}
+
+static void OnInit(void) {
+	Sounds_Init();
+	Music_Init();
+}
+
+static void OnFree(void) {
+	Sounds_Free();
+	Music_Free();
+	AudioBackend_Free();
+}
+
+struct IGameComponent Audio_Component = {
+	OnInit, /* Init  */
+	OnFree  /* Free  */
+};