-
-
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.h
is on the same folder as this filegcc sound_pitching_example.c -o test -Wall -lm `sdl2-config --cflags --libs` -lSDL2_mixer
./test yoursoundfile.ogg