├── .gitignore ├── screentunes.css ├── song.json ├── Readme.md ├── index.html └── screentunes.js /.gitignore: -------------------------------------------------------------------------------- 1 | .c9/ 2 | -------------------------------------------------------------------------------- /screentunes.css: -------------------------------------------------------------------------------- 1 | * { margin:0; padding:0; } /* to remove the top and left whitespace */ 2 | html, body { width:100%; height:100%; } /* just to be sure these are full screen*/ 3 | canvas { display:block; } /* To remove the scrollbars */ 4 | 5 | /* canvas { */ 6 | /* position: absolute; top: 0px; left: 0px; z-index: -1; */ 7 | /* } */ 8 | 9 | input[type=range] { 10 | width: 300px; 11 | } 12 | 13 | .bubble { 14 | position: relative; 15 | top: 10px; left: 10px; 16 | z-index: 100; 17 | background: #D9E9F5; 18 | width: 350px; 19 | padding: 9px; 20 | border-radius: 5px; 21 | font-family: sans-serif; 22 | } 23 | 24 | .bubble + .bubble + .bubble { 25 | top: 20px; 26 | } 27 | 28 | p { 29 | margin-top: 5pt; 30 | } 31 | -------------------------------------------------------------------------------- /song.json: -------------------------------------------------------------------------------- 1 | { 2 | "duration": 16000, 3 | 4 | "0": 7, 5 | "500": 9, 6 | "1000": 3, 7 | "1250": 4, 8 | "1750": 0, 9 | "2000": 3, 10 | "2250": 2, 11 | "2500": 0, 12 | "3500": 2, 13 | 14 | "4000": 3, 15 | "4750": 2, 16 | "5000": 0, 17 | "5250": 2, 18 | "5500": 4, 19 | "5750": 7, 20 | "6000": 9, 21 | "6250": 4, 22 | "6500": 7, 23 | "6750": 2, 24 | "7000": 4, 25 | "7250": 0, 26 | "7500": 2, 27 | "7750": 0, 28 | 29 | "8000": 4, 30 | "8500": 7, 31 | "9000": 9, 32 | "9250": 4, 33 | "9500": 7, 34 | "9750": 2, 35 | "10000": 4, 36 | "10250": 0, 37 | "10500": 3, 38 | "10750": 4, 39 | "11000": 3, 40 | "11250": 2, 41 | "11500": 0, 42 | "11750": 2, 43 | 44 | "12000": 3, 45 | "12500": 0, 46 | "12750": 2, 47 | "13000": 4, 48 | "13250": 7, 49 | "13500": 3, 50 | "13750": 4, 51 | "14000": 2, 52 | "14250": 0, 53 | "14500": 2, 54 | "15000": 0, 55 | "15500": 2 56 | } -------------------------------------------------------------------------------- /Readme.md: -------------------------------------------------------------------------------- 1 | # Screen Tunes 2 | 3 | [![Code Climate](https://codeclimate.com/github/GenaBitu/screentunes/badges/gpa.svg)](https://codeclimate.com/github/GenaBitu/screentunes) 4 | 5 | It turns out that some LCD monitors will emit a tone when patterns of repeating bars are displayed on the screen. 6 | This pattern varies based on the size of the bars. The goal of this project is to eventually be able to play some 7 | music through the screen using a web page. 8 | 9 | I found out about this effect from the Hacker News comments on a page that was about a completely different bar spacing effect 10 | but produced sound out of some monitors (including my external Samsung LCD). 11 | [Some commenters](https://news.ycombinator.com/item?id=8856930) produced theories including that this is caused by 12 | the expansion and contraction of capacitors/inductors in the display as the screen scans down and the black pixels take a different 13 | amount of power than the white pixels. This theory suggests that varying the width of the bars would change the pitch, 14 | which indeed does happen. 15 | 16 | This is mainly intended as an experiment out of curiosity and geekery; it has no practical use. 17 | 18 | ## Technique 19 | 20 | I simply created a full screen canvas where I draw an animated and variably spaced sequence of black and white bars. 21 | Displaying this pattern in a large browser window on some LCD monitors produces a tone. 22 | 23 | ## Music 24 | 25 | The script can play some basic music, which it loads from the file song.json. In this file, there must be: 26 | 27 | * duration - the overall duration of the song, after which the song will be repeated 28 | * individual notes with starting duration as the attribute and the note as the value 29 | 30 | Notes are not named(due to inconsistent naming) but instead are represented as the number of semitones (half-steps) from the base note. 31 | The script is calibrating the device in such a way, that it tries to fit the first octave (values 0 to 12) to the device tonal range. 32 | 33 | ## Calibration 34 | 35 | Before the actual script is started, the user is prompted to sed the lowest and highest tone, which he can hear. The script then 36 | shifts the whole scale to best fit these settings. 37 | -------------------------------------------------------------------------------- /index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | Screen Tunes 5 | 6 | 7 | 8 | 9 | Screen Tunes 10 | 11 | 12 |
13 |

Tuning

14 |
15 |
21 |
22 |

Tuning

23 |
24 |
30 |
31 |

Screen Tunes

32 | By Tristan Hume and GenaBitu 33 |

34 | On some LCD monitors this page will cause the screen to emit a 35 | tone that varies in pitch with the bar height. Maximize window for best results. 36 |

37 |

38 |

39 | 40 | 41 |
42 |

43 |

44 | For more see the Github Page 45 |

46 |

47 | Edit: I kinda managed to get this to play music but it doesn't work very well. If you're ambitious you can download and try out the "music" branch on Github. 48 |

49 |
50 | 51 | 61 | 62 | 63 | -------------------------------------------------------------------------------- /screentunes.js: -------------------------------------------------------------------------------- 1 | (function() { 2 | var canvas; 3 | var ctx; 4 | var start = null; 5 | var t = null; 6 | var minimum = 0; 7 | var maximum = 0; 8 | var stage = 0; 9 | var baseFreq = 0; // frequency, NOT bar height 10 | 11 | var AnimationFrame = (function() { 12 | var FPS = 16.6666666667; // 1000 / 60 = Frames Per Second 13 | var RAF = window.requestAnimationFrame || window.webkitRequestAnimationFrame || window.mozRequestAnimationFrame || window.msRequestAnimationFrame || window.oRequestAnimationFrame || function(a) { window.setTimeout(a, FPS); }; 14 | var CAF = window.cancelAnimationFrame || window.webkitCancelAnimationFrame || window.mozCancelAnimationFrame || window.msCancelAnimationFrame || window.oCancelAnimationFrame || function(a) { window.clearTimeout(a); }; 15 | return { 16 | request: function(a) { 17 | RAF(a); 18 | }, 19 | cancel: function(a) { 20 | CAF(a); 21 | } 22 | }; 23 | })(); 24 | 25 | function resize() { 26 | canvas.width = window.innerWidth; 27 | canvas.height = window.innerHeight; 28 | /* $("#canvas").css("width", w + "px"); 29 | $("#canvas").css("height", h + "px"); */ 30 | /* render(); */ 31 | } 32 | 33 | function bar(y, height) { 34 | ctx.fillRect(0,y,canvas.width,height); 35 | } 36 | 37 | function bars(spacing, height) { 38 | ctx.fillStyle = "rgb(0,0,0)"; 39 | var y = 0; 40 | var maxy = canvas.height; 41 | while(y < maxy) { 42 | bar(y,height); 43 | y += spacing + height; 44 | } 45 | } 46 | 47 | function note(height) { 48 | if(height > 0) { 49 | bars(height, height); 50 | } 51 | } 52 | 53 | function render() { 54 | ctx.clearRect(0, 0, canvas.width, canvas.height); 55 | var magic = (maximum - minimum) * (Math.sin(t / 2000))/2 + (maximum + minimum)/2; 56 | bars(magic, magic); 57 | } 58 | 59 | function toneGen (pitch) { 60 | return baseFreq * Math.pow(2, -pitch/12); 61 | } 62 | 63 | function play() { 64 | ctx.clearRect(0, 0, canvas.width, canvas.height); 65 | var pitch; 66 | $.getJSON("song.json", function(data){ 67 | var rel = t % data.duration; 68 | $.each(data, function(i, field) { 69 | if(rel > i) { 70 | pitch = field; 71 | } 72 | }); 73 | }); 74 | if (pitch !== "s") 75 | { 76 | note(toneGen(pitch)); 77 | } 78 | } 79 | 80 | function setExtrema(id, extrema) { 81 | ctx.clearRect(0, 0, canvas.width, canvas.height); 82 | extrema = -49 * $(id).val() / 99 + 51; 83 | note(extrema); 84 | } 85 | 86 | function frame(timestamp) { 87 | if (start === null && stage === 2) { 88 | start = timestamp; 89 | } 90 | t = timestamp - start; 91 | if(stage === 0) { 92 | setExtrema("#min", minimum); 93 | } else if (stage === 1) { 94 | setExtrema("#max", maximum); 95 | } else { 96 | if($("#music").prop("checked")) { 97 | play(); 98 | } else { 99 | render(); 100 | } 101 | } 102 | AnimationFrame.request(frame); 103 | } 104 | 105 | $(document).ready(function() { 106 | $.ajaxSetup({ async: false }); 107 | canvas = document.getElementById("main"); 108 | ctx = canvas.getContext("2d"); 109 | $(window).bind("resize", resize); 110 | $("#tune2").hide(); 111 | $("#is_set1").click(function() { 112 | stage++; 113 | $("#tune1").hide(); 114 | $("#tune2").show(); 115 | }); 116 | $("#is_set2").click(function() { 117 | stage++; 118 | $("#tune2").hide(); 119 | baseFreq = (minimum + maximum) / 2.82842712475; // 2*sqrt(2) 120 | }); 121 | resize(); 122 | AnimationFrame.request(frame); 123 | }); 124 | })(); 125 | --------------------------------------------------------------------------------