Work on SampleEditor

This commit is contained in:
Jack Andersen 2018-07-30 22:04:43 -10:00
parent f00904dd76
commit 6f0a26a86c
18 changed files with 905 additions and 61 deletions

View File

@ -1,6 +1,7 @@
#include "Common.hpp"
#include <QMessageBox>
#include <QObject>
#include <QProcess>
boo::SystemString QStringToSysString(const QString& str)
{
@ -36,3 +37,52 @@ bool MkPath(const QDir& dir, const QString& file, UIMessenger& messenger)
}
return true;
}
void ShowInGraphicalShell(QWidget* parent, const QString& pathIn)
{
const QFileInfo fileInfo(pathIn);
// Mac, Windows support folder or file.
#if defined(Q_OS_WIN)
const FileName explorer = Environment::systemEnvironment().searchInPath(QLatin1String("explorer.exe"));
if (explorer.isEmpty()) {
QMessageBox::warning(parent,
QApplication::translate("Core::Internal",
"Launching Windows Explorer Failed"),
QApplication::translate("Core::Internal",
"Could not find explorer.exe in path to launch Windows Explorer."));
return;
}
QStringList param;
if (!fileInfo.isDir())
param += QLatin1String("/select,");
param += QDir::toNativeSeparators(fileInfo.canonicalFilePath());
QProcess::startDetached(explorer.toString(), param);
#elif defined(Q_OS_MAC)
QStringList scriptArgs;
scriptArgs << QLatin1String("-e")
<< QString::fromLatin1("tell application \"Finder\" to reveal POSIX file \"%1\"")
.arg(fileInfo.canonicalFilePath());
QProcess::execute(QLatin1String("/usr/bin/osascript"), scriptArgs);
scriptArgs.clear();
scriptArgs << QLatin1String("-e")
<< QLatin1String("tell application \"Finder\" to activate");
QProcess::execute(QLatin1String("/usr/bin/osascript"), scriptArgs);
#else
// we cannot select a file here, because no file browser really supports it...
const QString folder = fileInfo.isDir() ? fileInfo.absoluteFilePath() : fileInfo.filePath();
QProcess browserProc;
const QString browserArgs = QStringLiteral("xdg-open \"%1\"").arg(QFileInfo(folder).path());
browserProc.startDetached(browserArgs);
#endif
}
QString ShowInGraphicalShellString()
{
#if defined(Q_OS_WIN)
return QObject::tr("Show in Explorer");
#elif defined(Q_OS_MAC)
return QObject::tr("Show in Finder");
#else
return QObject::tr("Show in Browser");
#endif
}

View File

@ -36,6 +36,9 @@ QString SysStringToQString(const boo::SystemString& str);
bool MkPath(const QString& path, UIMessenger& messenger);
bool MkPath(const QDir& dir, const QString& file, UIMessenger& messenger);
void ShowInGraphicalShell(QWidget* parent, const QString& pathIn);
QString ShowInGraphicalShellString();
static QLatin1String StringViewToQString(std::string_view sv)
{
return QLatin1String(sv.data(), int(sv.size()));

View File

@ -46,17 +46,7 @@ void MIDIReader::noteOn(uint8_t chan, uint8_t key, uint8_t velocity)
m_chanVoxs.erase(keySearch);
}
ProjectModel::INode* node = g_MainWindow->getEditorNode();
if (node && node->type() == ProjectModel::INode::Type::SoundMacro)
{
ProjectModel::SoundMacroNode* cNode = static_cast<ProjectModel::SoundMacroNode*>(node);
amuse::AudioGroupDatabase* group = g_MainWindow->projectModel()->getGroupNode(node)->getAudioGroup();
amuse::ObjToken<amuse::Voice>& vox = m_chanVoxs[key];
vox = m_engine.macroStart(group, cNode->id(), key, velocity, g_MainWindow->m_ctrlVals[1]);
vox->setPedal(g_MainWindow->m_ctrlVals[64] >= 0x40);
vox->setPitchWheel(g_MainWindow->m_pitch);
vox->installCtrlValues(g_MainWindow->m_ctrlVals);
}
m_chanVoxs[key] = g_MainWindow->startEditorVoice(key, velocity);
}
void MIDIReader::notePressure(uint8_t /*chan*/, uint8_t /*key*/, uint8_t /*pressure*/) {}

View File

