music_set_theory 0.0.2

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 ADDED
@@ -0,0 +1,7 @@
1
+ ---
2
+ SHA256:
3
+ metadata.gz: 2a54615c8b9ddf6ecabe27b78c6d84d76fb9c618fb3120267eadd21ed67e7267
4
+ data.tar.gz: 9d56b5499b87e6eed6e6c36cf3d396885b6cfb28340f855e79ae8ce2e8fecc0c
5
+ SHA512:
6
+ metadata.gz: ffabf6585aade8f22055f93594e9b0f265f4da8fc110917a0460072abb67b2b82953a72c4d31dab145ea9bccdf91bb51743d26f371a1e112f41a201c82b62831
7
+ data.tar.gz: d5e6d8ed0e69e4e9a865cb233ff96efe92ba48690ae5ac3a7a087ec03e6a8b77e4d3d57e46d96a43528fce92985b6111cba4c86252f0f0dab033591f8a63d45f
data/.standard.yml ADDED
@@ -0,0 +1,3 @@
1
+ # For available configuration options, see:
2
+ # https://github.com/standardrb/standard
3
+ ruby_version: 3.1
data/CHANGELOG.md ADDED
@@ -0,0 +1,5 @@
1
+ ## [Unreleased]
2
+
3
+ ## [0.1.0] - 2025-07-13
4
+
5
+ - Initial release
data/LICENSE.txt ADDED
@@ -0,0 +1,21 @@
1
+ The MIT License (MIT)
2
+
3
+ Copyright (c) 2025 YAMAMOTO, Masayuki (https://voidptrjp.blogspot.com)
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in
13
+ all copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
21
+ THE SOFTWARE.
data/README.md ADDED
@@ -0,0 +1,104 @@
1
+ # MusicSetTheory
2
+
3
+ `music_set_theory.gem` is ruby version of `musictheory` of python package, by Peter Murphy.
4
+ > The package has roughly the same philosophy as [Music Set Theory](https://www.jaytomlin.com/music/settheory/help.html) but uses different terminology.
5
+
6
+ The music theory treats notes and the relationships among them, i.e., temperament, chords, scale, modes, etc.
7
+
8
+
9
+
10
+ ## Installation
11
+
12
+ TODO: Replace `UPDATE_WITH_YOUR_GEM_NAME_IMMEDIATELY_AFTER_RELEASE_TO_RUBYGEMS_ORG` with your gem name right after releasing it to RubyGems.org. Please do not do it earlier due to security reasons. Alternatively, replace this section with instructions to install your gem from git if you don't plan to release to RubyGems.org.
13
+
14
+ Install the gem and add to the application's Gemfile by executing:
15
+
16
+ ```bash
17
+ bundle add UPDATE_WITH_YOUR_GEM_NAME_IMMEDIATELY_AFTER_RELEASE_TO_RUBYGEMS_ORG
18
+ ```
19
+
20
+ If bundler is not being used to manage dependencies, install the gem by executing:
21
+
22
+ ```bash
23
+ gem install UPDATE_WITH_YOUR_GEM_NAME_IMMEDIATELY_AFTER_RELEASE_TO_RUBYGEMS_ORG
24
+ ```
25
+
26
+ ## Usage
27
+
28
+ See the `examples/` directory. The scripts for listing chords and scales are there.
29
+
30
+
31
+ ## Development
32
+
33
+ After checking out the repo, run `bin/setup` to install dependencies. Then, run `rake test` to run the tests. You can also run `bin/console` for an interactive prompt that will allow you to experiment.
34
+
35
+ To install this gem onto your local machine, run `bundle exec rake install`.
36
+ To release a new version, update the version number in `version.rb`, and then run `bundle exec rake release`, which will create a git tag for the version, push git commits and the created tag, and push the `.gem` file to [rubygems.org](https://rubygems.org).
37
+
38
+ ### Testing
39
+
40
+ If you want to test all:
41
+
42
+ ```bash
43
+ bundle exec rake test
44
+ ```
45
+
46
+ test a specified one:
47
+
48
+ ```bash
49
+ TEST=./test/something_test.rb bundle exec rake test
50
+ ```
51
+
52
+ If you want to test a test in a specified file, then:
53
+
54
+ ```bash
55
+ TESTOPTS=--name=/pattern-matches-to-the-description/ TEST=./test/something_test.rb bundle exec rake test
56
+ ```
57
+
58
+ ## Future Works, etc.
59
+
60
+ I would like to add some examples, debug more, and refactor the structure and method names...
61
+ (Actually, I don't have the knowledge about temperament and modes at (almost) all, though I
62
+ can understand chords and scales a little since I had played the guitar.)
63
+
64
+ The state-dependencies exists in the original test cases on `WestTemp`. So I have already removed them.
65
+ All tests, including commented out one (`TestChords`), are passed now.
66
+
67
+ ## Contributing
68
+
69
+ Bug reports and pull requests are welcome on GitHub at `https://github.com/mephistobooks/music_set_theory`.
70
+
71
+
72
+ ## License
73
+
74
+ The gem is available as open source under the terms of the [MIT License](https://opensource.org/licenses/MIT).
75
+
76
+ The original [`musictheory`](https://github.com/peterkmurphy/musictheory) is BSD 3-clauses Licence.
77
+
78
+ ```
79
+ Copyright (c) 2009-2020 Peter Murphy <peterkmurphy@gmail.com>
80
+ All rights reserved.
81
+
82
+ Redistribution and use in source and binary forms, with or without
83
+ modification, are permitted provided that the following conditions are met:
84
+ * Redistributions of source code must retain the above copyright
85
+ notice, this list of conditions and the following disclaimer.
86
+ * Redistributions in binary form must reproduce the above copyright
87
+ notice, this list of conditions and the following disclaimer in the
88
+ documentation and/or other materials provided with the distribution.
89
+ * The names of its contributors may not be used to endorse or promote
90
+ products derived from this software without specific prior written
91
+ permission.
92
+
93
+ THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND
94
+ ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
95
+ WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
96
+ DISCLAIMED. IN NO EVENT SHALL THE CONTRIBUTORS BE LIABLE FOR ANY
97
+ DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
98
+ (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
99
+ LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND
100
+ ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
101
+ (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
102
+ SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
103
+ ```
104
+
data/Rakefile ADDED
@@ -0,0 +1,14 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "bundler/gem_tasks"
4
+ require "rake/testtask"
5
+
6
+ Rake::TestTask.new(:test) do |t|
7
+ t.libs << "test"
8
+ t.libs << "lib"
9
+ t.test_files = FileList["test/**/*_test.rb"]
10
+ end
11
+
12
+ require "standard/rake"
13
+
14
+ task default: %i[test standard]
@@ -0,0 +1,25 @@
1
+ #! /usr/bin/env ruby
2
+ #
3
+ #
4
+
5
+ require_relative "../lib/music_set_theory"
6
+
7
+
8
+ include MusicSetTheory
9
+ tmp = WestTempNew()
10
+ generate_west_chords(tmp).each_with_index{|e, i|
11
+ puts "(#{i+1}) #{e}:#{e.class}"
12
+ puts " name: #{e.name}"
13
+ puts " type: #{e.type}"
14
+ puts " tment: #{e.tment}"
15
+ puts " notes: #{e.notes} (unit: number of semitones)"
16
+ puts " #{e.notes_deg} (unit: degree, base 0)"
17
+
18
+ abbr = "abbr: #{e.abbr}"
19
+ abbr += "; #{e.abbr_others}" if e.abbr_others.size > 0
20
+ puts " #{abbr}"
21
+ puts " syns: #{e.syns}"
22
+ }
23
+
24
+
25
+ #### endof filename: examples/mt_chords.rb
@@ -0,0 +1,46 @@
1
+ #! /usr/bin/env ruby
2
+ #
3
+ # filename: examples/mt_temperament.rb
4
+ #
5
+
6
+ require_relative "../lib/music_set_theory"
7
+
8
+
9
+ include MusicSetTheory
10
+ # tmp = WestTempNew()
11
+ generate_west_chords(WestTemp)
12
+
13
+ #
14
+ puts "Temperament."
15
+ seq_dict = WestTemp.seq_dict
16
+ nseqtype_dict = seq_dict.nseqtype_maps
17
+
18
+ pad = " "*2
19
+ pad2 = pad*2
20
+ nseqtype_dict.each {|k, v|
21
+ puts pad+"dict type (nseqtype): #{NSEQ_TYPE_HASH[k]} (#{k})"
22
+
23
+ #
24
+ dict = v
25
+ puts pad2+"name_dict (#{dict.name_dict.keys.size}):"
26
+ dict.name_dict.each_with_index {|(k, v), i|
27
+ puts pad2+pad+"(#{i+1}) #{k}: #{v}"
28
+ }
29
+ puts
30
+
31
+ puts pad2+"abbr_dict (#{dict.abbr_dict.keys.size}):"
32
+ dict.abbr_dict.each_with_index {|(k, v), i|
33
+ puts pad2+pad+"(#{i+1}) #{k}: #{v}"
34
+ }
35
+ puts
36
+
37
+ puts pad2+"seqpos_dict (#{dict.seqpos_dict.keys.size}):"
38
+ dict.seqpos_dict.each_with_index {|(k, v), i|
39
+ puts pad2+pad+"(#{i+1}) #{k}: #{v}"
40
+ }
41
+
42
+ puts '-'*40
43
+ }
44
+
45
+
46
+ #### endof filename: examples/mt_temperament.rb
@@ -0,0 +1,331 @@
1
+ #
2
+ # frozen_string_literal: true
3
+ #
4
+ # filename: music_set_theory/chord_generator.rb
5
+ #
6
+ # Generating tables of chords associated with scales.
7
+
8
+
9
+ #-*- coding: UTF-8 -*-
10
+ # chordgenerator.py: Generating tables of chords associated with scales.
11
+ #
12
+ # Copyright (c) 2008-2020 Peter Murphy <peterkmurphy@gmail.com>
13
+ # All rights reserved.
14
+ #
15
+ # Redistribution and use in source and binary forms, with or without
16
+ # modification, are permitted provided that the following conditions are met:
17
+ # * Redistributions of source code must retain the above copyright
18
+ # notice, this list of conditions and the following disclaimer.
19
+ # * Redistributions in binary form must reproduce the above copyright
20
+ # notice, this list of conditions and the following disclaimer in the
21
+ # documentation and/or other materials provided with the distribution.
22
+ # * The names of its contributors may not be used to endorse or promote
23
+ # products derived from this software without specific prior written
24
+ # permission.
25
+ #
26
+ # THIS SOFTWARE IS PROVIDED BY THE CONTRIBUTORS ''AS IS'' AND ANY
27
+ # EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
28
+ # WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
29
+ # DISCLAIMED. IN NO EVENT SHALL THE CONTRIBUTORS BE LIABLE FOR ANY
30
+ # DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
31
+ # (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
32
+ # LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND
33
+ # ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
34
+ # (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
35
+ # SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
36
+ #
37
+ # This software was originally written by Peter Murphy (2008). It has been
38
+ # updated in 2011 to use the assoicated musictheory classes. Updated for 2011.
39
+ #
40
+ # The goal of this utility is to find all the possible chords (major, minor,
41
+ # 7th) in common diatonic scales (such as Major and Harmonic Minor). The
42
+ # results are tables that can be displayed in an HTML file.
43
+
44
+
45
+ require_relative "./musutility"
46
+ require_relative "./temperament"
47
+ require_relative "./scales"
48
+ require_relative "./chords"
49
+
50
+ require 'romannumerals'
51
+
52
+
53
+ # import copy, codecs
54
+ # from .musutility import seqtostr;
55
+ # from .temperament import WestTemp, temperament, WestTemp, seq_dict,\
56
+ # NSEQ_SCALE, NSEQ_CHORD, M_SHARP, M_FLAT;
57
+ # from .scales import MajorScale, MelMinorScale, HarmMinorScale,\
58
+ # HarmMajorScale;
59
+ # from .chords import CHORDTYPE_DICT;
60
+
61
+
62
+ #
63
+ #
64
+ #
65
+ module MusicSetTheory
66
+
67
+ class ScaleChords; end
68
+ class ScaleChordRow; end
69
+ class ScaleChordCell; end
70
+
71
+ end
72
+
73
+
74
+ #
75
+ #
76
+ #
77
+ module MusicSetTheory
78
+
79
+ # Used to give a full list of chord types.
80
+ CHORDTYPE_ARRAY = [
81
+ "Fifth", "Triad", "Seventh", "Ninth", "Eleventh", "Thirteenth",
82
+ "Added Ninth", "Suspended", "Suspended Seventh", "Sixth", "Sixth/Ninth",
83
+ "Added Eleventh",
84
+ ]
85
+
86
+ #
87
+ PRINT_ABBRV = 0
88
+ PRINT_FNAME = 1
89
+ PRINT_BOTH = 2
90
+
91
+ end
92
+
93
+
94
+ module MusicSetTheory
95
+
96
+ # Roman numerals are used for the table headers.
97
+
98
+ # Following routine comes from "Roman Numerals (Python recipe)".
99
+ # Author: Paul Winkler on Sun, 14 Oct 2001. See:
100
+ # http://code.activestate.com/recipes/81611-roman-numerals/
101
+
102
+ # """
103
+ # Convert an integer to Roman numerals.
104
+ #
105
+ # Examples:
106
+ # >>> int_to_roman(0)
107
+ # Traceback (most recent call last):
108
+ # ValueError: Argument must be between 1 and 3999
109
+ #
110
+ # >>> int_to_roman(-1)
111
+ # Traceback (most recent call last):
112
+ # ValueError: Argument must be between 1 and 3999
113
+ #
114
+ # >>> int_to_roman(1.5)
115
+ # Traceback (most recent call last):
116
+ # TypeError: expected integer, got <type 'float'>
117
+ #
118
+ # >>> for i in range(1, 21): print int_to_roman(i)
119
+ # ...
120
+ # I
121
+ # II
122
+ # III
123
+ # IV
124
+ # V
125
+ # VI
126
+ # VII
127
+ # VIII
128
+ # IX
129
+ # X
130
+ # XI
131
+ # XII
132
+ # XIII
133
+ # XIV
134
+ # XV
135
+ # XVI
136
+ # XVII
137
+ # XVIII
138
+ # XIX
139
+ # XX
140
+ # >>> print int_to_roman(2000)
141
+ # MM
142
+ # >>> print int_to_roman(1999)
143
+ # MCMXCIX
144
+ # """
145
+ def int_to_roman(input)
146
+ raise "#{input} must be greater than zero." if input <= 0
147
+
148
+ input.to_roman
149
+ end
150
+
151
+ #
152
+ # Make a sequence of roman numerals from 1 to num.
153
+ def make_roman_numeral_list( num )
154
+ #return([int_to_roman(i) for i in range(1, num + 1)])
155
+ (1...(num + 1)).map{|i| int_to_roman(i) }
156
+ end
157
+
158
+ end
159
+
160
+
161
+ module MusicSetTheory
162
+ # Represents all the chords associated with a scale. This is
163
+ # used to make a tabular representation.
164
+ #
165
+ class ScaleChords
166
+
167
+ attr_accessor :full_name, :key, :full_notes, :table_title, :rows
168
+ def initialize( full_name, key, full_notes, table_title )
169
+ self.full_name = full_name;
170
+ self.key = key;
171
+ self.full_notes = full_notes;
172
+ self.table_title = table_title;
173
+ self.rows = [];
174
+ end
175
+ end
176
+
177
+ # Represents different rows (triads, 7ths, 9ths) in tabular
178
+ # representations of scale chords.
179
+ #
180
+ class ScaleChordRow
181
+ attr_accessor :chord_type, :notes
182
+ def initialize( chord_type )
183
+ self.chord_type = chord_type;
184
+ self.notes = [];
185
+ end
186
+ end
187
+
188
+ # Represents different cells in the table of chords.
189
+ class ScaleChordCell
190
+ attr_accessor :chordname_1, :chordname_2, :notes
191
+ def initialize( chordname_1, chordname_2, notes )
192
+ self.chordname_1 = chordname_1;
193
+ self.chordname_2 = chordname_2;
194
+ self.notes = notes;
195
+ end
196
+ end
197
+
198
+ end
199
+
200
+
201
+ module MusicSetTheory
202
+
203
+ # Used for converting "C" -> 1, "Db"-> 2b, etc.
204
+ def makebaserep( notex, base = 0 )
205
+ notexparsed = WestTemp.note_parse(notex)
206
+ #pos_rep = str(WestTemp.nat_key_lookup_order[notexparsed[0]] + base)
207
+ pos_rep = (WestTemp.nat_key_lookup_order[notexparsed[0]] + base).to_s
208
+
209
+ if notexparsed[1] > 0
210
+ ret = pos_rep + (M_SHARP * notexparsed[1])
211
+ elsif notexparsed[1] < 0
212
+ ret = pos_rep + (M_FLAT * (-1 * notexparsed[1]))
213
+ else
214
+ ret = pos_rep;
215
+ end
216
+
217
+ ret
218
+ end
219
+
220
+
221
+ # Returns an instance of scale_chords using the following inputs:
222
+ #
223
+ # scale_name: a name of a scale like "Dorian".
224
+ # key: generally a standard music key like "C".
225
+ # possiblechords: a sequence of chord types like "Seventh" and "Ninth".
226
+ #
227
+ def populate_scale_chords( scale_name, key, possiblechords )
228
+ our_scale = WestTemp.get_nseqby_name(scale_name, NSEQ_SCALE);
229
+ num_elem = len(our_scale.nseq_posn);
230
+ begin
231
+ int_of_key = key.to_i
232
+ is_key_an_int = true;
233
+ rescue ValueError
234
+ is_key_an_int = false;
235
+ int_of_key = nil;
236
+ end
237
+
238
+ if is_key_an_int
239
+ #our_scale_notes = [makebaserep(x, int_of_key) for x in
240
+ #our_scale.get_notes_for_key("C")];
241
+ our_scale_notes = our_scale.get_notes_for_key("C").map{|x|
242
+ makebaserep(x, int_of_key) }
243
+ else
244
+ our_scale_notes = our_scale.get_notes_for_key(key)
245
+ end
246
+
247
+ our_chord_data = ScaleChords.new(scale_name, key, our_scale_notes,
248
+ make_roman_numeral_list(num_elem))
249
+
250
+ for i in possiblechords
251
+ ourchordrow = ScaleChordRow.new(i)
252
+ our_chord_data.rows.append(ourchordrow)
253
+
254
+ for j in range(num_elem):
255
+ our_slice = CHORDTYPE_DICT[i];
256
+ if is_key_an_int
257
+ #our_chord_notes = [makebaserep(x, int_of_key) for x in
258
+ # our_scale.get_notes_for_key("C", j, our_slice)];
259
+ our_chord_notes = our_scale.get_notes_for_key("C", j, our_slice).
260
+ map{|x| makebaserep(x, int_of_key) }
261
+ else
262
+ our_chord_notes = our_scale.get_notes_for_key(key, j, our_slice)
263
+ end
264
+
265
+ our_posn = our_scale.get_posn_for_offset(j, our_slice, true)
266
+ our_chord = WestTemp.get_nseqby_seqpos(our_posn, NSEQ_CHORD)
267
+ if our_chord
268
+ ourchordrow.notes.append(ScaleChordCell.new(our_chord.nseq_name,
269
+ our_chord.nseq_abbrev, our_chord_notes))
270
+ else
271
+ ourchordrow.notes.append(ScaleChordCell.new("", "", our_chord_notes))
272
+ end
273
+
274
+ end
275
+ end
276
+
277
+ return our_chord_data
278
+ end
279
+
280
+
281
+ # (**) I dont need this.
282
+ # Generates a table representation of a series of scales,
283
+ # represented by a scale_chords instance.
284
+ #
285
+ def chordgentable( scales )
286
+ startrow = "<tr>\n"
287
+ endrow = "</tr>\n"
288
+ thestring = ""
289
+
290
+
291
+ for scale in scales
292
+ thestring += "<h2>%s</h2>\n" % (scale.key + " " +scale.full_name)
293
+ thestring += ("<table id=\"%s\" class=\"chordtable\">\n" %
294
+ (scale.key + ""))
295
+ thestring += "<caption>%s</caption>\n" %
296
+ (scale.key + " " + scale.full_name +
297
+ ": " + seqtostr(scale.full_notes))
298
+ thestring += "<thead>\n" + startrow + "<th>Chord Types</th>\n";
299
+ for q in range(7):
300
+ # thestring += "<th>%s</th>\n" % str(int_to_roman(q+1));
301
+ end
302
+ thestring += endrow + "</thead>\n<tbody>\n";
303
+
304
+ for i in scale.rows:
305
+ thestring += startrow;
306
+ thestring += "<td>%s</td>\n" % i.chord_type;
307
+ for j in i.notes:
308
+ if not j.chordname_1
309
+ thestring += ("<td><p>%s<br />" % (str(j.chordname_1)))
310
+ thestring += ("<i>%s</i><br />" % (str(j.chordname_2)))
311
+ else
312
+ thestring += ("<td><p>%s<br />" %
313
+ (j.notes[0]+" "+str(j.chordname_1)))
314
+ thestring += ("<i>%s</i><br />" % (j.notes[0]+str(j.chordname_2)))
315
+ end
316
+
317
+ thestring += ("<b>%s</b></p></td>" % seqtostr(j.notes))
318
+ end
319
+ thestring += endrow
320
+ end
321
+ thestring += "\n</tbody>\n</table>\n"
322
+ end
323
+
324
+ return thestring
325
+ end
326
+
327
+
328
+ end
329
+
330
+
331
+ #### endof filename: music_set_theory/chord_generator.rb