Last active
April 26, 2023 08:22
-
-
Save hydren/ea794e65e95c7713c00c88f74b71f8b1 to your computer and use it in GitHub Desktop.
SDL_mixer chunk pitching C example
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
/* | |
* custom_mix_pitch_func.h | |
* | |
* Created on: 10 de jan de 2018 | |
* Author: carlosfaruolo | |
*/ | |
// Mix_EffectFunc_t callback that redirects to handler method (handler passed via user_data) | |
// Processing function to be able to change chunk speed/pitch. | |
// AUDIO_FORMAT_TYPE depends on the current audio format (queryable via Mix_QuerySpec) | |
void Custom_Mix_PlaybackSpeedEffectFuncCallback(int mix_channel, void* stream, int length, void* user_data) | |
{ | |
Custom_Mix_PlaybackSpeedEffectHandler* handler = (Custom_Mix_PlaybackSpeedEffectHandler*) user_data; | |
const AUDIO_FORMAT_TYPE* chunk_data = (AUDIO_FORMAT_TYPE*) handler->chunk->abuf; | |
AUDIO_FORMAT_TYPE* buffer = (AUDIO_FORMAT_TYPE*) stream; | |
const int buffer_size = length / sizeof(AUDIO_FORMAT_TYPE); // buffer size (as array) | |
const float speed_factor = *(handler->speed); // take a "snapshot" of speed factor | |
// if there is still sound to be played | |
if(handler->position < handler->duration || handler->loop) | |
{ | |
const float delta = 1000.0 / audio_frequency, // normal duration of each sample | |
vdelta = delta * speed_factor; // virtual stretched duration, scaled by 'speedFactor' | |
// if playback is unaltered and pitch is required (for the first time) | |
if(!handler->altered && speed_factor != 1.0f) | |
handler->altered = 1; // flags playback modification and proceed to the pitch routine | |
if(handler->altered) // if unaltered, this pitch routine is skipped | |
{ | |
for(int i = 0; i < buffer_size; i += audio_channel_count) | |
{ | |
const int j = i / audio_channel_count; // j goes from 0 to size/channelCount, incremented 1 by 1 | |
const float x = handler->position + j * vdelta; // get "virtual" index. its corresponding value will be interpolated. | |
const int k = floor(x / delta); // get left index to interpolate from original chunk data (right index will be this plus 1) | |
const float prop = (x / delta) - k; // get the proportion of the right value (left will be 1.0 minus this) | |
// const float prop2 = prop * prop; // cache the square of the proportion (needed only for cubic interpolation) | |
// usually just 2 channels: 0 (left) and 1 (right), but who knows... | |
for(int c = 0; c < audio_channel_count; c++) | |
{ | |
// check if k will be within bounds | |
if(k * audio_channel_count + audio_channel_count - 1 < handler->chunk_size || handler->loop) | |
{ | |
AUDIO_FORMAT_TYPE v0 = chunk_data[( k * audio_channel_count + c) % handler->chunk_size], | |
// v_ = chunk_data[((k-1) * audio_channel_count + c) % handler->chunk_size], | |
// v2 = chunk_data[((k+2) * audio_channel_count + c) % handler->chunk_size], | |
v1 = chunk_data[((k+1) * audio_channel_count + c) % handler->chunk_size]; | |
// put interpolated value on 'data' | |
// buffer[i + c] = (1 - prop) * v0 + prop * v1; // linear interpolation | |
buffer[i + c] = v0 + prop * (v1 - v0); // linear interpolation (single-multiplication version) | |
// buffer[i + c] = v0 + 0.5f * prop * ((prop - 3) * v0 - (prop - 2) * 2 * v1 + (prop - 1) * v2); // quadratic interpolation | |
// buffer[i + c] = v0 + (prop / 6) * ((3 * prop - prop2 - 2) * v_ + (prop2 - 2 * prop - 1) * 3 * v0 + (prop - prop2 + 2) * 3 * v1 + (prop2 - 1) * v2); // cubic interpolation | |
// buffer[i + c] = v0 + 0.5f * prop * ((2 * prop2 - 3 * prop - 1) * (v0 - v1) + (prop2 - 2 * prop + 1) * (v0 - v_) + (prop2 - prop) * (v2 - v2)); // cubic spline interpolation | |
} | |
else // if k will be out of bounds (chunk bounds), it means we already finished; thus, we'll pass silence | |
{ | |
buffer[i + c] = 0; | |
} | |
} | |
} | |
} | |
// update position | |
handler->position += (buffer_size / audio_channel_count) * vdelta; | |
// reset position if looping | |
if(handler->loop) while(handler->position > handler->duration) | |
handler->position -= handler->duration; | |
} | |
else // if we already played the whole sound but finished earlier than expected by SDL_mixer (due to faster playback speed) | |
{ | |
// set silence on the buffer since Mix_HaltChannel() poops out some of it for a few ms. | |
for(int i = 0; i < buffer_size; i++) | |
buffer[i] = 0; | |
if(handler->self_halt) | |
Mix_HaltChannel(mix_channel); // XXX unsafe call, since it locks audio; but no safer solution was found yet... | |
} | |
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
/* | |
* sound_pitching_example.c | |
* | |
* Created on: 10 de jan de 2018 | |
* Author: carlosfaruolo | |
*/ | |
#include <SDL2/SDL.h> | |
#include <SDL2/SDL_mixer.h> | |
#include <stdio.h> | |
#include <stdlib.h> | |
#include <math.h> | |
/* global vars */ | |
Uint16 audio_format; // current audio format constant | |
int audio_frequency, // frequency rate of the current audio format | |
audio_channel_count, // number of channels of the current audio format | |
audio_allocated_mix_channels_count; // number of mix channels allocated | |
static Uint16 format_sample_size(Uint16 format) { return (format & 0xFF) / 8; } | |
/* get chunk time length (in ms) given its size and current audio format */ | |
int Custom_Mix_ComputeChunkLengthMillisec(int chunkSize) | |
{ | |
const Uint32 points = chunkSize / format_sample_size(audio_format); // bytes / samplesize == sample points | |
const Uint32 frames = (points / audio_channel_count); // sample points / channels == sample frames | |
return ((frames * 1000) / audio_frequency); // (sample frames * 1000) / frequency == play length, in ms | |
} | |
/* custom handler object to control which part of the Mix_Chunk's audio data will be played, with which pitch-related modifications. */ | |
typedef struct Custom_Mix_PlaybackSpeedEffectHandler | |
{ | |
const Mix_Chunk* chunk; | |
const float* speed; /* ptr to the desired playback speed */ | |
float position; /* current position of the sound, in ms */ | |
int altered; /* false if this playback has never been pitched. */ | |
// read-only! | |
int loop; /* whether this is a looped playback */ | |
int duration; /* the duration of the sound, in ms */ | |
int chunk_size; /* the size of the sound, as a number of indexes (or sample points). thinks of this as a array size when using the proper array type (instead of just Uint8*). */ | |
int self_halt; /* flags whether playback should be halted by this callback when playback is finished */ | |
} Custom_Mix_PlaybackSpeedEffectHandler; | |
/* "Constructor" for Custom_Mix_PlaybackSpeedEffectHandler */ | |
Custom_Mix_PlaybackSpeedEffectHandler* Custom_Mix_CreatePlaybackSpeedEffectHandler(const Mix_Chunk* chunk, const float* speed, int loop, int self_halt) | |
{ | |
Custom_Mix_PlaybackSpeedEffectHandler* handler = malloc(sizeof(Custom_Mix_PlaybackSpeedEffectHandler)); | |
handler->chunk = chunk; | |
handler->speed = speed; | |
handler->position = 0; | |
handler->altered = 0; | |
handler->loop = loop; | |
handler->duration = Custom_Mix_ComputeChunkLengthMillisec(chunk->alen); | |
handler->chunk_size = chunk->alen / format_sample_size(audio_format); | |
handler->self_halt = self_halt; | |
return handler; | |
} | |
/* implementation of Uint8 version of the callback */ | |
#define Custom_Mix_PlaybackSpeedEffectFuncCallback Custom_Mix_PlaybackSpeedEffectFuncCallbackUint8 | |
#define AUDIO_FORMAT_TYPE Uint8 | |
#include "custom_mix_pitch_func.h" | |
#undef Custom_Mix_PlaybackSpeedEffectFuncCallback | |
#undef AUDIO_FORMAT_TYPE | |
/* implementation of Sint8 version of the callback */ | |
#define Custom_Mix_PlaybackSpeedEffectFuncCallback Custom_Mix_PlaybackSpeedEffectFuncCallbackSint8 | |
#define AUDIO_FORMAT_TYPE Sint8 | |
#include "custom_mix_pitch_func.h" | |
#undef Custom_Mix_PlaybackSpeedEffectFuncCallback | |
#undef AUDIO_FORMAT_TYPE | |
/* implementation of Uint16 version of the callback */ | |
#define Custom_Mix_PlaybackSpeedEffectFuncCallback Custom_Mix_PlaybackSpeedEffectFuncCallbackUint16 | |
#define AUDIO_FORMAT_TYPE Uint16 | |
#include "custom_mix_pitch_func.h" | |
#undef Custom_Mix_PlaybackSpeedEffectFuncCallback | |
#undef AUDIO_FORMAT_TYPE | |
/* implementation of Sint16 version of the callback */ | |
#define Custom_Mix_PlaybackSpeedEffectFuncCallback Custom_Mix_PlaybackSpeedEffectFuncCallbackSint16 | |
#define AUDIO_FORMAT_TYPE Sint16 | |
#include "custom_mix_pitch_func.h" | |
#undef Custom_Mix_PlaybackSpeedEffectFuncCallback | |
#undef AUDIO_FORMAT_TYPE | |
/* implementation of Sint32 version of the callback */ | |
#define Custom_Mix_PlaybackSpeedEffectFuncCallback Custom_Mix_PlaybackSpeedEffectFuncCallbackSint32 | |
#define AUDIO_FORMAT_TYPE Sint32 | |
#include "custom_mix_pitch_func.h" | |
#undef Custom_Mix_PlaybackSpeedEffectFuncCallback | |
#undef AUDIO_FORMAT_TYPE | |
/* implementation of Float version of the callback */ | |
#define Custom_Mix_PlaybackSpeedEffectFuncCallback Custom_Mix_PlaybackSpeedEffectFuncCallbackFloat | |
#define AUDIO_FORMAT_TYPE float | |
#include "custom_mix_pitch_func.h" | |
#undef Custom_Mix_PlaybackSpeedEffectFuncCallback | |
#undef AUDIO_FORMAT_TYPE | |
/* Mix_EffectDone_t callback that deletes the handler at the end of the effect usage (handler passed via userData) */ | |
void Custom_Mix_PlaybackSpeedEffectDoneCallback(int channel, void *userData) | |
{ | |
free(userData); | |
} | |
/* Register a proper playback speed effect handler for this channel according to the current audio format. Effect valid for the current (or next) playback only. */ | |
void Custom_Mix_RegisterPlaybackSpeedEffect(int channel, Mix_Chunk* chunk, float* speed, int loop, int selfHalt) | |
{ | |
Mix_EffectFunc_t effect_func_callback; | |
/* select the register function for the current audio format and register the effect using the compatible handlers | |
xxx is it correct to behave the same way to all S16 and U16 formats? Should we create case statements for AUDIO_S16SYS, AUDIO_S16LSB, AUDIO_S16MSB, etc, individually? */ | |
switch(audio_format) | |
{ | |
case AUDIO_U8: effect_func_callback = Custom_Mix_PlaybackSpeedEffectFuncCallbackUint8; break; | |
case AUDIO_S8: effect_func_callback = Custom_Mix_PlaybackSpeedEffectFuncCallbackSint8; break; | |
case AUDIO_U16: effect_func_callback = Custom_Mix_PlaybackSpeedEffectFuncCallbackUint16; break; | |
default: | |
case AUDIO_S16: effect_func_callback = Custom_Mix_PlaybackSpeedEffectFuncCallbackSint16; break; | |
case AUDIO_S32: effect_func_callback = Custom_Mix_PlaybackSpeedEffectFuncCallbackSint32; break; | |
case AUDIO_F32: effect_func_callback = Custom_Mix_PlaybackSpeedEffectFuncCallbackFloat; break; | |
} | |
Mix_RegisterEffect(channel, effect_func_callback, Custom_Mix_PlaybackSpeedEffectDoneCallback, Custom_Mix_CreatePlaybackSpeedEffectHandler(chunk, speed, loop, selfHalt)); | |
} | |
/* example | |
run the executable passing an filename of a sound file that SDL_mixer is able to open (ogg, wav, ...) */ | |
int main(int argc, char** argv) | |
{ | |
if(argc < 2) { puts("Missing argument."); return 0; } | |
SDL_Init(SDL_INIT_AUDIO); | |
Mix_OpenAudio(MIX_DEFAULT_FREQUENCY, MIX_DEFAULT_FORMAT, MIX_DEFAULT_CHANNELS, 4096); | |
Mix_QuerySpec(&audio_frequency, &audio_format, &audio_channel_count); /* query specs */ | |
audio_allocated_mix_channels_count = Mix_AllocateChannels(MIX_CHANNELS); | |
float speed = 1.0; | |
Mix_Chunk* chunk = Mix_LoadWAV(argv[1]); | |
if(chunk != NULL) | |
{ | |
const int channel = Mix_PlayChannelTimed(-1, chunk, -1, 8000); | |
Custom_Mix_RegisterPlaybackSpeedEffect(channel, chunk, &speed, 1, 0); | |
puts("Looping for 8 seconds, changing the pitch dynamically...\n"); | |
/* loop for 8 seconds, changing the pitch dynamically */ | |
while(SDL_GetTicks() < 8000) | |
speed = 1 + 0.25*sin(0.001*SDL_GetTicks()); | |
puts("Finished."); | |
} | |
else | |
puts("No data."); | |
Mix_FreeChunk(chunk); | |
Mix_CloseAudio(); | |
Mix_Quit(); | |
SDL_Quit(); | |
return EXIT_SUCCESS; | |
} |
Last revision fixes a sound artifact (crackling) and adds other interpolation methods (commented out).
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
You're welcome!
About the Mix_HaltChannel problem, well... to be honest I wrote this a while ago and if I remember correctly, the call was supposed to prevent silence between loops.
I didn't realise back then that it could cause such access violations. I never actually tested it with lots of sounds simultaneously.
Now that you pointed that out, I did experience some occasional seg. faults later on, which I never figured out the cause.