@ -330,6 +330,15 @@ void MainWindow::timerEvent(QTimerEvent* ev)
m_ui.menubar->setEnabled(false);
m_uiDisabled = true;
}
if (SampleEditor* sampleEditor = qobject_cast<SampleEditor*>(m_ui.editorContents->currentWidget()))
{
const auto& activeVoxs = m_engine->getActiveVoices();
if (activeVoxs.size() && activeVoxs.back()->state() != amuse::VoiceState::Dead)
sampleEditor->setSamplePos(activeVoxs.back()->getSamplePos());
else
sampleEditor->setSamplePos(-1);
}
}
}
@ -496,6 +505,32 @@ ProjectModel::INode* MainWindow::getEditorNode() const
return nullptr;
}
amuse::ObjToken<amuse::Voice> MainWindow::startEditorVoice(uint8_t key, uint8_t velocity)
{
amuse::ObjToken<amuse::Voice> vox;
if (ProjectModel::INode* node = getEditorNode())
{
amuse::AudioGroupDatabase* group = projectModel()->getGroupNode(node)->getAudioGroup();
if (node->type() == ProjectModel::INode::Type::SoundMacro)
{
ProjectModel::SoundMacroNode* cNode = static_cast<ProjectModel::SoundMacroNode*>(node);
vox = m_engine->macroStart(group, cNode->id(), key, velocity, m_ctrlVals[1]);
}
else if (node->type() == ProjectModel::INode::Type::Sample)
{
SampleEditor* editor = static_cast<SampleEditor*>(m_ui.editorContents->currentWidget());
vox = m_engine->macroStart(group, editor->soundMacro(), key, velocity, m_ctrlVals[1]);
}
if (vox)
{
vox->setPedal(m_ctrlVals[64] >= 0x40);
vox->setPitchWheel(m_pitch);
vox->installCtrlValues(m_ctrlVals);
}
}
return vox;
}
void MainWindow::pushUndoCommand(QUndoCommand* cmd)
{
m_undoStack->push(cmd);
@ -863,20 +898,7 @@ void MainWindow::setMIDIIO()
void MainWindow::notePressed(int key)
{
if (m_engine)
{
ProjectModel::INode* node = getEditorNode();
if (node && node->type() == ProjectModel::INode::Type::SoundMacro)
{
ProjectModel::SoundMacroNode* cNode = static_cast<ProjectModel::SoundMacroNode*>(node);
amuse::AudioGroupDatabase* group = m_projectModel->getGroupNode(node)->getAudioGroup();
if (m_lastSound)
m_lastSound->keyOff();
m_lastSound = m_engine->macroStart(group, cNode->id(), key, m_velocity, m_ctrlVals[1]);
m_lastSound->setPedal(m_ctrlVals[64] >= 0x40);
m_lastSound->setPitchWheel(m_pitch);
m_lastSound->installCtrlValues(m_ctrlVals);
}
}
m_lastSound = startEditorVoice(key, m_velocity);
}
void MainWindow::noteReleased()

View File

@ -144,6 +144,7 @@ public:
void closeEditor();
ProjectModel::INode* getEditorNode() const;
amuse::ObjToken<amuse::Voice> startEditorVoice(uint8_t key, uint8_t vel);
void pushUndoCommand(QUndoCommand* cmd);
void aboutToDeleteNode(ProjectModel::INode* node);

View File

@ -32,6 +32,18 @@
</property>
<item row="0" column="0">
<widget class="QSplitter" name="splitter">
<property name="sizePolicy">
<sizepolicy hsizetype="MinimumExpanding" vsizetype="Preferred">
<horstretch>0</horstretch>
<verstretch>0</verstretch>
</sizepolicy>
</property>
<property name="minimumSize">
<size>
<width>950</width>
<height>0</height>
</size>
</property>
<property name="orientation">
<enum>Qt::Horizontal</enum>
</property>

View File

@ -3,6 +3,14 @@
#include "amuse/DSPCodec.hpp"
#include <QPainter>
#include <QPaintEvent>
#include <QSpinBox>
#include <QScrollBar>
#include <QCheckBox>
SampleEditor* SampleView::getEditor() const
{
return qobject_cast<SampleEditor*>(parentWidget()->parentWidget()->parentWidget());
}
void SampleView::seekToSample(qreal sample)
{
@ -117,9 +125,9 @@ std::pair<std::pair<qreal, qreal>, std::pair<qreal, qreal>> SampleView::iterateS
void SampleView::paintEvent(QPaintEvent* ev)
{
QPainter painter(this);
if (m_sample)
{
m_samplesPerPx = m_sample->getNumSamples() / (width() * devicePixelRatioF());
constexpr int rulerHeight = 28;
int sampleHeight = height() - rulerHeight;
qreal rectStart = ev->rect().x();
qreal startSample = rectStart * m_samplesPerPx;
@ -127,11 +135,13 @@ void SampleView::paintEvent(QPaintEvent* ev)
qreal increment = 1.0 / devicePixelRatioF();
qreal deviceSamplesPerPx = m_samplesPerPx / devicePixelRatioF();
if (m_sample)
{
QPen peakPen(QColor(255, 147, 41), increment);
QPen avgPen(QColor(254, 177, 68), increment);
qreal scale = -height() / 2.0;
qreal trans = height() / 2.0;
qreal scale = -sampleHeight / 2.0;
qreal trans = sampleHeight / 2.0;
seekToSample(startSample);
@ -148,21 +158,178 @@ void SampleView::paintEvent(QPaintEvent* ev)
QPointF(rectStart + i, avgPeak.second.first * scale + trans));
}
}
painter.setBrush(palette().brush(QPalette::Base));
painter.setPen(Qt::NoPen);
painter.drawRect(QRect(ev->rect().x(), sampleHeight, ev->rect().width(), rulerHeight));
constexpr qreal minNumSpacing = 40.0;
qreal binaryDiv = 1.0;
while (m_samplesPerPx * minNumSpacing > binaryDiv)
binaryDiv *= 2.0;
qreal numSpacing = binaryDiv / m_samplesPerPx;
qreal tickSpacing = numSpacing / 5;
int lastNumDiv = int(ev->rect().x() / numSpacing);
int lastTickDiv = int(ev->rect().x() / tickSpacing);
qreal samplePos = startSample;
painter.setFont(m_rulerFont);
painter.setPen(QPen(QColor(127, 127, 127), increment));
for (int i = ev->rect().x(); i < ev->rect().x() + ev->rect().width(); ++i)
{
int thisNumDiv = int(i / numSpacing);
int thisTickDiv = int(i / tickSpacing);
if (thisNumDiv != lastNumDiv)
{
painter.drawLine(i, sampleHeight + 1, i, sampleHeight + 8);
lastNumDiv = thisNumDiv;
lastTickDiv = thisTickDiv;
painter.drawText(QRectF(i - numSpacing / 2.0, sampleHeight + 11.0, numSpacing, 12.0),
QString::number(int(samplePos)), QTextOption(Qt::AlignCenter));
}
else if (thisTickDiv != lastTickDiv)
{
painter.drawLine(i, sampleHeight + 1, i, sampleHeight + 5);
lastTickDiv = thisTickDiv;
}
samplePos += m_samplesPerPx;
}
if (m_sample)
{
if (m_sample->m_loopLengthSamples != 0)
{
int loopStart = m_sample->m_loopStartSample;
int loopEnd = loopStart + m_sample->m_loopLengthSamples - 1;
QPointF points[4];
painter.setPen(QPen(Qt::green, increment));
painter.setBrush(Qt::green);
qreal startPos = loopStart / m_samplesPerPx;
painter.drawLine(QPointF(startPos, 0.0), QPointF(startPos, height()));
points[0] = QPointF(startPos, height() - 6.0);
points[1] = QPointF(startPos - 6.0, height());
points[2] = QPointF(startPos + 6.0, height());
points[3] = QPointF(startPos, height() - 6.0);
painter.drawPolygon(points, 4);
painter.setPen(QPen(Qt::red, increment));
painter.setBrush(Qt::red);
qreal endPos = loopEnd / m_samplesPerPx;
painter.drawLine(QPointF(endPos, 0.0), QPointF(endPos, height()));
points[0] = QPointF(endPos, height() - 6.0);
points[1] = QPointF(endPos - 6.0, height());
points[2] = QPointF(endPos + 6.0, height());
points[3] = QPointF(endPos, height() - 6.0);
painter.drawPolygon(points, 4);
}
if (m_displaySamplePos >= 0)
{
painter.setPen(QPen(Qt::white, increment));
qreal pos = m_displaySamplePos / m_samplesPerPx;
painter.drawLine(QPointF(pos, 0.0), QPointF(pos, height()));
}
}
}
void SampleView::calculateSamplesPerPx()
{
m_samplesPerPx = (1.0 - m_zoomFactor) * m_baseSamplesPerPx + m_zoomFactor * 1.0;
}
void SampleView::resetZoom()
{
if (QScrollArea* scroll = qobject_cast<QScrollArea*>(parentWidget()->parentWidget()))
{
m_baseSamplesPerPx = m_sample->getNumSamples() / qreal(scroll->width() - 4);
calculateSamplesPerPx();
setMinimumWidth(int(m_sample->getNumSamples() / m_samplesPerPx) + 1);
update();
}
}
void SampleView::setZoom(int zVal)
{
m_zoomFactor = zVal / 99.0;
calculateSamplesPerPx();
setMinimumWidth(int(m_sample->getNumSamples() / m_samplesPerPx) + 1);
update();
}
void SampleView::showEvent(QShowEvent* ev)
{
setZoom(0);
}
void SampleView::mousePressEvent(QMouseEvent* ev)
{
if (m_sample && m_sample->m_loopLengthSamples != 0)
{
int loopStart = m_sample->m_loopStartSample;
int startPos = int(loopStart / m_samplesPerPx);
int startDelta = std::abs(startPos - ev->pos().x());
bool startPass = startDelta < 20;
int loopEnd = loopStart + m_sample->m_loopLengthSamples - 1;
int endPos = int(loopEnd / m_samplesPerPx);
int endDelta = std::abs(endPos - ev->pos().x());
bool endPass = endDelta < 20;
if (startPass && endPass)
{
if (startDelta == endDelta)
{
if (ev->pos().x() < startPos)
m_dragState = DragState::Start;
else
m_dragState = DragState::End;
}
else if (startDelta < endDelta)
m_dragState = DragState::Start;
else
m_dragState = DragState::End;
}
else if (startPass)
m_dragState = DragState::Start;
else if (endPass)
m_dragState = DragState::End;
mouseMoveEvent(ev);
}
}
void SampleView::mouseReleaseEvent(QMouseEvent* ev)
{
m_dragState = DragState::None;
}
void SampleView::mouseMoveEvent(QMouseEvent* ev)
{
if (m_dragState == DragState::Start)
getEditor()->m_controls->setLoopStartSample(int(ev->pos().x() * m_samplesPerPx));
else if (m_dragState == DragState::End)
getEditor()->m_controls->setLoopEndSample(int(ev->pos().x() * m_samplesPerPx));
}
void SampleView::wheelEvent(QWheelEvent* ev)
{
if (QScrollArea* scroll = qobject_cast<QScrollArea*>(parentWidget()->parentWidget()))
{
/* Send wheel event directly to the scroll bar */
QApplication::sendEvent(scroll->horizontalScrollBar(), ev);
}
}
void SampleView::moveEvent(QMoveEvent* ev)
{
update();
}
void SampleView::loadData(ProjectModel::SampleNode* node)
@ -170,12 +337,23 @@ void SampleView::loadData(ProjectModel::SampleNode* node)
m_node = node;
ProjectModel::GroupNode* group = g_MainWindow->projectModel()->getGroupNode(m_node.get());
std::tie(m_sample, m_sampleData) = group->getAudioGroup()->getSampleData(m_node->id(), m_node->m_obj.get());
resetZoom();
m_playbackMacro = amuse::MakeObj<amuse::SoundMacro>();
amuse::SoundMacro::CmdStartSample* startSample =
static_cast<amuse::SoundMacro::CmdStartSample*>(
m_playbackMacro->insertNewCmd(0, amuse::SoundMacro::CmdOp::StartSample));
startSample->sample.id = m_node->id();
m_playbackMacro->insertNewCmd(1, amuse::SoundMacro::CmdOp::End);
update();
}
void SampleView::unloadData()
{
m_node.reset();
m_sample.reset();
m_playbackMacro.reset();
update();
}
@ -184,19 +362,296 @@ ProjectModel::INode* SampleView::currentNode() const
return m_node.get();
}
amuse::SampleEntryData* SampleView::entryData() const
{
return m_sample.get();
}
const amuse::SoundMacro* SampleView::soundMacro() const
{
return m_playbackMacro.get();
}
void SampleView::setSamplePos(int pos)
{
if (pos != m_displaySamplePos)
{
m_displaySamplePos = pos;
update();
}
}
SampleView::SampleView(QWidget* parent)
: QWidget(parent)
{}
{
setSizePolicy(QSizePolicy::Minimum, QSizePolicy::Preferred);
m_rulerFont.setPointSize(8);
}
void SampleControls::zoomSliderChanged(int val)
{
SampleEditor* editor = qobject_cast<SampleEditor*>(parentWidget());
editor->m_sampleView->setZoom(val);
}
void SampleControls::loopStateChanged(int state)
{
if (m_updateFile)
{
SampleEditor* editor = qobject_cast<SampleEditor*>(parentWidget());
amuse::SampleEntryData* data = editor->m_sampleView->entryData();
if (state == Qt::Checked)
{
m_loopStart->setEnabled(true);
m_loopEnd->setEnabled(true);
if (m_loopStart->value() == 0 && m_loopEnd->value() == 0)
{
m_updateFile = false;
m_loopEnd->setValue(data->getNumSamples() - 1);
m_loopStart->setMaximum(m_loopEnd->value());
m_updateFile = true;
}
data->m_loopStartSample = atUint32(m_loopStart->value());
data->m_loopLengthSamples = m_loopEnd->value() + 1 - data->m_loopStartSample;
}
else
{
m_loopStart->setEnabled(false);
m_loopEnd->setEnabled(false);
data->m_loopStartSample = 0;
data->m_loopLengthSamples = 0;
}
editor->m_sampleView->update();
}
}
void SampleControls::startValueChanged(int val)
{
if (m_updateFile)
{
SampleEditor* editor = qobject_cast<SampleEditor*>(parentWidget());
amuse::SampleEntryData* data = editor->m_sampleView->entryData();
data->setLoopStartSample(atUint32(val));
m_loopEnd->setMinimum(val);
editor->m_sampleView->update();
}
}
void SampleControls::endValueChanged(int val)
{
if (m_updateFile)
{
SampleEditor* editor = qobject_cast<SampleEditor*>(parentWidget());
amuse::SampleEntryData* data = editor->m_sampleView->entryData();
data->setLoopEndSample(atUint32(val));
m_loopStart->setMaximum(val);
editor->m_sampleView->update();
}
}
void SampleControls::pitchValueChanged(int val)
{
if (m_updateFile)
{
SampleEditor* editor = qobject_cast<SampleEditor*>(parentWidget());
amuse::SampleEntryData* data = editor->m_sampleView->entryData();
data->m_pitch = atUint8(val);
}
}
void SampleControls::makeWAVVersion()
{
}
void SampleControls::makeCompressedVersion()
{
}
void SampleControls::showInBrowser()
{
ShowInGraphicalShell(this, m_path);
}
void SampleControls::updateFileState()
{
m_updateFile = false;
SampleEditor* editor = qobject_cast<SampleEditor*>(parentWidget());
ProjectModel::SampleNode* node = static_cast<ProjectModel::SampleNode*>(editor->currentNode());
amuse::SystemString path;
amuse::SampleFileState state =
g_MainWindow->projectModel()->getGroupNode(node)->getAudioGroup()->
getSampleFileState(node->id(), node->m_obj.get(), &path);
disconnect(m_makeOtherConn);
switch (state)
{
case amuse::SampleFileState::MemoryOnlyWAV:
case amuse::SampleFileState::CompressedNoWAV:
m_makeOtherVersion->setText(tr("Make WAV Version"));
m_makeOtherVersion->setDisabled(false);
m_makeOtherConn = connect(m_makeOtherVersion, SIGNAL(clicked(bool)), this, SLOT(makeWAVVersion()));
break;
case amuse::SampleFileState::MemoryOnlyCompressed:
case amuse::SampleFileState::WAVRecent:
case amuse::SampleFileState::WAVNoCompressed:
m_makeOtherVersion->setText(tr("Make Compressed Version"));
m_makeOtherVersion->setDisabled(false);
m_makeOtherConn = connect(m_makeOtherVersion, SIGNAL(clicked(bool)), this, SLOT(makeCompressedVersion()));
break;
default:
m_makeOtherVersion->setText(tr("Up To Date"));
m_makeOtherVersion->setDisabled(true);
break;
}
amuse::SampleEntryData* data = editor->m_sampleView->entryData();
m_loopCheck->setChecked(data->m_loopLengthSamples != 0);
int loopStart = 0;
int loopEnd = 0;
int loopMax = data->getNumSamples() - 1;
if (data->m_loopLengthSamples != 0)
{
loopStart = data->m_loopStartSample;
loopEnd = loopStart + data->m_loopLengthSamples - 1;
}
m_loopStart->setMaximum(loopEnd);
m_loopEnd->setMinimum(loopStart);
m_loopEnd->setMaximum(loopMax);
m_loopStart->setValue(loopStart);
m_loopEnd->setValue(loopEnd);
m_basePitch->setValue(data->m_pitch);
m_loopStart->setEnabled(data->m_loopLengthSamples != 0);
m_loopEnd->setEnabled(data->m_loopLengthSamples != 0);
if (!path.empty())
{
m_path = SysStringToQString(path);
m_showInBrowser->setDisabled(false);
}
else
{
m_path = QString();
m_showInBrowser->setDisabled(true);
}
m_updateFile = true;
}
void SampleControls::loadData()
{
m_zoomSlider->setDisabled(false);
m_loopCheck->setDisabled(false);
m_loopStart->setDisabled(false);
m_loopEnd->setDisabled(false);
m_basePitch->setDisabled(false);
m_zoomSlider->setValue(0);
updateFileState();
}
void SampleControls::unloadData()
{
m_zoomSlider->setDisabled(true);
m_loopCheck->setDisabled(true);
m_loopStart->setDisabled(true);
m_loopEnd->setDisabled(true);
m_basePitch->setDisabled(true);
m_makeOtherVersion->setText(tr("Nothing Loaded"));
m_makeOtherVersion->setDisabled(true);
m_showInBrowser->setDisabled(true);
}
SampleControls::SampleControls(QWidget* parent)
: QFrame(parent)
{
setSizePolicy(QSizePolicy::Preferred, QSizePolicy::Fixed);
setFixedHeight(100);
setFrameShape(QFrame::StyledPanel);
setFrameShadow(QFrame::Sunken);
setBackgroundRole(QPalette::Base);
setAutoFillBackground(true);
QHBoxLayout* mainLayout = new QHBoxLayout;
QGridLayout* leftLayout = new QGridLayout;
leftLayout->addWidget(new QLabel(tr("Zoom")), 0, 0);
m_zoomSlider = new QSlider(Qt::Horizontal);
m_zoomSlider->setDisabled(true);
m_zoomSlider->setRange(0, 99);
connect(m_zoomSlider, SIGNAL(valueChanged(int)), this, SLOT(zoomSliderChanged(int)));
leftLayout->addWidget(m_zoomSlider, 1, 0);
leftLayout->addWidget(new QLabel(tr("Loop")), 0, 1);
m_loopCheck = new QCheckBox;
QPalette palette = m_loopCheck->palette();
palette.setColor(QPalette::Base, palette.color(QPalette::Background));
m_loopCheck->setPalette(palette);
m_loopCheck->setDisabled(true);
connect(m_loopCheck, SIGNAL(stateChanged(int)), this, SLOT(loopStateChanged(int)));
leftLayout->addWidget(m_loopCheck, 1, 1);
leftLayout->addWidget(new QLabel(tr("Start")), 0, 2);
m_loopStart = new QSpinBox;
m_loopStart->setDisabled(true);
connect(m_loopStart, SIGNAL(valueChanged(int)), this, SLOT(startValueChanged(int)));
leftLayout->addWidget(m_loopStart, 1, 2);
leftLayout->addWidget(new QLabel(tr("End")), 0, 3);
m_loopEnd = new QSpinBox;
m_loopEnd->setDisabled(true);
connect(m_loopEnd, SIGNAL(valueChanged(int)), this, SLOT(endValueChanged(int)));
leftLayout->addWidget(m_loopEnd, 1, 3);
leftLayout->addWidget(new QLabel(tr("Base Pitch")), 0, 4);
m_basePitch = new QSpinBox;
m_basePitch->setDisabled(true);
m_basePitch->setMinimum(0);
m_basePitch->setMaximum(127);
connect(m_basePitch, SIGNAL(valueChanged(int)), this, SLOT(pitchValueChanged(int)));
leftLayout->addWidget(m_basePitch, 1, 4);
leftLayout->setColumnMinimumWidth(0, 100);
leftLayout->setColumnMinimumWidth(1, 50);
leftLayout->setColumnMinimumWidth(2, 75);
leftLayout->setColumnMinimumWidth(3, 75);
leftLayout->setColumnMinimumWidth(4, 75);
leftLayout->setRowMinimumHeight(0, 22);
leftLayout->setRowMinimumHeight(1, 37);
leftLayout->setContentsMargins(10, 6, 0, 14);
QVBoxLayout* rightLayout = new QVBoxLayout;
m_makeOtherVersion = new QPushButton(tr("Nothing Loaded"));
m_makeOtherVersion->setMinimumWidth(250);
m_makeOtherVersion->setDisabled(true);
rightLayout->addWidget(m_makeOtherVersion);
m_showInBrowser = new QPushButton(ShowInGraphicalShellString());
m_showInBrowser->setMinimumWidth(250);
m_showInBrowser->setDisabled(true);
connect(m_showInBrowser, SIGNAL(clicked(bool)), this, SLOT(showInBrowser()));
rightLayout->addWidget(m_showInBrowser);
mainLayout->addLayout(leftLayout);
mainLayout->addStretch(1);
mainLayout->addLayout(rightLayout);
setLayout(mainLayout);
}
bool SampleEditor::loadData(ProjectModel::SampleNode* node)
{
m_sampleView->loadData(node);
m_controls->loadData();
return true;
}
void SampleEditor::unloadData()
{
m_sampleView->unloadData();
m_controls->unloadData();
}
ProjectModel::INode* SampleEditor::currentNode() const
@ -204,10 +659,32 @@ ProjectModel::INode* SampleEditor::currentNode() const
return m_sampleView->currentNode();
}
SampleEditor::SampleEditor(QWidget* parent)
: EditorWidget(parent), m_sampleView(new SampleView)
const amuse::SoundMacro* SampleEditor::soundMacro() const
{
return m_sampleView->soundMacro();
}
void SampleEditor::setSamplePos(int pos)
{
m_sampleView->setSamplePos(pos);
}
void SampleEditor::resizeEvent(QResizeEvent* ev)
{
m_sampleView->resetZoom();
}
SampleEditor::SampleEditor(QWidget* parent)
: EditorWidget(parent), m_scrollArea(new QScrollArea),
m_sampleView(new SampleView), m_controls(new SampleControls)
{
m_scrollArea->setWidget(m_sampleView);
m_scrollArea->setWidgetResizable(true);
QVBoxLayout* layout = new QVBoxLayout;
layout->addWidget(m_sampleView);
layout->setContentsMargins(QMargins());
layout->setSpacing(1);
layout->addWidget(m_scrollArea);
layout->addWidget(m_controls);
setLayout(layout);
}

