├── .gitignore ├── GranularSynth.sln ├── GranularSynth.vcxproj ├── GranularSynth.vcxproj.filters ├── README.md ├── Source.cpp ├── legend1.mp3 ├── legend1.wav ├── legend2.mp3 ├── legend2.wav ├── out_A_FastHigh.mp3 ├── out_A_FastHigh.wav ├── out_A_FasterHigher.mp3 ├── out_A_FasterHigher.wav ├── out_A_SlowLow.mp3 ├── out_A_SlowLow.wav ├── out_A_SlowerLower.mp3 ├── out_A_SlowerLower.wav ├── out_B_Fast.mp3 ├── out_B_Fast.wav ├── out_B_Faster.mp3 ├── out_B_Faster.wav ├── out_B_Slow.mp3 ├── out_B_Slow.wav ├── out_B_Slower.mp3 ├── out_B_Slower.wav ├── out_C_High.mp3 ├── out_C_High.wav ├── out_C_HighAlternate.mp3 ├── out_C_HighAlternate.wav ├── out_C_Higher.mp3 ├── out_C_Higher.wav ├── out_C_Low.mp3 ├── out_C_Low.wav ├── out_C_Lower.mp3 ├── out_C_Lower.wav ├── out_D_FastLow.mp3 ├── out_D_FastLow.wav ├── out_D_SlowHigh.mp3 ├── out_D_SlowHigh.wav ├── out_E_Pitch.mp3 ├── out_E_Pitch.wav ├── out_E_Time.mp3 ├── out_E_Time.wav ├── out_E_TimePitch.mp3 └── out_E_TimePitch.wav /.gitignore: -------------------------------------------------------------------------------- 1 | # Prerequisites 2 | *.d 3 | 4 | **/Debug/*.* 5 | **/Release/*.* 6 | **/.vs/*.* 7 | 8 | *.vcxproj.user 9 | 10 | # Compiled Object files 11 | *.slo 12 | *.lo 13 | *.o 14 | *.obj 15 | 16 | # Precompiled Headers 17 | *.gch 18 | *.pch 19 | 20 | # Compiled Dynamic libraries 21 | *.so 22 | *.dylib 23 | *.dll 24 | 25 | # Fortran module files 26 | *.mod 27 | *.smod 28 | 29 | # Compiled Static libraries 30 | *.lai 31 | *.la 32 | *.a 33 | *.lib 34 | 35 | # Executables 36 | *.exe 37 | *.out 38 | *.app 39 | *.suo 40 | .vs/PoissonBlending/v15/.suo 41 | *.opendb 42 | *.ipch 43 | *.user 44 | *.db 45 | -------------------------------------------------------------------------------- /GranularSynth.sln: -------------------------------------------------------------------------------- 1 |  2 | Microsoft Visual Studio Solution File, Format Version 12.00 3 | # Visual Studio 15 4 | VisualStudioVersion = 15.0.27130.2026 5 | MinimumVisualStudioVersion = 10.0.40219.1 6 | Project("{8BC9CEB8-8B4A-11D0-8D11-00A0C91BC942}") = "GranularSynth", "GranularSynth.vcxproj", "{3001C905-ACD8-4E9C-BC56-CF04C9EC7105}" 7 | EndProject 8 | Global 9 | GlobalSection(SolutionConfigurationPlatforms) = preSolution 10 | Debug|x64 = Debug|x64 11 | Debug|x86 = Debug|x86 12 | Release|x64 = Release|x64 13 | Release|x86 = Release|x86 14 | EndGlobalSection 15 | GlobalSection(ProjectConfigurationPlatforms) = postSolution 16 | {3001C905-ACD8-4E9C-BC56-CF04C9EC7105}.Debug|x64.ActiveCfg = Debug|x64 17 | {3001C905-ACD8-4E9C-BC56-CF04C9EC7105}.Debug|x64.Build.0 = Debug|x64 18 | {3001C905-ACD8-4E9C-BC56-CF04C9EC7105}.Debug|x86.ActiveCfg = Debug|Win32 19 | {3001C905-ACD8-4E9C-BC56-CF04C9EC7105}.Debug|x86.Build.0 = Debug|Win32 20 | {3001C905-ACD8-4E9C-BC56-CF04C9EC7105}.Release|x64.ActiveCfg = Release|x64 21 | {3001C905-ACD8-4E9C-BC56-CF04C9EC7105}.Release|x64.Build.0 = Release|x64 22 | {3001C905-ACD8-4E9C-BC56-CF04C9EC7105}.Release|x86.ActiveCfg = Release|Win32 23 | {3001C905-ACD8-4E9C-BC56-CF04C9EC7105}.Release|x86.Build.0 = Release|Win32 24 | EndGlobalSection 25 | GlobalSection(SolutionProperties) = preSolution 26 | HideSolutionNode = FALSE 27 | EndGlobalSection 28 | GlobalSection(ExtensibilityGlobals) = postSolution 29 | SolutionGuid = {2E85A6EE-E128-4EFB-A229-8A4317F63E17} 30 | EndGlobalSection 31 | EndGlobal 32 | -------------------------------------------------------------------------------- /GranularSynth.vcxproj: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | Debug 6 | Win32 7 | 8 | 9 | Release 10 | Win32 11 | 12 | 13 | Debug 14 | x64 15 | 16 | 17 | Release 18 | x64 19 | 20 | 21 | 22 | 15.0 23 | {3001C905-ACD8-4E9C-BC56-CF04C9EC7105} 24 | GranularSynth 25 | 10.0.16299.0 26 | 27 | 28 | 29 | Application 30 | true 31 | v141 32 | MultiByte 33 | 34 | 35 | Application 36 | false 37 | v141 38 | true 39 | MultiByte 40 | 41 | 42 | Application 43 | true 44 | v141 45 | MultiByte 46 | 47 | 48 | Application 49 | false 50 | v141 51 | true 52 | MultiByte 53 | 54 | 55 | 56 | 57 | 58 | 59 | 60 | 61 | 62 | 63 | 64 | 65 | 66 | 67 | 68 | 69 | 70 | 71 | 72 | 73 | 74 | 75 | Level3 76 | Disabled 77 | true 78 | true 79 | 80 | 81 | 82 | 83 | Level3 84 | Disabled 85 | true 86 | true 87 | 88 | 89 | 90 | 91 | Level3 92 | MaxSpeed 93 | true 94 | true 95 | true 96 | true 97 | 98 | 99 | true 100 | true 101 | 102 | 103 | 104 | 105 | Level3 106 | MaxSpeed 107 | true 108 | true 109 | true 110 | true 111 | 112 | 113 | true 114 | true 115 | 116 | 117 | 118 | 119 | 120 | 121 | 122 | 123 | -------------------------------------------------------------------------------- /GranularSynth.vcxproj.filters: -------------------------------------------------------------------------------- 1 |  2 | 3 | 4 | 5 | 6 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # GranularSynth 2 | 3 | The blog post that goes along with this code is at: https://blog.demofox.org/2018/03/05/granular-audio-synthesis/ 4 | -------------------------------------------------------------------------------- /Source.cpp: -------------------------------------------------------------------------------- 1 | #include 2 | #include 3 | #include 4 | #include 5 | #include 6 | #include 7 | 8 | // typedefs 9 | typedef uint16_t uint16; 10 | typedef uint32_t uint32; 11 | typedef int32_t int32; 12 | 13 | const float c_pi = 3.14159265359f; 14 | 15 | //this struct is the minimal required header data for a wav file 16 | struct SMinimalWaveFileHeader 17 | { 18 | //the main chunk 19 | unsigned char m_chunkID[4]; 20 | uint32 m_chunkSize; 21 | unsigned char m_format[4]; 22 | 23 | //sub chunk 1 "fmt " 24 | unsigned char m_subChunk1ID[4]; 25 | uint32 m_subChunk1Size; 26 | uint16 m_audioFormat; 27 | uint16 m_numChannels; 28 | uint32 m_sampleRate; 29 | uint32 m_byteRate; 30 | uint16 m_blockAlign; 31 | uint16 m_bitsPerSample; 32 | 33 | //sub chunk 2 "data" 34 | unsigned char m_subChunk2ID[4]; 35 | uint32 m_subChunk2Size; 36 | 37 | //then comes the data! 38 | }; 39 | 40 | enum class ECrossFade 41 | { 42 | None, 43 | In, 44 | Out, 45 | }; 46 | 47 | inline void FloatToPCM(unsigned char *PCM, const float& in, size_t numBytes) 48 | { 49 | // 8 bit is unsigned 50 | if (numBytes == 1) 51 | { 52 | PCM[0] = unsigned char((in * 0.5f + 0.5f) * 255.0f); 53 | return; 54 | } 55 | 56 | // casting to double because floats can't exactly store 0x7fffffff, but doubles can. 57 | // Details of that: https://blog.demofox.org/2017/11/21/floating-point-precision/ 58 | uint32 data; 59 | if (in < 0.0f) 60 | data = uint32(double(in) * double(0x80000000)); 61 | else 62 | data = uint32(double(in) * double(0x7fffffff)); 63 | 64 | switch (numBytes) 65 | { 66 | case 4: PCM[3] = ((data >> 24) & 0xFF); PCM[2] = ((data >> 16) & 0xFF); PCM[1] = ((data >> 8) & 0xFF); PCM[0] = (data & 0xFF); break; 67 | case 3: PCM[2] = ((data >> 24) & 0xFF); PCM[1] = ((data >> 16) & 0xFF); PCM[0] = ((data >> 8) & 0xFF); break; 68 | case 2: PCM[1] = ((data >> 24) & 0xFF); PCM[0] = ((data >> 16) & 0xFF); break; 69 | } 70 | } 71 | 72 | inline void PCMToFloat(float& out, const unsigned char *PCM, size_t numBytes) 73 | { 74 | // 8 bit is unsigned 75 | if (numBytes == 1) 76 | { 77 | out = (float(PCM[0]) / float(255.0f)) * 2.0f - 1.0f; 78 | return; 79 | } 80 | 81 | uint32 data = 0; 82 | switch (numBytes) 83 | { 84 | case 4: data = (uint32(PCM[3]) << 24) | (uint32(PCM[2]) << 16) | (uint32(PCM[1]) << 8) | uint32(PCM[0]); break; 85 | case 3: data = (uint32(PCM[2]) << 24) | (uint32(PCM[1]) << 16) | (uint32(PCM[0]) << 8); break; 86 | case 2: data = (uint32(PCM[1]) << 24) | (uint32(PCM[0]) << 16); break; 87 | } 88 | 89 | // casting to double because floats can't exactly store 0x7fffffff, but doubles can. 90 | // Details of that: https://blog.demofox.org/2017/11/21/floating-point-precision/ 91 | if (data & 0x80000000) 92 | out = float(double(int32(data)) / double(0x80000000)); 93 | else 94 | out = float(double(data) / double(0x7fffffff)); 95 | } 96 | 97 | // numBytes can be 1, 2, 3, or 4. 98 | // Coresponding to 8 bit, 16 bit, 24 bit, and 32 bit audio. 99 | bool WriteWaveFile(const char *fileName, std::vector& dataFloat, uint16 numChannels, uint32 sampleRate, uint16 numBytes) 100 | { 101 | std::vector data; 102 | data.resize(dataFloat.size() * numBytes); 103 | for (size_t i = 0; i < dataFloat.size(); ++i) 104 | FloatToPCM((unsigned char*)&data[i*numBytes], dataFloat[i], numBytes); 105 | 106 | uint32 dataSize = (uint32)data.size(); 107 | uint16 bitsPerSample = numBytes * 8; 108 | 109 | //open the file if we can 110 | FILE *File = nullptr; 111 | fopen_s(&File, fileName, "w+b"); 112 | if (!File) 113 | { 114 | printf("[-----ERROR-----] Could not open %s for writing.\n", fileName); 115 | return false; 116 | } 117 | 118 | SMinimalWaveFileHeader waveHeader; 119 | 120 | //fill out the main chunk 121 | memcpy(waveHeader.m_chunkID, "RIFF", 4); 122 | waveHeader.m_chunkSize = dataSize + 36; 123 | memcpy(waveHeader.m_format, "WAVE", 4); 124 | 125 | //fill out sub chunk 1 "fmt " 126 | memcpy(waveHeader.m_subChunk1ID, "fmt ", 4); 127 | waveHeader.m_subChunk1Size = 16; 128 | waveHeader.m_audioFormat = 1; 129 | waveHeader.m_numChannels = numChannels; 130 | waveHeader.m_sampleRate = sampleRate; 131 | waveHeader.m_byteRate = sampleRate * numChannels * bitsPerSample / 8; 132 | waveHeader.m_blockAlign = numChannels * bitsPerSample / 8; 133 | waveHeader.m_bitsPerSample = bitsPerSample; 134 | 135 | //fill out sub chunk 2 "data" 136 | memcpy(waveHeader.m_subChunk2ID, "data", 4); 137 | waveHeader.m_subChunk2Size = dataSize; 138 | 139 | //write the header 140 | fwrite(&waveHeader, sizeof(SMinimalWaveFileHeader), 1, File); 141 | 142 | //write the wave data itself 143 | fwrite(&data[0], dataSize, 1, File); 144 | 145 | //close the file and return success 146 | fclose(File); 147 | printf("%s saved.\n", fileName); 148 | return true; 149 | } 150 | 151 | bool ReadFileIntoMemory (const char *fileName, std::vector& data) 152 | { 153 | //open the file if we can 154 | FILE *file = nullptr; 155 | fopen_s(&file, fileName, "rb"); 156 | if (!file) 157 | { 158 | printf("[-----ERROR-----]Could not open %s for reading.\n", fileName); 159 | return false; 160 | } 161 | 162 | // get the file size and resize the vector to hold the data 163 | fseek(file, 0, SEEK_END); 164 | data.resize(ftell(file)); 165 | 166 | // read the file into the vector 167 | fseek(file, 0, SEEK_SET); 168 | fread(&data[0], 1, data.size(), file); 169 | 170 | // return success 171 | fclose(file); 172 | return true; 173 | } 174 | 175 | bool ReadWaveFile(const char *fileName, std::vector& data, uint16& numChannels, uint32& sampleRate, uint16& numBytes) 176 | { 177 | // read the whole file into memory if we can 178 | std::vector fileData; 179 | if (!ReadFileIntoMemory(fileName, fileData)) 180 | return false; 181 | size_t fileIndex = 0; 182 | 183 | //make sure the main chunk ID is "RIFF" 184 | if ((fileData.size() < fileIndex + 4) || memcmp(&fileData[fileIndex], "RIFF", 4)) 185 | { 186 | printf("[-----ERROR-----]%s is an invalid input file. (1)\n", fileName); 187 | return false; 188 | } 189 | fileIndex += 4; 190 | 191 | //get the main chunk size 192 | uint32 chunkSize; 193 | if (fileData.size() < fileIndex + 4) 194 | { 195 | printf("[-----ERROR-----]%s is an invalid input file. (2)\n", fileName); 196 | return false; 197 | } 198 | chunkSize = *(uint32*)&fileData[fileIndex]; 199 | fileIndex += 4; 200 | 201 | //make sure the format is "WAVE" 202 | if ((fileData.size() < fileIndex + 4) || memcmp(&fileData[fileIndex], "WAVE", 4)) 203 | { 204 | printf("[-----ERROR-----]%s is an invalid input file. (3)\n", fileName); 205 | return false; 206 | } 207 | fileIndex += 4; 208 | 209 | size_t chunkPosFmt = -1; 210 | size_t chunkPosData = -1; 211 | while(chunkPosFmt == -1 || chunkPosData == -1) 212 | { 213 | // get a chunk id and chunk size if we can 214 | if (fileData.size() < fileIndex + 8) 215 | { 216 | printf("[-----ERROR-----]%s is an invalid input file. (4)\n", fileName); 217 | return false; 218 | } 219 | 220 | // get the chunk id if we can 221 | const unsigned char* chunkID = (unsigned char*)&fileData[fileIndex]; 222 | fileIndex += 4; 223 | chunkSize = *(uint32*)&fileData[fileIndex]; 224 | fileIndex += 4; 225 | 226 | //if we hit a fmt 227 | if(!memcmp(chunkID,"fmt ", 4)) 228 | { 229 | chunkPosFmt = (long)(fileIndex - 8); 230 | } 231 | //else if we hit a data 232 | else if(!memcmp(chunkID,"data", 4)) 233 | { 234 | chunkPosData = (long)(fileIndex - 8); 235 | } 236 | 237 | //skip to the next chunk 238 | fileIndex += chunkSize; 239 | } 240 | 241 | //we'll use this handy struct to load in 242 | SMinimalWaveFileHeader waveData; 243 | 244 | //load the fmt part if we can 245 | fileIndex = chunkPosFmt; 246 | if (fileData.size() < fileIndex + 24) 247 | { 248 | printf("[-----ERROR-----]%s is an invalid input file. (5)\n", fileName); 249 | return false; 250 | } 251 | memcpy(&waveData.m_subChunk1ID, &fileData[fileIndex], 24); 252 | fileIndex += 24; 253 | 254 | //load the data part if we can 255 | fileIndex = chunkPosData; 256 | if (fileData.size() < fileIndex + 8) 257 | { 258 | printf("[-----ERROR-----]%s is an invalid input file. (6)\n", fileName); 259 | return false; 260 | } 261 | memcpy(&waveData.m_subChunk2ID, &fileData[fileIndex], 8); 262 | fileIndex += 8; 263 | 264 | //verify a couple things about the file data 265 | if(waveData.m_audioFormat != 1 || //only pcm data 266 | waveData.m_numChannels < 1 || //must have a channel 267 | waveData.m_numChannels > 2 || //must not have more than 2 268 | waveData.m_bitsPerSample > 32 || //32 bits per sample max 269 | waveData.m_bitsPerSample % 8 != 0 || //must be a multiple of 8 bites 270 | waveData.m_blockAlign > 8) //blocks must be 8 bytes or lower 271 | { 272 | printf("[-----ERROR-----]%s is an invalid input file. (7)\n", fileName); 273 | return false; 274 | } 275 | 276 | //figure out how many samples and blocks there are total in the source data 277 | size_t bytesPerSample = waveData.m_blockAlign / waveData.m_numChannels; 278 | size_t numSourceSamples = waveData.m_subChunk2Size / bytesPerSample; 279 | 280 | //allocate space for the source samples 281 | data.resize(numSourceSamples); 282 | 283 | //read in the source samples at whatever sample rate / number of channels it might be in 284 | if (fileData.size() < fileIndex + numSourceSamples * bytesPerSample) 285 | { 286 | printf("[-----ERROR-----]%s is an invalid input file. (8)\n", fileName); 287 | return false; 288 | } 289 | 290 | for(size_t nIndex = 0; nIndex < numSourceSamples; ++nIndex) 291 | { 292 | PCMToFloat(data[nIndex], &fileData[fileIndex], bytesPerSample); 293 | fileIndex += bytesPerSample; 294 | } 295 | 296 | //return our data 297 | numChannels = waveData.m_numChannels; 298 | sampleRate = waveData.m_sampleRate; 299 | numBytes = waveData.m_bitsPerSample / 8; 300 | 301 | printf("%s loaded.\n", fileName); 302 | return true; 303 | } 304 | 305 | // Cubic hermite interpolation. More information available here: https://blog.demofox.org/2015/08/08/cubic-hermite-interpolation/ 306 | // t is a value that goes from 0 to 1 to interpolate in a C1 continuous way across uniformly sampled data points. 307 | // when t is 0, this will return B. When t is 1, this will return C. 308 | static float CubicHermite (float A, float B, float C, float D, float t) 309 | { 310 | float a = -A/2.0f + (3.0f*B)/2.0f - (3.0f*C)/2.0f + D/2.0f; 311 | float b = A - (5.0f*B)/2.0f + 2.0f*C - D / 2.0f; 312 | float c = -A/2.0f + C/2.0f; 313 | float d = B; 314 | 315 | return a*t*t*t + b*t*t + c*t + d; 316 | } 317 | 318 | inline float SampleChannelFractional (const std::vector& input, float sampleFloat, uint16 channel, uint16 numChannels) 319 | { 320 | // change this to #if 0 to use linear interpolation instead, which is faster but lower quality 321 | #if 1 322 | 323 | // This uses cubic hermite interpolation to get values between samples 324 | 325 | size_t sample = size_t(sampleFloat); 326 | float sampleFraction = sampleFloat - std::floorf(sampleFloat); 327 | 328 | size_t sampleIndexNeg1 = (sample > 0) ? sample - 1 : sample; 329 | size_t sampleIndex0 = sample; 330 | size_t sampleIndex1 = sample + 1; 331 | size_t sampleIndex2 = sample + 2; 332 | 333 | sampleIndexNeg1 = sampleIndexNeg1 * numChannels + channel; 334 | sampleIndex0 = sampleIndex0 * numChannels + channel; 335 | sampleIndex1 = sampleIndex1 * numChannels + channel; 336 | sampleIndex2 = sampleIndex2 * numChannels + channel; 337 | 338 | sampleIndexNeg1 = std::min(sampleIndexNeg1, input.size() - 1); 339 | sampleIndex0 = std::min(sampleIndex0, input.size() - 1); 340 | sampleIndex1 = std::min(sampleIndex1, input.size() - 1); 341 | sampleIndex2 = std::min(sampleIndex2, input.size() - 1); 342 | 343 | return CubicHermite(input[sampleIndexNeg1], input[sampleIndex0], input[sampleIndex1], input[sampleIndex2], sampleFraction); 344 | #else 345 | 346 | // This uses linear interpolation to get values between samples. 347 | 348 | size_t sample = size_t(sampleFloat); 349 | float sampleFraction = sampleFloat - std::floorf(sampleFloat); 350 | 351 | size_t sample1Index = sample * numChannels + channel; 352 | sample1Index = std::min(sample1Index, input.size() - 1); 353 | float value1 = input[sample1Index]; 354 | 355 | size_t sample2Index = (sample+1) * numChannels + channel; 356 | sample2Index = std::min(sample2Index, input.size() - 1); 357 | float value2 = input[sample1Index]; 358 | 359 | return value1 * (1.0f - sampleFraction) + value2 * sampleFraction; 360 | #endif 361 | } 362 | 363 | // Resample 364 | void TimeAdjust (const std::vector& input, std::vector& output, uint16 numChannels, float timeMultiplier) 365 | { 366 | size_t numSrcSamples = input.size() / numChannels; 367 | size_t numOutSamples = (size_t)(float(numSrcSamples) * timeMultiplier); 368 | output.resize(numOutSamples * numChannels); 369 | 370 | for (size_t outSample = 0; outSample < numOutSamples; ++outSample) 371 | { 372 | float percent = float(outSample) / float(numOutSamples-1); 373 | 374 | float srcSampleFloat = float(numSrcSamples) * percent; 375 | 376 | for (uint16 channel = 0; channel < numChannels; ++channel) 377 | output[outSample*numChannels + channel] = SampleChannelFractional(input, srcSampleFloat, channel, numChannels); 378 | } 379 | } 380 | 381 | // writes a grain to the output buffer, applying a fade in or fade out at the beginning if it should, as well as a pitch multiplier (playback speed multiplier) for the grain 382 | size_t SplatGrainToOutput(const std::vector& input, std::vector& output, uint16 numChannels, size_t grainStart, size_t grainSize, size_t outputSampleIndex, ECrossFade crossFade, size_t crossFadeSize, float pitchMultiplier, bool isFinalGrain) 383 | { 384 | // calculate starting indices 385 | size_t outputIndex = outputSampleIndex * numChannels; 386 | 387 | // write the samples 388 | size_t numSamplesWritten = 0; 389 | for (float sample = 0; sample < float(grainSize); sample += pitchMultiplier) 390 | { 391 | // break out of the loop if we are out of bounds on the input or output 392 | if (outputIndex + numChannels > output.size()) 393 | break; 394 | 395 | float inputIndexSamples = float(grainStart) + sample; 396 | if (size_t(inputIndexSamples) * numChannels + numChannels > input.size()) 397 | break; 398 | 399 | // calculate envelope for this sample 400 | float envelope = 1.0f; 401 | if (crossFade != ECrossFade::None) 402 | { 403 | if (sample <= float(crossFadeSize)) 404 | envelope = sample / float(crossFadeSize); 405 | if (crossFade == ECrossFade::Out) 406 | envelope = 1.0f - envelope; 407 | } 408 | 409 | // write the enveloped sample 410 | for (uint16 channel = 0; channel < numChannels; ++channel) 411 | output[outputIndex + channel] += SampleChannelFractional(input, inputIndexSamples, channel, numChannels) * envelope; 412 | 413 | // move to the next samples 414 | outputIndex += numChannels; 415 | ++numSamplesWritten; 416 | } 417 | 418 | // report an error if ever the cross fade size was bigger than the actual grain size, since this causes popping and would be hard to find the cause of. 419 | // suppress error on final grain since there can be false positives due to sound ending. That makes false negatives but calling this good enough. 420 | if (!isFinalGrain && crossFadeSize > numSamplesWritten) 421 | { 422 | static bool reportedError = false; 423 | if (!reportedError) 424 | { 425 | printf("[-----ERROR-----] cross fade is longer than a grain size! (error only reported once)\n"); 426 | reportedError = true; 427 | } 428 | } 429 | 430 | // return how many samples we wrote 431 | return numSamplesWritten; 432 | } 433 | 434 | void GranularTimePitchAdjust (const std::vector& input, std::vector& output, uint16 numChannels, uint32 sampleRate, float timeMultiplier, float pitchMultiplier, float grainSizeSeconds, float crossFadeSeconds) 435 | { 436 | // calculate size of output buffer and resize it 437 | size_t numInputSamples = input.size() / numChannels; 438 | size_t numOutputSamples = (size_t)(float(numInputSamples) * timeMultiplier); 439 | output.clear(); 440 | output.resize(numOutputSamples * numChannels, 0.0f); 441 | 442 | // calculate how many grains are in the input data 443 | size_t grainSizeSamples = size_t(float(sampleRate)*grainSizeSeconds); 444 | size_t numGrains = numInputSamples / grainSizeSamples; 445 | if (numInputSamples % grainSizeSamples) 446 | numGrains++; 447 | 448 | // calculate the cross fade size 449 | size_t crossFadeSizeSamples = size_t(float(sampleRate)*crossFadeSeconds); 450 | 451 | // Repeat each grain 0 or more times to make the output be the correct size 452 | size_t outputSampleIndex = 0; 453 | size_t lastGrainWritten = -1; 454 | for (size_t grain = 0; grain < numGrains; ++grain) 455 | { 456 | // calculate the boundaries of the grain 457 | size_t inputGrainStart = grain * grainSizeSamples; 458 | 459 | // calculate the end of where this grain should go in the output buffer 460 | size_t outputSampleWindowEnd = size_t(float(inputGrainStart + grainSizeSamples) * timeMultiplier); 461 | 462 | // Splat out zero or more copies of the grain to get our output to be at least as far as we want it to be. 463 | // Zero copies happens when we shorten time and need to cut pieces (grains) out of the original sound 464 | while (outputSampleIndex < outputSampleWindowEnd) 465 | { 466 | bool isFinalGrain = (grain == numGrains - 1); 467 | 468 | // if we are writing our first grain, or the last grain we wrote was the previous grain, then we don't need to do a cross fade` 469 | if ((lastGrainWritten == -1) || (lastGrainWritten == grain - 1)) 470 | { 471 | outputSampleIndex += SplatGrainToOutput(input, output, numChannels, inputGrainStart, grainSizeSamples, outputSampleIndex, ECrossFade::None, crossFadeSizeSamples, pitchMultiplier, isFinalGrain); 472 | lastGrainWritten = grain; 473 | continue; 474 | } 475 | 476 | // else we need to fade out the old grain and then fade in the new one. 477 | // NOTE: fading out the old grain means starting to play the grain after the last one and bringing it's volume down to zero. 478 | SplatGrainToOutput(input, output, numChannels, (lastGrainWritten + 1) * grainSizeSamples, grainSizeSamples, outputSampleIndex, ECrossFade::Out, crossFadeSizeSamples, pitchMultiplier, isFinalGrain); 479 | outputSampleIndex += SplatGrainToOutput(input, output, numChannels, inputGrainStart, grainSizeSamples, outputSampleIndex, ECrossFade::In, crossFadeSizeSamples, pitchMultiplier, isFinalGrain); 480 | lastGrainWritten = grain; 481 | } 482 | } 483 | } 484 | 485 | template 486 | void GranularTimePitchAdjustDynamic (const std::vector& input, std::vector& output, uint16 numChannels, uint32 sampleRate, float grainSizeSeconds, float crossFadeSeconds, const LAMBDA& settingsCallback) 487 | { 488 | // calculate how many grains are in the input data 489 | size_t numInputSamples = input.size() / numChannels; 490 | size_t grainSizeSamples = size_t(float(sampleRate)*grainSizeSeconds); 491 | size_t numGrains = numInputSamples / grainSizeSamples; 492 | if (numInputSamples % grainSizeSamples) 493 | numGrains++; 494 | 495 | // calculate size of output buffer and resize it 496 | size_t numOutputSamples = 0; 497 | for (size_t i = 0; i < numGrains; ++i) 498 | { 499 | size_t grainStart = i * grainSizeSamples; 500 | size_t grainEnd = grainStart + grainSizeSamples; 501 | grainEnd = std::min(grainEnd, input.size()); 502 | size_t grainSize = grainEnd - grainStart; 503 | 504 | float percent = float(i) / float(numGrains); 505 | float timeMultiplier = 1.0f; 506 | float pitchMultiplier = 1.0f; 507 | settingsCallback(percent, timeMultiplier, pitchMultiplier); 508 | 509 | numOutputSamples += (size_t)(float(grainSize) * timeMultiplier); 510 | } 511 | output.clear(); 512 | output.resize(numOutputSamples * numChannels, 0.0f); 513 | 514 | // calculate the cross fade size 515 | size_t crossFadeSizeSamples = size_t(float(sampleRate)*crossFadeSeconds); 516 | 517 | // Repeat each grain 0 or more times to make the output be the correct size 518 | size_t outputSampleIndex = 0; 519 | size_t lastGrainWritten = -1; 520 | float lastGrainPitchMultiplier = 1.0f; 521 | size_t outputSampleWindowEnd = 0; 522 | for (size_t grain = 0; grain < numGrains; ++grain) 523 | { 524 | // calculate the boundaries of the grain 525 | size_t inputGrainStart = grain * grainSizeSamples; 526 | 527 | // calculate the end of where this grain should go in the output buffer 528 | float percent = float(grain) / float(numGrains); 529 | float timeMultiplier = 1.0f; 530 | float pitchMultiplier = 1.0f; 531 | settingsCallback(percent, timeMultiplier, pitchMultiplier); 532 | outputSampleWindowEnd += size_t(float(grainSizeSamples) * timeMultiplier); 533 | 534 | // Splat out zero or more copies of the grain to get our output to be at least as far as we want it to be. 535 | // Zero copies happens when we shorten time and need to cut pieces (grains) out of the original sound 536 | while (outputSampleIndex < outputSampleWindowEnd) 537 | { 538 | bool isFinalGrain = (grain == numGrains - 1); 539 | 540 | // if we are writing our first grain, or the last grain we wrote was the previous grain, then we don't need to do a cross fade` 541 | if ((lastGrainWritten == -1) || (lastGrainWritten == grain - 1)) 542 | { 543 | outputSampleIndex += SplatGrainToOutput(input, output, numChannels, inputGrainStart, grainSizeSamples, outputSampleIndex, ECrossFade::None, crossFadeSizeSamples, pitchMultiplier, isFinalGrain); 544 | lastGrainWritten = grain; 545 | lastGrainPitchMultiplier = pitchMultiplier; 546 | continue; 547 | } 548 | 549 | // else we need to fade out the old grain and then fade in the new one. 550 | // NOTE: fading out the old grain means starting to play the grain after the last one and bringing it's volume down to zero, using the previous grain's pitch multiplier. 551 | SplatGrainToOutput(input, output, numChannels, (lastGrainWritten + 1) * grainSizeSamples, grainSizeSamples, outputSampleIndex, ECrossFade::Out, crossFadeSizeSamples, lastGrainPitchMultiplier, isFinalGrain); 552 | outputSampleIndex += SplatGrainToOutput(input, output, numChannels, inputGrainStart, grainSizeSamples, outputSampleIndex, ECrossFade::In, crossFadeSizeSamples, pitchMultiplier, isFinalGrain); 553 | lastGrainWritten = grain; 554 | lastGrainPitchMultiplier = pitchMultiplier; 555 | } 556 | } 557 | } 558 | 559 | //the entry point of our application 560 | int main(int argc, char **argv) 561 | { 562 | // load the wave file 563 | uint16 numChannels; 564 | uint32 sampleRate; 565 | uint16 numBytes; 566 | std::vector source, out, sourceLeft, sourceRight; 567 | ReadWaveFile("legend1.wav", source, numChannels, sampleRate, numBytes); 568 | 569 | // speed up the audio and increase pitch 570 | { 571 | TimeAdjust(source, out, numChannels, 0.7f); 572 | WriteWaveFile("out_A_FastHigh.wav", out, numChannels, sampleRate, numBytes); 573 | 574 | TimeAdjust(source, out, numChannels, 0.4f); 575 | WriteWaveFile("out_A_FasterHigher.wav", out, numChannels, sampleRate, numBytes); 576 | } 577 | 578 | // slow down the audio and decrease pitch 579 | { 580 | TimeAdjust(source, out, numChannels, 1.3f); 581 | WriteWaveFile("out_A_SlowLow.wav", out, numChannels, sampleRate, numBytes); 582 | 583 | TimeAdjust(source, out, numChannels, 2.1f); 584 | WriteWaveFile("out_A_SlowerLower.wav", out, numChannels, sampleRate, numBytes); 585 | } 586 | 587 | // speed up audio without affecting pitch 588 | { 589 | GranularTimePitchAdjust(source, out, numChannels, sampleRate, 0.7f, 1.0f, 0.02f, 0.002f); 590 | WriteWaveFile("out_B_Fast.wav", out, numChannels, sampleRate, numBytes); 591 | 592 | GranularTimePitchAdjust(source, out, numChannels, sampleRate, 0.4f, 1.0f, 0.02f, 0.002f); 593 | WriteWaveFile("out_B_Faster.wav", out, numChannels, sampleRate, numBytes); 594 | } 595 | 596 | // slow down audio without affecting pitch 597 | { 598 | GranularTimePitchAdjust(source, out, numChannels, sampleRate, 1.3f, 1.0f, 0.02f, 0.002f); 599 | WriteWaveFile("out_B_Slow.wav", out, numChannels, sampleRate, numBytes); 600 | 601 | GranularTimePitchAdjust(source, out, numChannels, sampleRate, 2.1f, 1.0f, 0.02f, 0.002f); 602 | WriteWaveFile("out_B_Slower.wav", out, numChannels, sampleRate, numBytes); 603 | } 604 | 605 | // Make pitch higher without affecting length 606 | { 607 | // do it in two steps - first as a granular time adjust, and then as a pitch/time adjust 608 | std::vector out2; 609 | GranularTimePitchAdjust(source, out2, numChannels, sampleRate, 1.0f / 0.7f, 1.0f, 0.02f, 0.002f); 610 | TimeAdjust(out2, out, numChannels, 0.7f); 611 | WriteWaveFile("out_C_HighAlternate.wav", out, numChannels, sampleRate, numBytes); 612 | 613 | // do it in one step by changing grain playback speeds 614 | GranularTimePitchAdjust(source, out, numChannels, sampleRate, 1.0f, 1.0f / 0.7f, 0.02f, 0.002f); 615 | WriteWaveFile("out_C_High.wav", out, numChannels, sampleRate, numBytes); 616 | 617 | GranularTimePitchAdjust(source, out, numChannels, sampleRate, 1.0f, 1.0f / 0.4f, 0.02f, 0.002f); 618 | WriteWaveFile("out_C_Higher.wav", out, numChannels, sampleRate, numBytes); 619 | } 620 | 621 | // make pitch lower without affecting length 622 | { 623 | GranularTimePitchAdjust(source, out, numChannels, sampleRate, 1.0f, 1.0f / 1.3f, 0.02f, 0.002f); 624 | WriteWaveFile("out_C_Low.wav", out, numChannels, sampleRate, numBytes); 625 | 626 | GranularTimePitchAdjust(source, out, numChannels, sampleRate, 1.0f, 1.0f / 2.1f, 0.02f, 0.002f); 627 | WriteWaveFile("out_C_Lower.wav", out, numChannels, sampleRate, numBytes); 628 | } 629 | 630 | // Make pitch lower but speed higher 631 | { 632 | GranularTimePitchAdjust(source, out, numChannels, sampleRate, 1.3f, 1.0f / 0.7f, 0.02f, 0.002f); 633 | WriteWaveFile("out_D_SlowHigh.wav", out, numChannels, sampleRate, numBytes); 634 | 635 | GranularTimePitchAdjust(source, out, numChannels, sampleRate, 0.7f, 1.0f / 1.3f, 0.02f, 0.002f); 636 | WriteWaveFile("out_D_FastLow.wav", out, numChannels, sampleRate, numBytes); 637 | } 638 | 639 | // dynamic tests which change time and pitch multipliers over time (for each input grain) 640 | { 641 | // adjust pitch on a sine wave 642 | GranularTimePitchAdjustDynamic(source, out, numChannels, sampleRate, 0.02f, 0.002f, 643 | [] (float percent, float& timeMultiplier, float& pitchMultiplier) 644 | { 645 | // time is 1 646 | // pitch is 10hz from 0.75 to 1.25 647 | timeMultiplier = 1.0f; 648 | pitchMultiplier = 1.0f / ((std::sinf(percent * c_pi * 10.0f) * 0.5f + 0.5f) * 0.5f + 0.75f); 649 | } 650 | ); 651 | WriteWaveFile("out_E_Pitch.wav", out, numChannels, sampleRate, numBytes); 652 | 653 | // adjust speed on a sine wave 654 | GranularTimePitchAdjustDynamic(source, out, numChannels, sampleRate, 0.02f, 0.002f, 655 | [] (float percent, float& timeMultiplier, float& pitchMultiplier) 656 | { 657 | // time is 13hz from 0.5 to 2.5 658 | // pitch is 1 659 | timeMultiplier = (std::sinf(percent * c_pi * 13.0f) * 0.5f + 0.5f) * 2.0f + 0.5f; 660 | pitchMultiplier = 1.0f; 661 | } 662 | ); 663 | WriteWaveFile("out_E_Time.wav", out, numChannels, sampleRate, numBytes); 664 | 665 | // adjust time and speed on a sine wave 666 | GranularTimePitchAdjustDynamic(source, out, numChannels, sampleRate, 0.02f, 0.002f, 667 | [] (float percent, float& timeMultiplier, float& pitchMultiplier) 668 | { 669 | // time is 13hz from 0.5 to 2.5 670 | // pitch is 10hz from 0.75 to 1.25 671 | timeMultiplier = (std::sinf(percent * c_pi * 10.0f) * 0.5f + 0.5f) * 2.0f + 0.5f; 672 | pitchMultiplier = 1.0f / ((std::sinf(percent * c_pi * 10.0f) * 0.5f + 0.5f) * 0.5f + 0.75f); 673 | } 674 | ); 675 | WriteWaveFile("out_E_TimePitch.wav", out, numChannels, sampleRate, numBytes); 676 | } 677 | 678 | system("pause"); 679 | } -------------------------------------------------------------------------------- /legend1.mp3: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Atrix256/GranularSynth/f742c2b492a34e4bed69d9581311aca7205f7935/legend1.mp3 -------------------------------------------------------------------------------- /legend1.wav: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Atrix256/GranularSynth/f742c2b492a34e4bed69d9581311aca7205f7935/legend1.wav -------------------------------------------------------------------------------- /legend2.mp3: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Atrix256/GranularSynth/f742c2b492a34e4bed69d9581311aca7205f7935/legend2.mp3 -------------------------------------------------------------------------------- /legend2.wav: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Atrix256/GranularSynth/f742c2b492a34e4bed69d9581311aca7205f7935/legend2.wav -------------------------------------------------------------------------------- /out_A_FastHigh.mp3: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Atrix256/GranularSynth/f742c2b492a34e4bed69d9581311aca7205f7935/out_A_FastHigh.mp3 -------------------------------------------------------------------------------- /out_A_FastHigh.wav: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Atrix256/GranularSynth/f742c2b492a34e4bed69d9581311aca7205f7935/out_A_FastHigh.wav -------------------------------------------------------------------------------- /out_A_FasterHigher.mp3: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Atrix256/GranularSynth/f742c2b492a34e4bed69d9581311aca7205f7935/out_A_FasterHigher.mp3 -------------------------------------------------------------------------------- /out_A_FasterHigher.wav: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Atrix256/GranularSynth/f742c2b492a34e4bed69d9581311aca7205f7935/out_A_FasterHigher.wav -------------------------------------------------------------------------------- /out_A_SlowLow.mp3: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Atrix256/GranularSynth/f742c2b492a34e4bed69d9581311aca7205f7935/out_A_SlowLow.mp3 -------------------------------------------------------------------------------- /out_A_SlowLow.wav: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Atrix256/GranularSynth/f742c2b492a34e4bed69d9581311aca7205f7935/out_A_SlowLow.wav -------------------------------------------------------------------------------- /out_A_SlowerLower.mp3: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Atrix256/GranularSynth/f742c2b492a34e4bed69d9581311aca7205f7935/out_A_SlowerLower.mp3 -------------------------------------------------------------------------------- /out_A_SlowerLower.wav: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Atrix256/GranularSynth/f742c2b492a34e4bed69d9581311aca7205f7935/out_A_SlowerLower.wav -------------------------------------------------------------------------------- /out_B_Fast.mp3: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Atrix256/GranularSynth/f742c2b492a34e4bed69d9581311aca7205f7935/out_B_Fast.mp3 -------------------------------------------------------------------------------- /out_B_Fast.wav: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Atrix256/GranularSynth/f742c2b492a34e4bed69d9581311aca7205f7935/out_B_Fast.wav -------------------------------------------------------------------------------- /out_B_Faster.mp3: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Atrix256/GranularSynth/f742c2b492a34e4bed69d9581311aca7205f7935/out_B_Faster.mp3 -------------------------------------------------------------------------------- /out_B_Faster.wav: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Atrix256/GranularSynth/f742c2b492a34e4bed69d9581311aca7205f7935/out_B_Faster.wav -------------------------------------------------------------------------------- /out_B_Slow.mp3: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Atrix256/GranularSynth/f742c2b492a34e4bed69d9581311aca7205f7935/out_B_Slow.mp3 -------------------------------------------------------------------------------- /out_B_Slow.wav: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Atrix256/GranularSynth/f742c2b492a34e4bed69d9581311aca7205f7935/out_B_Slow.wav -------------------------------------------------------------------------------- /out_B_Slower.mp3: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Atrix256/GranularSynth/f742c2b492a34e4bed69d9581311aca7205f7935/out_B_Slower.mp3 -------------------------------------------------------------------------------- /out_B_Slower.wav: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Atrix256/GranularSynth/f742c2b492a34e4bed69d9581311aca7205f7935/out_B_Slower.wav -------------------------------------------------------------------------------- /out_C_High.mp3: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Atrix256/GranularSynth/f742c2b492a34e4bed69d9581311aca7205f7935/out_C_High.mp3 -------------------------------------------------------------------------------- /out_C_High.wav: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Atrix256/GranularSynth/f742c2b492a34e4bed69d9581311aca7205f7935/out_C_High.wav -------------------------------------------------------------------------------- /out_C_HighAlternate.mp3: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Atrix256/GranularSynth/f742c2b492a34e4bed69d9581311aca7205f7935/out_C_HighAlternate.mp3 -------------------------------------------------------------------------------- /out_C_HighAlternate.wav: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Atrix256/GranularSynth/f742c2b492a34e4bed69d9581311aca7205f7935/out_C_HighAlternate.wav -------------------------------------------------------------------------------- /out_C_Higher.mp3: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Atrix256/GranularSynth/f742c2b492a34e4bed69d9581311aca7205f7935/out_C_Higher.mp3 -------------------------------------------------------------------------------- /out_C_Higher.wav: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Atrix256/GranularSynth/f742c2b492a34e4bed69d9581311aca7205f7935/out_C_Higher.wav -------------------------------------------------------------------------------- /out_C_Low.mp3: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Atrix256/GranularSynth/f742c2b492a34e4bed69d9581311aca7205f7935/out_C_Low.mp3 -------------------------------------------------------------------------------- /out_C_Low.wav: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Atrix256/GranularSynth/f742c2b492a34e4bed69d9581311aca7205f7935/out_C_Low.wav -------------------------------------------------------------------------------- /out_C_Lower.mp3: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Atrix256/GranularSynth/f742c2b492a34e4bed69d9581311aca7205f7935/out_C_Lower.mp3 -------------------------------------------------------------------------------- /out_C_Lower.wav: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Atrix256/GranularSynth/f742c2b492a34e4bed69d9581311aca7205f7935/out_C_Lower.wav -------------------------------------------------------------------------------- /out_D_FastLow.mp3: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Atrix256/GranularSynth/f742c2b492a34e4bed69d9581311aca7205f7935/out_D_FastLow.mp3 -------------------------------------------------------------------------------- /out_D_FastLow.wav: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Atrix256/GranularSynth/f742c2b492a34e4bed69d9581311aca7205f7935/out_D_FastLow.wav -------------------------------------------------------------------------------- /out_D_SlowHigh.mp3: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Atrix256/GranularSynth/f742c2b492a34e4bed69d9581311aca7205f7935/out_D_SlowHigh.mp3 -------------------------------------------------------------------------------- /out_D_SlowHigh.wav: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Atrix256/GranularSynth/f742c2b492a34e4bed69d9581311aca7205f7935/out_D_SlowHigh.wav -------------------------------------------------------------------------------- /out_E_Pitch.mp3: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Atrix256/GranularSynth/f742c2b492a34e4bed69d9581311aca7205f7935/out_E_Pitch.mp3 -------------------------------------------------------------------------------- /out_E_Pitch.wav: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Atrix256/GranularSynth/f742c2b492a34e4bed69d9581311aca7205f7935/out_E_Pitch.wav -------------------------------------------------------------------------------- /out_E_Time.mp3: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Atrix256/GranularSynth/f742c2b492a34e4bed69d9581311aca7205f7935/out_E_Time.mp3 -------------------------------------------------------------------------------- /out_E_Time.wav: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Atrix256/GranularSynth/f742c2b492a34e4bed69d9581311aca7205f7935/out_E_Time.wav -------------------------------------------------------------------------------- /out_E_TimePitch.mp3: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Atrix256/GranularSynth/f742c2b492a34e4bed69d9581311aca7205f7935/out_E_TimePitch.mp3 -------------------------------------------------------------------------------- /out_E_TimePitch.wav: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Atrix256/GranularSynth/f742c2b492a34e4bed69d9581311aca7205f7935/out_E_TimePitch.wav --------------------------------------------------------------------------------