playnote 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +7 -0
- data/LICENSE.md +22 -0
- data/README.md +51 -0
- data/Rakefile +8 -0
- data/exe/playnote +280 -0
- data/lib/playnote/version.rb +5 -0
- data/lib/playnote.rb +8 -0
- data/sig/playnote.rbs +4 -0
- metadata +49 -0
checksums.yaml
ADDED
@@ -0,0 +1,7 @@
|
|
1
|
+
---
|
2
|
+
SHA256:
|
3
|
+
metadata.gz: 62a214c59bec51ba07ed57311fa9dc2c03a891ed1735e8e3a8010a4f0ff7e815
|
4
|
+
data.tar.gz: ee76f5db51cac164310d97738bf3ebe5d5f44f0a46b4e174a4677af09346be71
|
5
|
+
SHA512:
|
6
|
+
metadata.gz: 5f332337bc9ea437b642c714968279ba4c979aa81ba6e565aedf0ffce221a42841b42efbce6c13df05fbd14885e1f758c21a4b46f0395a0b1fc5c54cb7121347
|
7
|
+
data.tar.gz: e002225bbf5bc59ef716ea9130e9bd8eff2b4e0162317d80c36c4710fb4fb0e21dbd810e601e78af21cf902b113d068f4aa940238cdc625a3cd602a23d598d77
|
data/LICENSE.md
ADDED
@@ -0,0 +1,22 @@
|
|
1
|
+
Copyright (c) 2025 asciodev
|
2
|
+
|
3
|
+
Permission is hereby granted, free of charge, to any person
|
4
|
+
obtaining a copy of this software and associated documentation
|
5
|
+
files (the "Software"), to deal in the Software without
|
6
|
+
restriction, including without limitation the rights to use,
|
7
|
+
copy, modify, merge, publish, distribute, sublicense, and/or sell
|
8
|
+
copies of the Software, and to permit persons to whom the
|
9
|
+
Software is furnished to do so, subject to the following
|
10
|
+
conditions:
|
11
|
+
|
12
|
+
The above copyright notice and this permission notice shall be
|
13
|
+
included in all copies or substantial portions of the Software.
|
14
|
+
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
|
16
|
+
EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES
|
17
|
+
OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
|
18
|
+
NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT
|
19
|
+
HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY,
|
20
|
+
WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
|
21
|
+
FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR
|
22
|
+
OTHER DEALINGS IN THE SOFTWARE.
|
data/README.md
ADDED
@@ -0,0 +1,51 @@
|
|
1
|
+
# Playnote
|
2
|
+
|
3
|
+
Playnote is a utility for ergonomically playing tones from the CLI.
|
4
|
+
|
5
|
+
## Dependencies
|
6
|
+
|
7
|
+
* Ruby 3.4 or later
|
8
|
+
* ffplay 7.1.1 or later
|
9
|
+
|
10
|
+
## Installation
|
11
|
+
|
12
|
+
```bash
|
13
|
+
gem install playnote
|
14
|
+
```
|
15
|
+
|
16
|
+
## Usage
|
17
|
+
|
18
|
+
```manpage
|
19
|
+
Usage: playnote [options] <notes>
|
20
|
+
|
21
|
+
Playnote is a utility for ergonomically playing tones from the CLI.
|
22
|
+
|
23
|
+
Each note is a string taking the form <tone>[semitone][octave], with
|
24
|
+
sharps denoted by `#`, and flats denoted by `%`. Both semitones and octaves
|
25
|
+
are optional; by default, notes are neither flat nor sharp, and each inherits
|
26
|
+
the octave of the previous note if its octave is unspecified. (If no notes
|
27
|
+
specify an octave, the octave for all notes is 0.) There is no delimiter character.
|
28
|
+
|
29
|
+
-q, --quiet Suppresses all terminal output from command
|
30
|
+
-p, --print-only Prints the given notes with their frequencies,
|
31
|
+
without audio playback (overrides -q)
|
32
|
+
-d, --duration [DURATION] Time in seconds to play the given notes
|
33
|
+
-h, --help Prints this message and exits
|
34
|
+
-v, --[no-]verbose Run verbosely (overrides -q and -p)
|
35
|
+
|
36
|
+
EXAMPLES
|
37
|
+
playnote c3eg Play a C Major Chord in the 3rd Octave
|
38
|
+
|
39
|
+
playnote c3e%4g Play a C Minor Chord, with C in 3rd Octave,
|
40
|
+
and E♭ and G in the 4th
|
41
|
+
|
42
|
+
playnote g#5 Play a G♯ Note in the 5th Octave
|
43
|
+
|
44
|
+
playnote -p f2ac Print the notes comprising an F Major chord
|
45
|
+
along with their frequencies, and exit
|
46
|
+
without audio playback
|
47
|
+
```
|
48
|
+
|
49
|
+
## Contributing
|
50
|
+
|
51
|
+
Bug reports and pull requests are welcome on GitHub at https://github.com/asciodev/playnote.
|
data/Rakefile
ADDED
data/exe/playnote
ADDED
@@ -0,0 +1,280 @@
|
|
1
|
+
#!/usr/bin/env ruby
|
2
|
+
require 'optparse'
|
3
|
+
require 'curses'
|
4
|
+
|
5
|
+
Note = Struct.new(:letter, :semitone, :octave, :frequency, :pretty, :original)
|
6
|
+
|
7
|
+
class NotePlayer
|
8
|
+
@@hz_map = {
|
9
|
+
"C0": 16.35,
|
10
|
+
"C#0": 17.32,
|
11
|
+
"D0": 18.35,
|
12
|
+
"E%0": 19.45,
|
13
|
+
"E0": 20.6,
|
14
|
+
"F0": 21.83,
|
15
|
+
"F#0": 23.12,
|
16
|
+
"G0": 24.5,
|
17
|
+
"A%0": 25.96,
|
18
|
+
"A0": 27.5,
|
19
|
+
"B%0": 29.14,
|
20
|
+
"B0": 30.87,
|
21
|
+
"C1": 32.7,
|
22
|
+
"C#1": 34.65,
|
23
|
+
"D1": 36.71,
|
24
|
+
"E%1": 38.89,
|
25
|
+
"E1": 41.2,
|
26
|
+
"F1": 43.65,
|
27
|
+
"F#1": 46.25,
|
28
|
+
"G1": 49,
|
29
|
+
"A%1": 51.91,
|
30
|
+
"A1": 55,
|
31
|
+
"B%1": 58.27,
|
32
|
+
"B1": 61.74,
|
33
|
+
"C2": 65.41,
|
34
|
+
"C#2": 69.3,
|
35
|
+
"D2": 73.42,
|
36
|
+
"E%2": 77.78,
|
37
|
+
"E2": 82.41,
|
38
|
+
"F2": 87.31,
|
39
|
+
"F#2": 92.5,
|
40
|
+
"G2": 98,
|
41
|
+
"A%2": 103.83,
|
42
|
+
"A2": 110,
|
43
|
+
"B%2": 116.54,
|
44
|
+
"B2": 123.47,
|
45
|
+
"C3": 130.81,
|
46
|
+
"C#3": 138.59,
|
47
|
+
"D3": 146.83,
|
48
|
+
"E%3": 155.56,
|
49
|
+
"E3": 164.81,
|
50
|
+
"F3": 174.61,
|
51
|
+
"F#3": 185,
|
52
|
+
"G3": 196,
|
53
|
+
"A%3": 207.65,
|
54
|
+
"A3": 220,
|
55
|
+
"B%3": 233.08,
|
56
|
+
"B3": 246.94,
|
57
|
+
"C4": 261.63,
|
58
|
+
"C#4": 277.18,
|
59
|
+
"D4": 293.66,
|
60
|
+
"E%4": 311.13,
|
61
|
+
"E4": 329.63,
|
62
|
+
"F4": 349.23,
|
63
|
+
"F#4": 369.99,
|
64
|
+
"G4": 392,
|
65
|
+
"A%4": 415.3,
|
66
|
+
"A4": 440,
|
67
|
+
"B%4": 466.16,
|
68
|
+
"B4": 493.88,
|
69
|
+
"C5": 523.25,
|
70
|
+
"C#5": 554.37,
|
71
|
+
"D5": 587.33,
|
72
|
+
"E%5": 622.25,
|
73
|
+
"E5": 659.25,
|
74
|
+
"F5": 698.46,
|
75
|
+
"F#5": 739.99,
|
76
|
+
"G5": 783.99,
|
77
|
+
"A%5": 830.61,
|
78
|
+
"A5": 880,
|
79
|
+
"B%5": 932.33,
|
80
|
+
"B5": 987.77,
|
81
|
+
"C6": 1046.5,
|
82
|
+
"C#6": 1108.73,
|
83
|
+
"D6": 1174.66,
|
84
|
+
"E%6": 1244.51,
|
85
|
+
"E6": 1318.51,
|
86
|
+
"F6": 1396.91,
|
87
|
+
"F#6": 1479.98,
|
88
|
+
"G6": 1567.98,
|
89
|
+
"A%6": 1661.22,
|
90
|
+
"A6": 1760,
|
91
|
+
"B%6": 1864.66,
|
92
|
+
"B6": 1975.53,
|
93
|
+
"C7": 2093,
|
94
|
+
"C#7": 2217.46,
|
95
|
+
"D7": 2349.32,
|
96
|
+
"E%7": 2489.02,
|
97
|
+
"E7": 2637.02,
|
98
|
+
"F7": 2793.83,
|
99
|
+
"F#7": 2959.96,
|
100
|
+
"G7": 3135.96,
|
101
|
+
"A%7": 3322.44,
|
102
|
+
"A7": 3520,
|
103
|
+
"B%7": 3729.31,
|
104
|
+
"B7": 3951.07,
|
105
|
+
"C8": 4186.01,
|
106
|
+
"C#8": 4434.92,
|
107
|
+
"D8": 4698.63,
|
108
|
+
"E%8": 4978.03,
|
109
|
+
"E8": 5274.04,
|
110
|
+
"F8": 5587.65,
|
111
|
+
"F#8": 5919.91,
|
112
|
+
"G8": 6271.93,
|
113
|
+
"A%8": 6644.88,
|
114
|
+
"A8": 7040,
|
115
|
+
"B%8": 7458.62,
|
116
|
+
"B8": 7902.13
|
117
|
+
}
|
118
|
+
|
119
|
+
def initialize(args=ARGV)
|
120
|
+
@pids = []
|
121
|
+
@duration = :infinite
|
122
|
+
@options = {duration: @duration}
|
123
|
+
@optionsparser = OptionParser.new do |opts|
|
124
|
+
opts.banner = "Usage: #{opts.program_name} [options] <notes>"
|
125
|
+
opts.on_tail("\nAUTHORS", " asciodev")
|
126
|
+
opts.on("\nPlaynote is a utility for ergonomically playing tones from the CLI.")
|
127
|
+
opts.on("\nEach note is a string taking the form <tone>[semitone][octave], with")
|
128
|
+
opts.on("sharps denoted by `#`, and flats denoted by `%`. Both semitones and octaves")
|
129
|
+
opts.on("are optional; by default, notes are neither flat nor sharp, and each inherits")
|
130
|
+
opts.on("the octave of the previous note if its octave is unspecified. (If no notes")
|
131
|
+
opts.on("specify an octave, the octave for all notes is 0.) There is no delimiter character.")
|
132
|
+
opts.on("")
|
133
|
+
opts.on("-q", "--quiet", FalseClass, "Suppresses all terminal output from command") { @options[:quiet] = @quiet = true }
|
134
|
+
opts.on("-p", "--print-only", FalseClass, "Prints the given notes with their frequencies,", "without audio playback (overrides -q)") { @options[:print_only] = @print_only = true }
|
135
|
+
opts.on("-d", "--duration [DURATION]", Float, "Time in seconds to play the given notes") { @options[:duration] = @duration = it }
|
136
|
+
opts.on("-h", "--help", "Prints this message and exits") { @options[:help] = true }
|
137
|
+
opts.on("-v", "--[no-]verbose", FalseClass, "Run verbosely (overrides -q and -z)") { @options[:verbose] = @verbose = true }
|
138
|
+
opts.on("-V", "--version", "Prints the currently running version of #{opts.program_name}", "and exits") { @options[:version] = @version = true }
|
139
|
+
opts.on(
|
140
|
+
"\nEXAMPLES",
|
141
|
+
sprintf(" %-#{opts.summary_width}s %s", "#{opts.program_name} c3eg", "Play a C Major Chord in the 3rd Octave"),
|
142
|
+
sprintf("\n %-#{opts.summary_width}s %s", "#{opts.program_name} c3e%4g", "Play a C Minor Chord, with C in 3rd Octave,"),
|
143
|
+
sprintf(" %-#{opts.summary_width}s %s", "", "and E♭ and G in the 4th"),
|
144
|
+
sprintf("\n %-#{opts.summary_width}s %s", "#{opts.program_name} g#5", "Play a G♯ Note in the 5th Octave"),
|
145
|
+
sprintf("\n %-#{opts.summary_width}s %s", "#{opts.program_name} -p f2ac", "Print the notes comprising an F Major chord"),
|
146
|
+
sprintf(" %-#{opts.summary_width}s %s", "", "along with their frequencies, and exit"),
|
147
|
+
sprintf(" %-#{opts.summary_width}s %s", "", "without audio playback"),
|
148
|
+
)
|
149
|
+
@raw_notes = opts.permute(args)
|
150
|
+
end
|
151
|
+
version! if @options[:version]
|
152
|
+
help! if @options[:help]
|
153
|
+
if(@raw_notes.is_a?(Array) && @raw_notes.first.is_a?(String))
|
154
|
+
@options[:notes] = @raw_notes.first.scan(/[a-gA-G][#%nN]?\d?/).map(&:downcase)
|
155
|
+
else
|
156
|
+
help!
|
157
|
+
end
|
158
|
+
@optionsparser.parse!
|
159
|
+
octave = 0
|
160
|
+
@notes = @options[:notes].map do
|
161
|
+
note = it.upcase
|
162
|
+
_has_octave = _octave = note[/\d$/]
|
163
|
+
octave = _octave if _has_octave
|
164
|
+
note = "#{note}#{octave}" unless _has_octave
|
165
|
+
hz = @@hz_map[note.to_sym]
|
166
|
+
if hz
|
167
|
+
Note.new(note[/^[a-gA-G]/], note[/^[a-gA-G]([#%nN])/, 1], note[/\d$/], hz, pretty_note(note), pretty_note(it))
|
168
|
+
else
|
169
|
+
alt = alternate_note(note)
|
170
|
+
hz = @@hz_map[alternate_note(note).to_sym]
|
171
|
+
Note.new(alt[/^[a-gA-G]/], alt[/^[a-gA-G]([#%nN])/, 1], alt[/\d$/], hz, pretty_note(alt), pretty_note(it))
|
172
|
+
end
|
173
|
+
end
|
174
|
+
end
|
175
|
+
|
176
|
+
def show_message
|
177
|
+
message = self.to_s
|
178
|
+
Curses::init_screen
|
179
|
+
msgwidth = message.lines.map(&:length).max
|
180
|
+
msgheight = @notes.length + 1
|
181
|
+
winwidth = msgwidth + 6
|
182
|
+
winheight = msgheight * 3
|
183
|
+
current_line = 0
|
184
|
+
win = Curses::Window.new(winheight, winwidth, (Curses::lines - winheight) / 2, (Curses::cols - winwidth) / 2)
|
185
|
+
win.box(?|, ?-)
|
186
|
+
message.lines.each do |it|
|
187
|
+
win.setpos(current_line + (winheight - msgheight) / 2, 1 + (winwidth - msgwidth) / 2)
|
188
|
+
win.addstr "#{it.strip}"
|
189
|
+
current_line += 1
|
190
|
+
end
|
191
|
+
Curses::curs_set(0)
|
192
|
+
win.refresh
|
193
|
+
Curses::cbreak
|
194
|
+
win.getch
|
195
|
+
Curses::curs_set(1)
|
196
|
+
win.close
|
197
|
+
Curses::nocbreak
|
198
|
+
end
|
199
|
+
|
200
|
+
def play
|
201
|
+
puts self if !@quiet || @print_only || @verbose
|
202
|
+
ffargs = [
|
203
|
+
"ffplay",
|
204
|
+
"-autoexit",
|
205
|
+
"-nodisp",
|
206
|
+
"-loglevel",
|
207
|
+
(@verbose ? "verbose" : "error"),
|
208
|
+
(@verbose ? nil : "-hide_banner"),
|
209
|
+
"-f", "lavfi",
|
210
|
+
"-i", ffplay_cmd_str,
|
211
|
+
[:out, :err] => (@verbose ? :out : File::NULL)
|
212
|
+
].reject(&:nil?)
|
213
|
+
puts ffargs.inspect if @verbose
|
214
|
+
return if @print_only
|
215
|
+
@pids.push spawn(*ffargs)
|
216
|
+
show_message
|
217
|
+
until @pids.empty?
|
218
|
+
pid = @pids.pop
|
219
|
+
Process.kill "TERM", pid
|
220
|
+
Process.wait pid
|
221
|
+
end
|
222
|
+
exit
|
223
|
+
end
|
224
|
+
|
225
|
+
def inspect
|
226
|
+
to_str
|
227
|
+
end
|
228
|
+
|
229
|
+
def to_str
|
230
|
+
@notes.map { sprintf "%-5s %-9s %d", it.pretty, it.original, it.frequency }.join("\n").then do
|
231
|
+
[sprintf("%-5s %-9s %s", "note", "original", "frequency"), it].join("\n")
|
232
|
+
end
|
233
|
+
end
|
234
|
+
|
235
|
+
def to_s
|
236
|
+
to_str
|
237
|
+
end
|
238
|
+
|
239
|
+
private
|
240
|
+
|
241
|
+
def version!
|
242
|
+
printf "%s v%s\n", @optionsparser.program_name, Gem.loaded_specs["playnote"].version
|
243
|
+
exit
|
244
|
+
end
|
245
|
+
|
246
|
+
def help!
|
247
|
+
puts @optionsparser
|
248
|
+
exit
|
249
|
+
end
|
250
|
+
|
251
|
+
def alternate_note(note)
|
252
|
+
return note if note.length < 2 || @@hz_map[note.to_sym]
|
253
|
+
flat = note[1] == "%"
|
254
|
+
sharp = note[1] == "#"
|
255
|
+
letter = note[0]
|
256
|
+
octave = note[2..-1].to_i
|
257
|
+
return note unless flat || sharp
|
258
|
+
step = (flat ? -1 : 0) + (sharp ? 1 : 0) + 0
|
259
|
+
scale = ("A".."G").to_a
|
260
|
+
return "#{scale[(scale.index(letter) + step) % scale.length]}#{flat ? "#" : "%"}#{octave + step}"
|
261
|
+
end
|
262
|
+
|
263
|
+
def pretty_note(note)
|
264
|
+
note.gsub("#","♯").gsub("%","♭").gsub(/[nN]/, "♮")
|
265
|
+
end
|
266
|
+
|
267
|
+
def ffplay_cmd_str
|
268
|
+
alphabet = ("a".."z").to_a
|
269
|
+
arg_count = @notes.reject{it.frequency.nil?}.length
|
270
|
+
raise "Too many notes!" if arg_count > 26
|
271
|
+
@notes.each_with_index.map {|note,n| note.frequency ? "sine=f=#{note.frequency.to_i}[#{alphabet[n]}];" : ""}.join("").then do
|
272
|
+
used_letters = alphabet[0..arg_count-1]
|
273
|
+
dirstr = @duration == :infinite ? "" : "=end=#@duration"
|
274
|
+
[it, used_letters.map{"[#{it}]"}, "amerge=inputs=#{arg_count},atrim", dirstr].join("")
|
275
|
+
end
|
276
|
+
end
|
277
|
+
|
278
|
+
end
|
279
|
+
|
280
|
+
NotePlayer.new().play if(caller.empty? || caller&.last&.match(/bin\/playnote/))
|
data/lib/playnote.rb
ADDED
data/sig/playnote.rbs
ADDED
metadata
ADDED
@@ -0,0 +1,49 @@
|
|
1
|
+
--- !ruby/object:Gem::Specification
|
2
|
+
name: playnote
|
3
|
+
version: !ruby/object:Gem::Version
|
4
|
+
version: 0.1.0
|
5
|
+
platform: ruby
|
6
|
+
authors:
|
7
|
+
- asciodev
|
8
|
+
bindir: exe
|
9
|
+
cert_chain: []
|
10
|
+
date: 1980-01-02 00:00:00.000000000 Z
|
11
|
+
dependencies: []
|
12
|
+
email:
|
13
|
+
- 81930475+asciodev@users.noreply.github.com
|
14
|
+
executables:
|
15
|
+
- playnote
|
16
|
+
extensions: []
|
17
|
+
extra_rdoc_files: []
|
18
|
+
files:
|
19
|
+
- LICENSE.md
|
20
|
+
- README.md
|
21
|
+
- Rakefile
|
22
|
+
- exe/playnote
|
23
|
+
- lib/playnote.rb
|
24
|
+
- lib/playnote/version.rb
|
25
|
+
- sig/playnote.rbs
|
26
|
+
homepage: https://github.com/asciodev/playnote
|
27
|
+
licenses:
|
28
|
+
- MIT
|
29
|
+
metadata:
|
30
|
+
homepage_uri: https://github.com/asciodev/playnote
|
31
|
+
source_code_uri: https://github.com/asciodev/playnote
|
32
|
+
rdoc_options: []
|
33
|
+
require_paths:
|
34
|
+
- lib
|
35
|
+
required_ruby_version: !ruby/object:Gem::Requirement
|
36
|
+
requirements:
|
37
|
+
- - ">="
|
38
|
+
- !ruby/object:Gem::Version
|
39
|
+
version: 3.1.0
|
40
|
+
required_rubygems_version: !ruby/object:Gem::Requirement
|
41
|
+
requirements:
|
42
|
+
- - ">="
|
43
|
+
- !ruby/object:Gem::Version
|
44
|
+
version: '0'
|
45
|
+
requirements: []
|
46
|
+
rubygems_version: 3.6.7
|
47
|
+
specification_version: 4
|
48
|
+
summary: Playnote is a utility for ergonomically playing tones from the CLI.
|
49
|
+
test_files: []
|