From 7fff156b574d3d1671c1d9f246b85007b08275da Mon Sep 17 00:00:00 2001 From: Takamichi Horikawa Date: Fri, 17 Mar 2017 22:33:37 +0900 Subject: win32: add toneviewer --- win32/fmplayer.mak | 3 + win32/main.c | 219 +++++++++++++++++++++++++++++++++++++++++++++-------- win32/toneview.c | 218 ++++++++++++++++++++++++++++++++++++++++++++++++++++ win32/toneview.h | 16 ++++ win32/x86/Makefile | 1 + 5 files changed, 424 insertions(+), 33 deletions(-) create mode 100644 win32/toneview.c create mode 100644 win32/toneview.h diff --git a/win32/fmplayer.mak b/win32/fmplayer.mak index 11f98f2..c71ef6a 100644 --- a/win32/fmplayer.mak +++ b/win32/fmplayer.mak @@ -20,7 +20,9 @@ LIBOPNA_OBJS=opna \ FMDSP_OBJS=fmdsp \ font_rom \ font_fmdsp_small +TONEDATA_OBJS=tonedata OBJBASE=main \ + toneview \ soundout \ dsoundout \ waveout \ @@ -29,6 +31,7 @@ OBJBASE=main \ guid \ $(FMDRIVER_OBJS) \ $(LIBOPNA_OBJS) \ + $(TONEDATA_OBJS) \ $(FMDSP_OBJS) RESBASE=lnf LIBBASE=user32 \ diff --git a/win32/main.c b/win32/main.c index 16d2f3e..9cef778 100644 --- a/win32/main.c +++ b/win32/main.c @@ -12,10 +12,14 @@ #include "fmdsp/fmdsp.h" #include "soundout.h" #include "winfont.h" +#include "version.h" +#include "toneview.h" enum { ID_OPENFILE = 0x10, ID_PAUSE, + ID_2X, + ID_TONEVIEW, }; #define FMPLAYER_CLASSNAME L"myon_fmplayer_ym2608_win32" @@ -55,10 +59,15 @@ static struct { uint8_t opna_adpcm_ram[OPNA_ADPCM_RAM_SIZE]; void *ppz8_buf; bool paused; + HWND mainwnd; + WNDPROC btn_defproc; HWND driverinfo; + HWND button_2x; const wchar_t *lastopenpath; + bool fmdsp_2x; } g; +HWND g_currentdlg; static void opna_int_cb(void *userptr) { struct fmdriver_work *work = (struct fmdriver_work *)userptr; @@ -93,6 +102,11 @@ static void sound_cb(void *p, int16_t *buf, unsigned frames) { struct opna_timer *timer = (struct opna_timer *)p; ZeroMemory(buf, sizeof(int16_t)*frames*2); opna_timer_mix(timer, buf, frames); + if (!atomic_flag_test_and_set_explicit( + &toneview_g.flag, memory_order_acquire)) { + tonedata_from_opna(&toneview_g.tonedata, &g.opna); + atomic_flag_clear_explicit(&toneview_g.flag, memory_order_release); + } } static void on_timer(HWND hwnd, UINT id) { @@ -302,7 +316,9 @@ static void openfile(HWND hwnd, const wchar_t *path) { g.driver = driver; if (g.data) HeapFree(g.heap, 0, g.data); g.data = data; + unsigned mask = opna_get_mask(&g.opna); opna_reset(&g.opna); + opna_set_mask(&g.opna, mask); if (g.drum_rom) opna_drum_set_rom(&g.opna.drum, g.drum_rom); opna_adpcm_set_ram_256k(&g.opna.adpcm, g.opna_adpcm_ram); opna_timer_reset(&g.opna_timer, &g.opna); @@ -314,6 +330,7 @@ static void openfile(HWND hwnd, const wchar_t *path) { g.work.opna = &g.opna_timer; g.work.ppz8 = &g.ppz8; g.work.ppz8_functbl = &ppz8_functbl; + WideCharToMultiByte(932, WC_NO_BEST_FIT_CHARS, path, -1, g.work.filename, sizeof(g.work.filename), 0, 0); opna_timer_set_int_callback(&g.opna_timer, opna_int_cb, &g.work); opna_timer_set_mix_callback(&g.opna_timer, opna_mix_cb, &g.ppz8); if (driver_type == DRIVER_PMD) { @@ -430,6 +447,118 @@ static void on_dropfiles(HWND hwnd, HDROP hdrop) { } #endif // ENABLE_WM_DROPFILES +static void mask_set(unsigned mask, bool shift) { + if (shift) { + opna_set_mask(&g.opna, ~mask); + } else { + opna_set_mask(&g.opna, opna_get_mask(&g.opna) ^ mask); + } +} + +static void toggle_2x(HWND hwnd) { + g.fmdsp_2x ^= 1; + RECT wr; + wr.left = 0; + wr.right = 640; + wr.top = 0; + wr.bottom = 480; + if (g.fmdsp_2x) { + wr.right = 1280; + wr.bottom = 880; + } + DWORD style = GetWindowLongPtr(hwnd, GWL_STYLE); + DWORD exstyle = GetWindowLongPtr(hwnd, GWL_EXSTYLE); + AdjustWindowRectEx(&wr, style, 0, exstyle); + SetWindowPos(hwnd, HWND_TOP, 0, 0, wr.right-wr.left, wr.bottom-wr.top, + SWP_NOZORDER | SWP_NOMOVE); +} + +static bool proc_key(UINT vk, bool down, int repeat) { + if (down) { + if (VK_F1 <= vk && vk <= VK_F12) { + if (GetKeyState(VK_CONTROL) & 0x8000U) { + fmdsp_palette_set(&g.fmdsp, vk - VK_F1); + return true; + } else { + switch (vk) { + case VK_F6: + if (g.lastopenpath) { + openfile(g.mainwnd, g.lastopenpath); + } + return true; + case VK_F7: + if (g.sound) { + g.paused = !g.paused; + g.sound->pause(g.sound, g.paused); + } + return true; + case VK_F11: + fmdsp_dispstyle_set(&g.fmdsp, (g.fmdsp.style+1) % FMDSP_DISPSTYLE_CNT); + return true; + case VK_F12: + toggle_2x(g.mainwnd); + } + } + } else { + bool shift = GetKeyState(VK_SHIFT) & 0x8000U; + switch (vk) { + case '1': + mask_set(LIBOPNA_CHAN_FM_1, shift); + return true; + case '2': + mask_set(LIBOPNA_CHAN_FM_2, shift); + return true; + case '3': + mask_set(LIBOPNA_CHAN_FM_3, shift); + return true; + case '4': + mask_set(LIBOPNA_CHAN_FM_4, shift); + return true; + case '5': + mask_set(LIBOPNA_CHAN_FM_5, shift); + return true; + case '6': + mask_set(LIBOPNA_CHAN_FM_6, shift); + return true; + case '7': + mask_set(LIBOPNA_CHAN_SSG_1, shift); + return true; + case '8': + mask_set(LIBOPNA_CHAN_SSG_2, shift); + return true; + case '9': + mask_set(LIBOPNA_CHAN_SSG_3, shift); + return true; + case '0': + mask_set(LIBOPNA_CHAN_DRUM_ALL, shift); + return true; + case VK_OEM_MINUS: + mask_set(LIBOPNA_CHAN_ADPCM, shift); + return true; + case VK_OEM_PLUS: + opna_set_mask(&g.opna, ~opna_get_mask(&g.opna)); + return true; + case VK_OEM_5: + opna_set_mask(&g.opna, 0); + return true; + } + } + } + return false; +} + +static LRESULT CALLBACK btn_wndproc( + HWND hwnd, UINT msg, WPARAM wParam, LPARAM lParam +) { + switch (msg) { + case WM_KEYDOWN: + case WM_KEYUP: + if (proc_key((UINT)wParam, msg == WM_KEYDOWN, (int)(short)LOWORD(lParam))) return 0; + break; + } + return CallWindowProc(g.btn_defproc, hwnd, msg, wParam, lParam); +} + static bool on_create(HWND hwnd, CREATESTRUCT *cs) { (void)cs; HWND button = CreateWindowEx( @@ -459,6 +588,29 @@ static bool on_create(HWND hwnd, CREATESTRUCT *cs) { 100, 25, hwnd, 0, g.hinst, 0 ); + g.button_2x = CreateWindowEx( + 0, + L"BUTTON", + L"2&x", + WS_TABSTOP | WS_VISIBLE | WS_CHILD | BS_CHECKBOX | BS_PUSHLIKE, + 215, 10, + 30, 25, + hwnd, (HMENU)ID_2X, g.hinst, 0 + ); + HWND button_toneview = CreateWindowEx( + 0, + L"BUTTON", + L"Tone &viewer", + WS_TABSTOP | WS_VISIBLE | WS_CHILD, + 250, 10, + 100, 25, + hwnd, (HMENU)ID_TONEVIEW, g.hinst, 0 + ); + g.btn_defproc = (WNDPROC)GetWindowLongPtr(button, GWLP_WNDPROC); + SetWindowLongPtr(button, GWLP_WNDPROC, (intptr_t)btn_wndproc); + SetWindowLongPtr(pbutton, GWLP_WNDPROC, (intptr_t)btn_wndproc); + SetWindowLongPtr(g.button_2x, GWLP_WNDPROC, (intptr_t)btn_wndproc); + SetWindowLongPtr(button_toneview, GWLP_WNDPROC, (intptr_t)btn_wndproc); NONCLIENTMETRICS ncm; ncm.cbSize = sizeof(ncm); SystemParametersInfo(SPI_GETNONCLIENTMETRICS, sizeof(ncm), &ncm, 0); @@ -466,6 +618,8 @@ static bool on_create(HWND hwnd, CREATESTRUCT *cs) { SetWindowFont(button, font, TRUE); SetWindowFont(pbutton, font, TRUE); SetWindowFont(g.driverinfo, font, TRUE); + SetWindowFont(g.button_2x, font, TRUE); + SetWindowFont(button_toneview, font, TRUE); loadrom(); loadfont(); fmdsp_init(&g.fmdsp, g.font_loaded ? &g.font : 0); @@ -489,6 +643,14 @@ static void on_command(HWND hwnd, int id, HWND hwnd_c, UINT code) { g.paused = !g.paused; g.sound->pause(g.sound, g.paused); } + break; + case ID_2X: + toggle_2x(hwnd); + Button_SetCheck(g.button_2x, g.fmdsp_2x); + break; + case ID_TONEVIEW: + show_toneview(g.hinst, hwnd); + break; } } @@ -531,7 +693,11 @@ static void on_paint(HWND hwnd) { g.vram, bi, DIB_RGB_COLORS); SelectObject(mdc, bitmap); - BitBlt(dc, 0, 80, 640, 400, mdc, 0, 0, SRCCOPY); + if (g.fmdsp_2x) { + StretchBlt(dc, 0, 80, 1280, 800, mdc, 0, 0, 640, 400, SRCCOPY); + } else { + BitBlt(dc, 0, 80, 640, 400, mdc, 0, 0, SRCCOPY); + } DeleteDC(mdc); DeleteObject(bitmap); EndPaint(hwnd, &ps); @@ -559,36 +725,20 @@ static void on_syskey(HWND hwnd, UINT vk, BOOL down, int repeat, UINT scan) { } static void on_key(HWND hwnd, UINT vk, BOOL down, int repeat, UINT scan) { - if (down) { - if (VK_F1 <= vk && vk <= VK_F12) { - if (GetKeyState(VK_CONTROL) & 0x8000U) { - fmdsp_palette_set(&g.fmdsp, vk - VK_F1); - return; - } else { - switch (vk) { - case VK_F6: - if (g.lastopenpath) { - openfile(hwnd, g.lastopenpath); - } - break; - case VK_F7: - if (g.sound) { - g.paused = !g.paused; - g.sound->pause(g.sound, g.paused); - } - break; - case VK_F11: - fmdsp_dispstyle_set(&g.fmdsp, (g.fmdsp.style+1) % FMDSP_DISPSTYLE_CNT); - break; - } - } + if (!proc_key(vk, down, repeat)) { + if (down) { + FORWARD_WM_KEYDOWN(hwnd, vk, repeat, scan, DefWindowProc); + } else { + FORWARD_WM_KEYUP(hwnd, vk, repeat, scan, DefWindowProc); } - FORWARD_WM_KEYDOWN(hwnd, vk, repeat, scan, DefWindowProc); - } else { - FORWARD_WM_KEYUP(hwnd, vk, repeat, scan, DefWindowProc); } } +static void on_activate(HWND hwnd, bool activate, HWND targetwnd, WINBOOL state) { + if (activate) g_currentdlg = hwnd; + else g_currentdlg = 0; +} + static LRESULT CALLBACK wndproc( HWND hwnd, UINT msg, WPARAM wParam, LPARAM lParam ) { @@ -607,6 +757,7 @@ static LRESULT CALLBACK wndproc( HANDLE_MSG(hwnd, WM_KEYUP, on_key); HANDLE_MSG(hwnd, WM_SYSKEYDOWN, on_syskey); HANDLE_MSG(hwnd, WM_SYSKEYUP, on_syskey); + HANDLE_MSG(hwnd, WM_ACTIVATE, on_activate); } return DefWindowProc(hwnd, msg, wParam, lParam); } @@ -665,24 +816,26 @@ int CALLBACK wWinMain(HINSTANCE hinst, HINSTANCE hpinst, wr.top = 0; wr.bottom = 480; AdjustWindowRectEx(&wr, style, 0, exStyle); - HWND hwnd = CreateWindowEx( + g.mainwnd = CreateWindowEx( exStyle, - (wchar_t*)((uintptr_t)wcatom), L"FMPlayer/Win32", + (wchar_t*)((uintptr_t)wcatom), L"FMPlayer/Win32 v" FMPLAYER_VERSION_STR, style, CW_USEDEFAULT, CW_USEDEFAULT, wr.right-wr.left, wr.bottom-wr.top, 0, 0, g.hinst, 0 ); - ShowWindow(hwnd, cmdshow); + ShowWindow(g.mainwnd, cmdshow); if (argfile) { - openfile(hwnd, argfile); + openfile(g.mainwnd, argfile); } MSG msg = {0}; while (GetMessage(&msg, 0, 0, 0)) { - TranslateMessage(&msg); - DispatchMessage(&msg); + if (!g_currentdlg || !IsDialogMessage(g_currentdlg, &msg)) { + TranslateMessage(&msg); + DispatchMessage(&msg); + } } return msg.wParam; } diff --git a/win32/toneview.c b/win32/toneview.c new file mode 100644 index 0000000..a0a920a --- /dev/null +++ b/win32/toneview.c @@ -0,0 +1,218 @@ +#include "toneview.h" +#include +#include + +enum { + TIMER_UPDATE = 1 +}; + +enum { + ID_COPY0 = 0x10, + ID_COPY1, + ID_COPY2, + ID_COPY3, + ID_COPY4, + ID_COPY5, + ID_NORMALIZE, + ID_LIST, +}; + +struct toneview_g toneview_g = { + .flag = ATOMIC_FLAG_INIT +}; + +static struct { + HINSTANCE hinst; + HWND toneviewer; + HWND tonelabel[6]; + ATOM toneviewer_class; + struct fmplayer_tonedata tonedata; + struct fmplayer_tonedata tonedata_n; + char strbuf[FMPLAYER_TONEDATA_STR_SIZE]; + wchar_t strbuf_w[FMPLAYER_TONEDATA_STR_SIZE]; + enum fmplayer_tonedata_format format; + bool normalize; + HFONT font; + HFONT font_mono; + HWND checkbox; + HWND formatlist; +} g = { + .normalize = true +}; + +extern HWND g_currentdlg; + +static void on_destroy(HWND hwnd) { + for (int i = 0; i < 6; i++) { + DestroyWindow(g.tonelabel[i]); + } + DestroyWindow(g.checkbox); + DestroyWindow(g.formatlist); + g.toneviewer = 0; +} + +enum { + LIST_W = 200, + NORMALIZE_X = 10 + LIST_W + 5, + NORMALIZE_W = 200, + TOP_H = 25, + TONELABEL_X = 10, + TONELABEL_H = 100, + TONELABEL_W = 300, + COPY_X = TONELABEL_X + TONELABEL_W + 5, + COPY_W = 100, + WIN_H = 10 + TOP_H + 5 + TONELABEL_H*6 + 5*5 + 10, + WIN_W = 10 + TONELABEL_W + 5 + COPY_W + 10, +}; + +static void on_command(HWND hwnd, int id, HWND hwnd_c, UINT code) { + if (code == BN_CLICKED && ((ID_COPY0 <= id) && (id <= ID_COPY5))) { + int i = id - ID_COPY0; + HGLOBAL gmem = GlobalAlloc(GMEM_MOVEABLE, FMPLAYER_TONEDATA_STR_SIZE*sizeof(wchar_t)); + if (!gmem) return; + wchar_t *buf = GlobalLock(gmem); + GetWindowText(g.tonelabel[i], buf, FMPLAYER_TONEDATA_STR_SIZE*sizeof(wchar_t)); + GlobalUnlock(gmem); + if (!OpenClipboard(hwnd)) return; + EmptyClipboard(); + SetClipboardData(CF_UNICODETEXT, gmem); + CloseClipboard(); + } else if (id == ID_NORMALIZE && code == BN_CLICKED) { + g.normalize ^= 1; + Button_SetCheck(hwnd_c, g.normalize); + } else if (id == ID_LIST && code == CBN_SELCHANGE) { + g.format = ComboBox_GetCurSel(g.formatlist); + } +} + +static bool on_create(HWND hwnd, const CREATESTRUCT *cs) { + RECT wr; + wr.left = 0; + wr.right = WIN_W; + wr.top = 0; + wr.bottom = WIN_H; + DWORD style = GetWindowLongPtr(hwnd, GWL_STYLE); + DWORD exstyle = GetWindowLongPtr(hwnd, GWL_EXSTYLE); + AdjustWindowRectEx(&wr, style, 0, exstyle); + SetWindowPos(hwnd, HWND_TOP, 0, 0, wr.right-wr.left, wr.bottom-wr.top, + SWP_NOZORDER | SWP_NOMOVE); + + if (!g.font_mono) { + g.font_mono = CreateFont( + 16, 0, 0, 0, + FW_NORMAL, FALSE, FALSE, FALSE, + ANSI_CHARSET, OUT_DEFAULT_PRECIS, CLIP_DEFAULT_PRECIS, + CLEARTYPE_QUALITY, FIXED_PITCH, 0); + } + + if (!g.font) { + NONCLIENTMETRICS ncm; + ncm.cbSize = sizeof(ncm); + SystemParametersInfo(SPI_GETNONCLIENTMETRICS, sizeof(ncm), &ncm, 0); + g.font = CreateFontIndirect(&ncm.lfMessageFont); + } + g.formatlist = CreateWindowEx(0, L"combobox", 0, + WS_TABSTOP | WS_CHILD | WS_VISIBLE | CBS_DROPDOWNLIST, + 10, 10, LIST_W, 100, + hwnd, (HMENU)ID_LIST, g.hinst, 0); + SetWindowFont(g.formatlist, g.font, TRUE); + ComboBox_AddString(g.formatlist, L"PMD"); + ComboBox_AddString(g.formatlist, L"FMP"); + ComboBox_SetCurSel(g.formatlist, 0); + g.checkbox = CreateWindowEx(0, L"button", + L"&Normalize", + WS_CHILD | WS_VISIBLE | BS_CHECKBOX | WS_TABSTOP, + NORMALIZE_X, 10, NORMALIZE_W, 25, + hwnd, (HMENU)ID_NORMALIZE, g.hinst, 0 + ); + Button_SetCheck(g.checkbox, g.normalize); + SetWindowFont(g.checkbox, g.font, TRUE); + for (int i = 0; i < 6; i++) { + g.tonelabel[i] = CreateWindowEx(WS_EX_CLIENTEDGE, L"static", + L"@ 0\n123 123", + WS_VISIBLE | WS_CHILD, + 10, 40 + (TONELABEL_H+5)*i, TONELABEL_W, TONELABEL_H, + hwnd, 0, g.hinst, 0); + SetWindowFont(g.tonelabel[i], g.font_mono, TRUE); + wchar_t text[] = L"Copy (& )"; + text[7] = L'1' + i; + HWND copybutton = CreateWindowEx(0, L"button", + text, + WS_VISIBLE | WS_CHILD | WS_TABSTOP, + COPY_X, 40 + (TONELABEL_H+5)*i, 100, TONELABEL_H, + hwnd, (HMENU)(ID_COPY0+i), g.hinst, 0); + SetWindowFont(copybutton, g.font, TRUE); + } + ShowWindow(hwnd, SW_SHOW); + SetTimer(hwnd, TIMER_UPDATE, 16, 0); + return true; +} + +static void on_timer(HWND hwnd, UINT id) { + if (id == TIMER_UPDATE) { + if (!atomic_flag_test_and_set_explicit( + &toneview_g.flag, memory_order_acquire)) { + g.tonedata = toneview_g.tonedata; + atomic_flag_clear_explicit(&toneview_g.flag, memory_order_release); + } + g.tonedata_n = g.tonedata; + for (int c = 0; c < 6; c++) { + if (g.normalize) { + tonedata_ch_normalize_tl(&g.tonedata_n.ch[c]); + } + tonedata_ch_string(g.format, g.strbuf, &g.tonedata_n.ch[c], 0); + for (int i = 0; i < FMPLAYER_TONEDATA_STR_SIZE; i++) { + g.strbuf_w[i] = g.strbuf[i]; + } + // RedrawWindow(g.tonelabel[c], 0, 0, RDW_ERASE); + SetWindowText(g.tonelabel[c], g.strbuf_w); + } + } +} + +static void on_activate(HWND hwnd, bool activate, HWND targetwnd, WINBOOL state) { + if (activate) g_currentdlg = hwnd; + else g_currentdlg = 0; +} + +static LRESULT CALLBACK wndproc( + HWND hwnd, UINT msg, WPARAM wParam, LPARAM lParam +) { + switch (msg) { + HANDLE_MSG(hwnd, WM_DESTROY, on_destroy); + HANDLE_MSG(hwnd, WM_CREATE, on_create); + HANDLE_MSG(hwnd, WM_TIMER, on_timer); + HANDLE_MSG(hwnd, WM_COMMAND, on_command); + HANDLE_MSG(hwnd, WM_ACTIVATE, on_activate); + } + return DefWindowProc(hwnd, msg, wParam, lParam); +} + +void show_toneview(HINSTANCE hinst, HWND parent) { + g.hinst = hinst; + if (!g.toneviewer) { + if (!g.toneviewer_class) { + WNDCLASS wc = {0}; + wc.style = CS_HREDRAW | CS_VREDRAW; + wc.lpfnWndProc = wndproc; + wc.hInstance = hinst; + wc.hIcon = LoadIcon(g.hinst, MAKEINTRESOURCE(1)); + wc.hCursor = LoadCursor(NULL, IDC_ARROW); + wc.hbrBackground = (HBRUSH)(COLOR_BTNFACE+1); + wc.lpszClassName = L"myon_fmplayer_ym2608_toneviewer"; + g.toneviewer_class = RegisterClass(&wc); + } + if (!g.toneviewer_class) { + MessageBox(parent, L"Cannot register class", L"Error", MB_ICONSTOP); + return; + } + g.toneviewer = CreateWindowEx(0, + MAKEINTATOM(g.toneviewer_class), + L"FMPlayer Tone Viewer", + WS_CAPTION | WS_SYSMENU | WS_MINIMIZEBOX, + CW_USEDEFAULT, CW_USEDEFAULT, CW_USEDEFAULT, CW_USEDEFAULT, + parent, 0, g.hinst, 0); + } else { + SetForegroundWindow(g.toneviewer); + } +} diff --git a/win32/toneview.h b/win32/toneview.h new file mode 100644 index 0000000..32f402b --- /dev/null +++ b/win32/toneview.h @@ -0,0 +1,16 @@ +#ifndef MYON_FMPLAYER_WIN32_TONEVIEW_H_INCLUDED +#define MYON_FMPLAYER_WIN32_TONEVIEW_H_INCLUDED + +#include "tonedata/tonedata.h" +#include +#define WIN32_LEAN_AND_MEAN +#include + +extern struct toneview_g { + struct fmplayer_tonedata tonedata; + atomic_flag flag; +} toneview_g; + +void show_toneview(HINSTANCE hinst, HWND parent); + +#endif // MYON_FMPLAYER_WIN32_TONEVIEW_H_INCLUDED diff --git a/win32/x86/Makefile b/win32/x86/Makefile index 2f523a3..1a4c6d8 100644 --- a/win32/x86/Makefile +++ b/win32/x86/Makefile @@ -2,6 +2,7 @@ vpath %.c ../ vpath %.c ../../fmdriver vpath %.c ../../libopna vpath %.c ../../fmdsp +vpath %.c ../../tonedata vpath %.rc .. include ../fmplayer.mak -- cgit v1.2.3