View Issue Details

IDProjectCategoryView StatusLast Update
0000501OpenMPT[All Projects] Feature Requestpublic2018-05-18 07:11
ReportercodaAssigned Tomanx 
PrioritynormalSeverityminorReproducibilityhave not tried
Status acknowledgedResolutionopen 
Product Version 
Target VersionOpenMPT 1.?? (long term goals)Fixed in Version 
Summary0000501: Audio recording to sample
Description

Non-trivial feature but a long-term goal that would prove very useful.

Rationale: FT2 and Renoise both have various methods to create sample data from within the tracker. They're still definitely trackers and not DAWs, although Renoise gets a bit close with its pattern-syncing. OpenMPT only allows importing external samples or creating sample data with the pencil tool.

My approach would be to minimize the amount of extra UI needed:

  • Samples have Arm/Disarm toggle
  • Playing back the song while a sample is armed begins writing audio input to the sample when the sample is triggered within the pattern. That is, a sample armed for recording receives audio instead of plays audio.
  • Hitting stop could optionally disarm the samples so subsequent playbacks do not overwrite the samples.

This allows for recording live audio along with a pattern at any point, as well as splitting a recording across multiple samples that are all armed. This could also be a starting point for routing parts of the graph back to the input for a Freeze/"Sample VSTi" feature (another useful thing in Renoise, which is made more necessary by its utter lack of tracker-style pitch control on VSTis).

I provided a proof-of-concept patch which implements this sort of behavior - with, of course, no error checking or UI (naming a sample starting with '%' arms it, only ASIO is supported, the sample must have some initial data and be triggered at the playback rate and have a 16-bit mono format).

TagsNo tags attached.
Has the bug occurred in previous versions?
Tested code revision (in case you know it)

Relationships

related to 0001042 new Render pattern channels to separate buffers 
parent of 0000722 assignedmanx Support sound device with only input channels 
Not all the children of this issue are yet resolved or closed.

Activities

coda

coda

2014-04-03 20:31

reporter  

samplerecord.patch (5,940 bytes)
Index: sounddev/SoundDevice.h
===================================================================
--- sounddev/SoundDevice.h	(revision 3634)
+++ sounddev/SoundDevice.h	(working copy)
@@ -248,6 +248,7 @@
 	uint32 UpdateIntervalMS;
 	uint32 Samplerate;
 	uint8 Channels;
+	uint8 InChannels;
 	SampleFormat sampleFormat;
 	bool ExclusiveMode; // Use hardware buffers directly
 	bool BoostThreadPriority; // Boost thread priority for glitch-free audio rendering
@@ -261,6 +262,7 @@
 		, UpdateIntervalMS(5)
 		, Samplerate(48000)
 		, Channels(2)
+		, InChannels(2)
 		, sampleFormat(SampleFormatFloat32)
 		, ExclusiveMode(false)
 		, BoostThreadPriority(true)
Index: sounddev/SoundDeviceASIO.cpp
===================================================================
--- sounddev/SoundDeviceASIO.cpp	(revision 3634)
+++ sounddev/SoundDeviceASIO.cpp	(working copy)
@@ -322,11 +322,11 @@
 			ASSERT(false);
 		}
 	
-		m_BufferInfo.resize(m_Settings.Channels);
-		for(int channel = 0; channel < m_Settings.Channels; ++channel)
+		m_BufferInfo.resize(m_Settings.Channels + m_Settings.InChannels);
+		for(int channel = 0; channel < m_Settings.Channels + m_Settings.InChannels; ++channel)
 		{
 			MemsetZero(m_BufferInfo[channel]);
-			m_BufferInfo[channel].isInput = ASIOFalse;
+			m_BufferInfo[channel].isInput = channel < m_Settings.Channels ? ASIOFalse : ASIOTrue;
 			m_BufferInfo[channel].channelNum = m_Settings.ChannelMapping.ToDevice(channel);
 		}
 		m_Callbacks.bufferSwitch = CallbackBufferSwitch;
@@ -336,20 +336,20 @@
 		ALWAYS_ASSERT(g_CallbacksInstance == nullptr);
 		g_CallbacksInstance = this;
 		Log(mpt::String::Print("ASIO: createBuffers(numChannels=%1, bufferSize=%2)", m_Settings.Channels, m_nAsioBufferLen));
-		asioCall(createBuffers(&m_BufferInfo[0], m_Settings.Channels, m_nAsioBufferLen, &m_Callbacks));
+		asioCall(createBuffers(&m_BufferInfo[0], m_Settings.Channels + m_Settings.InChannels, m_nAsioBufferLen, &m_Callbacks));
 		m_BuffersCreated = true;
 
