winamp/Src/external_dependencies/openmpt-trunk/mptrack/Ctrl_smp.cpp

3793 lines
115 KiB
C++
Raw Normal View History

2024-09-24 12:54:57 +00:00
/*
* Ctrl_smp.cpp
* ------------
* Purpose: Sample tab, upper 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 "Childfrm.h"
#include "ImageLists.h"
#include "Moddoc.h"
#include "../soundlib/mod_specifications.h"
#include "Globals.h"
#include "Ctrl_smp.h"
#include "View_smp.h"
#include "SampleEditorDialogs.h"
#include "dlg_misc.h"
#include "PSRatioCalc.h"
#include <soundtouch/include/SoundTouch.h>
#include <soundtouch/source/SoundTouchDLL/SoundTouchDLL.h>
#include <smbPitchShift/smbPitchShift.h>
#include "../tracklib/SampleEdit.h"
#include "Autotune.h"
#include "../common/mptStringBuffer.h"
#include "../common/mptFileIO.h"
#include "../common/FileReader.h"
#include "openmpt/soundbase/Copy.hpp"
#include "openmpt/soundbase/SampleConvert.hpp"
#include "openmpt/soundbase/SampleDecode.hpp"
#include "../soundlib/SampleCopy.h"
#include "FileDialog.h"
#include "ProgressDialog.h"
#include "../include/r8brain/CDSPResampler.h"
#include "../soundlib/MixFuncTable.h"
#include "mpt/audio/span.hpp"
OPENMPT_NAMESPACE_BEGIN
#define BASENOTE_MIN (1*12) // C-1
#define BASENOTE_MAX (10*12+11) // B-10
BEGIN_MESSAGE_MAP(CCtrlSamples, CModControlDlg)
//{{AFX_MSG_MAP(CCtrlSamples)
ON_WM_VSCROLL()
ON_WM_XBUTTONUP()
ON_NOTIFY(TBN_DROPDOWN, IDC_TOOLBAR1, &CCtrlSamples::OnTbnDropDownToolBar)
ON_COMMAND(IDC_SAMPLE_NEW, &CCtrlSamples::OnSampleNew)
ON_COMMAND(IDC_SAMPLE_DUPLICATE, &CCtrlSamples::OnSampleDuplicate)
ON_COMMAND(IDC_SAMPLE_OPEN, &CCtrlSamples::OnSampleOpen)
ON_COMMAND(IDC_SAMPLE_OPENKNOWN, &CCtrlSamples::OnSampleOpenKnown)
ON_COMMAND(IDC_SAMPLE_OPENRAW, &CCtrlSamples::OnSampleOpenRaw)
ON_COMMAND(IDC_SAMPLE_SAVEAS, &CCtrlSamples::OnSampleSave)
ON_COMMAND(IDC_SAVE_ONE, &CCtrlSamples::OnSampleSaveOne)
ON_COMMAND(IDC_SAVE_ALL, &CCtrlSamples::OnSampleSaveAll)
ON_COMMAND(IDC_SAMPLE_PLAY, &CCtrlSamples::OnSamplePlay)
ON_COMMAND(IDC_SAMPLE_NORMALIZE, &CCtrlSamples::OnNormalize)
ON_COMMAND(IDC_SAMPLE_AMPLIFY, &CCtrlSamples::OnAmplify)
ON_COMMAND(IDC_SAMPLE_RESAMPLE, &CCtrlSamples::OnResample)
ON_COMMAND(IDC_SAMPLE_REVERSE, &CCtrlSamples::OnReverse)
ON_COMMAND(IDC_SAMPLE_SILENCE, &CCtrlSamples::OnSilence)
ON_COMMAND(IDC_SAMPLE_INVERT, &CCtrlSamples::OnInvert)
ON_COMMAND(IDC_SAMPLE_SIGN_UNSIGN, &CCtrlSamples::OnSignUnSign)
ON_COMMAND(IDC_SAMPLE_DCOFFSET, &CCtrlSamples::OnRemoveDCOffset)
ON_COMMAND(IDC_SAMPLE_XFADE, &CCtrlSamples::OnXFade)
ON_COMMAND(IDC_SAMPLE_STEREOSEPARATION, &CCtrlSamples::OnStereoSeparation)
ON_COMMAND(IDC_SAMPLE_AUTOTUNE, &CCtrlSamples::OnAutotune)
ON_COMMAND(IDC_CHECK1, &CCtrlSamples::OnSetPanningChanged)
ON_COMMAND(IDC_CHECK2, &CCtrlSamples::OnKeepSampleOnDisk)
ON_COMMAND(ID_PREVINSTRUMENT, &CCtrlSamples::OnPrevInstrument)
ON_COMMAND(ID_NEXTINSTRUMENT, &CCtrlSamples::OnNextInstrument)
ON_COMMAND(IDC_BUTTON1, &CCtrlSamples::OnPitchShiftTimeStretch)
ON_COMMAND(IDC_BUTTON2, &CCtrlSamples::OnEstimateSampleSize)
ON_COMMAND(IDC_CHECK3, &CCtrlSamples::OnEnableStretchToSize)
ON_COMMAND(IDC_SAMPLE_INITOPL, &CCtrlSamples::OnInitOPLInstrument)
ON_EN_CHANGE(IDC_SAMPLE_NAME, &CCtrlSamples::OnNameChanged)
ON_EN_CHANGE(IDC_SAMPLE_FILENAME, &CCtrlSamples::OnFileNameChanged)
ON_EN_CHANGE(IDC_EDIT_SAMPLE, &CCtrlSamples::OnSampleChanged)
ON_EN_CHANGE(IDC_EDIT1, &CCtrlSamples::OnLoopPointsChanged)
ON_EN_CHANGE(IDC_EDIT2, &CCtrlSamples::OnLoopPointsChanged)
ON_EN_CHANGE(IDC_EDIT3, &CCtrlSamples::OnSustainPointsChanged)
ON_EN_CHANGE(IDC_EDIT4, &CCtrlSamples::OnSustainPointsChanged)
ON_EN_CHANGE(IDC_EDIT5, &CCtrlSamples::OnFineTuneChanged)
ON_EN_CHANGE(IDC_EDIT7, &CCtrlSamples::OnVolumeChanged)
ON_EN_CHANGE(IDC_EDIT8, &CCtrlSamples::OnGlobalVolChanged)
ON_EN_CHANGE(IDC_EDIT9, &CCtrlSamples::OnPanningChanged)
ON_EN_CHANGE(IDC_EDIT14, &CCtrlSamples::OnVibSweepChanged)
ON_EN_CHANGE(IDC_EDIT15, &CCtrlSamples::OnVibDepthChanged)
ON_EN_CHANGE(IDC_EDIT16, &CCtrlSamples::OnVibRateChanged)
ON_EN_SETFOCUS(IDC_SAMPLE_NAME, &CCtrlSamples::OnEditFocus)
ON_EN_SETFOCUS(IDC_SAMPLE_FILENAME, &CCtrlSamples::OnEditFocus)
ON_EN_SETFOCUS(IDC_EDIT1, &CCtrlSamples::OnEditFocus)
ON_EN_SETFOCUS(IDC_EDIT2, &CCtrlSamples::OnEditFocus)
ON_EN_SETFOCUS(IDC_EDIT3, &CCtrlSamples::OnEditFocus)
ON_EN_SETFOCUS(IDC_EDIT4, &CCtrlSamples::OnEditFocus)
ON_EN_SETFOCUS(IDC_EDIT5, &CCtrlSamples::OnEditFocus)
ON_EN_SETFOCUS(IDC_EDIT7, &CCtrlSamples::OnEditFocus)
ON_EN_SETFOCUS(IDC_EDIT8, &CCtrlSamples::OnEditFocus)
ON_EN_SETFOCUS(IDC_EDIT9, &CCtrlSamples::OnEditFocus)
ON_EN_SETFOCUS(IDC_EDIT14, &CCtrlSamples::OnEditFocus)
ON_EN_SETFOCUS(IDC_EDIT15, &CCtrlSamples::OnEditFocus)
ON_EN_SETFOCUS(IDC_EDIT16, &CCtrlSamples::OnEditFocus)
ON_EN_KILLFOCUS(IDC_EDIT5, &CCtrlSamples::OnFineTuneChangedDone)
ON_CBN_SELCHANGE(IDC_COMBO_BASENOTE,&CCtrlSamples::OnBaseNoteChanged)
ON_CBN_SELCHANGE(IDC_COMBO_ZOOM, &CCtrlSamples::OnZoomChanged)
ON_CBN_SELCHANGE(IDC_COMBO1, &CCtrlSamples::OnLoopTypeChanged)
ON_CBN_SELCHANGE(IDC_COMBO2, &CCtrlSamples::OnSustainTypeChanged)
ON_CBN_SELCHANGE(IDC_COMBO3, &CCtrlSamples::OnVibTypeChanged)
ON_MESSAGE(WM_MOD_KEYCOMMAND, &CCtrlSamples::OnCustomKeyMsg)
//}}AFX_MSG_MAP
END_MESSAGE_MAP()
void CCtrlSamples::DoDataExchange(CDataExchange* pDX)
{
CModControlDlg::DoDataExchange(pDX);
//{{AFX_DATA_MAP(CCtrlSamples)
DDX_Control(pDX, IDC_TOOLBAR1, m_ToolBar1);
DDX_Control(pDX, IDC_TOOLBAR2, m_ToolBar2);
DDX_Control(pDX, IDC_SAMPLE_NAME, m_EditName);
DDX_Control(pDX, IDC_SAMPLE_FILENAME, m_EditFileName);
DDX_Control(pDX, IDC_SAMPLE_NAME, m_EditName);
DDX_Control(pDX, IDC_SAMPLE_FILENAME, m_EditFileName);
DDX_Control(pDX, IDC_COMBO_ZOOM, m_ComboZoom);
DDX_Control(pDX, IDC_COMBO_BASENOTE, m_CbnBaseNote);
DDX_Control(pDX, IDC_SPIN_SAMPLE, m_SpinSample);
DDX_Control(pDX, IDC_EDIT_SAMPLE, m_EditSample);
DDX_Control(pDX, IDC_CHECK1, m_CheckPanning);
DDX_Control(pDX, IDC_SPIN1, m_SpinLoopStart);
DDX_Control(pDX, IDC_SPIN2, m_SpinLoopEnd);
DDX_Control(pDX, IDC_SPIN3, m_SpinSustainStart);
DDX_Control(pDX, IDC_SPIN4, m_SpinSustainEnd);
DDX_Control(pDX, IDC_SPIN5, m_SpinFineTune);
DDX_Control(pDX, IDC_SPIN7, m_SpinVolume);
DDX_Control(pDX, IDC_SPIN8, m_SpinGlobalVol);
DDX_Control(pDX, IDC_SPIN9, m_SpinPanning);
DDX_Control(pDX, IDC_SPIN11, m_SpinVibSweep);
DDX_Control(pDX, IDC_SPIN12, m_SpinVibDepth);
DDX_Control(pDX, IDC_SPIN13, m_SpinVibRate);
DDX_Control(pDX, IDC_COMBO1, m_ComboLoopType);
DDX_Control(pDX, IDC_COMBO2, m_ComboSustainType);
DDX_Control(pDX, IDC_COMBO3, m_ComboAutoVib);
DDX_Control(pDX, IDC_EDIT1, m_EditLoopStart);
DDX_Control(pDX, IDC_EDIT2, m_EditLoopEnd);
DDX_Control(pDX, IDC_EDIT3, m_EditSustainStart);
DDX_Control(pDX, IDC_EDIT4, m_EditSustainEnd);
DDX_Control(pDX, IDC_EDIT5, m_EditFineTune);
DDX_Control(pDX, IDC_EDIT7, m_EditVolume);
DDX_Control(pDX, IDC_EDIT8, m_EditGlobalVol);
DDX_Control(pDX, IDC_EDIT9, m_EditPanning);
DDX_Control(pDX, IDC_EDIT14, m_EditVibSweep);
DDX_Control(pDX, IDC_EDIT15, m_EditVibDepth);
DDX_Control(pDX, IDC_EDIT16, m_EditVibRate);
DDX_Control(pDX, IDC_COMBO4, m_ComboPitch);
DDX_Control(pDX, IDC_COMBO5, m_ComboQuality);
DDX_Control(pDX, IDC_COMBO6, m_ComboFFT);
DDX_Control(pDX, IDC_SPIN10, m_SpinSequenceMs);
DDX_Control(pDX, IDC_SPIN14, m_SpinSeekWindowMs);
DDX_Control(pDX, IDC_SPIN15, m_SpinOverlap);
DDX_Control(pDX, IDC_SPIN16, m_SpinStretchAmount);
DDX_Text(pDX, IDC_EDIT6, m_dTimeStretchRatio);
//}}AFX_DATA_MAP
}
CCtrlSamples::CCtrlSamples(CModControlView &parent, CModDoc &document)
: CModControlDlg(parent, document)
{
m_nLockCount = 1;
}
CCtrlSamples::~CCtrlSamples()
{
}
CRuntimeClass *CCtrlSamples::GetAssociatedViewClass()
{
return RUNTIME_CLASS(CViewSample);
}
void CCtrlSamples::OnEditFocus()
{
m_startedEdit = false;
}
BOOL CCtrlSamples::OnInitDialog()
{
CModControlDlg::OnInitDialog();
m_bInitialized = FALSE;
SetRedraw(FALSE);
// Zoom Selection
static constexpr std::pair<const TCHAR *, int> ZoomLevels[] =
{
{_T("Auto"), 0},
{_T("1:1"), 1},
{_T("2:1"), -2},
{_T("4:1"), -3},
{_T("8:1"), -4},
{_T("16:1"), -5},
{_T("32:1"), -6},
{_T("1:2"), 2},
{_T("1:4"), 3},
{_T("1:8"), 4},
{_T("1:16"), 5},
{_T("1:32"), 6},
{_T("1:64"), 7},
{_T("1:128"), 8},
{_T("1:256"), 9},
{_T("1:512"), 10},
};
m_ComboZoom.SetRedraw(FALSE);
m_ComboZoom.InitStorage(static_cast<int>(std::size(ZoomLevels)), 4);
for(const auto &[str, data] : ZoomLevels)
{
m_ComboZoom.SetItemData(m_ComboZoom.AddString(str), static_cast<DWORD_PTR>(data));
}
m_ComboZoom.SetRedraw(TRUE);
m_ComboZoom.SetCurSel(0);
// File ToolBar
m_ToolBar1.SetExtendedStyle(m_ToolBar1.GetExtendedStyle() | TBSTYLE_EX_DRAWDDARROWS);
m_ToolBar1.Init(CMainFrame::GetMainFrame()->m_PatternIcons,CMainFrame::GetMainFrame()->m_PatternIconsDisabled);
m_ToolBar1.AddButton(IDC_SAMPLE_NEW, TIMAGE_SAMPLE_NEW, TBSTYLE_BUTTON | TBSTYLE_DROPDOWN);
m_ToolBar1.AddButton(IDC_SAMPLE_OPEN, TIMAGE_OPEN, TBSTYLE_BUTTON | TBSTYLE_DROPDOWN);
m_ToolBar1.AddButton(IDC_SAMPLE_SAVEAS, TIMAGE_SAVE, TBSTYLE_BUTTON | TBSTYLE_DROPDOWN);
// Edit ToolBar
m_ToolBar2.Init(CMainFrame::GetMainFrame()->m_PatternIcons,CMainFrame::GetMainFrame()->m_PatternIconsDisabled);
m_ToolBar2.AddButton(IDC_SAMPLE_PLAY, TIMAGE_PREVIEW);
m_ToolBar2.AddButton(IDC_SAMPLE_NORMALIZE, TIMAGE_SAMPLE_NORMALIZE);
m_ToolBar2.AddButton(IDC_SAMPLE_AMPLIFY, TIMAGE_SAMPLE_AMPLIFY);
m_ToolBar2.AddButton(IDC_SAMPLE_DCOFFSET, TIMAGE_SAMPLE_DCOFFSET);
m_ToolBar2.AddButton(IDC_SAMPLE_STEREOSEPARATION, TIMAGE_SAMPLE_STEREOSEP);
m_ToolBar2.AddButton(IDC_SAMPLE_RESAMPLE, TIMAGE_SAMPLE_RESAMPLE);
m_ToolBar2.AddButton(IDC_SAMPLE_REVERSE, TIMAGE_SAMPLE_REVERSE);
m_ToolBar2.AddButton(IDC_SAMPLE_SILENCE, TIMAGE_SAMPLE_SILENCE);
m_ToolBar2.AddButton(IDC_SAMPLE_INVERT, TIMAGE_SAMPLE_INVERT);
m_ToolBar2.AddButton(IDC_SAMPLE_SIGN_UNSIGN, TIMAGE_SAMPLE_UNSIGN);
m_ToolBar2.AddButton(IDC_SAMPLE_XFADE, TIMAGE_SAMPLE_FIXLOOP);
m_ToolBar2.AddButton(IDC_SAMPLE_AUTOTUNE, TIMAGE_SAMPLE_AUTOTUNE);
// Setup Controls
m_SpinVolume.SetRange(0, 64);
m_SpinGlobalVol.SetRange(0, 64);
m_CbnBaseNote.InitStorage(BASENOTE_MAX - BASENOTE_MIN, 4);
m_CbnBaseNote.SetRedraw(FALSE);
for(ModCommand::NOTE i = BASENOTE_MIN; i <= BASENOTE_MAX; i++)
{
CString noteName = mpt::ToCString(CSoundFile::GetDefaultNoteName(i % 12)) + mpt::cfmt::val(i / 12);
m_CbnBaseNote.SetItemData(m_CbnBaseNote.AddString(noteName), i - (NOTE_MIDDLEC - NOTE_MIN));
}
m_CbnBaseNote.SetRedraw(TRUE);
m_ComboFFT.ShowWindow(SW_SHOW);
m_ComboPitch.ShowWindow(SW_SHOW);
m_ComboQuality.ShowWindow(SW_SHOW);
m_ComboFFT.ShowWindow(SW_SHOW);
// Pitch selection
// Allow pitch from -12 (1 octave down) to +12 (1 octave up)
m_ComboPitch.InitStorage(25, 4);
m_ComboPitch.SetRedraw(FALSE);
for(int i = -12 ; i <= 12 ; i++)
{
mpt::tstring str;
if(i == 0)
str = _T("none");
else if(i < 0)
str = mpt::tfmt::dec(i);
else
str = _T("+") + mpt::tfmt::dec(i);
m_ComboPitch.SetItemData(m_ComboPitch.AddString(str.c_str()), i + 12);
}
m_ComboPitch.SetRedraw(TRUE);
// Set "none" as default pitch
m_ComboPitch.SetCurSel(12);
// Quality selection
// Allow quality from 4 to 128
m_ComboQuality.InitStorage(128 - 4, 4);
m_ComboQuality.SetRedraw(FALSE);
for(int i = 4; i <= 128; i++)
{
m_ComboQuality.SetItemData(m_ComboQuality.AddString(mpt::tfmt::dec(i).c_str()), i - 4);
}
m_ComboQuality.SetRedraw(TRUE);
// Set 32 as default quality
m_ComboQuality.SetCurSel(32 - 4);
// FFT size selection
// Deduce exponent from equation : MAX_FRAME_LENGTH = 2^exponent
constexpr int exponent = mpt::bit_width(uint32(MAX_FRAME_LENGTH)) - 1;
// Allow FFT size from 2^8 (256) to 2^exponent (MAX_FRAME_LENGTH)
m_ComboFFT.InitStorage(exponent - 8, 4);
m_ComboFFT.SetRedraw(FALSE);
for(int i = 8 ; i <= exponent ; i++)
{
m_ComboFFT.SetItemData(m_ComboFFT.AddString(mpt::tfmt::dec(1 << i).c_str()), i - 8);
}
m_ComboFFT.SetRedraw(TRUE);
// Set 4096 as default FFT size
m_ComboFFT.SetCurSel(4);
// Stretch to size check box
OnEnableStretchToSize();
m_SpinSequenceMs.SetRange32(0, 9999);
m_SpinSeekWindowMs.SetRange32(0, 9999);
m_SpinOverlap.SetRange32(0, 9999);
m_SpinStretchAmount.SetRange32(50, 200);
SetRedraw(TRUE);
return TRUE;
}
void CCtrlSamples::RecalcLayout()
{
}
bool CCtrlSamples::SetCurrentSample(SAMPLEINDEX nSmp, LONG lZoom, bool bUpdNum)
{
if(m_sndFile.GetNumSamples() < 1)
m_sndFile.m_nSamples = 1;
if((nSmp < 1) || (nSmp > m_sndFile.GetNumSamples()))
return FALSE;
LockControls();
if(m_nSample != nSmp)
{
m_nSample = nSmp;
UpdateView(SampleHint(m_nSample).Info());
m_parent.SampleChanged(m_nSample);
}
if(bUpdNum)
{
SetDlgItemInt(IDC_EDIT_SAMPLE, m_nSample);
m_SpinSample.SetRange(1, m_sndFile.GetNumSamples());
}
if(lZoom == -1)
{
lZoom = static_cast<int>(m_ComboZoom.GetItemData(m_ComboZoom.GetCurSel()));
} else
{
for(int i = 0; i< m_ComboZoom.GetCount(); i++)
{
if(static_cast<int>(m_ComboZoom.GetItemData(i)) == lZoom)
{
m_ComboZoom.SetCurSel(i);
break;
}
}
}
static_assert(MAX_SAMPLES < uint16_max);
SendViewMessage(VIEWMSG_SETCURRENTSAMPLE, (lZoom << 16) | m_nSample);
UnlockControls();
return true;
}
void CCtrlSamples::OnActivatePage(LPARAM lParam)
{
if (lParam < 0)
{
int nIns = m_parent.GetInstrumentChange();
if (m_sndFile.GetNumInstruments())
{
if ((nIns > 0) && (!m_modDoc.IsChildSample((INSTRUMENTINDEX)nIns, m_nSample)))
{
SAMPLEINDEX k = m_modDoc.FindInstrumentChild((INSTRUMENTINDEX)nIns);
if (k > 0) lParam = k;
}
} else
{
if (nIns > 0) lParam = nIns;
}
} else if (lParam > 0)
{
if (m_sndFile.GetNumInstruments())
{
INSTRUMENTINDEX k = (INSTRUMENTINDEX)m_parent.GetInstrumentChange();
if (!m_modDoc.IsChildSample(k, (SAMPLEINDEX)lParam))
{
INSTRUMENTINDEX nins = m_modDoc.FindSampleParent((SAMPLEINDEX)lParam);
if(nins != INSTRUMENTINDEX_INVALID)
{
m_parent.InstrumentChanged(nins);
}
}
} else
{
m_parent.InstrumentChanged(static_cast<int>(lParam));
}
}
CChildFrame *pFrame = (CChildFrame *)GetParentFrame();
SAMPLEVIEWSTATE &sampleState = pFrame->GetSampleViewState();
if(sampleState.initialSample != 0)
{
m_nSample = sampleState.initialSample;
sampleState.initialSample = 0;
}
SetCurrentSample((lParam > 0) ? ((SAMPLEINDEX)lParam) : m_nSample);
// Initial Update
if (!m_bInitialized) UpdateView(SampleHint(m_nSample).Info().ModType(), NULL);
if (m_hWndView) PostViewMessage(VIEWMSG_LOADSTATE, (LPARAM)&sampleState);
SwitchToView();
// Combo boxes randomly disappear without this... why?
Invalidate();
}
void CCtrlSamples::OnDeactivatePage()
{
CChildFrame *pFrame = (CChildFrame *)GetParentFrame();
if ((pFrame) && (m_hWndView)) SendViewMessage(VIEWMSG_SAVESTATE, (LPARAM)&pFrame->GetSampleViewState());
m_modDoc.NoteOff(0, true);
}
LRESULT CCtrlSamples::OnModCtrlMsg(WPARAM wParam, LPARAM lParam)
{
switch(wParam)
{
case CTRLMSG_GETCURRENTINSTRUMENT:
return m_nSample;
break;
case CTRLMSG_SMP_PREVINSTRUMENT:
OnPrevInstrument();
break;
case CTRLMSG_SMP_NEXTINSTRUMENT:
OnNextInstrument();
break;
case CTRLMSG_SMP_OPENFILE:
if(lParam)
return OpenSample(*reinterpret_cast<const mpt::PathString *>(lParam));
break;
case CTRLMSG_SMP_SONGDROP:
if(lParam)
{
const auto &dropInfo = *reinterpret_cast<const DRAGONDROP *>(lParam);
if(dropInfo.sndFile)
return OpenSample(*dropInfo.sndFile, static_cast<SAMPLEINDEX>(dropInfo.dropItem)) ? TRUE : FALSE;
}
break;
case CTRLMSG_SMP_SETZOOM:
SetCurrentSample(m_nSample, static_cast<int>(lParam), FALSE);
break;
case CTRLMSG_SETCURRENTINSTRUMENT:
SetCurrentSample((SAMPLEINDEX)lParam, -1, TRUE);
break;
case CTRLMSG_SMP_INITOPL:
OnInitOPLInstrument();
break;
case CTRLMSG_SMP_NEWSAMPLE:
return InsertSample(false) ? 1 : 0;
case IDC_SAMPLE_REVERSE:
OnReverse();
break;
case IDC_SAMPLE_SILENCE:
OnSilence();
break;
case IDC_SAMPLE_INVERT:
OnInvert();
break;
case IDC_SAMPLE_XFADE:
OnXFade();
break;
case IDC_SAMPLE_STEREOSEPARATION:
OnStereoSeparation();
break;
case IDC_SAMPLE_AUTOTUNE:
OnAutotune();
break;
case IDC_SAMPLE_SIGN_UNSIGN:
OnSignUnSign();
break;
case IDC_SAMPLE_DCOFFSET:
RemoveDCOffset(false);
break;
case IDC_SAMPLE_NORMALIZE:
Normalize(false);
break;
case IDC_SAMPLE_AMPLIFY:
OnAmplify();
break;
case IDC_SAMPLE_QUICKFADE:
OnQuickFade();
break;
case IDC_SAMPLE_OPEN:
OnSampleOpen();
break;
case IDC_SAMPLE_SAVEAS:
OnSampleSave();
break;
case IDC_SAMPLE_NEW:
InsertSample(false);
break;
default:
return CModControlDlg::OnModCtrlMsg(wParam, lParam);
}
return 0;
}
BOOL CCtrlSamples::GetToolTipText(UINT uId, LPTSTR pszText)
{
if ((pszText) && (uId))
{
UINT val = GetDlgItemInt(uId);
const TCHAR *s = nullptr;
CommandID cmd = kcNull;
switch(uId)
{
case IDC_SAMPLE_NEW: s = _T("Insert Sample"); cmd = kcSampleNew; break;
case IDC_SAMPLE_OPEN: s = _T("Import Sample"); cmd = kcSampleLoad; break;
case IDC_SAMPLE_SAVEAS: s = _T("Save Sample"); cmd = kcSampleSave; break;
case IDC_SAMPLE_PLAY: s = _T("Play Sample"); break;
case IDC_SAMPLE_NORMALIZE: s = _T("Normalize (hold shift to normalize all samples)"); cmd = kcSampleNormalize; break;
case IDC_SAMPLE_AMPLIFY: s = _T("Amplify"); cmd = kcSampleAmplify; break;
case IDC_SAMPLE_DCOFFSET: s = _T("Remove DC Offset and Normalize (hold shift to process all samples)"); cmd = kcSampleRemoveDCOffset; break;
case IDC_SAMPLE_STEREOSEPARATION: s = _T("Change Stereo Separation / Stereo Width of the sample"); cmd = kcSampleStereoSep; break;
case IDC_SAMPLE_RESAMPLE: s = _T("Resample"); cmd = kcSampleResample; break;
case IDC_SAMPLE_REVERSE: s = _T("Reverse"); cmd = kcSampleReverse; break;
case IDC_SAMPLE_SILENCE: s = _T("Silence"); cmd = kcSampleSilence; break;
case IDC_SAMPLE_INVERT: s = _T("Invert Phase"); cmd = kcSampleInvert; break;
case IDC_SAMPLE_SIGN_UNSIGN: s = _T("Signed/Unsigned Conversion"); cmd = kcSampleSignUnsign; break;
case IDC_SAMPLE_XFADE: s = _T("Crossfade Sample Loops"); cmd = kcSampleXFade; break;
case IDC_SAMPLE_AUTOTUNE: s = _T("Tune the sample to a given note"); cmd = kcSampleAutotune; break;
case IDC_EDIT7:
case IDC_EDIT8:
// Volume to dB
if(IsOPLInstrument())
_tcscpy(pszText, (mpt::tfmt::fix((static_cast<int32>(val) - 64) * 0.75, 2) + _T(" dB")).c_str());
else
_tcscpy(pszText, CModDoc::LinearToDecibels(val, 64.0));
return TRUE;
case IDC_EDIT9:
// Panning
if(m_nSample)
{
const ModSample &sample = m_sndFile.GetSample(m_nSample);
_tcscpy(pszText, CModDoc::PanningToString(sample.nPan, 128));
}
return TRUE;
case IDC_EDIT5:
case IDC_SPIN5:
case IDC_COMBO_BASENOTE:
if(m_nSample)
{
const ModSample &sample = m_sndFile.GetSample(m_nSample);
const auto freqHz = sample.GetSampleRate(m_sndFile.GetType());
if(sample.uFlags[CHN_ADLIB])
{
// Translate to actual note frequency
_tcscpy(pszText, MPT_TFORMAT("{}Hz")(mpt::tfmt::flt(freqHz * (261.625 / 8363.0), 6)).c_str());
return TRUE;
}
if(m_sndFile.UseFinetuneAndTranspose())
{
// Transpose + Finetune to Frequency
_tcscpy(pszText, MPT_TFORMAT("{}Hz")(freqHz).c_str());
return TRUE;
}
}
break;
case IDC_EDIT14:
// Vibrato Sweep
if(!(m_sndFile.GetType() & (MOD_TYPE_IT | MOD_TYPE_MPT | MOD_TYPE_XM)))
{
s = _T("Only available in IT / MPTM / XM format");
break;
} else if(m_nSample)
{
const ModSample &sample = m_sndFile.GetSample(m_nSample);
int ticks = -1;
if(m_sndFile.m_playBehaviour[kITVibratoTremoloPanbrello])
{
if(val > 0)
ticks = Util::muldivr_unsigned(sample.nVibDepth, 256, val);
} else if(m_sndFile.GetType() & (MOD_TYPE_IT | MOD_TYPE_MPT))
{
if(val > 0)
ticks = Util::muldivr_unsigned(sample.nVibDepth, 128, val);
} else
{
ticks = val;
}
if(ticks >= 0)
_stprintf(pszText, _T("%d ticks"), ticks);
else
_tcscpy(pszText, _T("No Vibrato"));
}
return TRUE;
case IDC_EDIT15:
// Vibrato Depth
if(!(m_sndFile.GetType() & (MOD_TYPE_IT | MOD_TYPE_MPT | MOD_TYPE_XM)))
_tcscpy(pszText, _T("Only available in IT / MPTM / XM format"));
else
_stprintf(pszText, _T("%u cents"), Util::muldivr_unsigned(val, 100, 64));
return TRUE;
case IDC_EDIT16:
// Vibrato Rate
if(!(m_sndFile.GetType() & (MOD_TYPE_IT | MOD_TYPE_MPT | MOD_TYPE_XM)))
{
s = _T("Only available in IT / MPTM / XM format");
break;
} else if(val == 0)
{
s = _T("Stopped");
break;
} else
{
const double ticksPerCycle = 256.0 / val;
const uint32 ticksPerBeat = std::max(1u, m_sndFile.m_PlayState.m_nCurrentRowsPerBeat * m_sndFile.m_PlayState.m_nMusicSpeed);
_stprintf(pszText, _T("%.2f beats per cycle (%.2f ticks)"), ticksPerCycle / ticksPerBeat, ticksPerCycle);
}
return TRUE;
case IDC_CHECK1:
case IDC_EDIT3:
case IDC_EDIT4:
case IDC_COMBO2:
if(!(m_sndFile.GetType() & (MOD_TYPE_IT | MOD_TYPE_MPT)))
s = _T("Only available in IT / MPTM format");
break;
case IDC_COMBO3:
if(!(m_sndFile.GetType() & (MOD_TYPE_IT | MOD_TYPE_MPT | MOD_TYPE_XM)))
s = _T("Only available in IT / MPTM / XM format");
break;
case IDC_CHECK2:
s = _T("Keep a reference to the original waveform instead of saving it in the module.");
break;
}
if(s != nullptr)
{
_tcscpy(pszText, s);
if(cmd != kcNull)
{
auto keyText = CMainFrame::GetInputHandler()->m_activeCommandSet->GetKeyTextFromCommand(cmd, 0);
if (!keyText.IsEmpty())
_tcscat(pszText, MPT_TFORMAT(" ({})")(keyText).c_str());
}
return TRUE;
}
}
return FALSE;
}
void CCtrlSamples::UpdateView(UpdateHint hint, CObject *pObj)
{
if(pObj == this) return;
if (hint.GetType()[HINT_MPTOPTIONS])
{
m_ToolBar1.UpdateStyle();
m_ToolBar2.UpdateStyle();
}
const SampleHint sampleHint = hint.ToType<SampleHint>();
FlagSet<HintType> hintType = sampleHint.GetType();
if (!m_bInitialized) hintType.set(HINT_MODTYPE);
if(!hintType[HINT_SMPNAMES | HINT_SAMPLEINFO | HINT_MODTYPE]) return;
const SAMPLEINDEX updateSmp = sampleHint.GetSample();
if(updateSmp != m_nSample && updateSmp != 0 && !hintType[HINT_MODTYPE]) return;
const CModSpecifications &specs = m_sndFile.GetModSpecifications();
const bool isOPL = IsOPLInstrument();
LockControls();
// Updating Ranges
if(hintType[HINT_MODTYPE])
{
// Limit text fields
m_EditName.SetLimitText(specs.sampleNameLengthMax);
m_EditFileName.SetLimitText(specs.sampleFilenameLengthMax);
// Loop Type
m_ComboLoopType.ResetContent();
m_ComboLoopType.AddString(_T("Off"));
m_ComboLoopType.AddString(_T("On"));
// Sustain Loop Type
m_ComboSustainType.ResetContent();
m_ComboSustainType.AddString(_T("Off"));
m_ComboSustainType.AddString(_T("On"));
// Bidirectional Loops
if (m_sndFile.GetType() & (MOD_TYPE_XM|MOD_TYPE_IT|MOD_TYPE_MPT))
{
m_ComboLoopType.AddString(_T("Bidi"));
m_ComboSustainType.AddString(_T("Bidi"));
}
// Loop Start
m_SpinLoopStart.SetRange(-1, 1);
m_SpinLoopStart.SetPos(0);
// Loop End
m_SpinLoopEnd.SetRange(-1, 1);
m_SpinLoopEnd.SetPos(0);
// Sustain Loop Start
m_SpinSustainStart.SetRange(-1, 1);
m_SpinSustainStart.SetPos(0);
// Sustain Loop End
m_SpinSustainEnd.SetRange(-1, 1);
m_SpinSustainEnd.SetPos(0);
// Finetune / C-5 Speed / BaseNote
BOOL b = m_sndFile.UseFinetuneAndTranspose() ? FALSE : TRUE;
SetDlgItemText(IDC_TEXT7, (b) ? _T("Freq. (Hz)") : _T("Finetune"));
m_SpinFineTune.SetRange(-1, 1);
m_EditFileName.EnableWindow(b);
// AutoVibrato
b = (m_sndFile.GetType() & (MOD_TYPE_XM|MOD_TYPE_IT|MOD_TYPE_MPT)) ? TRUE : FALSE;
m_ComboAutoVib.EnableWindow(b);
m_SpinVibSweep.EnableWindow(b);
m_SpinVibDepth.EnableWindow(b);
m_SpinVibRate.EnableWindow(b);
m_EditVibSweep.EnableWindow(b);
m_EditVibDepth.EnableWindow(b);
m_EditVibRate.EnableWindow(b);
m_SpinVibSweep.SetRange(0, 255);
if(m_sndFile.GetType() & MOD_TYPE_XM)
{
m_SpinVibDepth.SetRange(0, 15);
m_SpinVibRate.SetRange(0, 63);
} else
{
m_SpinVibDepth.SetRange(0, 32);
m_SpinVibRate.SetRange(0, 64);
}
// Global Volume
b = (m_sndFile.GetType() & (MOD_TYPE_IT|MOD_TYPE_MPT)) ? TRUE : FALSE;
m_EditGlobalVol.EnableWindow(b);
m_SpinGlobalVol.EnableWindow(b);
// Panning
b = (m_sndFile.GetType() & (MOD_TYPE_XM|MOD_TYPE_IT|MOD_TYPE_MPT)) ? TRUE : FALSE;
m_CheckPanning.EnableWindow(b && !(m_sndFile.GetType() & MOD_TYPE_XM));
m_EditPanning.EnableWindow(b);
m_SpinPanning.EnableWindow(b);
m_SpinPanning.SetRange(0, (m_sndFile.GetType() == MOD_TYPE_XM) ? 255 : 64);
b = (m_sndFile.GetType() & MOD_TYPE_MOD) ? FALSE : TRUE;
m_CbnBaseNote.EnableWindow(b);
}
// Updating Values
if (hintType[HINT_MODTYPE | HINT_SAMPLEINFO])
{
if(m_nSample > m_sndFile.GetNumSamples())
{
SetCurrentSample(m_sndFile.GetNumSamples());
}
const ModSample &sample = m_sndFile.GetSample(m_nSample);
CString s;
DWORD d;
m_SpinSample.SetRange(1, m_sndFile.GetNumSamples());
m_SpinSample.Invalidate(FALSE); // In case the spin button was previously disabled
// Length / Type
if(isOPL)
s = _T("OPL instrument");
else
s = MPT_CFORMAT("{}-bit {}, len: {}")(sample.GetElementarySampleSize() * 8, CString(sample.uFlags[CHN_STEREO] ? _T("stereo") : _T("mono")), mpt::cfmt::dec(3, ',', sample.nLength));
SetDlgItemText(IDC_TEXT5, s);
// File Name
s = mpt::ToCString(m_sndFile.GetCharsetInternal(), sample.filename);
if (specs.sampleFilenameLengthMax == 0) s.Empty();
SetDlgItemText(IDC_SAMPLE_FILENAME, s);
// Volume
if(sample.uFlags[SMP_NODEFAULTVOLUME])
SetDlgItemText(IDC_EDIT7, _T("none"));
else
SetDlgItemInt(IDC_EDIT7, sample.nVolume / 4u);
// Global Volume
SetDlgItemInt(IDC_EDIT8, sample.nGlobalVol);
// Panning
CheckDlgButton(IDC_CHECK1, (sample.uFlags[CHN_PANNING]) ? BST_CHECKED : BST_UNCHECKED);
if (m_sndFile.GetType() == MOD_TYPE_XM)
SetDlgItemInt(IDC_EDIT9, sample.nPan); //displayed panning with XM is 0-256, just like MPT's internal engine
else
SetDlgItemInt(IDC_EDIT9, sample.nPan / 4u); //displayed panning with anything but XM is 0-64 so we divide by 4
// FineTune / C-4 Speed / BaseNote
int transp = 0;
if (!m_sndFile.UseFinetuneAndTranspose())
{
s = mpt::cfmt::val(sample.nC5Speed);
m_EditFineTune.SetWindowText(s);
if(sample.nC5Speed != 0)
transp = ModSample::FrequencyToTranspose(sample.nC5Speed).first;
} else
{
int ftune = ((int)sample.nFineTune);
// MOD finetune range -8 to 7 translates to -128 to 112
if(m_sndFile.GetType() & MOD_TYPE_MOD) ftune >>= 4;
SetDlgItemInt(IDC_EDIT5, ftune);
transp = (int)sample.RelativeTone;
}
int basenote = (NOTE_MIDDLEC - NOTE_MIN) + transp;
Limit(basenote, BASENOTE_MIN, BASENOTE_MAX);
basenote -= BASENOTE_MIN;
if (basenote != m_CbnBaseNote.GetCurSel()) m_CbnBaseNote.SetCurSel(basenote);
// Auto vibrato
// Ramp up and ramp down are swapped in XM - probably because they ramp up the *period* instead of *frequency*.
const VibratoType rampUp = m_sndFile.GetType() == MOD_TYPE_XM ? VIB_RAMP_DOWN : VIB_RAMP_UP;
const VibratoType rampDown = m_sndFile.GetType() == MOD_TYPE_XM ? VIB_RAMP_UP : VIB_RAMP_DOWN;
m_ComboAutoVib.ResetContent();
m_ComboAutoVib.SetItemData(m_ComboAutoVib.AddString(_T("Sine")), VIB_SINE);
m_ComboAutoVib.SetItemData(m_ComboAutoVib.AddString(_T("Square")), VIB_SQUARE);
if(m_sndFile.GetType() != MOD_TYPE_IT || sample.nVibType == VIB_RAMP_UP)
m_ComboAutoVib.SetItemData(m_ComboAutoVib.AddString(_T("Ramp Up")), rampUp);
m_ComboAutoVib.SetItemData(m_ComboAutoVib.AddString(_T("Ramp Down")), rampDown);
if(m_sndFile.GetType() != MOD_TYPE_XM || sample.nVibType == VIB_RANDOM)
m_ComboAutoVib.SetItemData(m_ComboAutoVib.AddString(_T("Random")), VIB_RANDOM);
for(int i = 0; i < m_ComboAutoVib.GetCount(); i++)
{
if(m_ComboAutoVib.GetItemData(i) == sample.nVibType)
{
m_ComboAutoVib.SetCurSel(i);
break;
}
}
SetDlgItemInt(IDC_EDIT14, sample.nVibSweep);
SetDlgItemInt(IDC_EDIT15, sample.nVibDepth);
SetDlgItemInt(IDC_EDIT16, sample.nVibRate);
// Loop
d = 0;
if (sample.uFlags[CHN_LOOP]) d = sample.uFlags[CHN_PINGPONGLOOP] ? 2 : 1;
if (sample.uFlags[CHN_REVERSE]) d |= 4;
m_ComboLoopType.SetCurSel(d);
s = mpt::cfmt::val(sample.nLoopStart);
m_EditLoopStart.SetWindowText(s);
s = mpt::cfmt::val(sample.nLoopEnd);
m_EditLoopEnd.SetWindowText(s);
// Sustain Loop
d = 0;
if (sample.uFlags[CHN_SUSTAINLOOP]) d = sample.uFlags[CHN_PINGPONGSUSTAIN] ? 2 : 1;
m_ComboSustainType.SetCurSel(d);
s = mpt::cfmt::val(sample.nSustainStart);
m_EditSustainStart.SetWindowText(s);
s = mpt::cfmt::val(sample.nSustainEnd);
m_EditSustainEnd.SetWindowText(s);
// Disable certain buttons for OPL instruments
BOOL b = isOPL ? FALSE : TRUE;
static constexpr int sampleButtons[] =
{
IDC_SAMPLE_NORMALIZE, IDC_SAMPLE_AMPLIFY,
IDC_SAMPLE_DCOFFSET, IDC_SAMPLE_STEREOSEPARATION,
IDC_SAMPLE_RESAMPLE, IDC_SAMPLE_REVERSE,
IDC_SAMPLE_SILENCE, IDC_SAMPLE_INVERT,
IDC_SAMPLE_SIGN_UNSIGN, IDC_SAMPLE_XFADE,
};
for(auto btn : sampleButtons)
{
m_ToolBar2.EnableButton(btn, b);
}
m_ComboLoopType.EnableWindow(b);
m_SpinLoopStart.EnableWindow(b);
m_SpinLoopEnd.EnableWindow(b);
m_EditLoopStart.EnableWindow(b);
m_EditLoopEnd.EnableWindow(b);
const bool hasSustainLoop = !isOPL && ((m_sndFile.GetType() & (MOD_TYPE_IT | MOD_TYPE_MPT)) || (m_nSample <= m_sndFile.GetNumSamples() && m_sndFile.GetSample(m_nSample).uFlags[CHN_SUSTAINLOOP]));
b = hasSustainLoop ? TRUE : FALSE;
m_ComboSustainType.EnableWindow(b);
m_SpinSustainStart.EnableWindow(b);
m_SpinSustainEnd.EnableWindow(b);
m_EditSustainStart.EnableWindow(b);
m_EditSustainEnd.EnableWindow(b);
}
if(hintType[HINT_MODTYPE | HINT_SAMPLEINFO | HINT_SMPNAMES])
{
// Name
SetDlgItemText(IDC_SAMPLE_NAME, mpt::ToWin(m_sndFile.GetCharsetInternal(), m_sndFile.m_szNames[m_nSample]).c_str());
CheckDlgButton(IDC_CHECK2, m_sndFile.GetSample(m_nSample).uFlags[SMP_KEEPONDISK] ? BST_CHECKED : BST_UNCHECKED);
GetDlgItem(IDC_CHECK2)->EnableWindow((m_sndFile.SampleHasPath(m_nSample) && m_sndFile.GetType() == MOD_TYPE_MPT) ? TRUE : FALSE);
}
if (!m_bInitialized)
{
// First update
m_bInitialized = TRUE;
UnlockControls();
}
m_ComboLoopType.Invalidate(FALSE);
m_ComboSustainType.Invalidate(FALSE);
m_ComboAutoVib.Invalidate(FALSE);
UnlockControls();
}
// updateAll: Update all views including this one. Otherwise, only update update other views.
void CCtrlSamples::SetModified(SampleHint hint, bool updateAll, bool waveformModified)
{
m_modDoc.SetModified();
if(waveformModified)
{
// Update on-disk sample status in tree
ModSample &sample = m_sndFile.GetSample(m_nSample);
if(sample.uFlags[SMP_KEEPONDISK] && !sample.uFlags[SMP_MODIFIED]) hint.Names();
sample.uFlags.set(SMP_MODIFIED);
}
m_modDoc.UpdateAllViews(nullptr, hint.SetData(m_nSample), updateAll ? nullptr : this);
}
void CCtrlSamples::PrepareUndo(const char *description, sampleUndoTypes type, SmpLength start, SmpLength end)
{
m_startedEdit = true;
m_modDoc.GetSampleUndo().PrepareUndo(m_nSample, type, description, start, end);
}
bool CCtrlSamples::OpenSample(const mpt::PathString &fileName, FlagSet<OpenSampleTypes> types)
{
BeginWaitCursor();
InputFile f(fileName, TrackerSettings::Instance().MiscCacheCompleteFileBeforeLoading);
if(!f.IsValid())
{
EndWaitCursor();
return false;
}
FileReader file = GetFileReader(f);
if(!file.IsValid())
{
EndWaitCursor();
return false;
}
PrepareUndo("Replace", sundo_replace);
const auto parentIns = GetParentInstrumentWithSameName();
bool bOk = false;
if(types[OpenSampleKnown])
{
bOk = m_sndFile.ReadSampleFromFile(m_nSample, file, TrackerSettings::Instance().m_MayNormalizeSamplesOnLoad);
if(!bOk)
{
// Try loading as module
bOk = CMainFrame::GetMainFrame()->SetTreeSoundfile(file);
if(bOk)
{
m_modDoc.GetSampleUndo().RemoveLastUndoStep(m_nSample);
return true;
}
}
}
ModSample &sample = m_sndFile.GetSample(m_nSample);
if(!bOk && types[OpenSampleRaw])
{
CRawSampleDlg dlg(file, this);
EndWaitCursor();
if(m_rememberRawFormat || dlg.DoModal() == IDOK)
{
SampleIO sampleIO = dlg.GetSampleFormat();
m_rememberRawFormat = m_rememberRawFormat || dlg.GetRemeberFormat();
BeginWaitCursor();
file.Seek(dlg.GetOffset());
m_sndFile.DestroySampleThreadsafe(m_nSample);
const auto bytesPerSample = sampleIO.GetNumChannels() * sampleIO.GetBitDepth() / 8u;
sample.nLength = mpt::saturate_cast<SmpLength>(file.BytesLeft() / bytesPerSample);
if(TrackerSettings::Instance().m_MayNormalizeSamplesOnLoad)
{
sampleIO.MayNormalize();
}
if(sampleIO.ReadSample(sample, file))
{
bOk = true;
sample.nGlobalVol = 64;
sample.nVolume = 256;
sample.nPan = 128;
sample.uFlags.reset(CHN_LOOP | CHN_SUSTAINLOOP | SMP_MODIFIED);
sample.filename = "";
m_sndFile.m_szNames[m_nSample] = "";
if(!sample.nC5Speed) sample.nC5Speed = 22050;
sample.PrecomputeLoops(m_sndFile, false);
} else
{
m_modDoc.GetSampleUndo().Undo(m_nSample);
}
} else
{
m_modDoc.GetSampleUndo().RemoveLastUndoStep(m_nSample);
}
} else
{
m_sndFile.SetSamplePath(m_nSample, fileName);
}
EndWaitCursor();
if (bOk)
{
TrackerSettings::Instance().PathSamples.SetWorkingDir(fileName, true);
if(sample.filename.empty())
{
mpt::PathString name, ext;
fileName.SplitPath(nullptr, nullptr, &name, &ext);
if(m_sndFile.m_szNames[m_nSample].empty()) m_sndFile.m_szNames[m_nSample] = name.ToLocale();
if(name.AsNative().length() < 9) name += ext;
sample.filename = name.ToLocale();
}
if ((m_sndFile.GetType() & MOD_TYPE_XM) && !sample.uFlags[CHN_PANNING])
{
sample.nPan = 128;
sample.uFlags.set(CHN_PANNING);
}
SetModified(SampleHint().Info().Data().Names(), true, false);
sample.uFlags.reset(SMP_KEEPONDISK);
if(parentIns <= m_sndFile.GetNumInstruments())
{
if(auto instr = m_sndFile.Instruments[parentIns]; instr != nullptr)
{
m_modDoc.GetInstrumentUndo().PrepareUndo(parentIns, "Set Name");
instr->name = m_sndFile.m_szNames[m_nSample];
m_modDoc.UpdateAllViews(nullptr, InstrumentHint(parentIns).Names(), this);
}
}
}
return true;
}
bool CCtrlSamples::OpenSample(const CSoundFile &sndFile, SAMPLEINDEX nSample)
{
if(!nSample || nSample > sndFile.GetNumSamples()) return false;
BeginWaitCursor();
PrepareUndo("Replace", sundo_replace);
const auto parentIns = GetParentInstrumentWithSameName();
if(m_sndFile.ReadSampleFromSong(m_nSample, sndFile, nSample))
{
SetModified(SampleHint().Info().Data().Names(), true, false);
if(parentIns <= m_sndFile.GetNumInstruments())
{
if(auto instr = m_sndFile.Instruments[parentIns]; instr != nullptr)
{
m_modDoc.GetInstrumentUndo().PrepareUndo(parentIns, "Set Name");
instr->name = m_sndFile.m_szNames[m_nSample];
m_modDoc.UpdateAllViews(nullptr, InstrumentHint(parentIns).Names(), this);
}
}
} else
{
m_modDoc.GetSampleUndo().RemoveLastUndoStep(m_nSample);
}
EndWaitCursor();
return true;
}
//////////////////////////////////////////////////////////////////////////////////
// CCtrlSamples messages
void CCtrlSamples::OnSampleChanged()
{
if(!IsLocked())
{
SAMPLEINDEX n = (SAMPLEINDEX)GetDlgItemInt(IDC_EDIT_SAMPLE);
if ((n > 0) && (n <= m_sndFile.GetNumSamples()) && (n != m_nSample))
{
SetCurrentSample(n, -1, FALSE);
m_parent.SampleChanged(m_nSample);
}
}
}
void CCtrlSamples::OnZoomChanged()
{
if (!IsLocked()) SetCurrentSample(m_nSample);
SwitchToView();
}
void CCtrlSamples::OnTbnDropDownToolBar(NMHDR *pNMHDR, LRESULT *pResult)
{
CInputHandler *ih = CMainFrame::GetInputHandler();
NMTOOLBAR *pToolBar = reinterpret_cast<NMTOOLBAR *>(pNMHDR);
ClientToScreen(&(pToolBar->rcButton)); // TrackPopupMenu uses screen coords
const int offset = Util::ScalePixels(4, m_hWnd); // Compared to the main toolbar, the offset seems to be a bit wrong here...?
int x = pToolBar->rcButton.left + offset, y = pToolBar->rcButton.bottom + offset;
CMenu menu;
switch(pToolBar->iItem)
{
case IDC_SAMPLE_NEW:
{
menu.CreatePopupMenu();
menu.AppendMenu(MF_STRING, IDC_SAMPLE_DUPLICATE, ih->GetKeyTextFromCommand(kcSampleDuplicate, m_sndFile.GetSample(m_nSample).uFlags[CHN_ADLIB] ? _T("&Duplicate Instrument") : _T("&Duplicate Sample")));
menu.AppendMenu(MF_STRING | (m_sndFile.SupportsOPL() ? 0 : MF_DISABLED), IDC_SAMPLE_INITOPL, ih->GetKeyTextFromCommand(kcSampleInitializeOPL, _T("Initialize &OPL Instrument")));
menu.TrackPopupMenu(TPM_LEFTALIGN | TPM_RIGHTBUTTON, x, y, this);
menu.DestroyMenu();
}
break;
case IDC_SAMPLE_OPEN:
{
menu.CreatePopupMenu();
menu.AppendMenu(MF_STRING, IDC_SAMPLE_OPENKNOWN, ih->GetKeyTextFromCommand(kcSampleLoad, _T("Import &Sample...")));
menu.AppendMenu(MF_STRING, IDC_SAMPLE_OPENRAW, ih->GetKeyTextFromCommand(kcSampleLoadRaw, _T("Import &Raw Sample...")));
menu.TrackPopupMenu(TPM_LEFTALIGN | TPM_RIGHTBUTTON, x, y, this);
menu.DestroyMenu();
}
break;
case IDC_SAMPLE_SAVEAS:
{
menu.CreatePopupMenu();
menu.AppendMenu(MF_STRING, IDC_SAVE_ALL, _T("Save &All..."));
menu.TrackPopupMenu(TPM_LEFTALIGN | TPM_RIGHTBUTTON, x, y, this);
menu.DestroyMenu();
}
break;
}
*pResult = 0;
}
void CCtrlSamples::OnSampleNew()
{
InsertSample(CMainFrame::GetInputHandler()->ShiftPressed());
SwitchToView();
}
bool CCtrlSamples::InsertSample(bool duplicate, int8 *confirm)
{
const SAMPLEINDEX smp = m_modDoc.InsertSample();
if(smp != SAMPLEINDEX_INVALID)
{
const SAMPLEINDEX oldSmp = m_nSample;
CSoundFile &sndFile = m_modDoc.GetSoundFile();
SetCurrentSample(smp);
if(duplicate && oldSmp >= 1 && oldSmp <= sndFile.GetNumSamples())
{
m_modDoc.GetSampleUndo().PrepareUndo(smp, sundo_replace, "Duplicate");
sndFile.ReadSampleFromSong(smp, sndFile, oldSmp);
}
m_modDoc.UpdateAllViews(nullptr, SampleHint(smp).Info().Data().Names());
if(m_modDoc.GetNumInstruments() > 0 && m_modDoc.FindSampleParent(smp) == INSTRUMENTINDEX_INVALID)
{
bool insertInstrument;
if(confirm == nullptr || *confirm == -1)
{
insertInstrument = Reporting::Confirm("This sample is not used by any instrument. Do you want to create a new instrument using this sample?") == cnfYes;
if(confirm != nullptr) *confirm = insertInstrument;
} else
{
insertInstrument = (*confirm) != 0;
}
if(insertInstrument)
{
INSTRUMENTINDEX nins = m_modDoc.InsertInstrument(smp);
m_modDoc.UpdateAllViews(nullptr, InstrumentHint(nins).Info().Envelope().Names());
m_parent.InstrumentChanged(nins);
}
}
}
return (smp != SAMPLEINDEX_INVALID);
}
static constexpr std::pair<const mpt::uchar *, const mpt::uchar *> SampleFormats[]
{
{ UL_("Wave Files (*.wav)"), UL_("*.wav") },
#ifdef MPT_WITH_FLAC
{ UL_("FLAC Files (*.flac,*.oga)"), UL_("*.flac;*.oga") },
#endif // MPT_WITH_FLAC
#if defined(MPT_WITH_OPUSFILE)
{ UL_("Opus Files (*.opus,*.oga)"), UL_("*.opus;*.oga") },
#endif // MPT_WITH_OPUSFILE
#if defined(MPT_WITH_VORBISFILE) || defined(MPT_WITH_STBVORBIS)
{ UL_("Ogg Vorbis Files (*.ogg,*.oga)"), UL_("*.ogg;*.oga") },
#endif // VORBIS
#if defined(MPT_ENABLE_MP3_SAMPLES)
{ UL_("MPEG Files (*.mp1,*.mp2,*.mp3)"), UL_("*.mp1;*.mp2;*.mp3") },
#endif // MPT_ENABLE_MP3_SAMPLES
{ UL_("XI Samples (*.xi)"), UL_("*.xi") },
{ UL_("Impulse Tracker Samples (*.its)"), UL_("*.its") },
{ UL_("Scream Tracker Samples (*.s3i,*.smp)"), UL_("*.s3i;*.smp") },
{ UL_("OPL Instruments (*.sb0,*.sb2,*.sbi)"), UL_("*.sb0;*.sb2;*.sbi") },
{ UL_("GF1 Patches (*.pat)"), UL_("*.pat") },
{ UL_("Wave64 Files (*.w64)"), UL_("*.w64") },
{ UL_("CAF Files (*.wav)"), UL_("*.caf") },
{ UL_("AIFF Files (*.aiff,*.8svx)"), UL_("*.aif;*.aiff;*.iff;*.8sv;*.8svx;*.svx") },
{ UL_("Sun Audio (*.au,*.snd)"), UL_("*.au;*.snd") },
{ UL_("SNES BRR Files (*.brr)"), UL_("*.brr") },
};
static mpt::ustring ConstructFileFilter(bool includeRaw)
{
mpt::ustring s = U_("All Samples (*.wav,*.flac,*.xi,*.its,*.s3i,*.sbi,...)|");
bool first = true;
for(const auto &[name, exts] : SampleFormats)
{
if(!first)
s += U_(";");
else
first = false;
s += exts;
}
#if defined(MPT_WITH_MEDIAFOUNDATION)
std::vector<FileType> mediaFoundationTypes = CSoundFile::GetMediaFoundationFileTypes();
s += ToFilterOnlyString(mediaFoundationTypes, true).ToUnicode();
#endif
if(includeRaw)
{
s += U_(";*.raw;*.snd;*.pcm;*.sam");
}
s += U_("|");
for(const auto &[name, exts] : SampleFormats)
{
s += name + U_("|");
s += exts + U_("|");
}
#if defined(MPT_WITH_MEDIAFOUNDATION)
s += ToFilterString(mediaFoundationTypes, FileTypeFormatShowExtensions).ToUnicode();
#endif
if(includeRaw)
{
s += U_("Raw Samples (*.raw,*.snd,*.pcm,*.sam)|*.raw;*.snd;*.pcm;*.sam|");
}
s += U_("All Files (*.*)|*.*||");
return s;
}
void CCtrlSamples::OnSampleOpen()
{
static int nLastIndex = 0;
std::vector<FileType> mediaFoundationTypes = CSoundFile::GetMediaFoundationFileTypes();
FileDialog dlg = OpenFileDialog()
.AllowMultiSelect()
.EnableAudioPreview()
.ExtensionFilter(ConstructFileFilter(true))
.WorkingDirectory(TrackerSettings::Instance().PathSamples.GetWorkingDir())
.FilterIndex(&nLastIndex);
if(!dlg.Show(this)) return;
TrackerSettings::Instance().PathSamples.SetWorkingDir(dlg.GetWorkingDirectory());
OpenSamples(dlg.GetFilenames(), OpenSampleKnown | OpenSampleRaw);
SwitchToView();
}
void CCtrlSamples::OnSampleOpenKnown()
{
static int nLastIndex = 0;
std::vector<FileType> mediaFoundationTypes = CSoundFile::GetMediaFoundationFileTypes();
FileDialog dlg = OpenFileDialog()
.AllowMultiSelect()
.EnableAudioPreview()
.ExtensionFilter(ConstructFileFilter(false))
.WorkingDirectory(TrackerSettings::Instance().PathSamples.GetWorkingDir())
.FilterIndex(&nLastIndex);
if(!dlg.Show(this)) return;
TrackerSettings::Instance().PathSamples.SetWorkingDir(dlg.GetWorkingDirectory());
OpenSamples(dlg.GetFilenames(), OpenSampleKnown);
}
void CCtrlSamples::OnSampleOpenRaw()
{
static int nLastIndex = 0;
FileDialog dlg = OpenFileDialog()
.AllowMultiSelect()
.EnableAudioPreview()
.ExtensionFilter("Raw Samples (*.raw,*.snd,*.pcm,*.sam)|*.raw;*.snd;*.pcm;*.sam|"
"All Files (*.*)|*.*||")
.WorkingDirectory(TrackerSettings::Instance().PathSamples.GetWorkingDir())
.FilterIndex(&nLastIndex);
if(!dlg.Show(this)) return;
TrackerSettings::Instance().PathSamples.SetWorkingDir(dlg.GetWorkingDirectory());
OpenSamples(dlg.GetFilenames(), OpenSampleRaw);
}
void CCtrlSamples::OpenSamples(const std::vector<mpt::PathString> &files, FlagSet<OpenSampleTypes> types)
{
int8 confirm = -1;
bool first = true;
for(const auto &file : files)
{
// If loading multiple samples, create new slots for them
if(!first)
{
if(!InsertSample(false, &confirm))
break;
}
if(OpenSample(file, types))
first = false;
else
ErrorBox(IDS_ERR_FILEOPEN, this);
}
SwitchToView();
}
void CCtrlSamples::OnSampleSave()
{
SaveSample(CMainFrame::GetInputHandler()->ShiftPressed());
}
void CCtrlSamples::SaveSample(bool doBatchSave)
{
mpt::PathString fileName, defaultPath = TrackerSettings::Instance().PathSamples.GetWorkingDir();
SampleEditorDefaultFormat defaultFormat = TrackerSettings::Instance().m_defaultSampleFormat;
bool hasAdlib = false;
if(!doBatchSave)
{
// Save this sample
const ModSample &sample = m_sndFile.GetSample(m_nSample);
if((!m_nSample) || (!sample.HasSampleData()))
{
SwitchToView();
return;
}
if(m_sndFile.SampleHasPath(m_nSample))
{
// For on-disk samples, propose their original filename and location
auto path = m_sndFile.GetSamplePath(m_nSample);
fileName = path.GetFullFileName();
defaultPath = path.GetPath();
}
if(fileName.empty()) fileName = mpt::PathString::FromLocale(sample.filename);
if(fileName.empty()) fileName = mpt::PathString::FromLocale(m_sndFile.m_szNames[m_nSample]);
if(fileName.empty()) fileName = P_("untitled");
const mpt::PathString ext = fileName.GetFileExt();
if(!mpt::PathString::CompareNoCase(ext, P_(".flac"))) defaultFormat = dfFLAC;
else if(!mpt::PathString::CompareNoCase(ext, P_(".wav"))) defaultFormat = dfWAV;
else if(!mpt::PathString::CompareNoCase(ext, P_(".s3i"))) defaultFormat = dfS3I;
hasAdlib = sample.uFlags[CHN_ADLIB];
} else
{
// Save all samples
fileName = m_sndFile.GetpModDoc()->GetPathNameMpt().GetFileName();
if(fileName.empty()) fileName = P_("untitled");
fileName += P_(" - %sample_number% - ");
if(m_sndFile.GetModSpecifications().sampleFilenameLengthMax == 0)
fileName += P_("%sample_name%");
else
fileName += P_("%sample_filename%");
}
SanitizeFilename(fileName);
int filter;
switch(defaultFormat)
{
case dfWAV:
filter = 1;
break;
case dfFLAC:
default:
filter = 2;
break;
case dfS3I:
filter = 3;
break;
case dfRAW:
filter = 4;
break;
}
// Do we have to use a format that can save OPL instruments?
if(hasAdlib)
filter = 3;
FileDialog dlg = SaveFileDialog()
.DefaultExtension(ToSettingValue(defaultFormat).as<mpt::ustring>())
.DefaultFilename(fileName)
.ExtensionFilter("Wave File (*.wav)|*.wav|"
"FLAC File (*.flac)|*.flac|"
"S3I Scream Tracker 3 Instrument (*.s3i)|*.s3i|"
"RAW Audio (*.raw)|*.raw||")
.WorkingDirectory(defaultPath)
.FilterIndex(&filter);
if(!dlg.Show(this)) return;
BeginWaitCursor();
const auto saveFormat = FromSettingValue<SampleEditorDefaultFormat>(dlg.GetExtension().ToUnicode());
SAMPLEINDEX minSmp = m_nSample, maxSmp = m_nSample;
if(doBatchSave)
{
minSmp = 1;
maxSmp = m_sndFile.GetNumSamples();
}
const auto numberFmt = mpt::FormatSpec().Dec().FillNul().Width(1 + static_cast<int>(std::log10(maxSmp)));
bool ok = false;
CString sampleName, sampleFilename;
for(SAMPLEINDEX smp = minSmp; smp <= maxSmp; smp++)
{
ModSample &sample = m_sndFile.GetSample(smp);
if(sample.HasSampleData())
{
const bool isAdlib = sample.uFlags[CHN_ADLIB];
fileName = dlg.GetFirstFile();
if(doBatchSave)
{
sampleName = mpt::ToCString(m_sndFile.GetCharsetInternal(), (!m_sndFile.m_szNames[smp].empty()) ? std::string(m_sndFile.m_szNames[smp]) : "untitled");
sampleFilename = mpt::ToCString(m_sndFile.GetCharsetInternal(), (!sample.filename.empty()) ? sample.GetFilename() : m_sndFile.m_szNames[smp]);
SanitizeFilename(sampleName);
SanitizeFilename(sampleFilename);
mpt::ustring fileNameU = fileName.ToUnicode();
fileNameU = mpt::String::Replace(fileNameU, U_("%sample_number%"), mpt::ufmt::fmt(smp, numberFmt));
fileNameU = mpt::String::Replace(fileNameU, U_("%sample_filename%"), mpt::ToUnicode(sampleFilename));
fileNameU = mpt::String::Replace(fileNameU, U_("%sample_name%"), mpt::ToUnicode(sampleName));
fileName = mpt::PathString::FromUnicode(fileNameU);
// Need to enforce S3I for Adlib samples
if(isAdlib && saveFormat != dfS3I)
fileName = fileName.ReplaceExt(P_(".s3i"));
}
try
{
mpt::SafeOutputFile sf(fileName, std::ios::binary, mpt::FlushModeFromBool(TrackerSettings::Instance().MiscFlushFileBuffersOnSave));
mpt::ofstream &f = sf;
if(!f)
{
ok = false;
continue;
}
f.exceptions(f.exceptions() | std::ios::badbit | std::ios::failbit);
// Need to enforce S3I for Adlib samples
const auto thisFormat = isAdlib ? dfS3I : saveFormat;
if(thisFormat == dfRAW)
ok = m_sndFile.SaveRAWSample(smp, f);
else if(thisFormat == dfFLAC)
ok = m_sndFile.SaveFLACSample(smp, f);
else if(thisFormat == dfS3I)
ok = m_sndFile.SaveS3ISample(smp, f);
else
ok = m_sndFile.SaveWAVSample(smp, f);
} catch(const std::exception &)
{
ok = false;
}
if(ok)
{
m_sndFile.SetSamplePath(smp, fileName);
sample.uFlags.reset(SMP_MODIFIED);
UpdateView(SampleHint().Info());
// Check if any other samples refer to the same file - that would be dangerous.
if(sample.uFlags[SMP_KEEPONDISK])
{
for(SAMPLEINDEX i = 1; i <= m_sndFile.GetNumSamples(); i++)
{
if(i != smp && m_sndFile.GetSample(i).uFlags[SMP_KEEPONDISK] && m_sndFile.GetSamplePath(i) == m_sndFile.GetSamplePath(smp))
{
m_sndFile.GetSample(i).uFlags.reset(SMP_KEEPONDISK);
m_modDoc.UpdateAllViews(nullptr, SampleHint(i).Names().Info(), this);
}
}
}
}
}
}
EndWaitCursor();
if (!ok)
{
ErrorBox(IDS_ERR_SAVESMP, this);
} else
{
TrackerSettings::Instance().PathSamples.SetWorkingDir(dlg.GetWorkingDirectory());
}
SwitchToView();
}
void CCtrlSamples::OnSamplePlay()
{
if (m_modDoc.IsNotePlaying(NOTE_NONE, m_nSample, 0))
{
m_modDoc.NoteOff(0, true);
} else
{
m_modDoc.PlayNote(PlayNoteParam(NOTE_MIDDLEC).Sample(m_nSample));
}
SwitchToView();
}
template<typename T>
static bool DoNormalize(T *p, SmpLength selStart, SmpLength selEnd)
{
auto [min, max] = CViewSample::FindMinMax(p + selStart, selEnd - selStart, 1);
max = std::max(-min, max);
if(max < std::numeric_limits<T>::max())
{
max++;
for(SmpLength i = selStart; i < selEnd; i++)
{
p[i] = static_cast<T>((static_cast<int>(p[i]) << (sizeof(T) * 8 - 1)) / max);
}
return true;
}
return false;
}
void CCtrlSamples::Normalize(bool allSamples)
{
//Default case: Normalize current sample
SAMPLEINDEX minSample = m_nSample, maxSample = m_nSample;
//If only one sample is selected, parts of it may be amplified
SmpLength selStart = 0, selEnd = 0;
if(allSamples)
{
if(Reporting::Confirm(_T("This will normalize all samples independently. Continue?"), _T("Normalize")) == cnfNo)
return;
minSample = 1;
maxSample = m_sndFile.GetNumSamples();
} else
{
SampleSelectionPoints selection = GetSelectionPoints();
selStart = selection.nStart;
selEnd = selection.nEnd;
}
BeginWaitCursor();
bool modified = false;
for(SAMPLEINDEX smp = minSample; smp <= maxSample; smp++)
{
if(m_sndFile.GetSample(smp).HasSampleData())
{
ModSample &sample = m_sndFile.GetSample(smp);
if(minSample != maxSample)
{
// If more than one sample is selected, always amplify the whole sample.
selStart = 0;
selEnd = sample.nLength;
} else
{
// One sample: correct the boundaries, if needed
LimitMax(selEnd, sample.nLength);
LimitMax(selStart, selEnd);
if(selStart == selEnd)
{
selStart = 0;
selEnd = sample.nLength;
}
}
m_modDoc.GetSampleUndo().PrepareUndo(smp, sundo_update, "Normalize", selStart, selEnd);
selStart *= sample.GetNumChannels();
selEnd *= sample.GetNumChannels();
if(sample.uFlags[CHN_16BIT])
{
modified |= DoNormalize(sample.sample16(), selStart, selEnd);
} else
{
modified |= DoNormalize(sample.sample8(), selStart, selEnd);
}
if(modified)
{
sample.PrecomputeLoops(m_sndFile, false);
m_modDoc.UpdateAllViews(nullptr, SampleHint(smp).Data());
}
}
}
if(modified)
{
SetModified(SampleHint().Data(), false, true);
}
EndWaitCursor();
SwitchToView();
}
void CCtrlSamples::OnNormalize()
{
Normalize(CMainFrame::GetInputHandler()->ShiftPressed());
}
void CCtrlSamples::RemoveDCOffset(bool allSamples)
{
SAMPLEINDEX minSample = m_nSample, maxSample = m_nSample;
//Shift -> Process all samples
if(allSamples)
{
if(Reporting::Confirm(_T("This will process all samples independently. Continue?"), _T("DC Offset Removal")) == cnfNo)
return;
minSample = 1;
maxSample = m_sndFile.GetNumSamples();
}
BeginWaitCursor();
// for report / SetModified
SAMPLEINDEX numModified = 0;
double reportOffset = 0;
for(SAMPLEINDEX smp = minSample; smp <= maxSample; smp++)
{
SmpLength selStart, selEnd;
if(!m_sndFile.GetSample(smp).HasSampleData())
continue;
if (minSample != maxSample)
{
selStart = 0;
selEnd = m_sndFile.GetSample(smp).nLength;
} else
{
SampleSelectionPoints selection = GetSelectionPoints();
selStart = selection.nStart;
selEnd = selection.nEnd;
}
m_modDoc.GetSampleUndo().PrepareUndo(smp, sundo_update, "Remove DC Offset", selStart, selEnd);
const double offset = SampleEdit::RemoveDCOffset(m_sndFile.GetSample(smp), selStart, selEnd, m_sndFile);
if(offset == 0.0f) // No offset removed.
continue;
reportOffset += offset;
numModified++;
m_modDoc.UpdateAllViews(nullptr, SampleHint(smp).Info().Data());
}
EndWaitCursor();
SwitchToView();
// fill the statusbar with some nice information
CString dcInfo;
if(numModified)
{
SetModified(SampleHint().Info().Data(), true, true);
if(numModified == 1)
{
dcInfo.Format(_T("Removed DC offset (%.1f%%)"), reportOffset * 100);
} else
{
dcInfo.Format(_T("Removed DC offset from %u samples (avg %0.1f%%)"), numModified, reportOffset / numModified * 100);
}
} else
{
dcInfo.SetString(_T("No DC offset found"));
}
CMainFrame *pMainFrm = CMainFrame::GetMainFrame();
pMainFrm->SetXInfoText(dcInfo);
}
void CCtrlSamples::OnRemoveDCOffset()
{
RemoveDCOffset(CMainFrame::GetInputHandler()->ShiftPressed());
}
void CCtrlSamples::ApplyAmplify(const double amp, const double fadeInStart, const double fadeOutEnd, const bool fadeIn, const bool fadeOut, const Fade::Law fadeLaw)
{
ModSample &sample = m_sndFile.GetSample(m_nSample);
if(!sample.HasSampleData() || sample.uFlags[CHN_ADLIB]) return;
BeginWaitCursor();
SampleSelectionPoints selection = GetSelectionPoints();
const auto start = selection.nStart, end = selection.nEnd, mid = (start + end) / 2;
PrepareUndo("Amplify", sundo_update, start, end);
if(fadeIn && fadeOut)
{
SampleEdit::AmplifySample(sample, start, mid, fadeInStart, amp, true, fadeLaw, m_sndFile);
SampleEdit::AmplifySample(sample, mid, end, amp, fadeOutEnd, false, fadeLaw, m_sndFile);
} else if(fadeIn)
{
SampleEdit::AmplifySample(sample, start, end, fadeInStart, amp, true, fadeLaw, m_sndFile);
} else if(fadeOut)
{
SampleEdit::AmplifySample(sample, start, end, amp, fadeOutEnd, false, fadeLaw, m_sndFile);
} else
{
SampleEdit::AmplifySample(sample, start, end, amp, amp, true, Fade::kLinear, m_sndFile);
}
sample.PrecomputeLoops(m_sndFile, false);
SetModified(SampleHint().Data(), false, true);
EndWaitCursor();
SwitchToView();
}
void CCtrlSamples::OnAmplify()
{
static CAmpDlg::AmpSettings settings { Fade::kLinear, 0, 0, 100, false, false };
CAmpDlg dlg(this, settings);
if (dlg.DoModal() != IDOK) return;
ApplyAmplify(settings.factor / 100.0, settings.fadeInStart / 100.0, settings.fadeOutEnd / 100.0, settings.fadeIn, settings.fadeOut, settings.fadeLaw);
}
// Quickly fade the selection in/out without asking the user.
// Fade-In is applied if the selection starts at the beginning of the sample.
// Fade-Out is applied if the selection ends and the end of the sample.
void CCtrlSamples::OnQuickFade()
{
ModSample &sample = m_sndFile.GetSample(m_nSample);
if(!sample.HasSampleData() || sample.uFlags[CHN_ADLIB]) return;
SampleSelectionPoints sel = GetSelectionPoints();
if(sel.selectionActive && (sel.nStart == 0 || sel.nEnd == sample.nLength))
{
ApplyAmplify(1.0, (sel.nStart == 0) ? 0.0 : 1.0, (sel.nEnd == sample.nLength) ? 0.0 : 1.0, sel.nStart == 0, sel.nEnd == sample.nLength, Fade::kLinear);
} else
{
// Can't apply quick fade as no appropriate selection has been made, so ask the user to amplify the whole sample instead.
OnAmplify();
}
}
void CCtrlSamples::OnResample()
{
ModSample &sample = m_sndFile.GetSample(m_nSample);
if(!sample.HasSampleData() || sample.uFlags[CHN_ADLIB])
return;
SAMPLEINDEX first = m_nSample, last = m_nSample;
if(CMainFrame::GetInputHandler()->ShiftPressed())
{
first = 1;
last = m_sndFile.GetNumSamples();
}
const uint32 oldRate = sample.GetSampleRate(m_sndFile.GetType());
CResamplingDlg dlg(this, oldRate, TrackerSettings::Instance().sampleEditorDefaultResampler, first != last);
if(dlg.DoModal() != IDOK)
return;
TrackerSettings::Instance().sampleEditorDefaultResampler = dlg.GetFilter();
for(SAMPLEINDEX smp = first; smp <= last; smp++)
{
const uint32 sampleFreq = m_sndFile.GetSample(smp).GetSampleRate(m_sndFile.GetType());
uint32 newFreq = dlg.GetFrequency();
if(dlg.GetResamplingOption() == CResamplingDlg::Upsample)
newFreq = sampleFreq * 2;
else if(dlg.GetResamplingOption() == CResamplingDlg::Downsample)
newFreq = sampleFreq / 2;
else if(newFreq == sampleFreq)
continue;
ApplyResample(smp, newFreq, dlg.GetFilter(), first != last, dlg.UpdatePatternCommands());
}
}
void CCtrlSamples::ApplyResample(SAMPLEINDEX smp, uint32 newRate, ResamplingMode mode, bool ignoreSelection, bool updatePatternCommands)
{
BeginWaitCursor();
ModSample &sample = m_sndFile.GetSample(smp);
if(!sample.HasSampleData() || sample.uFlags[CHN_ADLIB])
{
EndWaitCursor();
return;
}
SampleSelectionPoints selection = GetSelectionPoints();
LimitMax(selection.nEnd, sample.nLength);
if(selection.nStart >= selection.nEnd || ignoreSelection)
{
selection.nStart = 0;
selection.nEnd = sample.nLength;
}
const uint32 oldRate = sample.GetSampleRate(m_sndFile.GetType());
if(newRate < 1 || oldRate < 1)
{
MessageBeep(MB_ICONWARNING);
EndWaitCursor();
return;
}
const SmpLength oldLength = sample.nLength;
const SmpLength selLength = (selection.nEnd - selection.nStart);
const SmpLength newSelLength = Util::muldivr_unsigned(selLength, newRate, oldRate);
const SmpLength newSelEnd = selection.nStart + newSelLength;
const SmpLength newTotalLength = sample.nLength - selLength + newSelLength;
const uint8 numChannels = sample.GetNumChannels();
if(newTotalLength <= 1)
{
MessageBeep(MB_ICONWARNING);
EndWaitCursor();
return;
}
void *newSample = ModSample::AllocateSample(newTotalLength, sample.GetBytesPerSample());
if(newSample != nullptr)
{
// First, copy parts of the sample that are not affected by partial upsampling
const SmpLength bps = sample.GetBytesPerSample();
std::memcpy(newSample, sample.sampleb(), selection.nStart * bps);
std::memcpy(static_cast<char *>(newSample) + newSelEnd * bps, sample.sampleb() + selection.nEnd * bps, (sample.nLength - selection.nEnd) * bps);
if(mode == SRCMODE_DEFAULT)
{
// Resample using r8brain
const SmpLength bufferSize = std::min(std::max(selLength, SmpLength(oldRate)), SmpLength(1024 * 1024));
std::vector<double> convBuffer(bufferSize);
r8b::CDSPResampler16 resampler(oldRate, newRate, bufferSize);
for(uint8 chn = 0; chn < numChannels; chn++)
{
if(chn != 0) resampler.clear();
SmpLength readCount = selLength, writeCount = newSelLength;
SmpLength readOffset = selection.nStart * numChannels + chn, writeOffset = readOffset;
SmpLength outLatency = newRate;
double *outBuffer, lastVal = 0.0;
{
// Pre-fill the resampler with the first sampling point.
// Otherwise, it will assume that all samples before the first sampling point are 0,
// which can lead to unwanted artefacts (ripples) if the sample doesn't start with a zero crossing.
double firstVal = 0.0;
switch(sample.GetElementarySampleSize())
{
case 1:
firstVal = SC::Convert<double, int8>()(sample.sample8()[readOffset]);
lastVal = SC::Convert<double, int8>()(sample.sample8()[readOffset + selLength - numChannels]);
break;
case 2:
firstVal = SC::Convert<double, int16>()(sample.sample16()[readOffset]);
lastVal = SC::Convert<double, int16>()(sample.sample16()[readOffset + selLength - numChannels]);
break;
default:
// When higher bit depth is added, feel free to also replace CDSPResampler16 by CDSPResampler24 above.
MPT_ASSERT_MSG(false, "Bit depth not implemented");
}
// 10ms or less would probably be enough, but we will pre-fill the buffer with exactly "oldRate" samples
// to prevent any further rounding errors when using smaller buffers or when dividing oldRate or newRate.
uint32 remain = oldRate;
for(SmpLength i = 0; i < bufferSize; i++) convBuffer[i] = firstVal;
while(remain > 0)
{
uint32 procIn = std::min(remain, mpt::saturate_cast<uint32>(bufferSize));
SmpLength procCount = resampler.process(convBuffer.data(), procIn, outBuffer);
MPT_ASSERT(procCount <= outLatency);
LimitMax(procCount, outLatency);
outLatency -= procCount;
remain -= procIn;
}
}
// Now we can start with the actual resampling work...
while(writeCount > 0)
{
SmpLength smpCount = (SmpLength)convBuffer.size();
if(readCount != 0)
{
LimitMax(smpCount, readCount);
switch(sample.GetElementarySampleSize())
{
case 1:
CopySample<SC::ConversionChain<SC::Convert<double, int8>, SC::DecodeIdentity<int8> > >(convBuffer.data(), smpCount, 1, sample.sample8() + readOffset, sample.GetSampleSizeInBytes(), sample.GetNumChannels());
break;
case 2:
CopySample<SC::ConversionChain<SC::Convert<double, int16>, SC::DecodeIdentity<int16> > >(convBuffer.data(), smpCount, 1, sample.sample16() + readOffset, sample.GetSampleSizeInBytes(), sample.GetNumChannels());
break;
}
readOffset += smpCount * numChannels;
readCount -= smpCount;
} else
{
// Nothing to read, but still to write (compensate for r8brain's output latency)
for(SmpLength i = 0; i < smpCount; i++) convBuffer[i] = lastVal;
}
SmpLength procCount = resampler.process(convBuffer.data(), smpCount, outBuffer);
const SmpLength procLatency = std::min(outLatency, procCount);
procCount = std::min(procCount- procLatency, writeCount);
switch(sample.GetElementarySampleSize())
{
case 1:
CopySample<SC::ConversionChain<SC::Convert<int8, double>, SC::DecodeIdentity<double> > >(static_cast<int8 *>(newSample) + writeOffset, procCount, sample.GetNumChannels(), outBuffer + procLatency, procCount * sizeof(double), 1);
break;
case 2:
CopySample<SC::ConversionChain<SC::Convert<int16, double>, SC::DecodeIdentity<double> > >(static_cast<int16 *>(newSample) + writeOffset, procCount, sample.GetNumChannels(), outBuffer + procLatency, procCount * sizeof(double), 1);
break;
}
writeOffset += procCount * numChannels;
writeCount -= procCount;
outLatency -= procLatency;
}
}
} else
{
// Resample using built-in filters
uint32 functionNdx = MixFuncTable::ResamplingModeToMixFlags(mode);
if(sample.uFlags[CHN_16BIT]) functionNdx |= MixFuncTable::ndx16Bit;
if(sample.uFlags[CHN_STEREO]) functionNdx |= MixFuncTable::ndxStereo;
ModChannel chn{};
chn.pCurrentSample = sample.samplev();
chn.increment = SamplePosition::Ratio(oldRate, newRate);
chn.position.Set(selection.nStart);
chn.leftVol = chn.rightVol = (1 << 8);
chn.nLength = sample.nLength;
SmpLength writeCount = newSelLength;
SmpLength writeOffset = selection.nStart * sample.GetNumChannels();
while(writeCount > 0)
{
SmpLength procCount = std::min(static_cast<SmpLength>(MIXBUFFERSIZE), writeCount);
mixsample_t buffer[MIXBUFFERSIZE * 2];
MemsetZero(buffer);
MixFuncTable::Functions[functionNdx](chn, m_sndFile.m_Resampler, buffer, procCount);
for(uint8 c = 0; c < numChannels; c++)
{
switch(sample.GetElementarySampleSize())
{
case 1:
CopySample<SC::ConversionChain<SC::ConvertFixedPoint<int8, mixsample_t, 23>, SC::DecodeIdentity<mixsample_t> > >(static_cast<int8 *>(newSample) + writeOffset + c, procCount, sample.GetNumChannels(), buffer + c, sizeof(buffer), 2);
break;
case 2:
CopySample<SC::ConversionChain<SC::ConvertFixedPoint<int16, mixsample_t, 23>, SC::DecodeIdentity<mixsample_t> > >(static_cast<int16 *>(newSample) + writeOffset + c, procCount, sample.GetNumChannels(), buffer + c, sizeof(buffer), 2);
break;
}
}
writeCount -= procCount;
writeOffset += procCount * sample.GetNumChannels();
}
}
m_modDoc.GetSampleUndo().PrepareUndo(smp, sundo_replace, (newRate > oldRate) ? "Upsample" : "Downsample");
// Adjust loops and cues
const auto oldCues = sample.cues;
for(SmpLength &point : SampleEdit::GetCuesAndLoops(sample))
{
if(point >= oldLength)
point = newTotalLength;
else if(point >= selection.nEnd)
point += newSelLength - selLength;
else if(point > selection.nStart)
point = selection.nStart + Util::muldivr_unsigned(point - selection.nStart, newRate, oldRate);
LimitMax(point, newTotalLength);
}
if(updatePatternCommands)
{
bool patternUndoCreated = false;
m_sndFile.Patterns.ForEachModCommand([&](ModCommand &m)
{
if(m.command != CMD_OFFSET && m.command != CMD_REVERSEOFFSET && m.command != CMD_OFFSETPERCENTAGE)
return;
if(m_sndFile.GetSampleIndex(m.note, m.instr) != smp)
return;
SmpLength point = m.param * 256u;
if(m.command == CMD_OFFSETPERCENTAGE || (m.volcmd == VOLCMD_OFFSET && m.vol == 0))
point = Util::muldivr_unsigned(point, oldLength, 65536);
else if(m.volcmd == VOLCMD_OFFSET && m.vol <= std::size(oldCues))
point += oldCues[m.vol - 1];
if(point >= oldLength)
point = newTotalLength;
else if (point >= selection.nEnd)
point += newSelLength - selLength;
else if (point > selection.nStart)
point = selection.nStart + Util::muldivr_unsigned(point - selection.nStart, newRate, oldRate);
LimitMax(point, newTotalLength);
if(m.command == CMD_OFFSETPERCENTAGE || (m.volcmd == VOLCMD_OFFSET && m.vol == 0))
point = Util::muldivr_unsigned(point, 65536, newTotalLength);
else if(m.volcmd == VOLCMD_OFFSET && m.vol <= std::size(sample.cues))
point -= sample.cues[m.vol - 1];
if(!patternUndoCreated)
{
patternUndoCreated = true;
m_modDoc.PrepareUndoForAllPatterns(false, "Resample (Adjust Offsets)");
}
m.param = mpt::saturate_cast<ModCommand::PARAM>(point / 256u);
});
}
if(!selection.selectionActive)
{
if(m_sndFile.GetType() != MOD_TYPE_MOD)
{
sample.nC5Speed = newRate;
sample.FrequencyToTranspose();
}
}
ctrlSmp::ReplaceSample(sample, newSample, newTotalLength, m_sndFile);
// Update loop wrap-around buffer
sample.PrecomputeLoops(m_sndFile);
auto updateHint = SampleHint(smp).Info().Data();
if(sample.uFlags[SMP_KEEPONDISK] && !sample.uFlags[SMP_MODIFIED])
updateHint.Names();
sample.uFlags.set(SMP_MODIFIED);
m_modDoc.SetModified();
m_modDoc.UpdateAllViews(nullptr, updateHint, nullptr);
if(selection.selectionActive && !ignoreSelection)
{
SetSelectionPoints(selection.nStart, newSelEnd);
}
}
EndWaitCursor();
SwitchToView();
}
void CCtrlSamples::ReadTimeStretchParameters()
{
m_nSequenceMs = GetDlgItemInt(IDC_EDIT10);
m_nSeekWindowMs = GetDlgItemInt(IDC_EDIT11);
m_nOverlapMs = GetDlgItemInt(IDC_EDIT12);
}
void CCtrlSamples::UpdateTimeStretchParameters()
{
GetDlgItem(IDC_EDIT10)->SetWindowText(((m_nSequenceMs <= 0) ? _T("auto") : MPT_TFORMAT("{}ms")(m_nSequenceMs)).c_str());
GetDlgItem(IDC_EDIT11)->SetWindowText(((m_nSeekWindowMs <= 0) ? _T("auto") : MPT_TFORMAT("{}ms")(m_nSeekWindowMs)).c_str());
GetDlgItem(IDC_EDIT12)->SetWindowText(((m_nOverlapMs <= 0) ? _T("auto") : MPT_TFORMAT("{}ms")(m_nOverlapMs)).c_str());
}
void CCtrlSamples::OnEnableStretchToSize()
{
// Enable time-stretching / disable unused pitch-shifting UI elements
bool timeStretch = IsDlgButtonChecked(IDC_CHECK3) != BST_UNCHECKED;
if(!timeStretch) ReadTimeStretchParameters();
((CComboBox *)GetDlgItem(IDC_COMBO4))->EnableWindow(timeStretch ? FALSE : TRUE);
((CEdit *)GetDlgItem(IDC_EDIT6))->EnableWindow(timeStretch ? TRUE : FALSE);
((CButton *)GetDlgItem(IDC_BUTTON2))->EnableWindow(timeStretch ? TRUE : FALSE);
GetDlgItem(IDC_TEXT_PITCH)->SetWindowText(timeStretch ? _T("Sequence") : _T("Pitch"));
GetDlgItem(IDC_TEXT_QUALITY)->SetWindowText(timeStretch ? _T("Seek Window") : _T("Quality"));
GetDlgItem(IDC_TEXT_FFT)->SetWindowText(timeStretch ? _T("Overlap") : _T("FFT Size"));
GetDlgItem(IDC_EDIT10)->ShowWindow(timeStretch ? SW_SHOW : SW_HIDE);
GetDlgItem(IDC_EDIT11)->ShowWindow(timeStretch ? SW_SHOW : SW_HIDE);
GetDlgItem(IDC_EDIT12)->ShowWindow(timeStretch ? SW_SHOW : SW_HIDE);
GetDlgItem(IDC_SPIN10)->ShowWindow(timeStretch ? SW_SHOW : SW_HIDE);
GetDlgItem(IDC_SPIN14)->ShowWindow(timeStretch ? SW_SHOW : SW_HIDE);
GetDlgItem(IDC_SPIN15)->ShowWindow(timeStretch ? SW_SHOW : SW_HIDE);
GetDlgItem(IDC_COMBO4)->ShowWindow(timeStretch ? SW_HIDE : SW_SHOW);
GetDlgItem(IDC_COMBO5)->ShowWindow(timeStretch ? SW_HIDE : SW_SHOW);
GetDlgItem(IDC_COMBO6)->ShowWindow(timeStretch ? SW_HIDE : SW_SHOW);
SetDlgItemText(IDC_BUTTON1, timeStretch ? _T("Time Stretch") : _T("Pitch Shift"));
if(timeStretch)
UpdateTimeStretchParameters();
}
void CCtrlSamples::OnEstimateSampleSize()
{
if(!m_sndFile.GetSample(m_nSample).HasSampleData())
return;
//Ensure m_dTimeStretchRatio is up-to-date with textbox content
UpdateData(TRUE);
//Open dialog
CPSRatioCalc dlg(m_sndFile, m_nSample, m_dTimeStretchRatio, this);
if (dlg.DoModal() != IDOK) return;
//Update ratio value&textbox
m_dTimeStretchRatio = dlg.m_dRatio;
UpdateData(FALSE);
}
enum TimeStretchPitchShiftResult
{
kUnknown,
kOK,
kAbort,
kInvalidRatio,
kStretchTooShort,
kStretchTooLong,
kOutOfMemory,
kSampleTooShort,
kStretchInvalidSampleRate,
};
class DoPitchShiftTimeStretch : public CProgressDialog
{
public:
CCtrlSamples &m_parent;
CModDoc &m_modDoc;
const float m_ratio;
TimeStretchPitchShiftResult m_result = kUnknown;
uint32 m_updateInterval;
const SAMPLEINDEX m_sample;
const bool m_pitchShift;
DoPitchShiftTimeStretch(CCtrlSamples &parent, CModDoc &modDoc, SAMPLEINDEX sample, float ratio, bool pitchShift)
: CProgressDialog(&parent)
, m_parent(parent)
, m_modDoc(modDoc)
, m_ratio(ratio)
, m_sample(sample)
, m_pitchShift(pitchShift)
{
m_updateInterval = TrackerSettings::Instance().GUIUpdateInterval;
if(m_updateInterval < 15) m_updateInterval = 15;
}
void Run() override
{
SetTitle(m_pitchShift ? _T("Pitch Shift") : _T("Time Stretch"));
SetRange(0, 100);
if(m_pitchShift)
m_result = PitchShift();
else
m_result = TimeStretch();
EndDialog((m_result == kOK) ? IDOK : IDCANCEL);
}
TimeStretchPitchShiftResult TimeStretch()
{
ModSample &sample = m_modDoc.GetSoundFile().GetSample(m_sample);
const uint32 sampleRate = sample.GetSampleRate(m_modDoc.GetModType());
if(!sample.HasSampleData()) return kAbort;
if(m_ratio == 1.0) return kAbort;
if(m_ratio < 0.5f) return kStretchTooShort;
if(m_ratio > 2.0f) return kStretchTooLong;
if(sampleRate > 192000) return kStretchInvalidSampleRate;
HANDLE handleSt = soundtouch_createInstance();
if(handleSt == NULL)
{
mpt::throw_out_of_memory();
}
const uint8 smpSize = sample.GetElementarySampleSize();
const uint8 numChannels = sample.GetNumChannels();
// Initialize soundtouch object.
soundtouch_setSampleRate(handleSt, sampleRate);
soundtouch_setChannels(handleSt, numChannels);
// Given ratio is time stretch ratio, and must be converted to
// tempo change ratio: for example time stretch ratio 2 means
// tempo change ratio 0.5.
soundtouch_setTempoChange(handleSt, (1.0f / m_ratio - 1.0f) * 100.0f);
// Read settings from GUI.
m_parent.ReadTimeStretchParameters();
// Set settings to soundtouch. Zero value means 'use default', and
// setting value is read back after setting because not all settings are accepted.
soundtouch_setSetting(handleSt, SETTING_SEQUENCE_MS, m_parent.m_nSequenceMs);
m_parent.m_nSequenceMs = soundtouch_getSetting(handleSt, SETTING_SEQUENCE_MS);
soundtouch_setSetting(handleSt, SETTING_SEEKWINDOW_MS, m_parent.m_nSeekWindowMs);
m_parent.m_nSeekWindowMs = soundtouch_getSetting(handleSt, SETTING_SEEKWINDOW_MS);
soundtouch_setSetting(handleSt, SETTING_OVERLAP_MS, m_parent.m_nOverlapMs);
m_parent.m_nOverlapMs = soundtouch_getSetting(handleSt, SETTING_OVERLAP_MS);
// Update GUI with the actual SoundTouch parameters in effect.
m_parent.UpdateTimeStretchParameters();
const SmpLength inBatchSize = soundtouch_getSetting(handleSt, SETTING_NOMINAL_INPUT_SEQUENCE) + 1; // approximate value, add 1 to play safe
const SmpLength outBatchSize = soundtouch_getSetting(handleSt, SETTING_NOMINAL_OUTPUT_SEQUENCE) + 1; // approximate value, add 1 to play safe
const auto selection = m_parent.GetSelectionPoints();
const SmpLength selLength = selection.selectionActive ? selection.nEnd - selection.nStart : sample.nLength;
const SmpLength remainLength = sample.nLength - selLength;
if(selLength < inBatchSize)
{
soundtouch_destroyInstance(handleSt);
return kSampleTooShort;
}
if(static_cast<SmpLength>(std::ceil(static_cast<double>(m_ratio) * selLength)) < outBatchSize)
{
soundtouch_destroyInstance(handleSt);
return kSampleTooShort;
}
const SmpLength stretchLength = mpt::saturate_round<SmpLength>(m_ratio * selLength);
const SmpLength stretchEnd = selection.nStart + stretchLength;
const SmpLength newSampleLength = remainLength + stretchLength;
void *pNewSample = nullptr;
if(newSampleLength <= MAX_SAMPLE_LENGTH)
{
pNewSample = ModSample::AllocateSample(newSampleLength, sample.GetBytesPerSample());
}
if(pNewSample == nullptr)
{
soundtouch_destroyInstance(handleSt);
return kOutOfMemory;
}
// Show wait mouse cursor
BeginWaitCursor();
memcpy(pNewSample, sample.sampleb(), selection.nStart * sample.GetBytesPerSample());
memcpy(static_cast<std::byte *>(pNewSample) + stretchEnd * sample.GetBytesPerSample(), sample.sampleb() + selection.nEnd * sample.GetBytesPerSample(), (sample.nLength - selection.nEnd) * sample.GetBytesPerSample());
constexpr SmpLength MaxInputChunkSize = 1024;
std::vector<float> buffer(MaxInputChunkSize * numChannels);
SmpLength inPos = selection.nStart;
SmpLength outPos = selection.nStart; // Keeps count of the sample length received from stretching process.
DWORD timeLast = 0;
// Process sample in steps.
while(inPos < selection.nEnd)
{
// Current chunk size limit test
const SmpLength inChunkSize = std::min(MaxInputChunkSize, sample.nLength - inPos);
DWORD timeNow = timeGetTime();
if(timeNow - timeLast >= m_updateInterval)
{
// Show progress bar using process button painting & text label
TCHAR progress[32];
uint32 percent = static_cast<uint32>(100 * (inPos + inChunkSize) / sample.nLength);
wsprintf(progress, _T("Time Stretch... %u%%"), percent);
SetText(progress);
SetProgress(percent);
ProcessMessages();
if(m_abort)
break;
timeLast = timeNow;
}
// Send sampledata for processing.
switch(smpSize)
{
case 1:
CopyAudioChannelsInterleaved(buffer.data(), sample.sample8() + inPos * numChannels, numChannels, inChunkSize);
break;
case 2:
CopyAudioChannelsInterleaved(buffer.data(), sample.sample16() + inPos * numChannels, numChannels, inChunkSize);
break;
}
soundtouch_putSamples(handleSt, buffer.data(), inChunkSize);
// Receive some processed samples (it's not guaranteed that there is any available).
{
SmpLength outChunkSize = std::min(static_cast<SmpLength>(soundtouch_numSamples(handleSt)), stretchLength - outPos);
if(outChunkSize > 0)
{
buffer.resize(outChunkSize * numChannels);
soundtouch_receiveSamples(handleSt, buffer.data(), outChunkSize);
switch(smpSize)
{
case 1:
CopyAudioChannelsInterleaved(static_cast<int8 *>(pNewSample) + numChannels * outPos, buffer.data(), numChannels, outChunkSize);
break;
case 2:
CopyAudioChannelsInterleaved(static_cast<int16 *>(pNewSample) + numChannels * outPos, buffer.data(), numChannels, outChunkSize);
break;
}
outPos += outChunkSize;
}
}
// Next buffer chunk
inPos += inChunkSize;
}
if(!m_abort)
{
// The input sample should now be processed. Receive remaining samples.
soundtouch_flush(handleSt);
SmpLength outChunkSize = std::min(static_cast<SmpLength>(soundtouch_numSamples(handleSt)), stretchLength - (outPos - selection.nStart));
if(outChunkSize > 0)
{
buffer.resize(outChunkSize * numChannels);
soundtouch_receiveSamples(handleSt, buffer.data(), outChunkSize);
switch(smpSize)
{
case 1:
CopyAudioChannelsInterleaved(static_cast<int8 *>(pNewSample) + numChannels * outPos, buffer.data(), numChannels, outChunkSize);
break;
case 2:
CopyAudioChannelsInterleaved(static_cast<int16 *>(pNewSample) + numChannels * outPos, buffer.data(), numChannels, outChunkSize);
break;
}
outPos += outChunkSize;
}
soundtouch_clear(handleSt);
MPT_ASSERT(soundtouch_isEmpty(handleSt) != 0);
CSoundFile &sndFile = m_modDoc.GetSoundFile();
m_parent.PrepareUndo("Time Stretch", sundo_replace);
// Swap sample buffer pointer to new buffer, update song + sample data & free old sample buffer
ctrlSmp::ReplaceSample(sample, pNewSample, std::min(outPos + remainLength, newSampleLength), sndFile);
// Update loops and wrap-around buffer
sample.SetLoop(
mpt::saturate_round<SmpLength>(sample.nLoopStart * m_ratio),
mpt::saturate_round<SmpLength>(sample.nLoopEnd * m_ratio),
sample.uFlags[CHN_LOOP],
sample.uFlags[CHN_PINGPONGLOOP],
sndFile);
sample.SetSustainLoop(
mpt::saturate_round<SmpLength>(sample.nSustainStart * m_ratio),
mpt::saturate_round<SmpLength>(sample.nSustainEnd * m_ratio),
sample.uFlags[CHN_SUSTAINLOOP],
sample.uFlags[CHN_PINGPONGSUSTAIN],
sndFile);
} else
{
ModSample::FreeSample(pNewSample);
}
soundtouch_destroyInstance(handleSt);
// Restore mouse cursor
EndWaitCursor();
if(selection.selectionActive)
m_parent.SetSelectionPoints(selection.nStart, selection.nStart + stretchLength);
return m_abort ? kAbort : kOK;
}
TimeStretchPitchShiftResult PitchShift()
{
static constexpr SmpLength MAX_BUFFER_LENGTH = 8192;
ModSample &sample = m_modDoc.GetSoundFile().GetSample(m_sample);
if(!sample.HasSampleData() || m_ratio < 0.5f || m_ratio > 2.0f)
{
return kAbort;
}
// Get selected oversampling - quality - (also refered as FFT overlapping) factor
CComboBox *combo = (CComboBox *)m_parent.GetDlgItem(IDC_COMBO5);
long ovs = combo->GetCurSel() + 4;
// Get selected FFT size (power of 2; should not exceed MAX_BUFFER_LENGTH - see smbPitchShift.h)
combo = (CComboBox *)m_parent.GetDlgItem(IDC_COMBO6);
UINT fft = 1 << (combo->GetCurSel() + 8);
while(fft > MAX_BUFFER_LENGTH) fft >>= 1;
// Show wait mouse cursor
BeginWaitCursor();
// Get original sample rate
const float sampleRate = static_cast<float>(sample.GetSampleRate(m_modDoc.GetModType()));
// Allocate working buffer
const size_t bufferSize = MAX_BUFFER_LENGTH + fft;
std::vector<float> buffer;
try
{
buffer.resize(bufferSize);
} catch(mpt::out_of_memory e)
{
mpt::delete_out_of_memory(e);
return kOutOfMemory;
}
const auto smpSize = sample.GetElementarySampleSize();
const auto numChans = sample.GetNumChannels();
const auto bps = sample.GetBytesPerSample();
int8 *pNewSample = static_cast<int8 *>(ModSample::AllocateSample(sample.nLength, bps));
if(pNewSample == nullptr)
return kOutOfMemory;
DWORD timeLast = 0;
const auto selection = m_parent.GetSelectionPoints();
// Process each channel separately
for(uint8 chn = 0; chn < numChans; chn++)
{
// Process sample buffer using MAX_BUFFER_LENGTH (max) sized chunk steps (in order to allow
// the processing of BIG samples...)
for(SmpLength pos = selection.nStart; pos < selection.nEnd;)
{
DWORD timeNow = timeGetTime();
if(timeNow - timeLast >= m_updateInterval)
{
TCHAR progress[32];
uint32 percent = static_cast<uint32>(chn * 50.0 + (100.0 / numChans) * (pos - selection.nStart) / (selection.nEnd - selection.nStart));
wsprintf(progress, _T("Pitch Shift... %u%%"), percent);
SetText(progress);
SetProgress(percent);
ProcessMessages();
if(m_abort)
break;
timeLast = timeNow;
}
// TRICK : output buffer offset management
// as the pitch-shifter adds some blank signal in head of output buffer (matching FFT
// length - in short it needs a certain amount of data before being able to output some
// meaningful processed samples) , in order to avoid this behaviour , we will ignore
// the first FFT_length samples and process the same amount of extra blank samples
// (all 0.0f) at the end of the buffer (those extra samples will benefit from internal
// FFT data computed during the previous steps resulting in a correct and consistent
// signal output).
const SmpLength processLen = (pos + MAX_BUFFER_LENGTH <= selection.nEnd) ? MAX_BUFFER_LENGTH : (selection.nEnd - pos);
const bool bufStart = (pos == selection.nStart);
const bool bufEnd = (pos + processLen >= selection.nEnd);
const SmpLength startOffset = (bufStart ? fft : 0);
const SmpLength innerOffset = (bufStart ? 0 : fft);
const SmpLength finalOffset = (bufEnd ? fft : 0);
// Re-initialize pitch-shifter with blank FFT before processing 1st chunk of current channel
if(bufStart)
{
std::fill(buffer.begin(), buffer.begin() + fft, 0.0f);
smbPitchShift(m_ratio, fft, fft, ovs, sampleRate, buffer.data(), buffer.data());
}
// Convert current channel's data chunk to float
SmpLength offset = pos * numChans + chn;
switch(smpSize)
{
case 1:
CopySample<SC::ConversionChain<SC::Convert<float, int8>, SC::DecodeIdentity<int8>>>(buffer.data(), processLen, 1, sample.sample8() + offset, sizeof(int8) * processLen * numChans, numChans);
break;
case 2:
CopySample<SC::ConversionChain<SC::Convert<float, int16>, SC::DecodeIdentity<int16>>>(buffer.data(), processLen, 1, sample.sample16() + offset, sizeof(int16) * processLen * numChans, numChans);
break;
}
// Fills extra blank samples (read TRICK description comment above)
if(bufEnd)
std::fill(buffer.begin() + processLen, buffer.begin() + processLen + finalOffset, 0.0f);
// Apply pitch shifting
smbPitchShift(m_ratio, static_cast<long>(processLen + finalOffset), fft, ovs, sampleRate, buffer.data(), buffer.data());
// Restore pitched-shifted float sample into original sample buffer
void *ptr = pNewSample + (pos - innerOffset) * smpSize * numChans + chn * smpSize;
const SmpLength copyLength = processLen + finalOffset - startOffset + 1;
switch(smpSize)
{
case 1:
CopySample<SC::ConversionChain<SC::Convert<int8, float>, SC::DecodeIdentity<float>>>(static_cast<int8 *>(ptr), copyLength, numChans, buffer.data() + startOffset, sizeof(float) * bufferSize, 1);
break;
case 2:
CopySample<SC::ConversionChain<SC::Convert<int16, float>, SC::DecodeIdentity<float>>>(static_cast<int16 *>(ptr), copyLength, numChans, buffer.data() + startOffset, sizeof(float) * bufferSize, 1);
break;
}
// Next buffer chunk
pos += processLen;
}
}
if(!m_abort)
{
m_parent.PrepareUndo("Pitch Shift", sundo_replace);
memcpy(pNewSample, sample.sampleb(), selection.nStart * bps);
memcpy(pNewSample + selection.nEnd * bps, sample.sampleb() + selection.nEnd * bps, (sample.nLength - selection.nEnd) * bps);
ctrlSmp::ReplaceSample(sample, pNewSample, sample.nLength, m_modDoc.GetSoundFile());
} else
{
ModSample::FreeSample(pNewSample);
}
// Restore mouse cursor
EndWaitCursor();
return m_abort ? kAbort : kOK;
}
};
void CCtrlSamples::OnPitchShiftTimeStretch()
{
TimeStretchPitchShiftResult errorcode = kAbort;
ModSample &sample = m_sndFile.GetSample(m_nSample);
if(!sample.HasSampleData()) goto error;
if(IsDlgButtonChecked(IDC_CHECK3))
{
// Time stretching
UpdateData(TRUE); //Ensure m_dTimeStretchRatio is up-to-date with textbox content
DoPitchShiftTimeStretch timeStretch(*this, m_modDoc, m_nSample, static_cast<float>(m_dTimeStretchRatio / 100.0), false);
timeStretch.DoModal();
errorcode = timeStretch.m_result;
} else
{
// Pitch shifting
// Get selected pitch modifier [-12,+12]
CString text;
static_cast<CComboBox *>(GetDlgItem(IDC_COMBO4))->GetWindowText(text);
float pm = ConvertStrTo<float>(text);
if(pm == 0.0f) goto error;
// Compute pitch ratio in range [0.5f ; 2.0f] (1.0f means output == input)
// * pitch up -> 1.0f + n / 12.0f -> (12.0f + n) / 12.0f , considering n : pitch modifier > 0
// * pitch dn -> 1.0f - n / 24.0f -> (24.0f - n) / 24.0f , considering n : pitch modifier > 0
float pitch = pm < 0 ? ((24.0f + pm) / 24.0f) : ((12.0f + pm) / 12.0f);
// Apply pitch modifier
DoPitchShiftTimeStretch pitchShift(*this, m_modDoc, m_nSample, pitch, true);
pitchShift.DoModal();
errorcode = pitchShift.m_result;
}
if(errorcode == kOK)
{
// Update sample view
SetModified(SampleHint().Info().Data(), true, true);
return;
}
// Error management
error:
if(errorcode != kAbort)
{
CString str;
switch(errorcode)
{
case kInvalidRatio:
str = _T("Invalid stretch ratio!");
break;
case kStretchTooShort:
case kStretchTooLong:
str = MPT_CFORMAT("Stretch ratio is too {}. Must be between 50% and 200%.")((errorcode == kStretchTooShort) ? CString(_T("low")) : CString(_T("high")));
break;
case kOutOfMemory:
str = _T("Out of memory.");
break;
case kSampleTooShort:
str = _T("Sample too short.");
break;
case kStretchInvalidSampleRate:
str = _T("Sample rate must be 192,000 Hz or lower.");
break;
default:
str = _T("Unknown Error.");
break;
}
Reporting::Error(str);
}
}
void CCtrlSamples::OnReverse()
{
ModSample &sample = m_sndFile.GetSample(m_nSample);
SampleSelectionPoints selection = GetSelectionPoints();
PrepareUndo("Reverse", sundo_reverse, selection.nStart, selection.nEnd);
if(SampleEdit::ReverseSample(sample, selection.nStart, selection.nEnd, m_sndFile))
{
SetModified(SampleHint().Data(), false, true);
} else
{
m_modDoc.GetSampleUndo().RemoveLastUndoStep(m_nSample);
}
EndWaitCursor();
SwitchToView();
}
void CCtrlSamples::OnInvert()
{
ModSample &sample = m_sndFile.GetSample(m_nSample);
SampleSelectionPoints selection = GetSelectionPoints();
PrepareUndo("Invert", sundo_invert, selection.nStart, selection.nEnd);
if(SampleEdit::InvertSample(sample, selection.nStart, selection.nEnd, m_sndFile) == true)
{
SetModified(SampleHint().Data(), false, true);
} else
{
m_modDoc.GetSampleUndo().RemoveLastUndoStep(m_nSample);
}
EndWaitCursor();
SwitchToView();
}
void CCtrlSamples::OnSignUnSign()
{
if(!m_sndFile.GetSample(m_nSample).HasSampleData()) return;
if(m_modDoc.IsNotePlaying(0, m_nSample, 0))
MsgBoxHidable(ConfirmSignUnsignWhenPlaying);
BeginWaitCursor();
ModSample &sample = m_sndFile.GetSample(m_nSample);
SampleSelectionPoints selection = GetSelectionPoints();
PrepareUndo("Unsign", sundo_unsign, selection.nStart, selection.nEnd);
if(SampleEdit::UnsignSample(sample, selection.nStart, selection.nEnd, m_sndFile) == true)
{
SetModified(SampleHint().Data(), false, true);
} else
{
m_modDoc.GetSampleUndo().RemoveLastUndoStep(m_nSample);
}
EndWaitCursor();
SwitchToView();
}
void CCtrlSamples::OnSilence()
{
if(!m_sndFile.GetSample(m_nSample).HasSampleData()) return;
BeginWaitCursor();
SampleSelectionPoints selection = GetSelectionPoints();
// never apply silence to a sample that has no selection
const SmpLength len = selection.nEnd - selection.nStart;
if(selection.selectionActive && len > 1)
{
ModSample &sample = m_sndFile.GetSample(m_nSample);
PrepareUndo("Silence", sundo_update, selection.nStart, selection.nEnd);
if(SampleEdit::SilenceSample(sample, selection.nStart, selection.nEnd, m_sndFile))
{
SetModified(SampleHint().Data(), false, true);
}
}
EndWaitCursor();
SwitchToView();
}
void CCtrlSamples::OnPrevInstrument()
{
if (m_nSample > 1)
SetCurrentSample(m_nSample - 1);
else
SetCurrentSample(m_sndFile.GetNumSamples());
}
void CCtrlSamples::OnNextInstrument()
{
if (m_nSample < m_sndFile.GetNumSamples())
SetCurrentSample(m_nSample + 1);
else
SetCurrentSample(1);
}
void CCtrlSamples::OnNameChanged()
{
if(IsLocked() || !m_nSample) return;
CString tmp;
m_EditName.GetWindowText(tmp);
const std::string s = mpt::ToCharset(m_sndFile.GetCharsetInternal(), tmp);
if(s != m_sndFile.m_szNames[m_nSample])
{
if(!m_startedEdit)
{
PrepareUndo("Set Name");
m_editInstrumentName = GetParentInstrumentWithSameName();
if(m_editInstrumentName != INSTRUMENTINDEX_INVALID)
m_modDoc.GetInstrumentUndo().PrepareUndo(m_editInstrumentName, "Set Name");
}
if(m_editInstrumentName <= m_sndFile.GetNumInstruments())
{
if(auto instr = m_sndFile.Instruments[m_editInstrumentName]; instr != nullptr)
{
instr->name = s;
m_modDoc.UpdateAllViews(nullptr, InstrumentHint(m_editInstrumentName).Names(), this);
}
}
m_sndFile.m_szNames[m_nSample] = s;
SetModified(SampleHint().Names(), false, false);
}
}
void CCtrlSamples::OnFileNameChanged()
{
if(IsLocked()) return;
CString tmp;
m_EditFileName.GetWindowText(tmp);
const std::string s = mpt::ToCharset(m_sndFile.GetCharsetInternal(), tmp);
if(s != m_sndFile.GetSample(m_nSample).filename)
{
if(!m_startedEdit) PrepareUndo("Set Filename");
m_sndFile.GetSample(m_nSample).filename = s;
SetModified(SampleHint().Info(), false, false);
}
}
void CCtrlSamples::OnVolumeChanged()
{
if (IsLocked()) return;
int nVol = GetDlgItemInt(IDC_EDIT7);
Limit(nVol, 0, 64);
nVol *= 4;
ModSample &sample = m_sndFile.GetSample(m_nSample);
if (nVol != sample.nVolume)
{
if(!m_startedEdit) PrepareUndo("Set Default Volume");
sample.nVolume = static_cast<uint16>(nVol);
sample.uFlags.reset(SMP_NODEFAULTVOLUME);
SetModified(SampleHint().Info(), false, false);
}
}
void CCtrlSamples::OnGlobalVolChanged()
{
if (IsLocked()) return;
int nVol = GetDlgItemInt(IDC_EDIT8);
Limit(nVol, 0, 64);
ModSample &sample = m_sndFile.GetSample(m_nSample);
if (nVol != sample.nGlobalVol)
{
if(!m_startedEdit) PrepareUndo("Set Global Volume");
sample.nGlobalVol = static_cast<uint16>(nVol);
// Live-adjust volume
for(auto &chn : m_sndFile.m_PlayState.Chn)
{
if(chn.pModSample == &sample)
{
chn.UpdateInstrumentVolume(chn.pModSample, chn.pModInstrument);
}
}
SetModified(SampleHint().Info(), false, false);
}
}
void CCtrlSamples::OnSetPanningChanged()
{
if (IsLocked()) return;
bool b = false;
if (m_sndFile.GetType() & (MOD_TYPE_IT|MOD_TYPE_MPT))
{
b = IsDlgButtonChecked(IDC_CHECK1) != FALSE;
}
ModSample &sample = m_sndFile.GetSample(m_nSample);
if(b != sample.uFlags[CHN_PANNING])
{
PrepareUndo("Toggle Panning");
sample.uFlags.set(CHN_PANNING, b);
SetModified(SampleHint().Info(), false, false);
}
}
void CCtrlSamples::OnPanningChanged()
{
if (IsLocked()) return;
int nPan = GetDlgItemInt(IDC_EDIT9);
if (nPan < 0) nPan = 0;
if (m_sndFile.GetType() == MOD_TYPE_XM)
{
if (nPan > 255) nPan = 255; // displayed panning will be 0-255 with XM
} else
{
if (nPan > 64) nPan = 64; // displayed panning will be 0-64 with anything but XM.
nPan = nPan * 4; // so we x4 to get MPT's internal 0-256 range.
}
ModSample &sample = m_sndFile.GetSample(m_nSample);
if (nPan != sample.nPan)
{
if(!m_startedEdit) PrepareUndo("Set Panning");
sample.nPan = static_cast<uint16>(nPan);
SetModified(SampleHint().Info(), false, false);
}
}
void CCtrlSamples::OnFineTuneChanged()
{
if (IsLocked()) return;
int n = GetDlgItemInt(IDC_EDIT5);
if(!m_startedEdit)
PrepareUndo("Finetune");
ModSample &sample = m_sndFile.GetSample(m_nSample);
if (!m_sndFile.UseFinetuneAndTranspose())
{
if ((n > 0) && (n <= (m_sndFile.GetType() == MOD_TYPE_S3M ? 65535 : 9999999)) && (n != (int)m_sndFile.GetSample(m_nSample).nC5Speed))
{
sample.nC5Speed = n;
int transp = ModSample::FrequencyToTranspose(n).first;
int basenote = (NOTE_MIDDLEC - NOTE_MIN) + transp;
Clamp(basenote, BASENOTE_MIN, BASENOTE_MAX);
basenote -= BASENOTE_MIN;
if (basenote != m_CbnBaseNote.GetCurSel())
{
LockControls();
m_CbnBaseNote.SetCurSel(basenote);
UnlockControls();
}
SetModified(SampleHint().Info(), false, false);
}
} else
{
if(m_sndFile.GetType() & MOD_TYPE_MOD)
n = MOD2XMFineTune(n);
if((n >= -128) && (n <= 127))
{
sample.nFineTune = static_cast<int8>(n);
SetModified(SampleHint().Info(), false, false);
}
}
}
void CCtrlSamples::OnFineTuneChangedDone()
{
// Update all playing channels
ModSample &sample = m_sndFile.GetSample(m_nSample);
for(auto &chn : m_sndFile.m_PlayState.Chn)
{
if(chn.pModSample == &sample)
{
chn.nTranspose = sample.RelativeTone;
chn.nFineTune = sample.nFineTune;
if(chn.nC5Speed != 0 && sample.nC5Speed != 0)
{
if(m_sndFile.PeriodsAreFrequencies())
chn.nPeriod = Util::muldivr(chn.nPeriod, sample.nC5Speed, chn.nC5Speed);
else if(!m_sndFile.m_SongFlags[SONG_LINEARSLIDES])
chn.nPeriod = Util::muldivr(chn.nPeriod, chn.nC5Speed, sample.nC5Speed);
}
chn.nC5Speed = sample.nC5Speed;
}
}
}
void CCtrlSamples::OnBaseNoteChanged()
{
if (IsLocked()) return;
int n = static_cast<int>(m_CbnBaseNote.GetItemData(m_CbnBaseNote.GetCurSel()));
ModSample &sample = m_sndFile.GetSample(m_nSample);
PrepareUndo("Transpose");
if(!m_sndFile.UseFinetuneAndTranspose())
{
const int oldTransp = ModSample::FrequencyToTranspose(sample.nC5Speed).first;
const uint32 newFreq = mpt::saturate_round<uint32>(sample.nC5Speed * std::pow(2.0, (n - oldTransp) / 12.0));
if (newFreq > 0 && newFreq <= (m_sndFile.GetType() == MOD_TYPE_S3M ? 65535u : 9999999u) && newFreq != sample.nC5Speed)
{
sample.nC5Speed = newFreq;
LockControls();
SetDlgItemInt(IDC_EDIT5, newFreq, FALSE);
// Due to rounding imprecisions if the base note is below 0, we recalculate it here to make sure that the value stays consistent.
int basenote = (NOTE_MIDDLEC - NOTE_MIN) + ModSample::FrequencyToTranspose(newFreq).first;
Limit(basenote, BASENOTE_MIN, BASENOTE_MAX);
basenote -= BASENOTE_MIN;
if(basenote != m_CbnBaseNote.GetCurSel())
m_CbnBaseNote.SetCurSel(basenote);
OnFineTuneChangedDone();
UnlockControls();
SetModified(SampleHint().Info(), false, false);
}
} else
{
if ((n >= -128) && (n < 128))
{
sample.RelativeTone = (int8)n;
OnFineTuneChangedDone();
SetModified(SampleHint().Info(), false, false);
}
}
}
void CCtrlSamples::OnVibTypeChanged()
{
if (IsLocked()) return;
int n = m_ComboAutoVib.GetCurSel();
if (n >= 0)
{
PrepareUndo("Set Vibrato Type");
m_sndFile.GetSample(m_nSample).nVibType = static_cast<VibratoType>(m_ComboAutoVib.GetItemData(n));
PropagateAutoVibratoChanges();
SetModified(SampleHint().Info(), false, false);
}
}
void CCtrlSamples::OnVibDepthChanged()
{
if (IsLocked()) return;
int lmin = 0, lmax = 0;
m_SpinVibDepth.GetRange(lmin, lmax);
int n = GetDlgItemInt(IDC_EDIT15);
if ((n >= lmin) && (n <= lmax))
{
if(!m_startedEdit) PrepareUndo("Set Vibrato Depth");
m_sndFile.GetSample(m_nSample).nVibDepth = static_cast<uint8>(n);
PropagateAutoVibratoChanges();
SetModified(SampleHint().Info(), false, false);
}
}
void CCtrlSamples::OnVibSweepChanged()
{
if (IsLocked()) return;
int lmin = 0, lmax = 0;
m_SpinVibSweep.GetRange(lmin, lmax);
int n = GetDlgItemInt(IDC_EDIT14);
if ((n >= lmin) && (n <= lmax))
{
if(!m_startedEdit) PrepareUndo("Set Vibrato Sweep");
m_sndFile.GetSample(m_nSample).nVibSweep = static_cast<uint8>(n);
PropagateAutoVibratoChanges();
SetModified(SampleHint().Info(), false, false);
}
}
void CCtrlSamples::OnVibRateChanged()
{
if (IsLocked()) return;
int lmin = 0, lmax = 0;
m_SpinVibRate.GetRange(lmin, lmax);
int n = GetDlgItemInt(IDC_EDIT16);
if ((n >= lmin) && (n <= lmax))
{
if(!m_startedEdit) PrepareUndo("Set Vibrato Rate");
m_sndFile.GetSample(m_nSample).nVibRate = static_cast<uint8>(n);
PropagateAutoVibratoChanges();
SetModified(SampleHint().Info(), false, false);
}
}
void CCtrlSamples::OnLoopTypeChanged()
{
if(IsLocked()) return;
const int n = m_ComboLoopType.GetCurSel();
ModSample &sample = m_sndFile.GetSample(m_nSample);
bool wasDisabled = !sample.uFlags[CHN_LOOP];
PrepareUndo("Set Loop Type");
// Loop type index: 0: Off, 1: On, 2: PingPong
sample.uFlags.set(CHN_LOOP, n > 0);
sample.uFlags.set(CHN_PINGPONGLOOP, n == 2);
// set loop points if theren't any
if(wasDisabled && sample.uFlags[CHN_LOOP] && sample.nLoopStart == sample.nLoopEnd)
{
SampleSelectionPoints selection = GetSelectionPoints();
if(selection.selectionActive)
{
sample.SetLoop(selection.nStart, selection.nEnd, true, n == 2, m_sndFile);
} else
{
sample.SetLoop(0, sample.nLength, true, n == 2, m_sndFile);
}
m_modDoc.UpdateAllViews(NULL, SampleHint(m_nSample).Info());
} else
{
sample.PrecomputeLoops(m_sndFile);
}
ctrlSmp::UpdateLoopPoints(sample, m_sndFile);
SetModified(SampleHint().Info(), false, false);
}
void CCtrlSamples::OnLoopPointsChanged()
{
if(IsLocked()) return;
ModSample &sample = m_sndFile.GetSample(m_nSample);
SmpLength start = GetDlgItemInt(IDC_EDIT1, NULL, FALSE), end = GetDlgItemInt(IDC_EDIT2, NULL, FALSE);
if(start < end || !sample.uFlags[CHN_LOOP])
{
if(!m_startedEdit) PrepareUndo("Set Loop");
const int n = m_ComboLoopType.GetCurSel();
sample.SetLoop(start, end, n > 0, n == 2, m_sndFile);
SetModified(SampleHint().Info(), false, false);
}
}
void CCtrlSamples::OnSustainTypeChanged()
{
if(IsLocked()) return;
const int n = m_ComboSustainType.GetCurSel();
ModSample &sample = m_sndFile.GetSample(m_nSample);
bool wasDisabled = !sample.uFlags[CHN_SUSTAINLOOP];
PrepareUndo("Set Sustain Loop Type");
// Loop type index: 0: Off, 1: On, 2: PingPong
sample.uFlags.set(CHN_SUSTAINLOOP, n > 0);
sample.uFlags.set(CHN_PINGPONGSUSTAIN, n == 2);
// set sustain loop points if theren't any
if(wasDisabled && sample.uFlags[CHN_SUSTAINLOOP] && sample.nSustainStart == sample.nSustainEnd)
{
SampleSelectionPoints selection = GetSelectionPoints();
if(selection.selectionActive)
{
sample.SetSustainLoop(selection.nStart, selection.nEnd, true, n == 2, m_sndFile);
} else
{
sample.SetSustainLoop(0, sample.nLength, true, n == 2, m_sndFile);
}
m_modDoc.UpdateAllViews(NULL, SampleHint(m_nSample).Info());
} else
{
sample.PrecomputeLoops(m_sndFile);
}
ctrlSmp::UpdateLoopPoints(sample, m_sndFile);
SetModified(SampleHint().Info(), false, false);
}
void CCtrlSamples::OnSustainPointsChanged()
{
if(IsLocked()) return;
ModSample &sample = m_sndFile.GetSample(m_nSample);
SmpLength start = GetDlgItemInt(IDC_EDIT3, NULL, FALSE), end = GetDlgItemInt(IDC_EDIT4, NULL, FALSE);
if(start < end || !sample.uFlags[CHN_SUSTAINLOOP])
{
if(!m_startedEdit) PrepareUndo("Set Sustain Loop");
const int n = m_ComboSustainType.GetCurSel();
sample.SetSustainLoop(start, end, n > 0, n == 2, m_sndFile);
SetModified(SampleHint().Info(), false, false);
}
}
#define SMPLOOP_ACCURACY 7 // 5%
#define BIDILOOP_ACCURACY 2 // 5%
bool MPT_LoopCheck(int sstart0, int sstart1, int send0, int send1)
{
int dse0 = send0 - sstart0;
if ((dse0 < -SMPLOOP_ACCURACY) || (dse0 > SMPLOOP_ACCURACY)) return false;
int dse1 = send1 - sstart1;
if ((dse1 < -SMPLOOP_ACCURACY) || (dse1 > SMPLOOP_ACCURACY)) return false;
int dstart = sstart1 - sstart0;
int dend = send1 - send0;
if (!dstart) dstart = dend >> 7;
if (!dend) dend = dstart >> 7;
if ((dstart ^ dend) < 0) return false;
int delta = dend - dstart;
return ((delta > -SMPLOOP_ACCURACY) && (delta < SMPLOOP_ACCURACY));
}
bool MPT_BidiEndCheck(int spos0, int spos1, int spos2)
{
int delta0 = spos1 - spos0;
int delta1 = spos2 - spos1;
int delta2 = spos2 - spos0;
if (!delta0) delta0 = delta1 >> 7;
if (!delta1) delta1 = delta0 >> 7;
if ((delta1 ^ delta0) < 0) return false;
return ((delta0 >= -1) && (delta0 <= 0) && (delta1 >= -1) && (delta1 <= 0) && (delta2 >= -1) && (delta2 <= 0));
}
bool MPT_BidiStartCheck(int spos0, int spos1, int spos2)
{
int delta1 = spos1 - spos0;
int delta0 = spos2 - spos1;
int delta2 = spos2 - spos0;
if (!delta0) delta0 = delta1 >> 7;
if (!delta1) delta1 = delta0 >> 7;
if ((delta1 ^ delta0) < 0) return false;
return ((delta0 >= -1) && (delta0 <= 0) && (delta1 > -1) && (delta1 <= 0) && (delta2 >= -1) && (delta2 <= 0));
}
void CCtrlSamples::OnVScroll(UINT nCode, UINT, CScrollBar *scrollBar)
{
TCHAR s[256];
if(IsLocked()) return;
ModSample &sample = m_sndFile.GetSample(m_nSample);
const uint8 *pSample = mpt::byte_cast<const uint8 *>(sample.sampleb());
const uint32 inc = sample.GetBytesPerSample();
SmpLength i;
int pos;
bool redraw = false;
static CScrollBar *lastScrollbar = nullptr;
LockControls();
if ((!sample.nLength) || (!pSample)) goto NoSample;
if (sample.uFlags[CHN_16BIT])
{
pSample++;
}
// Loop Start
if ((pos = m_SpinLoopStart.GetPos32()) != 0 && sample.nLoopEnd > 0)
{
bool bOk = false;
const uint8 *p = pSample + sample.nLoopStart * inc;
int find0 = (int)pSample[sample.nLoopEnd*inc-inc];
int find1 = (int)pSample[sample.nLoopEnd*inc];
// Find Next LoopStart Point
if (pos > 0)
{
for (i = sample.nLoopStart + 1; i + 16 < sample.nLoopEnd; i++)
{
p += inc;
bOk = sample.uFlags[CHN_PINGPONGLOOP] ? MPT_BidiStartCheck(p[0], p[inc], p[inc*2]) : MPT_LoopCheck(find0, find1, p[0], p[inc]);
if (bOk) break;
}
} else
// Find Prev LoopStart Point
{
for (i = sample.nLoopStart; i; )
{
i--;
p -= inc;
bOk = sample.uFlags[CHN_PINGPONGLOOP] ? MPT_BidiStartCheck(p[0], p[inc], p[inc*2]) : MPT_LoopCheck(find0, find1, p[0], p[inc]);
if (bOk) break;
}
}
if (bOk)
{
if(!m_startedEdit && lastScrollbar != scrollBar) PrepareUndo("Set Loop Start");
sample.nLoopStart = i;
wsprintf(s, _T("%u"), sample.nLoopStart);
m_EditLoopStart.SetWindowText(s);
redraw = true;
sample.PrecomputeLoops(m_sndFile);
}
m_SpinLoopStart.SetPos(0);
}
// Loop End
if ((pos = m_SpinLoopEnd.GetPos32()) != 0)
{
bool bOk = false;
const uint8 *p = pSample + sample.nLoopEnd * inc;
int find0 = (int)pSample[sample.nLoopStart*inc];
int find1 = (int)pSample[sample.nLoopStart*inc+inc];
// Find Next LoopEnd Point
if (pos > 0)
{
for (i = sample.nLoopEnd + 1; i <= sample.nLength; i++, p += inc)
{
bOk = sample.uFlags[CHN_PINGPONGLOOP] ? MPT_BidiEndCheck(p[0], p[inc], p[inc*2]) : MPT_LoopCheck(find0, find1, p[0], p[inc]);
if (bOk) break;
}
} else
// Find Prev LoopEnd Point
{
for (i = sample.nLoopEnd; i > sample.nLoopStart + 16; )
{
i--;
p -= inc;
bOk = sample.uFlags[CHN_PINGPONGLOOP] ? MPT_BidiEndCheck(p[0], p[inc], p[inc*2]) : MPT_LoopCheck(find0, find1, p[0], p[inc]);
if (bOk) break;
}
}
if (bOk)
{
if(!m_startedEdit && lastScrollbar != scrollBar) PrepareUndo("Set Loop End");
sample.nLoopEnd = i;
wsprintf(s, _T("%u"), sample.nLoopEnd);
m_EditLoopEnd.SetWindowText(s);
redraw = true;
sample.PrecomputeLoops(m_sndFile);
}
m_SpinLoopEnd.SetPos(0);
}
// Sustain Loop Start
if ((pos = m_SpinSustainStart.GetPos32()) != 0 && sample.nSustainEnd > 0)
{
bool bOk = false;
const uint8 *p = pSample + sample.nSustainStart * inc;
int find0 = (int)pSample[sample.nSustainEnd*inc-inc];
int find1 = (int)pSample[sample.nSustainEnd*inc];
// Find Next Sustain LoopStart Point
if (pos > 0)
{
for (i = sample.nSustainStart + 1; i + 16 < sample.nSustainEnd; i++)
{
p += inc;
bOk = sample.uFlags[CHN_PINGPONGSUSTAIN] ? MPT_BidiStartCheck(p[0], p[inc], p[inc*2]) : MPT_LoopCheck(find0, find1, p[0], p[inc]);
if (bOk) break;
}
} else
// Find Prev Sustain LoopStart Point
{
for (i = sample.nSustainStart; i; )
{
i--;
p -= inc;
bOk = sample.uFlags[CHN_PINGPONGSUSTAIN] ? MPT_BidiStartCheck(p[0], p[inc], p[inc*2]) : MPT_LoopCheck(find0, find1, p[0], p[inc]);
if (bOk) break;
}
}
if (bOk)
{
if(!m_startedEdit && lastScrollbar != scrollBar) PrepareUndo("Set Sustain Loop Start");
sample.nSustainStart = i;
wsprintf(s, _T("%u"), sample.nSustainStart);
m_EditSustainStart.SetWindowText(s);
redraw = true;
sample.PrecomputeLoops(m_sndFile);
}
m_SpinSustainStart.SetPos(0);
}
// Sustain Loop End
if ((pos = m_SpinSustainEnd.GetPos32()) != 0)
{
bool bOk = false;
const uint8 *p = pSample + sample.nSustainEnd * inc;
int find0 = (int)pSample[sample.nSustainStart*inc];
int find1 = (int)pSample[sample.nSustainStart*inc+inc];
// Find Next LoopEnd Point
if (pos > 0)
{
for (i = sample.nSustainEnd + 1; i + 1 < sample.nLength; i++, p += inc)
{
bOk = sample.uFlags[CHN_PINGPONGSUSTAIN] ? MPT_BidiEndCheck(p[0], p[inc], p[inc*2]) : MPT_LoopCheck(find0, find1, p[0], p[inc]);
if (bOk) break;
}
} else
// Find Prev LoopEnd Point
{
for (i = sample.nSustainEnd; i > sample.nSustainStart + 16; )
{
i--;
p -= inc;
bOk = sample.uFlags[CHN_PINGPONGSUSTAIN] ? MPT_BidiEndCheck(p[0], p[inc], p[inc*2]) : MPT_LoopCheck(find0, find1, p[0], p[inc]);
if (bOk) break;
}
}
if (bOk)
{
if(!m_startedEdit && lastScrollbar != scrollBar) PrepareUndo("Set Sustain Loop End");
sample.nSustainEnd = i;
wsprintf(s, _T("%u"), sample.nSustainEnd);
m_EditSustainEnd.SetWindowText(s);
redraw = true;
sample.PrecomputeLoops(m_sndFile);
}
m_SpinSustainEnd.SetPos(0);
}
NoSample:
// FineTune / C-5 Speed
if ((pos = m_SpinFineTune.GetPos32()) != 0)
{
if (!m_sndFile.UseFinetuneAndTranspose())
{
if(!m_startedEdit && lastScrollbar != scrollBar) PrepareUndo("Finetune");
if(sample.nC5Speed < 1)
sample.nC5Speed = 8363;
auto oldFreq = sample.nC5Speed;
sample.Transpose((pos * TrackerSettings::Instance().m_nFinetuneStep) / 1200.0);
if(sample.nC5Speed == oldFreq)
sample.nC5Speed += pos;
Limit(sample.nC5Speed, 1u, 9999999u); // 9999999 is max. in Impulse Tracker
int transp = ModSample::FrequencyToTranspose(sample.nC5Speed).first;
int basenote = (NOTE_MIDDLEC - NOTE_MIN) + transp;
Clamp(basenote, BASENOTE_MIN, BASENOTE_MAX);
basenote -= BASENOTE_MIN;
if (basenote != m_CbnBaseNote.GetCurSel()) m_CbnBaseNote.SetCurSel(basenote);
SetDlgItemInt(IDC_EDIT5, sample.nC5Speed, FALSE);
} else
{
if(!m_startedEdit && lastScrollbar != scrollBar) PrepareUndo("Finetune");
int ftune = (int)sample.nFineTune;
// MOD finetune range -8 to 7 translates to -128 to 112
if(m_sndFile.GetType() & MOD_TYPE_MOD)
{
ftune = Clamp((ftune >> 4) + pos, -8, 7);
sample.nFineTune = MOD2XMFineTune((signed char)ftune);
} else
{
ftune = Clamp(ftune + pos, -128, 127);
sample.nFineTune = (signed char)ftune;
}
SetDlgItemInt(IDC_EDIT5, ftune, TRUE);
}
redraw = true;
m_SpinFineTune.SetPos(0);
OnFineTuneChangedDone();
}
if(scrollBar->m_hWnd == m_SpinSequenceMs.m_hWnd || scrollBar->m_hWnd == m_SpinSeekWindowMs.m_hWnd || scrollBar->m_hWnd == m_SpinOverlap.m_hWnd)
{
ReadTimeStretchParameters();
UpdateTimeStretchParameters();
}
if(nCode == SB_ENDSCROLL) SwitchToView();
if(redraw)
{
SetModified(SampleHint().Info().Data(), false, false);
}
lastScrollbar = scrollBar;
UnlockControls();
}
BOOL CCtrlSamples::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 = (UINT)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.
}
}
return CModControlDlg::PreTranslateMessage(pMsg);
}
LRESULT CCtrlSamples::OnCustomKeyMsg(WPARAM wParam, LPARAM /*lParam*/)
{
int transpose = 0;
switch(wParam)
{
case kcSampleLoad: OnSampleOpen(); return wParam;
case kcSampleLoadRaw: OnSampleOpenRaw(); return wParam;
case kcSampleSave: OnSampleSaveOne(); return wParam;
case kcSampleNew: InsertSample(false); return wParam;
case kcSampleDuplicate: InsertSample(true); return wParam;
case kcSampleTransposeUp: transpose = 1; break;
case kcSampleTransposeDown: transpose = -1; break;
case kcSampleTransposeOctUp: transpose = 12; break;
case kcSampleTransposeOctDown: transpose = -12; break;
case kcSampleUpsample:
case kcSampleDownsample:
{
uint32 oldRate = m_sndFile.GetSample(m_nSample).GetSampleRate(m_sndFile.GetType());
ApplyResample(m_nSample, wParam == kcSampleUpsample ? oldRate * 2 : oldRate / 2, TrackerSettings::Instance().sampleEditorDefaultResampler);
}
return wParam;
case kcSampleResample:
OnResample();
return wParam;
case kcSampleStereoSep:
OnStereoSeparation();
return wParam;
case kcSampleInitializeOPL:
OnInitOPLInstrument();
return wParam;
}
if(transpose)
{
if(m_CbnBaseNote.IsWindowEnabled())
{
int sel = Clamp(m_CbnBaseNote.GetCurSel() + transpose, 0, m_CbnBaseNote.GetCount() - 1);
if(sel != m_CbnBaseNote.GetCurSel())
{
m_CbnBaseNote.SetCurSel(sel);
OnBaseNoteChanged();
}
}
return wParam;
}
return kcNull;
}
// Return currently selected part of the sample.
// The whole sample size will be returned if no part of the sample is selected.
// However, point.bSelected indicates whether a sample selection exists or not.
CCtrlSamples::SampleSelectionPoints CCtrlSamples::GetSelectionPoints()
{
SampleSelectionPoints points;
SAMPLEVIEWSTATE viewstate;
const ModSample &sample = m_sndFile.GetSample(m_nSample);
Clear(viewstate);
SendViewMessage(VIEWMSG_SAVESTATE, (LPARAM)&viewstate);
points.nStart = viewstate.dwBeginSel;
points.nEnd = viewstate.dwEndSel;
if(points.nEnd > sample.nLength) points.nEnd = sample.nLength;
if(points.nStart > points.nEnd) points.nStart = points.nEnd;
points.selectionActive = true;
if(points.nStart >= points.nEnd)
{
points.nStart = 0;
points.nEnd = sample.nLength;
points.selectionActive = false;
}
return points;
}
// Set the currently selected part of the sample.
// To reset the selection, use nStart = nEnd = 0.
void CCtrlSamples::SetSelectionPoints(SmpLength nStart, SmpLength nEnd)
{
const ModSample &sample = m_sndFile.GetSample(m_nSample);
Limit(nStart, SmpLength(0), sample.nLength);
Limit(nEnd, SmpLength(0), sample.nLength);
SAMPLEVIEWSTATE viewstate;
Clear(viewstate);
SendViewMessage(VIEWMSG_SAVESTATE, (LPARAM)&viewstate);
viewstate.dwBeginSel = nStart;
viewstate.dwEndSel = nEnd;
SendViewMessage(VIEWMSG_LOADSTATE, (LPARAM)&viewstate);
}
// Crossfade loop to create smooth loop transitions
void CCtrlSamples::OnXFade()
{
ModSample &sample = m_sndFile.GetSample(m_nSample);
if(!sample.HasSampleData())
{
MessageBeep(MB_ICONWARNING);
SwitchToView();
return;
}
bool resetLoopOnCancel = false;
if((sample.nLoopEnd <= sample.nLoopStart || sample.nLoopEnd > sample.nLength)
&& (sample.nSustainEnd <= sample.nSustainStart || sample.nSustainEnd > sample.nLength))
{
const auto selection = GetSelectionPoints();
if(selection.nStart > 0 && selection.nEnd > selection.nStart)
{
sample.SetLoop(selection.nStart, selection.nEnd, true, false, m_sndFile);
resetLoopOnCancel = true;
} else
{
Reporting::Error("Crossfade requires a sample loop to work.", this);
SwitchToView();
return;
}
}
if(sample.nLoopStart == 0 && sample.nSustainStart == 0)
{
Reporting::Error("Crossfade requires the sample to have data before the loop start.", this);
SwitchToView();
return;
}
CSampleXFadeDlg dlg(this, sample);
if(dlg.DoModal() == IDOK)
{
const SmpLength loopStart = dlg.m_useSustainLoop ? sample.nSustainStart: sample.nLoopStart;
const SmpLength loopEnd = dlg.m_useSustainLoop ? sample.nSustainEnd: sample.nLoopEnd;
const SmpLength maxSamples = std::min({ sample.nLength, loopStart, loopEnd / 2 });
SmpLength fadeSamples = dlg.PercentToSamples(dlg.m_fadeLength);
LimitMax(fadeSamples, maxSamples);
if(fadeSamples < 2) return;
PrepareUndo("Crossfade", sundo_update,
loopEnd - fadeSamples,
loopEnd + (dlg.m_afterloopFade ? std::min(sample.nLength - loopEnd, fadeSamples) : 0));
if(SampleEdit::XFadeSample(sample, fadeSamples, dlg.m_fadeLaw, dlg.m_afterloopFade, dlg.m_useSustainLoop, m_sndFile))
{
SetModified(SampleHint().Info().Data(), true, true);
} else
{
m_modDoc.GetSampleUndo().RemoveLastUndoStep(m_nSample);
}
} else if(resetLoopOnCancel)
{
sample.SetLoop(0, 0, false, false, m_sndFile);
}
SwitchToView();
}
void CCtrlSamples::OnStereoSeparation()
{
ModSample &sample = m_sndFile.GetSample(m_nSample);
if(!sample.HasSampleData()
|| sample.GetNumChannels() != 2
|| sample.uFlags[CHN_ADLIB])
{
MessageBeep(MB_ICONWARNING);
SwitchToView();
return;
}
static double separation = 100.0;
CInputDlg dlg(this, _T("Stereo separation amount\n0% = mono, 100% = no change, 200% = double separation\nNegative values swap channels"), -200.0, 200.0, separation);
if(dlg.DoModal() == IDOK)
{
separation = dlg.resultAsDouble;
SampleSelectionPoints selection = GetSelectionPoints();
PrepareUndo("Stereo Separation", sundo_update,
selection.nStart, selection.nEnd);
if(SampleEdit::StereoSepSample(sample, selection.nStart, selection.nEnd, separation, m_sndFile))
{
SetModified(SampleHint().Info().Data(), true, true);
} else
{
m_modDoc.GetSampleUndo().RemoveLastUndoStep(m_nSample);
}
}
SwitchToView();
}
void CCtrlSamples::OnAutotune()
{
SampleSelectionPoints selection = GetSelectionPoints();
if(!selection.selectionActive)
{
selection.nStart = selection.nEnd = 0;
}
ModSample &sample = m_sndFile.GetSample(m_nSample);
Autotune at(sample, m_sndFile.GetType(), selection.nStart, selection.nEnd);
if(at.CanApply())
{
CAutotuneDlg dlg(this);
if(dlg.DoModal() == IDOK)
{
BeginWaitCursor();
PrepareUndo("Automatic Sample Tuning");
bool modified = true;
if(IsOPLInstrument())
{
const uint32 newFreq = mpt::saturate_round<uint32>(dlg.GetPitchReference() * (8363.0 / 440.0) * std::pow(2.0, dlg.GetTargetNote() / 12.0));
modified = (newFreq != sample.nC5Speed);
sample.nC5Speed = newFreq;
} else
{
modified = at.Apply(static_cast<double>(dlg.GetPitchReference()), dlg.GetTargetNote());
}
OnFineTuneChangedDone();
if(modified)
SetModified(SampleHint().Info(), true, false);
EndWaitCursor();
}
}
SwitchToView();
}
void CCtrlSamples::OnKeepSampleOnDisk()
{
SAMPLEINDEX first = m_nSample, last = m_nSample;
if(CMainFrame::GetInputHandler()->ShiftPressed())
{
first = 1;
last = m_sndFile.GetNumSamples();
}
const bool enable = IsDlgButtonChecked(IDC_CHECK2) != BST_UNCHECKED;
for(SAMPLEINDEX i = first; i <= last; i++)
{
if(bool newState = enable && m_sndFile.SampleHasPath(i); newState != m_sndFile.GetSample(i).uFlags[SMP_KEEPONDISK])
{
m_sndFile.GetSample(i).uFlags.set(SMP_KEEPONDISK, newState);
m_modDoc.UpdateAllViews(nullptr, SampleHint(i).Info().Names(), this);
}
}
m_modDoc.SetModified();
}
// When changing auto vibrato properties, propagate them to other samples of the same instrument in XM edit mode.
void CCtrlSamples::PropagateAutoVibratoChanges()
{
if(!(m_sndFile.GetType() & MOD_TYPE_XM))
{
return;
}
for(INSTRUMENTINDEX i = 1; i <= m_sndFile.GetNumInstruments(); i++)
{
if(m_sndFile.IsSampleReferencedByInstrument(m_nSample, i))
{
const auto referencedSamples = m_sndFile.Instruments[i]->GetSamples();
// Propagate changes to all samples that belong to this instrument.
const ModSample &it = m_sndFile.GetSample(m_nSample);
m_sndFile.PropagateXMAutoVibrato(i, it.nVibType, it.nVibSweep, it.nVibDepth, it.nVibRate);
for(auto smp : referencedSamples)
{
m_modDoc.UpdateAllViews(nullptr, SampleHint(smp).Info(), this);
}
}
}
}
void CCtrlSamples::OnXButtonUp(UINT nFlags, UINT nButton, CPoint point)
{
if(nButton == XBUTTON1) OnPrevInstrument();
else if(nButton == XBUTTON2) OnNextInstrument();
CModControlDlg::OnXButtonUp(nFlags, nButton, point);
SwitchToView();
}
bool CCtrlSamples::IsOPLInstrument() const
{
return m_nSample >= 1 && m_nSample <= m_sndFile.GetNumSamples() && m_sndFile.GetSample(m_nSample).uFlags[CHN_ADLIB];
}
void CCtrlSamples::OnInitOPLInstrument()
{
if(m_sndFile.SupportsOPL())
{
CriticalSection cs;
PrepareUndo("Initialize OPL Instrument", sundo_replace);
m_sndFile.DestroySample(m_nSample);
m_sndFile.InitOPL();
ModSample &sample = m_sndFile.GetSample(m_nSample);
sample.nC5Speed = 8363;
// Initialize with instant attack, release and enabled sustain for carrier and instant attack for modulator
sample.SetAdlib(true, { 0x00, 0x20, 0x00, 0x00, 0xF0, 0xF0, 0x00, 0x0F, 0x00, 0x00, 0x00, 0x00 });
SetModified(SampleHint().Info().Data().Names(), true, true);
SwitchToView();
}
}
INSTRUMENTINDEX CCtrlSamples::GetParentInstrumentWithSameName() const
{
auto ins = m_modDoc.FindSampleParent(m_nSample);
if(ins == INSTRUMENTINDEX_INVALID)
return INSTRUMENTINDEX_INVALID;
auto instr = m_sndFile.Instruments[ins];
if(instr == nullptr)
return INSTRUMENTINDEX_INVALID;
if((!instr->name.empty() && instr->name != m_sndFile.m_szNames[m_nSample]) || instr->GetSamples().size() != 1)
return INSTRUMENTINDEX_INVALID;
return ins;
}
OPENMPT_NAMESPACE_END