View File

@ -3,40 +3,109 @@
#include "EditorWidget.hpp"
#include "ProjectModel.hpp"
#include <QScrollArea>
#include <QSlider>
#include <QCheckBox>
#include <QSpinBox>
class SampleEditor;
class SampleView : public QWidget
{
Q_OBJECT
qreal m_baseSamplesPerPx = 100.0;
qreal m_samplesPerPx = 100.0;
qreal m_zoomFactor = 1.0;
amuse::ObjToken<ProjectModel::SampleNode> m_node;
amuse::ObjToken<amuse::SampleEntryData> m_sample;
amuse::ObjToken<amuse::SoundMacro> m_playbackMacro;
const unsigned char* m_sampleData = nullptr;
qreal m_curSamplePos = 0.0;
int16_t m_prev1 = 0;
int16_t m_prev2 = 0;
QFont m_rulerFont;
int m_displaySamplePos = -1;
enum class DragState
{
None,
Start,
End
};
DragState m_dragState = DragState::None;
void seekToSample(qreal sample);
std::pair<std::pair<qreal, qreal>, std::pair<qreal, qreal>> iterateSampleInterval(qreal interval);
void calculateSamplesPerPx();
SampleEditor* getEditor() const;
public:
explicit SampleView(QWidget* parent = Q_NULLPTR);
void loadData(ProjectModel::SampleNode* node);
void unloadData();
ProjectModel::INode* currentNode() const;
amuse::SampleEntryData* entryData() const;
const amuse::SoundMacro* soundMacro() const;
void setSamplePos(int pos);
void paintEvent(QPaintEvent* ev);
void resetZoom();
void setZoom(int zVal);
void showEvent(QShowEvent* ev);
void mousePressEvent(QMouseEvent* ev);
void mouseReleaseEvent(QMouseEvent* ev);
void mouseMoveEvent(QMouseEvent* ev);
void wheelEvent(QWheelEvent* ev);
void moveEvent(QMoveEvent* ev);
};
class SampleControls : public QFrame
{
Q_OBJECT
QString m_path;
QSlider* m_zoomSlider;
QCheckBox* m_loopCheck;
QSpinBox* m_loopStart;
QSpinBox* m_loopEnd;
QSpinBox* m_basePitch;
QPushButton* m_makeOtherVersion;
QMetaObject::Connection m_makeOtherConn;
QPushButton* m_showInBrowser;
bool m_updateFile = true;
public:
SampleControls(QWidget* parent = Q_NULLPTR);
public slots:
void zoomSliderChanged(int val);
void loopStateChanged(int state);
void startValueChanged(int val);
void endValueChanged(int val);
void pitchValueChanged(int val);
void makeWAVVersion();
void makeCompressedVersion();
void showInBrowser();
void updateFileState();
void setLoopStartSample(int sample) { m_loopStart->setValue(sample); }
void setLoopEndSample(int sample) { m_loopEnd->setValue(sample); }
void loadData();
void unloadData();
};
class SampleEditor : public EditorWidget
{
Q_OBJECT
friend class SampleView;
friend class SampleControls;
QScrollArea* m_scrollArea;
SampleView* m_sampleView;
SampleControls* m_controls;
public:
explicit SampleEditor(QWidget* parent = Q_NULLPTR);
bool loadData(ProjectModel::SampleNode* node);
void unloadData();
ProjectModel::INode* currentNode() const;
const amuse::SoundMacro* soundMacro() const;
void setSamplePos(int pos);
void resizeEvent(QResizeEvent* ev);
};

