Skip to content

Commit d306b86

Browse files
committed
Implemented stateless resample
1 parent 25f2dc4 commit d306b86

File tree

2 files changed

+328
-6
lines changed

2 files changed

+328
-6
lines changed

include/ReSinc.hpp

Lines changed: 243 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,21 @@
99

1010
#define _USE_MATH_DEFINES
1111

12+
// =============================================================================
13+
// TEMPLATE UTILITIES
14+
// =============================================================================
15+
16+
/** * @brief Helper to create a non-deducible context.
17+
* This forces the compiler to deduce TYPE from other arguments (like buffers)
18+
* and then simply convert the sample rate arguments to that TYPE.
19+
*/
20+
template<typename T>
21+
struct identity {
22+
using type = T;
23+
};
24+
template<typename T>
25+
using non_deduced = typename identity<T>::type;
26+
1227
// =============================================================================
1328
// SFINAE TRAITS
1429
// =============================================================================
@@ -47,8 +62,8 @@ namespace resinc_traits {
4762
struct is_multi_channel<
4863
T,
4964
std::enable_if_t<has_size_and_data<T>::value &&
50-
std::is_arithmetic_v<std::decay_t<
51-
decltype(std::declval<T>()[0][0])>>>> :
65+
has_size_and_data<std::decay_t<
66+
decltype(std::declval<T>()[0])>>::value>> :
5267
std::true_type {};
5368

5469
/** @brief Detects JUCE-compatible AudioBuffer classes (has
@@ -58,10 +73,17 @@ namespace resinc_traits {
5873
template<typename T>
5974
struct is_juce_type<
6075
T,
61-
std::void_t<decltype(std::declval<T>().getNumChannels()),
62-
decltype(std::declval<T>().getNumSamples()),
63-
decltype(std::declval<T>().getReadPointer(0)),
64-
decltype(std::declval<T>().getWritePointer(0))>> :
76+
std::void_t<
77+
// Method Existence
78+
decltype(std::declval<const T>().getNumChannels()),
79+
decltype(std::declval<const T>().getNumSamples()),
80+
decltype(std::declval<const T>().getReadPointer(0)),
81+
decltype(std::declval<T>().getWritePointer(0)),
82+
decltype(std::declval<T>().clear()),
83+
std::enable_if_t<std::is_integral_v<
84+
decltype(std::declval<T>().getNumChannels())>>,
85+
std::enable_if_t<std::is_pointer_v<
86+
decltype(std::declval<T>().getReadPointer(0))>>>> :
6587
std::true_type {};
6688
} // namespace resinc_traits
6789

@@ -152,6 +174,25 @@ namespace internal {
152174
(SINC_RADIUS + 1) * OVERSAMPLE_FACTOR * 2>&
153175
window);
154176
};
177+
178+
/**
179+
* @brief Core implementation for stateless one-shot resampling.
180+
* * This helper performs a full sample rate conversion by locally
181+
* configuring a Sinc filter and Kaiser window. Unlike the class-based
182+
* resampler, this function treats the boundaries of the input buffer as
183+
* silence (zero-padding).
184+
* *
185+
* * Performance Note: This function re-calculates the filter table on every
186+
* call. For repeated block-based processing, use the Resampler class
187+
* instead.
188+
*/
189+
template<typename TYPE, int SINC_RADIUS, int RESOLUTION = 256>
190+
int resample_helper(const TYPE* const* ptrToInBuffers,
191+
TYPE* const* ptrToOutBuffers,
192+
int numChannels,
193+
int numSamples,
194+
TYPE sourceSampleRate,
195+
TYPE targetSampleRate);
155196
} // namespace internal
156197

157198
// =============================================================================
@@ -420,6 +461,70 @@ class Resampler {
420461
int numSamples);
421462
};
422463

