3793 lines
115 KiB
C++
3793 lines
115 KiB
C++
/*
|
|
* 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
|