View File

@ -126,24 +126,32 @@ FieldSoundMacroStep::FieldSoundMacroStep(FieldProjectNode* macroField, QWidget*
setLayout(layout);
}
static QIcon SoundMacroDeleteIcon;
static QIcon SoundMacroDeleteHoveredIcon;
void SoundMacroDeleteButton::enterEvent(QEvent* event)
{
setIcon(QIcon(QStringLiteral(":/icons/IconSoundMacroDeleteHovered.svg")));
setIcon(SoundMacroDeleteHoveredIcon);
}
void SoundMacroDeleteButton::leaveEvent(QEvent* event)
{
setIcon(QIcon(QStringLiteral(":/icons/IconSoundMacroDelete.svg")));
setIcon(SoundMacroDeleteIcon);
}
SoundMacroDeleteButton::SoundMacroDeleteButton(QWidget* parent)
: QPushButton(parent)
{
if (SoundMacroDeleteIcon.isNull())
SoundMacroDeleteIcon = QIcon(QStringLiteral(":/icons/IconSoundMacroDelete.svg"));
if (SoundMacroDeleteHoveredIcon.isNull())
SoundMacroDeleteHoveredIcon = QIcon(QStringLiteral(":/icons/IconSoundMacroDeleteHovered.svg"));
setVisible(false);
setFixedSize(21, 21);
setFlat(true);
setToolTip(tr("Delete this SoundMacro"));
setIcon(QIcon(QStringLiteral(":/icons/IconSoundMacroDelete.svg")));
setIcon(SoundMacroDeleteIcon);
}
CommandWidget::CommandWidget(amuse::SoundMacro::ICmd* cmd, amuse::SoundMacro::CmdOp op, SoundMacroListing* listing)

