/* * View_smp.cpp * ------------ * Purpose: Sample tab, lower panel. * Notes : (currently none) * Authors: Olivier Lapicque * OpenMPT Devs * The OpenMPT source code is released under the BSD license. Read LICENSE for more details. */ #include "stdafx.h" #include "Mptrack.h" #include "Mainfrm.h" #include "InputHandler.h" #include "ImageLists.h" #include "Childfrm.h" #include "Clipboard.h" #include "Moddoc.h" #include "Globals.h" #include "Ctrl_smp.h" #include "Dlsbank.h" #include "ChannelManagerDlg.h" #include "View_smp.h" #include "OPLInstrDlg.h" #include "../soundlib/MIDIEvents.h" #include "SampleEditorDialogs.h" #include "../soundlib/WAVTools.h" #include "../common/FileReader.h" #include "../tracklib/SampleEdit.h" #include "openmpt/soundbase/SampleConvert.hpp" #include "openmpt/soundbase/SampleDecode.hpp" #include "../soundlib/SampleCopy.h" #include "../soundlib/mod_specifications.h" #include "../soundlib/S3MTools.h" #include "mpt/io/base.hpp" #include "mpt/io/io.hpp" #include "mpt/io/io_span.hpp" #include "mpt/io/io_stdstream.hpp" #include "mpt/io/io_virtual_wrapper.hpp" OPENMPT_NAMESPACE_BEGIN // Non-client toolbar #define SMP_LEFTBAR_CY Util::ScalePixels(29, m_hWnd) #define SMP_LEFTBAR_CXSEP Util::ScalePixels(14, m_hWnd) #define SMP_LEFTBAR_CXSPC Util::ScalePixels(3, m_hWnd) #define SMP_LEFTBAR_CXBTN Util::ScalePixels(24, m_hWnd) #define SMP_LEFTBAR_CYBTN Util::ScalePixels(22, m_hWnd) static constexpr int TIMELINE_HEIGHT = 26; static int TimelineHeight(HWND hwnd) { auto height = Util::ScalePixels(TIMELINE_HEIGHT, hwnd); if(height % 2) height++; // Avoid weird-looking triangles if timeline is scaled to odd height return height; } static constexpr int MIN_ZOOM = -6; static constexpr int MAX_ZOOM = 10; // Defines the minimum length for selection for which // trimming will be done. This is the minimum value for // selection difference, so the minimum length of result // of trimming is nTrimLengthMin + 1. static constexpr SmpLength MIN_TRIM_LENGTH = 4; static constexpr UINT cLeftBarButtons[SMP_LEFTBAR_BUTTONS] = { ID_SAMPLE_ZOOMUP, ID_SAMPLE_ZOOMDOWN, ID_SEPARATOR, ID_SAMPLE_DRAW, ID_SAMPLE_ADDSILENCE, ID_SEPARATOR, ID_SAMPLE_GRID, ID_SEPARATOR, }; IMPLEMENT_SERIAL(CViewSample, CModScrollView, 0) BEGIN_MESSAGE_MAP(CViewSample, CModScrollView) //{{AFX_MSG_MAP(CViewSample) ON_WM_ERASEBKGND() ON_WM_SETFOCUS() ON_WM_SIZE() ON_WM_NCCALCSIZE() ON_WM_NCHITTEST() ON_WM_NCPAINT() ON_WM_MOUSEMOVE() ON_WM_NCMOUSEMOVE() ON_WM_LBUTTONDOWN() ON_WM_LBUTTONUP() ON_WM_LBUTTONDBLCLK() ON_WM_NCLBUTTONDOWN() ON_WM_NCLBUTTONUP() ON_WM_NCLBUTTONDBLCLK() ON_WM_RBUTTONDOWN() ON_WM_CHAR() ON_WM_DROPFILES() ON_WM_MOUSEWHEEL() ON_WM_XBUTTONUP() ON_WM_SETCURSOR() ON_COMMAND(ID_EDIT_UNDO, &CViewSample::OnEditUndo) ON_COMMAND(ID_EDIT_REDO, &CViewSample::OnEditRedo) ON_COMMAND(ID_EDIT_SELECT_ALL, &CViewSample::OnEditSelectAll) ON_COMMAND(ID_EDIT_CUT, &CViewSample::OnEditCut) ON_COMMAND(ID_EDIT_COPY, &CViewSample::OnEditCopy) ON_COMMAND(ID_EDIT_PASTE, &CViewSample::OnEditPaste) ON_COMMAND(ID_EDIT_MIXPASTE, &CViewSample::OnEditMixPaste) ON_COMMAND(ID_EDIT_PUSHFORWARDPASTE, &CViewSample::OnEditInsertPaste) ON_COMMAND(ID_SAMPLE_SETLOOP, &CViewSample::OnSetLoop) ON_COMMAND(ID_SAMPLE_SETSUSTAINLOOP, &CViewSample::OnSetSustainLoop) ON_COMMAND(ID_SAMPLE_8BITCONVERT, &CViewSample::On8BitConvert) ON_COMMAND(ID_SAMPLE_16BITCONVERT, &CViewSample::On16BitConvert) ON_COMMAND(ID_SAMPLE_MONOCONVERT, &CViewSample::OnMonoConvertMix) ON_COMMAND(ID_SAMPLE_MONOCONVERT_LEFT, &CViewSample::OnMonoConvertLeft) ON_COMMAND(ID_SAMPLE_MONOCONVERT_RIGHT, &CViewSample::OnMonoConvertRight) ON_COMMAND(ID_SAMPLE_MONOCONVERT_SPLIT, &CViewSample::OnMonoConvertSplit) ON_COMMAND(ID_SAMPLE_TRIM, &CViewSample::OnSampleTrim) ON_COMMAND(ID_PREVINSTRUMENT, &CViewSample::OnPrevInstrument) ON_COMMAND(ID_NEXTINSTRUMENT, &CViewSample::OnNextInstrument) ON_COMMAND(ID_SAMPLE_ZOOMONSEL, &CViewSample::OnZoomOnSel) ON_COMMAND(ID_SAMPLE_SETLOOPSTART, &CViewSample::OnSetLoopStart) ON_COMMAND(ID_SAMPLE_SETLOOPEND, &CViewSample::OnSetLoopEnd) ON_COMMAND(ID_CONVERT_PINGPONG_LOOP, &CViewSample::OnConvertPingPongLoop) ON_COMMAND(ID_SAMPLE_SETSUSTAINSTART, &CViewSample::OnSetSustainStart) ON_COMMAND(ID_SAMPLE_SETSUSTAINEND, &CViewSample::OnSetSustainEnd) ON_COMMAND(ID_CONVERT_PINGPONG_SUSTAIN, &CViewSample::OnConvertPingPongSustain) ON_COMMAND(ID_SAMPLE_ZOOMUP, &CViewSample::OnZoomUp) ON_COMMAND(ID_SAMPLE_ZOOMDOWN, &CViewSample::OnZoomDown) ON_COMMAND(ID_SAMPLE_DRAW, &CViewSample::OnDrawingToggle) ON_COMMAND(ID_SAMPLE_ADDSILENCE, &CViewSample::OnAddSilence) ON_COMMAND(ID_SAMPLE_GRID, &CViewSample::OnChangeGridSize) ON_COMMAND(ID_SAMPLE_QUICKFADE, &CViewSample::OnQuickFade) ON_COMMAND(ID_SAMPLE_SLICE, &CViewSample::OnSampleSlice) ON_COMMAND(ID_SAMPLE_INSERT_CUEPOINT, &CViewSample::OnSampleInsertCuePoint) ON_COMMAND(ID_SAMPLE_DELETE_CUEPOINT, &CViewSample::OnSampleDeleteCuePoint) ON_COMMAND(ID_SAMPLE_TIMELINE_SECONDS, &CViewSample::OnTimelineFormatSeconds) ON_COMMAND(ID_SAMPLE_TIMELINE_SAMPLES, &CViewSample::OnTimelineFormatSamples) ON_COMMAND(ID_SAMPLE_TIMELINE_SAMPLES_POW2, &CViewSample::OnTimelineFormatSamplesPow2) ON_COMMAND_RANGE(ID_SAMPLE_CUE_1, ID_SAMPLE_CUE_9, &CViewSample::OnSetCuePoint) ON_UPDATE_COMMAND_UI(ID_EDIT_UNDO, &CViewSample::OnUpdateUndo) ON_UPDATE_COMMAND_UI(ID_EDIT_REDO, &CViewSample::OnUpdateRedo) ON_MESSAGE(WM_MOD_MIDIMSG, &CViewSample::OnMidiMsg) ON_MESSAGE(WM_MOD_KEYCOMMAND, &CViewSample::OnCustomKeyMsg) //rewbs.customKeys //}}AFX_MSG_MAP END_MESSAGE_MAP() /////////////////////////////////////////////////////////////// // CViewSample operations CViewSample::CViewSample() : m_lastDrawPoint(-1, -1) , m_timelineHeight(TIMELINE_HEIGHT) { MemsetZero(m_NcButtonState); m_dwNotifyPos.fill(Notification::PosInvalid); m_bmpEnvBar.Create(&CMainFrame::GetMainFrame()->m_SampleIcons); } void CViewSample::OnInitialUpdate() { CModScrollView::OnInitialUpdate(); m_dwBeginSel = m_dwEndSel = 0; m_dwStatus.reset(SMPSTATUS_DRAWING); ModifyStyleEx(0, WS_EX_ACCEPTFILES); CMainFrame *pMainFrm = CMainFrame::GetMainFrame(); if (pMainFrm) { pMainFrm->SetInfoText(_T("")); pMainFrm->SetXInfoText(_T("")); } UpdateOPLEditor(); UpdateScrollSize(); UpdateNcButtonState(); EnableToolTips(); } // updateAll: Update all views including this one. Otherwise, only update update other views. void CViewSample::SetModified(SampleHint hint, bool updateAll, bool waveformModified) { CModDoc *pModDoc = GetDocument(); pModDoc->SetModified(); if(waveformModified) { // Update on-disk sample status in tree ModSample &sample = pModDoc->GetSoundFile().GetSample(m_nSample); if(sample.uFlags[SMP_KEEPONDISK] && !sample.uFlags[SMP_MODIFIED]) hint.Names(); sample.uFlags.set(SMP_MODIFIED); } pModDoc->UpdateAllViews(nullptr, hint.SetData(m_nSample), updateAll ? nullptr : this); } void CViewSample::UpdateScrollSize(int newZoom, bool forceRefresh, SmpLength centeredSample) { CModDoc *pModDoc = GetDocument(); if(pModDoc == nullptr || (newZoom == m_nZoom && !forceRefresh)) { return; } const int oldZoom = m_nZoom; m_nZoom = newZoom; GetClientRect(&m_rcClient); if(m_oplEditor && IsOPLInstrument()) { const auto size = m_oplEditor->GetMinimumSize(); m_oplEditor->SetWindowPos(nullptr, -m_nScrollPosX, -m_nScrollPosY, std::max(size.cx, m_rcClient.right), std::max(size.cy, m_rcClient.bottom), SWP_NOZORDER | SWP_NOACTIVATE); SetScrollSizes(MM_TEXT, size); return; } const CSoundFile &sndFile = pModDoc->GetSoundFile(); SIZE sizePage, sizeLine; SmpLength dwLen = 0; uint32 sampleRate = 8363; if((m_nSample > 0) && (m_nSample <= sndFile.GetNumSamples())) { const ModSample &sample = sndFile.GetSample(m_nSample); if(sample.HasSampleData()) dwLen = sample.nLength; sampleRate = sample.GetSampleRate(sndFile.GetType()); } // Compute scroll size in pixels if (newZoom == 0) // Fit to display m_sizeTotal.cx = m_rcClient.Width(); else if(newZoom == 1) // 1:1 m_sizeTotal.cx = dwLen; else if(newZoom > 1) // Zoom out m_sizeTotal.cx = (dwLen + (1 << (newZoom - 1)) - 1) >> (newZoom - 1); else // Zoom in - here, we don't compute the real number of visible pixels so that the scroll bar doesn't grow unnecessarily long. The scrolling code in OnScrollBy() compensates for this. m_sizeTotal.cx = dwLen + m_rcClient.Width() - (m_rcClient.Width() >> (-newZoom - 1)); m_sizeTotal.cy = 1; sizeLine.cx = (m_rcClient.right / 16) + 1; if(newZoom < 0) sizeLine.cx >>= (-newZoom - 1); sizeLine.cy = 1; sizePage.cx = sizeLine.cx * 4; sizePage.cy = 1; SetScrollSizes(MM_TEXT, m_sizeTotal, sizePage, sizeLine); if(oldZoom != newZoom) // After zoom change, keep the view position. { if(centeredSample != SmpLength(-1)) { ScrollToSample(centeredSample, false); } else { const SmpLength nOldPos = ScrollPosToSamplePos(oldZoom); const float fPosFraction = (dwLen > 0) ? static_cast(nOldPos) / dwLen : 0; SetScrollPos(SB_HORZ, static_cast(fPosFraction * GetScrollLimit(SB_HORZ))); } } // Choose optimal timeline interval for this zoom level if(m_sizeTotal.cx == 0 || dwLen == 0) return; const TimelineFormat format = TrackerSettings::Instance().sampleEditorTimelineFormat; double timelineInterval = MulDiv(150, m_nDPIx, 96); // Timeline interval should be around 150 pixels if(m_nZoom > 0) timelineInterval *= 1 << (m_nZoom - 1); else if(m_nZoom < 0) timelineInterval /= 1 << (-m_nZoom - 1); else if(m_sizeTotal.cx != 0) timelineInterval = timelineInterval * dwLen / m_sizeTotal.cx; if(format == TimelineFormat::Seconds) timelineInterval *= 1000.0 / sampleRate; if(timelineInterval < 1) timelineInterval = 1; const double power = (format == TimelineFormat::SamplesPow2) ? 2.0 : 10.0; m_timelineUnit = mpt::saturate_round(std::log(static_cast(timelineInterval)) / std::log(power)); if(m_timelineUnit < 1) m_timelineUnit = 0; m_timelineUnit = mpt::saturate_cast(std::pow(power, m_timelineUnit)); timelineInterval = std::max(1.0, std::round(timelineInterval / m_timelineUnit)) * m_timelineUnit; if(format == TimelineFormat::Seconds) timelineInterval *= sampleRate / 1000.0; if(m_nZoom > 0) timelineInterval /= 1 << (m_nZoom - 1); else if(m_nZoom < 0) timelineInterval *= 1 << (-m_nZoom - 1); else timelineInterval = timelineInterval * m_sizeTotal.cx / dwLen; m_timelineInterval = mpt::saturate_round(timelineInterval); m_cachedSampleRate = sampleRate; } // Center given sample in the view void CViewSample::ScrollToSample(SmpLength centeredSample, bool refresh) { int scrollToSample = centeredSample >> (std::max(1, m_nZoom) - 1); scrollToSample -= (m_rcClient.Width() / 2) >> (-std::min(-1, m_nZoom) - 1); Limit(scrollToSample, 0, GetScrollLimit(SB_HORZ)); SetScrollPos(SB_HORZ, scrollToSample); if(refresh) InvalidateSample(); } int CViewSample::CalcScroll(int ¤tPos, int amount, int style, int bar) { // Don't scroll if there is no valid scroll range (ie. no scroll bar) const CScrollBar *pBar = GetScrollBarCtrl(bar); DWORD dwStyle = GetStyle(); if((pBar != nullptr && !pBar->IsWindowEnabled()) || (pBar == nullptr && !(dwStyle & style))) { // Scroll bar not enabled amount = 0; } // Adjust current position int orig = GetScrollPos(bar); currentPos = Clamp(orig + amount, 0, GetScrollLimit(bar)); return -(currentPos - orig); } BOOL CViewSample::OnScrollBy(CSize sizeScroll, BOOL bDoScroll) { int x = 0, y = 0; int scrollByX = CalcScroll(x, sizeScroll.cx, WS_HSCROLL, SB_HORZ); int scrollByY = CalcScroll(y, sizeScroll.cy, WS_VSCROLL, SB_VERT); if(!scrollByX && !scrollByY) { // Nothing changed! return FALSE; } if (bDoScroll) { // Don't allow to scroll into the middle of a sampling point if(m_nZoom < 0 && !IsOPLInstrument()) { scrollByX *= (1 << (-m_nZoom - 1)); } ScrollWindow(scrollByX, scrollByY); if(scrollByX) SetScrollPos(SB_HORZ, x); if(scrollByY) SetScrollPos(SB_VERT, y); m_forceRedrawWaveform = true; } return TRUE; } void CViewSample::SetCurrentSample(SAMPLEINDEX nSmp) { CModDoc *pModDoc = GetDocument(); if(!pModDoc) return; if(nSmp < 1 || nSmp > pModDoc->GetNumSamples()) return; pModDoc->SetNotifications(Notification::Sample, nSmp); pModDoc->SetFollowWnd(m_hWnd); if(nSmp == m_nSample) return; m_dwBeginSel = m_dwEndSel = 0; m_dwStatus.reset(SMPSTATUS_DRAWING); if(CMainFrame *pMainFrm = CMainFrame::GetMainFrame(); pMainFrm) pMainFrm->SetInfoText(_T("")); const bool wasOPL = IsOPLInstrument(); m_nSample = nSmp; m_dwNotifyPos.fill(Notification::PosInvalid); if(!wasOPL && IsOPLInstrument()) SetScrollPos(SB_HORZ, 0); UpdateOPLEditor(); UpdateScrollSize(); UpdateNcButtonState(); InvalidateSample(); } bool CViewSample::IsOPLInstrument() const { return m_nSample >= 1 && m_nSample <= GetDocument()->GetNumSamples() && GetDocument()->GetSoundFile().GetSample(m_nSample).uFlags[CHN_ADLIB]; } void CViewSample::UpdateOPLEditor() { if(!IsOPLInstrument()) { if(m_oplEditor) m_oplEditor->ShowWindow(SW_HIDE); return; } CSoundFile &sndFile = GetDocument()->GetSoundFile(); ModSample &sample = sndFile.GetSample(m_nSample); if(sample.uFlags[CHN_ADLIB]) { if(!m_oplEditor) { try { m_oplEditor = std::make_unique(*this, sndFile); } catch(mpt::out_of_memory e) { mpt::delete_out_of_memory(e); } } if(m_oplEditor) { m_oplEditor->SetPatch(sample.adlib); auto size = m_oplEditor->GetMinimumSize(); m_oplEditor->SetWindowPos(nullptr, -m_nScrollPosX, -m_nScrollPosY, std::max(size.cx, m_rcClient.right), std::max(size.cy, m_rcClient.bottom), SWP_NOZORDER | SWP_NOACTIVATE | SWP_SHOWWINDOW); } } } void CViewSample::OnSetFocus(CWnd *pOldWnd) { CScrollView::OnSetFocus(pOldWnd); SetCurrentSample(m_nSample); } void CViewSample::SetZoom(int nZoom, SmpLength centeredSample) { if(nZoom == m_nZoom && centeredSample == SmpLength(-1)) return; if(nZoom > MAX_ZOOM) return; UpdateScrollSize(nZoom, true, centeredSample); InvalidateSample(); } SmpLength CViewSample::SnapToGrid(const SmpLength pos) const { if(m_nGridSegments <= 0 || GetDocument() == nullptr) return pos; const auto &sample = GetDocument()->GetSoundFile().GetSample(m_nSample); if(m_nGridSegments >= sample.nLength) return pos; const auto samplesPerSegment = static_cast(sample.nLength) / m_nGridSegments; return static_cast(mpt::round(pos / samplesPerSegment) * samplesPerSegment); } void CViewSample::SetCurSel(SmpLength nBegin, SmpLength nEnd) { if(GetDocument() == nullptr) return; CSoundFile &sndFile = GetDocument()->GetSoundFile(); const ModSample &sample = sndFile.GetSample(m_nSample); nBegin = SnapToGrid(nBegin); nEnd = SnapToGrid(nEnd); if(nBegin > nEnd) { std::swap(nBegin, nEnd); } if ((nBegin != m_dwBeginSel) || (nEnd != m_dwEndSel)) { RECT rect; SmpLength dMin = m_dwBeginSel, dMax = m_dwEndSel; if (m_dwBeginSel >= m_dwEndSel) { dMin = nBegin; dMax = nEnd; } if ((nBegin == dMin) && (dMax != nEnd)) { dMin = dMax; if (nEnd < dMin) dMin = nEnd; if (nEnd > dMax) dMax = nEnd; } else if ((nEnd == dMax) && (dMin != nBegin)) { dMax = dMin; if (nBegin < dMin) dMin = nBegin; if (nBegin > dMax) dMax = nBegin; } else { if (nBegin < dMin) dMin = nBegin; if (nEnd > dMax) dMax = nEnd; } m_dwBeginSel = nBegin; m_dwEndSel = nEnd; rect.top = m_rcClient.top; rect.bottom = m_rcClient.bottom; rect.left = SampleToScreen(dMin); rect.right = SampleToScreen(dMax) + 1; if (rect.left < 0) rect.left = 0; if (rect.right > m_rcClient.right) rect.right = m_rcClient.right; if (rect.right > rect.left) InvalidateRect(&rect, FALSE); CMainFrame *pMainFrm = CMainFrame::GetMainFrame(); if(pMainFrm) { mpt::ustring s; if(m_dwEndSel > m_dwBeginSel) { const SmpLength selLength = m_dwEndSel - m_dwBeginSel; mpt::ustring (*fmt)(unsigned int, char, const SmpLength &) = &mpt::ufmt::dec; if(TrackerSettings::Instance().cursorPositionInHex) fmt = &mpt::ufmt::HEX; s = MPT_UFORMAT("[{}-{}] ({} sample{}, ")(fmt(3, ',', m_dwBeginSel), fmt(3, ',', m_dwEndSel), fmt(3, ',', selLength), (selLength == 1) ? U_("") : U_("s")); // Length in seconds auto sampleRate = sample.GetSampleRate(sndFile.GetType()); if(sampleRate <= 0) sampleRate = 8363; double sec = selLength / static_cast(sampleRate); if(sec < 1) s += MPT_UFORMAT("{}ms")(mpt::ufmt::flt(sec * 1000.0, 3)); else s += MPT_UFORMAT("{}s")(mpt::ufmt::flt(sec, 3)); // Length in beats double beats = selLength; if(sndFile.m_nTempoMode == TempoMode::Modern) { beats *= sndFile.m_PlayState.m_nMusicTempo.ToDouble() * (1.0 / 60.0) / sampleRate; } else { sndFile.RecalculateSamplesPerTick(); beats *= sndFile.GetSampleRate() / static_cast(Util::mul32to64_unsigned(sndFile.m_PlayState.m_nCurrentRowsPerBeat, sndFile.m_PlayState.m_nMusicSpeed) * Util::mul32to64_unsigned(sndFile.m_PlayState.m_nSamplesPerTick, sampleRate)); } s += MPT_UFORMAT(", {} beats)")(mpt::ufmt::flt(beats, 5)); } pMainFrm->SetInfoText(mpt::ToCString(s)); } } } int32 CViewSample::SampleToScreen(SmpLength pos, bool ignoreScrollPos) const { CModDoc *pModDoc = GetDocument(); if((pModDoc) && (m_nSample <= pModDoc->GetNumSamples())) { SmpLength nLen = pModDoc->GetSoundFile().GetSample(m_nSample).nLength; if(!nLen) return 0; const SmpLength scrollPos = ignoreScrollPos ? 0 : m_nScrollPosX; if(m_nZoom > 0) return (pos >> (m_nZoom - 1)) - scrollPos; else if(m_nZoom < 0) return (pos - scrollPos) << (-m_nZoom - 1); else return Util::muldiv(pos, m_sizeTotal.cx, nLen); } return 0; } SmpLength CViewSample::ScreenToSample(int32 x, bool ignoreSampleLength) const { const CModDoc *pModDoc = GetDocument(); SmpLength n = 0; if((pModDoc) && (m_nSample <= pModDoc->GetNumSamples())) { SmpLength smpLen = pModDoc->GetSoundFile().GetSample(m_nSample).nLength; if(!smpLen) return 0; if(m_nZoom > 0) n = std::max(0, m_nScrollPosX + x) << (m_nZoom - 1); else if(m_nZoom < 0) n = std::max(0, m_nScrollPosX + mpt::rshift_signed(x, (-m_nZoom - 1))); else { if(x < 0) x = 0; if(m_sizeTotal.cx) n = Util::muldiv(x, smpLen, m_sizeTotal.cx); } if(!ignoreSampleLength) LimitMax(n, smpLen); } return n; } int32 CViewSample::SecondsToScreen(double x) const { const ModSample &sample = GetDocument()->GetSoundFile().GetSample(m_nSample); const auto sampleRate = sample.GetSampleRate(GetDocument()->GetModType()); if(sampleRate == 0 || sample.nLength == 0) return 0; x *= sampleRate; // This is essentially duplicated from SampleToScreen but carried out in double precision to avoid rounding errors at very high zoom levels if(m_nZoom > 0) x = x / (1 << (m_nZoom - 1)) - m_nScrollPosX; else if(m_nZoom < 0) x = (x - m_nScrollPosX) * (1 << (-m_nZoom - 1)); else x = x * m_sizeTotal.cx / sample.nLength; return mpt::saturate_round(x); } double CViewSample::ScreenToSeconds(int32 x, bool ignoreSampleLength) const { const ModSample &sample = GetDocument()->GetSoundFile().GetSample(m_nSample); const auto sampleRate = sample.GetSampleRate(GetDocument()->GetModType()); if(sampleRate == 0) return 0; return ScreenToSample(x, ignoreSampleLength) / static_cast(sampleRate); } static bool HitTest(int pointX, int objX, int marginL, int marginR, int top, int bottom, CRect *rect) { if(!mpt::is_in_range(pointX, objX - marginL, objX + marginR)) return false; if(rect) *rect = CRect{objX - marginL, top, objX + marginR + 1, bottom}; return true; } std::pair CViewSample::PointToItem(CPoint point, CRect *rect) const { if(IsOPLInstrument()) return {HitTestItem::Nothing, MAX_SAMPLE_LENGTH}; const bool inTimeline = point.y < m_timelineHeight; if(m_dwEndSel > m_dwBeginSel && !inTimeline && !m_dwStatus[SMPSTATUS_DRAWING]) { const int margin = Util::ScalePixels(5, m_hWnd); if(HitTest(point.x, SampleToScreen(m_dwBeginSel), margin, margin, m_timelineHeight, m_rcClient.bottom, rect)) return {HitTestItem::SelectionStart, m_dwBeginSel}; if(HitTest(point.x, SampleToScreen(m_dwEndSel), margin, margin, m_timelineHeight, m_rcClient.bottom, rect)) return {HitTestItem::SelectionEnd, m_dwEndSel}; } const auto &sndFile = GetDocument()->GetSoundFile(); if(m_nSample <= sndFile.GetNumSamples() && inTimeline) { const auto &sample = sndFile.GetSample(m_nSample); if(sample.nSustainStart < sample.nSustainEnd && sample.nSustainStart < sample.nLength) { if(HitTest(point.x, SampleToScreen(sample.nSustainEnd), m_timelineHeight / 2, 0, 0, m_timelineHeight, rect)) return {HitTestItem::SustainEnd, sample.nSustainEnd}; if(HitTest(point.x, SampleToScreen(sample.nSustainStart), 0, m_timelineHeight / 2, 0, m_timelineHeight, rect)) return {HitTestItem::SustainStart, sample.nSustainStart}; } if (sample.nLoopStart < sample.nLoopEnd && sample.nLoopStart < sample.nLength) { if(HitTest(point.x, SampleToScreen(sample.nLoopEnd), m_timelineHeight / 2, 0, 0, m_timelineHeight, rect)) return {HitTestItem::LoopEnd, sample.nLoopEnd}; if(HitTest(point.x, SampleToScreen(sample.nLoopStart), 0, m_timelineHeight / 2, 0, m_timelineHeight, rect)) return {HitTestItem::LoopStart, sample.nLoopStart }; } for(size_t i = 0; i < std::size(sample.cues); i++) { size_t cue = std::size(sample.cues) - 1 - i; // If two cues overlap visually, the cue with the higher ID is drawn on top, so pick it first if(sample.cues[cue] < sample.nLength && HitTest(point.x, SampleToScreen(sample.cues[cue]), m_timelineHeight / 2, m_timelineHeight / 2, 0, m_timelineHeight, rect)) return {static_cast(static_cast(HitTestItem::CuePointFirst) + cue), sample.cues[cue]}; } } if(inTimeline) return {HitTestItem::Nothing, MAX_SAMPLE_LENGTH}; else return {HitTestItem::SampleData, ScreenToSample(point.x)}; } void CViewSample::InvalidateSample(bool invalidateWaveform) { if(invalidateWaveform) m_forceRedrawWaveform = true; InvalidateRect(nullptr, FALSE); } void CViewSample::InvalidateTimeline() { auto rect = m_rcClient; rect.bottom = m_timelineHeight; InvalidateRect(rect, FALSE); } LRESULT CViewSample::OnModViewMsg(WPARAM wParam, LPARAM lParam) { switch(wParam) { case VIEWMSG_SETCURRENTSAMPLE: SetZoom(static_cast(lParam) >> 16); SetCurrentSample(lParam & 0xFFFF); break; case VIEWMSG_LOADSTATE: if (lParam) { SAMPLEVIEWSTATE *pState = (SAMPLEVIEWSTATE *)lParam; if (pState->nSample == m_nSample) { SetCurSel(pState->dwBeginSel, pState->dwEndSel); SetScrollPos(SB_HORZ, pState->dwScrollPos); UpdateScrollSize(); InvalidateSample(); } } break; case VIEWMSG_SAVESTATE: if (lParam) { SAMPLEVIEWSTATE *pState = (SAMPLEVIEWSTATE *)lParam; pState->dwScrollPos = m_nScrollPosX; pState->dwBeginSel = m_dwBeginSel; pState->dwEndSel = m_dwEndSel; pState->nSample = m_nSample; } break; case VIEWMSG_SETMODIFIED: // Update from OPL editor SetModified(UpdateHint::FromLPARAM(lParam).ToType(), false, true); GetDocument()->UpdateOPLInstrument(m_nSample); break; case VIEWMSG_PREPAREUNDO: GetDocument()->GetSampleUndo().PrepareUndo(m_nSample, sundo_none, "Edit OPL Patch"); break; case VIEWMSG_SETFOCUS: case VIEWMSG_SETACTIVE: GetParentFrame()->SetActiveView(this); if(IsOPLInstrument() && m_oplEditor) m_oplEditor->SetFocus(); else SetFocus(); break; default: return CModScrollView::OnModViewMsg(wParam, lParam); } return 0; } /////////////////////////////////////////////////////////////// // CViewSample drawing void CViewSample::UpdateView(UpdateHint hint, CObject *pObj) { if(pObj == this) { return; } auto modDoc = GetDocument(); auto &sndFile = modDoc->GetSoundFile(); const SampleHint sampleHint = hint.ToType(); FlagSet hintType = sampleHint.GetType(); const SAMPLEINDEX updateSmp = sampleHint.GetSample(); if(hintType[HINT_MPTOPTIONS | HINT_MODTYPE] || (hintType[HINT_SAMPLEDATA] && (m_nSample == updateSmp || updateSmp == 0))) { if(hintType[HINT_SAMPLEDATA] && m_oplEditor && m_nSample <= sndFile.GetNumSamples()) { ModSample &sample = sndFile.GetSample(m_nSample); if(sample.uFlags[CHN_ADLIB]) m_oplEditor->SetPatch(sample.adlib); } UpdateOPLEditor(); UpdateScrollSize(); UpdateNcButtonState(); InvalidateSample(); } if(hintType[HINT_SAMPLEINFO]) { // Sample rate change may imply redrawing of timeline if(m_nSample <= sndFile.GetNumSamples() && m_cachedSampleRate != sndFile.GetSample(m_nSample).GetSampleRate(sndFile.GetType())) { UpdateScrollSize(); InvalidateTimeline(); } if(m_nSample > sndFile.GetNumSamples() || !sndFile.GetSample(m_nSample).HasSampleData()) { // Disable sample drawing if we cannot actually draw anymore. m_dwStatus.reset(SMPSTATUS_DRAWING); UpdateNcButtonState(); } if(m_nSample == updateSmp || updateSmp == 0) InvalidateSample(false); } } #define YCVT(n, bits) (ymed - (((n) * yrange) >> (bits))) // Draw one channel of sample data, 1:1 ratio or higher (zoomed in) void CViewSample::DrawSampleData1(HDC hdc, int ymed, int cx, int cy, SmpLength len, SampleFlags uFlags, const void *pSampleData) { int smplsize; int yrange = cy/2; const int8 *psample = static_cast(pSampleData); int y0 = 0; smplsize = (uFlags & CHN_16BIT) ? 2 : 1; if (uFlags & CHN_STEREO) smplsize *= 2; if (uFlags & CHN_16BIT) { y0 = YCVT(*((signed short *)(psample-smplsize)),15); } else { y0 = YCVT(*(psample-smplsize),7); } ::MoveToEx(hdc, -1, y0, NULL); SmpLength numDrawSamples, loopDiv = 0; int loopShift = 0; if (m_nZoom == 1) { // Linear 1:1 scale numDrawSamples = cx; } else if(m_nZoom < 0) { // 2:1, 4:1, etc... zoom loopShift = (-m_nZoom - 1); // Round up numDrawSamples = (cx + (1 << loopShift) - 1) >> loopShift; } else { // Stretch to screen ASSERT(!m_nZoom); numDrawSamples = len; loopDiv = numDrawSamples; } LimitMax(numDrawSamples, len); if (uFlags & CHN_16BIT) { // 16-Bit for (SmpLength n = 0; n <= numDrawSamples; n++) { int x = loopDiv ? ((n * cx) / loopDiv) : (n << loopShift); int y = *(const int16 *)psample; ::LineTo(hdc, x, YCVT(y,15)); psample += smplsize; } } else { // 8-bit for (SmpLength n = 0; n <= numDrawSamples; n++) { int x = loopDiv ? ((n * cx) / loopDiv) : (n << loopShift); int y = *psample; ::LineTo(hdc, x, YCVT(y,7)); psample += smplsize; } } } #if defined(MPT_ENABLE_ARCH_INTRINSICS_SSE2) OPENMPT_NAMESPACE_END #include OPENMPT_NAMESPACE_BEGIN // SSE2 implementation for min/max finder, packs 8*int16 in a 128-bit XMM register. // scanlen = How many samples to process on this channel static void sse2_findminmax16(const void *p, SmpLength scanlen, int channels, int &smin, int &smax) { scanlen *= channels; // Put minimum / maximum in 8 packed int16 values __m128i minVal = _mm_set1_epi16(static_cast(smin)); __m128i maxVal = _mm_set1_epi16(static_cast(smax)); SmpLength scanlen8 = scanlen / 8; if(scanlen8) { const __m128i *v = static_cast(p); p = static_cast(p) + scanlen8; while(scanlen8--) { __m128i curVals = _mm_loadu_si128(v++); minVal = _mm_min_epi16(minVal, curVals); maxVal = _mm_max_epi16(maxVal, curVals); } // Now we have 8 minima and maxima each, in case of stereo they are interleaved L/R values. // Move the upper 4 values to the lower half and compute the minima/maxima of that. __m128i minVal2 = _mm_unpackhi_epi64(minVal, minVal); __m128i maxVal2 = _mm_unpackhi_epi64(maxVal, maxVal); minVal = _mm_min_epi16(minVal, minVal2); maxVal = _mm_max_epi16(maxVal, maxVal2); // Now we have 4 minima and maxima each, in case of stereo they are interleaved L/R values. // Move the upper 2 values to the lower half and compute the minima/maxima of that. minVal2 = _mm_shuffle_epi32(minVal, _MM_SHUFFLE(1, 1, 1, 1)); maxVal2 = _mm_shuffle_epi32(maxVal, _MM_SHUFFLE(1, 1, 1, 1)); minVal = _mm_min_epi16(minVal, minVal2); maxVal = _mm_max_epi16(maxVal, maxVal2); if(channels < 2) { // Mono: Compute the minima/maxima of the both remaining values minVal2 = _mm_shufflelo_epi16(minVal, _MM_SHUFFLE(1, 1, 1, 1)); maxVal2 = _mm_shufflelo_epi16(maxVal, _MM_SHUFFLE(1, 1, 1, 1)); minVal = _mm_min_epi16(minVal, minVal2); maxVal = _mm_max_epi16(maxVal, maxVal2); } } const int16 *p16 = static_cast(p); while(scanlen & 7) { scanlen -= channels; __m128i curVals = _mm_set1_epi16(*p16); p16 += channels; minVal = _mm_min_epi16(minVal, curVals); maxVal = _mm_max_epi16(maxVal, curVals); } smin = static_cast(_mm_cvtsi128_si32(minVal)); smax = static_cast(_mm_cvtsi128_si32(maxVal)); } // SSE2 implementation for min/max finder, packs 16*int8 in a 128-bit XMM register. // scanlen = How many samples to process on this channel static void sse2_findminmax8(const void *p, SmpLength scanlen, int channels, int &smin, int &smax) { scanlen *= channels; // Put minimum / maximum in 16 packed int8 values __m128i minVal = _mm_set1_epi8(static_cast(smin ^ 0x80u)); __m128i maxVal = _mm_set1_epi8(static_cast(smax ^ 0x80u)); // For signed <-> unsigned conversion (_mm_min_epi8/_mm_max_epi8 is SSE4) __m128i xorVal = _mm_set1_epi8(0x80u); SmpLength scanlen16 = scanlen / 16; if(scanlen16) { const __m128i *v = static_cast(p); p = static_cast(p) + scanlen16; while(scanlen16--) { __m128i curVals = _mm_loadu_si128(v++); curVals = _mm_xor_si128(curVals, xorVal); minVal = _mm_min_epu8(minVal, curVals); maxVal = _mm_max_epu8(maxVal, curVals); } // Now we have 16 minima and maxima each, in case of stereo they are interleaved L/R values. // Move the upper 8 values to the lower half and compute the minima/maxima of that. __m128i minVal2 = _mm_unpackhi_epi64(minVal, minVal); __m128i maxVal2 = _mm_unpackhi_epi64(maxVal, maxVal); minVal = _mm_min_epu8(minVal, minVal2); maxVal = _mm_max_epu8(maxVal, maxVal2); // Now we have 8 minima and maxima each, in case of stereo they are interleaved L/R values. // Move the upper 4 values to the lower half and compute the minima/maxima of that. minVal2 = _mm_shuffle_epi32(minVal, _MM_SHUFFLE(1, 1, 1, 1)); maxVal2 = _mm_shuffle_epi32(maxVal, _MM_SHUFFLE(1, 1, 1, 1)); minVal = _mm_min_epu8(minVal, minVal2); maxVal = _mm_max_epu8(maxVal, maxVal2); // Now we have 4 minima and maxima each, in case of stereo they are interleaved L/R values. // Move the upper 2 values to the lower half and compute the minima/maxima of that. minVal2 = _mm_srai_epi32(minVal, 16); maxVal2 = _mm_srai_epi32(maxVal, 16); minVal = _mm_min_epu8(minVal, minVal2); maxVal = _mm_max_epu8(maxVal, maxVal2); if(channels < 2) { // Mono: Compute the minima/maxima of the both remaining values minVal2 = _mm_srai_epi16(minVal, 8); maxVal2 = _mm_srai_epi16(maxVal, 8); minVal = _mm_min_epu8(minVal, minVal2); maxVal = _mm_max_epu8(maxVal, maxVal2); } } const int8 *p8 = static_cast(p); while(scanlen & 15) { scanlen -= channels; __m128i curVals = _mm_set1_epi8((*p8) ^ 0x80u); p8 += channels; minVal = _mm_min_epu8(minVal, curVals); maxVal = _mm_max_epu8(maxVal, curVals); } smin = static_cast(_mm_cvtsi128_si32(minVal) ^ 0x80u); smax = static_cast(_mm_cvtsi128_si32(maxVal) ^ 0x80u); } #endif std::pair CViewSample::FindMinMax(const int8 *p, SmpLength numSamples, int numChannels) { int minVal = 127; int maxVal = -128; #if defined(MPT_ENABLE_ARCH_INTRINSICS_SSE2) if(CPU::HasFeatureSet(CPU::feature::sse2) && numSamples >= 16) { sse2_findminmax8(p, numSamples, numChannels, minVal, maxVal); } else #endif { while(numSamples--) { int s = *p; if(s < minVal) minVal = s; if(s > maxVal) maxVal = s; p += numChannels; } } return { minVal, maxVal }; } std::pair CViewSample::FindMinMax(const int16 *p, SmpLength numSamples, int numChannels) { int minVal = 32767; int maxVal = -32768; #if defined(MPT_ENABLE_ARCH_INTRINSICS_SSE2) if(CPU::HasFeatureSet(CPU::feature::sse2) && numSamples >= 8) { sse2_findminmax16(p, numSamples, numChannels, minVal, maxVal); } else #endif { while(numSamples--) { int s = *p; if(s < minVal) minVal = s; if(s > maxVal) maxVal = s; p += numChannels; } } return { minVal, maxVal }; } // Draw one channel of zoomed-out sample data void CViewSample::DrawSampleData2(HDC hdc, int ymed, int cx, int cy, SmpLength len, SampleFlags uFlags, const void *pSampleData) { int oldsmin, oldsmax; int yrange = cy/2; const int8 *psample = static_cast(pSampleData); int32 y0 = 0, xmax; SmpLength poshi; uint64 posincr, posfrac; // Increments have 16-bit fractional part if (len <= 0) return; const int numChannels = (uFlags & CHN_STEREO) ? 2 : 1; const int smplsize = ((uFlags & CHN_16BIT) ? 2 : 1) * numChannels; if (uFlags & CHN_16BIT) { y0 = YCVT(*((const int16 *)(psample-smplsize)), 15); } else { y0 = YCVT(*(psample-smplsize), 7); } oldsmin = oldsmax = y0; if (m_nZoom > 0) { xmax = len>>(m_nZoom-1); if (xmax > cx) xmax = cx; posincr = (uint64(1) << (m_nZoom-1+16)); } else { xmax = cx; //posincr = Util::muldiv(len, 0x10000, cx); posincr = uint64(len) * uint64(0x10000) / uint64(cx); } ::MoveToEx(hdc, 0, ymed, NULL); posfrac = 0; poshi = 0; for (int x=0; x((posfrac+0xffff) >> 16); if (poshi >= len) poshi = len-1; if (poshi + scanlen > len) scanlen = len-poshi; if (scanlen < 1) scanlen = 1; // 16-bit if (uFlags & CHN_16BIT) { signed short *p = (signed short *)(psample + poshi*smplsize); auto minMax = FindMinMax(p, scanlen, numChannels); smin = YCVT(minMax.first, 15); smax = YCVT(minMax.second, 15); } else // 8-bit { const int8 *p = psample + poshi * smplsize; auto minMax = FindMinMax(p, scanlen, numChannels); smin = YCVT(minMax.first, 7); smax = YCVT(minMax.second, 7); } if (smin > oldsmax) { ::MoveToEx(hdc, x-1, oldsmax - 1, NULL); ::LineTo(hdc, x, smin); } if (smax < oldsmin) { ::MoveToEx(hdc, x-1, oldsmin, NULL); ::LineTo(hdc, x, smax); } ::MoveToEx(hdc, x, smax-1, NULL); ::LineTo(hdc, x, smin); oldsmin = smin; oldsmax = smax; poshi += static_cast(posfrac>>16); posfrac &= 0xffff; } } static void DrawTriangleHorz(CDC &dc, int x, int width, int height, COLORREF color) { const POINT points[] = { {x, 0}, {x, height}, {x + width, height / 2}, }; dc.SetDCPenColor(RGB(GetRValue(color) / 2, GetGValue(color) / 2, GetBValue(color) / 2)); dc.SetDCBrushColor(color); dc.Polygon(points, static_cast(std::size(points))); } static void DrawTriangleVert(CDC &dc, int x, int width, int height, COLORREF color) { const POINT points[] = { {x - width, height / 2}, {x + width, height / 2}, {x, height}, }; dc.SetDCPenColor(RGB(GetRValue(color) / 2, GetGValue(color) / 2, GetBValue(color) / 2)); dc.SetDCBrushColor(color); dc.Polygon(points, static_cast(std::size(points))); } void CViewSample::OnDraw(CDC *pDC) { const CModDoc *pModDoc = GetDocument(); if ((!pModDoc) || (!pDC)) return; const CRect rcClient = m_rcClient; CRect rect, rc; const SmpLength smpScrollPos = ScrollPosToSamplePos(); const auto &colors = TrackerSettings::Instance().rgbCustomColors; const CSoundFile &sndFile = pModDoc->GetSoundFile(); const ModSample &sample = sndFile.GetSample((m_nSample <= sndFile.GetNumSamples()) ? m_nSample : 0); if(sample.uFlags[CHN_ADLIB]) { CModScrollView::OnDraw(pDC); return; } // Create off-screen image and timeline font if(!m_offScreenDC.m_hDC) { m_offScreenDC.CreateCompatibleDC(pDC); m_offScreenBitmap.CreateCompatibleBitmap(pDC, m_rcClient.Width(), m_rcClient.Height()); m_offScreenDC.SelectObject(m_offScreenBitmap); NONCLIENTMETRICS metrics; metrics.cbSize = sizeof(metrics); SystemParametersInfo(SPI_GETNONCLIENTMETRICS, sizeof(metrics), &metrics, 0); metrics.lfMessageFont.lfHeight = mpt::saturate_round(metrics.lfMessageFont.lfHeight * 0.8); metrics.lfMessageFont.lfWidth = mpt::saturate_round(metrics.lfMessageFont.lfWidth * 0.8); m_timelineFont.DeleteObject(); m_timelineFont.CreateFontIndirect(&metrics.lfMessageFont); } if(!m_waveformDC.m_hDC) { m_waveformDC.CreateCompatibleDC(pDC); m_waveformBitmap.CreateCompatibleBitmap(pDC, m_rcClient.Width(), m_rcClient.Height()); m_waveformDC.SelectObject(m_waveformBitmap); m_forceRedrawWaveform = true; } const auto oldPen = m_offScreenDC.SelectObject(CMainFrame::penDarkGray); const auto oldBrush = m_offScreenDC.SelectStockObject(DC_BRUSH); const auto oldFont = m_offScreenDC.SelectObject(m_timelineFont); // Draw timeline const int timelineHeight = TimelineHeight(m_hWnd); if(timelineHeight != m_timelineHeight) { m_timelineHeight = timelineHeight; m_forceRedrawWaveform = true; } { const TimelineFormat format = TrackerSettings::Instance().sampleEditorTimelineFormat; CRect timeline = rcClient; timeline.bottom = timeline.top + timelineHeight + 1; m_offScreenDC.DrawEdge(timeline, EDGE_ETCHED, BF_MIDDLE | BF_BOTTOM); m_offScreenDC.SetTextColor(GetSysColor(COLOR_BTNTEXT)); m_offScreenDC.SetBkMode(TRANSPARENT); if(!m_timelineUnit) m_timelineUnit = 1; if(m_timelineInterval && sample.nLength) { rc = timeline; const auto sampleRate = sample.GetSampleRate(sndFile.GetType()); const int textOffset = Util::ScalePixels(4, m_hWnd); mpt::tstring text; for(int x = -(SampleToScreen(ScrollPosToSamplePos(), true) % m_timelineInterval); x < m_rcClient.right + m_timelineInterval; x += m_timelineInterval) { text.clear(); if(format == TimelineFormat::Seconds && sampleRate) { const int64 time = mpt::saturate_round(std::round(ScreenToSeconds(x, true) * 1000.0 / m_timelineUnit) * m_timelineUnit); rc.left = SecondsToScreen(time / 1000.0); if(rc.left >= m_rcClient.right) break; const bool showSeconds = time >= 1000 || (time == 0 && m_timelineUnit >= 1000); const auto secMs = std::div(time, int64(1000)); if(showSeconds) { const auto minSec = std::div(secMs.quot, int64(60)); const bool showMinutes = minSec.quot != 0 || (time == 0 && m_timelineUnit >= 60000); if(showMinutes) text += mpt::tfmt::dec(3, _T(','), minSec.quot) + _T("mn"); if(minSec.rem || !showMinutes) text += mpt::tfmt::dec(3, _T(','), minSec.rem) + _T("s"); } if(secMs.rem || !showSeconds) { text += mpt::tfmt::val(secMs.rem) + _T("ms"); } } else { const SmpLength smp = mpt::saturate_round(std::round(ScreenToSample(x, true) / static_cast(m_timelineUnit)) * m_timelineUnit); rc.left = SampleToScreen(smp); if(rc.left >= m_rcClient.right) break; text += mpt::tfmt::dec(3, _T(','), smp) + _T(" smp"); } rc.bottom = timelineHeight; for(int i = 0; i < 10; i++) { rect = rc; rect.left += i * m_timelineInterval / 10; if(i == 0) rect.top = 0; else if(i == 5) rect.top = timelineHeight / 2; else rect.top = timelineHeight - timelineHeight / 4; m_offScreenDC.DrawEdge(rect, EDGE_ETCHED, BF_LEFT); } rc.bottom = timelineHeight / 2; rc.left += textOffset; m_offScreenDC.DrawText(text.c_str(), rc, DT_VCENTER | DT_SINGLELINE | DT_NOPREFIX); } // Cues m_offScreenDC.SelectStockObject(DC_PEN); m_offScreenDC.SetTextColor(RGB(0, 0, 0)); const int arrowWidth = timelineHeight / 2; for(size_t i = 0; i < std::size(sample.cues); i++) { if(sample.cues[i] >= sample.nLength) continue; int xl = SampleToScreen(sample.cues[i]); if((xl >= -arrowWidth) && (xl < rcClient.right)) { DrawTriangleVert(m_offScreenDC, xl, arrowWidth, timelineHeight, colors[MODCOLOR_SAMPLE_CUEPOINT]); rc.SetRect(xl - arrowWidth, timelineHeight / 2 - 1, xl + arrowWidth, timelineHeight); m_offScreenDC.DrawText(mpt::tfmt::val(i + 1).c_str(), rc, DT_CENTER | DT_SINGLELINE | DT_NOPREFIX); } } // Loop Start/End if(sample.nLoopEnd > sample.nLoopStart) { int xl = SampleToScreen(sample.nLoopStart); if((xl >= -arrowWidth) && (xl <= rcClient.right + arrowWidth)) DrawTriangleHorz(m_offScreenDC, xl, arrowWidth, timelineHeight, colors[MODCOLOR_SAMPLE_LOOPMARKER]); xl = SampleToScreen(sample.nLoopEnd); if((xl >= -arrowWidth) && (xl <= rcClient.right + arrowWidth)) DrawTriangleHorz(m_offScreenDC, xl, -timelineHeight / 2, timelineHeight, colors[MODCOLOR_SAMPLE_LOOPMARKER]); } // Sustain Loop Start/End if(sample.nSustainEnd > sample.nSustainStart) { int xl = SampleToScreen(sample.nSustainStart); if((xl >= -arrowWidth) && (xl <= rcClient.right + arrowWidth)) DrawTriangleHorz(m_offScreenDC, xl, timelineHeight / 2, timelineHeight, colors[MODCOLOR_SAMPLE_SUSTAINMARKER]); xl = SampleToScreen(sample.nSustainEnd); if((xl >= -arrowWidth) && (xl <= rcClient.right + arrowWidth)) DrawTriangleHorz(m_offScreenDC, xl, -timelineHeight / 2, timelineHeight, colors[MODCOLOR_SAMPLE_SUSTAINMARKER]); } } } rect = rcClient; rect.top = timelineHeight; if((rcClient.bottom > rcClient.top) && (rcClient.right > rcClient.left)) { const int ymed = (rect.top + rect.bottom) / 2; const int yrange = (rect.bottom - rect.top) / 2; // Erase background if ((m_dwBeginSel < m_dwEndSel) && (m_dwEndSel > smpScrollPos)) { rc = rect; if (m_dwBeginSel > smpScrollPos) { rc.right = SampleToScreen(m_dwBeginSel); if (rc.right > rcClient.right) rc.right = rcClient.right; if (rc.right > rc.left) m_offScreenDC.FillSolidRect(&rc, colors[MODCOLOR_BACKSAMPLE]); rc.left = rc.right; } if (rc.left < 0) rc.left = 0; rc.right = SampleToScreen(m_dwEndSel) + 1; if (rc.right > rcClient.right) rc.right = rcClient.right; if(rc.right > rc.left) m_offScreenDC.FillSolidRect(&rc, colors[MODCOLOR_SAMPLESELECTED]); rc.left = rc.right; if (rc.left < 0) rc.left = 0; rc.right = rcClient.right; if(rc.right > rc.left) m_offScreenDC.FillSolidRect(&rc, colors[MODCOLOR_BACKSAMPLE]); } else { m_offScreenDC.FillSolidRect(&rect, colors[MODCOLOR_BACKSAMPLE]); } m_offScreenDC.SelectObject(CMainFrame::penDarkGray); if (sample.uFlags[CHN_STEREO]) { m_offScreenDC.MoveTo(0, ymed - yrange / 2); m_offScreenDC.LineTo(rcClient.right, ymed - yrange / 2); m_offScreenDC.MoveTo(0, ymed + yrange / 2); m_offScreenDC.LineTo(rcClient.right, ymed + yrange / 2); } else { m_offScreenDC.MoveTo(0, ymed); m_offScreenDC.LineTo(rcClient.right, ymed); } // Drawing sample if(sample.HasSampleData() && yrange && (sample.nLength > 1) && (rect.right > 1)) { // Loop Start/End if ((sample.nLoopEnd > smpScrollPos) && (sample.nLoopEnd > sample.nLoopStart)) { int xl = SampleToScreen(sample.nLoopStart); if ((xl >= 0) && (xl < rcClient.right)) { m_offScreenDC.MoveTo(xl, rect.top); m_offScreenDC.LineTo(xl, rect.bottom); } xl = SampleToScreen(sample.nLoopEnd); if((xl >= 0) && (xl < rcClient.right)) { m_offScreenDC.MoveTo(xl, rect.top); m_offScreenDC.LineTo(xl, rect.bottom); } } // Sustain Loop Start/End if ((sample.nSustainEnd > smpScrollPos) && (sample.nSustainEnd > sample.nSustainStart)) { m_offScreenDC.SetBkMode(OPAQUE); m_offScreenDC.SetBkColor(RGB(0xFF, 0xFF, 0xFF)); m_offScreenDC.SelectObject(CMainFrame::penHalfDarkGray); int xl = SampleToScreen(sample.nSustainStart); if ((xl >= 0) && (xl < rcClient.right)) { m_offScreenDC.MoveTo(xl, rect.top); m_offScreenDC.LineTo(xl, rect.bottom); } xl = SampleToScreen(sample.nSustainEnd); if ((xl >= 0) && (xl < rcClient.right)) { m_offScreenDC.MoveTo(xl, rect.top); m_offScreenDC.LineTo(xl, rect.bottom); } } // Active cue point if(IsCuePoint(m_dragItem)) { m_offScreenDC.SetBkMode(TRANSPARENT); m_offScreenDC.SelectObject(CMainFrame::penHalfDarkGray); int xl = SampleToScreen(sample.cues[CuePointFromItem(m_dragItem)]); if((xl >= 0) && (xl < rcClient.right)) { m_offScreenDC.MoveTo(xl, rect.top); m_offScreenDC.LineTo(xl, rect.bottom); } } // Drawing Sample Data const auto backgroundCol = ~colors[MODCOLOR_SAMPLE]; if(m_forceRedrawWaveform) { m_forceRedrawWaveform = false; m_waveformDC.SelectStockObject(DC_PEN); m_waveformDC.FillSolidRect(rect, backgroundCol); m_waveformDC.SetDCPenColor(colors[MODCOLOR_SAMPLE]); const int smplsize = sample.GetBytesPerSample(); if(m_nZoom == 1 || m_nZoom < 0 || ((!m_nZoom) && (sample.nLength <= (SmpLength)rect.Width()))) { // Draw sample data in 1:1 ratio or higher (zoom in) SmpLength len = sample.nLength - smpScrollPos; const std::byte *psample = sample.sampleb() + smpScrollPos * smplsize; if(sample.uFlags[CHN_STEREO]) { DrawSampleData1(m_waveformDC, ymed - yrange / 2, rect.right, yrange, len, sample.uFlags, psample); DrawSampleData1(m_waveformDC, ymed + yrange / 2, rect.right, yrange, len, sample.uFlags, psample + smplsize / 2); } else { DrawSampleData1(m_waveformDC, ymed, rect.right, yrange * 2, len, sample.uFlags, psample); } } else { // Draw zoomed-out saple data SmpLength len = sample.nLength; int xscroll = 0; if(m_nZoom > 0) { xscroll = smpScrollPos; len -= smpScrollPos; } const std::byte *psample = sample.sampleb() + xscroll * smplsize; if(sample.uFlags[CHN_STEREO]) { DrawSampleData2(m_waveformDC, ymed - yrange / 2, rect.right, yrange, len, sample.uFlags, psample); DrawSampleData2(m_waveformDC, ymed + yrange / 2, rect.right, yrange, len, sample.uFlags, psample + smplsize / 2); } else { DrawSampleData2(m_waveformDC, ymed, rect.right, yrange * 2, len, sample.uFlags, psample); } } } m_offScreenDC.TransparentBlt(rect.left, rect.top, rect.Width(), rect.Height(), &m_waveformDC, rect.left, rect.top, rect.Width(), rect.Height(), backgroundCol); } } if(m_nGridSegments > 0 && m_nGridSegments < sample.nLength && sample.nLength != 0) { // Draw sample grid m_offScreenDC.SetBkColor(TrackerSettings::Instance().rgbCustomColors[MODCOLOR_BACKSAMPLE]); m_offScreenDC.SelectObject(CMainFrame::penHalfDarkGray); const auto segmentsByLength = static_cast(m_nGridSegments) / sample.nLength; const auto samplesPerSegment = static_cast(sample.nLength) / m_nGridSegments; const auto leftSegment = std::max(uint32(1), mpt::saturate_round(ScreenToSample(rect.left) * segmentsByLength)); const auto rightSegment = std::min(m_nGridSegments, mpt::saturate_round(ScreenToSample(rect.right) * segmentsByLength)); for(uint32 i = leftSegment; i <= rightSegment; i++) { int screenPos = SampleToScreen(mpt::saturate_round(samplesPerSegment * i)); m_offScreenDC.MoveTo(screenPos, rect.top); m_offScreenDC.LineTo(screenPos, rect.bottom); } } DrawPositionMarks(); BitBlt(pDC->m_hDC, m_rcClient.left, m_rcClient.top, m_rcClient.Width(), m_rcClient.Height(), m_offScreenDC, 0, 0, SRCCOPY); if(oldFont) m_offScreenDC.SelectObject(oldFont); if(oldBrush) m_offScreenDC.SelectObject(oldBrush); if(oldPen) m_offScreenDC.SelectObject(oldPen); } void CViewSample::DrawPositionMarks() { const ModSample &sample = GetDocument()->GetSoundFile().GetSample(m_nSample); if(!sample.HasSampleData() || sample.uFlags[CHN_ADLIB]) { return; } CRect rect; for(auto pos : m_dwNotifyPos) if (pos != Notification::PosInvalid) { rect.top = m_timelineHeight; rect.left = SampleToScreen(pos); rect.right = rect.left + 1; rect.bottom = m_rcClient.bottom + 1; if ((rect.right >= 0) && (rect.right < m_rcClient.right)) m_offScreenDC.InvertRect(&rect); } } LRESULT CViewSample::OnPlayerNotify(Notification *pnotify) { CModDoc *pModDoc = GetDocument(); if ((!pnotify) || (!pModDoc)) return 0; if (pnotify->type[Notification::Stop]) { bool invalidate = false; for(auto &pos : m_dwNotifyPos) { if(pos != Notification::PosInvalid) { pos = Notification::PosInvalid; invalidate = true; } } if(invalidate) InvalidateSample(false); } else if (pnotify->type[Notification::Sample] && pnotify->item == m_nSample && !IsOPLInstrument()) { if(m_dwNotifyPos != pnotify->pos) { HDC hdc = ::GetDC(m_hWnd); DrawPositionMarks(); // Erase old marks... m_dwNotifyPos = pnotify->pos; DrawPositionMarks(); // ...and draw new ones BitBlt(hdc, m_rcClient.left, m_rcClient.top, m_rcClient.Width(), m_rcClient.Height(), m_offScreenDC, 0, 0, SRCCOPY); ::ReleaseDC(m_hWnd, hdc); } } return 0; } bool CViewSample::GetNcButtonRect(UINT button, CRect &rect) const { rect.left = 4; rect.top = 3; rect.bottom = rect.top + SMP_LEFTBAR_CYBTN; if(button >= SMP_LEFTBAR_BUTTONS) return false; for(UINT i = 0; i < button; i++) { if(cLeftBarButtons[i] == ID_SEPARATOR) rect.left += SMP_LEFTBAR_CXSEP; else rect.left += SMP_LEFTBAR_CXBTN + SMP_LEFTBAR_CXSPC; } if(cLeftBarButtons[button] == ID_SEPARATOR) { rect.left += SMP_LEFTBAR_CXSEP/2 - 2; rect.right = rect.left + 2; return false; } else { rect.right = rect.left + SMP_LEFTBAR_CXBTN; } return true; } UINT CViewSample::GetNcButtonAtPoint(CPoint point, CRect *outRect) const { CRect rect, rcWnd; UINT button = uint32_max; GetWindowRect(&rcWnd); for(UINT i = 0; i < SMP_LEFTBAR_BUTTONS; i++) { if(!(m_NcButtonState[i] & NCBTNS_DISABLED) && GetNcButtonRect(i, rect)) { rect.OffsetRect(rcWnd.left, rcWnd.top); if(rect.PtInRect(point)) { button = i; break; } } } if(outRect) *outRect = rect; return button; } void CViewSample::DrawNcButton(CDC *pDC, UINT nBtn) { CRect rect; COLORREF crHi = GetSysColor(COLOR_3DHILIGHT); COLORREF crDk = GetSysColor(COLOR_3DSHADOW); COLORREF crFc = GetSysColor(COLOR_3DFACE); COLORREF c1, c2; if(GetNcButtonRect(nBtn, rect)) { DWORD dwStyle = m_NcButtonState[nBtn]; COLORREF c3, c4; int xofs = 0, yofs = 0, nImage = 0; c1 = c2 = c3 = c4 = crFc; if (!(TrackerSettings::Instance().m_dwPatternSetup & PATTERN_FLATBUTTONS)) { c1 = c3 = crHi; c2 = crDk; c4 = RGB(0,0,0); } if (dwStyle & (NCBTNS_PUSHED|NCBTNS_CHECKED)) { c1 = crDk; c2 = crHi; if (!(TrackerSettings::Instance().m_dwPatternSetup & PATTERN_FLATBUTTONS)) { c4 = crHi; c3 = (dwStyle & NCBTNS_PUSHED) ? RGB(0,0,0) : crDk; } xofs = yofs = 1; } else if ((dwStyle & NCBTNS_MOUSEOVER) && (TrackerSettings::Instance().m_dwPatternSetup & PATTERN_FLATBUTTONS)) { c1 = crHi; c2 = crDk; } switch(cLeftBarButtons[nBtn]) { case ID_SAMPLE_ZOOMUP: nImage = SIMAGE_ZOOMUP; break; case ID_SAMPLE_ZOOMDOWN: nImage = SIMAGE_ZOOMDOWN; break; case ID_SAMPLE_DRAW: nImage = SIMAGE_DRAW; break; case ID_SAMPLE_ADDSILENCE: nImage = SIMAGE_RESIZE; break; case ID_SAMPLE_GRID: nImage = SIMAGE_GRID; break; } pDC->Draw3dRect(rect.left-1, rect.top-1, SMP_LEFTBAR_CXBTN+2, SMP_LEFTBAR_CYBTN+2, c3, c4); pDC->Draw3dRect(rect.left, rect.top, SMP_LEFTBAR_CXBTN, SMP_LEFTBAR_CYBTN, c1, c2); rect.DeflateRect(1, 1); pDC->FillSolidRect(&rect, crFc); rect.left += xofs; rect.top += yofs; if (dwStyle & NCBTNS_CHECKED) m_bmpEnvBar.Draw(pDC, SIMAGE_CHECKED, rect.TopLeft(), ILD_NORMAL); m_bmpEnvBar.Draw(pDC, nImage, rect.TopLeft(), ILD_NORMAL); } else { c1 = c2 = crFc; if (TrackerSettings::Instance().m_dwPatternSetup & PATTERN_FLATBUTTONS) { c1 = crDk; c2 = crHi; } pDC->Draw3dRect(rect.left, rect.top, 2, SMP_LEFTBAR_CYBTN, c1, c2); } } void CViewSample::OnNcPaint() { RECT rect; CModScrollView::OnNcPaint(); GetWindowRect(&rect); // Assumes there is no other non-client items rect.bottom = SMP_LEFTBAR_CY; rect.right -= rect.left; rect.left = 0; rect.top = 0; if ((rect.left < rect.right) && (rect.top < rect.bottom)) { CDC *pDC = GetWindowDC(); { // Shadow auto shadowRect = rect; shadowRect.top = shadowRect.bottom - 1; pDC->FillSolidRect(&shadowRect, GetSysColor(COLOR_BTNSHADOW)); } rect.bottom--; if(rect.top < rect.bottom) pDC->FillSolidRect(&rect, GetSysColor(COLOR_BTNFACE)); if(rect.top + 2 < rect.bottom) { for (UINT i=0; i pModDoc->GetNumSamples() || IsOPLInstrument()) { dwStyle |= NCBTNS_DISABLED; } break; case ID_SAMPLE_ZOOMUP: case ID_SAMPLE_ZOOMDOWN: case ID_SAMPLE_GRID: if(IsOPLInstrument()) dwStyle |= NCBTNS_DISABLED; break; } if (dwStyle != m_NcButtonState[i]) { m_NcButtonState[i] = dwStyle; if (!pDC) pDC = GetWindowDC(); DrawNcButton(pDC, i); } } if (pDC) ReleaseDC(pDC); } /////////////////////////////////////////////////////////////// // CViewSample messages void CViewSample::OnSize(UINT nType, int cx, int cy) { CModScrollView::OnSize(nType, cx, cy); m_offScreenBitmap.DeleteObject(); m_offScreenDC.DeleteDC(); m_waveformBitmap.DeleteObject(); m_waveformDC.DeleteDC(); if (((nType == SIZE_RESTORED) || (nType == SIZE_MAXIMIZED)) && (cx > 0) && (cy > 0)) { UpdateScrollSize(); } } void CViewSample::OnNcCalcSize(BOOL bCalcValidRects, NCCALCSIZE_PARAMS* lpncsp) { CModScrollView::OnNcCalcSize(bCalcValidRects, lpncsp); if (lpncsp) { lpncsp->rgrc[0].top += SMP_LEFTBAR_CY; if (lpncsp->rgrc[0].bottom < lpncsp->rgrc[0].top) lpncsp->rgrc[0].top = lpncsp->rgrc[0].bottom; } } void CViewSample::ScrollToPosition(int x) // logical coordinates { CPoint pt; // now in device coordinates - limit if out of range int xMax = GetScrollLimit(SB_HORZ); pt.x = x; pt.y = 0; if (pt.x < 0) pt.x = 0; else if (pt.x > xMax) pt.x = xMax; ScrollToDevicePosition(pt); } template T CViewSample::GetSampleValueFromPoint(const ModSample &smp, const CPoint &point) const { static_assert(sizeof(T) == sizeof(uT) && sizeof(T) <= 2); const int channelHeight = (m_rcClient.Height() - m_timelineHeight) / smp.GetNumChannels(); int yPos = point.y - m_drawChannel * channelHeight - m_timelineHeight; int value = std::numeric_limits::max() - std::numeric_limits::max() * yPos / channelHeight; Limit(value, std::numeric_limits::min(), std::numeric_limits::max()); return static_cast(value); } template void CViewSample::SetInitialDrawPoint(ModSample &smp, const CPoint &point) { if(m_rcClient.Height() >= m_timelineHeight) m_drawChannel = (point.y - m_timelineHeight) * smp.GetNumChannels() / (m_rcClient.Height() - m_timelineHeight); else m_drawChannel = 0; Limit(m_drawChannel, 0, (int)smp.GetNumChannels() - 1); T *data = static_cast(smp.samplev()) + m_drawChannel; data[m_dwEndDrag * smp.GetNumChannels()] = GetSampleValueFromPoint(smp, point); } template void CViewSample::SetSampleData(ModSample &smp, const CPoint &point, const SmpLength old) { T *data = static_cast(smp.samplev()) + m_drawChannel + old * smp.GetNumChannels(); const int oldvalue = *data; const int value = GetSampleValueFromPoint(smp, point); const int inc = (m_dwEndDrag > old ? 1 : -1); const int ptrInc = inc * smp.GetNumChannels(); for(SmpLength i = old; i != m_dwEndDrag; i += inc, data += ptrInc) { *data = static_cast(static_cast(oldvalue) + (value - oldvalue) * (static_cast(i - old) / static_cast(m_dwEndDrag - old))); } *data = static_cast(value); } void CViewSample::OnMouseMove(UINT flags, CPoint point) { CModDoc *pModDoc = GetDocument(); if(m_nBtnMouseOver < SMP_LEFTBAR_BUTTONS || m_dwStatus[SMPSTATUS_NCLBTNDOWN]) { m_dwStatus.reset(SMPSTATUS_NCLBTNDOWN); m_nBtnMouseOver = 0xFFFF; UpdateNcButtonState(); CMainFrame *pMainFrm = CMainFrame::GetMainFrame(); if (pMainFrm) pMainFrm->SetHelpText(_T("")); } if(!pModDoc) return; CSoundFile &sndFile = pModDoc->GetSoundFile(); if(m_nSample > sndFile.GetNumSamples()) return; auto &sample = sndFile.GetSample(m_nSample); if (m_rcClient.PtInRect(point)) { const SmpLength x = ScreenToSample(point.x); CString(*fmt)(unsigned int, char, const SmpLength &) = &mpt::cfmt::dec; if(TrackerSettings::Instance().cursorPositionInHex) fmt = &mpt::cfmt::HEX; UpdateIndicator(MPT_CFORMAT("Cursor: {}")(fmt(3, ',', x))); CMainFrame *pMainFrm = CMainFrame::GetMainFrame(); if(pMainFrm && m_dwEndSel <= m_dwBeginSel) { // Show cursor position as offset effect if no selection is made. if(m_nSample > 0 && sample.HasSampleData() && x < sample.nLength) { const SmpLength xLow = (x / 0x100) % 0x100; const SmpLength xHigh = x / 0x10000; const char offsetChar = sndFile.GetModSpecifications().GetEffectLetter(CMD_OFFSET); const bool hasHighOffset = (sndFile.GetType() & (MOD_TYPE_S3M | MOD_TYPE_IT | MOD_TYPE_MPT | MOD_TYPE_XM)); const char highOffsetChar = sndFile.GetModSpecifications().GetEffectLetter(static_cast(sndFile.GetModSpecifications().HasCommand(CMD_S3MCMDEX) ? CMD_S3MCMDEX : CMD_XFINEPORTAUPDOWN)); CString s; if(xHigh == 0) s.Format(_T("Offset: %c%02X"), offsetChar, xLow); else if(hasHighOffset && xHigh < 0x10) s.Format(_T("Offset: %c%02X, %cA%X"), offsetChar, xLow, highOffsetChar, xHigh); else s = _T("Beyond offset range"); pMainFrm->SetInfoText(s); double linear; SmpLength offset = x * sample.GetNumChannels() + (point.y - m_timelineHeight) * sample.GetNumChannels() / (m_rcClient.Height() - m_timelineHeight); if(sample.uFlags[CHN_16BIT]) linear = sample.sample16()[offset] / 32768.0; else linear = sample.sample8()[offset] / 128.0; pMainFrm->SetXInfoText(MPT_TFORMAT("Value At Cursor: {}% / {}")(mpt::tfmt::fix(linear * 100.0, 3), CModDoc::LinearToDecibels(std::abs(linear), 1.0)).c_str()); } else { pMainFrm->SetInfoText(_T("")); pMainFrm->SetXInfoText(_T("")); } } } else { UpdateIndicator(nullptr); } if(m_dwStatus[SMPSTATUS_MOUSEDRAG]) { const SmpLength len = sndFile.GetSample(m_nSample).nLength; if(!len) return; SmpLength old = m_dwEndDrag; if(m_nZoom) { if(point.x < 0) { CPoint pt; pt.x = point.x; pt.y = 0; if(OnScrollBy(pt)) { UpdateWindow(); } point.x = 0; } if (point.x > m_rcClient.right) { CPoint pt; pt.x = point.x - m_rcClient.right; pt.y = 0; if (OnScrollBy(pt)) { UpdateWindow(); } point.x = m_rcClient.right; } } // Note: point.x might have changed in if block above in case we're scrolling. SmpLength x; if(m_dwStatus[SMPSTATUS_DRAWING]) { // Do not snap to grid and adjust for mouse-down position when drawing x = ScreenToSample(point.x); } else if(m_fineDrag) { x = m_startDragValue + (point.x - m_startDragPoint.x) / Util::ScalePixels(2, m_hWnd); } else if (m_nZoom < 0 || (m_nZoom == 0 && sample.nLength > static_cast(m_rcClient.Width()))) { // Don't adjust selection to mouse down point when zooming into the sample x = SnapToGrid(ScreenToSample(point.x)); } else { x = SnapToGrid(ScreenToSample(SampleToScreen(m_startDragValue) + point.x - m_startDragPoint.x)); } if((flags & MK_SHIFT) && !m_fineDrag) { m_fineDrag = true; m_startDragPoint = point; m_startDragValue = x; } else if(!(flags & MK_SHIFT) && m_fineDrag) { m_fineDrag = false; m_startDragPoint = point; m_startDragValue = ScreenToSample(point.x); } bool update = false; SmpLength *updateLoopPoint = nullptr; const char *updateLoopDesc = nullptr; switch(m_dragItem) { case HitTestItem::SelectionStart: case HitTestItem::SelectionEnd: if(m_dwEndDrag != x) { m_dwEndDrag = x; SetCurSel(m_dwBeginDrag, m_dwEndDrag); update = true; } break; case HitTestItem::LoopStart: if(x < sample.nLoopEnd) { updateLoopPoint = &sample.nLoopStart; updateLoopDesc = "Set Loop Start"; } break; case HitTestItem::LoopEnd: if(x > sample.nLoopStart) { updateLoopPoint = &sample.nLoopEnd; updateLoopDesc = "Set Loop End"; } break; case HitTestItem::SustainStart: if(x < sample.nSustainEnd) { updateLoopPoint = &sample.nSustainStart; updateLoopDesc = "Set Sustain Start"; } break; case HitTestItem::SustainEnd: if(x > sample.nSustainStart) { updateLoopPoint = &sample.nSustainEnd; updateLoopDesc = "Set Sustain End"; } break; default: if(IsCuePoint(m_dragItem)) { int cue = CuePointFromItem(m_dragItem); updateLoopPoint = &sample.cues[cue]; updateLoopDesc ="Set Cue Point"; } break; } if(updateLoopPoint && updateLoopDesc && *updateLoopPoint != x) { if(!m_dragPreparedUndo) pModDoc->GetSampleUndo().PrepareUndo(m_nSample, sundo_none, updateLoopDesc); m_dragPreparedUndo = true; update = true; *updateLoopPoint = x; sample.PrecomputeLoops(sndFile, true); SetModified(SampleHint().Info(), true, false); } if(m_dwStatus[SMPSTATUS_DRAWING] && m_dragItem == HitTestItem::SampleData) { m_dwEndDrag = x; if(m_dwEndDrag < len) { // Shift = draw horizontal lines if(flags & MK_SHIFT) { if(m_lastDrawPoint.y != -1) point.y = m_lastDrawPoint.y; m_lastDrawPoint = point; } else { m_lastDrawPoint.SetPoint(-1, -1); } LimitMax(old, sample.nLength); if(sample.GetElementarySampleSize() == 2) SetSampleData(sample, point, old); else if(sample.GetElementarySampleSize() == 1) SetSampleData(sample, point, old); sample.PrecomputeLoops(sndFile, false); InvalidateSample(); SetModified(SampleHint().Data(), false, true); } } else if(update) { UpdateWindow(); } } } BOOL CViewSample::OnSetCursor(CWnd *pWnd, UINT nHitTest, UINT message) { // Update mouse cursor if we are close to a selection point if(nHitTest == HTCLIENT && (message == WM_MOUSEMOVE || message == WM_LBUTTONDOWN)) { CPoint point; GetCursorPos(&point); ScreenToClient(&point); const auto item = PointToItem(point).first; if(item != HitTestItem::Nothing && item != HitTestItem::SampleData) { SetCursor(CMainFrame::curVSplit); return TRUE; } } return CModScrollView::OnSetCursor(pWnd, nHitTest, message); } void CViewSample::OnLButtonDown(UINT flags, CPoint point) { CModDoc *pModDoc = GetDocument(); if(m_dwStatus[SMPSTATUS_MOUSEDRAG] || (!pModDoc)) return; CSoundFile &sndFile = pModDoc->GetSoundFile(); ModSample &sample = sndFile.GetSample(m_nSample); if (!sample.nLength) return; m_dwStatus.set(SMPSTATUS_MOUSEDRAG); SetFocus(); SetCapture(); bool oldsel = (m_dwBeginSel != m_dwEndSel); // shift + click = update selection const auto [item, itemPos] = PointToItem(point); if(!m_dwStatus[SMPSTATUS_DRAWING] && (flags & MK_SHIFT) && item == HitTestItem::SampleData) { oldsel = true; m_dwEndDrag = itemPos; SetCurSel(m_dwBeginDrag, m_dwEndDrag); } else { m_dragItem = item; m_startDragPoint = point; m_startDragValue = itemPos; m_fineDrag = (flags & MK_SHIFT); m_dragPreparedUndo = false; switch(m_dragItem) { case HitTestItem::SampleData: m_dwBeginDrag = m_dwEndDrag = ScreenToSample(point.x); if(!m_dwStatus[SMPSTATUS_DRAWING]) m_dragItem = HitTestItem::SelectionEnd; break; case HitTestItem::SelectionStart: m_dwBeginDrag = m_dwEndSel; m_dwEndDrag = itemPos; break; case HitTestItem::SelectionEnd: m_dwBeginDrag = m_dwBeginSel; m_dwEndDrag = itemPos; break; default: if(IsCuePoint(m_dragItem)) InvalidateSample(false); break; } } if(oldsel) SetCurSel(m_dwBeginDrag, m_dwEndDrag); // set initial point for sample drawing if (m_dwStatus[SMPSTATUS_DRAWING] && m_dragItem == HitTestItem::SampleData) { m_lastDrawPoint = point; pModDoc->GetSampleUndo().PrepareUndo(m_nSample, sundo_replace, "Draw Sample"); if(sample.GetElementarySampleSize() == 2) SetInitialDrawPoint(sample, point); else if(sample.GetElementarySampleSize() == 1) SetInitialDrawPoint(sample, point); sndFile.GetSample(m_nSample).PrecomputeLoops(sndFile, false); InvalidateSample(); SetModified(SampleHint().Data(), false, true); } else { // ctrl + click = play from cursor pos if(flags & MK_CONTROL) PlayNote(NOTE_MIDDLEC, ScreenToSample(point.x)); } } void CViewSample::OnLButtonUp(UINT, CPoint) { if(m_dwStatus[SMPSTATUS_MOUSEDRAG]) { m_dwStatus.reset(SMPSTATUS_MOUSEDRAG); ReleaseCapture(); } if(IsCuePoint(m_dragItem)) InvalidateSample(false); m_dragItem = HitTestItem::Nothing; m_startDragValue = MAX_SAMPLE_LENGTH; m_lastDrawPoint.SetPoint(-1, -1); } void CViewSample::OnLButtonDblClk(UINT, CPoint) { CModDoc *pModDoc = GetDocument(); if (pModDoc) { SmpLength len = pModDoc->GetSoundFile().GetSample(m_nSample).nLength; if (len && !m_dwStatus[SMPSTATUS_DRAWING]) SetCurSel(0, len); } } void CViewSample::OnRButtonDown(UINT, CPoint pt) { CModDoc *pModDoc = GetDocument(); if(pModDoc) { const CSoundFile &sndFile = pModDoc->GetSoundFile(); const ModSample &sample = sndFile.GetSample(m_nSample); HMENU hMenu = ::CreatePopupMenu(); CInputHandler* ih = CMainFrame::GetInputHandler(); if(!hMenu) return; TCHAR s[256]; if(pt.y < m_timelineHeight) { const auto item = PointToItem(pt).first; if(IsCuePoint(item)) { m_dwMenuParam = CuePointFromItem(item); wsprintf(s, _T("&Delete Cue Point %d"), 1 + static_cast(m_dwMenuParam)); ::AppendMenu(hMenu, MF_STRING, ID_SAMPLE_DELETE_CUEPOINT, s); ::AppendMenu(hMenu, MF_SEPARATOR, 0, _T("")); } else { if(*std::max_element(sample.cues.begin(), sample.cues.end()) >= sample.nLength) { m_dwMenuParam = ScreenToSample(pt.x); wsprintf(s, _T("&Insert Cue Point at %s"), mpt::cfmt::dec(3, ',', m_dwMenuParam).GetString()); ::AppendMenu(hMenu, MF_STRING, ID_SAMPLE_INSERT_CUEPOINT, s); ::AppendMenu(hMenu, MF_SEPARATOR, 0, _T("")); } } auto fmt = TrackerSettings::Instance().sampleEditorTimelineFormat.Get(); ::AppendMenu(hMenu, MF_STRING | (fmt == TimelineFormat::Seconds ? MF_CHECKED : 0), ID_SAMPLE_TIMELINE_SECONDS, _T("&Seconds")); ::AppendMenu(hMenu, MF_STRING | (fmt == TimelineFormat::Samples ? MF_CHECKED : 0), ID_SAMPLE_TIMELINE_SAMPLES, _T("S&les")); ::AppendMenu(hMenu, MF_STRING | (fmt == TimelineFormat::SamplesPow2 ? MF_CHECKED : 0), ID_SAMPLE_TIMELINE_SAMPLES_POW2, _T("Samples (&Power of 2)")); ClientToScreen(&pt); ::TrackPopupMenu(hMenu, TPM_LEFTALIGN | TPM_RIGHTBUTTON, pt.x, pt.y, 0, m_hWnd, NULL); ::DestroyMenu(hMenu); return; } if (sample.HasSampleData() && !sample.uFlags[CHN_ADLIB]) { if (m_dwEndSel >= m_dwBeginSel + 4) { ::AppendMenu(hMenu, MF_STRING | (CanZoomSelection() ? 0 : MF_GRAYED), ID_SAMPLE_ZOOMONSEL, ih->GetKeyTextFromCommand(kcSampleZoomSelection, _T("Zoom"))); ::AppendMenu(hMenu, MF_STRING, ID_SAMPLE_SETLOOP, _T("Set As Loop")); if (sndFile.GetType() & (MOD_TYPE_IT|MOD_TYPE_MPT)) ::AppendMenu(hMenu, MF_STRING, ID_SAMPLE_SETSUSTAINLOOP, _T("Set As Sustain Loop")); ::AppendMenu(hMenu, MF_SEPARATOR, 0, _T("")); } else { SmpLength dwPos = ScreenToSample(pt.x); CString pos = mpt::cfmt::dec(3, ',', dwPos); if (dwPos <= sample.nLength) { //Set loop points SmpLength loopEnd = (sample.nLoopEnd > 0) ? sample.nLoopEnd : sample.nLength; wsprintf(s, _T("Set &Loop Start to:\t%s"), pos.GetString()); ::AppendMenu(hMenu, MF_STRING | (dwPos + 4 <= loopEnd ? 0 : MF_GRAYED), ID_SAMPLE_SETLOOPSTART, s); wsprintf(s, _T("Set &Loop End to:\t%s"), pos.GetString()); ::AppendMenu(hMenu, MF_STRING | (dwPos >= sample.nLoopStart + 4 ? 0 : MF_GRAYED), ID_SAMPLE_SETLOOPEND, s); if(sample.HasPingPongLoop()) ::AppendMenu(hMenu, MF_STRING, ID_CONVERT_PINGPONG_LOOP, ih->GetKeyTextFromCommand(kcSampleConvertPingPongLoop, _T("Convert to Unidirectional Loop"))); if (sndFile.GetType() & (MOD_TYPE_IT|MOD_TYPE_MPT)) { //Set sustain loop points SmpLength sustainEnd = (sample.nSustainEnd > 0) ? sample.nSustainEnd : sample.nLength; ::AppendMenu(hMenu, MF_SEPARATOR, 0, _T("")); wsprintf(s, _T("Set &Sustain Start to:\t%s"), pos.GetString()); ::AppendMenu(hMenu, MF_STRING | (dwPos + 4 <= sustainEnd ? 0 : MF_GRAYED), ID_SAMPLE_SETSUSTAINSTART, s); wsprintf(s, _T("Set &Sustain End to:\t%s"), pos.GetString()); ::AppendMenu(hMenu, MF_STRING | (dwPos >= sample.nSustainStart + 4 ? 0 : MF_GRAYED), ID_SAMPLE_SETSUSTAINEND, s); if(sample.HasPingPongSustainLoop()) ::AppendMenu(hMenu, MF_STRING, ID_CONVERT_PINGPONG_SUSTAIN, ih->GetKeyTextFromCommand(kcSampleConvertPingPongSustain, _T("Convert to Unidirectional Sustain Loop"))); } //if(sndFile.GetModSpecifications().HasVolCommand(VOLCMD_OFFSET)) { // Sample cues ::AppendMenu(hMenu, MF_SEPARATOR, 0, _T("")); HMENU hCueMenu = ::CreatePopupMenu(); bool hasValidCues = false; for(std::size_t i = 0; i < std::size(sample.cues); i++) { const SmpLength cue = sample.cues[i]; wsprintf(s, _T("Cue &%d: %s"), 1 + static_cast(i), cue < sample.nLength ? mpt::cfmt::dec(3, ',', cue).GetString() : _T("unused")); ::AppendMenu(hCueMenu, MF_STRING, ID_SAMPLE_CUE_1 + i, s); if(cue > 0 && cue < sample.nLength) hasValidCues = true; } wsprintf(s, _T("Set Sample Cu&e to:\t%s"), pos.GetString()); ::AppendMenu(hMenu, MF_POPUP, reinterpret_cast(hCueMenu), s); ::AppendMenu(hMenu, MF_STRING | (hasValidCues ? 0 : MF_GRAYED), ID_SAMPLE_SLICE, ih->GetKeyTextFromCommand(kcSampleSlice, _T("Slice at cue points"))); } ::AppendMenu(hMenu, MF_SEPARATOR, 0, _T("")); m_dwMenuParam = dwPos; } } if(sample.GetElementarySampleSize() > 1) ::AppendMenu(hMenu, MF_STRING, ID_SAMPLE_8BITCONVERT, ih->GetKeyTextFromCommand(kcSample8Bit, _T("Convert to &8-bit"))); else ::AppendMenu(hMenu, MF_STRING, ID_SAMPLE_16BITCONVERT, ih->GetKeyTextFromCommand(kcSample8Bit, _T("Convert to &16-bit"))); if(sample.GetNumChannels() > 1) { HMENU hMonoMenu = ::CreatePopupMenu(); ::AppendMenu(hMonoMenu, MF_STRING, ID_SAMPLE_MONOCONVERT, ih->GetKeyTextFromCommand(kcSampleMonoMix, _T("&Mix Channels"))); ::AppendMenu(hMonoMenu, MF_STRING, ID_SAMPLE_MONOCONVERT_LEFT, ih->GetKeyTextFromCommand(kcSampleMonoLeft, _T("&Left Channel"))); ::AppendMenu(hMonoMenu, MF_STRING, ID_SAMPLE_MONOCONVERT_RIGHT, ih->GetKeyTextFromCommand(kcSampleMonoRight, _T("&Right Channel"))); ::AppendMenu(hMonoMenu, MF_STRING, ID_SAMPLE_MONOCONVERT_SPLIT, ih->GetKeyTextFromCommand(kcSampleMonoSplit, _T("&Split Sample"))); ::AppendMenu(hMenu, MF_POPUP, reinterpret_cast(hMonoMenu), _T("Convert to &Mono")); } // "Trim" menu item is responding differently if there's no selection, // but a loop present: "trim around loop point"! (jojo in topic 2258) CString trimMenuText = _T("Tr&im"); bool isGrayed = ((m_dwEndSel <= m_dwBeginSel) || (m_dwEndSel - m_dwBeginSel < MIN_TRIM_LENGTH) || (m_dwEndSel - m_dwBeginSel == sample.nLength)); if ((m_dwBeginSel == m_dwEndSel) && (sample.nLoopStart < sample.nLoopEnd)) { // no selection => use loop points trimMenuText += _T(" around loop points"); // Check whether trim menu item can be enabled (loop not too short or long for trimming). if( (sample.nLoopEnd <= sample.nLength) && (sample.nLoopEnd - sample.nLoopStart >= MIN_TRIM_LENGTH) && (sample.nLoopEnd - sample.nLoopStart < sample.nLength) ) isGrayed = false; } ::AppendMenu(hMenu, MF_STRING | (isGrayed ? MF_GRAYED : 0), ID_SAMPLE_TRIM, ih->GetKeyTextFromCommand(kcSampleTrim, trimMenuText)); if((m_dwBeginSel == 0 && m_dwEndSel != 0) || (m_dwBeginSel < sample.nLength && m_dwEndSel == sample.nLength)) { ::AppendMenu(hMenu, MF_STRING, ID_SAMPLE_QUICKFADE, ih->GetKeyTextFromCommand(kcSampleQuickFade, _T("Quick &Fade"))); } ::AppendMenu(hMenu, MF_STRING, ID_EDIT_CUT, ih->GetKeyTextFromCommand(kcEditCut, _T("Cu&t"))); ::AppendMenu(hMenu, MF_STRING, ID_EDIT_COPY, ih->GetKeyTextFromCommand(kcEditCopy, _T("&Copy"))); } const UINT clipboardFlag = (IsClipboardFormatAvailable(CF_WAVE) ? 0 : MF_GRAYED); ::AppendMenu(hMenu, MF_STRING | clipboardFlag, ID_EDIT_PASTE, ih->GetKeyTextFromCommand(kcEditPaste, _T("&Paste (Replace)"))); ::AppendMenu(hMenu, MF_STRING | clipboardFlag, ID_EDIT_PUSHFORWARDPASTE, ih->GetKeyTextFromCommand(kcEditPushForwardPaste, _T("Paste (&Insert)"))); ::AppendMenu(hMenu, MF_STRING | clipboardFlag, ID_EDIT_MIXPASTE, ih->GetKeyTextFromCommand(kcEditMixPaste, _T("Mi&x Paste"))); ::AppendMenu(hMenu, MF_STRING | (pModDoc->GetSampleUndo().CanUndo(m_nSample) ? 0 : MF_GRAYED), ID_EDIT_UNDO, ih->GetKeyTextFromCommand(kcEditUndo, _T("&Undo ") + mpt::ToCString(pModDoc->GetSoundFile().GetCharsetInternal(), pModDoc->GetSampleUndo().GetUndoName(m_nSample)))); ::AppendMenu(hMenu, MF_STRING | (pModDoc->GetSampleUndo().CanRedo(m_nSample) ? 0 : MF_GRAYED), ID_EDIT_REDO, ih->GetKeyTextFromCommand(kcEditRedo, _T("&Redo ") + mpt::ToCString(pModDoc->GetSoundFile().GetCharsetInternal(), pModDoc->GetSampleUndo().GetRedoName(m_nSample)))); ClientToScreen(&pt); ::TrackPopupMenu(hMenu, TPM_LEFTALIGN|TPM_RIGHTBUTTON, pt.x, pt.y, 0, m_hWnd, NULL); ::DestroyMenu(hMenu); } } void CViewSample::OnNcMouseMove(UINT nHitTest, CPoint point) { const auto button = GetNcButtonAtPoint(point); if(button != m_nBtnMouseOver) { CMainFrame *pMainFrm = CMainFrame::GetMainFrame(); if(pMainFrm) { CString strText; if(button < SMP_LEFTBAR_BUTTONS && cLeftBarButtons[button] != ID_SEPARATOR) { strText = LoadResourceString(cLeftBarButtons[button]); } pMainFrm->SetHelpText(strText); } m_nBtnMouseOver = button; UpdateNcButtonState(); } CModScrollView::OnNcMouseMove(nHitTest, point); } void CViewSample::OnNcLButtonDown(UINT uFlags, CPoint point) { if (m_nBtnMouseOver < SMP_LEFTBAR_BUTTONS) { m_dwStatus.set(SMPSTATUS_NCLBTNDOWN); if (cLeftBarButtons[m_nBtnMouseOver] != ID_SEPARATOR) { PostMessage(WM_COMMAND, cLeftBarButtons[m_nBtnMouseOver]); UpdateNcButtonState(); } } CModScrollView::OnNcLButtonDown(uFlags, point); } void CViewSample::OnNcLButtonUp(UINT uFlags, CPoint point) { if(m_dwStatus[SMPSTATUS_NCLBTNDOWN]) { m_dwStatus.reset(SMPSTATUS_NCLBTNDOWN); UpdateNcButtonState(); } CModScrollView::OnNcLButtonUp(uFlags, point); } void CViewSample::OnNcLButtonDblClk(UINT uFlags, CPoint point) { OnNcLButtonDown(uFlags, point); } LRESULT CViewSample::OnNcHitTest(CPoint point) { CRect rect; GetWindowRect(&rect); rect.bottom = rect.top + SMP_LEFTBAR_CY; if (rect.PtInRect(point)) { return HTBORDER; } return CModScrollView::OnNcHitTest(point); } void CViewSample::OnPrevInstrument() { SendCtrlMessage(CTRLMSG_SMP_PREVINSTRUMENT); } void CViewSample::OnNextInstrument() { SendCtrlMessage(CTRLMSG_SMP_NEXTINSTRUMENT); } void CViewSample::OnSetLoop() { CModDoc *pModDoc = GetDocument(); if (pModDoc) { CSoundFile &sndFile = pModDoc->GetSoundFile(); ModSample &sample = sndFile.GetSample(m_nSample); if ((m_dwEndSel > m_dwBeginSel + 15) && (m_dwEndSel <= sample.nLength)) { if ((sample.nLoopStart != m_dwBeginSel) || (sample.nLoopEnd != m_dwEndSel)) { pModDoc->GetSampleUndo().PrepareUndo(m_nSample, sundo_none, "Set Loop"); sample.SetLoop(m_dwBeginSel, m_dwEndSel, true, sample.uFlags[CHN_PINGPONGLOOP], sndFile); SetModified(SampleHint().Info(), true, false); } } } } void CViewSample::OnSetSustainLoop() { CModDoc *pModDoc = GetDocument(); if (pModDoc) { CSoundFile &sndFile = pModDoc->GetSoundFile(); ModSample &sample = sndFile.GetSample(m_nSample); if ((m_dwEndSel > m_dwBeginSel + 15) && (m_dwEndSel <= sample.nLength)) { if ((sample.nSustainStart != m_dwBeginSel) || (sample.nSustainEnd != m_dwEndSel)) { pModDoc->GetSampleUndo().PrepareUndo(m_nSample, sundo_none, "Set Sustain Loop"); sample.SetSustainLoop(m_dwBeginSel, m_dwEndSel, true, sample.uFlags[CHN_PINGPONGSUSTAIN], sndFile); SetModified(SampleHint().Info(), true, false); } } } } void CViewSample::OnEditSelectAll() { CModDoc *pModDoc = GetDocument(); if (pModDoc) { SmpLength len = pModDoc->GetSoundFile().GetSample(m_nSample).nLength; if (len) SetCurSel(0, len); } } void CViewSample::OnEditDelete() { CModDoc *pModDoc = GetDocument(); SampleHint updateHint; updateHint.Info().Data(); if (!pModDoc) return; CSoundFile &sndFile = pModDoc->GetSoundFile(); ModSample &sample = sndFile.GetSample(m_nSample); if (!sample.HasSampleData()) return; if (m_dwEndSel > sample.nLength) m_dwEndSel = sample.nLength; if ((m_dwBeginSel >= m_dwEndSel) || (m_dwEndSel - m_dwBeginSel + 4 >= sample.nLength)) { if (Reporting::Confirm("Remove this sample?", "Remove Sample", true) != cnfYes) return; pModDoc->GetSampleUndo().PrepareUndo(m_nSample, sundo_replace, "Delete Sample"); sndFile.DestroySampleThreadsafe(m_nSample); updateHint.Names(); } else { pModDoc->GetSampleUndo().PrepareUndo(m_nSample, sundo_delete, "Delete Selection", m_dwBeginSel, m_dwEndSel); CriticalSection cs; SampleEdit::RemoveRange(sample, m_dwBeginSel, m_dwEndSel, sndFile); } SetCurSel(0, 0); SetModified(updateHint, true, true); } void CViewSample::OnEditCut() { OnEditCopy(); OnEditDelete(); } void CViewSample::OnEditCopy() { CMainFrame *pMainFrm = CMainFrame::GetMainFrame(); if(pMainFrm == nullptr || GetDocument() == nullptr) { return; } const CSoundFile &sndFile = GetDocument()->GetSoundFile(); const ModSample &sample = sndFile.GetSample(m_nSample); if(sample.uFlags[CHN_ADLIB]) { // We cannot store an OPL patch in a Wave file... Clipboard clipboard(CF_WAVE, sizeof(S3MSampleHeader)); if(clipboard.IsValid()) { S3MSampleHeader sampleHeader; MemsetZero(sampleHeader); sampleHeader.ConvertToS3M(sample); mpt::String::WriteBuf(mpt::String::nullTerminated, sampleHeader.name) = sndFile.m_szNames[m_nSample]; mpt::String::WriteBuf(mpt::String::maybeNullTerminated, sampleHeader.reserved2) = mpt::ToCharset(mpt::Charset::UTF8, Version::Current().GetOpenMPTVersionString()); clipboard = sampleHeader; } return; } bool addLoopInfo = true; size_t smpSize = sample.nLength; size_t smpOffset = 0; // First things first: Calculate sample size, taking partial selections into account. LimitMax(m_dwEndSel, sample.nLength); if(m_dwEndSel > m_dwBeginSel) { smpSize = m_dwEndSel - m_dwBeginSel; smpOffset = m_dwBeginSel; addLoopInfo = false; } smpSize *= sample.GetBytesPerSample(); smpOffset *= sample.GetBytesPerSample(); // Ok, now calculate size of the resulting WAV file. size_t memSize = sizeof(RIFFHeader) // RIFF Header + sizeof(RIFFChunk) + sizeof(WAVFormatChunk) // Sample format + sizeof(RIFFChunk) + ((smpSize + 1) & ~1) // Sample data + sizeof(RIFFChunk) + sizeof(WAVExtraChunk) // Sample metadata + MAX_SAMPLENAME + MAX_SAMPLEFILENAME; // Sample name static_assert((sizeof(WAVExtraChunk) % 2u) == 0); static_assert((MAX_SAMPLENAME % 2u) == 0); static_assert((MAX_SAMPLEFILENAME % 2u) == 0); if(addLoopInfo) { // We want to store some loop metadata as well. memSize += sizeof(RIFFChunk) + sizeof(WAVSampleInfoChunk) + 2 * sizeof(WAVSampleLoop); // ...and cue points, too. memSize += sizeof(RIFFChunk) + sizeof(uint32) + std::size(sample.cues) * sizeof(WAVCuePoint); } ASSERT((memSize % 2u) == 0); BeginWaitCursor(); Clipboard clipboard(CF_WAVE, memSize); if(auto data = clipboard.Get(); data.data()) { std::pair mf(data, 0); mpt::IO::OFile> ff(mf); WAVWriter file(ff); // Write sample format file.WriteFormat(sample.GetSampleRate(sndFile.GetType()), sample.GetElementarySampleSize() * 8, sample.GetNumChannels(), WAVFormatChunk::fmtPCM); // Write sample data file.StartChunk(RIFFChunk::iddata); uint8 *sampleData = mpt::byte_cast(data.data()) + file.GetPosition(); memcpy(sampleData, sample.sampleb() + smpOffset, smpSize); if(sample.GetElementarySampleSize() == 1) { // 8-Bit samples have to be unsigned. for(size_t i = smpSize; i != 0; i--) { *(sampleData++) ^= 0x80u; } } file.Skip(smpSize); if(addLoopInfo) { file.WriteLoopInformation(sample); file.WriteCueInformation(sample); } file.WriteExtraInformation(sample, sndFile.GetType(), sndFile.GetSampleName(m_nSample)); file.Finalize(); clipboard.Close(); } EndWaitCursor(); } void CViewSample::OnEditPaste() { DoPaste(PasteMode::Replace); } void CViewSample::OnEditMixPaste() { CMixSampleDlg::sampleOffset = m_dwMenuParam; DoPaste(PasteMode::MixPaste); } void CViewSample::OnEditInsertPaste() { if(m_dwBeginSel <= m_dwEndSel) m_dwBeginSel = m_dwEndSel = m_dwBeginDrag = m_dwEndDrag = m_dwMenuParam; DoPaste(PasteMode::Insert); } template static void MixSampleLoop(SmpLength numSamples, const Tsrc *src, uint8 srcInc, int srcFact, Tdst *dst, uint8 dstInc) { SC::Convert conv; while(numSamples--) { *dst = mpt::saturate_cast(*dst + Util::muldivr(conv(*src), srcFact, 100)); src += srcInc; dst += dstInc; } } static void MixSampleOnto(const ModSample &sample, SmpLength offset, int amplify, uint8 chn, uint8 newNumChannels, int16 *pNewSample) { uint8 numChannels = sample.GetNumChannels(); switch(sample.GetElementarySampleSize()) { case 1: MixSampleLoop(sample.nLength, sample.sample8() + (chn % numChannels), numChannels, amplify, pNewSample + offset * newNumChannels + chn, newNumChannels); break; case 2: MixSampleLoop(sample.nLength, sample.sample16() + (chn % numChannels), numChannels, amplify, pNewSample + offset * newNumChannels + chn, newNumChannels); break; default: MPT_ASSERT_NOTREACHED(); } } void CViewSample::DoPaste(PasteMode pasteMode) { CModDoc *pModDoc = GetDocument(); BeginWaitCursor(); Clipboard clipboard(CF_WAVE); if(auto data = clipboard.Get(); data.data()) { SmpLength selBegin = 0, selEnd = 0; pModDoc->GetSampleUndo().PrepareUndo(m_nSample, sundo_replace, "Paste"); CSoundFile &sndFile = pModDoc->GetSoundFile(); ModSample &sample = sndFile.GetSample(m_nSample); if(!sample.HasSampleData() || sample.uFlags[CHN_ADLIB]) pasteMode = PasteMode::Replace; // Show mix paste dialog if(pasteMode == PasteMode::MixPaste) { CMixSampleDlg dlg(this); if(dlg.DoModal() != IDOK) { EndWaitCursor(); return; } } // Save old data for mixpaste ModSample oldSample = sample; std::string oldSampleName = sndFile.m_szNames[m_nSample]; if(pasteMode != PasteMode::Replace) { sample.pData.pSample = nullptr; // prevent old sample from being deleted. } FileReader file(data); CriticalSection cs; bool ok = sndFile.ReadSampleFromFile(m_nSample, file, TrackerSettings::Instance().m_MayNormalizeSamplesOnLoad); clipboard.Close(); if(sample.uFlags[CHN_ADLIB] != oldSample.uFlags[CHN_ADLIB] && pasteMode != PasteMode::Replace) { // Cannot mix PCM with FM pasteMode = PasteMode::Replace; oldSample.FreeSample(); } if (!sndFile.m_szNames[m_nSample][0] || pasteMode != PasteMode::Replace) { sndFile.m_szNames[m_nSample] = oldSampleName; } if (!sample.filename[0]) { sample.filename = oldSample.filename; } if(pasteMode == PasteMode::MixPaste && ok) { // Mix new sample (stored in the actual sample slot) and old sample (stored in oldSample) SmpLength newLength = std::max(oldSample.nLength, CMixSampleDlg::sampleOffset + sample.nLength); const uint8 newNumChannels = std::max(oldSample.GetNumChannels(), sample.GetNumChannels()); // Result is at least 9 bits (when mixing two 8-bit samples), so always enforce 16-bit resolution int16 *pNewSample = static_cast(ModSample::AllocateSample(newLength, 2u * newNumChannels)); if(pNewSample == nullptr) { ErrorBox(IDS_ERR_OUTOFMEMORY, this); ok = false; } else { selBegin = CMixSampleDlg::sampleOffset; selEnd = selBegin + sample.nLength; for(uint8 chn = 0; chn < newNumChannels; chn++) { MixSampleOnto(oldSample, 0, CMixSampleDlg::amplifyOriginal, chn, newNumChannels, pNewSample); MixSampleOnto(sample, CMixSampleDlg::sampleOffset, CMixSampleDlg::amplifyMix, chn, newNumChannels, pNewSample); } sndFile.DestroySample(m_nSample); sample = oldSample; sample.uFlags.set(CHN_16BIT); sample.uFlags.set(CHN_STEREO, newNumChannels == 2); ctrlSmp::ReplaceSample(sample, pNewSample, newLength, sndFile); } } else if(pasteMode == PasteMode::Insert && ok) { // Insert / replace selection SmpLength oldLength = oldSample.nLength; SmpLength selLength = m_dwEndSel - m_dwBeginSel; if(m_dwEndSel > m_dwBeginSel) { // Replace selection with pasted data if(selLength >= sample.nLength) ok = true; else ok = SampleEdit::InsertSilence(oldSample, sample.nLength - selLength, m_dwBeginSel, sndFile) > oldLength; } else { m_dwBeginSel = m_dwBeginDrag; ok = SampleEdit::InsertSilence(oldSample, sample.nLength, m_dwBeginSel, sndFile) > oldLength; } if(ok && sample.GetNumChannels() > oldSample.GetNumChannels()) { // Keep channel configuration with higher channel count ok = ctrlSmp::ConvertToStereo(oldSample, sndFile); } if(ok && sample.GetElementarySampleSize() > oldSample.GetElementarySampleSize()) { // Keep higher bit depth of the two samples ok = SampleEdit::ConvertTo16Bit(oldSample, sndFile); } if(ok) { selBegin = m_dwBeginSel; selEnd = selBegin + sample.nLength; uint8 numChannels = oldSample.GetNumChannels(); SmpLength offset = m_dwBeginSel * numChannels; for(uint8 chn = 0; chn < numChannels; chn++) { uint8 newChn = chn % sample.GetNumChannels(); if(oldSample.GetElementarySampleSize() == 1 && sample.GetElementarySampleSize() == 1) { CopySample(oldSample.sample8() + offset + chn, sample.nLength, numChannels, sample.sample8() + newChn, sample.GetSampleSizeInBytes(), sample.GetNumChannels(), SC::ConversionChain, SC::DecodeIdentity >()); } else if(oldSample.GetElementarySampleSize() == 2 && sample.GetElementarySampleSize() == 1) { CopySample(oldSample.sample16() + offset + chn, sample.nLength, numChannels, sample.sample8() + newChn, sample.GetSampleSizeInBytes(), sample.GetNumChannels(), SC::ConversionChain, SC::DecodeIdentity >()); } else if(oldSample.GetElementarySampleSize() == 2 && sample.GetElementarySampleSize() == 2) { CopySample(oldSample.sample16() + offset + chn, sample.nLength, numChannels, sample.sample16() + newChn, sample.GetSampleSizeInBytes(), sample.GetNumChannels(), SC::ConversionChain, SC::DecodeIdentity >()); } else { MPT_ASSERT_NOTREACHED(); } } } else { ErrorBox(IDS_ERR_OUTOFMEMORY, this); } sndFile.DestroySample(m_nSample); sample = oldSample; } if(ok) { SetCurSel(selBegin, selEnd); sample.PrecomputeLoops(sndFile, true); SetModified(SampleHint().Info().Data().Names(), true, false); if(pasteMode == PasteMode::Replace) sndFile.ResetSamplePath(m_nSample); } else { if(pasteMode == PasteMode::MixPaste) ModSample::FreeSample(oldSample.samplev()); pModDoc->GetSampleUndo().Undo(m_nSample); sndFile.m_szNames[m_nSample] = oldSampleName; } } EndWaitCursor(); } void CViewSample::OnEditUndo() { CModDoc *pModDoc = GetDocument(); if(pModDoc == nullptr) return; if(pModDoc->GetSampleUndo().Undo(m_nSample)) { SetModified(SampleHint().Info().Data().Names(), true, false); } } void CViewSample::OnEditRedo() { CModDoc *pModDoc = GetDocument(); if(pModDoc == nullptr) return; if(pModDoc->GetSampleUndo().Redo(m_nSample)) { SetModified(SampleHint().Info().Data().Names(), true, false); } } void CViewSample::On8BitConvert() { CModDoc *pModDoc = GetDocument(); BeginWaitCursor(); if ((pModDoc) && (m_nSample <= pModDoc->GetNumSamples())) { CSoundFile &sndFile = pModDoc->GetSoundFile(); ModSample &sample = sndFile.GetSample(m_nSample); if(sample.uFlags[CHN_16BIT] && !sample.uFlags[CHN_ADLIB] && sample.HasSampleData()) { ASSERT(sample.GetElementarySampleSize() == 2); pModDoc->GetSampleUndo().PrepareUndo(m_nSample, sundo_replace, "8-Bit Conversion"); CriticalSection cs; SampleEdit::ConvertTo8Bit(sample, sndFile); cs.Leave(); SetModified(SampleHint().Info().Data(), true, true); } } EndWaitCursor(); } void CViewSample::On16BitConvert() { CModDoc *pModDoc = GetDocument(); BeginWaitCursor(); if ((pModDoc) && (m_nSample <= pModDoc->GetNumSamples())) { CSoundFile &sndFile = pModDoc->GetSoundFile(); ModSample &sample = sndFile.GetSample(m_nSample); if(!sample.uFlags[CHN_16BIT] && !sample.uFlags[CHN_ADLIB] && sample.HasSampleData()) { ASSERT(sample.GetElementarySampleSize() == 1); pModDoc->GetSampleUndo().PrepareUndo(m_nSample, sundo_replace, "16-Bit Conversion"); if(!SampleEdit::ConvertTo16Bit(sample, sndFile)) { pModDoc->GetSampleUndo().RemoveLastUndoStep(m_nSample); } else { SetModified(SampleHint().Info().Data(), true, true); } } } EndWaitCursor(); } void CViewSample::OnMonoConvert(ctrlSmp::StereoToMonoMode convert) { CModDoc *pModDoc = GetDocument(); BeginWaitCursor(); if(pModDoc != nullptr && (m_nSample <= pModDoc->GetNumSamples())) { CSoundFile &sndFile = pModDoc->GetSoundFile(); ModSample &sample = sndFile.GetSample(m_nSample); if(sample.GetNumChannels() > 1 && sample.HasSampleData() && !sample.uFlags[CHN_ADLIB]) { SAMPLEINDEX rightSmp = SAMPLEINDEX_INVALID; if(convert == ctrlSmp::splitSample) { // Split sample into two slots rightSmp = pModDoc->InsertSample(); if(rightSmp == SAMPLEINDEX_INVALID) return; } pModDoc->GetSampleUndo().PrepareUndo(m_nSample, sundo_replace, "Mono Conversion"); bool success = false; if(convert == ctrlSmp::splitSample) { ModSample &right = sndFile.GetSample(rightSmp); success = ctrlSmp::SplitStereo(sample, sample, right, sndFile); // Try to create a new instrument as well which maps to the right sample. if(success) { INSTRUMENTINDEX ins = pModDoc->FindSampleParent(m_nSample); if(ins != INSTRUMENTINDEX_INVALID) { INSTRUMENTINDEX rightIns = pModDoc->InsertInstrument(0, ins); if(rightIns != INSTRUMENTINDEX_INVALID) { for(auto &smp : sndFile.Instruments[rightIns]->Keyboard) { if(smp == m_nSample) smp = rightSmp; } } pModDoc->UpdateAllViews(this, InstrumentHint(rightIns).Info().Envelope().Names(), this); } // Finally, adjust sample panning if(sndFile.GetType() & (MOD_TYPE_IT | MOD_TYPE_MPT | MOD_TYPE_XM)) { sample.uFlags.set(CHN_PANNING); sample.nPan = 0; right.uFlags.set(CHN_PANNING); right.nPan = 256; } pModDoc->UpdateAllViews(this, SampleHint(rightSmp).Info().Data().Names(), this); } } else { success = ctrlSmp::ConvertToMono(sample, sndFile, convert); } if(success) SetModified(SampleHint().Info().Data().Names(), true, true); else pModDoc->GetSampleUndo().RemoveLastUndoStep(m_nSample); } } EndWaitCursor(); } void CViewSample::TrimSample(bool trimToLoopEnd) { CModDoc *pModDoc = GetDocument(); //nothing loaded or invalid sample slot. if(!pModDoc || m_nSample > pModDoc->GetNumSamples() || IsOPLInstrument()) return; CSoundFile &sndFile = pModDoc->GetSoundFile(); ModSample &sample = sndFile.GetSample(m_nSample); if(trimToLoopEnd) { m_dwBeginSel = 0; m_dwEndSel = sample.nLoopEnd; } else if(m_dwBeginSel == m_dwEndSel) { // Trim around loop points if there's no selection m_dwBeginSel = sample.nLoopStart; m_dwEndSel = sample.nLoopEnd; } if (m_dwBeginSel >= m_dwEndSel) return; // invalid selection BeginWaitCursor(); SmpLength nStart = m_dwBeginSel; SmpLength nEnd = m_dwEndSel - m_dwBeginSel; if(sample.HasSampleData() && (nStart + nEnd <= sample.nLength) && (nEnd >= MIN_TRIM_LENGTH)) { pModDoc->GetSampleUndo().PrepareUndo(m_nSample, sundo_replace, "Trim"); CriticalSection cs; // Note: Sample is overwritten in-place! Unused data is not deallocated! memmove(sample.sampleb(), sample.sampleb() + nStart * sample.GetBytesPerSample(), nEnd * sample.GetBytesPerSample()); for(SmpLength &point : SampleEdit::GetCuesAndLoops(sample)) { if(point >= nStart) point -= nStart; else point = sample.nLength; } sample.nLength = nEnd; sample.PrecomputeLoops(sndFile); cs.Leave(); SetCurSel(0, 0); SetModified(SampleHint().Info().Data(), true, true); } EndWaitCursor(); } void CViewSample::OnChar(UINT /*nChar*/, UINT, UINT /*nFlags*/) { } void CViewSample::PlayNote(ModCommand::NOTE note, const SmpLength nStartPos, int volume) { CMainFrame *pMainFrm = CMainFrame::GetMainFrame(); CModDoc *pModDoc = GetDocument(); if ((pModDoc) && (pMainFrm)) { if (note >= NOTE_MIN_SPECIAL) { pModDoc->NoteOff(0, (note == NOTE_NOTECUT)); } else { if(m_dwStatus[SMPSTATUS_KEYDOWN]) pModDoc->NoteOff(note, true, m_noteChannel[note - NOTE_MIN]); else pModDoc->NoteOff(0, true); const CSoundFile &sndFile = pModDoc->GetSoundFile(); const ModSample &sample = sndFile.GetSample(m_nSample); SmpLength loopstart = m_dwBeginSel, loopend = m_dwEndSel; // If selection is too small -> no loop if((m_nZoom >= 0 && loopend - loopstart < (SmpLength)(4 << m_nZoom)) || (m_nZoom < 0 && loopend - loopstart < 4) || (loopstart >= sample.nLength)) { loopend = loopstart = 0; } pModDoc->PlayNote(PlayNoteParam(note).Sample(m_nSample).Volume(volume).LoopStart(loopstart).LoopEnd(loopend).Offset(nStartPos), &m_noteChannel); m_dwStatus.set(SMPSTATUS_KEYDOWN); uint32 freq = sndFile.GetFreqFromPeriod(sndFile.GetPeriodFromNote(note + (sndFile.GetType() == MOD_TYPE_XM ? sample.RelativeTone : 0), sample.nFineTune, sample.nC5Speed), sample.nC5Speed, 0); pMainFrm->SetInfoText(MPT_CFORMAT("{} ({}.{} Hz)")( mpt::ToCString(sndFile.GetNoteName((ModCommand::NOTE)note)), freq >> FREQ_FRACBITS, mpt::cfmt::dec0<2>(Util::muldiv(freq & ((1 << FREQ_FRACBITS) - 1), 100, 1 << FREQ_FRACBITS)))); } } } void CViewSample::NoteOff(ModCommand::NOTE note) { CSoundFile &sndFile = GetDocument()->GetSoundFile(); ModChannel &chn = sndFile.m_PlayState.Chn[m_noteChannel[note - NOTE_MIN]]; sndFile.KeyOff(chn); chn.dwFlags.set(CHN_NOTEFADE); m_noteChannel[note - NOTE_MIN] = CHANNELINDEX_INVALID; } // Drop files from Windows void CViewSample::OnDropFiles(HDROP hDropInfo) { const UINT nFiles = ::DragQueryFile(hDropInfo, (UINT)-1, NULL, 0); CMainFrame::GetMainFrame()->SetForegroundWindow(); for(UINT f = 0; f < nFiles; f++) { UINT size = ::DragQueryFile(hDropInfo, f, nullptr, 0) + 1; std::vector fileName(size, _T('\0')); if(::DragQueryFile(hDropInfo, f, fileName.data(), size)) { const mpt::PathString file = mpt::PathString::FromNative(fileName.data()); if(SendCtrlMessage(CTRLMSG_SMP_OPENFILE, (LPARAM)&file) && f < nFiles - 1) { // Insert more sample slots if(!SendCtrlMessage(CTRLMSG_SMP_NEWSAMPLE)) break; } } } ::DragFinish(hDropInfo); } BOOL CViewSample::OnDragonDrop(BOOL doDrop, const DRAGONDROP *dropInfo) { CModDoc *modDoc = GetDocument(); bool canDrop = false; if ((!dropInfo) || (!modDoc)) return FALSE; CSoundFile &sndFile = modDoc->GetSoundFile(); switch(dropInfo->dropType) { case DRAGONDROP_SAMPLE: if (dropInfo->sndFile == &sndFile) { canDrop = ((dropInfo->dropItem) && (dropInfo->dropItem <= sndFile.GetNumSamples())); } else { canDrop = ((dropInfo->dropItem) && ((dropInfo->dropParam) || (dropInfo->sndFile))); } break; case DRAGONDROP_DLS: canDrop = ((dropInfo->dropItem < CTrackApp::gpDLSBanks.size()) && (CTrackApp::gpDLSBanks[dropInfo->dropItem])); break; case DRAGONDROP_SOUNDFILE: case DRAGONDROP_MIDIINSTR: canDrop = !dropInfo->GetPath().empty(); break; } const bool insertNew = CMainFrame::GetInputHandler()->ShiftPressed(); if(insertNew && !sndFile.CanAddMoreSamples()) canDrop = false; if(!canDrop || !doDrop) return canDrop; // Do the drop BeginWaitCursor(); bool modified = false; switch(dropInfo->dropType) { case DRAGONDROP_SAMPLE: if(dropInfo->sndFile == &sndFile) { SendCtrlMessage(CTRLMSG_SETCURRENTINSTRUMENT, dropInfo->dropItem); } else { if(insertNew && !SendCtrlMessage(CTRLMSG_SMP_NEWSAMPLE)) canDrop = false; else SendCtrlMessage(CTRLMSG_SMP_SONGDROP, (LPARAM)dropInfo); } break; case DRAGONDROP_MIDIINSTR: if (CDLSBank::IsDLSBank(dropInfo->GetPath())) { CDLSBank dlsbank; if (dlsbank.Open(dropInfo->GetPath())) { const DLSINSTRUMENT *pDlsIns; UINT nIns = 0, nRgn = 0xFF; int transpose = 0; // Drums if (dropInfo->dropItem & 0x80) { UINT key = dropInfo->dropItem & 0x7F; pDlsIns = dlsbank.FindInstrument(TRUE, 0xFFFF, 0xFF, key, &nIns); if(pDlsIns) { nRgn = dlsbank.GetRegionFromKey(nIns, key); const auto ®ion = pDlsIns->Regions[nRgn]; if(region.tuning != 0) transpose = (region.uKeyMin + (region.uKeyMax - region.uKeyMin) / 2) - 60; } } else // Melodic { pDlsIns = dlsbank.FindInstrument(FALSE, 0xFFFF, dropInfo->dropItem, 60, &nIns); if (pDlsIns) nRgn = dlsbank.GetRegionFromKey(nIns, 60); } canDrop = FALSE; if (pDlsIns) { if(!insertNew || SendCtrlMessage(CTRLMSG_SMP_NEWSAMPLE)) { CriticalSection cs; modDoc->GetSampleUndo().PrepareUndo(m_nSample, sundo_replace, "Replace"); canDrop = modified = dlsbank.ExtractSample(sndFile, m_nSample, nIns, nRgn, transpose); } } break; } } [[fallthrough]]; case DRAGONDROP_SOUNDFILE: if(!insertNew || SendCtrlMessage(CTRLMSG_SMP_NEWSAMPLE)) SendCtrlMessage(CTRLMSG_SMP_OPENFILE, dropInfo->dropParam); break; case DRAGONDROP_DLS: { const CDLSBank dlsBank = *CTrackApp::gpDLSBanks[dropInfo->dropItem]; UINT nIns = dropInfo->dropParam & 0xFFFF; UINT nRgn; int transpose = 0; // Drums: (0x80000000) | (Region << 16) | (Instrument) if (dropInfo->dropParam & 0x80000000) { nRgn = (dropInfo->dropParam & 0x7FFF0000) >> 16; const auto ®ion = dlsBank.GetInstrument(nIns)->Regions[nRgn]; if(region.tuning != 0) transpose = (region.uKeyMin + (region.uKeyMax - region.uKeyMin) / 2) - 60; } else // Melodic: (Instrument) { nRgn = dlsBank.GetRegionFromKey(nIns, 60); } if(!insertNew || SendCtrlMessage(CTRLMSG_SMP_NEWSAMPLE)) { CriticalSection cs; modDoc->GetSampleUndo().PrepareUndo(m_nSample, sundo_replace, "Replace"); canDrop = modified = dlsBank.ExtractSample(sndFile, m_nSample, nIns, nRgn, transpose); } } break; } if(modified) { SetModified(SampleHint().Info().Data().Names(), true, false); } CMDIChildWnd *pMDIFrame = (CMDIChildWnd *)GetParentFrame(); if (pMDIFrame) { pMDIFrame->MDIActivate(); pMDIFrame->SetActiveView(this); SetFocus(); } EndWaitCursor(); return canDrop; } void CViewSample::OnZoomOnSel() { int zoom = 0; SmpLength selLength = (m_dwEndSel - m_dwBeginSel); if (selLength > 0 && m_rcClient.right > 0) { zoom = GetZoomLevel(selLength); if(zoom < 0) { zoom++; if(zoom >= -1) zoom = 1; else if(zoom < MIN_ZOOM) zoom = MIN_ZOOM; } else if(zoom > MAX_ZOOM) { zoom = 0; } } if(zoom) { SetZoom(zoom, m_dwBeginSel + selLength / 2); } SendCtrlMessage(CTRLMSG_SMP_SETZOOM, zoom); } SmpLength CViewSample::ScrollPosToSamplePos(int nZoom) const { if(nZoom < 0) return m_nScrollPosX; else if(nZoom > 0) return m_nScrollPosX << (nZoom - 1); else return 0; } void CViewSample::OnSetLoopStart() { CModDoc *pModDoc = GetDocument(); if (pModDoc) { CSoundFile &sndFile = pModDoc->GetSoundFile(); ModSample &sample = sndFile.GetSample(m_nSample); SmpLength loopEnd = (sample.nLoopEnd > 0) ? sample.nLoopEnd : sample.nLength; if ((m_dwMenuParam + 4 <= loopEnd) && (sample.nLoopStart != m_dwMenuParam)) { pModDoc->GetSampleUndo().PrepareUndo(m_nSample, sundo_none, "Set Loop Start"); sample.SetLoop(m_dwMenuParam, loopEnd, true, sample.uFlags[CHN_PINGPONGLOOP], sndFile); SetModified(SampleHint().Info(), true, false); } } } void CViewSample::OnSetLoopEnd() { CModDoc *pModDoc = GetDocument(); if (pModDoc) { CSoundFile &sndFile = pModDoc->GetSoundFile(); ModSample &sample = sndFile.GetSample(m_nSample); if ((m_dwMenuParam >= sample.nLoopStart + 4) && (sample.nLoopEnd != m_dwMenuParam)) { pModDoc->GetSampleUndo().PrepareUndo(m_nSample, sundo_none, "Set Loop End"); sample.SetLoop(sample.nLoopStart, m_dwMenuParam, true, sample.uFlags[CHN_PINGPONGLOOP], sndFile); SetModified(SampleHint().Info(), true, false); } } } void CViewSample::OnConvertPingPongLoop() { CModDoc *pModDoc = GetDocument(); if(pModDoc) { CSoundFile &sndFile = pModDoc->GetSoundFile(); ModSample &sample = sndFile.GetSample(m_nSample); if(!sample.HasPingPongLoop()) return; pModDoc->GetSampleUndo().PrepareUndo(m_nSample, sundo_replace, "Convert Bidi Loop"); if(SampleEdit::ConvertPingPongLoop(sample, sndFile, false)) SetModified(SampleHint().Info().Data(), true, true); else pModDoc->GetSampleUndo().RemoveLastUndoStep(m_nSample); } } void CViewSample::OnSetSustainStart() { CModDoc *pModDoc = GetDocument(); if (pModDoc) { CSoundFile &sndFile = pModDoc->GetSoundFile(); ModSample &sample = sndFile.GetSample(m_nSample); SmpLength sustainEnd = (sample.nSustainEnd > 0) ? sample.nSustainEnd : sample.nLength; if ((m_dwMenuParam + 4 <= sustainEnd) && (sample.nSustainStart != m_dwMenuParam)) { pModDoc->GetSampleUndo().PrepareUndo(m_nSample, sundo_none, "Set Sustain Start"); sample.SetSustainLoop(m_dwMenuParam, sustainEnd, true, sample.uFlags[CHN_PINGPONGSUSTAIN], sndFile); SetModified(SampleHint().Info(), true, false); } } } void CViewSample::OnSetSustainEnd() { CModDoc *pModDoc = GetDocument(); if (pModDoc) { CSoundFile &sndFile = pModDoc->GetSoundFile(); ModSample &sample = sndFile.GetSample(m_nSample); if ((m_dwMenuParam >= sample.nSustainStart + 4) && (sample.nSustainEnd != m_dwMenuParam)) { pModDoc->GetSampleUndo().PrepareUndo(m_nSample, sundo_none, "Set Sustain End"); sample.SetSustainLoop(sample.nSustainStart, m_dwMenuParam, true, sample.uFlags[CHN_PINGPONGSUSTAIN], sndFile); SetModified(SampleHint().Info(), true, false); } } } void CViewSample::OnConvertPingPongSustain() { CModDoc *pModDoc = GetDocument(); if(pModDoc) { CSoundFile &sndFile = pModDoc->GetSoundFile(); ModSample &sample = sndFile.GetSample(m_nSample); if(!sample.HasPingPongSustainLoop()) return; pModDoc->GetSampleUndo().PrepareUndo(m_nSample, sundo_replace, "Convert Bidi Sustain Loop"); if(SampleEdit::ConvertPingPongLoop(sample, sndFile, true)) SetModified(SampleHint().Info().Data(), true, true); else pModDoc->GetSampleUndo().RemoveLastUndoStep(m_nSample); } } void CViewSample::OnSetCuePoint(UINT nID) { nID -= ID_SAMPLE_CUE_1; CModDoc *pModDoc = GetDocument(); if (pModDoc) { CSoundFile &sndFile = pModDoc->GetSoundFile(); ModSample &sample = sndFile.GetSample(m_nSample); pModDoc->GetSampleUndo().PrepareUndo(m_nSample, sundo_none, "Set Cue Point"); sample.cues[nID] = m_dwMenuParam; SetModified(SampleHint().Info(), true, false); } } void CViewSample::OnZoomUp() { DoZoom(1); } void CViewSample::OnZoomDown() { DoZoom(-1); } void CViewSample::OnDrawingToggle() { const CModDoc *pModDoc = GetDocument(); if(!pModDoc) return; const CSoundFile &sndFile = pModDoc->GetSoundFile(); const ModSample &sample = sndFile.GetSample(m_nSample); if(!sample.HasSampleData()) { OnAddSilence(); if(!sample.HasSampleData()) { return; } } m_dwStatus.flip(SMPSTATUS_DRAWING); UpdateNcButtonState(); } void CViewSample::OnAddSilence() { CModDoc *pModDoc = GetDocument(); if (!pModDoc) return; CSoundFile &sndFile = pModDoc->GetSoundFile(); ModSample &sample = sndFile.GetSample(m_nSample); const SmpLength oldLength = IsOPLInstrument() ? 0 : sample.nLength; AddSilenceDlg dlg(this, oldLength, sample.GetSampleRate(sndFile.GetType()), sndFile.SupportsOPL()); if (dlg.DoModal() != IDOK) return; if(dlg.m_editOption == AddSilenceDlg::kOPLInstrument) { SendCtrlMessage(CTRLMSG_SMP_INITOPL); return; } if(MAX_SAMPLE_LENGTH - oldLength < dlg.m_numSamples && dlg.m_editOption != AddSilenceDlg::kResize) { CString str; str.Format(_T("Cannot add silence because the new sample length would exceed maximum sample length %u."), MAX_SAMPLE_LENGTH); Reporting::Information(str); return; } BeginWaitCursor(); if(sample.nLength == 0 && sample.nVolume == 0) { sample.nVolume = 256; } if(dlg.m_editOption == AddSilenceDlg::kResize) { // resize - dlg.m_nSamples = new size if(dlg.m_numSamples == 0) { pModDoc->GetSampleUndo().PrepareUndo(m_nSample, sundo_replace, "Delete Sample"); sndFile.DestroySampleThreadsafe(m_nSample); } else if(dlg.m_numSamples != sample.nLength) { CriticalSection cs; if(dlg.m_numSamples < sample.nLength) // make it shorter! pModDoc->GetSampleUndo().PrepareUndo(m_nSample, sundo_delete, "Resize", dlg.m_numSamples, sample.nLength); else // make it longer! pModDoc->GetSampleUndo().PrepareUndo(m_nSample, sundo_insert, "Add Silence", sample.nLength, dlg.m_numSamples); sample.SetAdlib(false); SampleEdit::ResizeSample(sample, dlg.m_numSamples, sndFile); } } else { // add silence - dlg.m_nSamples = amount of bytes to be added if(dlg.m_numSamples > 0) { CriticalSection cs; SmpLength nStart = (dlg.m_editOption == AddSilenceDlg::kSilenceAtEnd) ? sample.nLength : 0; pModDoc->GetSampleUndo().PrepareUndo(m_nSample, sundo_insert, "Add Silence", nStart, nStart + dlg.m_numSamples); sample.SetAdlib(false); SampleEdit::InsertSilence(sample, dlg.m_numSamples, nStart, sndFile); } } EndWaitCursor(); if(oldLength != sample.nLength) { SetCurSel(0, 0); SetModified(SampleHint().Info().Data().Names(), true, true); } } LRESULT CViewSample::OnMidiMsg(WPARAM midiDataParam, LPARAM) { const uint32 midiData = static_cast(midiDataParam); static uint8 midiVolume = 127; CModDoc *pModDoc = GetDocument(); const uint8 midiByte1 = MIDIEvents::GetDataByte1FromEvent(midiData); const uint8 midiByte2 = MIDIEvents::GetDataByte2FromEvent(midiData); const uint8 channel = MIDIEvents::GetChannelFromEvent(midiData); CSoundFile *pSndFile = (pModDoc) ? &pModDoc->GetSoundFile() : nullptr; if (!pSndFile) return 0; uint8 nNote = midiByte1 + NOTE_MIN; int nVol = midiByte2; MIDIEvents::EventType event = MIDIEvents::GetTypeFromEvent(midiData); if(event == MIDIEvents::evNoteOn && !nVol) event = MIDIEvents::evNoteOff; //Convert event to note-off if req'd // Handle MIDI messages assigned to shortcuts CInputHandler *ih = CMainFrame::GetInputHandler(); if(ih->HandleMIDIMessage(kCtxViewSamples, midiData) != kcNull || ih->HandleMIDIMessage(kCtxAllContexts, midiData) != kcNull) { // Mapped to a command, no need to pass message on. return 1; } switch(event) { case MIDIEvents::evNoteOff: // Note Off if(m_midiSustainActive[channel]) { m_midiSustainBuffer[channel].push_back(midiData); return 1; } [[fallthrough]]; case MIDIEvents::evNoteOn: // Note On LimitMax(nNote, NOTE_MAX); pModDoc->NoteOff(nNote, true); if(event != MIDIEvents::evNoteOff) { nVol = CMainFrame::ApplyVolumeRelatedSettings(midiData, midiVolume); PlayNote(nNote, 0, nVol); } break; case MIDIEvents::evControllerChange: //Controller change switch(midiByte1) { case MIDIEvents::MIDICC_Volume_Coarse: //Volume midiVolume = midiByte2; break; case MIDIEvents::MIDICC_HoldPedal_OnOff: m_midiSustainActive[channel] = (midiByte2 >= 0x40); if(!m_midiSustainActive[channel]) { // Release all notes for(const auto offEvent : m_midiSustainBuffer[channel]) { OnMidiMsg(offEvent, 0); } m_midiSustainBuffer[channel].clear(); } break; } } return 1; } BOOL CViewSample::PreTranslateMessage(MSG *pMsg) { if (pMsg) { //We handle keypresses before Windows has a chance to handle them (for alt etc..) if ((pMsg->message == WM_SYSKEYUP) || (pMsg->message == WM_KEYUP) || (pMsg->message == WM_SYSKEYDOWN) || (pMsg->message == WM_KEYDOWN)) { CInputHandler* ih = CMainFrame::GetInputHandler(); //Translate message manually UINT nChar = static_cast(pMsg->wParam); UINT nRepCnt = LOWORD(pMsg->lParam); UINT nFlags = HIWORD(pMsg->lParam); KeyEventType kT = ih->GetKeyEventType(nFlags); InputTargetContext ctx = (InputTargetContext)(kCtxViewSamples); if (ih->KeyEvent(ctx, nChar, nRepCnt, nFlags, kT) != kcNull) return true; // Mapped to a command, no need to pass message on. // Handle Application (menu) key if(pMsg->message == WM_KEYDOWN && nChar == VK_APPS) { int x = Util::ScalePixels(32, m_hWnd); OnRButtonDown(0, CPoint(x, x)); } } } return CModScrollView::PreTranslateMessage(pMsg); } LRESULT CViewSample::OnCustomKeyMsg(WPARAM wParam, LPARAM lParam) { CModDoc *pModDoc = GetDocument(); if(!pModDoc) return kcNull; CSoundFile &sndFile = pModDoc->GetSoundFile(); switch(wParam) { case kcSampleTrim: TrimSample(false); return wParam; case kcSampleTrimToLoopEnd: TrimSample(true); return wParam; case kcSampleZoomUp: OnZoomUp(); return wParam; case kcSampleZoomDown: OnZoomDown(); return wParam; case kcSampleZoomSelection: OnZoomOnSel(); return wParam; case kcSampleCenterSampleStart: case kcSampleCenterSampleEnd: case kcSampleCenterLoopStart: case kcSampleCenterLoopEnd: case kcSampleCenterSustainStart: case kcSampleCenterSustainEnd: { SmpLength point = 0; ModSample &sample = sndFile.GetSample(m_nSample); switch(wParam) { case kcSampleCenterSampleStart: point = 0; break; case kcSampleCenterSampleEnd: point = sample.nLength; break; case kcSampleCenterLoopStart: point = sample.nLoopStart; break; case kcSampleCenterLoopEnd: point = sample.nLoopEnd; break; case kcSampleCenterSustainStart: point = sample.nSustainStart; break; case kcSampleCenterSustainEnd: point = sample.nSustainEnd; break; } if(!m_nZoom) SendCtrlMessage(CTRLMSG_SMP_SETZOOM, 1); ScrollToSample(point); } return wParam; case kcPrevInstrument: OnPrevInstrument(); return wParam; case kcNextInstrument: OnNextInstrument(); return wParam; case kcEditSelectAll: OnEditSelectAll(); return wParam; case kcSampleDelete: OnEditDelete(); return wParam; case kcEditCut: OnEditCut(); return wParam; case kcEditCopy: OnEditCopy(); return wParam; case kcEditPaste: OnEditPaste(); return wParam; case kcEditMixPasteITStyle: case kcEditMixPaste: DoPaste(PasteMode::MixPaste); return wParam; case kcEditPushForwardPaste: DoPaste(PasteMode::Insert); return wParam; case kcEditUndo: OnEditUndo(); return wParam; case kcEditRedo: OnEditRedo(); return wParam; case kcSampleConvertPingPongLoop: OnConvertPingPongLoop(); return wParam; case kcSampleConvertPingPongSustain: OnConvertPingPongSustain(); return wParam; case kcSample8Bit: if(sndFile.GetSample(m_nSample).uFlags[CHN_16BIT]) On8BitConvert(); else On16BitConvert(); return wParam; case kcSampleMonoMix: OnMonoConvertMix(); return wParam; case kcSampleMonoLeft: OnMonoConvertLeft(); return wParam; case kcSampleMonoRight: OnMonoConvertRight(); return wParam; case kcSampleMonoSplit: OnMonoConvertSplit(); return wParam; case kcSampleReverse: PostCtrlMessage(IDC_SAMPLE_REVERSE); return wParam; case kcSampleSilence: PostCtrlMessage(IDC_SAMPLE_SILENCE); return wParam; case kcSampleNormalize: PostCtrlMessage(IDC_SAMPLE_NORMALIZE); return wParam; case kcSampleAmplify: PostCtrlMessage(IDC_SAMPLE_AMPLIFY); return wParam; case kcSampleInvert: PostCtrlMessage(IDC_SAMPLE_INVERT); return wParam; case kcSampleSignUnsign: PostCtrlMessage(IDC_SAMPLE_SIGN_UNSIGN); return wParam; case kcSampleRemoveDCOffset: PostCtrlMessage(IDC_SAMPLE_DCOFFSET); return wParam; case kcSampleXFade: PostCtrlMessage(IDC_SAMPLE_XFADE); return wParam; case kcSampleAutotune: PostCtrlMessage(IDC_SAMPLE_AUTOTUNE); return wParam; case kcSampleQuickFade: PostCtrlMessage(IDC_SAMPLE_QUICKFADE); return wParam; case kcSampleStereoSep: PostCtrlMessage(IDC_SAMPLE_STEREOSEPARATION); return wParam; case kcSampleSlice: OnSampleSlice(); return wParam; case kcSampleToggleDrawing: OnDrawingToggle(); return wParam; case kcSampleResize: OnAddSilence(); return wParam; case kcSampleGrid: OnChangeGridSize(); return wParam; // Those don't seem to work. case kcNoteOff: PlayNote(NOTE_KEYOFF); return wParam; case kcNoteCut: PlayNote(NOTE_NOTECUT); return wParam; } if(wParam >= kcSampStartNotes && wParam <= kcSampEndNotes) { const ModCommand::NOTE note = pModDoc->GetNoteWithBaseOctave(static_cast(wParam - kcSampStartNotes), 0); if(ModCommand::IsNote(note)) { switch(TrackerSettings::Instance().sampleEditorKeyBehaviour) { case seNoteOffOnKeyRestrike: if(m_noteChannel[note - NOTE_MIN] != CHANNELINDEX_INVALID) { NoteOff(note); break; } // Fall-through default: PlayNote(note); } return wParam; } } else if(wParam >= kcSampStartNoteStops && wParam <= kcSampEndNoteStops) { const ModCommand::NOTE note = pModDoc->GetNoteWithBaseOctave(static_cast(wParam - kcSampStartNoteStops), 0); if(ModCommand::IsNote(note)) { switch(TrackerSettings::Instance().sampleEditorKeyBehaviour) { case seNoteOffOnNewKey: m_dwStatus.reset(SMPSTATUS_KEYDOWN); if(m_noteChannel[note - NOTE_MIN] != CHANNELINDEX_INVALID) { // Release sustain loop on key up sndFile.KeyOff(sndFile.m_PlayState.Chn[m_noteChannel[note - NOTE_MIN]]); } break; case seNoteOffOnKeyUp: if(m_noteChannel[note - NOTE_MIN] != CHANNELINDEX_INVALID) { NoteOff(note); } break; } return wParam; } } else if(wParam >= kcStartSampleCues && wParam <= kcEndSampleCues) { const ModSample &sample = sndFile.GetSample(m_nSample); SmpLength offset = sample.cues[wParam - kcStartSampleCues]; if(offset < sample.nLength) PlayNote(NOTE_MIDDLEC, offset); return wParam; } // Pass on to ctrl_smp return GetControlDlg()->SendMessage(WM_MOD_KEYCOMMAND, wParam, lParam); } void CViewSample::OnSampleSlice() { CModDoc *modDoc = GetDocument(); if(modDoc == nullptr || m_nSample > modDoc->GetNumSamples()) return; CSoundFile &sndFile = modDoc->GetSoundFile(); ModSample &sample = sndFile.GetSample(m_nSample); if(!sample.HasSampleData() || sample.uFlags[CHN_ADLIB]) return; // Sort cue points and add two fake cue points to make things easier below... constexpr size_t NUM_CUES = mpt::array_size::size; std::array cues; bool hasValidCues = false; // Any cues in ]0, length[ for(std::size_t i = 0; i < std::size(sample.cues); i++) { cues[i] = sample.cues[i]; if(cues[i] == 0 || cues[i] >= sample.nLength) cues[i] = sample.nLength; else hasValidCues = true; } // Nothing to slice? if(!hasValidCues) return; cues[NUM_CUES] = 0; cues[NUM_CUES + 1] = sample.nLength; std::sort(cues.begin(), cues.end()); // Now slice the sample at each cue point for(std::size_t i = 1; i < std::size(cues) - 1; i++) { const SmpLength cue = cues[i]; if(cue > cues[i - 1] && cue < cues[i + 1]) { SAMPLEINDEX nextSmp = modDoc->InsertSample(); if(nextSmp == SAMPLEINDEX_INVALID) break; ModSample &newSample = sndFile.GetSample(nextSmp); newSample = sample; newSample.nLength = cues[i + 1] - cues[i]; newSample.pData.pSample = nullptr; sndFile.m_szNames[nextSmp] = sndFile.m_szNames[m_nSample]; if(newSample.AllocateSample() > 0) { Util::DeleteRange(SmpLength(0), cues[i] - SmpLength(1), newSample.nLoopStart, newSample.nLoopEnd); memcpy(newSample.sampleb(), sample.sampleb() + cues[i] * sample.GetBytesPerSample(), newSample.nLength * sample.GetBytesPerSample()); newSample.PrecomputeLoops(sndFile, false); if(sndFile.GetNumInstruments() > 0) { if(auto instr = modDoc->InsertInstrument(nextSmp); instr != INSTRUMENTINDEX_INVALID) sndFile.Instruments[instr]->name = sndFile.m_szNames[nextSmp]; } } } } modDoc->GetSampleUndo().PrepareUndo(m_nSample, sundo_delete, "Slice Sample", cues[1], sample.nLength); SampleEdit::ResizeSample(sample, cues[1], sndFile); sample.PrecomputeLoops(sndFile, true); SetModified(SampleHint().Info().Data().Names(), true, true); modDoc->UpdateAllViews(this, SampleHint().Info().Data().Names(), this); modDoc->UpdateAllViews(this, InstrumentHint().Info().Envelope().Names(), this); } void CViewSample::OnSampleInsertCuePoint() { CModDoc *modDoc = GetDocument(); if(modDoc == nullptr || m_nSample > modDoc->GetNumSamples()) return; CSoundFile &sndFile = modDoc->GetSoundFile(); ModSample &sample = sndFile.GetSample(m_nSample); if(!sample.HasSampleData() || sample.uFlags[CHN_ADLIB] || m_dwMenuParam >= sample.nLength) return; for(auto &pt : sample.cues) { if(pt >= sample.nLength) { modDoc->GetSampleUndo().PrepareUndo(m_nSample, sundo_none, "Insert Cue Point"); pt = m_dwMenuParam; SetModified(SampleHint().Info().Data(), true, true); break; } } } void CViewSample::OnSampleDeleteCuePoint() { CModDoc *modDoc = GetDocument(); if(modDoc == nullptr || m_nSample > modDoc->GetNumSamples()) return; CSoundFile &sndFile = modDoc->GetSoundFile(); ModSample &sample = sndFile.GetSample(m_nSample); if(!sample.HasSampleData() || sample.uFlags[CHN_ADLIB] || m_dwMenuParam >= std::size(sample.cues)) return; modDoc->GetSampleUndo().PrepareUndo(m_nSample, sundo_none, "Delete Cue Point"); sample.cues[m_dwMenuParam] = MAX_SAMPLE_LENGTH; SetModified(SampleHint().Info().Data(), true, true); } bool CViewSample::CanZoomSelection() const { return GetZoomLevel(m_dwEndSel - m_dwBeginSel) <= MAX_ZOOM; } // Returns auto-zoom level compared to other zoom levels. // Result is not limited to MIN_ZOOM...MAX_ZOOM range. int CViewSample::GetZoomLevel(SmpLength length) const { if(m_rcClient.Width() == 0 || length == 0) return MAX_ZOOM + 1; // When m_nZoom > 0, 2^(m_nZoom - 1) = samplesPerPixel [1] // With auto-zoom setting the whole sample is fitted to screen: // ViewScreenWidthInPixels * samplesPerPixel = sampleLength (approximately) [2]. // Solve samplesPerPixel from [2], then "m_nZoom" from [1]. double zoom = static_cast(length) / m_rcClient.Width(); zoom = 1 + (std::log10(zoom) / std::log10(2.0)); if(zoom <= 0) zoom -= 2; return static_cast(zoom + mpt::signum(zoom)); } void CViewSample::DoZoom(int direction, const CPoint &zoomPoint) { const CSoundFile &sndFile = GetDocument()->GetSoundFile(); // zoomOrder: Biggest to smallest zoom order. std::array zoomOrder; for(int i = 2; i < -MIN_ZOOM + 1; ++i) zoomOrder[i - 2] = MIN_ZOOM + i - 2; // -6, -5, -4, -3... for(int i = 1; i <= MAX_ZOOM; ++i) zoomOrder[i - 1 + (-MIN_ZOOM - 1)] = i; // 1, 2, 3... zoomOrder.back() = 0; const int autoZoomLevel = std::max(MIN_ZOOM, GetZoomLevel(sndFile.GetSample(m_nSample).nLength)); // Move auto-zoom index (=zero) to the right position in the zoom order according to its real zoom strength. if (autoZoomLevel < MAX_ZOOM + 1) { auto p = std::find(zoomOrder.begin(), zoomOrder.end(), autoZoomLevel); if(p != zoomOrder.end()) { std::move(p, zoomOrder.end() - 1, p + 1); *p = 0; } else MPT_ASSERT_NOTREACHED(); } const std::ptrdiff_t nPos = std::distance(zoomOrder.begin(), std::find(zoomOrder.begin(), zoomOrder.end(), m_nZoom)); int newZoom; if(direction > 0 && nPos > 0) // Zoom in newZoom = zoomOrder[nPos - 1]; else if(direction < 0 && nPos + 1 < static_cast(zoomOrder.size())) newZoom = zoomOrder[nPos + 1]; else return; if(m_rcClient.PtInRect(zoomPoint)) { SetZoom(newZoom, ScreenToSample(zoomPoint.x)); } else { SetZoom(newZoom); } SendCtrlMessage(CTRLMSG_SMP_SETZOOM, newZoom); } BOOL CViewSample::OnMouseWheel(UINT nFlags, short zDelta, CPoint pt) { // Ctrl + mouse wheel: zoom control. // One scroll direction zooms in and the other zooms out. // This behaviour is different from what would happen if simply scrolling // the zoom levels in the zoom combobox. if (nFlags == MK_CONTROL && GetDocument()) { ScreenToClient(&pt); DoZoom(zDelta, pt); } return CModScrollView::OnMouseWheel(nFlags, zDelta, pt); } void CViewSample::OnXButtonUp(UINT nFlags, UINT nButton, CPoint point) { if(nButton == XBUTTON1) OnPrevInstrument(); else if(nButton == XBUTTON2) OnNextInstrument(); CModScrollView::OnXButtonUp(nFlags, nButton, point); } void CViewSample::OnChangeGridSize() { CSampleGridDlg dlg(this, m_nGridSegments, GetDocument()->GetSoundFile().GetSample(m_nSample).nLength); if(dlg.DoModal() == IDOK) { m_nGridSegments = dlg.m_nSegments; InvalidateSample(false); } } void CViewSample::OnQuickFade() { PostCtrlMessage(IDC_SAMPLE_QUICKFADE); } void CViewSample::SetTimelineFormat(TimelineFormat fmt) { TrackerSettings::Instance().sampleEditorTimelineFormat = fmt; UpdateScrollSize(); InvalidateTimeline(); } void CViewSample::OnUpdateUndo(CCmdUI *pCmdUI) { CModDoc *pModDoc = GetDocument(); if ((pCmdUI) && (pModDoc)) { pCmdUI->Enable(pModDoc->GetSampleUndo().CanUndo(m_nSample)); pCmdUI->SetText(CMainFrame::GetInputHandler()->GetKeyTextFromCommand(kcEditUndo, _T("Undo ") + mpt::ToCString(pModDoc->GetSoundFile().GetCharsetInternal(), pModDoc->GetSampleUndo().GetUndoName(m_nSample)))); } } void CViewSample::OnUpdateRedo(CCmdUI *pCmdUI) { CModDoc *pModDoc = GetDocument(); if ((pCmdUI) && (pModDoc)) { pCmdUI->Enable(pModDoc->GetSampleUndo().CanRedo(m_nSample)); pCmdUI->SetText(CMainFrame::GetInputHandler()->GetKeyTextFromCommand(kcEditRedo, _T("Redo ") + mpt::ToCString(pModDoc->GetSoundFile().GetCharsetInternal(), pModDoc->GetSampleUndo().GetRedoName(m_nSample)))); } } INT_PTR CViewSample::OnToolHitTest(CPoint point, TOOLINFO *pTI) const { CString text; CRect ncRect; CPoint screenPoint = point; ClientToScreen(&screenPoint); const auto ncButton = GetNcButtonAtPoint(screenPoint, &ncRect); int buttonID = 0; if(ncButton != uint32_max) { buttonID = cLeftBarButtons[ncButton]; ScreenToClient(&ncRect); text = LoadResourceString(buttonID); CommandID cmd = kcNull; switch(buttonID) { case ID_SAMPLE_ZOOMUP: cmd = kcSampleZoomUp; break; case ID_SAMPLE_ZOOMDOWN: cmd = kcSampleZoomDown; break; case ID_SAMPLE_DRAW: cmd = kcSampleToggleDrawing; break; case ID_SAMPLE_ADDSILENCE: cmd = kcSampleResize; break; case ID_SAMPLE_GRID: cmd = kcSampleGrid; break; } if(cmd != kcNull) { auto keyText = CMainFrame::GetInputHandler()->m_activeCommandSet->GetKeyTextFromCommand(cmd, 0); if(!keyText.IsEmpty()) text += MPT_CFORMAT(" ({})")(keyText); } } else { const ModSample &sample = GetDocument()->GetSoundFile().GetSample(m_nSample <= GetDocument()->GetNumSamples() ? m_nSample : 0); auto [item, pos] = PointToItem(point, &ncRect); switch(item) { case HitTestItem::LoopStart: text = _T("Loop Start"); break; case HitTestItem::LoopEnd: text = _T("Loop End"); break; case HitTestItem::SustainStart: text = _T("Sustain Start"); break; case HitTestItem::SustainEnd: text = _T("Sustain End"); break; default: if(!IsCuePoint(item)) return CModScrollView::OnToolHitTest(point, pTI); auto cue = CuePointFromItem(item); text = MPT_CFORMAT("Cue Point {}")(cue + 1); } if(pos <= sample.nLength) text += _T(": ") + mpt::cfmt::dec(3, ',', pos); buttonID = static_cast(item) + 1; } pTI->hwnd = m_hWnd; pTI->uId = buttonID; pTI->rect = ncRect; // MFC will free() the text auto size = text.GetLength() + 1; TCHAR *textP = static_cast(calloc(size, sizeof(TCHAR))); std::copy(text.GetString(), text.GetString() + size, textP); pTI->lpszText = textP; return buttonID; } OPENMPT_NAMESPACE_END