Skip to content

Instantly share code, notes, and snippets.

@hydren
Last active April 26, 2023 08:22
Show Gist options
  • Star 4 You must be signed in to star a gist
  • Fork 1 You must be signed in to fork a gist
  • Save hydren/ea794e65e95c7713c00c88f74b71f8b1 to your computer and use it in GitHub Desktop.
Save hydren/ea794e65e95c7713c00c88f74b71f8b1 to your computer and use it in GitHub Desktop.
SDL_mixer chunk pitching C example
/*
* 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;
}
@hydren
Copy link
Author

hydren commented Jan 10, 2018

Instructions:

  • Make sure that custom_mix_pitch_func.h is on the same folder as this file
  • Build using: gcc sound_pitching_example.c -o test -Wall -lm `sdl2-config --cflags --libs` -lSDL2_mixer
  • Run using: ./test yoursoundfile.ogg

@RonenNess
Copy link

RonenNess commented Aug 31, 2020

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.

@hydren
Copy link
Author

hydren commented Sep 24, 2020

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.

@hydren
Copy link
Author

hydren commented Mar 29, 2022

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