View File

@ -1,6 +1,19 @@
<?xml version="1.0" encoding="utf-8"?>
<!DOCTYPE TS>
<TS version="2.1" language="de_DE">
<context>
<name>Core::Internal</name>
<message>
<location filename="../Common.cpp" line="+49"/>
<source>Launching Windows Explorer Failed</source>
<translation type="unfinished"></translation>
</message>
<message>
<location line="+2"/>
<source>Could not find explorer.exe in path to launch Windows Explorer.</source>
<translation type="unfinished"></translation>
</message>
</context>
<context>
<name>MainWindow</name>
<message>
@ -10,7 +23,7 @@
<translation>Amuse</translation>
</message>
<message>
<location line="+280"/>
<location line="+292"/>
<source>&amp;File</source>
<translation>&amp;Datei</translation>
</message>
@ -212,7 +225,7 @@
<translation>Keine MIDI-Geräte gefunden</translation>
</message>
<message>
<location line="+202"/>
<location line="+237"/>
<source>New Project</source>
<translation>Neues Projekt</translation>
</message>
@ -227,7 +240,7 @@
<translation>Das Verzeichnis &apos;% 1&apos; existiert nicht.</translation>
</message>
<message>
<location line="-469"/>
<location line="-504"/>
<source>Clear Recent Projects</source>
<translation type="unfinished"></translation>
</message>
@ -243,23 +256,23 @@
</message>
<message>
<location line="+31"/>
<location line="+303"/>
<location line="+338"/>
<source>The directory at &apos;%1&apos; must not be empty.</source>
<translation type="unfinished"></translation>
</message>
<message>
<location line="-302"/>
<location line="+303"/>
<location line="-337"/>
<location line="+338"/>
<source>Directory empty</source>
<translation type="unfinished"></translation>
</message>
<message>
<location line="-190"/>
<location line="-216"/>
<source>SUSTAIN</source>
<translation type="unfinished"></translation>
</message>
<message>
<location line="+196"/>
<location line="+222"/>
<source>Bad Directory</source>
<translation>Schlechtes Verzeichnis</translation>
</message>
@ -416,7 +429,7 @@
<context>
<name>QObject</name>
<message>
<location filename="../Common.cpp" line="+33"/>
<location filename="../Common.cpp" line="-17"/>
<source>A directory at &apos;%1/%2&apos; could not be created.</source>
<translation>Ein Verzeichnis unter &apos;% 1 /% 2&apos; konnte nicht erstellt werden.</translation>
</message>
@ -425,11 +438,26 @@
<source>Unable to create directory</source>
<translation>Kann kein Verzeichnis erstellen</translation>
</message>
<message>
<location line="+47"/>
<source>Show in Explorer</source>
<translation type="unfinished"></translation>
</message>
<message>
<location line="+2"/>
<source>Show in Finder</source>
<translation type="unfinished"></translation>
</message>
<message>
<location line="+2"/>
<source>Show in Browser</source>
<translation type="unfinished"></translation>
</message>
</context>
<context>
<name>QUndoStack</name>
<message>
<location filename="../SoundMacroEditor.cpp" line="+342"/>
<location filename="../SoundMacroEditor.cpp" line="+350"/>
<source>Change %1</source>
<translation type="unfinished"></translation>
</message>
@ -450,6 +478,55 @@
<translation type="unfinished"></translation>
</message>
</context>
<context>
<name>SampleControls</name>
<message>
<location filename="../SampleEditor.cpp" line="+501"/>
<source>Make Compressed Version</source>
<translation type="unfinished"></translation>
</message>
<message>
<location line="+5"/>
<source>Up To Date</source>
<translation type="unfinished"></translation>
</message>
<message>
<location line="+56"/>
<location line="+66"/>
<source>Nothing Loaded</source>
<translation type="unfinished"></translation>
</message>
<message>
<location line="-47"/>
<source>Zoom</source>
<translation type="unfinished"></translation>
</message>
<message>
<location line="+7"/>
<source>Loop</source>
<translation type="unfinished"></translation>
</message>
<message>
<location line="+9"/>
<source>Start</source>
<translation type="unfinished"></translation>
</message>
<message>
<location line="+6"/>
<source>End</source>
<translation type="unfinished"></translation>
</message>
<message>
<location line="+6"/>
<source>Base Pitch</source>
<translation type="unfinished"></translation>
</message>
<message>
<location line="-115"/>
<source>Make WAV Version</source>
<translation type="unfinished"></translation>
</message>
</context>
<context>
<name>SoundMacroCatalogue</name>
<message>
@ -542,7 +619,7 @@
<context>
<name>TargetButton</name>
<message>
<location filename="../SoundMacroEditor.cpp" line="-118"/>
<location filename="../SoundMacroEditor.cpp" line="-126"/>
<source>Set step with target click</source>
<translation type="unfinished"></translation>
</message>

