├── .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
--------------------------------------------------------------------------------