You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

695 lines
22KB

  1. /*
  2. * DISTRHO Cardinal Plugin
  3. * Copyright (C) 2021-2022 Filipe Coelho <falktx@falktx.com>
  4. *
  5. * This program is free software; you can redistribute it and/or
  6. * modify it under the terms of the GNU General Public License as
  7. * published by the Free Software Foundation; either version 3 of
  8. * the License, or any later version.
  9. *
  10. * This program is distributed in the hope that it will be useful,
  11. * but WITHOUT ANY WARRANTY; without even the implied warranty of
  12. * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
  13. * GNU General Public License for more details.
  14. *
  15. * For a full copy of the GNU General Public License see the LICENSE file.
  16. */
  17. #include <juce_audio_processors/juce_audio_processors.h>
  18. #include <AvailabilityMacros.h>
  19. #if MAC_OS_X_VERSION_MAX_ALLOWED > 101200
  20. #error unwanted macOS version, too new
  21. #endif
  22. #define createPlugin createStaticPlugin
  23. #include "src/DistrhoPluginInternal.hpp"
  24. #include "src/DistrhoUIInternal.hpp"
  25. START_NAMESPACE_DISTRHO
  26. // --------------------------------------------------------------------------------------------------------------------
  27. class ParameterFromDPF : public juce::AudioProcessorParameter
  28. {
  29. PluginExporter& plugin;
  30. const ParameterEnumerationValues& enumValues;
  31. const ParameterRanges& ranges;
  32. const uint32_t hints;
  33. const uint index;
  34. bool* const updatedPtr;
  35. mutable juce::StringArray dpfValueStrings;
  36. public:
  37. ParameterFromDPF(PluginExporter& plugin_, const uint index_, bool* const updatedPtr_)
  38. : plugin(plugin_),
  39. enumValues(plugin_.getParameterEnumValues(index_)),
  40. ranges(plugin_.getParameterRanges(index_)),
  41. hints(plugin_.getParameterHints(index_)),
  42. index(index_),
  43. updatedPtr(updatedPtr_) {}
  44. void setValueNotifyingHostFromDPF(const float newValue)
  45. {
  46. setValueNotifyingHost(ranges.getNormalizedValue(newValue));
  47. *updatedPtr = false;
  48. }
  49. protected:
  50. float getValue() const override
  51. {
  52. return ranges.getNormalizedValue(plugin.getParameterValue(index));
  53. }
  54. void setValue(const float newValue) override
  55. {
  56. *updatedPtr = true;
  57. plugin.setParameterValue(index, ranges.getUnnormalizedValue(newValue));
  58. }
  59. float getDefaultValue() const override
  60. {
  61. return ranges.getNormalizedValue(plugin.getParameterDefault(index));
  62. }
  63. juce::String getName(const int maximumStringLength) const override
  64. {
  65. if (maximumStringLength <= 0)
  66. return juce::String(plugin.getParameterName(index).buffer());
  67. return juce::String(plugin.getParameterName(index).buffer(), static_cast<size_t>(maximumStringLength));
  68. }
  69. juce::String getLabel() const override
  70. {
  71. return plugin.getParameterUnit(index).buffer();
  72. }
  73. int getNumSteps() const override
  74. {
  75. if (hints & kParameterIsBoolean)
  76. return 2;
  77. if (enumValues.restrictedMode)
  78. return enumValues.count;
  79. if (hints & kParameterIsInteger)
  80. return ranges.max - ranges.min;
  81. return juce::AudioProcessorParameter::getNumSteps();
  82. }
  83. bool isDiscrete() const override
  84. {
  85. if (hints & (kParameterIsBoolean|kParameterIsInteger))
  86. return true;
  87. if (enumValues.restrictedMode)
  88. return true;
  89. return false;
  90. }
  91. bool isBoolean() const override
  92. {
  93. return (hints & kParameterIsBoolean) != 0x0;
  94. }
  95. juce::String getText(const float normalizedValue, const int maximumStringLength) const override
  96. {
  97. float value = ranges.getUnnormalizedValue(normalizedValue);
  98. if (hints & kParameterIsBoolean)
  99. {
  100. const float midRange = ranges.min + (ranges.max - ranges.min) * 0.5f;
  101. value = value > midRange ? ranges.max : ranges.min;
  102. }
  103. else if (hints & kParameterIsInteger)
  104. {
  105. value = std::round(value);
  106. }
  107. if (enumValues.restrictedMode)
  108. {
  109. for (uint32_t i=0; i < enumValues.count; ++i)
  110. {
  111. if (d_isEqual(enumValues.values[i].value, value))
  112. {
  113. if (maximumStringLength <= 0)
  114. return juce::String(enumValues.values[i].label);
  115. return juce::String(enumValues.values[i].label, static_cast<size_t>(maximumStringLength));
  116. }
  117. }
  118. }
  119. juce::String text;
  120. if (hints & kParameterIsInteger)
  121. text = juce::String(static_cast<int>(value));
  122. else
  123. text = juce::String(value);
  124. if (maximumStringLength <= 0)
  125. return text;
  126. return juce::String(text.toRawUTF8(), static_cast<size_t>(maximumStringLength));
  127. }
  128. float getValueForText(const juce::String& text) const override
  129. {
  130. if (enumValues.restrictedMode)
  131. {
  132. for (uint32_t i=0; i < enumValues.count; ++i)
  133. {
  134. if (text == enumValues.values[i].label.buffer())
  135. return ranges.getNormalizedValue(enumValues.values[i].value);
  136. }
  137. }
  138. float value;
  139. if (hints & kParameterIsInteger)
  140. value = std::atoi(text.toRawUTF8());
  141. else
  142. value = std::atof(text.toRawUTF8());
  143. return ranges.getFixedAndNormalizedValue(value);
  144. }
  145. bool isAutomatable() const override
  146. {
  147. return (hints & kParameterIsAutomatable) != 0x0;
  148. }
  149. juce::String getCurrentValueAsText() const override
  150. {
  151. const float value = plugin.getParameterValue(index);
  152. if (enumValues.restrictedMode)
  153. {
  154. for (uint32_t i=0; i < enumValues.count; ++i)
  155. {
  156. if (d_isEqual(enumValues.values[i].value, value))
  157. return juce::String(enumValues.values[i].label);
  158. }
  159. }
  160. if (hints & kParameterIsInteger)
  161. return juce::String(static_cast<int>(value));
  162. return juce::String(value);
  163. }
  164. juce::StringArray getAllValueStrings() const override
  165. {
  166. if (dpfValueStrings.size() != 0)
  167. return dpfValueStrings;
  168. if (enumValues.restrictedMode)
  169. {
  170. for (uint32_t i=0; i < enumValues.count; ++i)
  171. dpfValueStrings.add(enumValues.values[i].label.buffer());
  172. return dpfValueStrings;
  173. }
  174. if (hints & kParameterIsBoolean)
  175. {
  176. if (hints & kParameterIsInteger)
  177. {
  178. dpfValueStrings.add(juce::String(static_cast<int>(ranges.min)));
  179. dpfValueStrings.add(juce::String(static_cast<int>(ranges.max)));
  180. }
  181. else
  182. {
  183. dpfValueStrings.add(juce::String(ranges.min));
  184. dpfValueStrings.add(juce::String(ranges.max));
  185. }
  186. }
  187. else if (hints & kParameterIsInteger)
  188. {
  189. const int imin = static_cast<int>(ranges.min);
  190. const int imax = static_cast<int>(ranges.max);
  191. for (int i=imin; i<=imax; ++i)
  192. dpfValueStrings.add(juce::String(i));
  193. }
  194. return dpfValueStrings;
  195. }
  196. };
  197. // --------------------------------------------------------------------------------------------------------------------
  198. // unused in cardinal
  199. static constexpr const requestParameterValueChangeFunc nullRequestParameterValueChangeFunc = nullptr;
  200. // only needed for headless builds, which this wrapper never builds for
  201. static constexpr const updateStateValueFunc nullUpdateStateValueFunc = nullptr;
  202. // DSP/processor implementation
  203. class CardinalWrapperProcessor : public juce::AudioProcessor
  204. {
  205. friend class CardinalWrapperEditor;
  206. PluginExporter plugin;
  207. MidiEvent midiEvents[kMaxMidiEvents];
  208. TimePosition timePosition;
  209. const uint32_t parameterCount;
  210. juce::AudioProcessorParameter* bypassParameter;
  211. juce::MidiBuffer* currentMidiMessages;
  212. bool* updatedParameters;
  213. public:
  214. CardinalWrapperProcessor()
  215. : plugin(this, writeMidiFunc, nullRequestParameterValueChangeFunc, nullUpdateStateValueFunc),
  216. parameterCount(plugin.getParameterCount()),
  217. bypassParameter(nullptr),
  218. currentMidiMessages(nullptr),
  219. updatedParameters(nullptr)
  220. {
  221. if (const double sampleRate = getSampleRate())
  222. if (sampleRate > 0.0)
  223. plugin.setSampleRate(sampleRate, true);
  224. if (const int samplesPerBlock = getBlockSize())
  225. if (samplesPerBlock > 0)
  226. plugin.setBufferSize(static_cast<uint32_t>(samplesPerBlock), true);
  227. if (parameterCount != 0)
  228. {
  229. updatedParameters = new bool[parameterCount];
  230. std::memset(updatedParameters, 0, sizeof(bool)*parameterCount);
  231. for (uint i=0; i<parameterCount; ++i)
  232. {
  233. ParameterFromDPF* const param = new ParameterFromDPF(plugin, i, updatedParameters + i);
  234. addParameter(param);
  235. if (plugin.getParameterDesignation(i) == kParameterDesignationBypass)
  236. bypassParameter = param;
  237. }
  238. }
  239. }
  240. ~CardinalWrapperProcessor() override
  241. {
  242. delete[] updatedParameters;
  243. }
  244. protected:
  245. const juce::String getName() const override
  246. {
  247. return plugin.getName();
  248. }
  249. juce::StringArray getAlternateDisplayNames() const override
  250. {
  251. return juce::StringArray(plugin.getLabel());
  252. }
  253. void prepareToPlay(const double sampleRate, const int samplesPerBlock) override
  254. {
  255. DISTRHO_SAFE_ASSERT_RETURN(samplesPerBlock > 0,);
  256. plugin.deactivateIfNeeded();
  257. plugin.setSampleRate(sampleRate, true);
  258. plugin.setBufferSize(static_cast<uint32_t>(samplesPerBlock), true);
  259. plugin.activate();
  260. }
  261. void releaseResources() override
  262. {
  263. plugin.deactivateIfNeeded();
  264. }
  265. void processBlock(juce::AudioBuffer<float>& buffer, juce::MidiBuffer& midiMessages) override
  266. {
  267. const int numSamples = buffer.getNumSamples();
  268. DISTRHO_SAFE_ASSERT_INT_RETURN(numSamples > 0, numSamples, midiMessages.clear());
  269. uint32_t midiEventCount = 0;
  270. for (const juce::MidiMessageMetadata midiMessage : midiMessages)
  271. {
  272. DISTRHO_SAFE_ASSERT_CONTINUE(midiMessage.numBytes > 0);
  273. DISTRHO_SAFE_ASSERT_CONTINUE(midiMessage.samplePosition >= 0);
  274. if (midiMessage.numBytes > static_cast<int>(MidiEvent::kDataSize))
  275. continue;
  276. MidiEvent& midiEvent(midiEvents[midiEventCount++]);
  277. midiEvent.frame = static_cast<uint32_t>(midiMessage.samplePosition);
  278. midiEvent.size = (static_cast<uint8_t>(midiMessage.numBytes));
  279. std::memcpy(midiEvent.data, midiMessage.data, midiEvent.size);
  280. if (midiEventCount == kMaxMidiEvents)
  281. break;
  282. }
  283. midiMessages.clear();
  284. const juce::ScopedValueSetter<juce::MidiBuffer*> cvs(currentMidiMessages, &midiMessages, nullptr);
  285. juce::AudioPlayHead* const playhead = getPlayHead();
  286. juce::AudioPlayHead::CurrentPositionInfo posInfo;
  287. if (playhead != nullptr && playhead->getCurrentPosition(posInfo))
  288. {
  289. timePosition.playing = posInfo.isPlaying;
  290. timePosition.bbt.valid = true;
  291. // ticksPerBeat is not possible with JUCE
  292. timePosition.bbt.ticksPerBeat = 1920.0;
  293. if (posInfo.timeInSamples >= 0)
  294. timePosition.frame = static_cast<uint64_t>(posInfo.timeInSamples);
  295. else
  296. timePosition.frame = 0;
  297. // use 4/4 as fallback time signature if not provided by the host
  298. if (posInfo.timeSigNumerator == 0)
  299. posInfo.timeSigNumerator = 4;
  300. if (posInfo.timeSigDenominator == 0)
  301. posInfo.timeSigDenominator = 4;
  302. timePosition.bbt.beatsPerMinute = posInfo.bpm;
  303. const double ppqPos = std::abs(posInfo.ppqPosition);
  304. const int ppqPerBar = posInfo.timeSigNumerator * 4 / posInfo.timeSigDenominator;
  305. const double barBeats = (std::fmod(ppqPos, ppqPerBar) / ppqPerBar) * posInfo.timeSigNumerator;
  306. const double rest = std::fmod(barBeats, 1.0);
  307. timePosition.bbt.bar = static_cast<int32_t>(ppqPos) / ppqPerBar + 1;
  308. timePosition.bbt.beat = static_cast<int32_t>(barBeats - rest + 0.5) + 1;
  309. timePosition.bbt.tick = rest * timePosition.bbt.ticksPerBeat;
  310. timePosition.bbt.beatsPerBar = posInfo.timeSigNumerator;
  311. timePosition.bbt.beatType = posInfo.timeSigDenominator;
  312. if (posInfo.ppqPosition < 0.0)
  313. {
  314. --timePosition.bbt.bar;
  315. timePosition.bbt.beat = posInfo.timeSigNumerator - timePosition.bbt.beat + 1;
  316. timePosition.bbt.tick = timePosition.bbt.ticksPerBeat - timePosition.bbt.tick - 1;
  317. }
  318. timePosition.bbt.barStartTick = timePosition.bbt.ticksPerBeat*
  319. timePosition.bbt.beatsPerBar*
  320. (timePosition.bbt.bar-1);
  321. }
  322. else
  323. {
  324. timePosition.frame = 0;
  325. timePosition.playing = false;
  326. timePosition.bbt.valid = false;
  327. }
  328. plugin.setTimePosition(timePosition);
  329. DISTRHO_SAFE_ASSERT_RETURN(buffer.getNumChannels() >= 2,);
  330. const float* audioBufferIn[18] = {};
  331. float* audioBufferOut[18] = {};
  332. for (int i=buffer.getNumChannels(); --i >= 0;)
  333. {
  334. audioBufferIn[i] = buffer.getReadPointer(i);
  335. audioBufferOut[i] = buffer.getWritePointer(i);
  336. }
  337. plugin.run(audioBufferIn, audioBufferOut, static_cast<uint32_t>(numSamples), midiEvents, midiEventCount);
  338. }
  339. // fix compiler warning
  340. void processBlock(juce::AudioBuffer<double>&, juce::MidiBuffer&) override {}
  341. double getTailLengthSeconds() const override
  342. {
  343. return 0.0;
  344. }
  345. bool acceptsMidi() const override
  346. {
  347. return true;
  348. }
  349. bool producesMidi() const override
  350. {
  351. return true;
  352. }
  353. juce::AudioProcessorParameter* getBypassParameter() const override
  354. {
  355. return bypassParameter;
  356. }
  357. juce::AudioProcessorEditor* createEditor() override;
  358. bool hasEditor() const override
  359. {
  360. return true;
  361. }
  362. int getNumPrograms() override
  363. {
  364. return 1;
  365. }
  366. int getCurrentProgram() override
  367. {
  368. return 0;
  369. }
  370. void setCurrentProgram(int) override
  371. {
  372. }
  373. const juce::String getProgramName(int) override
  374. {
  375. return "Default";
  376. }
  377. void changeProgramName(int, const juce::String&) override
  378. {
  379. }
  380. void getStateInformation(juce::MemoryBlock& destData) override
  381. {
  382. juce::XmlElement xmlState("CardinalState");
  383. for (uint32_t i=0; i<parameterCount; ++i)
  384. xmlState.setAttribute(plugin.getParameterSymbol(i).buffer(), plugin.getParameterValue(i));
  385. for (uint32_t i=0, stateCount=plugin.getStateCount(); i<stateCount; ++i)
  386. {
  387. const String& key(plugin.getStateKey(i));
  388. xmlState.setAttribute(key.buffer(), plugin.getStateValue(key).buffer());
  389. }
  390. copyXmlToBinary(xmlState, destData);
  391. }
  392. void setStateInformation(const void* const data, const int sizeInBytes) override
  393. {
  394. std::unique_ptr<juce::XmlElement> xmlState(getXmlFromBinary(data, sizeInBytes));
  395. DISTRHO_SAFE_ASSERT_RETURN(xmlState.get() != nullptr,);
  396. const juce::Array<juce::AudioProcessorParameter*>& parameters(getParameters());
  397. for (uint32_t i=0; i<parameterCount; ++i)
  398. {
  399. const double value = xmlState->getDoubleAttribute(plugin.getParameterSymbol(i).buffer(),
  400. plugin.getParameterDefault(i));
  401. const float normalizedValue = plugin.getParameterRanges(i).getFixedAndNormalizedValue(value);
  402. parameters.getUnchecked(static_cast<int>(i))->setValueNotifyingHost(normalizedValue);
  403. }
  404. for (uint32_t i=0, stateCount=plugin.getStateCount(); i<stateCount; ++i)
  405. {
  406. const String& key(plugin.getStateKey(i));
  407. const juce::String value = xmlState->getStringAttribute(key.buffer(),
  408. plugin.getStateDefaultValue(i).buffer());
  409. plugin.setState(key, value.toRawUTF8());
  410. }
  411. }
  412. private:
  413. static bool writeMidiFunc(void* const ptr, const MidiEvent& midiEvent)
  414. {
  415. CardinalWrapperProcessor* const processor = static_cast<CardinalWrapperProcessor*>(ptr);
  416. DISTRHO_SAFE_ASSERT_RETURN(processor != nullptr, false);
  417. juce::MidiBuffer* const currentMidiMessages = processor->currentMidiMessages;
  418. DISTRHO_SAFE_ASSERT_RETURN(currentMidiMessages != nullptr, false);
  419. const uint8_t* const data = midiEvent.size > MidiEvent::kDataSize ? midiEvent.dataExt : midiEvent.data;
  420. return currentMidiMessages->addEvent(data,
  421. static_cast<int>(midiEvent.size),
  422. static_cast<int>(midiEvent.frame));
  423. }
  424. };
  425. // --------------------------------------------------------------------------------------------------------------------
  426. // unused in cardinal
  427. static constexpr const sendNoteFunc nullSendNoteFunc = nullptr;
  428. // unwanted, juce file dialogs are ugly
  429. static constexpr const fileRequestFunc nullFileRequestFunc = nullptr;
  430. // UI/editor implementation
  431. class CardinalWrapperEditor : public juce::AudioProcessorEditor,
  432. private juce::Timer
  433. {
  434. CardinalWrapperProcessor& cardinalProcessor;
  435. UIExporter* ui;
  436. void* const dspPtr;
  437. public:
  438. CardinalWrapperEditor(CardinalWrapperProcessor& cardinalProc)
  439. : juce::AudioProcessorEditor(cardinalProc),
  440. cardinalProcessor(cardinalProc),
  441. ui(nullptr),
  442. dspPtr(cardinalProc.plugin.getInstancePointer())
  443. {
  444. setOpaque(true);
  445. setResizable(true, false);
  446. // setResizeLimits(648, 538, -1, -1);
  447. setSize(1228, 666);
  448. startTimer(1000.0 / 60.0);
  449. }
  450. ~CardinalWrapperEditor() override
  451. {
  452. stopTimer();
  453. delete ui;
  454. }
  455. protected:
  456. void timerCallback() override
  457. {
  458. if (ui == nullptr)
  459. return;
  460. for (uint32_t i=0; i<cardinalProcessor.parameterCount; ++i)
  461. {
  462. if (cardinalProcessor.updatedParameters[i])
  463. {
  464. cardinalProcessor.updatedParameters[i] = false;
  465. ui->parameterChanged(i, cardinalProcessor.plugin.getParameterValue(i));
  466. }
  467. }
  468. repaint();
  469. }
  470. void paint(juce::Graphics&) override
  471. {
  472. if (ui == nullptr)
  473. {
  474. juce::ComponentPeer* const peer = getPeer();
  475. DISTRHO_SAFE_ASSERT_RETURN(peer != nullptr,);
  476. void* const nativeHandle = peer->getNativeHandle();
  477. DISTRHO_SAFE_ASSERT_RETURN(nativeHandle != nullptr,);
  478. ui = new UIExporter(this,
  479. (uintptr_t)nativeHandle,
  480. cardinalProcessor.getSampleRate(),
  481. editParamFunc,
  482. setParamFunc,
  483. setStateFunc,
  484. nullSendNoteFunc,
  485. setSizeFunc,
  486. nullFileRequestFunc,
  487. nullptr, // bundlePath
  488. dspPtr,
  489. 0.0 // scaleFactor
  490. );
  491. if (cardinalProcessor.wrapperType == juce::AudioProcessor::wrapperType_Standalone)
  492. {
  493. const double scaleFactor = ui->getScaleFactor();
  494. ui->setWindowOffset(4 * scaleFactor, 30 * scaleFactor);
  495. }
  496. }
  497. ui->plugin_idle();
  498. }
  499. private:
  500. static void editParamFunc(void* const ptr, const uint32_t index, const bool started)
  501. {
  502. CardinalWrapperEditor* const editor = static_cast<CardinalWrapperEditor*>(ptr);
  503. DISTRHO_SAFE_ASSERT_RETURN(editor != nullptr,);
  504. CardinalWrapperProcessor& cardinalProcessor(editor->cardinalProcessor);
  505. if (started)
  506. cardinalProcessor.getParameters().getUnchecked(static_cast<int>(index))->beginChangeGesture();
  507. else
  508. cardinalProcessor.getParameters().getUnchecked(static_cast<int>(index))->endChangeGesture();
  509. }
  510. static void setParamFunc(void* const ptr, const uint32_t index, const float value)
  511. {
  512. CardinalWrapperEditor* const editor = static_cast<CardinalWrapperEditor*>(ptr);
  513. DISTRHO_SAFE_ASSERT_RETURN(editor != nullptr,);
  514. CardinalWrapperProcessor& cardinalProcessor(editor->cardinalProcessor);
  515. const juce::Array<juce::AudioProcessorParameter*>& parameters(cardinalProcessor.getParameters());
  516. juce::AudioProcessorParameter* const parameter = parameters.getUnchecked(static_cast<int>(index));
  517. static_cast<ParameterFromDPF*>(parameter)->setValueNotifyingHostFromDPF(value);
  518. }
  519. static void setStateFunc(void* const ptr, const char* const key, const char* const value)
  520. {
  521. CardinalWrapperEditor* const editor = static_cast<CardinalWrapperEditor*>(ptr);
  522. DISTRHO_SAFE_ASSERT_RETURN(editor != nullptr,);
  523. CardinalWrapperProcessor& cardinalProcessor(editor->cardinalProcessor);
  524. cardinalProcessor.plugin.setState(key, value);
  525. }
  526. static void setSizeFunc(void* const ptr, uint width, uint height)
  527. {
  528. CardinalWrapperEditor* const editor = static_cast<CardinalWrapperEditor*>(ptr);
  529. DISTRHO_SAFE_ASSERT_RETURN(editor != nullptr,);
  530. #ifdef DISTRHO_OS_MAC
  531. UIExporter* const ui = editor->ui;
  532. DISTRHO_SAFE_ASSERT_RETURN(ui != nullptr,);
  533. const double scaleFactor = ui->getScaleFactor();
  534. width /= scaleFactor;
  535. height /= scaleFactor;
  536. #endif
  537. editor->setSize(static_cast<int>(width), static_cast<int>(height));
  538. }
  539. };
  540. juce::AudioProcessorEditor* CardinalWrapperProcessor::createEditor()
  541. {
  542. return new CardinalWrapperEditor(*this);
  543. }
  544. // --------------------------------------------------------------------------------------------------------------------
  545. END_NAMESPACE_DISTRHO
  546. // --------------------------------------------------------------------------------------------------------------------
  547. juce::AudioProcessor* createPluginFilter()
  548. {
  549. // set valid but dummy values
  550. d_nextBufferSize = 512;
  551. d_nextSampleRate = 48000.0;
  552. return new DISTRHO_NAMESPACE::CardinalWrapperProcessor;
  553. }
  554. // --------------------------------------------------------------------------------------------------------------------