├── __init__.py ├── linux ├── __init__.py └── LoopbackCapture.py ├── mac ├── __init__.py └── LoopbackCapture.py ├── win32 ├── __init__.py ├── py │ ├── requirements.txt │ └── LoopbackCapture.py ├── cpp │ ├── guid.cpp │ ├── log.h │ ├── common.h │ ├── prefs.h │ ├── LoopbackCapture.h │ ├── LoopbackCapture.vcxproj │ ├── cleanup.h │ ├── main.cpp │ ├── prefs.cpp │ └── LoopbackCapture.cpp ├── dll │ ├── App.config │ ├── LoopbackCapture.csproj │ └── LoopbackCapture.cs ├── csharp │ ├── App.config │ ├── LoopbackCapture.csproj │ └── LoopbackCapture.cs └── LoopbackCapture.py ├── 3rdparty ├── CSCore.dll └── RGiesecke.DllExport.Metadata.dll ├── .gitignore ├── LICENSE └── README.md /__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /linux/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /mac/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /win32/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /win32/py/requirements.txt: -------------------------------------------------------------------------------- 1 | pythonnet -------------------------------------------------------------------------------- /win32/cpp/guid.cpp: -------------------------------------------------------------------------------- 1 | // guid.cpp 2 | 3 | #include 4 | #include "common.h" -------------------------------------------------------------------------------- /3rdparty/CSCore.dll: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/peitaosu/LoopbackCapture/HEAD/3rdparty/CSCore.dll -------------------------------------------------------------------------------- /3rdparty/RGiesecke.DllExport.Metadata.dll: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/peitaosu/LoopbackCapture/HEAD/3rdparty/RGiesecke.DllExport.Metadata.dll -------------------------------------------------------------------------------- /win32/cpp/log.h: -------------------------------------------------------------------------------- 1 | // log.h 2 | 3 | #define LOG(format, ...) wprintf(format L"\n", __VA_ARGS__) 4 | #define ERR(format, ...) LOG(L"Error: " format, __VA_ARGS__) 5 | -------------------------------------------------------------------------------- /win32/dll/App.config: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | -------------------------------------------------------------------------------- /win32/csharp/App.config: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Visual Studio Code Configuration Files 2 | .vscode/ 3 | 4 | # Visual Studio Configuration Files 5 | .vs/ 6 | *.dll.config 7 | *.exe.config 8 | *.vcxproj.user 9 | *.tlog 10 | 11 | # Python 12 | *.pyc 13 | 14 | # LoopbackCapture Build Output Folder 15 | /bin 16 | /obj 17 | 18 | # Binaries 19 | *.lib 20 | *.pdb 21 | *.exe 22 | -------------------------------------------------------------------------------- /win32/cpp/common.h: -------------------------------------------------------------------------------- 1 | // common.h 2 | 3 | #include 4 | #include 5 | #include 6 | #include 7 | #include 8 | #include 9 | #include 10 | 11 | #include "log.h" 12 | #include "cleanup.h" 13 | #include "prefs.h" 14 | #include "LoopbackCapture.h" 15 | -------------------------------------------------------------------------------- /win32/cpp/prefs.h: -------------------------------------------------------------------------------- 1 | // prefs.h 2 | 3 | class CPrefs { 4 | public: 5 | IMMDevice *m_pMMDevice; 6 | HMMIO m_hFile; 7 | bool m_bInt16; 8 | PWAVEFORMATEX m_pwfx; 9 | LPCWSTR m_szFilename; 10 | int m_time; 11 | 12 | // set hr to S_FALSE to abort but return success 13 | CPrefs(int argc, LPCWSTR argv[], HRESULT &hr); 14 | ~CPrefs(); 15 | 16 | }; 17 | -------------------------------------------------------------------------------- /win32/cpp/LoopbackCapture.h: -------------------------------------------------------------------------------- 1 | // LoopbackCapture.h 2 | 3 | // call CreateThread on this function 4 | // feed it the address of a LoopbackCaptureThreadFunctionArguments 5 | // it will capture via loopback from the IMMDevice 6 | // and dump output to the HMMIO 7 | // until the stop event is set 8 | // any failures will be propagated back via hr 9 | 10 | struct LoopbackCaptureThreadFunctionArguments { 11 | IMMDevice *pMMDevice; 12 | bool bInt16; 13 | HMMIO hFile; 14 | HANDLE hStartedEvent; 15 | HANDLE hStopEvent; 16 | UINT32 nFrames; 17 | HRESULT hr; 18 | }; 19 | 20 | DWORD WINAPI LoopbackCaptureThreadFunction(LPVOID pContext); 21 | -------------------------------------------------------------------------------- /linux/LoopbackCapture.py: -------------------------------------------------------------------------------- 1 | import subprocess 2 | import pulsectl 3 | 4 | def record_sounds(output_file="record.wav", time=0): 5 | pulse = pulsectl.Pulse().source_list()[0].name 6 | fmt = "pulse" 7 | if time is not 0: 8 | subprocess.check_output("avconv -f {} -i {} -t {} -y {}".format(fmt, pulse, time/1000, output_file), stderr=subprocess.STDOUT, shell=True) 9 | else: 10 | try: 11 | subprocess.check_output("avconv -f {} -i {} -y {}".format(fmt, pulse, output_file), stderr=subprocess.STDOUT, shell=True) 12 | except KeyboardInterrupt: 13 | pass 14 | except: 15 | return -1 16 | return 0 17 | -------------------------------------------------------------------------------- /win32/LoopbackCapture.py: -------------------------------------------------------------------------------- 1 | import os, sys, subprocess 2 | 3 | def record_sounds(output_file="record.wav", time=0): 4 | if "LOOPBACK_CAPTURE" not in os.environ: 5 | print("Please set the %LOOPBACK_CAPTURE% before you start to capture.") 6 | sys.exit(-1) 7 | if not os.path.isfile(os.environ["LOOPBACK_CAPTURE"]): 8 | print("File Not Found. Please make sure the %LOOPBACK_CAPTURE% is correct.") 9 | sys.exit(-1) 10 | Loopback_Capture_Path = os.environ["LOOPBACK_CAPTURE"] 11 | if time is not 0: 12 | process = subprocess.Popen("{} {} {}".format( 13 | Loopback_Capture_Path, output_file, time), stdout=subprocess.PIPE) 14 | exit_code = process.wait() 15 | else: 16 | process = subprocess.Popen("{} {}".format( 17 | Loopback_Capture_Path, output_file), stdout=subprocess.PIPE) 18 | exit_code = process.wait() 19 | return exit_code 20 | -------------------------------------------------------------------------------- /win32/cpp/LoopbackCapture.vcxproj: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | {D5E40402-40F8-4CA2-AAA0-9A145F804F63} 6 | loopbackcapture 7 | LoopbackCapture 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2017-2019 Tony Su 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /win32/py/LoopbackCapture.py: -------------------------------------------------------------------------------- 1 | import os, sys, clr 2 | 3 | sys.path.append(os.path.join(os.path.dirname(os.path.realpath(__file__)), "..\\..\\3rdparty")) 4 | clr.AddReference("CSCore") 5 | from System import EventHandler, EventArgs, Array, Byte, Threading 6 | from CSCore import * 7 | from CSCore.SoundIn import * 8 | from CSCore.Codecs.WAV import * 9 | from CSCore.Streams import * 10 | 11 | output_file = "out.wav" 12 | time = 5000 13 | 14 | sampleRate = 48000 15 | 16 | soundIn = WasapiLoopbackCapture() 17 | soundIn.Initialize() 18 | soundInSource = SoundInSource(soundIn) 19 | soundInSource.FillWithZeros = False 20 | convertedSource = soundInSource 21 | convertedSource = FluentExtensions.ChangeSampleRate(convertedSource, sampleRate) 22 | waveWriter = WaveWriter(output_file, convertedSource.WaveFormat) 23 | 24 | def capture(sender, event_args): 25 | buffer = Array.CreateInstance(Byte, convertedSource.WaveFormat.BytesPerSecond / 2) 26 | while True: 27 | read = convertedSource.Read(buffer, 0, buffer.Length) 28 | waveWriter.Write(buffer, 0, read) 29 | if read <= 0: 30 | break 31 | 32 | delegate = EventHandler(capture) 33 | soundInSource.DataAvailable += delegate 34 | 35 | soundIn.Start() 36 | Threading.Thread.Sleep(time) 37 | soundIn.Stop() 38 | -------------------------------------------------------------------------------- /mac/LoopbackCapture.py: -------------------------------------------------------------------------------- 1 | import pyaudio 2 | import wave 3 | 4 | def record_sounds(output_file="record.wav", time=0): 5 | FORMAT = pyaudio.paInt16 6 | CHANNELS = 2 7 | RATE = 44100 8 | CHUNK = 1024 9 | RECORD_SECONDS = time/1000 10 | WAVE_OUTPUT_FILENAME = output_file 11 | audio = pyaudio.PyAudio() 12 | stream = audio.open(format=FORMAT, channels=CHANNELS, rate=RATE, input=True, frames_per_buffer=CHUNK) 13 | frames = [] 14 | 15 | if time is not 0: 16 | for i in range(0, int(RATE / CHUNK * RECORD_SECONDS)): 17 | data = stream.read(CHUNK, False) 18 | frames.append(data) 19 | else: 20 | try: 21 | print("Press Ctrl+C to exit...") 22 | while True: 23 | data = stream.read(CHUNK, False) 24 | frames.append(data) 25 | except KeyboardInterrupt: 26 | pass 27 | except: 28 | return -1 29 | 30 | stream.stop_stream() 31 | stream.close() 32 | audio.terminate() 33 | 34 | wave_file = wave.open(WAVE_OUTPUT_FILENAME, 'wb') 35 | wave_file.setnchannels(CHANNELS) 36 | wave_file.setsampwidth(audio.get_sample_size(FORMAT)) 37 | wave_file.setframerate(RATE) 38 | wave_file.writeframes(b''.join(frames)) 39 | wave_file.close() 40 | 41 | return 0 42 | -------------------------------------------------------------------------------- /win32/csharp/LoopbackCapture.csproj: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | {36591122-E580-4FEC-86CE-FAE0CE926C0C} 7 | Exe 8 | LoopbackCapture 9 | LoopbackCapture 10 | 11 | 12 | 13 | ..\..\3rdparty\CSCore.dll 14 | True 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | -------------------------------------------------------------------------------- /win32/dll/LoopbackCapture.csproj: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | {36591122-E580-4FEC-86CE-FAE0CE926C0D} 7 | Library 8 | LoopbackCapture 9 | LoopbackCapture 10 | 11 | 12 | 13 | ..\..\3rdparty\CSCore.dll 14 | True 15 | 16 | 17 | ..\..\3rdparty\RGiesecke.DllExport.Metadata.dll 18 | True 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | -------------------------------------------------------------------------------- /win32/dll/LoopbackCapture.cs: -------------------------------------------------------------------------------- 1 | /* Some Code Fragments from: 2 | 1. https://stackoverflow.com/questions/18812224/c-sharp-recording-audio-from-soundcard - Florian R. 3 | 2. https://github.com/filoe/cscore/blob/master/Samples/RecordWithSpecificFormat/Program.cs - CSCore Source Code 4 | */ 5 | 6 | using System; 7 | using CSCore; 8 | using CSCore.SoundIn; 9 | using CSCore.Codecs.WAV; 10 | using CSCore.Streams; 11 | using System.Threading; 12 | using System.Runtime.InteropServices; 13 | using RGiesecke.DllExport; 14 | 15 | namespace LoopbackCapture 16 | { 17 | public class LoopbackCapture 18 | { 19 | [DllExport("Capture", CallingConvention = CallingConvention.Cdecl)] 20 | public static int Capture(string output_file, int time) 21 | { 22 | 23 | int sampleRate = 48000; 24 | int bitsPerSample = 24; 25 | 26 | 27 | //create a new soundIn instance 28 | using (WasapiCapture soundIn = new WasapiLoopbackCapture()) 29 | { 30 | 31 | //initialize the soundIn instance 32 | soundIn.Initialize(); 33 | 34 | //create a SoundSource around the the soundIn instance 35 | SoundInSource soundInSource = new SoundInSource(soundIn) { FillWithZeros = false }; 36 | 37 | //create a source, that converts the data provided by the soundInSource to any other format 38 | IWaveSource convertedSource = soundInSource 39 | .ChangeSampleRate(sampleRate) // sample rate 40 | .ToSampleSource() 41 | .ToWaveSource(bitsPerSample); //bits per sample 42 | 43 | //channels... 44 | using (convertedSource = convertedSource.ToStereo()) 45 | { 46 | 47 | //create a new wavefile 48 | using (WaveWriter waveWriter = new WaveWriter(output_file, convertedSource.WaveFormat)) 49 | { 50 | 51 | //register an event handler for the DataAvailable event of the soundInSource 52 | soundInSource.DataAvailable += (s, e) => 53 | { 54 | //read data from the converedSource 55 | byte[] buffer = new byte[convertedSource.WaveFormat.BytesPerSecond / 2]; 56 | int read; 57 | 58 | //keep reading as long as we still get some data 59 | while ((read = convertedSource.Read(buffer, 0, buffer.Length)) > 0) 60 | { 61 | //write the read data to a file 62 | waveWriter.Write(buffer, 0, read); 63 | } 64 | }; 65 | 66 | //start recording 67 | soundIn.Start(); 68 | 69 | //delay and keep recording 70 | Thread.Sleep(time); 71 | 72 | //stop recording 73 | soundIn.Stop(); 74 | } 75 | } 76 | } 77 | return 0; 78 | } 79 | } 80 | } 81 | -------------------------------------------------------------------------------- /win32/cpp/cleanup.h: -------------------------------------------------------------------------------- 1 | // cleanup.h 2 | 3 | class AudioClientStopOnExit { 4 | public: 5 | AudioClientStopOnExit(IAudioClient *p) : m_p(p) {} 6 | ~AudioClientStopOnExit() { 7 | HRESULT hr = m_p->Stop(); 8 | if (FAILED(hr)) { 9 | ERR(L"IAudioClient::Stop failed: hr = 0x%08x", hr); 10 | } 11 | } 12 | 13 | private: 14 | IAudioClient *m_p; 15 | }; 16 | 17 | class AvRevertMmThreadCharacteristicsOnExit { 18 | public: 19 | AvRevertMmThreadCharacteristicsOnExit(HANDLE hTask) : m_hTask(hTask) {} 20 | ~AvRevertMmThreadCharacteristicsOnExit() { 21 | if (!AvRevertMmThreadCharacteristics(m_hTask)) { 22 | ERR(L"AvRevertMmThreadCharacteristics failed: last error is %d", GetLastError()); 23 | } 24 | } 25 | private: 26 | HANDLE m_hTask; 27 | }; 28 | 29 | class CancelWaitableTimerOnExit { 30 | public: 31 | CancelWaitableTimerOnExit(HANDLE h) : m_h(h) {} 32 | ~CancelWaitableTimerOnExit() { 33 | if (!CancelWaitableTimer(m_h)) { 34 | ERR(L"CancelWaitableTimer failed: last error is %d", GetLastError()); 35 | } 36 | } 37 | private: 38 | HANDLE m_h; 39 | }; 40 | 41 | class CloseHandleOnExit { 42 | public: 43 | CloseHandleOnExit(HANDLE h) : m_h(h) {} 44 | ~CloseHandleOnExit() { 45 | if (!CloseHandle(m_h)) { 46 | ERR(L"CloseHandle failed: last error is %d", GetLastError()); 47 | } 48 | } 49 | 50 | private: 51 | HANDLE m_h; 52 | }; 53 | 54 | class CoTaskMemFreeOnExit { 55 | public: 56 | CoTaskMemFreeOnExit(PVOID p) : m_p(p) {} 57 | ~CoTaskMemFreeOnExit() { 58 | CoTaskMemFree(m_p); 59 | } 60 | 61 | private: 62 | PVOID m_p; 63 | }; 64 | 65 | class CoUninitializeOnExit { 66 | public: 67 | ~CoUninitializeOnExit() { 68 | CoUninitialize(); 69 | } 70 | }; 71 | 72 | class PropVariantClearOnExit { 73 | public: 74 | PropVariantClearOnExit(PROPVARIANT *p) : m_p(p) {} 75 | ~PropVariantClearOnExit() { 76 | HRESULT hr = PropVariantClear(m_p); 77 | if (FAILED(hr)) { 78 | ERR(L"PropVariantClear failed: hr = 0x%08x", hr); 79 | } 80 | } 81 | 82 | private: 83 | PROPVARIANT *m_p; 84 | }; 85 | 86 | class ReleaseOnExit { 87 | public: 88 | ReleaseOnExit(IUnknown *p) : m_p(p) {} 89 | ~ReleaseOnExit() { 90 | m_p->Release(); 91 | } 92 | 93 | private: 94 | IUnknown *m_p; 95 | }; 96 | 97 | class SetEventOnExit { 98 | public: 99 | SetEventOnExit(HANDLE h) : m_h(h) {} 100 | ~SetEventOnExit() { 101 | if (!SetEvent(m_h)) { 102 | ERR(L"SetEvent failed: last error is %d", GetLastError()); 103 | } 104 | } 105 | private: 106 | HANDLE m_h; 107 | }; 108 | 109 | class WaitForSingleObjectOnExit { 110 | public: 111 | WaitForSingleObjectOnExit(HANDLE h) : m_h(h) {} 112 | ~WaitForSingleObjectOnExit() { 113 | DWORD dwWaitResult = WaitForSingleObject(m_h, INFINITE); 114 | if (WAIT_OBJECT_0 != dwWaitResult) { 115 | ERR(L"WaitForSingleObject returned unexpected result 0x%08x, last error is %d", dwWaitResult, GetLastError()); 116 | } 117 | } 118 | 119 | private: 120 | HANDLE m_h; 121 | }; -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Loopback Capture 2 | 3 | [![GitHub license](https://img.shields.io/badge/license-MIT-blue.svg)](https://raw.githubusercontent.com/peitaosu/LoopbackCapture/master/LICENSE) 4 | 5 | ## What is Loopback Capture 6 | 7 | Loopback Capture is a tool which can be used to capture the loopback from audio devices. 8 | 9 | ## Build (Windows) 10 | 11 | The `win32\csharp` and `win32\dll` version require some packages, before you build it, you need to install these packages and put dll files to `3rdparty` folder: 12 | 13 | * CSCore - An advanced audio library, written in C#. 14 | * Unmanaged Exports - DllExport for .Net. 15 | 16 | Suggest to use NuGet to install them: 17 | 18 | ``` 19 | nuget install .\build\packages.config 20 | ``` 21 | 22 | Then in MSBuild Command Prompt: 23 | ``` 24 | > build.all.bat 25 | or 26 | > build.cpp.bat 27 | or 28 | > build.csharp.bat 29 | or 30 | > build.dll.bat 31 | ``` 32 | 33 | ## Install Soundflower (macOS) 34 | 35 | macOS not support to capture Loopback from device directly. The workaround is to route what is playing on the computer digitally back to the input without using a cable. 36 | 37 | Soundflower is a free open source system add-on for Mac computers that allows you to route what is playing on the computer digitally back to the input without using a cable. Set Soundflower as your system output device, then in Audacity, set Soundflower as your recording device. 38 | 39 | You can get compiled Soundflower kernel extension in here: https://github.com/mattingalls/Soundflower/releases 40 | 41 | About how to setup device, there is an example in Release Note please take a look. 42 | 43 | ## Install Avconv Tool (Linux) 44 | 45 | avconv is a part from "libav-tools" package which is support to record audio use command line with specific format specific duration and from specific device. 46 | 47 | ``` 48 | sudo apt-get install libav-tools 49 | ``` 50 | 51 | BTW, the pulsectl is required for `linux\LoopbackCapture.py`, use following command to install it: 52 | 53 | ``` 54 | pip install pulsectl 55 | ``` 56 | 57 | ## Usage 58 | 59 | ``` 60 | # CSharp 61 | 62 | > LoopbackCapture.exe