mirror of
https://github.com/mudita/MuditaOS.git
synced 2026-01-16 10:00:03 -05:00
* Fix of the issue that 'File has been deleted' popup would show in Relaxation app at the end of playback if the playback was paused at least once, even though the file wasn't actually deleted. * Added very basic audio decoder error handling and propagation mechanism. * Minor refactor around several audio-related parts.
233 lines
7.6 KiB
C++
233 lines
7.6 KiB
C++
// Copyright (c) 2017-2024, Mudita Sp. z.o.o. All rights reserved.
|
|
// For licensing, see https://github.com/mudita/MuditaOS/LICENSE.md
|
|
|
|
#include "PlaybackOperation.hpp"
|
|
|
|
#include "Audio/decoder/Decoder.hpp"
|
|
#include "Audio/Profiles/Profile.hpp"
|
|
#include "Audio/StreamFactory.hpp"
|
|
|
|
#include "Audio/AudioCommon.hpp"
|
|
|
|
#include <log/log.hpp>
|
|
|
|
namespace audio
|
|
{
|
|
using namespace AudioServiceMessage;
|
|
|
|
PlaybackOperation::PlaybackOperation(const std::string &filePath,
|
|
const audio::PlaybackType &playbackType,
|
|
Callback callback)
|
|
: Operation(std::move(callback), playbackType), dec(nullptr)
|
|
{
|
|
// order defines priority
|
|
AddProfile(Profile::Type::PlaybackHeadphones, playbackType, false);
|
|
AddProfile(Profile::Type::PlaybackBluetoothA2DP, playbackType, false);
|
|
AddProfile(Profile::Type::PlaybackLoudspeaker, playbackType, true);
|
|
|
|
endOfFileCallback = [this]() {
|
|
state = State::Idle;
|
|
const auto msg = AudioServiceMessage::EndOfFile(operationToken);
|
|
serviceCallback(&msg);
|
|
};
|
|
|
|
fileDeletedCallback = [this]() {
|
|
state = State::Idle;
|
|
const auto msg = AudioServiceMessage::FileDeleted(operationToken);
|
|
serviceCallback(&msg);
|
|
};
|
|
|
|
dec = Decoder::Create(filePath);
|
|
if (dec == nullptr) {
|
|
throw AudioInitException("Error during initializing decoder", RetCode::FileDoesntExist);
|
|
}
|
|
auto format = dec->getSourceFormat();
|
|
LOG_DEBUG("Source format: %s", format.toString().c_str());
|
|
|
|
auto retCode = SwitchToPriorityProfile(playbackType);
|
|
if (retCode != RetCode::Success) {
|
|
throw AudioInitException("Failed to switch audio profile", retCode);
|
|
}
|
|
}
|
|
|
|
audio::RetCode PlaybackOperation::Start(audio::Token token)
|
|
{
|
|
if (state == State::Active || (state == State::Paused && outputConnection != nullptr)) {
|
|
return RetCode::InvokedInIncorrectState;
|
|
}
|
|
|
|
// create stream
|
|
StreamFactory streamFactory(playbackTimeConstraint);
|
|
try {
|
|
dataStreamOut = streamFactory.makeStream(*dec, *audioDevice, currentProfile->getAudioFormat());
|
|
}
|
|
catch (std::invalid_argument &e) {
|
|
LOG_FATAL("Cannot create audio stream: %s", e.what());
|
|
return audio::RetCode::Failed;
|
|
}
|
|
|
|
// create audio connection
|
|
outputConnection = std::make_unique<StreamConnection>(dec.get(), audioDevice.get(), dataStreamOut.get());
|
|
|
|
// decoder worker soft start - must be called after connection setup
|
|
dec->startDecodingWorker(endOfFileCallback, fileDeletedCallback);
|
|
|
|
// start output device and enable audio connection
|
|
auto ret = audioDevice->Start();
|
|
outputConnection->enable();
|
|
|
|
// update state and token
|
|
state = State::Active;
|
|
operationToken = token;
|
|
|
|
return GetDeviceError(ret);
|
|
}
|
|
|
|
audio::RetCode PlaybackOperation::Stop()
|
|
{
|
|
state = State::Idle;
|
|
if (!audioDevice) {
|
|
return audio::RetCode::DeviceFailure;
|
|
}
|
|
|
|
// stop playback by destroying audio connection
|
|
outputConnection.reset();
|
|
dec->stopDecodingWorker();
|
|
dataStreamOut.reset();
|
|
|
|
return GetDeviceError(audioDevice->Stop());
|
|
}
|
|
|
|
audio::RetCode PlaybackOperation::Pause()
|
|
{
|
|
if (state == State::Paused || state == State::Idle || outputConnection == nullptr) {
|
|
return RetCode::InvokedInIncorrectState;
|
|
}
|
|
const auto retCode = GetDeviceError(audioDevice->Pause());
|
|
if (retCode == audio::RetCode::Success) {
|
|
state = State::Paused;
|
|
outputConnection->disable();
|
|
}
|
|
return retCode;
|
|
}
|
|
|
|
audio::RetCode PlaybackOperation::Resume()
|
|
{
|
|
if (state == State::Active || state == State::Idle) {
|
|
return RetCode::InvokedInIncorrectState;
|
|
}
|
|
|
|
if (outputConnection == nullptr) {
|
|
Start(operationToken);
|
|
}
|
|
|
|
state = State::Active;
|
|
outputConnection->enable();
|
|
return GetDeviceError(audioDevice->Resume());
|
|
}
|
|
|
|
audio::RetCode PlaybackOperation::SetOutputVolume(float vol)
|
|
{
|
|
currentProfile->SetOutputVolume(vol);
|
|
auto ret = audioDevice->setOutputVolume(vol);
|
|
return GetDeviceError(ret);
|
|
}
|
|
|
|
audio::RetCode PlaybackOperation::SetInputGain(float gain)
|
|
{
|
|
currentProfile->SetInputGain(gain);
|
|
auto ret = audioDevice->setInputGain(gain);
|
|
return GetDeviceError(ret);
|
|
}
|
|
|
|
Position PlaybackOperation::GetPosition()
|
|
{
|
|
return dec->getCurrentPosition();
|
|
}
|
|
|
|
audio::RetCode PlaybackOperation::SwitchToPriorityProfile(audio::PlaybackType playbackType)
|
|
{
|
|
for (const auto &p : supportedProfiles) {
|
|
const auto profileType = p.profile->GetType();
|
|
if (profileType == audio::Profile::Type::PlaybackBluetoothA2DP &&
|
|
playbackType == audio::PlaybackType::CallRingtone) {
|
|
continue;
|
|
}
|
|
if (p.isAvailable) {
|
|
return SwitchProfile(profileType);
|
|
}
|
|
}
|
|
return audio::RetCode::ProfileNotSet;
|
|
}
|
|
|
|
audio::RetCode PlaybackOperation::SendEvent(std::shared_ptr<Event> evt)
|
|
{
|
|
const auto isAvailable = evt->getDeviceState() == Event::DeviceState::Connected;
|
|
switch (evt->getType()) {
|
|
case EventType::JackState:
|
|
SetProfileAvailability({Profile::Type::PlaybackHeadphones}, isAvailable);
|
|
Operation::SwitchToPriorityProfile();
|
|
break;
|
|
case EventType::BluetoothA2DPDeviceState:
|
|
SetProfileAvailability({Profile::Type::PlaybackBluetoothA2DP}, isAvailable);
|
|
Operation::SwitchToPriorityProfile();
|
|
break;
|
|
default:
|
|
return RetCode::UnsupportedEvent;
|
|
}
|
|
|
|
return RetCode::Success;
|
|
}
|
|
|
|
audio::RetCode PlaybackOperation::SwitchProfile(const Profile::Type type)
|
|
{
|
|
auto newProfile = GetProfile(type);
|
|
if (newProfile == nullptr) {
|
|
LOG_ERROR("Unsupported profile");
|
|
return RetCode::UnsupportedProfile;
|
|
}
|
|
|
|
if (currentProfile && currentProfile->GetType() == newProfile->GetType()) {
|
|
return RetCode::Success;
|
|
}
|
|
|
|
// adjust new profile with information from file's tags
|
|
newProfile->SetSampleRate(dec->getSourceFormat().getSampleRate());
|
|
newProfile->SetInOutFlags(static_cast<std::uint32_t>(audio::codec::Flags::OutputStereo));
|
|
|
|
/// profile change - (re)create output device; stop audio first by
|
|
/// killing audio connection
|
|
outputConnection.reset();
|
|
dec->stopDecodingWorker();
|
|
audioDevice.reset();
|
|
dataStreamOut.reset();
|
|
audioDevice = CreateDevice(*newProfile);
|
|
if (audioDevice == nullptr) {
|
|
LOG_ERROR("Error creating AudioDevice");
|
|
return RetCode::Failed;
|
|
}
|
|
|
|
// check if audio device supports Decoder's profile
|
|
if (auto format = dec->getSourceFormat(); !audioDevice->isFormatSupportedBySink(format)) {
|
|
LOG_ERROR("Format unsupported by the audio device: %s", format.toString().c_str());
|
|
return RetCode::Failed;
|
|
}
|
|
|
|
// store profile
|
|
currentProfile = newProfile;
|
|
|
|
if (state == State::Active) {
|
|
// playback in progress, restart
|
|
state = State::Idle;
|
|
Start(operationToken);
|
|
}
|
|
|
|
return audio::RetCode::Success;
|
|
}
|
|
|
|
PlaybackOperation::~PlaybackOperation()
|
|
{
|
|
Stop();
|
|
}
|
|
} // namespace audio
|