464+
// =============================================================================
465+
// STATELESS RESAMPLING FUNCTIONS
466+
// =============================================================================
467+
468+
/**
469+
* @brief Resamples a JUCE-style AudioBuffer without internal history.
470+
* * This is a one-shot function that ignores previous or future audio blocks.
471+
* Ideal for offline processing or UI assets.
472+
* * @tparam TYPE Sample type (float, double).
473+
* @tparam SINC_RADIUS Sinc kernel radius.
474+
* @tparam RESOLUTION Phase lookup table resolution.
475+
* @return The number of output samples produced.
476+
*/
477+
template<typename TYPE, int SINC_RADIUS, int RESOLUTION = 256, typename T>
478+
typename std::enable_if_t<resinc_traits::is_juce_type<std::decay_t<T>>::value,
479+
int>
480+
resample(T&& input,
481+
non_deduced<std::remove_reference_t<T>>& output,
482+
non_deduced<TYPE> sourceSampleRate,
483+
non_deduced<TYPE> targetSampleRate);
484+
485+
/**
486+
* @brief Resamples a single-channel container (e.g., std::vector<float>)
487+
* without internal history.
488+
*/
489+
template<typename TYPE, int SINC_RADIUS, int RESOLUTION = 256, typename T>
490+
typename std::
491+
enable_if_t<resinc_traits::is_single_channel<std::decay_t<T>>::value, int>
492+
resample(T&& input,
493+
non_deduced<std::remove_reference_t<T>>& output,
494+
non_deduced<TYPE> sourceSampleRate,
495+
non_deduced<TYPE> targetSampleRate);
496+
497+
/**
498+
* @brief Resamples a multi-channel container (e.g., vector of vectors) without
499+
* internal history.
500+
*/
501+
template<typename TYPE, int SINC_RADIUS, int RESOLUTION = 256, typename T>
502+
typename std::
503+
enable_if_t<resinc_traits::is_multi_channel<std::decay_t<T>>::value, int>
504+
resample(T&& input,
505+
non_deduced<std::remove_reference_t<T>>& output,
506+
non_deduced<TYPE> sourceSampleRate,
507+
non_deduced<TYPE> targetSampleRate);
508+
509+
/**
510+
* @brief Resamples from raw pointers without internal history (Base
511+
* Implementation).
512+
* * @param ptrToInBuffers Array of pointers to input channel data.
513+
* @param ptrToOutBuffers Array of pointers to output channel data.
514+
* @param numChannels Number of channels to process.
515+
* @param numSamples Number of input samples.
516+
* @param sourceSampleRate The sample rate of the input data.
517+
* @param targetSampleRate The desired output sample rate.
518+
* @return The number of output samples produced.
519+
*/
520+
template<typename TYPE, int SINC_RADIUS, int RESOLUTION = 256>
521+
int resample(const TYPE* const* ptrToInBuffers,
522+
TYPE* const* ptrToOutBuffers,
523+
int numChannels,
524+
int numSamples,
525+
non_deduced<TYPE> sourceSampleRate,
526+
non_deduced<TYPE> targetSampleRate);
527+
423528
// =============================================================================
424529
// IMPLEMENTATIONS: CircularBuffer
425530
// =============================================================================
@@ -986,3 +1091,135 @@ int Resampler<TYPE, SINC_RADIUS, RESOLUTION>::resample_helper(
9861091
(current_phase + (outputCount / oversampleFactor)) - numSamples;
9871092
return outputCount;
9881093
}
1094+
1095+
// =============================================================================
1096+
// IMPLEMENTATIONS: resample
1097+
// =============================================================================
1098+
template<typename TYPE, int SINC_RADIUS, int RESOLUTION, typename T>
1099+
typename std::enable_if_t<resinc_traits::is_juce_type<std::decay_t<T>>::value,
1100+
int>
1101+
resample(T&& input,
1102+
non_deduced<std::remove_reference_t<T>>& output,
1103+
non_deduced<TYPE> sourceSampleRate,
1104+
non_deduced<TYPE> targetSampleRate) {
1105+
return internal::resample_helper<TYPE, SINC_RADIUS, RESOLUTION>(
1106+
input.getArrayOfReadPointers(),
1107+
output.getArrayOfWritePointers(),
1108+
input.getNumChannels(),
1109+
output.getNumSamples(),
1110+
sourceSampleRate,
1111+
targetSampleRate);
1112+
}
1113+
1114+
template<typename TYPE, int SINC_RADIUS, int RESOLUTION, typename T>
1115+
typename std::
1116+
enable_if_t<resinc_traits::is_single_channel<std::decay_t<T>>::value, int>
1117+
resample(T&& input,
1118+
non_deduced<std::remove_reference_t<T>>& output,
1119+
non_deduced<TYPE> sourceSampleRate,
1120+
non_deduced<TYPE> targetSampleRate) {
1121+
const TYPE* ptr = input.data();
1122+
TYPE* outPtr = output.data();
1123+
return internal::resample_helper<TYPE, SINC_RADIUS, RESOLUTION>(
1124+
&ptr,
1125+
&outPtr,
1126+
1,
1127+
static_cast<int>(input.size()),
1128+
sourceSampleRate,
1129+
targetSampleRate);
1130+
}
1131+
1132+
template<typename TYPE, int SINC_RADIUS, int RESOLUTION, typename T>
1133+
typename std::
1134+
enable_if_t<resinc_traits::is_multi_channel<std::decay_t<T>>::value, int>
1135+
resample(T&& input,
1136+
non_deduced<std::remove_reference_t<T>>& output,
1137+
non_deduced<TYPE> sourceSampleRate,
1138+
non_deduced<TYPE> targetSampleRate) {
1139+
int channels = static_cast<int>(input.size());
1140+
if(channels == 0) return 0;
1141+
std::vector<const TYPE*> inPtrs(channels);
1142+
std::vector<TYPE*> outPtrs(channels);
1143+
for(int i = 0; i < channels; i++) {
1144+
inPtrs[i] = input[i].data();
1145+
outPtrs[i] = output[i].data();
1146+
}
1147+
return internal::resample_helper<TYPE, SINC_RADIUS, RESOLUTION>(
1148+
inPtrs.data(),
1149+
outPtrs.data(),
1150+
channels,
1151+
static_cast<int>(input[0].size()),
1152+
sourceSampleRate,
1153+
targetSampleRate);
1154+
}
1155+
1156+
template<typename TYPE, int SINC_RADIUS, int RESOLUTION>
1157+
int resample(const TYPE* const* ptrToInBuffers,
1158+
TYPE* const* ptrToOutBuffers,
1159+
int numChannels,
1160+
int numSamples,
1161+
non_deduced<TYPE> sourceSampleRate,
1162+
non_deduced<TYPE> targetSampleRate) {
1163+
return internal::resample_helper<TYPE, SINC_RADIUS, RESOLUTION>(
1164+
ptrToInBuffers,
1165+
ptrToOutBuffers,
1166+
numChannels,
1167+
numSamples,
1168+
sourceSampleRate,
1169+
targetSampleRate);
1170+
}
1171+
1172+
// =============================================================================
1173+
// IMPLEMENTATIONS: Helpers
1174+
// =============================================================================
1175+
1176+
template<typename TYPE, int SINC_RADIUS, int RESOLUTION>
1177+
int internal::resample_helper(const TYPE* const* ptrToInBuffers,
1178+
TYPE* const* ptrToOutBuffers,
1179+
int numChannels,
1180+
int numSamples,
1181+
TYPE sourceSampleRate,
1182+
TYPE targetSampleRate) {
1183+
if(numChannels <= 0 || numSamples <= 0) return 0;
1184+
1185+
internal::Sinc<TYPE, RESOLUTION, SINC_RADIUS> sinc;
1186+
TYPE cutoff = std::min(sourceSampleRate, targetSampleRate) / TYPE(2.0);
1187+
sinc.configure_with_cutoff(cutoff, sourceSampleRate);
1188+
sinc.applyWindow(
1189+
Window::Kaiser<TYPE, (SINC_RADIUS + 1) * RESOLUTION * 2> {5.0});
1190+
1191+
const TYPE invOversampleFactor = sourceSampleRate / targetSampleRate;
1192+
const TYPE oversampleFactor = targetSampleRate / sourceSampleRate;
1193+
const int outputCount = static_cast<int>(numSamples * oversampleFactor);
1194+
const TYPE gainScale = (oversampleFactor < TYPE(1.0)) ?
1195+
static_cast<TYPE>(oversampleFactor) :
1196+
TYPE(1.0);
1197+
1198+
for(int k = 0; k < outputCount; ++k) {
1199+
const double virtualIndex =
1200+
static_cast<double>(k) * invOversampleFactor;
1201+
const int index = static_cast<int>(virtualIndex);
1202+
const int delta = static_cast<int>((virtualIndex - index) * RESOLUTION);
1203+
1204+
// --- Skip Zeros: Calculate valid n-range ---
1205+
// Constraint 1: n >= -SINC_RADIUS && n <= SINC_RADIUS
1206+
// Constraint 2: 0 <= (index - n) < numSamples
1207+
// => n <= index
1208+
// => n > index - numSamples
1209+
const int n_start = std::max(-SINC_RADIUS, index - numSamples + 1);
1210+
const int n_end = std::min(SINC_RADIUS, index);
1211+
1212+
for(int channel = 0; channel < numChannels; channel++) {
1213+
TYPE acc = 0;
1214+
const TYPE* in = ptrToInBuffers[channel];
1215+
1216+
// Hot loop: No branches, minimal iterations at boundaries
1217+
for(int n = n_start; n <= n_end; n++) {
1218+
acc += sinc(n, delta) * in[index - n];
1219+
}
1220+
1221+
ptrToOutBuffers[channel][k] = acc * gainScale;
1222+
}
1223+
}
1224+
return outputCount;
1225+
}

