Merge remote-tracking branch 'origin/rr1-maint' into rr1-maint
This commit is contained in:
commit
1297f683e0
12 changed files with 649 additions and 100 deletions
2
VERSION
2
VERSION
|
@ -1 +1 @@
|
|||
1-Δ15ε3
|
||||
1-Δ16
|
|
@ -413,6 +413,27 @@ public:
|
|||
* Call on_dumper_update on on all objects.
|
||||
*/
|
||||
static void do_dumper_update() throw();
|
||||
/**
|
||||
* Notify about changes to voice streams.
|
||||
*
|
||||
* Default implementation does nothing.
|
||||
*/
|
||||
virtual void on_voice_stream_change();
|
||||
/**
|
||||
* Call on_voice_stream_change on all objects.
|
||||
*/
|
||||
static void do_voice_stream_change() throw();
|
||||
/**
|
||||
* Notify about changes to subtitles.
|
||||
*
|
||||
* Default implementation does nothing.
|
||||
*/
|
||||
virtual void on_subtitle_change();
|
||||
/**
|
||||
* Call on_subtitle_change on all objects.
|
||||
*/
|
||||
static void do_subtitle_change() throw();
|
||||
|
||||
protected:
|
||||
/**
|
||||
* Call to indicate this target is interested in sound sample data.
|
||||
|
|
117
manual.lyx
117
manual.lyx
|
@ -2122,6 +2122,67 @@ Returns bitwise OR of 1 left shifted by bit1 places, 1 left shifted by bit2
|
|||
As special value, nil argument is no-op.
|
||||
\end_layout
|
||||
|
||||
\begin_layout Subsubsection
|
||||
bit.test_any(number a, number b)
|
||||
\end_layout
|
||||
|
||||
\begin_layout Standard
|
||||
Is there a common set bit in a and b?
|
||||
\end_layout
|
||||
|
||||
\begin_layout Subsubsection
|
||||
bit.test_all(number a, number b)
|
||||
\end_layout
|
||||
|
||||
\begin_layout Standard
|
||||
Are all set bits in b also set in a?
|
||||
\end_layout
|
||||
|
||||
\begin_layout Subsubsection
|
||||
bit.popcount(number a)
|
||||
\end_layout
|
||||
|
||||
\begin_layout Standard
|
||||
Population count of a.
|
||||
\end_layout
|
||||
|
||||
\begin_layout Subsubsection
|
||||
bit.clshift(number a, number b, [number amount,[number bits]])
|
||||
\end_layout
|
||||
|
||||
\begin_layout Standard
|
||||
Does chained left shift on a, b by amount positions, assuming numbers to
|
||||
be of specified number of bits.
|
||||
\end_layout
|
||||
|
||||
\begin_layout Subsubsection
|
||||
bit.crshift(number a, number b, [number amount,[number bits]])
|
||||
\end_layout
|
||||
|
||||
\begin_layout Standard
|
||||
Does chained right shift on a, b by amount positions, assuming numbers to
|
||||
be of specified number of bits.
|
||||
\end_layout
|
||||
|
||||
\begin_layout Subsubsection
|
||||
bit.flagdecode(number a, number bits, [string on, [string off]])
|
||||
\end_layout
|
||||
|
||||
\begin_layout Standard
|
||||
Return string of length bits where ith character is ith character of on
|
||||
if bit i is on, otherwise ith character of off.
|
||||
Out of range reads give last character, or '*'/'-' if empty.
|
||||
\end_layout
|
||||
|
||||
\begin_layout Subsubsection
|
||||
bit.rflagdecode(number a, number bits, [string on, [string off]])
|
||||
\end_layout
|
||||
|
||||
\begin_layout Standard
|
||||
Like bit.flagdecode, but outputs the string in the opposite order (most significa
|
||||
nt bit first).
|
||||
\end_layout
|
||||
|
||||
\begin_layout Subsection
|
||||
Table gui:
|
||||
\end_layout
|
||||
|
@ -7343,5 +7404,61 @@ Compensate for nuts bsnes superscope/justifier handling
|
|||
Lua: Fix bit.extract boolean handling
|
||||
\end_layout
|
||||
|
||||
\begin_layout Subsection
|
||||
rr1-delta16
|
||||
\end_layout
|
||||
|
||||
\begin_layout Itemize
|
||||
Stop at movie end: Don't off-by-one
|
||||
\end_layout
|
||||
|
||||
\begin_layout Itemize
|
||||
Fix crash closing lsnes with voice playback active.
|
||||
\end_layout
|
||||
|
||||
\begin_layout Itemize
|
||||
Import/Export OggOpus for commentary tracks
|
||||
\end_layout
|
||||
|
||||
\begin_layout Itemize
|
||||
16-button controllers.
|
||||
\end_layout
|
||||
|
||||
\begin_layout Itemize
|
||||
Don't show nonexistent controllers in input display
|
||||
\end_layout
|
||||
|
||||
\begin_layout Itemize
|
||||
Set voice record/playback volume from UI
|
||||
\end_layout
|
||||
|
||||
\begin_layout Itemize
|
||||
Patches for gambatte SVN364.
|
||||
\end_layout
|
||||
|
||||
\begin_layout Itemize
|
||||
Load markup (if exists) even without gamepack file.
|
||||
\end_layout
|
||||
|
||||
\begin_layout Itemize
|
||||
Screen rotation & flipping
|
||||
\end_layout
|
||||
|
||||
\begin_layout Itemize
|
||||
Lua: Some new bit functions
|
||||
\end_layout
|
||||
|
||||
\begin_layout Itemize
|
||||
Auto-refresh voice streams on change.
|
||||
\end_layout
|
||||
|
||||
\begin_layout Itemize
|
||||
Auto-refresh subtitles on change & new subtitle editor.
|
||||
\end_layout
|
||||
|
||||
\begin_layout Itemize
|
||||
Fix music volume adjustment.
|
||||
\end_layout
|
||||
|
||||
\end_body
|
||||
\end_document
|
||||
|
|
65
manual.txt
65
manual.txt
|
@ -1047,6 +1047,43 @@ Returns bitwise OR of 1 left shifted by bit1 places, 1 left
|
|||
shifted by bit2 places and so on. As special value, nil argument
|
||||
is no-op.
|
||||
|
||||
8.2.12 bit.test_any(number a, number b)
|
||||
|
||||
Is there a common set bit in a and b?
|
||||
|
||||
8.2.13 bit.test_all(number a, number b)
|
||||
|
||||
Are all set bits in b also set in a?
|
||||
|
||||
8.2.14 bit.popcount(number a)
|
||||
|
||||
Population count of a.
|
||||
|
||||
8.2.15 bit.clshift(number a, number b, [number amount,[number
|
||||
bits]])
|
||||
|
||||
Does chained left shift on a, b by amount positions, assuming
|
||||
numbers to be of specified number of bits.
|
||||
|
||||
8.2.16 bit.crshift(number a, number b, [number amount,[number
|
||||
bits]])
|
||||
|
||||
Does chained right shift on a, b by amount positions, assuming
|
||||
numbers to be of specified number of bits.
|
||||
|
||||
8.2.17 bit.flagdecode(number a, number bits, [string on, [string
|
||||
off]])
|
||||
|
||||
Return string of length bits where ith character is ith character
|
||||
of on if bit i is on, otherwise ith character of off. Out of
|
||||
range reads give last character, or '*'/'-' if empty.
|
||||
|
||||
8.2.18 bit.rflagdecode(number a, number bits, [string on, [string
|
||||
off]])
|
||||
|
||||
Like bit.flagdecode, but outputs the string in the opposite order
|
||||
(most significant bit first).
|
||||
|
||||
8.3 Table gui:
|
||||
|
||||
Most of these functions can only be called in on_paint and
|
||||
|
@ -3616,3 +3653,31 @@ set-axis joystick0axis19 disabled
|
|||
|
||||
• Lua: Fix bit.extract boolean handling
|
||||
|
||||
18.76 rr1-delta16
|
||||
|
||||
• Stop at movie end: Don't off-by-one
|
||||
|
||||
• Fix crash closing lsnes with voice playback active.
|
||||
|
||||
• Import/Export OggOpus for commentary tracks
|
||||
|
||||
• 16-button controllers.
|
||||
|
||||
• Don't show nonexistent controllers in input display
|
||||
|
||||
• Set voice record/playback volume from UI
|
||||
|
||||
• Patches for gambatte SVN364.
|
||||
|
||||
• Load markup (if exists) even without gamepack file.
|
||||
|
||||
• Screen rotation & flipping
|
||||
|
||||
• Lua: Some new bit functions
|
||||
|
||||
• Auto-refresh voice streams on change.
|
||||
|
||||
• Auto-refresh subtitles on change & new subtitle editor.
|
||||
|
||||
• Fix music volume adjustment.
|
||||
|
||||
|
|
|
@ -538,3 +538,35 @@ void information_dispatch::do_dumper_update() throw()
|
|||
END_EH_BLOCK(i, "on_dumper_update");
|
||||
}
|
||||
}
|
||||
|
||||
void information_dispatch::on_voice_stream_change()
|
||||
{
|
||||
//Do nothing.
|
||||
}
|
||||
|
||||
void information_dispatch::do_voice_stream_change() throw()
|
||||
{
|
||||
if(in_global_ctors())
|
||||
return;
|
||||
for(auto& i : dispatch()) {
|
||||
START_EH_BLOCK
|
||||
i->on_voice_stream_change();
|
||||
END_EH_BLOCK(i, "on_voice_stream_change");
|
||||
}
|
||||
}
|
||||
|
||||
void information_dispatch::on_subtitle_change()
|
||||
{
|
||||
//Do nothing.
|
||||
}
|
||||
|
||||
void information_dispatch::do_subtitle_change() throw()
|
||||
{
|
||||
if(in_global_ctors())
|
||||
return;
|
||||
for(auto& i : dispatch()) {
|
||||
START_EH_BLOCK
|
||||
i->on_subtitle_change();
|
||||
END_EH_BLOCK(i, "on_subtitle_change");
|
||||
}
|
||||
}
|
||||
|
|
|
@ -9,6 +9,7 @@
|
|||
#include "library/ogg.hpp"
|
||||
#include "core/audioapi.hpp"
|
||||
#include "core/command.hpp"
|
||||
#include "core/dispatch.hpp"
|
||||
#include "core/framerate.hpp"
|
||||
#include "core/inthread.hpp"
|
||||
#include "core/keymapper.hpp"
|
||||
|
@ -1669,6 +1670,7 @@ out:
|
|||
messages << "Can't add stream: " << e.what() << std::endl;
|
||||
active_stream->put_ref();
|
||||
}
|
||||
information_dispatch::do_voice_stream_change();
|
||||
} else
|
||||
active_stream->put_ref();
|
||||
active_stream = NULL;
|
||||
|
@ -1872,6 +1874,7 @@ namespace
|
|||
}
|
||||
s->put_ref();
|
||||
current_collection->delete_stream(id);
|
||||
information_dispatch::do_voice_stream_change();
|
||||
messages << "Deleted stream #" << id << "." << std::endl;
|
||||
});
|
||||
|
||||
|
@ -1921,6 +1924,7 @@ namespace
|
|||
}
|
||||
s->put_ref();
|
||||
current_collection->alter_stream_timebase(id, tbase);
|
||||
information_dispatch::do_voice_stream_change();
|
||||
messages << "Timebase of stream #" << id << " is now " << (tbase / 48000.0) << "s"
|
||||
<< std::endl;
|
||||
});
|
||||
|
@ -1953,6 +1957,7 @@ namespace
|
|||
throw;
|
||||
}
|
||||
st->unlock(); //Not locked.
|
||||
information_dispatch::do_voice_stream_change();
|
||||
messages << "Imported stream (" << st->length() / 48000.0 << "s) as ID #" << id << std::endl;
|
||||
}
|
||||
|
||||
|
@ -2058,6 +2063,7 @@ namespace
|
|||
if(current_collection)
|
||||
delete current_collection;
|
||||
current_collection = newc;
|
||||
information_dispatch::do_voice_stream_change();
|
||||
messages << "Loaded '" << x << "'" << std::endl;
|
||||
});
|
||||
|
||||
|
@ -2068,6 +2074,7 @@ namespace
|
|||
if(current_collection)
|
||||
delete current_collection;
|
||||
current_collection = NULL;
|
||||
information_dispatch::do_voice_stream_change();
|
||||
messages << "Collection unloaded" << std::endl;
|
||||
});
|
||||
|
||||
|
@ -2160,6 +2167,7 @@ uint64_t voicesub_import_stream(uint64_t ts, const std::string& filename, extern
|
|||
throw;
|
||||
}
|
||||
st->unlock(); //Not locked.
|
||||
information_dispatch::do_voice_stream_change();
|
||||
return id;
|
||||
}
|
||||
|
||||
|
@ -2169,6 +2177,7 @@ void voicesub_delete_stream(uint64_t id)
|
|||
if(!current_collection)
|
||||
throw std::runtime_error("No collection loaded");
|
||||
current_collection->delete_stream(id);
|
||||
information_dispatch::do_voice_stream_change();
|
||||
}
|
||||
|
||||
void voicesub_export_superstream(const std::string& filename)
|
||||
|
@ -2192,6 +2201,7 @@ void voicesub_load_collection(const std::string& filename)
|
|||
if(current_collection)
|
||||
delete current_collection;
|
||||
current_collection = newc;
|
||||
information_dispatch::do_voice_stream_change();
|
||||
}
|
||||
|
||||
void voicesub_unload_collection()
|
||||
|
@ -2200,6 +2210,7 @@ void voicesub_unload_collection()
|
|||
if(current_collection)
|
||||
delete current_collection;
|
||||
current_collection = NULL;
|
||||
information_dispatch::do_voice_stream_change();
|
||||
}
|
||||
|
||||
void voicesub_alter_timebase(uint64_t id, uint64_t ts)
|
||||
|
@ -2208,6 +2219,7 @@ void voicesub_alter_timebase(uint64_t id, uint64_t ts)
|
|||
if(!current_collection)
|
||||
throw std::runtime_error("No collection loaded");
|
||||
current_collection->alter_stream_timebase(id, ts);
|
||||
information_dispatch::do_voice_stream_change();
|
||||
}
|
||||
|
||||
double voicesub_ts_seconds(uint64_t ts)
|
||||
|
|
|
@ -1,4 +1,6 @@
|
|||
#include "core/command.hpp"
|
||||
#include "core/dispatch.hpp"
|
||||
#include "core/framebuffer.hpp"
|
||||
#include "core/moviedata.hpp"
|
||||
#include "core/subtitles.hpp"
|
||||
#include "core/window.hpp"
|
||||
|
@ -109,6 +111,8 @@ namespace
|
|||
our_movie.subtitles.erase(key);
|
||||
else
|
||||
our_movie.subtitles[key] = s_unescape(text);
|
||||
information_dispatch::do_subtitle_change();
|
||||
redraw_framebuffer();
|
||||
});
|
||||
|
||||
function_ptr_command<> list_subtitle("list-subtitle", "List the subtitles",
|
||||
|
@ -229,4 +233,6 @@ void set_subtitle_for(uint64_t f, uint64_t l, const std::string& x)
|
|||
our_movie.subtitles.erase(key);
|
||||
else
|
||||
our_movie.subtitles[key] = s_unescape(x);
|
||||
information_dispatch::do_subtitle_change();
|
||||
redraw_framebuffer();
|
||||
}
|
||||
|
|
|
@ -639,8 +639,6 @@ void platform::queue(const std::string& c) throw(std::bad_alloc)
|
|||
|
||||
void platform::queue(void (*f)(void* arg), void* arg, bool sync) throw(std::bad_alloc)
|
||||
{
|
||||
if(sync && queue_synchronous_fn_warning)
|
||||
std::cerr << "WARNING: Synchronous queue in callback to UI, this may deadlock!" << std::endl;
|
||||
init_threading();
|
||||
mutex::holder h(*queue_lock);
|
||||
++next_function;
|
||||
|
|
106
src/lua/bit.cpp
106
src/lua/bit.cpp
|
@ -1,4 +1,5 @@
|
|||
#include "lua/internal.hpp"
|
||||
#include "library/minmax.hpp"
|
||||
|
||||
#define BITWISE_BITS 48
|
||||
#define BITWISE_MASK ((1ULL << (BITWISE_BITS)) - 1)
|
||||
|
@ -133,6 +134,111 @@ namespace
|
|||
return 1;
|
||||
});
|
||||
|
||||
function_ptr_luafun lua_testany("bit.test_any", [](lua_State* LS, const std::string& fname) -> int {
|
||||
uint64_t a = get_numeric_argument<uint64_t>(LS, 1, fname.c_str());
|
||||
uint64_t b = get_numeric_argument<uint64_t>(LS, 2, fname.c_str());
|
||||
lua_pushboolean(LS, (a & b) != 0);
|
||||
return 1;
|
||||
});
|
||||
|
||||
function_ptr_luafun lua_testall("bit.test_all", [](lua_State* LS, const std::string& fname) -> int {
|
||||
uint64_t a = get_numeric_argument<uint64_t>(LS, 1, fname.c_str());
|
||||
uint64_t b = get_numeric_argument<uint64_t>(LS, 2, fname.c_str());
|
||||
lua_pushboolean(LS, (a & b) == b);
|
||||
return 1;
|
||||
});
|
||||
|
||||
int poptable[] = {0, 1, 1, 2, 1, 2, 2, 3, 1, 2, 2, 3, 2, 3, 3, 4};
|
||||
|
||||
int popcount(uint64_t x)
|
||||
{
|
||||
int c = 0;
|
||||
for(unsigned i = 0; i < 16; i++) {
|
||||
c += poptable[x & 15];
|
||||
x >>= 4;
|
||||
}
|
||||
return c;
|
||||
}
|
||||
|
||||
function_ptr_luafun lua_popcount("bit.popcount", [](lua_State* LS, const std::string& fname) -> int {
|
||||
uint64_t a = get_numeric_argument<uint64_t>(LS, 1, fname.c_str());
|
||||
lua_pushnumber(LS,popcount(a));
|
||||
return 1;
|
||||
});
|
||||
|
||||
function_ptr_luafun lua_clshift("bit.clshift", [](lua_State* LS, const std::string& fname) -> int {
|
||||
unsigned amount = 1;
|
||||
unsigned bits = 48;
|
||||
uint64_t a = get_numeric_argument<uint64_t>(LS, 1, fname.c_str());
|
||||
uint64_t b = get_numeric_argument<uint64_t>(LS, 2, fname.c_str());
|
||||
get_numeric_argument(LS, 3, amount, fname.c_str());
|
||||
get_numeric_argument(LS, 4, bits, fname.c_str());
|
||||
uint64_t mask = ((1ULL << bits) - 1);
|
||||
a &= mask;
|
||||
b &= mask;
|
||||
a <<= amount;
|
||||
a &= mask;
|
||||
a |= (b >> (bits - amount));
|
||||
b <<= amount;
|
||||
b &= mask;
|
||||
lua_pushnumber(LS, a);
|
||||
lua_pushnumber(LS, b);
|
||||
return 2;
|
||||
});
|
||||
|
||||
function_ptr_luafun lua_crshift("bit.crshift", [](lua_State* LS, const std::string& fname) -> int {
|
||||
unsigned amount = 1;
|
||||
unsigned bits = 48;
|
||||
uint64_t a = get_numeric_argument<uint64_t>(LS, 1, fname.c_str());
|
||||
uint64_t b = get_numeric_argument<uint64_t>(LS, 2, fname.c_str());
|
||||
get_numeric_argument(LS, 3, amount, fname.c_str());
|
||||
get_numeric_argument(LS, 4, bits, fname.c_str());
|
||||
uint64_t mask = ((1ULL << bits) - 1);
|
||||
a &= mask;
|
||||
b &= mask;
|
||||
b >>= amount;
|
||||
b |= (a << (bits - amount));
|
||||
b &= mask;
|
||||
a >>= amount;
|
||||
lua_pushnumber(LS, a);
|
||||
lua_pushnumber(LS, b);
|
||||
return 2;
|
||||
});
|
||||
|
||||
int flagdecode_core(lua_State* LS, const std::string& fname, bool reverse)
|
||||
{
|
||||
uint64_t a = get_numeric_argument<uint64_t>(LS, 1, fname.c_str());
|
||||
uint64_t b = get_numeric_argument<uint64_t>(LS, 2, fname.c_str());
|
||||
std::string on, off;
|
||||
if(lua_type(LS, 3) == LUA_TSTRING)
|
||||
on = get_string_argument(LS, 3, fname.c_str());
|
||||
if(lua_type(LS, 4) == LUA_TSTRING)
|
||||
off = get_string_argument(LS, 4, fname.c_str());
|
||||
size_t onl = on.length();
|
||||
size_t offl = off.length();
|
||||
char onc = onl ? on[onl - 1] : '*';
|
||||
char offc = offl ? off[offl - 1] : '-';
|
||||
char buffer[65];
|
||||
unsigned i;
|
||||
size_t bias = min(b, (uint64_t)64) - 1;
|
||||
for(i = 0; i < 64 && i < b; i++) {
|
||||
char onc2 = (i < onl) ? on[i] : onc;
|
||||
char offc2 = (i < offl) ? off[i] : offc;
|
||||
buffer[reverse ? (bias - i) : i] = ((a >> i) & 1) ? onc2 : offc2;
|
||||
}
|
||||
buffer[i] = '\0';
|
||||
lua_pushstring(LS, buffer);
|
||||
return 1;
|
||||
}
|
||||
|
||||
function_ptr_luafun lua_flagdecode("bit.flagdecode", [](lua_State* LS, const std::string& fname) -> int {
|
||||
return flagdecode_core(LS, fname, false);
|
||||
});
|
||||
|
||||
function_ptr_luafun lua_rflagdecode("bit.rflagdecode", [](lua_State* LS, const std::string& fname) -> int {
|
||||
return flagdecode_core(LS, fname, true);
|
||||
});
|
||||
|
||||
lua_symmetric_bitwise<combine_none, BITWISE_MASK> bit_none("bit.none");
|
||||
lua_symmetric_bitwise<combine_none, BITWISE_MASK> bit_bnot("bit.bnot");
|
||||
lua_symmetric_bitwise<combine_any, 0> bit_any("bit.any");
|
||||
|
|
|
@ -10,57 +10,204 @@
|
|||
#include <wx/radiobut.h>
|
||||
|
||||
#include "library/string.hpp"
|
||||
#include "core/dispatch.hpp"
|
||||
#include "core/emucore.hpp"
|
||||
#include "core/subtitles.hpp"
|
||||
|
||||
class wxeditor_subtitles : public wxDialog
|
||||
namespace
|
||||
{
|
||||
struct subdata
|
||||
{
|
||||
uint64_t first;
|
||||
uint64_t last;
|
||||
std::string text;
|
||||
};
|
||||
}
|
||||
|
||||
class wxeditor_subtitles : public wxFrame
|
||||
{
|
||||
public:
|
||||
wxeditor_subtitles(wxWindow* parent);
|
||||
~wxeditor_subtitles() throw();
|
||||
bool ShouldPreventAppExit() const;
|
||||
void on_subtitles_change(wxCommandEvent& e);
|
||||
void on_ok(wxCommandEvent& e);
|
||||
void on_cancel(wxCommandEvent& e);
|
||||
void on_change(wxCommandEvent& e);
|
||||
void on_add(wxCommandEvent& e);
|
||||
void on_edit(wxCommandEvent& e);
|
||||
void on_delete(wxCommandEvent& e);
|
||||
void on_close(wxCommandEvent& e);
|
||||
void on_wclose(wxCloseEvent& e);
|
||||
void refresh();
|
||||
private:
|
||||
wxTextCtrl* subs;
|
||||
struct refresh_listener : public information_dispatch
|
||||
{
|
||||
refresh_listener(wxeditor_subtitles* v)
|
||||
: information_dispatch("subtitle-editor-change-listener")
|
||||
{
|
||||
obj = v;
|
||||
}
|
||||
void on_subtitle_change()
|
||||
{
|
||||
wxeditor_subtitles* _obj = obj;
|
||||
runuifun([_obj]() -> void { _obj->refresh(); });
|
||||
}
|
||||
wxeditor_subtitles* obj;
|
||||
};
|
||||
bool closing;
|
||||
wxListBox* subs;
|
||||
wxTextCtrl* subtext;
|
||||
wxButton* add;
|
||||
wxButton* edit;
|
||||
wxButton* _delete;
|
||||
wxButton* close;
|
||||
std::map<int, subdata> subtexts;
|
||||
refresh_listener* rlistener;
|
||||
};
|
||||
|
||||
namespace
|
||||
{
|
||||
|
||||
class wxeditor_subtitles_subtitle : public wxDialog
|
||||
{
|
||||
public:
|
||||
wxeditor_subtitles_subtitle(wxWindow* parent, subdata d);
|
||||
void on_change(wxCommandEvent& e);
|
||||
void on_cancel(wxCommandEvent& e);
|
||||
void on_ok(wxCommandEvent& e);
|
||||
subdata get_result();
|
||||
private:
|
||||
wxTextCtrl* first;
|
||||
wxTextCtrl* last;
|
||||
wxTextCtrl* text;
|
||||
wxButton* ok;
|
||||
wxButton* cancel;
|
||||
};
|
||||
|
||||
|
||||
wxeditor_subtitles::wxeditor_subtitles(wxWindow* parent)
|
||||
: wxDialog(parent, wxID_ANY, wxT("lsnes: Edit subtitles"), wxDefaultPosition, wxSize(-1, -1))
|
||||
wxeditor_subtitles_subtitle::wxeditor_subtitles_subtitle(wxWindow* parent, subdata d)
|
||||
: wxDialog(parent, wxID_ANY, wxT("lsnes: Edit subtitle"), wxDefaultPosition, wxSize(-1, -1))
|
||||
{
|
||||
Centre();
|
||||
wxFlexGridSizer* top_s = new wxFlexGridSizer(2, 1, 0, 0);
|
||||
SetSizer(top_s);
|
||||
|
||||
top_s->Add(subs = new wxTextCtrl(this, wxID_ANY, wxT(""), wxDefaultPosition, wxSize(400, 300),
|
||||
wxTE_MULTILINE), 1, wxGROW);
|
||||
subs->Connect(wxEVT_COMMAND_TEXT_UPDATED,
|
||||
wxCommandEventHandler(wxeditor_subtitles::on_subtitles_change), NULL, this);
|
||||
wxFlexGridSizer* data_s = new wxFlexGridSizer(3, 2, 0, 0);
|
||||
data_s->Add(new wxStaticText(this, wxID_ANY, wxT("First frame:")));
|
||||
data_s->Add(first = new wxTextCtrl(this, wxID_ANY, wxT(""), wxDefaultPosition, wxSize(200, -1)));
|
||||
data_s->Add(new wxStaticText(this, wxID_ANY, wxT("Last frame:")));
|
||||
data_s->Add(last = new wxTextCtrl(this, wxID_ANY, wxT(""), wxDefaultPosition, wxSize(200, -1)));
|
||||
data_s->Add(new wxStaticText(this, wxID_ANY, wxT("Text:")));
|
||||
data_s->Add(text = new wxTextCtrl(this, wxID_ANY, wxT(""), wxDefaultPosition, wxSize(400, -1)));
|
||||
top_s->Add(data_s, 1, wxGROW);
|
||||
|
||||
first->Connect(wxEVT_COMMAND_TEXT_UPDATED,
|
||||
wxCommandEventHandler(wxeditor_subtitles_subtitle::on_change), NULL, this);
|
||||
last->Connect(wxEVT_COMMAND_TEXT_UPDATED,
|
||||
wxCommandEventHandler(wxeditor_subtitles_subtitle::on_change), NULL, this);
|
||||
text->Connect(wxEVT_COMMAND_TEXT_UPDATED,
|
||||
wxCommandEventHandler(wxeditor_subtitles_subtitle::on_change), NULL, this);
|
||||
|
||||
wxBoxSizer* pbutton_s = new wxBoxSizer(wxHORIZONTAL);
|
||||
pbutton_s->AddStretchSpacer();
|
||||
pbutton_s->Add(ok = new wxButton(this, wxID_ANY, wxT("OK")), 0, wxGROW);
|
||||
pbutton_s->Add(cancel = new wxButton(this, wxID_ANY, wxT("Cancel")), 0, wxGROW);
|
||||
ok->Connect(wxEVT_COMMAND_BUTTON_CLICKED, wxCommandEventHandler(wxeditor_subtitles::on_ok), NULL, this);
|
||||
cancel->Connect(wxEVT_COMMAND_BUTTON_CLICKED, wxCommandEventHandler(wxeditor_subtitles::on_cancel), NULL,
|
||||
ok->Connect(wxEVT_COMMAND_BUTTON_CLICKED, wxCommandEventHandler(wxeditor_subtitles_subtitle::on_ok),
|
||||
NULL, this);
|
||||
cancel->Connect(wxEVT_COMMAND_BUTTON_CLICKED,
|
||||
wxCommandEventHandler(wxeditor_subtitles_subtitle::on_cancel), NULL, this);
|
||||
top_s->Add(pbutton_s, 0, wxGROW);
|
||||
|
||||
pbutton_s->SetSizeHints(this);
|
||||
top_s->SetSizeHints(this);
|
||||
|
||||
first->SetValue(towxstring((stringfmt() << d.first).str()));
|
||||
last->SetValue(towxstring((stringfmt() << d.last).str()));
|
||||
text->SetValue(towxstring(d.text));
|
||||
Fit();
|
||||
}
|
||||
|
||||
void wxeditor_subtitles_subtitle::on_change(wxCommandEvent& e)
|
||||
{
|
||||
bool valid = true;
|
||||
std::string _first = tostdstring(first->GetValue());
|
||||
std::string _last = tostdstring(last->GetValue());
|
||||
std::string _text = tostdstring(text->GetValue());
|
||||
valid = valid && regex_match("[0-9]{1,19}", _first);
|
||||
valid = valid && regex_match("[0-9]{1,19}", _last);
|
||||
valid = valid && (_text != "");
|
||||
ok->Enable(valid);
|
||||
}
|
||||
|
||||
void wxeditor_subtitles_subtitle::on_cancel(wxCommandEvent& e)
|
||||
{
|
||||
EndModal(wxID_CANCEL);
|
||||
}
|
||||
|
||||
void wxeditor_subtitles_subtitle::on_ok(wxCommandEvent& e)
|
||||
{
|
||||
EndModal(wxID_OK);
|
||||
}
|
||||
|
||||
subdata wxeditor_subtitles_subtitle::get_result()
|
||||
{
|
||||
subdata d;
|
||||
d.first = parse_value<uint64_t>(tostdstring(first->GetValue()));
|
||||
d.last = parse_value<uint64_t>(tostdstring(last->GetValue()));
|
||||
d.text = tostdstring(text->GetValue());
|
||||
return d;
|
||||
}
|
||||
|
||||
bool edit_subtext(wxWindow* w, struct subdata& d)
|
||||
{
|
||||
wxeditor_subtitles_subtitle* editor = NULL;
|
||||
try {
|
||||
editor = new wxeditor_subtitles_subtitle(w, d);
|
||||
int ret = editor->ShowModal();
|
||||
if(ret == wxID_OK)
|
||||
d = editor->get_result();
|
||||
} catch(...) {
|
||||
}
|
||||
if(editor)
|
||||
editor->Destroy();
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
wxeditor_subtitles::wxeditor_subtitles(wxWindow* parent)
|
||||
: wxFrame(NULL, wxID_ANY, wxT("lsnes: Edit subtitles"), wxDefaultPosition, wxSize(-1, -1))
|
||||
{
|
||||
closing = false;
|
||||
Centre();
|
||||
wxFlexGridSizer* top_s = new wxFlexGridSizer(2, 1, 0, 0);
|
||||
SetSizer(top_s);
|
||||
|
||||
//TODO: Apppropriate controls.
|
||||
top_s->Add(subs = new wxListBox(this, wxID_ANY, wxDefaultPosition, wxSize(300, 400), 0, NULL,
|
||||
wxLB_SINGLE), 1, wxGROW);
|
||||
subs->Connect(wxEVT_COMMAND_LISTBOX_SELECTED,
|
||||
wxCommandEventHandler(wxeditor_subtitles::on_change), NULL, this);
|
||||
|
||||
wxBoxSizer* pbutton_s = new wxBoxSizer(wxHORIZONTAL);
|
||||
pbutton_s->AddStretchSpacer();
|
||||
pbutton_s->Add(add = new wxButton(this, wxID_ANY, wxT("Add")), 0, wxGROW);
|
||||
pbutton_s->Add(edit = new wxButton(this, wxID_ANY, wxT("Edit")), 0, wxGROW);
|
||||
pbutton_s->Add(_delete = new wxButton(this, wxID_ANY, wxT("Delete")), 0, wxGROW);
|
||||
pbutton_s->Add(close = new wxButton(this, wxID_ANY, wxT("Close")), 0, wxGROW);
|
||||
add->Connect(wxEVT_COMMAND_BUTTON_CLICKED, wxCommandEventHandler(wxeditor_subtitles::on_add), NULL, this);
|
||||
edit->Connect(wxEVT_COMMAND_BUTTON_CLICKED, wxCommandEventHandler(wxeditor_subtitles::on_edit), NULL, this);
|
||||
_delete->Connect(wxEVT_COMMAND_BUTTON_CLICKED, wxCommandEventHandler(wxeditor_subtitles::on_delete), NULL,
|
||||
this);
|
||||
close->Connect(wxEVT_COMMAND_BUTTON_CLICKED, wxCommandEventHandler(wxeditor_subtitles::on_close), NULL, this);
|
||||
top_s->Add(pbutton_s, 0, wxGROW);
|
||||
|
||||
pbutton_s->SetSizeHints(this);
|
||||
top_s->SetSizeHints(this);
|
||||
Fit();
|
||||
|
||||
std::string txt = "";
|
||||
runemufn([&txt]() {
|
||||
for(auto i : get_subtitles()) {
|
||||
std::ostringstream line;
|
||||
line << i.first << " " << i.second << " " << get_subtitle_for(i.first, i.second) << std::endl;
|
||||
txt = txt + line.str();
|
||||
rlistener = new refresh_listener(this);
|
||||
refresh();
|
||||
}
|
||||
});
|
||||
subs->SetValue(towxstring(txt));
|
||||
|
||||
wxeditor_subtitles::~wxeditor_subtitles() throw()
|
||||
{
|
||||
delete rlistener;
|
||||
}
|
||||
|
||||
bool wxeditor_subtitles::ShouldPreventAppExit() const
|
||||
|
@ -68,76 +215,107 @@ bool wxeditor_subtitles::ShouldPreventAppExit() const
|
|||
return false;
|
||||
}
|
||||
|
||||
void wxeditor_subtitles::on_subtitles_change(wxCommandEvent& e)
|
||||
void wxeditor_subtitles::on_close(wxCommandEvent& e)
|
||||
{
|
||||
std::string txt = tostdstring(subs->GetValue());
|
||||
std::string line;
|
||||
while(txt != "") {
|
||||
extract_token(txt, line, "\n");
|
||||
istrip_CR(line);
|
||||
if(line == "")
|
||||
continue;
|
||||
auto r = regex("([0-9]+)[ \t]+([0-9]+)[ \t]+(.+)", line);
|
||||
if(!r) {
|
||||
ok->Disable();
|
||||
return;
|
||||
}
|
||||
try {
|
||||
parse_value<uint64_t>(r[1]);
|
||||
parse_value<uint64_t>(r[2]);
|
||||
} catch(...) {
|
||||
ok->Disable();
|
||||
return;
|
||||
}
|
||||
}
|
||||
ok->Enable();
|
||||
closing = true;
|
||||
Destroy();
|
||||
}
|
||||
|
||||
void wxeditor_subtitles::on_ok(wxCommandEvent& e)
|
||||
void wxeditor_subtitles::on_wclose(wxCloseEvent& e)
|
||||
{
|
||||
std::map<std::pair<uint64_t, uint64_t>, std::string> data;
|
||||
runemufn([&data]() {
|
||||
for(auto i : get_subtitles())
|
||||
data[std::make_pair(i.first, i.second)] = "";
|
||||
});
|
||||
std::string txt = tostdstring(subs->GetValue());
|
||||
std::string line;
|
||||
while(txt != "") {
|
||||
extract_token(txt, line, "\n");
|
||||
istrip_CR(line);
|
||||
if(line == "")
|
||||
continue;
|
||||
auto r = regex("([0-9]+)[ \t]+([0-9]+)[ \t]+(.+)", line);
|
||||
if(!r)
|
||||
return;
|
||||
try {
|
||||
uint64_t f = parse_value<uint64_t>(r[1]);
|
||||
uint64_t l = parse_value<uint64_t>(r[2]);
|
||||
data[std::make_pair(f, l)] = r[3];
|
||||
} catch(...) {
|
||||
return;
|
||||
}
|
||||
}
|
||||
runemufn([&data]() {
|
||||
for(auto i : data)
|
||||
set_subtitle_for(i.first.first, i.first.second, i.second);
|
||||
});
|
||||
EndModal(wxID_OK);
|
||||
closing = true;
|
||||
}
|
||||
|
||||
void wxeditor_subtitles::on_cancel(wxCommandEvent& e)
|
||||
void wxeditor_subtitles::refresh()
|
||||
{
|
||||
EndModal(wxID_CANCEL);
|
||||
if(closing)
|
||||
return;
|
||||
std::map<std::pair<uint64_t, uint64_t>, std::string> _subtitles;
|
||||
runemufn([&_subtitles]() -> void {
|
||||
auto keys = get_subtitles();
|
||||
for(auto i : keys)
|
||||
_subtitles[i] = get_subtitle_for(i.first, i.second);
|
||||
});
|
||||
int sel = subs->GetSelection();
|
||||
bool found = (subtexts.count(sel) != 0);
|
||||
subdata matching = subtexts[sel];
|
||||
subs->Clear();
|
||||
unsigned num = 0;
|
||||
subtexts.clear();
|
||||
for(auto i : _subtitles) {
|
||||
subdata newdata;
|
||||
newdata.first = i.first.first;
|
||||
newdata.last = i.first.second;
|
||||
newdata.text = i.second;
|
||||
subtexts[num++] = newdata;
|
||||
std::string s = (stringfmt() << i.first.first << "-" << i.first.second << ": " << i.second).str();
|
||||
subs->Append(towxstring(s));
|
||||
}
|
||||
for(int i = 0; i < subs->GetCount(); i++)
|
||||
if(subtexts[i].first == matching.first && subtexts[i].last == matching.last)
|
||||
subs->SetSelection(i);
|
||||
if(subs->GetSelection() == wxNOT_FOUND && sel < subs->GetCount())
|
||||
subs->SetSelection(sel);
|
||||
sel = subs->GetSelection();
|
||||
found = (subtexts.count(sel) != 0);
|
||||
edit->Enable(found);
|
||||
_delete->Enable(found);
|
||||
}
|
||||
|
||||
void wxeditor_subtitles::on_change(wxCommandEvent& e)
|
||||
{
|
||||
if(closing)
|
||||
return;
|
||||
int sel = subs->GetSelection();
|
||||
bool found = (subtexts.count(sel) != 0);
|
||||
edit->Enable(found);
|
||||
_delete->Enable(found);
|
||||
}
|
||||
|
||||
void wxeditor_subtitles::on_add(wxCommandEvent& e)
|
||||
{
|
||||
if(closing)
|
||||
return;
|
||||
subdata t;
|
||||
t.first = 0;
|
||||
t.last = 0;
|
||||
t.text = "";
|
||||
if(edit_subtext(this, t))
|
||||
set_subtitle_for(t.first, t.last, t.text);
|
||||
}
|
||||
|
||||
void wxeditor_subtitles::on_edit(wxCommandEvent& e)
|
||||
{
|
||||
if(closing)
|
||||
return;
|
||||
int sel = subs->GetSelection();
|
||||
if(!subtexts.count(sel))
|
||||
return;
|
||||
auto t = subtexts[sel];
|
||||
auto old = t;
|
||||
if(edit_subtext(this, t)) {
|
||||
set_subtitle_for(old.first, old.last, "");
|
||||
set_subtitle_for(t.first, t.last, t.text);
|
||||
}
|
||||
}
|
||||
|
||||
void wxeditor_subtitles::on_delete(wxCommandEvent& e)
|
||||
{
|
||||
if(closing)
|
||||
return;
|
||||
int sel = subs->GetSelection();
|
||||
if(!subtexts.count(sel))
|
||||
return;
|
||||
auto t = subtexts[sel];
|
||||
set_subtitle_for(t.first, t.last, "");
|
||||
}
|
||||
|
||||
void wxeditor_subtitles_display(wxWindow* parent)
|
||||
{
|
||||
modal_pause_holder hld;
|
||||
wxDialog* editor;
|
||||
wxFrame* editor;
|
||||
try {
|
||||
editor = new wxeditor_subtitles(parent);
|
||||
editor->ShowModal();
|
||||
editor->Show();
|
||||
} catch(...) {
|
||||
}
|
||||
editor->Destroy();
|
||||
}
|
||||
|
|
|
@ -9,6 +9,7 @@
|
|||
#include <wx/control.h>
|
||||
#include <wx/combobox.h>
|
||||
|
||||
#include "core/dispatch.hpp"
|
||||
#include "library/string.hpp"
|
||||
|
||||
#define NOTHING 0xFFFFFFFFFFFFFFFFULL
|
||||
|
@ -22,6 +23,7 @@ class wxeditor_voicesub : public wxDialog
|
|||
{
|
||||
public:
|
||||
wxeditor_voicesub(wxWindow* parent);
|
||||
~wxeditor_voicesub() throw();
|
||||
bool ShouldPreventAppExit() const;
|
||||
void on_select(wxCommandEvent& e);
|
||||
void on_play(wxCommandEvent& e);
|
||||
|
@ -39,9 +41,23 @@ public:
|
|||
void on_refresh(wxCommandEvent& e);
|
||||
void on_close(wxCommandEvent& e);
|
||||
void on_wclose(wxCloseEvent& e);
|
||||
private:
|
||||
bool closing;
|
||||
void refresh();
|
||||
private:
|
||||
struct refresh_listener : public information_dispatch
|
||||
{
|
||||
refresh_listener(wxeditor_voicesub* v)
|
||||
: information_dispatch("voicesub-editor-change-listner")
|
||||
{
|
||||
obj = v;
|
||||
}
|
||||
void on_voice_stream_change()
|
||||
{
|
||||
wxeditor_voicesub* _obj = obj;
|
||||
runuifun([_obj]() -> void { _obj->refresh(); });
|
||||
}
|
||||
wxeditor_voicesub* obj;
|
||||
};
|
||||
bool closing;
|
||||
uint64_t get_id();
|
||||
std::map<int, uint64_t> smap;
|
||||
wxListBox* subtitles;
|
||||
|
@ -59,6 +75,8 @@ private:
|
|||
wxButton* unloadbutton;
|
||||
wxButton* refreshbutton;
|
||||
wxButton* closebutton;
|
||||
refresh_listener* rlistener;
|
||||
|
||||
};
|
||||
|
||||
wxeditor_voicesub::wxeditor_voicesub(wxWindow* parent)
|
||||
|
@ -150,9 +168,15 @@ wxeditor_voicesub::wxeditor_voicesub(wxWindow* parent)
|
|||
|
||||
top_s->SetSizeHints(this);
|
||||
Fit();
|
||||
rlistener = new refresh_listener(this);
|
||||
refresh();
|
||||
}
|
||||
|
||||
wxeditor_voicesub::~wxeditor_voicesub() throw()
|
||||
{
|
||||
delete rlistener;
|
||||
}
|
||||
|
||||
void wxeditor_voicesub::on_select(wxCommandEvent& e)
|
||||
{
|
||||
if(closing)
|
||||
|
@ -177,7 +201,6 @@ void wxeditor_voicesub::on_play(wxCommandEvent& e)
|
|||
} catch(std::exception& e) {
|
||||
show_message_ok(this, "Error playing", e.what(), wxICON_EXCLAMATION);
|
||||
}
|
||||
refresh();
|
||||
}
|
||||
|
||||
void wxeditor_voicesub::on_delete(wxCommandEvent& e)
|
||||
|
@ -190,7 +213,6 @@ void wxeditor_voicesub::on_delete(wxCommandEvent& e)
|
|||
} catch(std::exception& e) {
|
||||
show_message_ok(this, "Error deleting", e.what(), wxICON_EXCLAMATION);
|
||||
}
|
||||
refresh();
|
||||
}
|
||||
|
||||
void wxeditor_voicesub::on_export_o(wxCommandEvent& e)
|
||||
|
@ -209,7 +231,6 @@ void wxeditor_voicesub::on_export_o(wxCommandEvent& e)
|
|||
} catch(std::exception& e) {
|
||||
show_message_ok(this, "Error exporting", e.what(), wxICON_EXCLAMATION);
|
||||
}
|
||||
refresh();
|
||||
}
|
||||
|
||||
void wxeditor_voicesub::on_export_p(wxCommandEvent& e)
|
||||
|
@ -228,7 +249,6 @@ void wxeditor_voicesub::on_export_p(wxCommandEvent& e)
|
|||
} catch(std::exception& e) {
|
||||
show_message_ok(this, "Error exporting", e.what(), wxICON_EXCLAMATION);
|
||||
}
|
||||
refresh();
|
||||
}
|
||||
|
||||
void wxeditor_voicesub::on_export_q(wxCommandEvent& e)
|
||||
|
@ -247,7 +267,6 @@ void wxeditor_voicesub::on_export_q(wxCommandEvent& e)
|
|||
} catch(std::exception& e) {
|
||||
show_message_ok(this, "Error exporting", e.what(), wxICON_EXCLAMATION);
|
||||
}
|
||||
refresh();
|
||||
}
|
||||
|
||||
void wxeditor_voicesub::on_export_s(wxCommandEvent& e)
|
||||
|
@ -263,7 +282,6 @@ void wxeditor_voicesub::on_export_s(wxCommandEvent& e)
|
|||
} catch(std::exception& e) {
|
||||
show_message_ok(this, "Error exporting superstream", e.what(), wxICON_EXCLAMATION);
|
||||
}
|
||||
refresh();
|
||||
}
|
||||
|
||||
void wxeditor_voicesub::on_import_o(wxCommandEvent& e)
|
||||
|
@ -282,7 +300,6 @@ void wxeditor_voicesub::on_import_o(wxCommandEvent& e)
|
|||
} catch(std::exception& e) {
|
||||
show_message_ok(this, "Error importing", e.what(), wxICON_EXCLAMATION);
|
||||
}
|
||||
refresh();
|
||||
}
|
||||
|
||||
void wxeditor_voicesub::on_import_p(wxCommandEvent& e)
|
||||
|
@ -301,7 +318,6 @@ void wxeditor_voicesub::on_import_p(wxCommandEvent& e)
|
|||
} catch(std::exception& e) {
|
||||
show_message_ok(this, "Error importing", e.what(), wxICON_EXCLAMATION);
|
||||
}
|
||||
refresh();
|
||||
}
|
||||
|
||||
void wxeditor_voicesub::on_import_q(wxCommandEvent& e)
|
||||
|
@ -320,7 +336,6 @@ void wxeditor_voicesub::on_import_q(wxCommandEvent& e)
|
|||
} catch(std::exception& e) {
|
||||
show_message_ok(this, "Error importing", e.what(), wxICON_EXCLAMATION);
|
||||
}
|
||||
refresh();
|
||||
}
|
||||
|
||||
void wxeditor_voicesub::on_change_ts(wxCommandEvent& e)
|
||||
|
@ -340,7 +355,6 @@ void wxeditor_voicesub::on_change_ts(wxCommandEvent& e)
|
|||
} catch(std::exception& e) {
|
||||
show_message_ok(this, "Error changing timebase", e.what(), wxICON_EXCLAMATION);
|
||||
}
|
||||
refresh();
|
||||
}
|
||||
|
||||
void wxeditor_voicesub::on_load(wxCommandEvent& e)
|
||||
|
@ -356,13 +370,11 @@ void wxeditor_voicesub::on_load(wxCommandEvent& e)
|
|||
} catch(std::exception& e) {
|
||||
show_message_ok(this, "Error loading collection", e.what(), wxICON_EXCLAMATION);
|
||||
}
|
||||
refresh();
|
||||
}
|
||||
|
||||
void wxeditor_voicesub::on_unload(wxCommandEvent& e)
|
||||
{
|
||||
voicesub_unload_collection();
|
||||
refresh();
|
||||
}
|
||||
|
||||
void wxeditor_voicesub::on_refresh(wxCommandEvent& e)
|
||||
|
@ -378,6 +390,8 @@ void wxeditor_voicesub::on_close(wxCommandEvent& e)
|
|||
|
||||
void wxeditor_voicesub::refresh()
|
||||
{
|
||||
if(closing)
|
||||
return;
|
||||
bool cflag = voicesub_collection_loaded();
|
||||
unloadbutton->Enable(cflag);
|
||||
exportsbutton->Enable(cflag);
|
||||
|
|
|
@ -1229,7 +1229,7 @@ void wxwin_mainwindow::handle_menu_click_cancelable(wxCommandEvent& e)
|
|||
case wxID_SET_VOLUME:
|
||||
parsed = pick_volume(this, "Set volume", last_volume);
|
||||
if(parsed >= -1e-10)
|
||||
runemufn([parsed]() { platform::global_volume = parsed; });
|
||||
runemufn([parsed]() { audioapi_music_volume(parsed); });
|
||||
return;
|
||||
case wxID_SET_VOLUME_RECORD:
|
||||
parsed = pick_volume(this, "Set recording volume", last_volume_record);
|
||||
|
|
Loading…
Add table
Reference in a new issue