├── resource.rc ├── icon.ico ├── resource.res ├── .gitattributes ├── LICENSE ├── README.md └── epct.cpp /resource.rc: -------------------------------------------------------------------------------- 1 | 1 ICON "icon.ico" -------------------------------------------------------------------------------- /icon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ran4erep/EPCT/main/icon.ico -------------------------------------------------------------------------------- /resource.res: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ran4erep/EPCT/main/resource.res -------------------------------------------------------------------------------- /.gitattributes: -------------------------------------------------------------------------------- 1 | # Auto detect text files and perform LF normalization 2 | * text=auto 3 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Creative Commons Attribution-NonCommercial 4.0 International (CC BY-NC 4.0) 2 | 3 | Copyright (c) 2025 ran4erep 4 | 5 | This work is licensed under a Creative Commons Attribution-NonCommercial 4.0 International License. 6 | 7 | You are free to 8 | - Share — copy and redistribute the material in any medium or format 9 | - Adapt — remix, transform, and build upon the material 10 | 11 | Under the following terms 12 | - Attribution — You must give appropriate credit, provide a link to the license, and indicate if changes were made. 13 | - NonCommercial — You may not use the material for commercial purposes. 14 | 15 | The licensor cannot revoke these freedoms as long as you follow the license terms. 16 | 17 | Full license text httpscreativecommons.orglicensesby-nc4.0legalcode 18 | 19 | Notices 20 | - You do not have to comply with the license for elements of the material in the public domain or where your use is permitted by an applicable exception or limitation. 21 | - No warranties are given. The license may not give you all of the permissions necessary for your intended use. -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # EPCT (Earphone Channel Test) 2 | 3 | EPCT is a lightweight Windows utility that helps users identify the correct orientation of their headphones (Left/Right) through a stereo sound test when connecting them to the computer. Perfect for headphones with worn-out L/R markings or those without any markings at all. 4 | 5 | ## Features 6 | 7 | - Detects headphone connection event 8 | - Plays a stereo test sound (moving from left to right channel) 9 | - Minimal resource usage (<1.5 MB RAM) 10 | - Works in background without UI 11 | 12 | ## Why Use It? 13 | 14 | - Your headphones have worn-out L/R markings 15 | - Your headphones came without L/R markings 16 | - You need a simple tool for testing stereo channels 17 | 18 | ## How It Works 19 | 20 | When you connect your headphones, the program plays a short musical sequence that moves from the left to the right channel. This helps you determine if your headphones are worn correctly: 21 | - If the sound moves from left to right naturally - your headphones are oriented correctly 22 | - If the sound moves from right to left - swap the headphones 23 | 24 | ## Auto-start with Windows 25 | To make the program start automatically with Windows: 26 | 27 | Press Win + R 28 | Type `shell:startup` 29 | Copy EPCT.exe in the opened folder 30 | 31 | ## Installation 32 | 33 | 1. Download the latest release from [Releases](../../releases) 34 | 2. Run `EPCT.exe` 35 | 3. Program will create necessary files in temp directory on first run 36 | 37 | ## Building from Source 38 | 39 | Requirements: 40 | - MinGW-w64 or other C++ compiler with Windows API support 41 | - Windows SDK 42 | 43 | ### Compile resources 44 | `windres resource.rc -O coff -o resource.res` 45 | 46 | ### Compile program 47 | `g++ -O2 -o EPCT.exe epct.cpp resource.res -lwinmm -mwindows -DUNICODE` 48 | 49 | ## License 50 | 51 | [CC BY-NC 4.0](LICENSE) 52 | 53 | This software is licensed under Creative Commons Attribution-NonCommercial 4.0 International License. This means you can freely use and modify the code for non-commercial purposes, as long as you provide attribution to the original author. 54 | 55 | Commercial use is prohibited without explicit permission from the author. 56 | 57 | ## Author 58 | ran4erep 59 | 60 | Last updated: 2025-02-11 61 | -------------------------------------------------------------------------------- /epct.cpp: -------------------------------------------------------------------------------- 1 | #include 2 | #include 3 | #include 4 | #include 5 | #include 6 | #include 7 | #include 8 | #include 9 | #include 10 | #include 11 | 12 | #pragma comment(lib, "user32.lib") 13 | #pragma comment(lib, "winmm.lib") 14 | 15 | DEFINE_GUID(GUID_DEVINTERFACE_AUDIO_RENDER, 0xE6327CAD, 0xDCEC, 0x4949, 0xAE, 0x8A, 0x99, 0x1E, 0x97, 0x6A, 0x79, 0xD2); 16 | 17 | HWND g_hwnd = NULL; 18 | bool g_lastState = false; 19 | bool g_firstRun = true; 20 | WCHAR g_wavPath[MAX_PATH]; 21 | 22 | #pragma pack(push, 1) 23 | struct WAVHeader { 24 | char riffID[4] = {'R', 'I', 'F', 'F'}; 25 | uint32_t size; 26 | char waveID[4] = {'W', 'A', 'V', 'E'}; 27 | char fmtID[4] = {'f', 'm', 't', ' '}; 28 | uint32_t fmtSize = 16; 29 | uint16_t audioFormat = 1; 30 | uint16_t numChannels = 2; 31 | uint32_t sampleRate = 44100; 32 | uint32_t byteRate; 33 | uint16_t blockAlign; 34 | uint16_t bitsPerSample = 16; 35 | char dataID[4] = {'d', 'a', 't', 'a'}; 36 | uint32_t dataSize; 37 | }; 38 | #pragma pack(pop) 39 | 40 | struct Note { 41 | int16_t note; 42 | uint16_t duration; 43 | }; 44 | 45 | void GenerateWavFile(const WCHAR* filename) { 46 | const Note melody[] = { 47 | {76, 150}, 48 | {81, 150}, 49 | {83, 300} 50 | }; 51 | 52 | FILE* file; 53 | if (_wfopen_s(&file, filename, L"wb") != 0 || !file) return; 54 | 55 | constexpr size_t BUFFER_SIZE = 1024; 56 | int16_t buffer[BUFFER_SIZE]; 57 | size_t bufferIndex = 0; 58 | 59 | WAVHeader header; 60 | header.blockAlign = 4; 61 | header.byteRate = 176400; 62 | header.dataSize = 105840; 63 | header.size = header.dataSize + 36; 64 | 65 | fwrite(&header, sizeof(header), 1, file); 66 | 67 | int noteStartSample = 0; 68 | const int totalSamples = 26460; 69 | 70 | for (const Note& m : melody) { 71 | const int noteSamples = (44100 * m.duration) / 1000; 72 | const float freq = 440.0f * pow(2.0f, (m.note - 69) / 12.0f); 73 | 74 | for (int i = 0; i < noteSamples; i++) { 75 | const float time = (float)i / 44100; 76 | const float progress = (float)i / noteSamples; 77 | 78 | float envelope = 1.0f; 79 | if (progress < 0.1f) 80 | envelope = progress * 10; 81 | else if (progress > 0.8f) 82 | envelope = (1.0f - progress) * 5; 83 | 84 | const float totalProgress = (float)(noteStartSample + i) / totalSamples; 85 | const int16_t sample = (int16_t)(16383.5f * envelope * sin(6.28318f * freq * time)); 86 | 87 | buffer[bufferIndex++] = (int16_t)(sample * (1.0f - totalProgress)); 88 | buffer[bufferIndex++] = (int16_t)(sample * totalProgress); 89 | 90 | if (bufferIndex >= BUFFER_SIZE) { 91 | fwrite(buffer, sizeof(int16_t), bufferIndex, file); 92 | bufferIndex = 0; 93 | } 94 | } 95 | noteStartSample += noteSamples; 96 | } 97 | 98 | if (bufferIndex > 0) { 99 | fwrite(buffer, sizeof(int16_t), bufferIndex, file); 100 | } 101 | 102 | fclose(file); 103 | } 104 | 105 | bool InitializeAudio() { 106 | if (!GetTempPathW(MAX_PATH, g_wavPath)) return false; 107 | wcscat_s(g_wavPath, MAX_PATH, L"EPCT.wav"); 108 | 109 | if (GetFileAttributesW(g_wavPath) == INVALID_FILE_ATTRIBUTES) { 110 | GenerateWavFile(g_wavPath); 111 | } 112 | return true; 113 | } 114 | 115 | bool IsAudioDeviceReady() { 116 | WAVEOUTCAPS woc; 117 | return waveOutGetDevCaps(WAVE_MAPPER, &woc, sizeof(WAVEOUTCAPS)) == MMSYSERR_NOERROR; 118 | } 119 | 120 | void PlayStereoSound(bool isConnected) { 121 | if (!isConnected) return; 122 | 123 | timeBeginPeriod(1); 124 | 125 | for (int i = 0; i < 10; i++) { 126 | if (IsAudioDeviceReady()) { 127 | Sleep(100); 128 | PlaySoundW(g_wavPath, NULL, SND_FILENAME | SND_SYNC); 129 | break; 130 | } 131 | Sleep(100); 132 | } 133 | 134 | timeEndPeriod(1); 135 | } 136 | 137 | LRESULT CALLBACK WndProc(HWND hwnd, UINT msg, WPARAM wParam, LPARAM lParam) { 138 | switch (msg) { 139 | case WM_DEVICECHANGE: { 140 | if (wParam == DBT_DEVICEARRIVAL || wParam == DBT_DEVICEREMOVECOMPLETE) { 141 | DEV_BROADCAST_HDR* pHdr = (DEV_BROADCAST_HDR*)lParam; 142 | if (pHdr->dbch_devicetype == DBT_DEVTYP_DEVICEINTERFACE) { 143 | DEV_BROADCAST_DEVICEINTERFACE* pDevInf = (DEV_BROADCAST_DEVICEINTERFACE*)pHdr; 144 | if (IsEqualGUID(pDevInf->dbcc_classguid, GUID_DEVINTERFACE_AUDIO_RENDER)) { 145 | bool currentState = (wParam == DBT_DEVICEARRIVAL); 146 | if (g_firstRun) { 147 | g_lastState = currentState; 148 | g_firstRun = false; 149 | } 150 | else if (currentState != g_lastState) { 151 | PlayStereoSound(currentState); 152 | g_lastState = currentState; 153 | } 154 | } 155 | } 156 | } 157 | break; 158 | } 159 | case WM_DESTROY: 160 | PostQuitMessage(0); 161 | return 0; 162 | } 163 | return DefWindowProcW(hwnd, msg, wParam, lParam); 164 | } 165 | 166 | int WINAPI WinMain(HINSTANCE hInstance, HINSTANCE hPrevInstance, LPSTR lpCmdLine, int nCmdShow) { 167 | if (!InitializeAudio()) return 1; 168 | 169 | WNDCLASSEXW wc = {0}; 170 | wc.cbSize = sizeof(WNDCLASSEXW); 171 | wc.lpfnWndProc = WndProc; 172 | wc.hInstance = hInstance; 173 | wc.hIcon = LoadIcon(hInstance, MAKEINTRESOURCE(1)); 174 | wc.hIconSm = LoadIcon(hInstance, MAKEINTRESOURCE(1)); 175 | wc.lpszClassName = L"EPCTClass"; 176 | 177 | if (!RegisterClassExW(&wc)) return 1; 178 | 179 | g_hwnd = CreateWindowExW(0, L"EPCTClass", L"EPCT", 180 | WS_OVERLAPPED, CW_USEDEFAULT, CW_USEDEFAULT, 1, 1, NULL, NULL, hInstance, NULL); 181 | if (!g_hwnd) return 1; 182 | 183 | DEV_BROADCAST_DEVICEINTERFACE_W notificationFilter = {0}; 184 | notificationFilter.dbcc_size = sizeof(notificationFilter); 185 | notificationFilter.dbcc_devicetype = DBT_DEVTYP_DEVICEINTERFACE; 186 | notificationFilter.dbcc_classguid = GUID_DEVINTERFACE_AUDIO_RENDER; 187 | 188 | HDEVNOTIFY deviceNotify = RegisterDeviceNotificationW(g_hwnd, ¬ificationFilter, 189 | DEVICE_NOTIFY_WINDOW_HANDLE); 190 | if (!deviceNotify) { 191 | DestroyWindow(g_hwnd); 192 | return 1; 193 | } 194 | 195 | MSG msg; 196 | while (GetMessage(&msg, NULL, 0, 0)) { 197 | TranslateMessage(&msg); 198 | DispatchMessageW(&msg); 199 | } 200 | 201 | UnregisterDeviceNotification(deviceNotify); 202 | return (int)msg.wParam; 203 | } 204 | --------------------------------------------------------------------------------