tests/tests.cpp

Lines changed: 85 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -518,6 +518,91 @@ int main() {
518518
<< std::endl;
519519
allTestsPassed = false;
520520
}
521+
522+
// ---
523+
// Test 9: Stateless Resample (Standalone Function - Raw Pointers)
524+
// ---
525+
std::cout << "\n## Test 9: Stateless Standalone Function (44.1k -> 48k) ##"
526+
<< std::endl;
527+
try {
528+
constexpr float FS_IN = 44100.0f;
529+
constexpr float FS_OUT = 48000.0f;
530+
constexpr int CHANNELS = 1;
531+
constexpr int SAMPLES_IN = 512;
532+
constexpr int SINC_RADIUS = 32;
533+
534+
std::vector<std::vector<float>> inputBuffer, outputBuffer;
535+
generateSine(inputBuffer, CHANNELS, SAMPLES_IN, 1000.0f, FS_IN);
536+
537+
int expectedSize = static_cast<int>(SAMPLES_IN * (FS_OUT / FS_IN));
538+
outputBuffer.assign(CHANNELS,
539+
std::vector<float>(expectedSize + 10, 0.0f));
540+
541+
const float* inPtrs[] = {inputBuffer[0].data()};
542+
float* outPtrs[] = {outputBuffer[0].data()};
543+
544+
// Calling the standalone template function
545+
int produced = resample<float, SINC_RADIUS, 256>(
546+
inPtrs, outPtrs, CHANNELS, SAMPLES_IN, FS_IN, FS_OUT);
547+
548+
std::cout << "Stateless conversion produced: " << produced
549+
<< " samples." << std::endl;
550+
551+
if(produced <= 0)
552+
throw std::runtime_error("Stateless resample produced no samples.");
553+
554+
// Verify signal presence (RMS)
555+
float sumSq = 0;
556+
for(int i = 0; i < produced; ++i)
557+
sumSq += outputBuffer[0][i] * outputBuffer[0][i];
558+
float rms = std::sqrt(sumSq / produced);
559+
560+
std::cout << "Stateless RMS: " << rms << std::endl;
561+
562+
if(rms < 0.5f)
563+
throw std::runtime_error("Stateless output signal too weak.");
564+
565+
std::cout << "PASSED: Stateless standalone function test." << std::endl;
566+
} catch(const std::exception& e) {
567+
std::cout << "!!! FAILED: Test 9. Caught: " << e.what() << std::endl;
568+
allTestsPassed = false;
569+
}
570+
std::cout << "---" << std::endl;
571+
572+
// ---
573+
// Test 10: Stateless Resample (SFINAE Vector Overload)
574+
// ---
575+
std::cout << "\n## Test 10: Stateless Vector Overload (96k -> 44.1k) ##"
576+
<< std::endl;
577+
try {
578+
constexpr double FS_IN = 96000.0;
579+
constexpr double FS_OUT = 44100.0;
580+
constexpr int SINC_RADIUS = 32;
581+
582+
std::vector<std::vector<double>> inputVec, outputVec;
583+
generateSine(inputVec, 2, 1024, 1000.0, FS_IN);
584+
585+
int expectedSize = static_cast<int>(1024 * (FS_OUT / FS_IN));
586+
outputVec.assign(2, std::vector<double>(expectedSize + 10, 0.0));
587+
588+
// Testing the SFINAE overload for std::vector<std::vector<T>>
589+
// Note: Make sure your implementation of this overload accepts the
590+
// Sample Rates
591+
int produced = resample<double, SINC_RADIUS, 256>(
592+
inputVec, outputVec, FS_IN, FS_OUT);
593+
594+
std::cout << "Multi-channel stateless produced: " << produced
595+
<< " samples." << std::endl;
596+
597+
if(produced < expectedSize - 1)
598+
throw std::runtime_error(
599+
"Output size mismatch in stateless vector test.");
600+
601+
std::cout << "PASSED: Stateless vector overload test." << std::endl;
602+
} catch(const std::exception& e) {
603+
std::cout << "!!! FAILED: Test 10. Caught: " << e.what() << std::endl;
604+
allTestsPassed = false;
605+
}
521606
std::cout << "---" << std::endl;
522607

523608
// ---

0 commit comments

Comments
 (0)