diff --git a/examples/Makefile b/examples/Makefile index 574975d23..0b524015f 100644 --- a/examples/Makefile +++ b/examples/Makefile @@ -297,17 +297,17 @@ ifeq ($(PLATFORM),PLATFORM_WEB) # --preload-file resources # specify a resources folder for data compilation # --source-map-base # allow debugging in browser with source map LDFLAGS += -s USE_GLFW=3 -s TOTAL_MEMORY=$(BUILD_WEB_HEAP_SIZE) -s FORCE_FILESYSTEM=1 - + # Build using asyncify ifeq ($(BUILD_WEB_ASYNCIFY),TRUE) LDFLAGS += -s ASYNCIFY endif - + # Add resources building if required ifeq ($(BUILD_WEB_RESOURCES),TRUE) LDFLAGS += --preload-file $(BUILD_WEB_RESOURCES_PATH) endif - + # Add debug mode flags if required ifeq ($(BUILD_MODE),DEBUG) LDFLAGS += -s ASSERTIONS=1 --profiling @@ -316,7 +316,7 @@ ifeq ($(PLATFORM),PLATFORM_WEB) # Define a custom shell .html and output extension LDFLAGS += --shell-file $(BUILD_WEB_SHELL) EXT = .html - + # NOTE: Simple raylib examples are compiled to be interpreter with asyncify, that way, # we can compile same code for ALL platforms with no change required, but, working on bigger # projects, code needs to be refactored to avoid a blocking while() loop, moving Update and Draw @@ -354,7 +354,7 @@ ifeq ($(PLATFORM),PLATFORM_DESKTOP) ifeq ($(RAYLIB_LIBTYPE),SHARED) LDLIBS += -lc endif - + # NOTE: On ARM 32bit arch, miniaudio requires atomics library LDLIBS += -latomic endif diff --git a/examples/audio/resources/target.qoa b/examples/audio/resources/target.qoa new file mode 100644 index 000000000..d48c4f9f0 Binary files /dev/null and b/examples/audio/resources/target.qoa differ diff --git a/examples/core/core_drop_files.c b/examples/core/core_drop_files.c index cfe03e196..9f6220393 100644 --- a/examples/core/core_drop_files.c +++ b/examples/core/core_drop_files.c @@ -15,6 +15,11 @@ #include "raylib.h" +#include // Required for: calloc(), free() + +#define MAX_FILEPATH_RECORDED 4096 +#define MAX_FILEPATH_SIZE 2048 + //------------------------------------------------------------------------------------ // Program main entry point //------------------------------------------------------------------------------------ @@ -27,7 +32,14 @@ int main(void) InitWindow(screenWidth, screenHeight, "raylib [core] example - drop files"); - FilePathList droppedFiles = { 0 }; + int filePathCounter = 0; + char *filePaths[MAX_FILEPATH_RECORDED] = { 0 }; // We will register a maximum of filepaths + + // Allocate space for the required file paths + for (int i = 0; i < MAX_FILEPATH_RECORDED; i++) + { + filePaths[i] = (char *)RL_CALLOC(MAX_FILEPATH_SIZE, 1); + } SetTargetFPS(60); // Set our game to run at 60 frames-per-second //-------------------------------------------------------------------------------------- @@ -39,11 +51,18 @@ int main(void) //---------------------------------------------------------------------------------- if (IsFileDropped()) { - // Is some files have been previously loaded, unload them - if (droppedFiles.count > 0) UnloadDroppedFiles(droppedFiles); - - // Load new dropped files - droppedFiles = LoadDroppedFiles(); + FilePathList droppedFiles = LoadDroppedFiles(); + + for (int i = 0, offset = filePathCounter; i < droppedFiles.count; i++) + { + if (filePathCounter < (MAX_FILEPATH_RECORDED - 1)) + { + TextCopy(filePaths[offset + i], droppedFiles.paths[i]); + filePathCounter++; + } + } + + UnloadDroppedFiles(droppedFiles); // Unload filepaths from memory } //---------------------------------------------------------------------------------- @@ -53,20 +72,20 @@ int main(void) ClearBackground(RAYWHITE); - if (droppedFiles.count == 0) DrawText("Drop your files to this window!", 100, 40, 20, DARKGRAY); + if (filePathCounter == 0) DrawText("Drop your files to this window!", 100, 40, 20, DARKGRAY); else { DrawText("Dropped files:", 100, 40, 20, DARKGRAY); - for (unsigned int i = 0; i < droppedFiles.count; i++) + for (unsigned int i = 0; i < filePathCounter; i++) { if (i%2 == 0) DrawRectangle(0, 85 + 40*i, screenWidth, 40, Fade(LIGHTGRAY, 0.5f)); else DrawRectangle(0, 85 + 40*i, screenWidth, 40, Fade(LIGHTGRAY, 0.3f)); - DrawText(droppedFiles.paths[i], 120, 100 + 40*i, 10, GRAY); + DrawText(filePaths[i], 120, 100 + 40*i, 10, GRAY); } - DrawText("Drop new files...", 100, 110 + 40*droppedFiles.count, 20, DARKGRAY); + DrawText("Drop new files...", 100, 110 + 40*filePathCounter, 20, DARKGRAY); } EndDrawing(); @@ -75,7 +94,10 @@ int main(void) // De-Initialization //-------------------------------------------------------------------------------------- - UnloadDroppedFiles(droppedFiles); // Unload files memory + for (int i = 0; i < MAX_FILEPATH_RECORDED; i++) + { + RL_FREE(filePaths[i]); // Free allocated memory for all filepaths + } CloseWindow(); // Close window and OpenGL context //-------------------------------------------------------------------------------------- diff --git a/src/config.h b/src/config.h index d5f8e486c..de9f4641a 100644 --- a/src/config.h +++ b/src/config.h @@ -214,7 +214,7 @@ #define SUPPORT_FILEFORMAT_WAV 1 #define SUPPORT_FILEFORMAT_OGG 1 #define SUPPORT_FILEFORMAT_MP3 1 -//#define SUPPORT_FILEFORMAT_QOA 1 +#define SUPPORT_FILEFORMAT_QOA 1 //#define SUPPORT_FILEFORMAT_FLAC 1 #define SUPPORT_FILEFORMAT_XM 1 #define SUPPORT_FILEFORMAT_MOD 1 diff --git a/src/external/qoa.h b/src/external/qoa.h index 65752d1b7..59d90aded 100644 --- a/src/external/qoa.h +++ b/src/external/qoa.h @@ -22,12 +22,12 @@ not in the file header. A decoder may peek into the first frame of the file to find these values. In a valid QOA file all frames have the same number of channels and the same -samplerate. These restriction may be releaxed for streaming. This remains to +samplerate. These restrictions may be relaxed for streaming. This remains to be decided. All values in a QOA file are BIG ENDIAN. Luckily, EVERYTHING in a QOA file, including the headers, is 64 bit aligned, so it's possible to read files with -just a read_u64() that does the byte swapping if neccessary. +just a read_u64() that does the byte swapping if necessary. In pseudocode, the file layout is as follows: @@ -66,7 +66,7 @@ Wheras the 64bit qoa_slice_t is defined as follows: `sf_index` defines the scalefactor to use for this slice as an index into the qoa_scalefactor_tab[16] -`r00`--`r19` are the residuals for the individiual samples, divided by the +`r00`--`r19` are the residuals for the individual samples, divided by the scalefactor and quantized by the qoa_quant_tab[]. In the decoder, a prediction of the next sample is computed by multiplying the @@ -153,7 +153,7 @@ typedef unsigned long long qoa_uint64_t; /* The quant_tab provides an index into the dequant_tab for residuals in the -range of -8 .. 8. It maps this range to just 3bits and becommes less accurate at +range of -8 .. 8. It maps this range to just 3bits and becomes less accurate at the higher end. Note that the residual zero is identical to the lowest positive value. This is mostly fine, since the qoa_div() function always rounds away from zero. */ @@ -169,7 +169,7 @@ static int qoa_quant_tab[17] = { less accurate at the higher end. In theory, the highest scalefactor that we would need to encode the highest 16bit residual is (2**16)/8 = 8192. However we rely on the LMS filter to predict samples accurately enough that a maximum -residual of one quarter of the 16 bit range is high sufficent. I.e. with the +residual of one quarter of the 16 bit range is high sufficient. I.e. with the scalefactor 2048 times the quant range of 8 we can encode residuals up to 2**14. The scalefactor values are computed as: @@ -230,7 +230,7 @@ The next sample is predicted as the sum of (weight[i] * history[i]). The adjustment of the weights is done with a "Sign-Sign-LMS" that adds or subtracts the residual to each weight, based on the corresponding sample from -the history. This, suprisingly, is sufficent to get worthwhile predictions. +the history. This, surprisingly, is sufficient to get worthwhile predictions. This is all done with fixed point integers. Hence the right-shifts when updating the weights and calculating the prediction. */ @@ -369,7 +369,7 @@ unsigned int qoa_encode_frame(const short *sample_data, qoa_desc *qoa, unsigned int dequantized = qoa_dequant_tab[scalefactor][quantized]; int reconstructed = qoa_clamp(predicted + dequantized, -32768, 32767); - int error = (sample - reconstructed); + long long error = (sample - reconstructed); current_error += error * error; if (current_error > best_error) { break; diff --git a/src/external/qoaplay.c b/src/external/qoaplay.c new file mode 100644 index 000000000..549e9f778 --- /dev/null +++ b/src/external/qoaplay.c @@ -0,0 +1,278 @@ +/******************************************************************************************* +* +* qoaplay - QOA stream playing helper functions +* +* qoaplay is a tiny abstraction to read and decode a QOA file "on the fly". +* It reads and decodes one frame at a time with minimal memory requirements. +* qoaplay also provides some functions to seek to a specific frame. +* +* LICENSE: MIT License +* +* Copyright (c) 2023 Dominic Szablewski (@phoboslab), reviewed by Ramon Santamaria (@raysan5) +* +* Permission is hereby granted, free of charge, to any person obtaining a copy +* of this software and associated documentation files (the "Software"), to deal +* in the Software without restriction, including without limitation the rights +* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +* copies of the Software, and to permit persons to whom the Software is +* furnished to do so, subject to the following conditions: +* +* The above copyright notice and this permission notice shall be included in all +* copies or substantial portions of the Software. +* +* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +* SOFTWARE. +* +**********************************************************************************************/ + +//---------------------------------------------------------------------------------- +// Types and Structures Definition +//---------------------------------------------------------------------------------- +// QOA streaming data descriptor +typedef struct { + qoa_desc info; // QOA descriptor data + + FILE *file; // QOA file to read, if NULL, using memory buffer -> file_data + unsigned char *file_data; // QOA file data on memory + unsigned int file_data_size; // QOA file data on memory size + unsigned int file_data_offset; // QOA file data on memory offset for next read + + unsigned int first_frame_pos; // First frame position (after QOA header, required for offset) + unsigned int sample_position; // Current streaming sample position + + unsigned char *buffer; // Buffer used to read samples from file/memory (used on decoding) + unsigned int buffer_len; // Buffer length to read samples for streaming + + short *sample_data; // Sample data decoded + unsigned int sample_data_len; // Sample data decoded length + unsigned int sample_data_pos; // Sample data decoded position + +} qoaplay_desc; + +//---------------------------------------------------------------------------------- +// Module Functions Declaration +//---------------------------------------------------------------------------------- + +#if defined(__cplusplus) +extern "C" { // Prevents name mangling of functions +#endif + +qoaplay_desc *qoaplay_open(char *path); +qoaplay_desc *qoaplay_open_memory(const unsigned char *data, int data_size); +void qoaplay_close(qoaplay_desc *qoa_ctx); + +void qoaplay_rewind(qoaplay_desc *qoa_ctx); +void qoaplay_seek_frame(qoaplay_desc *qoa_ctx, int frame); +unsigned int qoaplay_decode(qoaplay_desc *qoa_ctx, float *sample_data, int num_samples); +unsigned int qoaplay_decode_frame(qoaplay_desc *qoa_ctx); +double qoaplay_get_duration(qoaplay_desc *qoa_ctx); +double qoaplay_get_time(qoaplay_desc *qoa_ctx); +int qoaplay_get_frame(qoaplay_desc *qoa_ctx); + +#if defined(__cplusplus) +} // Prevents name mangling of functions +#endif + +//---------------------------------------------------------------------------------- +// Module Functions Definition +//---------------------------------------------------------------------------------- + +// Open QOA file, keep FILE pointer to keep reading from file +qoaplay_desc *qoaplay_open(char *path) +{ + FILE *file = fopen(path, "rb"); + if (!file) return NULL; + + // Read and decode the file header + unsigned char header[QOA_MIN_FILESIZE]; + int read = fread(header, QOA_MIN_FILESIZE, 1, file); + if (!read) return NULL; + + qoa_desc qoa; + unsigned int first_frame_pos = qoa_decode_header(header, QOA_MIN_FILESIZE, &qoa); + if (!first_frame_pos) return NULL; + + // Rewind the file back to beginning of the first frame + fseek(file, first_frame_pos, SEEK_SET); + + // Allocate one chunk of memory for the qoaplay_desc struct + // + the sample data for one frame + // + a buffer to hold one frame of encoded data + unsigned int buffer_size = qoa_max_frame_size(&qoa); + unsigned int sample_data_size = qoa.channels*QOA_FRAME_LEN*sizeof(short)*2; + qoaplay_desc *qoa_ctx = QOA_MALLOC(sizeof(qoaplay_desc) + buffer_size + sample_data_size); + memset(qoa_ctx, 0, sizeof(qoaplay_desc)); + + qoa_ctx->file = file; + qoa_ctx->file_data = NULL; + qoa_ctx->file_data_size = 0; + qoa_ctx->file_data_offset = 0; + qoa_ctx->first_frame_pos = first_frame_pos; + + // Setup data pointers to previously allocated data + qoa_ctx->buffer = ((unsigned char *)qoa_ctx) + sizeof(qoaplay_desc); + qoa_ctx->sample_data = (short *)(((unsigned char *)qoa_ctx) + sizeof(qoaplay_desc) + buffer_size); + + qoa_ctx->info.channels = qoa.channels; + qoa_ctx->info.samplerate = qoa.samplerate; + qoa_ctx->info.samples = qoa.samples; + + return qoa_ctx; +} + +// Open QOA file from memory, no FILE pointer required +qoaplay_desc *qoaplay_open_memory(const unsigned char *data, int data_size) +{ + // Read and decode the file header + unsigned char header[QOA_MIN_FILESIZE]; + memcpy(header, data, QOA_MIN_FILESIZE); + + qoa_desc qoa; + unsigned int first_frame_pos = qoa_decode_header(header, QOA_MIN_FILESIZE, &qoa); + if (!first_frame_pos) return NULL; + + // Allocate one chunk of memory for the qoaplay_desc struct + // + the sample data for one frame + // + a buffer to hold one frame of encoded data + unsigned int buffer_size = qoa_max_frame_size(&qoa); + unsigned int sample_data_size = qoa.channels*QOA_FRAME_LEN*sizeof(short)*2; + qoaplay_desc *qoa_ctx = QOA_MALLOC(sizeof(qoaplay_desc) + buffer_size + sample_data_size); + memset(qoa_ctx, 0, sizeof(qoaplay_desc)); + + qoa_ctx->file = NULL; + + // Keep a copy of file data provided to be managed internally + qoa_ctx->file_data = (unsigned char *)QOA_MALLOC(data_size); + memcpy(qoa_ctx->file_data, data, data_size); + qoa_ctx->file_data_size = data_size; + qoa_ctx->file_data_offset = 0; + qoa_ctx->first_frame_pos = first_frame_pos; + + // Setup data pointers to previously allocated data + qoa_ctx->buffer = ((unsigned char *)qoa_ctx) + sizeof(qoaplay_desc); + qoa_ctx->sample_data = (short *)(((unsigned char *)qoa_ctx) + sizeof(qoaplay_desc) + buffer_size); + + qoa_ctx->info.channels = qoa.channels; + qoa_ctx->info.samplerate = qoa.samplerate; + qoa_ctx->info.samples = qoa.samples; + + return qoa_ctx; +} + +// Close QOA file (if open) and free internal memory +void qoaplay_close(qoaplay_desc *qoa_ctx) +{ + if (qoa_ctx->file) fclose(qoa_ctx->file); + + if ((qoa_ctx->file_data) && (qoa_ctx->file_data_size > 0)) + { + QOA_FREE(qoa_ctx->file_data); + qoa_ctx->file_data_size = 0; + } + + QOA_FREE(qoa_ctx); +} + +// Decode one frame from QOA data +unsigned int qoaplay_decode_frame(qoaplay_desc *qoa_ctx) +{ + if (qoa_ctx->file) qoa_ctx->buffer_len = fread(qoa_ctx->buffer, 1, qoa_max_frame_size(&qoa_ctx->info), qoa_ctx->file); + else + { + qoa_ctx->buffer_len = qoa_max_frame_size(&qoa_ctx->info); + memcpy(qoa_ctx->buffer, qoa_ctx->file_data + qoa_ctx->file_data_offset, qoa_ctx->buffer_len); + qoa_ctx->file_data_offset += qoa_ctx->buffer_len; + } + + unsigned int frame_len; + qoa_decode_frame(qoa_ctx->buffer, qoa_ctx->buffer_len, &qoa_ctx->info, qoa_ctx->sample_data, &frame_len); + qoa_ctx->sample_data_pos = 0; + qoa_ctx->sample_data_len = frame_len; + + return frame_len; +} + +// Rewind QOA file or memory pointer to beginning +void qoaplay_rewind(qoaplay_desc *qoa_ctx) +{ + if (qoa_ctx->file) fseek(qoa_ctx->file, qoa_ctx->first_frame_pos, SEEK_SET); + else qoa_ctx->file_data_offset = 0; + + qoa_ctx->sample_position = 0; + qoa_ctx->sample_data_len = 0; + qoa_ctx->sample_data_pos = 0; +} + +// Decode required QOA frames +unsigned int qoaplay_decode(qoaplay_desc *qoa_ctx, float *sample_data, int num_samples) +{ + int src_index = qoa_ctx->sample_data_pos*qoa_ctx->info.channels; + int dst_index = 0; + + for (int i = 0; i < num_samples; i++) + { + // Do we have to decode more samples? + if (qoa_ctx->sample_data_len - qoa_ctx->sample_data_pos == 0) + { + if (!qoaplay_decode_frame(qoa_ctx)) + { + // Loop to the beginning + qoaplay_rewind(qoa_ctx); + qoaplay_decode_frame(qoa_ctx); + } + + src_index = 0; + } + + // Normalize to -1..1 floats and write to dest + for (int c = 0; c < qoa_ctx->info.channels; c++) + { + sample_data[dst_index++] = qoa_ctx->sample_data[src_index++]/32768.0; + } + + qoa_ctx->sample_data_pos++; + qoa_ctx->sample_position++; + } + + return num_samples; +} + +// Get QOA total time duration in seconds +double qoaplay_get_duration(qoaplay_desc *qoa_ctx) +{ + return (double)qoa_ctx->info.samples/(double)qoa_ctx->info.samplerate; +} + +// Get QOA current time position in seconds +double qoaplay_get_time(qoaplay_desc *qoa_ctx) +{ + return (double)qoa_ctx->sample_position/(double)qoa_ctx->info.samplerate; +} + +// Get QOA current audio frame +int qoaplay_get_frame(qoaplay_desc *qoa_ctx) +{ + return qoa_ctx->sample_position/QOA_FRAME_LEN; +} + +// Seek QOA audio frame +void qoaplay_seek_frame(qoaplay_desc *qoa_ctx, int frame) +{ + if (frame < 0) frame = 0; + + if (frame > qoa_ctx->info.samples/QOA_FRAME_LEN) frame = qoa_ctx->info.samples/QOA_FRAME_LEN; + + qoa_ctx->sample_position = frame*QOA_FRAME_LEN; + qoa_ctx->sample_data_len = 0; + qoa_ctx->sample_data_pos = 0; + + unsigned int offset = qoa_ctx->first_frame_pos + frame*qoa_max_frame_size(&qoa_ctx->info); + + if (qoa_ctx->file) fseek(qoa_ctx->file, offset, SEEK_SET); + else qoa_ctx->file_data_offset = offset; +} diff --git a/src/raudio.c b/src/raudio.c index 04386167d..8bd9052c2 100644 --- a/src/raudio.c +++ b/src/raudio.c @@ -168,6 +168,10 @@ typedef struct tagBITMAPINFOHEADER { #define MA_NO_WAV #define MA_NO_FLAC #define MA_NO_MP3 + +// Threading model: Default: [0] COINIT_MULTITHREADED: COM calls objects on any thread (free threading) +#define MA_COINIT_VALUE 2 // [2] COINIT_APARTMENTTHREADED: Each object has its own thread (apartment model) + #define MINIAUDIO_IMPLEMENTATION //#define MA_DEBUG_OUTPUT #include "external/miniaudio.h" // Audio device initialization and management @@ -226,6 +230,7 @@ typedef struct tagBITMAPINFOHEADER { #define QOA_IMPLEMENTATION #include "external/qoa.h" // QOA loading and saving functions + #include "external/qoaplay.c" // QOA stream playing helper functions #endif #if defined(SUPPORT_FILEFORMAT_FLAC) @@ -283,21 +288,6 @@ typedef struct tagBITMAPINFOHEADER { //---------------------------------------------------------------------------------- // Types and Structures Definition //---------------------------------------------------------------------------------- - -// Music context type -// NOTE: Depends on data structure provided by the library -// in charge of reading the different file types -typedef enum { - MUSIC_AUDIO_NONE = 0, // No audio context loaded - MUSIC_AUDIO_WAV, // WAV audio context - MUSIC_AUDIO_OGG, // OGG audio context - MUSIC_AUDIO_FLAC, // FLAC audio context - MUSIC_AUDIO_MP3, // MP3 audio context - MUSIC_AUDIO_QOA, // QOA audio context - MUSIC_MODULE_XM, // XM module audio context - MUSIC_MODULE_MOD // MOD module audio context -} MusicContextType; - #if defined(RAUDIO_STANDALONE) // Trace log level // NOTE: Organized by priority level @@ -313,6 +303,20 @@ typedef enum { } TraceLogLevel; #endif +// Music context type +// NOTE: Depends on data structure provided by the library +// in charge of reading the different file types +typedef enum { + MUSIC_AUDIO_NONE = 0, // No audio context loaded + MUSIC_AUDIO_WAV, // WAV audio context + MUSIC_AUDIO_OGG, // OGG audio context + MUSIC_AUDIO_FLAC, // FLAC audio context + MUSIC_AUDIO_MP3, // MP3 audio context + MUSIC_AUDIO_QOA, // QOA audio context + MUSIC_MODULE_XM, // XM module audio context + MUSIC_MODULE_MOD // MOD module audio context +} MusicContextType; + // NOTE: Different logic is used when feeding data to the playback device // depending on whether data is streamed (Music vs Sound) typedef enum { @@ -999,13 +1003,17 @@ bool ExportWave(Wave wave, const char *fileName) #if defined(SUPPORT_FILEFORMAT_QOA) else if (IsFileExtension(fileName, ".qoa")) { - qoa_desc qoa = { 0 }; - qoa.channels = wave.channels; - qoa.samplerate = wave.sampleRate; - qoa.samples = wave.frameCount; + if (wave.sampleSize == 16) + { + qoa_desc qoa = { 0 }; + qoa.channels = wave.channels; + qoa.samplerate = wave.sampleRate; + qoa.samples = wave.frameCount; - // TODO: Review wave.data format required for export - success = qoa_write(fileName, wave.data, &qoa); + int bytesWritten = qoa_write(fileName, wave.data, &qoa); + if (bytesWritten > 0) success = true; + } + else TRACELOG(LOG_WARNING, "AUDIO: Wave data must be 16 bit per sample for QOA format export"); } #endif else if (IsFileExtension(fileName, ".raw")) @@ -1314,7 +1322,7 @@ void UnloadWaveSamples(float *samples) } //---------------------------------------------------------------------------------- -// Module Functions Definition - Music loading and stream playing (.OGG) +// Module Functions Definition - Music loading and stream playing //---------------------------------------------------------------------------------- // Load music stream from file @@ -1387,21 +1395,16 @@ Music LoadMusicStream(const char *fileName) #if defined(SUPPORT_FILEFORMAT_QOA) else if (IsFileExtension(fileName, ".qoa")) { - qoa_desc *ctxQoa = RL_CALLOC(1, sizeof(qoa_desc)); - - // TODO: QOA stream support: Init context from file - int result = 0; - + qoaplay_desc *ctxQoa = qoaplay_open(fileName); music.ctxType = MUSIC_AUDIO_QOA; music.ctxData = ctxQoa; - if (result > 0) + if (ctxQoa->file != NULL) { - music.stream = LoadAudioStream(ctxQoa->samplerate, 16, ctxQoa->channels); - - // TODO: Read next frame(s) from QOA stream - //music.frameCount = qoa_decode_frame(const unsigned char *bytes, unsigned int size, ctxQoa, short *sample_data, unsigned int *frame_len); - + // NOTE: We are loading samples are 32bit float normalized data, so, + // we configure the output audio stream to also use float 32bit + music.stream = LoadAudioStream(ctxQoa->info.samplerate, 32, ctxQoa->info.channels); + music.frameCount = ctxQoa->info.samples; music.looping = true; // Looping enabled by default musicLoaded = true; } @@ -1586,21 +1589,16 @@ Music LoadMusicStreamFromMemory(const char *fileType, const unsigned char *data, #if defined(SUPPORT_FILEFORMAT_QOA) else if (strcmp(fileType, ".qoa") == 0) { - qoa_desc *ctxQoa = RL_CALLOC(1, sizeof(qoa_desc)); - - // TODO: Init QOA context data - int result = 0; - + qoaplay_desc *ctxQoa = qoaplay_open_memory(data, dataSize); music.ctxType = MUSIC_AUDIO_QOA; music.ctxData = ctxQoa; - if (result > 0) + if ((ctxQoa->file_data != NULL) && (ctxQoa->file_data_size != 0)) { - music.stream = LoadAudioStream(ctxQoa->samplerate, 16, ctxQoa->channels); - - // TODO: Read next frame(s) from QOA stream - //music.frameCount = qoa_decode_frame(const unsigned char *bytes, unsigned int size, ctxQoa, short *sample_data, unsigned int *frame_len); - + // NOTE: We are loading samples are 32bit float normalized data, so, + // we configure the output audio stream to also use float 32bit + music.stream = LoadAudioStream(ctxQoa->info.samplerate, 32, ctxQoa->info.channels); + music.frameCount = ctxQoa->info.samples; music.looping = true; // Looping enabled by default musicLoaded = true; } @@ -1689,27 +1687,27 @@ Music LoadMusicStreamFromMemory(const char *fileType, const unsigned char *data, if (!musicLoaded) { if (false) { } - #if defined(SUPPORT_FILEFORMAT_WAV) +#if defined(SUPPORT_FILEFORMAT_WAV) else if (music.ctxType == MUSIC_AUDIO_WAV) drwav_uninit((drwav *)music.ctxData); - #endif - #if defined(SUPPORT_FILEFORMAT_OGG) +#endif +#if defined(SUPPORT_FILEFORMAT_OGG) else if (music.ctxType == MUSIC_AUDIO_OGG) stb_vorbis_close((stb_vorbis *)music.ctxData); - #endif - #if defined(SUPPORT_FILEFORMAT_MP3) +#endif +#if defined(SUPPORT_FILEFORMAT_MP3) else if (music.ctxType == MUSIC_AUDIO_MP3) { drmp3_uninit((drmp3 *)music.ctxData); RL_FREE(music.ctxData); } - #endif - #if defined(SUPPORT_FILEFORMAT_QOA) - else if (music.ctxType == MUSIC_AUDIO_QOA) { /*TODO: Release QOA context*/ RL_FREE(music.ctxData); } - #endif - #if defined(SUPPORT_FILEFORMAT_FLAC) +#endif +#if defined(SUPPORT_FILEFORMAT_QOA) + else if (music.ctxType == MUSIC_AUDIO_QOA) qoaplay_close((qoaplay_desc *)music.ctxData); +#endif +#if defined(SUPPORT_FILEFORMAT_FLAC) else if (music.ctxType == MUSIC_AUDIO_FLAC) drflac_free((drflac *)music.ctxData, NULL); - #endif - #if defined(SUPPORT_FILEFORMAT_XM) +#endif +#if defined(SUPPORT_FILEFORMAT_XM) else if (music.ctxType == MUSIC_MODULE_XM) jar_xm_free_context((jar_xm_context_t *)music.ctxData); - #endif - #if defined(SUPPORT_FILEFORMAT_MOD) +#endif +#if defined(SUPPORT_FILEFORMAT_MOD) else if (music.ctxType == MUSIC_MODULE_MOD) { jar_mod_unload((jar_mod_context_t *)music.ctxData); RL_FREE(music.ctxData); } - #endif +#endif music.ctxData = NULL; TRACELOG(LOG_WARNING, "FILEIO: Music data could not be loaded"); @@ -1755,7 +1753,7 @@ void UnloadMusicStream(Music music) else if (music.ctxType == MUSIC_AUDIO_MP3) { drmp3_uninit((drmp3 *)music.ctxData); RL_FREE(music.ctxData); } #endif #if defined(SUPPORT_FILEFORMAT_QOA) - else if (music.ctxType == MUSIC_AUDIO_QOA) { /*TODO: Release QOA context*/ RL_FREE(music.ctxData); } + else if (music.ctxType == MUSIC_AUDIO_QOA) qoaplay_close((qoaplay_desc *)music.ctxData); #endif #if defined(SUPPORT_FILEFORMAT_FLAC) else if (music.ctxType == MUSIC_AUDIO_FLAC) drflac_free((drflac *)music.ctxData, NULL); @@ -1813,7 +1811,7 @@ void StopMusicStream(Music music) case MUSIC_AUDIO_MP3: drmp3_seek_to_start_of_stream((drmp3 *)music.ctxData); break; #endif #if defined(SUPPORT_FILEFORMAT_QOA) - case MUSIC_AUDIO_QOA: /*TODO: Restart QOA context to beginning*/ break; + case MUSIC_AUDIO_QOA: qoaplay_rewind((qoaplay_desc *)music.ctxData); break; #endif #if defined(SUPPORT_FILEFORMAT_FLAC) case MUSIC_AUDIO_FLAC: drflac__seek_to_first_frame((drflac *)music.ctxData); break; @@ -1848,7 +1846,7 @@ void SeekMusicStream(Music music, float position) case MUSIC_AUDIO_MP3: drmp3_seek_to_pcm_frame((drmp3 *)music.ctxData, positionInFrames); break; #endif #if defined(SUPPORT_FILEFORMAT_QOA) - case MUSIC_AUDIO_QOA: /*TODO: Seek to specific QOA frame*/ break; + case MUSIC_AUDIO_QOA: qoaplay_seek_frame((qoaplay_desc *)music.ctxData, positionInFrames); break; #endif #if defined(SUPPORT_FILEFORMAT_FLAC) case MUSIC_AUDIO_FLAC: drflac_seek_to_pcm_frame((drflac *)music.ctxData, positionInFrames); break; @@ -1884,11 +1882,13 @@ void UpdateMusicStream(Music music) unsigned int framesLeft = music.frameCount - music.stream.buffer->framesProcessed; // Frames left to be processed unsigned int framesToStream = 0; // Total frames to be streamed + if ((framesLeft >= subBufferSizeInFrames) || music.looping) framesToStream = subBufferSizeInFrames; else framesToStream = framesLeft; int frameCountStillNeeded = framesToStream; - int frameCountRedTotal = 0; + int frameCountReadTotal = 0; + switch (music.ctxType) { #if defined(SUPPORT_FILEFORMAT_WAV) @@ -1898,8 +1898,8 @@ void UpdateMusicStream(Music music) { while (true) { - int frameCountRed = (int)drwav_read_pcm_frames_s16((drwav *)music.ctxData, frameCountStillNeeded, (short *)((char *)AUDIO.System.pcmBuffer + frameCountRedTotal*frameSize)); - frameCountRedTotal += frameCountRed; + int frameCountRed = (int)drwav_read_pcm_frames_s16((drwav *)music.ctxData, frameCountStillNeeded, (short *)((char *)AUDIO.System.pcmBuffer + frameCountReadTotal*frameSize)); + frameCountReadTotal += frameCountRed; frameCountStillNeeded -= frameCountRed; if (frameCountStillNeeded == 0) break; else drwav_seek_to_first_pcm_frame((drwav *)music.ctxData); @@ -1909,8 +1909,8 @@ void UpdateMusicStream(Music music) { while (true) { - int frameCountRed = (int)drwav_read_pcm_frames_f32((drwav *)music.ctxData, frameCountStillNeeded, (float *)((char *)AUDIO.System.pcmBuffer + frameCountRedTotal*frameSize)); - frameCountRedTotal += frameCountRed; + int frameCountRed = (int)drwav_read_pcm_frames_f32((drwav *)music.ctxData, frameCountStillNeeded, (float *)((char *)AUDIO.System.pcmBuffer + frameCountReadTotal*frameSize)); + frameCountReadTotal += frameCountRed; frameCountStillNeeded -= frameCountRed; if (frameCountStillNeeded == 0) break; else drwav_seek_to_first_pcm_frame((drwav *)music.ctxData); @@ -1923,8 +1923,8 @@ void UpdateMusicStream(Music music) { while (true) { - int frameCountRed = stb_vorbis_get_samples_short_interleaved((stb_vorbis *)music.ctxData, music.stream.channels, (short *)((char *)AUDIO.System.pcmBuffer + frameCountRedTotal*frameSize), frameCountStillNeeded*music.stream.channels); - frameCountRedTotal += frameCountRed; + int frameCountRed = stb_vorbis_get_samples_short_interleaved((stb_vorbis *)music.ctxData, music.stream.channels, (short *)((char *)AUDIO.System.pcmBuffer + frameCountReadTotal*frameSize), frameCountStillNeeded*music.stream.channels); + frameCountReadTotal += frameCountRed; frameCountStillNeeded -= frameCountRed; if (frameCountStillNeeded == 0) break; else stb_vorbis_seek_start((stb_vorbis *)music.ctxData); @@ -1936,9 +1936,9 @@ void UpdateMusicStream(Music music) { while (true) { - int frameCountRed = (int)drmp3_read_pcm_frames_f32((drmp3 *)music.ctxData, frameCountStillNeeded, (float *)((char *)AUDIO.System.pcmBuffer + frameCountRedTotal*frameSize)); - frameCountRedTotal += frameCountRed; - frameCountStillNeeded -= frameCountRed; + int frameCountRead = (int)drmp3_read_pcm_frames_f32((drmp3 *)music.ctxData, frameCountStillNeeded, (float *)((char *)AUDIO.System.pcmBuffer + frameCountReadTotal*frameSize)); + frameCountReadTotal += frameCountRead; + frameCountStillNeeded -= frameCountRead; if (frameCountStillNeeded == 0) break; else drmp3_seek_to_start_of_stream((drmp3 *)music.ctxData); } @@ -1947,7 +1947,18 @@ void UpdateMusicStream(Music music) #if defined(SUPPORT_FILEFORMAT_QOA) case MUSIC_AUDIO_QOA: { - // TODO: Read QOA required framecount to fill buffer to keep music playing + unsigned int frameCountRead = qoaplay_decode((qoaplay_desc *)music.ctxData, (float *)AUDIO.System.pcmBuffer, framesToStream); + frameCountReadTotal += frameCountRead; + /* + while (true) + { + int frameCountRead = (int)qoaplay_decode((qoaplay_desc *)music.ctxData, (float *)((char *)AUDIO.System.pcmBuffer + frameCountReadTotal*frameSize), frameCountStillNeeded); + frameCountReadTotal += frameCountRead; + frameCountStillNeeded -= frameCountRead; + if (frameCountStillNeeded == 0) break; + else qoaplay_rewind((qoaplay_desc *)music.ctxData); + } + */ } break; #endif #if defined(SUPPORT_FILEFORMAT_FLAC) @@ -1955,9 +1966,9 @@ void UpdateMusicStream(Music music) { while (true) { - int frameCountRed = drflac_read_pcm_frames_s16((drflac *)music.ctxData, frameCountStillNeeded, (short *)((char *)AUDIO.System.pcmBuffer + frameCountRedTotal*frameSize)); - frameCountRedTotal += frameCountRed; - frameCountStillNeeded -= frameCountRed; + int frameCountRead = drflac_read_pcm_frames_s16((drflac *)music.ctxData, frameCountStillNeeded, (short *)((char *)AUDIO.System.pcmBuffer + frameCountReadTotal*frameSize)); + frameCountReadTotal += frameCountRead; + frameCountStillNeeded -= frameCountRead; if (frameCountStillNeeded == 0) break; else drflac__seek_to_first_frame((drflac *)music.ctxData); } @@ -1970,7 +1981,6 @@ void UpdateMusicStream(Music music) if (AUDIO_DEVICE_FORMAT == ma_format_f32) jar_xm_generate_samples((jar_xm_context_t *)music.ctxData, (float *)AUDIO.System.pcmBuffer, framesToStream); else if (AUDIO_DEVICE_FORMAT == ma_format_s16) jar_xm_generate_samples_16bit((jar_xm_context_t *)music.ctxData, (short *)AUDIO.System.pcmBuffer, framesToStream); else if (AUDIO_DEVICE_FORMAT == ma_format_u8) jar_xm_generate_samples_8bit((jar_xm_context_t *)music.ctxData, (char *)AUDIO.System.pcmBuffer, framesToStream); - //jar_xm_reset((jar_xm_context_t *)music.ctxData); } break; @@ -1980,7 +1990,6 @@ void UpdateMusicStream(Music music) { // NOTE: 3rd parameter (nbsample) specify the number of stereo 16bits samples you want, so sampleCount/2 jar_mod_fillbuffer((jar_mod_context_t *)music.ctxData, (short *)AUDIO.System.pcmBuffer, framesToStream, 0); - //jar_mod_seek_start((jar_mod_context_t *)music.ctxData); } break; @@ -2048,7 +2057,7 @@ float GetMusicTimePlayed(Music music) float secondsPlayed = 0.0f; if (music.stream.buffer != NULL) { - #if defined(SUPPORT_FILEFORMAT_XM) +#if defined(SUPPORT_FILEFORMAT_XM) if (music.ctxType == MUSIC_MODULE_XM) { uint64_t framesPlayed = 0; @@ -2057,7 +2066,7 @@ float GetMusicTimePlayed(Music music) secondsPlayed = (float)framesPlayed/music.stream.sampleRate; } else - #endif +#endif { //ma_uint32 frameSizeInBytes = ma_get_bytes_per_sample(music.stream.buffer->dsp.formatConverterIn.config.formatIn)*music.stream.buffer->dsp.formatConverterIn.config.channels; int framesProcessed = (int)music.stream.buffer->framesProcessed; @@ -2107,7 +2116,7 @@ AudioStream LoadAudioStream(unsigned int sampleRate, unsigned int sampleSize, un } // Checks if an audio stream is ready -RLAPI bool IsAudioStreamReady(AudioStream stream) +bool IsAudioStreamReady(AudioStream stream) { return ((stream.buffer != NULL) && // Validate stream buffer (stream.sampleRate > 0) && // Validate sample rate is supported @@ -2269,6 +2278,7 @@ void AttachAudioStreamProcessor(AudioStream stream, AudioCallback process) ma_mutex_unlock(&AUDIO.System.lock); } +// Remove processor from audio stream void DetachAudioStreamProcessor(AudioStream stream, AudioCallback process) { ma_mutex_lock(&AUDIO.System.lock); @@ -2296,9 +2306,8 @@ void DetachAudioStreamProcessor(AudioStream stream, AudioCallback process) } // Add processor to audio pipeline. Order of processors is important -// Works the same way as {Attach,Detach}AudioStreamProcessor functions, except -// these two work on the already mixed output just before sending it to the -// sound hardware. +// Works the same way as {Attach,Detach}AudioStreamProcessor() functions, except +// these two work on the already mixed output just before sending it to the sound hardware void AttachAudioMixedProcessor(AudioCallback process) { ma_mutex_lock(&AUDIO.System.lock); @@ -2322,6 +2331,7 @@ void AttachAudioMixedProcessor(AudioCallback process) ma_mutex_unlock(&AUDIO.System.lock); } +// Remove processor from audio pipeline void DetachAudioMixedProcessor(AudioCallback process) { ma_mutex_lock(&AUDIO.System.lock); @@ -2500,7 +2510,6 @@ static ma_uint32 ReadAudioBufferFramesInMixingFormat(AudioBuffer *audioBuffer, f return totalOutputFramesProcessed; } - // Sending audio data to device callback function // This function will be called when miniaudio needs more data // NOTE: All the mixing takes place here diff --git a/src/rcamera.h b/src/rcamera.h index 6aee26f38..c3a101f0b 100644 --- a/src/rcamera.h +++ b/src/rcamera.h @@ -160,7 +160,7 @@ Matrix GetCameraProjectionMatrix(Camera* camera, float aspect); // MatrixOrtho() // MatrixIdentity() -// raylib required functionality: +// raylib required functionality: // GetMouseDelta() // GetMouseWheelMove() // IsKeyDown() @@ -223,7 +223,7 @@ Vector3 GetCameraRight(Camera *camera) { Vector3 forward = GetCameraForward(camera); Vector3 up = GetCameraUp(camera); - + return Vector3CrossProduct(forward, up); } @@ -251,7 +251,7 @@ void CameraMoveForward(Camera *camera, float distance, bool moveInWorldPlane) void CameraMoveUp(Camera *camera, float distance) { Vector3 up = GetCameraUp(camera); - + // Scale by distance up = Vector3Scale(up, distance); @@ -410,7 +410,7 @@ Matrix GetCameraProjectionMatrix(Camera *camera, float aspect) return MatrixOrtho(-right, right, -top, top, CAMERA_CULL_DISTANCE_NEAR, CAMERA_CULL_DISTANCE_FAR); } - + return MatrixIdentity(); } @@ -425,7 +425,7 @@ void UpdateCamera(Camera *camera, int mode) bool rotateAroundTarget = ((mode == CAMERA_THIRD_PERSON) || (mode == CAMERA_ORBITAL)); bool lockView = ((mode == CAMERA_FIRST_PERSON) || (mode == CAMERA_THIRD_PERSON) || (mode == CAMERA_ORBITAL)); bool rotateUp = (mode == CAMERA_FREE); - + if (mode == CAMERA_ORBITAL) { // Orbital can just orbit @@ -446,7 +446,7 @@ void UpdateCamera(Camera *camera, int mode) CameraYaw(camera, -mousePositionDelta.x*CAMERA_MOUSE_MOVE_SENSITIVITY, rotateAroundTarget); CameraPitch(camera, -mousePositionDelta.y*CAMERA_MOUSE_MOVE_SENSITIVITY, lockView, rotateAroundTarget, rotateUp); - + // Camera movement if (IsKeyDown(KEY_W)) CameraMoveForward(camera, CAMERA_MOVE_SPEED, moveInWorldPlane); if (IsKeyDown(KEY_A)) CameraMoveRight(camera, -CAMERA_MOVE_SPEED, moveInWorldPlane); diff --git a/src/rcore.c b/src/rcore.c index 7a5f106dd..84ff72878 100644 --- a/src/rcore.c +++ b/src/rcore.c @@ -1558,7 +1558,7 @@ void ClearWindowState(unsigned int flags) // Set icon for window (only PLATFORM_DESKTOP) // NOTE 1: Image must be in RGBA format, 8bit per channel -// NOTE 2: Image is scaled by the OS for all required sizes +// NOTE 2: Image is scaled by the OS for all required sizes void SetWindowIcon(Image image) { #if defined(PLATFORM_DESKTOP) @@ -1589,7 +1589,7 @@ void SetWindowIcon(Image image) // Set icon for window (multiple images, only PLATFORM_DESKTOP) // NOTE 1: Images must be in RGBA format, 8bit per channel // NOTE 2: The multiple images are used depending on provided sizes -// Standard Windows icon sizes: 256, 128, 96, 64, 48, 32, 24, 16 +// Standard Windows icon sizes: 256, 128, 96, 64, 48, 32, 24, 16 void SetWindowIcons(Image *images, int count) { #if defined(PLATFORM_DESKTOP) @@ -1716,12 +1716,13 @@ void *GetWindowHandle(void) // NOTE: Returned handle is: void *HWND (windows.h) return glfwGetWin32Window(CORE.Window.handle); #endif -#if defined(__linux__) +#if defined(PLATFORM_DESKTOP) && defined(__linux__) // NOTE: Returned handle is: unsigned long Window (X.h) // typedef unsigned long XID; // typedef XID Window; //unsigned long id = (unsigned long)glfwGetX11Window(window); - return NULL; // TODO: Find a way to return value... cast to void *? + //return NULL; // TODO: Find a way to return value... cast to void *? + return (void *)CORE.Window.handle; #endif #if defined(__APPLE__) // NOTE: Returned handle is: (objc_object *) @@ -2615,7 +2616,7 @@ bool IsShaderReady(Shader shader) // The following locations are tried to be set automatically (locs[i] >= 0), // any of them can be checked for validation but the only mandatory one is, afaik, SHADER_LOC_VERTEX_POSITION // NOTE: Users can also setup manually their own attributes/uniforms and do not used the default raylib ones - + // Vertex shader attribute locations (default) // shader.locs[SHADER_LOC_VERTEX_POSITION] // Set by default internal shader // shader.locs[SHADER_LOC_VERTEX_TEXCOORD01] // Set by default internal shader @@ -5633,25 +5634,28 @@ static void CursorEnterCallback(GLFWwindow *window, int enter) // GLFW3 Window Drop Callback, runs when drop files into window static void WindowDropCallback(GLFWwindow *window, int count, const char **paths) { - // In case previous dropped filepaths have not been freed, we free them - if (CORE.Window.dropFileCount > 0) + if (count > 0) { - for (unsigned int i = 0; i < CORE.Window.dropFileCount; i++) RL_FREE(CORE.Window.dropFilepaths[i]); + // In case previous dropped filepaths have not been freed, we free them + if (CORE.Window.dropFileCount > 0) + { + for (unsigned int i = 0; i < CORE.Window.dropFileCount; i++) RL_FREE(CORE.Window.dropFilepaths[i]); - RL_FREE(CORE.Window.dropFilepaths); + RL_FREE(CORE.Window.dropFilepaths); - CORE.Window.dropFileCount = 0; - CORE.Window.dropFilepaths = NULL; - } + CORE.Window.dropFileCount = 0; + CORE.Window.dropFilepaths = NULL; + } - // WARNING: Paths are freed by GLFW when the callback returns, we must keep an internal copy - CORE.Window.dropFileCount = count; - CORE.Window.dropFilepaths = (char **)RL_CALLOC(CORE.Window.dropFileCount, sizeof(char *)); + // WARNING: Paths are freed by GLFW when the callback returns, we must keep an internal copy + CORE.Window.dropFileCount = count; + CORE.Window.dropFilepaths = (char **)RL_CALLOC(CORE.Window.dropFileCount, sizeof(char *)); - for (unsigned int i = 0; i < CORE.Window.dropFileCount; i++) - { - CORE.Window.dropFilepaths[i] = (char *)RL_CALLOC(MAX_FILEPATH_LENGTH, sizeof(char)); - strcpy(CORE.Window.dropFilepaths[i], paths[i]); + for (unsigned int i = 0; i < CORE.Window.dropFileCount; i++) + { + CORE.Window.dropFilepaths[i] = (char *)RL_CALLOC(MAX_FILEPATH_LENGTH, sizeof(char)); + strcpy(CORE.Window.dropFilepaths[i], paths[i]); + } } } #endif diff --git a/src/rtextures.c b/src/rtextures.c index f9b5c862d..56b200ce8 100644 --- a/src/rtextures.c +++ b/src/rtextures.c @@ -320,7 +320,7 @@ Image LoadImageAnim(const char *fileName, int *frames) #else if (false) { } #endif - else + else { image = LoadImage(fileName); frameCount = 1; @@ -507,7 +507,7 @@ Image LoadImageFromScreen(void) bool IsImageReady(Image image) { return ((image.data != NULL) && // Validate pixel data available - (image.width > 0) && + (image.width > 0) && (image.height > 0) && // Validate image size (image.format > 0) && // Validate image format (image.mipmaps > 0)); // Validate image mipmaps (at least 1 for basic mipmap level) @@ -3340,10 +3340,10 @@ RenderTexture2D LoadRenderTexture(int width, int height) bool IsTextureReady(Texture2D texture) { // TODO: Validate maximum texture size supported by GPU? - + return ((texture.id > 0) && // Validate OpenGL id - (texture.width > 0) && - (texture.height > 0) && // Validate texture size + (texture.width > 0) && + (texture.height > 0) && // Validate texture size (texture.format > 0) && // Validate texture pixel format (texture.mipmaps > 0)); // Validate texture mipmaps (at least 1 for basic mipmap level) }