Audacity 3.2.0
ShareAudioDialog.cpp
Go to the documentation of this file.
1/* SPDX-License-Identifier: GPL-2.0-or-later */
2/*!********************************************************************
3
4 Audacity: A Digital Audio Editor
5
6 ShareAudioDialog.cpp
7
8 Dmitry Vedenko
9
10**********************************************************************/
11#include "ShareAudioDialog.h"
12
13#include <cassert>
14#include <rapidjson/document.h>
15
16#include <wx/bmpbuttn.h>
17#include <wx/button.h>
18#include <wx/clipbrd.h>
19#include <wx/gauge.h>
20#include <wx/frame.h>
21#include <wx/stattext.h>
22#include <wx/statline.h>
23#include <wx/textctrl.h>
24#include <wx/radiobut.h>
25
26#include "AllThemeResources.h"
27#include "BasicUI.h"
28#include "MemoryX.h"
29#include "Project.h"
30#include "ShuttleGui.h"
31#include "Theme.h"
32#include "Track.h"
33#include "WaveTrack.h"
34
35#include "ServiceConfig.h"
36#include "OAuthService.h"
37#include "UploadService.h"
38#include "UserService.h"
39
41#include "../UserPanel.h"
42
43#include "CodeConversions.h"
44
45#include "Export.h"
46#include "ExportProgressUI.h"
47#include "ExportUtils.h"
50
51#include "WindowAccessible.h"
52#include "HelpSystem.h"
53#include "ProjectRate.h"
54#include "ProjectWindows.h"
55
56#include "CloudLocationDialog.h"
57#include "ExportUtils.h"
58
60{
61namespace
62{
64{
65 const auto tempPath = GetUploadTempPath();
66
67 wxFileName fileName(
68 tempPath,
69 wxString::Format(
70 "%lld", std::chrono::system_clock::now().time_since_epoch().count()),
71 extension);
72
73 fileName.Mkdir(0700, wxPATH_MKDIR_FULL);
74
75 if (fileName.Exists())
76 {
77 if (!wxRemoveFile(fileName.GetFullPath()))
78 return {};
79 }
80
81 return fileName.GetFullPath();
82}
83
84const auto publicLabelText = XO("Public");
86 XO("Anyone will be able to listen to this audio.");
87
88const auto unlistedLabelText = XO("Unlisted");
90 "Only you and people you share a link with will be able to listen to this audio.");
91
92}
93
94// A helper structures holds UploadService and UploadPromise
96{
98
100
103 {
104 }
105};
106
108{
109public:
111 : mParent(parent)
112 {
113
114 }
115
117
118 void Cancel()
119 {
120 mCancelled.store(true, std::memory_order_release);
121 }
122
124 {
125 return mResult;
126 }
127
129 {
130 mResult = result;
131 }
132
134 {
135 }
136
137 bool IsCancelled() const override
138 {
139 return mCancelled.load(std::memory_order_acquire);
140 }
141
142 bool IsStopped() const override
143 {
144 return false;
145 }
146
147 void OnProgress(double value) override
148 {
149 mProgress.store(value, std::memory_order_release);
150 }
151
152 void UpdateUI()
153 {
154 constexpr auto ProgressSteps = 1000ull;
155
156 mParent.UpdateProgress(mProgress.load(std::memory_order_acquire) * ProgressSteps, ProgressSteps);
157 }
158
159private:
160
162
163 std::atomic<bool> mCancelled{false};
164 std::atomic<double> mProgress;
166};
167
169 AudacityProject& project, AudiocomTrace trace, wxWindow* parent)
171 parent, wxID_ANY, XO("Share Audio"), wxDefaultPosition, { 480, 250 },
172 wxDEFAULT_DIALOG_STYLE)
173 , mProject(project)
174 , mInitialStatePanel(*this)
175 , mServices(std::make_unique<Services>())
176 , mAudiocomTrace(trace)
177{
179
180 ShuttleGui s(this, eIsCreating);
181
182 s.StartVerticalLay();
183 {
184 Populate(s);
185 }
186 s.EndVerticalLay();
187
188 Layout();
189 Fit();
190 Centre();
191
192 const auto size = GetSize();
193
194 SetMinSize({ size.x, std::min(250, size.y) });
195 SetMaxSize({ size.x, -1 });
196
197 mContinueAction = [this]() {
198 if (mInitialStatePanel.root->IsShown())
199 StartUploadProcess();
200 };
201
202 Bind(
203 wxEVT_CHAR_HOOK,
204 [this](auto& evt)
205 {
206 if (!IsEscapeKey(evt))
207 {
208 evt.Skip();
209 return;
210 }
211
212 OnCancel();
213 });
214}
215
217{
219 // Clean up the temp file when the dialog is closed
220 if (!mFilePath.empty() && wxFileExists(mFilePath))
221 wxRemoveFile(mFilePath);
222}
223
225{
228
229 s.StartHorizontalLay(wxEXPAND, 0);
230 {
232 {
233 s.SetBorder(2);
234 s.StartHorizontalLay(wxEXPAND, 0);
235 {
236 s.AddSpace(0, 0, 1);
237
238 mCancelButton = s.AddButton(XXO("&Cancel"));
239 mCancelButton->Bind(wxEVT_BUTTON, [this](auto) { OnCancel(); });
240
241 s.AddSpace(4, 0, 0);
242
243 mContinueButton = s.AddButton(XXO("C&ontinue"));
244 mContinueButton->Bind(wxEVT_BUTTON, [this](auto) { OnContinue(); });
245 }
247 }
249 }
251
252 const auto title = mProject.GetProjectName();
253
254 if (!title.empty())
255 {
257 mInitialStatePanel.trackTitle->SetInsertionPoint(title.length());
258 }
259
261
263 wxEVT_TEXT,
264 [this](auto&) {
265 mContinueButton->Enable(
267 });
268}
269
271{
272 if (mInProgress)
273 {
274 AudacityMessageDialog dlgMessage(
275 this, XO("Are you sure you want to cancel?"), XO("Cancel upload to Audio.com"),
276 wxYES_NO | wxICON_QUESTION | wxNO_DEFAULT | wxSTAY_ON_TOP);
277
278 const auto result = dlgMessage.ShowModal();
279
280 if (result != wxID_YES)
281 return;
282
283 // If export has started, notify it that it should be canceled
285 mExportProgressUpdater->Cancel();
286 }
287
288
289 // If upload was started - ask it to discard the result.
290 // The result should be discarded even after the upload has finished
291 if (mServices->uploadPromise)
292 mServices->uploadPromise->DiscardResult();
293
294 EndModal(wxID_CANCEL);
295}
296
298{
300}
301
302namespace
303{
304int CalculateChannels(const TrackList& trackList)
305{
306 auto range = trackList.Any<const WaveTrack>();
307 return std::all_of(range.begin(), range.end(), [](const WaveTrack *track){
308 return IsMono(*track) && track->GetPan() == 0;
309 }) ? 1 : 2;
310}
311}
312
314{
316
317 const double t0 = 0.0;
318 const double t1 = tracks.GetEndTime();
319
320 const int nChannels = CalculateChannels(tracks);
321
322 auto hasMimeType = [](const auto&& mimeTypes, const std::string& mimeType)
323 {
324 return std::find(mimeTypes.begin(), mimeTypes.end(), mimeType) != mimeTypes.end();
325 };
326
327 const auto& registry = ExportPluginRegistry::Get();
328
329 for(const auto& preferredMimeType : GetServiceConfig().GetPreferredAudioFormats())
330 {
331 auto config = GetServiceConfig().GetExportConfig(preferredMimeType);
333 auto pluginIt = std::find_if(registry.begin(), registry.end(), [&](auto t)
334 {
335 auto [plugin, formatIndex] = t;
336 parameters.clear();
337 return hasMimeType(plugin->GetMimeTypes(formatIndex), preferredMimeType) &&
338 plugin->ParseConfig(formatIndex, config, parameters);
339 });
340
341 if(pluginIt == registry.end())
342 continue;
343
344 const auto [plugin, formatIndex] = *pluginIt;
345
346 const auto formatInfo = plugin->GetFormatInfo(formatIndex);
347 const auto path = GenerateTempPath(formatInfo.extensions[0]);
348
349 if(path.empty())
350 continue;
351
352 mExportProgressUpdater = std::make_unique<ExportProgressUpdater>(*this);
353
354 auto builder = ExportTaskBuilder{}
355 .SetParameters(parameters)
356 .SetNumChannels(nChannels)
358 .SetPlugin(plugin)
359 .SetFileName(path)
360 .SetRange(t0, t1, false);
361
362 auto result = ExportResult::Error;
364 {
365 auto exportTask = builder.Build(mProject);
366
367 auto f = exportTask.get_future();
368 std::thread(std::move(exportTask), std::ref(*mExportProgressUpdater)).detach();
369
371 {
372 while(f.wait_for(std::chrono::milliseconds(50)) != std::future_status::ready)
373 mExportProgressUpdater->UpdateUI();
374 result = f.get();
375 });
376 });
377
378 mExportProgressUpdater->SetResult(result);
379 const auto success = result == ExportResult::Success;
380 if(!success && wxFileExists(path))
381 wxRemoveFile(path);
382 if(success)
383 return path;
384 }
385 return {};
386}
387
389{
390 mInProgress = true;
391
392 mInitialStatePanel.root->Hide();
393 mProgressPanel.root->Show();
394
395 mProgressPanel.info->Hide();
396
397 mContinueButton->Hide();
398
399 Layout();
400 Fit();
401
403
405
406 if(mFilePath.empty())
407 {
410 {
412 }
413
414 return;
415 }
416
417 mProgressPanel.title->SetLabel(XO("Uploading audio...").Translation());
419
420 mServices->uploadPromise = mServices->uploadService.Upload(
422 [this](const auto& result) {
423 CallAfter(
424 [this, result]()
425 {
426 mInProgress = false;
427
428 if (result.result == UploadOperationCompleted::Result::Success)
429 {
430 // Success indicates that UploadSuccessfulPayload is in the payload
431 assert(std::holds_alternative<UploadSuccessfulPayload>(result.payload));
432
433 if (
434 auto payload =
435 std::get_if<UploadSuccessfulPayload>(&result.payload))
436 HandleUploadSucceeded(*payload);
437 else
438 HandleUploadSucceeded({});
439
440 }
441 else if (
442 result.result != UploadOperationCompleted::Result::Aborted)
443 {
444 if (
445 auto payload =
446 std::get_if<UploadFailedPayload>(&result.payload))
447 HandleUploadFailed(*payload);
448 else
449 HandleUploadFailed({});
450 }
451 });
452 },
453 [this](auto current, auto total) {
454 CallAfter(
455 [this, current, total]()
456 {
457 UpdateProgress(current, total);
458 });
459 },
460 mAudiocomTrace);
461}
462
463void ShareAudioDialog::HandleUploadSucceeded(
464 const UploadSuccessfulPayload& payload)
465{
466 EndModal(wxID_CLOSE);
467 OpenInDefaultBrowser(wxString { payload.audioUrl });
468}
469
470void ShareAudioDialog::HandleUploadFailed(const UploadFailedPayload& payload)
471{
472 EndModal(wxID_ABORT);
473
474 TranslatableString message;
475
476 if (!payload.message.empty())
477 {
478 auto details = payload.message;
479
480 for (auto& err : payload.additionalErrors)
481 details += " " + err.second;
482
483 message = XO("Error: %s").Format(details);
484 }
485 else
486 {
487 message = XO(
488 "We are unable to upload this file. Please try again and make sure to link to your audio.com account before uploading.");
489 }
490
492 {}, XO("Upload error"),
493 message,
494 {},
496
497}
498
499void ShareAudioDialog::HandleExportFailure()
500{
501 EndModal(wxID_ABORT);
502
504 {}, XO("Export error"),
505 XO("We are unable to prepare this file for uploading."), {},
507}
508
509void ShareAudioDialog::ResetProgress()
510{
511 mStageStartTime = Clock::now();
512 mLastUIUpdateTime = mStageStartTime;
513
514 mProgressPanel.elapsedTime->SetLabel(" 00:00:00");
515 mProgressPanel.remainingTime->SetLabel(" 00:00:00");
516 mProgressPanel.progress->SetValue(0);
517
518 mLastProgressValue = 0;
519
520 mExportProgressUpdater.reset();
521
523}
524
525namespace
526{
527void SetTimeLabel(wxStaticText* label, std::chrono::milliseconds time)
528{
529 wxTimeSpan tsElapsed(0, 0, 0, time.count());
530
531 label->SetLabel(tsElapsed.Format(wxT(" %H:%M:%S")));
532 label->SetName(label->GetLabel());
533 label->Update();
534}
535}
536
537void ShareAudioDialog::UpdateProgress(uint64_t current, uint64_t total)
538{
539 using namespace std::chrono;
540
541 const auto now = Clock::now();
542
543 if (current == 0)
544 return;
545
546 if (current > total)
547 current = total;
548
549 if (mLastProgressValue != current)
550 {
551 constexpr int scale = 10000;
552
553 mLastProgressValue = static_cast<int>(current);
554
555 mProgressPanel.progress->SetRange(scale);
556 mProgressPanel.progress->SetValue((current * scale) / total);
557
558 if (current == total && mServices->uploadPromise)
559 {
560 mProgressPanel.timePanel->Hide();
561 mProgressPanel.title->SetLabel(XO("Finalizing upload...").Translation());
562 }
563 }
564
565 const auto elapsedSinceUIUpdate = now - mLastUIUpdateTime;
566
567 constexpr auto uiUpdateTimeout = 500ms;
568
569 if (elapsedSinceUIUpdate < uiUpdateTimeout && current < total)
570 return;
571
572 mLastUIUpdateTime = now;
573
574 const auto elapsed = duration_cast<milliseconds>(now - mStageStartTime);
575
576 SetTimeLabel(mProgressPanel.elapsedTime, elapsed);
577
578 const auto estimate = elapsed * total / current;
579 const auto remains = estimate - elapsed;
580
582 mProgressPanel.remainingTime,
583 std::chrono::duration_cast<std::chrono::milliseconds>(remains));
584}
585
586ShareAudioDialog::InitialStatePanel::InitialStatePanel(ShareAudioDialog& parent)
587 : parent { parent }
588{
589}
590
592 ShuttleGui& s)
593{
594 root = s.StartInvisiblePanel();
595 s.StartVerticalLay(wxEXPAND, 1);
596 {
597 s.SetBorder(16);
598
601 parent.mAudiocomTrace, s.GetParent() };
602
603 mUserDataChangedSubscription = userPanel->Subscribe(
604 [this](auto message) { UpdateUserData(message.IsAuthorized); });
605
606 s.Prop(0).AddWindow(userPanel, wxEXPAND);
607
608 s.SetBorder(0);
609
610 s.AddWindow(safenew wxStaticLine { s.GetParent() }, wxEXPAND);
611
613 {
615 {
616 s.AddFixedText(XO("Track Title"));
617 s.AddSpace(8);
618 trackTitle = s.AddTextBox({}, {}, 60);
619 trackTitle->SetName(XO("Track Title").Translation());
620 trackTitle->SetFocus();
621 trackTitle->SetMaxLength(100);
622 s.AddSpace(16);
623
624 anonInfoPanel = s.StartInvisiblePanel();
625 {
626 AccessibleLinksFormatter privacyPolicy(XO(
627 /*i18n-hint: %s substitutes for audio.com. %% creates a linebreak in this context. */
628 "Sharing audio requires a free %s account linked to Audacity. %%Press \"Link account\" above to proceed."));
629
630 privacyPolicy.FormatLink(
631 L"%s", XO("audio.com"), "https://audio.com");
632
633 privacyPolicy.FormatLink(
634 L"%%", TranslatableString {},
636
637 privacyPolicy.Populate(s);
638 }
640
641 authorizedInfoPanel = s.StartInvisiblePanel();
642 s.StartHorizontalLay(wxEXPAND, 1);
643 {
644 s.AddFixedText(XO("Press \"Continue\" to upload to audio.com"));
645 }
648 }
650 }
652 }
653 s.EndVerticalLay();
655
656 UpdateUserData(
657 GetOAuthService().HasRefreshToken() &&
658 !GetUserService().GetUserSlug().empty());
659}
660
662{
663 parent.mIsAuthorised = authorized;
664
665 anonInfoPanel->Show(!authorized);
666 authorizedInfoPanel->Show(authorized);
667
668 if (parent.mContinueButton != nullptr)
669 parent.mContinueButton->Enable(authorized && !GetTrackTitle().empty());
670
671 root->GetParent()->Layout();
672}
673
675{
676 wxString ret { trackTitle->GetValue() };
677 ret.Trim(true).Trim(false);
678 return ret;
679}
680
682{
683 return !GetTrackTitle().empty();
684}
685
687{
688 root = s.StartInvisiblePanel(16);
689 root->Hide();
690 s.StartVerticalLay(wxEXPAND, 1);
691 {
692 s.SetBorder(0);
693
694 title = s.AddVariableText(XO("Preparing audio..."));
695 s.AddSpace(0, 16, 0);
696
697 progress = safenew wxGauge { s.GetParent(), wxID_ANY, 100 };
698 s.AddWindow(progress, wxEXPAND);
699
700 timePanel = s.StartInvisiblePanel();
701 {
702 s.AddSpace(0, 16, 0);
703
704 s.StartWrapLay();
705 {
706 s.AddFixedText(XO("Elapsed Time:"));
707 elapsedTime = s.AddVariableText(Verbatim(" 00:00:00"));
708 }
709 s.EndWrapLay();
710
711 s.StartWrapLay();
712 {
713 s.AddFixedText(XO("Remaining Time:"));
714 remainingTime = s.AddVariableText(Verbatim(" 00:00:00"));
715 }
716 s.EndWrapLay();
717 }
719
720 s.AddSpace(0, 16, 0);
721
723 }
724
725 s.EndVerticalLay();
727
728 wxFont font = elapsedTime->GetFont();
729 font.MakeBold();
730
731 elapsedTime->SetFont(font);
732 remainingTime->SetFont(font);
733}
734
735namespace
736{
737auto hooked = [] {
740 bool selectedOnly) {
741 if(selectedOnly)
743
744 const auto window = &GetProjectFrame(project);
745
746 sync::CloudLocationDialog locationDialog {
748 };
749
750 const auto result = locationDialog.ShowDialog();
751
754
757
758 ShareAudioDialog shareDialog { project, trace, window };
759 shareDialog.ShowModal();
760
762 },
763 1000);
764 return true;
765}();
766} // namespace
767} // namespace audacity::cloud::audiocom
wxT("CloseDown"))
Toolkit-neutral facade for basic user interface services.
Declare functions to perform UTF-8 to std::wstring conversions.
int min(int a, int b)
#define str(a)
ExportResult
Definition: ExportTypes.h:24
AudiocomTrace
Definition: ExportUtils.h:27
XO("Cut/Copy/Paste")
XXO("&Cut/Copy/Paste Toolbar")
wxString FileExtension
File extension, not including any leading dot.
Definition: Identifier.h:224
#define safenew
Definition: MemoryX.h:10
static const auto title
an object holding per-project preferred sample rate
AUDACITY_DLL_API wxFrame & GetProjectFrame(AudacityProject &project)
Get the top-level window associated with the project (as a wxFrame only, when you do not need to use ...
accessors for certain important windows associated with each project
@ eIsCreating
Definition: ShuttleGui.h:37
TranslatableString label
Definition: TagsEditor.cpp:165
const auto tracks
const auto project
declares abstract base class Track, TrackList, and iterators over TrackList
TranslatableString Verbatim(wxString str)
Require calls to the one-argument constructor to go through this distinct global function name.
Wrap wxMessageDialog so that caption IS translatable.
The top-level handle to an Audacity project. It serves as a source of events that other objects can b...
Definition: Project.h:90
const wxString & GetProjectName() const
Definition: Project.cpp:100
static ExportPluginRegistry & Get()
std::vector< std::tuple< ExportOptionID, ExportValue > > Parameters
Definition: ExportPlugin.h:93
ExportTaskBuilder & SetPlugin(const ExportPlugin *plugin, int format=0) noexcept
Definition: Export.cpp:59
ExportTaskBuilder & SetParameters(ExportProcessor::Parameters parameters) noexcept
Definition: Export.cpp:47
ExportTaskBuilder & SetNumChannels(unsigned numChannels) noexcept
Definition: Export.cpp:53
ExportTaskBuilder & SetSampleRate(double sampleRate) noexcept
Definition: Export.cpp:72
ExportTaskBuilder & SetFileName(const wxFileName &filename)
Definition: Export.cpp:33
ExportTaskBuilder & SetRange(double t0, double t1, bool selectedOnly=false) noexcept
Definition: Export.cpp:39
static void RegisterExportHook(ExportHook hook, Priority=DEFAULT_EXPORT_HOOK_PRIORITY)
Definition: ExportUtils.cpp:67
static ProjectRate & Get(AudacityProject &project)
Definition: ProjectRate.cpp:28
void SetBorder(int Border)
Definition: ShuttleGui.h:495
void EndVerticalLay()
void EndInvisiblePanel()
wxPanel * StartInvisiblePanel(int border=0)
wxWindow * GetParent()
Definition: ShuttleGui.h:502
wxTextCtrl * AddTextBox(const TranslatableString &Caption, const wxString &Value, const int nChars)
Definition: ShuttleGui.cpp:659
void StartVerticalLay(int iProp=1)
wxButton * AddButton(const TranslatableString &Text, int PositionFlags=wxALIGN_CENTRE, bool setDefault=false)
Definition: ShuttleGui.cpp:362
void EndHorizontalLay()
void StartWrapLay(int PositionFlags=wxEXPAND, int iProp=0)
void StartHorizontalLay(int PositionFlags=wxALIGN_CENTRE, int iProp=1)
wxWindow * AddWindow(wxWindow *pWindow, int PositionFlags=wxALIGN_CENTRE)
Definition: ShuttleGui.cpp:301
void AddFixedText(const TranslatableString &Str, bool bCenter=false, int wrapWidth=0)
Definition: ShuttleGui.cpp:442
wxStaticText * AddVariableText(const TranslatableString &Str, bool bCenter=false, int PositionFlags=0, int wrapWidth=0)
Definition: ShuttleGui.cpp:465
Derived from ShuttleGuiBase, an Audacity specific class for shuttling data to and from GUI.
Definition: ShuttleGui.h:640
wxSizerItem * AddSpace(int width, int height, int prop=0)
ShuttleGui & Prop(int iProp)
Definition: ShuttleGui.h:733
A flat linked list of tracks supporting Add, Remove, Clear, and Contains, serialization of the list o...
Definition: Track.h:850
auto Any() -> TrackIterRange< TrackType >
Definition: Track.h:950
static TrackList & Get(AudacityProject &project)
Definition: Track.cpp:314
Holds a msgid for the translation catalog; may also bind format arguments.
A Track that contains audio waveform data.
Definition: WaveTrack.h:203
rapidjson::Document GetExportConfig(const std::string &exporterName) const
Export configuration suitable for the mime type provided.
void SetStatusString(const TranslatableString &str) override
ShareAudioDialog(AudacityProject &project, AudiocomTrace, wxWindow *parent=nullptr)
void UpdateProgress(uint64_t current, uint64_t total)
struct audacity::cloud::audiocom::ShareAudioDialog::InitialStatePanel mInitialStatePanel
struct audacity::cloud::audiocom::ShareAudioDialog::ProgressPanel mProgressPanel
std::unique_ptr< ExportProgressUpdater > mExportProgressUpdater
A unique_ptr like class that holds a pointer to UploadOperation.
Service, responsible for uploading audio files to audio.com.
bool OpenInDefaultBrowser(const wxString &url)
Open an URL in default browser.
Definition: BasicUI.cpp:246
void CallAfter(Action action)
Schedule an action to be done later, and in the main thread.
Definition: BasicUI.cpp:214
void ShowErrorDialog(const WindowPlacement &placement, const TranslatableString &dlogTitle, const TranslatableString &message, const ManualPageID &helpPage, const ErrorDialogOptions &options={})
Show an error dialog with a link to the manual for further help.
Definition: BasicUI.h:272
void Yield()
Dispatch waiting events, including actions enqueued by CallAfter.
Definition: BasicUI.cpp:225
void ExceptionWrappedCall(Callable callable)
double GetRate(const Track &track)
Definition: TimeTrack.cpp:182
void SetTimeLabel(wxStaticText *label, std::chrono::milliseconds time)
AuthorizationHandler & GetAuthorizationHandler()
UserService & GetUserService()
OAuthService & GetOAuthService()
Returns the instance of the OAuthService.
const ServiceConfig & GetServiceConfig()
Returns the instance of the ServiceConfig.
Options for variations of error dialogs; the default is for modal dialogs.
Definition: BasicUI.h:52
This structure represents an upload error as returned by the server.
Definition: UploadService.h:31
std::vector< AdditionalError > additionalErrors
Definition: UploadService.h:39
This structure represents the payload associated with successful upload.
Definition: UploadService.h:44
std::string audioUrl
URL to the uploaded audio.
Definition: UploadService.h:52