View File

@ -19,6 +19,8 @@ class AudioGroup
SystemString m_groupPath;
bool m_valid;
SystemString getSampleBasePath(SampleId sfxId) const;
public:
operator bool() const { return m_valid; }
AudioGroup() = default;
@ -31,6 +33,8 @@ public:
const SampleEntry* getSample(SampleId sfxId) const;
std::pair<ObjToken<SampleEntryData>, const unsigned char*>
getSampleData(SampleId sfxId, const SampleEntry* sample) const;
SampleFileState getSampleFileState(SampleId sfxId,
const SampleEntry* sample, SystemString* pathOut = nullptr);
const AudioGroupProject& getProj() const { return m_proj; }
const AudioGroupPool& getPool() const { return m_pool; }
const AudioGroupSampleDirectory& getSdir() const { return m_sdir; }

View File

@ -116,6 +116,17 @@ enum class SampleFormat : uint8_t
PCM_PC /**< Little-endian PCM found in PC Rogue Squadron (actually enum 0 which conflicts with DSP-ADPCM) */
};
enum class SampleFileState
{
NoData,
MemoryOnlyWAV,
MemoryOnlyCompressed,
WAVRecent,
CompressedRecent,
CompressedNoWAV,
WAVNoCompressed
};
/** Indexes individual samples in SAMP chunk */
class AudioGroupSampleDirectory
{
@ -214,6 +225,16 @@ public:
return fmt == SampleFormat::DSP || fmt == SampleFormat::DSP_DRUM;
}
void setLoopStartSample(atUint32 sample)
{
m_loopLengthSamples += m_loopStartSample - sample;
m_loopStartSample = sample;
}
void setLoopEndSample(atUint32 sample)
{
m_loopLengthSamples = sample + 1 - m_loopStartSample;
}
EntryData() = default;
template <athena::Endian DNAE>
@ -285,6 +306,7 @@ public:
}
void loadLooseData(SystemStringView basePath);
SampleFileState getFileState(SystemStringView basePath, SystemString* pathOut = nullptr) const;
};
private:

