Skip to content

Commit

Permalink
refactor: add user-facing documentation and internal cleanup
Browse files Browse the repository at this point in the history
  • Loading branch information
lmichaelis committed May 1, 2024
1 parent be98fd0 commit 19120d1
Show file tree
Hide file tree
Showing 11 changed files with 387 additions and 98 deletions.
172 changes: 155 additions & 17 deletions include/dmusic.h
Original file line number Diff line number Diff line change
Expand Up @@ -93,7 +93,7 @@ typedef void DmMemoryFree(void* ctx, void* ptr);
/// \param free[in] A `free`-like function, which free memory previously allocated using \p alloc. May not be NULL.
/// \param ctx[in] An arbitrary pointer passed to \p alloc and \p free on every invocation.
///
/// \returns #DmResult_SUCCESS if the operation completed and an error code if it did not.
/// \return #DmResult_SUCCESS if the operation completed and an error code if it did not.
/// \retval #DmResult_INVALID_ARGUMENT Either \p alloc or \p free was `NULL`.
/// \retval #DmResult_INVALID_STATE The function was called after an allocation was already made.
///
Expand Down Expand Up @@ -155,23 +155,54 @@ DMAPI void Dm_setLoggerLevel(DmLogLevel lvl);

typedef struct DmSegment DmSegment;
typedef struct DmLoader DmLoader;
typedef struct DmPerformance DmPerformance;

/// \defgroup DmSegmentGroup Segment
/// \addtogroup DmSegmentGroup
/// \{

/// \brief Add one to the reference count of a segment.
/// \param slf[in] The segment to retain.
/// \return The same segment as was given in \p slf or `NULL` if \p slf was `NULL`.
DMAPI DmSegment* DmSegment_retain(DmSegment* slf);

/// \brief Subtract one from the reference count of a segment.
///
/// If a call to this function reduces the reference count to zero, it also de-allocates the segment and
/// releases any resources referenced by it.
///
/// \param slf[in] The segment to release.
DMAPI void DmSegment_release(DmSegment* slf);

/// \brief Download all resources needed by the segment.
///
/// In order to play a segment, its internal resources, like references to styles and bands need to be resolved and
/// downloaded. This is done by either calling #DmSegment_download manually or by providing the #DmLoader_DOWNLOAD flag
/// when creating the loader.
///
/// \param slf[in] The segment to download resources for.
/// \param loader[in] The loader to use for downloading resources
///
/// \return #DmResult_SUCCESS if the operation completed and an error code if it did not.
/// \retval #DmResult_INVALID_ARGUMENT Either \p slf or \p loader was `NULL`.
/// \retval #DmResult_NOT_FOUND An internal resource required by the segment was not found.
/// \retval #DmResult_MUTEX_ERROR An error occurred while trying to lock an internal mutex.
/// \retval #DmResult_MEMORY_EXHAUSTED A dynamic memory allocation failed.
DMAPI DmResult DmSegment_download(DmSegment* slf, DmLoader* loader);

/// \}

/// \defgroup DmLoaderGroup Loader
/// \addtogroup DmLoaderGroup
/// \{

/// \brief Configuration play_mode_flags for DirectMusic Loaders.
/// \brief Configuration flags for DirectMusic Loaders.
/// \see DmLoader_create
typedef enum DmLoaderOptions {
/// \brief Automatically load references.
/// \brief Automatically download references.
DmLoader_DOWNLOAD = 1U << 0U,

/// \brief Default play_mode_flags for loader objects.
/// \brief Default options for loader objects.
DmLoader_DEFAULT = 0U,
} DmLoaderOptions;

