From 41085bc337343d24068b72b509f2c910bb177b25 Mon Sep 17 00:00:00 2001 From: Morgan Wildermuth Date: Fri, 27 Feb 2015 16:20:59 -0800 Subject: [PATCH 01/10] added waveform :type option to include phonocardiogram waveform rendering --- lib/waveform.rb | 67 ++++++++++++++++++++++++++++++++----------- test/waveform_test.rb | 8 ++++++ 2 files changed, 58 insertions(+), 17 deletions(-) diff --git a/lib/waveform.rb b/lib/waveform.rb index 7619cd0..965d186 100644 --- a/lib/waveform.rb +++ b/lib/waveform.rb @@ -15,7 +15,8 @@ class Waveform :background_color => "#666666", :color => "#00ccff", :force => false, - :logger => nil + :logger => nil, + :type => :audio } TransparencyMask = "#00ff00" @@ -67,6 +68,10 @@ class << self # # :logger => IOStream to log progress to. # + # :type => form of waveform + # Can be :audio or :phonocardiogram + # Default is traditional audio waveform which includes plotting mirrored absolute values of points + # # Example: # Waveform.generate("Kickstart My Heart.wav", "Kickstart My Heart.png") # Waveform.generate("Kickstart My Heart.wav", "Kickstart My Heart.png", :method => :rms) @@ -95,7 +100,7 @@ def generate(source, filename, options={}) # frames are very wide (i.e. the image width is very small) -- I *think* # the larger the frames are, the more "peaky" the waveform should get, # perhaps to the point of inaccurately reflecting the actual sound. - samples = frames(source, options[:width], options[:method]).collect do |frame| + samples = frames(source, options[:width], options[:method], options[:type]).collect do |frame| frame.inject(0.0) { |sum, peak| sum + peak } / frame.size end @@ -119,7 +124,7 @@ def generate(source, filename, options={}) # Returns a sampling of frames from the given RubyAudio::Sound using the # given method the sample size is determined by the given pixel width -- # we want one sample frame per horizontal pixel. - def frames(source, width, method = :peak) + def frames(source, width, method = :peak, type = :audio) raise ArgumentError.new("Unknown sampling method #{method}") unless [ :peak, :rms ].include?(method) frames = [] @@ -128,10 +133,9 @@ def frames(source, width, method = :peak) frames_read = 0 frames_per_sample = (audio.info.frames.to_f / width.to_f).to_i sample = RubyAudio::Buffer.new("float", frames_per_sample, audio.info.channels) - @log.timed("Sampling #{frames_per_sample} frames per sample: ") do while(frames_read = audio.read(sample)) > 0 - frames << send(method, sample, audio.info.channels) + frames << send(method, sample, audio.info.channels, type) @log.out(".") end end @@ -160,6 +164,22 @@ def draw(samples, options) color = ChunkyPNG::Color.from_hex(options[:color]) end + options[:type] == :audio ? image = drawAudio(samples, image, options, color) : image = drawPhonocardiogram(samples, image, options, color); + + # Simple transparency masking, it just loops over every pixel and makes + # ones which match the transparency mask color completely clear. + if transparent + (0..image.width - 1).each do |x| + (0..image.height - 1).each do |y| + image[x, y] = ChunkyPNG::Color.rgba(0, 0, 0, 0) if image[x, y] == transparent + end + end + end + + image + end + + def drawAudio(samples, image, options, color) # Calling "zero" the middle of the waveform, like there's positive and # negative amplitude zero = options[:height] / 2.0 @@ -171,27 +191,36 @@ def draw(samples, options) # go haywire. image.line(x, (zero - amplitude).round, x, (zero + amplitude).round, color) end + image + end - # Simple transparency masking, it just loops over every pixel and makes - # ones which match the transparency mask color completely clear. - if transparent - (0..image.width - 1).each do |x| - (0..image.height - 1).each do |y| - image[x, y] = ChunkyPNG::Color.rgba(0, 0, 0, 0) if image[x, y] == transparent - end + def drawPhonocardiogram(samples, image, options, color) + zero = options[:height] / 2.0 + last_point_x_y = [] + samples.each_with_index do |sample, x| + amplitude = sample * options[:height].to_f / 2.0 + if last_point_x_y.empty? + last_point_x_y[0] = x + last_point_x_y[1] = (zero - amplitude).round end + # Half the amplitude goes above zero, half below + # If you give ChunkyPNG floats for pixel positions all sorts of things + # go haywire. + image.line(last_point_x_y[0], last_point_x_y[1], x, (zero - amplitude).round, color) + # image.line(last_point_x_y[0], last_point_x_y[1], x, (zero + amplitude).round, color) + last_point_x_y[0] = x + last_point_x_y[1] = (zero - amplitude).round end - image end # Returns an array of the peak of each channel for the given collection of # frames -- the peak is individual to the channel, and the returned collection # of peaks are not (necessarily) from the same frame(s). - def peak(frames, channels=1) + def peak(frames, channels=1, type) peak_frame = [] (0..channels-1).each do |channel| - peak_frame << channel_peak(frames, channel) + peak_frame << channel_peak(frames, channel, type) end peak_frame end @@ -213,12 +242,16 @@ def rms(frames, channels=1) # likely still generate the same waveform as the waveform is so comparitively # low resolution to the original input (in most cases), and would increase # the analyzation speed (maybe). - def channel_peak(frames, channel=0) + def channel_peak(frames, channel=0, type) peak = 0.0 frames.each do |frame| next if frame.nil? frame = Array(frame) - peak = frame[channel].abs if frame[channel].abs > peak + if type == :audio + peak = frame[channel].abs if frame[channel].abs > peak + else + peak = frame[channel] + end end peak end diff --git a/test/waveform_test.rb b/test/waveform_test.rb index 6202917..9f8643d 100644 --- a/test/waveform_test.rb +++ b/test/waveform_test.rb @@ -54,6 +54,14 @@ def test_generates_waveform_from_mono_audio_source_via_rms assert_equal ChunkyPNG::Color.from_hex(Waveform::DefaultOptions[:background_color]), image[0, 0] end + def test_generates_phonocardiogram_waveform + Waveform.generate(fixture("phonocardiogram_sample.wav"), output("phonocardiogram_sample.png"), :type => :phonocardiogram) + assert File.exists?(output("phonocardiogram_sample.png")) + + image = open_png(output("phonocardiogram_sample.png")) + assert_equal ChunkyPNG::Color.from_hex(Waveform::DefaultOptions[:background_color]), image[0, 0] + end + def test_logs_to_given_io File.open(output("waveform.log"), "w") do |io| Waveform.generate(fixture("sample.wav"), output("logged.png"), :logger => io) From 23a098877002eef0f813ba47fa1f2a28fdc2fbb4 Mon Sep 17 00:00:00 2001 From: Morgan Wildermuth Date: Fri, 27 Feb 2015 16:37:48 -0800 Subject: [PATCH 02/10] include comments for phonocardiogram creation --- lib/waveform.rb | 20 +++++++++----------- 1 file changed, 9 insertions(+), 11 deletions(-) diff --git a/lib/waveform.rb b/lib/waveform.rb index 965d186..b01e232 100644 --- a/lib/waveform.rb +++ b/lib/waveform.rb @@ -195,21 +195,19 @@ def drawAudio(samples, image, options, color) end def drawPhonocardiogram(samples, image, options, color) + #generally follows drawAudio with minor adjustments to remove mirroring and graph points with negative values + zero = options[:height] / 2.0 - last_point_x_y = [] + + #establish starting point of first line in graph + last_point_x_y = [0, (zero - (samples[0] * options[:height].to_f/2.0).round)] + samples.each_with_index do |sample, x| amplitude = sample * options[:height].to_f / 2.0 - if last_point_x_y.empty? - last_point_x_y[0] = x - last_point_x_y[1] = (zero - amplitude).round - end - # Half the amplitude goes above zero, half below - # If you give ChunkyPNG floats for pixel positions all sorts of things - # go haywire. + #connect end of last line with current point in sample data image.line(last_point_x_y[0], last_point_x_y[1], x, (zero - amplitude).round, color) - # image.line(last_point_x_y[0], last_point_x_y[1], x, (zero + amplitude).round, color) - last_point_x_y[0] = x - last_point_x_y[1] = (zero - amplitude).round + #update last point data so next line will begin from correct point + last_point_x_y.replace([x, (zero - amplitude).round]) end image end From 8634786d2e959250e075a3fb0b0e856ef0707362 Mon Sep 17 00:00:00 2001 From: Morgan Wildermuth Date: Mon, 2 Mar 2015 09:16:56 -0800 Subject: [PATCH 03/10] changed variable names to enhance clarity --- lib/waveform.rb | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/lib/waveform.rb b/lib/waveform.rb index b01e232..a02102a 100644 --- a/lib/waveform.rb +++ b/lib/waveform.rb @@ -195,19 +195,19 @@ def drawAudio(samples, image, options, color) end def drawPhonocardiogram(samples, image, options, color) - #generally follows drawAudio with minor adjustments to remove mirroring and graph points with negative values + #generally follows drawAudio with minor adjustments to remove mirroring and graph points with negative values (had to channel peaks in order to retain negative values in samples) zero = options[:height] / 2.0 #establish starting point of first line in graph - last_point_x_y = [0, (zero - (samples[0] * options[:height].to_f/2.0).round)] + starting_point = [0, (zero - (samples[0] * options[:height].to_f/2.0).round)] samples.each_with_index do |sample, x| amplitude = sample * options[:height].to_f / 2.0 #connect end of last line with current point in sample data - image.line(last_point_x_y[0], last_point_x_y[1], x, (zero - amplitude).round, color) + image.line(starting_point[0], starting_point[1], x, (zero - amplitude).round, color) #update last point data so next line will begin from correct point - last_point_x_y.replace([x, (zero - amplitude).round]) + starting_point.replace([x, (zero - amplitude).round]) end image end From 81498ae2d92836e41bf4c5043ba0d0e9e761fb93 Mon Sep 17 00:00:00 2001 From: Morgan Wildermuth Date: Mon, 2 Mar 2015 09:24:55 -0800 Subject: [PATCH 04/10] removed unnecessary passing of arguments --- lib/waveform.rb | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/waveform.rb b/lib/waveform.rb index a02102a..cdea12a 100644 --- a/lib/waveform.rb +++ b/lib/waveform.rb @@ -225,7 +225,7 @@ def peak(frames, channels=1, type) # Returns an array of rms values for the given frameset where each rms value is # the rms value for that channel. - def rms(frames, channels=1) + def rms(frames, channels=1, type) rms_frame = [] (0..channels-1).each do |channel| rms_frame << channel_rms(frames, channel) From f820b0dcade42096ea085e772bede34decc6e780 Mon Sep 17 00:00:00 2001 From: Morgan Wildermuth Date: Mon, 2 Mar 2015 11:25:07 -0800 Subject: [PATCH 05/10] added ability to draw wave from provided sample array --- lib/waveform.rb | 23 ++++++++++++++++++----- test/waveform_test.rb | 8 ++++++++ 2 files changed, 26 insertions(+), 5 deletions(-) diff --git a/lib/waveform.rb b/lib/waveform.rb index cdea12a..e9e89a9 100644 --- a/lib/waveform.rb +++ b/lib/waveform.rb @@ -16,7 +16,8 @@ class Waveform :color => "#00ccff", :force => false, :logger => nil, - :type => :audio + :type => :audio, + :samples => :read } TransparencyMask = "#00ff00" @@ -72,6 +73,11 @@ class << self # Can be :audio or :phonocardiogram # Default is traditional audio waveform which includes plotting mirrored absolute values of points # + # :samples => origin of sample data + # Can be array of samples or :read + # Default is :read which means the audio's samples will be created by the gem + # When array of samples is provided, assumption is each float will be between -1 and 1 + # # Example: # Waveform.generate("Kickstart My Heart.wav", "Kickstart My Heart.png") # Waveform.generate("Kickstart My Heart.wav", "Kickstart My Heart.png", :method => :rms) @@ -100,9 +106,7 @@ def generate(source, filename, options={}) # frames are very wide (i.e. the image width is very small) -- I *think* # the larger the frames are, the more "peaky" the waveform should get, # perhaps to the point of inaccurately reflecting the actual sound. - samples = frames(source, options[:width], options[:method], options[:type]).collect do |frame| - frame.inject(0.0) { |sum, peak| sum + peak } / frame.size - end + samples = retrieve_samples(source, options) @log.timed("\nDrawing...") do # Don't remove the file even if force is true until we're sure the @@ -121,6 +125,16 @@ def generate(source, filename, options={}) private + def retrieve_samples(source, options) + if options[:samples] == :read + samples = frames(source, options[:width], options[:method], options[:type]).collect do |frame| + frame.inject(0.0) { |sum, peak| sum + peak } / frame.size + end + elsif options[:samples].class == Array + samples = options[:samples] + end + end + # Returns a sampling of frames from the given RubyAudio::Sound using the # given method the sample size is determined by the given pixel width -- # we want one sample frame per horizontal pixel. @@ -198,7 +212,6 @@ def drawPhonocardiogram(samples, image, options, color) #generally follows drawAudio with minor adjustments to remove mirroring and graph points with negative values (had to channel peaks in order to retain negative values in samples) zero = options[:height] / 2.0 - #establish starting point of first line in graph starting_point = [0, (zero - (samples[0] * options[:height].to_f/2.0).round)] diff --git a/test/waveform_test.rb b/test/waveform_test.rb index 9f8643d..4b23807 100644 --- a/test/waveform_test.rb +++ b/test/waveform_test.rb @@ -62,6 +62,14 @@ def test_generates_phonocardiogram_waveform assert_equal ChunkyPNG::Color.from_hex(Waveform::DefaultOptions[:background_color]), image[0, 0] end + def test_generates_phonocardiogram_waveform_via_passed_data + Waveform.generate(fixture("phonocardiogram_sample.wav"), output("phonocardiogram_array_sample.png"), :type => :phonocardiogram, :array => [-0.052887, -0.074229, -0.094981, -0.100566, -0.090027, -0.084483, -0.088877, -0.088816, -0.089976, -0.090607, -0.085347, -0.075551, -0.056081, -0.033030, -0.011159, 0.008809, 0.016886, 0.017008]) + assert File.exists?(output("phonocardiogram_array_sample.png")) + + image = open_png(output("phonocardiogram_array_sample.png")) + assert_equal ChunkyPNG::Color.from_hex(Waveform::DefaultOptions[:background_color]), image[0, 0] + end + def test_logs_to_given_io File.open(output("waveform.log"), "w") do |io| Waveform.generate(fixture("sample.wav"), output("logged.png"), :logger => io) From 925f18a854974ec3eb4c96be8dede30beb489977 Mon Sep 17 00:00:00 2001 From: Morgan Wildermuth Date: Mon, 2 Mar 2015 13:36:32 -0800 Subject: [PATCH 06/10] updated tests to ensure output from phonocardiogram generation did not match audio generation --- test/waveform_test.rb | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/test/waveform_test.rb b/test/waveform_test.rb index 4b23807..627d22d 100644 --- a/test/waveform_test.rb +++ b/test/waveform_test.rb @@ -55,18 +55,20 @@ def test_generates_waveform_from_mono_audio_source_via_rms end def test_generates_phonocardiogram_waveform - Waveform.generate(fixture("phonocardiogram_sample.wav"), output("phonocardiogram_sample.png"), :type => :phonocardiogram) + Waveform.generate(fixture("sample.wav"), output("phonocardiogram_sample.png"), :type => :phonocardiogram) assert File.exists?(output("phonocardiogram_sample.png")) image = open_png(output("phonocardiogram_sample.png")) + assert_not_equal ChunkyPNG::Color.from_hex(Waveform::DefaultOptions[:color]), image[60, 120] assert_equal ChunkyPNG::Color.from_hex(Waveform::DefaultOptions[:background_color]), image[0, 0] end def test_generates_phonocardiogram_waveform_via_passed_data - Waveform.generate(fixture("phonocardiogram_sample.wav"), output("phonocardiogram_array_sample.png"), :type => :phonocardiogram, :array => [-0.052887, -0.074229, -0.094981, -0.100566, -0.090027, -0.084483, -0.088877, -0.088816, -0.089976, -0.090607, -0.085347, -0.075551, -0.056081, -0.033030, -0.011159, 0.008809, 0.016886, 0.017008]) + Waveform.generate(fixture("sample.wav"), output("phonocardiogram_array_sample.png"), :type => :phonocardiogram, :array => [-0.052887, -0.074229, -0.094981, -0.100566, -0.090027, -0.084483, -0.088877, -0.088816, -0.089976, -0.090607, -0.085347, -0.075551, -0.056081, -0.033030, -0.011159, 0.008809, 0.016886, 0.017008]) assert File.exists?(output("phonocardiogram_array_sample.png")) image = open_png(output("phonocardiogram_array_sample.png")) + assert_not_equal ChunkyPNG::Color.from_hex(Waveform::DefaultOptions[:color]), image[60, 120] assert_equal ChunkyPNG::Color.from_hex(Waveform::DefaultOptions[:background_color]), image[0, 0] end From cff01c90a0dd88ebacc7088784091f09ba4ff7d4 Mon Sep 17 00:00:00 2001 From: Morgan Wildermuth Date: Mon, 2 Mar 2015 14:03:31 -0800 Subject: [PATCH 07/10] added CLI usage + readme update --- README.md | 9 +++++++++ bin/waveform | 8 ++++++++ 2 files changed, 17 insertions(+) diff --git a/README.md b/README.md index 3dc99b9..8c01ee9 100644 --- a/README.md +++ b/README.md @@ -36,6 +36,15 @@ There are some nifty options you can supply to switch things up: -m sets the method used to sample the source audio file, it can either be 'peak' or 'rms'. 'peak' is probably what you want because it looks cooler, but 'rms' is closer to what you actually hear. + -s sets the method used to retrieve samples of audio file, it can either be + 'read' or an array of points within -1..1 range. 'read' is default and samples + from the audio file provided + -t sets the type of waveform to render, it can either be 'audio' or 'phonocardiogram'. + 'audio' is default and commonly seen on sites that play music: example of an audio wave http://www.bza.biz/indexhibit/files/gimgs/waveform.gif) + 'phonocardiogram' is specific to heartbeats example: http://www.stethographics.com/newimages/products/phono/murmur.jpg + The core difference between th two is audio plots the absolute value of a sample point + and mirrors it, while the phonocardiogram maintains the original value and does not mirror. + There are also some less-nifty options: diff --git a/bin/waveform b/bin/waveform index 512cb96..bcbd028 100755 --- a/bin/waveform +++ b/bin/waveform @@ -39,6 +39,14 @@ optparse = OptionParser.new do |o| options[:method] = method.to_sym end + o.on("-t", "--type TYPE", "Type of waveform generated (can be 'audio' or 'phonocardiogram') -- Default '#{Waveform::DefaultOptions[:type]}'.") do |type| + options[:type] = type.to_sym + end + + # o.on("-s", "--samples SAMPLES", "Origin of samples (can be 'read' or an array of floats -1..1) -- Default '#{Waveform::DefaultOptions[:samples]}'.") do |samples| + # options[:samples] = samples.to_sym + # end + options[:logger] = $stdout o.on("-q", "--quiet", "Don't print anything out when generating waveform") do options[:logger] = nil From c63f3d7f9eb614bf4c939cf2acb68c58e89dd505 Mon Sep 17 00:00:00 2001 From: Morgan Wildermuth Date: Mon, 2 Mar 2015 14:06:36 -0800 Subject: [PATCH 08/10] added clarity to -m vs -s, hopefully sufficient --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index 8c01ee9..5a81f9a 100644 --- a/README.md +++ b/README.md @@ -38,7 +38,7 @@ There are some nifty options you can supply to switch things up: cooler, but 'rms' is closer to what you actually hear. -s sets the method used to retrieve samples of audio file, it can either be 'read' or an array of points within -1..1 range. 'read' is default and samples - from the audio file provided + from the audio file provided as indicated by the method given in -m -t sets the type of waveform to render, it can either be 'audio' or 'phonocardiogram'. 'audio' is default and commonly seen on sites that play music: example of an audio wave http://www.bza.biz/indexhibit/files/gimgs/waveform.gif) 'phonocardiogram' is specific to heartbeats example: http://www.stethographics.com/newimages/products/phono/murmur.jpg From cab174aee23067e93a28e799326784a810802a46 Mon Sep 17 00:00:00 2001 From: Morgan Wildermuth Date: Mon, 2 Mar 2015 17:47:52 -0800 Subject: [PATCH 09/10] added samples to CLI usage --- bin/waveform | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/bin/waveform b/bin/waveform index bcbd028..feaab61 100755 --- a/bin/waveform +++ b/bin/waveform @@ -43,9 +43,9 @@ optparse = OptionParser.new do |o| options[:type] = type.to_sym end - # o.on("-s", "--samples SAMPLES", "Origin of samples (can be 'read' or an array of floats -1..1) -- Default '#{Waveform::DefaultOptions[:samples]}'.") do |samples| - # options[:samples] = samples.to_sym - # end + o.on("-s", "--samples SAMPLES", "Origin of samples (can be 'read' or an array of floats -1..1) -- Default '#{Waveform::DefaultOptions[:samples]}'.") do |samples| + options[:samples] = samples.to_sym + end options[:logger] = $stdout o.on("-q", "--quiet", "Don't print anything out when generating waveform") do From 5915982043a2a8028050f3583f9cdba9e55b23a3 Mon Sep 17 00:00:00 2001 From: Morgan Wildermuth Date: Mon, 2 Mar 2015 18:19:57 -0800 Subject: [PATCH 10/10] updated version --- lib/waveform/version.rb | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/waveform/version.rb b/lib/waveform/version.rb index 1b5eb74..e27bf75 100644 --- a/lib/waveform/version.rb +++ b/lib/waveform/version.rb @@ -1,3 +1,3 @@ class Waveform - VERSION = "0.1.2" + VERSION = "0.1.3" end