View File

@ -103,6 +103,15 @@ public:
return macroStart(group, id, key, vel, mod, m_defaultStudio);
}
/** Start SoundMacro object playing directly (for editor use) */
ObjToken<Voice> macroStart(const AudioGroup* group, const SoundMacro* macro, uint8_t key,
uint8_t vel, uint8_t mod, ObjToken<Studio> smx);
ObjToken<Voice> macroStart(const AudioGroup* group, const SoundMacro* macro, uint8_t key,
uint8_t vel, uint8_t mod)
{
return macroStart(group, macro, key, vel, mod, m_defaultStudio);
}
/** Start soundFX playing from loaded audio groups, attach to positional emitter */
ObjToken<Emitter> addEmitter(const float* pos, const float* dir, float maxDist, float falloff,
int sfxId, float minVol, float maxVol, bool doppler, ObjToken<Studio> smx);

View File

@ -215,6 +215,10 @@ public:
bool loadMacroObject(SoundMacroId macroId, int macroStep, double ticksPerSec, uint8_t midiKey, uint8_t midiVel,
uint8_t midiMod, bool pushPc = false);
/** Load specified SoundMacro Object into voice */
bool loadMacroObject(const SoundMacro* macro, int macroStep, double ticksPerSec, uint8_t midiKey, uint8_t midiVel,
uint8_t midiMod, bool pushPc = false);
/** Load specified song page object (Keymap/Layer) from within group into voice */
bool loadPageObject(ObjectId objectId, double ticksPerSec, uint8_t midiKey, uint8_t midiVel, uint8_t midiMod);
@ -358,6 +362,9 @@ public:
/** Get count of all voices in hierarchy, including this one */
size_t getTotalVoices() const;
/** Get latest decoded sample index */
uint32_t getSamplePos() const { return m_curSamplePos; }
/** Recursively mark voice as dead for Engine to deallocate on next cycle */
void kill();
};

