669 lines
17 KiB
C++
669 lines
17 KiB
C++
/*
|
|
* Olive Community Edition - Non-Linear Video Editor
|
|
* Copyright (C) 2025 Olive CE Team
|
|
*
|
|
* This program is free software: you can redistribute it and/or modify
|
|
* it under the terms of the GNU General Public License as published by
|
|
* the Free Software Foundation, either version 3 of the License, or
|
|
* (at your option) any later version.
|
|
*
|
|
* This program is distributed in the hope that it will be useful,
|
|
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
|
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
|
* GNU General Public License for more details.
|
|
*
|
|
* You should have received a copy of the GNU General Public License
|
|
* along with this program. If not, see <http://www.gnu.org/licenses/>.
|
|
*/
|
|
|
|
#include "viewer.h"
|
|
|
|
#include "config/config.h"
|
|
#include "core.h"
|
|
#include "node/traverser.h"
|
|
|
|
namespace olive
|
|
{
|
|
|
|
const QString ViewerOutput::kVideoParamsInput =
|
|
QStringLiteral("video_param_in");
|
|
const QString ViewerOutput::kAudioParamsInput =
|
|
QStringLiteral("audio_param_in");
|
|
const QString ViewerOutput::kSubtitleParamsInput =
|
|
QStringLiteral("subtitle_param_in");
|
|
const QString ViewerOutput::kTextureInput = QStringLiteral("tex_in");
|
|
const QString ViewerOutput::kSamplesInput = QStringLiteral("samples_in");
|
|
|
|
const SampleFormat ViewerOutput::kDefaultSampleFormat = SampleFormat::F32P;
|
|
|
|
#define super Node
|
|
|
|
ViewerOutput::ViewerOutput(bool create_buffer_inputs,
|
|
bool create_default_streams)
|
|
: last_length_(0)
|
|
, video_length_(0)
|
|
, audio_length_(0)
|
|
, autocache_input_video_(false)
|
|
, autocache_input_audio_(false)
|
|
, waveform_requests_enabled_(false)
|
|
{
|
|
AddInput(kVideoParamsInput, NodeValue::kVideoParams,
|
|
InputFlags(kInputFlagNotConnectable | kInputFlagNotKeyframable |
|
|
kInputFlagArray | kInputFlagHidden));
|
|
|
|
AddInput(kAudioParamsInput, NodeValue::kAudioParams,
|
|
InputFlags(kInputFlagNotConnectable | kInputFlagNotKeyframable |
|
|
kInputFlagArray | kInputFlagHidden));
|
|
|
|
AddInput(kSubtitleParamsInput, NodeValue::kSubtitleParams,
|
|
InputFlags(kInputFlagNotConnectable | kInputFlagNotKeyframable |
|
|
kInputFlagArray | kInputFlagHidden));
|
|
|
|
if (create_buffer_inputs) {
|
|
AddInput(kTextureInput, NodeValue::kTexture,
|
|
InputFlags(kInputFlagNotKeyframable));
|
|
AddInput(kSamplesInput, NodeValue::kSamples,
|
|
InputFlags(kInputFlagNotKeyframable));
|
|
}
|
|
|
|
if (create_default_streams) {
|
|
AddStream(Track::kVideo, QVariant());
|
|
AddStream(Track::kAudio, QVariant());
|
|
set_default_parameters();
|
|
}
|
|
|
|
SetFlag(kDontShowInParamView);
|
|
|
|
workarea_ = new TimelineWorkArea(this);
|
|
markers_ = new TimelineMarkerList(this);
|
|
}
|
|
|
|
QString ViewerOutput::Name() const
|
|
{
|
|
return tr("Viewer");
|
|
}
|
|
|
|
QString ViewerOutput::id() const
|
|
{
|
|
return QStringLiteral("org.olivevideoeditor.Olive.vieweroutput");
|
|
}
|
|
|
|
QVector<Node::CategoryID> ViewerOutput::Category() const
|
|
{
|
|
return { kCategoryOutput };
|
|
}
|
|
|
|
QString ViewerOutput::Description() const
|
|
{
|
|
return tr("Interface between a Viewer panel and the node system.");
|
|
}
|
|
|
|
QVariant ViewerOutput::data(const DataType &d) const
|
|
{
|
|
switch (d) {
|
|
case DURATION: {
|
|
rational using_timebase;
|
|
Timecode::Display using_display =
|
|
Core::instance()->GetTimecodeDisplay();
|
|
|
|
// Get first enabled streams
|
|
VideoParams video = GetFirstEnabledVideoStream();
|
|
AudioParams audio = GetFirstEnabledAudioStream();
|
|
SubtitleParams sub = GetFirstEnabledSubtitleStream();
|
|
|
|
if (video.is_valid() &&
|
|
video.video_type() != VideoParams::kVideoTypeStill) {
|
|
// Prioritize video
|
|
using_timebase = video.frame_rate_as_time_base();
|
|
} else if (audio.is_valid()) {
|
|
// Use audio as a backup
|
|
// If we're showing in a timecode, we prefer showing audio in seconds instead
|
|
if (using_display == Timecode::kTimecodeDropFrame ||
|
|
using_display == Timecode::kTimecodeNonDropFrame) {
|
|
using_display = Timecode::kTimecodeSeconds;
|
|
}
|
|
|
|
using_timebase = audio.sample_rate_as_time_base();
|
|
} else if (sub.is_valid()) {
|
|
using_timebase =
|
|
OLIVE_CONFIG("DefaultSequenceFrameRate").value<rational>();
|
|
}
|
|
|
|
if (!using_timebase.isNull()) {
|
|
// Return time transformed to timecode
|
|
return QString::fromStdString(Timecode::time_to_timecode(
|
|
GetLength(), using_timebase, using_display));
|
|
}
|
|
break;
|
|
}
|
|
case FREQUENCY_RATE: {
|
|
VideoParams video_stream;
|
|
|
|
if (HasEnabledVideoStreams() &&
|
|
(video_stream = GetFirstEnabledVideoStream()).video_type() !=
|
|
VideoParams::kVideoTypeStill) {
|
|
// This is a video editor, prioritize video streams
|
|
return tr("%1 FPS").arg(video_stream.frame_rate().toDouble());
|
|
} else if (HasEnabledAudioStreams()) {
|
|
// No video streams, return audio
|
|
AudioParams audio_stream = GetFirstEnabledAudioStream();
|
|
return tr("%1 Hz").arg(audio_stream.sample_rate());
|
|
}
|
|
break;
|
|
}
|
|
default:
|
|
break;
|
|
}
|
|
|
|
return super::data(d);
|
|
}
|
|
|
|
bool ViewerOutput::HasEnabledVideoStreams() const
|
|
{
|
|
return GetFirstEnabledVideoStream().is_valid();
|
|
}
|
|
|
|
bool ViewerOutput::HasEnabledAudioStreams() const
|
|
{
|
|
return GetFirstEnabledAudioStream().is_valid();
|
|
}
|
|
|
|
bool ViewerOutput::HasEnabledSubtitleStreams() const
|
|
{
|
|
return GetFirstEnabledSubtitleStream().is_valid();
|
|
}
|
|
|
|
VideoParams ViewerOutput::GetFirstEnabledVideoStream() const
|
|
{
|
|
int sz = GetVideoStreamCount();
|
|
|
|
for (int i = 0; i < sz; i++) {
|
|
VideoParams vp = GetVideoParams(i);
|
|
|
|
if (vp.enabled()) {
|
|
return vp;
|
|
}
|
|
}
|
|
|
|
return VideoParams();
|
|
}
|
|
|
|
AudioParams ViewerOutput::GetFirstEnabledAudioStream() const
|
|
{
|
|
int sz = GetAudioStreamCount();
|
|
|
|
for (int i = 0; i < sz; i++) {
|
|
AudioParams ap = GetAudioParams(i);
|
|
|
|
if (ap.enabled()) {
|
|
return ap;
|
|
}
|
|
}
|
|
|
|
return AudioParams();
|
|
}
|
|
|
|
SubtitleParams ViewerOutput::GetFirstEnabledSubtitleStream() const
|
|
{
|
|
int sz = GetSubtitleStreamCount();
|
|
|
|
for (int i = 0; i < sz; i++) {
|
|
SubtitleParams sp = GetSubtitleParams(i);
|
|
|
|
if (sp.enabled()) {
|
|
return sp;
|
|
}
|
|
}
|
|
|
|
return SubtitleParams();
|
|
}
|
|
|
|
void ViewerOutput::set_default_parameters()
|
|
{
|
|
int width = OLIVE_CONFIG("DefaultSequenceWidth").toInt();
|
|
int height = OLIVE_CONFIG("DefaultSequenceHeight").toInt();
|
|
|
|
SetVideoParams(VideoParams(
|
|
width, height,
|
|
OLIVE_CONFIG("DefaultSequenceFrameRate").value<rational>(),
|
|
static_cast<PixelFormat::Format>(
|
|
OLIVE_CONFIG("OfflinePixelFormat").toInt()),
|
|
VideoParams::kInternalChannelCount,
|
|
OLIVE_CONFIG("DefaultSequencePixelAspect").value<rational>(),
|
|
OLIVE_CONFIG("DefaultSequenceInterlacing")
|
|
.value<VideoParams::Interlacing>(),
|
|
VideoParams::generate_auto_divider(width, height)));
|
|
AVChannelLayout layout;
|
|
av_channel_layout_from_mask(
|
|
&layout, OLIVE_CONFIG("DefaultSequenceAudioLayout").toULongLong());
|
|
SetAudioParams(
|
|
AudioParams(OLIVE_CONFIG("DefaultSequenceAudioFrequency").toInt(),
|
|
layout, kDefaultSampleFormat));
|
|
}
|
|
|
|
void ViewerOutput::InvalidateCache(const TimeRange &range, const QString &from,
|
|
int element, InvalidateCacheOptions options)
|
|
{
|
|
Q_UNUSED(element)
|
|
|
|
if (Node *connected = GetConnectedOutput(from, element)) {
|
|
if (from == kTextureInput) {
|
|
//connected->thumbnail_cache()->Request(range.Intersected(max_range), PlaybackCache::kPreviewsOnly);
|
|
if (autocache_input_video_) {
|
|
TimeRange max_range = InputTimeAdjustment(
|
|
from, element, TimeRange(0, GetVideoLength()), false);
|
|
connected->video_frame_cache()->Request(
|
|
this, range.Intersected(max_range));
|
|
}
|
|
} else if (from == kSamplesInput) {
|
|
TimeRange max_range = InputTimeAdjustment(
|
|
from, element, TimeRange(0, GetAudioLength()), false);
|
|
if (waveform_requests_enabled_) {
|
|
connected->waveform_cache()->Request(
|
|
this, range.Intersected(max_range));
|
|
}
|
|
if (autocache_input_audio_) {
|
|
connected->audio_playback_cache()->Request(
|
|
this, range.Intersected(max_range));
|
|
}
|
|
}
|
|
}
|
|
|
|
VerifyLength();
|
|
|
|
super::InvalidateCache(range, from, element, options);
|
|
}
|
|
|
|
QVector<Track::Reference> ViewerOutput::GetEnabledStreamsAsReferences() const
|
|
{
|
|
QVector<Track::Reference> refs;
|
|
|
|
{
|
|
int vp_sz = GetVideoStreamCount();
|
|
|
|
for (int i = 0; i < vp_sz; i++) {
|
|
if (GetVideoParams(i).enabled()) {
|
|
refs.append(Track::Reference(Track::kVideo, i));
|
|
}
|
|
}
|
|
}
|
|
|
|
{
|
|
int ap_sz = GetAudioStreamCount();
|
|
|
|
for (int i = 0; i < ap_sz; i++) {
|
|
if (GetAudioParams(i).enabled()) {
|
|
refs.append(Track::Reference(Track::kAudio, i));
|
|
}
|
|
}
|
|
}
|
|
|
|
{
|
|
int sp_sz = GetSubtitleStreamCount();
|
|
|
|
for (int i = 0; i < sp_sz; i++) {
|
|
if (GetSubtitleParams(i).enabled()) {
|
|
refs.append(Track::Reference(Track::kSubtitle, i));
|
|
}
|
|
}
|
|
}
|
|
|
|
return refs;
|
|
}
|
|
|
|
void ViewerOutput::Retranslate()
|
|
{
|
|
super::Retranslate();
|
|
|
|
SetInputName(kVideoParamsInput, tr("Video Parameters"));
|
|
SetInputName(kAudioParamsInput, tr("Audio Parameters"));
|
|
SetInputName(kSubtitleParamsInput, tr("Subtitle Parameters"));
|
|
|
|
if (HasInputWithID(kTextureInput)) {
|
|
SetInputName(kTextureInput, tr("Texture"));
|
|
}
|
|
|
|
if (HasInputWithID(kSamplesInput)) {
|
|
SetInputName(kSamplesInput, tr("Samples"));
|
|
}
|
|
}
|
|
|
|
void ViewerOutput::VerifyLength()
|
|
{
|
|
video_length_ = VerifyLengthInternal(Track::kVideo);
|
|
|
|
audio_length_ = VerifyLengthInternal(Track::kAudio);
|
|
|
|
rational subtitle_length = VerifyLengthInternal(Track::kSubtitle);
|
|
|
|
rational real_length =
|
|
qMax(subtitle_length, qMax(video_length_, audio_length_));
|
|
|
|
if (real_length != last_length_) {
|
|
last_length_ = real_length;
|
|
emit LengthChanged(last_length_);
|
|
}
|
|
}
|
|
|
|
void ViewerOutput::SetPlayhead(const rational &t)
|
|
{
|
|
playhead_ = t;
|
|
emit PlayheadChanged(t);
|
|
}
|
|
|
|
void ViewerOutput::InputConnectedEvent(const QString &input, int element,
|
|
Node *output)
|
|
{
|
|
if (input == kTextureInput) {
|
|
emit TextureInputChanged();
|
|
} else if (input == kSamplesInput) {
|
|
connect(output->waveform_cache(), &AudioWaveformCache::Validated, this,
|
|
&ViewerOutput::ConnectedWaveformChanged);
|
|
}
|
|
|
|
super::InputConnectedEvent(input, element, output);
|
|
}
|
|
|
|
void ViewerOutput::InputDisconnectedEvent(const QString &input, int element,
|
|
Node *output)
|
|
{
|
|
if (input == kTextureInput) {
|
|
emit TextureInputChanged();
|
|
} else if (input == kSamplesInput) {
|
|
disconnect(output->waveform_cache(), &AudioWaveformCache::Validated,
|
|
this, &ViewerOutput::ConnectedWaveformChanged);
|
|
}
|
|
|
|
super::InputDisconnectedEvent(input, element, output);
|
|
}
|
|
|
|
rational ViewerOutput::VerifyLengthInternal(Track::Type type) const
|
|
{
|
|
NodeTraverser traverser;
|
|
|
|
switch (type) {
|
|
case Track::kVideo:
|
|
if (IsInputConnected(kTextureInput)) {
|
|
NodeValueTable t = traverser.GenerateTable(
|
|
GetConnectedOutput(kTextureInput), TimeRange(0, 0));
|
|
rational r = t.Get(NodeValue::kRational, QStringLiteral("length"))
|
|
.toRational();
|
|
if (!r.isNaN()) {
|
|
return r;
|
|
}
|
|
}
|
|
break;
|
|
case Track::kAudio:
|
|
if (IsInputConnected(kSamplesInput)) {
|
|
NodeValueTable t = traverser.GenerateTable(
|
|
GetConnectedOutput(kSamplesInput), TimeRange(0, 0));
|
|
rational r = t.Get(NodeValue::kRational, QStringLiteral("length"))
|
|
.toRational();
|
|
if (!r.isNaN()) {
|
|
return r;
|
|
}
|
|
}
|
|
break;
|
|
case Track::kNone:
|
|
case Track::kSubtitle:
|
|
case Track::kCount:
|
|
break;
|
|
}
|
|
|
|
return 0;
|
|
}
|
|
|
|
Node *ViewerOutput::GetConnectedTextureOutput()
|
|
{
|
|
return GetConnectedOutput(kTextureInput);
|
|
}
|
|
|
|
Node::ValueHint ViewerOutput::GetConnectedTextureValueHint()
|
|
{
|
|
return GetValueHintForInput(kTextureInput);
|
|
}
|
|
|
|
Node *ViewerOutput::GetConnectedSampleOutput()
|
|
{
|
|
return GetConnectedOutput(kSamplesInput);
|
|
}
|
|
|
|
Node::ValueHint ViewerOutput::GetConnectedSampleValueHint()
|
|
{
|
|
return GetValueHintForInput(kSamplesInput);
|
|
}
|
|
|
|
void ViewerOutput::SetWaveformEnabled(bool e)
|
|
{
|
|
if ((waveform_requests_enabled_ = e)) {
|
|
if (Node *connected = this->GetConnectedSampleOutput()) {
|
|
TimeRange max_range = InputTimeAdjustment(
|
|
kSamplesInput, -1, TimeRange(0, GetAudioLength()), false);
|
|
TimeRangeList invalid =
|
|
connected->waveform_cache()->GetInvalidatedRanges(max_range);
|
|
for (const TimeRange &r : invalid) {
|
|
connected->waveform_cache()->Request(this, r);
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
void ViewerOutput::Value(const NodeValueRow &value, const NodeGlobals &globals,
|
|
NodeValueTable *table) const
|
|
{
|
|
if (HasInputWithID(kTextureInput)) {
|
|
NodeValue repush = value[kTextureInput];
|
|
repush.set_tag(Track::Reference(Track::kVideo, 0).ToString());
|
|
table->Push(repush);
|
|
}
|
|
if (HasInputWithID(kSamplesInput)) {
|
|
NodeValue repush = value[kSamplesInput];
|
|
repush.set_tag(Track::Reference(Track::kAudio, 0).ToString());
|
|
table->Push(value[kSamplesInput]);
|
|
}
|
|
}
|
|
|
|
bool ViewerOutput::LoadCustom(QXmlStreamReader *reader, SerializedData *data)
|
|
{
|
|
while (XMLReadNextStartElement(reader)) {
|
|
if (reader->name() == QStringLiteral("markers")) {
|
|
if (!this->GetMarkers()->load(reader)) {
|
|
return false;
|
|
}
|
|
} else if (reader->name() == QStringLiteral("workarea")) {
|
|
if (!this->GetWorkArea()->load(reader)) {
|
|
return false;
|
|
}
|
|
} else {
|
|
reader->skipCurrentElement();
|
|
}
|
|
}
|
|
|
|
return true;
|
|
}
|
|
|
|
void ViewerOutput::SaveCustom(QXmlStreamWriter *writer) const
|
|
{
|
|
writer->writeStartElement(QStringLiteral("workarea"));
|
|
this->GetWorkArea()->save(writer);
|
|
writer->writeEndElement(); // workarea
|
|
|
|
writer->writeStartElement(QStringLiteral("markers"));
|
|
this->GetMarkers()->save(writer);
|
|
writer->writeEndElement(); // markers
|
|
}
|
|
|
|
void ViewerOutput::InputValueChangedEvent(const QString &input, int element)
|
|
{
|
|
if (element == 0) {
|
|
if (input == kVideoParamsInput) {
|
|
VideoParams new_video_params = GetVideoParams();
|
|
|
|
bool size_changed =
|
|
cached_video_params_.width() != new_video_params.width() ||
|
|
cached_video_params_.height() != new_video_params.height();
|
|
bool frame_rate_changed = cached_video_params_.frame_rate() !=
|
|
new_video_params.frame_rate();
|
|
bool pixel_aspect_changed =
|
|
cached_video_params_.pixel_aspect_ratio() !=
|
|
new_video_params.pixel_aspect_ratio();
|
|
bool interlacing_changed = cached_video_params_.interlacing() !=
|
|
new_video_params.interlacing();
|
|
|
|
if (size_changed) {
|
|
emit SizeChanged(new_video_params.width(),
|
|
new_video_params.height());
|
|
}
|
|
|
|
if (pixel_aspect_changed) {
|
|
emit PixelAspectChanged(new_video_params.pixel_aspect_ratio());
|
|
}
|
|
|
|
if (interlacing_changed) {
|
|
emit InterlacingChanged(new_video_params.interlacing());
|
|
}
|
|
|
|
if (frame_rate_changed) {
|
|
emit FrameRateChanged(new_video_params.frame_rate());
|
|
}
|
|
|
|
emit VideoParamsChanged();
|
|
|
|
cached_video_params_ = new_video_params;
|
|
|
|
} else if (input == kAudioParamsInput) {
|
|
AudioParams new_audio_params = GetAudioParams();
|
|
|
|
bool sample_rate_changed = new_audio_params.sample_rate() !=
|
|
cached_audio_params_.sample_rate();
|
|
|
|
if (sample_rate_changed) {
|
|
emit SampleRateChanged(new_audio_params.sample_rate());
|
|
}
|
|
|
|
emit AudioParamsChanged();
|
|
|
|
cached_audio_params_ = new_audio_params;
|
|
}
|
|
}
|
|
|
|
super::InputValueChangedEvent(input, element);
|
|
}
|
|
|
|
void ViewerOutput::set_parameters_from_footage(
|
|
const QVector<ViewerOutput *> footage)
|
|
{
|
|
foreach (ViewerOutput *f, footage) {
|
|
QVector<VideoParams> video_streams = f->GetEnabledVideoStreams();
|
|
QVector<AudioParams> audio_streams = f->GetEnabledAudioStreams();
|
|
|
|
for (int i = 0; i < video_streams.size(); i++) {
|
|
const VideoParams &s = video_streams.at(i);
|
|
|
|
bool found_video_params = false;
|
|
rational using_timebase;
|
|
|
|
if (s.video_type() == VideoParams::kVideoTypeStill) {
|
|
// If this is a still image, we'll use it's resolution but won't set
|
|
// `found_video_params` in case something with a frame rate comes along which we'll
|
|
// prioritize
|
|
if (i > 0) {
|
|
// Ignore still images past stream 0
|
|
continue;
|
|
}
|
|
|
|
using_timebase = GetVideoParams().time_base();
|
|
} else {
|
|
using_timebase = s.frame_rate_as_time_base();
|
|
found_video_params = true;
|
|
}
|
|
|
|
SetVideoParams(VideoParams(
|
|
s.width(), s.height(), using_timebase,
|
|
static_cast<PixelFormat::Format>(
|
|
OLIVE_CONFIG("OfflinePixelFormat").toInt()),
|
|
VideoParams::kInternalChannelCount, s.pixel_aspect_ratio(),
|
|
s.interlacing(),
|
|
VideoParams::generate_auto_divider(s.width(), s.height())));
|
|
|
|
if (found_video_params) {
|
|
break;
|
|
}
|
|
}
|
|
|
|
if (!audio_streams.isEmpty()) {
|
|
const AudioParams &s = audio_streams.first();
|
|
SetAudioParams(AudioParams(s.sample_rate(), s.channel_layout(),
|
|
kDefaultSampleFormat));
|
|
}
|
|
}
|
|
}
|
|
|
|
int ViewerOutput::AddStream(Track::Type type, const QVariant &value)
|
|
{
|
|
return SetStream(type, value, -1);
|
|
}
|
|
|
|
int ViewerOutput::SetStream(Track::Type type, const QVariant &value,
|
|
int index_in)
|
|
{
|
|
QString id;
|
|
|
|
if (type == Track::kVideo) {
|
|
id = kVideoParamsInput;
|
|
} else if (type == Track::kAudio) {
|
|
id = kAudioParamsInput;
|
|
} else if (type == Track::kSubtitle) {
|
|
id = kSubtitleParamsInput;
|
|
} else {
|
|
return -1;
|
|
}
|
|
|
|
// Add another video/audio param to the array for this stream
|
|
int index = (index_in == -1) ? InputArraySize(id) : index_in;
|
|
|
|
if (index >= InputArraySize(id)) {
|
|
InputArrayResize(id, index + 1);
|
|
}
|
|
|
|
SetStandardValue(id, value, index);
|
|
|
|
return index;
|
|
}
|
|
|
|
QVector<VideoParams> ViewerOutput::GetEnabledVideoStreams() const
|
|
{
|
|
QVector<VideoParams> streams;
|
|
|
|
int vp_sz = GetVideoStreamCount();
|
|
|
|
for (int i = 0; i < vp_sz; i++) {
|
|
VideoParams vp = GetVideoParams(i);
|
|
|
|
if (vp.enabled()) {
|
|
streams.append(vp);
|
|
}
|
|
}
|
|
|
|
return streams;
|
|
}
|
|
|
|
QVector<AudioParams> ViewerOutput::GetEnabledAudioStreams() const
|
|
{
|
|
QVector<AudioParams> streams;
|
|
|
|
int ap_sz = GetAudioStreamCount();
|
|
|
|
for (int i = 0; i < ap_sz; i++) {
|
|
AudioParams ap = GetAudioParams(i);
|
|
|
|
if (ap.enabled()) {
|
|
streams.append(ap);
|
|
}
|
|
}
|
|
|
|
return streams;
|
|
}
|
|
|
|
}
|