stringyfi 0.0.1 → 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 +5 -5
- data/.github/workflows/ruby-tests.yml +32 -0
- data/.rubocop.yml +53 -0
- data/.ruby-version +1 -1
- data/Gemfile +11 -1
- data/Guardfile +8 -13
- data/README.md +4 -4
- data/Rakefile +7 -6
- data/bin/stringyfi +2 -0
- data/lib/stringyfi/converter.rb +42 -40
- data/lib/stringyfi/measures.rb +5 -3
- data/lib/stringyfi/note.rb +25 -23
- data/lib/stringyfi/shell.rb +5 -3
- data/lib/stringyfi/version.rb +3 -1
- data/lib/stringyfi.rb +2 -0
- data/spec/spec_helper.rb +3 -2
- data/spec/support/fixtures_helper.rb +4 -3
- data/spec/unit/converter_spec.rb +31 -30
- data/spec/unit/measures_spec.rb +13 -13
- data/spec/unit/note_spec.rb +43 -43
- data/spec/unit/shell_spec.rb +6 -4
- data/spec/unit/version_spec.rb +4 -4
- data/stringyfi.gemspec +12 -20
- metadata +8 -111
- data/.rspec +0 -2
- data/.travis.yml +0 -5
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
|
-
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
2
|
+
SHA256:
|
3
|
+
metadata.gz: 2504e7e1f7968f25505c18c985b8fa910e65f2508505ae43c9df9f63e96ff8f2
|
4
|
+
data.tar.gz: f8ae2eb44c809c6510935bc0c51b7707da561b9012abad46b945e75282f815e8
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: 0f1dca73381e9bfbce3004d2da120020855bab46b740745d81c07a4ae6addd1695be10ab84ca383ffff2758e57b8b413726f62b6c4eb738c71a9a334c8226827
|
7
|
+
data.tar.gz: 78d71c3f0424a5be29291753e546e7e06271ffb05af6d30fd832fc592e69e3ab81c422c6d07afb97f1a9f6c9dd7ac9a4f02c054eeedaed25eef8da0df320f1f2
|
@@ -0,0 +1,32 @@
|
|
1
|
+
name: Ruby Tests
|
2
|
+
|
3
|
+
on:
|
4
|
+
push:
|
5
|
+
branches:
|
6
|
+
- main
|
7
|
+
pull_request:
|
8
|
+
branches:
|
9
|
+
- main
|
10
|
+
|
11
|
+
jobs:
|
12
|
+
test:
|
13
|
+
runs-on: ubuntu-latest
|
14
|
+
|
15
|
+
steps:
|
16
|
+
- name: Checkout code
|
17
|
+
uses: actions/checkout@v3
|
18
|
+
|
19
|
+
- name: Set up Ruby
|
20
|
+
uses: ruby/setup-ruby@v1
|
21
|
+
with:
|
22
|
+
ruby-version: 3.3
|
23
|
+
bundler-cache: true
|
24
|
+
|
25
|
+
- name: Install dependencies
|
26
|
+
run: bundle install
|
27
|
+
|
28
|
+
- name: Run tests
|
29
|
+
run: bundle exec rspec
|
30
|
+
|
31
|
+
- name: Run rubocop
|
32
|
+
run: bundle exec rubocop
|
data/.rubocop.yml
ADDED
@@ -0,0 +1,53 @@
|
|
1
|
+
AllCops:
|
2
|
+
NewCops: disable
|
3
|
+
SuggestExtensions: false
|
4
|
+
TargetRubyVersion: 3.0
|
5
|
+
|
6
|
+
Layout/FirstArrayElementIndentation:
|
7
|
+
EnforcedStyle: consistent
|
8
|
+
|
9
|
+
Layout/FirstHashElementIndentation:
|
10
|
+
EnforcedStyle: consistent
|
11
|
+
|
12
|
+
Layout/LineLength:
|
13
|
+
Max: 125
|
14
|
+
|
15
|
+
Metrics/AbcSize:
|
16
|
+
Max: 46
|
17
|
+
|
18
|
+
Metrics/BlockLength:
|
19
|
+
Max: 150
|
20
|
+
|
21
|
+
Metrics/ClassLength:
|
22
|
+
Max: 150
|
23
|
+
|
24
|
+
Metrics/CyclomaticComplexity:
|
25
|
+
Max: 9
|
26
|
+
|
27
|
+
Metrics/MethodLength:
|
28
|
+
Max: 30
|
29
|
+
|
30
|
+
Metrics/ParameterLists:
|
31
|
+
MaxOptionalParameters: 4
|
32
|
+
|
33
|
+
Metrics/PerceivedComplexity:
|
34
|
+
Max: 9
|
35
|
+
|
36
|
+
Style/BlockComments:
|
37
|
+
Exclude:
|
38
|
+
- 'spec/spec_helper.rb'
|
39
|
+
|
40
|
+
Style/ClassAndModuleChildren:
|
41
|
+
EnforcedStyle: compact
|
42
|
+
|
43
|
+
Style/Documentation:
|
44
|
+
Exclude:
|
45
|
+
- 'spec/**/*'
|
46
|
+
|
47
|
+
Style/SoleNestedConditional:
|
48
|
+
Exclude:
|
49
|
+
- 'lib/stringyfi/converter.rb'
|
50
|
+
|
51
|
+
Style/StderrPuts:
|
52
|
+
Exclude:
|
53
|
+
- 'lib/stringyfi/converter.rb'
|
data/.ruby-version
CHANGED
@@ -1 +1 @@
|
|
1
|
-
|
1
|
+
3.3.3
|
data/Gemfile
CHANGED
@@ -1,4 +1,14 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
1
3
|
source 'https://rubygems.org'
|
2
4
|
|
3
|
-
# Specify
|
5
|
+
# Specify gem dependencies in stringyfi.gemspec
|
4
6
|
gemspec
|
7
|
+
|
8
|
+
# development dependencies
|
9
|
+
gem 'bundler', '>= 2.2.33'
|
10
|
+
gem 'guard-rspec'
|
11
|
+
gem 'guard-rubocop'
|
12
|
+
gem 'rake'
|
13
|
+
gem 'rspec'
|
14
|
+
gem 'rubocop', require: false
|
data/Guardfile
CHANGED
@@ -1,18 +1,13 @@
|
|
1
|
-
#
|
2
|
-
# More info at https://github.com/guard/guard#readme
|
1
|
+
# frozen_string_literal: true
|
3
2
|
|
4
|
-
#
|
5
|
-
# rspec may be run, below are examples of the most common uses.
|
6
|
-
# * bundler: 'bundle exec rspec'
|
7
|
-
# * bundler binstubs: 'bin/rspec'
|
8
|
-
# * spring: 'bin/rsspec' (This will use spring if running and you have
|
9
|
-
# installed the spring binstubs per the docs)
|
10
|
-
# * zeus: 'zeus rspec' (requires the server to be started separetly)
|
11
|
-
# * 'just' rspec: 'rspec'
|
3
|
+
# More info at https://github.com/guard/guard#readme
|
12
4
|
guard :rspec, cmd: 'bundle exec rspec' do
|
13
5
|
watch(%r{^spec/.+_spec\.rb$})
|
14
|
-
watch(%r{^lib/stringyfi/(.+)\.rb$})
|
15
|
-
watch('spec/spec_helper.rb')
|
16
|
-
|
6
|
+
watch(%r{^lib/stringyfi/(.+)\.rb$}) { |m| "spec/unit/#{m[1]}_spec.rb" }
|
7
|
+
watch('spec/spec_helper.rb') { 'spec' }
|
17
8
|
end
|
18
9
|
|
10
|
+
guard :rubocop, cli: ['--display-cop-names'] do
|
11
|
+
watch(/.+\.rb$/)
|
12
|
+
watch(%r{(?:.+/)?\.rubocop(?:_todo)?\.yml$}) { |m| File.dirname(m[0]) }
|
13
|
+
end
|
data/README.md
CHANGED
@@ -2,7 +2,7 @@
|
|
2
2
|
|
3
3
|
Convert MusicXML to PIC assembler for running on the Boldport Club Stringy.
|
4
4
|
|
5
|
-
[](https://github.com/tardate/stringyfi/actions/workflows/ruby-tests.yml)
|
6
6
|
|
7
7
|
## About the Stringy
|
8
8
|
|
@@ -36,11 +36,11 @@ Add this line to your application's Gemfile:
|
|
36
36
|
|
37
37
|
And then execute:
|
38
38
|
|
39
|
-
|
39
|
+
bundle
|
40
40
|
|
41
41
|
Or install it yourself as:
|
42
42
|
|
43
|
-
|
43
|
+
gem install stringyfi
|
44
44
|
|
45
45
|
## Usage
|
46
46
|
|
@@ -49,7 +49,7 @@ and emits the assembler source on STDOUT (so it can be redirected as appropriate
|
|
49
49
|
|
50
50
|
For example:
|
51
51
|
|
52
|
-
|
52
|
+
stringyfi ./spec/fixtures/music_xml/chromatic.xml > ../CustomDemo.X/demo.tun
|
53
53
|
|
54
54
|
The
|
55
55
|
[spec/fixtures](./spec/fixtures/music_xml)
|
data/Rakefile
CHANGED
@@ -1,12 +1,13 @@
|
|
1
|
-
|
2
|
-
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require 'bundler/gem_tasks'
|
4
|
+
require 'rspec/core/rake_task'
|
3
5
|
|
4
6
|
RSpec::Core::RakeTask.new(:spec)
|
5
7
|
|
6
|
-
task :
|
8
|
+
task default: :spec
|
7
9
|
|
8
|
-
desc
|
10
|
+
desc 'Open an irb session preloaded with this library'
|
9
11
|
task :console do
|
10
|
-
sh
|
12
|
+
sh 'irb -rubygems -I lib -r stringyfi.rb'
|
11
13
|
end
|
12
|
-
|
data/bin/stringyfi
CHANGED
data/lib/stringyfi/converter.rb
CHANGED
@@ -1,11 +1,14 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
1
3
|
require 'nokogiri'
|
2
4
|
|
5
|
+
# Main controller for the conversion process
|
3
6
|
class StringyFi::Converter
|
4
|
-
|
5
7
|
INITIAL_OCTAVE = 1 # setup by the stringy firmware
|
6
8
|
OCTAVE_OFFSET = 3 # how many octaves to offset the source
|
7
9
|
|
8
|
-
attr_accessor :filename
|
10
|
+
attr_accessor :filename
|
11
|
+
attr_writer :xml_doc
|
9
12
|
|
10
13
|
def initialize(filename)
|
11
14
|
self.filename = filename
|
@@ -21,40 +24,40 @@ class StringyFi::Converter
|
|
21
24
|
puts score_body(shortest_fractional_duration)
|
22
25
|
puts score_coda
|
23
26
|
|
24
|
-
$stderr.puts
|
27
|
+
$stderr.puts '.. done.'
|
25
28
|
end
|
26
29
|
|
27
30
|
def score_preamble
|
28
|
-
|
29
|
-
;**************************************************************************
|
30
|
-
;** Title: #{title}
|
31
|
-
;** Tempo: #{tempo}
|
32
|
-
;** Encoded: #{encoding_date} with #{encoding_software}
|
33
|
-
;** Stringyfied: #{Time.now}
|
34
|
-
;**************************************************************************
|
31
|
+
<<~PREAMBLE
|
32
|
+
;**************************************************************************
|
33
|
+
;** Title: #{title}
|
34
|
+
;** Tempo: #{tempo}
|
35
|
+
;** Encoded: #{encoding_date} with #{encoding_software}
|
36
|
+
;** Stringyfied: #{Time.now}
|
37
|
+
;**************************************************************************
|
35
38
|
|
36
|
-
\ttstart DemoTune
|
37
|
-
|
39
|
+
\ttstart DemoTune
|
40
|
+
PREAMBLE
|
38
41
|
end
|
39
42
|
|
40
43
|
def score_coda
|
41
|
-
|
42
|
-
\ttrest 8
|
43
|
-
\ttstop
|
44
|
-
|
44
|
+
<<~CODA
|
45
|
+
\ttrest 8
|
46
|
+
\ttstop
|
47
|
+
CODA
|
45
48
|
end
|
46
49
|
|
47
50
|
def score_body(shortest_fractional_duration)
|
48
51
|
lines = []
|
49
52
|
current_octave = INITIAL_OCTAVE + OCTAVE_OFFSET
|
50
53
|
measures.each_with_index do |measure, measure_index|
|
51
|
-
lines << "\t; measure #{measure_index+1}"
|
54
|
+
lines << "\t; measure #{measure_index + 1}"
|
52
55
|
measure.each do |note|
|
53
56
|
unless note.rest?
|
54
57
|
if note.octave != current_octave
|
55
58
|
delta = note.octave - current_octave
|
56
|
-
sign = delta
|
57
|
-
|
59
|
+
sign = delta.positive? ? '+' : '-'
|
60
|
+
delta.abs.times do
|
58
61
|
lines << "\ttoctave #{sign}1"
|
59
62
|
end
|
60
63
|
current_octave = note.octave
|
@@ -62,15 +65,15 @@ EOS
|
|
62
65
|
end
|
63
66
|
short_repeats, medium_repeats, long_repeats, very_long_repeats = note.stringy_durations(shortest_fractional_duration)
|
64
67
|
if note.rest?
|
65
|
-
|
66
|
-
|
67
|
-
|
68
|
-
|
68
|
+
short_repeats.times { lines << "\ttrest 1" }
|
69
|
+
medium_repeats.times { lines << "\ttrest 2" }
|
70
|
+
long_repeats.times { lines << "\ttrest 3" }
|
71
|
+
very_long_repeats.times { lines << "\ttrest 4" }
|
69
72
|
else
|
70
|
-
|
71
|
-
|
72
|
-
|
73
|
-
|
73
|
+
short_repeats.times { lines << "\ttnote #{note.to_stringy(current_octave)},1,0 ; #{note.to_note_id}" }
|
74
|
+
medium_repeats.times { lines << "\ttnote #{note.to_stringy(current_octave)},2,0 ; #{note.to_note_id}" }
|
75
|
+
long_repeats.times { lines << "\ttnote #{note.to_stringy(current_octave)},3,0 ; #{note.to_note_id}" }
|
76
|
+
very_long_repeats.times { lines << "\ttnote #{note.to_stringy(current_octave)},4,0 ; #{note.to_note_id}" }
|
74
77
|
end
|
75
78
|
end
|
76
79
|
end
|
@@ -104,8 +107,9 @@ EOS
|
|
104
107
|
}
|
105
108
|
}
|
106
109
|
end
|
110
|
+
|
107
111
|
def part_list
|
108
|
-
xml_doc.xpath('//part-list/score-part').each_with_object([]) do |part,memo|
|
112
|
+
xml_doc.xpath('//part-list/score-part').each_with_object([]) do |part, memo|
|
109
113
|
h = {}
|
110
114
|
h[:id] = part.attr('id')
|
111
115
|
memo << h
|
@@ -126,16 +130,15 @@ EOS
|
|
126
130
|
part.xpath('measure').each_with_index do |part_measure, m|
|
127
131
|
measures[m] ||= []
|
128
132
|
part_measure.xpath('note').each_with_object(measures[m]) do |note, memo|
|
129
|
-
next unless note.xpath(
|
130
|
-
next unless note.xpath(
|
131
|
-
|
132
|
-
|
133
|
-
|
134
|
-
|
135
|
-
|
136
|
-
|
137
|
-
|
138
|
-
duration_type = note.xpath("type").text
|
133
|
+
next unless note.xpath('staff').text == '1'
|
134
|
+
next unless note.xpath('voice').text == '1'
|
135
|
+
|
136
|
+
pitch = note.xpath('pitch')
|
137
|
+
duration = note.xpath('duration').text.to_i
|
138
|
+
actual_notes = note.xpath('actual-notes').text.to_i
|
139
|
+
normal_notes = note.xpath('normal-notes').text.to_i
|
140
|
+
duration = duration * 1.0 * normal_notes / actual_notes if actual_notes.positive? && normal_notes.positive?
|
141
|
+
duration_type = note.xpath('type').text
|
139
142
|
memo << StringyFi::Note.new(
|
140
143
|
pitch.xpath('step').text,
|
141
144
|
pitch.xpath('octave').text,
|
@@ -160,5 +163,4 @@ EOS
|
|
160
163
|
def io_stream
|
161
164
|
File.open(filename).read
|
162
165
|
end
|
163
|
-
|
164
|
-
end
|
166
|
+
end
|
data/lib/stringyfi/measures.rb
CHANGED
@@ -1,5 +1,7 @@
|
|
1
|
-
|
1
|
+
# frozen_string_literal: true
|
2
2
|
|
3
|
+
# extend Array with methods for working with measures
|
4
|
+
class StringyFi::Measures < Array
|
3
5
|
def shortest_fractional_duration
|
4
6
|
result = 1
|
5
7
|
each do |measure|
|
@@ -11,10 +13,11 @@ class StringyFi::Measures < Array
|
|
11
13
|
end
|
12
14
|
|
13
15
|
def octave_range
|
14
|
-
lo =
|
16
|
+
lo = hi = nil
|
15
17
|
each do |measure|
|
16
18
|
measure.each do |note|
|
17
19
|
next if note.rest?
|
20
|
+
|
18
21
|
lo ||= note.octave
|
19
22
|
hi ||= note.octave
|
20
23
|
lo = note.octave if note.octave < lo
|
@@ -23,5 +26,4 @@ class StringyFi::Measures < Array
|
|
23
26
|
end
|
24
27
|
[lo, hi]
|
25
28
|
end
|
26
|
-
|
27
29
|
end
|
data/lib/stringyfi/note.rb
CHANGED
@@ -1,41 +1,44 @@
|
|
1
|
-
|
1
|
+
# frozen_string_literal: true
|
2
2
|
|
3
|
-
|
4
|
-
|
5
|
-
attr_accessor :fractional_duration
|
3
|
+
# Represents a musical note
|
4
|
+
class StringyFi::Note
|
5
|
+
attr_accessor :name, :octave, :alter, :tempo, :fractional_duration
|
6
6
|
|
7
|
-
def initialize(name, octave=4, alter=nil, duration=1, duration_type=
|
8
|
-
self.name =
|
9
|
-
self.octave =
|
10
|
-
self.alter =
|
7
|
+
def initialize(name, octave = 4, alter = nil, duration = 1, duration_type = 'quarter')
|
8
|
+
self.name = name.to_s.upcase
|
9
|
+
self.octave = octave.to_s.to_i
|
10
|
+
self.alter = begin
|
11
|
+
alter.to_i
|
12
|
+
rescue StandardError
|
13
|
+
0
|
14
|
+
end
|
11
15
|
self.fractional_duration = calculate_fractional_duration(duration, duration_type)
|
12
16
|
end
|
13
17
|
|
14
18
|
def rest?
|
15
|
-
name ==
|
19
|
+
name == ''
|
16
20
|
end
|
17
21
|
|
18
22
|
def calculate_fractional_duration(duration, duration_type)
|
19
23
|
divisor = {
|
20
|
-
|
21
|
-
|
22
|
-
|
23
|
-
|
24
|
-
|
24
|
+
'half' => 2.0,
|
25
|
+
'quarter' => 4.0,
|
26
|
+
'eighth' => 8.0,
|
27
|
+
'16th' => 16.0,
|
28
|
+
'32nd' => 32.0
|
25
29
|
}[duration_type] || 1.0
|
26
|
-
duration/divisor
|
30
|
+
duration / divisor
|
27
31
|
end
|
28
32
|
|
29
|
-
#
|
33
|
+
# return [short, medium, long, very_long] repeats for the note
|
30
34
|
# TODO: scale medium, long, very_long durations correctly
|
31
35
|
def stringy_durations(shortest_fractional_duration)
|
32
36
|
time_units = (fractional_duration / shortest_fractional_duration).to_i
|
33
|
-
|
34
|
-
when time_units >= 8
|
37
|
+
if time_units >= 8
|
35
38
|
[0, 0, 0, 1]
|
36
|
-
|
39
|
+
elsif time_units >= 4
|
37
40
|
[0, 0, 1, 0]
|
38
|
-
|
41
|
+
elsif time_units >= 2
|
39
42
|
[0, 1, 0, 0]
|
40
43
|
else
|
41
44
|
[1, 0, 0, 0]
|
@@ -53,7 +56,7 @@ class StringyFi::Note
|
|
53
56
|
'E' => 'D',
|
54
57
|
'A' => 'G',
|
55
58
|
'D' => 'C',
|
56
|
-
'G' => 'F'
|
59
|
+
'G' => 'F'
|
57
60
|
}[name]
|
58
61
|
"#{sharpy_name}#{relative_octave}S"
|
59
62
|
else
|
@@ -91,7 +94,6 @@ class StringyFi::Note
|
|
91
94
|
end
|
92
95
|
|
93
96
|
def <=>(other)
|
94
|
-
|
97
|
+
to_note_id <=> other.to_note_id
|
95
98
|
end
|
96
|
-
|
97
99
|
end
|
data/lib/stringyfi/shell.rb
CHANGED
@@ -1,8 +1,10 @@
|
|
1
|
-
|
1
|
+
# frozen_string_literal: true
|
2
2
|
|
3
|
+
# CLI support
|
4
|
+
class StringyFi::Shell
|
3
5
|
attr_accessor :converter
|
4
6
|
|
5
|
-
def initialize(args=[])
|
7
|
+
def initialize(args = [])
|
6
8
|
filename = args.first || File.join(File.dirname(__FILE__), '..', '..', 'spec', 'fixtures', 'music_xml', 'chromatic.xml')
|
7
9
|
self.converter = StringyFi::Converter.new(filename)
|
8
10
|
end
|
@@ -10,4 +12,4 @@ class StringyFi::Shell
|
|
10
12
|
def convert!
|
11
13
|
converter.convert!
|
12
14
|
end
|
13
|
-
end
|
15
|
+
end
|
data/lib/stringyfi/version.rb
CHANGED
data/lib/stringyfi.rb
CHANGED
data/spec/spec_helper.rb
CHANGED
@@ -1,9 +1,10 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
1
3
|
require 'stringyfi'
|
2
4
|
|
3
5
|
# Requires supporting files with custom matchers and macros, etc,
|
4
6
|
# in ./support/ and its subdirectories.
|
5
|
-
Dir["#{File.dirname(__FILE__)}/support/**/*.rb"].each {|f| require f}
|
6
|
-
|
7
|
+
Dir["#{File.dirname(__FILE__)}/support/**/*.rb"].each { |f| require f }
|
7
8
|
|
8
9
|
# This file was generated by the `rspec --init` command. Conventionally, all
|
9
10
|
# specs live under a `spec` directory, which RSpec adds to the `$LOAD_PATH`.
|
@@ -1,6 +1,7 @@
|
|
1
|
-
|
1
|
+
# frozen_string_literal: true
|
2
2
|
|
3
|
-
|
3
|
+
module FixturesHelper
|
4
|
+
def xml_sample_path(name = 'chromatic.xml')
|
4
5
|
File.join(xml_samples_path, name)
|
5
6
|
end
|
6
7
|
|
@@ -11,4 +12,4 @@ end
|
|
11
12
|
|
12
13
|
RSpec.configure do |conf|
|
13
14
|
conf.include FixturesHelper
|
14
|
-
end
|
15
|
+
end
|
data/spec/unit/converter_spec.rb
CHANGED
@@ -1,89 +1,91 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
1
3
|
require 'spec_helper'
|
2
4
|
|
3
5
|
describe StringyFi::Converter do
|
4
6
|
subject(:converter) { described_class.new(filename) }
|
5
7
|
|
6
|
-
context
|
8
|
+
context 'with chromatic.xml sample' do
|
7
9
|
let(:filename) { xml_sample_path }
|
8
10
|
|
9
|
-
describe
|
11
|
+
describe '#xml_doc' do
|
10
12
|
subject { converter.xml_doc }
|
11
|
-
it
|
13
|
+
it 'returns the XML doc' do
|
12
14
|
expect(subject).to be_a(Nokogiri::XML::Document)
|
13
15
|
end
|
14
16
|
end
|
15
17
|
|
16
|
-
describe
|
18
|
+
describe '#identification' do
|
17
19
|
subject(:identification) { converter.identification }
|
18
|
-
it
|
20
|
+
it 'returns expected metadata' do
|
19
21
|
expect(subject).to eql({
|
20
|
-
title:
|
22
|
+
title: 'chromatic scale',
|
21
23
|
encoding: {
|
22
|
-
date:
|
23
|
-
software:
|
24
|
+
date: '2017-10-15',
|
25
|
+
software: 'Guitar Pro 7.0.6'
|
24
26
|
}
|
25
27
|
})
|
26
28
|
end
|
27
29
|
end
|
28
30
|
|
29
|
-
describe
|
31
|
+
describe '#part_list' do
|
30
32
|
subject(:part_list) { converter.part_list }
|
31
|
-
it
|
33
|
+
it 'returns an array of parts' do
|
32
34
|
expect(part_list).to eql([
|
33
|
-
{id:
|
35
|
+
{ id: 'P1' }
|
34
36
|
])
|
35
37
|
end
|
36
38
|
end
|
37
39
|
|
38
|
-
describe
|
40
|
+
describe '#parts' do
|
39
41
|
subject(:parts) { converter.parts }
|
40
|
-
it
|
42
|
+
it 'returns parts corresponding to parts list' do
|
41
43
|
expect(parts.count).to eql(converter.part_list.count)
|
42
44
|
end
|
43
45
|
end
|
44
46
|
|
45
|
-
describe
|
47
|
+
describe '#measures' do
|
46
48
|
subject(:measures) { converter.measures }
|
47
|
-
it
|
49
|
+
it 'has the correct number of measures' do
|
48
50
|
expect(measures).to be_a(StringyFi::Measures)
|
49
51
|
expect(measures.count).to eql(7)
|
50
52
|
end
|
51
53
|
end
|
52
54
|
|
53
|
-
describe
|
55
|
+
describe '#measure' do
|
54
56
|
subject(:measure) { converter.measure(measure_id) }
|
55
|
-
context
|
57
|
+
context 'with first measure' do
|
56
58
|
let(:measure_id) { 0 }
|
57
|
-
it
|
59
|
+
it 'has the correct note info' do
|
58
60
|
expect(measure).to be_an(Array)
|
59
61
|
expect(measure.size).to eql(1)
|
60
62
|
expect(measure[0]).to be_a(StringyFi::Note)
|
61
|
-
expect(measure[0].to_str).to eql(
|
63
|
+
expect(measure[0].to_str).to eql('3:E:0')
|
62
64
|
end
|
63
65
|
end
|
64
66
|
end
|
65
67
|
|
66
|
-
describe
|
68
|
+
describe '#score_preamble' do
|
67
69
|
subject { converter.score_preamble }
|
68
|
-
it
|
70
|
+
it 'includes the expected identification' do
|
69
71
|
expect(subject).to include(';** Title: chromatic scale')
|
70
72
|
end
|
71
|
-
it
|
73
|
+
it 'includes the tstart' do
|
72
74
|
expect(subject).to include("\ttstart DemoTune")
|
73
75
|
end
|
74
76
|
end
|
75
77
|
|
76
|
-
describe
|
78
|
+
describe '#score_coda' do
|
77
79
|
subject { converter.score_coda }
|
78
|
-
it
|
80
|
+
it 'includes a rest and tstop' do
|
79
81
|
expect(subject).to include("\ttrest 8")
|
80
82
|
expect(subject).to include("\ttstop")
|
81
83
|
end
|
82
84
|
end
|
83
85
|
|
84
|
-
describe
|
85
|
-
subject { converter.score_body(1/32.0) }
|
86
|
-
it
|
86
|
+
describe '#score_body' do
|
87
|
+
subject { converter.score_body(1 / 32.0) }
|
88
|
+
it 'returns the expected conversion' do
|
87
89
|
expect(subject).to eql([
|
88
90
|
"\t; measure 1",
|
89
91
|
"\ttoctave -1",
|
@@ -150,9 +152,9 @@ describe StringyFi::Converter do
|
|
150
152
|
end
|
151
153
|
end
|
152
154
|
|
153
|
-
describe
|
155
|
+
describe '#convert!' do
|
154
156
|
subject { converter.convert! }
|
155
|
-
it
|
157
|
+
it 'calls all the necessary bits' do
|
156
158
|
expect(converter).to receive(:score_preamble)
|
157
159
|
expect(converter).to receive(:score_body)
|
158
160
|
expect(converter).to receive(:score_coda)
|
@@ -161,6 +163,5 @@ describe StringyFi::Converter do
|
|
161
163
|
subject
|
162
164
|
end
|
163
165
|
end
|
164
|
-
|
165
166
|
end
|
166
167
|
end
|
data/spec/unit/measures_spec.rb
CHANGED
@@ -1,34 +1,34 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
1
3
|
require 'spec_helper'
|
2
4
|
|
3
5
|
describe StringyFi::Measures do
|
4
|
-
|
5
6
|
let(:note_class) { StringyFi::Note }
|
6
7
|
let(:measures) { described_class.new }
|
7
8
|
|
8
9
|
before do
|
9
10
|
measures << [
|
10
|
-
note_class.new('G', 4, 1, 1,
|
11
|
-
note_class.new('G', 2, 0, 1,
|
11
|
+
note_class.new('G', 4, 1, 1, 'half'),
|
12
|
+
note_class.new('G', 2, 0, 1, 'quarter')
|
12
13
|
]
|
13
14
|
measures << [
|
14
|
-
note_class.new('G', 3, 0, 1,
|
15
|
-
note_class.new('A', 4, 0, 1,
|
16
|
-
note_class.new('G', 4, 1, 1,
|
15
|
+
note_class.new('G', 3, 0, 1, 'whole'),
|
16
|
+
note_class.new('A', 4, 0, 1, '32nd'),
|
17
|
+
note_class.new('G', 4, 1, 1, 'eighth')
|
17
18
|
]
|
18
19
|
end
|
19
20
|
|
20
|
-
describe
|
21
|
+
describe '#shortest_fractional_duration' do
|
21
22
|
subject { measures.shortest_fractional_duration }
|
22
|
-
it
|
23
|
-
expect(subject).to eql(1/32.0)
|
23
|
+
it 'returns the shortest of any note' do
|
24
|
+
expect(subject).to eql(1 / 32.0)
|
24
25
|
end
|
25
26
|
end
|
26
27
|
|
27
|
-
describe
|
28
|
+
describe '#octave_range' do
|
28
29
|
subject { measures.octave_range }
|
29
|
-
it
|
30
|
+
it 'returns the lowest and highest as an array' do
|
30
31
|
expect(subject).to eql([2, 4])
|
31
32
|
end
|
32
33
|
end
|
33
|
-
|
34
|
-
end
|
34
|
+
end
|
data/spec/unit/note_spec.rb
CHANGED
@@ -1,51 +1,52 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
1
3
|
require 'spec_helper'
|
2
4
|
|
3
5
|
describe StringyFi::Note do
|
4
|
-
|
5
6
|
let(:name) { 'G' }
|
6
7
|
let(:octave) { '4' }
|
7
8
|
let(:alter) { nil }
|
8
9
|
let(:duration) { 1 }
|
9
|
-
let(:duration_type) {
|
10
|
+
let(:duration_type) { 'quarter' }
|
10
11
|
subject(:note) { described_class.new(name, octave, alter, duration, duration_type) }
|
11
12
|
|
12
|
-
|
13
|
-
|
14
|
-
|
15
|
-
|
16
|
-
let(:shortest_fractional_duration) { 1/4.0 }
|
13
|
+
describe '#stringy_durations' do
|
14
|
+
subject { note.stringy_durations(shortest_fractional_duration) }
|
15
|
+
context 'when quarter is shortest' do
|
16
|
+
let(:shortest_fractional_duration) { 1 / 4.0 }
|
17
17
|
[
|
18
|
-
{duration: 1, duration_type:
|
19
|
-
{duration: 2, duration_type:
|
20
|
-
{duration: 3, duration_type:
|
21
|
-
{duration: 4, duration_type:
|
22
|
-
{duration: 5, duration_type:
|
23
|
-
{duration: 6, duration_type:
|
24
|
-
{duration: 7, duration_type:
|
25
|
-
{duration: 1, duration_type:
|
26
|
-
{duration: 4, duration_type:
|
27
|
-
{duration: 1, duration_type:
|
28
|
-
{duration: 2, duration_type:
|
18
|
+
{ duration: 1, duration_type: 'quarter', expect: [1, 0, 0, 0] },
|
19
|
+
{ duration: 2, duration_type: 'quarter', expect: [0, 1, 0, 0] },
|
20
|
+
{ duration: 3, duration_type: 'quarter', expect: [0, 1, 0, 0] },
|
21
|
+
{ duration: 4, duration_type: 'quarter', expect: [0, 0, 1, 0] },
|
22
|
+
{ duration: 5, duration_type: 'quarter', expect: [0, 0, 1, 0] },
|
23
|
+
{ duration: 6, duration_type: 'quarter', expect: [0, 0, 1, 0] },
|
24
|
+
{ duration: 7, duration_type: 'quarter', expect: [0, 0, 1, 0] },
|
25
|
+
{ duration: 1, duration_type: 'half', expect: [0, 1, 0, 0] },
|
26
|
+
{ duration: 4, duration_type: 'half', expect: [0, 0, 0, 1] },
|
27
|
+
{ duration: 1, duration_type: 'whole', expect: [0, 0, 1, 0] },
|
28
|
+
{ duration: 2, duration_type: 'whole', expect: [0, 0, 0, 1] }
|
29
29
|
].each do |options|
|
30
30
|
context "and duration=#{options[:duration]}-#{options[:duration_type]}" do
|
31
31
|
let(:duration) { options[:duration] }
|
32
32
|
let(:duration_type) { options[:duration_type] }
|
33
|
-
it
|
33
|
+
it 'returns expected repeats/remainder' do
|
34
34
|
expect(subject).to eql(options[:expect])
|
35
35
|
end
|
36
36
|
end
|
37
37
|
end
|
38
38
|
end
|
39
|
-
|
40
|
-
|
39
|
+
|
40
|
+
context 'when 32nd is shortest' do
|
41
|
+
let(:shortest_fractional_duration) { 1 / 32.0 }
|
41
42
|
[
|
42
|
-
{duration: 1, duration_type:
|
43
|
-
{duration: 1, duration_type:
|
43
|
+
{ duration: 1, duration_type: '32nd', expect: [1, 0, 0, 0] },
|
44
|
+
{ duration: 1, duration_type: 'quarter', expect: [0, 0, 0, 1] }
|
44
45
|
].each do |options|
|
45
46
|
context "and duration=#{options[:duration]}-#{options[:duration_type]}" do
|
46
47
|
let(:duration) { options[:duration] }
|
47
48
|
let(:duration_type) { options[:duration_type] }
|
48
|
-
it
|
49
|
+
it 'returns expected repeats/remainder' do
|
49
50
|
expect(subject).to eql(options[:expect])
|
50
51
|
end
|
51
52
|
end
|
@@ -53,54 +54,53 @@ describe StringyFi::Note do
|
|
53
54
|
end
|
54
55
|
end
|
55
56
|
|
56
|
-
describe
|
57
|
+
describe '#to_s' do
|
57
58
|
subject { note.to_s }
|
58
|
-
context
|
59
|
-
it { should eql('G')}
|
59
|
+
context 'when not altered' do
|
60
|
+
it { should eql('G') }
|
60
61
|
end
|
61
|
-
context
|
62
|
+
context 'when altered up' do
|
62
63
|
let(:alter) { 1 }
|
63
|
-
it { should eql('G#')}
|
64
|
+
it { should eql('G#') }
|
64
65
|
end
|
65
|
-
context
|
66
|
+
context 'when altered down' do
|
66
67
|
let(:alter) { -1 }
|
67
|
-
it { should eql('Gb')}
|
68
|
+
it { should eql('Gb') }
|
68
69
|
end
|
69
70
|
end
|
70
71
|
|
71
|
-
describe
|
72
|
+
describe '#to_str' do
|
72
73
|
subject { note.to_str }
|
73
|
-
context
|
74
|
-
it { should eql('4:G:0')}
|
74
|
+
context 'when not altered' do
|
75
|
+
it { should eql('4:G:0') }
|
75
76
|
end
|
76
|
-
context
|
77
|
+
context 'when altered up' do
|
77
78
|
let(:alter) { 1 }
|
78
|
-
it { should eql('4:G:1')}
|
79
|
+
it { should eql('4:G:1') }
|
79
80
|
end
|
80
|
-
context
|
81
|
+
context 'when altered down' do
|
81
82
|
let(:alter) { -1 }
|
82
|
-
it { should eql('4:G:-1')}
|
83
|
+
it { should eql('4:G:-1') }
|
83
84
|
end
|
84
85
|
end
|
85
86
|
|
86
|
-
describe
|
87
|
+
describe '#<=>' do
|
87
88
|
let(:other_name) { name }
|
88
89
|
let(:other_octave) { octave }
|
89
90
|
let(:other_alter) { alter }
|
90
91
|
let(:other_note) { described_class.new(other_name, other_octave, other_alter) }
|
91
92
|
|
92
93
|
subject { note <=> other_note }
|
93
|
-
context
|
94
|
+
context 'when same' do
|
94
95
|
it { should eql(0) }
|
95
96
|
end
|
96
|
-
context
|
97
|
+
context 'when vary by alter up' do
|
97
98
|
let(:other_alter) { 1 }
|
98
99
|
it { should eql(-1) }
|
99
100
|
end
|
100
|
-
context
|
101
|
+
context 'when vary by octave down' do
|
101
102
|
let(:other_octave) { 3 }
|
102
103
|
it { should eql(1) }
|
103
104
|
end
|
104
105
|
end
|
105
|
-
|
106
106
|
end
|
data/spec/unit/shell_spec.rb
CHANGED
@@ -1,12 +1,14 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
1
3
|
require 'spec_helper'
|
2
4
|
|
3
5
|
describe StringyFi::Shell do
|
4
6
|
subject(:shell) { described_class.new }
|
5
7
|
|
6
|
-
context
|
7
|
-
it
|
8
|
+
context 'when no args passed' do
|
9
|
+
it 'initialises with sample fixture' do
|
8
10
|
expect(subject).to be_a(described_class)
|
9
|
-
expect(subject.converter.filename).to include(
|
11
|
+
expect(subject.converter.filename).to include('chromatic.xml')
|
10
12
|
end
|
11
13
|
end
|
12
|
-
end
|
14
|
+
end
|
data/spec/unit/version_spec.rb
CHANGED
data/stringyfi.gemspec
CHANGED
@@ -1,31 +1,23 @@
|
|
1
|
-
#
|
2
|
-
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
lib = File.expand_path('lib', __dir__)
|
3
4
|
$LOAD_PATH.unshift(lib) unless $LOAD_PATH.include?(lib)
|
4
5
|
require 'stringyfi/version'
|
5
6
|
|
6
7
|
Gem::Specification.new do |spec|
|
7
|
-
spec.name =
|
8
|
+
spec.name = 'stringyfi'
|
8
9
|
spec.version = StringyFi::VERSION
|
9
|
-
spec.authors = [
|
10
|
-
spec.email = [
|
11
|
-
spec.summary =
|
12
|
-
spec.homepage =
|
13
|
-
spec.license =
|
10
|
+
spec.authors = ['Paul Gallagher']
|
11
|
+
spec.email = ['gallagher.paul@gmail.com']
|
12
|
+
spec.summary = 'Convert MusicXML to PIC assembler for the Boldport Stringy'
|
13
|
+
spec.homepage = 'https://github.com/tardate/stringyfi'
|
14
|
+
spec.license = 'MIT'
|
14
15
|
|
15
16
|
spec.files = `git ls-files -z`.split("\x0")
|
16
17
|
spec.executables = spec.files.grep(%r{^bin/}) { |f| File.basename(f) }
|
17
18
|
spec.test_files = spec.files.grep(%r{^(test|spec|features)/})
|
18
|
-
spec.require_paths = [
|
19
|
-
|
20
|
-
spec.add_runtime_dependency "nokogiri", "~> 1.8"
|
21
|
-
|
22
|
-
spec.add_development_dependency "bundler", "~> 1.6"
|
23
|
-
spec.add_development_dependency "rake"
|
24
|
-
spec.add_development_dependency "rspec"
|
19
|
+
spec.require_paths = ['lib']
|
25
20
|
|
26
|
-
|
27
|
-
spec.
|
28
|
-
spec.add_development_dependency "rb-fsevent", "0.9.6"
|
29
|
-
spec.add_development_dependency "rb-inotify", "0.9.5"
|
30
|
-
spec.add_development_dependency "pry-coolline", "0.2.5" # avoid readline dependency
|
21
|
+
spec.required_ruby_version = '>= 3.0'
|
22
|
+
spec.add_runtime_dependency 'nokogiri', '~> 1.18'
|
31
23
|
end
|
metadata
CHANGED
@@ -1,14 +1,13 @@
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
2
2
|
name: stringyfi
|
3
3
|
version: !ruby/object:Gem::Version
|
4
|
-
version: 0.0.
|
4
|
+
version: 0.0.2
|
5
5
|
platform: ruby
|
6
6
|
authors:
|
7
7
|
- Paul Gallagher
|
8
|
-
autorequire:
|
9
8
|
bindir: bin
|
10
9
|
cert_chain: []
|
11
|
-
date:
|
10
|
+
date: 2025-07-29 00:00:00.000000000 Z
|
12
11
|
dependencies:
|
13
12
|
- !ruby/object:Gem::Dependency
|
14
13
|
name: nokogiri
|
@@ -16,113 +15,14 @@ dependencies:
|
|
16
15
|
requirements:
|
17
16
|
- - "~>"
|
18
17
|
- !ruby/object:Gem::Version
|
19
|
-
version: '1.
|
18
|
+
version: '1.18'
|
20
19
|
type: :runtime
|
21
20
|
prerelease: false
|
22
21
|
version_requirements: !ruby/object:Gem::Requirement
|
23
22
|
requirements:
|
24
23
|
- - "~>"
|
25
24
|
- !ruby/object:Gem::Version
|
26
|
-
version: '1.
|
27
|
-
- !ruby/object:Gem::Dependency
|
28
|
-
name: bundler
|
29
|
-
requirement: !ruby/object:Gem::Requirement
|
30
|
-
requirements:
|
31
|
-
- - "~>"
|
32
|
-
- !ruby/object:Gem::Version
|
33
|
-
version: '1.6'
|
34
|
-
type: :development
|
35
|
-
prerelease: false
|
36
|
-
version_requirements: !ruby/object:Gem::Requirement
|
37
|
-
requirements:
|
38
|
-
- - "~>"
|
39
|
-
- !ruby/object:Gem::Version
|
40
|
-
version: '1.6'
|
41
|
-
- !ruby/object:Gem::Dependency
|
42
|
-
name: rake
|
43
|
-
requirement: !ruby/object:Gem::Requirement
|
44
|
-
requirements:
|
45
|
-
- - ">="
|
46
|
-
- !ruby/object:Gem::Version
|
47
|
-
version: '0'
|
48
|
-
type: :development
|
49
|
-
prerelease: false
|
50
|
-
version_requirements: !ruby/object:Gem::Requirement
|
51
|
-
requirements:
|
52
|
-
- - ">="
|
53
|
-
- !ruby/object:Gem::Version
|
54
|
-
version: '0'
|
55
|
-
- !ruby/object:Gem::Dependency
|
56
|
-
name: rspec
|
57
|
-
requirement: !ruby/object:Gem::Requirement
|
58
|
-
requirements:
|
59
|
-
- - ">="
|
60
|
-
- !ruby/object:Gem::Version
|
61
|
-
version: '0'
|
62
|
-
type: :development
|
63
|
-
prerelease: false
|
64
|
-
version_requirements: !ruby/object:Gem::Requirement
|
65
|
-
requirements:
|
66
|
-
- - ">="
|
67
|
-
- !ruby/object:Gem::Version
|
68
|
-
version: '0'
|
69
|
-
- !ruby/object:Gem::Dependency
|
70
|
-
name: guard-rspec
|
71
|
-
requirement: !ruby/object:Gem::Requirement
|
72
|
-
requirements:
|
73
|
-
- - '='
|
74
|
-
- !ruby/object:Gem::Version
|
75
|
-
version: 4.6.4
|
76
|
-
type: :development
|
77
|
-
prerelease: false
|
78
|
-
version_requirements: !ruby/object:Gem::Requirement
|
79
|
-
requirements:
|
80
|
-
- - '='
|
81
|
-
- !ruby/object:Gem::Version
|
82
|
-
version: 4.6.4
|
83
|
-
- !ruby/object:Gem::Dependency
|
84
|
-
name: rb-fsevent
|
85
|
-
requirement: !ruby/object:Gem::Requirement
|
86
|
-
requirements:
|
87
|
-
- - '='
|
88
|
-
- !ruby/object:Gem::Version
|
89
|
-
version: 0.9.6
|
90
|
-
type: :development
|
91
|
-
prerelease: false
|
92
|
-
version_requirements: !ruby/object:Gem::Requirement
|
93
|
-
requirements:
|
94
|
-
- - '='
|
95
|
-
- !ruby/object:Gem::Version
|
96
|
-
version: 0.9.6
|
97
|
-
- !ruby/object:Gem::Dependency
|
98
|
-
name: rb-inotify
|
99
|
-
requirement: !ruby/object:Gem::Requirement
|
100
|
-
requirements:
|
101
|
-
- - '='
|
102
|
-
- !ruby/object:Gem::Version
|
103
|
-
version: 0.9.5
|
104
|
-
type: :development
|
105
|
-
prerelease: false
|
106
|
-
version_requirements: !ruby/object:Gem::Requirement
|
107
|
-
requirements:
|
108
|
-
- - '='
|
109
|
-
- !ruby/object:Gem::Version
|
110
|
-
version: 0.9.5
|
111
|
-
- !ruby/object:Gem::Dependency
|
112
|
-
name: pry-coolline
|
113
|
-
requirement: !ruby/object:Gem::Requirement
|
114
|
-
requirements:
|
115
|
-
- - '='
|
116
|
-
- !ruby/object:Gem::Version
|
117
|
-
version: 0.2.5
|
118
|
-
type: :development
|
119
|
-
prerelease: false
|
120
|
-
version_requirements: !ruby/object:Gem::Requirement
|
121
|
-
requirements:
|
122
|
-
- - '='
|
123
|
-
- !ruby/object:Gem::Version
|
124
|
-
version: 0.2.5
|
125
|
-
description:
|
25
|
+
version: '1.18'
|
126
26
|
email:
|
127
27
|
- gallagher.paul@gmail.com
|
128
28
|
executables:
|
@@ -130,11 +30,11 @@ executables:
|
|
130
30
|
extensions: []
|
131
31
|
extra_rdoc_files: []
|
132
32
|
files:
|
33
|
+
- ".github/workflows/ruby-tests.yml"
|
133
34
|
- ".gitignore"
|
134
|
-
- ".
|
35
|
+
- ".rubocop.yml"
|
135
36
|
- ".ruby-gemset"
|
136
37
|
- ".ruby-version"
|
137
|
-
- ".travis.yml"
|
138
38
|
- Gemfile
|
139
39
|
- Guardfile
|
140
40
|
- LICENSE.txt
|
@@ -163,7 +63,6 @@ homepage: https://github.com/tardate/stringyfi
|
|
163
63
|
licenses:
|
164
64
|
- MIT
|
165
65
|
metadata: {}
|
166
|
-
post_install_message:
|
167
66
|
rdoc_options: []
|
168
67
|
require_paths:
|
169
68
|
- lib
|
@@ -171,16 +70,14 @@ required_ruby_version: !ruby/object:Gem::Requirement
|
|
171
70
|
requirements:
|
172
71
|
- - ">="
|
173
72
|
- !ruby/object:Gem::Version
|
174
|
-
version: '0'
|
73
|
+
version: '3.0'
|
175
74
|
required_rubygems_version: !ruby/object:Gem::Requirement
|
176
75
|
requirements:
|
177
76
|
- - ">="
|
178
77
|
- !ruby/object:Gem::Version
|
179
78
|
version: '0'
|
180
79
|
requirements: []
|
181
|
-
|
182
|
-
rubygems_version: 2.6.12
|
183
|
-
signing_key:
|
80
|
+
rubygems_version: 3.6.2
|
184
81
|
specification_version: 4
|
185
82
|
summary: Convert MusicXML to PIC assembler for the Boldport Stringy
|
186
83
|
test_files:
|
data/.rspec
DELETED