1 /** 2 * ae.demo.inputtiming.main 3 * 4 * License: 5 * This Source Code Form is subject to the terms of 6 * the Mozilla Public License, v. 2.0. If a copy of 7 * the MPL was not distributed with this file, You 8 * can obtain one at http://mozilla.org/MPL/2.0/. 9 * 10 * Authors: 11 * Vladimir Panteleev <ae@cy.md> 12 */ 13 14 module ae.demo.inputtiming.main; 15 16 import core.time; 17 18 import std.conv; 19 import std.format; 20 import std.math; 21 22 import ae.ui.app.application; 23 import ae.ui.app.main; 24 import ae.ui.audio.mixer.software; 25 import ae.ui.audio.sdl2.audio; 26 import ae.ui.audio.source.base; 27 import ae.ui.audio.source.memory; 28 import ae.ui.shell.shell; 29 import ae.ui.shell.sdl2.shell; 30 import ae.ui.video.bmfont; 31 import ae.ui.video.renderer; 32 import ae.ui.video.sdl2.video; 33 import ae.utils.fps; 34 import ae.utils.graphics.fonts.draw; 35 import ae.utils.graphics.fonts.font8x8; 36 import ae.utils.math; 37 import ae.utils.graphics.image; 38 import ae.utils.meta; 39 40 final class MyApplication : Application 41 { 42 override string getName() { return "Demo/Input"; } 43 override string getCompanyName() { return "CyberShadow"; } 44 45 Shell shell; 46 47 enum BAND_WIDTH = 800; 48 enum BAND_INTERVAL = 100; 49 enum BAND_TOP = 50; 50 enum BAND_HEIGHT = 30; 51 enum BAND_HNSECS_PER_PIXEL = 10_000_0; 52 enum HISTORY_TOP = 150; 53 enum HISTORY_HEIGHT = 50; 54 enum HISTORY_LEFT = 150; 55 56 enum Mode { video, audio } 57 enum Device : int { keyboard, joypad, mouse } 58 enum SampleType : int { precision, duration } 59 60 int[][enumLength!SampleType][enumLength!Device] history; 61 Mode mode; 62 enum SAMPLE_COLORS = [BGRX(0, 0, 255), BGRX(0, 255, 0)]; 63 64 /// Some (precise) time value of the moment, in hnsecs. 65 @property long now() { return TickDuration.currSystemTick.to!("hnsecs", long); } 66 67 FontTextureSource!Font8x8 font; 68 MemorySoundSource!SoundSample tick; 69 70 this() 71 { 72 font = new FontTextureSource!Font8x8(font8x8, BGRX(128, 128, 128)); 73 tick = memorySoundSource([short.max, short.min], 44100); 74 } 75 76 override bool needSound() { return true; } 77 78 static long lastTick; 79 80 override void render(Renderer s) 81 { 82 s.clear(); 83 auto t = now; 84 85 if (!lastTick) 86 { 87 shell.setCaption("Press m to switch mode, Esc to exit, any other key to measure latency"); 88 lastTick = t; 89 } 90 91 final switch (mode) 92 { 93 case Mode.video: 94 { 95 auto x = t / BAND_HNSECS_PER_PIXEL; 96 s.line(BAND_WIDTH/2, BAND_TOP, BAND_WIDTH/2, BAND_TOP + BAND_HEIGHT, BGRX(0, 0, 255)); 97 foreach (lx; 0..BAND_WIDTH) 98 if ((lx+x)%BAND_INTERVAL == 0) 99 s.line(lx, BAND_TOP+BAND_HEIGHT, lx, BAND_TOP+BAND_HEIGHT*2, BGRX(0, 255, 0)); 100 break; 101 } 102 case Mode.audio: 103 { 104 auto str = "Press a key when you hear the tick sound"; 105 font.drawText(s, (BAND_WIDTH - str.length.to!int * font8x8.maxWidth)/2, BAND_TOP+BAND_HEIGHT, str); 106 enum sampleInterval = BAND_HNSECS_PER_PIXEL * BAND_INTERVAL; 107 if (t / sampleInterval != lastTick / sampleInterval) 108 shell.audio.mixer.playSound(tick); 109 break; 110 } 111 } 112 113 foreach (device, deviceSamples; history) 114 foreach (sampleType, samples; deviceSamples) 115 { 116 auto y = HISTORY_TOP + HISTORY_HEIGHT * (device*3 + sampleType*2 + 1); 117 s.line(0, y - HISTORY_HEIGHT, s.width, y - HISTORY_HEIGHT, BGRX.monochrome(0x40)); 118 foreach (index, sample; samples) 119 { 120 if (sample > HISTORY_HEIGHT) 121 sample = HISTORY_HEIGHT; 122 s.line(HISTORY_LEFT + index, y - sample, HISTORY_LEFT + index, y, SAMPLE_COLORS[sampleType]); 123 } 124 } 125 126 foreach (Device device, deviceSamples; history) 127 foreach (SampleType sampleType, samples; deviceSamples) 128 { 129 auto y = HISTORY_TOP + HISTORY_HEIGHT * (device*3 + sampleType*2 + 1); 130 if (sampleType == SampleType.duration) y -= HISTORY_HEIGHT/2; 131 y -= font8x8.height / 2; 132 font.drawText(s, 2, y.to!int, "%8s %-9s".format(device, sampleType)); 133 } 134 135 lastTick = t; 136 } 137 138 override int run(string[] args) 139 { 140 shell = new SDL2Shell(this); 141 shell.video = new SDL2Video(); 142 143 shell.audio = new SDL2Audio(); 144 shell.audio.mixer = new SoftwareMixer(); 145 146 shell.run(); 147 shell.video.shutdown(); 148 return 0; 149 } 150 151 long pressed; 152 153 void keyDown(Device device) 154 { 155 // Skip keyDown events without corresponding keyUp 156 if (history[device][SampleType.precision].length > history[device][SampleType.duration].length) 157 return; 158 159 pressed = now; 160 auto x = cast(int)(pressed / BAND_HNSECS_PER_PIXEL + BAND_WIDTH/2) % BAND_INTERVAL; 161 if (x > BAND_INTERVAL/2) 162 x -= BAND_INTERVAL; 163 history[device][SampleType.precision] ~= x; 164 } 165 166 void keyUp(Device device) 167 { 168 auto duration = now - pressed; 169 history[device][SampleType.duration] ~= cast(int)(duration / BAND_HNSECS_PER_PIXEL); 170 } 171 172 bool ignoreKeyUp; 173 174 override void handleKeyDown(Key key, dchar character) 175 { 176 ignoreKeyUp = true; 177 if (key == Key.esc) 178 shell.quit(); 179 else 180 if (character == 'm') 181 mode++, mode %= enumLength!Mode; 182 else 183 if (character == 's') 184 shell.audio.mixer.playSound(tick); 185 else 186 { 187 keyDown(Device.keyboard); 188 ignoreKeyUp = false; 189 } 190 } 191 192 override void handleKeyUp(Key key) 193 { 194 if (ignoreKeyUp) 195 ignoreKeyUp = false; 196 else 197 keyUp(Device.keyboard); 198 } 199 200 override bool needJoystick() { return true; } 201 202 override void handleJoyButtonDown(int button) 203 { 204 keyDown(Device.joypad); 205 } 206 207 override void handleJoyButtonUp (int button) 208 { 209 keyUp (Device.joypad); 210 } 211 212 override void handleMouseDown(uint x, uint y, MouseButton button) 213 { 214 keyDown(Device.mouse); 215 } 216 217 override void handleMouseUp(uint x, uint y, MouseButton button) 218 { 219 keyUp (Device.mouse); 220 } 221 222 override void handleQuit() 223 { 224 shell.quit(); 225 } 226 } 227 228 shared static this() 229 { 230 createApplication!MyApplication(); 231 }