summaryrefslogtreecommitdiffstats
path: root/Examples/WhisperDesktop/CaptureDlg.cpp
diff options
context:
space:
mode:
Diffstat (limited to 'Examples/WhisperDesktop/CaptureDlg.cpp')
-rw-r--r--Examples/WhisperDesktop/CaptureDlg.cpp505
1 files changed, 505 insertions, 0 deletions
diff --git a/Examples/WhisperDesktop/CaptureDlg.cpp b/Examples/WhisperDesktop/CaptureDlg.cpp
new file mode 100644
index 0000000..f1030dd
--- /dev/null
+++ b/Examples/WhisperDesktop/CaptureDlg.cpp
@@ -0,0 +1,505 @@
+#include "stdafx.h"
+#include "CaptureDlg.h"
+
+HRESULT CaptureDlg::show()
+{
+ auto res = DoModal( nullptr );
+ if( res == -1 )
+ return HRESULT_FROM_WIN32( GetLastError() );
+ switch( res )
+ {
+ case IDC_BACK:
+ return SCREEN_MODEL;
+ case IDC_TRANSCRIBE:
+ return SCREEN_TRANSCRIBE;
+ }
+ return S_OK;
+}
+
+static const LPCTSTR regValDevice = L"captureDevice";
+static const LPCTSTR regValOutPath = L"captureTextFile";
+static const LPCTSTR regValOutFormat = L"captureTextFlags";
+
+enum struct CaptureDlg::eTextFlags : uint32_t
+{
+ Save = 1,
+ Append = 2,
+ Timestamps = 4,
+};
+
+LRESULT CaptureDlg::OnInitDialog( UINT nMessage, WPARAM wParam, LPARAM lParam, BOOL& bHandled )
+{
+ // First DDX call, hooks up variables to controls.
+ DoDataExchange( false );
+
+ languageSelector.initialize( m_hWnd, IDC_LANGUAGE, appState );
+ cbTranslate.initialize( m_hWnd, IDC_TRANSLATE, appState );
+ cbConsole.initialize( m_hWnd, IDC_CONSOLE, appState );
+
+ pendingState.initialize(
+ // Controls to disable while pending, re-enable afterwards
+ {
+ languageSelector,
+ cbCaptureDevice,
+ checkSave, checkAppend, checkTimestamps, transcribeOutputPath, transcribeOutputBrowse,
+ GetDlgItem( IDC_DEV_REFRESH ),
+ GetDlgItem( IDC_BACK ),
+ GetDlgItem( IDC_TRANSCRIBE ),
+ GetDlgItem( IDCANCEL ),
+ },
+ // Controls to show while pending, hide afterwards
+ {
+ voiceActivity, GetDlgItem( IDC_VOICE_ACTIVITY_LBL ),
+ transcribeActivity, GetDlgItem( IDC_TRANS_LBL ),
+ stalled, GetDlgItem( IDC_STALL_LBL ),
+ progressBar,
+ } );
+
+ stalled.setActiveColor( flipRgb( 0xffcc33 ) );
+
+ HRESULT hr = work.create( this );
+ if( FAILED( hr ) )
+ {
+ reportError( m_hWnd, L"CreateThreadpoolWork failed", nullptr, hr );
+ EndDialog( IDCANCEL );
+ }
+
+ listDevices();
+ selectDevice( appState.stringLoad( regValDevice ) );
+
+ constexpr uint32_t defaultFlags = (uint32_t)eTextFlags::Append;
+ uint32_t flags = appState.dwordLoad( regValOutFormat, defaultFlags );
+ if( flags & (uint32_t)eTextFlags::Save )
+ checkSave.SetCheck( BST_CHECKED );
+ if( flags & (uint32_t)eTextFlags::Append )
+ checkAppend.SetCheck( BST_CHECKED );
+ if( flags & (uint32_t)eTextFlags::Timestamps )
+ checkTimestamps.SetCheck( BST_CHECKED );
+
+ transcribeOutputPath.SetWindowText( appState.stringLoad( regValOutPath ) );
+ onSaveTextCheckbox();
+
+ appState.lastScreenSave( SCREEN_CAPTURE );
+ appState.setupIcon( this );
+ ATLVERIFY( CenterWindow() );
+ return 0;
+}
+
+HRESULT __stdcall CaptureDlg::listDevicesCallback( int len, const Whisper::sCaptureDevice* buffer, void* pv ) noexcept
+{
+ std::vector<sCaptureDevice>& devices = *( std::vector<sCaptureDevice> * )pv;
+ devices.resize( len );
+ for( int i = 0; i < len; i++ )
+ {
+ devices[ i ].displayName = buffer[ i ].displayName;
+ devices[ i ].endpoint = buffer[ i ].endpoint;
+ }
+ return S_OK;
+}
+
+bool CaptureDlg::listDevices()
+{
+ appState.mediaFoundation->listCaptureDevices( &listDevicesCallback, &devices );
+ cbCaptureDevice.ResetContent();
+ for( const auto& dev : devices )
+ cbCaptureDevice.AddString( dev.displayName );
+ return !devices.empty();
+}
+
+void CaptureDlg::onDeviceRefresh()
+{
+ // Save the current selection
+ const int curSel = cbCaptureDevice.GetCurSel();
+ CString str;
+ if( curSel >= 0 && curSel < (int)devices.size() )
+ str = std::move( devices[ curSel ].endpoint );
+
+ // Refresh
+ listDevices();
+
+ // Restore the selection
+ selectDevice( str );
+
+ const size_t len = devices.size();
+ if( len == 0 )
+ {
+ MessageBox( L"No capture devices found on this computer.\nIf you have a USB microphone, connect it to this PC,\nand press “refresh” button.",
+ L"Capture Devices", MB_OK | MB_ICONWARNING );
+ }
+ else
+ {
+ const char* suffix = ( len != 1 ) ? "s" : "";
+ str.Format( L"Detected %zu audio capture device%S.", len, suffix );
+ MessageBox( str, L"Capture Devices", MB_OK | MB_ICONINFORMATION );
+ }
+}
+
+bool CaptureDlg::selectDevice( LPCTSTR endpoint )
+{
+ if( nullptr != endpoint && 0 != *endpoint )
+ {
+ for( size_t i = 0; i < devices.size(); i++ )
+ {
+ if( devices[ i ].endpoint == endpoint )
+ {
+ cbCaptureDevice.SetCurSel( (int)i );
+ return true;
+ }
+ }
+ }
+
+ if( !devices.empty() )
+ cbCaptureDevice.SetCurSel( 0 );
+ return false;
+}
+
+void CaptureDlg::onSaveTextCheckbox()
+{
+ const BOOL enabled = ( checkSave.GetCheck() == BST_CHECKED );
+ std::array<HWND, 4> controls = { checkAppend, checkTimestamps, transcribeOutputPath, transcribeOutputBrowse };
+ for( HWND w : controls )
+ ::EnableWindow( w, enabled );
+}
+
+void CaptureDlg::onBrowseResult()
+{
+ LPCTSTR title = L"Output Text File";
+ LPCTSTR outputFilters = L"Text files (*.txt)\0*.txt\0\0";
+ CString path;
+ transcribeOutputPath.GetWindowText( path );
+ if( !getSaveFileName( m_hWnd, title, outputFilters, path ) )
+ return;
+
+ LPCTSTR ext = PathFindExtension( path );
+ if( 0 == *ext )
+ {
+ wchar_t* const buffer = path.GetBufferSetLength( path.GetLength() + 5 );
+ PathRenameExtension( buffer, L".txt" );
+ path.ReleaseBuffer();
+ }
+
+ transcribeOutputPath.SetWindowText( path );
+}
+
+CaptureDlg::eTextFlags CaptureDlg::getOutputFlags()
+{
+ uint32_t flags = 0;
+ if( checkSave.GetCheck() == BST_CHECKED )
+ flags |= (uint32_t)eTextFlags::Save;
+ if( checkAppend.GetCheck() == BST_CHECKED )
+ flags |= (uint32_t)eTextFlags::Append;
+ if( checkTimestamps.GetCheck() == BST_CHECKED )
+ flags |= (uint32_t)eTextFlags::Timestamps;
+ return (eTextFlags)flags;
+}
+
+void CaptureDlg::setPending( bool nowPending )
+{
+ pendingState.setPending( nowPending );
+ if( nowPending )
+ {
+ progressBar.SetMarquee( TRUE, 0 );
+ btnRunCapture.SetWindowText( L"Stop" );
+ }
+ else
+ {
+ progressBar.SetMarquee( FALSE, 0 );
+ btnRunCapture.SetWindowText( L"Capture" );
+ btnRunCapture.EnableWindow( TRUE );
+ captureRunning = false;
+ }
+}
+
+void CaptureDlg::onRunCapture()
+{
+ if( captureRunning )
+ {
+ threadState.stopRequested = true;
+ btnRunCapture.EnableWindow( FALSE );
+ return;
+ }
+
+ int dev = cbCaptureDevice.GetCurSel();
+ if( dev < 0 || dev >= (int)devices.size() )
+ {
+ showError( L"Please select a capture device", S_FALSE );
+ return;
+ }
+ threadState.endpoint = devices[ dev ].endpoint;
+ threadState.language = languageSelector.selectedLanguage();
+ threadState.translate = cbTranslate.checked();
+ if( isInvalidTranslate( m_hWnd, threadState.language, threadState.translate ) )
+ return;
+
+ threadState.flags = getOutputFlags();
+ if( (uint32_t)threadState.flags & (uint32_t)eTextFlags::Save )
+ {
+ transcribeOutputPath.GetWindowText( threadState.textOutputPath );
+ if( threadState.textOutputPath.GetLength() <= 0 )
+ {
+ showError( L"Please specify the output text file", S_FALSE );
+ return;
+ }
+ appState.stringStore( regValOutPath, threadState.textOutputPath );
+ }
+ else
+ cbConsole.ensureChecked();
+
+ languageSelector.saveSelection( appState );
+ cbTranslate.saveSelection( appState );
+ appState.stringStore( regValDevice, threadState.endpoint );
+ appState.dwordStore( regValOutFormat, (uint32_t)threadState.flags );
+
+ captureRunning = true;
+ threadState.errorMessage = L"";
+ threadState.stopRequested = false;
+ threadState.captureParams.minDuration = 7;
+ threadState.captureParams.maxDuration = 11;
+ setPending( true );
+ work.post();
+}
+
+void __declspec( noinline ) CaptureDlg::getThreadError()
+{
+ getLastError( threadState.errorMessage );
+}
+
+#define CHECK_EX( hr ) { const HRESULT __hr = ( hr ); if( FAILED( __hr ) ) { getThreadError(); return __hr; } }
+
+static HRESULT appendDate( CString& str, const SYSTEMTIME& time )
+{
+ constexpr DWORD dateFlags = DATE_LONGDATE;
+ int cc = GetDateFormatEx( LOCALE_NAME_USER_DEFAULT, dateFlags, &time, nullptr, nullptr, 0, nullptr );
+ if( 0 == cc )
+ return getLastHr();
+
+ const int oldLength = str.GetLength();
+ wchar_t* const buffer = str.GetBufferSetLength( oldLength + cc );
+ cc = GetDateFormatEx( LOCALE_NAME_USER_DEFAULT, dateFlags, &time, nullptr, buffer + oldLength, cc, nullptr );
+ if( 0 != cc )
+ {
+ str.ReleaseBuffer();
+ return S_OK;
+ }
+ HRESULT hr = getLastHr();
+ str.ReleaseBuffer();
+ return hr;
+}
+
+static HRESULT appendTime( CString& str, const SYSTEMTIME& time )
+{
+ constexpr DWORD timeFlags = 0;
+ int cc = GetTimeFormatEx( LOCALE_NAME_USER_DEFAULT, timeFlags, &time, nullptr, nullptr, 0 );
+ if( 0 == cc )
+ return getLastHr();
+
+ const int oldLength = str.GetLength();
+ wchar_t* const buffer = str.GetBufferSetLength( oldLength + cc );
+ cc = GetTimeFormatEx( LOCALE_NAME_USER_DEFAULT, timeFlags, &time, nullptr, buffer + oldLength, cc );
+ if( 0 != cc )
+ {
+ str.ReleaseBuffer();
+ return S_OK;
+ }
+ HRESULT hr = getLastHr();
+ str.ReleaseBuffer();
+ return hr;
+}
+
+static HRESULT printDateTime( CAtlFile& file )
+{
+ SYSTEMTIME time;
+ GetLocalTime( &time );
+
+ CString str;
+ str = L"==== Captured on ";
+ CHECK( appendDate( str, time ) );
+ str += L", ";
+ CHECK( appendTime( str, time ) );
+ str += L" ====\r\n";
+
+ CStringA u8;
+ makeUtf8( u8, str );
+ return file.Write( cstr( u8 ), (DWORD)u8.GetLength() );
+}
+
+inline HRESULT CaptureDlg::runCapture()
+{
+ clearLastError();
+ using namespace Whisper;
+ CComPtr<iAudioCapture> capture;
+ CHECK_EX( appState.mediaFoundation->openCaptureDevice( threadState.endpoint, threadState.captureParams, &capture ) );
+
+ HRESULT hr;
+ CAtlFile file;
+ const uint32_t flags = (uint32_t)threadState.flags;
+ if( flags & (uint32_t)eTextFlags::Save )
+ {
+ const bool append = 0 != ( flags & (uint32_t)eTextFlags::Append );
+ const DWORD creation = append ? OPEN_ALWAYS : CREATE_ALWAYS;
+ hr = file.Create( threadState.textOutputPath, GENERIC_WRITE, FILE_SHARE_READ, creation );
+ if( FAILED( hr ) )
+ {
+ threadState.errorMessage = L"Unable to create the output text file";
+ return hr;
+ }
+ if( append )
+ {
+ ULONGLONG size;
+ CHECK( file.GetSize( size ) );
+ if( size == 0 )
+ CHECK( writeUtf8Bom( file ) )
+ else
+ CHECK( file.Seek( 0, SEEK_END ) );
+ }
+ else
+ {
+ CHECK( writeUtf8Bom( file ) );
+ }
+
+ if( flags & (uint32_t)eTextFlags::Timestamps )
+ CHECK( printDateTime( file ) );
+
+ threadState.file = &file;
+ }
+ else
+ threadState.file = nullptr;
+
+ CComPtr<iContext> context;
+ CHECK_EX( appState.model->createContext( &context ) );
+
+ sFullParams fullParams;
+ CHECK_EX( context->fullDefaultParams( eSamplingStrategy::Greedy, &fullParams ) );
+ fullParams.language = threadState.language;
+ fullParams.setFlag( eFullParamsFlags::Translate, threadState.translate );
+ fullParams.resetFlag( eFullParamsFlags::PrintRealtime );
+ fullParams.new_segment_callback = &newSegmentCallback;
+ fullParams.new_segment_callback_user_data = this;
+
+ sCaptureCallbacks callbacks;
+ callbacks.shouldCancel = &cbCancel;
+ callbacks.captureStatus = &cbStatus;
+ callbacks.pv = this;
+
+ CHECK_EX( context->runCapture( fullParams, callbacks, capture ) );
+ threadState.file = nullptr;
+
+ context->timingsPrint();
+ return S_OK;
+}
+
+void __stdcall CaptureDlg::poolCallback() noexcept
+{
+ const HRESULT hr = runCapture();
+ PostMessage( WM_CALLBACK_COMPLETION, hr );
+}
+
+void CaptureDlg::showError( LPCTSTR text, HRESULT hr )
+{
+ reportError( m_hWnd, text, L"Capture failed", hr );
+}
+
+LRESULT CaptureDlg::onThreadQuit( UINT nMessage, WPARAM wParam, LPARAM lParam, BOOL& bHandled )
+{
+ setPending( false );
+
+ const HRESULT hr = (HRESULT)wParam;
+ if( FAILED( hr ) )
+ {
+ LPCTSTR failMessage = L"Capture failed";
+
+ if( threadState.errorMessage.GetLength() > 0 )
+ {
+ CString tmp = failMessage;
+ tmp += L"\n";
+ tmp += threadState.errorMessage;
+ showError( tmp, hr );
+ }
+ else
+ showError( failMessage, hr );
+
+ return 0;
+ }
+ else
+ {
+ if( (uint32_t)threadState.flags & (uint32_t)eTextFlags::Save )
+ ShellExecute( NULL, L"open", threadState.textOutputPath, NULL, NULL, SW_SHOW );
+ }
+
+ return 0;
+}
+
+LRESULT CaptureDlg::onThreadStatus( UINT nMessage, WPARAM wParam, LPARAM lParam, BOOL& bHandled )
+{
+ using namespace Whisper;
+ const uint8_t newStatus = (uint8_t)wParam;
+ // Update the GUI
+ voiceActivity.setActive( 0 != ( newStatus & (uint8_t)eCaptureStatus::Voice ) );
+ transcribeActivity.setActive( 0 != ( newStatus & (uint8_t)eCaptureStatus::Transcribing ) );
+ stalled.setActive( 0 != ( newStatus & (uint8_t)eCaptureStatus::Stalled ) );
+ return 0;
+}
+
+HRESULT __stdcall CaptureDlg::cbCancel( void* pv ) noexcept
+{
+ CaptureDlg& dialog = *(CaptureDlg*)pv;
+ return dialog.threadState.stopRequested ? S_OK : S_FALSE;
+}
+
+HRESULT __stdcall CaptureDlg::cbStatus( void* pv, Whisper::eCaptureStatus status ) noexcept
+{
+ CaptureDlg& dialog = *(CaptureDlg*)pv;
+ if( dialog.PostMessage( WM_CALLBACK_STATUS, (uint8_t)status ) )
+ return S_OK;
+ return getLastHr();
+}
+
+HRESULT __cdecl CaptureDlg::newSegmentCallback( Whisper::iContext* ctx, uint32_t n_new, void* user_data ) noexcept
+{
+ using namespace Whisper;
+ CComPtr<iTranscribeResult> result;
+ const eResultFlags flags = eResultFlags::Timestamps | eResultFlags::Tokens;
+ CHECK( ctx->getResults( flags, &result ) );
+ CHECK( logNewSegments( result, n_new ) );
+
+ CaptureDlg& dialog = *(CaptureDlg*)user_data;
+ return dialog.appendTextFile( result, n_new );
+}
+
+HRESULT CaptureDlg::appendTextFile( Whisper::iTranscribeResult* results, uint32_t newSegments )
+{
+ if( nullptr == threadState.file || 0 == newSegments )
+ return S_OK;
+
+ using namespace Whisper;
+ sTranscribeLength length;
+ CHECK( results->getSize( length ) );
+
+ const size_t len = length.countSegments;
+ size_t i = len - newSegments;
+
+ const sSegment* const segments = results->getSegments();
+ CStringA str;
+ for( ; i < len; i++ )
+ {
+ const sSegment& seg = segments[ i ];
+ if( 0 != ( (uint32_t)threadState.flags & (uint32_t)eTextFlags::Timestamps ) )
+ {
+ str = "[";
+ printTimeStamp( str, seg.time.begin );
+ str += " --> ";
+ printTimeStamp( str, seg.time.end );
+ str += "] ";
+ }
+ else
+ str = "";
+
+ str += seg.text;
+ str += "\r\n";
+
+ CHECK( threadState.file->Write( cstr( str ), (DWORD)str.GetLength() ) );
+ }
+
+ CHECK( threadState.file->Flush() );
+ return S_OK;
+} \ No newline at end of file