├── .gitignore ├── Gemfile ├── Gemfile.lock ├── README.md ├── build ├── index.html ├── javascripts │ ├── example.js │ └── karaoke.js └── stylesheets │ └── karaoke.css ├── config.rb ├── license.txt └── source ├── index.html.slim ├── javascripts ├── example.js.coffee └── karaoke.js.coffee ├── layouts └── layout.html.slim └── stylesheets └── karaoke.css.sass /.gitignore: -------------------------------------------------------------------------------- 1 | # See http://help.github.com/ignore-files/ for more about ignoring files. 2 | # 3 | # If you find yourself ignoring temporary files generated by your text editor 4 | # or operating system, you probably want to add a global ignore instead: 5 | # git config --global core.excludesfile ~/.gitignore_global 6 | 7 | # Ignore bundler config 8 | /.bundle 9 | 10 | # Ignore cache 11 | /.sass-cache 12 | /.cache 13 | 14 | # Ignore .DS_store file 15 | .DS_Store 16 | -------------------------------------------------------------------------------- /Gemfile: -------------------------------------------------------------------------------- 1 | # If you do not have OpenSSL installed, update 2 | # the following line to use "http://" instead 3 | source 'https://rubygems.org' 4 | 5 | gem "middleman", "~>3.3.7" 6 | 7 | # Live-reloading plugin 8 | gem "middleman-livereload", "~> 3.1.0" 9 | 10 | # For faster file watcher updates on Windows: 11 | gem "wdm", "~> 0.1.0", :platforms => [:mswin, :mingw] 12 | 13 | # Windows does not come with time zone data 14 | gem "tzinfo-data", platforms: [:mswin, :mingw] 15 | 16 | gem 'middleman-slim' 17 | gem 'coffee' 18 | gem 'sass' -------------------------------------------------------------------------------- /Gemfile.lock: -------------------------------------------------------------------------------- 1 | GEM 2 | remote: https://rubygems.org/ 3 | specs: 4 | activesupport (4.1.9) 5 | i18n (~> 0.6, >= 0.6.9) 6 | json (~> 1.7, >= 1.7.7) 7 | minitest (~> 5.1) 8 | thread_safe (~> 0.1) 9 | tzinfo (~> 1.1) 10 | celluloid (0.16.0) 11 | timers (~> 4.0.0) 12 | chunky_png (1.3.3) 13 | coffee (0.0.5) 14 | httparty 15 | json_pure 16 | thor 17 | coffee-script (2.3.0) 18 | coffee-script-source 19 | execjs 20 | coffee-script-source (1.9.0) 21 | compass (1.0.3) 22 | chunky_png (~> 1.2) 23 | compass-core (~> 1.0.2) 24 | compass-import-once (~> 1.0.5) 25 | rb-fsevent (>= 0.9.3) 26 | rb-inotify (>= 0.9) 27 | sass (>= 3.3.13, < 3.5) 28 | compass-core (1.0.3) 29 | multi_json (~> 1.0) 30 | sass (>= 3.3.0, < 3.5) 31 | compass-import-once (1.0.5) 32 | sass (>= 3.2, < 3.5) 33 | em-websocket (0.5.1) 34 | eventmachine (>= 0.12.9) 35 | http_parser.rb (~> 0.6.0) 36 | erubis (2.7.0) 37 | eventmachine (1.0.7) 38 | execjs (2.3.0) 39 | ffi (1.9.6) 40 | haml (4.0.6) 41 | tilt 42 | hike (1.2.3) 43 | hitimes (1.2.2) 44 | hooks (0.4.0) 45 | uber (~> 0.0.4) 46 | http_parser.rb (0.6.0) 47 | httparty (0.13.3) 48 | json (~> 1.8) 49 | multi_xml (>= 0.5.2) 50 | i18n (0.6.11) 51 | json (1.8.2) 52 | json_pure (1.8.2) 53 | kramdown (1.5.0) 54 | listen (2.8.5) 55 | celluloid (>= 0.15.2) 56 | rb-fsevent (>= 0.9.3) 57 | rb-inotify (>= 0.9) 58 | middleman (3.3.7) 59 | coffee-script (~> 2.2) 60 | compass (>= 1.0.0, < 2.0.0) 61 | compass-import-once (= 1.0.5) 62 | execjs (~> 2.0) 63 | haml (>= 4.0.5) 64 | kramdown (~> 1.2) 65 | middleman-core (= 3.3.7) 66 | middleman-sprockets (>= 3.1.2) 67 | sass (>= 3.4.0, < 4.0) 68 | uglifier (~> 2.5) 69 | middleman-core (3.3.7) 70 | activesupport (~> 4.1.0) 71 | bundler (~> 1.1) 72 | erubis 73 | hooks (~> 0.3) 74 | i18n (~> 0.6.9) 75 | listen (>= 2.7.9, < 3.0) 76 | padrino-helpers (~> 0.12.3) 77 | rack (>= 1.4.5, < 2.0) 78 | rack-test (~> 0.6.2) 79 | thor (>= 0.15.2, < 2.0) 80 | tilt (~> 1.4.1, < 2.0) 81 | middleman-livereload (3.1.1) 82 | em-websocket (>= 0.2.0) 83 | middleman-core (>= 3.0.2) 84 | multi_json (~> 1.0) 85 | rack-livereload 86 | middleman-slim (0.2.1) 87 | middleman (>= 3.2) 88 | slim (>= 2.0) 89 | middleman-sprockets (3.4.1) 90 | middleman-core (>= 3.3) 91 | sprockets (~> 2.12.1) 92 | sprockets-helpers (~> 1.1.0) 93 | sprockets-sass (~> 1.3.0) 94 | minitest (5.5.1) 95 | multi_json (1.10.1) 96 | multi_xml (0.5.5) 97 | padrino-helpers (0.12.4) 98 | i18n (~> 0.6, >= 0.6.7) 99 | padrino-support (= 0.12.4) 100 | tilt (~> 1.4.1) 101 | padrino-support (0.12.4) 102 | activesupport (>= 3.1) 103 | rack (1.6.0) 104 | rack-livereload (0.3.15) 105 | rack 106 | rack-test (0.6.3) 107 | rack (>= 1.0) 108 | rb-fsevent (0.9.4) 109 | rb-inotify (0.9.5) 110 | ffi (>= 0.5.0) 111 | sass (3.4.11) 112 | slim (3.0.2) 113 | temple (~> 0.7.3) 114 | tilt (>= 1.3.3, < 2.1) 115 | sprockets (2.12.3) 116 | hike (~> 1.2) 117 | multi_json (~> 1.0) 118 | rack (~> 1.0) 119 | tilt (~> 1.1, != 1.3.0) 120 | sprockets-helpers (1.1.0) 121 | sprockets (~> 2.0) 122 | sprockets-sass (1.3.1) 123 | sprockets (~> 2.0) 124 | tilt (~> 1.1) 125 | temple (0.7.5) 126 | thor (0.19.1) 127 | thread_safe (0.3.4) 128 | tilt (1.4.1) 129 | timers (4.0.1) 130 | hitimes 131 | tzinfo (1.2.2) 132 | thread_safe (~> 0.1) 133 | uber (0.0.13) 134 | uglifier (2.7.0) 135 | execjs (>= 0.3.0) 136 | json (>= 1.8.0) 137 | 138 | PLATFORMS 139 | ruby 140 | 141 | DEPENDENCIES 142 | coffee 143 | middleman (~> 3.3.7) 144 | middleman-livereload (~> 3.1.0) 145 | middleman-slim 146 | sass 147 | tzinfo-data 148 | wdm (~> 0.1.0) 149 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Karaoke JS 2 | A simple JavaScript-powered karaoke script. The script takes in a multi-dimensional array of lyrics 3 | with specific timings for each word to be highlighted binded to a designated audio player element. 4 | 5 | # Dependencies 6 | The script uses the Middleman framework. 7 | The app uses the following: 8 |
gem install middleman
onto one's system and running middleman server
or
20 | middleman build
. More information can be found on the framework's website.
21 | "; 72 | $.each(line, function(word_index, word) { 73 | if (word_index > 0) { 74 | build_line += " "; 75 | } 76 | return build_line += "" + word[1] + ""; 77 | }); 78 | build_line += "
"; 79 | return _this.lyrics_elem.append(build_line); 80 | }; 81 | })(this)); 82 | return this.moveToLine(0); 83 | }; 84 | 85 | Karaoke.prototype.initWordTimers = function() { 86 | var current_time, moved_to_line; 87 | current_time = this.player_elem.currentTime; 88 | moved_to_line = false; 89 | return $.each(this.lyrics, (function(_this) { 90 | return function(line_index, line) { 91 | return $.each(line, function(word_index, word_piece) { 92 | var get_next_time, time_offset, time_until_next, timing, word; 93 | timing = word_piece[0]; 94 | word = word_piece[1]; 95 | if (timing > _this.player_elem.currentTime) { 96 | if (!moved_to_line) { 97 | _this.moveToLine(line_index); 98 | moved_to_line = true; 99 | } 100 | time_offset = (timing - _this.player_elem.currentTime) * 1000; 101 | if (_this.lyrics[line_index].length > word_index + 1) { 102 | get_next_time = _this.lyrics[line_index][word_index + 1][0]; 103 | } else if (_this.lyrics.length > line_index + 1) { 104 | get_next_time = _this.lyrics[line_index + 1][0][0]; 105 | } else { 106 | get_next_time = timing + _this.settings['last-word-highlight-time']; 107 | } 108 | time_until_next = get_next_time - timing; 109 | return _this.word_timers.push(setTimeout(function() { 110 | return _this.playWord(line_index, word_index, time_until_next); 111 | }, time_offset)); 112 | } 113 | }); 114 | }; 115 | })(this)); 116 | }; 117 | 118 | Karaoke.prototype.cancelWordTimers = function() { 119 | $.each(this.word_timers, (function(_this) { 120 | return function(timer) { 121 | return clearTimeout(_this.word_timers[timer]); 122 | }; 123 | })(this)); 124 | return this.word_timers = []; 125 | }; 126 | 127 | Karaoke.prototype.playWord = function(line_index, word_index, time_offset) { 128 | var line_elem, word_elem; 129 | line_elem = this.lyrics_elem.find('.line')[line_index]; 130 | word_elem = $(line_elem).find('.word')[word_index]; 131 | $(word_elem).addClass('active'); 132 | setTimeout(function() { 133 | return $(word_elem).removeClass('active'); 134 | }, time_offset * 1000); 135 | return this.moveToLine(line_index); 136 | }; 137 | 138 | Karaoke.prototype.moveToLine = function(line_index) { 139 | var height; 140 | height = $(this.lyrics_elem.find('.line')[line_index]).outerHeight(); 141 | return this.lyrics_elem.animate({ 142 | scrollTop: height * line_index 143 | }, this.settings['scroll-animation-time'] * 1000); 144 | }; 145 | 146 | return Karaoke; 147 | 148 | })(); 149 | 150 | }).call(this); 151 | -------------------------------------------------------------------------------- /build/stylesheets/karaoke.css: -------------------------------------------------------------------------------- 1 | /* line 1, /Users/Jake/dev/karaoke-js/source/stylesheets/karaoke.css.sass */ 2 | body { 3 | background-color: #ecf0f1; 4 | font-family: "Open Sans"; 5 | font-size: 16px; } 6 | 7 | /* line 6, /Users/Jake/dev/karaoke-js/source/stylesheets/karaoke.css.sass */ 8 | h1 { 9 | color: #8e44ad; 10 | font-family: "Ubuntu"; } 11 | 12 | /* line 10, /Users/Jake/dev/karaoke-js/source/stylesheets/karaoke.css.sass */ 13 | #karaoke-panel { 14 | width: 80%; 15 | margin: 0 auto; 16 | text-align: center; } 17 | /* line 15, /Users/Jake/dev/karaoke-js/source/stylesheets/karaoke.css.sass */ 18 | #karaoke-panel #karaoke-lyrics { 19 | margin: 50px auto; 20 | height: 168px; 21 | overflow: hidden; } 22 | /* line 20, /Users/Jake/dev/karaoke-js/source/stylesheets/karaoke.css.sass */ 23 | #karaoke-panel #karaoke-lyrics p.line { 24 | line-height: 16px; 25 | padding-top: 20px; 26 | padding-bottom: 20px; 27 | margin: 0; 28 | color: #95a5a6; } 29 | /* line 27, /Users/Jake/dev/karaoke-js/source/stylesheets/karaoke.css.sass */ 30 | #karaoke-panel #karaoke-lyrics p.line:first-child { 31 | margin-top: 56px; } 32 | /* line 30, /Users/Jake/dev/karaoke-js/source/stylesheets/karaoke.css.sass */ 33 | #karaoke-panel #karaoke-lyrics p.line span.word { 34 | transition: all linear 0.5s; } 35 | /* line 33, /Users/Jake/dev/karaoke-js/source/stylesheets/karaoke.css.sass */ 36 | #karaoke-panel #karaoke-lyrics p.line span.word.active { 37 | color: #8e44ad; 38 | text-decoration: underline; } 39 | -------------------------------------------------------------------------------- /config.rb: -------------------------------------------------------------------------------- 1 | ### 2 | # Compass 3 | ### 4 | 5 | # Change Compass configuration 6 | # compass_config do |config| 7 | # config.output_style = :compact 8 | # end 9 | 10 | ### 11 | # Page options, layouts, aliases and proxies 12 | ### 13 | 14 | # Per-page layout changes: 15 | # 16 | # With no layout 17 | # page "/path/to/file.html", :layout => false 18 | # 19 | # With alternative layout 20 | # page "/path/to/file.html", :layout => :otherlayout 21 | # 22 | # A path which all have the same layout 23 | # with_layout :admin do 24 | # page "/admin/*" 25 | # end 26 | 27 | # Proxy pages (http://middlemanapp.com/basics/dynamic-pages/) 28 | # proxy "/this-page-has-no-template.html", "/template-file.html", :locals => { 29 | # :which_fake_page => "Rendering a fake page with a local variable" } 30 | 31 | ### 32 | # Helpers 33 | ### 34 | 35 | # Automatic image dimensions on image_tag helper 36 | # activate :automatic_image_sizes 37 | 38 | # Reload the browser automatically whenever files change 39 | # configure :development do 40 | # activate :livereload 41 | # end 42 | 43 | # Methods defined in the helpers block are available in templates 44 | # helpers do 45 | # def some_helper 46 | # "Helping" 47 | # end 48 | # end 49 | 50 | set :css_dir, 'stylesheets' 51 | 52 | set :js_dir, 'javascripts' 53 | 54 | set :images_dir, 'images' 55 | 56 | # Build-specific configuration 57 | configure :build do 58 | # For example, change the Compass output style for deployment 59 | # activate :minify_css 60 | 61 | # Minify Javascript on build 62 | # activate :minify_javascript 63 | 64 | # Enable cache buster 65 | # activate :asset_hash 66 | 67 | # Use relative URLs 68 | # activate :relative_assets 69 | 70 | # Or use a different image path 71 | # set :http_prefix, "/Content/images/" 72 | end 73 | -------------------------------------------------------------------------------- /license.txt: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2015 Jake Larson 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in 13 | all copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 21 | THE SOFTWARE. -------------------------------------------------------------------------------- /source/index.html.slim: -------------------------------------------------------------------------------- 1 | h1 Karaoke Demo 2 | 3 | #karaoke-panel 4 | 5 | #karaoke-lyrics 6 | 7 | audio#karaoke-player controls=true 8 | source src='/audio/example.mp3' type='audio/mp3' -------------------------------------------------------------------------------- /source/javascripts/example.js.coffee: -------------------------------------------------------------------------------- 1 | $ -> 2 | # The lyrics list must be organized like so: 3 | # lyrics = [ 4 | # [ # first line 5 | # [2.5, "first",], #time, first word 6 | # [2.8, "second"] #time, second word 7 | # ] 8 | # ] 9 | # 10 | 11 | lyrics = [ 12 | [ 13 | [1.0, "Here"] 14 | [1.3, "is"] 15 | [1.45, "the"] 16 | [1.55, "first"] 17 | [1.7, "line"] 18 | ] 19 | [ 20 | [1.92, "And"] 21 | [2.08, "this"] 22 | [2.25, "is"] 23 | [2.4, "the"] 24 | [2.7, "second!"] 25 | ] 26 | [ 27 | [3.1, "This"] 28 | [3.47, "song"] 29 | [3.8, "is"] 30 | [3.95, "not"] 31 | [4.15, "real"] 32 | ] 33 | [ 34 | [4.4, "At"] 35 | [4.5, "I"] 36 | [4.6, "hope"] 37 | [4.7, "it's"] 38 | [4.9, "not!"] 39 | ] 40 | ] 41 | 42 | settings = { 43 | 'last-word-highlight-time': 5.5 # how long the last word will be higlighted (seconds) 44 | 'scroll-animation-time': .1 # speed of the scrolling (seconds) 45 | 'karaoke-player-elem': $('audio#karaoke-player') 46 | 'karaoke-lyrics-elem': $('#karaoke-lyrics') 47 | } 48 | 49 | window.karaoke = new window.Karaoke(lyrics, settings) -------------------------------------------------------------------------------- /source/javascripts/karaoke.js.coffee: -------------------------------------------------------------------------------- 1 | class window.Karaoke 2 | constructor: (lyrics, settings = undefined) -> 3 | @settings = settings 4 | default_settings = { 5 | 'last-word-highlight-time': 1.0 6 | 'scroll-animation-time': .1 7 | 'karaoke-player-elem': $('audio#karaoke-player') 8 | 'karaoke-lyrics-elem': $('#karaoke-lyrics') 9 | } 10 | 11 | unless typeof settings == 'undefined' 12 | $.each default_settings, (setting, value) => 13 | @settings[setting] = if (typeof @settings[setting] != 'undefined') then @settings[setting] else default_settings[setting] 14 | else 15 | @settings = default_settings 16 | 17 | @lyrics = lyrics 18 | @initVars() 19 | @initPlayerBinds() 20 | @initLyrics() 21 | 22 | initVars: => 23 | @player_elem = @settings['karaoke-player-elem'][0] 24 | @lyrics_elem = @settings['karaoke-lyrics-elem'] 25 | @word_timers = [] 26 | 27 | initPlayerBinds: => 28 | @player_elem.addEventListener 'play', () => 29 | if @word_timers.length > 0 30 | @cancelWordTimers() 31 | 32 | @initWordTimers() 33 | 34 | @player_elem.addEventListener 'pause', () => 35 | @cancelWordTimers() 36 | 37 | @player_elem.addEventListener 'seeked', () => 38 | @cancelWordTimers() 39 | @initWordTimers() 40 | 41 | initLyrics: => 42 | $.each @lyrics, (line_index, line) => 43 | build_line = "" 44 | 45 | $.each line, (word_index, word) => 46 | if word_index > 0 47 | build_line += " " 48 | build_line += "#{word[1]}" 49 | 50 | build_line += "
" 51 | @lyrics_elem.append build_line 52 | 53 | @moveToLine(0) 54 | 55 | initWordTimers: => 56 | current_time = @player_elem.currentTime 57 | moved_to_line = false 58 | 59 | $.each @lyrics, (line_index, line) => 60 | $.each line, (word_index, word_piece) => 61 | timing = word_piece[0] 62 | word = word_piece[1] 63 | 64 | if timing > @player_elem.currentTime 65 | if !moved_to_line 66 | @moveToLine(line_index) 67 | moved_to_line = true 68 | 69 | time_offset = (timing - @player_elem.currentTime) * 1000 70 | 71 | if @lyrics[line_index].length > word_index + 1 # next word in current line 72 | get_next_time = @lyrics[line_index][word_index+1][0] 73 | 74 | else if @lyrics.length > line_index + 1 # next word in new line 75 | get_next_time = @lyrics[line_index+1][0][0] 76 | 77 | else # last word in lyrics 78 | get_next_time = timing + @settings['last-word-highlight-time'] 79 | 80 | time_until_next = get_next_time - timing 81 | 82 | @word_timers.push(setTimeout(() => 83 | @playWord line_index, word_index, time_until_next 84 | , time_offset)) 85 | 86 | cancelWordTimers: => 87 | $.each @word_timers, (timer) => 88 | clearTimeout(@word_timers[timer]) 89 | 90 | @word_timers = [] 91 | 92 | playWord: (line_index, word_index, time_offset) => 93 | line_elem = @lyrics_elem.find('.line')[line_index] 94 | word_elem = $(line_elem).find('.word')[word_index] 95 | $(word_elem).addClass('active') 96 | 97 | setTimeout () -> 98 | $(word_elem).removeClass('active') 99 | , time_offset*1000 100 | 101 | @moveToLine(line_index) 102 | 103 | moveToLine: (line_index) => 104 | height = $(@lyrics_elem.find('.line')[line_index]).outerHeight() # all lines should be equal height for now 105 | @lyrics_elem.animate({scrollTop: (height * (line_index))}, @settings['scroll-animation-time']*1000) -------------------------------------------------------------------------------- /source/layouts/layout.html.slim: -------------------------------------------------------------------------------- 1 | doctype html 2 | html 3 | head 4 | meta charset="utf-8" 5 | meta content="IE=edge,chrome=1" http-equiv="X-UA-Compatible" 6 | title== current_page.data.title || "Karaoke" 7 | 8 | == stylesheet_link_tag "karaoke" 9 | == javascript_include_tag "https://code.jquery.com/jquery-2.1.3.min.js", "karaoke", "example" 10 | link href='http://fonts.googleapis.com/css?family=Open+Sans|Ubuntu' rel='stylesheet' type='text/css' 11 | 12 | body class="#{page_classes}" 13 | == yield 14 | -------------------------------------------------------------------------------- /source/stylesheets/karaoke.css.sass: -------------------------------------------------------------------------------- 1 | body 2 | background-color: #ecf0f1 3 | font-family: 'Open Sans' 4 | font-size: 16px 5 | 6 | h1 7 | color: #8e44ad 8 | font-family: 'Ubuntu' 9 | 10 | #karaoke-panel 11 | width: 80% 12 | margin: 0 auto 13 | text-align: center 14 | 15 | #karaoke-lyrics 16 | margin: 50px auto 17 | height: 168px 18 | overflow: hidden 19 | 20 | p.line 21 | line-height: 16px 22 | padding-top: 20px 23 | padding-bottom: 20px 24 | margin: 0 25 | color: #95a5a6 26 | 27 | &:first-child 28 | margin-top: 56px 29 | 30 | span.word 31 | transition: all linear .5s 32 | 33 | &.active 34 | color: #8e44ad 35 | text-decoration: underline 36 | --------------------------------------------------------------------------------