mirror of
https://github.com/cookiengineer/audacity
synced 2025-09-17 16:50:26 +02:00
Journal playback and recording utilities, not yet used
This commit is contained in:
parent
b8f22981ee
commit
af0aab83c3
@ -142,6 +142,8 @@ list( APPEND SOURCES
|
|||||||
HitTestResult.h
|
HitTestResult.h
|
||||||
ImageManipulation.cpp
|
ImageManipulation.cpp
|
||||||
ImageManipulation.h
|
ImageManipulation.h
|
||||||
|
Journal.cpp
|
||||||
|
Journal.h
|
||||||
KeyboardCapture.cpp
|
KeyboardCapture.cpp
|
||||||
KeyboardCapture.h
|
KeyboardCapture.h
|
||||||
LabelDialog.cpp
|
LabelDialog.cpp
|
||||||
|
331
src/Journal.cpp
Normal file
331
src/Journal.cpp
Normal file
@ -0,0 +1,331 @@
|
|||||||
|
/**********************************************************************
|
||||||
|
|
||||||
|
Audacity: A Digital Audio Editor
|
||||||
|
|
||||||
|
Journal.cpp
|
||||||
|
|
||||||
|
Paul Licameli
|
||||||
|
|
||||||
|
*******************************************************************//*!
|
||||||
|
|
||||||
|
\namespace Journal
|
||||||
|
\brief Facilities for recording and playback of sequences of user interaction
|
||||||
|
|
||||||
|
*//*******************************************************************/
|
||||||
|
|
||||||
|
#include "Journal.h"
|
||||||
|
|
||||||
|
#include <algorithm>
|
||||||
|
#include <unordered_map>
|
||||||
|
#include <wx/app.h>
|
||||||
|
#include <wx/filename.h>
|
||||||
|
#include <wx/textfile.h>
|
||||||
|
|
||||||
|
#include "MemoryX.h"
|
||||||
|
#include "Prefs.h"
|
||||||
|
|
||||||
|
namespace {
|
||||||
|
|
||||||
|
constexpr auto SeparatorCharacter = ',';
|
||||||
|
constexpr auto CommentCharacter = '#';
|
||||||
|
|
||||||
|
wxString sFileNameIn;
|
||||||
|
wxTextFile sFileIn;
|
||||||
|
|
||||||
|
wxString sLine;
|
||||||
|
// Invariant: the input file has not been opened, or else sLineNumber counts
|
||||||
|
// the number of lines consumed by the tokenizer
|
||||||
|
int sLineNumber = -1;
|
||||||
|
|
||||||
|
struct FlushingTextFile : wxTextFile {
|
||||||
|
// Flush output when the program quits, even if that makes an incomplete
|
||||||
|
// journal file without an exit
|
||||||
|
~FlushingTextFile() { if ( IsOpened() ) { Write(); Close(); } }
|
||||||
|
} sFileOut;
|
||||||
|
|
||||||
|
BoolSetting JournalEnabled{ L"/Journal/Enabled", false };
|
||||||
|
|
||||||
|
bool sError = false;
|
||||||
|
|
||||||
|
using Dictionary = std::unordered_map< wxString, Journal::Dispatcher >;
|
||||||
|
Dictionary &sDictionary()
|
||||||
|
{
|
||||||
|
static Dictionary theDictionary;
|
||||||
|
return theDictionary;
|
||||||
|
}
|
||||||
|
|
||||||
|
inline void NextIn()
|
||||||
|
{
|
||||||
|
if ( !sFileIn.Eof() ) {
|
||||||
|
sLine = sFileIn.GetNextLine();
|
||||||
|
++sLineNumber;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
wxArrayStringEx PeekTokens()
|
||||||
|
{
|
||||||
|
wxArrayStringEx tokens;
|
||||||
|
if ( Journal::IsReplaying() )
|
||||||
|
for ( ; !sFileIn.Eof(); NextIn() ) {
|
||||||
|
if ( sLine.StartsWith( CommentCharacter ) )
|
||||||
|
continue;
|
||||||
|
|
||||||
|
tokens = wxSplit( sLine, SeparatorCharacter );
|
||||||
|
if ( tokens.empty() )
|
||||||
|
// Ignore blank lines
|
||||||
|
continue;
|
||||||
|
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
return tokens;
|
||||||
|
}
|
||||||
|
|
||||||
|
constexpr auto VersionToken = wxT("Version");
|
||||||
|
|
||||||
|
// Numbers identifying the journal format version
|
||||||
|
int journalVersionNumbers[] = {
|
||||||
|
1
|
||||||
|
};
|
||||||
|
|
||||||
|
wxString VersionString()
|
||||||
|
{
|
||||||
|
wxString result;
|
||||||
|
for ( auto number : journalVersionNumbers ) {
|
||||||
|
auto str = wxString::Format( "%d", number );
|
||||||
|
result += ( result.empty() ? str : ( '.' + str ) );
|
||||||
|
}
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
|
//! True if value is an acceptable journal version number to be rerun
|
||||||
|
bool VersionCheck( const wxString &value )
|
||||||
|
{
|
||||||
|
auto strings = wxSplit( value, '.' );
|
||||||
|
std::vector<int> numbers;
|
||||||
|
for ( auto &string : strings ) {
|
||||||
|
long value;
|
||||||
|
if ( !string.ToCLong( &value ) )
|
||||||
|
return false;
|
||||||
|
numbers.push_back( value );
|
||||||
|
}
|
||||||
|
// OK if the static version number is not less than the given value
|
||||||
|
// Maybe in the future there will be a compatibility break
|
||||||
|
return !std::lexicographical_compare(
|
||||||
|
std::begin( journalVersionNumbers ), std::end( journalVersionNumbers ),
|
||||||
|
numbers.begin(), numbers.end() );
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
namespace Journal {
|
||||||
|
|
||||||
|
SyncException::SyncException()
|
||||||
|
{
|
||||||
|
// If the exception is ever constructed, cause nonzero program exit code
|
||||||
|
sError = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
SyncException::~SyncException() {}
|
||||||
|
|
||||||
|
void SyncException::DelayedHandlerAction()
|
||||||
|
{
|
||||||
|
// Simulate the application Exit menu item
|
||||||
|
wxCommandEvent evt{ wxEVT_MENU, wxID_EXIT };
|
||||||
|
wxTheApp->AddPendingEvent( evt );
|
||||||
|
}
|
||||||
|
|
||||||
|
bool RecordEnabled()
|
||||||
|
{
|
||||||
|
return JournalEnabled.Read();
|
||||||
|
}
|
||||||
|
|
||||||
|
bool SetRecordEnabled(bool value)
|
||||||
|
{
|
||||||
|
auto result = JournalEnabled.Write(value);
|
||||||
|
gPrefs->Flush();
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
|
bool IsRecording()
|
||||||
|
{
|
||||||
|
return sFileOut.IsOpened();
|
||||||
|
}
|
||||||
|
|
||||||
|
bool IsReplaying()
|
||||||
|
{
|
||||||
|
return sFileIn.IsOpened();
|
||||||
|
}
|
||||||
|
|
||||||
|
void SetInputFileName(const wxString &path)
|
||||||
|
{
|
||||||
|
sFileNameIn = path;
|
||||||
|
}
|
||||||
|
|
||||||
|
bool Begin( const FilePath &dataDir )
|
||||||
|
{
|
||||||
|
if ( !sError && !sFileNameIn.empty() ) {
|
||||||
|
wxFileName fName{ sFileNameIn };
|
||||||
|
fName.MakeAbsolute( dataDir );
|
||||||
|
const auto path = fName.GetFullPath();
|
||||||
|
sFileIn.Open( path );
|
||||||
|
if ( !sFileIn.IsOpened() )
|
||||||
|
sError = true;
|
||||||
|
else {
|
||||||
|
sLine = sFileIn.GetFirstLine();
|
||||||
|
sLineNumber = 0;
|
||||||
|
|
||||||
|
auto tokens = PeekTokens();
|
||||||
|
NextIn();
|
||||||
|
sError = !(
|
||||||
|
tokens.size() == 2 &&
|
||||||
|
tokens[0] == VersionToken &&
|
||||||
|
VersionCheck( tokens[1] )
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if ( !sError && RecordEnabled() ) {
|
||||||
|
wxFileName fName{ dataDir, "journal", "txt" };
|
||||||
|
const auto path = fName.GetFullPath();
|
||||||
|
sFileOut.Open( path );
|
||||||
|
if ( sFileOut.IsOpened() )
|
||||||
|
sFileOut.Clear();
|
||||||
|
else {
|
||||||
|
sFileOut.Create();
|
||||||
|
sFileOut.Open( path );
|
||||||
|
}
|
||||||
|
if ( !sFileOut.IsOpened() )
|
||||||
|
sError = true;
|
||||||
|
else {
|
||||||
|
// Generate a header
|
||||||
|
Comment( wxString::Format(
|
||||||
|
wxT("Journal recorded by %s on %s")
|
||||||
|
, wxGetUserName()
|
||||||
|
, wxDateTime::Now().Format()
|
||||||
|
) );
|
||||||
|
Output({ VersionToken, VersionString() });
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return !sError;
|
||||||
|
}
|
||||||
|
|
||||||
|
wxArrayStringEx GetTokens()
|
||||||
|
{
|
||||||
|
auto result = PeekTokens();
|
||||||
|
if ( !result.empty() ) {
|
||||||
|
NextIn();
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
throw SyncException{};
|
||||||
|
}
|
||||||
|
|
||||||
|
RegisteredCommand::RegisteredCommand(
|
||||||
|
const wxString &name, Dispatcher dispatcher )
|
||||||
|
{
|
||||||
|
if ( !sDictionary().insert( { name, dispatcher } ).second ) {
|
||||||
|
wxLogDebug( wxString::Format (
|
||||||
|
wxT("Duplicated registration of Journal command name %s"),
|
||||||
|
name
|
||||||
|
) );
|
||||||
|
// Cause failure of startup of journalling and graceful exit
|
||||||
|
sError = true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
bool Dispatch()
|
||||||
|
{
|
||||||
|
if ( sError )
|
||||||
|
// Don't repeatedly indicate error
|
||||||
|
// Do nothing
|
||||||
|
return false;
|
||||||
|
|
||||||
|
if ( !IsReplaying() )
|
||||||
|
return false;
|
||||||
|
|
||||||
|
// This will throw if no lines remain. A proper journal should exit the
|
||||||
|
// program before that happens.
|
||||||
|
auto words = GetTokens();
|
||||||
|
|
||||||
|
// Lookup dispatch function by the first field of the line
|
||||||
|
auto &table = sDictionary();
|
||||||
|
auto &name = words[0];
|
||||||
|
auto iter = table.find( name );
|
||||||
|
if ( iter == table.end() )
|
||||||
|
throw SyncException{};
|
||||||
|
|
||||||
|
// Pass all the fields including the command name to the function
|
||||||
|
if ( !iter->second( words ) )
|
||||||
|
throw SyncException{};
|
||||||
|
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
void Output( const wxString &string )
|
||||||
|
{
|
||||||
|
if ( IsRecording() )
|
||||||
|
sFileOut.AddLine( string );
|
||||||
|
}
|
||||||
|
|
||||||
|
void Output( const wxArrayString &strings )
|
||||||
|
{
|
||||||
|
if ( IsRecording() )
|
||||||
|
Output( ::wxJoin( strings, SeparatorCharacter ) );
|
||||||
|
}
|
||||||
|
|
||||||
|
void Output( std::initializer_list< const wxString > strings )
|
||||||
|
{
|
||||||
|
return Output( wxArrayStringEx( strings ) );
|
||||||
|
}
|
||||||
|
|
||||||
|
void Comment( const wxString &string )
|
||||||
|
{
|
||||||
|
if ( IsRecording() )
|
||||||
|
sFileOut.AddLine( CommentCharacter + string );
|
||||||
|
}
|
||||||
|
|
||||||
|
void Sync( const wxString &string )
|
||||||
|
{
|
||||||
|
if ( IsRecording() || IsReplaying() ) {
|
||||||
|
if ( IsRecording() )
|
||||||
|
sFileOut.AddLine( string );
|
||||||
|
if ( IsReplaying() ) {
|
||||||
|
if ( sFileIn.Eof() || sLine != string )
|
||||||
|
throw SyncException{};
|
||||||
|
NextIn();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void Sync( const wxArrayString &strings )
|
||||||
|
{
|
||||||
|
if ( IsRecording() || IsReplaying() ) {
|
||||||
|
auto string = ::wxJoin( strings, SeparatorCharacter );
|
||||||
|
Sync( string );
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void Sync( std::initializer_list< const wxString > strings )
|
||||||
|
{
|
||||||
|
return Sync( wxArrayStringEx( strings ) );
|
||||||
|
}
|
||||||
|
|
||||||
|
int GetExitCode()
|
||||||
|
{
|
||||||
|
// Unconsumed commands remaining in the input file is also an error condition.
|
||||||
|
if( !sError && !PeekTokens().empty() ) {
|
||||||
|
NextIn();
|
||||||
|
sError = true;
|
||||||
|
}
|
||||||
|
if ( sError ) {
|
||||||
|
// Return nonzero
|
||||||
|
// Returning the (1-based) line number at which the script failed is a
|
||||||
|
// simple way to communicate that information to the test driver script.
|
||||||
|
return sLineNumber ? sLineNumber : -1;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Return zero to mean all is well, the convention for command-line tools
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
111
src/Journal.h
Normal file
111
src/Journal.h
Normal file
@ -0,0 +1,111 @@
|
|||||||
|
/**********************************************************************
|
||||||
|
|
||||||
|
Audacity: A Digital Audio Editor
|
||||||
|
|
||||||
|
Journal.h
|
||||||
|
|
||||||
|
Paul Licameli
|
||||||
|
|
||||||
|
**********************************************************************/
|
||||||
|
|
||||||
|
#ifndef __AUDACITY_JOURNAL__
|
||||||
|
#define __AUDACITY_JOURNAL__
|
||||||
|
|
||||||
|
#include <functional>
|
||||||
|
#include <initializer_list>
|
||||||
|
#include "Identifier.h"
|
||||||
|
class wxArrayString;
|
||||||
|
class wxArrayStringEx;
|
||||||
|
class wxString;
|
||||||
|
|
||||||
|
#include "AudacityException.h"
|
||||||
|
|
||||||
|
// Whether the journalling feature is shown to the end user
|
||||||
|
#undef END_USER_JOURNALLING
|
||||||
|
|
||||||
|
namespace Journal
|
||||||
|
{
|
||||||
|
//\brief Whether recording is enabled; but recording will happen only if this
|
||||||
|
// was true at application start up
|
||||||
|
bool RecordEnabled();
|
||||||
|
|
||||||
|
//\brief Change the enablement of recording and store in preferences
|
||||||
|
//\return whether successful
|
||||||
|
bool SetRecordEnabled(bool value);
|
||||||
|
|
||||||
|
//\brief Whether actually recording.
|
||||||
|
// IsRecording() && IsReplaying() is possible
|
||||||
|
bool IsRecording();
|
||||||
|
|
||||||
|
//\brief Whether actually replaying.
|
||||||
|
// IsRecording() && IsReplaying() is possible
|
||||||
|
bool IsReplaying();
|
||||||
|
|
||||||
|
//\brief Set the played back journal file at start up
|
||||||
|
void SetInputFileName( const wxString &path );
|
||||||
|
|
||||||
|
//\brief Initialize playback if a file name has been set, and initialize
|
||||||
|
// output if recording is enabled.
|
||||||
|
// @param dataDir the output journal.txt will be in this directory, and the
|
||||||
|
// input file, if it was relative, is made absolute with respect to it
|
||||||
|
// @return true if successful
|
||||||
|
bool Begin( const FilePath &dataDir );
|
||||||
|
|
||||||
|
//\brief Consume next line from the input journal (skipping blank lines and
|
||||||
|
// comments) and tokenize it.
|
||||||
|
// Throws SyncException if no next line or not replaying
|
||||||
|
wxArrayStringEx GetTokens();
|
||||||
|
|
||||||
|
//\brief Type of a function that interprets a line of the input journal.
|
||||||
|
// It may indicate failure either by throwing SyncException or returning
|
||||||
|
// false (which will cause Journal::Dispatch to throw a SyncException)
|
||||||
|
using Dispatcher = std::function< bool(const wxArrayString &fields) >;
|
||||||
|
|
||||||
|
//\brief Associates a dispatcher with a keyword in the default dictionary.
|
||||||
|
// The keyword will also be the first field passed to the dispatcher. This
|
||||||
|
// struct is meant for static construction
|
||||||
|
struct RegisteredCommand{
|
||||||
|
explicit RegisteredCommand(
|
||||||
|
const wxString &name, Dispatcher dispatcher );
|
||||||
|
};
|
||||||
|
|
||||||
|
//\brief if playing back and commands remain, may execute one.
|
||||||
|
// May throw SyncException if playing back but none remain, or if other error
|
||||||
|
// conditions are encountered.
|
||||||
|
// Returns true if any command was dispatched
|
||||||
|
bool Dispatch();
|
||||||
|
|
||||||
|
//\brief write the strings to the output journal, if recording
|
||||||
|
// None of them may contain newlines
|
||||||
|
void Output( const wxString &string );
|
||||||
|
void Output( const wxArrayString &strings );
|
||||||
|
void Output( std::initializer_list< const wxString > strings );
|
||||||
|
|
||||||
|
//\brief if recording, emit a comment in the output journal that will have
|
||||||
|
// no effect on playback
|
||||||
|
void Comment( const wxString &string );
|
||||||
|
|
||||||
|
//\brief If recording, output the strings; if playing back, require
|
||||||
|
// identical strings. None of them may contain newlines
|
||||||
|
void Sync( const wxString &string );
|
||||||
|
void Sync( const wxArrayString &strings );
|
||||||
|
void Sync( std::initializer_list< const wxString > strings );
|
||||||
|
|
||||||
|
//\brief Get the value that the application will return to the command line
|
||||||
|
int GetExitCode();
|
||||||
|
|
||||||
|
//\brief thrown when playback of a journal doesn't match the recording
|
||||||
|
class SyncException : public AudacityException {
|
||||||
|
public:
|
||||||
|
SyncException();
|
||||||
|
~SyncException() override;
|
||||||
|
|
||||||
|
// The delayed handler action forces the program to quit gracefully,
|
||||||
|
// so that the test playback failure is prompty reported. This is
|
||||||
|
// unlike other AudacityExceptions that roll back the project and
|
||||||
|
// continue.
|
||||||
|
void DelayedHandlerAction() override;
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
#endif
|
Loading…
x
Reference in New Issue
Block a user