├── test ├── fixtures │ ├── sample.txt │ ├── sample.wav │ ├── sample_2.wav │ └── mono_sample.wav └── carrierwave │ └── audio_waveform │ ├── output │ ├── peak.png │ ├── rms.png │ ├── logged.png │ ├── width-900.png │ ├── height-100.png │ ├── overwritten.png │ ├── width-auto.png │ ├── color-#000000.png │ ├── sample-spaced.png │ ├── background_color-#ff0000.png │ ├── sample-spaced-with-gap.png │ ├── waveform_from_audio_source.png │ ├── background_color-transparent.png │ ├── waveform_from_mono_audio_source_via_rms.png │ ├── waveform_from_mono_audio_source_via_peak.png │ ├── background_color-#00ff00+color-transparent.png │ └── background_color-#ff0000+color-transparent.png │ └── waveformer_test.rb ├── Rakefile ├── lib ├── carrierwave-audio-waveform.rb └── carrierwave │ ├── audio_waveform │ ├── version.rb │ ├── waveform_data.rb │ └── waveformer.rb │ └── audio_waveform.rb ├── .gitignore ├── sample.png ├── sample_2.png ├── mono_sample.png ├── Gemfile ├── LICENSE.txt ├── Gemfile.lock ├── carrierwave-audio-waveform.gemspec └── README.md /test/fixtures/sample.txt: -------------------------------------------------------------------------------- 1 | foo bar baz -------------------------------------------------------------------------------- /Rakefile: -------------------------------------------------------------------------------- 1 | require "bundler/gem_tasks" 2 | -------------------------------------------------------------------------------- /lib/carrierwave-audio-waveform.rb: -------------------------------------------------------------------------------- 1 | require 'carrierwave/audio_waveform' -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | pkg/ 2 | carrierwave-audio-waveform-1.0.3.gem 3 | lib/.DS_Store 4 | -------------------------------------------------------------------------------- /sample.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/TrevorHinesley/carrierwave-audio-waveform/master/sample.png -------------------------------------------------------------------------------- /sample_2.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/TrevorHinesley/carrierwave-audio-waveform/master/sample_2.png -------------------------------------------------------------------------------- /mono_sample.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/TrevorHinesley/carrierwave-audio-waveform/master/mono_sample.png -------------------------------------------------------------------------------- /Gemfile: -------------------------------------------------------------------------------- 1 | source 'https://rubygems.org' 2 | 3 | # Specify your gem's dependencies in carrierwave-ffmpeg.gemspec 4 | gemspec 5 | -------------------------------------------------------------------------------- /test/fixtures/sample.wav: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/TrevorHinesley/carrierwave-audio-waveform/master/test/fixtures/sample.wav -------------------------------------------------------------------------------- /test/fixtures/sample_2.wav: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/TrevorHinesley/carrierwave-audio-waveform/master/test/fixtures/sample_2.wav -------------------------------------------------------------------------------- /lib/carrierwave/audio_waveform/version.rb: -------------------------------------------------------------------------------- 1 | module CarrierWave 2 | module AudioWaveform 3 | VERSION = '1.0.6' 4 | end 5 | end 6 | -------------------------------------------------------------------------------- /test/fixtures/mono_sample.wav: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/TrevorHinesley/carrierwave-audio-waveform/master/test/fixtures/mono_sample.wav -------------------------------------------------------------------------------- /test/carrierwave/audio_waveform/output/peak.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/TrevorHinesley/carrierwave-audio-waveform/master/test/carrierwave/audio_waveform/output/peak.png -------------------------------------------------------------------------------- /test/carrierwave/audio_waveform/output/rms.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/TrevorHinesley/carrierwave-audio-waveform/master/test/carrierwave/audio_waveform/output/rms.png -------------------------------------------------------------------------------- /test/carrierwave/audio_waveform/output/logged.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/TrevorHinesley/carrierwave-audio-waveform/master/test/carrierwave/audio_waveform/output/logged.png -------------------------------------------------------------------------------- /test/carrierwave/audio_waveform/output/width-900.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/TrevorHinesley/carrierwave-audio-waveform/master/test/carrierwave/audio_waveform/output/width-900.png -------------------------------------------------------------------------------- /test/carrierwave/audio_waveform/output/height-100.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/TrevorHinesley/carrierwave-audio-waveform/master/test/carrierwave/audio_waveform/output/height-100.png -------------------------------------------------------------------------------- /test/carrierwave/audio_waveform/output/overwritten.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/TrevorHinesley/carrierwave-audio-waveform/master/test/carrierwave/audio_waveform/output/overwritten.png -------------------------------------------------------------------------------- /test/carrierwave/audio_waveform/output/width-auto.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/TrevorHinesley/carrierwave-audio-waveform/master/test/carrierwave/audio_waveform/output/width-auto.png -------------------------------------------------------------------------------- /test/carrierwave/audio_waveform/output/color-#000000.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/TrevorHinesley/carrierwave-audio-waveform/master/test/carrierwave/audio_waveform/output/color-#000000.png -------------------------------------------------------------------------------- /test/carrierwave/audio_waveform/output/sample-spaced.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/TrevorHinesley/carrierwave-audio-waveform/master/test/carrierwave/audio_waveform/output/sample-spaced.png -------------------------------------------------------------------------------- /test/carrierwave/audio_waveform/output/background_color-#ff0000.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/TrevorHinesley/carrierwave-audio-waveform/master/test/carrierwave/audio_waveform/output/background_color-#ff0000.png -------------------------------------------------------------------------------- /test/carrierwave/audio_waveform/output/sample-spaced-with-gap.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/TrevorHinesley/carrierwave-audio-waveform/master/test/carrierwave/audio_waveform/output/sample-spaced-with-gap.png -------------------------------------------------------------------------------- /test/carrierwave/audio_waveform/output/waveform_from_audio_source.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/TrevorHinesley/carrierwave-audio-waveform/master/test/carrierwave/audio_waveform/output/waveform_from_audio_source.png -------------------------------------------------------------------------------- /test/carrierwave/audio_waveform/output/background_color-transparent.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/TrevorHinesley/carrierwave-audio-waveform/master/test/carrierwave/audio_waveform/output/background_color-transparent.png -------------------------------------------------------------------------------- /test/carrierwave/audio_waveform/output/waveform_from_mono_audio_source_via_rms.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/TrevorHinesley/carrierwave-audio-waveform/master/test/carrierwave/audio_waveform/output/waveform_from_mono_audio_source_via_rms.png -------------------------------------------------------------------------------- /test/carrierwave/audio_waveform/output/waveform_from_mono_audio_source_via_peak.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/TrevorHinesley/carrierwave-audio-waveform/master/test/carrierwave/audio_waveform/output/waveform_from_mono_audio_source_via_peak.png -------------------------------------------------------------------------------- /test/carrierwave/audio_waveform/output/background_color-#00ff00+color-transparent.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/TrevorHinesley/carrierwave-audio-waveform/master/test/carrierwave/audio_waveform/output/background_color-#00ff00+color-transparent.png -------------------------------------------------------------------------------- /test/carrierwave/audio_waveform/output/background_color-#ff0000+color-transparent.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/TrevorHinesley/carrierwave-audio-waveform/master/test/carrierwave/audio_waveform/output/background_color-#ff0000+color-transparent.png -------------------------------------------------------------------------------- /LICENSE.txt: -------------------------------------------------------------------------------- 1 | Copyright (c) 2013 Jiri Kolarik 2 | 3 | MIT License 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining 6 | a copy of this software and associated documentation files (the 7 | "Software"), to deal in the Software without restriction, including 8 | without limitation the rights to use, copy, modify, merge, publish, 9 | distribute, sublicense, and/or sell copies of the Software, and to 10 | permit persons to whom the Software is furnished to do so, subject to 11 | the following conditions: 12 | 13 | The above copyright notice and this permission notice shall be 14 | included in all copies or substantial portions of the Software. 15 | 16 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, 17 | EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF 18 | MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND 19 | NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE 20 | LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION 21 | OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION 22 | WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 23 | -------------------------------------------------------------------------------- /Gemfile.lock: -------------------------------------------------------------------------------- 1 | PATH 2 | remote: . 3 | specs: 4 | carrierwave-audio-waveform (1.0.6) 5 | carrierwave 6 | oily_png 7 | ruby-audio 8 | ruby-sox 9 | 10 | GEM 11 | remote: https://rubygems.org/ 12 | specs: 13 | activemodel (5.1.5) 14 | activesupport (= 5.1.5) 15 | activesupport (5.1.5) 16 | concurrent-ruby (~> 1.0, >= 1.0.2) 17 | i18n (~> 0.7) 18 | minitest (~> 5.1) 19 | tzinfo (~> 1.1) 20 | byebug (9.0.6) 21 | carrierwave (1.2.2) 22 | activemodel (>= 4.0.0) 23 | activesupport (>= 4.0.0) 24 | mime-types (>= 1.16) 25 | chunky_png (1.3.10) 26 | concurrent-ruby (1.0.5) 27 | i18n (0.9.5) 28 | concurrent-ruby (~> 1.0) 29 | mime-types (3.1) 30 | mime-types-data (~> 3.2015) 31 | mime-types-data (3.2016.0521) 32 | minitest (5.11.3) 33 | oily_png (1.2.1) 34 | chunky_png (~> 1.3.7) 35 | rake (11.1.0) 36 | ruby-audio (1.6.1) 37 | ruby-sox (0.0.3) 38 | thread_safe (0.3.6) 39 | tzinfo (1.2.5) 40 | thread_safe (~> 0.1) 41 | 42 | PLATFORMS 43 | ruby 44 | 45 | DEPENDENCIES 46 | bundler 47 | byebug 48 | carrierwave-audio-waveform! 49 | rake 50 | 51 | BUNDLED WITH 52 | 1.14.1 53 | -------------------------------------------------------------------------------- /lib/carrierwave/audio_waveform.rb: -------------------------------------------------------------------------------- 1 | require 'carrierwave' 2 | require 'carrierwave/audio_waveform/waveformer' 3 | require 'carrierwave/audio_waveform/waveform_data' 4 | 5 | module CarrierWave 6 | module AudioWaveform 7 | module ClassMethods 8 | extend ActiveSupport::Concern 9 | 10 | def waveform options={} 11 | process waveform: [ options ] 12 | end 13 | 14 | def waveform_data options={} 15 | process waveform_data: [ options ] 16 | end 17 | end 18 | 19 | def waveform options={} 20 | cache_stored_file! if !cached? 21 | 22 | image_filename = Waveformer.generate(current_path, options) 23 | File.rename image_filename, current_path 24 | 25 | if options[:type] == :svg 26 | self.file.instance_variable_set(:@content_type, "image/svg+xml") 27 | else 28 | self.file.instance_variable_set(:@content_type, "image/png") 29 | end 30 | end 31 | 32 | def waveform_data options={} 33 | cache_stored_file! if !cached? 34 | 35 | data_filename = WaveformData.generate(current_path, options) 36 | File.rename data_filename, current_path 37 | self.file.instance_variable_set(:@content_type, "application/json") 38 | end 39 | end 40 | end 41 | -------------------------------------------------------------------------------- /carrierwave-audio-waveform.gemspec: -------------------------------------------------------------------------------- 1 | # coding: utf-8 2 | lib = File.expand_path('../lib', __FILE__) 3 | $LOAD_PATH.unshift(lib) unless $LOAD_PATH.include?(lib) 4 | require 'carrierwave/audio_waveform/version' 5 | 6 | Gem::Specification.new do |spec| 7 | spec.name = "carrierwave-audio-waveform" 8 | spec.version = CarrierWave::AudioWaveform::VERSION 9 | spec.authors = ["Trevor Hinesley"] 10 | spec.email = ["trevor@trevorhinesley.com"] 11 | spec.description = %q{CarrierWave Audio Waveform} 12 | spec.summary = %q{Generate waveform images from audio files within Carrierwave} 13 | spec.homepage = "https://github.com/TrevorHinesley/carrierwave-audio-waveform" 14 | spec.license = "MIT" 15 | 16 | spec.files = Dir["{lib}/**/*"] + ["LICENSE.txt", "README.md"] 17 | spec.executables = spec.files.grep(%r{^bin/}) { |f| File.basename(f) } 18 | spec.test_files = spec.files.grep(%r{^(test|spec|features)/}) 19 | spec.require_paths = ["lib"] 20 | 21 | spec.add_dependency "carrierwave" 22 | spec.add_dependency "ruby-audio" 23 | spec.add_dependency "ruby-sox" 24 | spec.add_dependency "oily_png" 25 | 26 | spec.add_development_dependency "bundler" 27 | spec.add_development_dependency "rake" 28 | spec.add_development_dependency "byebug" 29 | end -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # CarrierWave::AudioWaveform 2 | 3 | Generate waveform images from audio files within Carrierwave 4 | 5 | ## Installation 6 | 7 | Add this line to your application's Gemfile: 8 | 9 | gem 'carrierwave-audio-waveform' 10 | 11 | And then execute: 12 | 13 | $ bundle 14 | 15 | Or install it yourself as: 16 | 17 | $ gem install carrierwave-audio-waveform 18 | 19 | ## Usage 20 | 21 | First, install [SoX](http://sox.sourceforge.net/) on your local environment and production servers. 22 | 23 | Second, install [Audio Waveform](https://github.com/bbc/audiowaveform) on your local environment and production servers if you plan to generate waveform data rather than an image. 24 | 25 | Lastly, include CarrierWave::AudioWaveform in your CarrierWave uploader class: 26 | 27 | ```ruby 28 | class AudioUploader < CarrierWave::Uploader::Base 29 | include CarrierWave::AudioWaveform 30 | end 31 | ``` 32 | 33 | See the sections below for specific implementations. 34 | 35 | ### PNG 36 | 37 | To generate a PNG image: 38 | 39 | ```ruby 40 | class AudioUploader < CarrierWave::Uploader::Base 41 | include CarrierWave::AudioWaveform 42 | 43 | version :waveform_image do 44 | process :waveform => [{ 45 | background_color: :transparent, 46 | color: "#666", 47 | sample_width: 2, 48 | gap_width: 2, 49 | height: 75, 50 | width: 1500 51 | }] 52 | 53 | def full_filename(for_file) 54 | "#{super.chomp(File.extname(super))}.png" 55 | end 56 | end 57 | end 58 | ``` 59 | 60 | #### Options 61 | 62 | | Parameter | Description | Permitted Values | Default | 63 | | ------------------ | ------------------- | ------------------ | ------------------ | 64 | | `background_color` | The image's background color | String (hex value) or `:transparent` | `:transparent` | 65 | | `color` | The waveform's color; Only valid when type is `:png` | String (hex value) | `"#00ccff"` (![#00ccff](https://placehold.it/15/00ccff/000000?text=+) Cyan) | 66 | | `sample_width` | Integer specifying the sample width. If this is specified, there will be gaps (minimum of 1px wide, as specified by `gap_width`) between samples that are this wide in pixels. | Integer >= 1 | `nil` | 67 | | `gap_width` | Integer specifying the width of the gaps between samples. If `sample_width` is specified, this will be the size of the gaps between samples in pixels. | Integer >= 1 | `nil` | 68 | | `height` | The image's height | Integer | `280` | 69 | | `width` | The image's width | Integer | `1800` | 70 | | `auto_width` | Milliseconds per pixel. This will overwrite the width of the final waveform image depending on the length of the audio file. Example: `100` => 1 pixel per 100 msec; a one minute audio file will result in a width of 600 pixels | Integer | `nil` | 71 | | `method` | The method used to read sample frames, `:peak` or `:rms`. Peak is the norm. It uses the maximum amplitude per sample to generate the waveform, so the waveform looks more dynamic. RMS is more of an average, and the waveform isn't as jerky. | Symbol (`:peak` or `:rms`) | `:peak` | 72 | | `logger` | IOStream to log progress | IOStream | `nil` | 73 | 74 | ### Waveform Data 75 | 76 | >**Note:** Make sure to install [Audio Waveform](https://github.com/bbc/audiowaveform) on your local environment and production servers if you plan to generate waveform data. 77 | 78 | To generate an array of waveform data: 79 | 80 | ```ruby 81 | class AudioUploader < CarrierWave::Uploader::Base 82 | include CarrierWave::AudioWaveform 83 | 84 | version :waveform_peak_data do 85 | process :waveform_data => [{ 86 | convert_to_extension_before_processing: :wav, 87 | pixels_per_second: 10 88 | }] 89 | 90 | def full_filename(for_file) 91 | "#{super.chomp(File.extname(super))}.json" 92 | end 93 | end 94 | end 95 | ``` 96 | 97 | #### Options 98 | 99 | | Parameter | Description | Permitted Values | Default | 100 | | ------------------ | ------------------- | ------------------ | ------------------ | 101 | | `convert_to_extension_before_processing` | Useful if `.wav` or `.mp3` isn't being passed in as the source file--you can convert to the specified format first before reading the peaks. | Symbol (`:wav` or `:mp3`) | `nil` | 102 | | `set_extension_before_processing` | This is useful because CarrierWave will send files in with the wrong extension sometimes. For instance, if this is nested under a version, that version may be an `.mp3`, but its parent might be `.wav`, so even though the version is a different extension, the file type will be read from the original file's extension (not the version file) if you don't set this parameter. | Symbol (`:wav` or `:mp3`) | `nil` | 103 | | `pixels_per_second` | The number of pixels per second to evaluate. | Integer | `10` | 104 | | `bits` | 8- or 16-bit precision | Integer (`8` or `16`) | `16` | 105 | | `logger` | IOStream to log progress | IOStream | `nil` | 106 | 107 | ## Contributing 108 | 109 | 1. Fork it 110 | 2. Create your feature branch (`git checkout -b my-new-feature`) 111 | 3. Commit your changes (`git commit -am 'Add some feature'`) 112 | 4. Push to the branch (`git push origin my-new-feature`) 113 | 5. Create new Pull Request 114 | -------------------------------------------------------------------------------- /lib/carrierwave/audio_waveform/waveform_data.rb: -------------------------------------------------------------------------------- 1 | require 'ruby-sox' 2 | require 'fileutils' 3 | 4 | module CarrierWave 5 | module AudioWaveform 6 | class WaveformData 7 | DefaultOptions = { 8 | pixels_per_second: 10, 9 | bits: 16 10 | } 11 | 12 | # Scope these under Waveform so you can catch the ones generated by just this 13 | # class. 14 | class RuntimeError < ::RuntimeError;end; 15 | class ArgumentError < ::ArgumentError;end; 16 | 17 | class << self 18 | # Generate a Waveform image at the given filename with the given options. 19 | # 20 | # Available options (all optional) are: 21 | # 22 | # :convert_to_extension_before_processing => Symbolized extension (:wav, :mp3, etc.) 23 | # Useful if .wav or .mp3 isn't being passed in--you can convert to that format first. 24 | # 25 | # :set_extension_before_processing => Symbolized extension (:wav, :mp3, etc.) 26 | # This is useful because CarrierWave will send files in with the wrong extension sometimes. 27 | # For instance, if this is nested under a version, that version may be an .mp3, but its parent 28 | # might be .wav, so even though the version is a different extension, the file type will be read 29 | # from the original file's extension (not the version file) if you don't set this parameter. 30 | # 31 | # :pixels_per_second => The number of pixels per second to evaluate. 32 | # 33 | # :bits => 8- or 16-bit precision 34 | # 35 | # :logger => IOStream to log progress to. 36 | # 37 | # Example: 38 | # CarrierWave::AudioWaveform::Waveformer.generate("Kickstart My Heart.wav") 39 | # CarrierWave::AudioWaveform::Waveformer.generate("Kickstart My Heart.wav", :method => :rms) 40 | # CarrierWave::AudioWaveform::Waveformer.generate("Kickstart My Heart.wav", :color => "#ff00ff", :logger => $stdout) 41 | # 42 | def generate(source, options={}) 43 | options = DefaultOptions.merge(options) 44 | options[:filename] ||= self.generate_json_filename(source) 45 | old_source = source 46 | if options[:convert_to_extension_before_processing] 47 | source = generate_valid_source(source, options[:convert_to_extension_before_processing]) 48 | elsif options[:set_extension_before_processing] 49 | source = generate_proper_source(source, options[:set_extension_before_processing]) 50 | end 51 | 52 | raise ArgumentError.new("No source audio filename given, must be an existing sound file.") unless source 53 | raise ArgumentError.new("No destination filename given for waveform") unless options[:filename] 54 | raise RuntimeError.new("Source audio file '#{source}' not found.") unless File.exist?(source) 55 | 56 | @log = Log.new(options[:logger]) 57 | @log.start! 58 | 59 | @log.timed("\nGenerating...") do 60 | stdout_str, stderr_str, status = self.generate_waveform_data(source, options) 61 | if stderr_str.present? && !stderr_str.include?("Recoverable") 62 | raise RuntimeError.new(stderr_str) 63 | end 64 | end 65 | 66 | if source != old_source && options[:convert_to_extension_before_processing] 67 | @log.out("Removing temporary file at #{source}") 68 | FileUtils.rm(source) 69 | elsif source != old_source && options[:set_extension_before_processing] 70 | @log.out("Renaming file at #{source}") 71 | old_ext = File.extname(source).gsub(/\./, '').to_sym 72 | generate_proper_source(source, old_ext) 73 | end 74 | 75 | @log.done!("Generated waveform data '#{options[:filename]}'") 76 | 77 | options[:filename] 78 | end 79 | 80 | def generate_json_filename(source) 81 | ext = File.extname(source) 82 | source_file_path_without_extension = File.join File.dirname(source), File.basename(source, ext) 83 | "#{source_file_path_without_extension}.json" 84 | end 85 | 86 | def generate_waveform_data(source, options = DefaultOptions) 87 | options[:filename] ||= self.generate_json_filename(source) 88 | Open3.capture3( 89 | "audiowaveform -i #{source} --pixels-per-second #{options[:pixels_per_second]} -b #{options[:bits]} -o #{options[:filename]}" 90 | ) 91 | end 92 | 93 | private 94 | 95 | # Returns the proper file type if the one passed in was 96 | # wrong, or the original if it wasn't. 97 | def generate_proper_source(source, proper_ext) 98 | ext = File.extname(source) 99 | ext_gsubbed = ext.gsub(/\./, '') 100 | 101 | if ext_gsubbed != proper_ext.to_s 102 | filename_with_proper_extension = "#{source.chomp(File.extname(source))}.#{proper_ext}" 103 | File.rename source, filename_with_proper_extension 104 | filename_with_proper_extension 105 | else 106 | source 107 | end 108 | rescue Sox::Error => e 109 | raise e unless e.message.include?("FAIL formats:") 110 | raise RuntimeError.new("Source file #{source} could not be converted to .wav by Sox (Sox: #{e.message})") 111 | end 112 | 113 | # Returns a converted file. 114 | def generate_valid_source(source, proper_ext) 115 | ext = File.extname(source) 116 | ext_gsubbed = ext.gsub(/\./, '') 117 | 118 | if ext_gsubbed != proper_ext.to_s 119 | input_options = { type: ext_gsubbed } 120 | output_options = { type: proper_ext.to_s } 121 | source_filename_without_extension = File.basename(source, ext) 122 | output_file_path = File.join File.dirname(source), "tmp_#{source_filename_without_extension}_#{Time.now.to_i}.#{proper_ext}" 123 | converter = Sox::Cmd.new 124 | converter.add_input source, input_options 125 | converter.set_output output_file_path, output_options 126 | converter.run 127 | output_file_path 128 | else 129 | source 130 | end 131 | rescue Sox::Error => e 132 | raise e unless e.message.include?("FAIL formats:") 133 | raise RuntimeError.new("Source file #{source} could not be converted to .wav by Sox (Sox: #{e.message})") 134 | end 135 | end 136 | end 137 | 138 | class WaveformData 139 | # A simple class for logging + benchmarking, nice to have good feedback on a 140 | # long batch operation. 141 | # 142 | # There's probably 10,000,000 other bechmarking classes, but writing this was 143 | # easier than using Google. 144 | class Log 145 | attr_accessor :io 146 | 147 | def initialize(io=$stdout) 148 | @io = io 149 | end 150 | 151 | # Prints the given message to the log 152 | def out(msg) 153 | io.print(msg) if io 154 | end 155 | 156 | # Prints the given message to the log followed by the most recent benchmark 157 | # (note that it calls .end! which will stop the benchmark) 158 | def done!(msg="") 159 | out "#{msg} (#{self.end!}s)\n" 160 | end 161 | 162 | # Starts a new benchmark clock and returns the index of the new clock. 163 | # 164 | # If .start! is called again before .end! then the time returned will be 165 | # the elapsed time from the next call to start!, and calling .end! again 166 | # will return the time from *this* call to start! (that is, the clocks are 167 | # LIFO) 168 | def start! 169 | (@benchmarks ||= []) << Time.now 170 | @current = @benchmarks.size - 1 171 | end 172 | 173 | # Returns the elapsed time from the most recently started benchmark clock 174 | # and ends the benchmark, so that a subsequent call to .end! will return 175 | # the elapsed time from the previously started benchmark clock. 176 | def end! 177 | elapsed = (Time.now - @benchmarks[@current]) 178 | @current -= 1 179 | elapsed 180 | end 181 | 182 | # Returns the elapsed time from the benchmark clock w/ the given index (as 183 | # returned from when .start! was called). 184 | def time?(index) 185 | Time.now - @benchmarks[index] 186 | end 187 | 188 | # Benchmarks the given block, printing out the given message first (if 189 | # given). 190 | def timed(message=nil, &block) 191 | start! 192 | out(message) if message 193 | yield 194 | done! 195 | end 196 | end 197 | end 198 | end 199 | end 200 | -------------------------------------------------------------------------------- /test/carrierwave/audio_waveform/waveformer_test.rb: -------------------------------------------------------------------------------- 1 | require File.expand_path(File.join(File.dirname(__FILE__), "..", "..", "..", "lib", "carrierwave", "audio_waveform", "waveformer.rb")) 2 | 3 | require "test/unit" 4 | require "fileutils" 5 | require "byebug" 6 | 7 | module Helpers 8 | def get_fixture(file) 9 | File.join(File.dirname(__FILE__), "..", "..", "fixtures", file) 10 | end 11 | 12 | def get_output(file) 13 | File.join(File.dirname(__FILE__), "output", file) 14 | end 15 | 16 | def open_png(file) 17 | ChunkyPNG::Image.from_datastream(ChunkyPNG::Datastream.from_file(file)) 18 | end 19 | end 20 | 21 | module CarrierWave 22 | module AudioWaveform 23 | class WaveformerTest < ::Test::Unit::TestCase 24 | include Helpers 25 | extend Helpers 26 | 27 | def self.cleanup 28 | puts "Removing existing testing artifacts..." 29 | Dir[get_output("*.*")].each{ |f| FileUtils.rm(f) } 30 | FileUtils.mkdir_p(get_output("")) 31 | FileUtils.rm(get_fixture("sample_2.png")) if File.exists?(get_fixture("sample_2.png")) 32 | end 33 | 34 | def test_generates_waveform_with_default_filename_in_same_directory 35 | Waveformer.generate(get_fixture("sample_2.wav")) 36 | assert File.exists?(get_fixture("sample_2.png")) 37 | 38 | image = open_png(get_fixture("sample_2.png")) 39 | assert_equal ChunkyPNG::Color.from_hex(Waveformer::DefaultOptions[:color]), image[60, 120] 40 | assert_equal ChunkyPNG::Color.from_hex(Waveformer::DefaultOptions[:background_color]), image[0, 0] 41 | FileUtils.rm(get_fixture("sample_2.png")) 42 | end 43 | 44 | def test_generates_waveform_with_custom_filename 45 | Waveformer.generate(get_fixture("sample.wav"), filename: get_output("waveform_from_audio_source.png")) 46 | assert File.exists?(get_output("waveform_from_audio_source.png")) 47 | 48 | image = open_png(get_output("waveform_from_audio_source.png")) 49 | assert_equal ChunkyPNG::Color.from_hex(Waveformer::DefaultOptions[:color]), image[60, 120] 50 | assert_equal ChunkyPNG::Color.from_hex(Waveformer::DefaultOptions[:background_color]), image[0, 0] 51 | end 52 | 53 | def test_generates_waveform_from_mono_audio_source_via_peak 54 | Waveformer.generate(get_fixture("mono_sample.wav"), filename: get_output("waveform_from_mono_audio_source_via_peak.png")) 55 | assert File.exists?(get_output("waveform_from_mono_audio_source_via_peak.png")) 56 | 57 | image = open_png(get_output("waveform_from_mono_audio_source_via_peak.png")) 58 | assert_equal ChunkyPNG::Color.from_hex(Waveformer::DefaultOptions[:color]), image[60, 120] 59 | assert_equal ChunkyPNG::Color.from_hex(Waveformer::DefaultOptions[:background_color]), image[0, 0] 60 | end 61 | 62 | def test_generates_waveform_from_mono_audio_source_via_rms 63 | Waveformer.generate(get_fixture("mono_sample.wav"), filename: get_output("waveform_from_mono_audio_source_via_rms.png"), :method => :rms) 64 | assert File.exists?(get_output("waveform_from_mono_audio_source_via_rms.png")) 65 | 66 | image = open_png(get_output("waveform_from_mono_audio_source_via_rms.png")) 67 | assert_equal ChunkyPNG::Color.from_hex(Waveformer::DefaultOptions[:color]), image[60, 120] 68 | assert_equal ChunkyPNG::Color.from_hex(Waveformer::DefaultOptions[:background_color]), image[0, 0] 69 | end 70 | 71 | def test_logs_to_given_io 72 | File.open(get_output("waveform.log"), "w") do |io| 73 | Waveformer.generate(get_fixture("sample.wav"), filename: get_output("logged.png"), :logger => io) 74 | end 75 | 76 | assert_match /Generated waveform/, File.read(get_output("waveform.log")) 77 | end 78 | 79 | def test_uses_rms_instead_of_peak 80 | Waveformer.generate(get_fixture("sample.wav"), filename: get_output("peak.png")) 81 | Waveformer.generate(get_fixture("sample.wav"), filename: get_output("rms.png"), :method => :rms) 82 | 83 | rms = open_png(get_output("rms.png")) 84 | peak = open_png(get_output("peak.png")) 85 | 86 | assert_equal ChunkyPNG::Color.from_hex(Waveformer::DefaultOptions[:color]), peak[44, 43] 87 | assert_equal ChunkyPNG::Color.from_hex(Waveformer::DefaultOptions[:background_color]), rms[44, 43] 88 | assert_equal ChunkyPNG::Color.from_hex(Waveformer::DefaultOptions[:color]), rms[60, 120] 89 | end 90 | 91 | def test_is_900px_wide 92 | Waveformer.generate(get_fixture("sample.wav"), filename: get_output("width-900.png"), :width => 900) 93 | 94 | image = open_png(get_output("width-900.png")) 95 | 96 | assert_equal 900, image.width 97 | end 98 | 99 | def test_is_100px_tall 100 | Waveformer.generate(get_fixture("sample.wav"), filename: get_output("height-100.png"), :height => 100) 101 | 102 | image = open_png(get_output("height-100.png")) 103 | 104 | assert_equal 100, image.height 105 | end 106 | 107 | def test_has_auto_width 108 | Waveformer.generate(get_fixture("sample.wav"), filename: get_output("width-auto.png"), :auto_width => 10) 109 | 110 | image = open_png(get_output("width-auto.png")) 111 | 112 | assert_equal 209, image.width 113 | end 114 | 115 | def test_has_red_background_color 116 | Waveformer.generate(get_fixture("sample.wav"), filename: get_output("background_color-#ff0000.png"), :background_color => "#ff0000") 117 | 118 | image = open_png(get_output("background_color-#ff0000.png")) 119 | 120 | assert_equal ChunkyPNG::Color.from_hex("#ff0000"), image[0, 0] 121 | end 122 | 123 | def test_has_transparent_background_color 124 | Waveformer.generate(get_fixture("sample.wav"), filename: get_output("background_color-transparent.png"), :background_color => :transparent) 125 | 126 | image = open_png(get_output("background_color-transparent.png")) 127 | 128 | assert_equal ChunkyPNG::Color::TRANSPARENT, image[0, 0] 129 | end 130 | 131 | def test_has_black_foreground_color 132 | Waveformer.generate(get_fixture("sample.wav"), filename: get_output("color-#000000.png"), :color => "#000000") 133 | 134 | image = open_png(get_output("color-#000000.png")) 135 | 136 | assert_equal ChunkyPNG::Color.from_hex("#000000"), image[60, 120] 137 | end 138 | 139 | def test_has_red_background_color_with_transparent_foreground_cutout 140 | Waveformer.generate(get_fixture("sample.wav"), filename: get_output("background_color-#ff0000+color-transparent.png"), :background_color => "#ff0000", :color => :transparent) 141 | 142 | image = open_png(get_output("background_color-#ff0000+color-transparent.png")) 143 | 144 | assert_equal ChunkyPNG::Color.from_hex("#ff0000"), image[0, 0] 145 | assert_equal ChunkyPNG::Color::TRANSPARENT, image[60, 120] 146 | end 147 | 148 | # Bright green is our transparency mask color, so this test ensures that we 149 | # don't destroy the image if the background also uses the transparency mask 150 | # color 151 | def test_has_transparent_foreground_on_bright_green_background 152 | Waveformer.generate(get_fixture("sample.wav"), filename: get_output("background_color-#00ff00+color-transparent.png"), :background_color => "#00ff00", :color => :transparent) 153 | 154 | image = open_png(get_output("background_color-#00ff00+color-transparent.png")) 155 | 156 | assert_equal ChunkyPNG::Color.from_hex("#00ff00"), image[0, 0] 157 | assert_equal ChunkyPNG::Color::TRANSPARENT, image[60, 120] 158 | end 159 | 160 | # Test that passing a sample_width will space out the samples, leaving 161 | # gaps in between 162 | def test_has_spaced_samples_with_sample_width 163 | Waveformer.generate(get_fixture("sample.wav"), filename: get_output("sample-spaced.png"), sample_width: 5) 164 | 165 | image = open_png(get_output("sample-spaced.png")) 166 | 167 | (0..4).each do |i| 168 | assert_equal ChunkyPNG::Color.from_hex("#00ccff"), image[i, 140] 169 | end 170 | assert_equal ChunkyPNG::Color.from_hex("#666666"), image[5, 140] 171 | end 172 | 173 | # Test that passing a sample_width with gap_width will space out the samples, leaving 174 | # gaps in between that are sized by gap_width 175 | def test_has_spaced_samples_with_sample_width_with_gap_width 176 | Waveformer.generate(get_fixture("sample.wav"), filename: get_output("sample-spaced-with-gap.png"), sample_width: 1, gap_width: 3) 177 | 178 | image = open_png(get_output("sample-spaced-with-gap.png")) 179 | 180 | assert_equal ChunkyPNG::Color.from_hex("#00ccff"), image[0, 140] 181 | (1..3).each do |i| 182 | assert_equal ChunkyPNG::Color.from_hex("#666666"), image[i, 140] 183 | end 184 | end 185 | 186 | def test_raises_error_if_not_given_readable_audio_source 187 | assert_raise(Waveformer::RuntimeError) do 188 | Waveformer.generate(get_fixture("sample.txt"), filename: get_output("shouldnt_exist.png")) 189 | end 190 | end 191 | 192 | def test_overwrites_existing_waveform_if_force_is_true_and_file_exists 193 | FileUtils.touch get_output("overwritten.png") 194 | 195 | Waveformer.generate(get_fixture("sample.wav"), filename: get_output("overwritten.png")) 196 | end 197 | 198 | def test_raises_deprecation_exception_if_sox_fails_to_read_source_file 199 | begin 200 | Waveformer.generate(get_fixture("sample.txt"), filename: get_output("shouldnt_exist.png")) 201 | rescue Waveformer::RuntimeError => e 202 | assert_match /FAIL formats: no handler for given file type `txt'/, e.message 203 | end 204 | end 205 | end 206 | end 207 | end 208 | 209 | CarrierWave::AudioWaveform::WaveformerTest.cleanup -------------------------------------------------------------------------------- /lib/carrierwave/audio_waveform/waveformer.rb: -------------------------------------------------------------------------------- 1 | require 'ruby-audio' 2 | require 'ruby-sox' 3 | require 'oily_png' 4 | require 'fileutils' 5 | 6 | module CarrierWave 7 | module AudioWaveform 8 | class Waveformer 9 | DefaultOptions = { 10 | :method => :peak, 11 | :width => 1800, 12 | :height => 280, 13 | :background_color => "#666666", 14 | :color => "#00ccff", 15 | :logger => nil, 16 | :type => :png 17 | } 18 | 19 | TransparencyMask = "#00ff00" 20 | TransparencyAlternate = "#ffff00" # in case the mask is the background color! 21 | 22 | attr_reader :source 23 | 24 | # Scope these under Waveform so you can catch the ones generated by just this 25 | # class. 26 | class RuntimeError < ::RuntimeError;end; 27 | class ArgumentError < ::ArgumentError;end; 28 | 29 | class << self 30 | # Generate a Waveform image at the given filename with the given options. 31 | # 32 | # Available options (all optional) are: 33 | # 34 | # :method => The method used to read sample frames, available methods 35 | # are peak and rms. peak is probably what you're used to seeing, it uses 36 | # the maximum amplitude per sample to generate the waveform, so the 37 | # waveform looks more dynamic. RMS gives a more fluid waveform and 38 | # probably more accurately reflects what you hear, but isn't as 39 | # pronounced (typically). 40 | # 41 | # Can be :rms or :peak 42 | # Default is :peak. 43 | # 44 | # :width => The width (in pixels) of the final waveform image. 45 | # Default is 1800. 46 | # 47 | # :height => The height (in pixels) of the final waveform image. 48 | # Default is 280. 49 | # 50 | # :auto_width => msec per pixel. This will overwrite the width of the 51 | # final waveform image depending on the length of the audio file. 52 | # Example: 53 | # 100 => 1 pixel per 100 msec; a one minute audio file will result in a width of 600 pixels 54 | # 55 | # :background_color => Hex code of the background color of the generated 56 | # waveform image. Pass :transparent for transparent background. 57 | # Default is #666666 (gray). 58 | # 59 | # :color => Hex code of the color to draw the waveform, or can pass 60 | # :transparent to render the waveform transparent (use w/ a solid 61 | # color background to achieve a "cutout" effect). 62 | # Default is #00ccff (cyan-ish). 63 | # 64 | # :sample_width => Integer specifying the sample width. If this 65 | # is specified, there will be gaps (minimum of 1px wide, as specified 66 | # by :gap_width) between samples that are this wide in pixels. 67 | # Default is nil 68 | # Minimum is 1 (for anything other than nil) 69 | # 70 | # :gap_width => Integer specifying the gap width. If sample_width 71 | # is specified, this will be the size of the gaps between samples in pixels. 72 | # Default is nil 73 | # Minimum is 1 (for anything other than nil, or when sample_width is present but gap_width is not) 74 | # 75 | # :logger => IOStream to log progress to. 76 | # 77 | # Example: 78 | # CarrierWave::AudioWaveform::Waveformer.generate("Kickstart My Heart.wav") 79 | # CarrierWave::AudioWaveform::Waveformer.generate("Kickstart My Heart.wav", :method => :rms) 80 | # CarrierWave::AudioWaveform::Waveformer.generate("Kickstart My Heart.wav", :color => "#ff00ff", :logger => $stdout) 81 | # 82 | def generate(source, options={}) 83 | options = DefaultOptions.merge(options) 84 | filename = options[:filename] || self.generate_image_filename(source, options[:type]) 85 | 86 | raise ArgumentError.new("No source audio filename given, must be an existing sound file.") unless source 87 | raise ArgumentError.new("No destination filename given for waveform") unless filename 88 | raise RuntimeError.new("Source audio file '#{source}' not found.") unless File.exist?(source) 89 | 90 | old_source = source 91 | source = generate_wav_source(source) 92 | 93 | @log = Log.new(options[:logger]) 94 | @log.start! 95 | 96 | if options[:auto_width] 97 | RubyAudio::Sound.open(source) do |audio| 98 | options[:width] = (audio.info.length * 1000 / options[:auto_width].to_i).ceil 99 | end 100 | end 101 | 102 | # Frames gives the amplitudes for each channel, for our waveform we're 103 | # saying the "visual" amplitude is the average of the amplitude across all 104 | # the channels. This might be a little weird w/ the "peak" method if the 105 | # frames are very wide (i.e. the image width is very small) -- I *think* 106 | # the larger the frames are, the more "peaky" the waveform should get, 107 | # perhaps to the point of inaccurately reflecting the actual sound. 108 | samples = frames(source, options[:width], options[:method]).collect do |frame| 109 | frame.inject(0.0) { |sum, peak| sum + peak } / frame.size 110 | end 111 | 112 | @log.timed("\nDrawing...") do 113 | # Don't remove the file until we're sure the 114 | # source was readable 115 | if File.exists?(filename) 116 | @log.out("Output file #{filename} encountered. Removing.") 117 | File.unlink(filename) 118 | end 119 | 120 | image = draw samples, options 121 | 122 | if options[:type] == :svg 123 | File.open(filename, 'w') do |f| 124 | f.puts image 125 | end 126 | else 127 | image.save filename 128 | end 129 | end 130 | 131 | if source != old_source 132 | @log.out("Removing temporary file at #{source}") 133 | FileUtils.rm(source) 134 | end 135 | 136 | @log.done!("Generated waveform '#{filename}'") 137 | 138 | filename 139 | end 140 | 141 | def generate_image_filename(source, image_type) 142 | ext = File.extname(source) 143 | source_file_path_without_extension = File.join File.dirname(source), File.basename(source, ext) 144 | 145 | if image_type == :svg 146 | "#{source_file_path_without_extension}.svg" 147 | else 148 | "#{source_file_path_without_extension}.png" 149 | end 150 | end 151 | 152 | private 153 | 154 | 155 | # Returns a wav file if one was not passed in, or the original if it was 156 | def generate_wav_source(source) 157 | ext = File.extname(source) 158 | ext_gsubbed = ext.gsub(/\./, '') 159 | 160 | if ext != ".wav" 161 | input_options = { type: ext_gsubbed } 162 | output_options = { type: "wav" } 163 | source_filename_without_extension = File.basename(source, ext) 164 | output_file_path = File.join File.dirname(source), "tmp_#{source_filename_without_extension}_#{Time.now.to_i}.wav" 165 | converter = Sox::Cmd.new 166 | converter.add_input source, input_options 167 | converter.set_output output_file_path, output_options 168 | converter.run 169 | output_file_path 170 | else 171 | source 172 | end 173 | rescue Sox::Error => e 174 | raise e unless e.message.include?("FAIL formats:") 175 | raise RuntimeError.new("Source file #{source} could not be converted to .wav by Sox (Sox: #{e.message})") 176 | end 177 | 178 | # Returns a sampling of frames from the given RubyAudio::Sound using the 179 | # given method the sample size is determined by the given pixel width -- 180 | # we want one sample frame per horizontal pixel. 181 | def frames(source, width, method = :peak) 182 | raise ArgumentError.new("Unknown sampling method #{method}") unless [ :peak, :rms ].include?(method) 183 | 184 | frames = [] 185 | 186 | RubyAudio::Sound.open(source) do |audio| 187 | frames_read = 0 188 | frames_per_sample = (audio.info.frames.to_f / width.to_f).to_i 189 | sample = RubyAudio::Buffer.new("float", frames_per_sample, audio.info.channels) 190 | 191 | @log.timed("Sampling #{frames_per_sample} frames per sample: ") do 192 | while(frames_read = audio.read(sample)) > 0 193 | frames << send(method, sample, audio.info.channels) 194 | @log.out(".") 195 | end 196 | end 197 | end 198 | 199 | frames 200 | rescue RubyAudio::Error => e 201 | raise e unless e.message == "File contains data in an unknown format." 202 | raise RuntimeError.new("Source audio file #{source} could not be read by RubyAudio library -- Hint: non-WAV files are no longer supported, convert to WAV first using something like ffmpeg (RubyAudio: #{e.message})") 203 | end 204 | 205 | def draw(samples, options) 206 | if options[:type] == :svg 207 | draw_svg(samples, options) 208 | else 209 | draw_png(samples, options) 210 | end 211 | end 212 | 213 | def draw_svg(samples, options) 214 | wave_image = "" 215 | samples = spaced_samples(samples, options[:sample_width], options[:gap_width]) if options[:sample_width] 216 | height_factor = (options[:height] * 0.85 / 2.0) 217 | 218 | bar_pos = 0 219 | samples.each_with_index do |sample, pos| 220 | next if sample.nil? 221 | 222 | if (pos%3 == 0) 223 | amplitude = sample * height_factor 224 | top = (0 - amplitude).round 225 | bottom = (0 + amplitude).round 226 | 227 | wave_image+= "M#{bar_pos},#{top}V#{bottom}" 228 | 229 | bar_pos += (1 + options[:gap_width]) 230 | end 231 | end 232 | 233 | image = "" 234 | if (options[:hide_style].nil? || options[:hide_style] == false) 235 | image+= "" 249 | end 250 | image+= "" 251 | if options[:gradient] 252 | options[:gradient].each_with_index do |grad, id| 253 | image+= "" 254 | image+= "" 255 | image+= "" 256 | image+= "" 257 | end 258 | end 259 | uniqueWaveformID = "waveform-#{SecureRandom.uuid}" 260 | image+= "" 261 | image+= "" 262 | image+= '' 267 | image+= "" 268 | image+= "" 269 | image+= "" 270 | image+= "" 271 | image+= "" 272 | image+= "" 273 | end 274 | 275 | # Draws the given samples using the given options, returns a ChunkyPNG::Image. 276 | def draw_png(samples, options) 277 | image = ChunkyPNG::Image.new(options[:width], options[:height], 278 | options[:background_color] == :transparent ? ChunkyPNG::Color::TRANSPARENT : options[:background_color] 279 | ) 280 | 281 | if options[:color] == :transparent 282 | color = transparent = ChunkyPNG::Color.from_hex( 283 | # Have to do this little bit because it's possible the color we were 284 | # intending to use a transparency mask *is* the background color, and 285 | # then we'd end up wiping out the whole image. 286 | options[:background_color].downcase == TransparencyMask ? TransparencyAlternate : TransparencyMask 287 | ) 288 | else 289 | color = ChunkyPNG::Color.from_hex(options[:color]) 290 | end 291 | 292 | # Calling "zero" the middle of the waveform, like there's positive and 293 | # negative amplitude 294 | zero = options[:height] / 2.0 295 | 296 | # If a sample_width is passed, let's space those things out 297 | if options[:sample_width] 298 | samples = spaced_samples(samples, options[:sample_width], options[:gap_width]) 299 | end 300 | 301 | samples.each_with_index do |sample, x| 302 | next if sample.nil? 303 | # Half the amplitude goes above zero, half below 304 | amplitude = sample * options[:height].to_f / 2.0 305 | # If you give ChunkyPNG floats for pixel positions all sorts of things 306 | # go haywire. 307 | image.line(x, (zero - amplitude).round, x, (zero + amplitude).round, color) 308 | end 309 | 310 | # Simple transparency masking, it just loops over every pixel and makes 311 | # ones which match the transparency mask color completely clear. 312 | if transparent 313 | (0..image.width - 1).each do |x| 314 | (0..image.height - 1).each do |y| 315 | image[x, y] = ChunkyPNG::Color.rgba(0, 0, 0, 0) if image[x, y] == transparent 316 | end 317 | end 318 | end 319 | 320 | image 321 | end 322 | 323 | def spaced_samples samples, sample_width = 1, gap_width = 1 324 | sample_width = sample_width.to_i >= 1 ? sample_width.to_i : 1 325 | gap_width = gap_width.to_i >= 0 ? gap_width.to_i : 1 326 | width_counter = sample_width 327 | current_sample_index = 0 328 | spaced_samples = [] 329 | avg = nil 330 | while samples[current_sample_index] 331 | at_front_of_image = current_sample_index < sample_width 332 | 333 | # This determines if it's a gap, but we don't want 334 | # a gap to start with, hence the last booelan check 335 | if width_counter.to_i > sample_width.to_i && !at_front_of_image 336 | # This is a gap 337 | spaced_samples << nil 338 | width_counter -= 1 339 | else 340 | # This is a sample 341 | # If this is a new block of samples, get the average 342 | if avg.nil? 343 | avg = calculate_avg_sample(samples, current_sample_index, sample_width) 344 | end 345 | spaced_samples << avg 346 | # This is 1-indexed since it starts at sample_width 347 | # (or sample_width + gap_width for anything other than the initial passes) 348 | if width_counter.to_i < 2 349 | width_counter = sample_width + gap_width 350 | avg = nil 351 | else 352 | width_counter -= 1 353 | end 354 | end 355 | current_sample_index += 1 356 | end 357 | 358 | spaced_samples 359 | end 360 | 361 | # Calculate the average of a group of samples 362 | # Return the sample's value if it's a group of 1 363 | def calculate_avg_sample(samples, current_sample_index, sample_width) 364 | if sample_width > 1 365 | floats = samples[current_sample_index..(current_sample_index + sample_width - 1)].collect(&:to_f) 366 | #floats.inject(:+) / sample_width 367 | channel_rms(floats) 368 | else 369 | samples[current_sample_index] 370 | end 371 | end 372 | 373 | # Returns an array of the peak of each channel for the given collection of 374 | # frames -- the peak is individual to the channel, and the returned collection 375 | # of peaks are not (necessarily) from the same frame(s). 376 | def peak(frames, channels=1) 377 | peak_frame = [] 378 | (0..channels-1).each do |channel| 379 | peak_frame << channel_peak(frames, channel) 380 | end 381 | peak_frame 382 | end 383 | 384 | # Returns an array of rms values for the given frameset where each rms value is 385 | # the rms value for that channel. 386 | def rms(frames, channels=1) 387 | rms_frame = [] 388 | (0..channels-1).each do |channel| 389 | rms_frame << channel_rms(frames, channel) 390 | end 391 | rms_frame 392 | end 393 | 394 | # Returns the peak voltage reached on the given channel in the given collection 395 | # of frames. 396 | # 397 | # TODO: Could lose some resolution and only sample every other frame, would 398 | # likely still generate the same waveform as the waveform is so comparitively 399 | # low resolution to the original input (in most cases), and would increase 400 | # the analyzation speed (maybe). 401 | def channel_peak(frames, channel=0) 402 | peak = 0.0 403 | frames.each do |frame| 404 | next if frame.nil? 405 | frame = Array(frame) 406 | peak = frame[channel].abs if frame[channel].abs > peak 407 | end 408 | peak 409 | end 410 | 411 | # Returns the rms value across the given collection of frames for the given 412 | # channel. 413 | def channel_rms(frames, channel=0) 414 | Math.sqrt(frames.inject(0.0){ |sum, frame| sum += (frame ? Array(frame)[channel] ** 2 : 0) } / frames.size) 415 | end 416 | end 417 | end 418 | 419 | class Waveformer 420 | # A simple class for logging + benchmarking, nice to have good feedback on a 421 | # long batch operation. 422 | # 423 | # There's probably 10,000,000 other bechmarking classes, but writing this was 424 | # easier than using Google. 425 | class Log 426 | attr_accessor :io 427 | 428 | def initialize(io=$stdout) 429 | @io = io 430 | end 431 | 432 | # Prints the given message to the log 433 | def out(msg) 434 | io.print(msg) if io 435 | end 436 | 437 | # Prints the given message to the log followed by the most recent benchmark 438 | # (note that it calls .end! which will stop the benchmark) 439 | def done!(msg="") 440 | out "#{msg} (#{self.end!}s)\n" 441 | end 442 | 443 | # Starts a new benchmark clock and returns the index of the new clock. 444 | # 445 | # If .start! is called again before .end! then the time returned will be 446 | # the elapsed time from the next call to start!, and calling .end! again 447 | # will return the time from *this* call to start! (that is, the clocks are 448 | # LIFO) 449 | def start! 450 | (@benchmarks ||= []) << Time.now 451 | @current = @benchmarks.size - 1 452 | end 453 | 454 | # Returns the elapsed time from the most recently started benchmark clock 455 | # and ends the benchmark, so that a subsequent call to .end! will return 456 | # the elapsed time from the previously started benchmark clock. 457 | def end! 458 | elapsed = (Time.now - @benchmarks[@current]) 459 | @current -= 1 460 | elapsed 461 | end 462 | 463 | # Returns the elapsed time from the benchmark clock w/ the given index (as 464 | # returned from when .start! was called). 465 | def time?(index) 466 | Time.now - @benchmarks[index] 467 | end 468 | 469 | # Benchmarks the given block, printing out the given message first (if 470 | # given). 471 | def timed(message=nil, &block) 472 | start! 473 | out(message) if message 474 | yield 475 | done! 476 | end 477 | end 478 | end 479 | end 480 | end 481 | --------------------------------------------------------------------------------