diff --git a/sources/Adapters/picoTracker/system/picoTrackerSamplePool.cpp b/sources/Adapters/picoTracker/system/picoTrackerSamplePool.cpp index d233466f8..9d78e68db 100644 --- a/sources/Adapters/picoTracker/system/picoTrackerSamplePool.cpp +++ b/sources/Adapters/picoTracker/system/picoTrackerSamplePool.cpp @@ -162,7 +162,6 @@ bool picoTrackerSamplePool::LoadInFlash(WavFile *wave) { uint32_t sectorsToErase = ((additionalData / FLASH_SECTOR_SIZE) + ((additionalData % FLASH_SECTOR_SIZE) != 0)) * FLASH_SECTOR_SIZE; - // Erase required number of sectors flash_range_erase(flashEraseOffset_, sectorsToErase); // Move erase pointer to new position diff --git a/sources/Application/AppWindow.cpp b/sources/Application/AppWindow.cpp index c6f6129f8..a87f143b0 100644 --- a/sources/Application/AppWindow.cpp +++ b/sources/Application/AppWindow.cpp @@ -30,6 +30,7 @@ #include "Application/Views/ProjectView.h" #include "Application/Views/RecordView.h" #include "Application/Views/SampleEditorView.h" +#include "Application/Views/SampleSlicesView.h" #include "Application/Views/SelectProjectView.h" #include "Application/Views/SongView.h" #include "Application/Views/TableView.h" @@ -130,6 +131,7 @@ AppWindow::AppWindow(I_GUIWindowImp &imp) : GUIWindow(imp) { _tableView = 0; _mixerView = 0; _sampleEditorView = 0; + _sampleSlicesView = 0; _recordView = 0; _nullView = 0; _grooveView = 0; @@ -482,6 +484,12 @@ AppWindow::LoadProjectResult AppWindow::LoadProject(const char *projectName) { new (sampleEditorViewMemBuf) SampleEditorView((*this), _viewData); _sampleEditorView->AddObserver((*this)); + alignas(SampleSlicesView) static char + sampleSlicesViewMemBuf[sizeof(SampleSlicesView)]; + _sampleSlicesView = + new (sampleSlicesViewMemBuf) SampleSlicesView((*this), _viewData); + _sampleSlicesView->AddObserver((*this)); + alignas(RecordView) static char recordViewMemBuf[sizeof(RecordView)]; _recordView = new (recordViewMemBuf) RecordView((*this), _viewData); _recordView->AddObserver((*this)); @@ -529,6 +537,10 @@ void AppWindow::CloseProject() { SAFE_DELETE(_instrumentView); SAFE_DELETE(_tableView); SAFE_DELETE(_grooveView); + SAFE_DELETE(_mixerView); + SAFE_DELETE(_sampleEditorView); + SAFE_DELETE(_sampleSlicesView); + SAFE_DELETE(_recordView); UIController *controller = UIController::GetInstance(); controller->Reset(); @@ -839,6 +851,9 @@ void AppWindow::Update(Observable &o, I_ObservableData *d) { case VT_SAMPLE_EDITOR: _currentView = _sampleEditorView; break; + case VT_SAMPLE_SLICES: + _currentView = _sampleSlicesView; + break; case VT_RECORD: _currentView = _recordView; break; diff --git a/sources/Application/AppWindow.h b/sources/Application/AppWindow.h index c695b019c..79e0bbc1a 100644 --- a/sources/Application/AppWindow.h +++ b/sources/Application/AppWindow.h @@ -52,6 +52,7 @@ class MixerView; class ThemeView; class ThemeImportView; class SampleEditorView; +class SampleSlicesView; class RecordView; class View; @@ -126,6 +127,7 @@ class AppWindow : public GUIWindow, I_Observer, Status { MixerView *_mixerView; SelectProjectView *_selectProjectView; SampleEditorView *_sampleEditorView; + SampleSlicesView *_sampleSlicesView; RecordView *_recordView; NullView *_nullView; diff --git a/sources/Application/Instruments/InstrumentBank.cpp b/sources/Application/Instruments/InstrumentBank.cpp index dcac03b5a..471cbd6cb 100644 --- a/sources/Application/Instruments/InstrumentBank.cpp +++ b/sources/Application/Instruments/InstrumentBank.cpp @@ -70,29 +70,38 @@ void InstrumentBank::RestoreContent(PersistencyDocument *doc) { if (!strcasecmp(doc->ElemName(), "INSTRUMENT")) { // Get the instrument ID unsigned char id = '\0'; - char *instype = NULL; + char instype[16]; + instype[0] = '\0'; + bool hasId = false; + bool hasType = false; bool hasAttr = doc->NextAttribute(); while (hasAttr) { if (!strcasecmp(doc->attrname_, "ID")) { unsigned char b1 = (c2h__(doc->attrval_[0])) << 4; unsigned char b2 = c2h__(doc->attrval_[1]); id = b1 + b2; + hasId = true; #if XML_DEBUG_LOGGING Trace::Log("INSTRUMENTBANK", "instrument ID from xml:%d", id); #endif } if (!strcasecmp(doc->attrname_, "TYPE")) { - instype = doc->attrval_; + strncpy(instype, doc->attrval_, sizeof(instype) - 1); + instype[sizeof(instype) - 1] = '\0'; + hasType = true; #if XML_DEBUG_LOGGING Trace::Log("INSTRUMENTBANK", "instrument type from xml:%s", instype); #endif } + if (hasId && hasType) { + break; + } hasAttr = doc->NextAttribute(); } InstrumentType instrType = IT_SAMPLE; // default if no type in project XML - if (instype) { - for (uint i = 0; i < sizeof(InstrumentTypeNames); i++) { + if (instype[0] != '\0') { + for (uint i = 0; i < IT_LAST; i++) { if (!strcasecmp(instype, InstrumentTypeNames[i])) { instrType = (InstrumentType)i; break; diff --git a/sources/Application/Instruments/SampleInstrument.cpp b/sources/Application/Instruments/SampleInstrument.cpp index 1d0f72a03..da6a6ccd3 100644 --- a/sources/Application/Instruments/SampleInstrument.cpp +++ b/sources/Application/Instruments/SampleInstrument.cpp @@ -21,8 +21,11 @@ #include "System/io/Status.h" #include +#include "System/Console/nanoprintf.h" +#include #include #include +#include #include #include "Application/Player/SyncMaster.h" @@ -99,10 +102,177 @@ SampleInstrument::SampleInstrument() variables_.insert(variables_.end(), &tableAuto_); tableState_.Reset(); + slicePoints_.fill(0); } SampleInstrument::~SampleInstrument() {} +uint32_t SampleInstrument::GetSlicePoint(size_t index) const { + if (index >= MaxSlices) { + return 0; + } + return slicePoints_[index]; +} + +void SampleInstrument::SetSlicePoint(size_t index, uint32_t start) { + if (index >= MaxSlices) { + return; + } + uint32_t clamped = start; + if (source_ && !source_->IsMulti()) { + int size = source_->GetSize(0); + if (size > 0) { + uint32_t limit = static_cast(size); + if (clamped > limit) { + clamped = limit; + } + } + } + if (slicePoints_[index] == clamped) { + return; + } + slicePoints_[index] = clamped; + SetChanged(); + NotifyObservers(); +} + +void SampleInstrument::ClearSlices() { + bool hadSlices = hasAnySliceValue(); + slicePoints_.fill(0); + if (hadSlices) { + SetChanged(); + NotifyObservers(); + } +} + +bool SampleInstrument::HasSlicesForPlayback() const { + return hasAnySliceValue(); +} + +bool SampleInstrument::HasSlicesForWarning() const { + return (slicePoints_[0] > 0) || (slicePoints_[1] > 0); +} + +bool SampleInstrument::IsSliceDefined(size_t index) const { + return isSliceIndexActive(index); +} + +bool SampleInstrument::hasAnySliceValue() const { + for (auto value : slicePoints_) { + if (value > 0) { + return true; + } + } + return false; +} + +bool SampleInstrument::isSliceIndexActive(size_t index) const { + if (index >= MaxSlices) { + return false; + } + if (index == 0) { + if (slicePoints_[0] > 0) { + return true; + } + for (size_t i = 1; i < MaxSlices; ++i) { + if (slicePoints_[i] > 0) { + return true; + } + } + return false; + } + return slicePoints_[index] > 0; +} + +bool SampleInstrument::shouldUseSlice(unsigned char midinote, + size_t &sliceIndex, + uint32_t sampleSize) const { + if (!HasSlicesForPlayback()) { + return false; + } + if (source_ == nullptr || source_->IsMulti()) { + return false; + } + if (midinote < SliceNoteBase) { + return false; + } + size_t index = midinote - SliceNoteBase; + if (index >= MaxSlices) { + return false; + } + if (!isSliceIndexActive(index)) { + return false; + } + // Ensure slice start is within range + uint32_t start = computeSliceStart(index, sampleSize); + uint32_t end = computeSliceEnd(index, sampleSize); + if (start >= end) { + return false; + } + sliceIndex = index; + return true; +} + +uint32_t SampleInstrument::computeSliceStart(size_t index, + uint32_t sampleSize) const { + if (index >= MaxSlices || sampleSize == 0) { + return 0; + } + uint32_t stored = slicePoints_[index]; + if (index == 0 && stored == 0) { + return 0; + } + if (stored > sampleSize) { + return sampleSize; + } + return stored; +} + +uint32_t SampleInstrument::computeSliceEnd(size_t index, + uint32_t sampleSize) const { + if (sampleSize == 0) { + return 0; + } + uint32_t start = computeSliceStart(index, sampleSize); + uint32_t end = sampleSize; + for (size_t i = index + 1; i < MaxSlices; ++i) { + uint32_t candidate = slicePoints_[i]; + if (candidate > start) { + if (candidate < end) { + end = candidate; + } + break; + } + } + if (end <= start) { + end = std::min(start + 1, sampleSize); + } + return end; +} + +void SampleInstrument::clampSlicePoints(uint32_t sampleSize) { + bool changed = false; + if (sampleSize == 0) { + for (auto &value : slicePoints_) { + if (value != 0) { + value = 0; + changed = true; + } + } + } else { + for (auto &value : slicePoints_) { + if (value > sampleSize) { + value = sampleSize; + changed = true; + } + } + } + if (changed) { + SetChanged(); + NotifyObservers(); + } +} + bool SampleInstrument::Init() { SamplePool *pool = SamplePool::GetInstance(); @@ -148,6 +318,8 @@ bool SampleInstrument::Start(int channel, unsigned char midinote, return false; }; rp->channelCount_ = source_->GetChannelCount(rp->midiNote_); + int sampleSize = source_->GetSize(rp->midiNote_); + uint32_t sampleSizeU = sampleSize > 0 ? static_cast(sampleSize) : 0; int rootNote = (rootNote_.GetInt() - 60) + source_->GetRootNote(rp->midiNote_); @@ -175,6 +347,26 @@ bool SampleInstrument::Start(int channel, unsigned char midinote, SampleInstrumentLoopMode loopmode = (SampleInstrumentLoopMode)loopMode_.GetInt(); + size_t sliceIndex = 0; + bool sliceActive = shouldUseSlice(midinote, sliceIndex, sampleSizeU); + uint32_t sliceStart = 0; + uint32_t sliceEnd = 0; + if (sliceActive) { + sliceStart = computeSliceStart(sliceIndex, sampleSizeU); + sliceEnd = computeSliceEnd(sliceIndex, sampleSizeU); + if (sliceStart >= sliceEnd) { + sliceActive = false; + } + } + + rp->sliceActive_ = sliceActive; + rp->activeSliceIndex_ = sliceActive ? static_cast(sliceIndex) : 0; + + if (sliceActive) { + loopmode = SILM_ONESHOT; + } + rp->loopModeValue_ = static_cast(loopmode); + /* if (loopmode==SILM_OSCFINE) { if (rp->rendLoopEnd_>source_->GetSize()-1) { // check for older instrument that were not correctly handled @@ -244,11 +436,22 @@ bool SampleInstrument::Start(int channel, unsigned char midinote, break; } + if (sliceActive) { + rp->rendLoopStart_ = static_cast(sliceStart); + rp->rendLoopEnd_ = static_cast(sliceEnd); + rp->rendFirst_ = static_cast(sliceStart); + rp->position_ = float(sliceStart); + rp->reverse_ = false; + } + // Compute octave & note difference from root float fineTune = float(fineTune_.GetInt() - 0x7F); fineTune /= float(0x80); int offset = midinote - rootNote; + if (sliceActive) { + offset = 0; + } while (offset > 127) { offset -= 12; } @@ -429,7 +632,7 @@ bool SampleInstrument::Render(int channel, fixed *buffer, int size, // Loop mode SampleInstrumentLoopMode loopMode = - (SampleInstrumentLoopMode)loopMode_.GetInt(); + (SampleInstrumentLoopMode)rp->loopModeValue_; // Interpolation @@ -859,6 +1062,7 @@ void SampleInstrument::updateInstrumentData(bool search) { v->SetInt(0); v = FindVariable(FourCC::SampleInstrumentStart); v->SetInt(0); + clampSlicePoints(static_cast(instrSize)); dirty_ = false; }; @@ -1189,6 +1393,91 @@ etl::string SampleInstrument::GetDisplayName() { return sampleFileName; }; +void SampleInstrument::SaveContent(tinyxml2::XMLPrinter *printer) { + I_Instrument::SaveContent(printer); + + for (size_t i = 0; i < slicePoints_.size(); ++i) { + if (slicePoints_[i] == 0) { + continue; + } + printer->OpenElement("PARAM"); + char sliceName[6]; + npf_snprintf(sliceName, sizeof(sliceName), "SL%02u", + static_cast(i)); + printer->PushAttribute("NAME", sliceName); + printer->PushAttribute("VALUE", static_cast(slicePoints_[i])); + printer->CloseElement(); + } +} + +void SampleInstrument::RestoreContent(PersistencyDocument *doc) { + auto setSliceFromString = [this](const char *indexStr, const char *valueStr) { + int idx = atoi(indexStr); + if (idx < 0 || idx >= static_cast(MaxSlices)) { + return; + } + uint32_t value = static_cast(strtoul(valueStr, nullptr, 10)); + slicePoints_[static_cast(idx)] = value; + }; + + bool hasAttr = doc->NextAttribute(); + while (hasAttr) { + if (!strcasecmp(doc->attrname_, "TYPE")) { + Trace::Log("I_INSTRUMENT", "Instrument type from XML: %s", doc->attrval_); + } else if (!strncasecmp(doc->attrname_, "SL", 2)) { + setSliceFromString(doc->attrname_ + 2, doc->attrval_); + } + hasAttr = doc->NextAttribute(); + } + + bool subelem = doc->FirstChild(); + + while (subelem) { + bool attr = doc->NextAttribute(); + char name[24] = ""; + char value[24] = ""; + + while (attr) { + if (!strcasecmp(doc->attrname_, "NAME")) { + strncpy(name, doc->attrval_, sizeof(name) - 1); + name[sizeof(name) - 1] = '\0'; + } + if (!strcasecmp(doc->attrname_, "VALUE")) { + strncpy(value, doc->attrval_, sizeof(value) - 1); + value[sizeof(value) - 1] = '\0'; + } + attr = doc->NextAttribute(); + } + + if (name[0] != '\0' && value[0] != '\0') { + if (!strcasecmp(name, "InstrumentName")) { + SetName(value); + } else if (!strncasecmp(name, "SL", 2)) { + setSliceFromString(name + 2, value); + } else { + bool found = false; + for (auto it = Variables()->begin(); it != Variables()->end(); it++) { + if (!strcasecmp((*it)->GetName(), name)) { + (*it)->SetString(value); + found = true; + break; + } + } + if (!found) { + Trace::Error("Parameter '%s' not found in instrument", name); + } + } + } + + subelem = doc->NextSibling(); + } + + Variable *nameVar = FindVariable(FourCC::InstrumentName); + if (nameVar && !name_.empty()) { + nameVar->SetString(name_.c_str()); + } +} + bool SampleInstrument::IsEmpty() { Variable *v = FindVariable(FourCC::SampleInstrumentSample); return (v->GetInt() == -1); diff --git a/sources/Application/Instruments/SampleInstrument.h b/sources/Application/Instruments/SampleInstrument.h index a260ffc61..91d3c78ab 100644 --- a/sources/Application/Instruments/SampleInstrument.h +++ b/sources/Application/Instruments/SampleInstrument.h @@ -12,6 +12,7 @@ #include "Application/Model/Song.h" #include "Application/Persistency/PersistenceConstants.h" +#include "Externals/etl/include/etl/array.h" #include "Foundation/Observable.h" #include "Foundation/Types/Types.h" #include "Foundation/Variables/WatchedVariable.h" @@ -59,6 +60,15 @@ class SampleInstrument : public I_Instrument, I_Observer { // Engine playback start callback virtual void OnStart(); + static constexpr size_t MaxSlices = 16; + static constexpr unsigned char SliceNoteBase = 60; + + uint32_t GetSlicePoint(size_t index) const; + void SetSlicePoint(size_t index, uint32_t start); + void ClearSlices(); + bool HasSlicesForPlayback() const; + bool HasSlicesForWarning() const; + bool IsSliceDefined(size_t index) const; // I_Observer virtual void Update(Observable &o, I_ObservableData *d); @@ -75,6 +85,8 @@ class SampleInstrument : public I_Instrument, I_Observer { virtual etl::string GetSampleFileName(); static void EnableDownsamplingLegacy(); + virtual void SaveContent(tinyxml2::XMLPrinter *printer) override; + virtual void RestoreContent(PersistencyDocument *doc) override; protected: void updateInstrumentData(bool search); @@ -112,7 +124,15 @@ class SampleInstrument : public I_Instrument, I_Observer { WatchedVariable loopEnd_; Variable table_; Variable tableAuto_; + etl::array slicePoints_; static bool useDirtyDownsampling_; + bool isSliceIndexActive(size_t index) const; + bool shouldUseSlice(unsigned char midinote, size_t &sliceIndex, + uint32_t sampleSize) const; + uint32_t computeSliceStart(size_t index, uint32_t sampleSize) const; + uint32_t computeSliceEnd(size_t index, uint32_t sampleSize) const; + bool hasAnySliceValue() const; + void clampSlicePoints(uint32_t sampleSize); }; #endif diff --git a/sources/Application/Instruments/SampleRenderingParams.h b/sources/Application/Instruments/SampleRenderingParams.h index 8dd5ad095..ae3327033 100644 --- a/sources/Application/Instruments/SampleRenderingParams.h +++ b/sources/Application/Instruments/SampleRenderingParams.h @@ -74,5 +74,8 @@ struct renderParams { bool couldClick_; char midiNote_; // Current midi note + bool sliceActive_; + uint8_t activeSliceIndex_; + int loopModeValue_; }; #endif diff --git a/sources/Application/Persistency/PersistencyDocument.cpp b/sources/Application/Persistency/PersistencyDocument.cpp index 4c7592d3f..ec7c8ef95 100644 --- a/sources/Application/Persistency/PersistencyDocument.cpp +++ b/sources/Application/Persistency/PersistencyDocument.cpp @@ -77,16 +77,17 @@ bool PersistencyDocument::FirstChild() { return true; case YXML_ELEMEND: return false; - case YXML_CONTENT: - case YXML_ATTRSTART: - case YXML_ATTRVAL: - case YXML_ATTREND: case YXML_EEOF: case YXML_EREF: case YXML_ECLOSE: case YXML_ESTACK: case YXML_ESYN: - // Error + Trace::Error("FirstChild parse error: %d", r_); + return false; + case YXML_CONTENT: + case YXML_ATTRSTART: + case YXML_ATTRVAL: + case YXML_ATTREND: default: // Any other values we skip, including YXML_OK break; diff --git a/sources/Application/Views/BaseClasses/View.h b/sources/Application/Views/BaseClasses/View.h index 58b4721e9..f2cbb94f2 100644 --- a/sources/Application/Views/BaseClasses/View.h +++ b/sources/Application/Views/BaseClasses/View.h @@ -57,6 +57,7 @@ enum ViewType { VT_SELECTTHEME, // Theme selection VT_THEME_IMPORT, // Theme file import VT_SAMPLE_EDITOR, // Sample Editor + VT_SAMPLE_SLICES, // Sample slice editor VT_RECORD // Recording screen }; diff --git a/sources/Application/Views/CMakeLists.txt b/sources/Application/Views/CMakeLists.txt index e29f4927d..f46431d13 100644 --- a/sources/Application/Views/CMakeLists.txt +++ b/sources/Application/Views/CMakeLists.txt @@ -21,6 +21,7 @@ add_library(application_views ScreenView.cpp MixerView.cpp SampleEditorView.cpp + SampleSlicesView.cpp SampleEditProgressDisplay.cpp ) diff --git a/sources/Application/Views/InstrumentView.cpp b/sources/Application/Views/InstrumentView.cpp index 0eac27115..805589ceb 100644 --- a/sources/Application/Views/InstrumentView.cpp +++ b/sources/Application/Views/InstrumentView.cpp @@ -29,7 +29,8 @@ InstrumentView::InstrumentView(GUIWindow &w, ViewData *data) : FieldView(w, data), instrumentType_(FourCC::VarInstrumentType, - InstrumentTypeNames, IT_LAST, 0) { + InstrumentTypeNames, IT_LAST, 0), + lastSampleIndex_(-1), suppressSampleChangeWarning_(false) { project_ = data->project_; @@ -200,6 +201,9 @@ void InstrumentView::refreshInstrumentFields() { for (auto &f : bitmaskVarField_) { f.RemoveObserver(*this); } + for (auto &f : sampleActionField_) { + f.RemoveObserver(*this); + } fieldList_.clear(); intVarField_.clear(); @@ -207,9 +211,11 @@ void InstrumentView::refreshInstrumentFields() { staticField_.clear(); bigHexVarField_.clear(); intVarOffField_.clear(); + sampleActionField_.clear(); bitmaskVarField_.clear(); nameTextField_.clear(); nameVariables_.clear(); + lastSampleIndex_ = -1; // first put back the type field as its shown on *all* instrument types fieldList_.insert(fieldList_.end(), &(*typeIntVarField_.rbegin())); @@ -293,8 +299,10 @@ void InstrumentView::fillSampleParameters() { InstrumentBank *bank = viewData_->project_->GetInstrumentBank(); I_Instrument *instr = bank->GetInstrument(i); SampleInstrument *instrument = (SampleInstrument *)instr; + lastSampleIndex_ = instrument->GetSampleIndex(); GUIPoint position = GetAnchor(); + const int baseX = position._x; // offset y to account for instrument type and export/import fields position._y += 1; @@ -395,6 +403,12 @@ void InstrumentView::fillSampleParameters() { instrument->GetSampleSize() - 1, 16); fieldList_.insert(fieldList_.end(), &(*bigHexVarField_.rbegin())); + position._y += 1; + sampleActionField_.emplace_back("Slices", FourCC::ActionShowSampleSlices, + position); + fieldList_.insert(fieldList_.end(), &sampleActionField_.back()); + sampleActionField_.back().AddObserver(*this); + v = instrument->FindVariable(FourCC::SampleInstrumentTableAutomation); position._y += 2; intVarField_.emplace_back(position, *v, "automation: %s", 0, 1, 1, 1); @@ -1016,6 +1030,66 @@ void InstrumentView::Update(Observable &o, I_ObservableData *data) { SetChanged(); NotifyObservers(&ve); } break; + case FourCC::SampleInstrumentSample: { + I_Instrument *instr = getInstrument(); + if (!instr || instr->GetType() != IT_SAMPLE) { + break; + } + + SampleInstrument *sampleInstr = static_cast(instr); + int newIndex = sampleInstr->GetSampleIndex(); + + if (suppressSampleChangeWarning_) { + suppressSampleChangeWarning_ = false; + lastSampleIndex_ = newIndex; + break; + } + + if (newIndex == lastSampleIndex_) { + break; + } + + if (!sampleInstr->HasSlicesForWarning()) { + lastSampleIndex_ = newIndex; + break; + } + + Variable *sampleVar = + sampleInstr->FindVariable(FourCC::SampleInstrumentSample); + MessageBox *mb = new MessageBox(*this, "Change sample &", "clear slices?", + MBBF_YES | MBBF_NO); + + DoModal(mb, [this, sampleInstr, sampleVar, newIndex](View &view, + ModalView &dialog) { + if (dialog.GetReturnCode() == MBL_YES) { + sampleInstr->ClearSlices(); + lastSampleIndex_ = newIndex; + isDirty_ = true; + } else { + suppressSampleChangeWarning_ = true; + if (sampleVar) { + sampleVar->SetInt(lastSampleIndex_); + } + isDirty_ = true; + } + }); + } break; + case FourCC::ActionShowSampleSlices: { + I_Instrument *instr = getInstrument(); + if (!instr || instr->GetType() != IT_SAMPLE) { + break; + } + SampleInstrument *sampleInstr = static_cast(instr); + if (sampleInstr->GetSampleIndex() < 0) { + MessageBox *mb = new MessageBox(*this, "Assign a sample first", MBBF_OK); + DoModal(mb); + break; + } + ViewType vt = VT_SAMPLE_SLICES; + ViewEvent ve(VET_SWITCH_VIEW, &vt); + SetChanged(); + NotifyObservers(&ve); + } break; case FourCC::MidiInstrumentProgram: { // When program value changes, send a MIDI Program Change message I_Instrument *instr = getInstrument(); diff --git a/sources/Application/Views/InstrumentView.h b/sources/Application/Views/InstrumentView.h index 3d5ac8060..20d6175ae 100644 --- a/sources/Application/Views/InstrumentView.h +++ b/sources/Application/Views/InstrumentView.h @@ -58,6 +58,8 @@ class InstrumentView : public FieldView, public I_Observer { Project *project_; FourCC lastFocusID_; WatchedVariable instrumentType_; + int lastSampleIndex_; + bool suppressSampleChangeWarning_; // Variables for export confirmation dialog I_Instrument *exportInstrument_ = nullptr; @@ -70,6 +72,7 @@ class InstrumentView : public FieldView, public I_Observer { etl::vector staticField_; etl::vector bigHexVarField_; etl::vector intVarOffField_; + etl::vector sampleActionField_; etl::vector bitmaskVarField_; etl::vector, 1> nameTextField_; etl::vector nameVariables_; diff --git a/sources/Application/Views/SampleSlicesView.cpp b/sources/Application/Views/SampleSlicesView.cpp new file mode 100644 index 000000000..28c6d5785 --- /dev/null +++ b/sources/Application/Views/SampleSlicesView.cpp @@ -0,0 +1,416 @@ +/* + * SPDX-License-Identifier: BSD-3-Clause + * + * Copyright (c) 2024 xiphonics, inc. + * + * This file is part of the picoTracker firmware + */ + +#include "SampleSlicesView.h" + +#ifdef CHAR_WIDTH +#undef CHAR_WIDTH +#endif +#ifdef CHAR_HEIGHT +#undef CHAR_HEIGHT +#endif + +#include "Application/AppWindow.h" +#include "Application/Instruments/InstrumentBank.h" +#include "Application/Instruments/SamplePool.h" +#include "Application/Model/Project.h" +#include "Application/Model/Song.h" +#include "Application/Player/Player.h" +#include "Application/Utils/char.h" +#include "System/Console/Trace.h" +#include +#include +#include +#include + +namespace { +constexpr unsigned short PreviewChannel = SONG_CHANNEL_COUNT - 1; +constexpr int SliceXOffset = 0; +#ifdef ADV +constexpr int SliceYOffset = 2 * CHAR_HEIGHT * 4; +#else +constexpr int SliceYOffset = 2 * CHAR_HEIGHT; +#endif +} // namespace + +SampleSlicesView::SampleSlicesView(GUIWindow &w, ViewData *data) + : FieldView(w, data), sliceIndexVar_(FourCC::SampleInstrumentSlices, 0), + sliceStartVar_(FourCC::SampleInstrumentStart, 0), waveformValid_(false), + needsWaveformRedraw_(true), instrument_(nullptr), instrumentIndex_(0), + sampleSize_(0), playKeyHeld_(false), previewActive_(false), + previewNote_(SampleInstrument::SliceNoteBase) { + sliceIndexVar_.AddObserver(*this); + sliceStartVar_.AddObserver(*this); + slicePixelPositions_.fill(-1); + std::memset(waveformCache_, 0, sizeof(waveformCache_)); +} + +SampleSlicesView::~SampleSlicesView() { stopPreview(); } + +void SampleSlicesView::OnFocus() { + stopPreview(); + instrumentIndex_ = viewData_->currentInstrumentID_; + instrument_ = currentInstrument(); + playKeyHeld_ = false; + previewActive_ = false; + waveformValid_ = false; + needsWaveformRedraw_ = true; + sampleSize_ = 0; + + if (instrument_) { + SamplePool *pool = SamplePool::GetInstance(); + int sampleIndex = instrument_->GetSampleIndex(); + if (sampleIndex >= 0) { + if (SoundSource *source = pool->GetSource(sampleIndex)) { + sampleSize_ = static_cast(source->GetSize(0)); + } + } + } + + sliceIndexVar_.SetInt(0, false); + updateSliceSelectionFromInstrument(); + rebuildWaveform(); + refreshSliceMarkers(); + buildFieldLayout(); + isDirty_ = true; +} + +void SampleSlicesView::ProcessButtonMask(unsigned short mask, bool pressed) { + if (!pressed) { + if (playKeyHeld_ && !(mask & EPBM_PLAY)) { + playKeyHeld_ = false; + stopPreview(); + needsWaveformRedraw_ = true; + } + FieldView::ProcessButtonMask(mask, pressed); + return; + } + + if (mask & EPBM_NAV) { + if (mask & EPBM_LEFT) { + // Go back to sample browser NAV+LEFT + ViewType vt = VT_INSTRUMENT; + ViewEvent ve(VET_SWITCH_VIEW, &vt); + SetChanged(); + NotifyObservers(&ve); + return; + } + // For other NAV combinations, let parent handle it + FieldView::ProcessButtonMask(mask, pressed); + return; + } + + if (mask & EPBM_PLAY) { + if (!playKeyHeld_) { + startPreview(); + playKeyHeld_ = true; + needsWaveformRedraw_ = true; + } + return; + } + + FieldView::ProcessButtonMask(mask, pressed); +} + +void SampleSlicesView::DrawView() { + Clear(); + + GUITextProperties props; + GUIPoint titlePos = GetTitlePosition(); + DrawString(titlePos._x, titlePos._y, "Sample Slices", props); + + if (needsWaveformRedraw_) { + drawWaveform(); + needsWaveformRedraw_ = false; + } + + FieldView::Redraw(); +} + +void SampleSlicesView::AnimationUpdate() { + GUITextProperties props; + drawBattery(props); + drawPowerButtonUI(props); +} + +void SampleSlicesView::Update(Observable &o, I_ObservableData *d) { + if (!hasFocus_) { + return; + } + + uintptr_t fourcc = (uintptr_t)d; + + switch (fourcc) { + case FourCC::SampleInstrumentSlices: + handleSliceSelectionChange(); + break; + case FourCC::SampleInstrumentStart: + applySliceStart(static_cast(sliceStartVar_.GetInt())); + refreshSliceMarkers(); + needsWaveformRedraw_ = true; + isDirty_ = true; + break; + default: + break; + } +} + +void SampleSlicesView::buildFieldLayout() { + for (auto &f : intVarField_) { + f.RemoveObserver(*this); + } + for (auto &f : bigHexVarField_) { + f.RemoveObserver(*this); + } + + fieldList_.clear(); + intVarField_.clear(); + bigHexVarField_.clear(); + staticField_.clear(); + + GUIPoint position = GetAnchor(); + position._x += 5; + position._y = 12; + + intVarField_.emplace_back(position, sliceIndexVar_, "slice: %d", 0, + static_cast(SliceCount) - 1, 1, 1); + fieldList_.insert(fieldList_.end(), &intVarField_.back()); + intVarField_.back().AddObserver(*this); + + position._y += 1; + int maxStart = (sampleSize_ > 0) ? static_cast(sampleSize_ - 1) : 0; + bigHexVarField_.emplace_back(position, sliceStartVar_, 7, "start: %7.7X", 0, + maxStart, 16); + fieldList_.insert(fieldList_.end(), &bigHexVarField_.back()); + bigHexVarField_.back().AddObserver(*this); + + position._y += 2; + position._x = GetAnchor()._x + 5; + staticField_.emplace_back(position, "PLAY: preview slice"); + fieldList_.insert(fieldList_.end(), &staticField_.back()); + + if (!fieldList_.empty()) { + SetFocus(*fieldList_.begin()); + } +} + +void SampleSlicesView::rebuildWaveform() { + waveformValid_ = false; + std::memset(waveformCache_, 0, sizeof(waveformCache_)); + + if (!instrument_) { + return; + } + + SamplePool *pool = SamplePool::GetInstance(); + int sampleIndex = instrument_->GetSampleIndex(); + if (sampleIndex < 0) { + return; + } + + SoundSource *source = pool->GetSource(sampleIndex); + if (!source) { + return; + } + + sampleSize_ = static_cast(source->GetSize(0)); + if (sampleSize_ == 0) { + return; + } + + int channels = source->GetChannelCount(0); + short *samples = static_cast(source->GetSampleBuffer(0)); + if (!samples) { + return; + } + + std::fill(std::begin(waveformCache_), std::end(waveformCache_), 0); + static int64_t sumSquares[SliceWaveformCacheSize]; + static uint32_t counts[SliceWaveformCacheSize]; + std::fill_n(sumSquares, SliceWaveformCacheSize, int64_t{0}); + std::fill_n(counts, SliceWaveformCacheSize, uint32_t{0}); + float samplesPerPixel = + std::max(1.0f, static_cast(sampleSize_) / SliceWaveformCacheSize); + + for (uint32_t i = 0; i < sampleSize_; ++i) { + uint32_t pixel = + static_cast(std::floor(i / samplesPerPixel + 0.5f)); + if (pixel >= SliceWaveformCacheSize) { + pixel = SliceWaveformCacheSize - 1; + } + short value = samples[i * channels]; + sumSquares[pixel] += static_cast(value) * value; + counts[pixel]++; + } + + for (int i = 0; i < SliceWaveformCacheSize; ++i) { + if (counts[i] == 0) { + waveformCache_[i] = 0; + continue; + } + float meanSquare = static_cast(sumSquares[i]) / counts[i]; + float rms = std::sqrt(meanSquare) / 32768.0f; + uint8_t height = static_cast(std::min( + rms * SliceBitmapHeight, static_cast(SliceBitmapHeight))); + waveformCache_[i] = height; + } + + waveformValid_ = true; + needsWaveformRedraw_ = true; +} + +void SampleSlicesView::drawWaveform() { + GUIRect area(SliceXOffset, SliceYOffset, SliceXOffset + SliceBitmapWidth, + SliceYOffset + SliceBitmapHeight); + DrawRect(area, CD_BACKGROUND); + + if (!waveformValid_) { + return; + } + + int centerY = SliceYOffset + SliceBitmapHeight / 2; + for (int x = 1; x < SliceBitmapWidth - 1; ++x) { + uint8_t amplitude = waveformCache_[x - 1]; + if (amplitude == 0) { + continue; + } + int startY = centerY - amplitude / 2; + int endY = startY + amplitude; + GUIRect column(SliceXOffset + x, startY, SliceXOffset + x + 1, endY); + DrawRect(column, CD_NORMAL); + } + + for (size_t i = 0; i < SliceCount; ++i) { + int x = slicePixelPositions_[i]; + if (x < 0) { + continue; + } + ColorDefinition color = (static_cast(i) == sliceIndexVar_.GetInt()) + ? CD_CURSOR + : CD_ACCENT; + GUIRect marker(x, SliceYOffset + 2, x + 1, + SliceYOffset + SliceBitmapHeight - 2); + DrawRect(marker, color); + } +} + +void SampleSlicesView::refreshSliceMarkers() { + slicePixelPositions_.fill(-1); + + if (!instrument_ || sampleSize_ == 0) { + return; + } + + for (size_t i = 0; i < SliceCount; ++i) { + if (!instrument_->IsSliceDefined(i)) { + continue; + } + uint32_t start = instrument_->GetSlicePoint(i); + if (i == 0 && start == 0 && !instrument_->HasSlicesForPlayback()) { + continue; + } + slicePixelPositions_[i] = sliceToPixel(start); + } +} + +SampleInstrument *SampleSlicesView::currentInstrument() { + if (!viewData_ || !viewData_->project_) { + return nullptr; + } + + InstrumentBank *bank = viewData_->project_->GetInstrumentBank(); + if (!bank) { + return nullptr; + } + + I_Instrument *instr = bank->GetInstrument(viewData_->currentInstrumentID_); + if (!instr || instr->GetType() != IT_SAMPLE) { + return nullptr; + } + return static_cast(instr); +} + +void SampleSlicesView::updateSliceSelectionFromInstrument() { + if (!instrument_) { + sliceStartVar_.SetInt(0, false); + return; + } + + int index = sliceIndexVar_.GetInt(); + if (index < 0) { + index = 0; + } + if (index >= static_cast(SliceCount)) { + index = static_cast(SliceCount) - 1; + } + + size_t sliceIndex = static_cast(index); + uint32_t start = instrument_->GetSlicePoint(sliceIndex); + if (index > 0 && !instrument_->IsSliceDefined(sliceIndex) && + instrument_->IsSliceDefined(sliceIndex - 1)) { + uint32_t previousStart = instrument_->GetSlicePoint(sliceIndex - 1); + instrument_->SetSlicePoint(sliceIndex, previousStart); + start = instrument_->GetSlicePoint(sliceIndex); + } + sliceStartVar_.SetInt(static_cast(start), false); +} + +void SampleSlicesView::applySliceStart(uint32_t start) { + if (!instrument_) { + return; + } + size_t index = static_cast(sliceIndexVar_.GetInt()); + instrument_->SetSlicePoint(index, start); +} + +void SampleSlicesView::startPreview() { + if (!hasInstrumentSample()) { + return; + } + + stopPreview(); + + unsigned char note = static_cast( + SampleInstrument::SliceNoteBase + sliceIndexVar_.GetInt()); + Player::GetInstance()->PlayNote(static_cast(instrumentIndex_), + PreviewChannel, note, 0x7F); + previewNote_ = note; + previewActive_ = true; +} + +void SampleSlicesView::stopPreview() { + if (!previewActive_) { + return; + } + Player::GetInstance()->StopNote(static_cast(instrumentIndex_), + PreviewChannel); + previewActive_ = false; +} + +void SampleSlicesView::handleSliceSelectionChange() { + updateSliceSelectionFromInstrument(); + refreshSliceMarkers(); + needsWaveformRedraw_ = true; + isDirty_ = true; +} + +int SampleSlicesView::sliceToPixel(uint32_t start) const { + if (sampleSize_ == 0) { + return -1; + } + uint32_t clamped = std::min( + start, sampleSize_ > 0 ? sampleSize_ - 1 : static_cast(0)); + float ratio = static_cast(clamped) / + static_cast(std::max(1, sampleSize_)); + int local = static_cast(ratio * (SliceBitmapWidth - 2)); + return SliceXOffset + 1 + local; +} + +bool SampleSlicesView::hasInstrumentSample() const { + return instrument_ && instrument_->GetSampleIndex() >= 0 && sampleSize_ > 0; +} diff --git a/sources/Application/Views/SampleSlicesView.h b/sources/Application/Views/SampleSlicesView.h new file mode 100644 index 000000000..cca45afb9 --- /dev/null +++ b/sources/Application/Views/SampleSlicesView.h @@ -0,0 +1,83 @@ +/* + * SPDX-License-Identifier: BSD-3-Clause + * + * Copyright (c) 2024 xiphonics, inc. + * + * This file is part of the picoTracker firmware + */ + +#ifndef _SAMPLE_SLICES_VIEW_H_ +#define _SAMPLE_SLICES_VIEW_H_ + +#include "BaseClasses/UIBigHexVarField.h" +#include "BaseClasses/UIIntVarField.h" +#include "BaseClasses/UIStaticField.h" +#include "Externals/etl/include/etl/array.h" +#include "FieldView.h" +#include "Foundation/Observable.h" +#include "Foundation/Variables/WatchedVariable.h" +#include "ViewData.h" + +class SampleInstrument; + +#ifdef ADV +static constexpr int SliceBitmapWidth = 720; +static constexpr int SliceBitmapHeight = 160; +#else +static constexpr int SliceBitmapWidth = 320; +static constexpr int SliceBitmapHeight = 80; +#endif + +static constexpr int SliceWaveformCacheSize = SliceBitmapWidth; +static constexpr size_t SliceCount = 16; + +class SampleSlicesView : public FieldView, public I_Observer { +public: + SampleSlicesView(GUIWindow &w, ViewData *data); + ~SampleSlicesView() override; + + void ProcessButtonMask(unsigned short mask, bool pressed) override; + void DrawView() override; + void OnPlayerUpdate(PlayerEventType, unsigned int) override{}; + void OnFocus() override; + void AnimationUpdate() override; + void Update(Observable &o, I_ObservableData *d) override; + +private: + void buildFieldLayout(); + void rebuildWaveform(); + void drawWaveform(); + void refreshSliceMarkers(); + SampleInstrument *currentInstrument(); + void updateSliceSelectionFromInstrument(); + void applySliceStart(uint32_t start); + void startPreview(); + void stopPreview(); + void handleSliceSelectionChange(); + int sliceToPixel(uint32_t start) const; + uint32_t selectedSliceStart() const; + bool hasInstrumentSample() const; + + WatchedVariable sliceIndexVar_; + WatchedVariable sliceStartVar_; + + etl::vector intVarField_; + etl::vector bigHexVarField_; + etl::vector staticField_; + + uint8_t waveformCache_[SliceWaveformCacheSize]; + bool waveformValid_; + bool needsWaveformRedraw_; + + SampleInstrument *instrument_; + int instrumentIndex_; + uint32_t sampleSize_; + + etl::array slicePixelPositions_; + + bool playKeyHeld_; + bool previewActive_; + unsigned char previewNote_; +}; + +#endif diff --git a/sources/Foundation/Types/Types.h b/sources/Foundation/Types/Types.h index 2508ab6fd..674624a8c 100644 --- a/sources/Foundation/Types/Types.h +++ b/sources/Foundation/Types/Types.h @@ -211,6 +211,8 @@ struct FourCC { // 181 is taken for VarSampleEditOperation // 182 is taken for VarRecordLineGain // 183 is taken for VarRecordMicGain + // 184 is taken for VarOutputVolume + // 185 is taken for ActionShowSampleSlices VarChannel1Volume = 163, VarChannel2Volume = 164, @@ -247,6 +249,7 @@ struct FourCC { SampleInstrumentSlices = 171, VarBacklightLevel = 174, ActionShowSampleEditor = 175, + ActionShowSampleSlices = 185, VarRecordSource = 176, VarSampleEditStart = 177, VarSampleEditEnd = 178, diff --git a/usermanual/advance-edition/content/pages/instruments.md b/usermanual/advance-edition/content/pages/instruments.md index d248c48f4..148164326 100644 --- a/usermanual/advance-edition/content/pages/instruments.md +++ b/usermanual/advance-edition/content/pages/instruments.md @@ -53,9 +53,16 @@ Once you've created an instrument, you can save it for use in other projects: - **start:** start point of the sample regardless of if loop is enabled (note value is in hex) - **loop Start:** start point of the sample when loop is enabled (note value is in hex) - **loop End:** end point of the sample (note value is in hex). You can play samples backwards by setting the end value lower than the start +- **slices:** Opens the Sample Slices view where you can define up to 16 slice start points for the currently selected sample. Notes from `C3` through `D#4` trigger slices 1–16 instead of pitching the entire sample. - **automation:** If On, the table play arrows will advance one row every time the instrument is triggered, and execute only the commands on the new rows. If this is Off, table behavior is normal (play arrows will move at the speed of 1 row per tick) - **table:** Select a table the instrument will always run. To clone a table here: `NAV`+(`EDIT`, `ENTER`). Make a new table by selecting a higher number not yet in use. +### Sample Slices View + +The Sample Slices view provides a focused editor for slice start points. Use the `slice` field to choose which of the 16 slots you want to edit (`slice 0` corresponds to `C3`, `slice 15` to `D#4`). The `start` field lets you enter the slice start offset as a hexadecimal sample index, and a waveform preview helps visualize the positions of every defined slice. Press `PLAY` to audition only the currently selected slice at its original pitch. + +Slices are stored per instrument and always reference the currently assigned sample. Changing the instrument's sample when slices are present prompts for confirmation, because accepting the change clears all slice start points. + ## Sample Import Screen