Mac OS X ambilight
You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

AppDelegate.m 18KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498
  1. //
  2. // AppDelegate.m
  3. // DisplayBacklight
  4. //
  5. // Created by Thomas Buck on 21.12.15.
  6. // Copyright © 2015 xythobuz. All rights reserved.
  7. //
  8. #import "AppDelegate.h"
  9. #import "Serial.h"
  10. #import "Screenshot.h"
  11. // This defined the update-speed of the Ambilight, in seconds.
  12. // With a baudrate of 115200 and 156 LEDs and 14-bytes Magic-Word,
  13. // theoretically you could transmit:
  14. // 115200 / (14 + (156 * 3) * 8) =~ 30 Frames per Second
  15. // Inserting (1.0 / 30.0) here would try to reach these 30FPS,
  16. // but will probably cause high CPU-Usage.
  17. // (Run-Time of the algorithm is ignored here, so real speed will be
  18. // slightly lower.)
  19. #define DISPLAY_DELAY (1.0 / 30.0)
  20. // Magic identifying string used to differntiate start of packets.
  21. // Has to be the same here and in the Arduino Sketch.
  22. #define MAGIC_WORD @"xythobuzRGBled"
  23. // These are the values stored persistently in the preferences
  24. #define PREF_SERIAL_PORT @"SerialPort"
  25. #define PREF_BRIGHTNESS @"Brightness"
  26. #define PREF_TURNED_ON @"IsEnabled"
  27. @interface AppDelegate ()
  28. @property (weak) IBOutlet NSMenu *statusMenu;
  29. @property (weak) IBOutlet NSMenu *menuPorts;
  30. @property (weak) IBOutlet NSMenuItem *buttonAmbilight;
  31. @property (weak) IBOutlet NSMenuItem *brightnessItem;
  32. @property (weak) IBOutlet NSSlider *brightnessSlider;
  33. @property (weak) IBOutlet NSMenuItem *brightnessLabel;
  34. @property (strong) NSStatusItem *statusItem;
  35. @property (strong) NSImage *statusImage;
  36. @property (strong) NSTimer *timer;
  37. @property (strong) Serial *serial;
  38. @property (strong) NSArray *lastDisplayIDs;
  39. @property (assign) BOOL restartAmbilight;
  40. @end
  41. @implementation AppDelegate
  42. @synthesize statusMenu, application;
  43. @synthesize menuPorts, buttonAmbilight;
  44. @synthesize brightnessItem, brightnessSlider, brightnessLabel;
  45. @synthesize statusItem, statusImage, lastDisplayIDs;
  46. @synthesize timer, serial, restartAmbilight;
  47. - (void)applicationDidFinishLaunching:(NSNotification *)aNotification {
  48. serial = [[Serial alloc] init];
  49. timer = nil;
  50. restartAmbilight = NO;
  51. // Set default configuration values
  52. NSUserDefaults *store = [NSUserDefaults standardUserDefaults];
  53. NSMutableDictionary *appDefaults = [NSMutableDictionary dictionaryWithObject:@"" forKey:PREF_SERIAL_PORT];
  54. [appDefaults setObject:[NSNumber numberWithFloat:50.0] forKey:PREF_BRIGHTNESS];
  55. [appDefaults setObject:[NSNumber numberWithBool:NO] forKey:PREF_TURNED_ON];
  56. [store registerDefaults:appDefaults];
  57. [store synchronize];
  58. // Load existing configuration values
  59. NSString *savedPort = [store stringForKey:PREF_SERIAL_PORT];
  60. float brightness = [store floatForKey:PREF_BRIGHTNESS];
  61. BOOL ambilightIsOn = [store boolForKey:PREF_TURNED_ON];
  62. // Prepare status bar menu
  63. statusImage = [NSImage imageNamed:@"MenuIcon"];
  64. statusItem = [[NSStatusBar systemStatusBar] statusItemWithLength:NSSquareStatusItemLength];
  65. [statusImage setTemplate:YES];
  66. [statusItem setImage:statusImage];
  67. [statusItem setMenu:statusMenu];
  68. // Prepare brightness menu
  69. brightnessItem.view = brightnessSlider;
  70. [brightnessSlider setFloatValue:brightness];
  71. [brightnessLabel setTitle:[NSString stringWithFormat:@"Value: %.0f%%", brightness]];
  72. // Prepare serial port menu
  73. BOOL foundPort = NO;
  74. NSArray *ports = [Serial listSerialPorts];
  75. if ([ports count] > 0) {
  76. [menuPorts removeAllItems];
  77. for (int i = 0; i < [ports count]; i++) {
  78. // Add Menu Item for this port
  79. NSMenuItem *item = [[NSMenuItem alloc] initWithTitle:[ports objectAtIndex:i] action:@selector(selectedSerialPort:) keyEquivalent:@""];
  80. [menuPorts addItem:item];
  81. // Set Enabled if it was used the last time
  82. if ((savedPort != nil) && [[ports objectAtIndex:i] isEqualToString:savedPort]) {
  83. // Try to open serial port
  84. [serial setPortName:savedPort];
  85. if (![serial openPort]) {
  86. foundPort = YES;
  87. [[menuPorts itemAtIndex:i] setState:NSOnState];
  88. }
  89. }
  90. }
  91. if (!foundPort) {
  92. // I'm using a cheap chinese Arduino Nano clone with a CH340 chipset.
  93. // This driver creates device-files in /dev/cu.* that don't correspond
  94. // to the chip-id and change every time the adapter is re-enumerated.
  95. // That means we may have to try and find the device again after the
  96. // stored name does no longer exist. In this case, we simply try the first
  97. // device that starts with /dev/cu.wchusbserial*...
  98. for (int i = 0; i < [ports count]; i++) {
  99. if ([[ports objectAtIndex:i] hasPrefix:@"/dev/cu.wchusbserial"]) {
  100. // Try to open serial port
  101. [serial setPortName:savedPort];
  102. if (![serial openPort]) {
  103. [[menuPorts itemAtIndex:i] setState:NSOnState];
  104. // Reattempt next matching device when opening this one fails.
  105. break;
  106. }
  107. }
  108. }
  109. }
  110. }
  111. [Screenshot init:self];
  112. lastDisplayIDs = [Screenshot listDisplays];
  113. if (ambilightIsOn) {
  114. timer = [NSTimer scheduledTimerWithTimeInterval:DISPLAY_DELAY target:self selector:@selector(visualizeDisplay:) userInfo:nil repeats:NO];
  115. [buttonAmbilight setState:NSOnState];
  116. }
  117. }
  118. - (void)applicationWillTerminate:(NSNotification *)aNotification {
  119. // Stop previous timer setting
  120. if (timer != nil) {
  121. [timer invalidate];
  122. timer = nil;
  123. }
  124. // Remove display callback
  125. [Screenshot close:self];
  126. // Turn off all lights if possible
  127. if ([serial isOpen]) {
  128. [self sendNullFrame];
  129. [serial closePort];
  130. }
  131. }
  132. - (IBAction)relistSerialPorts:(id)sender {
  133. // Refill port list
  134. NSArray *ports = [Serial listSerialPorts];
  135. [menuPorts removeAllItems];
  136. for (int i = 0; i < [ports count]; i++) {
  137. // Add Menu Item for this port
  138. NSMenuItem *item = [[NSMenuItem alloc] initWithTitle:[ports objectAtIndex:i] action:@selector(selectedSerialPort:) keyEquivalent:@""];
  139. [menuPorts addItem:item];
  140. // Mark it if it is currently open
  141. if ([serial isOpen]) {
  142. if ([[ports objectAtIndex:i] isEqualToString:[serial portName]]) {
  143. [[menuPorts itemAtIndex:i] setState:NSOnState];
  144. }
  145. }
  146. }
  147. }
  148. - (void)selectedSerialPort:(NSMenuItem *)source {
  149. // Store selection for next start-up
  150. NSUserDefaults *store = [NSUserDefaults standardUserDefaults];
  151. [store setObject:[source title] forKey:PREF_SERIAL_PORT];
  152. [store synchronize];
  153. // De-select all other ports
  154. for (int i = 0; i < [menuPorts numberOfItems]; i++) {
  155. [[menuPorts itemAtIndex:i] setState:NSOffState];
  156. }
  157. // Select only the current port
  158. [source setState:NSOnState];
  159. // Close previously opened port, if any
  160. if ([serial isOpen]) {
  161. [serial closePort];
  162. }
  163. // Stop previous timer setting
  164. if (timer != nil) {
  165. [timer invalidate];
  166. timer = nil;
  167. }
  168. // Turn off ambilight button
  169. [buttonAmbilight setState:NSOffState];
  170. // Try to open selected port
  171. [serial setPortName:[source title]];
  172. if ([serial openPort] != 0) {
  173. [source setState:NSOffState];
  174. }
  175. }
  176. - (IBAction)toggleAmbilight:(NSMenuItem *)sender {
  177. if ([sender state] == NSOnState) {
  178. [sender setState:NSOffState];
  179. // Stop previous timer setting
  180. if (timer != nil) {
  181. [timer invalidate];
  182. timer = nil;
  183. }
  184. [self sendNullFrame];
  185. // Store state
  186. NSUserDefaults *store = [NSUserDefaults standardUserDefaults];
  187. [store setObject:[NSNumber numberWithBool:NO] forKey:PREF_TURNED_ON];
  188. [store synchronize];
  189. } else {
  190. [sender setState:NSOnState];
  191. timer = [NSTimer scheduledTimerWithTimeInterval:DISPLAY_DELAY target:self selector:@selector(visualizeDisplay:) userInfo:nil repeats:NO];
  192. // Store state
  193. NSUserDefaults *store = [NSUserDefaults standardUserDefaults];
  194. [store setObject:[NSNumber numberWithBool:YES] forKey:PREF_TURNED_ON];
  195. [store synchronize];
  196. }
  197. }
  198. - (void)stopAmbilight {
  199. restartAmbilight = NO;
  200. if (timer != nil) {
  201. restartAmbilight = YES;
  202. [timer invalidate];
  203. timer = nil;
  204. [buttonAmbilight setState:NSOffState];
  205. }
  206. }
  207. - (void)newDisplayList:(NSArray *)displayIDs {
  208. lastDisplayIDs = displayIDs;
  209. if (restartAmbilight) {
  210. restartAmbilight = NO;
  211. [buttonAmbilight setState:NSOnState];
  212. timer = [NSTimer scheduledTimerWithTimeInterval:DISPLAY_DELAY target:self selector:@selector(visualizeDisplay:) userInfo:nil repeats:NO];
  213. }
  214. }
  215. - (IBAction)brightnessMoved:(NSSlider *)sender {
  216. [brightnessLabel setTitle:[NSString stringWithFormat:@"Value: %.0f%%", [sender floatValue]]];
  217. // Store changed value in preferences
  218. NSUserDefaults *store = [NSUserDefaults standardUserDefaults];
  219. [store setObject:[NSNumber numberWithFloat:[sender floatValue]] forKey:PREF_BRIGHTNESS];
  220. [store synchronize];
  221. }
  222. - (IBAction)showAbout:(id)sender {
  223. [NSApp activateIgnoringOtherApps:YES];
  224. [application orderFrontStandardAboutPanel:self];
  225. }
  226. // ----------------------------------------------------
  227. // ------------ 'Ambilight' Visualizations ------------
  228. // ----------------------------------------------------
  229. // ToDo: add support for display names or IDs here, so we can distinguish
  230. // between multiple displays with the same resolution
  231. struct DisplayAssignment {
  232. int width, height;
  233. };
  234. struct LEDStrand {
  235. int idMin, idMax;
  236. int display;
  237. int startX, startY;
  238. int direction;
  239. int size;
  240. };
  241. #define DIR_LEFT 0
  242. #define DIR_RIGHT 1
  243. #define DIR_UP 2
  244. #define DIR_DOWN 3
  245. // ----------------------- Config starts here -----------------------
  246. // The idea behind this algorithm is very simple. It assumes that each LED strand
  247. // follows one edge of one of your displays. So one of the two coordinates should
  248. // always be zero or the width / height of your display.
  249. // Define the amount of LEDs in your strip here
  250. #define LED_COUNT 156
  251. // This defined how large the averaging-boxes should be in the dimension perpendicular
  252. // to the strand. So eg. for a bottom strand, how high the box should be in px.
  253. #define COLOR_AVERAGE_OTHER_DIMENSION_SIZE 150
  254. // Identify your displays here. Currently they're only distinguished by their resolution.
  255. // The ID will be the index in the list, so the first entry is display 0 and so on.
  256. struct DisplayAssignment displays[] = {
  257. { 1920, 1080 },
  258. { 900, 1600 }
  259. };
  260. // This defined the orientation and placement of your strands and is the most important part.
  261. // It begins with the LED IDs this strand includes, starting with ID 0 up to LED_COUNT - 1.
  262. // The third item is the display ID, defined by the previous struct.
  263. // The fourth and fifth items are the starting X and Y coordinates of the strand.
  264. // As described above, one should always be zero or the display width / height.
  265. // The sixth element is the direction the strand goes (no diagonals supported yet).
  266. // The last element is the size of the averaging-box for each LED, moving with the strand.
  267. // So, if your strand contains 33 LEDs and spans 1920 pixels, this should be (1920 / 33).
  268. struct LEDStrand strands[] = {
  269. { 0, 32, 0, 1920, 1080, DIR_LEFT, 1920 / 33 },
  270. { 33, 51, 0, 0, 1080, DIR_UP, 1080 / 19 },
  271. { 52, 84, 0, 0, 0, DIR_RIGHT, 1920 / 33 },
  272. { 85, 89, 1, 0, 250, DIR_UP, 250 / 5 },
  273. { 90, 106, 1, 0, 0, DIR_RIGHT, 900 / 17 },
  274. { 107, 134, 1, 900, 0, DIR_DOWN, 1600 / 28 },
  275. { 135, 151, 1, 900, 1600, DIR_LEFT, 900 / 17 },
  276. { 152, 155, 1, 0, 1600, DIR_UP, 180 / 4 }
  277. };
  278. // ------------------------ Config ends here ------------------------
  279. UInt8 ledColorData[LED_COUNT * 3];
  280. - (UInt32)calculateAverage:(unsigned char *)data Width:(NSInteger)width Height:(NSInteger)height SPP:(NSInteger)spp Alpha:(BOOL)alpha StartX:(NSInteger)startX StartY:(NSInteger)startY EndX:(NSInteger)endX EndY:(NSInteger)endY {
  281. int redC = 0, greenC = 1, blueC = 2;
  282. if (alpha) {
  283. redC = 1; greenC = 2; blueC = 3;
  284. }
  285. NSInteger xa, xb, ya, yb;
  286. if (startX < endX) {
  287. xa = startX;
  288. xb = endX;
  289. } else {
  290. xa = endX;
  291. xb = startX;
  292. }
  293. if (startY < endY) {
  294. ya = startY;
  295. yb = endY;
  296. } else {
  297. ya = endY;
  298. yb = startY;
  299. }
  300. unsigned long red = 0, green = 0, blue = 0, count = 0;
  301. for (NSInteger i = xa; i < xb; i++) {
  302. for (NSInteger j = ya; j < yb; j++) {
  303. count++;
  304. unsigned long index = i + (j * width);
  305. red += data[(index * spp) + redC];
  306. green += data[(index * spp) + greenC];
  307. blue += data[(index * spp) + blueC];
  308. }
  309. }
  310. red /= count;
  311. green /= count;
  312. blue /= count;
  313. red *= [brightnessSlider floatValue] / 100.0f;
  314. green *= [brightnessSlider floatValue] / 100.0f;
  315. blue *= [brightnessSlider floatValue] / 100.0f;
  316. return ((UInt32)red << 16) | ((UInt32)green << 8) | ((UInt32)blue);
  317. }
  318. - (void)visualizeSingleDisplay:(NSInteger)disp Data:(unsigned char *)data Width:(unsigned long)width Height:(unsigned long)height SPP:(NSInteger)spp Alpha:(BOOL)alpha {
  319. for (int i = 0; i < (sizeof(strands) / sizeof(strands[0])); i++) {
  320. if (strands[i].display == disp) {
  321. // Walk the strand, calculating value for each LED
  322. unsigned long x = strands[i].startX;
  323. unsigned long y = strands[i].startY;
  324. unsigned long blockWidth = COLOR_AVERAGE_OTHER_DIMENSION_SIZE;
  325. unsigned long blockHeight = COLOR_AVERAGE_OTHER_DIMENSION_SIZE;
  326. if ((strands[i].direction == DIR_LEFT) || (strands[i].direction == DIR_RIGHT)) {
  327. blockWidth = strands[i].size;
  328. } else {
  329. blockHeight = strands[i].size;
  330. }
  331. for (int led = strands[i].idMin; led <= strands[i].idMax; led++) {
  332. // First move appropriately in the direction of the strand
  333. unsigned long endX = x, endY = y;
  334. if (strands[i].direction == DIR_LEFT) {
  335. endX -= blockWidth;
  336. } else if (strands[i].direction == DIR_RIGHT) {
  337. endX += blockWidth;
  338. } else if (strands[i].direction == DIR_UP) {
  339. endY -= blockHeight;
  340. } else if (strands[i].direction == DIR_DOWN) {
  341. endY += blockHeight;
  342. }
  343. // But also span the averaging-square in the other dimension, depending on which
  344. // side of the monitor we're at.
  345. if ((strands[i].direction == DIR_LEFT) || (strands[i].direction == DIR_RIGHT)) {
  346. if (y == 0) {
  347. endY = blockHeight;
  348. } else if (y == displays[disp].height) {
  349. endY -= blockHeight;
  350. }
  351. } else {
  352. if (x == 0) {
  353. endX = blockWidth;
  354. } else if (x == displays[disp].width) {
  355. endX -= blockWidth;
  356. }
  357. }
  358. // Calculate average color for this led
  359. UInt32 color = [self calculateAverage:data Width:width Height:height SPP:spp Alpha:alpha StartX:x StartY:y EndX:endX EndY:endY];
  360. ledColorData[led * 3] = (color & 0xFF0000) >> 16;
  361. ledColorData[(led * 3) + 1] = (color & 0x00FF00) >> 8;
  362. ledColorData[(led * 3) + 2] = color & 0x0000FF;
  363. // Move to next LED
  364. if ((strands[i].direction == DIR_LEFT) || (strands[i].direction == DIR_RIGHT)) {
  365. x = endX;
  366. } else {
  367. y = endY;
  368. }
  369. }
  370. }
  371. }
  372. }
  373. - (void)visualizeDisplay:(NSTimer *)time {
  374. //NSLog(@"Running Ambilight-Algorithm (%lu)...", (unsigned long)[lastDisplayIDs count]);
  375. // Create a Screenshot for all connected displays
  376. for (NSInteger i = 0; i < [lastDisplayIDs count]; i++) {
  377. NSBitmapImageRep *screen = [Screenshot screenshot:[lastDisplayIDs objectAtIndex:i]];
  378. unsigned long width = [screen pixelsWide];
  379. unsigned long height = [screen pixelsHigh];
  380. // Ensure we can handle the format of this display
  381. NSInteger spp = [screen samplesPerPixel];
  382. if (((spp != 3) && (spp != 4)) || ([screen isPlanar] == YES) || ([screen numberOfPlanes] != 1)) {
  383. NSLog(@"Unknown image format for %ld (%ld, %c, %ld)!\n", (long)i, (long)spp, ([screen isPlanar] == YES) ? 'p' : 'n', (long)[screen numberOfPlanes]);
  384. continue;
  385. }
  386. // Find out how the color components are ordered
  387. BOOL alpha = NO;
  388. if ([screen bitmapFormat] & NSAlphaFirstBitmapFormat) {
  389. alpha = YES;
  390. }
  391. // Try to find the matching display id for the strand associations
  392. for (int n = 0; n < (sizeof(displays) / sizeof(displays[0])); n++) {
  393. if ((width == displays[n].width) && (height == displays[n].height)) {
  394. unsigned char *data = [screen bitmapData];
  395. [self visualizeSingleDisplay:n Data:data Width:width Height:height SPP:spp Alpha:alpha];
  396. break;
  397. }
  398. }
  399. }
  400. [self sendLEDFrame];
  401. timer = [NSTimer scheduledTimerWithTimeInterval:DISPLAY_DELAY target:self selector:@selector(visualizeDisplay:) userInfo:nil repeats:NO];
  402. }
  403. - (void)sendLEDFrame {
  404. if ([serial isOpen]) {
  405. [serial sendString:MAGIC_WORD];
  406. [serial sendData:(char *)ledColorData withLength:(sizeof(ledColorData) / sizeof(ledColorData[0]))];
  407. }
  408. }
  409. - (void)sendNullFrame {
  410. for (int i = 0; i < (sizeof(ledColorData) / sizeof(ledColorData[0])); i++) {
  411. ledColorData[i] = 0;
  412. }
  413. [self sendLEDFrame];
  414. }
  415. @end