diff --git a/toonz/sources/toonz/audiorecordingpopup.cpp b/toonz/sources/toonz/audiorecordingpopup.cpp index 7e6e343..8c8e3fd 100644 --- a/toonz/sources/toonz/audiorecordingpopup.cpp +++ b/toonz/sources/toonz/audiorecordingpopup.cpp @@ -64,29 +64,46 @@ AudioRecordingPopup::AudioRecordingPopup() 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_refreshDevicesButton = new QPushButton(this); + m_duration = new QLabel("00:00.000"); + m_playDuration = new QLabel("00:00.000"); m_deviceListCB = new QComboBox(); m_audioLevelsDisplay = new AudioLevelsDisplay(this); - m_playXSheetCB = new QCheckBox(tr("Sync with XSheet"), this); + m_playXSheetCB = new QCheckBox(tr("Sync with XSheet/Timeline"), this); m_timer = new QElapsedTimer(); m_recordedLevels = QMap(); - 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); + m_labelDevice = new QLabel(tr("Device: ")); + m_labelSamplerate = new QLabel(tr("Sample rate: ")); + m_labelSamplefmt = new QLabel(tr("Sample format: ")); + m_comboSamplerate = new QComboBox(); + m_comboSamplefmt = new QComboBox(); + m_comboSamplerate->addItem(tr("8000 Hz"), QVariant::fromValue(8000)); + m_comboSamplerate->addItem(tr("11025 Hz"), QVariant::fromValue(11025)); + m_comboSamplerate->addItem(tr("22050 Hz"), QVariant::fromValue(22050)); + m_comboSamplerate->addItem(tr("44100 Hz"), QVariant::fromValue(44100)); + m_comboSamplerate->addItem(tr("48000 Hz"), QVariant::fromValue(48000)); + m_comboSamplerate->addItem(tr("96000 Hz"), QVariant::fromValue(96000)); + m_comboSamplerate->setCurrentIndex(3); // 44.1KHz + m_comboSamplefmt->addItem(tr("Mono 8-Bits"), QVariant::fromValue(9)); + m_comboSamplefmt->addItem(tr("Stereo 8-Bits"), QVariant::fromValue(10)); + m_comboSamplefmt->addItem(tr("Mono 16-Bits"), QVariant::fromValue(17)); + m_comboSamplefmt->addItem(tr("Stereo 16-Bits"), QVariant::fromValue(18)); + m_comboSamplefmt->setCurrentIndex(2); // Mono 16-Bits + + m_recordButton->setMaximumWidth(32); + m_playButton->setMaximumWidth(32); + m_pauseRecordingButton->setMaximumWidth(32); + m_pausePlaybackButton->setMaximumWidth(32); + m_refreshDevicesButton->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"); + QString refreshDisabled = QString(":Resources/repeat_icon.svg"); m_pauseIcon = createQIcon("pause"); m_pauseIcon.addFile(pauseDisabled, QSize(), QIcon::Disabled); @@ -96,6 +113,9 @@ AudioRecordingPopup::AudioRecordingPopup() m_recordIcon.addFile(recordDisabled, QSize(), QIcon::Disabled); m_stopIcon = createQIcon("stop"); m_stopIcon.addFile(stopDisabled, QSize(), QIcon::Disabled); + m_refreshIcon = createQIcon("repeat"); + m_refreshIcon.addFile(refreshDisabled, QSize(), QIcon::Disabled); + m_pauseRecordingButton->setIcon(m_pauseIcon); m_pauseRecordingButton->setIconSize(QSize(17, 17)); m_playButton->setIcon(m_playIcon); @@ -104,12 +124,32 @@ AudioRecordingPopup::AudioRecordingPopup() m_recordButton->setIconSize(QSize(17, 17)); m_pausePlaybackButton->setIcon(m_pauseIcon); m_pausePlaybackButton->setIconSize(QSize(17, 17)); + m_refreshDevicesButton->setIcon(m_refreshIcon); + m_refreshDevicesButton->setIconSize(QSize(17, 17)); + + // Enumerate devices and initialize default device + enumerateAudioDevices(""); + QAudioDeviceInfo m_audioDeviceInfo = QAudioDeviceInfo::defaultInputDevice(); + QAudioFormat format; + format.setSampleRate(44100); + format.setChannelCount(1); + format.setSampleSize(16); + format.setSampleType(QAudioFormat::SignedInt); + format.setByteOrder(QAudioFormat::LittleEndian); + format.setCodec("audio/pcm"); + if (!m_audioDeviceInfo.isFormatSupported(format)) { + format = m_audioDeviceInfo.nearestFormat(format); + } + m_audioInput = new QAudioInput(m_audioDeviceInfo, format); + m_audioWriterWAV = new AudioWriterWAV(format); - QStringList inputs = m_audioRecorder->audioInputs(); - m_deviceListCB->addItems(inputs); - QString selectedInput = m_audioRecorder->defaultAudioInput(); - m_deviceListCB->setCurrentText(selectedInput); - m_audioRecorder->setAudioInput(selectedInput); + // Tool tips to provide additional info to the user + m_deviceListCB->setToolTip(tr("Audio input device to record")); + m_comboSamplerate->setToolTip(tr("Number of samples per second, 44.1KHz = CD Quality")); + m_comboSamplefmt->setToolTip(tr("Number of channels and bits per sample, 16-bits recommended")); + m_playXSheetCB->setToolTip(tr("Play animation from current frame while recording/playback")); + m_saveButton->setToolTip(tr("Save recording and insert into new column")); + m_refreshDevicesButton->setToolTip(tr("Refresh list of connected audio input devices")); m_topLayout->setMargin(5); m_topLayout->setSpacing(8); @@ -122,12 +162,20 @@ AudioRecordingPopup::AudioRecordingPopup() 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, + recordGridLay->addWidget(m_labelDevice, 0, 0, 1, 2, Qt::AlignRight); + recordGridLay->addWidget(m_deviceListCB, 0, 2, 1, 2, Qt::AlignLeft); + + recordGridLay->addWidget(m_labelSamplerate, 1, 0, 1, 2, Qt::AlignRight); + recordGridLay->addWidget(m_comboSamplerate, 1, 2, 1, 1, Qt::AlignLeft); + recordGridLay->addWidget(m_refreshDevicesButton, 1, 3, Qt::AlignRight); + + recordGridLay->addWidget(m_labelSamplefmt, 2, 0, 1, 2, Qt::AlignRight); + recordGridLay->addWidget(m_comboSamplefmt, 2, 2, 1, 2, Qt::AlignLeft); + + recordGridLay->addWidget(m_audioLevelsDisplay, 3, 0, 1, 4, Qt::AlignCenter); - QHBoxLayout *recordLay = new QHBoxLayout(this); + recordGridLay->addWidget(m_playXSheetCB, 4, 0, 1, 5, Qt::AlignCenter); + QHBoxLayout *recordLay = new QHBoxLayout(); recordLay->setSpacing(4); recordLay->setContentsMargins(0, 0, 0, 0); { @@ -137,8 +185,8 @@ AudioRecordingPopup::AudioRecordingPopup() recordLay->addWidget(m_duration); recordLay->addStretch(); } - recordGridLay->addLayout(recordLay, 3, 0, 1, 4, Qt::AlignCenter); - QHBoxLayout *playLay = new QHBoxLayout(this); + recordGridLay->addLayout(recordLay, 5, 0, 1, 4, Qt::AlignCenter); + QHBoxLayout *playLay = new QHBoxLayout(); playLay->setSpacing(4); playLay->setContentsMargins(0, 0, 0, 0); { @@ -148,11 +196,9 @@ AudioRecordingPopup::AudioRecordingPopup() 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, + recordGridLay->addLayout(playLay, 6, 0, 1, 4, Qt::AlignCenter); + recordGridLay->addWidget(new QLabel(tr(" ")), 7, 0, Qt::AlignCenter); + recordGridLay->addWidget(m_saveButton, 8, 0, 1, 4, Qt::AlignCenter | Qt::AlignVCenter); } recordGridLay->setColumnStretch(0, 0); @@ -170,23 +216,6 @@ AudioRecordingPopup::AudioRecordingPopup() 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())); @@ -197,16 +226,18 @@ AudioRecordingPopup::AudioRecordingPopup() SLOT(onPauseRecordingButtonPressed())); connect(m_pausePlaybackButton, SIGNAL(clicked()), this, SLOT(onPausePlaybackButtonPressed())); - connect(m_audioRecorder, SIGNAL(durationChanged(qint64)), this, + connect(m_audioWriterWAV, SIGNAL(update(qint64)), this, SLOT(updateRecordDuration(qint64))); - connect(m_console, SIGNAL(playStateChanged(bool)), this, + if (m_console) 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())); + connect(m_refreshDevicesButton, SIGNAL(clicked()), this, + SLOT(onRefreshButtonPressed())); + connect(m_comboSamplerate, SIGNAL(currentTextChanged(const QString)), this, + SLOT(onAudioSettingChanged())); + connect(m_comboSamplefmt, SIGNAL(currentTextChanged(const QString)), this, + SLOT(onAudioSettingChanged())); } //----------------------------------------------------------------------------- @@ -216,11 +247,16 @@ AudioRecordingPopup::~AudioRecordingPopup() {} //----------------------------------------------------------------------------- void AudioRecordingPopup::onRecordButtonPressed() { - if (m_audioRecorder->state() == QAudioRecorder::StoppedState) { - if (m_audioRecorder->status() == QMediaRecorder::UnavailableStatus) { + if (m_audioInput->state() == QAudio::InterruptedState) { + DVGui::warning( + tr("The microphone is not available: " + "\nPlease select a different device or check the microphone.")); + return; + } else if (m_audioInput->state() == QAudio::StoppedState) { + if (!m_console) { DVGui::warning( - tr("The microphone is not available: " - "\nPlease select a different device or check the microphone.")); + tr("Record failed: " + "\nMake sure there's XSheet or Timeline in the room.")); return; } // clear the player in case the file is open there @@ -234,25 +270,36 @@ void AudioRecordingPopup::onRecordButtonPressed() { // (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); } + // The audio writer support either writing to buffer or directly to disk + // each method have their own pros and cons + // For now using false to mimic previous QAudioRecorder behaviour + m_audioWriterWAV->restart(m_audioInput->format()); + if (!m_audioWriterWAV->start(m_filePath.getQString(), false)) { + DVGui::warning( + tr("Failed to save WAV file:\nMake sure you have write permissions " + "in folder.")); + return; + } m_recordButton->setIcon(m_stopIcon); m_saveButton->setDisabled(true); m_playButton->setDisabled(true); m_pausePlaybackButton->setDisabled(true); m_pauseRecordingButton->setEnabled(true); + m_deviceListCB->setDisabled(true); + m_refreshDevicesButton->setDisabled(true); + m_comboSamplerate->setDisabled(true); + m_comboSamplefmt->setDisabled(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_playDuration->setText("00:00.000"); m_timer->restart(); - m_audioRecorder->record(); + m_audioInput->start(m_audioWriterWAV); // this sometimes sets to one frame off, so + 1. m_currentFrame = TApp::instance()->getCurrentFrame()->getFrame() + 1; if (m_syncPlayback && !m_isPlaying) { @@ -262,13 +309,20 @@ void AudioRecordingPopup::onRecordButtonPressed() { } } else { - m_audioRecorder->stop(); - m_audioLevelsDisplay->setLevel(0); + m_audioInput->stop(); + bool success = m_audioWriterWAV->stop(); + if (success) { + m_saveButton->setEnabled(true); + m_playButton->setEnabled(true); + } + m_audioLevelsDisplay->setLevel(-1, -1); m_recordButton->setIcon(m_recordIcon); - m_saveButton->setEnabled(true); - m_playButton->setEnabled(true); m_pauseRecordingButton->setDisabled(true); m_pauseRecordingButton->setIcon(m_pauseIcon); + m_deviceListCB->setEnabled(true); + m_refreshDevicesButton->setEnabled(true); + m_comboSamplerate->setEnabled(true); + m_comboSamplefmt->setEnabled(true); if (m_syncPlayback) { if (m_isPlaying) { m_console->pressButton(FlipConsole::ePause); @@ -277,21 +331,29 @@ void AudioRecordingPopup::onRecordButtonPressed() { TApp::instance()->getCurrentFrame()->setCurrentFrame(m_currentFrame); } m_isPlaying = false; + if (!success) { + DVGui::warning(tr( + "Failed to save WAV file:\nMake sure you have write permissions in folder.")); + } } } //----------------------------------------------------------------------------- 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; + int milis = duration % 1000; QString strMinutes = QString::number(minutes).rightJustified(2, '0'); QString strSeconds = QString::number(seconds).rightJustified(2, '0'); - m_duration->setText(strMinutes + ":" + strSeconds); + QString strMilis = QString::number(milis).rightJustified(3, '0'); + m_duration->setText(strMinutes + ":" + strSeconds + "." + strMilis); + + // Show and record amplitude + qreal level = m_audioWriterWAV->level(); + qreal peakL = m_audioWriterWAV->peakLevel(); + m_audioLevelsDisplay->setLevel(level, peakL); + m_recordedLevels[duration / 20] = level; } //----------------------------------------------------------------------------- @@ -299,15 +361,17 @@ void AudioRecordingPopup::updateRecordDuration(qint64 duration) { void AudioRecordingPopup::updatePlaybackDuration(qint64 duration) { int minutes = duration / 60000; int seconds = (duration / 1000) % 60; + int milis = duration % 1000; QString strMinutes = QString::number(minutes).rightJustified(2, '0'); QString strSeconds = QString::number(seconds).rightJustified(2, '0'); - m_playDuration->setText(strMinutes + ":" + strSeconds); + QString strMilis = QString::number(milis).rightJustified(3, '0'); + m_playDuration->setText(strMinutes + ":" + strSeconds + "." + strMilis); // 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)); + m_audioLevelsDisplay->setLevel(m_recordedLevels.value(duration / 20), -1); } } @@ -326,6 +390,10 @@ void AudioRecordingPopup::onPlayButtonPressed() { m_recordButton->setDisabled(true); m_saveButton->setDisabled(true); m_pausePlaybackButton->setEnabled(true); + m_deviceListCB->setDisabled(true); + m_refreshDevicesButton->setDisabled(true); + m_comboSamplerate->setDisabled(true); + m_comboSamplefmt->setDisabled(true); m_stoppedAtEnd = false; m_player->play(); // this sometimes sets to one frame off, so + 1. @@ -341,25 +409,29 @@ void AudioRecordingPopup::onPlayButtonPressed() { m_playButton->setIcon(m_playIcon); m_pausePlaybackButton->setDisabled(true); m_pausePlaybackButton->setIcon(m_pauseIcon); + m_deviceListCB->setEnabled(true); + m_refreshDevicesButton->setEnabled(true); + m_comboSamplerate->setEnabled(true); + m_comboSamplefmt->setEnabled(true); } } //----------------------------------------------------------------------------- void AudioRecordingPopup::onPauseRecordingButtonPressed() { - if (m_audioRecorder->state() == QAudioRecorder::StoppedState) { + if (m_audioInput->state() == QAudio::StoppedState) { return; - } else if (m_audioRecorder->state() == QAudioRecorder::PausedState) { + } else if (m_audioInput->state() == QAudio::SuspendedState) { m_endPause = m_timer->elapsed(); m_pausedTime += m_endPause - m_startPause; - m_audioRecorder->record(); + m_audioInput->resume(); 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_audioInput->suspend(); m_pauseRecordingButton->setIcon(m_recordIcon); m_startPause = m_timer->elapsed(); if (m_syncPlayback && m_isPlaying) { @@ -396,7 +468,7 @@ void AudioRecordingPopup::onPausePlaybackButtonPressed() { 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); + m_audioLevelsDisplay->setLevel(-1, -1); if (m_syncPlayback) { if (m_isPlaying) { m_console->pressButton(FlipConsole::ePause); @@ -408,6 +480,10 @@ void AudioRecordingPopup::onMediaStateChanged(QMediaPlayer::State state) { m_pausePlaybackButton->setIcon(m_pauseIcon); m_pausePlaybackButton->setDisabled(true); m_recordButton->setEnabled(true); + m_deviceListCB->setEnabled(true); + m_refreshDevicesButton->setEnabled(true); + m_comboSamplerate->setEnabled(true); + m_comboSamplefmt->setEnabled(true); m_saveButton->setEnabled(true); m_isPlaying = false; } @@ -424,36 +500,36 @@ void AudioRecordingPopup::onPlayXSheetCBChanged(int status) { //----------------------------------------------------------------------------- -// 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::onRefreshButtonPressed() { + QAudioDeviceInfo m_audioDeviceInfo = + m_deviceListCB->itemData(m_deviceListCB->currentIndex()) + .value(); + + enumerateAudioDevices(m_audioDeviceInfo.deviceName()); +} //----------------------------------------------------------------------------- void AudioRecordingPopup::onInputDeviceChanged() { - m_audioRecorder->setAudioInput(m_deviceListCB->currentText()); + reinitAudioInput(); +} + +//----------------------------------------------------------------------------- + +void AudioRecordingPopup::onAudioSettingChanged() { + reinitAudioInput(); } //----------------------------------------------------------------------------- void AudioRecordingPopup::onSaveButtonPressed() { - if (m_audioRecorder->state() != QAudioRecorder::StoppedState) { - m_audioRecorder->stop(); - m_audioLevelsDisplay->setLevel(0); + if (m_audioInput->state() != QAudio::StoppedState) { + m_audioInput->stop(); + m_audioLevelsDisplay->setLevel(-1, -1); } if (m_player->state() != QMediaPlayer::StoppedState) { m_player->stop(); - m_audioLevelsDisplay->setLevel(0); + m_audioLevelsDisplay->setLevel(-1, -1); } if (!TSystem::doesExistFileOrLevel(m_filePath)) return; @@ -497,31 +573,6 @@ void AudioRecordingPopup::makePaths() { //----------------------------------------------------------------------------- -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(); - 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; @@ -543,28 +594,254 @@ void AudioRecordingPopup::resetEverything() { m_pauseRecordingButton->setIcon(m_pauseIcon); m_pauseRecordingButton->setDisabled(true); m_pausePlaybackButton->setDisabled(true); + m_deviceListCB->setEnabled(true); + m_refreshDevicesButton->setEnabled(true); + m_comboSamplerate->setEnabled(true); + m_comboSamplefmt->setEnabled(true); m_recordedLevels.clear(); - m_duration->setText("00:00"); - m_playDuration->setText("00:00"); - m_audioLevelsDisplay->setLevel(0); + m_duration->setText("00:00.000"); + m_playDuration->setText("00:00.000"); + m_audioLevelsDisplay->setLevel(-1, -1); + if (!m_console) { + m_console = FlipConsole::getCurrent(); + if (m_console) + connect(m_console, SIGNAL(playStateChanged(bool)), this, + SLOT(onPlayStateChanged(bool))); + } } //----------------------------------------------------------------------------- void AudioRecordingPopup::hideEvent(QHideEvent *event) { - if (m_audioRecorder->state() != QAudioRecorder::StoppedState) { - m_audioRecorder->stop(); + if (m_audioInput->state() != QAudio::StoppedState) { + m_audioInput->stop(); + m_audioWriterWAV->stop(); } if (m_player->state() != QMediaPlayer::StoppedState) { m_player->stop(); } // make sure the file is freed before deleting + delete m_player; 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())); } + // Free up memory used in recording + m_recordedLevels.clear(); +} + +//----------------------------------------------------------------------------- + +void AudioRecordingPopup::enumerateAudioDevices(const QString &selectedDeviceName) { + const QAudioDeviceInfo &defaultDeviceInfo = + QAudioDeviceInfo::defaultInputDevice(); + + m_blockAudioSettings = true; + m_deviceListCB->clear(); + m_deviceListCB->addItem(defaultDeviceInfo.deviceName(), + QVariant::fromValue(defaultDeviceInfo)); + + for (auto &deviceInfo : + QAudioDeviceInfo::availableDevices(QAudio::AudioInput)) { + if (deviceInfo != defaultDeviceInfo && + m_deviceListCB->findText(deviceInfo.deviceName()) == -1) { + m_deviceListCB->addItem(deviceInfo.deviceName(), + QVariant::fromValue(deviceInfo)); + } + } + + int deviceIndex = m_deviceListCB->findText(selectedDeviceName); + if (deviceIndex != -1) m_deviceListCB->setCurrentIndex(deviceIndex); + m_blockAudioSettings = false; +} + +//----------------------------------------------------------------------------- + +void AudioRecordingPopup::reinitAudioInput() { + if (m_blockAudioSettings) return; + + QAudioDeviceInfo m_audioDeviceInfo = + m_deviceListCB->itemData(m_deviceListCB->currentIndex()) + .value(); + int samplerate = + m_comboSamplerate->itemData(m_comboSamplerate->currentIndex()) + .value(); + int sampletype = + m_comboSamplefmt->itemData(m_comboSamplefmt->currentIndex()).value(); + int bitdepth = sampletype & 56; + int channels = sampletype & 7; + + QAudioFormat format; + format.setSampleRate(samplerate); + format.setChannelCount(channels); + format.setSampleSize(bitdepth); + format.setSampleType(bitdepth == 8 ? QAudioFormat::UnSignedInt + : QAudioFormat::SignedInt); + format.setByteOrder(QAudioFormat::LittleEndian); + format.setCodec("audio/pcm"); + if (!m_audioDeviceInfo.isFormatSupported(format)) { + DVGui::warning(tr( + "Audio format unsupported:\nNearest format will be internally used.")); + format = m_audioDeviceInfo.nearestFormat(format); + } + + // Recreate input + delete m_audioInput; + m_audioInput = new QAudioInput(m_audioDeviceInfo, format); + m_audioWriterWAV->restart(format); +} + +//----------------------------------------------------------------------------- +// AudioWriterWAV Class +//----------------------------------------------------------------------------- +// IODevice to write standard WAV files, performs peak level calc +// +// 8-bits audio must be unsigned +// 16-bits audio must be signed +// 32-bits isn't supported + +AudioWriterWAV::AudioWriterWAV(const QAudioFormat &format) + : m_level(0.0) + , m_peakL(0.0) + , m_maxAmp(0.0) + , m_wrRawB(0) + , m_wavFile(NULL) + , m_wavBuff(NULL) { + restart(format); +} + +bool AudioWriterWAV::restart(const QAudioFormat &format) { + m_format = format; + if (m_format.sampleSize() == 8) { + m_rbytesms = 1000.0 / (m_format.sampleRate() * m_format.channelCount()); + m_maxAmp = 127.0; + } else if (m_format.sampleSize() == 16) { + m_rbytesms = 500.0 / (m_format.sampleRate() * m_format.channelCount()); + m_maxAmp = 32767.0; + } else { + // 32-bits isn't supported + m_rbytesms = 250.0 / (m_format.sampleRate() * m_format.channelCount()); + m_maxAmp = 1.0; + } + m_wrRawB = 0; + m_peakL = 0.0; + if (m_wavBuff) m_wavBuff->clear(); + return this->reset(); +} + +// Just a tiny define to avoid a magic number +// this is the size of a WAV header with PCM format +#define AWWAV_HEADER_SIZE 44 + +bool AudioWriterWAV::start(const QString &filename, bool useMem) { + open(QIODevice::WriteOnly); + m_filename = filename; + + if (useMem) { + m_wavBuff = new QByteArray(); + } else { + m_wavFile = new QFile(m_filename); + if (!m_wavFile->open(QIODevice::WriteOnly | QIODevice::Truncate)) + return false; + m_wavFile->seek(AWWAV_HEADER_SIZE); // skip header + } + + m_wrRawB = 0; + m_peakL = 0.0; + return true; +} + +bool AudioWriterWAV::stop() { + close(); + + if (m_wavBuff) { + // Using memory + QFile file; + file.setFileName(m_filename); + if (!file.open(QIODevice::WriteOnly | QIODevice::Truncate)) return false; + writeWAVHeader(file); + file.write(m_wavBuff->constData(), m_wavBuff->size()); + delete m_wavBuff; + m_wavBuff = NULL; + } else { + // Using disk directly + writeWAVHeader(*m_wavFile); + m_wavFile->close(); + delete m_wavFile; + m_wavFile = NULL; + } + + m_wrRawB = 0; + m_peakL = 0.0; + return true; +} + +void AudioWriterWAV::writeWAVHeader(QFile &file) { + quint16 channels = m_format.channelCount(); + quint32 samplerate = m_format.sampleRate(); + quint16 bitrate = m_format.sampleSize(); + + qint64 pos = file.pos(); + file.seek(0); + + QDataStream out(&file); + out.setByteOrder(QDataStream::LittleEndian); + out.writeRawData("RIFF", 4); + out << (quint32)(m_wrRawB + AWWAV_HEADER_SIZE); + out.writeRawData("WAVEfmt ", 8); + out << (quint32)16 << (quint16)1; // magic numbers! + out << channels << samplerate; + out << quint32(samplerate * channels * bitrate / 8); + out << quint16(channels * bitrate / 8); + out << bitrate; + out.writeRawData("data", 4); + out << (quint32)m_wrRawB; +} + +qint64 AudioWriterWAV::readData(char *data, qint64 maxlen) { + Q_UNUSED(data) + Q_UNUSED(maxlen) + return 0; +} + +qint64 AudioWriterWAV::writeData(const char *data, qint64 len) { + int tmp, peak = 0.0; + + // Measure peak + if (m_format.sampleSize() == 8) { + const quint8 *sdata = (const quint8 *)data; + int slen = len; + for (int i = 0; i < slen; ++i) { + tmp = qAbs(sdata[i] - 128); + if (tmp > peak) peak = tmp; + } + } else if (m_format.sampleSize() == 16) { + const qint16 *sdata = (const qint16 *)data; + int slen = len / 2; + for (int i = 0; i < slen; ++i) { + tmp = qAbs(sdata[i]); + if (tmp > peak) peak = tmp; + } + } else { + // 32-bits isn't supported + peak = -1; + } + m_level = qreal(peak) / m_maxAmp; + if (m_level > m_peakL) m_peakL = m_level; + + // Write to memory or disk + if (m_wavBuff) { + m_wavBuff->append(data, len); + } else { + m_wavFile->write(data, len); + } + m_wrRawB += len; + + // Emit an update + emit update(qreal(m_wrRawB) * m_rbytesms); + return len; } //----------------------------------------------------------------------------- @@ -572,14 +849,15 @@ void AudioRecordingPopup::hideEvent(QHideEvent *event) { //----------------------------------------------------------------------------- AudioLevelsDisplay::AudioLevelsDisplay(QWidget *parent) - : QWidget(parent), m_level(0.0) { + : QWidget(parent), m_level(0.0), m_peakL(0.0) { setFixedHeight(20); setFixedWidth(300); } -void AudioLevelsDisplay::setLevel(qreal level) { - if (m_level != level) { +void AudioLevelsDisplay::setLevel(qreal level, qreal peakLevel) { + if (m_level != level || m_peakL != peakLevel) { m_level = level; + m_peakL = peakLevel; update(); } } @@ -589,20 +867,30 @@ void AudioLevelsDisplay::paintEvent(QPaintEvent *event) { QPainter painter(this); QColor color; - if (m_level < 0.5) { - color = Qt::green; - } - else if (m_level < 0.75) { + if (m_level < 0.0) { + return; // draw nothing... + } else 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); + int widthLevel = m_level * width(); + int widthPeakL = m_peakL * width(); painter.fillRect(widthLevel, 0, width(), height(), Qt::black); + if (widthPeakL >= 0) { + if (m_peakL >= 0.995) { + painter.fillRect(width() - 4, 0, 4, height(), Qt::red); + } else { + painter.setPen(QColor(64, 64, 64)); // very dark gray + painter.drawLine(widthPeakL, 0, widthPeakL, height()); + } + } + painter.fillRect(0, 0, widthLevel, height(), color); } //----------------------------------------------------------------------------- diff --git a/toonz/sources/toonz/audiorecordingpopup.h b/toonz/sources/toonz/audiorecordingpopup.h index d7c5088..e014957 100644 --- a/toonz/sources/toonz/audiorecordingpopup.h +++ b/toonz/sources/toonz/audiorecordingpopup.h @@ -15,14 +15,13 @@ class QComboBox; class QCheckBox; class QPushButton; -class QAudioRecorder; class QLabel; class AudioLevelsDisplay; class FlipConsole; -class QAudioProbe; class QAudioBuffer; class QMediaPlayer; class QElapsedTimer; +class AudioWriterWAV; //============================================================================= // AudioRecordingPopup @@ -31,24 +30,22 @@ class QElapsedTimer; class AudioRecordingPopup : public DVGui::Dialog { Q_OBJECT - QString m_deviceName; QPushButton - *m_recordButton, // *m_refreshDevicesButton, -refresh not working for now + *m_recordButton, *m_refreshDevicesButton, *m_playButton, *m_pauseRecordingButton, *m_pausePlaybackButton, *m_saveButton; QComboBox *m_deviceListCB; - QAudioRecorder *m_audioRecorder; + QAudioInput *m_audioInput; + AudioWriterWAV *m_audioWriterWAV; QLabel *m_duration, *m_playDuration; QCheckBox *m_playXSheetCB; int m_currentFrame; AudioLevelsDisplay *m_audioLevelsDisplay; - QAudioProbe *m_probe; QMediaPlayer *m_player; TFilePath m_filePath; FlipConsole *m_console; QElapsedTimer *m_timer; QMap m_recordedLevels; - qint64 m_oldElapsed; qint64 m_startPause = 0; qint64 m_endPause = 0; qint64 m_pausedTime = 0; @@ -56,7 +53,11 @@ class AudioRecordingPopup : public DVGui::Dialog { QIcon m_pauseIcon; QIcon m_recordIcon; QIcon m_stopIcon; + QIcon m_refreshIcon; bool m_isPlaying, m_syncPlayback, m_stoppedAtEnd; + QLabel *m_labelDevice, *m_labelSamplerate, *m_labelSamplefmt; + QComboBox *m_comboSamplerate, *m_comboSamplefmt; + bool m_blockAudioSettings; public: AudioRecordingPopup(); @@ -67,6 +68,8 @@ protected: void hideEvent(QHideEvent *event); void makePaths(); void resetEverything(); + void enumerateAudioDevices(const QString &deviceName); + void reinitAudioInput(); private slots: void onRecordButtonPressed(); @@ -76,12 +79,47 @@ private slots: void onSaveButtonPressed(); void onPauseRecordingButtonPressed(); void onPausePlaybackButtonPressed(); - void processBuffer(const QAudioBuffer &buffer); void onPlayStateChanged(bool playing); void onPlayXSheetCBChanged(int status); void onMediaStateChanged(QMediaPlayer::State state); void onInputDeviceChanged(); - // void onRefreshButtonPressed(); + void onRefreshButtonPressed(); + void onAudioSettingChanged(); +}; + +//============================================================================= +// AudioWriterWAV +//----------------------------------------------------------------------------- + +class AudioWriterWAV : public QIODevice { + Q_OBJECT +public: + AudioWriterWAV(const QAudioFormat &format); + bool restart(const QAudioFormat &format); + + bool start(const QString &filename, bool useMem); + bool stop(); + + qint64 readData(char *data, qint64 maxlen) override; + qint64 writeData(const char *data, qint64 len) override; + + qreal level() const { return m_level; } + qreal peakLevel() const { return m_peakL; } + +private: + QString m_filename; + QFile *m_wavFile; + QByteArray *m_wavBuff; // if not null then use memory + QAudioFormat m_format; + quint64 m_wrRawB; // Written raw bytes + qreal m_rbytesms; + qreal m_maxAmp; + qreal m_level, m_peakL; + + void writeWAVHeader(QFile &file); + +signals: + void update(qint64 duration); }; //============================================================================= @@ -94,12 +132,13 @@ public: explicit AudioLevelsDisplay(QWidget *parent = 0); // Using [0; 1.0] range - void setLevel(qreal level); + void setLevel(qreal level, qreal peak); protected: void paintEvent(QPaintEvent *event); private: qreal m_level; + qreal m_peakL; }; #endif \ No newline at end of file