View File

@ -29,22 +29,40 @@ const SampleEntry* AudioGroup::getSample(SampleId sfxId) const
return search->second.get();
}
SystemString AudioGroup::getSampleBasePath(SampleId sfxId) const
{
#if _WIN32
return m_groupPath + _S('/') +
athena::utility::utf8ToWide(SampleId::CurNameDB->resolveNameFromId(sfxId));
#else
return m_groupPath + _S('/') +
SampleId::CurNameDB->resolveNameFromId(sfxId).data();
#endif
}
std::pair<ObjToken<SampleEntryData>, const unsigned char*>
AudioGroup::getSampleData(SampleId sfxId, const SampleEntry* sample) const
{
if (sample->m_data->m_looseData)
{
setIdDatabases();
#if _WIN32
SystemString basePath = m_groupPath + _S('/') +
athena::utility::utf8ToWide(SampleId::CurNameDB->resolveNameFromId(sfxId));
#else
SystemString basePath = m_groupPath + _S('/') +
SampleId::CurNameDB->resolveNameFromId(sfxId).data();
#endif
SystemString basePath = getSampleBasePath(sfxId);
const_cast<SampleEntry*>(sample)->loadLooseData(basePath);
return {sample->m_data, sample->m_data->m_looseData.get()};
}
return {{}, m_samp + sample->m_data->m_sampleOff};
}
SampleFileState AudioGroup::getSampleFileState(SampleId sfxId, const SampleEntry* sample, SystemString* pathOut)
{
if (sample->m_data->m_looseData)
{
setIdDatabases();
SystemString basePath = getSampleBasePath(sfxId);
return sample->getFileState(basePath, pathOut);
}
if (sample->m_data->isFormatDSP() || sample->m_data->getSampleFormat() == SampleFormat::N64)
return SampleFileState::MemoryOnlyCompressed;
return SampleFileState::MemoryOnlyWAV;
}
}

View File

@ -259,6 +259,48 @@ void AudioGroupSampleDirectory::Entry::loadLooseData(SystemStringView basePath)
}
}
SampleFileState AudioGroupSampleDirectory::Entry::getFileState(SystemStringView basePath, SystemString* pathOut) const
{
SystemString wavPath = SystemString(basePath) + _S(".wav");
SystemString dspPath = SystemString(basePath) + _S(".dsp");
Sstat wavStat, dspStat;
bool wavValid = !Stat(wavPath.c_str(), &wavStat) && S_ISREG(wavStat.st_mode);
bool dspValid = !Stat(dspPath.c_str(), &dspStat) && S_ISREG(dspStat.st_mode);
EntryData& curData = *m_data;
if (!wavValid && !dspValid)
{
if (!curData.m_looseData)
return SampleFileState::NoData;
if (curData.isFormatDSP() || curData.getSampleFormat() == SampleFormat::N64)
return SampleFileState::MemoryOnlyCompressed;
return SampleFileState::MemoryOnlyWAV;
}
if (wavValid && dspValid)
{
if (wavStat.st_mtime > dspStat.st_mtime)
{
if (pathOut)
*pathOut = wavPath;
return SampleFileState::WAVRecent;
}
if (pathOut)
*pathOut = dspPath;
return SampleFileState::CompressedRecent;
}
if (dspValid)
{
if (pathOut)
*pathOut = dspPath;
return SampleFileState::CompressedNoWAV;
}
if (pathOut)
*pathOut = wavPath;
return SampleFileState::WAVNoCompressed;
}
AudioGroupSampleDirectory AudioGroupSampleDirectory::CreateAudioGroupSampleDirectory(SystemStringView groupPath)
{
AudioGroupSampleDirectory ret;

View File

@ -311,6 +311,27 @@ ObjToken<Voice> Engine::macroStart(const AudioGroup* group, SoundMacroId id, uin
return *ret;
}
/** Start SoundMacro object playing directly (for editor use) */
ObjToken<Voice> Engine::macroStart(const AudioGroup* group, const SoundMacro* macro, uint8_t key,
uint8_t vel, uint8_t mod, ObjToken<Studio> smx)
{
if (!group)
return {};
std::list<ObjToken<Voice>>::iterator ret =
_allocateVoice(*group, {}, NativeSampleRate, true, false, smx);
if (!(*ret)->loadMacroObject(macro, 0, 1000.f, key, vel, mod))
{
_destroyVoice(ret);
return {};
}
(*ret)->setVolume(1.f);
(*ret)->setPan(0.f);
return *ret;
}
/** Start soundFX playing from loaded audio groups, attach to positional emitter */
ObjToken<Emitter> Engine::addEmitter(const float* pos, const float* dir, float maxDist, float falloff,
int sfxId, float minVol, float maxVol, bool doppler, ObjToken<Studio> smx)

View File

@ -862,6 +862,18 @@ bool Voice::loadMacroObject(SoundMacroId macroId, int macroStep, double ticksPer
return false;
}
bool Voice::loadMacroObject(const SoundMacro* macro, int macroStep, double ticksPerSec,
uint8_t midiKey, uint8_t midiVel, uint8_t midiMod, bool pushPc)
{
if (m_destroyed)
return false;
if (macro)
return _loadSoundMacro({}, macro, macroStep, ticksPerSec, midiKey, midiVel, midiMod, pushPc);
return false;
}
bool Voice::loadPageObject(ObjectId objectId, double ticksPerSec, uint8_t midiKey, uint8_t midiVel, uint8_t midiMod)
{
if (m_destroyed)