From 81f0a91569ba8218abaa154a5aa2cb9d8b61e528 Mon Sep 17 00:00:00 2001 From: Jack Andersen Date: Sat, 8 Sep 2018 11:34:01 -1000 Subject: [PATCH] Looping SNG support; bug fixes --- Editor/MainWindow.cpp | 74 ++- Editor/MainWindow.hpp | 5 +- Editor/ProjectModel.cpp | 4 +- Editor/SampleEditor.cpp | 12 +- Editor/SongGroupEditor.cpp | 114 ++-- Editor/SongGroupEditor.hpp | 3 +- Editor/resources/lang_de.ts | 243 +++++---- driver/amuserender.cpp | 26 +- include/amuse/AudioGroupSampleDirectory.hpp | 13 + include/amuse/Engine.hpp | 12 +- include/amuse/Sequencer.hpp | 5 +- include/amuse/SongState.hpp | 54 +- lib/AudioGroup.cpp | 2 +- lib/AudioGroupPool.cpp | 12 + lib/AudioGroupProject.cpp | 6 +- lib/AudioGroupSampleDirectory.cpp | 162 +++++- lib/ContainerRegistry.cpp | 7 +- lib/Engine.cpp | 8 +- lib/Sequencer.cpp | 21 +- lib/SongConverter.cpp | 275 +++++++--- lib/SongState.cpp | 542 +++++++++++--------- lib/Voice.cpp | 14 +- 22 files changed, 1035 insertions(+), 579 deletions(-) diff --git a/Editor/MainWindow.cpp b/Editor/MainWindow.cpp index 9f6602a..33c4480 100644 --- a/Editor/MainWindow.cpp +++ b/Editor/MainWindow.cpp @@ -25,12 +25,26 @@ MainWindow::MainWindow(QWidget* parent) m_navIt(m_navList.begin()), m_treeDelegate(*this, this), m_mainMessenger(this), - m_fileDialog(this), + m_openDirectoryDialog(this), + m_openFileDialog(this), + m_newFileDialog(this), m_undoStack(new QUndoStack(this)), m_backgroundThread(this) { m_backgroundThread.start(); + m_newFileDialog.setAcceptMode(QFileDialog::AcceptSave); + m_newFileDialog.setFileMode(QFileDialog::AnyFile); + m_newFileDialog.setOption(QFileDialog::ShowDirsOnly, false); + + m_openDirectoryDialog.setAcceptMode(QFileDialog::AcceptOpen); + m_openDirectoryDialog.setFileMode(QFileDialog::Directory); + m_openDirectoryDialog.setOption(QFileDialog::ShowDirsOnly, true); + + m_openFileDialog.setAcceptMode(QFileDialog::AcceptOpen); + m_openFileDialog.setFileMode(QFileDialog::ExistingFile); + m_openFileDialog.setOption(QFileDialog::ShowDirsOnly, false); + m_ui.setupUi(this); m_ui.splitter->setCollapsible(1, false); QPalette palette = m_ui.projectOutlineFilter->palette(); @@ -303,7 +317,9 @@ bool MainWindow::setProjectPath(const QString& path) connect(m_ui.projectOutline->selectionModel(), SIGNAL(selectionChanged(const QItemSelection&, const QItemSelection&)), this, SLOT(onOutlineSelectionChanged(const QItemSelection&, const QItemSelection&))); - m_fileDialog.setDirectory(path); + m_openDirectoryDialog.setDirectory(path); + m_openFileDialog.setDirectory(path); + m_newFileDialog.setDirectory(path); m_ui.actionSave_Project->setEnabled(true); m_ui.actionRevert_Project->setEnabled(true); m_ui.actionReload_Sample_Data->setEnabled(true); @@ -801,16 +817,13 @@ void MainWindow::newAction() if (!askAboutSave()) return; - m_fileDialog.setWindowTitle(tr("New Project")); - m_fileDialog.setAcceptMode(QFileDialog::AcceptSave); - m_fileDialog.setFileMode(QFileDialog::AnyFile); - m_fileDialog.setOption(QFileDialog::ShowDirsOnly, false); - m_fileDialog.open(this, SLOT(_newAction(const QString&))); + m_newFileDialog.setWindowTitle(tr("New Project")); + m_newFileDialog.open(this, SLOT(_newAction(const QString&))); } void MainWindow::_newAction(const QString& path) { - m_fileDialog.close(); + m_newFileDialog.close(); if (path.isEmpty()) return; if (!MkPath(path, m_mainMessenger)) @@ -881,16 +894,13 @@ void MainWindow::openAction() if (!askAboutSave()) return; - m_fileDialog.setWindowTitle(tr("Open Project")); - m_fileDialog.setAcceptMode(QFileDialog::AcceptOpen); - m_fileDialog.setFileMode(QFileDialog::Directory); - m_fileDialog.setOption(QFileDialog::ShowDirsOnly, true); - m_fileDialog.open(this, SLOT(_openAction(const QString&))); + m_openDirectoryDialog.setWindowTitle(tr("Open Project")); + m_openDirectoryDialog.open(this, SLOT(_openAction(const QString&))); } void MainWindow::_openAction(const QString& path) { - m_fileDialog.close(); + m_openDirectoryDialog.close(); if (path.isEmpty()) return; openProject(path); @@ -989,16 +999,13 @@ void MainWindow::reloadSampleDataAction() void MainWindow::importAction() { - m_fileDialog.setWindowTitle(tr("Import Project")); - m_fileDialog.setAcceptMode(QFileDialog::AcceptOpen); - m_fileDialog.setFileMode(QFileDialog::ExistingFile); - m_fileDialog.setOption(QFileDialog::ShowDirsOnly, false); - m_fileDialog.open(this, SLOT(_importAction(const QString&))); + m_openFileDialog.setWindowTitle(tr("Import Project")); + m_openFileDialog.open(this, SLOT(_importAction(const QString&))); } void MainWindow::_importAction(const QString& path) { - m_fileDialog.close(); + m_openFileDialog.close(); if (path.isEmpty()) return; @@ -1131,16 +1138,13 @@ void MainWindow::importSongsAction() if (!m_projectModel) return; - m_fileDialog.setWindowTitle(tr("Import Songs")); - m_fileDialog.setAcceptMode(QFileDialog::AcceptOpen); - m_fileDialog.setFileMode(QFileDialog::ExistingFile); - m_fileDialog.setOption(QFileDialog::ShowDirsOnly, false); - m_fileDialog.open(this, SLOT(_importSongsAction(const QString&))); + m_openFileDialog.setWindowTitle(tr("Import Songs")); + m_openFileDialog.open(this, SLOT(_importSongsAction(const QString&))); } void MainWindow::_importSongsAction(const QString& path) { - m_fileDialog.close(); + m_openFileDialog.close(); if (path.isEmpty()) return; @@ -1198,16 +1202,13 @@ void MainWindow::importHeadersAction() if (confirm == QMessageBox::No) return; - m_fileDialog.setWindowTitle(tr("Import C Headers")); - m_fileDialog.setAcceptMode(QFileDialog::AcceptOpen); - m_fileDialog.setFileMode(QFileDialog::Directory); - m_fileDialog.setOption(QFileDialog::ShowDirsOnly, true); - m_fileDialog.open(this, SLOT(_importHeadersAction(const QString&))); + m_openDirectoryDialog.setWindowTitle(tr("Import C Headers")); + m_openDirectoryDialog.open(this, SLOT(_importHeadersAction(const QString&))); } void MainWindow::_importHeadersAction(const QString& path) { - m_fileDialog.close(); + m_openDirectoryDialog.close(); if (path.isEmpty()) return; @@ -1226,16 +1227,13 @@ void MainWindow::exportHeadersAction() if (!m_projectModel) return; - m_fileDialog.setWindowTitle(tr("Export C Headers")); - m_fileDialog.setAcceptMode(QFileDialog::AcceptOpen); - m_fileDialog.setFileMode(QFileDialog::Directory); - m_fileDialog.setOption(QFileDialog::ShowDirsOnly, true); - m_fileDialog.open(this, SLOT(_exportHeadersAction(const QString&))); + m_openDirectoryDialog.setWindowTitle(tr("Export C Headers")); + m_openDirectoryDialog.open(this, SLOT(_exportHeadersAction(const QString&))); } void MainWindow::_exportHeadersAction(const QString& path) { - m_fileDialog.close(); + m_openDirectoryDialog.close(); if (path.isEmpty()) return; diff --git a/Editor/MainWindow.hpp b/Editor/MainWindow.hpp index f77e346..97f433b 100644 --- a/Editor/MainWindow.hpp +++ b/Editor/MainWindow.hpp @@ -117,7 +117,9 @@ class MainWindow : public QMainWindow LayersEditor* m_layersEditor = nullptr; SampleEditor* m_sampleEditor = nullptr; StudioSetupWidget* m_studioSetup = nullptr; - QFileDialog m_fileDialog; + QFileDialog m_openDirectoryDialog; + QFileDialog m_openFileDialog; + QFileDialog m_newFileDialog; std::unique_ptr m_voxEngine; std::unique_ptr m_voxAllocator; @@ -189,7 +191,6 @@ public: amuse::ObjToken startSong(amuse::GroupId groupId, amuse::SongId songId, const unsigned char* arrData); void pushUndoCommand(EditorUndoCommand* cmd); - QFileDialog& fileDialog() { return m_fileDialog; } void updateFocus(); void aboutToDeleteNode(ProjectModel::INode* node); bool askAboutSave(); diff --git a/Editor/ProjectModel.cpp b/Editor/ProjectModel.cpp index 1fa98d8..40686f5 100644 --- a/Editor/ProjectModel.cpp +++ b/Editor/ProjectModel.cpp @@ -2219,7 +2219,7 @@ void ProjectModel::del(const QModelIndex& index) tr("

The subproject %1 will be permanently deleted from the project. " "Sample files will be permanently removed from the file system.

" "

This action cannot be undone!

Continue?

").arg(n->name()), - QMessageBox::Yes, QMessageBox::No); + QMessageBox::Yes | QMessageBox::No, QMessageBox::No); if (result == QMessageBox::No) return; NameUndoRegistry nameReg; @@ -2254,7 +2254,7 @@ void ProjectModel::del(const QModelIndex& index) int result = g_MainWindow->uiMessenger().warning(tr("Delete Sample"), tr("

The sample %1 will be permanently deleted from the file system. " "

This action cannot be undone!

Continue?

").arg(n->name()), - QMessageBox::Yes, QMessageBox::No); + QMessageBox::Yes | QMessageBox::No, QMessageBox::No); if (result == QMessageBox::No) return; NameUndoRegistry nameReg; diff --git a/Editor/SampleEditor.cpp b/Editor/SampleEditor.cpp index 61ced94..ffa295d 100644 --- a/Editor/SampleEditor.cpp +++ b/Editor/SampleEditor.cpp @@ -253,7 +253,7 @@ void SampleView::paintEvent(QPaintEvent* ev) if (m_sample) { - if (m_sample->m_loopLengthSamples != 0) + if (m_sample->isLooped()) { int loopStart = m_sample->m_loopStartSample; int loopEnd = loopStart + m_sample->m_loopLengthSamples - 1; @@ -320,7 +320,7 @@ void SampleView::showEvent(QShowEvent* ev) void SampleView::mousePressEvent(QMouseEvent* ev) { - if (m_sample && m_sample->m_loopLengthSamples != 0) + if (m_sample && m_sample->isLooped()) { int loopStart = m_sample->m_loopStartSample; int startPos = int(loopStart / m_samplesPerPx); @@ -697,11 +697,11 @@ void SampleControls::updateFileState() } amuse::SampleEntryData* data = editor->m_sampleView->entryData(); - m_loopCheck->setChecked(data->m_loopLengthSamples != 0); + m_loopCheck->setChecked(data->isLooped()); int loopStart = 0; int loopEnd = 0; int loopMax = data->getNumSamples() - 1; - if (data->m_loopLengthSamples != 0) + if (data->isLooped()) { loopStart = data->m_loopStartSample; loopEnd = loopStart + data->m_loopLengthSamples - 1; @@ -712,8 +712,8 @@ void SampleControls::updateFileState() 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); + m_loopStart->setEnabled(data->isLooped()); + m_loopEnd->setEnabled(data->isLooped()); if (!path.empty()) { diff --git a/Editor/SongGroupEditor.cpp b/Editor/SongGroupEditor.cpp index a08e893..d03955e 100644 --- a/Editor/SongGroupEditor.cpp +++ b/Editor/SongGroupEditor.cpp @@ -913,16 +913,29 @@ QVariant SetupListModel::data(const QModelIndex& index, int role) const auto entry = m_sorted[index.row()]; - if (role == Qt::DisplayRole || role == Qt::EditRole) + if (role == Qt::DisplayRole || role == Qt::EditRole || role == Qt::TextColorRole) { if (index.column() == 0) { + if (role == Qt::TextColorRole) + return QVariant(); g_MainWindow->projectModel()->setIdDatabases(m_node.get()); return amuse::SongId::CurNameDB->resolveNameFromId(entry->first.id).data(); } else if (index.column() == 1) { - return g_MainWindow->projectModel()->getMIDIPathOfSong(entry.m_it->first); + QString songPath = g_MainWindow->projectModel()->getMIDIPathOfSong(entry.m_it->first); + if (songPath.isEmpty()) + { + if (role == Qt::TextColorRole) + return g_MainWindow->palette().color(QPalette::Disabled, QPalette::Text); + else if (role == Qt::EditRole) + return QVariant(); + return tr("Double-click to select file"); + } + if (role == Qt::TextColorRole) + return QVariant(); + return songPath; } } @@ -978,6 +991,8 @@ QVariant SetupListModel::headerData(int section, Qt::Orientation orientation, in Qt::ItemFlags SetupListModel::flags(const QModelIndex& index) const { + if (!m_node) + return Qt::NoItemFlags; if (index.row() == m_sorted.size()) return Qt::NoItemFlags; return Qt::ItemIsSelectable | Qt::ItemIsEnabled | Qt::ItemIsEditable; @@ -1389,15 +1404,53 @@ ColoredTabWidget::ColoredTabWidget(QWidget* parent) setTabBar(&m_tabBar); } +static std::vector LoadSongFile(QString path) +{ + QFileInfo fi(path); + if (!fi.isFile()) + return {}; + + std::vector data; + { + QFile f(path); + if (!f.open(QFile::ReadOnly)) + return {}; + auto d = f.readAll(); + data.resize(d.size()); + memcpy(&data[0], d.data(), d.size()); + } + + if (!memcmp(data.data(), "MThd", 4)) + { + data = amuse::SongConverter::MIDIToSong(data, 1, true); + } + else + { + bool isBig; + if (amuse::SongState::DetectVersion(data.data(), isBig) < 0) + return {}; + } + + return data; +} + void MIDIPlayerWidget::clicked() { if (!m_seq) { - m_seq = g_MainWindow->startSong(m_groupId, m_songId, m_arrData.data()); - if (m_seq) + m_arrData = LoadSongFile(m_path); + if (!m_arrData.empty()) { - m_playAction.setText(tr("Stop")); - m_playAction.setIcon(QIcon(QStringLiteral(":/icons/IconStop.svg"))); + m_seq = g_MainWindow->startSong(m_groupId, m_songId, m_arrData.data()); + if (m_seq) + { + m_playAction.setText(tr("Stop")); + m_playAction.setIcon(QIcon(QStringLiteral(":/icons/IconStop.svg"))); + } + } + else + { + g_MainWindow->uiMessenger().critical(tr("Bad Song Data"), tr("Unable to load song data at %1").arg(m_path)); } } else @@ -1444,9 +1497,9 @@ MIDIPlayerWidget::~MIDIPlayerWidget() } MIDIPlayerWidget::MIDIPlayerWidget(QModelIndex index, amuse::GroupId gid, amuse::SongId id, - std::vector&& arrData, QWidget* parent) + const QString& path, QWidget* parent) : QWidget(parent), m_button(this), m_playAction(tr("Play")), m_index(index), - m_groupId(gid), m_songId(id), m_arrData(std::move(arrData)) + m_groupId(gid), m_songId(id), m_path(path) { m_playAction.setIcon(QIcon(QStringLiteral(":/icons/IconSoundMacro.svg"))); m_button.setDefaultAction(&m_playAction); @@ -1619,36 +1672,6 @@ void SongGroupEditor::setupModelAboutToBeReset() m_setup.unloadData(); } -static std::vector LoadSongFile(QString path) -{ - QFileInfo fi(path); - if (!fi.isFile()) - return {}; - - std::vector data; - { - QFile f(path); - if (!f.open(QFile::ReadOnly)) - return {}; - auto d = f.readAll(); - data.resize(d.size()); - memcpy(&data[0], d.data(), d.size()); - } - - if (!memcmp(data.data(), "MThd", 4)) - { - data = amuse::SongConverter::MIDIToSong(data, 1, true); - } - else - { - bool isBig; - if (amuse::SongState::DetectVersion(data.data(), isBig) < 0) - return {}; - } - - return data; -} - void SongGroupEditor::setupDataChanged() { int idx = 0; @@ -1670,17 +1693,10 @@ void SongGroupEditor::setupDataChanged() MIDIPlayerWidget* w = qobject_cast(m_setupTable->m_listView->indexWidget(index)); if (!w || w->songId() != p.m_it->first) { - std::vector arrData = LoadSongFile(g_MainWindow->projectModel()->dir().absoluteFilePath(path)); - if (!arrData.empty()) - { - MIDIPlayerWidget* newW = new MIDIPlayerWidget(index, m_setupList.m_node->m_id, p.m_it->first, - std::move(arrData), m_setupTable->m_listView->viewport()); - m_setupTable->m_listView->setIndexWidget(index, newW); - } - else - { - m_setupTable->m_listView->setIndexWidget(index, nullptr); - } + QString pathStr = g_MainWindow->projectModel()->dir().absoluteFilePath(path); + MIDIPlayerWidget* newW = new MIDIPlayerWidget(index, m_setupList.m_node->m_id, p.m_it->first, + pathStr, m_setupTable->m_listView->viewport()); + m_setupTable->m_listView->setIndexWidget(index, newW); } } ++idx; diff --git a/Editor/SongGroupEditor.hpp b/Editor/SongGroupEditor.hpp index 9a85aa1..c1a8530 100644 --- a/Editor/SongGroupEditor.hpp +++ b/Editor/SongGroupEditor.hpp @@ -241,11 +241,12 @@ class MIDIPlayerWidget : public QWidget QModelIndex m_index; amuse::GroupId m_groupId; amuse::SongId m_songId; + QString m_path; std::vector m_arrData; amuse::ObjToken m_seq; public: explicit MIDIPlayerWidget(QModelIndex index, amuse::GroupId gid, amuse::SongId id, - std::vector&& arrData, QWidget* parent = Q_NULLPTR); + const QString& path, QWidget* parent = Q_NULLPTR); ~MIDIPlayerWidget(); amuse::SongId songId() const { return m_songId; } amuse::Sequencer* sequencer() const { return m_seq.get(); } diff --git a/Editor/resources/lang_de.ts b/Editor/resources/lang_de.ts index d3a2c82..84ea5f8 100644 --- a/Editor/resources/lang_de.ts +++ b/Editor/resources/lang_de.ts @@ -427,13 +427,23 @@ MIDIPlayerWidget - + Stop - - + + Bad Song Data + + + + + Unable to load song data at %1 + + + + + Play @@ -666,344 +676,344 @@ - + Go Back - + Go Forward - + Clear Recent Projects - + Amuse[*] - + %1/%2/%3[*] - Amuse - + %1[*] - Amuse - - + + The directory at '%1' must not be empty. - - + + Directory empty - + The directory at '%1' must exist for the Amuse editor. - + Directory does not exist - + __amuse_test__ - + The directory at '%1' must be writable for the Amuse editor: %2 - + Unable to write to directory - + No Audio Devices Found - + Virtual MIDI-In - + No MIDI Devices Found - + SUSTAIN - - + + Unsaved Changes - + Save Changes in %1? - + New Project - + The directory at '%1' does not exist. - + Bad Directory - + Opening - - - - - + + + + + Scanning Project - + Opening %1 - + Open Project - + Discard Changes in %1? - + Reloading Samples - + Scanning %1 - + Import Project - + The file at '%1' could not be interpreted as a MusyX container. - + Unsupported MusyX Container - + Sample Import Mode - + Amuse can import samples as WAV files for ease of editing, import original compressed data for lossless repacking, or both. Exporting the project will prefer whichever version was modified most recently. - + Import Compressed - + Import WAVs - + Import Both - + Raw Import Mode - + Would you like to scan for all MusyX group files in this directory? - + Project Name - + What should this project be named? - - + + Importing - - + + Importing %1 - + Import Songs - + Exporting - + Exporting %1 - - + + Import C Headers - + <p>Importing names from C headers depends on up-to-date, consistent names relative to the sound group data.</p><p>Headers are imported on a per-subproject basis from a single directory. Headers must be named with the form <code>&lt;subproject&gt;.h</code>.</p><p>Group, Song and SFX definitions are matched according to the following forms:<pre>#define GRP&lt;name&gt; &lt;id&gt; #define SNG&lt;name&gt; &lt;id&gt; #define SFX&lt;name> &lt;id&gt;</pre></p><p><strong>This operation cannot be undone! It is recommended to make a backup of the project directory before proceeding.</strong></p><p>Continue?</p> - + Export C Headers - + New Subproject - + What should this subproject be named? - + New SFX Group - + What should the new SFX group in %1 be named? - + New Song Group - + What should the new Song group in %1 be named? - + New ADSR - + What should the new ADSR in %1 be named? - + New Curve - + What should the new Curve in %1 be named? - + New Keymap - + What should the new Keymap in %1 be named? - + New Layers - + What should the new Layers in %1 be named? - + About Amuse - + Export Complete - + %1? @@ -1192,12 +1202,12 @@ PageTableView - + Delete Page Entries - + Delete Page Entry @@ -1563,7 +1573,7 @@ QDialogButtonBox - + OK @@ -1571,12 +1581,12 @@ QMessageBox - + <h3>About Amuse</h3> - + <p>Amuse is an alternate editor and runtime library for MusyX sound groups.</p><p>MusyX originally served as a widely-deployed audio system for developing games on the Nintendo 64, GameCube, and GameBoy Advance.</p><p>Amuse is available under the MIT license.<br>Please see <a href="https://gitlab.axiodl.com/AxioDL/amuse/blob/master/LICENSE">https://gitlab.axiodl.com/AxioDL/amuse/blob/master/LICENSE</a> for futher information.</p><p>Copyright (C) 2015-2018 Antidote / Jackoalan.</p><p>MusyX is a trademark of Factor 5, LLC.</p><p>Nintendo 64, GameCube, and GameBoy Advance are trademarks of Nintendo Co., Ltd.</p> @@ -1748,27 +1758,32 @@ SetupListModel - + + Double-click to select file + + + + Song Conflict - + Song %1 is already defined in project - + Change Song Name - + Song - + MIDI File @@ -1776,32 +1791,32 @@ SetupModel - + Change %1 - + Program - + Volume - + Panning - + Reverb - + Chorus @@ -1809,12 +1824,12 @@ SetupTableView - + Delete Setup Entries - + Delete Setup Entry @@ -1822,37 +1837,37 @@ SongGroupEditor - + Add Page Entry - + Add Setup Entry - + Normal Pages - + Drum Pages - + MIDI Setups - + Add new page entry - + Remove selected page entries @@ -2026,52 +2041,52 @@ TreeDelegate - + Export GameCube Group - + Find Usages - + Cut - + Copy - + Paste - + Duplicate - + Delete - + Rename - + Exporting - + Exporting %1 diff --git a/driver/amuserender.cpp b/driver/amuserender.cpp index fe7bc10..c9ae093 100644 --- a/driver/amuserender.cpp +++ b/driver/amuserender.cpp @@ -101,6 +101,7 @@ int main(int argc, const boo::SystemChar** argv) m_args.reserve(argc); double rate = NativeSampleRate; int chCount = 2; + double volume = 1.0; for (int i = 1; i < argc; ++i) { #if _WIN32 @@ -124,6 +125,16 @@ int main(int argc, const boo::SystemChar** argv) ++i; } } + else if (!wcsncmp(argv[i], L"-v", 2)) + { + if (argv[i][2]) + volume = wcstod(&argv[i][2], nullptr); + else if (argc > (i + 1)) + { + volume = wcstod(argv[i + 1], nullptr); + ++i; + } + } else m_args.push_back(argv[i]); #else @@ -147,6 +158,16 @@ int main(int argc, const boo::SystemChar** argv) ++i; } } + else if (!strncmp(argv[i], "-v", 2)) + { + if (argv[i][2]) + volume = strtod(&argv[i][2], nullptr); + else if (argc > (i + 1)) + { + volume = strtod(argv[i + 1], nullptr); + ++i; + } + } else m_args.push_back(argv[i]); #endif @@ -155,7 +176,7 @@ int main(int argc, const boo::SystemChar** argv) /* Load data */ if (m_args.size() < 1) { - Log.report(logvisor::Error, "Usage: amuserender [] [-r ] [-c ]"); + Log.report(logvisor::Error, "Usage: amuserender [] [-r ] [-c ] [-v ]"); return 1; } @@ -476,6 +497,7 @@ int main(int argc, const boo::SystemChar** argv) std::unique_ptr voxEngine = boo::NewWAVAudioVoiceEngine(pathOut, rate, chCount); amuse::BooBackendVoiceAllocator booBackend(*voxEngine); amuse::Engine engine(booBackend, amuse::AmplitudeMode::PerSample); + engine.setVolume(float(amuse::clamp(0.0, volume, 1.0))); /* Load group into engine */ const amuse::AudioGroup* group = engine.addAudioGroup(*selData); @@ -486,7 +508,7 @@ int main(int argc, const boo::SystemChar** argv) } /* Enter playback loop */ - amuse::ObjToken seq = engine.seqPlay(m_groupId, m_setupId, m_arrData->m_data.get()); + amuse::ObjToken seq = engine.seqPlay(m_groupId, m_setupId, m_arrData->m_data.get(), false); size_t wroteFrames = 0; signal(SIGINT, SIGINTHandler); do diff --git a/include/amuse/AudioGroupSampleDirectory.hpp b/include/amuse/AudioGroupSampleDirectory.hpp index 918cb66..1002246 100644 --- a/include/amuse/AudioGroupSampleDirectory.hpp +++ b/include/amuse/AudioGroupSampleDirectory.hpp @@ -33,6 +33,15 @@ struct DSPADPCMHeader : BigDNA Seek<21, athena::Current> pad; }; +struct VADPCMHeader : BigDNA +{ + AT_DECL_DNA + Value m_pitchSampleRate; + Value m_numSamples; + Value m_loopStartSample; + Value m_loopLengthSamples; +}; + struct WAVFormatChunk : LittleDNA { AT_DECL_DNA @@ -236,6 +245,8 @@ public: return fmt == SampleFormat::DSP || fmt == SampleFormat::DSP_DRUM; } + bool isLooped() const { return m_loopLengthSamples != 0 && m_loopStartSample != 0xffffffff; } + void _setLoopStartSample(atUint32 sample) { m_loopLengthSamples += m_loopStartSample - sample; @@ -295,9 +306,11 @@ public: } void loadLooseDSP(SystemStringView dspPath); + void loadLooseVADPCM(SystemStringView vadpcmPath); void loadLooseWAV(SystemStringView wavPath); void patchMetadataDSP(SystemStringView dspPath); + void patchMetadataVADPCM(SystemStringView vadpcmPath); void patchMetadataWAV(SystemStringView wavPath); }; /* This double-wrapper allows Voices to keep a strong reference on diff --git a/include/amuse/Engine.hpp b/include/amuse/Engine.hpp index c4529bd..11a7743 100644 --- a/include/amuse/Engine.hpp +++ b/include/amuse/Engine.hpp @@ -148,17 +148,17 @@ public: void removeListener(Listener* listener); /** Start song playing from loaded audio groups */ - ObjToken seqPlay(GroupId groupId, SongId songId, const unsigned char* arrData, ObjToken smx); - ObjToken seqPlay(GroupId groupId, SongId songId, const unsigned char* arrData) + ObjToken seqPlay(GroupId groupId, SongId songId, const unsigned char* arrData, bool loop, ObjToken smx); + ObjToken seqPlay(GroupId groupId, SongId songId, const unsigned char* arrData, bool loop = true) { - return seqPlay(groupId, songId, arrData, m_defaultStudio); + return seqPlay(groupId, songId, arrData, loop, m_defaultStudio); } /** Start song playing from explicit group data (for editor use) */ - ObjToken seqPlay(const AudioGroup* group, GroupId groupId, SongId songId, const unsigned char* arrData, ObjToken smx); - ObjToken seqPlay(const AudioGroup* group, GroupId groupId, SongId songId, const unsigned char* arrData) + ObjToken seqPlay(const AudioGroup* group, GroupId groupId, SongId songId, const unsigned char* arrData, bool loop, ObjToken smx); + ObjToken seqPlay(const AudioGroup* group, GroupId groupId, SongId songId, const unsigned char* arrData, bool loop = true) { - return seqPlay(group, groupId, songId, arrData, m_defaultStudio); + return seqPlay(group, groupId, songId, arrData, loop, m_defaultStudio); } /** Set total volume of engine */ diff --git a/include/amuse/Sequencer.hpp b/include/amuse/Sequencer.hpp index b5f283b..0ab39c7 100644 --- a/include/amuse/Sequencer.hpp +++ b/include/amuse/Sequencer.hpp @@ -34,7 +34,6 @@ class Sequencer : public Entity const unsigned char* m_arrData = nullptr; /**< Current playing arrangement data */ SongState m_songState; /**< State of current arrangement playback */ - double m_ticksPerSec = 1000.0; /**< Current ticks per second (tempo) for arrangement data */ SequencerState m_state = SequencerState::Interactive; /**< Current high-level state of sequencer */ bool m_dieOnEnd = false; /**< Sequencer will be killed when current arrangement completes */ @@ -68,6 +67,7 @@ class Sequencer : public Entity float m_curVol = 1.f; /**< Current volume of channel */ float m_curPan = 0.f; /**< Current panning of channel */ uint16_t m_rpn = 0; /**< Current RPN (only pitch-range 0x0000 supported) */ + double m_ticksPerSec = 1000.0; /**< Current ticks per second (tempo) for channel */ void _bringOutYourDead(); size_t getVoiceCount() const; @@ -137,10 +137,11 @@ public: void sendMacroMessage(ObjectId macroId, int32_t val); /** Set tempo of sequencer and all voices in ticks per second */ + void setTempo(uint8_t chan, double ticksPerSec); void setTempo(double ticksPerSec); /** Play MIDI arrangement */ - void playSong(const unsigned char* arrData, bool dieOnEnd = true); + void playSong(const unsigned char* arrData, bool loop = true, bool dieOnEnd = true); /** Stop current MIDI arrangement */ void stopSong(float fadeTime = 0.f, bool now = false); diff --git a/include/amuse/SongState.hpp b/include/amuse/SongState.hpp index 1ca11c2..03c66e0 100644 --- a/include/amuse/SongState.hpp +++ b/include/amuse/SongState.hpp @@ -30,9 +30,14 @@ class SongState uint32_t m_regionIdxOff; uint32_t m_chanMapOff; uint32_t m_tempoTableOff; - uint32_t m_initialTempo; - uint32_t m_unkOff; - void swapBig(); + uint32_t m_initialTempo; /* Top bit indicates per-channel looping */ + uint32_t m_loopStartTicks[16]; + uint32_t m_chanMapOff2; + void swapToBig(); + void swapFromBig(); + Header& operator=(const Header& other); + Header(const Header& other) { *this = other; } + Header() = default; } m_header; /** Track region ('clip' in an NLA representation) */ @@ -42,9 +47,11 @@ class SongState uint8_t m_progNum; uint8_t m_unk1; uint16_t m_unk2; - int16_t m_regionIndex; - int16_t m_unk3; + int16_t m_regionIndex; /* -1 to terminate song, -2 to loop to previous region */ + int16_t m_loopToRegion; + bool indexDone(bool bigEndian, bool loop) const; bool indexValid(bool bigEndian) const; + int indexLoop(bool bigEndian) const; }; /** Tempo change entry */ @@ -71,9 +78,17 @@ class SongState }; SongState* m_parent = nullptr; - uint8_t m_midiChan; /**< MIDI channel number of song channel */ - const TrackRegion* m_curRegion; /**< Pointer to currently-playing track region */ - const TrackRegion* m_nextRegion; /**< Pointer to next-queued track region */ + uint8_t m_midiChan = 0xff; /**< MIDI channel number of song channel */ + const TrackRegion* m_initRegion = nullptr; /**< Pointer to first track region */ + const TrackRegion* m_curRegion = nullptr; /**< Pointer to currently-playing track region */ + const TrackRegion* m_nextRegion = nullptr; /**< Pointer to next-queued track region */ + + double m_remDt = 0.0; /**< Remaining dt for keeping remainder between cycles */ + uint32_t m_curTick = 0; /**< Current playback position for this track */ + uint32_t m_loopStartTick = 0; /**< Tick to loop back to */ + /** Current pointer to tempo control, iterated over playback */ + const TempoChange* m_tempoPtr; + uint32_t m_tempo; /**< Current tempo (beats per minute) */ const unsigned char* m_data = nullptr; /**< Pointer to upcoming command data */ const unsigned char* m_pitchWheelData = nullptr; /**< Pointer to upcoming pitch data */ @@ -91,22 +106,18 @@ class SongState 0; /**< Last command time on this channel (for computing delta times from absolute times in N64 songs) */ Track() = default; - Track(SongState& parent, uint8_t midiChan, const TrackRegion* regions); + Track(SongState& parent, uint8_t midiChan, uint32_t loopStart, const TrackRegion* regions, uint32_t tempo); operator bool() const { return m_parent != nullptr; } - void setRegion(Sequencer* seq, const TrackRegion* region); - void advanceRegion(Sequencer* seq); - bool advance(Sequencer& seq, int32_t ticks); + void setRegion(const TrackRegion* region); + void advanceRegion(); + bool advance(Sequencer& seq, double dt); + void resetTempo(); }; std::array m_tracks; const uint32_t* m_regionIdx; /**< Table of offsets to song-region data */ - /** Current pointer to tempo control, iterated over playback */ - const TempoChange* m_tempoPtr = nullptr; - uint32_t m_tempo = 120; /**< Current tempo (beats per minute) */ - - uint32_t m_curTick = 0; /**< Current playback position for all channels */ SongPlayState m_songState = SongPlayState::Playing; /**< High-level state of Song playback */ - double m_curDt = 0.f; /**< Cumulative dt value for time-remainder tracking */ + bool m_loop = true; /**< Enable looping */ public: /** Determine SNG version @@ -115,15 +126,14 @@ public: static int DetectVersion(const unsigned char* ptr, bool& isBig); /** initialize state for Song data at `ptr` */ - bool initialize(const unsigned char* ptr); + bool initialize(const unsigned char* ptr, bool loop); + + uint32_t getInitialTempo() const { return m_header.m_initialTempo & 0x7fffffff; } /** advances `dt` seconds worth of commands in the Song * @return `true` if END reached */ bool advance(Sequencer& seq, double dt); - - /** Get current song tempo in BPM */ - uint32_t getTempo() const { return m_tempo; } }; } diff --git a/lib/AudioGroup.cpp b/lib/AudioGroup.cpp index 3fbfad2..9e5c926 100644 --- a/lib/AudioGroup.cpp +++ b/lib/AudioGroup.cpp @@ -10,8 +10,8 @@ namespace amuse void AudioGroup::assign(const AudioGroupData& data) { - m_proj = AudioGroupProject::CreateAudioGroupProject(data); m_pool = AudioGroupPool::CreateAudioGroupPool(data); + m_proj = AudioGroupProject::CreateAudioGroupProject(data); m_sdir = AudioGroupSampleDirectory::CreateAudioGroupSampleDirectory(data); m_samp = data.getSamp(); } diff --git a/lib/AudioGroupPool.cpp b/lib/AudioGroupPool.cpp index e55bbc8..f8e2547 100644 --- a/lib/AudioGroupPool.cpp +++ b/lib/AudioGroupPool.cpp @@ -119,6 +119,9 @@ AudioGroupPool AudioGroupPool::_AudioGroupPool(athena::io::IStreamReader& r) ObjectHeader objHead; atInt64 startPos = r.position(); objHead.read(r); + if (SoundMacroId::CurNameDB) + SoundMacroId::CurNameDB->registerPair( + NameDB::generateName(objHead.objectId, NameDB::Type::SoundMacro), objHead.objectId); auto& macro = ret.m_soundMacros[objHead.objectId.id]; macro = MakeObj(); macro->template readCmds(r, objHead.size - 8); @@ -134,6 +137,9 @@ AudioGroupPool AudioGroupPool::_AudioGroupPool(athena::io::IStreamReader& r) ObjectHeader objHead; atInt64 startPos = r.position(); objHead.read(r); + if (TableId::CurNameDB) + TableId::CurNameDB->registerPair( + NameDB::generateName(objHead.objectId, NameDB::Type::Table), objHead.objectId); auto& ptr = ret.m_tables[objHead.objectId.id]; switch (objHead.size) { @@ -163,6 +169,9 @@ AudioGroupPool AudioGroupPool::_AudioGroupPool(athena::io::IStreamReader& r) ObjectHeader objHead; atInt64 startPos = r.position(); objHead.read(r); + if (KeymapId::CurNameDB) + KeymapId::CurNameDB->registerPair( + NameDB::generateName(objHead.objectId, NameDB::Type::Keymap), objHead.objectId); auto& km = ret.m_keymaps[objHead.objectId.id]; km = MakeObj>(); for (int i = 0; i < 128; ++i) @@ -183,6 +192,9 @@ AudioGroupPool AudioGroupPool::_AudioGroupPool(athena::io::IStreamReader& r) ObjectHeader objHead; atInt64 startPos = r.position(); objHead.read(r); + if (LayersId::CurNameDB) + LayersId::CurNameDB->registerPair( + NameDB::generateName(objHead.objectId, NameDB::Type::Layer), objHead.objectId); auto& lm = ret.m_layers[objHead.objectId.id]; lm = MakeObj>(); uint32_t count; diff --git a/lib/AudioGroupProject.cpp b/lib/AudioGroupProject.cpp index 8fbf725..cffba0e 100644 --- a/lib/AudioGroupProject.cpp +++ b/lib/AudioGroupProject.cpp @@ -103,6 +103,7 @@ AudioGroupProject::AudioGroupProject(athena::io::IStreamReader& r, GCNDataTag) if (GroupId::CurNameDB) GroupId::CurNameDB->registerPair(NameDB::generateName(header.groupId, NameDB::Type::Group), header.groupId); +#if 0 /* Sound Macros */ r.seek(header.soundMacroIdsOff, athena::Begin); while (!AtEnd16(r)) @@ -127,6 +128,7 @@ AudioGroupProject::AudioGroupProject(athena::io::IStreamReader& r, GCNDataTag) r.seek(header.layerIdsOff, athena::Begin); while (!AtEnd16(r)) ReadRangedObjectIds(LayersId::CurNameDB, r, NameDB::Type::Layer); +#endif if (header.type == GroupType::Song) { @@ -203,6 +205,7 @@ AudioGroupProject AudioGroupProject::_AudioGroupProject(athena::io::IStreamReade GroupId::CurNameDB->registerPair(NameDB::generateName(header.groupId, NameDB::Type::Group), header.groupId); +#if 0 /* Sound Macros */ r.seek(subDataOff + header.soundMacroIdsOff, athena::Begin); while (!AtEnd16(r)) @@ -227,6 +230,7 @@ AudioGroupProject AudioGroupProject::_AudioGroupProject(athena::io::IStreamReade r.seek(subDataOff + header.layerIdsOff, athena::Begin); while (!AtEnd16(r)) ReadRangedObjectIds(LayersId::CurNameDB, r, NameDB::Type::Layer); +#endif if (header.type == GroupType::Song) { @@ -288,7 +292,7 @@ AudioGroupProject AudioGroupProject::_AudioGroupProject(athena::io::IStreamReade /* MIDI setups */ r.seek(subDataOff + header.midiSetupsOff, athena::Begin); - while (r.position() < groupBegin + header.groupEndOff) + while (r.position() + 4 < groupBegin + header.groupEndOff) { uint16_t songId; athena::io::Read::Do({}, songId, r); diff --git a/lib/AudioGroupSampleDirectory.cpp b/lib/AudioGroupSampleDirectory.cpp index 710f5ec..22564e0 100644 --- a/lib/AudioGroupSampleDirectory.cpp +++ b/lib/AudioGroupSampleDirectory.cpp @@ -210,6 +210,29 @@ void AudioGroupSampleDirectory::EntryData::loadLooseDSP(SystemStringView dspPath } } +void AudioGroupSampleDirectory::EntryData::loadLooseVADPCM(SystemStringView vadpcmPath) +{ + athena::io::FileReader r(vadpcmPath); + if (!r.hasError()) + { + VADPCMHeader header; + header.read(r); + m_pitch = header.m_pitchSampleRate >> 24; + m_sampleRate = header.m_pitchSampleRate & 0xffff; + m_numSamples = header.m_numSamples & 0xffff; + m_numSamples |= atUint32(SampleFormat::N64) << 24; + m_loopStartSample = header.m_loopStartSample; + m_loopLengthSamples = header.m_loopLengthSamples; + + uint32_t dataLen = 256 + (m_numSamples + 63) / 64 * 40; + m_looseData.reset(new uint8_t[dataLen]); + r.readUBytesToBuf(m_looseData.get(), dataLen); + + memcpy(&m_ADPCMParms, m_looseData.get(), 256); + m_ADPCMParms.swapBigVADPCM(); + } +} + void AudioGroupSampleDirectory::EntryData::loadLooseWAV(SystemStringView wavPath) { athena::io::FileReader r(wavPath); @@ -263,9 +286,11 @@ void AudioGroupSampleDirectory::Entry::loadLooseData(SystemStringView basePath) { SystemString wavPath = SystemString(basePath) + _S(".wav"); SystemString dspPath = SystemString(basePath) + _S(".dsp"); - Sstat wavStat, dspStat; + SystemString vadpcmPath = SystemString(basePath) + _S(".vadpcm"); + Sstat wavStat, dspStat, vadpcmStat; bool wavValid = !Stat(wavPath.c_str(), &wavStat) && S_ISREG(wavStat.st_mode); bool dspValid = !Stat(dspPath.c_str(), &dspStat) && S_ISREG(dspStat.st_mode); + bool vadpcmValid = !Stat(vadpcmPath.c_str(), &vadpcmStat) && S_ISREG(vadpcmStat.st_mode); if (wavValid && dspValid) { @@ -274,6 +299,20 @@ void AudioGroupSampleDirectory::Entry::loadLooseData(SystemStringView basePath) else wavValid = false; } + if (wavValid && vadpcmValid) + { + if (wavStat.st_mtime > vadpcmStat.st_mtime) + vadpcmValid = false; + else + wavValid = false; + } + if (dspValid && vadpcmValid) + { + if (dspStat.st_mtime > vadpcmStat.st_mtime) + vadpcmValid = false; + else + dspValid = false; + } EntryData& curData = *m_data; @@ -283,6 +322,12 @@ void AudioGroupSampleDirectory::Entry::loadLooseData(SystemStringView basePath) m_data->loadLooseDSP(dspPath); m_data->m_looseModTime = dspStat.st_mtime; } + else if (vadpcmValid && (!curData.m_looseData || vadpcmStat.st_mtime > curData.m_looseModTime)) + { + m_data = MakeObj(); + m_data->loadLooseVADPCM(vadpcmPath); + m_data->m_looseModTime = vadpcmStat.st_mtime; + } else if (wavValid && (!curData.m_looseData || wavStat.st_mtime > curData.m_looseModTime)) { m_data = MakeObj(); @@ -295,12 +340,14 @@ SampleFileState AudioGroupSampleDirectory::Entry::getFileState(SystemStringView { SystemString wavPath = SystemString(basePath) + _S(".wav"); SystemString dspPath = SystemString(basePath) + _S(".dsp"); - Sstat wavStat, dspStat; + SystemString vadpcmPath = SystemString(basePath) + _S(".vadpcm"); + Sstat wavStat, dspStat, vadpcmStat; bool wavValid = !Stat(wavPath.c_str(), &wavStat) && S_ISREG(wavStat.st_mode); bool dspValid = !Stat(dspPath.c_str(), &dspStat) && S_ISREG(dspStat.st_mode); + bool vadpcmValid = !Stat(vadpcmPath.c_str(), &vadpcmStat) && S_ISREG(vadpcmStat.st_mode); EntryData& curData = *m_data; - if (!wavValid && !dspValid) + if (!wavValid && !dspValid && !vadpcmValid) { if (!curData.m_looseData) return SampleFileState::NoData; @@ -321,6 +368,30 @@ SampleFileState AudioGroupSampleDirectory::Entry::getFileState(SystemStringView *pathOut = dspPath; return SampleFileState::CompressedRecent; } + if (wavValid && vadpcmValid) + { + if (wavStat.st_mtime > vadpcmStat.st_mtime) + { + if (pathOut) + *pathOut = wavPath; + return SampleFileState::WAVRecent; + } + if (pathOut) + *pathOut = vadpcmPath; + return SampleFileState::CompressedRecent; + } + if (dspValid && vadpcmValid) + { + if (dspStat.st_mtime > vadpcmStat.st_mtime) + { + if (pathOut) + *pathOut = dspPath; + return SampleFileState::CompressedNoWAV; + } + if (pathOut) + *pathOut = vadpcmPath; + return SampleFileState::CompressedNoWAV; + } if (dspValid) { @@ -328,6 +399,12 @@ SampleFileState AudioGroupSampleDirectory::Entry::getFileState(SystemStringView *pathOut = dspPath; return SampleFileState::CompressedNoWAV; } + if (vadpcmValid) + { + if (pathOut) + *pathOut = vadpcmPath; + return SampleFileState::CompressedNoWAV; + } if (pathOut) *pathOut = wavPath; return SampleFileState::WAVNoCompressed; @@ -341,7 +418,7 @@ void AudioGroupSampleDirectory::EntryData::patchMetadataDSP(SystemStringView dsp DSPADPCMHeader head; head.read(r); - if (m_loopLengthSamples != 0) + if (isLooped()) { uint32_t block = getLoopStartSample() / 14; uint32_t rem = getLoopStartSample() % 14; @@ -384,6 +461,22 @@ void AudioGroupSampleDirectory::EntryData::patchMetadataDSP(SystemStringView dsp } } +void AudioGroupSampleDirectory::EntryData::patchMetadataVADPCM(SystemStringView vadpcmPath) +{ + athena::io::FileWriter w(vadpcmPath, false); + if (!w.hasError()) + { + w.seek(0, athena::Begin); + VADPCMHeader header; + header.m_pitchSampleRate = m_pitch << 24; + header.m_pitchSampleRate |= m_sampleRate & 0xffff; + header.m_numSamples = m_numSamples; + header.m_loopStartSample = m_loopStartSample; + header.m_loopLengthSamples = m_loopLengthSamples; + header.write(w); + } +} + void AudioGroupSampleDirectory::EntryData::patchMetadataWAV(SystemStringView wavPath) { athena::io::FileReader r(wavPath); @@ -445,7 +538,7 @@ void AudioGroupSampleDirectory::EntryData::patchMetadataWAV(SystemStringView wav WAVSampleChunk smpl; smpl.smplPeriod = 1000000000 / fmt.sampleRate; smpl.midiNote = m_pitch; - if (m_loopLengthSamples != 0) + if (isLooped()) { smpl.numSampleLoops = 1; smpl.additionalDataSize = 0; @@ -488,7 +581,7 @@ void AudioGroupSampleDirectory::EntryData::patchMetadataWAV(SystemStringView wav WAVSampleChunk smpl; smpl.smplPeriod = 1000000000 / fmt.sampleRate; smpl.midiNote = m_pitch; - if (m_loopLengthSamples != 0) + if (isLooped()) { smpl.numSampleLoops = 1; smpl.additionalDataSize = 0; @@ -530,9 +623,11 @@ void AudioGroupSampleDirectory::Entry::patchSampleMetadata(SystemStringView base { SystemString wavPath = SystemString(basePath) + _S(".wav"); SystemString dspPath = SystemString(basePath) + _S(".dsp"); - Sstat wavStat, dspStat; + SystemString vadpcmPath = SystemString(basePath) + _S(".vadpcm"); + Sstat wavStat, dspStat, vadpcmStat; bool wavValid = !Stat(wavPath.c_str(), &wavStat) && S_ISREG(wavStat.st_mode); bool dspValid = !Stat(dspPath.c_str(), &dspStat) && S_ISREG(dspStat.st_mode); + bool vadpcmValid = !Stat(vadpcmPath.c_str(), &vadpcmStat) && S_ISREG(vadpcmStat.st_mode); EntryData& curData = *m_data; @@ -542,6 +637,12 @@ void AudioGroupSampleDirectory::Entry::patchSampleMetadata(SystemStringView base SetAudioFileTime(wavPath, wavStat); } + if (vadpcmValid) + { + curData.patchMetadataVADPCM(vadpcmPath); + SetAudioFileTime(vadpcmPath, vadpcmStat); + } + if (dspValid) { curData.patchMetadataDSP(dspPath); @@ -559,9 +660,19 @@ AudioGroupSampleDirectory AudioGroupSampleDirectory::CreateAudioGroupSampleDirec if (ent.m_name.size() < 4) continue; SystemString baseName; + SystemString basePath; if (!CompareCaseInsensitive(ent.m_name.data() + ent.m_name.size() - 4, _S(".dsp")) || !CompareCaseInsensitive(ent.m_name.data() + ent.m_name.size() - 4, _S(".wav"))) + { baseName = SystemString(ent.m_name.begin(), ent.m_name.begin() + ent.m_name.size() - 4); + basePath = SystemString(ent.m_path.begin(), ent.m_path.begin() + ent.m_path.size() - 4); + } + else if (ent.m_name.size() > 7 && + !CompareCaseInsensitive(ent.m_name.data() + ent.m_name.size() - 7, _S(".vadpcm"))) + { + baseName = SystemString(ent.m_name.begin(), ent.m_name.begin() + ent.m_name.size() - 7); + basePath = SystemString(ent.m_path.begin(), ent.m_path.begin() + ent.m_path.size() - 7); + } else continue; @@ -574,7 +685,6 @@ AudioGroupSampleDirectory AudioGroupSampleDirectory::CreateAudioGroupSampleDirec auto& entry = ret.m_entries[sampleId]; entry = MakeObj(); - SystemString basePath = SystemString(ent.m_path.begin(), ent.m_path.begin() + ent.m_path.size() - 4); entry->loadLooseData(basePath); } @@ -598,7 +708,7 @@ void AudioGroupSampleDirectory::_extractWAV(SampleId id, const EntryData& ent, SampleFormat fmt = SampleFormat(ent.m_numSamples >> 24); uint32_t numSamples = ent.m_numSamples & 0xffffff; - if (ent.m_loopLengthSamples) + if (ent.isLooped()) { WAVHeaderLoop header; header.fmtChunk.sampleRate = ent.m_sampleRate; @@ -652,19 +762,19 @@ void AudioGroupSampleDirectory::_extractWAV(SampleId id, const EntryData& ent, else if (fmt == SampleFormat::N64) { uint32_t remSamples = numSamples; - uint32_t numFrames = (remSamples + 31) / 32; + uint32_t numFrames = (remSamples + 63) / 64; const unsigned char* cur = samp + sizeof(ADPCMParms::VADPCMParms); for (uint32_t i = 0; i < numFrames; ++i) { - int16_t decomp[32] = {}; - unsigned thisSamples = std::min(remSamples, 32u); + int16_t decomp[64] = {}; + unsigned thisSamples = std::min(remSamples, 64u); N64MusyXDecompressFrame(decomp, cur, ent.m_ADPCMParms.vadpcm.m_coefs, thisSamples); remSamples -= thisSamples; - cur += 16; + cur += 40; w.writeBytes(decomp, thisSamples * 2); } - dataLen = sizeof(ADPCMParms::VADPCMParms) + (numSamples + 31) / 32 * 16; + dataLen = sizeof(ADPCMParms::VADPCMParms) + (numSamples + 63) / 64 * 40; } else if (fmt == SampleFormat::PCM) { @@ -732,7 +842,7 @@ void AudioGroupSampleDirectory::_extractCompressed(SampleId id, const EntryData& header.x0_num_samples = numSamples; header.x4_num_nibbles = DSPSampleToNibble(numSamples); header.x8_sample_rate = ent.m_sampleRate; - header.xc_loop_flag = atUint16(ent.m_loopLengthSamples != 0); + header.xc_loop_flag = atUint16(ent.isLooped()); if (header.xc_loop_flag) { header.x10_loop_start_nibble = DSPSampleToNibble(ent.getLoopStartSample()); @@ -757,7 +867,14 @@ void AudioGroupSampleDirectory::_extractCompressed(SampleId id, const EntryData& { path += _S(".vadpcm"); athena::io::FileWriter w(path); - dataLen = sizeof(ADPCMParms::VADPCMParms) + (numSamples + 31) / 32 * 16; + VADPCMHeader header; + header.m_pitchSampleRate = ent.m_pitch << 24; + header.m_pitchSampleRate |= ent.m_sampleRate & 0xffff; + header.m_numSamples = ent.m_numSamples; + header.m_loopStartSample = ent.m_loopStartSample; + header.m_loopLengthSamples = ent.m_loopLengthSamples; + header.write(w); + dataLen = 256 + (numSamples + 63) / 64 * 40; w.writeUBytes(samp, dataLen); } else if (fmt == SampleFormat::PCM_PC || fmt == SampleFormat::PCM) @@ -779,7 +896,7 @@ void AudioGroupSampleDirectory::_extractCompressed(SampleId id, const EntryData& header.x0_num_samples = numSamples; header.x4_num_nibbles = DSPSampleToNibble(numSamples); header.x8_sample_rate = ent.m_sampleRate; - header.xc_loop_flag = atUint16(ent.m_loopLengthSamples != 0); + header.xc_loop_flag = atUint16(ent.isLooped()); header.m_pitch = ent.m_pitch; if (header.xc_loop_flag) { @@ -864,9 +981,19 @@ void AudioGroupSampleDirectory::reloadSampleData(SystemStringView groupPath) if (ent.m_name.size() < 4) continue; SystemString baseName; + SystemString basePath; if (!CompareCaseInsensitive(ent.m_name.data() + ent.m_name.size() - 4, _S(".dsp")) || !CompareCaseInsensitive(ent.m_name.data() + ent.m_name.size() - 4, _S(".wav"))) + { baseName = SystemString(ent.m_name.begin(), ent.m_name.begin() + ent.m_name.size() - 4); + basePath = SystemString(ent.m_path.begin(), ent.m_path.begin() + ent.m_path.size() - 4); + } + else if (ent.m_name.size() > 7 && + !CompareCaseInsensitive(ent.m_name.data() + ent.m_name.size() - 7, _S(".vadpcm"))) + { + baseName = SystemString(ent.m_name.begin(), ent.m_name.begin() + ent.m_name.size() - 7); + basePath = SystemString(ent.m_path.begin(), ent.m_path.begin() + ent.m_path.size() - 7); + } else continue; @@ -883,7 +1010,6 @@ void AudioGroupSampleDirectory::reloadSampleData(SystemStringView groupPath) auto& entry = m_entries[sampleId]; entry = MakeObj(); - SystemString basePath = SystemString(ent.m_path.begin(), ent.m_path.begin() + ent.m_path.size() - 4); entry->loadLooseData(basePath); } } diff --git a/lib/ContainerRegistry.cpp b/lib/ContainerRegistry.cpp index 5114fce..33b6ce0 100644 --- a/lib/ContainerRegistry.cpp +++ b/lib/ContainerRegistry.cpp @@ -1960,8 +1960,11 @@ std::vector> ContainerRegistry: { std::vector> ret; - const SystemChar* sep = std::max(StrRChr(path, _S('/')), StrRChr(path, _S('\\'))); - SystemString baseName(sep + 1, dot - sep - 1); + SystemString baseName; + if (const SystemChar* sep = std::max(StrRChr(path, _S('/')), StrRChr(path, _S('\\')))) + baseName = SystemString(sep + 1, dot - sep - 1); + else + baseName = SystemString(path, dot - path); /* Project */ SystemChar projPath[1024]; diff --git a/lib/Engine.cpp b/lib/Engine.cpp index a0e98c1..dc14cb2 100644 --- a/lib/Engine.cpp +++ b/lib/Engine.cpp @@ -441,7 +441,7 @@ void Engine::removeListener(Listener* listener) } /** Start song playing from loaded audio groups */ -ObjToken Engine::seqPlay(GroupId groupId, SongId songId, const unsigned char* arrData, ObjToken smx) +ObjToken Engine::seqPlay(GroupId groupId, SongId songId, const unsigned char* arrData, bool loop, ObjToken smx) { std::pair songGrp = _findSongGroup(groupId); if (songGrp.second) @@ -451,7 +451,7 @@ ObjToken Engine::seqPlay(GroupId groupId, SongId songId, const unsign return {}; if (arrData) - (*ret)->playSong(arrData); + (*ret)->playSong(arrData, loop); return *ret; } @@ -468,7 +468,7 @@ ObjToken Engine::seqPlay(GroupId groupId, SongId songId, const unsign } ObjToken Engine::seqPlay(const AudioGroup* group, GroupId groupId, SongId songId, - const unsigned char* arrData, ObjToken smx) + const unsigned char* arrData, bool loop, ObjToken smx) { const SongGroupIndex* sgIdx = group->getProj().getSongGroupIndex(groupId); if (sgIdx) @@ -478,7 +478,7 @@ ObjToken Engine::seqPlay(const AudioGroup* group, GroupId groupId, So return {}; if (arrData) - (*ret)->playSong(arrData); + (*ret)->playSong(arrData, loop); return *ret; } diff --git a/lib/Sequencer.cpp b/lib/Sequencer.cpp index 2ac2561..d712efd 100644 --- a/lib/Sequencer.cpp +++ b/lib/Sequencer.cpp @@ -257,7 +257,7 @@ ObjToken Sequencer::ChannelState::keyOn(uint8_t note, uint8_t velocity) if (m_parent->m_songGroup) { oid = m_page->objId; - res = (*ret)->loadPageObject(oid, m_parent->m_ticksPerSec, note, velocity, m_ctrlVals[1]); + res = (*ret)->loadPageObject(oid, m_ticksPerSec, note, velocity, m_ctrlVals[1]); } else if (m_parent->m_sfxMappings.size()) { @@ -265,7 +265,7 @@ ObjToken Sequencer::ChannelState::keyOn(uint8_t note, uint8_t velocity) const SFXGroupIndex::SFXEntry* sfxEntry = m_parent->m_sfxMappings[lookupIdx]; oid = sfxEntry->objId; note = sfxEntry->defKey; - res = (*ret)->loadPageObject(oid, m_parent->m_ticksPerSec, note, velocity, m_ctrlVals[1]); + res = (*ret)->loadPageObject(oid, m_ticksPerSec, note, velocity, m_ctrlVals[1]); } else return {}; @@ -446,7 +446,16 @@ void Sequencer::setPitchWheel(uint8_t chan, float pitchWheel) m_chanStates[chan].setPitchWheel(pitchWheel); } -void Sequencer::setTempo(double ticksPerSec) { m_ticksPerSec = ticksPerSec; } +void Sequencer::setTempo(uint8_t chan, double ticksPerSec) +{ + m_chanStates[chan].m_ticksPerSec = ticksPerSec; +} + +void Sequencer::setTempo(double ticksPerSec) +{ + for (auto& c : m_chanStates) + c.m_ticksPerSec = ticksPerSec; +} void Sequencer::ChannelState::allOff() { @@ -598,12 +607,12 @@ void Sequencer::sendMacroMessage(ObjectId macroId, int32_t val) chan.sendMacroMessage(macroId, val); } -void Sequencer::playSong(const unsigned char* arrData, bool dieOnEnd) +void Sequencer::playSong(const unsigned char* arrData, bool loop, bool dieOnEnd) { m_arrData = arrData; m_dieOnEnd = dieOnEnd; - m_songState.initialize(arrData); - setTempo(m_songState.getTempo() * 384 / 60); + m_songState.initialize(arrData, loop); + setTempo(m_songState.getInitialTempo() * 384 / 60.0); m_state = SequencerState::Playing; } diff --git a/lib/SongConverter.cpp b/lib/SongConverter.cpp index 21c457c..5c0a224 100644 --- a/lib/SongConverter.cpp +++ b/lib/SongConverter.cpp @@ -83,17 +83,28 @@ struct Event class MIDIDecoder { int m_tick = 0; - std::vector>> m_results[16]; + std::vector> m_results[16]; std::multimap m_tempos; std::array::iterator, 128> m_notes[16]; + int m_minLoopStart[16]; + int m_minLoopEnd[16]; - void _addProgramChange(int chan, int prog) + bool isEmptyIterator(int chan, std::multimap::iterator it) const + { + for (const auto& res : m_results[chan]) + if (res.end() == it) + return true; + return false; + } + + void _addRegionChange(int chan) { auto& results = m_results[chan]; + results.reserve(2); results.emplace_back(); - results.back().first = prog; - for (size_t i = 0; i < 128; ++i) - m_notes[chan][i] = results.back().second.end(); + if (results.size() == 1) + for (size_t i = 0; i < 128; ++i) + m_notes[chan][i] = results.back().end(); } uint8_t m_status = 0; @@ -125,8 +136,15 @@ class MIDIDecoder } public: + MIDIDecoder() + { + std::fill(std::begin(m_minLoopStart), std::end(m_minLoopStart), INT_MAX); + std::fill(std::begin(m_minLoopEnd), std::end(m_minLoopEnd), INT_MAX); + } + std::vector::const_iterator receiveBytes(std::vector::const_iterator begin, - std::vector::const_iterator end) + std::vector::const_iterator end, + int loopStart[16] = nullptr, int loopEnd[16] = nullptr) { std::vector::const_iterator it = begin; while (it != end) @@ -146,7 +164,7 @@ public: { /* Meta events */ if (it == end) - return begin; + break; a = *it++; uint32_t length; @@ -168,25 +186,36 @@ public: uint8_t chan = m_status & 0xf; auto& results = m_results[chan]; - /* Not actually used as such for now */ - if (results.empty()) - _addProgramChange(chan, 0); - std::multimap& res = results.back().second; + if (loopEnd && loopEnd[chan] != INT_MAX && m_tick >= loopEnd[chan]) + break; + + /* Split region at loop start point */ + if (loopStart && loopStart[chan] != INT_MAX && m_tick >= loopStart[chan]) + { + _addRegionChange(chan); + loopStart[chan] = INT_MAX; + } + else if (results.empty()) + { + _addRegionChange(chan); + } + + std::multimap& res = results.back(); switch (Status(m_status & 0xf0)) { case Status::NoteOff: { if (it == end) - return begin; + break; a = *it++; if (it == end) - return begin; + break; b = *it++; uint8_t notenum = clamp7(a); std::multimap::iterator note = m_notes[chan][notenum]; - if (note != res.end()) + if (!isEmptyIterator(chan, note)) { note->second.length = m_tick - note->first; m_notes[chan][notenum] = res.end(); @@ -196,16 +225,16 @@ public: case Status::NoteOn: { if (it == end) - return begin; + break; a = *it++; if (it == end) - return begin; + break; b = *it++; uint8_t notenum = clamp7(a); uint8_t vel = clamp7(b); std::multimap::iterator note = m_notes[chan][notenum]; - if (note != res.end()) + if (!isEmptyIterator(chan, note)) note->second.length = m_tick - note->first; if (vel != 0) @@ -218,28 +247,33 @@ public: case Status::NotePressure: { if (it == end) - return begin; + break; a = *it++; if (it == end) - return begin; + break; b = *it++; break; } case Status::ControlChange: { if (it == end) - return begin; + break; a = *it++; if (it == end) - return begin; + break; b = *it++; - res.emplace(m_tick, Event{CtrlEvent{}, chan, clamp7(a), clamp7(b), 0}); + if (a == 0x66) + m_minLoopStart[chan] = std::min(m_tick, m_minLoopStart[chan]); + else if (a == 0x67) + m_minLoopEnd[chan] = std::min(m_tick, m_minLoopEnd[chan]); + else + res.emplace(m_tick, Event{CtrlEvent{}, chan, clamp7(a), clamp7(b), 0}); break; } case Status::ProgramChange: { if (it == end) - return begin; + break; a = *it++; res.emplace(m_tick, Event{ProgEvent{}, chan, a}); break; @@ -247,17 +281,17 @@ public: case Status::ChannelPressure: { if (it == end) - return begin; + break; a = *it++; break; } case Status::PitchBend: { if (it == end) - return begin; + break; a = *it++; if (it == end) - return begin; + break; b = *it++; res.emplace(m_tick, Event{PitchEvent{}, chan, clamp7(b) * 128 + clamp7(a)}); break; @@ -270,30 +304,30 @@ public: { uint32_t len; if (!_readContinuedValue(it, end, len) || end - it < len) - return begin; + break; break; } case Status::TimecodeQuarterFrame: { if (it == end) - return begin; + break; a = *it++; break; } case Status::SongPositionPointer: { if (it == end) - return begin; + break; a = *it++; if (it == end) - return begin; + break; b = *it++; break; } case Status::SongSelect: { if (it == end) - return begin; + break; a = *it++; break; } @@ -315,11 +349,14 @@ public: } } } + return it; } - std::vector>>& getResults(int chan) { return m_results[chan]; } + std::vector>& getResults(int chan) { return m_results[chan]; } std::multimap& getTempos() { return m_tempos; } + int getMinLoopStart(int chan) const { return m_minLoopStart[chan]; } + int getMinLoopEnd(int chan) const { return m_minLoopEnd[chan]; } }; class MIDIEncoder @@ -653,7 +690,7 @@ std::vector SongConverter::SongToMIDI(const unsigned char* data, int& v ret.push_back(1); SongState song; - if (!song.initialize(data)) + if (!song.initialize(data, false)) return {}; versionOut = song.m_sngVersion; isBig = song.m_bigEndian; @@ -681,15 +718,18 @@ std::vector SongConverter::SongToMIDI(const unsigned char* data, int& v encoder.getResult().push_back(0x51); encoder.getResult().push_back(3); - uint32_t tempo24 = SBig(60000000 / song.m_tempo); + uint32_t tempo24 = SBig(60000000 / (song.m_header.m_initialTempo & 0x7fffffff)); for (int i = 1; i < 4; ++i) encoder.getResult().push_back(reinterpret_cast(&tempo24)[i]); /* Write out tempo changes */ int lastTick = 0; - while (song.m_tempoPtr && song.m_tempoPtr->m_tick != 0xffffffff) + const SongState::TempoChange* tempoPtr = nullptr; + if (song.m_header.m_tempoTableOff) + tempoPtr = reinterpret_cast(song.m_songData + song.m_header.m_tempoTableOff); + while (tempoPtr && tempoPtr->m_tick != 0xffffffff) { - SongState::TempoChange change = *song.m_tempoPtr; + SongState::TempoChange change = *tempoPtr; if (song.m_bigEndian) change.swapBig(); @@ -703,7 +743,7 @@ std::vector SongConverter::SongToMIDI(const unsigned char* data, int& v for (int i = 1; i < 4; ++i) encoder.getResult().push_back(reinterpret_cast(&tempo24)[i]); - ++song.m_tempoPtr; + ++tempoPtr; } encoder.getResult().push_back(0); @@ -721,6 +761,8 @@ std::vector SongConverter::SongToMIDI(const unsigned char* data, int& v ret.insert(ret.cend(), encoder.getResult().begin(), encoder.getResult().end()); } + bool loopsAdded = false; + /* Iterate each SNG track into type-1 MIDI track */ for (SongState::Track& trk : song.m_tracks) { @@ -733,7 +775,7 @@ std::vector SongConverter::SongToMIDI(const unsigned char* data, int& v while (trk.m_nextRegion->indexValid(song.m_bigEndian)) { std::multimap events; - trk.advanceRegion(nullptr); + trk.advanceRegion(); uint32_t regStart = song.m_bigEndian ? SBig(trk.m_curRegion->m_startTick) : trk.m_curRegion->m_startTick; @@ -900,6 +942,17 @@ std::vector SongConverter::SongToMIDI(const unsigned char* data, int& v } } + /* Add loop events */ + if (!loopsAdded && trk.m_nextRegion->indexLoop(song.m_bigEndian) != -1) + { + uint32_t loopEnd = + song.m_bigEndian ? SBig(trk.m_nextRegion->m_startTick) : trk.m_nextRegion->m_startTick; + allEvents.emplace(trk.m_loopStartTick, Event{CtrlEvent{}, trk.m_midiChan, 0x66, 0, 0}); + allEvents.emplace(loopEnd, Event{CtrlEvent{}, trk.m_midiChan, 0x67, 0, 0}); + if (!(song.m_header.m_initialTempo & 0x80000000)) + loopsAdded = true; + } + /* Emit MIDI events */ int lastTime = 0; for (auto& pair : allEvents) @@ -1021,6 +1074,55 @@ std::vector SongConverter::MIDIToSong(const std::vector& data, std::vector regions; int curRegionOff = 0; + /* Pre-iterate to extract loop events */ + int loopStart[16]; + int loopEnd[16]; + int loopChanCount = 0; + { + int loopChanIdx = -1; + for (int c = 0; c < 16; ++c) + { + loopStart[c] = INT_MAX; + loopEnd[c] = INT_MAX; + std::vector::const_iterator tmpIt = it; + for (int i = 0; i < header.count; ++i) + { + if (memcmp(&*tmpIt, "MTrk", 4)) + return {}; + tmpIt += 4; + uint32_t length = SBig(*reinterpret_cast(&*tmpIt)); + tmpIt += 4; + + std::vector::const_iterator begin = tmpIt; + std::vector::const_iterator end = tmpIt + length; + tmpIt = end; + + MIDIDecoder dec; + dec.receiveBytes(begin, end); + loopStart[c] = std::min(dec.getMinLoopStart(c), loopStart[c]); + loopEnd[c] = std::min(dec.getMinLoopEnd(c), loopEnd[c]); + } + if (loopStart[c] == INT_MAX || loopEnd[c] == INT_MAX) + { + loopStart[c] = INT_MAX; + loopEnd[c] = INT_MAX; + } + else + { + ++loopChanCount; + loopChanIdx = c; + } + } + if (loopChanCount == 1) + { + for (int c = 0; c < 16; ++c) + { + loopStart[c] = loopStart[loopChanIdx]; + loopEnd[c] = loopEnd[loopChanIdx]; + } + } + } + for (int i = 0; i < header.count; ++i) { if (memcmp(&*it, "MTrk", 4)) @@ -1069,25 +1171,29 @@ std::vector SongConverter::MIDIToSong(const std::vector& data, it = end; MIDIDecoder dec; - dec.receiveBytes(begin, end); + int tmpLoopStart[16]; + int tmpLoopEnd[16]; + std::copy(std::begin(loopStart), std::end(loopStart), std::begin(tmpLoopStart)); + std::copy(std::begin(loopEnd), std::end(loopEnd), std::begin(tmpLoopEnd)); + dec.receiveBytes(begin, end, tmpLoopStart, tmpLoopEnd); for (int c = 0; c < 16; ++c) { - std::vector>>& results = dec.getResults(c); - int lastTrackStartTick = 0; + std::vector>& results = dec.getResults(c); bool didChanInit = false; - for (auto& prog : results) + int lastEventTick = 0; + for (auto& chanRegion : results) { bool didInit = false; int startTick = 0; - int lastEventTick = 0; + lastEventTick = 0; int lastPitchTick = 0; int lastPitchVal = 0; int lastModTick = 0; int lastModVal = 0; Region region; - for (auto& event : prog.second) + for (auto& event : chanRegion) { uint32_t eventTick = event.first * 384 / header.div; @@ -1097,7 +1203,6 @@ std::vector SongConverter::MIDIToSong(const std::vector& data, { didInit = true; startTick = eventTick; - lastTrackStartTick = startTick; lastEventTick = startTick; lastPitchTick = startTick; lastPitchVal = 0; @@ -1322,7 +1427,7 @@ std::vector SongConverter::MIDIToSong(const std::vector& data, reg.m_unk1 = 0xff; reg.m_unk2 = 0; reg.m_regionIndex = SBig(uint16_t(regIdx)); - reg.m_unk3 = 0; + reg.m_loopToRegion = 0; } else { @@ -1331,7 +1436,7 @@ std::vector SongConverter::MIDIToSong(const std::vector& data, reg.m_unk1 = 0xff; reg.m_unk2 = 0; reg.m_regionIndex = uint16_t(regIdx); - reg.m_unk3 = 0; + reg.m_loopToRegion = 0; } } } @@ -1341,23 +1446,37 @@ std::vector SongConverter::MIDIToSong(const std::vector& data, /* Terminating region header */ regionBuf.emplace_back(); SongState::TrackRegion& reg = regionBuf.back(); + + uint32_t termStartTick = 0; + int16_t termRegionIdx = -1; + int16_t termLoopToRegion = 0; + if (loopEnd[c] != INT_MAX) + { + termStartTick = loopEnd[c]; + if (lastEventTick >= loopStart[c]) + { + termRegionIdx = -2; + termLoopToRegion = results.size() - 1; + } + } + if (big) { - reg.m_startTick = SBig(uint32_t(lastTrackStartTick)); + reg.m_startTick = SBig(termStartTick); reg.m_progNum = 0xff; reg.m_unk1 = 0xff; reg.m_unk2 = 0; - reg.m_regionIndex = -1; - reg.m_unk3 = 0; + reg.m_regionIndex = SBig(termRegionIdx); + reg.m_loopToRegion = SBig(termLoopToRegion); } else { - reg.m_startTick = uint32_t(lastTrackStartTick); + reg.m_startTick = termStartTick; reg.m_progNum = 0xff; reg.m_unk1 = 0xff; reg.m_unk2 = 0; - reg.m_regionIndex = -1; - reg.m_unk3 = 0; + reg.m_regionIndex = termRegionIdx; + reg.m_loopToRegion = termLoopToRegion; } } } @@ -1366,17 +1485,29 @@ std::vector SongConverter::MIDIToSong(const std::vector& data, if (version == 1) { SongState::Header head; - head.m_trackIdxOff = 0x18; - head.m_regionIdxOff = 0x18 + 4 * 64 + regionBuf.size() * 12; + head.m_initialTempo = initTempo; + head.m_loopStartTicks[0] = 0; + if (loopChanCount == 1) + { + head.m_loopStartTicks[0] = loopStart[0] == INT_MAX ? 0 : loopStart[0]; + } + else if (loopChanCount > 1) + { + for (int i = 0; i < 16; ++i) + head.m_loopStartTicks[i] = loopStart[i] == INT_MAX ? 0 : loopStart[i]; + head.m_initialTempo |= 0x80000000; + } + size_t headSz = (head.m_initialTempo & 0x80000000) ? 0x58 : 0x18; + head.m_trackIdxOff = headSz; + head.m_regionIdxOff = headSz + 4 * 64 + regionBuf.size() * 12; head.m_chanMapOff = head.m_regionIdxOff + 4 * regionDataIdxArr.size() + curRegionOff; head.m_tempoTableOff = tempoBuf.size() ? head.m_chanMapOff + 64 : 0; - head.m_initialTempo = initTempo; - head.m_unkOff = 0; + head.m_chanMapOff2 = head.m_chanMapOff; uint32_t regIdxOff = head.m_regionIdxOff; if (big) - head.swapBig(); - *reinterpret_cast(&*ret.insert(ret.cend(), 0x18, 0)) = head; + head.swapToBig(); + *reinterpret_cast(&*ret.insert(ret.cend(), headSz, 0)) = head; for (int i = 0; i < 64; ++i) { @@ -1388,7 +1519,7 @@ std::vector SongConverter::MIDIToSong(const std::vector& data, uint32_t idx = trackRegionIdxArr[i]; *reinterpret_cast(&*ret.insert(ret.cend(), 4, 0)) = - big ? SBig(uint32_t(0x18 + 4 * 64 + idx * 12)) : uint32_t(0x18 + 4 * 64 + idx * 12); + big ? SBig(uint32_t(headSz + 4 * 64 + idx * 12)) : uint32_t(headSz + 4 * 64 + idx * 12); } for (SongState::TrackRegion& reg : regionBuf) @@ -1442,17 +1573,29 @@ std::vector SongConverter::MIDIToSong(const std::vector& data, else { SongState::Header head; - head.m_trackIdxOff = 0x18 + regionBuf.size() * 12; + head.m_initialTempo = initTempo; + head.m_loopStartTicks[0] = 0; + if (loopChanCount == 1) + { + head.m_loopStartTicks[0] = loopStart[0] == INT_MAX ? 0 : loopStart[0]; + } + else if (loopChanCount > 1) + { + for (int i = 0; i < 16; ++i) + head.m_loopStartTicks[i] = loopStart[i] == INT_MAX ? 0 : loopStart[i]; + head.m_initialTempo |= 0x80000000; + } + size_t headSz = (head.m_initialTempo & 0x80000000) ? 0x58 : 0x18; + head.m_trackIdxOff = headSz + regionBuf.size() * 12; head.m_regionIdxOff = head.m_trackIdxOff + 4 * 64 + 64 + curRegionOff; head.m_chanMapOff = head.m_trackIdxOff + 4 * 64; head.m_tempoTableOff = tempoBuf.size() ? head.m_regionIdxOff + 4 * regionDataIdxArr.size() : 0; - head.m_initialTempo = initTempo; - head.m_unkOff = 0; + head.m_chanMapOff2 = head.m_chanMapOff; uint32_t chanMapOff = head.m_chanMapOff; if (big) - head.swapBig(); - *reinterpret_cast(&*ret.insert(ret.cend(), 0x18, 0)) = head; + head.swapToBig(); + *reinterpret_cast(&*ret.insert(ret.cend(), headSz, 0)) = head; for (SongState::TrackRegion& reg : regionBuf) *reinterpret_cast(&*ret.insert(ret.cend(), 12, 0)) = reg; @@ -1467,7 +1610,7 @@ std::vector SongConverter::MIDIToSong(const std::vector& data, uint32_t idx = trackRegionIdxArr[i]; *reinterpret_cast(&*ret.insert(ret.cend(), 4, 0)) = - big ? SBig(uint32_t(0x18 + 4 * 64 + idx * 12)) : uint32_t(0x18 + 4 * 64 + idx * 12); + big ? SBig(uint32_t(headSz + 4 * 64 + idx * 12)) : uint32_t(headSz + 4 * 64 + idx * 12); } memmove(&*ret.insert(ret.cend(), 64, 0), chanMap.data(), 64); diff --git a/lib/SongState.cpp b/lib/SongState.cpp index f5afc44..f134ee4 100644 --- a/lib/SongState.cpp +++ b/lib/SongState.cpp @@ -75,14 +75,68 @@ static uint32_t DecodeTime(const unsigned char*& data) return ret; } -void SongState::Header::swapBig() +void SongState::Header::swapFromBig() { m_trackIdxOff = SBig(m_trackIdxOff); m_regionIdxOff = SBig(m_regionIdxOff); m_chanMapOff = SBig(m_chanMapOff); m_tempoTableOff = SBig(m_tempoTableOff); m_initialTempo = SBig(m_initialTempo); - m_unkOff = SBig(m_unkOff); + if (m_initialTempo & 0x80000000) + { + for (int i = 0; i < 16; ++i) + m_loopStartTicks[i] = SBig(m_loopStartTicks[i]); + m_chanMapOff2 = SBig(m_chanMapOff2); + } + else + { + m_loopStartTicks[0] = SBig(m_loopStartTicks[0]); + } +} + +void SongState::Header::swapToBig() +{ + m_trackIdxOff = SBig(m_trackIdxOff); + m_regionIdxOff = SBig(m_regionIdxOff); + m_chanMapOff = SBig(m_chanMapOff); + m_tempoTableOff = SBig(m_tempoTableOff); + m_initialTempo = SBig(m_initialTempo); + if (m_initialTempo & 0x00000080) + { + for (int i = 0; i < 16; ++i) + m_loopStartTicks[i] = SBig(m_loopStartTicks[i]); + m_chanMapOff2 = SBig(m_chanMapOff2); + } + else + { + m_loopStartTicks[0] = SBig(m_loopStartTicks[0]); + } +} + +SongState::Header& SongState::Header::operator=(const Header& other) +{ + m_trackIdxOff = other.m_trackIdxOff; + m_regionIdxOff = other.m_regionIdxOff; + m_chanMapOff = other.m_chanMapOff; + m_tempoTableOff = other.m_tempoTableOff; + m_initialTempo = other.m_initialTempo; + if (SBig(m_initialTempo) & 0x80000000) + { + for (int i = 0; i < 16; ++i) + m_loopStartTicks[i] = other.m_loopStartTicks[i]; + m_chanMapOff2 = other.m_chanMapOff2; + } + else + { + m_loopStartTicks[0] = other.m_loopStartTicks[0]; + } + return *this; +} + +bool SongState::TrackRegion::indexDone(bool bigEndian, bool loop) const +{ + int16_t idx = (bigEndian ? SBig(m_regionIndex) : m_regionIndex); + return loop ? (idx == -1) : (idx < 0); } bool SongState::TrackRegion::indexValid(bool bigEndian) const @@ -90,6 +144,13 @@ bool SongState::TrackRegion::indexValid(bool bigEndian) const return (bigEndian ? SBig(m_regionIndex) : m_regionIndex) >= 0; } +int SongState::TrackRegion::indexLoop(bool bigEndian) const +{ + if ((bigEndian ? SBig(m_regionIndex) : m_regionIndex) != -2) + return -1; + return (bigEndian ? SBig(m_loopToRegion) : m_loopToRegion); +} + void SongState::TempoChange::swapBig() { m_tick = SBig(m_tick); @@ -103,12 +164,14 @@ void SongState::Track::Header::swapBig() m_modOff = SBig(m_modOff); } -SongState::Track::Track(SongState& parent, uint8_t midiChan, const TrackRegion* regions) -: m_parent(&parent), m_midiChan(midiChan), m_curRegion(nullptr), m_nextRegion(regions) +SongState::Track::Track(SongState& parent, uint8_t midiChan, uint32_t loopStart, const TrackRegion* regions, uint32_t tempo) +: m_parent(&parent), m_midiChan(midiChan), m_initRegion(regions), m_curRegion(nullptr), + m_nextRegion(regions), m_loopStartTick(loopStart), m_tempo(tempo) { + resetTempo(); } -void SongState::Track::setRegion(Sequencer* seq, const TrackRegion* region) +void SongState::Track::setRegion(const TrackRegion* region) { m_curRegion = region; uint32_t regionIdx = (m_parent->m_bigEndian ? SBig(m_curRegion->m_regionIndex) : m_curRegion->m_regionIndex); @@ -125,13 +188,14 @@ void SongState::Track::setRegion(Sequencer* seq, const TrackRegion* region) m_pitchWheelData = nullptr; m_nextPitchTick = 0x7fffffff; m_nextPitchDelta = 0; + m_pitchVal = 0; if (header.m_pitchOff) { m_pitchWheelData = m_parent->m_songData + header.m_pitchOff; if (m_pitchWheelData[0] != 0x80 || m_pitchWheelData[1] != 0x00) { auto delta = DecodeDelta(m_pitchWheelData); - m_nextPitchTick = m_parent->m_curTick + delta.first; + m_nextPitchTick = m_curTick + delta.first; m_nextPitchDelta = delta.second; } } @@ -139,27 +203,23 @@ void SongState::Track::setRegion(Sequencer* seq, const TrackRegion* region) m_modWheelData = nullptr; m_nextModTick = 0x7fffffff; m_nextModDelta = 0; + m_modVal = 0; if (header.m_modOff) { m_modWheelData = m_parent->m_songData + header.m_modOff; if (m_modWheelData[0] != 0x80 || m_modWheelData[1] != 0x00) { auto delta = DecodeDelta(m_modWheelData); - m_nextModTick = m_parent->m_curTick + delta.first; + m_nextModTick = m_curTick + delta.first; m_nextModDelta = delta.second; } } m_eventWaitCountdown = 0; - m_pitchVal = 0; - m_modVal = 0; - if (seq) - { - seq->setPitchWheel(m_midiChan, clamp(-1.f, m_pitchVal / 32768.f, 1.f)); - seq->setCtrlValue(m_midiChan, 1, clamp(0, m_modVal * 128 / 16384, 127)); - } if (m_parent->m_sngVersion == 1) + { m_eventWaitCountdown = int32_t(DecodeTime(m_data)); + } else { int32_t absTick = (m_parent->m_bigEndian ? SBig(*reinterpret_cast(m_data)) @@ -170,14 +230,14 @@ void SongState::Track::setRegion(Sequencer* seq, const TrackRegion* region) } } -void SongState::Track::advanceRegion(Sequencer* seq) { setRegion(seq, m_nextRegion); } +void SongState::Track::advanceRegion() { setRegion(m_nextRegion); } int SongState::DetectVersion(const unsigned char* ptr, bool& isBig) { isBig = ptr[0] == 0; Header header = *reinterpret_cast(ptr); if (isBig) - header.swapBig(); + header.swapFromBig(); const uint32_t* trackIdx = reinterpret_cast(ptr + header.m_trackIdxOff); const uint32_t* regionIdxTable = reinterpret_cast(ptr + header.m_regionIdxOff); @@ -347,8 +407,9 @@ int SongState::DetectVersion(const unsigned char* ptr, bool& isBig) return v; } -bool SongState::initialize(const unsigned char* ptr) +bool SongState::initialize(const unsigned char* ptr, bool loop) { + m_loop = loop; m_sngVersion = DetectVersion(ptr, m_bigEndian); if (m_sngVersion < 0) return false; @@ -356,7 +417,7 @@ bool SongState::initialize(const unsigned char* ptr) m_songData = ptr; m_header = *reinterpret_cast(ptr); if (m_bigEndian) - m_header.swapBig(); + m_header.swapFromBig(); const uint32_t* trackIdx = reinterpret_cast(ptr + m_header.m_trackIdxOff); m_regionIdx = reinterpret_cast(ptr + m_header.m_regionIdxOff); const uint8_t* chanMap = reinterpret_cast(ptr + m_header.m_chanMapOff); @@ -368,35 +429,69 @@ bool SongState::initialize(const unsigned char* ptr) { const TrackRegion* region = reinterpret_cast(ptr + (m_bigEndian ? SBig(trackIdx[i]) : trackIdx[i])); - m_tracks[i] = Track(*this, chanMap[i], region); + uint8_t chan = chanMap[i]; + uint32_t loopStart = + (m_header.m_initialTempo & 0x80000000) ? m_header.m_loopStartTicks[chan] : m_header.m_loopStartTicks[0]; + m_tracks[i] = Track(*this, chan, loopStart, region, m_header.m_initialTempo & 0x7fffffff); } else m_tracks[i] = Track(); } - /* Initialize tempo */ - if (m_header.m_tempoTableOff) - m_tempoPtr = reinterpret_cast(ptr + m_header.m_tempoTableOff); - else - m_tempoPtr = nullptr; - - m_tempo = m_header.m_initialTempo & 0x7fffffff; - m_curTick = 0; m_songState = SongPlayState::Playing; return true; } -bool SongState::Track::advance(Sequencer& seq, int32_t ticks) +void SongState::Track::resetTempo() { - int32_t endTick = m_parent->m_curTick + ticks; + if (m_parent->m_header.m_tempoTableOff) + m_tempoPtr = reinterpret_cast(m_parent->m_songData + m_parent->m_header.m_tempoTableOff); + else + m_tempoPtr = nullptr; +} + +bool SongState::Track::advance(Sequencer& seq, double dt) +{ + m_remDt += dt; + + /* Compute ticks to compute based on current tempo */ + double ticksPerSecond = m_tempo * 384 / 60; + uint32_t ticks = uint32_t(std::floor(m_remDt * ticksPerSecond)); + + /* See if there's an upcoming tempo change in this interval */ + while (m_tempoPtr && m_tempoPtr->m_tick != 0xffffffff) + { + TempoChange change = *m_tempoPtr; + if (m_parent->m_bigEndian) + change.swapBig(); + + if (m_curTick + ticks > change.m_tick) + ticks = change.m_tick - m_curTick; + + if (ticks <= 0) + { + /* Turn over tempo */ + m_tempo = change.m_tempo & 0x7fffffff; + ticksPerSecond = m_tempo * 384 / 60; + ticks = uint32_t(std::floor(m_remDt * ticksPerSecond)); + seq.setTempo(m_midiChan, m_tempo * 384 / 60.0); + ++m_tempoPtr; + continue; + } + break; + } + + m_remDt -= ticks / ticksPerSecond; + uint32_t endTick = m_curTick + ticks; /* Advance region if needed */ while (m_nextRegion->indexValid(m_parent->m_bigEndian)) { - uint32_t nextRegTick = (m_parent->m_bigEndian ? SBig(m_nextRegion->m_startTick) : m_nextRegion->m_startTick); + uint32_t nextRegTick = (m_parent->m_bigEndian ? + SBig(m_nextRegion->m_startTick) : m_nextRegion->m_startTick); if (uint32_t(endTick) > nextRegTick) - advanceRegion(&seq); + advanceRegion(); else break; } @@ -412,184 +507,208 @@ bool SongState::Track::advance(Sequencer& seq, int32_t ticks) } } - if (!m_data) - return !m_nextRegion->indexValid(m_parent->m_bigEndian); - - /* Update continuous pitch data */ - if (m_pitchWheelData) + if (m_data) { - int32_t pitchTick = m_parent->m_curTick; - int32_t remPitchTicks = ticks; - while (pitchTick < endTick) + /* Update continuous pitch data */ + if (m_pitchWheelData) { - /* See if there's an upcoming pitch change in this interval */ - int32_t nextTick = m_nextPitchTick; - if (pitchTick + remPitchTicks > nextTick) + int32_t pitchTick = m_curTick; + int32_t remPitchTicks = ticks; + while (pitchTick < endTick) { - /* Update pitch */ - m_pitchVal += m_nextPitchDelta; - seq.setPitchWheel(m_midiChan, clamp(-1.f, m_pitchVal / 8191.f, 1.f)); - if (m_pitchWheelData[0] != 0x80 || m_pitchWheelData[1] != 0x00) + /* See if there's an upcoming pitch change in this interval */ + int32_t nextTick = m_nextPitchTick; + if (pitchTick + remPitchTicks > nextTick) { - auto delta = DecodeDelta(m_pitchWheelData); - m_nextPitchTick += delta.first; - m_nextPitchDelta = delta.second; + /* Update pitch */ + m_pitchVal += m_nextPitchDelta; + seq.setPitchWheel(m_midiChan, clamp(-1.f, m_pitchVal / 8191.f, 1.f)); + if (m_pitchWheelData[0] != 0x80 || m_pitchWheelData[1] != 0x00) + { + auto delta = DecodeDelta(m_pitchWheelData); + m_nextPitchTick += delta.first; + m_nextPitchDelta = delta.second; + } + else + { + m_nextPitchTick = 0x7fffffff; + } + } + remPitchTicks -= (nextTick - pitchTick); + pitchTick = nextTick; + } + } + + /* Update continuous modulation data */ + if (m_modWheelData) + { + int32_t modTick = m_curTick; + int32_t remModTicks = ticks; + while (modTick < endTick) + { + /* See if there's an upcoming modulation change in this interval */ + int32_t nextTick = m_nextModTick; + if (modTick + remModTicks > nextTick) + { + /* Update modulation */ + m_modVal += m_nextModDelta; + seq.setCtrlValue(m_midiChan, 1, int8_t(clamp(0, m_modVal / 127, 127))); + if (m_modWheelData[0] != 0x80 || m_modWheelData[1] != 0x00) + { + auto delta = DecodeDelta(m_modWheelData); + m_nextModTick += delta.first; + m_nextModDelta = delta.second; + } + else + { + m_nextModTick = 0x7fffffff; + } + } + remModTicks -= (nextTick - modTick); + modTick = nextTick; + } + } + + /* Loop through as many commands as we can for this time period */ + if (m_parent->m_sngVersion == 1) + { + /* Revision */ + while (true) + { + /* Advance wait timer if active, returning if waiting */ + if (m_eventWaitCountdown) + { + m_eventWaitCountdown -= ticks; + ticks = 0; + if (m_eventWaitCountdown > 0) + break; + } + + /* Load next command */ + if (*reinterpret_cast(m_data) == 0xffff) + { + /* End of channel */ + m_data = nullptr; + break; + } + else if (m_data[0] & 0x80 && m_data[1] & 0x80) + { + /* Control change */ + uint8_t val = m_data[0] & 0x7f; + uint8_t ctrl = m_data[1] & 0x7f; + seq.setCtrlValue(m_midiChan, ctrl, val); + m_data += 2; + } + else if (m_data[0] & 0x80) + { + /* Program change */ + uint8_t prog = m_data[0] & 0x7f; + seq.setChanProgram(m_midiChan, prog); + m_data += 2; } else - { - m_nextPitchTick = 0x7fffffff; - } - } - remPitchTicks -= (nextTick - pitchTick); - pitchTick = nextTick; - } - } - - /* Update continuous modulation data */ - if (m_modWheelData) - { - int32_t modTick = m_parent->m_curTick; - int32_t remModTicks = ticks; - while (modTick < endTick) - { - /* See if there's an upcoming modulation change in this interval */ - int32_t nextTick = m_nextModTick; - if (modTick + remModTicks > nextTick) - { - /* Update modulation */ - m_modVal += m_nextModDelta; - seq.setCtrlValue(m_midiChan, 1, clamp(0, m_modVal / 128, 127)); - if (m_modWheelData[0] != 0x80 || m_modWheelData[1] != 0x00) - { - auto delta = DecodeDelta(m_modWheelData); - m_nextModTick += delta.first; - m_nextModDelta = delta.second; - } - else - { - m_nextModTick = 0x7fffffff; - } - } - remModTicks -= (nextTick - modTick); - modTick = nextTick; - } - } - - /* Loop through as many commands as we can for this time period */ - if (m_parent->m_sngVersion == 1) - { - /* Revision */ - while (true) - { - /* Advance wait timer if active, returning if waiting */ - if (m_eventWaitCountdown) - { - m_eventWaitCountdown -= ticks; - ticks = 0; - if (m_eventWaitCountdown > 0) - return false; - } - - /* Load next command */ - if (*reinterpret_cast(m_data) == 0xffff) - { - /* End of channel */ - m_data = nullptr; - return !m_nextRegion->indexValid(m_parent->m_bigEndian); - } - else if (m_data[0] & 0x80 && m_data[1] & 0x80) - { - /* Control change */ - uint8_t val = m_data[0] & 0x7f; - uint8_t ctrl = m_data[1] & 0x7f; - seq.setCtrlValue(m_midiChan, ctrl, val); - m_data += 2; - } - else if (m_data[0] & 0x80) - { - /* Program change */ - uint8_t prog = m_data[0] & 0x7f; - seq.setChanProgram(m_midiChan, prog); - m_data += 2; - } - else - { - /* Note */ - uint8_t note = m_data[0] & 0x7f; - uint8_t vel = m_data[1] & 0x7f; - uint16_t length = (m_parent->m_bigEndian ? SBig(*reinterpret_cast(m_data + 2)) - : *reinterpret_cast(m_data + 2)); - seq.keyOn(m_midiChan, note, vel); - if (length == 0) - seq.keyOff(m_midiChan, note, 0); - m_remNoteLengths[note] = length; - m_data += 4; - } - - /* Set next delta-time */ - m_eventWaitCountdown += int32_t(DecodeTime(m_data)); - } - } - else - { - /* Legacy */ - while (true) - { - /* Advance wait timer if active, returning if waiting */ - if (m_eventWaitCountdown) - { - m_eventWaitCountdown -= ticks; - ticks = 0; - if (m_eventWaitCountdown > 0) - return false; - } - - /* Load next command */ - if (*reinterpret_cast(&m_data[2]) == 0xffff) - { - /* End of channel */ - m_data = nullptr; - return !m_nextRegion->indexValid(m_parent->m_bigEndian); - } - else - { - if ((m_data[2] & 0x80) != 0x80) { /* Note */ - uint16_t length = (m_parent->m_bigEndian ? SBig(*reinterpret_cast(m_data)) - : *reinterpret_cast(m_data)); - uint8_t note = m_data[2] & 0x7f; - uint8_t vel = m_data[3] & 0x7f; + uint8_t note = m_data[0] & 0x7f; + uint8_t vel = m_data[1] & 0x7f; + uint16_t length = (m_parent->m_bigEndian ? SBig(*reinterpret_cast(m_data + 2)) + : *reinterpret_cast(m_data + 2)); seq.keyOn(m_midiChan, note, vel); if (length == 0) seq.keyOff(m_midiChan, note, 0); m_remNoteLengths[note] = length; + m_data += 4; } - else if (m_data[2] & 0x80 && m_data[3] & 0x80) + + /* Set next delta-time */ + m_eventWaitCountdown += int32_t(DecodeTime(m_data)); + } + } else + { + /* Legacy */ + while (true) + { + /* Advance wait timer if active, returning if waiting */ + if (m_eventWaitCountdown) { - /* Control change */ - uint8_t val = m_data[2] & 0x7f; - uint8_t ctrl = m_data[3] & 0x7f; - seq.setCtrlValue(m_midiChan, ctrl, val); + m_eventWaitCountdown -= ticks; + ticks = 0; + if (m_eventWaitCountdown > 0) + break; } - else if (m_data[2] & 0x80) + + /* Load next command */ + if (*reinterpret_cast(&m_data[2]) == 0xffff) { - /* Program change */ - uint8_t prog = m_data[2] & 0x7f; - seq.setChanProgram(m_midiChan, prog); + /* End of channel */ + m_data = nullptr; + break; } + else + { + if ((m_data[2] & 0x80) != 0x80) + { + /* Note */ + uint16_t length = (m_parent->m_bigEndian ? SBig(*reinterpret_cast(m_data)) + : *reinterpret_cast(m_data)); + uint8_t note = m_data[2] & 0x7f; + uint8_t vel = m_data[3] & 0x7f; + seq.keyOn(m_midiChan, note, vel); + if (length == 0) + seq.keyOff(m_midiChan, note, 0); + m_remNoteLengths[note] = length; + } + else if (m_data[2] & 0x80 && m_data[3] & 0x80) + { + /* Control change */ + uint8_t val = m_data[2] & 0x7f; + uint8_t ctrl = m_data[3] & 0x7f; + seq.setCtrlValue(m_midiChan, ctrl, val); + } + else if (m_data[2] & 0x80) + { + /* Program change */ + uint8_t prog = m_data[2] & 0x7f; + seq.setChanProgram(m_midiChan, prog); + } + m_data += 4; + } + + /* Set next delta-time */ + int32_t absTick = (m_parent->m_bigEndian ? SBig(*reinterpret_cast(m_data)) + : *reinterpret_cast(m_data)); + m_eventWaitCountdown += absTick - m_lastN64EventTick; + m_lastN64EventTick = absTick; m_data += 4; } - - /* Set next delta-time */ - int32_t absTick = (m_parent->m_bigEndian ? SBig(*reinterpret_cast(m_data)) - : *reinterpret_cast(m_data)); - m_eventWaitCountdown += absTick - m_lastN64EventTick; - m_lastN64EventTick = absTick; - m_data += 4; } } + m_curTick = endTick; + + /* Handle loop end */ + if (m_parent->m_loop) + { + int loopTo; + if ((loopTo = m_nextRegion->indexLoop(m_parent->m_bigEndian)) != -1) + { + uint32_t loopEndTick = (m_parent->m_bigEndian ? + SBig(m_nextRegion->m_startTick) : m_nextRegion->m_startTick); + if (uint32_t(endTick) > loopEndTick) + { + m_nextRegion = &m_initRegion[loopTo]; + m_curRegion = nullptr; + m_data = nullptr; + m_curTick = m_loopStartTick; + resetTempo(); + return false; + } + } + } + + if (!m_data) + return m_nextRegion->indexDone(m_parent->m_bigEndian, m_parent->m_loop); + return false; } @@ -599,50 +718,11 @@ bool SongState::advance(Sequencer& seq, double dt) if (m_songState == SongPlayState::Stopped) return true; - bool done = false; - m_curDt += dt; - while (m_curDt > 0.0) - { - done = true; - - /* Compute ticks to compute based on current tempo */ - double ticksPerSecond = m_tempo * 384 / 60; - int32_t remTicks = std::ceil(m_curDt * ticksPerSecond); - if (!remTicks) - break; - - /* See if there's an upcoming tempo change in this interval */ - if (m_tempoPtr && m_tempoPtr->m_tick != 0xffffffff) - { - TempoChange change = *m_tempoPtr; - if (m_bigEndian) - change.swapBig(); - - if (m_curTick + remTicks > change.m_tick) - remTicks = change.m_tick - m_curTick; - - if (remTicks <= 0) - { - /* Turn over tempo */ - m_tempo = change.m_tempo & 0x7fffffff; - seq.setTempo(m_tempo * 384 / 60); - ++m_tempoPtr; - continue; - } - } - - /* Advance all tracks */ - for (Track& trk : m_tracks) - if (trk) - done &= trk.advance(seq, remTicks); - - m_curTick += remTicks; - - if (m_tempo == 0) - m_curDt = 0.0; - else - m_curDt -= remTicks / ticksPerSecond; - } + /* Advance all tracks */ + bool done = true; + for (Track& trk : m_tracks) + if (trk) + done &= trk.advance(seq, dt); if (done) m_songState = SongPlayState::Stopped; diff --git a/lib/Voice.cpp b/lib/Voice.cpp index f0c0aa4..dd93aa7 100644 --- a/lib/Voice.cpp +++ b/lib/Voice.cpp @@ -84,7 +84,7 @@ bool Voice::_checkSamplePos(bool& looped) if (m_curSamplePos >= m_lastSamplePos) { - if (m_curSample->m_loopLengthSamples) + if (m_curSample->isLooped()) { /* Turn over looped sample */ m_curSamplePos = m_curSample->m_loopStartSample; @@ -493,7 +493,10 @@ void Voice::preSupplyAudio(double dt) m_vibratoTime += dt; float vibrato = TriangleWave(m_vibratoTime / m_vibratoPeriod); if (m_vibratoModWheel) - newPitch += m_vibratoModLevel * vibrato * (m_state.m_curMod / 127.f); + { + int32_t range = m_vibratoModLevel ? m_vibratoModLevel : m_vibratoLevel; + newPitch += range * vibrato * (m_state.m_curMod / 127.f); + } else newPitch += m_vibratoLevel * vibrato; refresh = true; @@ -1002,7 +1005,7 @@ void Voice::startSample(SampleId sampId, int32_t offset) int32_t numSamples = m_curSample->getNumSamples(); if (offset) { - if (m_curSample->m_loopLengthSamples) + if (m_curSample->isLooped()) { if (offset > int32_t(m_curSample->m_loopStartSample)) offset = @@ -1020,9 +1023,8 @@ void Voice::startSample(SampleId sampId, int32_t offset) if (m_curFormat == SampleFormat::DSP_DRUM) m_curFormat = SampleFormat::DSP; - m_lastSamplePos = m_curSample->m_loopLengthSamples - ? (m_curSample->m_loopStartSample + m_curSample->m_loopLengthSamples) - : numSamples; + m_lastSamplePos = m_curSample->isLooped() + ? (m_curSample->m_loopStartSample + m_curSample->m_loopLengthSamples) : numSamples; if (m_lastSamplePos) --m_lastSamplePos;