pitch 0.1
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +7 -0
- data/.gitignore +18 -0
- data/Gemfile +3 -0
- data/README.md +46 -0
- data/Rakefile +9 -0
- data/TODO.md +4 -0
- data/lib/pitch.rb +120 -0
- data/pitch.gemspec +22 -0
- data/test/pitch_test.rb +63 -0
- metadata +122 -0
checksums.yaml
ADDED
@@ -0,0 +1,7 @@
|
|
1
|
+
---
|
2
|
+
SHA256:
|
3
|
+
metadata.gz: f4dcf63953fb2bbbed2b83a7572997df807789064f2a693ff04943af4d894bb6
|
4
|
+
data.tar.gz: 0b5738fc7f5f6b1c0858f80e0f181dac6948d9865b0a5a932b46c003d456e027
|
5
|
+
SHA512:
|
6
|
+
metadata.gz: 9460a4205f88d03894653d2e7b518da11022e272b7aa54d2ae932d6aea31ff4e727ba7a982eed1eb0f36311c5a6de5c7afee37ee26fe45bee108156f61826df2
|
7
|
+
data.tar.gz: ad4edec8f1fcf51ac8764628123d72651d775d08419b7a75f07b22415e367eaa0ec9f31776ff98bc768845fe5acd4fefa31081fb845ba91265dc3972c2db6cf2
|
data/.gitignore
ADDED
data/Gemfile
ADDED
data/README.md
ADDED
@@ -0,0 +1,46 @@
|
|
1
|
+
# Pitch
|
2
|
+
|
3
|
+
Pitch is a Ruby gem that provides a way to generate audio tones, writes them to files, and detects those tones. It can be used in applications that need to test whether audio data is being written properly. For example, it is used in the [HD24Dig](https://github.com/jslabovitz/hd24dig.git) gem to build a test harness for testing audio extraction from a multitrack digital recorder.
|
4
|
+
|
5
|
+
Pitches can be created from scientific pitch notation (SPN), where 'C4' is designated to be middle C. Or from MIDI notation where either the string 'C3' or the value 60 is middle C. Pitches can be converted between various forms, as well as added to or subtracted from. Finally, pitches can be written to files as audio tones, and generated by analyzing files with audio tones.
|
6
|
+
|
7
|
+
|
8
|
+
## Usage
|
9
|
+
|
10
|
+
```ruby
|
11
|
+
require 'pitch'
|
12
|
+
|
13
|
+
# make a pitch of C4 (SPN)
|
14
|
+
pitch = Pitch.new_from_spn('C4')
|
15
|
+
|
16
|
+
# make a file containing a tone with the pitch
|
17
|
+
pitch.write_to_file('tone.wav', rate: 48000, depth: 24)
|
18
|
+
|
19
|
+
# read the file to determine the pitch
|
20
|
+
pitch = Pitch.read_from_file('tone.wav')
|
21
|
+
```
|
22
|
+
|
23
|
+
|
24
|
+
## Installation
|
25
|
+
|
26
|
+
Install as a gem:
|
27
|
+
|
28
|
+
gem install pitch
|
29
|
+
|
30
|
+
...or add to your `Gemfile` or `.gemspec` file as needed.
|
31
|
+
|
32
|
+
|
33
|
+
## Requirements
|
34
|
+
|
35
|
+
This gem was written to use Ruby 3.3. It should work with versions of Ruby that are close to that.
|
36
|
+
|
37
|
+
Under the hood, Pitch uses [SoX](https://sox.sourceforge.net/) to generate tones and write audio files, and [Aubio](https://aubio.org) to analyze and detect tones from those files. You will need to install the appropriate package in order for the `write_to_file` and `read_from_file` methods to work properly. On macOS using Homebrew, that looks like this:
|
38
|
+
|
39
|
+
brew install sox
|
40
|
+
brew install aubio
|
41
|
+
|
42
|
+
|
43
|
+
## References
|
44
|
+
|
45
|
+
- [Scientific pitch notation (Wikipedia)](https://en.wikipedia.org/wiki/Scientific_pitch_notation)
|
46
|
+
- [MIDI Note/Key Number Chart](https://computermusicresource.com/midikeys.html)
|
data/Rakefile
ADDED
data/TODO.md
ADDED
data/lib/pitch.rb
ADDED
@@ -0,0 +1,120 @@
|
|
1
|
+
require 'tty-command'
|
2
|
+
|
3
|
+
class Pitch
|
4
|
+
|
5
|
+
attr_accessor :semitone
|
6
|
+
attr_accessor :octave
|
7
|
+
|
8
|
+
SEMITONE_NAMES = %w[C C# D D# E F F# G G# A A# B]
|
9
|
+
NUM_SEMITONES = SEMITONE_NAMES.count
|
10
|
+
|
11
|
+
def self.read_from_file(file)
|
12
|
+
out, _ = run_command(
|
13
|
+
'aubiopitch',
|
14
|
+
'--samplerate', 48000, # doesn't work with higher sample rates?
|
15
|
+
'--input', file,
|
16
|
+
'--pitch-unit', 'midi',
|
17
|
+
)
|
18
|
+
values = out.split(/\n/).map { |line| line.split(/\s+/).last.to_f }.reject(&:zero?)
|
19
|
+
raise "Couldn't detect pitches" if values.empty?
|
20
|
+
value = (values.sum / values.count).round
|
21
|
+
new_from_midi_num(value)
|
22
|
+
end
|
23
|
+
|
24
|
+
def self.run_command(*args)
|
25
|
+
TTY::Command.new(printer: :quiet).run(
|
26
|
+
*args.flatten.compact.map(&:to_s),
|
27
|
+
only_output_on_error: true)
|
28
|
+
end
|
29
|
+
|
30
|
+
def self.new_from_midi(arg)
|
31
|
+
case arg
|
32
|
+
when Numeric
|
33
|
+
new_from_midi_num(arg)
|
34
|
+
when String
|
35
|
+
new_from_midi_name(arg)
|
36
|
+
else
|
37
|
+
raise ArgumentError
|
38
|
+
end
|
39
|
+
end
|
40
|
+
|
41
|
+
def self.new_from_midi_num(num)
|
42
|
+
semitone, octave = SEMITONE_NAMES[num % NUM_SEMITONES], (num / NUM_SEMITONES)
|
43
|
+
new(semitone, octave - 1)
|
44
|
+
end
|
45
|
+
|
46
|
+
def self.new_from_midi_name(name)
|
47
|
+
semitone, octave = parse_name(name)
|
48
|
+
new(semitone, octave + 1)
|
49
|
+
end
|
50
|
+
|
51
|
+
def self.new_from_spn(spn)
|
52
|
+
semitone, octave = parse_name(spn)
|
53
|
+
new(semitone, octave)
|
54
|
+
end
|
55
|
+
|
56
|
+
def self.parse_name(name)
|
57
|
+
semitone_name, octave = name.match(/^(.*?)(-?\d+)$/).captures
|
58
|
+
semitone = SEMITONE_NAMES.index(semitone_name)
|
59
|
+
octave = octave.to_i
|
60
|
+
[semitone, octave]
|
61
|
+
end
|
62
|
+
|
63
|
+
def initialize(semitone, octave)
|
64
|
+
@semitone = case semitone
|
65
|
+
when Numeric
|
66
|
+
semitone
|
67
|
+
when String
|
68
|
+
SEMITONE_NAMES.index(semitone)
|
69
|
+
else
|
70
|
+
raise ArgumentError
|
71
|
+
end
|
72
|
+
@octave = octave
|
73
|
+
end
|
74
|
+
|
75
|
+
def ==(other)
|
76
|
+
other &&
|
77
|
+
@octave == other.octave &&
|
78
|
+
@semitone == other.semitone
|
79
|
+
end
|
80
|
+
|
81
|
+
def to_midi_name
|
82
|
+
to_s(offset: -1)
|
83
|
+
end
|
84
|
+
|
85
|
+
def to_midi_num
|
86
|
+
((@octave + 1) * NUM_SEMITONES) + @semitone
|
87
|
+
end
|
88
|
+
|
89
|
+
def to_spn
|
90
|
+
to_s
|
91
|
+
end
|
92
|
+
|
93
|
+
def to_s(offset: 0)
|
94
|
+
'%s%d' % [
|
95
|
+
SEMITONE_NAMES[@semitone],
|
96
|
+
@octave + offset,
|
97
|
+
]
|
98
|
+
end
|
99
|
+
|
100
|
+
def +(n)
|
101
|
+
self.class.new_from_midi_num(to_midi_num + n)
|
102
|
+
end
|
103
|
+
|
104
|
+
def -(n)
|
105
|
+
self + (-n)
|
106
|
+
end
|
107
|
+
|
108
|
+
def write_to_file(file, duration: 5, rate: 48000, depth: 24)
|
109
|
+
self.class.run_command(
|
110
|
+
'sox',
|
111
|
+
'--null',
|
112
|
+
'--rate', rate,
|
113
|
+
'--bits', depth,
|
114
|
+
file,
|
115
|
+
'synth', duration, 'pluck', to_spn,
|
116
|
+
)
|
117
|
+
file
|
118
|
+
end
|
119
|
+
|
120
|
+
end
|
data/pitch.gemspec
ADDED
@@ -0,0 +1,22 @@
|
|
1
|
+
Gem::Specification.new do |gem|
|
2
|
+
gem.name = 'pitch'
|
3
|
+
gem.version = '0.1'
|
4
|
+
gem.authors = 'John Labovitz'
|
5
|
+
gem.email = 'johnl@johnlabovitz.com'
|
6
|
+
gem.summary = %q{Create and analyze musical pitches from MIDI names/numbers and scientific pitch notation (SPN)}
|
7
|
+
gem.description = %q{Create and analyze musical pitches from MIDI names/numbers and scientific pitch notation (SPN)..}
|
8
|
+
gem.homepage = 'https://github.com/jslabovitz/pitch.git'
|
9
|
+
|
10
|
+
gem.files = `git ls-files`.split($/)
|
11
|
+
gem.executables = gem.files.grep(%r{^bin/}).map{ |f| File.basename(f) }
|
12
|
+
gem.test_files = gem.files.grep(%r{^(test|spec|features)/})
|
13
|
+
gem.require_paths = ['lib']
|
14
|
+
|
15
|
+
gem.add_dependency 'tty-command', '~> 0.10'
|
16
|
+
|
17
|
+
gem.add_development_dependency 'bundler', '~> 2.5'
|
18
|
+
gem.add_development_dependency 'minitest', '~> 5.23'
|
19
|
+
gem.add_development_dependency 'minitest-power_assert', '~> 0.3'
|
20
|
+
gem.add_development_dependency 'rake', '~> 13.2'
|
21
|
+
|
22
|
+
end
|
data/test/pitch_test.rb
ADDED
@@ -0,0 +1,63 @@
|
|
1
|
+
$VERBOSE = false
|
2
|
+
|
3
|
+
require 'minitest/autorun'
|
4
|
+
require 'minitest/power_assert'
|
5
|
+
|
6
|
+
require_relative '../lib/pitch'
|
7
|
+
|
8
|
+
class Pitch
|
9
|
+
|
10
|
+
class Test < Minitest::Test
|
11
|
+
|
12
|
+
def test_spn
|
13
|
+
pitch = Pitch.new_from_spn('C4')
|
14
|
+
assert { pitch.semitone == 0 }
|
15
|
+
assert { pitch.octave == 4 }
|
16
|
+
assert { pitch.to_spn == 'C4' }
|
17
|
+
end
|
18
|
+
|
19
|
+
def test_midi_num
|
20
|
+
pitch = Pitch.new_from_midi(60)
|
21
|
+
assert { pitch.semitone == 0 }
|
22
|
+
assert { pitch.octave == 4 }
|
23
|
+
assert { pitch.to_midi_num == 60 }
|
24
|
+
assert { pitch.to_midi_name == 'C3' }
|
25
|
+
assert { pitch.to_spn == 'C4' }
|
26
|
+
end
|
27
|
+
|
28
|
+
def test_midi_name
|
29
|
+
pitch = Pitch.new_from_midi('C3')
|
30
|
+
assert { pitch.semitone == 0 }
|
31
|
+
assert { pitch.octave == 4 }
|
32
|
+
assert { pitch.to_midi_num == 60 }
|
33
|
+
assert { pitch.to_spn == 'C4' }
|
34
|
+
end
|
35
|
+
|
36
|
+
def test_increment
|
37
|
+
pitch = Pitch.new_from_spn('C4')
|
38
|
+
pitch += 1
|
39
|
+
assert { pitch.to_spn == 'C#4' }
|
40
|
+
pitch -= 12
|
41
|
+
assert { pitch.to_spn == 'C#3' }
|
42
|
+
end
|
43
|
+
|
44
|
+
def test_equals
|
45
|
+
pitch1 = Pitch.new_from_spn('C4')
|
46
|
+
pitch2 = Pitch.new_from_midi('C3')
|
47
|
+
assert { pitch1 == pitch2 }
|
48
|
+
end
|
49
|
+
|
50
|
+
OUTPUT_FILE = 'test/tmp/output.wav'
|
51
|
+
|
52
|
+
def test_analyze
|
53
|
+
File.unlink(OUTPUT_FILE) if File.exist?(OUTPUT_FILE)
|
54
|
+
FileUtils.mkpath(File.dirname(OUTPUT_FILE))
|
55
|
+
pitch = Pitch.new_from_spn('C4')
|
56
|
+
pitch.write_to_file(OUTPUT_FILE)
|
57
|
+
pitch2 = Pitch.read_from_file(OUTPUT_FILE)
|
58
|
+
assert { pitch2 == pitch }
|
59
|
+
end
|
60
|
+
|
61
|
+
end
|
62
|
+
|
63
|
+
end
|
metadata
ADDED
@@ -0,0 +1,122 @@
|
|
1
|
+
--- !ruby/object:Gem::Specification
|
2
|
+
name: pitch
|
3
|
+
version: !ruby/object:Gem::Version
|
4
|
+
version: '0.1'
|
5
|
+
platform: ruby
|
6
|
+
authors:
|
7
|
+
- John Labovitz
|
8
|
+
autorequire:
|
9
|
+
bindir: bin
|
10
|
+
cert_chain: []
|
11
|
+
date: 2024-06-04 00:00:00.000000000 Z
|
12
|
+
dependencies:
|
13
|
+
- !ruby/object:Gem::Dependency
|
14
|
+
name: tty-command
|
15
|
+
requirement: !ruby/object:Gem::Requirement
|
16
|
+
requirements:
|
17
|
+
- - "~>"
|
18
|
+
- !ruby/object:Gem::Version
|
19
|
+
version: '0.10'
|
20
|
+
type: :runtime
|
21
|
+
prerelease: false
|
22
|
+
version_requirements: !ruby/object:Gem::Requirement
|
23
|
+
requirements:
|
24
|
+
- - "~>"
|
25
|
+
- !ruby/object:Gem::Version
|
26
|
+
version: '0.10'
|
27
|
+
- !ruby/object:Gem::Dependency
|
28
|
+
name: bundler
|
29
|
+
requirement: !ruby/object:Gem::Requirement
|
30
|
+
requirements:
|
31
|
+
- - "~>"
|
32
|
+
- !ruby/object:Gem::Version
|
33
|
+
version: '2.5'
|
34
|
+
type: :development
|
35
|
+
prerelease: false
|
36
|
+
version_requirements: !ruby/object:Gem::Requirement
|
37
|
+
requirements:
|
38
|
+
- - "~>"
|
39
|
+
- !ruby/object:Gem::Version
|
40
|
+
version: '2.5'
|
41
|
+
- !ruby/object:Gem::Dependency
|
42
|
+
name: minitest
|
43
|
+
requirement: !ruby/object:Gem::Requirement
|
44
|
+
requirements:
|
45
|
+
- - "~>"
|
46
|
+
- !ruby/object:Gem::Version
|
47
|
+
version: '5.23'
|
48
|
+
type: :development
|
49
|
+
prerelease: false
|
50
|
+
version_requirements: !ruby/object:Gem::Requirement
|
51
|
+
requirements:
|
52
|
+
- - "~>"
|
53
|
+
- !ruby/object:Gem::Version
|
54
|
+
version: '5.23'
|
55
|
+
- !ruby/object:Gem::Dependency
|
56
|
+
name: minitest-power_assert
|
57
|
+
requirement: !ruby/object:Gem::Requirement
|
58
|
+
requirements:
|
59
|
+
- - "~>"
|
60
|
+
- !ruby/object:Gem::Version
|
61
|
+
version: '0.3'
|
62
|
+
type: :development
|
63
|
+
prerelease: false
|
64
|
+
version_requirements: !ruby/object:Gem::Requirement
|
65
|
+
requirements:
|
66
|
+
- - "~>"
|
67
|
+
- !ruby/object:Gem::Version
|
68
|
+
version: '0.3'
|
69
|
+
- !ruby/object:Gem::Dependency
|
70
|
+
name: rake
|
71
|
+
requirement: !ruby/object:Gem::Requirement
|
72
|
+
requirements:
|
73
|
+
- - "~>"
|
74
|
+
- !ruby/object:Gem::Version
|
75
|
+
version: '13.2'
|
76
|
+
type: :development
|
77
|
+
prerelease: false
|
78
|
+
version_requirements: !ruby/object:Gem::Requirement
|
79
|
+
requirements:
|
80
|
+
- - "~>"
|
81
|
+
- !ruby/object:Gem::Version
|
82
|
+
version: '13.2'
|
83
|
+
description: Create and analyze musical pitches from MIDI names/numbers and scientific
|
84
|
+
pitch notation (SPN)..
|
85
|
+
email: johnl@johnlabovitz.com
|
86
|
+
executables: []
|
87
|
+
extensions: []
|
88
|
+
extra_rdoc_files: []
|
89
|
+
files:
|
90
|
+
- ".gitignore"
|
91
|
+
- Gemfile
|
92
|
+
- README.md
|
93
|
+
- Rakefile
|
94
|
+
- TODO.md
|
95
|
+
- lib/pitch.rb
|
96
|
+
- pitch.gemspec
|
97
|
+
- test/pitch_test.rb
|
98
|
+
homepage: https://github.com/jslabovitz/pitch.git
|
99
|
+
licenses: []
|
100
|
+
metadata: {}
|
101
|
+
post_install_message:
|
102
|
+
rdoc_options: []
|
103
|
+
require_paths:
|
104
|
+
- lib
|
105
|
+
required_ruby_version: !ruby/object:Gem::Requirement
|
106
|
+
requirements:
|
107
|
+
- - ">="
|
108
|
+
- !ruby/object:Gem::Version
|
109
|
+
version: '0'
|
110
|
+
required_rubygems_version: !ruby/object:Gem::Requirement
|
111
|
+
requirements:
|
112
|
+
- - ">="
|
113
|
+
- !ruby/object:Gem::Version
|
114
|
+
version: '0'
|
115
|
+
requirements: []
|
116
|
+
rubygems_version: 3.5.11
|
117
|
+
signing_key:
|
118
|
+
specification_version: 4
|
119
|
+
summary: Create and analyze musical pitches from MIDI names/numbers and scientific
|
120
|
+
pitch notation (SPN)
|
121
|
+
test_files:
|
122
|
+
- test/pitch_test.rb
|