Expand All @@ -188,26 +219,29 @@ typedef enum DmLoaderOptions {
/// \param file[in] The name of the file to look up.
/// \param len[out] The length of the returned memory buffer in bytes.
///
/// \returns A memory buffer containing the file data or `NULL` if the lookup failed. **Ownership of this
/// buffer is transferred to the loader.**
/// \return A memory buffer containing the file data or `NULL` if the lookup failed. **Ownership of this
/// buffer is transferred to the loader.**
typedef void* DmLoaderResolverCallback(void* ctx, char const* file, size_t* len);

/// \brief Create a new DirectMusic Loader object.
///
/// \param slf[out] A pointer to a variable in which to store the newly created segment.
/// \param opt A bitfield containing loader configuration play_mode_flags.
/// If the #DmLoader_DOWNLOAD option is defined, all references for objects retrieved for the loader
/// are automatically resolved and downloaded.
///
/// \returns #DmResult_SUCCESS if the operation completed and an error code if it did not.
/// \param slf[out] A pointer to a variable in which to store the newly created loader.
/// \param opt A bitfield containing loader configuration flags.
///
/// \return #DmResult_SUCCESS if the operation completed and an error code if it did not.
/// \retval #DmResult_INVALID_ARGUMENT \p slf was `NULL`.
/// \retval #DmResult_MEMORY_EXHAUSTED A dynamic memory allocation failed.
DMAPI DmResult DmLoader_create(DmLoader** slf, DmLoaderOptions opt);

/// \brief Add one to the reference count of the loader.
/// \brief Add one to the reference count of a loader.
/// \param slf[in] The loader to retain.
/// \return The same loader as was given in \p slf or `NULL` if \p slf was `NULL`.
DMAPI DmLoader* DmLoader_retain(DmLoader* slf);

/// \brief Subtract one from the reference count of the loader.
/// \brief Subtract one from the reference count of a loader.
///
/// If a call to this function reduces the reference count to zero, it also de-allocates the loader and
/// releases any resources referenced by it.
Expand All @@ -225,9 +259,10 @@ DMAPI void DmLoader_release(DmLoader* slf);
/// \param resolve[in] The callback function used to resolve a file using the new resolver.
/// \param ctx[in] An arbitrary pointer passed to \p resolve on every invocation.
///
/// \returns #DmResult_SUCCESS if the operation completed and an error code if it did not.
/// \return #DmResult_SUCCESS if the operation completed and an error code if it did not.
/// \retval #DmResult_INVALID_ARGUMENT \p slf or \p resolve was `NULL`.
/// \retval #DmResult_MEMORY_EXHAUSTED A dynamic memory allocation failed.
/// \retval #DmResult_MUTEX_ERROR An error occurred while trying to lock an internal mutex.
DMAPI DmResult DmLoader_addResolver(DmLoader* slf, DmLoaderResolverCallback* resolve, void* ctx);

/// \brief Get a segment from the loader's cache or load it by file \p name.
Expand All @@ -236,21 +271,27 @@ DMAPI DmResult DmLoader_addResolver(DmLoader* slf, DmLoaderResolverCallback* res
/// using the resolvers added to the loader. If the requested segment is found in neither the loader,
/// nor by any resolver, an error is issued.
///
/// If the loader was created using the #DmLoader_DOWNLOAD option, this function automatically downloads
/// the segment by calling #DmSegment_download.
///
/// \param slf[in] The loader to load a segment from.
/// \param name[in] The file name of the segment to load.
/// \param segment[out] A pointer to a variable in which to store the segment.
///
/// \returns #DmResult_SUCCESS if the operation completed and an error code if it did not.
/// \return #DmResult_SUCCESS if the operation completed and an error code if it did not.
/// \retval #DmResult_INVALID_ARGUMENT \p slf, \p name or \p segment was `NULL`.
/// \retval #DmResult_MEMORY_EXHAUSTED A dynamic memory allocation failed.
/// \retval #DmResult_NOT_FOUND No segment with the given name could be found.
/// \retval #DmResult_MUTEX_ERROR An error occurred while trying to lock an internal mutex.
///
/// \see #DmLoader_addResolver
DMAPI DmResult DmLoader_getSegment(DmLoader* slf, char const* name, DmSegment** segment);

/// \}

typedef struct DmPerformance DmPerformance;
/// \defgroup DmPerformanceGroup Performance
/// \addtogroup DmPerformanceGroup
/// \{

typedef enum DmRenderOptions {
DmRender_SHORT = 1 << 0,
Expand All @@ -275,11 +316,108 @@ typedef enum DmEmbellishmentType {
DmEmbellishment_END_AND_INTRO = 6,
} DmEmbellishmentType;

DMAPI DmResult DmPerformance_create(DmPerformance** slf);
/// \brief Create a new DirectMusic Performance object.
///
/// \param slf[out] A pointer to a variable in which to store the newly created performance.
/// \param rate The sample rate for the synthesizer. Provide 0 to use the default (44100 Hz).
///
/// \returns #DmResult_SUCCESS if the operation completed and an error code if it did not.
/// \retval #DmResult_INVALID_ARGUMENT \p slf was `NULL`.
/// \retval #DmResult_MEMORY_EXHAUSTED A dynamic memory allocation failed.
DMAPI DmResult DmPerformance_create(DmPerformance** slf, uint32_t rate);

/// \brief Add one to the reference count of a performance.
/// \param slf[in] The performance to retain.
/// \return The same performance as was given in \p slf or `NULL` if \p slf was `NULL`.
DMAPI DmPerformance* DmPerformance_retain(DmPerformance* slf);

/// \brief Subtract one from the reference count of a performance.
///
/// If a call to this function reduces the reference count to zero, it also de-allocates the performance and
/// releases any resources referenced by it.
///
/// \param slf[in] The performance to release.
DMAPI void DmPerformance_release(DmPerformance* slf);

/// \brief Schedule a new segment to be played by the given performance.
///
/// The segment is played at the next timing boundary provided with \p timing. This function simply stops the currently
/// playing segment and starts playing the next one. To play a transition between the two segment, use
/// #DmPerformance_playTransition.
///
/// \note The segment will always start playing strictly after the last call to #DmPerformance_renderPcm since that
/// function advances the internal clock. This means, if you have already rendered ten seconds worth of PCM
/// using #DmPerformance_renderPcm, the transition can only audibly be heard after these ten seconds of PCM
/// have been played.
///
/// \param slf[in] The performance to play the segment in.
/// \param sgt[in] The segment to play.
/// \param timing The timing bounding to start playing the segment at.
///
/// \return #DmResult_SUCCESS if the operation completed and an error code if it did not.
/// \retval #DmResult_INVALID_ARGUMENT \p slf or \p sgt was `NULL`.
/// \retval #DmResult_MEMORY_EXHAUSTED A dynamic memory allocation failed.
/// \retval #DmResult_MUTEX_ERROR An error occurred while trying to lock an internal mutex.
///
/// \see DmPerformance_playTransition
DMAPI DmResult DmPerformance_playSegment(DmPerformance* slf, DmSegment* sgt, DmTiming timing);
DMAPI DmResult DmPerformance_playTransition(DmPerformance* slf, DmSegment* sgt, DmEmbellishmentType embellishment, DmTiming flags);
DMAPI DmResult DmPerformance_renderPcm(DmPerformance* slf, void* buf, size_t len, DmRenderOptions opts);

/// \brief Schedule a new segment to play by the given performance with a transition.
///
/// Schedules a new transitional segment to be played, which first plays a transitional pattern from the currently
/// playing segment's style and then starts playing the given segment. This can be used to smoothly transition from
/// one segment to another.
///
/// The transitional pattern is selected by its \p embellishment type provided when calling the function. Only
/// embellishments matching the current groove level are considered.
///
/// \note The #DmEmbellishment_END_AND_INTRO embellishment is currently not implemented.
///
/// \param slf[in] The performance to play the transition in.
/// \param sgt[in] The segment to transition to.
/// \param embellishment The embellishment type to use for the transition.
/// \param timing The timing bounding to start playing the transition at.
///
/// \return #DmResult_SUCCESS if the operation completed and an error code if it did not.
/// \retval #DmResult_INVALID_ARGUMENT \p slf or \p sgt was `NULL`.
/// \retval #DmResult_MEMORY_EXHAUSTED A dynamic memory allocation failed.
/// \retval #DmResult_MUTEX_ERROR An error occurred while trying to lock an internal mutex.
///
/// \see DmPerformance_playSegment
DMAPI DmResult DmPerformance_playTransition(DmPerformance* slf,
DmSegment* sgt,
DmEmbellishmentType embellishment,
DmTiming timing);

/// \brief Render a given number of PCM samples from a performance.
///
/// Since the performance is played "on demand", calling this function will advance the internal clock and perform all
/// musical operation for the rendered timeframe. If no segment is currently playing, the output will be set to zero
/// samples.
///
/// Using the \p opts parameter, you can control what data is output. The #DmRender_SHORT and #DmRender_FLOAT bits
/// indicate the format of the PCM data to output (either as int16_t or 32-bit float). All data is output as
/// host-endian. Setting the #DmRender_STEREO bit renders interleaved stereo samples.
///
/// \warning When rendering stereo audio, you must provide an output array with an even number of elements!
///
/// \param slf[in] The performance to render from.
/// \param buf[out] A buffer to render PCM into.
/// \param num The number of elements available in \p buf. This will be equal to the number of samples when rendering
/// mono PCM or 2 time the number of samples when rendering stereo.
/// \param opts A bitfield with options for the renderer.
///
/// \return #DmResult_SUCCESS if the operation completed and an error code if it did not.
/// \retval #DmResult_INVALID_ARGUMENT \p slf or \p buf was `NULL`, \p opts included multiple format specifiers
/// or \p opts included #DmRender_STEREO and \p num was not even.
/// \retval #DmResult_MEMORY_EXHAUSTED A dynamic memory allocation failed.
/// \retval #DmResult_MUTEX_ERROR An error occurred while trying to lock an internal mutex.
DMAPI DmResult DmPerformance_renderPcm(DmPerformance* slf, void* buf, size_t num, DmRenderOptions opts);

/// \brief Set the playback volume of a performance
/// \note This only affects the output created when calling #DmPerformance_renderPcm.
/// \param slf[in] The performance to set the volume of.
/// \param vol The new volume to set (between 0 and 1).
DMAPI void DmPerformance_setVolume(DmPerformance* slf, float vol);

/// \}
21 changes: 13 additions & 8 deletions src/Band.c
Original file line number Diff line number Diff line change
Expand Up @@ -35,7 +35,7 @@ void DmBand_release(DmBand* slf) {
return;
}

for (size_t i = 0; i < slf->instrument_count; ++i) {
for (size_t i = 0; i < slf->instruments_len; ++i) {
DmInstrument_free(&slf->instruments[i]);
}

Expand Down Expand Up @@ -82,7 +82,7 @@ DmResult DmBand_download(DmBand* slf, DmLoader* loader) {
Dm_report(DmLogLevel_INFO, "DmBand: Downloading instruments for band '%s'", slf->info.unam);

DmResult rv = DmResult_SUCCESS;
for (size_t i = 0; i < slf->instrument_count; ++i) {
for (size_t i = 0; i < slf->instruments_len; ++i) {
DmInstrument* instrument = &slf->instruments[i];

// The DLS has already been downloaded. We don't need to do it again.
Expand All @@ -92,29 +92,34 @@ DmResult DmBand_download(DmBand* slf, DmLoader* loader) {

// If the patch is not valid, this instrument cannot be played since we don't know
// where to find it in the DLS collection.
if (!(instrument->flags & DmInstrument_PATCH)) {
if (!(instrument->options & (DmInstrument_VALID_PATCH | DmInstrument_VALID_BANKSELECT))) {
Dm_report(DmLogLevel_DEBUG,
"DmBand: Not downloading instrument '%s' without valid patch",
instrument->reference.name);
continue;
}

// TODO(lmichaelis): The General MIDI and Roland GS collections are not supported.
if (instrument->flags & (DmInstrument_GS | DmInstrument_GM)) {
// TODO(lmichaelis): The General MIDI, Roland GS and Yamaha XG collections are not supported.
if (instrument->options & DmInstrument_PREDEFINED_COLLECTION) {
Dm_report(DmLogLevel_INFO,
"DmBand: Cannot download instrument '%s': GS and GM collections not available",
"DmBand: Cannot download instrument '%s': GM, GS and XG collections not available",
instrument->reference.name);
continue;
}

rv = DmLoader_getDownloadableSound(loader, &instrument->reference, &instrument->dls);
if (rv != DmResult_SUCCESS || instrument->dls == NULL) {
break;
continue;
}

DmDlsInstrument* dls_instrument = DmInstrument_getDlsInstrument(instrument);
if (dls_instrument == NULL) {
continue;
}

Dm_report(DmLogLevel_DEBUG,
"DmBand: DLS instrument '%s' assigned to channel %d for band '%s'",
instrument->dls->info.inam,
dls_instrument->info.inam,
instrument->channel,
slf->info.unam);
}
Expand Down
16 changes: 16 additions & 0 deletions src/Common.c
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,22 @@ int32_t max_s32(int32_t a, int32_t b) {
return a > b ? a : b;
}

uint8_t min_u8(uint8_t a, uint8_t b) {
return a < b ? a : b;
}

int32_t clamp_s32(int32_t val, int32_t min, int32_t max) {
if (val < min) {
return min;
}

if (val > max) {
return max;
}

return val;
}

float clamp_f32(float val, float min, float max) {
if (val < min) {
return min;
Expand Down
10 changes: 5 additions & 5 deletions src/Dls.c
Original file line number Diff line number Diff line change
Expand Up @@ -126,7 +126,7 @@ static size_t DmDlsWave_decodeShort(DmDlsWave const* slf, float* out, size_t len
int16_t const* raw = (int16_t const*) slf->pcm;
size_t i = 0;
for (i = 0; i < size && i < len; ++i) {
out[i] = (float) raw[i] / 32767.f;
out[i] = (float) raw[i] / INT16_MAX;
}

return i;
Expand Down Expand Up @@ -173,8 +173,8 @@ static uint8_t const* DmDls_decodeAdpcmBlock(uint8_t const* adpcm, float* pcm, u
int16_t sample_b;
DmInt_read(adpcm, &sample_b);

*pcm++ = (float) sample_b / 32767.f;
*pcm++ = (float) sample_a / 32767.f;
*pcm++ = (float) sample_b / (float) INT16_MAX;
*pcm++ = (float) sample_a / (float) INT16_MAX;

int coeff_1 = ADPCM_ADAPT_COEFF1[block_predictor];
int coeff_2 = ADPCM_ADAPT_COEFF2[block_predictor];
Expand All @@ -189,7 +189,7 @@ static uint8_t const* DmDls_decodeAdpcmBlock(uint8_t const* adpcm, float* pcm, u
int predictor = (coeff_1 * sample_a + coeff_2 * sample_b) / 256;
predictor += nibble * delta;
predictor = clamp_16bit(predictor);
*pcm++ = (float) ((int16_t) predictor) / INT16_MAX;
*pcm++ = (float) predictor / (float)INT16_MAX;
sample_b = sample_a;
sample_a = (int16_t) (predictor);
delta = max_s32((ADPCM_ADAPT_TABLE[(b & 0xF0) >> 4] * delta) / 256, 16);
Expand All @@ -199,7 +199,7 @@ static uint8_t const* DmDls_decodeAdpcmBlock(uint8_t const* adpcm, float* pcm, u
predictor = (coeff_1 * sample_a + coeff_2 * sample_b) / 256;
predictor += nibble * delta;
predictor = clamp_16bit(predictor);
*pcm++ = (float) ((int16_t) predictor) / INT16_MAX;
*pcm++ = (float) predictor / (float)INT16_MAX;
sample_b = sample_a;
sample_a = (int16_t) (predictor);
delta = max_s32((ADPCM_ADAPT_TABLE[b & 0x0F] * delta) / 256, 16);
Expand Down
Loading

0 comments on commit 19120d1

Please sign in to comment.