/*** The `audio` module. An audio channel can play only one audio object at a time. There are 24 audio channels available, numbered from 0 to 23. @module ctr.audio @usage local audio = require("ctr.audio") */ #include <3ds.h> #include #include #include #include #include #include #include // Audio object type typedef enum { TYPE_UNKNOWN = -1, TYPE_OGG = 0, TYPE_WAV = 1 } filetype; // Audio object userdata typedef struct { filetype type; // file type // OGG Vorbis specific OggVorbis_File vf; // ogg vorbis file // Needed for playback float rate; // sample rate (per channel) (Hz) u32 channels; // channel count u32 encoding; // data encoding (NDSP_ENCODING_*) u32 nsamples; // numbers of samples in the audio (per channel, not the total) u32 size; // number of bytes in the audio (total, ie data size) char* data; // raw audio data // Playing parameters (type-independant) float mix[12]; // mix parameters ndspInterpType interp; // interpolation type double speed; // playing speed } audio_userdata; // Indicate if NDSP was initialized or not. // NDSP doesn't work on citra yet. // Please only throw an error related to this when using a ndsp function, so other parts of the // audio module are still available on citra, like audio.load() and audio:info(). bool isAudioInitialized = false; // Array of the last audio_userdata sent to each channel; channels range from 0 to 23 audio_userdata* channels[24]; /*** Load an audio file. OGG Vorbis and PCM WAV file format are currently supported. (Most WAV files use the PCM encoding). @function load @tparam string path path to the file @tparam[opt=detect] string type file type, `"ogg"` or `"wav"`. If set to `"detect"`, will try to deduce the type from the filename. @treturn[1] audio the loaded audio object @treturn[2] nil if a error happened @treturn[2] string error message */ static int audio_load(lua_State *L) { const char *path = luaL_checkstring(L, 1); const char* argType = luaL_optstring(L, 2, "detect"); // Create userdata audio_userdata *audio = lua_newuserdata(L, sizeof(*audio)); luaL_getmetatable(L, "LAudio"); lua_setmetatable(L, -2); for (int i=0; i<12; i++) audio->mix[i] = 1; audio->interp = NDSP_INTERP_LINEAR; audio->speed = 1; // Get file type filetype type = TYPE_UNKNOWN; if (strcmp(argType, "detect") == 0) { const char *dot = strrchr(path, '.'); if (!dot || dot == path) dot = ""; const char *ext = dot + 1; if (strncmp(ext, "ogg", 3) == 0) type = TYPE_OGG; else if (strncmp(ext, "wav", 3) == 0) type = TYPE_WAV; } else if (strcmp(argType, "ogg") == 0) { type = TYPE_OGG; } else if (strcmp(argType, "wav") == 0) { type = TYPE_WAV; } // Open and read file if (type == TYPE_OGG) { audio->type = TYPE_OGG; // Load audio file FILE *file = fopen(path, "rb"); if (ov_open(file, &audio->vf, NULL, 0) < 0) { lua_pushnil(L); lua_pushstring(L, "input does not appear to be a valid ogg vorbis file or doesn't exist"); return 2; } // Decoding Ogg Vorbis bitstream vorbis_info* vi = ov_info(&audio->vf, -1); if (vi == NULL) luaL_error(L, "could not retrieve ogg audio stream informations"); audio->rate = vi->rate; audio->channels = vi->channels; audio->encoding = NDSP_ENCODING_PCM16; audio->nsamples = ov_pcm_total(&audio->vf, -1); audio->size = audio->nsamples * audio->channels * 2; // *2 because output is PCM16 (2 bytes/sample) if (linearSpaceFree() < audio->size) luaL_error(L, "not enough linear memory available"); audio->data = linearAlloc(audio->size); // Decoding loop int offset = 0; int eof = 0; int current_section; while (!eof) { long ret = ov_read(&audio->vf, &audio->data[offset], 4096, ¤t_section); if (ret == 0) { eof = 1; } else if (ret < 0) { ov_clear(&audio->vf); linearFree(audio->data); luaL_error(L, "error in the ogg vorbis stream"); return 0; } else { // TODO handle multiple links (http://xiph.org/vorbis/doc/vorbisfile/decoding.html) offset += ret; } } return 1; } else if (type == TYPE_WAV) { audio->type = TYPE_WAV; // Used this as a reference for the WAV format: http://www-mmsp.ece.mcgill.ca/Documents/AudioFormats/WAVE/WAVE.html // Load file FILE *file = fopen(path, "rb"); if (file) { bool valid = true; // if something goes wrong, this will be false char buff[8]; // Master chunk fread(buff, 4, 1, file); // ckId if (strncmp(buff, "RIFF", 4) != 0) valid = false; fseek(file, 4, SEEK_CUR); // skip ckSize fread(buff, 4, 1, file); // WAVEID if (strncmp(buff, "WAVE", 4) != 0) valid = false; // fmt Chunk fread(buff, 4, 1, file); // ckId if (strncmp(buff, "fmt ", 4) != 0) valid = false; fread(buff, 4, 1, file); // ckSize if (*buff != 16) valid = false; // should be 16 for PCM format fread(buff, 2, 1, file); // wFormatTag if (*buff != 0x0001) valid = false; // PCM format u16 channels; fread(&channels, 2, 1, file); // nChannels audio->channels = channels; u32 rate; fread(&rate, 4, 1, file); // nSamplesPerSec audio->rate = rate; fseek(file, 4, SEEK_CUR); // skip nAvgBytesPerSec u16 byte_per_block; // 1 block = 1*channelCount samples fread(&byte_per_block, 2, 1, file); // nBlockAlign u16 byte_per_sample; fread(&byte_per_sample, 2, 1, file); // wBitsPerSample byte_per_sample /= 8; // bits -> bytes // There may be some additionals chunks between fmt and data // TODO handle some usefull chunks that may be here fread(&buff, 4, 1, file); // ckId while (strncmp(buff, "data", 4) != 0) { u32 size; fread(&size, 4, 1, file); // ckSize fseek(file, size, SEEK_CUR); // skip chunk int i = fread(&buff, 4, 1, file); // next chunk ckId if (i < 4) { // reached EOF before finding a data chunk valid = false; break; } } // data Chunk (ckId already read) u32 size; fread(&size, 4, 1, file); // ckSize audio->size = size; audio->nsamples = audio->size / byte_per_block; if (byte_per_sample == 1) audio->encoding = NDSP_ENCODING_PCM8; else if (byte_per_sample == 2) audio->encoding = NDSP_ENCODING_PCM16; else luaL_error(L, "unknown encoding, needs to be PCM8 or PCM16"); if (!valid) { fclose(file); luaL_error(L, "invalid PCM wav file"); return 0; } // Read data if (linearSpaceFree() < audio->size) luaL_error(L, "not enough linear memory available"); audio->data = linearAlloc(audio->size); fread(audio->data, audio->size, 1, file); fclose(file); return 1; } else { lua_pushnil(L); lua_pushfstring(L, "error while opening wav file: %s", strerror(errno));; return 2; } } luaL_error(L, "unknown audio type"); return 0; } /*** Load raw audio data from a string. @function loadRaw @tparam string data raw audio data @tparam number rate sampling rate @tparam string encoding audio encoding, can be `"PCM8"`, `"PCM16"` or `"ADPCM"` @tparam[opt=1] number channels audio channels count @treturn[1] audio the loaded audio object @treturn[2] nil if a error happened @treturn[2] string error message */ static int audio_loadRaw(lua_State *L) { size_t dataSize; char* data = (char*)luaL_checklstring(L, 1, &dataSize); float rate = luaL_checkinteger(L, 2); const char* argEncoding = luaL_checkstring(L, 3); u32 channels = luaL_optinteger(L, 4, 1); audio_userdata *audio = lua_newuserdata(L, sizeof(*audio)); luaL_getmetatable(L, "LAudio"); lua_setmetatable(L, -2); audio->type = TYPE_WAV; audio->rate = rate; audio->channels = channels; u8 sampleSize = 2; // default to 2 if (strcmp(argEncoding, "PCM8")) { audio->encoding = NDSP_ENCODING_PCM8; sampleSize = 1; } else if (strcmp(argEncoding, "PCM16")) { audio->encoding = NDSP_ENCODING_PCM16; } else if (strcmp(argEncoding, "ADPCM")) { audio->encoding = NDSP_ENCODING_ADPCM; } else { lua_pushnil(L); lua_pushstring(L, "Wrong format"); return 2; } audio->nsamples = dataSize/sampleSize; audio->size = dataSize; audio->data = data; audio->speed = 1.0; return 1; } /*** Check if audio is currently playing on a channel. @function playing @tparam[opt] integer channel number; if `nil` will search the first channel playing an audio @treturn boolean `true` if the channel is currently playing the audio, `false` otherwise. If channel is not set, `false` means no audio is playing at all. */ static int audio_playing(lua_State *L) { if (!isAudioInitialized) { lua_pushboolean(L, false); return 1; } int channel = luaL_optinteger(L, 1, -1); if (channel < -1 || channel > 23) luaL_error(L, "channel number must be between 0 and 23"); // Search a channel playing audio if (channel == -1) { for (int i = 0; i <= 23; i++) { if (ndspChnIsPlaying(i)) { lua_pushboolean(L, true); return 1; } } lua_pushboolean(L, false); return 1; } else { lua_pushboolean(L, ndspChnIsPlaying(channel)); return 1; } return 0; } /*** Set the mix parameters (volumes) of a channel. Volumes go from 0 (0%) to 1 (100%). Note that when a new audio object will play on this channel, theses parameters will be reset with the new audio object defaults (set in `audio:mix()`). @function mix @tparam[opt] integer channel the channel number, if `nil` will change the mix parmaters of all channels @tparam[opt=1] number frontLeft front left volume @tparam[opt=frontLeft] number frontRight front right volume @tparam[opt=frontLeft] number backLeft back left volume @tparam[opt=frontRight] number backRight back right volume */ static int audio_mix(lua_State *L) { if (!isAudioInitialized) luaL_error(L, "audio wasn't initialized correctly"); int channel = luaL_optinteger(L, 1, -1); if (channel < -1 || channel > 23) luaL_error(L, "channel number must be between 0 and 23"); float mix[12]; mix[0] = luaL_optnumber(L, 2, 1); mix[1] = luaL_optnumber(L, 3, mix[0]); mix[2] = luaL_optnumber(L, 4, mix[0]); mix[3] = luaL_optnumber(L, 5, mix[2]); if (channel == -1) { for (int i=0; i<=23; i++) ndspChnSetMix(i, mix); } else { ndspChnSetMix(channel, mix); } return 0; } /*** Set the interpolation type of a channel. Note that when a new audio object will play on this channel, this parameter will be reset with the new audio object default (set in `audio:interpolation()`). @function interpolation @tparam[opt] integer channel stop playing audio on this channel; if `nil` will change interpolation type on all channels @tparam[opt=linear] string "none", "linear" or "polyphase" */ static int audio_interpolation(lua_State *L) { if (!isAudioInitialized) luaL_error(L, "audio wasn't initialized correctly"); int channel = luaL_optinteger(L, 1, -1); if (channel < -1 || channel > 23) luaL_error(L, "channel number must be between 0 and 23"); const char* interpArg = luaL_optstring(L, 2, "linear"); ndspInterpType interp; if (strcmp(interpArg, "none") == 0) interp = NDSP_INTERP_NONE; else if (strcmp(interpArg, "linear") == 0) interp = NDSP_INTERP_LINEAR; else if (strcmp(interpArg, "polyphase") == 0) interp = NDSP_INTERP_POLYPHASE; else { luaL_error(L, "unknown interpolation type"); return 0; } if (channel == -1) { for (int i=0; i<=23; i++) ndspChnSetInterp(i, interp); } else { ndspChnSetInterp(channel, interp); } return 0; } /*** Set the speed of the audio playing in a channel. Speed is expressed as a percentage of the normal playing speed. 1 is 100% speed and 2 is 200%, etc. Note that when a new audio object will play on this channel, this parameter will be reset with the new audio object default (set in `audio:speed()`). @function speed @tparam[opt] integer channel stop playing audio on this channel; if `nil` will change interpolation type on all channels @tparam[opt=1] number speed percentage */ static int audio_speed(lua_State *L) { if (!isAudioInitialized) luaL_error(L, "audio wasn't initialized correctly"); int channel = luaL_optinteger(L, 1, -1); if (channel < -1 || channel > 23) luaL_error(L, "channel number must be between 0 and 23"); double speed = luaL_optnumber(L, 2, 1); if (channel == -1) { for (int i=0; i<=23; i++) { if (channels[i]) ndspChnSetRate(i, channels[i]->rate * speed); } } else { if (channels[channel]) ndspChnSetRate(channel, channels[channel]->rate * speed); } return 0; } /*** Stop playing all audio on all channels or a specific channel. @function stop @tparam[opt] integer channel stop playing audio on this channel; if `nil` will stop audio on all channels @treturn integer number of channels where audio was stopped */ static int audio_stop(lua_State *L) { if (!isAudioInitialized) { lua_pushinteger(L, 0); return 1; } int channel = luaL_optinteger(L, 1, -1); int n = 0; if (channel == -1) { for (int i = 0; i <= 23; i++) { if (ndspChnIsPlaying(i)) { ndspChnWaveBufClear(i); n++; } } } else if (channel < 0 || channel > 23) { luaL_error(L, "channel number must be between 0 and 23"); } else { if (ndspChnIsPlaying(channel)) { ndspChnWaveBufClear(channel); n++; } } lua_pushinteger(L, n); return 1; } /*** audio object @section Methods */ /*** Returns the audio object duration. @function :duration @treturn number duration in seconds */ static int audio_object_duration(lua_State *L) { audio_userdata *audio = luaL_checkudata(L, 1, "LAudio"); lua_pushnumber(L, (double)(audio->nsamples) / audio->rate); return 1; } /*** Returns the current playing position. @function :time @tparam[opt] integer channel number; if `nil` will use the first channel found which played this audio @treturn number time in seconds */ static int audio_object_time(lua_State *L) { audio_userdata *audio = luaL_checkudata(L, 1, "LAudio"); int channel = luaL_optinteger(L, 2, -1); if (channel < -1 || channel > 23) luaL_error(L, "channel number must be between 0 and 23"); // Search a channel playing the audio object if (channel == -1) { for (int i = 0; i <= 23; i++) { if (channels[i] == audio) { channel = i; break; } } } if (channel == -1 || channels[channel] != audio || !isAudioInitialized) // audio not playing lua_pushnumber(L, 0); else lua_pushnumber(L, (double)(ndspChnGetSamplePos(channel)) / audio->rate); return 1; } /*** Check if the audio is currently playing. @function :playing @tparam[opt] integer channel channel number; if `nil` will search the first channel playing this audio @treturn boolean true if the channel is currently playing the audio, false otherwise */ static int audio_object_playing(lua_State *L) { if (!isAudioInitialized) { lua_pushboolean(L, false); return 1; } audio_userdata *audio = luaL_checkudata(L, 1, "LAudio"); int channel = luaL_optinteger(L, 2, -1); if (channel < -1 || channel > 23) luaL_error(L, "channel number must be between 0 and 23"); // Search a channel playing the audio object if (channel == -1) { for (int i = 0; i <= 23; i++) { if (channels[i] == audio && ndspChnIsPlaying(i)) { lua_pushboolean(L, true); return 1; } } lua_pushboolean(L, false); return 1; } else { lua_pushboolean(L, channels[channel] == audio && ndspChnIsPlaying(channel)); return 1; } return 0; } /*** Set the mix parameters (volumes) of the audio. Volumes go from 0 (0%) to 1 (100%). @function :mix @tparam[opt=1] number frontLeft front left volume @tparam[opt=frontLeft] number frontRight front right volume @tparam[opt=frontLeft] number backLeft back left volume @tparam[opt=frontRight] number backRight back right volume */ static int audio_object_mix(lua_State *L) { audio_userdata *audio = luaL_checkudata(L, 1, "LAudio"); audio->mix[0] = luaL_optnumber(L, 2, 1); audio->mix[1] = luaL_optnumber(L, 3, audio->mix[0]); audio->mix[2] = luaL_optnumber(L, 4, audio->mix[0]); audio->mix[3] = luaL_optnumber(L, 5, audio->mix[2]); return 0; } /*** Set the interpolation type of the audio. @function :interpolation @tparam[opt=linear] string "none", "linear" or "polyphase" */ static int audio_object_interpolation(lua_State *L) { audio_userdata *audio = luaL_checkudata(L, 1, "LAudio"); const char* interp = luaL_optstring(L, 2, "linear"); if (strcmp(interp, "none") == 0) audio->interp = NDSP_INTERP_NONE; else if (strcmp(interp, "linear") == 0) audio->interp = NDSP_INTERP_LINEAR; else if (strcmp(interp, "polyphase") == 0) audio->interp = NDSP_INTERP_POLYPHASE; else { luaL_error(L, "unknown interpolation type"); return 0; } return 0; } /*** Set the speed of the audio. Speed is expressed as a percentage of the normal playing speed. 1 is 100% speed and 2 is 200%, etc. @function :speed @tparam[opt=1] number speed percentage */ static int audio_object_speed(lua_State *L) { audio_userdata *audio = luaL_checkudata(L, 1, "LAudio"); audio->speed = luaL_optnumber(L, 2, 1); return 0; } /*** Plays the audio file. @function :play @tparam[opt=false] boolean loop if the audio should loop or not @tparam[opt] integer channel the channel to play the audio on (0-23); if `nil` will use the first available channel. If the channel was playing another audio, it will be stopped and replaced by this audio. If not set and no channel is available, will return nil plus an error message. @treturn[1] integer channel number the audio is playing on @treturn[2] nil an error happened and the audio was not played @treturn[2] error the error message */ static int audio_object_play(lua_State *L) { if (!isAudioInitialized) luaL_error(L, "audio wasn't initialized correctly"); audio_userdata *audio = luaL_checkudata(L, 1, "LAudio"); bool loop = lua_toboolean(L, 2); int channel = luaL_optinteger(L, 3, -1); // Find a free channel if (channel == -1) { for (int i = 0; i <= 23; i++) { if (!ndspChnIsPlaying(i)) { channel = i; break; } } } if (channel == -1) { lua_pushnil(L); lua_pushstring(L, "no audio channel is currently available"); return 2; } if (channel < 0 || channel > 23) luaL_error(L, "channel number must be between 0 and 23"); // Set channel parameters ndspChnWaveBufClear(channel); ndspChnReset(channel); ndspChnInitParams(channel); ndspChnSetMix(channel, audio->mix); ndspChnSetInterp(channel, audio->interp); ndspChnSetRate(channel, audio->rate * audio->speed); // maybe hackish way to set a different speed, but it works ndspChnSetFormat(channel, NDSP_CHANNELS(audio->channels) | NDSP_ENCODING(audio->encoding)); // Send & play audio data ndspWaveBuf* waveBuf = calloc(1, sizeof(ndspWaveBuf)); waveBuf->data_vaddr = audio->data; waveBuf->nsamples = audio->nsamples; waveBuf->looping = loop; DSP_FlushDataCache((u32*)audio->data, audio->size); ndspChnWaveBufAdd(channel, waveBuf); channels[channel] = audio; lua_pushinteger(L, channel); return 1; } /*** Stop playing an audio object. @function :stop @tparam[opt] integer channel stop playing the audio on this channel; if `nil` will stop all channels playing this audio. If the channel is playing another audio object, this function will do nothing. @treturn integer number of channels where this audio was stopped */ static int audio_object_stop(lua_State *L) { if (!isAudioInitialized) { lua_pushinteger(L, 0); return 1; } audio_userdata *audio = luaL_checkudata(L, 1, "LAudio"); int channel = luaL_optinteger(L, 2, -1); int n = 0; if (channel == -1) { for (int i = 0; i <= 23; i++) { if (channels[i] == audio && ndspChnIsPlaying(i)) { ndspChnWaveBufClear(i); n++; } } } else if (channel < 0 || channel > 23) { luaL_error(L, "channel number must be between 0 and 23"); } else { if (channels[channel] == audio && ndspChnIsPlaying(channel)) { ndspChnWaveBufClear(channel); n++; } } lua_pushinteger(L, n); return 1; } /*** Returns the audio object type. @function :type @treturn string "ogg" or "wav" */ static int audio_object_type(lua_State *L) { audio_userdata *audio = luaL_checkudata(L, 1, "LAudio"); if (audio->type == TYPE_OGG) lua_pushstring(L, "ogg"); else if (audio->type == TYPE_WAV) lua_pushstring(L, "wav"); return 1; } /*** Unload an audio object. @function :unload */ static int audio_object_unload(lua_State *L) { audio_userdata *audio = luaL_checkudata(L, 1, "LAudio"); // Stop playing the audio if (isAudioInitialized) { for (int i = 0; i <= 23; i++) { if (channels[i] == audio) { ndspChnWaveBufClear(i); } } } if (audio->type == TYPE_OGG) ov_clear(&audio->vf); // Free memory linearFree(audio->data); return 0; } /*** audio object (ogg-only). Ogg Vorbis files specific methods. Using one of theses methods will throw an error if used on an non-ogg audio object. @section Methods */ /*** Returns basic information about the audio in a vorbis bitstream. @function :info @treturn infoTable information table */ static int audio_object_info(lua_State *L) { audio_userdata *audio = luaL_checkudata(L, 1, "LAudio"); if (audio->type != TYPE_OGG) luaL_error(L, "only avaible on OGG audio objects"); vorbis_info* vi = ov_info(&audio->vf, -1); if (vi == NULL) luaL_error(L, "could not retrieve audio stream informations"); lua_createtable(L, 0, 6); lua_pushinteger(L, vi->version); lua_setfield(L, -2, "version"); lua_pushinteger(L, vi->channels); lua_setfield(L, -2, "channels"); lua_pushinteger(L, vi->rate); lua_setfield(L, -2, "rate"); lua_pushinteger(L, vi->bitrate_upper); lua_setfield(L, -2, "bitrateUpper"); lua_pushinteger(L, vi->bitrate_nominal); lua_setfield(L, -2, "bitrateNominal"); lua_pushinteger(L, vi->bitrate_lower); lua_setfield(L, -2, "bitrateLower"); return 1; } /*** Returns the Ogg Vorbis bitstream comment. @function :comment @treturn commentTable comment table */ static int audio_object_comment(lua_State *L) { audio_userdata *audio = luaL_checkudata(L, 1, "LAudio"); if (audio->type != TYPE_OGG) luaL_error(L, "only avaible on OGG audio objects"); vorbis_comment *vc = ov_comment(&audio->vf, -1); if (vc == NULL) luaL_error(L, "could not retrieve audio stream comment"); lua_createtable(L, 0, 5); lua_newtable(L); for (int i=0; icomments; i++) { lua_pushstring(L, vc->user_comments[i]); lua_seti(L, -2, i+1); } lua_setfield(L, -2, "userComments"); lua_pushstring(L, vc->vendor); lua_setfield(L, -2, "vendor"); return 1; } /*** Tables return. The detailled table structures returned by some methods of audio objects. @section */ /*** Vorbis bitstream information, returned by audio:info(). If bitrateLower == bitrateNominal == bitrateUpper, the stream is fixed bitrate. @table infoTable @tfield integer version Vorbis encoder version used to create this bitstream @tfield integer channels number of channels in bitstream @tfield integer rate sampling rate of the bitstream @tfield integer bitrateUpper the upper limit in a VBR bitstream; may be unset if no limit exists @tfield integer bitrateNominal the average bitrate for a VBR bitstream; may be unset @tfield integer bitrateLower the lower limit in a VBR bitstream; may be unset if no limit exists */ /*** Vorbis bitstream comment, returned by audio:comment(). @table commentTable @tfield table userComments list of all the user comment @tfield string vendor information about the Vorbis implementation that encoded the file */ // Audio object methods static const struct luaL_Reg audio_object_methods[] = { // common { "duration", audio_object_duration }, { "time", audio_object_time }, { "playing", audio_object_playing }, { "mix", audio_object_mix }, { "interpolation", audio_object_interpolation }, { "speed", audio_object_speed }, { "play", audio_object_play }, { "stop", audio_object_stop }, { "type", audio_object_type }, { "unload", audio_object_unload }, { "__gc", audio_object_unload }, // ogg only { "info", audio_object_info }, { "comment", audio_object_comment }, { NULL, NULL } }; // Library functions static const struct luaL_Reg audio_lib[] = { { "load", audio_load }, { "loadRaw", audio_loadRaw }, { "playing", audio_playing }, { "mix", audio_mix }, { "interpolation", audio_interpolation }, { "speed", audio_speed }, { "stop", audio_stop }, { NULL, NULL } }; int luaopen_audio_lib(lua_State *L) { luaL_newmetatable(L, "LAudio"); lua_pushvalue(L, -1); lua_setfield(L, -2, "__index"); luaL_setfuncs(L, audio_object_methods, 0); luaL_newlib(L, audio_lib); return 1; } void load_audio_lib(lua_State *L) { if (!isAudioInitialized) { isAudioInitialized = !ndspInit(); // ndspInit returns 0 in case of success } luaL_requiref(L, "ctr.audio", luaopen_audio_lib, false); } void unload_audio_lib(lua_State *L) { if (isAudioInitialized) ndspExit(); }