-
-
Save hydren/ea794e65e95c7713c00c88f74b71f8b1 to your computer and use it in GitHub Desktop.
| /* | |
| * 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... | |
| } | |
| } |
| /* | |
| * 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; | |
| } |
Very useful, thanks! 👍
Just a warning - the Mix_HaltChannel(mixChannel); calls from inside Custom_Mix_PlaybackSpeedEffectFuncCallback can cause access violation inside SDL_Mix when playing lots of sounds with pitch at the same time. I assume its because SDL & SDL_Mix is not thread safe.
Removing these calls fix the problem, and I don't see any difference in how sounds play without it.
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.
Last revision fixes a sound artifact (crackling) and adds other interpolation methods (commented out).
Instructions:
custom_mix_pitch_func.his on the same folder as this filegcc sound_pitching_example.c -o test -Wall -lm `sdl2-config --cflags --libs` -lSDL2_mixer./test yoursoundfile.ogg