soundAPI.cpp 18 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452
  1. 
  2. #include "soundAPI.h"
  3. #include "fileAPI.h"
  4. #include "../settings.h"
  5. #include "../base/format.h"
  6. #include "../base/noSimd.h"
  7. namespace dsr {
  8. // See the Source/soundManagers folder for implementations of sound_streamToSpeakers for different operating systems.
  9. bool sound_streamToSpeakers_fixed(int32_t channels, int32_t sampleRate, int32_t periodSamplesPerChannel, std::function<bool(SafePointer<float> fixedTarget)> soundOutput) {
  10. int32_t bufferSamplesPerChannel = periodSamplesPerChannel * 2;
  11. int32_t blockBytes = channels * sizeof(float);
  12. Buffer fixedBuffer = buffer_create(bufferSamplesPerChannel * blockBytes);
  13. SafePointer<float> bufferPointer = buffer_getSafeData<float>(fixedBuffer, "Fixed size output sound buffer");
  14. int32_t writeLocation = 0;
  15. int32_t readLocation = 0;
  16. return sound_streamToSpeakers(channels, sampleRate, [&](SafePointer<float> dynamicTarget, int32_t requestedSamplesPerChannel) -> bool {
  17. // TODO: Allow having a fixed period smaller than what the hardware requests, by delaying buffer allocation.
  18. if (requestedSamplesPerChannel > periodSamplesPerChannel) {
  19. throwError(U"The fixed period length was smaller than the requested period!\n");
  20. }
  21. int32_t availableSamplesPerChannel = writeLocation - readLocation;
  22. if (availableSamplesPerChannel < 0) availableSamplesPerChannel += bufferSamplesPerChannel;
  23. while (availableSamplesPerChannel < requestedSamplesPerChannel) {
  24. safeMemorySet(bufferPointer + (writeLocation * channels), 0, periodSamplesPerChannel * blockBytes);
  25. if (!soundOutput(bufferPointer + (writeLocation * channels))) {
  26. return false;
  27. }
  28. availableSamplesPerChannel += periodSamplesPerChannel;
  29. writeLocation += periodSamplesPerChannel;
  30. while (writeLocation >= bufferSamplesPerChannel) writeLocation -= bufferSamplesPerChannel;
  31. }
  32. int32_t readEndLocation = readLocation + requestedSamplesPerChannel;
  33. if (readEndLocation <= bufferSamplesPerChannel) {
  34. // Continuous memory.
  35. safeMemoryCopy(dynamicTarget, bufferPointer + (readLocation * channels), requestedSamplesPerChannel * blockBytes);
  36. } else {
  37. // Wraps around the fixed buffer's end.
  38. int32_t firstLength = bufferSamplesPerChannel - readLocation;
  39. int32_t secondLength = requestedSamplesPerChannel - firstLength;
  40. int32_t firstSize = firstLength * blockBytes;
  41. int32_t secondSize = secondLength * blockBytes;
  42. safeMemoryCopy(dynamicTarget, bufferPointer + (readLocation * channels), firstSize);
  43. safeMemoryCopy(dynamicTarget + (firstLength * channels), bufferPointer, secondSize);
  44. }
  45. readLocation = readEndLocation;
  46. while (readLocation >= bufferSamplesPerChannel) readLocation -= bufferSamplesPerChannel;
  47. return true;
  48. });
  49. }
  50. SoundBuffer::SoundBuffer(uint32_t samplesPerChannel, uint32_t channelCount, uint32_t sampleRate) {
  51. this->impl_samplesPerChannel = samplesPerChannel;
  52. if (this->impl_samplesPerChannel < 1) this->impl_samplesPerChannel = 1;
  53. this->impl_channelCount = channelCount;
  54. if (this->impl_channelCount < 1) this->impl_channelCount = 1;
  55. this->impl_sampleRate = sampleRate;
  56. if (this->impl_sampleRate < 1) this->impl_sampleRate = 1;
  57. this->impl_samples = buffer_create(this->impl_samplesPerChannel * this->impl_channelCount * sizeof(float));
  58. }
  59. // scaleOffset 0.0 preserves the mantissa better using power of two multiplications.
  60. // scaleOffset 1.0 allows using the full -1.0 to +1.0 range to prevent hard clipping of high values.
  61. static double scaleOffset = 1.0f;
  62. static double toIntegerScaleU8 = 128.0 - scaleOffset;
  63. static double toIntegerScaleI16 = 32768.0 - scaleOffset;
  64. static double toIntegerScaleI24 = 8388608.0 - scaleOffset;
  65. static double toIntegerScaleI32 = 2147483648.0 - scaleOffset;
  66. static double fromIntegerScaleU8 = 1.0 / toIntegerScaleU8;
  67. static double fromIntegerScaleI16 = 1.0 / toIntegerScaleI16;
  68. static double fromIntegerScaleI24 = 1.0 / toIntegerScaleI24;
  69. static double fromIntegerScaleI32 = 1.0 / toIntegerScaleI32;
  70. // TODO: Create a folder for implementations of sound formats.
  71. static const int fmtOffset_audioFormat = 0;
  72. static const int fmtOffset_channelCount = 2;
  73. static const int fmtOffset_sampleRate = 4;
  74. static const int fmtOffset_bytesPerSecond = 8;
  75. static const int fmtOffset_blockAlign = 12;
  76. static const int fmtOffset_bitsPerSample = 14;
  77. static uint32_t getSampleBits(RiffWaveFormat format) {
  78. if (format == RiffWaveFormat::RawU8) {
  79. return 8;
  80. } else if (format == RiffWaveFormat::RawI16) {
  81. return 16;
  82. } else if (format == RiffWaveFormat::RawI24) {
  83. return 24;
  84. } else {
  85. return 32;
  86. }
  87. }
  88. static inline int64_t roundTo(double value, RoundingMethod roundingMethod) {
  89. if (roundingMethod == RoundingMethod::Nearest){
  90. return int64_t(value + (value > 0.0 ? 0.5 : -0.5));
  91. } else { // RoundingMethod::Truncate
  92. return int64_t(value);
  93. }
  94. }
  95. static inline uint8_t floatToNormalizedU8(float value, RoundingMethod roundingMethod) {
  96. int64_t closest = roundTo((double(value) * toIntegerScaleU8) + 128.0, roundingMethod);
  97. if (closest < 0) closest = 0;
  98. if (closest > 255) closest = 255;
  99. return (uint8_t)closest;
  100. }
  101. static inline int16_t floatToNormalizedI16(float value, RoundingMethod roundingMethod) {
  102. int64_t closest = roundTo(double(value) * toIntegerScaleI16, roundingMethod);
  103. if (closest < -32768) closest = -32768;
  104. if (closest > 32767) closest = 32767;
  105. return (int16_t)closest;
  106. }
  107. static inline int32_t floatToNormalizedI24(float value, RoundingMethod roundingMethod) {
  108. int64_t closest = roundTo(double(value) * toIntegerScaleI24, roundingMethod);
  109. if (closest < -8388608) closest = -8388608;
  110. if (closest > 8388607) closest = 8388607;
  111. return (int32_t)closest;
  112. }
  113. static inline int32_t floatToNormalizedI32(float value, RoundingMethod roundingMethod) {
  114. int64_t closest = roundTo(double(value) * toIntegerScaleI32, roundingMethod);
  115. if (closest < -2147483648) closest = -2147483648;
  116. if (closest > 2147483647) closest = 2147483647;
  117. return (int32_t)closest;
  118. }
  119. static inline float floatFromNormalizedU8(uint8_t value) {
  120. return float((double(value) - 128.0) * fromIntegerScaleU8);
  121. }
  122. static inline float floatFromNormalizedI16(int16_t value) {
  123. return float(double(value) * fromIntegerScaleI16);
  124. }
  125. static inline float floatFromNormalizedI24(int32_t value) {
  126. return float(double(value) * fromIntegerScaleI24);
  127. }
  128. static inline float floatFromNormalizedI32(int32_t value) {
  129. return float(double(value) * fromIntegerScaleI32);
  130. }
  131. struct Chunk {
  132. String name;
  133. SafePointer<const uint8_t> chunkStart;
  134. intptr_t chunkSize = 0;
  135. Chunk(const ReadableString &name, const Buffer &buffer)
  136. : name(name), chunkStart(buffer_getSafeData<uint8_t>(buffer, "Chunk buffer")), chunkSize(buffer_getSize(buffer)) {}
  137. Chunk(const ReadableString &name, SafePointer<const uint8_t> chunkStart, intptr_t chunkSize)
  138. : name(name), chunkStart(chunkStart), chunkSize(chunkSize) {}
  139. Chunk() {}
  140. };
  141. static Buffer combineRiffChunks(List<Chunk> subChunks) {
  142. uintptr_t payloadSize = 4u; // "WAVE"
  143. for (intptr_t s = 0; s < subChunks.length(); s++) {
  144. payloadSize += 8 + subChunks[s].chunkSize;
  145. }
  146. uintptr_t totalSize = payloadSize + 8u; // RIFF size
  147. Buffer result = buffer_create(totalSize);
  148. SafePointer<uint8_t> targetBytes = buffer_getSafeData<uint8_t>(result, "RIFF encoding target buffer");
  149. targetBytes[0] = 'R';
  150. targetBytes[1] = 'I';
  151. targetBytes[2] = 'F';
  152. targetBytes[3] = 'F';
  153. targetBytes += 4;
  154. format_writeU32_LE(targetBytes, payloadSize);
  155. targetBytes += 4;
  156. targetBytes[0] = 'W';
  157. targetBytes[1] = 'A';
  158. targetBytes[2] = 'V';
  159. targetBytes[3] = 'E';
  160. targetBytes += 4;
  161. for (intptr_t s = 0; s < subChunks.length(); s++) {
  162. uintptr_t subChunkSize = subChunks[s].chunkSize;
  163. targetBytes[0] = char(subChunks[s].name[0]);
  164. targetBytes[1] = char(subChunks[s].name[1]);
  165. targetBytes[2] = char(subChunks[s].name[2]);
  166. targetBytes[3] = char(subChunks[s].name[3]);
  167. targetBytes += 4;
  168. format_writeU32_LE(targetBytes, subChunkSize);
  169. targetBytes += 4;
  170. safeMemoryCopy(targetBytes, subChunks[s].chunkStart, subChunkSize);
  171. targetBytes += subChunkSize;
  172. }
  173. return result;
  174. }
  175. Buffer sound_encode_RiffWave(const SoundBuffer &sound, RiffWaveFormat format, RoundingMethod roundingMethod) {
  176. uint32_t bitsPerSample = getSampleBits(format);
  177. uint32_t bytesPerSample = bitsPerSample / 8;
  178. uint32_t channelCount = sound_getChannelCount(sound);
  179. uint32_t samplesPerChannel = sound_getSamplesPerChannel(sound);
  180. uint32_t blockAlign = channelCount * bytesPerSample;
  181. uint32_t dataBytes = blockAlign * samplesPerChannel;
  182. uint32_t sampleRate = sound_getSampleRate(sound);
  183. uint32_t bytesPerSecond = blockAlign * sampleRate;
  184. Buffer fmt = buffer_create(16);
  185. SafePointer<uint8_t> formatBytes = buffer_getSafeData<uint8_t>(fmt, "RIFF encoding format buffer");
  186. format_writeU16_LE(formatBytes + fmtOffset_audioFormat, 1); // PCM
  187. format_writeU16_LE(formatBytes + fmtOffset_channelCount, channelCount);
  188. format_writeU32_LE(formatBytes + fmtOffset_sampleRate, sampleRate);
  189. format_writeU32_LE(formatBytes + fmtOffset_bytesPerSecond, bytesPerSecond);
  190. format_writeU16_LE(formatBytes + fmtOffset_blockAlign, blockAlign);
  191. format_writeU16_LE(formatBytes + fmtOffset_bitsPerSample, bitsPerSample);
  192. Buffer data = buffer_create(dataBytes);
  193. SafePointer<uint8_t> target = buffer_getSafeData<uint8_t>(data, "RIFF encoding data buffer");
  194. SafePointer<const float> source = sound_getSafePointer(sound);
  195. uintptr_t totalSamples = channelCount * samplesPerChannel;
  196. if (format == RiffWaveFormat::RawU8) {
  197. for (uintptr_t s = 0; s < totalSamples; s++) {
  198. target[s] = floatToNormalizedU8(source[s], roundingMethod);
  199. }
  200. } else if (format == RiffWaveFormat::RawI16) {
  201. for (uintptr_t s = 0; s < totalSamples; s++) {
  202. format_writeI16_LE(target + s * bytesPerSample, floatToNormalizedI16(source[s], roundingMethod));
  203. }
  204. } else if (format == RiffWaveFormat::RawI24) {
  205. for (uintptr_t s = 0; s < totalSamples; s++) {
  206. format_writeI24_LE(target + s * bytesPerSample, floatToNormalizedI24(source[s], roundingMethod));
  207. }
  208. } else if (format == RiffWaveFormat::RawI32) {
  209. for (uintptr_t s = 0; s < totalSamples; s++) {
  210. format_writeI32_LE(target + s * bytesPerSample, floatToNormalizedI32(source[s], roundingMethod));
  211. }
  212. }
  213. return combineRiffChunks(List<Chunk>(Chunk(U"fmt ", fmt), Chunk(U"data", data)));
  214. }
  215. static String readChar4(SafePointer<const uint8_t> nameStart) {
  216. String name;
  217. for (uintptr_t b = 0; b < 4; b++) {
  218. string_appendChar(name, DsrChar(nameStart[b]));
  219. }
  220. return name;
  221. }
  222. static void getRiffChunks(const Chunk &parentChunk, std::function<void(const ReadableString &name, const Chunk &chunk)> returnChunk) {
  223. SafePointer<const uint8_t> chunkStart = parentChunk.chunkStart;
  224. SafePointer<const uint8_t> chunkEnd = chunkStart + parentChunk.chunkSize;
  225. while (chunkStart.getUnchecked() + 8 <= chunkEnd.getUnchecked()) {
  226. String name = readChar4(chunkStart);
  227. uint32_t chunkSize = format_readU32_LE(chunkStart + 4);
  228. SafePointer<const uint8_t> chunkPayload = chunkStart + 8;
  229. if (chunkPayload.getUnchecked() + chunkSize > chunkEnd.getUnchecked()) {
  230. sendWarning(U"Not enough space remaining (", uint64_t((uintptr_t)chunkEnd.getUnchecked() - (uintptr_t)chunkPayload.getUnchecked()), U" bytes) in the RIFF wave file to read the ", name, U" chunk of ", chunkSize, U" bytes!\n");
  231. return;
  232. }
  233. returnChunk(name, Chunk(name, chunkPayload, chunkSize));
  234. chunkStart = chunkStart + 8 + chunkSize;
  235. }
  236. }
  237. static void getRiffChunks(const Buffer &fileBuffer, std::function<void(const ReadableString &name, const Chunk &chunk)> returnChunk) {
  238. Chunk rootChunk = Chunk(U"RIFF", fileBuffer);
  239. getRiffChunks(rootChunk, [&returnChunk](const ReadableString &name, const Chunk &chunk) {
  240. if (string_match(name, U"RIFF")) {
  241. if (!string_match(readChar4(chunk.chunkStart), U"WAVE")) {
  242. throwError(U"WAVE format expected in RIFF file!\n");
  243. }
  244. getRiffChunks(Chunk(name, chunk.chunkStart + 4, chunk.chunkSize - 4), returnChunk);
  245. }
  246. });
  247. }
  248. SoundBuffer sound_decode_RiffWave(const Buffer &fileBuffer) {
  249. Chunk fmtChunk;
  250. Chunk dataChunk;
  251. bool hasFmt = false;
  252. bool hasData = false;
  253. SafePointer<uint8_t> bufferStart = buffer_getSafeData<uint8_t>(fileBuffer, "File buffer");
  254. getRiffChunks(fileBuffer, [&bufferStart, &fmtChunk, &hasFmt, &dataChunk, &hasData](const ReadableString &name, const Chunk &chunk) {
  255. if (string_match(name, U"fmt ")) {
  256. fmtChunk = chunk;
  257. hasFmt = true;
  258. } else if (string_match(name, U"data")) {
  259. dataChunk = chunk;
  260. hasData = true;
  261. }
  262. });
  263. if (!hasFmt || !hasData) {
  264. if (!hasFmt) {
  265. sendWarning(U"Failed to find any fmt chunk in the RIFF wave file!\n");
  266. }
  267. if (!hasData) {
  268. sendWarning(U"Failed to find any data chunk in the RIFF wave file!\n");
  269. }
  270. return SoundBuffer();
  271. }
  272. if (fmtChunk.chunkSize < 16) {
  273. sendWarning(U"The fmt chunk of ", fmtChunk.chunkSize, U" bytes is not large enough in the RIFF wave file!\n");
  274. return SoundBuffer();
  275. }
  276. uintptr_t audioFormat = format_readU16_LE(fmtChunk.chunkStart + fmtOffset_audioFormat);
  277. uintptr_t channelCount = format_readU16_LE(fmtChunk.chunkStart + fmtOffset_channelCount);
  278. uintptr_t sampleRate = format_readU32_LE(fmtChunk.chunkStart + fmtOffset_sampleRate);
  279. //uintptr_t bytesPerSecond = format_readU32_LE(fmtChunk.chunkStart + fmtOffset_bytesPerSecond);
  280. uintptr_t blockAlign = format_readU16_LE(fmtChunk.chunkStart + fmtOffset_blockAlign);
  281. uintptr_t bitsPerSample = format_readU16_LE(fmtChunk.chunkStart + fmtOffset_bitsPerSample);
  282. uintptr_t bytesPerSample = bitsPerSample / 8;
  283. uintptr_t dataSize = dataChunk.chunkSize;
  284. uintptr_t blockCount = dataSize / blockAlign;
  285. SoundBuffer result = SoundBuffer(blockCount, channelCount, sampleRate);
  286. SafePointer<float> target = sound_getSafePointer(result);
  287. SafePointer<const uint8_t> waveContent = dataChunk.chunkStart;
  288. if (audioFormat == 1 && bitsPerSample == 8) {
  289. for (uintptr_t b = 0; b < blockCount; b++) {
  290. for (uintptr_t c = 0; c < channelCount; c++) {
  291. *target = floatFromNormalizedU8(waveContent[c]);
  292. target += 1;
  293. }
  294. waveContent += blockAlign;
  295. }
  296. return result;
  297. } else if (audioFormat == 1 && bitsPerSample == 16) {
  298. for (uintptr_t b = 0; b < blockCount; b++) {
  299. for (uintptr_t c = 0; c < channelCount; c++) {
  300. *target = floatFromNormalizedI16(format_readI16_LE(waveContent + c * bytesPerSample));
  301. target += 1;
  302. }
  303. waveContent += blockAlign;
  304. }
  305. return result;
  306. } else if (audioFormat == 1 && bitsPerSample == 24) {
  307. for (uintptr_t b = 0; b < blockCount; b++) {
  308. for (uintptr_t c = 0; c < channelCount; c++) {
  309. *target = floatFromNormalizedI24(format_readI24_LE(waveContent + c * bytesPerSample));
  310. target += 1;
  311. }
  312. waveContent += blockAlign;
  313. }
  314. return result;
  315. } else if (audioFormat == 1 && bitsPerSample == 32) {
  316. for (uintptr_t b = 0; b < blockCount; b++) {
  317. for (uintptr_t c = 0; c < channelCount; c++) {
  318. *target = floatFromNormalizedI32(format_readI32_LE(waveContent + c * bytesPerSample));
  319. target += 1;
  320. }
  321. waveContent += blockAlign;
  322. }
  323. return result;
  324. } else if (audioFormat == 3 && bitsPerSample == 32) {
  325. for (uintptr_t b = 0; b < blockCount; b++) {
  326. for (uintptr_t c = 0; c < channelCount; c++) {
  327. *target = format_bitsToF32_IEEE754(format_readU32_LE(waveContent + c * bytesPerSample));
  328. target += 1;
  329. }
  330. waveContent += blockAlign;
  331. }
  332. return result;
  333. } else if (audioFormat == 3 && bitsPerSample == 64) {
  334. for (uintptr_t b = 0; b < blockCount; b++) {
  335. for (uintptr_t c = 0; c < channelCount; c++) {
  336. *target = format_bitsToF64_IEEE754(format_readU64_LE(waveContent + c * bytesPerSample));
  337. target += 1;
  338. }
  339. waveContent += blockAlign;
  340. }
  341. return result;
  342. } else {
  343. sendWarning(U"Unsupported sound format ", audioFormat, U" of ", bitsPerSample, U" bits in RIFF wave file.\n");
  344. // Returning an empty buffer because of the failure.
  345. return SoundBuffer();
  346. }
  347. return SoundBuffer();
  348. }
  349. enum class SoundFileFormat {
  350. Unknown,
  351. WAV
  352. };
  353. static SoundFileFormat detectSoundFileExtension(const ReadableString& filename) {
  354. SoundFileFormat result = SoundFileFormat::Unknown;
  355. intptr_t lastDotIndex = string_findLast(filename, U'.');
  356. if (lastDotIndex != -1) {
  357. String extension = string_upperCase(file_getExtension(filename));
  358. if (string_match(extension, U"WAV")) {
  359. result = SoundFileFormat::WAV;
  360. }
  361. }
  362. return result;
  363. }
  364. SoundBuffer sound_load(const ReadableString& filename, bool mustExist) {
  365. SoundFileFormat extension = detectSoundFileExtension(filename);
  366. Buffer fileContent = file_loadBuffer(filename, mustExist);
  367. SoundBuffer result;
  368. if (buffer_exists(fileContent)) {
  369. if (extension == SoundFileFormat::WAV) {
  370. result = sound_decode_RiffWave(fileContent);
  371. if (mustExist && !sound_exists(result)) {
  372. throwError(U"sound_load: Failed to load the sound at ", filename, U".\n");
  373. }
  374. }
  375. }
  376. return result;
  377. }
  378. bool sound_save(const ReadableString& filename, const SoundBuffer &sound, bool mustWork) {
  379. SoundFileFormat extension = detectSoundFileExtension(filename);
  380. if (extension == SoundFileFormat::WAV) {
  381. Buffer fileContent = sound_encode_RiffWave(sound, RiffWaveFormat::RawI16);
  382. return file_saveBuffer(filename, fileContent, mustWork);
  383. // TODO: Add more sound formats.
  384. } else {
  385. if (mustWork) {
  386. throwError(U"The extension of \"", filename, U"\" did not match any supported sound format!\n");
  387. }
  388. return false;
  389. }
  390. }
  391. bool sound_save_RiffWave(const ReadableString& filename, const SoundBuffer &sound, RiffWaveFormat format, RoundingMethod roundingMethod, bool mustWork) {
  392. SoundFileFormat extension = detectSoundFileExtension(filename);
  393. if (extension == SoundFileFormat::WAV) {
  394. Buffer fileContent = sound_encode_RiffWave(sound, format, roundingMethod);
  395. return file_saveBuffer(filename, fileContent, mustWork);
  396. } else {
  397. if (mustWork) {
  398. throwError(U"The extension of \"", filename, U"\" did not match RIFF wave's extension of *.wav!\n");
  399. }
  400. return false;
  401. }
  402. }
  403. SoundBuffer sound_generate_function(uint32_t samplesPerChannel, uint32_t channelCount, uint32_t sampleRate, std::function<float(double time, uint32_t channelIndex)> generator) {
  404. SoundBuffer result = sound_create(samplesPerChannel, channelCount, sampleRate);
  405. SafePointer<float> target = sound_getSafePointer(result);
  406. double time = 0.0;
  407. double step = 1.0 / sampleRate;
  408. for (uintptr_t b = 0u; b < samplesPerChannel; b++) {
  409. for (uintptr_t c = 0u; c < channelCount; c++) {
  410. *target = generator(time, c);
  411. target += 1;
  412. }
  413. time += step;
  414. }
  415. return result;
  416. }
  417. }