2022-08-26 15:15:16 +00:00
//----------------------------------------------------------------------------------------------------
// ___ ___ __ _ _ ___ ___ _ _ _ ___ _
// / _ \| _ ) / /| | || _ \ __| /_\ _ _ __| (_)___ | __|_ _ __ _(_)_ _ ___
// | (_) | _ \/ _ \_ _| _/ _| / _ \ || / _` | / _ \ | _|| ' \/ _` | | ' \/ -_)
// \__\_\___/\___/ |_||_| |___| /_/ \_\_,_\__,_|_\___/ |___|_||_\__, |_|_||_\___|
// |___/
//
// QB64-PE Audio Engine powered by miniaudio (https://miniaud.io/)
//
//-----------------------------------------------------------------------------------------------------
2022-12-21 06:17:41 +00:00
// Set this to 1 if we want to print debug messages to stderr
2023-04-10 20:19:14 +00:00
# define AUDIO_DEBUG 1
2022-12-21 06:17:41 +00:00
# include "audio.h"
// We need 'qbs' and 'mem' stuff from here. This should eventually change when things are moved to smaller, logical and self-contained files
2022-08-26 15:15:16 +00:00
# include "../../libqb.h"
2023-04-20 18:32:40 +00:00
# define STB_VORBIS_HEADER_ONLY
# include "extras/stb_vorbis.c"
# include "miniaudio.h"
# include "mutex.h"
# include <algorithm>
# include <stack>
# include <unordered_map>
# include <vector>
2022-08-26 15:15:16 +00:00
// This should be defined elsewhere (in libqb?). Since it is not, we are doing it here
# define INVALID_MEM_LOCK 1073741821
// This should be defined elsewhere (in libqb?). Since it is not, we are doing it here
# define MEM_TYPE_SOUND 5
// In QuickBASIC false means 0 and true means -1 (sad, but true XD)
# define QB_FALSE MA_FALSE
# define QB_TRUE -MA_TRUE
2022-09-09 22:38:31 +00:00
// This is returned to the caller if handle allocation fails with a -1
// AllocateSoundHandle() does not return 0 because it is a valid internal handle
// Handle 0 is 'handled' as a special case
# define INVALID_SOUND_HANDLE 0
2022-12-22 15:12:27 +00:00
// This is the string that can be passed in the requirements parameter to stream a sound from storage
2022-08-26 15:15:16 +00:00
# define REQUIREMENT_STRING_STREAM "STREAM"
2022-12-22 15:12:27 +00:00
// This is the string that can be passed in the requirements parameter to load a sound from memory
# define REQUIREMENT_STRING_MEMORY "MEMORY"
2022-08-26 15:15:16 +00:00
# define SAMPLE_FRAME_SIZE(_type_, _channels_) (sizeof(_type_) * (_channels_))
2023-04-10 20:19:14 +00:00
# define CLAMP(_x_, _low_, _high_) (((_x_) > (_high_)) ? (_high_) : (((_x_) < (_low_)) ? (_low_) : (_x_)))
2022-08-26 15:15:16 +00:00
// This basically checks if the handle is within vector limits and 'isUsed' is set to true
// We are relying on C's boolean short-circuit to not evaluate the last 'isUsed' if previous conditions are false
// Here we are checking > 0 because this is meant to check user handles only
# define IS_SOUND_HANDLE_VALID(_handle_) \
( ( _handle_ ) > 0 & & ( _handle_ ) < audioEngine . soundHandles . size ( ) & & audioEngine . soundHandles [ _handle_ ] - > isUsed & & \
! audioEngine . soundHandles [ _handle_ ] - > autoKill )
# ifdef QB64_WINDOWS
# define ZERO_VARIABLE(_v_) ZeroMemory(&(_v_), sizeof(_v_))
# else
# define ZERO_VARIABLE(_v_) memset(&(_v_), 0, sizeof(_v_))
# endif
2022-12-22 15:12:27 +00:00
// These attaches our customer backend (format decoders) VTables to various miniaudio structs
2022-08-26 15:15:16 +00:00
void AudioEngineAttachCustomBackendVTables ( ma_resource_manager_config * maResourceManagerConfig ) ;
2022-12-22 15:12:27 +00:00
void AudioEngineAttachCustomBackendVTables ( ma_decoder_config * maDecoderConfig ) ;
2022-08-26 15:15:16 +00:00
// These are stuff that was not declared anywhere else
// We will wait for Matt to cleanup the C/C++ source file and include header files that declare this stuff
int32 func_instr ( int32 start , qbs * str , qbs * substr , int32 passed ) ; // Did not find this declared anywhere
void new_mem_lock ( ) ; // This is required for MemSound()
void free_mem_lock ( mem_lock * lock ) ; // Same as above
# ifndef QB64_WINDOWS
2023-05-10 18:14:59 +00:00
void Sleep ( uint32 milliseconds ) ; // There is a non-Windows implementation. However it is not declared anywhere
2022-08-26 15:15:16 +00:00
# endif
extern ptrszint dblock ; // Required for Play(). Did not find this declared anywhere
extern uint64 mem_lock_id ; // Another one that we need for the mem stuff
extern mem_lock * mem_lock_base ; // Same as above
extern mem_lock * mem_lock_tmp ; // Same as above
2022-12-13 03:42:52 +00:00
/// @brief A simple FP32 stereo sample frame
struct SampleFrame {
float l ;
float r ;
2022-12-15 03:46:42 +00:00
} ;
2022-12-21 06:17:41 +00:00
/// @brief A miniaudiio raw audio stream datasource
struct RawStream {
2022-12-30 20:19:23 +00:00
ma_data_source_base maDataSource ; // miniaudio data source (this must be the first member of our struct)
ma_data_source_config maDataSourceConfig ; // config struct for the data source
ma_engine * maEngine ; // pointer to a ma_engine object that was passed while creating the data source
ma_sound * maSound ; // pointer to a ma_sound object that was passed while creating the data source
ma_uint32 sampleRate ; // the sample rate reported by ma_engine
struct Buffer { // we'll give this a name that we'll use below
std : : vector < SampleFrame > data ; // this holds the actual sample frames
size_t cursor ; // the read cursor (in frames) in the stream
} buffer [ 2 ] ; // we need two of these to do a proper ping-pong
Buffer * consumer ; // this is what the miniaudio thread will use to pull data from
Buffer * producer ; // this is what the main thread will use to push data to
libqb_mutex * m ; // we'll use a mutex to give exclusive access to resources used by both threads
bool stop ; // set this to true to stop supply of samples completely (including silent samples)
2023-05-10 18:14:59 +00:00
static const size_t DEFAULT_SIZE = 1024 ; // this is almost twice the amout what miniaudio actually asks for in frameCount
2022-12-21 06:17:41 +00:00
// Delete default, copy and move constructors and assignments
RawStream ( ) = delete ;
RawStream ( const RawStream & ) = delete ;
RawStream & operator = ( const RawStream & ) = delete ;
RawStream & operator = ( RawStream & & ) = delete ;
RawStream ( RawStream & & ) = delete ;
/// @brief This is use to setup the vectors, mutex and set some defaults
RawStream ( ma_engine * pmaEngine , ma_sound * pmaSound ) {
maSound = pmaSound ; // Save the pointer to the ma_sound object (this is basically from a QBPE sound handle)
maEngine = pmaEngine ; // Save the pointer to the ma_engine object (this should come from the QBPE sound engine)
sampleRate = ma_engine_get_sample_rate ( maEngine ) ; // Save the sample rate
2022-12-30 20:19:23 +00:00
buffer [ 0 ] . cursor = buffer [ 1 ] . cursor = 0 ; // reset the cursors
buffer [ 0 ] . data . reserve ( DEFAULT_SIZE ) ; // ensure we have a contigious block to account for expansion without reallocation
buffer [ 1 ] . data . reserve ( DEFAULT_SIZE ) ; // ensure we have a contigious block to account for expansion without reallocation
consumer = & buffer [ 0 ] ; // set default consumer
producer = & buffer [ 1 ] ; // set default producer
stop = false ; // by default we will send silent samples to keep the playback going
2022-12-15 03:46:42 +00:00
m = libqb_mutex_new ( ) ;
}
2022-08-26 15:15:16 +00:00
2022-12-21 06:17:41 +00:00
/// @brief We use this to destroy the mutex
~ RawStream ( ) { libqb_mutex_free ( m ) ; }
2022-12-15 03:46:42 +00:00
2022-12-30 20:19:23 +00:00
/// @brief This swaps the consumer and producer Buffers. This is mutex protected and called by the miniaudio thread
void SwapBuffers ( ) {
libqb_mutex_guard lock ( m ) ; // lock the mutex before accessing the vectors
consumer - > cursor = 0 ; // reset the cursor
2022-12-22 09:41:20 +00:00
consumer - > data . clear ( ) ; // clear the consumer vector
2022-12-21 06:17:41 +00:00
std : : swap ( consumer , producer ) ; // quicky swap the Buffer pointers
}
2022-12-15 03:46:42 +00:00
2022-12-21 06:17:41 +00:00
/// @brief This pushes a sample frame at the end of the queue. This is mutex protected and called by the main thread
/// @param l Sample frame left channel data
/// @param r Sample frame right channel data
void PushSampleFrame ( float l , float r ) {
2022-12-30 20:19:23 +00:00
libqb_mutex_guard lock ( m ) ; // lock the mutex before accessing the vectors
2022-12-21 06:17:41 +00:00
producer - > data . push_back ( { l , r } ) ; // push the sample frame to the back of the producer queue
2022-12-15 03:46:42 +00:00
}
2023-01-06 06:51:17 +00:00
/// @brief This pushes a whole buffer of mono sample frames to the queue. This is mutex protected and called by the main thread
/// @param buffer The buffer containing the sample frames. This cannot be NULL
/// @param frames The total number of frames in the buffer
void PushMonoSampleFrames ( float * buffer , ma_uint64 frames ) {
libqb_mutex_guard lock ( m ) ; // lock the mutex before accessing the vectors
for ( ma_uint64 i = 0 ; i < frames ; i + + ) {
producer - > data . push_back ( { buffer [ i ] , buffer [ i ] } ) ;
}
}
2022-12-21 06:17:41 +00:00
/// @brief Returns the length, in sample frames of sound queued
/// @return The length left to play in sample frames
ma_uint64 GetSampleFramesRemaining ( ) {
2022-12-30 20:19:23 +00:00
libqb_mutex_guard lock ( m ) ; // lock the mutex before accessing the vectors
return ( consumer - > data . size ( ) - consumer - > cursor ) + ( producer - > data . size ( ) - producer - > cursor ) ; // sum of producer and consumer sample frames
2022-12-21 06:17:41 +00:00
}
/// @brief Returns the length, in seconds of sound queued
/// @return The length left to play in seconds
double GetTimeRemaining ( ) { return ( double ) GetSampleFramesRemaining ( ) / ( double ) sampleRate ; }
2022-12-15 03:46:42 +00:00
} ;
2022-08-26 15:15:16 +00:00
2022-12-15 03:46:42 +00:00
/// @brief This is what is used by miniaudio to pull a chunk of raw sample frames to play. The samples being read is removed from the queue
/// @param pDataSource Pointer to the raw stream data source (cast to RawStream type)
/// @param pFramesOut The sample frames sent to miniaudio
/// @param frameCount The sample frame count requested by miniaudio
/// @param pFramesRead The sample frame count that was actually sent (this must not exceed frameCount)
/// @return MA_SUCCESS on success or an appropriate MA_FAILED_* value on failure
static ma_result RawStreamOnRead ( ma_data_source * pDataSource , void * pFramesOut , ma_uint64 frameCount , ma_uint64 * pFramesRead ) {
if ( pFramesRead )
* pFramesRead = 0 ;
if ( frameCount = = 0 )
return MA_INVALID_ARGS ;
if ( ! pDataSource )
return MA_INVALID_ARGS ;
2023-05-10 18:14:59 +00:00
auto pRawStream = ( RawStream * ) pDataSource ; // cast to RawStream instance pointer
auto result = MA_SUCCESS ; // must be initialized to MA_SUCCESS
auto maBuffer = ( SampleFrame * ) pFramesOut ; // cast to sample frame pointer
2022-12-21 06:17:41 +00:00
2022-12-30 23:48:22 +00:00
ma_uint64 sampleFramesCount = pRawStream - > consumer - > data . size ( ) - pRawStream - > consumer - > cursor ; // total amount of samples we need to send to miniaudio
2022-12-30 20:19:23 +00:00
// Swap buffers if we do not have anything left to play
2022-12-21 06:17:41 +00:00
if ( ! sampleFramesCount ) {
2022-12-30 20:19:23 +00:00
pRawStream - > SwapBuffers ( ) ;
2022-12-21 06:17:41 +00:00
sampleFramesCount = pRawStream - > consumer - > data . size ( ) - pRawStream - > consumer - > cursor ; // get the total number of samples again
}
2023-05-10 18:14:59 +00:00
sampleFramesCount = std : : min ( sampleFramesCount , frameCount ) ; // we'll always send lower of what miniaudio wants or what we have
2022-12-15 03:46:42 +00:00
2023-05-10 18:14:59 +00:00
ma_uint64 sampleFramesRead = 0 ; // sample frame counter
2022-12-21 06:17:41 +00:00
// Now send the samples to miniaudio
2022-12-15 22:20:32 +00:00
while ( sampleFramesRead < sampleFramesCount ) {
2022-12-21 06:17:41 +00:00
maBuffer [ sampleFramesRead ] = pRawStream - > consumer - > data [ pRawStream - > consumer - > cursor ] ;
+ + sampleFramesRead ; // increment the frame counter
pRawStream - > consumer - > cursor + + ; // increment the read cursor
2022-12-15 03:46:42 +00:00
}
// To keep the stream going, play silence if there are no frames to play
2022-12-21 06:17:41 +00:00
if ( ! sampleFramesRead & & ! pRawStream - > stop ) {
2022-12-15 03:46:42 +00:00
while ( sampleFramesRead < frameCount ) {
2022-12-21 06:17:41 +00:00
maBuffer [ sampleFramesRead ] = { } ;
2022-12-15 03:46:42 +00:00
+ + sampleFramesRead ;
2022-08-26 15:15:16 +00:00
}
2022-12-15 03:46:42 +00:00
}
if ( pFramesRead )
* pFramesRead = sampleFramesRead ;
return result ;
}
/// @brief This is a dummy callback function which just tells miniaudio that it succeeded
/// @param pDataSource Pointer to the raw stream data source (cast to RawStream type)
/// @param frameIndex The frame index to seek to (unused)
/// @return Always MA_SUCCESS
static ma_result RawStreamOnSeek ( ma_data_source * pDataSource , ma_uint64 frameIndex ) {
// NOP. Just pretend to be successful.
( void ) pDataSource ;
( void ) frameIndex ;
return MA_SUCCESS ;
}
/// @brief Returns the audio format to miniaudio
/// @param pDataSource Pointer to the raw stream data source (cast to RawStream type)
/// @param pFormat The ma_format to use (we always return ma_format_f32 because that is what QB64 uses)
/// @param pChannels The number of audio channels (always 2 - stereo)
/// @param pSampleRate The sample rate of the stream (we always return the engine sample rate)
/// @param pChannelMap Sent to ma_channel_map_init_standard
/// @param channelMapCap Sent to ma_channel_map_init_standard
/// @return Always MA_SUCCESS
static ma_result RawStreamOnGetDataFormat ( ma_data_source * pDataSource , ma_format * pFormat , ma_uint32 * pChannels , ma_uint32 * pSampleRate ,
ma_channel * pChannelMap , size_t channelMapCap ) {
auto pRawStream = ( RawStream * ) pDataSource ;
if ( pFormat )
* pFormat = ma_format : : ma_format_f32 ; // QB64 SndRaw API uses FP32 samples
if ( pChannels )
* pChannels = 2 ; // stereo
if ( pSampleRate )
* pSampleRate = pRawStream - > sampleRate ; // we'll play at the audio engine sampling rate
2022-08-26 15:15:16 +00:00
2022-12-15 03:46:42 +00:00
if ( pChannelMap )
ma_channel_map_init_standard ( ma_standard_channel_map_default , pChannelMap , channelMapCap , 2 ) ; // stereo
return MA_SUCCESS ;
}
/// @brief Raw stream data source vtable
2022-12-21 06:17:41 +00:00
static ma_data_source_vtable rawStreamDataSourceVtable = {
2022-12-15 03:46:42 +00:00
RawStreamOnRead , // Returns a bunch of samples from a raw sample stream queue. The samples being returned is removed from the queue
RawStreamOnSeek , // NOP for raw sample stream
RawStreamOnGetDataFormat , // Returns the audio format to miniaudio
NULL , // No notion of a cursor for raw sample stream
NULL , // No notion of a length for raw sample stream
NULL , // Cannot loop raw sample stream
0 // flags
} ;
/// @brief This creates, initializes and sets up a raw stream for playback
/// @param pmaEngine This should come from the QBPE sound engine
/// @param pmaSound This should come from a QBPE sound handle
/// @return Returns a pointer to a data souce if successful, NULL otherwise
static RawStream * RawStreamCreate ( ma_engine * pmaEngine , ma_sound * pmaSound ) {
if ( ! pmaEngine | | ! pmaSound ) { // these should not be NULL
AUDIO_DEBUG_PRINT ( " Invalid arguments " ) ;
return nullptr ;
2022-08-26 15:15:16 +00:00
}
2022-12-21 06:17:41 +00:00
auto pRawStream = new RawStream ( pmaEngine , pmaSound ) ; // create the data source object
2022-12-15 03:46:42 +00:00
if ( ! pRawStream ) {
AUDIO_DEBUG_PRINT ( " Failed to create data source " ) ;
2022-08-26 15:15:16 +00:00
2022-12-15 03:46:42 +00:00
return nullptr ;
}
2022-08-26 15:15:16 +00:00
2022-12-30 20:19:23 +00:00
ZERO_VARIABLE ( pRawStream - > maDataSource ) ;
2022-08-26 15:15:16 +00:00
2022-12-30 20:19:23 +00:00
pRawStream - > maDataSourceConfig = ma_data_source_config_init ( ) ;
pRawStream - > maDataSourceConfig . vtable = & rawStreamDataSourceVtable ; // attach the vtable to the data source
2022-08-26 15:15:16 +00:00
2022-12-30 20:19:23 +00:00
auto result = ma_data_source_init ( & pRawStream - > maDataSourceConfig , & pRawStream - > maDataSource ) ;
2022-12-15 03:46:42 +00:00
if ( result ! = MA_SUCCESS ) {
AUDIO_DEBUG_PRINT ( " Error %i: failed to initialize data source " , result ) ;
delete pRawStream ;
return nullptr ;
2022-08-26 15:15:16 +00:00
}
2022-12-30 20:19:23 +00:00
result = ma_sound_init_from_data_source ( pmaEngine , & pRawStream - > maDataSource , MA_SOUND_FLAG_NO_PITCH | MA_SOUND_FLAG_NO_SPATIALIZATION , NULL ,
2022-12-16 06:58:22 +00:00
pmaSound ) ; // attach data source to the ma_sound
2022-12-15 03:46:42 +00:00
if ( result ! = MA_SUCCESS ) {
AUDIO_DEBUG_PRINT ( " Error %i: failed to initalize sound from data source " , result ) ;
2022-08-26 15:15:16 +00:00
2022-12-15 03:46:42 +00:00
delete pRawStream ;
2022-08-26 15:15:16 +00:00
2022-12-15 03:46:42 +00:00
return nullptr ;
2022-08-26 15:15:16 +00:00
}
2022-12-15 03:46:42 +00:00
result = ma_sound_start ( pmaSound ) ; // play the ma_sound
if ( result ! = MA_SUCCESS ) {
AUDIO_DEBUG_PRINT ( " Error %i: failed to start sound playback " , result ) ;
2022-08-26 15:15:16 +00:00
2022-12-15 03:46:42 +00:00
ma_sound_uninit ( pmaSound ) ; // delete the ma_sound object
delete pRawStream ;
return nullptr ;
2022-08-26 15:15:16 +00:00
}
2022-12-15 03:46:42 +00:00
AUDIO_DEBUG_PRINT ( " Raw sound stream created " ) ;
2022-08-26 15:15:16 +00:00
2022-12-15 03:46:42 +00:00
return pRawStream ;
}
2022-08-26 15:15:16 +00:00
2022-12-15 03:46:42 +00:00
/// @brief Stops and then frees a raw stream data source previously created with RawStreamCreate()
/// @param pRawStream Pointer to the data source object
static void RawStreamDestroy ( RawStream * pRawStream ) {
if ( pRawStream ) {
auto result = ma_sound_stop ( pRawStream - > maSound ) ; // stop playback
AUDIO_DEBUG_CHECK ( result = = MA_SUCCESS ) ;
2022-12-13 03:42:52 +00:00
2022-12-15 03:46:42 +00:00
ma_sound_uninit ( pRawStream - > maSound ) ; // delete the ma_sound object
2022-08-26 15:15:16 +00:00
2023-05-10 18:14:59 +00:00
delete pRawStream ; // delete the raw stream object
2022-12-15 03:46:42 +00:00
AUDIO_DEBUG_PRINT ( " Raw sound stream destroyed " ) ;
2022-08-26 15:15:16 +00:00
}
2022-12-15 03:46:42 +00:00
}
2023-01-09 00:03:39 +00:00
/// @brief A class that can manage a list of buffers using unique keys
class BufferMap {
private :
/// @brief A buffer that is made up of a raw pointer, size and reference count
struct Buffer {
void * data ;
size_t size ;
size_t refCount ;
} ;
std : : unordered_map < intptr_t , Buffer > buffers ;
public :
2023-01-09 00:41:21 +00:00
// Delete assignment operators
2023-01-09 00:32:29 +00:00
BufferMap & operator = ( const BufferMap & ) = delete ;
BufferMap & operator = ( BufferMap & & ) = delete ;
2023-01-09 00:03:39 +00:00
/// @brief This will simply free all buffers that were allocated
~ BufferMap ( ) {
for ( auto & it : buffers ) {
free ( it . second . data ) ;
AUDIO_DEBUG_PRINT ( " Buffer freed of size %llu " , it . second . size ) ;
}
}
/// @brief Adds a buffer to the map using a unique key only if it was not added before
/// @param data The raw data pointer. The data is copied
/// @param size The size of the data
/// @param key The unique key that should be used
/// @return True if successful
bool AddBuffer ( const void * data , size_t size , intptr_t key ) {
if ( data & & size & & key & & buffers . find ( key ) = = buffers . end ( ) ) {
Buffer buf = { } ;
buf . data = malloc ( size ) ;
if ( ! buf . data )
return false ;
buf . size = size ;
buf . refCount = 1 ;
memcpy ( buf . data , data , size ) ;
buffers . emplace ( key , std : : move ( buf ) ) ;
AUDIO_DEBUG_PRINT ( " Added buffer of size %llu to map " , size ) ;
return true ;
}
AUDIO_DEBUG_PRINT ( " Failed to add buffer of size %llu " , size ) ;
return false ;
}
/// @brief Increments the buffer reference count
/// @param key The unique key for the buffer
void AddRef ( intptr_t key ) {
const auto it = buffers . find ( key ) ;
if ( it ! = buffers . end ( ) ) {
auto & buf = it - > second ;
buf . refCount + = 1 ;
AUDIO_DEBUG_PRINT ( " Increased reference count to %llu " , buf . refCount ) ;
} else {
AUDIO_DEBUG_PRINT ( " Buffer not found " ) ;
}
}
/// @brief Decrements the buffer reference count and frees the buffer if the reference count reaches zero
/// @param key The unique key for the buffer
void Release ( intptr_t key ) {
const auto it = buffers . find ( key ) ;
if ( it ! = buffers . end ( ) ) {
auto & buf = it - > second ;
buf . refCount - = 1 ;
AUDIO_DEBUG_PRINT ( " Decreased reference count to %llu " , buf . refCount ) ;
if ( buf . refCount < 1 ) {
free ( buf . data ) ;
AUDIO_DEBUG_PRINT ( " Buffer freed of size %llu " , buf . size ) ;
buffers . erase ( key ) ;
}
} else {
AUDIO_DEBUG_PRINT ( " Buffer not found " ) ;
}
}
/// @brief Gets the raw pointer and size of the buffer with the given key
/// @param key The unique key for the buffer
/// @return An std::pair of the buffer raw pointer and size
std : : pair < const void * , size_t > GetBuffer ( intptr_t key ) const {
const auto it = buffers . find ( key ) ;
if ( it = = buffers . end ( ) ) {
AUDIO_DEBUG_PRINT ( " Buffer not found " ) ;
return { nullptr , 0 } ;
}
const auto & buf = it - > second ;
AUDIO_DEBUG_PRINT ( " Returning buffer of size %llu " , buf . size ) ;
return { buf . data , buf . size } ;
}
} ;
2023-04-10 20:19:14 +00:00
/// @brief A waveform class and wrapper around the miniaudio waveform APIs
class Waveform {
public :
/// @brief Various types of waveform that can be generated
enum Type { SILENCE = 0 , SAWTOOTH , TRIANGLE , SQUARE , SINE , NOISE , COUNT } ;
private :
ma_waveform_config maWaveformConfig ; // miniaudio waveform configuration
ma_waveform maWaveform ; // miniaudio waveform
ma_noise_config maNoiseConfig ; // miniaudio noise configuration
ma_noise maNoise ; // miniaudio noise
ma_result maResult ; // result of the last miniaudio operation
RawStream * rawStream ; // this is the RawStream where the samples data will be pushed to
float * bufferWorking ; // this is where stuff is rendered temporarily when mixing is needed
float * bufferFinal ; // this is where the waveform is rendered before being pushed to RawStream
ma_uint64 frames ; // size of the buffer in sample frames
Type type ; // the current waveform type selected
bool played ; // was the last generated waveform played?
/// @brief Resizes the working and final buffers if frames requested is not the same as the one already allocated
/// @param newFrames New sample frames required
/// @return True if the buffers were allocated correctly
bool ResizeBuffers ( ma_uint64 newFrames ) {
2023-04-20 18:32:40 +00:00
if ( newFrames < = frames | | ! newFrames ) {
frames = newFrames ; // no need to re-allocate if we are reducing the buffer size
2023-04-10 20:19:14 +00:00
return bufferWorking ! = nullptr & & bufferFinal ! = nullptr ;
2023-04-20 18:32:40 +00:00
}
2023-04-10 20:19:14 +00:00
auto bufferSize = newFrames * SAMPLE_FRAME_SIZE ( float , 1 ) ;
float * tmpBufferWorking = ( float * ) realloc ( bufferWorking , bufferSize ) ;
if ( ! tmpBufferWorking )
return false ;
bufferWorking = tmpBufferWorking ;
float * tmpBufferFinal = ( float * ) realloc ( bufferFinal , bufferSize ) ;
if ( ! tmpBufferFinal )
return false ;
bufferFinal = tmpBufferFinal ;
if ( newFrames > frames ) {
auto deltaSize = ( newFrames - frames ) * SAMPLE_FRAME_SIZE ( float , 1 ) ;
memset ( bufferWorking + frames , 0 , deltaSize ) ;
memset ( bufferFinal + frames , 0 , deltaSize ) ;
}
AUDIO_DEBUG_PRINT ( " Buffers resized from %llu to %llu " , frames , newFrames ) ;
frames = newFrames ;
return true ;
}
public :
// Delete default, copy and move constructors and assignments
Waveform ( ) = delete ;
Waveform ( const Waveform & ) = delete ;
Waveform & operator = ( const Waveform & ) = delete ;
Waveform & operator = ( Waveform & & ) = delete ;
Waveform ( Waveform & & ) = delete ;
/// @brief Contructors that can set few or more defaults
/// @param pRawStream Pointer to a raw stream where the samples data will be pushed to. This cannot be NULL
/// @param type The waveform type. See ma_waveform_type
/// @param amplitude The amplitude of the waveform
/// @param frequency The frequency of the waveform
Waveform ( RawStream * pRawStream , Type waveType , double amplitude , double frequency ) {
rawStream = pRawStream ; // Save the raw queue object pointer
bufferWorking = bufferFinal = nullptr ;
frames = 0 ;
played = true ; // because there is nothing to play yet
maWaveformConfig =
ma_waveform_config_init ( ma_format : : ma_format_f32 , 1 , rawStream - > sampleRate , ma_waveform_type : : ma_waveform_type_square , amplitude , frequency ) ;
maResult = ma_waveform_init ( & maWaveformConfig , & maWaveform ) ;
AUDIO_DEBUG_CHECK ( maResult = = MA_SUCCESS ) ;
maNoiseConfig = ma_noise_config_init ( ma_format : : ma_format_f32 , 1 , ma_noise_type : : ma_noise_type_white , 0 , amplitude ) ;
maResult = ma_noise_init ( & maNoiseConfig , NULL , & maNoise ) ;
AUDIO_DEBUG_CHECK ( maResult = = MA_SUCCESS ) ;
SetType ( waveType ) ;
AUDIO_DEBUG_PRINT ( " Type = %i, amplitude = %lf, frequency = %lf " , type , amplitude , frequency ) ;
}
/// @brief Contructors that can set few or more defaults
/// @param pRawStream Pointer to a raw stream where the samples data will be pushed to
/// @param type The waveform type. See ma_waveform_type
Waveform ( RawStream * pRawStream , Type waveType ) : Waveform ( pRawStream , waveType , 1.0 , 440.0 ) { }
/// @brief Contructors that can set few or more defaults
/// @param pRawStream Pointer to a raw stream where the samples data will be pushed to
Waveform ( RawStream * pRawStream ) : Waveform ( pRawStream , Type : : SQUARE ) { }
/// @brief This just frees the waveform buffer and cleans up the waveform resources
~ Waveform ( ) {
ma_noise_uninit ( & maNoise , NULL ) ; // destroy miniaudio noise
ma_waveform_uninit ( & maWaveform ) ; // destroy miniaudio waveform
free ( bufferFinal ) ;
free ( bufferWorking ) ;
AUDIO_DEBUG_PRINT ( " Waveform object destroyed " ) ;
}
/// @brief Generates a waveform based on member values. Subsequent calls will overwrite the buffer if mix is false
/// @param totalDuration The duration of the sound in seconds. This can be more than realDuration if silence padding is needed at the end
/// @param realDuration The real duration of the sound in seconds (should always be less or equal to duration)
/// @param mix Mixes the generated waveform to the buffer instead of overwriting it
/// @return True if successful
bool Generate ( double totalDuration , double realDuration , bool mix = false ) {
auto totalFramesRequested = ( ma_uint64 ) ( totalDuration * rawStream - > sampleRate ) ;
auto realFramesRequested = ( ma_uint64 ) ( realDuration * rawStream - > sampleRate ) ;
if ( ! totalFramesRequested | | ! realFramesRequested | | realFramesRequested > totalFramesRequested | |
! ResizeBuffers ( mix ? ( std : : max ) ( totalFramesRequested , frames ) : totalFramesRequested ) )
return false ;
auto targetBuffer = mix ? bufferWorking : bufferFinal ;
auto framesGenerated = realFramesRequested ;
maResult = MA_SUCCESS ;
switch ( type ) {
case Type : : TRIANGLE :
case Type : : SAWTOOTH :
case Type : : SINE :
case Type : : SQUARE :
maResult = ma_waveform_read_pcm_frames ( & maWaveform , targetBuffer , realFramesRequested , & framesGenerated ) ;
break ;
case Type : : NOISE :
maResult = ma_noise_read_pcm_frames ( & maNoise , targetBuffer , realFramesRequested , & framesGenerated ) ;
break ;
case Type : : SILENCE :
default :
memset ( targetBuffer , 0 , realFramesRequested * SAMPLE_FRAME_SIZE ( float , 1 ) ) ;
break ;
}
if ( totalFramesRequested > realFramesRequested ) {
auto deltaSize = ( totalFramesRequested - realFramesRequested ) * SAMPLE_FRAME_SIZE ( float , 1 ) ;
memset ( targetBuffer + realFramesRequested , 0 , deltaSize ) ;
}
if ( maResult ! = MA_SUCCESS )
return false ;
if ( mix ) {
for ( size_t i = 0 ; i < framesGenerated ; i + + )
bufferFinal [ i ] + = bufferWorking [ i ] ;
AUDIO_DEBUG_PRINT ( " Mixed %llu frames of type %i waveform to %llu frames buffer " , framesGenerated , type , frames ) ;
} else {
if ( totalFramesRequested = = realFramesRequested )
frames = framesGenerated ;
else
frames = totalFramesRequested ;
AUDIO_DEBUG_PRINT ( " Generated %llu frames of type %i waveform to %llu frames buffer " , framesGenerated , type , frames ) ;
}
played = false ;
return true ;
}
/// @brief Sets the amplitude of the waveform
/// @param amplitude The amplitude of the waveform
/// @return True if successful
bool SetAmplitude ( double amplitude ) {
maResult = MA_SUCCESS ;
switch ( type ) {
case Type : : TRIANGLE :
case Type : : SAWTOOTH :
case Type : : SINE :
case Type : : SQUARE :
maResult = ma_waveform_set_amplitude ( & maWaveform , amplitude ) ;
break ;
case Type : : NOISE :
maResult = ma_noise_set_amplitude ( & maNoise , amplitude ) ;
break ;
}
if ( maResult ! = MA_SUCCESS )
return false ;
AUDIO_DEBUG_PRINT ( " Amplitude set to %lf " , amplitude ) ;
return true ;
}
/// @brief Sets the frequency of the waveform
/// @param frequency The frequency of the waveform
/// @return True if successful
bool SetFrequency ( double frequency ) {
maResult = MA_SUCCESS ;
switch ( type ) {
case Type : : TRIANGLE :
case Type : : SAWTOOTH :
case Type : : SINE :
case Type : : SQUARE :
maResult = ma_waveform_set_frequency ( & maWaveform , frequency ) ;
break ;
}
if ( maResult ! = MA_SUCCESS )
return false ;
AUDIO_DEBUG_PRINT ( " Frequency set to %lf " , frequency ) ;
return true ;
}
/// @brief Sets the waveform type
/// @param type The waveform type. See Waveform::Type
/// @return True if successful
bool SetType ( Type waveType ) {
maResult = MA_SUCCESS ;
switch ( waveType ) {
case Type : : TRIANGLE :
maResult = ma_waveform_set_type ( & maWaveform , ma_waveform_type : : ma_waveform_type_triangle ) ;
break ;
case Type : : SAWTOOTH :
maResult = ma_waveform_set_type ( & maWaveform , ma_waveform_type : : ma_waveform_type_sawtooth ) ;
break ;
case Type : : SINE :
maResult = ma_waveform_set_type ( & maWaveform , ma_waveform_type : : ma_waveform_type_sine ) ;
break ;
case Type : : SQUARE :
maResult = ma_waveform_set_type ( & maWaveform , ma_waveform_type : : ma_waveform_type_square ) ;
break ;
}
if ( maResult ! = MA_SUCCESS )
return false ;
type = waveType ;
AUDIO_DEBUG_PRINT ( " Waveform type set to %i " , type ) ;
return true ;
}
/// @brief Returns the type of waveform
/// @return The type of waveform
Type GetType ( ) { return type ; }
/// @brief Sends the buffer for playback
/// @return True if successful
bool Play ( ) {
if ( bufferFinal ) {
rawStream - > PushMonoSampleFrames ( bufferFinal , frames ) ;
AUDIO_DEBUG_PRINT ( " Sent %llu samples for playback " , frames ) ;
played = true ;
return true ;
}
return false ;
}
/// @brief
/// @return
bool Played ( ) { return played ; }
/// @brief Waits for any playback to complete
void AwaitPlaybackCompletion ( ) {
AUDIO_DEBUG_PRINT ( " Waiting for playback to complete " ) ;
auto timeMs = ( ma_uint32 ) ( rawStream - > GetSampleFramesRemaining ( ) * 1000 ) / rawStream - > sampleRate ;
if ( timeMs )
Sleep ( timeMs ) ;
}
ma_uint32 GetSampleRate ( ) { return rawStream - > sampleRate ; }
} ;
/// @brief This is an MML parser and player class that implements the QB64 MML spec and more
/// https://qb64phoenix.com/qb64wiki/index.php/PLAY
/// http://vgmpf.com/Wiki/index.php?title=Music_Macro_Language
/// https://en.wikipedia.org/wiki/Music_Macro_Language
/// https://sneslab.net/wiki/Music_Macro_Language
/// http://www.mirbsd.org/htman/i386/man4/speaker.htm
/// https://www.qbasic.net/en/reference/qb11/Statement/PLAY-006.htm
/// https://woolyss.com/chipmusic-mml.php
/// frequency = 440.0 * pow(2.0, (note + (octave - 2.0) * 12.0 - 9.0) / 12.0)
/*
const float FREQUENCY_TABLE [ ] = {
0 ,
//1 2 3 4 5 6 7 8 9 10 11 12
//C C# D D# E F F# G G# A A# B
16.35f , 17.32f , 18.35f , 19.45f , 20.60f , 21.83f , 23.12f , 24.50f , 25.96f , 27.50f , 29.14f , 30.87f , // Octave 0
32.70f , 34.65f , 36.71f , 38.89f , 41.20f , 43.65f , 46.25f , 49.00f , 51.91f , 55.00f , 58.27f , 61.74f , // Octave 1
65.41f , 69.30f , 73.42f , 77.78f , 82.41f , 87.31f , 92.50f , 98.00f , 103.83f , 110.00f , 116.54f , 123.47f , // Octave 2
130.81f , 138.59f , 146.83f , 155.56f , 164.81f , 174.62f , 185.00f , 196.00f , 207.65f , 220.00f , 233.08f , 246.94f , // Octave 3
261.63f , 277.18f , 293.67f , 311.13f , 329.63f , 349.23f , 370.00f , 392.00f , 415.31f , 440.00f , 466.17f , 493.89f , // Octave 4
523.25f , 554.37f , 587.33f , 622.26f , 659.26f , 698.46f , 739.99f , 783.99f , 830.61f , 880.00f , 932.33f , 987.77f , // Octave 5
1046.51f , 1108.74f , 1174.67f , 1244.51f , 1318.52f , 1396.92f , 1479.99f , 1567.99f , 1661.23f , 1760.01f , 1864.66f , 1975.54f , // Octave 6
2093.02f , 2217.47f , 2349.33f , 2489.03f , 2637.03f , 2793.84f , 2959.97f , 3135.98f , 3322.45f , 3520.02f , 3729.33f , 3951.09f , // Octave 7
} ;
*/
class MMLPlayer {
private :
/// @brief This struct to used to hold the current string state and also used for the state stack
struct State {
uint8_t * string ;
size_t length ;
size_t position ;
} ;
int command ; // the command we need to execute
int note ; // the current note
int tempo ; // the tempo of the tune (this impacts all lengths)
int octave ; // the current octave that we'll use for notes
double length ; // the length of each note (1 = full, 4 = quarter etc.)
double pause ; // the duration of silence after a note (this eats away from the note length)
double volume ; // the current volume
double duration ; // the duration of the note / silence (in seconds)
bool background ; // if this is true, then control will be returned back to the caller as soon as the MML is rendered
bool mix ; // the next note should be mixed to the previous one
Waveform * waveform ; // samples are generated and pushed for playback using this waveform object pointer
std : : stack < State > stateStack ; // this maintains the state stack if we need to process substrings (VARPTR$)
State currentState ; // this is the current state. See State struct
// These are some constants that can be tweaked to change the behavior of the parser
// These mostly conform to the QBasic and QB64 spec.
static constexpr auto MIN_VOLUME = 0.0 ;
static constexpr auto MAX_VOLUME = 100.0 ;
static constexpr auto DEFAULT_VOLUME = MAX_VOLUME / 2 ;
static const auto MIN_TEMPO = 32 ;
static const auto MAX_TEMPO = 255 ;
static const auto DEFAULT_TEMPO = 120 ;
static const auto MIN_OCTAVE = 0 ;
static const auto MAX_OCTAVE = 7 ;
static const auto DEFAULT_OCTAVE = 4 ;
static const auto MIN_NOTE = 0 ;
static const auto MAX_NOTE = 12 * ( 1 + MAX_OCTAVE ) ;
static constexpr auto MIN_LENGTH = 1.0 ;
static constexpr auto MAX_LENGTH = 64.0 ;
static constexpr auto DEFAULT_LENGTH = 4.0 ;
static constexpr auto DEFAULT_PAUSE = 1.0 / 8.0 ;
static const auto INVALID = - 1 ;
/// @brief Get stores the current character from the string to 'command' member skipping all whitespace characters
/// @return True if it was successful
bool GetCommand ( ) {
while ( currentState . position < currentState . length ) {
command = toupper ( currentState . string [ currentState . position ] ) ;
if ( ! isspace ( command ) )
return true ;
currentState . position + + ; // skip space characters
}
return false ;
}
/// @brief Gets the next non-whitespace character from the string without changing the index
/// @return A valid ASCII character or -1 if there is none
int PeekNextCharacter ( ) {
for ( auto p = currentState . position + 1 ; p < currentState . length ; + + p ) {
auto c = toupper ( currentState . string [ p ] ) ;
if ( ! isspace ( c ) )
return toupper ( c ) ;
}
return INVALID ;
}
/// @brief Gets the next non-whitespace character from the string and advances the index to the character location
/// @return A valid ASCII character or -1 if there is none
int GetNextCharacter ( ) {
while ( + + currentState . position < currentState . length ) {
auto c = toupper ( currentState . string [ currentState . position ] ) ;
if ( ! isspace ( c ) )
return toupper ( c ) ;
}
return INVALID ;
}
/// @brief Returns a numeric argument for a command
/// @return A valid positive number or -1 if there is none
int GetCommandArgument ( ) {
// TODO: Implement "=" + VARPTR$(n) support for numbers
auto number = 0 ;
auto commandArgument = INVALID ;
for ( auto c = PeekNextCharacter ( ) ; c ! = INVALID & & isdigit ( c ) ; c = PeekNextCharacter ( ) ) {
number = number * 10 + c - ' 0 ' ;
commandArgument = number ;
GetNextCharacter ( ) ; // consume the character
}
return commandArgument ;
}
/// @brief Counts the dots after a note or a pause and increases the duration
void DoDots ( ) {
auto dots = 0 ;
for ( auto c = PeekNextCharacter ( ) ; c ! = INVALID & & c = = ' . ' ; c = PeekNextCharacter ( ) ) {
+ + dots ;
GetNextCharacter ( ) ; // consume the character
}
auto dotDuration = duration ;
for ( auto i = 0 ; i < dots ; i + + ) {
dotDuration / = 2.0 ;
duration + = dotDuration ;
}
}
/// @brief Calculates the note duration in seconds and stores it in the 'duration' member
/// @param customLength This is optional and can be specified if a duration is required for a custom length (useful for 'note' x and P x commands)
void CalculateDuration ( double customLength = INVALID ) {
auto l = customLength = = INVALID ? length : customLength ;
duration = 1.0 / ( tempo / 60.0 ) * ( 4.0 / l ) ;
}
/// @brief This converts a note string to a number and handles all notes (including sharps and flats) and also 'N' commands
void GetNote ( ) {
static const int notes [ ] = { 9 , 11 , 0 , 2 , 4 , 5 , 7 } ;
auto argument = PeekNextCharacter ( ) ;
if ( command = = ' N ' ) {
if ( isdigit ( argument ) ) {
argument = GetCommandArgument ( ) ;
if ( argument > = MIN_NOTE & & argument < = MAX_NOTE )
note = argument - 1 ; // rest (N0) will become -1
else
AUDIO_DEBUG_PRINT ( " Invalid note (%i) requested " , argument ) ;
} else {
AUDIO_DEBUG_PRINT ( " Expected number for note. Got '%c' " , argument ) ;
}
} else if ( command > = ' A ' & & command < = ' G ' ) {
note = notes [ command - ' A ' ] ;
if ( argument = = ' + ' | | argument = = ' # ' ) {
+ + note ;
GetNextCharacter ( ) ; // consume the character
} else if ( argument = = ' - ' ) {
- - note ;
GetNextCharacter ( ) ; // consume the character
}
} else {
AUDIO_DEBUG_PRINT ( " Expected A/B/C/D/E/F/G/N. Got '%c' " , command ) ;
}
}
/// @brief This is what playes the notes based on the note number
void DoNote ( ) {
GetNote ( ) ;
auto argument = PeekNextCharacter ( ) ;
if ( isdigit ( argument ) ) {
argument = GetCommandArgument ( ) ;
if ( argument > = MIN_LENGTH & & argument < = MAX_LENGTH )
CalculateDuration ( argument ) ;
else
AUDIO_DEBUG_PRINT ( " Invalid length (%i) requested " , argument ) ;
} else {
CalculateDuration ( ) ;
}
DoDots ( ) ;
if ( note = = INVALID ) {
auto waveType = waveform - > GetType ( ) ;
waveform - > SetType ( Waveform : : Type : : SILENCE ) ;
waveform - > Generate ( duration , duration ) ;
waveform - > Play ( ) ;
waveform - > SetType ( waveType ) ;
} else {
waveform - > SetFrequency ( 440.0 * pow ( 2.0 , ( note + ( octave - 2.0 ) * 12.0 - 9.0 ) / 12.0 ) ) ;
waveform - > SetAmplitude ( volume / ( MAX_VOLUME + 1.0 ) ) ;
waveform - > Generate ( duration , duration - ( duration * pause ) , mix ) ;
if ( PeekNextCharacter ( ) ! = ' , ' ) // peek ahead to see if we need to mix this but don't consume the ',' yet
waveform - > Play ( ) ; // play the note only if we do not need to mix
}
mix = false ; // unset the last mix flag as this will be set again by main loop if mix is needed
}
/// @brief Used for playing silence when 'P' or 'R' is encountered
void DoSilence ( ) {
if ( isdigit ( PeekNextCharacter ( ) ) ) {
auto argument = GetCommandArgument ( ) ;
if ( argument > = MIN_LENGTH & & argument < = MAX_LENGTH )
CalculateDuration ( argument ) ;
else
AUDIO_DEBUG_PRINT ( " Invalid length (%i) requested " , argument ) ;
} else {
CalculateDuration ( ) ;
}
DoDots ( ) ;
auto waveType = waveform - > GetType ( ) ;
waveform - > SetType ( Waveform : : Type : : SILENCE ) ;
waveform - > Generate ( duration , duration ) ;
waveform - > Play ( ) ;
waveform - > SetType ( waveType ) ;
}
/// @brief Used to change the volume when a 'V' command is encoutered
void DoVolume ( ) {
auto argument = PeekNextCharacter ( ) ;
if ( isdigit ( argument ) ) {
argument = GetCommandArgument ( ) ;
if ( argument > = MIN_VOLUME & & argument < = MAX_VOLUME )
volume = argument ;
else
AUDIO_DEBUG_PRINT ( " Invalid volume (%i) requested " , argument ) ;
} else {
if ( argument = = ' + ' ) {
+ + volume ;
volume = CLAMP ( volume , MIN_VOLUME , MAX_VOLUME ) ;
} else if ( argument = = ' - ' ) {
- - volume ;
volume = CLAMP ( volume , MIN_VOLUME , MAX_VOLUME ) ;
} else {
AUDIO_DEBUG_PRINT ( " Expected +, - or a number for volume. Got '%c' " , argument ) ;
return ;
}
GetNextCharacter ( ) ; // consume the character
}
}
/// @brief Used to change the tune tempo when a 'T' command is encountered
void DoTempo ( ) {
auto argument = PeekNextCharacter ( ) ;
if ( isdigit ( argument ) ) {
argument = GetCommandArgument ( ) ;
if ( argument > = MIN_TEMPO & & argument < = MAX_TEMPO )
tempo = argument ;
else
AUDIO_DEBUG_PRINT ( " Invalid tempo (%i) requested " , argument ) ;
} else {
if ( argument = = ' + ' ) {
+ + tempo ;
tempo = CLAMP ( tempo , MIN_TEMPO , MAX_TEMPO ) ;
} else if ( argument = = ' - ' ) {
- - tempo ;
tempo = CLAMP ( tempo , MIN_TEMPO , MAX_TEMPO ) ;
} else {
AUDIO_DEBUG_PRINT ( " Expected +, - or a number for tempo. Got '%c' " , argument ) ;
return ;
}
GetNextCharacter ( ) ; // consume the character
}
}
/// @brief Used to change the note length when an 'L' command is encountered
void DoLength ( ) {
auto argument = PeekNextCharacter ( ) ;
if ( isdigit ( argument ) ) {
argument = GetCommandArgument ( ) ;
if ( argument > = MIN_LENGTH & & argument < = MAX_LENGTH )
length = argument ;
else
AUDIO_DEBUG_PRINT ( " Invalid length (%i) requested " , argument ) ;
} else {
if ( argument = = ' + ' ) {
+ + length ;
length = CLAMP ( length , MIN_LENGTH , MAX_LENGTH ) ;
} else if ( argument = = ' - ' ) {
- - length ;
length = CLAMP ( length , MIN_LENGTH , MAX_LENGTH ) ;
} else {
AUDIO_DEBUG_PRINT ( " Expected +, - or a number for length. Got '%c' " , argument ) ;
return ;
}
GetNextCharacter ( ) ; // consume the character
}
}
/// @brief Used to change the octave when an 'O' command is encountered
void DoOctave ( ) {
auto argument = PeekNextCharacter ( ) ;
if ( isdigit ( argument ) ) {
argument = GetCommandArgument ( ) ;
if ( argument > = MIN_OCTAVE & & argument < = MAX_OCTAVE )
octave = argument ;
else
AUDIO_DEBUG_PRINT ( " Invalid octave (%i) requested " , argument ) ;
} else {
if ( argument = = ' + ' ) {
+ + octave ;
octave = CLAMP ( octave , MIN_OCTAVE , MAX_OCTAVE ) ;
} else if ( argument = = ' - ' ) {
- - octave ;
octave = CLAMP ( octave , MIN_OCTAVE , MAX_OCTAVE ) ;
} else {
AUDIO_DEBUG_PRINT ( " Expected +, - or a number for octave. Got '%c' " , argument ) ;
return ;
}
GetNextCharacter ( ) ; // consume the character
}
}
/// @brief Used to handle all 'M' commands
void DoTiming ( ) {
auto argument = PeekNextCharacter ( ) ;
switch ( argument ) {
case ' B ' : // background
background = true ;
break ;
case ' F ' : // foreground
background = false ;
break ;
case ' L ' : // legato
pause = 0.0 ;
break ;
case ' N ' : // normal
pause = 1.0 / 8.0 ;
break ;
case ' S ' : // staccato
pause = 1.0 / 4.0 ;
break ;
default :
AUDIO_DEBUG_PRINT ( " Expected B/F/L/N/S. Got '%c' " , argument ) ;
return ;
}
GetNextCharacter ( ) ; // consume the character
}
/// @brief Used to push the current string state to the stack when an 'X' command is encountered
void DoSubstring ( ) {
// get string address
// if (successful) >
// currentState.position++; // increment to the next position (important!)
// stateStack.push(currentState); // push the current string & state to the state stack
// currentState.string = new_string;
// currentState.length = strlen(new_string);
// currentState.position = 0;
AUDIO_DEBUG_PRINT ( " X not implemented " ) ;
}
/// @brief Used to change the waveform type when a 'W' command is encountered
void DoWaveform ( ) {
auto argument = PeekNextCharacter ( ) ;
if ( isdigit ( argument ) ) {
argument = GetCommandArgument ( ) ;
if ( argument > Waveform : : Type : : SILENCE & & argument < Waveform : : Type : : COUNT )
waveform - > SetType ( ( Waveform : : Type ) argument ) ;
else
AUDIO_DEBUG_PRINT ( " Invalid waveform (%i) requested " , argument ) ;
} else {
AUDIO_DEBUG_PRINT ( " Expected number for waveform. Got '%c' " , argument ) ;
}
}
public :
// Delete default, copy and move constructors and assignments
MMLPlayer ( ) = delete ;
MMLPlayer ( const MMLPlayer & ) = delete ;
MMLPlayer & operator = ( const MMLPlayer & ) = delete ;
MMLPlayer & operator = ( MMLPlayer & & ) = delete ;
MMLPlayer ( MMLPlayer & & ) = delete ;
/// @brief The only constructor
/// @param pWaveform A valid Waveform object pointer. This cannot be NULL
MMLPlayer ( Waveform * pWaveform ) {
command = INVALID ;
note = INVALID ;
tempo = DEFAULT_TEMPO ;
octave = DEFAULT_OCTAVE ;
length = DEFAULT_LENGTH ;
pause = DEFAULT_PAUSE ;
volume = DEFAULT_VOLUME ;
background = false ;
mix = false ;
duration = 0 ;
waveform = pWaveform ;
ZERO_VARIABLE ( currentState ) ;
}
/// @brief Returns if music is to be played in the background
/// @return True for background, false for foreground
bool IsBackgroundPlayback ( ) { return background ; }
/// @brief The is what should be called to parse and play an MML string
/// @param mml A string containing the MML tune
void Play ( qbs * mml ) {
if ( ! mml | | ! mml - > len ) // exit if string is empty
return ;
stateStack . push ( { mml - > chr , ( size_t ) mml - > len , 0 } ) ; // push the string to the state stack
// Process until our state stack is empty
while ( ! stateStack . empty ( ) ) {
// Pop and use the top item in the state stack
currentState = stateStack . top ( ) ;
stateStack . pop ( ) ;
// Parse and play each character in the MML string
while ( GetCommand ( ) ) {
// Check the command and take appropriate action
switch ( command ) {
case ' A ' : // note A
case ' B ' : // note B
case ' C ' : // note C
case ' D ' : // note D
case ' E ' : // note E
case ' F ' : // note F
case ' G ' : // note G
case ' N ' : // note 'n'
DoNote ( ) ;
break ;
case ' L ' : // length
DoLength ( ) ;
break ;
case ' M ' : // timing
DoTiming ( ) ;
break ;
case ' O ' : // octave
DoOctave ( ) ;
break ;
case ' P ' : // pause
case ' R ' : // rest
DoSilence ( ) ;
break ;
case ' T ' : // tempo
DoTempo ( ) ;
break ;
case ' V ' : // volume
DoVolume ( ) ;
break ;
case ' W ' : // waveform
DoWaveform ( ) ;
break ;
case ' X ' : // substring
DoSubstring ( ) ;
break ;
case ' > ' : // octave ++
+ + octave ;
if ( octave > MAX_OCTAVE )
octave = MAX_OCTAVE ;
break ;
case ' < ' : // octave --
- - octave ;
if ( octave < MIN_OCTAVE )
octave = MIN_OCTAVE ;
break ;
case ' , ' : // mix
mix = not mix ;
break ;
default : // unhandled stuff
AUDIO_DEBUG_PRINT ( " Command not handled '%c' " , command ) ;
break ;
}
currentState . position + + ; // move to the next character
}
if ( ! waveform - > Played ( ) ) // send any leftover samples for playback
waveform - > Play ( ) ;
}
}
} ;
2022-08-26 15:15:16 +00:00
/// <summary>
/// Sound handle type
/// This describes every sound the system will ever play (including raw streams).
/// </summary>
struct SoundHandle {
2023-04-20 18:32:40 +00:00
/// @brief Type of sound.
/// NONE: No sound or internal sound whose buffer is managed by the QBPE audio engine.
/// STATIC: Static sounds that are completely managed by miniaudio.
/// RAW: Raw sound stream that is managed by the QBPE audio engine
enum Type { NONE , STATIC , RAW } ;
2022-12-30 20:19:23 +00:00
bool isUsed ; // Is this handle in active use?
2023-04-20 18:32:40 +00:00
Type type ; // Type of sound (see Type enum above)
2022-12-30 20:19:23 +00:00
bool autoKill ; // Do we need to auto-clean this sample / stream after playback is done?
ma_sound maSound ; // miniaudio sound
ma_uint32 maFlags ; // miniaudio flags that were used when initializing the sound
ma_decoder_config maDecoderConfig ; // miniaudio decoder configuration
ma_decoder * maDecoder ; // this is used for files that are loaded directly from memory
2023-01-09 00:03:39 +00:00
intptr_t bufferKey ; // a key that will uniquely identify the data the decoder will use
2022-12-30 20:19:23 +00:00
ma_audio_buffer_config maAudioBufferConfig ; // miniaudio buffer configuration
ma_audio_buffer * maAudioBuffer ; // this is used for user created audio buffers (memory is managed by miniaudio)
RawStream * rawStream ; // Raw sample frame queue
void * memLockOffset ; // This is a pointer from new_mem_lock()
uint64 memLockId ; // This is mem_lock_id created by new_mem_lock()
2022-08-26 15:15:16 +00:00
2022-12-21 06:17:41 +00:00
// Delete copy and move constructors and assignments
SoundHandle ( const SoundHandle & ) = delete ;
SoundHandle & operator = ( const SoundHandle & ) = delete ;
SoundHandle ( SoundHandle & & ) = delete ;
SoundHandle & operator = ( SoundHandle & & ) = delete ;
2022-08-26 15:15:16 +00:00
/// <summary>
/// Just initializes some important members.
/// 'inUse' will be set to true by AllocateSoundHandle().
/// This is done here, as well as slightly differently in AllocateSoundHandle() for safety.
/// </summary>
SoundHandle ( ) {
isUsed = false ;
2023-04-20 18:32:40 +00:00
type = Type : : NONE ;
2022-08-26 15:15:16 +00:00
autoKill = false ;
ZERO_VARIABLE ( maSound ) ;
maFlags = MA_SOUND_FLAG_NO_PITCH | MA_SOUND_FLAG_NO_SPATIALIZATION | MA_SOUND_FLAG_WAIT_INIT ;
2022-12-22 15:12:27 +00:00
maDecoder = nullptr ;
2023-01-09 00:03:39 +00:00
bufferKey = 0 ;
2022-12-30 20:19:23 +00:00
maAudioBuffer = nullptr ;
rawStream = nullptr ;
2022-08-26 15:15:16 +00:00
memLockOffset = nullptr ;
2023-01-09 00:03:39 +00:00
memLockId = INVALID_MEM_LOCK ;
2022-08-26 15:15:16 +00:00
}
} ;
/// <summary>
/// Type will help us keep track of the audio engine state
/// </summary>
struct AudioEngine {
2023-04-10 20:19:14 +00:00
bool isInitialized ; // this is set to true if we were able to initialize miniaudio and allocated all required resources
bool initializationFailed ; // this is set to true if a past initialization attempt failed
2022-08-26 15:15:16 +00:00
ma_resource_manager_config maResourceManagerConfig ; // miniaudio resource manager configuration
ma_resource_manager maResourceManager ; // miniaudio resource manager
2022-12-22 15:12:27 +00:00
ma_engine_config maEngineConfig ; // miniaudio engine configuration (will be used to pass in the resource manager)
2023-04-10 20:19:14 +00:00
ma_engine maEngine ; // this is the primary miniaudio engine 'context'. Everything happens using this!
ma_result maResult ; // this is the result of the last miniaudio operation (used for trapping errors)
ma_uint32 sampleRate ; // sample rate used by the miniaudio engine
int32_t sndInternal ; // internal sound handle that we will use for Play(), Beep() & Sound()
Waveform * waveform ; // internal waveform object that we will use for Play(), Beep() & Sound()
MMLPlayer * mmlPlayer ; // internal MML player that we will use for Play()
int32_t sndInternalRaw ; // internal sound handle that we will use for the QB64 'handle-less' raw stream
std : : vector < SoundHandle * > soundHandles ; // this is the audio handle list used by the engine and by everything else
int32_t lowestFreeHandle ; // this is the lowest handle then was recently freed. We'll start checking for free handles from here
bool musicBackground ; // should 'Sound' and 'Play' work in the background or block the caller?
BufferMap bufferMap ; // this is used to keep track of and manage memory used by 'in-memory' sound files
2022-08-26 15:15:16 +00:00
2022-12-21 06:17:41 +00:00
// Delete copy and move constructors and assignments
AudioEngine ( const AudioEngine & ) = delete ;
AudioEngine & operator = ( const AudioEngine & ) = delete ;
AudioEngine & operator = ( AudioEngine & & ) = delete ;
AudioEngine ( AudioEngine & & ) = delete ;
2022-08-26 15:15:16 +00:00
/// <summary>
/// Just initializes some important members.
/// </summary>
AudioEngine ( ) {
isInitialized = initializationFailed = false ;
sampleRate = 0 ;
lowestFreeHandle = 0 ;
sndInternal = sndInternalRaw = INVALID_SOUND_HANDLE ;
2023-04-10 20:19:14 +00:00
waveform = nullptr ;
mmlPlayer = nullptr ;
2022-09-09 22:38:31 +00:00
musicBackground = false ;
2022-08-26 15:15:16 +00:00
}
/// <summary>
/// This allocates a sound handle. It will return -1 on error.
/// Handle 0 is used internally for Beep, Sound and Play and thus cannot be used by the user.
/// Basically, we go through the vector and find an object pointer were 'isUsed' is set as false and return the index.
/// If such an object pointer is not found, then we add a pointer to a new object at the end of the vector and return the index.
/// We are using pointers because miniaudio keeps using stuff from ma_sound and these cannot move in memory when the vector is resized.
/// The handle is put-up for recycling simply by setting the 'isUsed' member to false.
/// Note that this means the vector will keep growing until the largest handle (index) and never shrink.
/// The choice of using a vector was simple - performance. Vector performance when using 'indexes' is next to no other.
/// The vector will be pruned only when snd_un_init gets called.
/// We will however, be good citizens and will also 'delete' the objects when snd_un_init gets called.
/// This also increments 'lowestFreeHandle' to allocated handle + 1.
/// </summary>
/// <returns>Returns a non-negative handle if successful</returns>
int32_t AllocateSoundHandle ( ) {
if ( ! isInitialized )
2023-05-10 18:14:59 +00:00
return - 1 ; // We cannot return 0 here. Since 0 is a valid internal handle
2022-08-26 15:15:16 +00:00
size_t h , vectorSize = soundHandles . size ( ) ; // Save the vector size
// Scan the vector starting from lowestFreeHandle
// This will help us quickly allocate a free handle and should be a decent optimization for SndPlayCopy()
for ( h = lowestFreeHandle ; h < vectorSize ; h + + ) {
if ( ! soundHandles [ h ] - > isUsed ) {
AUDIO_DEBUG_PRINT ( " Recent sound handle %i recycled " , h ) ;
break ;
}
}
if ( h > = vectorSize ) {
// Scan through the entire vector and return a slot that is not being used
// Ideally this should execute in extremely few (if at all) senarios
// Also, this loop should not execute if size is 0
for ( h = 0 ; h < vectorSize ; h + + ) {
if ( ! soundHandles [ h ] - > isUsed ) {
AUDIO_DEBUG_PRINT ( " Sound handle %i recycled " , h ) ;
break ;
}
}
}
if ( h > = vectorSize ) {
// If we have reached here then either the vector is empty or there are no empty slots
// Simply create a new SoundHandle at the back of the vector
SoundHandle * newHandle = new SoundHandle ;
if ( ! newHandle )
2022-09-09 22:38:31 +00:00
return - 1 ; // We cannot return 0 here. Since 0 is a valid internal handle
2022-08-26 15:15:16 +00:00
soundHandles . push_back ( newHandle ) ;
size_t newVectorSize = soundHandles . size ( ) ;
// If newVectorSize == vectorSize then push_back() failed
if ( newVectorSize < = vectorSize ) {
delete newHandle ;
2022-09-09 22:38:31 +00:00
return - 1 ; // We cannot return 0 here. Since 0 is a valid internal handle
2022-08-26 15:15:16 +00:00
}
h = newVectorSize - 1 ; // The handle is simply newVectorSize - 1
AUDIO_DEBUG_PRINT ( " Sound handle %i created " , h ) ;
}
AUDIO_DEBUG_CHECK ( soundHandles [ h ] - > isUsed = = false ) ;
// Initializes a sound handle that was just allocated.
// This will set it to 'in use' after applying some defaults.
2023-04-20 18:32:40 +00:00
soundHandles [ h ] - > type = SoundHandle : : Type : : NONE ;
2022-08-26 15:15:16 +00:00
soundHandles [ h ] - > autoKill = false ;
ZERO_VARIABLE ( soundHandles [ h ] - > maSound ) ;
// We do not use pitch shifting, so this will give a little performance boost
// Spatialization is disabled by default but will be enabled on the fly if required
soundHandles [ h ] - > maFlags = MA_SOUND_FLAG_NO_PITCH | MA_SOUND_FLAG_NO_SPATIALIZATION | MA_SOUND_FLAG_WAIT_INIT ;
2022-12-22 15:12:27 +00:00
soundHandles [ h ] - > maDecoder = nullptr ;
2023-01-09 00:03:39 +00:00
soundHandles [ h ] - > bufferKey = 0 ;
2022-12-30 20:19:23 +00:00
soundHandles [ h ] - > maAudioBuffer = nullptr ;
soundHandles [ h ] - > rawStream = nullptr ;
2022-08-26 15:15:16 +00:00
soundHandles [ h ] - > memLockId = INVALID_MEM_LOCK ;
soundHandles [ h ] - > memLockOffset = nullptr ;
soundHandles [ h ] - > isUsed = true ;
AUDIO_DEBUG_PRINT ( " Sound handle %i returned " , h ) ;
lowestFreeHandle = h + 1 ; // Set lowestFreeHandle to allocated handle + 1
return ( int32_t ) ( h ) ;
}
/// <summary>
/// The frees and unloads an open sound.
/// If the sound is playing or looping, it will be stopped.
/// If the sound is a stream of raw samples then it is stopped and freed.
/// Finally the handle is invalidated and put-up for recycling.
/// If the handle being freed is lower than 'lowestFreeHandle' then this saves the handle to 'lowestFreeHandle'.
/// </summary>
/// <param name="handle">A sound handle</param>
void FreeSoundHandle ( int32_t handle ) {
if ( isInitialized & & handle > = 0 & & handle < soundHandles . size ( ) & & soundHandles [ handle ] - > isUsed ) {
// Sound type specific cleanup
switch ( soundHandles [ handle ] - > type ) {
2023-04-20 18:32:40 +00:00
case SoundHandle : : Type : : STATIC :
2022-08-26 15:15:16 +00:00
ma_sound_uninit ( & soundHandles [ handle ] - > maSound ) ;
break ;
2023-04-20 18:32:40 +00:00
case SoundHandle : : Type : : RAW :
2022-12-15 03:46:42 +00:00
RawStreamDestroy ( soundHandles [ handle ] - > rawStream ) ;
soundHandles [ handle ] - > rawStream = nullptr ;
2022-08-26 15:15:16 +00:00
break ;
2023-04-20 18:32:40 +00:00
case SoundHandle : : Type : : NONE :
2022-08-26 15:15:16 +00:00
if ( handle ! = 0 )
AUDIO_DEBUG_PRINT ( " Sound type is 'None' when handle value is not 0 " ) ;
break ;
default :
AUDIO_DEBUG_PRINT ( " Condition not handled " ) ; // It should not come here
}
2022-12-30 20:19:23 +00:00
// Free any initialized miniaudio decoder
2022-12-22 15:12:27 +00:00
if ( soundHandles [ handle ] - > maDecoder ) {
ma_decoder_uninit ( soundHandles [ handle ] - > maDecoder ) ;
delete soundHandles [ handle ] - > maDecoder ;
soundHandles [ handle ] - > maDecoder = nullptr ;
2023-01-09 00:03:39 +00:00
bufferMap . Release ( soundHandles [ handle ] - > bufferKey ) ;
2022-12-30 20:19:23 +00:00
AUDIO_DEBUG_PRINT ( " Decoder uninitialized " ) ;
}
// Free any initialized audio buffer
if ( soundHandles [ handle ] - > maAudioBuffer ) {
ma_audio_buffer_uninit_and_free ( soundHandles [ handle ] - > maAudioBuffer ) ;
soundHandles [ handle ] - > maAudioBuffer = nullptr ;
AUDIO_DEBUG_PRINT ( " Audio buffer uninitialized & freed " ) ;
2022-12-22 15:12:27 +00:00
}
2022-08-26 15:15:16 +00:00
// Invalidate any memsound stuff
if ( soundHandles [ handle ] - > memLockOffset ) {
free_mem_lock ( ( mem_lock * ) soundHandles [ handle ] - > memLockOffset ) ;
soundHandles [ handle ] - > memLockId = INVALID_MEM_LOCK ;
soundHandles [ handle ] - > memLockOffset = nullptr ;
2022-12-30 20:19:23 +00:00
AUDIO_DEBUG_PRINT ( " MemSound stuff invalidated " ) ;
2022-08-26 15:15:16 +00:00
}
// Now simply set the 'isUsed' member to false so that the handle can be recycled
soundHandles [ handle ] - > isUsed = false ;
2023-04-20 18:32:40 +00:00
soundHandles [ handle ] - > type = SoundHandle : : Type : : NONE ;
2022-08-26 15:15:16 +00:00
// Save the free hanndle to lowestFreeHandle if it is lower than lowestFreeHandle
if ( handle < lowestFreeHandle )
lowestFreeHandle = handle ;
AUDIO_DEBUG_PRINT ( " Sound handle %i marked as free " , handle ) ;
}
}
} ;
// This keeps track of the audio engine state
static AudioEngine audioEngine ;
2023-04-10 20:19:14 +00:00
/// @brief This generates a sound at the specified frequency for the specified amount of time
/// @param frequency Sound frequency
/// @param lengthInClockTicks Duration in clock ticks. There are 18.2 clock ticks per second
2022-08-26 15:15:16 +00:00
void sub_sound ( double frequency , double lengthInClockTicks ) {
2022-12-22 09:41:20 +00:00
if ( new_error | | ! audioEngine . isInitialized | | audioEngine . sndInternal ! = 0 | | lengthInClockTicks = = 0.0 )
2022-08-26 15:15:16 +00:00
return ;
2022-12-22 09:41:20 +00:00
if ( ( frequency < 37.0 & & frequency ! = 0 ) | | frequency > 32767.0 | | lengthInClockTicks < 0.0 | | lengthInClockTicks > 65535.0 ) {
error ( 5 ) ;
2022-08-26 15:15:16 +00:00
return ;
2022-12-22 09:41:20 +00:00
}
2022-08-26 15:15:16 +00:00
2022-12-22 09:41:20 +00:00
// Kickstart the raw stream if it is not already
2022-12-15 03:46:42 +00:00
if ( ! audioEngine . soundHandles [ audioEngine . sndInternal ] - > rawStream ) {
2023-04-10 20:19:14 +00:00
// Initialize internal RawStream object
2022-12-15 03:46:42 +00:00
audioEngine . soundHandles [ audioEngine . sndInternal ] - > rawStream =
RawStreamCreate ( & audioEngine . maEngine , & audioEngine . soundHandles [ audioEngine . sndInternal ] - > maSound ) ;
2023-04-10 20:19:14 +00:00
if ( ! audioEngine . soundHandles [ audioEngine . sndInternal ] - > rawStream ) { // failed
AUDIO_DEBUG_PRINT ( " Failed to initialize RawStream object " ) ;
2022-12-15 03:46:42 +00:00
return ;
2023-04-10 20:19:14 +00:00
}
2023-04-20 18:32:40 +00:00
audioEngine . soundHandles [ audioEngine . sndInternal ] - > type = SoundHandle : : Type : : RAW ; // set type of sound
2022-12-15 03:46:42 +00:00
}
2022-08-26 15:15:16 +00:00
2023-04-10 20:19:14 +00:00
// Initialize internal Waveform object
if ( ! audioEngine . waveform ) {
audioEngine . waveform = new Waveform ( audioEngine . soundHandles [ audioEngine . sndInternal ] - > rawStream ) ;
if ( ! audioEngine . waveform ) {
AUDIO_DEBUG_PRINT ( " Failed to initialize Waveform object " ) ;
return ;
}
}
auto duration = lengthInClockTicks / 18.2 ;
audioEngine . waveform - > SetFrequency ( frequency ) ;
audioEngine . waveform - > Generate ( duration , duration ) ;
audioEngine . waveform - > Play ( ) ;
if ( ! audioEngine . musicBackground ) // await playback to complete if we are in MF mode
audioEngine . waveform - > AwaitPlaybackCompletion ( ) ;
2022-08-26 15:15:16 +00:00
}
2023-04-19 12:45:07 +00:00
/// @brief This generates a default 'beep' sound
void sub_beep ( ) {
sub_sound ( 900 , 4.5 ) ;
2023-04-20 18:32:40 +00:00
// We'll send a very short silence after the beep so that two successive beeps sound unique
if ( audioEngine . waveform ) {
auto duration = 0.5 / 18.2 ;
auto waveType = audioEngine . waveform - > GetType ( ) ;
audioEngine . waveform - > SetType ( Waveform : : Type : : SILENCE ) ;
audioEngine . waveform - > Generate ( duration , duration ) ;
audioEngine . waveform - > Play ( ) ;
audioEngine . waveform - > SetType ( waveType ) ;
if ( ! audioEngine . musicBackground )
audioEngine . waveform - > AwaitPlaybackCompletion ( ) ; // await playback to complete if we are in MF mode
}
2023-04-19 12:45:07 +00:00
}
2022-08-26 15:15:16 +00:00
2023-04-19 12:45:07 +00:00
/// @brief This was designed to returned the number of notes in the background music queue.
/// However, here we'll just return the number of sample frame remaining to play when Play(), Sound() or Beep() are used
/// @param ignore Well, it's ignored
/// @return Returns the number of sample frames left to play for Play(), Sound() & Beep()
2022-08-26 15:15:16 +00:00
int32_t func_play ( int32_t ignore ) {
2023-04-10 20:19:14 +00:00
if ( audioEngine . isInitialized & & audioEngine . sndInternal = = 0 & & audioEngine . soundHandles [ audioEngine . sndInternal ] - > rawStream ) {
2022-12-21 06:17:41 +00:00
return ( int32_t ) audioEngine . soundHandles [ audioEngine . sndInternal ] - > rawStream - > GetSampleFramesRemaining ( ) ;
2022-08-26 15:15:16 +00:00
}
return 0 ;
}
2023-04-10 20:19:14 +00:00
/// @brief Processes and plays the MML specified in the string
/// @param str The string to play
2022-08-26 15:15:16 +00:00
void sub_play ( qbs * str ) {
if ( new_error | | ! audioEngine . isInitialized | | audioEngine . sndInternal ! = 0 )
return ;
2022-12-22 09:41:20 +00:00
// Kickstart the raw stream if it is not already
2022-12-15 03:46:42 +00:00
if ( ! audioEngine . soundHandles [ audioEngine . sndInternal ] - > rawStream ) {
2023-04-10 20:19:14 +00:00
// Initialize internal RawStream object
2022-12-15 03:46:42 +00:00
audioEngine . soundHandles [ audioEngine . sndInternal ] - > rawStream =
RawStreamCreate ( & audioEngine . maEngine , & audioEngine . soundHandles [ audioEngine . sndInternal ] - > maSound ) ;
2023-04-10 20:19:14 +00:00
if ( ! audioEngine . soundHandles [ audioEngine . sndInternal ] - > rawStream ) { // failed
AUDIO_DEBUG_PRINT ( " Failed to initialize RawStream object " ) ;
2022-12-15 03:46:42 +00:00
return ;
2022-08-26 15:15:16 +00:00
}
2023-04-20 18:32:40 +00:00
audioEngine . soundHandles [ audioEngine . sndInternal ] - > type = SoundHandle : : Type : : RAW ; // set type of sound
2023-04-10 20:19:14 +00:00
}
2022-08-26 15:15:16 +00:00
2023-04-10 20:19:14 +00:00
// Initialize internal Waveform object
if ( ! audioEngine . waveform ) {
audioEngine . waveform = new Waveform ( audioEngine . soundHandles [ audioEngine . sndInternal ] - > rawStream ) ;
2022-08-26 15:15:16 +00:00
2023-04-10 20:19:14 +00:00
if ( ! audioEngine . waveform ) {
AUDIO_DEBUG_PRINT ( " Failed to initialize Waveform object " ) ;
return ;
2022-08-26 15:15:16 +00:00
}
2023-04-10 20:19:14 +00:00
}
2022-08-26 15:15:16 +00:00
2023-04-10 20:19:14 +00:00
// Initialize internal MMLPlayer
if ( ! audioEngine . mmlPlayer ) {
audioEngine . mmlPlayer = new MMLPlayer ( audioEngine . waveform ) ;
2022-08-26 15:15:16 +00:00
2023-04-10 20:19:14 +00:00
if ( ! audioEngine . mmlPlayer ) {
AUDIO_DEBUG_PRINT ( " Failed to initialize MMLPlayer object " ) ;
return ;
2022-08-26 15:15:16 +00:00
}
2023-04-10 20:19:14 +00:00
}
2022-08-26 15:15:16 +00:00
2023-04-10 20:19:14 +00:00
audioEngine . mmlPlayer - > Play ( str ) ; // playback the string
audioEngine . musicBackground = audioEngine . mmlPlayer - > IsBackgroundPlayback ( ) ; // sync the background playback flag
2022-08-26 15:15:16 +00:00
2023-05-10 18:14:59 +00:00
if ( ! audioEngine . musicBackground ) // await playback to complete if we are in MF mode
2023-04-10 20:19:14 +00:00
audioEngine . waveform - > AwaitPlaybackCompletion ( ) ;
2022-08-26 15:15:16 +00:00
}
/// <summary>
/// This returns the sample rate from ma engine if ma is initialized.
/// </summary>
/// <returns>miniaudio sample rtate</returns>
int32_t func__sndrate ( ) { return audioEngine . sampleRate ; }
2023-01-09 00:03:39 +00:00
/// @brief Creates a ma_decoder and ma_sound from a memory buffer for a valid sound handle
/// @param buffer A raw pointer to the sound file in memory
/// @param size The size of the file in memory
/// @param handle A valid sound handle
/// @return MA_SUCCESS if successful. Else, a valid ma_result
static ma_result InitializeSoundFromMemory ( const void * buffer , size_t size , int32_t handle ) {
if ( ! IS_SOUND_HANDLE_VALID ( handle ) | | audioEngine . soundHandles [ handle ] - > maDecoder | | ! buffer | | ! size )
return MA_INVALID_ARGS ;
audioEngine . soundHandles [ handle ] - > maDecoder = new ma_decoder ( ) ; // allocate and zero memory
if ( ! audioEngine . soundHandles [ handle ] - > maDecoder ) {
AUDIO_DEBUG_PRINT ( " Failed to allocate memory for miniaudio decoder " ) ;
return MA_OUT_OF_MEMORY ;
}
// Setup the decoder & attach the custom backed vtables
audioEngine . soundHandles [ handle ] - > maDecoderConfig = ma_decoder_config_init_default ( ) ;
AudioEngineAttachCustomBackendVTables ( & audioEngine . soundHandles [ handle ] - > maDecoderConfig ) ;
audioEngine . soundHandles [ handle ] - > maDecoderConfig . sampleRate = audioEngine . sampleRate ;
audioEngine . maResult = ma_decoder_init_memory ( buffer , size , & audioEngine . soundHandles [ handle ] - > maDecoderConfig ,
audioEngine . soundHandles [ handle ] - > maDecoder ) ; // initialize the decoder
if ( audioEngine . maResult ! = MA_SUCCESS ) {
delete audioEngine . soundHandles [ handle ] - > maDecoder ;
audioEngine . soundHandles [ handle ] - > maDecoder = nullptr ;
AUDIO_DEBUG_PRINT ( " Error %i: failed to initialize miniaudio decoder " , audioEngine . maResult ) ;
return audioEngine . maResult ;
}
// Finally, load the sound as a data source
audioEngine . maResult = ma_sound_init_from_data_source ( & audioEngine . maEngine , audioEngine . soundHandles [ handle ] - > maDecoder ,
audioEngine . soundHandles [ handle ] - > maFlags , NULL , & audioEngine . soundHandles [ handle ] - > maSound ) ;
if ( audioEngine . maResult ! = MA_SUCCESS ) {
ma_decoder_uninit ( audioEngine . soundHandles [ handle ] - > maDecoder ) ;
delete audioEngine . soundHandles [ handle ] - > maDecoder ;
audioEngine . soundHandles [ handle ] - > maDecoder = nullptr ;
AUDIO_DEBUG_PRINT ( " Error %i: failed to initialize sound " , audioEngine . maResult ) ;
return audioEngine . maResult ;
}
return MA_SUCCESS ;
}
2022-08-26 15:15:16 +00:00
/// <summary>
/// This loads a sound file into memory and returns a LONG handle value above 0.
/// </summary>
/// <param name="fileName">The is the pathname for the sound file. This can be any format that miniaudio or a miniaudio plugin supports</param>
/// <param name="requirements">This is leftover from the old QB64-SDL days. But we use this to pass some parameters like 'stream'</param>
/// <param name="passed">How many parameters were passed?</param>
/// <returns>Returns a valid sound handle (> 0) if successful or 0 if it fails</returns>
int32_t func__sndopen ( qbs * fileName , qbs * requirements , int32_t passed ) {
// Some QB strings that we'll need
static qbs * fileNameZ = nullptr ;
static qbs * reqs = nullptr ;
2022-12-22 15:12:27 +00:00
if ( ! audioEngine . isInitialized | | ! fileName - > len )
2022-08-26 15:15:16 +00:00
return INVALID_SOUND_HANDLE ;
if ( ! fileNameZ )
fileNameZ = qbs_new ( 0 , 0 ) ;
if ( ! reqs )
reqs = qbs_new ( 0 , 0 ) ;
// Alocate a sound handle
int32_t handle = audioEngine . AllocateSoundHandle ( ) ;
if ( handle < 1 ) // We are not expected to open files with handle 0
return INVALID_SOUND_HANDLE ;
// Set some handle properties
2023-04-20 18:32:40 +00:00
audioEngine . soundHandles [ handle ] - > type = SoundHandle : : Type : : STATIC ;
2022-08-26 15:15:16 +00:00
2022-12-22 15:12:27 +00:00
// Prepare the requirements string
if ( passed & & requirements - > len )
2022-08-26 15:15:16 +00:00
qbs_set ( reqs , qbs_ucase ( requirements ) ) ; // Convert tmp str to perm str
2022-12-22 15:12:27 +00:00
// Set the flags to specifiy how we want the audio file to be opened
if ( passed & & requirements - > len & & func_instr ( 1 , reqs , qbs_new_txt ( REQUIREMENT_STRING_STREAM ) , 1 ) ) {
audioEngine . soundHandles [ handle ] - > maFlags | = MA_SOUND_FLAG_STREAM ; // Check if the user wants to stream the file
AUDIO_DEBUG_PRINT ( " Sound will stream " ) ;
2022-08-26 15:15:16 +00:00
} else {
audioEngine . soundHandles [ handle ] - > maFlags | = MA_SOUND_FLAG_DECODE ; // Else decode and load the whole sound in memory
2022-12-22 15:12:27 +00:00
AUDIO_DEBUG_PRINT ( " Sound will be fully decoded " ) ;
2022-08-26 15:15:16 +00:00
}
2022-12-22 15:12:27 +00:00
// Load the file from file or memory based on the requirements string
if ( passed & & requirements - > len & & func_instr ( 1 , reqs , qbs_new_txt ( REQUIREMENT_STRING_MEMORY ) , 1 ) ) {
// Configure a miniaudio decoder to load the sound from memory
AUDIO_DEBUG_PRINT ( " Loading sound from memory " ) ;
2023-01-09 00:03:39 +00:00
audioEngine . soundHandles [ handle ] - > bufferKey = ( intptr_t ) fileName - > chr ; // make a unique key and save it
audioEngine . bufferMap . AddBuffer ( fileName - > chr , fileName - > len , audioEngine . soundHandles [ handle ] - > bufferKey ) ; // make a copy of the buffer
auto [ buffer , bufferSize ] = audioEngine . bufferMap . GetBuffer ( audioEngine . soundHandles [ handle ] - > bufferKey ) ; // get the buffer pointer and size
audioEngine . maResult = InitializeSoundFromMemory ( buffer , bufferSize , handle ) ; // create the ma_sound
2022-12-22 15:12:27 +00:00
} else {
AUDIO_DEBUG_PRINT ( " Loading sound from file '%s' " , fileNameZ - > chr ) ;
qbs_set ( fileNameZ , qbs_add ( fileName , qbs_new_txt_len ( " \0 " , 1 ) ) ) ; // s1 = filename + CHR$(0)
// Forward the request to miniaudio to open the sound file
audioEngine . maResult = ma_sound_init_from_file ( & audioEngine . maEngine , ( const char * ) fileNameZ - > chr , audioEngine . soundHandles [ handle ] - > maFlags , NULL ,
NULL , & audioEngine . soundHandles [ handle ] - > maSound ) ;
}
2022-08-26 15:15:16 +00:00
2022-12-30 20:19:23 +00:00
// If the sound failed to initialize, then free the handle and return INVALID_SOUND_HANDLE
2022-08-26 15:15:16 +00:00
if ( audioEngine . maResult ! = MA_SUCCESS ) {
2023-01-09 00:03:39 +00:00
AUDIO_DEBUG_PRINT ( " Error %i: failed to open sound " , audioEngine . maResult ) ;
2022-08-26 15:15:16 +00:00
audioEngine . soundHandles [ handle ] - > isUsed = false ;
return INVALID_SOUND_HANDLE ;
}
2022-12-22 15:12:27 +00:00
AUDIO_DEBUG_PRINT ( " Sound successfully loaded " ) ;
2022-08-26 15:15:16 +00:00
return handle ;
}
/// <summary>
/// The frees and unloads an open sound.
/// If the sound is playing, it'll let it finish. Looping sounds will loop until the program is closed.
/// If the sound is a stream of raw samples then any remaining samples pending for playback will be sent to miniaudio and then the handle will be freed.
/// </summary>
/// <param name="handle">A sound handle</param>
void sub__sndclose ( int32_t handle ) {
if ( audioEngine . isInitialized & & IS_SOUND_HANDLE_VALID ( handle ) ) {
// If we have a raw stream then force it to push all it's data to miniaudio
// Note that this will take care of checking if the handle is a raw steam and other stuff
// So it is completly safe to call it this way
sub__sndrawdone ( handle , true ) ;
2023-04-20 18:32:40 +00:00
if ( audioEngine . soundHandles [ handle ] - > type = = SoundHandle : : Type : : RAW )
2022-12-21 06:17:41 +00:00
audioEngine . soundHandles [ handle ] - > rawStream - > stop = true ; // Signal miniaudio thread that we are going to end playback
2022-08-26 15:15:16 +00:00
// Simply set the autokill flag to true and let the sound loop handle disposing the sound
audioEngine . soundHandles [ handle ] - > autoKill = true ;
}
}
/// <summary>
/// This copies a sound to a new handle so that two or more of the same sound can be played at once.
/// </summary>
/// <param name="src_handle">A source sound handle</param>
/// <returns>A new sound handle if successful or 0 on failure</returns>
int32_t func__sndcopy ( int32_t src_handle ) {
// Check for all invalid cases
2023-04-20 18:32:40 +00:00
if ( ! audioEngine . isInitialized | | ! IS_SOUND_HANDLE_VALID ( src_handle ) | | audioEngine . soundHandles [ src_handle ] - > type ! = SoundHandle : : Type : : STATIC )
2022-08-26 15:15:16 +00:00
return INVALID_SOUND_HANDLE ;
2023-01-07 18:44:59 +00:00
int32_t dst_handle = INVALID_SOUND_HANDLE ;
2022-08-26 15:15:16 +00:00
2023-01-07 18:44:59 +00:00
// Miniaudio will not copy sounds attached to ma_audio_buffers so we'll handle the duplication ourselves
// Sadly, since this involves a buffer copy there may be a delay before the sound can play (especially if the sound is lengthy)
// The delay may be noticeable when _SNDPLAYCOPY is used multiple times on the a _SNDNEW sound
if ( audioEngine . soundHandles [ src_handle ] - > maAudioBuffer ) {
2023-01-09 00:03:39 +00:00
AUDIO_DEBUG_PRINT ( " Doing custom sound copy for ma_audio_buffer " ) ;
2022-08-26 15:15:16 +00:00
2023-01-07 18:44:59 +00:00
auto frames = audioEngine . soundHandles [ src_handle ] - > maAudioBuffer - > ref . sizeInFrames ;
auto channels = audioEngine . soundHandles [ src_handle ] - > maAudioBuffer - > ref . channels ;
auto format = audioEngine . soundHandles [ src_handle ] - > maAudioBuffer - > ref . format ;
2022-08-26 15:15:16 +00:00
2023-01-07 18:44:59 +00:00
// First create a new _SNDNEW sound with the same properties at the source
dst_handle = func__sndnew ( frames , channels , CHAR_BIT * ma_get_bytes_per_sample ( format ) ) ;
if ( dst_handle < 1 )
return INVALID_SOUND_HANDLE ;
// Next memcopy the samples from the source to the dest
memcpy ( ( void * ) audioEngine . soundHandles [ dst_handle ] - > maAudioBuffer - > ref . pData , audioEngine . soundHandles [ src_handle ] - > maAudioBuffer - > ref . pData ,
frames * ma_get_bytes_per_frame ( format , channels ) ) ; // naughty const void* casting, but should be OK
2023-01-09 00:03:39 +00:00
} else if ( audioEngine . soundHandles [ src_handle ] - > maDecoder ) {
AUDIO_DEBUG_PRINT ( " Doing custom sound copy for ma_decoder " ) ;
dst_handle = audioEngine . AllocateSoundHandle ( ) ; // alocate a sound handle
if ( dst_handle < 1 )
return INVALID_SOUND_HANDLE ;
2023-04-20 18:32:40 +00:00
audioEngine . soundHandles [ dst_handle ] - > type = SoundHandle : : Type : : STATIC ; // set some handle properties
2023-01-09 00:03:39 +00:00
audioEngine . soundHandles [ dst_handle ] - > maFlags = audioEngine . soundHandles [ src_handle ] - > maFlags ; // copy the flags
audioEngine . soundHandles [ dst_handle ] - > bufferKey = audioEngine . soundHandles [ src_handle ] - > bufferKey ; // copy the BufferMap unique key
audioEngine . bufferMap . AddRef ( audioEngine . soundHandles [ dst_handle ] - > bufferKey ) ; // increase the reference count
auto [ buffer , bufferSize ] = audioEngine . bufferMap . GetBuffer ( audioEngine . soundHandles [ dst_handle ] - > bufferKey ) ; // get the buffer pointer and size
audioEngine . maResult = InitializeSoundFromMemory ( buffer , bufferSize , dst_handle ) ; // create the ma_sound
if ( audioEngine . maResult ! = MA_SUCCESS ) {
audioEngine . bufferMap . Release ( audioEngine . soundHandles [ dst_handle ] - > bufferKey ) ;
audioEngine . soundHandles [ dst_handle ] - > isUsed = false ;
AUDIO_DEBUG_PRINT ( " Error %i: failed to copy sound " , audioEngine . maResult ) ;
return INVALID_SOUND_HANDLE ;
}
2023-01-07 18:44:59 +00:00
} else {
AUDIO_DEBUG_PRINT ( " Doing regular miniaudio sound copy " ) ;
2023-01-09 00:03:39 +00:00
2023-01-07 18:44:59 +00:00
dst_handle = audioEngine . AllocateSoundHandle ( ) ; // alocate a sound handle
if ( dst_handle < 1 )
return INVALID_SOUND_HANDLE ;
2023-04-20 18:32:40 +00:00
audioEngine . soundHandles [ dst_handle ] - > type = SoundHandle : : Type : : STATIC ; // set some handle properties
2023-01-07 18:44:59 +00:00
audioEngine . soundHandles [ dst_handle ] - > maFlags = audioEngine . soundHandles [ src_handle ] - > maFlags ; // copy the flags
// Initialize a new copy of the sound
audioEngine . maResult = ma_sound_init_copy ( & audioEngine . maEngine , & audioEngine . soundHandles [ src_handle ] - > maSound ,
audioEngine . soundHandles [ dst_handle ] - > maFlags , NULL , & audioEngine . soundHandles [ dst_handle ] - > maSound ) ;
// If the sound failed to copy, then free the handle and return INVALID_SOUND_HANDLE
if ( audioEngine . maResult ! = MA_SUCCESS ) {
audioEngine . soundHandles [ dst_handle ] - > isUsed = false ;
AUDIO_DEBUG_PRINT ( " Error %i: failed to copy sound " , audioEngine . maResult ) ;
return INVALID_SOUND_HANDLE ;
}
2022-08-26 15:15:16 +00:00
}
return dst_handle ;
}
/// <summary>
/// This plays a sound designated by a sound handle.
/// </summary>
/// <param name="handle">A sound handle</param>
void sub__sndplay ( int32_t handle ) {
2023-04-20 18:32:40 +00:00
if ( audioEngine . isInitialized & & IS_SOUND_HANDLE_VALID ( handle ) & & audioEngine . soundHandles [ handle ] - > type = = SoundHandle : : Type : : STATIC ) {
2022-08-26 15:15:16 +00:00
// Reset position to zero only if we are playing and (not looping or we've reached the end of the sound)
// This is based on the old OpenAl-soft code behavior
if ( ma_sound_is_playing ( & audioEngine . soundHandles [ handle ] - > maSound ) & &
( ! ma_sound_is_looping ( & audioEngine . soundHandles [ handle ] - > maSound ) | | ma_sound_at_end ( & audioEngine . soundHandles [ handle ] - > maSound ) ) ) {
audioEngine . maResult = ma_sound_seek_to_pcm_frame ( & audioEngine . soundHandles [ handle ] - > maSound , 0 ) ;
AUDIO_DEBUG_CHECK ( audioEngine . maResult = = MA_SUCCESS ) ;
}
// Kickstart playback
audioEngine . maResult = ma_sound_start ( & audioEngine . soundHandles [ handle ] - > maSound ) ;
AUDIO_DEBUG_CHECK ( audioEngine . maResult = = MA_SUCCESS ) ;
// Stop looping the sound if it is
if ( ma_sound_is_looping ( & audioEngine . soundHandles [ handle ] - > maSound ) ) {
ma_sound_set_looping ( & audioEngine . soundHandles [ handle ] - > maSound , MA_FALSE ) ;
}
2023-01-07 18:44:59 +00:00
AUDIO_DEBUG_PRINT ( " Playing sound %i " , handle ) ;
2022-08-26 15:15:16 +00:00
}
}
/// <summary>
/// This copies a sound, plays it, and automatically closes the copy.
/// </summary>
/// <param name="handle">A sound handle to copy</param>
/// <param name="volume">The volume at which the sound should be played (0.0 - 1.0)</param>
2022-10-04 21:03:44 +00:00
/// <param name="x">x distance values go from left (negative) to right (positive)</param>
/// <param name="y">y distance values go from below (negative) to above (positive).</param>
/// <param name="z">z distance values go from behind (negative) to in front (positive).</param>
2022-08-26 15:15:16 +00:00
/// <param name="passed">How many parameters were passed?</param>
2022-10-04 21:03:44 +00:00
void sub__sndplaycopy ( int32_t src_handle , double volume , double x , double y , double z , int32_t passed ) {
2022-08-26 15:15:16 +00:00
// We are simply going to use sndcopy, then setup some stuff like volume and autokill and then use sndplay
// We are not checking if the audio engine was initialized because if not we'll get an invalid handle anyway
2023-01-07 18:44:59 +00:00
auto dst_handle = func__sndcopy ( src_handle ) ;
AUDIO_DEBUG_PRINT ( " Source handle = %i, destination handle = %i " , src_handle , dst_handle ) ;
2022-08-26 15:15:16 +00:00
// Check if we succeeded and then proceed
if ( dst_handle > 0 ) {
// Set the volume if requested
2022-10-04 21:03:44 +00:00
if ( passed & 1 )
2022-08-26 15:15:16 +00:00
ma_sound_set_volume ( & audioEngine . soundHandles [ dst_handle ] - > maSound , volume ) ;
2022-10-05 09:58:04 +00:00
if ( passed & 4 | | passed & 8 ) { // If y or z or both are passed
ma_sound_set_spatialization_enabled ( & audioEngine . soundHandles [ dst_handle ] - > maSound , MA_TRUE ) ; // Enable 3D spatialization
ma_sound_set_position ( & audioEngine . soundHandles [ dst_handle ] - > maSound , x , y , z ) ; // Use full 3D positioning
} else if ( passed & 2 ) { // If x is passed
2022-10-04 21:03:44 +00:00
ma_sound_set_spatialization_enabled ( & audioEngine . soundHandles [ dst_handle ] - > maSound , MA_FALSE ) ; // Disable spatialization for better stereo sound
ma_sound_set_pan_mode ( & audioEngine . soundHandles [ dst_handle ] - > maSound , ma_pan_mode_pan ) ; // Set true panning
ma_sound_set_pan ( & audioEngine . soundHandles [ dst_handle ] - > maSound , x ) ; // Just use stereo panning
}
2022-08-26 15:15:16 +00:00
sub__sndplay ( dst_handle ) ; // Play the sound
audioEngine . soundHandles [ dst_handle ] - > autoKill = true ; // Set to auto kill
2022-10-05 01:01:46 +00:00
2023-01-07 18:44:59 +00:00
AUDIO_DEBUG_PRINT ( " Playing sound copy %i: volume %lf, 3D (%lf, %lf, %lf) " , dst_handle , volume , x , y , z ) ;
}
2022-08-26 15:15:16 +00:00
}
/// <summary>
/// This is a "fire and forget" style of function.
/// The engine will manage the sound handle internally.
/// When the sound finishes playing, the handle will be put up for recycling.
/// Playback starts asynchronously.
/// </summary>
/// <param name="fileName">The is the name of the file to be played</param>
/// <param name="sync">This paramater is ignored</param>
/// <param name="volume">This the sound playback volume (0 - silent ... 1 - full)</param>
/// <param name="passed">How many parameters were passed?</param>
void sub__sndplayfile ( qbs * fileName , int32_t sync , double volume , int32_t passed ) {
// We need this to send requirements to SndOpen
static qbs * reqs = nullptr ;
if ( ! reqs ) {
// Since this never changes, we can get away by doing this just once
reqs = qbs_new ( 0 , 0 ) ;
qbs_set ( reqs , qbs_new_txt ( REQUIREMENT_STRING_STREAM ) ) ;
}
// We will not wrap this in a 'if initialized' block because SndOpen will take care of that
int32_t handle = func__sndopen ( fileName , reqs , 1 ) ;
if ( handle > 0 ) {
if ( passed & 2 )
ma_sound_set_volume ( & audioEngine . soundHandles [ handle ] - > maSound , volume ) ;
sub__sndplay ( handle ) ; // Play the sound
audioEngine . soundHandles [ handle ] - > autoKill = true ; // Set to auto kill
}
}
/// <summary>
/// This pauses a sound using a sound handle.
/// </summary>
/// <param name="handle">A sound handle</param>
void sub__sndpause ( int32_t handle ) {
2023-04-20 18:32:40 +00:00
if ( audioEngine . isInitialized & & IS_SOUND_HANDLE_VALID ( handle ) & & audioEngine . soundHandles [ handle ] - > type = = SoundHandle : : Type : : STATIC ) {
2022-08-26 15:15:16 +00:00
// Stop the sound and just leave it at that
// miniaudio does not reset the play cursor
audioEngine . maResult = ma_sound_stop ( & audioEngine . soundHandles [ handle ] - > maSound ) ;
AUDIO_DEBUG_CHECK ( audioEngine . maResult = = MA_SUCCESS ) ;
}
}
/// <summary>
/// This returns whether a sound is being played.
/// </summary>
/// <param name="handle">A sound handle</param>
/// <returns>Return true if the sound is playing. False otherwise</returns>
int32_t func__sndplaying ( int32_t handle ) {
2023-04-20 18:32:40 +00:00
if ( audioEngine . isInitialized & & IS_SOUND_HANDLE_VALID ( handle ) & & audioEngine . soundHandles [ handle ] - > type = = SoundHandle : : Type : : STATIC ) {
2022-08-26 15:15:16 +00:00
return ma_sound_is_playing ( & audioEngine . soundHandles [ handle ] - > maSound ) ? QB_TRUE : QB_FALSE ;
}
return QB_FALSE ;
}
/// <summary>
/// This checks if a sound is paused.
/// </summary>
/// <param name="handle">A sound handle</param>
/// <returns>Returns true if the sound is paused. False otherwise</returns>
int32_t func__sndpaused ( int32_t handle ) {
2023-04-20 18:32:40 +00:00
if ( audioEngine . isInitialized & & IS_SOUND_HANDLE_VALID ( handle ) & & audioEngine . soundHandles [ handle ] - > type = = SoundHandle : : Type : : STATIC ) {
2022-08-26 15:15:16 +00:00
return ! ma_sound_is_playing ( & audioEngine . soundHandles [ handle ] - > maSound ) & &
( ma_sound_is_looping ( & audioEngine . soundHandles [ handle ] - > maSound ) | | ! ma_sound_at_end ( & audioEngine . soundHandles [ handle ] - > maSound ) )
? QB_TRUE
: QB_FALSE ;
}
return QB_FALSE ;
}
/// <summary>
/// This sets the volume of a sound loaded in memory using a sound handle.
/// New: This works for both static and raw sounds.
/// </summary>
/// <param name="handle">A sound handle</param>
/// <param name="volume">A float point value with 0 resulting in silence and anything above 1 resulting in amplification</param>
void sub__sndvol ( int32_t handle , float volume ) {
if ( audioEngine . isInitialized & & IS_SOUND_HANDLE_VALID ( handle ) & &
2023-04-20 18:32:40 +00:00
( audioEngine . soundHandles [ handle ] - > type = = SoundHandle : : Type : : STATIC | | audioEngine . soundHandles [ handle ] - > type = = SoundHandle : : Type : : RAW ) ) {
2022-08-26 15:15:16 +00:00
ma_sound_set_volume ( & audioEngine . soundHandles [ handle ] - > maSound , volume ) ;
}
}
/// <summary>
/// This is like sub__sndplay but the sound is looped.
/// </summary>
/// <param name="handle"></param>
void sub__sndloop ( int32_t handle ) {
2023-04-20 18:32:40 +00:00
if ( audioEngine . isInitialized & & IS_SOUND_HANDLE_VALID ( handle ) & & audioEngine . soundHandles [ handle ] - > type = = SoundHandle : : Type : : STATIC ) {
2022-08-26 15:15:16 +00:00
// Reset position to zero only if we are playing and (not looping or we've reached the end of the sound)
// This is based on the old OpenAl-soft code behavior
if ( ma_sound_is_playing ( & audioEngine . soundHandles [ handle ] - > maSound ) & &
( ! ma_sound_is_looping ( & audioEngine . soundHandles [ handle ] - > maSound ) | | ma_sound_at_end ( & audioEngine . soundHandles [ handle ] - > maSound ) ) ) {
audioEngine . maResult = ma_sound_seek_to_pcm_frame ( & audioEngine . soundHandles [ handle ] - > maSound , 0 ) ;
AUDIO_DEBUG_CHECK ( audioEngine . maResult = = MA_SUCCESS ) ;
}
// Kickstart playback
audioEngine . maResult = ma_sound_start ( & audioEngine . soundHandles [ handle ] - > maSound ) ;
AUDIO_DEBUG_CHECK ( audioEngine . maResult = = MA_SUCCESS ) ;
// Start looping the sound if it is not
if ( ! ma_sound_is_looping ( & audioEngine . soundHandles [ handle ] - > maSound ) ) {
ma_sound_set_looping ( & audioEngine . soundHandles [ handle ] - > maSound , MA_TRUE ) ;
}
}
}
/// <summary>
/// This will attempt to set the balance or 3D position of a sound.
/// Note that unlike the OpenAL code, we will do pure stereo panning if y & z are absent.
/// New: This works for both static and raw sounds.
/// </summary>
/// <param name="handle">A sound handle</param>
/// <param name="x">x distance values go from left (negative) to right (positive)</param>
/// <param name="y">y distance values go from below (negative) to above (positive).</param>
/// <param name="z">z distance values go from behind (negative) to in front (positive).</param>
/// <param name="channel">channel value 1 denotes left (mono) and 2 denotes right (stereo) channel. This has no meaning for miniaudio and is ignored</param>
/// <param name="passed">How many parameters were passed?</param>
void sub__sndbal ( int32_t handle , double x , double y , double z , int32_t channel , int32_t passed ) {
if ( audioEngine . isInitialized & & IS_SOUND_HANDLE_VALID ( handle ) & &
2023-04-20 18:32:40 +00:00
( audioEngine . soundHandles [ handle ] - > type = = SoundHandle : : Type : : STATIC | | audioEngine . soundHandles [ handle ] - > type = = SoundHandle : : Type : : RAW ) ) {
2022-08-26 15:15:16 +00:00
if ( passed & 2 | | passed & 4 ) { // If y or z or both are passed
ma_sound_set_spatialization_enabled ( & audioEngine . soundHandles [ handle ] - > maSound , MA_TRUE ) ; // Enable 3D spatialization
2023-05-10 18:14:59 +00:00
ma_vec3f v = ma_sound_get_position ( & audioEngine . soundHandles [ handle ] - > maSound ) ; // Get the current position in 3D space
2022-08-26 15:15:16 +00:00
// Set the previous values of x, y, z if these were not passed
if ( ! ( passed & 1 ) )
x = v . x ;
if ( ! ( passed & 2 ) )
y = v . y ;
if ( ! ( passed & 4 ) )
z = v . z ;
2022-10-05 09:58:04 +00:00
ma_sound_set_position ( & audioEngine . soundHandles [ handle ] - > maSound , x , y , z ) ; // Use full 3D positioning
} else if ( passed & 1 ) { // Only bother if x is passed
2022-08-26 15:15:16 +00:00
ma_sound_set_spatialization_enabled ( & audioEngine . soundHandles [ handle ] - > maSound , MA_FALSE ) ; // Disable spatialization for better stereo sound
ma_sound_set_pan_mode ( & audioEngine . soundHandles [ handle ] - > maSound , ma_pan_mode_pan ) ; // Set true panning
ma_sound_set_pan ( & audioEngine . soundHandles [ handle ] - > maSound , x ) ; // Just use stereo panning
}
}
}
/// <summary>
/// This returns the length in seconds of a loaded sound using a sound handle.
/// </summary>
/// <param name="handle">A sound handle</param>
/// <returns>Returns the length of a sound in seconds</returns>
double func__sndlen ( int32_t handle ) {
2023-04-20 18:32:40 +00:00
if ( audioEngine . isInitialized & & IS_SOUND_HANDLE_VALID ( handle ) & & audioEngine . soundHandles [ handle ] - > type = = SoundHandle : : Type : : STATIC ) {
2022-08-26 15:15:16 +00:00
float lengthSeconds = 0 ;
audioEngine . maResult = ma_sound_get_length_in_seconds ( & audioEngine . soundHandles [ handle ] - > maSound , & lengthSeconds ) ;
AUDIO_DEBUG_CHECK ( audioEngine . maResult = = MA_SUCCESS ) ;
return lengthSeconds ;
}
return 0 ;
}
/// <summary>
/// This returns the current playing position in seconds using a sound handle.
/// </summary>
/// <param name="handle">A sound handle</param>
/// <returns>Returns the current playing position in seconds from an open sound file</returns>
double func__sndgetpos ( int32_t handle ) {
2023-04-20 18:32:40 +00:00
if ( audioEngine . isInitialized & & IS_SOUND_HANDLE_VALID ( handle ) & & audioEngine . soundHandles [ handle ] - > type = = SoundHandle : : Type : : STATIC ) {
2022-08-26 15:15:16 +00:00
float playCursorSeconds = 0 ;
audioEngine . maResult = ma_sound_get_cursor_in_seconds ( & audioEngine . soundHandles [ handle ] - > maSound , & playCursorSeconds ) ;
AUDIO_DEBUG_CHECK ( audioEngine . maResult = = MA_SUCCESS ) ;
return playCursorSeconds ;
}
return 0 ;
}
/// <summary>
/// This changes the current/starting playing position in seconds of a sound.
/// </summary>
/// <param name="handle">A sound handle</param>
/// <param name="seconds">The position to set in seconds</param>
void sub__sndsetpos ( int32_t handle , double seconds ) {
2023-04-20 18:32:40 +00:00
if ( audioEngine . isInitialized & & IS_SOUND_HANDLE_VALID ( handle ) & & audioEngine . soundHandles [ handle ] - > type = = SoundHandle : : Type : : STATIC ) {
2023-01-06 15:02:03 +00:00
float lengthSeconds = 0 ;
2022-08-26 15:15:16 +00:00
audioEngine . maResult = ma_sound_get_length_in_seconds ( & audioEngine . soundHandles [ handle ] - > maSound , & lengthSeconds ) ; // Get the length in seconds
if ( audioEngine . maResult ! = MA_SUCCESS )
return ;
if ( seconds > lengthSeconds ) // If position is beyond length then simply stop playback and exit
{
audioEngine . maResult = ma_sound_stop ( & audioEngine . soundHandles [ handle ] - > maSound ) ;
AUDIO_DEBUG_CHECK ( audioEngine . maResult = = MA_SUCCESS ) ;
return ;
}
ma_uint64 lengthSampleFrames ;
audioEngine . maResult =
ma_sound_get_length_in_pcm_frames ( & audioEngine . soundHandles [ handle ] - > maSound , & lengthSampleFrames ) ; // Get the total sample frames
if ( audioEngine . maResult ! = MA_SUCCESS )
return ;
audioEngine . maResult = ma_sound_seek_to_pcm_frame ( & audioEngine . soundHandles [ handle ] - > maSound ,
lengthSampleFrames * ( seconds / lengthSeconds ) ) ; // Set the postion in PCM frames
AUDIO_DEBUG_CHECK ( audioEngine . maResult = = MA_SUCCESS ) ;
}
}
/// <summary>
/// This stops playing a sound after it has been playing for a set number of seconds.
/// </summary>
/// <param name="handle">A sound handle</param>
/// <param name="limit">The number of seconds that the sound will play</param>
void sub__sndlimit ( int32_t handle , double limit ) {
2023-04-20 18:32:40 +00:00
if ( audioEngine . isInitialized & & IS_SOUND_HANDLE_VALID ( handle ) & & audioEngine . soundHandles [ handle ] - > type = = SoundHandle : : Type : : STATIC ) {
2022-08-26 15:15:16 +00:00
ma_sound_set_stop_time_in_milliseconds ( & audioEngine . soundHandles [ handle ] - > maSound , limit * 1000 ) ;
}
}
/// <summary>
/// This stops a playing or paused sound using a sound handle.
/// </summary>
/// <param name="handle">A sound handle</param>
void sub__sndstop ( int32_t handle ) {
2023-04-20 18:32:40 +00:00
if ( audioEngine . isInitialized & & IS_SOUND_HANDLE_VALID ( handle ) & & audioEngine . soundHandles [ handle ] - > type = = SoundHandle : : Type : : STATIC ) {
2022-08-26 15:15:16 +00:00
// Stop the sound first
audioEngine . maResult = ma_sound_stop ( & audioEngine . soundHandles [ handle ] - > maSound ) ;
AUDIO_DEBUG_CHECK ( audioEngine . maResult = = MA_SUCCESS ) ;
// Also reset the playback cursor to zero
audioEngine . maResult = ma_sound_seek_to_pcm_frame ( & audioEngine . soundHandles [ handle ] - > maSound , 0 ) ;
AUDIO_DEBUG_CHECK ( audioEngine . maResult = = MA_SUCCESS ) ;
}
}
/// <summary>
/// This function opens a new channel to fill with _SNDRAW content to manage multiple dynamically generated sounds.
/// </summary>
/// <returns>A new sound handle if successful or 0 on failure</returns>
int32_t func__sndopenraw ( ) {
// Return invalid handle if audio engine is not initialized
if ( ! audioEngine . isInitialized )
return INVALID_SOUND_HANDLE ;
// Alocate a sound handle
int32_t handle = audioEngine . AllocateSoundHandle ( ) ;
if ( handle < 1 )
return INVALID_SOUND_HANDLE ;
// Set some handle properties
2023-04-20 18:32:40 +00:00
audioEngine . soundHandles [ handle ] - > type = SoundHandle : : Type : : RAW ;
2022-08-26 15:15:16 +00:00
// Create the raw sound object
2022-12-15 03:46:42 +00:00
audioEngine . soundHandles [ handle ] - > rawStream = RawStreamCreate ( & audioEngine . maEngine , & audioEngine . soundHandles [ handle ] - > maSound ) ;
if ( ! audioEngine . soundHandles [ handle ] - > rawStream )
2022-08-26 15:15:16 +00:00
return INVALID_SOUND_HANDLE ;
return handle ;
}
/// <summary>
/// This plays sound wave sample frequencies created by a program.
/// </summary>
/// <param name="left">Left channel sample</param>
/// <param name="right">Right channel sample</param>
/// <param name="handle">A sound handle</param>
/// <param name="passed">How many parameters were passed?</param>
void sub__sndraw ( float left , float right , int32_t handle , int32_t passed ) {
// Use the default raw handle if handle was not passed
if ( ! ( passed & 2 ) ) {
// Check if the default handle was created
if ( audioEngine . sndInternalRaw < 1 ) {
audioEngine . sndInternalRaw = func__sndopenraw ( ) ;
}
handle = audioEngine . sndInternalRaw ;
}
2023-04-20 18:32:40 +00:00
if ( audioEngine . isInitialized & & IS_SOUND_HANDLE_VALID ( handle ) & & audioEngine . soundHandles [ handle ] - > type = = SoundHandle : : Type : : RAW ) {
2022-08-26 15:15:16 +00:00
if ( ! ( passed & 1 ) )
right = left ;
2022-12-21 06:17:41 +00:00
audioEngine . soundHandles [ handle ] - > rawStream - > PushSampleFrame ( left , right ) ;
2022-08-26 15:15:16 +00:00
}
}
/// <summary>
/// This ensures that the final buffer portion is played in short sound effects even if it is incomplete.
/// </summary>
/// <param name="handle">A sound handle</param>
/// <param name="passed">How many parameters were passed?</param>
void sub__sndrawdone ( int32_t handle , int32_t passed ) {
2022-12-15 03:46:42 +00:00
// This is NOP now because miniaudio data source automatically pulls in all the samples without us doing anything
// As such, we need to think about the future of this function. Probably just leave it this way?
( void ) handle ;
( void ) passed ;
/*
2022-08-26 15:15:16 +00:00
// Use the default raw handle if handle was not passed
if ( ! passed )
handle = audioEngine . sndInternalRaw ;
2023-04-20 18:32:40 +00:00
if ( audioEngine . isInitialized & & IS_SOUND_HANDLE_VALID ( handle ) & & audioEngine . soundHandles [ handle ] - > type = = SoundHandle : : Type : : RAW ) {
2022-12-15 03:46:42 +00:00
// NOP
2022-08-26 15:15:16 +00:00
}
2022-12-15 03:46:42 +00:00
*/
2022-08-26 15:15:16 +00:00
}
/// <summary>
/// This function returns the length, in seconds, of a _SNDRAW sound currently queued.
/// </summary>
/// <param name="handle">A sound handle</param>
/// <param name="passed">How many parameters were passed?</param>
/// <returns></returns>
double func__sndrawlen ( int32_t handle , int32_t passed ) {
// Use the default raw handle if handle was not passed
if ( ! passed )
handle = audioEngine . sndInternalRaw ;
2023-04-20 18:32:40 +00:00
if ( audioEngine . isInitialized & & IS_SOUND_HANDLE_VALID ( handle ) & & audioEngine . soundHandles [ handle ] - > type = = SoundHandle : : Type : : RAW ) {
2022-12-21 06:17:41 +00:00
return audioEngine . soundHandles [ handle ] - > rawStream - > GetTimeRemaining ( ) ;
2022-08-26 15:15:16 +00:00
}
return 0 ;
}
2022-12-30 20:19:23 +00:00
/// <summary>
/// This returns a sound handle to a newly created sound's raw data in memory with the given specification.
/// The user can then fill the buffer with whatever they want (using _MEMSOUND) and play it.
/// This is basically the sound equivalent of _NEWIMAGE.
/// </summary>
/// <param name="frames">The number of sample frames required</param>
/// <param name="channels">The number of sound channels. This can be 1 (mono) or 2 (stereo)/param>
/// <param name="bits">The bit depth of the sound. This can be 8 (unsigned 8-bit), 16 (signed 16-bit) or 32 (FP32)</param>
/// <returns>A new sound handle if successful or 0 on failure</returns>
2022-12-31 06:30:38 +00:00
int32_t func__sndnew ( int32_t frames , int32_t channels , int32_t bits ) {
2022-12-30 20:19:23 +00:00
if ( ! audioEngine . isInitialized | | frames < = 0 ) {
AUDIO_DEBUG_CHECK ( frames > 0 ) ;
return INVALID_SOUND_HANDLE ;
}
// Validate all parameters
2022-12-31 06:30:38 +00:00
if ( ( channels ! = 1 & & channels ! = 2 ) | | ( bits ! = 16 & & bits ! = 32 & & bits ! = 8 ) ) {
AUDIO_DEBUG_PRINT ( " Invalid channels (%i) or bits (%i) " , channels , bits ) ;
2022-12-30 20:19:23 +00:00
return INVALID_SOUND_HANDLE ;
}
// Alocate a sound handle
int32_t handle = audioEngine . AllocateSoundHandle ( ) ;
if ( handle < 1 )
return INVALID_SOUND_HANDLE ;
// Set some handle properties
2023-04-20 18:32:40 +00:00
audioEngine . soundHandles [ handle ] - > type = SoundHandle : : Type : : STATIC ;
2022-12-30 20:19:23 +00:00
// Setup the ma_audio_buffer
audioEngine . soundHandles [ handle ] - > maAudioBufferConfig = ma_audio_buffer_config_init (
( bits = = 32 ? ma_format : : ma_format_f32 : ( bits = = 16 ? ma_format : : ma_format_s16 : ma_format : : ma_format_u8 ) ) , channels , frames , NULL , NULL ) ;
2022-12-31 06:30:38 +00:00
// This currently has no effect. Sample rate always defaults to engine sample rate
// Sample rate support for audio buffer is coming in miniaudio version 0.12
// Once we have support, we can add sample rate as an optional 4th parameter
// audioEngine.soundHandles[handle]->maAudioBufferConfig.sampleRate = audioEngine.sampleRate;
2022-12-30 20:19:23 +00:00
// Allocate and initialize ma_audio_buffer
audioEngine . maResult =
ma_audio_buffer_alloc_and_init ( & audioEngine . soundHandles [ handle ] - > maAudioBufferConfig , & audioEngine . soundHandles [ handle ] - > maAudioBuffer ) ;
if ( audioEngine . maResult ! = MA_SUCCESS ) {
AUDIO_DEBUG_PRINT ( " Error %i: failed to initialize audio buffer " , audioEngine . maResult ) ;
audioEngine . soundHandles [ handle ] - > isUsed = false ;
return INVALID_SOUND_HANDLE ;
}
// Create a ma_sound from the ma_audio_buffer
audioEngine . maResult = ma_sound_init_from_data_source ( & audioEngine . maEngine , audioEngine . soundHandles [ handle ] - > maAudioBuffer ,
audioEngine . soundHandles [ handle ] - > maFlags , NULL , & audioEngine . soundHandles [ handle ] - > maSound ) ;
if ( audioEngine . maResult ! = MA_SUCCESS ) {
AUDIO_DEBUG_PRINT ( " Error %i: failed to initialize data source " , audioEngine . maResult ) ;
ma_audio_buffer_uninit_and_free ( audioEngine . soundHandles [ handle ] - > maAudioBuffer ) ;
audioEngine . soundHandles [ handle ] - > maAudioBuffer = nullptr ;
2023-01-07 18:44:59 +00:00
audioEngine . soundHandles [ handle ] - > isUsed = false ;
2022-12-30 20:19:23 +00:00
return INVALID_SOUND_HANDLE ;
}
2023-01-07 18:44:59 +00:00
AUDIO_DEBUG_PRINT ( " Frames = %i, channels = %i, bits = %i, ma_format = %i, pointer = %p " , audioEngine . soundHandles [ handle ] - > maAudioBuffer - > ref . sizeInFrames ,
audioEngine . soundHandles [ handle ] - > maAudioBuffer - > ref . channels , bits , audioEngine . soundHandles [ handle ] - > maAudioBuffer - > ref . format ,
audioEngine . soundHandles [ handle ] - > maAudioBuffer - > ref . pData ) ;
2022-12-30 20:19:23 +00:00
return handle ;
}
2022-08-26 15:15:16 +00:00
/// <summary>
/// This function returns a _MEM value referring to a sound's raw data in memory using a designated sound handle created by the _SNDOPEN function.
/// miniaudio supports a variety of sample and channel formats. Translating all of that to basic 2 channel 16-bit format that
/// MemSound was originally supporting would require significant overhead both in terms of system resources and code.
/// For now we are just exposing the underlying PCM data directly from miniaudio. This fits rather well using the existing mem structure.
/// Mono sounds should continue to work just as it was before. Stereo and multi-channel sounds however will be required to be handled correctly
/// by the user by checking the 'elementsize' (for frame size in bytes) and 'type' (for data type) members.
/// </summary>
/// <param name="handle">A sound handle</param>
/// <param name="targetChannel">This should be 0 (for interleaved) or 1 (for mono). Anything else will result in failure</param>
/// <returns>A _MEM value that can be used to access the sound data</returns>
mem_block func__memsound ( int32_t handle , int32_t targetChannel ) {
2023-01-07 18:44:59 +00:00
ma_format maFormat = ma_format : : ma_format_unknown ;
ma_uint32 channels = 0 ;
ma_uint64 sampleFrames = 0 ;
ptrszint data = NULL ;
2022-12-30 20:19:23 +00:00
// Setup mem_block (assuming failure)
mem_block mb = { } ;
mb . lock_offset = ( ptrszint ) mem_lock_base ;
mb . lock_id = INVALID_MEM_LOCK ;
2022-08-26 15:15:16 +00:00
2022-12-30 20:19:23 +00:00
// Return invalid mem_block if audio is not initialized, handle is invalid or sound type is not static
2023-04-20 18:32:40 +00:00
if ( ! audioEngine . isInitialized | | ! IS_SOUND_HANDLE_VALID ( handle ) | | audioEngine . soundHandles [ handle ] - > type ! = SoundHandle : : Type : : STATIC | |
2022-12-30 20:19:23 +00:00
( targetChannel ! = 0 & & targetChannel ! = 1 ) ) {
AUDIO_DEBUG_PRINT ( " Invalid handle (%i), sound type (%i) or channel (%i) " , handle , audioEngine . soundHandles [ handle ] - > type , targetChannel ) ;
return mb ;
2022-08-26 15:15:16 +00:00
}
2022-12-30 20:19:23 +00:00
// Check what kind of sound we are dealing with and take appropriate path
if ( audioEngine . soundHandles [ handle ] - > maAudioBuffer ) { // we are dealing with a user created audio buffer
AUDIO_DEBUG_PRINT ( " Entering ma_audio_buffer path " ) ;
2023-01-07 18:44:59 +00:00
maFormat = audioEngine . soundHandles [ handle ] - > maAudioBuffer - > ref . format ;
channels = audioEngine . soundHandles [ handle ] - > maAudioBuffer - > ref . channels ;
sampleFrames = audioEngine . soundHandles [ handle ] - > maAudioBuffer - > ref . sizeInFrames ;
data = ( ptrszint ) audioEngine . soundHandles [ handle ] - > maAudioBuffer - > ref . pData ;
2022-12-30 20:19:23 +00:00
} else { // we are dealing with a sound loaded from file or memory
AUDIO_DEBUG_PRINT ( " Entering ma_resource_manager_data_buffer path " ) ;
// The sound cannot be steaming and must be completely decoded in memory
if ( audioEngine . soundHandles [ handle ] - > maFlags & MA_SOUND_FLAG_STREAM | | ! ( audioEngine . soundHandles [ handle ] - > maFlags & MA_SOUND_FLAG_DECODE ) ) {
AUDIO_DEBUG_PRINT ( " Sound data is not completely decoded " ) ;
return mb ;
}
2022-08-26 15:15:16 +00:00
2022-12-30 20:19:23 +00:00
// Get the pointer to the data source
auto ds = ( ma_resource_manager_data_buffer * ) ma_sound_get_data_source ( & audioEngine . soundHandles [ handle ] - > maSound ) ;
if ( ! ds | | ! ds - > pNode ) {
AUDIO_DEBUG_PRINT ( " Data source pointer OR data source node pointer is NULL " ) ;
return mb ;
}
2022-08-26 15:15:16 +00:00
2022-12-30 20:19:23 +00:00
// Check if the data is one contigious buffer or a link list of decoded pages
// We cannot have a mem object for a link list of decoded pages for obvious reasons
if ( ds - > pNode - > data . type ! = ma_resource_manager_data_supply_type : : ma_resource_manager_data_supply_type_decoded ) {
AUDIO_DEBUG_PRINT ( " Data is not a contigious buffer. Type = %u " , ds - > pNode - > data . type ) ;
return mb ;
}
2022-08-26 15:15:16 +00:00
2022-12-30 20:19:23 +00:00
// Check the data pointer
if ( ! ds - > pNode - > data . backend . decoded . pData ) {
AUDIO_DEBUG_PRINT ( " Data source data pointer is NULL " ) ;
return mb ;
}
2022-08-26 15:15:16 +00:00
2022-12-30 20:19:23 +00:00
// Query the data format
if ( ma_sound_get_data_format ( & audioEngine . soundHandles [ handle ] - > maSound , & maFormat , & channels , NULL , NULL , 0 ) ! = MA_SUCCESS ) {
AUDIO_DEBUG_PRINT ( " Data format query failed " ) ;
return mb ;
}
2022-08-26 15:15:16 +00:00
2022-12-30 20:19:23 +00:00
// Get the length in sample frames
if ( ma_sound_get_length_in_pcm_frames ( & audioEngine . soundHandles [ handle ] - > maSound , & sampleFrames ) ! = MA_SUCCESS ) {
AUDIO_DEBUG_PRINT ( " PCM frames query failed " ) ;
return mb ;
}
data = ( ptrszint ) ds - > pNode - > data . backend . decoded . pData ;
2022-08-26 15:15:16 +00:00
}
2023-01-07 18:44:59 +00:00
AUDIO_DEBUG_PRINT ( " Format = %u, channels = %u, frames = %llu " , maFormat , channels , sampleFrames ) ;
2022-08-26 15:15:16 +00:00
2022-12-30 20:19:23 +00:00
// Setup type: This was not done in the old code
// But we are doing it here. By examing the type the user can now figure out if they have to use FP32 or integers
switch ( maFormat ) {
case ma_format : : ma_format_f32 :
mb . type = 4 + 256 ; // FP32
break ;
case ma_format : : ma_format_s32 :
mb . type = 4 + 128 ; // signed int32
break ;
case ma_format : : ma_format_s16 :
mb . type = 2 + 128 ; // signed int16
break ;
case ma_format : : ma_format_u8 :
mb . type = 1 + 128 + 1024 ; // unsigned int8
break ;
default :
AUDIO_DEBUG_PRINT ( " Unsupported audio format " ) ;
return mb ;
}
2022-08-26 15:15:16 +00:00
if ( audioEngine . soundHandles [ handle ] - > memLockOffset ) {
2022-12-30 20:19:23 +00:00
AUDIO_DEBUG_PRINT ( " Returning previously created mem_lock " ) ;
2022-08-26 15:15:16 +00:00
mb . lock_offset = ( ptrszint ) audioEngine . soundHandles [ handle ] - > memLockOffset ;
mb . lock_id = audioEngine . soundHandles [ handle ] - > memLockId ;
} else {
2022-12-30 20:19:23 +00:00
AUDIO_DEBUG_PRINT ( " Returning new mem_lock " ) ;
2022-08-26 15:15:16 +00:00
new_mem_lock ( ) ;
mem_lock_tmp - > type = MEM_TYPE_SOUND ;
mb . lock_offset = ( ptrszint ) mem_lock_tmp ;
mb . lock_id = mem_lock_id ;
audioEngine . soundHandles [ handle ] - > memLockOffset = ( void * ) mem_lock_tmp ;
audioEngine . soundHandles [ handle ] - > memLockId = mem_lock_id ;
}
mb . elementsize = ma_get_bytes_per_frame ( maFormat , channels ) ; // Set the element size. This is the size of each PCM frame in bytes
2022-12-30 20:19:23 +00:00
mb . offset = data ; // Setup offset
2022-08-26 15:15:16 +00:00
mb . size = sampleFrames * mb . elementsize ; // Setup size (in bytes)
mb . sound = handle ; // Copy the handle
mb . image = 0 ; // Not needed. Set to 0
2023-01-07 18:44:59 +00:00
AUDIO_DEBUG_PRINT ( " ElementSize = %lli, size = %lli, type = %lli, pointer = %p " , mb . elementsize , mb . size , mb . type , mb . offset ) ;
2022-08-26 15:15:16 +00:00
return mb ;
}
2023-04-10 20:19:14 +00:00
/// @brief This initializes the audio subsystem. We simply attempt to initialize and then set some globals with the results
2022-08-26 15:15:16 +00:00
void snd_init ( ) {
// Exit if engine is initialize or already initialization was attempted but failed
if ( audioEngine . isInitialized | | audioEngine . initializationFailed )
return ;
// Initialize the miniaudio resource manager
audioEngine . maResourceManagerConfig = ma_resource_manager_config_init ( ) ;
AudioEngineAttachCustomBackendVTables ( & audioEngine . maResourceManagerConfig ) ;
audioEngine . maResourceManagerConfig . pCustomDecodingBackendUserData = NULL ; // <- pUserData parameter of each function in the decoding backend vtables
audioEngine . maResult = ma_resource_manager_init ( & audioEngine . maResourceManagerConfig , & audioEngine . maResourceManager ) ;
if ( audioEngine . maResult ! = MA_SUCCESS ) {
audioEngine . initializationFailed = true ;
AUDIO_DEBUG_PRINT ( " Failed to initialize miniaudio resource manager " ) ;
return ;
}
// Once we have a resource manager we can create the engine
audioEngine . maEngineConfig = ma_engine_config_init ( ) ;
audioEngine . maEngineConfig . pResourceManager = & audioEngine . maResourceManager ;
// Attempt to initialize with miniaudio defaults
audioEngine . maResult = ma_engine_init ( & audioEngine . maEngineConfig , & audioEngine . maEngine ) ;
// If failed, then set the global flag so that we don't attempt to initialize again
if ( audioEngine . maResult ! = MA_SUCCESS ) {
ma_resource_manager_uninit ( & audioEngine . maResourceManager ) ;
audioEngine . initializationFailed = true ;
AUDIO_DEBUG_PRINT ( " miniaudio initialization failed " ) ;
return ;
}
// Get and save the engine sample rate. We will let miniaudio choose the device sample rate for us
// This ensures we get the lowest latency
// Set the resource manager decorder sample rate to the device sample rate (miniaudio engine bug?)
audioEngine . maResourceManager . config . decodedSampleRate = audioEngine . sampleRate = ma_engine_get_sample_rate ( & audioEngine . maEngine ) ;
// Set the initialized flag as true
audioEngine . isInitialized = true ;
AUDIO_DEBUG_PRINT ( " Audio engine initialized at %uHz sample rate " , audioEngine . sampleRate ) ;
// Reserve sound handle 0 so that nothing else can use it
// We will use this handle internally for Play(), Beep(), Sound() etc.
audioEngine . sndInternal = audioEngine . AllocateSoundHandle ( ) ;
AUDIO_DEBUG_CHECK ( audioEngine . sndInternal = = 0 ) ; // The first handle must return 0 and this is what is used by Beep and Sound
}
2023-04-10 20:19:14 +00:00
/// @brief This shuts down the audio engine and frees any resources used
2022-08-26 15:15:16 +00:00
void snd_un_init ( ) {
if ( audioEngine . isInitialized ) {
2023-04-10 20:19:14 +00:00
// Free any MMLPlayer object if they were created
if ( audioEngine . mmlPlayer ) {
delete audioEngine . mmlPlayer ;
audioEngine . mmlPlayer = nullptr ;
}
// Free any Waveform object if they were created and
if ( audioEngine . waveform ) {
delete audioEngine . waveform ;
audioEngine . waveform = nullptr ;
}
2022-08-26 15:15:16 +00:00
// Free all sound handles here
for ( size_t handle = 0 ; handle < audioEngine . soundHandles . size ( ) ; handle + + ) {
2023-04-10 20:19:14 +00:00
audioEngine . FreeSoundHandle ( handle ) ; // let FreeSoundHandle do it's thing
delete audioEngine . soundHandles [ handle ] ; // now free the object created by AllocateSoundHandle()
2022-08-26 15:15:16 +00:00
}
// Now that all sounds are closed and SoundHandle objects are freed, clear the vector
audioEngine . soundHandles . clear ( ) ;
// Invalidate internal handles
audioEngine . sndInternal = audioEngine . sndInternalRaw = INVALID_SOUND_HANDLE ;
// Shutdown miniaudio
ma_engine_uninit ( & audioEngine . maEngine ) ;
// Shutdown the miniaudio resource manager
ma_resource_manager_uninit ( & audioEngine . maResourceManager ) ;
// Set engine initialized flag as false
audioEngine . isInitialized = false ;
AUDIO_DEBUG_PRINT ( " Audio engine shutdown " ) ;
}
}
2023-04-10 20:19:14 +00:00
/// @brief This is called by the QB64-PE internally at ~60Hz. We use this for housekeeping and other stuff.
2022-08-26 15:15:16 +00:00
void snd_mainloop ( ) {
if ( audioEngine . isInitialized ) {
// Scan through the whole handle vector to find anything we need to update or close
for ( size_t handle = 0 ; handle < audioEngine . soundHandles . size ( ) ; handle + + ) {
// Only process handles that are in use
if ( audioEngine . soundHandles [ handle ] - > isUsed ) {
// Look for stuff that is set to auto-destruct
if ( audioEngine . soundHandles [ handle ] - > autoKill ) {
switch ( audioEngine . soundHandles [ handle ] - > type ) {
2023-04-20 18:32:40 +00:00
case SoundHandle : : Type : : STATIC :
case SoundHandle : : Type : : RAW :
2022-08-26 15:15:16 +00:00
// Dispose the sound if it has finished playing
// Note that this means that temporary looping sounds will never close
// Well thats on the programmer. Probably they want it that way
if ( ! ma_sound_is_playing ( & audioEngine . soundHandles [ handle ] - > maSound ) )
audioEngine . FreeSoundHandle ( handle ) ;
break ;
2023-04-20 18:32:40 +00:00
case SoundHandle : : Type : : NONE :
2022-08-26 15:15:16 +00:00
if ( handle ! = 0 )
AUDIO_DEBUG_PRINT ( " Sound type is 'None' when handle value is not 0 " ) ;
break ;
default :
AUDIO_DEBUG_PRINT ( " Condition not handled " ) ; // It should not come here
}
}
}
}
}
}