feep 0.0.2 → 0.0.5
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +4 -4
- data/README.md +15 -9
- data/bin/feep +16 -10
- data/feep.gemspec +2 -1
- data/lib/feep.rb +44 -197
- data/lib/feep/constants.rb +73 -3
- data/lib/feep/scale.rb +51 -0
- data/lib/feep/sound_file.rb +63 -0
- data/lib/feep/sound_player.rb +87 -0
- data/lib/feep/utils.rb +40 -0
- data/lib/feep/version.rb +1 -1
- metadata +22 -4
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA1:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: a70a90d54a3e4507318bfc057c33ab7b6c880438
|
4
|
+
data.tar.gz: c3cf6d0841b6c5c4f0abf066827ef90741177307
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: 49504c639b61f8bd3f5a59012a2748b160a6bf58dd1c29f19dcfa6984debda642738e38963acc3080961138e844954ddb668752e063f6aa26e9308fec10b93c8
|
7
|
+
data.tar.gz: b7c41cf69ff4a5b3c9b1bbffb132cdcfd1cb0608f7c1443c6e3e9ac5b43195c146cef85cf01093e79dfd7afb01349dacd502604e81158b773202b55b2290a222
|
data/README.md
CHANGED
@@ -1,4 +1,5 @@
|
|
1
1
|
# Feep
|
2
|
+
[![Gem Version](https://badge.fury.io/rb/feep.svg)](http://badge.fury.io/rb/feep)
|
2
3
|
|
3
4
|
## Wha?
|
4
5
|
Use the power of Ruby gems to make your computer [feep](http://dictionary.reference.com/browse/feep) (except more musically) using sweet [WAV-file-writing technology](http://wavefilegem.com) from [Joel Strait](https://github.com/jstrait). Works on both Windows and *nix (including OS X) as it uses the standard WAV format to do its bidding.
|
@@ -17,23 +18,28 @@ Feep doesn't require any parameters, as it will play a 440Hz/A4 sine wave at 50%
|
|
17
18
|
|
18
19
|
The full usage looks like this:
|
19
20
|
|
20
|
-
`feep [-f, -n, --freq-or-note FREQUENCY|NOTE_NAME] [-w, --waveform WAVEFORM] [-a, --amplitude MAX_AMPLITUDE] [-d, --duration DURATION] [-save] [-loud]`
|
21
|
+
`feep [-f, -n, --freq-or-note FREQUENCY|NOTE_NAME] [-s, --scale SCALE_ID] [-d, --degrees NUMBER_OF_SCALE_DEGREES] [-w, --waveform WAVEFORM] [-a, --amplitude MAX_AMPLITUDE] [-d, --duration DURATION] [-save] [-loud]`
|
21
22
|
|
22
23
|
`-f, -n, --freq-or-note`: a number from 0 to 20000, or a valid note name from C0 to B9 (including sharps and flats). You can try a frequency outside of this range, but you may get odd results. You may also enter some combination of these with commas between them and it'll play all of them together in a chord.
|
23
24
|
|
25
|
+
`-s, -scale`: a scale ID that is part of the list that the gem understands (currently). If you put in an invalid one, it will list the valid ones.
|
26
|
+
|
27
|
+
`-d, --degrees`: the number of degrees of a scale you want to play. By default, the scale will play one octave.
|
28
|
+
|
24
29
|
`-w, --waveform`: a string equal to "sine", "square", "saw", "triangle", or "noise".
|
25
30
|
|
26
|
-
`-a, --amplitude`: a number from 0.0 (silence (why would you do this?)) to 1.0 (blast it)
|
31
|
+
`-a, --amplitude`: a number from 0.0 (silence (why would you do this?)) to 1.0 (blast it).
|
27
32
|
|
28
|
-
`-d, --duration`: number of milliseconds for the sound to last
|
33
|
+
`-d, --duration`: number of milliseconds for the sound to last.
|
29
34
|
|
30
|
-
`-save`: switch to save the resulting WAV file in the current directory. Will create it in the format of `waveform_frequency-in-Hz_volume_duration.wav
|
35
|
+
`-save`: switch to save the resulting WAV file in the current directory. Will create it in the format of `waveform_frequency-in-Hz_volume_duration.wav`.
|
31
36
|
|
32
|
-
`-loud`: switch that displays note and file-making information
|
37
|
+
`-loud`: switch that displays note and file-making information.
|
33
38
|
|
34
39
|
## Examples
|
35
40
|
|
36
|
-
`feep` - play a C4 sine wave note at 50% full volume for 200 ms
|
37
|
-
`feep -n Ab6 -w saw` - play a Ab6 sawtooth wave note at 50% full volume for 200 ms
|
38
|
-
`feep -n C#5 -w square -a 0.4 -d 500` - play a C#5 square wave note at 40% full volume for 500 ms
|
39
|
-
`feep -n 2000 -w triangle -a 0.8 -d 2000` - play a 2000Hz triangle wave note at 80% full volume for 2000 ms
|
41
|
+
* `feep` - play a C4 sine wave note at 50% full volume for 200 ms
|
42
|
+
* `feep -n Ab6 -w saw` - play a Ab6 sawtooth wave note at 50% full volume for 200 ms
|
43
|
+
* `feep -n C#5 -w square -a 0.4 -d 500` - play a C#5 square wave note at 40% full volume for 500 ms
|
44
|
+
* `feep -n 2000 -w triangle -a 0.8 -d 2000` - play a 2000Hz triangle wave note at 80% full volume for 2000 ms
|
45
|
+
* `feep -n C3 -s major` - play a major scale with C3 as the root note
|
data/bin/feep
CHANGED
@@ -1,20 +1,27 @@
|
|
1
1
|
#!/usr/bin/env ruby
|
2
2
|
|
3
|
+
require 'pry'
|
3
4
|
require 'optparse'
|
4
|
-
|
5
|
-
|
6
|
-
USAGE_INSTRUCTIONS = ''
|
5
|
+
require_relative '../lib/feep'
|
7
6
|
|
8
7
|
def parse_options
|
9
|
-
options = {:freq_or_note => '440.000', :waveform => 'sine', :volume => 0.5, :duration =>
|
8
|
+
options = {:freq_or_note => '440.000', :scale => nil, :waveform => 'sine', :volume => 0.5, :duration => 100, :save => false, :loud => false, :usage => nil}
|
10
9
|
|
11
10
|
optparse = OptionParser.new do |opts|
|
12
|
-
opts.banner = 'usage: feep [-f, -n, --freq-or-note FREQUENCY|NOTE_NAME] [-w, --waveform WAVEFORM] [-a, --amplitude MAX_AMPLITUDE] [-d, --duration DURATION] [-save] [-loud]'
|
11
|
+
opts.banner = 'usage: feep [-f, -n, --freq-or-note FREQUENCY|NOTE_NAME] [-s, --scale SCALE_ID] [-d, --degrees NUMBER_OF_SCALE_DEGREES] [-w, --waveform WAVEFORM] [-a, --amplitude MAX_AMPLITUDE] [-d, --duration DURATION] [-save] [-loud]'
|
13
12
|
|
14
13
|
opts.on('-f', '-n', '--freq-or-note FREQUENCY|NOTE_NAME', 'One or more frequencies or note names to play at once, e.g. 440 or A4 or 220,440,880') do |f_or_n|
|
15
14
|
options[:freq_or_note] = f_or_n
|
16
15
|
end
|
17
16
|
|
17
|
+
opts.on('-s', '--scale SCALE_ID', 'Name of a scale to play') do |s|
|
18
|
+
options[:scale] = s
|
19
|
+
end
|
20
|
+
|
21
|
+
opts.on('-d', '--degrees NUMBER_OF_DEGREES', 'Number of degrees of the scale to play') do |d|
|
22
|
+
options[:degrees] = d
|
23
|
+
end
|
24
|
+
|
18
25
|
opts.on('-w', '--wave WAVEFORM', 'Waveform type to use for the sound') do |w|
|
19
26
|
options[:waveform] = w
|
20
27
|
end
|
@@ -27,11 +34,11 @@ def parse_options
|
|
27
34
|
options[:duration] = d.to_i
|
28
35
|
end
|
29
36
|
|
30
|
-
opts.on('
|
37
|
+
opts.on('--save', 'Save the resulting WAV file in the current directory') do
|
31
38
|
options[:save] = true
|
32
39
|
end
|
33
40
|
|
34
|
-
opts.on('
|
41
|
+
opts.on('--loud', 'Displays information about note(s) being played') do
|
35
42
|
options[:loud] = true
|
36
43
|
end
|
37
44
|
|
@@ -46,7 +53,7 @@ def parse_options
|
|
46
53
|
end
|
47
54
|
end
|
48
55
|
|
49
|
-
|
56
|
+
options[:usage] = optparse.to_s
|
50
57
|
optparse.parse!()
|
51
58
|
|
52
59
|
return options
|
@@ -56,7 +63,6 @@ def print_error(error)
|
|
56
63
|
case error
|
57
64
|
when OptionParser::InvalidOption
|
58
65
|
puts "#{$PROGRAM_NAME}: illegal option #{error.args.join(' ')}"
|
59
|
-
puts USAGE_INSTRUCTIONS
|
60
66
|
else
|
61
67
|
puts "An unexpected error occurred while running #{$PROGRAM_NAME}:"
|
62
68
|
puts " #{error}\n"
|
@@ -66,7 +72,7 @@ end
|
|
66
72
|
begin
|
67
73
|
options = parse_options
|
68
74
|
|
69
|
-
Feep.new(options)
|
75
|
+
Feep::Base.new(options)
|
70
76
|
rescue => error
|
71
77
|
print_error(error)
|
72
78
|
exit(false)
|
data/feep.gemspec
CHANGED
@@ -25,5 +25,6 @@ Gem::Specification.new do |spec|
|
|
25
25
|
spec.add_development_dependency 'pry-byebug', '~> 3.0'
|
26
26
|
spec.add_development_dependency 'bundler', '~> 1.8'
|
27
27
|
spec.add_development_dependency 'rake', '~> 10.0'
|
28
|
-
spec.add_development_dependency 'rspec', '
|
28
|
+
spec.add_development_dependency 'rspec', '~> 3.0'
|
29
|
+
spec.add_development_dependency 'rubocop', '~> 0.29'
|
29
30
|
end
|
data/lib/feep.rb
CHANGED
@@ -1,213 +1,60 @@
|
|
1
|
+
# lib/feep.rb
|
2
|
+
require_relative 'feep/constants'
|
3
|
+
require_relative 'feep/scale'
|
4
|
+
require_relative 'feep/sound_file'
|
5
|
+
require_relative 'feep/sound_player'
|
6
|
+
require_relative 'feep/utils'
|
1
7
|
require 'wavefile'
|
2
|
-
require 'os'
|
3
|
-
require 'feep/constants'
|
4
8
|
|
5
|
-
|
6
|
-
|
7
|
-
# main entry point
|
8
|
-
def initialize(options)
|
9
|
-
@options = options
|
10
|
-
configure_sound(options)
|
11
|
-
end
|
12
|
-
|
13
|
-
# convert midi notes to frequencies
|
14
|
-
def midi_to_freq(midi_note)
|
15
|
-
440.0 * (2.0 ** ((midi_note.to_f-69)/12))
|
16
|
-
end
|
17
|
-
|
18
|
-
# convert frequencies to midi notes
|
19
|
-
def freq_to_midi(freq)
|
20
|
-
(69 + 12 * (Math.log2(freq.to_i.abs / 440.0))).round
|
21
|
-
end
|
22
|
-
|
23
|
-
# makes sure that whatever kind of sound was entered on the CLI
|
24
|
-
# it is now a frequency to feed into the sample data generator
|
25
|
-
def convert_note_to_freq(freq_or_note)
|
26
|
-
if freq_or_note.match(/[A-Za-z]/)
|
27
|
-
if NOTE_FREQ.key?(freq_or_note)
|
28
|
-
frequency = NOTE_FREQ[freq_or_note]
|
29
|
-
else
|
30
|
-
app_error(ERROR_MSG[:note_name])
|
31
|
-
end
|
32
|
-
else
|
33
|
-
frequency = freq_or_note
|
34
|
-
end
|
35
|
-
|
36
|
-
return frequency
|
37
|
-
end
|
9
|
+
module Feep
|
10
|
+
class Base
|
38
11
|
|
39
|
-
|
40
|
-
|
41
|
-
|
42
|
-
|
43
|
-
if !WAVE_TYPES.include?(options[:waveform])
|
44
|
-
app_error(ERROR_MSG[:wave_form])
|
12
|
+
# main entry point
|
13
|
+
def initialize(options)
|
14
|
+
@options = options
|
15
|
+
configure_sound(options)
|
45
16
|
end
|
46
17
|
|
47
|
-
#
|
48
|
-
|
49
|
-
|
50
|
-
|
51
|
-
|
52
|
-
|
53
|
-
### B. Set frequency/note, or group of frequencies/notes, to play
|
54
|
-
|
55
|
-
# Is it a chord or a note?
|
56
|
-
if options[:freq_or_note].include?(',')
|
57
|
-
# yes, it's a chord, so create threads
|
58
|
-
threads = []
|
59
|
-
options[:freq_or_note].split(',').each do |note|
|
60
|
-
sound_to_play = convert_note_to_freq(note)
|
61
|
-
output_filename = "#{options[:waveform]}_#{sound_to_play}Hz_#{options[:volume].to_f}_#{options[:duration]}.wav"
|
62
|
-
threads << Thread.new {
|
63
|
-
play_note(sound_to_play.to_f, options[:waveform], options[:volume].to_f, options[:duration].to_i, samples_to_write, output_filename)
|
64
|
-
}
|
18
|
+
# takes CLI options, massages them, and passes them to the
|
19
|
+
# sound generation methods
|
20
|
+
def configure_sound(options)
|
21
|
+
### A. Check non-essential options
|
22
|
+
if !WAVE_TYPES.include?(options[:waveform])
|
23
|
+
print_error(ERROR_MSG[:invalid_waveform])
|
65
24
|
end
|
66
|
-
threads.each { |th| th.join }
|
67
|
-
else
|
68
|
-
# no, it's a single note
|
69
|
-
sound_to_play = convert_note_to_freq(options[:freq_or_note])
|
70
|
-
output_filename = "#{options[:waveform]}_#{sound_to_play}Hz_#{options[:volume].to_f}_#{options[:duration]}.wav"
|
71
|
-
play_note(sound_to_play, options[:waveform], options[:volume].to_f, options[:duration].to_i, samples_to_write, output_filename)
|
72
|
-
end
|
73
|
-
end
|
74
25
|
|
75
|
-
|
76
|
-
|
77
|
-
delimiter = OS.windows? ? ';' : ':'
|
26
|
+
# Convert ms to secs in order to multiply the sample rate by
|
27
|
+
duration_s = (options[:duration].to_f / 1000)
|
78
28
|
|
79
|
-
|
29
|
+
# Make the samples to write a nice integer
|
30
|
+
samples_to_write = (SAMPLE_RATE * duration_s).to_i
|
80
31
|
|
81
|
-
|
82
|
-
if system_apps.include? SNDPLAYER_WIN
|
83
|
-
display_text_beep(duration)
|
84
|
-
system("#{SNDPLAYER_WIN} #{file}")
|
85
|
-
else
|
86
|
-
puts "couldn't find #{SNDPLAYER_WIN}"
|
87
|
-
end
|
88
|
-
end
|
32
|
+
### B. Set frequency/note, or group of frequencies/notes, to play
|
89
33
|
|
90
|
-
|
91
|
-
if
|
92
|
-
|
93
|
-
|
34
|
+
# Is it a chord, scale, or single note?
|
35
|
+
if options[:freq_or_note].include?(',')
|
36
|
+
# yes, it's a chord, so create threads
|
37
|
+
threads = []
|
38
|
+
options[:freq_or_note].split(',').each do |note|
|
39
|
+
sound_to_play = Utils.new.convert_note_to_freq(note)
|
40
|
+
output_filename = "#{options[:waveform]}_#{sound_to_play}Hz_#{options[:volume].to_f}_#{options[:duration]}.wav"
|
41
|
+
threads << Thread.new {
|
42
|
+
SoundPlayer.new.play_note(sound_to_play, output_filename, samples_to_write, options)
|
43
|
+
}
|
44
|
+
end
|
45
|
+
threads.each { |th| th.join }
|
94
46
|
else
|
95
|
-
|
47
|
+
# it's a scale
|
48
|
+
if options[:scale]
|
49
|
+
Feep::Scale.new.play_scale(options)
|
50
|
+
else
|
51
|
+
# no, it's a single note
|
52
|
+
sound_to_play = Utils.convert_note_to_freq(options[:freq_or_note])
|
53
|
+
output_filename = "#{options[:waveform]}_#{sound_to_play}Hz_#{options[:volume].to_f}_#{options[:duration]}.wav"
|
54
|
+
SoundPlayer.new.play_note(sound_to_play, output_filename, samples_to_write, options)
|
55
|
+
end
|
96
56
|
end
|
97
57
|
end
|
98
|
-
end
|
99
|
-
|
100
|
-
# displays a fun beep message
|
101
|
-
def display_text_beep(duration)
|
102
|
-
print 'Be'
|
103
|
-
1.upto(duration) {|ms|
|
104
|
-
if ms % 100 == 0
|
105
|
-
print 'e'
|
106
|
-
end
|
107
|
-
}
|
108
|
-
puts 'ep!'
|
109
|
-
end
|
110
|
-
|
111
|
-
# removes the sound, unless marked to save
|
112
|
-
def remove_sound(file)
|
113
|
-
if !@options[:save]
|
114
|
-
if OS.windows?
|
115
|
-
system("del #{file}")
|
116
|
-
else
|
117
|
-
system("rm #{file}")
|
118
|
-
end
|
119
|
-
else
|
120
|
-
if @options[:loud]
|
121
|
-
info = WaveFile::Reader.info(file)
|
122
|
-
duration = info.duration
|
123
|
-
formatted_duration = duration.minutes.to_s.rjust(2, '0') << ':' <<
|
124
|
-
duration.seconds.to_s.rjust(2, '0') << ':' <<
|
125
|
-
duration.milliseconds.to_s.rjust(3, '0')
|
126
|
-
puts ''
|
127
|
-
puts "Created #{file}"
|
128
|
-
puts '---'
|
129
|
-
puts "Length: #{formatted_duration}"
|
130
|
-
puts "Format: #{info.audio_format}"
|
131
|
-
puts "Channels: #{info.channels}"
|
132
|
-
puts "Frames: #{info.sample_frame_count}"
|
133
|
-
puts "Sample Rate: #{info.sample_rate}"
|
134
|
-
end
|
135
|
-
end
|
136
|
-
end
|
137
|
-
|
138
|
-
# code from Joel Strait's nanosynth to generate raw audio
|
139
|
-
def create_sound(frequency, waveform, samples, volume, output_filename)
|
140
|
-
# Generate sample data for the given frequency, amplitude, and duration.
|
141
|
-
# Since we are using a sample rate of 44,100Hz, 44,100 samples are required for one second of sound.
|
142
|
-
samples = generate_sample_data(waveform.to_sym, samples, frequency.to_f, volume.to_f)
|
143
58
|
|
144
|
-
# Wrap the array of samples in a Buffer, so that it can be written to a Wave file
|
145
|
-
# by the WaveFile gem. Since we generated samples between -1.0 and 1.0, the sample
|
146
|
-
# type should be :float
|
147
|
-
buffer = WaveFile::Buffer.new(samples, WaveFile::Format.new(:mono, :float, 44100))
|
148
|
-
|
149
|
-
# Write the Buffer containing our samples to a 16-bit, monophonic Wave file
|
150
|
-
# with a sample rate of 44,100Hz, using the WaveFile gem.
|
151
|
-
WaveFile::Writer.new(output_filename, WaveFile::Format.new(:mono, :pcm_16, 44100)) do |writer|
|
152
|
-
writer.write(buffer)
|
153
|
-
end
|
154
59
|
end
|
155
|
-
|
156
|
-
# more code from Joel Strait's nanosynth (http://joe)
|
157
|
-
# The dark heart of NanoSynth, the part that actually generates the audio data
|
158
|
-
def generate_sample_data(wave_type, num_samples, frequency, max_amplitude)
|
159
|
-
position_in_period = 0.0
|
160
|
-
position_in_period_delta = frequency / SAMPLE_RATE
|
161
|
-
|
162
|
-
# Initialize an array of samples set to 0.0. Each sample will be replaced with
|
163
|
-
# an actual value below.
|
164
|
-
samples = [].fill(0.0, 0, num_samples)
|
165
|
-
|
166
|
-
num_samples.times do |i|
|
167
|
-
# Add next sample to sample list. The sample value is determined by
|
168
|
-
# plugging position_in_period into the appropriate wave function.
|
169
|
-
if wave_type == :sine
|
170
|
-
samples[i] = Math::sin(position_in_period * TWO_PI) * max_amplitude
|
171
|
-
elsif wave_type == :square
|
172
|
-
samples[i] = (position_in_period >= 0.5) ? max_amplitude : -max_amplitude
|
173
|
-
elsif wave_type == :saw
|
174
|
-
samples[i] = ((position_in_period * 2.0) - 1.0) * max_amplitude
|
175
|
-
elsif wave_type == :triangle
|
176
|
-
samples[i] = max_amplitude - (((position_in_period * 2.0) - 1.0) * max_amplitude * 2.0).abs
|
177
|
-
elsif wave_type == :noise
|
178
|
-
samples[i] = RANDOM_GENERATOR.rand(-max_amplitude..max_amplitude)
|
179
|
-
end
|
180
|
-
|
181
|
-
position_in_period += position_in_period_delta
|
182
|
-
|
183
|
-
# Constrain the period between 0.0 and 1.0.
|
184
|
-
# That is, keep looping and re-looping over the same period.
|
185
|
-
if(position_in_period >= 1.0)
|
186
|
-
position_in_period -= 1.0
|
187
|
-
end
|
188
|
-
end
|
189
|
-
|
190
|
-
return samples
|
191
|
-
end
|
192
|
-
|
193
|
-
# creates, plays, and removes note
|
194
|
-
def play_note(frequency, waveform, volume, duration, samples_to_write, output_filename)
|
195
|
-
if @options[:loud]
|
196
|
-
puts 'Playing note'
|
197
|
-
puts " frequency: #{frequency.to_f.abs}"
|
198
|
-
puts " midi: #{freq_to_midi(frequency)}"
|
199
|
-
puts " duration: #{duration}"
|
200
|
-
end
|
201
|
-
create_sound(frequency, waveform, samples_to_write, volume, output_filename)
|
202
|
-
play_sound(output_filename, duration)
|
203
|
-
remove_sound(output_filename)
|
204
|
-
end
|
205
|
-
|
206
|
-
# displays error, usage, and exits
|
207
|
-
def app_error(msg)
|
208
|
-
puts "#{File.basename($PROGRAM_NAME).split('.')[0]}: #{msg}"
|
209
|
-
puts 'usage: feep [frequency|note_name|list_of_frequencies_or_note_names] [sine|square|saw|triangle|noise] [volume] [duration] [save]'
|
210
|
-
exit
|
211
|
-
end
|
212
|
-
|
213
60
|
end
|
data/lib/feep/constants.rb
CHANGED
@@ -1,4 +1,7 @@
|
|
1
|
-
|
1
|
+
# lib/feep/constants.rb
|
2
|
+
|
3
|
+
module Feep
|
4
|
+
# constants
|
2
5
|
SNDPLAYER_WIN = 'sounder.exe'
|
3
6
|
SNDPLAYER_UNIX = 'afplay'
|
4
7
|
SAMPLE_RATE = 44100
|
@@ -6,6 +9,70 @@ class Feep
|
|
6
9
|
RANDOM_GENERATOR = Random.new
|
7
10
|
WAVE_TYPES = %w[sine square saw triangle noise]
|
8
11
|
|
12
|
+
# tables of musical data
|
13
|
+
SCALES = Hash[
|
14
|
+
:chromatic => '1,1,1,1,1,1,1,1,1,1,1,1,1',
|
15
|
+
:whole_tone => '2,2,2,2,2,2,2',
|
16
|
+
:major => '2,2,2,1,2,2,2,1',
|
17
|
+
:minor_harm => '2,1,2,2,1,3,3',
|
18
|
+
:minor_melodic => '2,1,2,2,2,2,3',
|
19
|
+
:major_pentatonic => '2,2,3,2,3,2',
|
20
|
+
:minor_pentatonic => '3,2,2,3,2,3',
|
21
|
+
:blues => '3,2,1,1,3,2,3',
|
22
|
+
:phyrgian => '1,2,2,2,1,2,2,2',
|
23
|
+
:dorian => '2,1,2,2,2,1,2,2'
|
24
|
+
]
|
25
|
+
NOTES = Array[
|
26
|
+
'C0','C#0','D0','D#0','E0','F0','F#0','G0','G#0','A0','A#0','B0',
|
27
|
+
'C1','C#1','D1','D#1','E1','F1','F#1','G1','G#1','A1','A#1','B1',
|
28
|
+
'C2','C#2','D2','D#2','E2','F2','F#2','G2','G#2','A2','A#2','B2',
|
29
|
+
'C3','C#3','D3','D#3','E3','F3','F#3','G3','G#3','A3','A#3','B3',
|
30
|
+
'C4','C#4','D4','D#4','E4','F4','F#4','G4','G#4','A4','A#4','B4',
|
31
|
+
'C5','C#5','D5','D#5','E5','F5','F#5','G5','G#5','A5','A#5','B5',
|
32
|
+
'C6','C#6','D6','D#6','E6','F6','F#6','G6','G#6','A6','A#6','B6',
|
33
|
+
'C7','C#7','D7','D#7','E7','F7','F#7','G7','G#7','A7','A#7','B7',
|
34
|
+
'C8','C#8','D8','D#8','E8','F8','F#8','G8','G#8','A8','A#8','B8',
|
35
|
+
'C9','C#9','D9','D#9','E9','F9','F#9','G9','G#9','A9','A#9','B9'
|
36
|
+
]
|
37
|
+
NOTES_ALT = Array[
|
38
|
+
'C0','Db0','D0','Eb0','E0','F0','Gb0','G0','Ab0','A0','Bb0','B0',
|
39
|
+
'C1','Db1','D1','Eb1','E1','F1','Gb1','G1','Ab1','A1','Bb1','B1',
|
40
|
+
'C2','Db2','D2','Eb2','E2','F2','Gb2','G2','Ab2','A2','Bb2','B2',
|
41
|
+
'C3','Db3','D3','Eb3','E3','F3','Gb3','G3','Ab3','A3','Bb3','B3',
|
42
|
+
'C4','Db4','D4','Eb4','E4','F4','Gb4','G4','Ab4','A4','Bb4','B4',
|
43
|
+
'C5','Db5','D5','Eb5','E5','F5','Gb5','G5','Ab5','A5','Bb5','B5',
|
44
|
+
'C6','Db6','D6','Eb6','E6','F6','Gb6','G6','Ab6','A6','Bb6','B6',
|
45
|
+
'C7','Db7','D7','Eb7','E7','F7','Gb7','G7','Ab7','A7','Bb7','B7',
|
46
|
+
'C8','Db8','D8','Eb8','E8','F8','Gb8','G8','Ab8','A8','Bb8','B8',
|
47
|
+
'C9','Db9','D9','Eb9','E9','F9','Gb9','G9','Ab9','A9','Bb9','B9'
|
48
|
+
]
|
49
|
+
|
50
|
+
FREQS = Array[
|
51
|
+
16.351,17.324,18.354,19.445,20.601,21.827,23.124,24.499,25.956,27.500,29.135,30.868,
|
52
|
+
32.703,34.648,36.708,38.891,41.203,43.654,46.249,48.999,51.913,55.000,58.270,61.375,
|
53
|
+
65.406,69.296,73.416,77.782,82.407,87.307,92.499,97.999,103.826,110.000,116.541,
|
54
|
+
123.471,130.813,138.591,146.832,155.564,164.814,174.614,184.997,195.998,207.652,220.000,233.082,246.942,
|
55
|
+
261.626,277.183,293.665,311.127,329.628,349.228,369.994,391.995,415.305,440.000,466.164,493.883,
|
56
|
+
523.251,554.365,587.330,622.254,659.255,698.457,739.989,783.991,830.609,880.000,932.328,987.767,
|
57
|
+
1046.502,1108.731,1174.659,1244.508,1318.510,1396.913,1479.978,1567.982,1661.219,1760.000,1864.655,1975.533,
|
58
|
+
2093.005,2217.461,2349.318,2489.016,2637.021,2793.826,2959.956,3135.964,3322.438,3520.000,3729.310,3951.066,
|
59
|
+
4186.009,4434.922,4698.636,4978.032,5274.042,5587.652,5919.910,6271.928,6644.876,7040.000,7458.620,7902.132,
|
60
|
+
8372.018,8869.844,9397.272,9956.064,10548.084,11175.304,11839.820,12543.856,13289.752,14080.000,14917.240,15804.264
|
61
|
+
]
|
62
|
+
|
63
|
+
MIDIS = Array[
|
64
|
+
12, 13, 14, 15, 16, 17, 18, 19, 20, 21, 22, 23,
|
65
|
+
24, 25, 26, 27, 28, 29, 30, 31, 32, 33, 34, 35,
|
66
|
+
36, 37, 38, 39, 40, 41, 42, 43, 44, 45, 46, 47,
|
67
|
+
48, 49, 50, 51, 52, 53, 54, 55, 56, 57, 58, 59,
|
68
|
+
60, 61, 62, 63, 64, 65, 66, 67, 68, 69, 70, 71,
|
69
|
+
72, 73, 74, 75, 76, 77, 78, 79, 80, 81, 82, 83,
|
70
|
+
84, 85, 86, 87, 88, 89, 90, 91, 92, 93, 94, 95,
|
71
|
+
96, 97, 98, 99, 100, 101, 102, 103, 104, 105, 106, 107,
|
72
|
+
108, 109, 110, 111, 112, 113, 114, 115, 116, 117, 118, 119,
|
73
|
+
120, 121, 122, 123, 124, 125, 126, 127, 128, 129, 130, 131
|
74
|
+
]
|
75
|
+
|
9
76
|
NOTE_FREQ = Hash[
|
10
77
|
'C0' => 16.351,
|
11
78
|
'C#0' => 17.324,
|
@@ -179,8 +246,11 @@ class Feep
|
|
179
246
|
'B9' => 15804.264
|
180
247
|
]
|
181
248
|
|
249
|
+
# error messages
|
182
250
|
ERROR_MSG = Hash[
|
183
|
-
:
|
184
|
-
:
|
251
|
+
:invalid_note => 'Note name argument is invalid.',
|
252
|
+
:invalid_scale => "Scale ID is invalid. Valid IDs are: #{SCALES.keys}",
|
253
|
+
:invalid_waveform => 'Wave form type is invalid.',
|
254
|
+
:scale_needs_note => 'You need to enter a valid note (-n) if you want to play a scale.'
|
185
255
|
]
|
186
256
|
end
|
data/lib/feep/scale.rb
ADDED
@@ -0,0 +1,51 @@
|
|
1
|
+
# lib/feep/scale.rb
|
2
|
+
require_relative '../feep'
|
3
|
+
require_relative 'constants'
|
4
|
+
|
5
|
+
module Feep
|
6
|
+
class Scale
|
7
|
+
|
8
|
+
# plays the musical scale with Feep
|
9
|
+
def play_scale(options)
|
10
|
+
unless SCALES.include?(options[:scale].to_sym)
|
11
|
+
Utils.print_error(:invalid_scale)
|
12
|
+
end
|
13
|
+
|
14
|
+
unless NOTES.include?(options[:freq_or_note]) && NOTES_ALT.include?(options[:freq_or_note])
|
15
|
+
Utils.print_error(:invalid_note)
|
16
|
+
end
|
17
|
+
|
18
|
+
steps = SCALES[options[:scale].to_sym].split(',')
|
19
|
+
|
20
|
+
note = options[:freq_or_note]
|
21
|
+
note_index = NOTES.index(note)
|
22
|
+
freq = FREQS[NOTES.index(note)]
|
23
|
+
|
24
|
+
feep_options = {:freq_or_note => note, :waveform => options[:waveform], :volume => options[:volume], :duration => options[:duration], :save => options[:save], :loud => options[:loud]}
|
25
|
+
|
26
|
+
# play number of degrees of scale supplied or one octave by default
|
27
|
+
degrees = options[:degrees] || steps.length
|
28
|
+
|
29
|
+
if options[:loud]
|
30
|
+
puts "Playing a #{options[:scale]} scale..."
|
31
|
+
end
|
32
|
+
|
33
|
+
1.upto(degrees.to_i) {|deg|
|
34
|
+
if options[:loud]
|
35
|
+
puts "note: #{note}, freq: #{freq}"
|
36
|
+
end
|
37
|
+
|
38
|
+
# play note
|
39
|
+
Feep::Base.new(feep_options)
|
40
|
+
|
41
|
+
# go to the next note in the scale
|
42
|
+
note_index += steps[deg].to_i
|
43
|
+
|
44
|
+
# set new note to play next time around
|
45
|
+
note = feep_options[:freq_or_note] = NOTES[note_index]
|
46
|
+
freq = FREQS[note_index]
|
47
|
+
}
|
48
|
+
end
|
49
|
+
|
50
|
+
end
|
51
|
+
end
|
@@ -0,0 +1,63 @@
|
|
1
|
+
# lib/feep/sound_file.rb
|
2
|
+
# Contains code from Joel Strait's nanosynth to generate raw audio
|
3
|
+
|
4
|
+
require 'wavefile'
|
5
|
+
require_relative 'constants'
|
6
|
+
|
7
|
+
module Feep
|
8
|
+
class SoundFile
|
9
|
+
|
10
|
+
def create_sound(frequency, samples, output_filename, options)
|
11
|
+
# Generate sample data for the given frequency, amplitude, and duration.
|
12
|
+
# Since we are using a sample rate of 44,100Hz, 44,100 samples are required for one second of sound.
|
13
|
+
samples = generate_sample_data(options[:waveform].to_sym, samples, frequency.to_f, options[:volume].to_f)
|
14
|
+
|
15
|
+
# Wrap the array of samples in a Buffer, so that it can be written to a Wave file
|
16
|
+
# by the WaveFile gem. Since we generated samples between -1.0 and 1.0, the sample
|
17
|
+
# type should be :float
|
18
|
+
buffer = WaveFile::Buffer.new(samples, WaveFile::Format.new(:mono, :float, 44100))
|
19
|
+
|
20
|
+
# Write the Buffer containing our samples to a 16-bit, monophonic Wave file
|
21
|
+
# with a sample rate of 44,100Hz, using the WaveFile gem.
|
22
|
+
WaveFile::Writer.new(output_filename, WaveFile::Format.new(:mono, :pcm_16, 44100)) do |writer|
|
23
|
+
writer.write(buffer)
|
24
|
+
end
|
25
|
+
end
|
26
|
+
|
27
|
+
def generate_sample_data(wave_type, num_samples, frequency, max_amplitude)
|
28
|
+
position_in_period = 0.0
|
29
|
+
position_in_period_delta = frequency / SAMPLE_RATE
|
30
|
+
|
31
|
+
# Initialize an array of samples set to 0.0. Each sample will be replaced with
|
32
|
+
# an actual value below.
|
33
|
+
samples = [].fill(0.0, 0, num_samples)
|
34
|
+
|
35
|
+
num_samples.times do |i|
|
36
|
+
# Add next sample to sample list. The sample value is determined by
|
37
|
+
# plugging position_in_period into the appropriate wave function.
|
38
|
+
if wave_type == :sine
|
39
|
+
samples[i] = Math::sin(position_in_period * TWO_PI) * max_amplitude
|
40
|
+
elsif wave_type == :square
|
41
|
+
samples[i] = (position_in_period >= 0.5) ? max_amplitude : -max_amplitude
|
42
|
+
elsif wave_type == :saw
|
43
|
+
samples[i] = ((position_in_period * 2.0) - 1.0) * max_amplitude
|
44
|
+
elsif wave_type == :triangle
|
45
|
+
samples[i] = max_amplitude - (((position_in_period * 2.0) - 1.0) * max_amplitude * 2.0).abs
|
46
|
+
elsif wave_type == :noise
|
47
|
+
samples[i] = RANDOM_GENERATOR.rand(-max_amplitude..max_amplitude)
|
48
|
+
end
|
49
|
+
|
50
|
+
position_in_period += position_in_period_delta
|
51
|
+
|
52
|
+
# Constrain the period between 0.0 and 1.0.
|
53
|
+
# That is, keep looping and re-looping over the same period.
|
54
|
+
if(position_in_period >= 1.0)
|
55
|
+
position_in_period -= 1.0
|
56
|
+
end
|
57
|
+
end
|
58
|
+
|
59
|
+
return samples
|
60
|
+
end
|
61
|
+
|
62
|
+
end
|
63
|
+
end
|
@@ -0,0 +1,87 @@
|
|
1
|
+
require 'os'
|
2
|
+
require_relative 'sound_file'
|
3
|
+
require_relative 'constants'
|
4
|
+
|
5
|
+
module Feep
|
6
|
+
class SoundPlayer
|
7
|
+
|
8
|
+
# main function that creates, plays, and removes note
|
9
|
+
def play_note(frequency, output_filename, samples_to_write, options)
|
10
|
+
if options[:loud]
|
11
|
+
puts 'Playing note'
|
12
|
+
puts " frequency: #{frequency.to_f.abs}"
|
13
|
+
puts " midi: #{Utils.freq_to_midi(frequency)}"
|
14
|
+
puts " duration: #{options[:duration]}"
|
15
|
+
end
|
16
|
+
SoundFile.new.create_sound(frequency, samples_to_write, output_filename, options)
|
17
|
+
play_wav_file(output_filename, options[:duration])
|
18
|
+
remove_sound(output_filename, options)
|
19
|
+
end
|
20
|
+
|
21
|
+
# use command line app to play wav file
|
22
|
+
def play_wav_file(file, duration)
|
23
|
+
delimiter = OS.windows? ? ';' : ':'
|
24
|
+
|
25
|
+
system_apps = ENV['PATH'].split(delimiter).collect { |d| Dir.entries d if Dir.exist? d }.flatten
|
26
|
+
|
27
|
+
if OS.windows?
|
28
|
+
if system_apps.include? SNDPLAYER_WIN
|
29
|
+
display_text_beep(duration)
|
30
|
+
system("#{SNDPLAYER_WIN} #{file}")
|
31
|
+
else
|
32
|
+
puts "couldn't find #{SNDPLAYER_WIN}"
|
33
|
+
end
|
34
|
+
end
|
35
|
+
|
36
|
+
if OS.mac? || OS.linux?
|
37
|
+
if system_apps.include? SNDPLAYER_UNIX
|
38
|
+
display_text_beep(duration)
|
39
|
+
system("#{SNDPLAYER_UNIX} #{file}")
|
40
|
+
else
|
41
|
+
puts "couldn't find #{SNDPLAYER_UNIX}"
|
42
|
+
end
|
43
|
+
end
|
44
|
+
end
|
45
|
+
|
46
|
+
# displays a fun beep message after playing wav file
|
47
|
+
def display_text_beep(duration)
|
48
|
+
print 'Be'
|
49
|
+
1.upto(duration) {|ms|
|
50
|
+
if ms % 100 == 0
|
51
|
+
print 'e'
|
52
|
+
end
|
53
|
+
}
|
54
|
+
puts 'ep!'
|
55
|
+
end
|
56
|
+
|
57
|
+
# removes the sound, unless marked to save,
|
58
|
+
# and optionally display info about file
|
59
|
+
def remove_sound(file, options)
|
60
|
+
unless options[:save]
|
61
|
+
if OS.windows?
|
62
|
+
system("del #{file}")
|
63
|
+
else
|
64
|
+
|
65
|
+
system("rm #{file}")
|
66
|
+
end
|
67
|
+
else
|
68
|
+
if options[:loud]
|
69
|
+
info = WaveFile::Reader.info(file)
|
70
|
+
duration = info.duration
|
71
|
+
formatted_duration = duration.minutes.to_s.rjust(2, '0') << ':' <<
|
72
|
+
duration.seconds.to_s.rjust(2, '0') << ':' <<
|
73
|
+
duration.milliseconds.to_s.rjust(3, '0')
|
74
|
+
puts ''
|
75
|
+
puts "Created #{file}"
|
76
|
+
puts '---'
|
77
|
+
puts "Length: #{formatted_duration}"
|
78
|
+
puts "Format: #{info.audio_format}"
|
79
|
+
puts "Channels: #{info.channels}"
|
80
|
+
puts "Frames: #{info.sample_frame_count}"
|
81
|
+
puts "Sample Rate: #{info.sample_rate}"
|
82
|
+
end
|
83
|
+
end
|
84
|
+
end
|
85
|
+
|
86
|
+
end
|
87
|
+
end
|
data/lib/feep/utils.rb
ADDED
@@ -0,0 +1,40 @@
|
|
1
|
+
# lib/feep/utils.rb
|
2
|
+
module Feep
|
3
|
+
module Utils
|
4
|
+
|
5
|
+
# convert midi notes to frequencies
|
6
|
+
def self.midi_to_freq(midi_note)
|
7
|
+
440.0 * (2.0 ** ((midi_note.to_f-69)/12))
|
8
|
+
end
|
9
|
+
|
10
|
+
# convert frequencies to midi notes
|
11
|
+
def self.freq_to_midi(freq)
|
12
|
+
(69 + 12 * (Math.log2(freq.to_i.abs / 440.0))).round
|
13
|
+
end
|
14
|
+
|
15
|
+
# makes sure that whatever kind of sound was entered on the CLI
|
16
|
+
# it is now a frequency to feed into the sample data generator
|
17
|
+
def self.convert_note_to_freq(freq_or_note)
|
18
|
+
if freq_or_note.match(/[A-Za-z]/)
|
19
|
+
if NOTE_FREQ.key?(freq_or_note)
|
20
|
+
frequency = NOTE_FREQ[freq_or_note]
|
21
|
+
else
|
22
|
+
print_error(ERROR_MSG[:invalid_note])
|
23
|
+
end
|
24
|
+
else
|
25
|
+
frequency = freq_or_note
|
26
|
+
end
|
27
|
+
|
28
|
+
return frequency
|
29
|
+
end
|
30
|
+
|
31
|
+
# displays error, usage, and exits
|
32
|
+
def self.print_error(msg_id)
|
33
|
+
msg = ERROR_MSG[msg_id.to_sym]
|
34
|
+
puts "#{File.basename($PROGRAM_NAME).split(".")[0]}: #{msg}"
|
35
|
+
puts 'usage: feep [frequency|note-name|list-of-frequencies-or-note-names] [scale] [scale-degrees] [sine|square|saw|triangle|noise] [volume] [duration] [save] [loud]'
|
36
|
+
exit
|
37
|
+
end
|
38
|
+
|
39
|
+
end
|
40
|
+
end
|
data/lib/feep/version.rb
CHANGED
metadata
CHANGED
@@ -1,14 +1,14 @@
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
2
2
|
name: feep
|
3
3
|
version: !ruby/object:Gem::Version
|
4
|
-
version: 0.0.
|
4
|
+
version: 0.0.5
|
5
5
|
platform: ruby
|
6
6
|
authors:
|
7
7
|
- Michael Chadwick
|
8
8
|
autorequire:
|
9
9
|
bindir: bin
|
10
10
|
cert_chain: []
|
11
|
-
date: 2015-03-
|
11
|
+
date: 2015-03-17 00:00:00.000000000 Z
|
12
12
|
dependencies:
|
13
13
|
- !ruby/object:Gem::Dependency
|
14
14
|
name: wavefile
|
@@ -90,16 +90,30 @@ dependencies:
|
|
90
90
|
name: rspec
|
91
91
|
requirement: !ruby/object:Gem::Requirement
|
92
92
|
requirements:
|
93
|
-
- - "
|
93
|
+
- - "~>"
|
94
94
|
- !ruby/object:Gem::Version
|
95
95
|
version: '3.0'
|
96
96
|
type: :development
|
97
97
|
prerelease: false
|
98
98
|
version_requirements: !ruby/object:Gem::Requirement
|
99
99
|
requirements:
|
100
|
-
- - "
|
100
|
+
- - "~>"
|
101
101
|
- !ruby/object:Gem::Version
|
102
102
|
version: '3.0'
|
103
|
+
- !ruby/object:Gem::Dependency
|
104
|
+
name: rubocop
|
105
|
+
requirement: !ruby/object:Gem::Requirement
|
106
|
+
requirements:
|
107
|
+
- - "~>"
|
108
|
+
- !ruby/object:Gem::Version
|
109
|
+
version: '0.29'
|
110
|
+
type: :development
|
111
|
+
prerelease: false
|
112
|
+
version_requirements: !ruby/object:Gem::Requirement
|
113
|
+
requirements:
|
114
|
+
- - "~>"
|
115
|
+
- !ruby/object:Gem::Version
|
116
|
+
version: '0.29'
|
103
117
|
description: Use Ruby to make your computer beep at a certain frequency for a certain
|
104
118
|
duration. Do it for fun, or add it to other programs for easy alert sounds.
|
105
119
|
email: mike@codana.me
|
@@ -117,6 +131,10 @@ files:
|
|
117
131
|
- feep.gemspec
|
118
132
|
- lib/feep.rb
|
119
133
|
- lib/feep/constants.rb
|
134
|
+
- lib/feep/scale.rb
|
135
|
+
- lib/feep/sound_file.rb
|
136
|
+
- lib/feep/sound_player.rb
|
137
|
+
- lib/feep/utils.rb
|
120
138
|
- lib/feep/version.rb
|
121
139
|
- spec/feep_spec.rb
|
122
140
|
- spec/spec_helper.rb
|