pitch 0.1

Sign up to get free protection for your applications and to get access to all the features.
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
@@ -0,0 +1,18 @@
1
+ *.gem
2
+ *.rbc
3
+ .bundle
4
+ .config
5
+ .yardoc
6
+ .DS_Store
7
+ Gemfile.lock
8
+ InstalledFiles
9
+ _yardoc
10
+ coverage
11
+ doc/
12
+ lib/bundler/man
13
+ pkg
14
+ rdoc
15
+ spec/reports
16
+ test/tmp
17
+ test/version_tmp
18
+ tmp
data/Gemfile ADDED
@@ -0,0 +1,3 @@
1
+ source 'https://rubygems.org'
2
+
3
+ gemspec
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
@@ -0,0 +1,9 @@
1
+ require 'bundler/gem_tasks'
2
+ Bundler.require
3
+ require 'rake/testtask'
4
+
5
+ Rake::TestTask.new do |t|
6
+ t.test_files = FileList['test/*test.rb']
7
+ end
8
+
9
+ task :default => :test
data/TODO.md ADDED
@@ -0,0 +1,4 @@
1
+ # TODO
2
+
3
+ - Add frequency.
4
+ https://kagi.com/assistant?mode=4&sub_mode=12&thread=juUmb5tS3L8rCZ7ISpG2Y6KbdbtzZYU8
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
@@ -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