tahoma2d/toonz/sources/toonz/audiorecordingpopup.cpp
2021-04-02 09:58:59 -04:00

613 lines
22 KiB
C++

#include "audiorecordingpopup.h"
// Tnz6 includes
#include "tapp.h"
#include "menubarcommandids.h"
// TnzQt includes
#include "toonzqt/menubarcommand.h"
#include "toonzqt/flipconsole.h"
#include "toonzqt/gutil.h"
// Tnzlib includes
#include "toonz/tproject.h"
#include "toonz/tscenehandle.h"
#include "toonz/toonzscene.h"
#include "toonz/sceneproperties.h"
#include "toonz/txshleveltypes.h"
#include "toonz/toonzfolders.h"
#include "toonz/tframehandle.h"
#include "toonz/tcolumnhandle.h"
#include "toonz/txsheethandle.h"
#include "toonz/txshsimplelevel.h"
#include "toonz/levelproperties.h"
#include "toonz/preferences.h"
// TnzCore includes
#include "tsystem.h"
#include "tpixelutils.h"
#include "iocommand.h"
// Qt includes
#include <QMainWindow>
#include <QAudio>
#include <QMediaRecorder>
#include <QAudioProbe>
#include <QAudioRecorder>
#include <QAudioFormat>
#include <QWidget>
#include <QAudioBuffer>
#include <QMediaPlayer>
#include <QObject>
#include <QComboBox>
#include <QPushButton>
#include <QGroupBox>
#include <QCheckBox>
#include <QLabel>
#include <QHBoxLayout>
#include <QVBoxLayout>
#include <QGridLayout>
#include <QMultimedia>
#include <QPainter>
#include <QElapsedTimer>
//
//=============================================================================
AudioRecordingPopup::AudioRecordingPopup()
: Dialog(TApp::instance()->getMainWindow(), false, true, "AudioRecording") {
setWindowTitle(tr("Audio Recording"));
m_isPlaying = false;
m_syncPlayback = true;
m_currentFrame = 0;
m_recordButton = new QPushButton(this);
m_playButton = new QPushButton(this);
m_saveButton = new QPushButton(tr("Save and Insert"));
m_pauseRecordingButton = new QPushButton(this);
m_pausePlaybackButton = new QPushButton(this);
// m_refreshDevicesButton = new QPushButton(tr("Refresh"));
m_duration = new QLabel("00:00");
m_playDuration = new QLabel("00:00");
m_deviceListCB = new QComboBox();
m_audioLevelsDisplay = new AudioLevelsDisplay(this);
m_playXSheetCB = new QCheckBox(tr("Sync with Scene"), this);
m_timer = new QElapsedTimer();
m_recordedLevels = QMap<qint64, double>();
m_oldElapsed = 0;
m_probe = new QAudioProbe;
m_player = new QMediaPlayer(this);
m_console = FlipConsole::getCurrent();
m_audioRecorder = new QAudioRecorder;
m_recordButton->setMaximumWidth(25);
m_playButton->setMaximumWidth(25);
m_pauseRecordingButton->setMaximumWidth(25);
m_pausePlaybackButton->setMaximumWidth(25);
QString playDisabled = QString(":Resources/play_disabled.svg");
QString pauseDisabled = QString(":Resources/pause_disabled.svg");
QString stopDisabled = QString(":Resources/stop_disabled.svg");
QString recordDisabled = QString(":Resources/record_disabled.svg");
m_pauseIcon = createQIcon("pause");
m_pauseIcon.addFile(pauseDisabled, QSize(), QIcon::Disabled);
m_playIcon = createQIcon("play");
m_playIcon.addFile(playDisabled, QSize(), QIcon::Disabled);
m_recordIcon = createQIcon("record");
m_recordIcon.addFile(recordDisabled, QSize(), QIcon::Disabled);
m_stopIcon = createQIcon("stop");
m_stopIcon.addFile(stopDisabled, QSize(), QIcon::Disabled);
m_pauseRecordingButton->setIcon(m_pauseIcon);
m_pauseRecordingButton->setIconSize(QSize(17, 17));
m_playButton->setIcon(m_playIcon);
m_playButton->setIconSize(QSize(17, 17));
m_recordButton->setIcon(m_recordIcon);
m_recordButton->setIconSize(QSize(17, 17));
m_pausePlaybackButton->setIcon(m_pauseIcon);
m_pausePlaybackButton->setIconSize(QSize(17, 17));
QStringList inputs = m_audioRecorder->audioInputs();
m_deviceListCB->addItems(inputs);
QString selectedInput = m_audioRecorder->defaultAudioInput();
m_deviceListCB->setCurrentText(selectedInput);
m_audioRecorder->setAudioInput(selectedInput);
m_topLayout->setMargin(5);
m_topLayout->setSpacing(8);
QVBoxLayout *mainLay = new QVBoxLayout();
mainLay->setSpacing(3);
mainLay->setMargin(3);
{
QGridLayout *recordGridLay = new QGridLayout();
recordGridLay->setHorizontalSpacing(2);
recordGridLay->setVerticalSpacing(3);
{
recordGridLay->addWidget(m_deviceListCB, 0, 0, 1, 4, Qt::AlignCenter);
// recordGridLay->addWidget(m_refreshDevicesButton, 0, 3, Qt::AlignLeft);
recordGridLay->addWidget(new QLabel(tr(" ")), 1, 0, Qt::AlignCenter);
recordGridLay->addWidget(m_audioLevelsDisplay, 2, 0, 1, 4,
Qt::AlignCenter);
QHBoxLayout *recordLay = new QHBoxLayout();
recordLay->setSpacing(4);
recordLay->setContentsMargins(0, 0, 0, 0);
{
recordLay->addStretch();
recordLay->addWidget(m_recordButton);
recordLay->addWidget(m_pauseRecordingButton);
recordLay->addWidget(m_duration);
recordLay->addStretch();
}
recordGridLay->addLayout(recordLay, 3, 0, 1, 4, Qt::AlignCenter);
QHBoxLayout *playLay = new QHBoxLayout();
playLay->setSpacing(4);
playLay->setContentsMargins(0, 0, 0, 0);
{
playLay->addStretch();
playLay->addWidget(m_playButton);
playLay->addWidget(m_pausePlaybackButton);
playLay->addWidget(m_playDuration);
playLay->addStretch();
}
recordGridLay->addLayout(playLay, 4, 0, 1, 4, Qt::AlignCenter);
recordGridLay->addWidget(new QLabel(tr(" ")), 5, 0, Qt::AlignCenter);
recordGridLay->addWidget(m_saveButton, 6, 0, 1, 4,
Qt::AlignCenter | Qt::AlignVCenter);
recordGridLay->addWidget(m_playXSheetCB, 7, 0, 1, 4,
Qt::AlignCenter | Qt::AlignVCenter);
}
recordGridLay->setColumnStretch(0, 0);
recordGridLay->setColumnStretch(1, 0);
recordGridLay->setColumnStretch(2, 0);
recordGridLay->setColumnStretch(3, 0);
recordGridLay->setColumnStretch(4, 0);
recordGridLay->setColumnStretch(5, 0);
mainLay->addLayout(recordGridLay);
}
m_topLayout->addLayout(mainLay, 0);
makePaths();
m_playXSheetCB->setChecked(true);
m_probe->setSource(m_audioRecorder);
QAudioEncoderSettings audioSettings;
audioSettings.setCodec("audio/PCM");
// setting the sample rate to some value (like 44100)
// may cause divide-by-zero crash in QAudioDeviceInfo::nearestFormat()
// so here we set the value to -1, as the documentation says;
// "A value of -1 indicates the encoder should make an optimal choice"
audioSettings.setSampleRate(-1);
audioSettings.setChannelCount(1);
audioSettings.setBitRate(16);
audioSettings.setEncodingMode(QMultimedia::ConstantBitRateEncoding);
audioSettings.setQuality(QMultimedia::HighQuality);
m_audioRecorder->setContainerFormat("wav");
m_audioRecorder->setEncodingSettings(audioSettings);
connect(m_probe, SIGNAL(audioBufferProbed(QAudioBuffer)), this,
SLOT(processBuffer(QAudioBuffer)));
connect(m_playXSheetCB, SIGNAL(stateChanged(int)), this,
SLOT(onPlayXSheetCBChanged(int)));
connect(m_saveButton, SIGNAL(clicked()), this, SLOT(onSaveButtonPressed()));
connect(m_recordButton, SIGNAL(clicked()), this,
SLOT(onRecordButtonPressed()));
connect(m_playButton, SIGNAL(clicked()), this, SLOT(onPlayButtonPressed()));
connect(m_pauseRecordingButton, SIGNAL(clicked()), this,
SLOT(onPauseRecordingButtonPressed()));
connect(m_pausePlaybackButton, SIGNAL(clicked()), this,
SLOT(onPausePlaybackButtonPressed()));
connect(m_audioRecorder, SIGNAL(durationChanged(qint64)), this,
SLOT(updateRecordDuration(qint64)));
connect(m_console, SIGNAL(playStateChanged(bool)), this,
SLOT(onPlayStateChanged(bool)));
connect(m_deviceListCB, SIGNAL(currentTextChanged(const QString)), this,
SLOT(onInputDeviceChanged()));
// connect(m_refreshDevicesButton, SIGNAL(clicked()), this,
// SLOT(onRefreshButtonPressed()));
// connect(m_audioRecorder, SIGNAL(availableAudioInputsChanged()), this,
// SLOT(onRefreshButtonPressed()));
}
//-----------------------------------------------------------------------------
AudioRecordingPopup::~AudioRecordingPopup() {}
//-----------------------------------------------------------------------------
void AudioRecordingPopup::onRecordButtonPressed() {
if (m_audioRecorder->state() == QAudioRecorder::StoppedState) {
if (m_audioRecorder->status() == QMediaRecorder::UnavailableStatus) {
DVGui::warning(
tr("The microphone is not available: "
"\nPlease select a different device or check the microphone."));
return;
}
// clear the player in case the file is open there
// can't record to an opened file
if (m_player->mediaStatus() != QMediaPlayer::NoMedia) {
m_player->stop();
delete m_player;
m_player = new QMediaPlayer(this);
}
// I tried using a temp file in the cache, but copying and inserting
// (rarely)
// could cause a crash. I think OT tried to import the level before the
// final file was fully copied to the new location
m_audioRecorder->setOutputLocation(
QUrl::fromLocalFile(m_filePath.getQString()));
if (TSystem::doesExistFileOrLevel(m_filePath)) {
TSystem::removeFileOrLevel(m_filePath);
}
m_recordButton->setIcon(m_stopIcon);
m_saveButton->setDisabled(true);
m_playButton->setDisabled(true);
m_pausePlaybackButton->setDisabled(true);
m_pauseRecordingButton->setEnabled(true);
m_recordedLevels.clear();
m_oldElapsed = 0;
m_pausedTime = 0;
m_startPause = 0;
m_endPause = 0;
m_stoppedAtEnd = false;
m_playDuration->setText("00:00");
m_timer->restart();
m_audioRecorder->record();
// this sometimes sets to one frame off, so + 1.
m_currentFrame = TApp::instance()->getCurrentFrame()->getFrame() + 1;
if (m_syncPlayback && !m_isPlaying) {
m_console->setCurrentFrame(m_currentFrame);
m_console->pressButton(FlipConsole::ePlay);
m_isPlaying = true;
}
} else {
m_audioRecorder->stop();
m_audioLevelsDisplay->setLevel(0);
m_recordButton->setIcon(m_recordIcon);
m_saveButton->setEnabled(true);
m_playButton->setEnabled(true);
m_pauseRecordingButton->setDisabled(true);
m_pauseRecordingButton->setIcon(m_pauseIcon);
if (m_syncPlayback) {
if (m_isPlaying) {
m_console->pressButton(FlipConsole::ePause);
}
// put the frame back to before playback
TApp::instance()->getCurrentFrame()->setCurrentFrame(m_currentFrame);
}
m_isPlaying = false;
}
}
//-----------------------------------------------------------------------------
void AudioRecordingPopup::updateRecordDuration(qint64 duration) {
// this is only called every second or so - sometimes duration ~= 950
// this gives some padding so it doesn't take two seconds to show one second
// has passed
if (duration % 1000 > 850) duration += 150;
int minutes = duration / 60000;
int seconds = (duration / 1000) % 60;
QString strMinutes = QString::number(minutes).rightJustified(2, '0');
QString strSeconds = QString::number(seconds).rightJustified(2, '0');
m_duration->setText(strMinutes + ":" + strSeconds);
}
//-----------------------------------------------------------------------------
void AudioRecordingPopup::updatePlaybackDuration(qint64 duration) {
int minutes = duration / 60000;
int seconds = (duration / 1000) % 60;
QString strMinutes = QString::number(minutes).rightJustified(2, '0');
QString strSeconds = QString::number(seconds).rightJustified(2, '0');
m_playDuration->setText(strMinutes + ":" + strSeconds);
// the qmediaplayer probe doesn't work on all platforms, so we fake it by
// using
// a map that is made during recording
if (m_recordedLevels.contains(duration / 20)) {
m_audioLevelsDisplay->setLevel(m_recordedLevels.value(duration / 20));
}
}
//-----------------------------------------------------------------------------
void AudioRecordingPopup::onPlayButtonPressed() {
if (m_player->state() == QMediaPlayer::StoppedState) {
m_player->setMedia(QUrl::fromLocalFile(m_filePath.getQString()));
m_player->setVolume(50);
m_player->setNotifyInterval(20);
connect(m_player, SIGNAL(positionChanged(qint64)), this,
SLOT(updatePlaybackDuration(qint64)));
connect(m_player, SIGNAL(stateChanged(QMediaPlayer::State)), this,
SLOT(onMediaStateChanged(QMediaPlayer::State)));
m_playButton->setIcon(m_stopIcon);
m_recordButton->setDisabled(true);
m_saveButton->setDisabled(true);
m_pausePlaybackButton->setEnabled(true);
m_stoppedAtEnd = false;
m_player->play();
// this sometimes sets to one frame off, so + 1.
// m_currentFrame = TApp::instance()->getCurrentFrame()->getFrame() + 1;
if (m_syncPlayback && !m_isPlaying) {
TApp::instance()->getCurrentFrame()->setCurrentFrame(m_currentFrame);
m_console->setCurrentFrame(m_currentFrame);
m_console->pressButton(FlipConsole::ePlay);
m_isPlaying = true;
}
} else {
m_player->stop();
m_playButton->setIcon(m_playIcon);
m_pausePlaybackButton->setDisabled(true);
m_pausePlaybackButton->setIcon(m_pauseIcon);
}
}
//-----------------------------------------------------------------------------
void AudioRecordingPopup::onPauseRecordingButtonPressed() {
if (m_audioRecorder->state() == QAudioRecorder::StoppedState) {
return;
} else if (m_audioRecorder->state() == QAudioRecorder::PausedState) {
m_endPause = m_timer->elapsed();
m_pausedTime += m_endPause - m_startPause;
m_audioRecorder->record();
m_pauseRecordingButton->setIcon(m_pauseIcon);
if (m_syncPlayback && !m_isPlaying && !m_stoppedAtEnd) {
m_console->pressButton(FlipConsole::ePlay);
m_isPlaying = true;
}
} else {
m_audioRecorder->pause();
m_pauseRecordingButton->setIcon(m_recordIcon);
m_startPause = m_timer->elapsed();
if (m_syncPlayback && m_isPlaying) {
m_isPlaying = false;
m_console->pressButton(FlipConsole::ePause);
}
}
}
//-----------------------------------------------------------------------------
void AudioRecordingPopup::onPausePlaybackButtonPressed() {
if (m_player->state() == QMediaPlayer::StoppedState) {
return;
} else if (m_player->state() == QMediaPlayer::PausedState) {
m_player->play();
m_pausePlaybackButton->setIcon(m_pauseIcon);
if (m_syncPlayback && !m_isPlaying && !m_stoppedAtEnd) {
m_console->pressButton(FlipConsole::ePlay);
m_isPlaying = true;
}
} else {
m_player->pause();
m_pausePlaybackButton->setIcon(m_playIcon);
if (m_syncPlayback && m_isPlaying) {
m_isPlaying = false;
m_console->pressButton(FlipConsole::ePause);
}
}
}
//-----------------------------------------------------------------------------
void AudioRecordingPopup::onMediaStateChanged(QMediaPlayer::State state) {
// stopping can happen through the stop button or the file ending
if (state == QMediaPlayer::StoppedState) {
m_audioLevelsDisplay->setLevel(0);
if (m_syncPlayback) {
if (m_isPlaying) {
m_console->pressButton(FlipConsole::ePause);
}
// put the frame back to before playback
TApp::instance()->getCurrentFrame()->setCurrentFrame(m_currentFrame);
}
m_playButton->setIcon(m_playIcon);
m_pausePlaybackButton->setIcon(m_pauseIcon);
m_pausePlaybackButton->setDisabled(true);
m_recordButton->setEnabled(true);
m_saveButton->setEnabled(true);
m_isPlaying = false;
}
}
//-----------------------------------------------------------------------------
void AudioRecordingPopup::onPlayXSheetCBChanged(int status) {
if (status == 0) {
m_syncPlayback = false;
} else
m_syncPlayback = true;
}
//-----------------------------------------------------------------------------
// Refresh isn't working right now, but I'm leaving the code in case a future
// change
// makes it work
// void AudioRecordingPopup::onRefreshButtonPressed() {
// m_deviceListCB->clear();
// QStringList inputs = m_audioRecorder->audioInputs();
// int count = inputs.count();
// m_deviceListCB->addItems(inputs);
// QString selectedInput = m_audioRecorder->defaultAudioInput();
// m_deviceListCB->setCurrentText(selectedInput);
//
//}
//-----------------------------------------------------------------------------
void AudioRecordingPopup::onInputDeviceChanged() {
m_audioRecorder->setAudioInput(m_deviceListCB->currentText());
}
//-----------------------------------------------------------------------------
void AudioRecordingPopup::onSaveButtonPressed() {
if (m_audioRecorder->state() != QAudioRecorder::StoppedState) {
m_audioRecorder->stop();
m_audioLevelsDisplay->setLevel(0);
}
if (m_player->state() != QMediaPlayer::StoppedState) {
m_player->stop();
m_audioLevelsDisplay->setLevel(0);
}
if (!TSystem::doesExistFileOrLevel(m_filePath)) return;
std::vector<TFilePath> filePaths;
filePaths.push_back(m_filePath);
if (filePaths.empty()) return;
if (m_syncPlayback) {
TApp::instance()->getCurrentFrame()->setCurrentFrame(m_currentFrame);
m_console->setCurrentFrame(m_currentFrame);
}
IoCmd::LoadResourceArguments args;
args.resourceDatas.assign(filePaths.begin(), filePaths.end());
IoCmd::loadResources(args);
makePaths();
resetEverything();
}
//-----------------------------------------------------------------------------
void AudioRecordingPopup::makePaths() {
TFilePath savePath =
TApp::instance()->getCurrentScene()->getScene()->getDefaultLevelPath(
TXshLevelType::SND_XSHLEVEL);
savePath =
TApp::instance()->getCurrentScene()->getScene()->decodeFilePath(savePath);
savePath = savePath.getParentDir();
std::string strPath = savePath.getQString().toStdString();
int number = 1;
TFilePath finalPath =
savePath + TFilePath("recordedAudio" + QString::number(number) + ".wav");
while (TSystem::doesExistFileOrLevel(finalPath)) {
number++;
finalPath = savePath +
TFilePath("recordedAudio" + QString::number(number) + ".wav");
}
m_filePath = finalPath;
}
//-----------------------------------------------------------------------------
void AudioRecordingPopup::processBuffer(const QAudioBuffer &buffer) {
// keep from processing too many times
// get 50 signals per second
if (m_timer->elapsed() < m_oldElapsed + 20) return;
m_oldElapsed = m_timer->elapsed() - m_pausedTime;
qint16 value = 0;
if (!buffer.format().isValid() ||
buffer.format().byteOrder() != QAudioFormat::LittleEndian)
return;
if (buffer.format().codec() != "audio/pcm") return;
const qint16 *data = buffer.constData<qint16>();
qreal maxValue = 0;
qreal tempValue = 0;
for (int i = 0; i < buffer.frameCount(); ++i) {
tempValue = qAbs(qreal(data[i]));
if (tempValue > maxValue) maxValue = tempValue;
}
maxValue /= SHRT_MAX;
m_audioLevelsDisplay->setLevel(maxValue);
m_recordedLevels[m_oldElapsed / 20] = maxValue;
}
void AudioRecordingPopup::onPlayStateChanged(bool playing) {
// m_isPlaying = playing;
if (!playing && m_isPlaying) m_stoppedAtEnd = true;
}
//-----------------------------------------------------------------------------
void AudioRecordingPopup::showEvent(QShowEvent *event) { resetEverything(); }
//-----------------------------------------------------------------------------
void AudioRecordingPopup::resetEverything() {
m_saveButton->setDisabled(true);
m_playButton->setDisabled(true);
m_recordButton->setEnabled(true);
m_recordButton->setIcon(m_recordIcon);
m_playButton->setIcon(m_playIcon);
m_pausePlaybackButton->setIcon(m_pauseIcon);
m_pauseRecordingButton->setIcon(m_pauseIcon);
m_pauseRecordingButton->setDisabled(true);
m_pausePlaybackButton->setDisabled(true);
m_recordedLevels.clear();
m_duration->setText("00:00");
m_playDuration->setText("00:00");
m_audioLevelsDisplay->setLevel(0);
}
//-----------------------------------------------------------------------------
void AudioRecordingPopup::hideEvent(QHideEvent *event) {
if (m_audioRecorder->state() != QAudioRecorder::StoppedState) {
m_audioRecorder->stop();
}
if (m_player->state() != QMediaPlayer::StoppedState) {
m_player->stop();
}
// make sure the file is freed before deleting
m_player = new QMediaPlayer(this);
// this should only remove files that haven't been used in the scene
// make paths checks to only create path names that don't exist yet.
if (TSystem::doesExistFileOrLevel(TFilePath(m_filePath.getQString()))) {
TSystem::removeFileOrLevel(TFilePath(m_filePath.getQString()));
}
}
//-----------------------------------------------------------------------------
// AudioLevelsDisplay Class
//-----------------------------------------------------------------------------
AudioLevelsDisplay::AudioLevelsDisplay(QWidget *parent)
: QWidget(parent), m_level(0.0) {
setFixedHeight(20);
setFixedWidth(300);
}
void AudioLevelsDisplay::setLevel(qreal level) {
if (m_level != level) {
m_level = level;
update();
}
}
void AudioLevelsDisplay::paintEvent(QPaintEvent *event) {
Q_UNUSED(event);
QPainter painter(this);
QColor color;
if (m_level < 0.5) {
color = Qt::green;
}
else if (m_level < 0.75) {
color = QColor(204, 205, 0); // yellow
} else if (m_level < 0.95) {
color = QColor(255, 115, 0); // orange
} else
color = Qt::red;
qreal widthLevel = m_level * width();
painter.fillRect(0, 0, widthLevel, height(), color);
painter.fillRect(widthLevel, 0, width(), height(), Qt::black);
}
//-----------------------------------------------------------------------------
OpenPopupCommandHandler<AudioRecordingPopup> openAudioRecordingPopup(
MI_AudioRecording);