Quellcodebibliothek Statistik Leitseite products/Sources/formale Sprachen/C/Firefox/dom/media/webaudio/   (Browser von der Mozilla Stiftung Version 136.0.1©)  Datei vom 10.2.2025 mit Größe 19 kB image not shown  

Quelle  AudioNodeTrack.cpp   Sprache: C

 
/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*-*/
/* This Source Code Form is subject to the terms of the Mozilla Public
 * License, v. 2.0. If a copy of the MPL was not distributed with this file,
 * You can obtain one at http://mozilla.org/MPL/2.0/. */


#include "AudioNodeTrack.h"

#include "MediaTrackGraph.h"
#include "MediaTrackListener.h"
#include "AudioNodeEngine.h"
#include "ThreeDPoint.h"
#include "Tracing.h"
#include "AudioChannelFormat.h"
#include "AudioParamTimeline.h"
#include "AudioContext.h"
#include "nsMathUtils.h"
#include "AlignmentUtils.h"
#include "blink/Reverb.h"

using namespace mozilla::dom;

namespace mozilla {

/**
 * An AudioNodeTrack produces a single audio track with ID
 * AUDIO_TRACK. This track has rate AudioContext::sIdealAudioRate
 * for regular audio contexts, and the rate requested by the web content
 * for offline audio contexts.
 * Each chunk in the track is a single block of WEBAUDIO_BLOCK_SIZE samples.
 * Note: This must be a different value than MEDIA_STREAM_DEST_TRACK_ID
 */


AudioNodeTrack::AudioNodeTrack(AudioNodeEngine* aEngine, Flags aFlags,
                               TrackRate aSampleRate)
    : ProcessedMediaTrack(
          aSampleRate, MediaSegment::AUDIO,
          (aFlags & EXTERNAL_OUTPUT) ? new AudioSegment() : nullptr),
      mEngine(aEngine),
      mFlags(aFlags),
      mNumberOfInputChannels(2),
      mIsActive(aEngine->IsActive()),
      mMarkAsEndedAfterThisBlock(false),
      mAudioParamTrack(false),
      mPassThrough(false) {
  MOZ_ASSERT(NS_IsMainThread());
  mSuspendedCount = !(mIsActive || mFlags & EXTERNAL_OUTPUT);
  mChannelCountMode = ChannelCountMode::Max;
  mChannelInterpretation = ChannelInterpretation::Speakers;
  mLastChunks.SetLength(std::max(uint16_t(1), mEngine->OutputCount()));
  MOZ_COUNT_CTOR(AudioNodeTrack);
}

AudioNodeTrack::~AudioNodeTrack() {
  MOZ_ASSERT(mActiveInputCount == 0);
  MOZ_COUNT_DTOR(AudioNodeTrack);
}

void AudioNodeTrack::OnGraphThreadDone() { mEngine->OnGraphThreadDone(); }

void AudioNodeTrack::DestroyImpl() {
  // These are graph thread objects, so clean up on graph thread.
  mInputChunks.Clear();
  mLastChunks.Clear();

  ProcessedMediaTrack::DestroyImpl();
}

/* static */
already_AddRefed<AudioNodeTrack> AudioNodeTrack::Create(
    AudioContext* aCtx, AudioNodeEngine* aEngine, Flags aFlags,
    MediaTrackGraph* aGraph) {
  MOZ_ASSERT(NS_IsMainThread());
  MOZ_RELEASE_ASSERT(aGraph);

  // MediaRecorders use an AudioNodeTrack, but no AudioNode
  AudioNode* node = aEngine->NodeMainThread();

  RefPtr<AudioNodeTrack> track =
      new AudioNodeTrack(aEngine, aFlags, aGraph->GraphRate());
  if (node) {
    track->SetChannelMixingParametersImpl(node->ChannelCount(),
                                          node->ChannelCountModeValue(),
                                          node->ChannelInterpretationValue());
  }
  // All realtime tracks are initially suspended.
  // ApplyAudioContextOperation() is used to start tracks so that a new track
  // will not be started before the existing tracks, which may be awaiting an
  // AudioCallbackDriver to resume.
  bool isRealtime = !aCtx->IsOffline();
  track->mSuspendedCount += isRealtime;
  aGraph->AddTrack(track);
  if (isRealtime && !aCtx->ShouldSuspendNewTrack()) {
    nsTArray<RefPtr<mozilla::MediaTrack>> tracks;
    tracks.AppendElement(track);
    aGraph->ApplyAudioContextOperation(aCtx->DestinationTrack(),
                                       std::move(tracks),
                                       AudioContextOperation::Resume);
  }
  return track.forget();
}

size_t AudioNodeTrack::SizeOfExcludingThis(MallocSizeOf aMallocSizeOf) const {
  size_t amount = 0;

  // Not reported:
  // - mEngine

  amount += ProcessedMediaTrack::SizeOfExcludingThis(aMallocSizeOf);
  amount += mLastChunks.ShallowSizeOfExcludingThis(aMallocSizeOf);
  for (size_t i = 0; i < mLastChunks.Length(); i++) {
    // NB: This is currently unshared only as there are instances of
    //     double reporting in DMD otherwise.
    amount += mLastChunks[i].SizeOfExcludingThisIfUnshared(aMallocSizeOf);
  }

  return amount;
}

size_t AudioNodeTrack::SizeOfIncludingThis(MallocSizeOf aMallocSizeOf) const {
  return aMallocSizeOf(this) + SizeOfExcludingThis(aMallocSizeOf);
}

void AudioNodeTrack::SizeOfAudioNodesIncludingThis(
    MallocSizeOf aMallocSizeOf, AudioNodeSizes& aUsage) const {
  // Explicitly separate out the track memory.
  aUsage.mTrack = SizeOfIncludingThis(aMallocSizeOf);

  if (mEngine) {
    // This will fill out the rest of |aUsage|.
    mEngine->SizeOfIncludingThis(aMallocSizeOf, aUsage);
  }
}

void AudioNodeTrack::SetTrackTimeParameter(uint32_t aIndex,
                                           AudioContext* aContext,
                                           double aTrackTime) {
  QueueControlMessageWithNoShutdown(
      [self = RefPtr{this}, this, aIndex,
       relativeToTrack = RefPtr{aContext->DestinationTrack()}, aTrackTime] {
        TRACE("AudioNodeTrack::SetTrackTimeParameterImpl");
        SetTrackTimeParameterImpl(aIndex, relativeToTrack, aTrackTime);
      });
}

void AudioNodeTrack::SetTrackTimeParameterImpl(uint32_t aIndex,
                                               MediaTrack* aRelativeToTrack,
                                               double aTrackTime) {
  TrackTime ticks = aRelativeToTrack->SecondsToNearestTrackTime(aTrackTime);
  mEngine->SetTrackTimeParameter(aIndex, ticks);
}

void AudioNodeTrack::SetDoubleParameter(uint32_t aIndex, double aValue) {
  QueueControlMessageWithNoShutdown(
      [self = RefPtr{this}, this, aIndex, aValue] {
        TRACE("AudioNodeTrack::SetDoubleParameter");
        Engine()->SetDoubleParameter(aIndex, aValue);
      });
}

void AudioNodeTrack::SetInt32Parameter(uint32_t aIndex, int32_t aValue) {
  QueueControlMessageWithNoShutdown(
      [self = RefPtr{this}, this, aIndex, aValue] {
        TRACE("AudioNodeTrack::SetInt32Parameter");
        Engine()->SetInt32Parameter(aIndex, aValue);
      });
}

void AudioNodeTrack::SendTimelineEvent(uint32_t aIndex,
                                       const AudioParamEvent& aEvent) {
  QueueControlMessageWithNoShutdown(
      [self = RefPtr{this}, this, aIndex, event = aEvent]() mutable {
        TRACE("AudioNodeTrack::RecvTimelineEvent");
        Engine()->RecvTimelineEvent(aIndex, event);
      });
}

void AudioNodeTrack::SetBuffer(AudioChunk&& aBuffer) {
  QueueControlMessageWithNoShutdown(
      [self = RefPtr{this}, this, buffer = std::move(aBuffer)]() mutable {
        TRACE("AudioNodeTrack::SetBuffer");
        Engine()->SetBuffer(std::move(buffer));
      });
}

void AudioNodeTrack::SetReverb(WebCore::Reverb* aReverb,
                               uint32_t aImpulseChannelCount) {
  QueueControlMessageWithNoShutdown([self = RefPtr{this}, this,
                                     reverb = WrapUnique(aReverb),
                                     aImpulseChannelCount]() mutable {
    TRACE("AudioNodeTrack::SetReverb");
    Engine()->SetReverb(reverb.release(), aImpulseChannelCount);
  });
}

void AudioNodeTrack::SetRawArrayData(nsTArray<float>&& aData) {
  QueueControlMessageWithNoShutdown(
      [self = RefPtr{this}, this, data = std::move(aData)]() mutable {
        TRACE("AudioNodeTrack::SetRawArrayData");
        Engine()->SetRawArrayData(std::move(data));
      });
}

void AudioNodeTrack::SetChannelMixingParameters(
    uint32_t aNumberOfChannels, ChannelCountMode aChannelCountMode,
    ChannelInterpretation aChannelInterpretation) {
  QueueControlMessageWithNoShutdown([self = RefPtr{this}, this,
                                     aNumberOfChannels, aChannelCountMode,
                                     aChannelInterpretation] {
    TRACE("AudioNodeTrack::SetChannelMixingParameters");
    SetChannelMixingParametersImpl(aNumberOfChannels, aChannelCountMode,
                                   aChannelInterpretation);
  });
}

void AudioNodeTrack::SetPassThrough(bool aPassThrough) {
  QueueControlMessageWithNoShutdown([self = RefPtr{this}, this, aPassThrough] {
    TRACE("AudioNodeTrack::SetPassThrough");
    mPassThrough = aPassThrough;
  });
}

void AudioNodeTrack::SendRunnable(already_AddRefed<nsIRunnable> aRunnable) {
  QueueControlMessageWithNoShutdown([runnable = nsCOMPtr{aRunnable}] {
    TRACE("AudioNodeTrack::SendRunnable");
    runnable->Run();
  });
}

void AudioNodeTrack::SetChannelMixingParametersImpl(
    uint32_t aNumberOfChannels, ChannelCountMode aChannelCountMode,
    ChannelInterpretation aChannelInterpretation) {
  mNumberOfInputChannels = aNumberOfChannels;
  mChannelCountMode = aChannelCountMode;
  mChannelInterpretation = aChannelInterpretation;
}

uint32_t AudioNodeTrack::ComputedNumberOfChannels(uint32_t aInputChannelCount) {
  switch (mChannelCountMode) {
    case ChannelCountMode::Explicit:
      // Disregard the channel count we've calculated from inputs, and just use
      // mNumberOfInputChannels.
      return mNumberOfInputChannels;
    case ChannelCountMode::Clamped_max:
      // Clamp the computed output channel count to mNumberOfInputChannels.
      return std::min(aInputChannelCount, mNumberOfInputChannels);
    default:
    case ChannelCountMode::Max:
      // Nothing to do here, just shut up the compiler warning.
      return aInputChannelCount;
  }
}

uint32_t AudioNodeTrack::NumberOfChannels() const {
  AssertOnGraphThread();

  return mNumberOfInputChannels;
}

void AudioNodeTrack::AdvanceAndResume(TrackTime aAdvance) {
  mMainThreadCurrentTime += aAdvance;
  QueueControlMessageWithNoShutdown([self = RefPtr{this}, this, aAdvance] {
    TRACE("AudioNodeTrack::AdvanceAndResumeMessage");
    mStartTime -= aAdvance;
    mSegment->AppendNullData(aAdvance);
    DecrementSuspendCount();
  });
}

void AudioNodeTrack::ObtainInputBlock(AudioBlock& aTmpChunk,
                                      uint32_t aPortIndex) {
  uint32_t inputCount = mInputs.Length();
  uint32_t outputChannelCount = 1;
  AutoTArray<const AudioBlock*, 250> inputChunks;
  for (uint32_t i = 0; i < inputCount; ++i) {
    if (aPortIndex != mInputs[i]->InputNumber()) {
      // This input is connected to a different port
      continue;
    }
    MediaTrack* t = mInputs[i]->GetSource();
    AudioNodeTrack* a = static_cast<AudioNodeTrack*>(t);
    MOZ_ASSERT(a == t->AsAudioNodeTrack());
    if (a->IsAudioParamTrack()) {
      continue;
    }

    const AudioBlock* chunk = &a->mLastChunks[mInputs[i]->OutputNumber()];
    MOZ_ASSERT(chunk);
    if (chunk->IsNull() || chunk->mChannelData.IsEmpty()) {
      continue;
    }

    inputChunks.AppendElement(chunk);
    outputChannelCount =
        GetAudioChannelsSuperset(outputChannelCount, chunk->ChannelCount());
  }

  outputChannelCount = ComputedNumberOfChannels(outputChannelCount);

  uint32_t inputChunkCount = inputChunks.Length();
  if (inputChunkCount == 0 ||
      (inputChunkCount == 1 && inputChunks[0]->ChannelCount() == 0)) {
    aTmpChunk.SetNull(WEBAUDIO_BLOCK_SIZE);
    return;
  }

  if (inputChunkCount == 1 &&
      inputChunks[0]->ChannelCount() == outputChannelCount) {
    aTmpChunk = *inputChunks[0];
    return;
  }

  if (outputChannelCount == 0) {
    aTmpChunk.SetNull(WEBAUDIO_BLOCK_SIZE);
    return;
  }

  aTmpChunk.AllocateChannels(outputChannelCount);
  DownmixBufferType downmixBuffer;
  ASSERT_ALIGNED16(downmixBuffer.Elements());

  for (uint32_t i = 0; i < inputChunkCount; ++i) {
    AccumulateInputChunk(i, *inputChunks[i], &aTmpChunk, &downmixBuffer);
  }
}

void AudioNodeTrack::AccumulateInputChunk(uint32_t aInputIndex,
                                          const AudioBlock& aChunk,
                                          AudioBlock* aBlock,
                                          DownmixBufferType* aDownmixBuffer) {
  AutoTArray<const float*, GUESS_AUDIO_CHANNELS> channels;
  UpMixDownMixChunk(&aChunk, aBlock->ChannelCount(), channels, *aDownmixBuffer);

  for (uint32_t c = 0; c < channels.Length(); ++c) {
    const float* inputData = static_cast<const float*>(channels[c]);
    float* outputData = aBlock->ChannelFloatsForWrite(c);
    if (inputData) {
      if (aInputIndex == 0) {
        AudioBlockCopyChannelWithScale(inputData, aChunk.mVolume, outputData);
      } else {
        AudioBlockAddChannelWithScale(inputData, aChunk.mVolume, outputData);
      }
    } else {
      if (aInputIndex == 0) {
        PodZero(outputData, WEBAUDIO_BLOCK_SIZE);
      }
    }
  }
}

void AudioNodeTrack::UpMixDownMixChunk(const AudioBlock* aChunk,
                                       uint32_t aOutputChannelCount,
                                       nsTArray<const float*>& aOutputChannels,
                                       DownmixBufferType& aDownmixBuffer) {
  for (uint32_t i = 0; i < aChunk->ChannelCount(); i++) {
    aOutputChannels.AppendElement(
        static_cast<const float*>(aChunk->mChannelData[i]));
  }
  if (aOutputChannels.Length() < aOutputChannelCount) {
    if (mChannelInterpretation == ChannelInterpretation::Speakers) {
      AudioChannelsUpMix<float>(&aOutputChannels, aOutputChannelCount, nullptr);
      NS_ASSERTION(aOutputChannelCount == aOutputChannels.Length(),
                   "We called GetAudioChannelsSuperset to avoid this");
    } else {
      // Fill up the remaining aOutputChannels by zeros
      for (uint32_t j = aOutputChannels.Length(); j < aOutputChannelCount;
           ++j) {
        aOutputChannels.AppendElement(nullptr);
      }
    }
  } else if (aOutputChannels.Length() > aOutputChannelCount) {
    if (mChannelInterpretation == ChannelInterpretation::Speakers) {
      AutoTArray<float*, GUESS_AUDIO_CHANNELS> outputChannels;
      outputChannels.SetLength(aOutputChannelCount);
      aDownmixBuffer.SetLength(aOutputChannelCount * WEBAUDIO_BLOCK_SIZE);
      for (uint32_t j = 0; j < aOutputChannelCount; ++j) {
        outputChannels[j] = &aDownmixBuffer[j * WEBAUDIO_BLOCK_SIZE];
      }

      AudioChannelsDownMix<floatfloat>(aOutputChannels, outputChannels,
                                         WEBAUDIO_BLOCK_SIZE);

      aOutputChannels.SetLength(aOutputChannelCount);
      for (uint32_t j = 0; j < aOutputChannels.Length(); ++j) {
        aOutputChannels[j] = outputChannels[j];
      }
    } else {
      // Drop the remaining aOutputChannels
      aOutputChannels.RemoveLastElements(aOutputChannels.Length() -
                                         aOutputChannelCount);
    }
  }
}

// The MediaTrackGraph guarantees that this is actually one block, for
// AudioNodeTracks.
void AudioNodeTrack::ProcessInput(GraphTime aFrom, GraphTime aTo,
                                  uint32_t aFlags) {
  uint16_t outputCount = mLastChunks.Length();
  MOZ_ASSERT(outputCount == std::max(uint16_t(1), mEngine->OutputCount()));

  if (!mIsActive) {
    // mLastChunks are already null.
#ifdef DEBUG
    for (const auto& chunk : mLastChunks) {
      MOZ_ASSERT(chunk.IsNull());
    }
#endif
  } else if (InMutedCycle()) {
    mInputChunks.Clear();
    for (uint16_t i = 0; i < outputCount; ++i) {
      mLastChunks[i].SetNull(WEBAUDIO_BLOCK_SIZE);
    }
  } else {
    // We need to generate at least one input
    uint16_t maxInputs = std::max(uint16_t(1), mEngine->InputCount());
    mInputChunks.SetLength(maxInputs);
    for (uint16_t i = 0; i < maxInputs; ++i) {
      ObtainInputBlock(mInputChunks[i], i);
    }
    bool finished = false;
    if (mPassThrough) {
      MOZ_ASSERT(outputCount == 1,
                 "For now, we only support nodes that have one output port");
      mLastChunks[0] = mInputChunks[0];
    } else {
      if (maxInputs <= 1 && outputCount <= 1) {
        mEngine->ProcessBlock(this, aFrom, mInputChunks[0], &mLastChunks[0],
                              &finished);
      } else {
        mEngine->ProcessBlocksOnPorts(
            this, aFrom, Span(mInputChunks.Elements(), mEngine->InputCount()),
            Span(mLastChunks.Elements(), mEngine->OutputCount()), &finished);
      }
    }
    for (uint16_t i = 0; i < outputCount; ++i) {
      NS_ASSERTION(mLastChunks[i].GetDuration() == WEBAUDIO_BLOCK_SIZE,
                   "Invalid WebAudio chunk size");
    }
    if (finished) {
      mMarkAsEndedAfterThisBlock = true;
      if (mIsActive) {
        ScheduleCheckForInactive();
      }
    }

    if (mDisabledMode != DisabledTrackMode::ENABLED) {
      for (uint32_t i = 0; i < outputCount; ++i) {
        mLastChunks[i].SetNull(WEBAUDIO_BLOCK_SIZE);
      }
    }
  }

  if (!mEnded) {
    // Don't output anything while finished
    if (mFlags & EXTERNAL_OUTPUT) {
      AdvanceOutputSegment();
    }
    if (mMarkAsEndedAfterThisBlock && (aFlags & ALLOW_END)) {
      // This track was ended the last time that we looked at it, and all
      // of the depending tracks have ended their output as well, so now
      // it's time to mark this track as ended.
      mEnded = true;
    }
  }
}

void AudioNodeTrack::ProduceOutputBeforeInput(GraphTime aFrom) {
  MOZ_ASSERT(mEngine->AsDelayNodeEngine());
  MOZ_ASSERT(mEngine->OutputCount() == 1,
             "DelayNodeEngine output count should be 1");
  MOZ_ASSERT(!InMutedCycle(), "DelayNodes should break cycles");
  MOZ_ASSERT(mLastChunks.Length() == 1);

  if (!mIsActive) {
    mLastChunks[0].SetNull(WEBAUDIO_BLOCK_SIZE);
  } else {
    mEngine->ProduceBlockBeforeInput(this, aFrom, &mLastChunks[0]);
    NS_ASSERTION(mLastChunks[0].GetDuration() == WEBAUDIO_BLOCK_SIZE,
                 "Invalid WebAudio chunk size");
    if (mDisabledMode != DisabledTrackMode::ENABLED) {
      mLastChunks[0].SetNull(WEBAUDIO_BLOCK_SIZE);
    }
  }
}

void AudioNodeTrack::AdvanceOutputSegment() {
  AudioSegment* segment = GetData<AudioSegment>();

  AudioChunk copyChunk = *mLastChunks[0].AsMutableChunk();
  AudioSegment tmpSegment;
  tmpSegment.AppendAndConsumeChunk(std::move(copyChunk));

  for (const auto& l : mTrackListeners) {
    // Notify MediaTrackListeners.
    l->NotifyQueuedChanges(Graph(), segment->GetDuration(), tmpSegment);
  }

  if (mLastChunks[0].IsNull()) {
    segment->AppendNullData(tmpSegment.GetDuration());
  } else {
    segment->AppendFrom(&tmpSegment);
  }
}

void AudioNodeTrack::AddInput(MediaInputPort* aPort) {
  ProcessedMediaTrack::AddInput(aPort);
  AudioNodeTrack* ns = aPort->GetSource()->AsAudioNodeTrack();
  // Tracks that are not AudioNodeTracks are considered active.
  if (!ns || (ns->mIsActive && !ns->IsAudioParamTrack())) {
    IncrementActiveInputCount();
  }
}
void AudioNodeTrack::RemoveInput(MediaInputPort* aPort) {
  ProcessedMediaTrack::RemoveInput(aPort);
  AudioNodeTrack* ns = aPort->GetSource()->AsAudioNodeTrack();
  // Tracks that are not AudioNodeTracks are considered active.
  if (!ns || (ns->mIsActive && !ns->IsAudioParamTrack())) {
    DecrementActiveInputCount();
  }
}

void AudioNodeTrack::SetActive() {
  if (mIsActive || mMarkAsEndedAfterThisBlock) {
    return;
  }

  mIsActive = true;
  if (!(mFlags & EXTERNAL_OUTPUT)) {
    DecrementSuspendCount();
  }
  if (IsAudioParamTrack()) {
    // Consumers merely influence track order.
    // They do not read from the track.
    return;
  }

  for (const auto& consumer : mConsumers) {
    AudioNodeTrack* ns = consumer->GetDestination()->AsAudioNodeTrack();
    if (ns) {
      ns->IncrementActiveInputCount();
    }
  }
}

void AudioNodeTrack::ScheduleCheckForInactive() {
  if (mActiveInputCount > 0 && !mMarkAsEndedAfterThisBlock) {
    return;
  }

  RunAfterProcessing([self = RefPtr{this}, this] {
    TRACE("AudioNodeTrack::CheckForInactive");
    CheckForInactive();
  });
}

void AudioNodeTrack::CheckForInactive() {
  if (((mActiveInputCount > 0 || mEngine->IsActive()) &&
       !mMarkAsEndedAfterThisBlock) ||
      !mIsActive) {
    return;
  }

  mIsActive = false;
  mInputChunks.Clear();  // not required for foreseeable future
  for (auto& chunk : mLastChunks) {
    chunk.SetNull(WEBAUDIO_BLOCK_SIZE);
  }
  if (!(mFlags & EXTERNAL_OUTPUT)) {
    IncrementSuspendCount();
  }
  if (IsAudioParamTrack()) {
    return;
  }

  for (const auto& consumer : mConsumers) {
    AudioNodeTrack* ns = consumer->GetDestination()->AsAudioNodeTrack();
    if (ns) {
      ns->DecrementActiveInputCount();
    }
  }
}

void AudioNodeTrack::IncrementActiveInputCount() {
  ++mActiveInputCount;
  SetActive();
}

void AudioNodeTrack::DecrementActiveInputCount() {
  MOZ_ASSERT(mActiveInputCount > 0);
  --mActiveInputCount;
  CheckForInactive();
}

}  // namespace mozilla

Messung V0.5
C=89 H=90 G=89

¤ Dauer der Verarbeitung: 0.14 Sekunden  (vorverarbeitet)  ¤

*© Formatika GbR, Deutschland






Wurzel

Suchen

Beweissystem der NASA

Beweissystem Isabelle

NIST Cobol Testsuite

Cephes Mathematical Library

Wiener Entwicklungsmethode

Haftungshinweis

Die Informationen auf dieser Webseite wurden nach bestem Wissen sorgfältig zusammengestellt. Es wird jedoch weder Vollständigkeit, noch Richtigkeit, noch Qualität der bereit gestellten Informationen zugesichert.

Bemerkung:

Die farbliche Syntaxdarstellung und die Messung sind noch experimentell.