-		m_ChannelInfo.resize(m_Settings.Channels);
-		for(int channel = 0; channel < m_Settings.Channels; ++channel)
+		m_ChannelInfo.resize(m_Settings.Channels + m_Settings.InChannels);
+		for(int channel = 0; channel < m_Settings.Channels + m_Settings.InChannels; ++channel)
 		{
 			MemsetZero(m_ChannelInfo[channel]);
-			m_ChannelInfo[channel].isInput = ASIOFalse;
+			m_ChannelInfo[channel].isInput = channel < m_Settings.Channels ? ASIOFalse : ASIOTrue;
 			m_ChannelInfo[channel].channel = m_Settings.ChannelMapping.ToDevice(channel);
 			asioCall(getChannelInfo(&m_ChannelInfo[channel]));
 			ASSERT(m_ChannelInfo[channel].isActive);
 			mpt::String::SetNullTerminator(m_ChannelInfo[channel].name);
 			Log(mpt::String::Print("ASIO: getChannelInfo(isInput=%1 channel=%2) => isActive=%3 channelGroup=%4 type=%5 name='%6'"
-				, ASIOFalse
+				, m_ChannelInfo[channel].isInput
 				, m_Settings.ChannelMapping.ToDevice(channel)
 				, m_ChannelInfo[channel].isActive
 				, m_ChannelInfo[channel].channelGroup
@@ -361,7 +361,7 @@
 		bool allChannelsAreFloat = true;
 		bool allChannelsAreInt16 = true;
 		bool allChannelsAreInt24 = true;
-		for(int channel = 0; channel < m_Settings.Channels; ++channel)
+		for(int channel = 0; channel < m_Settings.Channels + m_Settings.InChannels; ++channel)
 		{
 			if(!IsSampleTypeFloat(m_ChannelInfo[channel].type))
 			{
Index: sounddev/SoundDeviceASIO.h
===================================================================
--- sounddev/SoundDeviceASIO.h	(revision 3634)
+++ sounddev/SoundDeviceASIO.h	(working copy)
@@ -30,12 +30,15 @@
 {
 	friend class TemporaryASIODriverOpener;
 
+public:
+	std::vector<ASIOBufferInfo> m_BufferInfo;
+	long m_BufferIndex;
 protected:
 
 	IASIO *m_pAsioDrv;
 
 	long m_nAsioBufferLen;
-	std::vector<ASIOBufferInfo> m_BufferInfo;
+	
 	ASIOCallbacks m_Callbacks;
 	static CASIODevice *g_CallbacksInstance; // only 1 opened instance allowed for ASIO
 	bool m_BuffersCreated;
@@ -48,7 +51,7 @@
 
 	bool m_DeviceRunning;
 	uint64 m_TotalFramesWritten;
-	long m_BufferIndex;
+	
 	LONG m_RenderSilence;
 	LONG m_RenderingSilence;
 
Index: soundlib/Sndmix.cpp
===================================================================
--- soundlib/Sndmix.cpp	(revision 3634)
+++ soundlib/Sndmix.cpp	(working copy)
@@ -141,6 +141,20 @@
 }
 
 
+#include "..\sounddev\SoundDevice.h"
+#include "..\sounddev\SoundDevices.h"
+
+#include "..\sounddev\SoundDeviceASIO.h"
+
+#include "../common/misc_util.h"
+#include "../common/StringFixer.h"
+#include "../soundlib/SampleFormatConverters.h"
+
+#include "..\mptrack\MainFrm.h"
+
+#include "modsmp_ctrl.h"
+
+
 CSoundFile::samplecount_t CSoundFile::Read(samplecount_t count, IAudioReadTarget &target)
 //---------------------------------------------------------------------------------------
 {
@@ -225,6 +239,20 @@
 			m_Reverb.Process(MixSoundBuffer, countChunk);
 		#endif // NO_REVERB
 
+
+		for(int chn=0;chn<MAX_CHANNELS;chn++)
+		if(Chn[chn].pCurrentSample && Chn[chn].nInc == 0x10000 && Chn[chn].pModSample && Chn[chn].pModSample->filename[0] == '%') {
+			CASIODevice *sda = dynamic_cast<CASIODevice*>(CMainFrame::GetMainFrame()->gpSoundDevice);
+			if(sda) {
+				if(Chn[chn].nPos >= Chn[chn].pModSample->nLength - countChunk)
+					ctrlSmp::InsertSilence( *Chn[chn].pModSample, Chn[chn].pModSample->nLength, Chn[chn].pModSample->nLength, *this);
+
+
+				CopyInterleavedToChannel<SC::Convert<int16, int32> >(reinterpret_cast<int16*>(const_cast<void*>(Chn[chn].pCurrentSample)) + Chn[chn].nPos, reinterpret_cast<int32*>(sda->m_BufferInfo[2].buffers[1 - sda->m_BufferIndex]) ,  1, countChunk, 0);
+				//CopyInterleavedToChannel<SC::Convert<int16, int32> >(reinterpret_cast<int16*>(Samples[1].pSample) + (m_lTotalSampleCount % (Samples[1].nLength - 1024)), reinterpret_cast<int32*>(sda->m_BufferInfo[2].buffers[1 - sda->m_BufferIndex]) ,  1, countChunk, 0);
+			}
+		}
+
 		if(mixPlugins)
 		{
 			ProcessPlugins(countChunk);
samplerecord.patch (5,940 bytes)
coda

coda

2018-01-29 02:00

reporter   ~0003397

Now that we have builtin plugins and some potential device input support it occurred to me the quickest path to exposing input would probably be something like the Audio version of Midi I/O (minus the O?). Might not need any params at all - just copy input into master/the next plugin in the graph.
There are already VST plugins like Edison that can do host-synced record/playback of their input pins so there would be less immediate need for recording UI.

StarWolf3000

StarWolf3000

2018-01-29 06:20

reporter   ~0003398

So you're looking for the feature commonly known as "bouncing"?

coda

coda

2018-01-29 07:13

reporter   ~0003399

No, this feature request is about audio input. OpenMPT can already bounce tracks.

manx

manx

2018-01-30 16:25

administrator   ~0003402

Now that we have builtin plugins and some potential device input support it occurred to me the quickest path to exposing input would probably be something like the Audio version of Midi I/O (minus the O?). Might not need any params at all - just copy input into master/the next plugin in the graph.

I do not think that would be easy to implement.
When not connecting audio recording directly to the already used audio output device (i.e. not using the exact same device in full-duplex mode), we would inevitably have to deal with audio-clock desync between the two clocks (which is frankly an enormous nightmare to do properly, implementation-wise). This is no problem in the MIDI case because MIDI is no monotonic contiguous stream and is only synchronized relatively and loosely to both the wall-clock and the audio clock.

manx

manx

2018-01-30 16:36

administrator   ~0003403

What I have in mind is along the lines of the following (just sketching ideas right now, there is no implementation yet, an honestly there are more internal refactorings that I would like to see before even starting an implementation of recording):

  • Recording function will be global, and if globally enabled, It will record ALWAYS and keep a (configurable length) back-buffer so that you could even hit record after the fact in case you came up with a brilliant idea while jamming around. This in my opinion is a rather crucial and distinct feature, and we should consider this with high priority when designing a recording feature. In articular, I think we should avoid designing a recording workflow, that would prohibit or complicate this use case.
  • Recording would have different, selectable sources. In particular: sound card input, OpenMPT master output, or individual channels or plugins (the latter with less priority in order to get the feature implemented in a somewhat timely manner). Multiple source could be selected at the same time, resulting in multiple recorded files, which would be very useful when recording some live performance played to the playing module.
  • There is no need to select the destination of the recorded data upfront. This can be deferred until after the recording has been stopped (at which point suitable options are: save to file, copy to sample slot, or simply discard)

Issue History

Date Modified Username Field Change
2014-04-03 20:31 coda New Issue
2014-04-03 20:31 coda File Added: samplerecord.patch
2014-08-14 10:49 manx Assigned To => manx
2014-08-14 10:49 manx Status new => acknowledged
2014-08-14 10:50 manx Target Version => OpenMPT 1.24.01.00 / libopenmpt 0.2-beta8 (upgrade first)
2014-12-12 00:02 Saga Musix Target Version OpenMPT 1.24.01.00 / libopenmpt 0.2-beta8 (upgrade first) => OpenMPT 1.?? (long term goals)
2015-11-07 17:18 manx Relationship added parent of 0000722
2018-01-29 02:00 coda Note Added: 0003397
2018-01-29 06:20 StarWolf3000 Note Added: 0003398
2018-01-29 07:13 coda Note Added: 0003399
2018-01-30 16:25 manx Note Added: 0003402
2018-01-30 16:36 manx Note Added: 0003403
2018-05-18 07:11 manx Relationship added related to 0001042