stringyfi 0.0.1
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/.gitignore +22 -0
- data/.rspec +2 -0
- data/.ruby-gemset +1 -0
- data/.ruby-version +1 -0
- data/.travis.yml +5 -0
- data/Gemfile +4 -0
- data/Guardfile +18 -0
- data/LICENSE.txt +22 -0
- data/README.md +65 -0
- data/Rakefile +12 -0
- data/bin/stringyfi +5 -0
- data/lib/stringyfi.rb +5 -0
- data/lib/stringyfi/converter.rb +164 -0
- data/lib/stringyfi/measures.rb +27 -0
- data/lib/stringyfi/note.rb +97 -0
- data/lib/stringyfi/shell.rb +13 -0
- data/lib/stringyfi/version.rb +3 -0
- data/spec/fixtures/music_xml/chromatic.gp +0 -0
- data/spec/fixtures/music_xml/chromatic.xml +2096 -0
- data/spec/fixtures/music_xml/quarters.gp +0 -0
- data/spec/fixtures/music_xml/quarters.xml +1339 -0
- data/spec/spec_helper.rb +85 -0
- data/spec/support/fixtures_helper.rb +14 -0
- data/spec/unit/converter_spec.rb +166 -0
- data/spec/unit/measures_spec.rb +34 -0
- data/spec/unit/note_spec.rb +106 -0
- data/spec/unit/shell_spec.rb +12 -0
- data/spec/unit/version_spec.rb +9 -0
- data/stringyfi.gemspec +31 -0
- metadata +197 -0
checksums.yaml
ADDED
@@ -0,0 +1,7 @@
|
|
1
|
+
---
|
2
|
+
SHA1:
|
3
|
+
metadata.gz: 796dc3f1361b70dbb6c86d6f7ef77639796d12e5
|
4
|
+
data.tar.gz: f7ce1efbc6ead2e92c99b37f629540cc69d6f5db
|
5
|
+
SHA512:
|
6
|
+
metadata.gz: 92a250942f88d6ee07ff351ce5a4fd7a2998a675f8773b1782a913cc94fc5bde540a98fcdbf2bc40b0d5d6baba3a0f1c142c4708eb776d6c3dc666f067a68c7e
|
7
|
+
data.tar.gz: 07ea32854390d1bcb07065ef3f5a44609399d56f1dbc04df387bd44044f66c5451ad9573c6965440b954653dfb353039f2889671a148cb744fc6a22d0f3485e4
|
data/.gitignore
ADDED
@@ -0,0 +1,22 @@
|
|
1
|
+
*.gem
|
2
|
+
*.rbc
|
3
|
+
.bundle
|
4
|
+
.config
|
5
|
+
.yardoc
|
6
|
+
Gemfile.lock
|
7
|
+
InstalledFiles
|
8
|
+
_yardoc
|
9
|
+
coverage
|
10
|
+
doc/
|
11
|
+
lib/bundler/man
|
12
|
+
pkg
|
13
|
+
rdoc
|
14
|
+
spec/reports
|
15
|
+
test/tmp
|
16
|
+
test/version_tmp
|
17
|
+
tmp
|
18
|
+
*.bundle
|
19
|
+
*.so
|
20
|
+
*.o
|
21
|
+
*.a
|
22
|
+
mkmf.log
|
data/.rspec
ADDED
data/.ruby-gemset
ADDED
@@ -0,0 +1 @@
|
|
1
|
+
stringyfi
|
data/.ruby-version
ADDED
@@ -0,0 +1 @@
|
|
1
|
+
2.4.1
|
data/.travis.yml
ADDED
data/Gemfile
ADDED
data/Guardfile
ADDED
@@ -0,0 +1,18 @@
|
|
1
|
+
# A sample Guardfile
|
2
|
+
# More info at https://github.com/guard/guard#readme
|
3
|
+
|
4
|
+
# Note: The cmd option is now required due to the increasing number of ways
|
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'
|
12
|
+
guard :rspec, cmd: 'bundle exec rspec' do
|
13
|
+
watch(%r{^spec/.+_spec\.rb$})
|
14
|
+
watch(%r{^lib/stringyfi/(.+)\.rb$}) { |m| "spec/unit/#{m[1]}_spec.rb" }
|
15
|
+
watch('spec/spec_helper.rb') { "spec" }
|
16
|
+
|
17
|
+
end
|
18
|
+
|
data/LICENSE.txt
ADDED
@@ -0,0 +1,22 @@
|
|
1
|
+
Copyright (c) 2017 Paul Gallagher
|
2
|
+
|
3
|
+
MIT License
|
4
|
+
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining
|
6
|
+
a copy of this software and associated documentation files (the
|
7
|
+
"Software"), to deal in the Software without restriction, including
|
8
|
+
without limitation the rights to use, copy, modify, merge, publish,
|
9
|
+
distribute, sublicense, and/or sell copies of the Software, and to
|
10
|
+
permit persons to whom the Software is furnished to do so, subject to
|
11
|
+
the following conditions:
|
12
|
+
|
13
|
+
The above copyright notice and this permission notice shall be
|
14
|
+
included in all copies or substantial portions of the Software.
|
15
|
+
|
16
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
|
17
|
+
EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
|
18
|
+
MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
|
19
|
+
NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
|
20
|
+
LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
|
21
|
+
OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
|
22
|
+
WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
data/README.md
ADDED
@@ -0,0 +1,65 @@
|
|
1
|
+
# StringyFi
|
2
|
+
|
3
|
+
Convert MusicXML to PIC assembler for running on the Boldport Club Stringy.
|
4
|
+
|
5
|
+
[](https://travis-ci.org/tardate/stringyfi)
|
6
|
+
|
7
|
+
## About the Stringy
|
8
|
+
|
9
|
+
The [Stringy](https://www.boldport.com/products/stringy/) is an open source hardware project
|
10
|
+
from the wonderful [Boldport Club](http://www.boldport.club/).
|
11
|
+
|
12
|
+
The Stringy was Project #14 from June 2017, and is a remix of
|
13
|
+
[MadLab's 'Funky guitar'](http://www.madlab.org/kits/guitar.html).
|
14
|
+
|
15
|
+
## About StringyFi
|
16
|
+
|
17
|
+
StringyFi is a simple gem that converts MusicXML source files to
|
18
|
+
a PIC assembler source format that can be compiled and programmed to the Stringy.
|
19
|
+
|
20
|
+
See [LEAP#349 DemoBurner](https://github.com/tardate/LittleArduinoProjects/tree/master/BoldportClub/stringy/DemoBurner)
|
21
|
+
for a complete example of this in practice.
|
22
|
+
|
23
|
+
StringyFi has some serious limitations, some of which are in its implementation of MusicXML parsing,
|
24
|
+
some are fundamental limitations of the Stringy. I have found that most scores need tweaking
|
25
|
+
to be reproduced acceptably on the Stringy, and some are just too complex (without re-writing the Stringy firmware en-masse).
|
26
|
+
Some key points to note:
|
27
|
+
|
28
|
+
* The String uses ony 2-bit (4 levels) of note duration, so the conversion squeezes the score into 4 note durations as best as possible
|
29
|
+
* many notation features ignored: slides, ties etc
|
30
|
+
|
31
|
+
## Installation
|
32
|
+
|
33
|
+
Add this line to your application's Gemfile:
|
34
|
+
|
35
|
+
gem 'stringyfi'
|
36
|
+
|
37
|
+
And then execute:
|
38
|
+
|
39
|
+
$ bundle
|
40
|
+
|
41
|
+
Or install it yourself as:
|
42
|
+
|
43
|
+
$ gem install stringyfi
|
44
|
+
|
45
|
+
## Usage
|
46
|
+
|
47
|
+
The StringyFi executable accepts a file path to the MusicXML source to convert,
|
48
|
+
and emits the assembler source on STDOUT (so it can be redirected as appropriate).
|
49
|
+
|
50
|
+
For example:
|
51
|
+
|
52
|
+
$ stringyfi ./spec/fixtures/music_xml/chromatic.xml > ../CustomDemo.X/demo.tun
|
53
|
+
|
54
|
+
The
|
55
|
+
[spec/fixtures](./spec/fixtures/music_xml)
|
56
|
+
contains a few examples that are also used in tests.
|
57
|
+
|
58
|
+
## Contributing
|
59
|
+
|
60
|
+
1. Fork it
|
61
|
+
2. Create your feature branch (`git checkout -b my-new-feature`)
|
62
|
+
3. Commit your changes (`git commit -am 'Add some feature'`)
|
63
|
+
4. Push to the branch (`git push origin my-new-feature`)
|
64
|
+
5. Create a new Pull Request
|
65
|
+
(6. Join Boldport Club!)
|
data/Rakefile
ADDED
@@ -0,0 +1,12 @@
|
|
1
|
+
require "bundler/gem_tasks"
|
2
|
+
require "rspec/core/rake_task"
|
3
|
+
|
4
|
+
RSpec::Core::RakeTask.new(:spec)
|
5
|
+
|
6
|
+
task :default => :spec
|
7
|
+
|
8
|
+
desc "Open an irb session preloaded with this library"
|
9
|
+
task :console do
|
10
|
+
sh "irb -rubygems -I lib -r stringyfi.rb"
|
11
|
+
end
|
12
|
+
|
data/bin/stringyfi
ADDED
data/lib/stringyfi.rb
ADDED
@@ -0,0 +1,164 @@
|
|
1
|
+
require 'nokogiri'
|
2
|
+
|
3
|
+
class StringyFi::Converter
|
4
|
+
|
5
|
+
INITIAL_OCTAVE = 1 # setup by the stringy firmware
|
6
|
+
OCTAVE_OFFSET = 3 # how many octaves to offset the source
|
7
|
+
|
8
|
+
attr_accessor :filename, :xml_doc
|
9
|
+
|
10
|
+
def initialize(filename)
|
11
|
+
self.filename = filename
|
12
|
+
end
|
13
|
+
|
14
|
+
def convert!
|
15
|
+
$stderr.puts "converting #{filename}.."
|
16
|
+
shortest_fractional_duration = measures.shortest_fractional_duration
|
17
|
+
$stderr.puts " shortest_fractional_duration: #{shortest_fractional_duration}"
|
18
|
+
$stderr.puts " octave_range: #{measures.octave_range.inspect}"
|
19
|
+
|
20
|
+
puts score_preamble
|
21
|
+
puts score_body(shortest_fractional_duration)
|
22
|
+
puts score_coda
|
23
|
+
|
24
|
+
$stderr.puts ".. done."
|
25
|
+
end
|
26
|
+
|
27
|
+
def score_preamble
|
28
|
+
<<-EOS
|
29
|
+
;**************************************************************************
|
30
|
+
;** Title: #{title}
|
31
|
+
;** Tempo: #{tempo}
|
32
|
+
;** Encoded: #{encoding_date} with #{encoding_software}
|
33
|
+
;** Stringyfied: #{Time.now}
|
34
|
+
;**************************************************************************
|
35
|
+
|
36
|
+
\ttstart DemoTune
|
37
|
+
EOS
|
38
|
+
end
|
39
|
+
|
40
|
+
def score_coda
|
41
|
+
<<-EOS
|
42
|
+
\ttrest 8
|
43
|
+
\ttstop
|
44
|
+
EOS
|
45
|
+
end
|
46
|
+
|
47
|
+
def score_body(shortest_fractional_duration)
|
48
|
+
lines = []
|
49
|
+
current_octave = INITIAL_OCTAVE + OCTAVE_OFFSET
|
50
|
+
measures.each_with_index do |measure, measure_index|
|
51
|
+
lines << "\t; measure #{measure_index+1}"
|
52
|
+
measure.each do |note|
|
53
|
+
unless note.rest?
|
54
|
+
if note.octave != current_octave
|
55
|
+
delta = note.octave - current_octave
|
56
|
+
sign = delta > 0 ? "+" : "-"
|
57
|
+
(delta.abs).times do
|
58
|
+
lines << "\ttoctave #{sign}1"
|
59
|
+
end
|
60
|
+
current_octave = note.octave
|
61
|
+
end
|
62
|
+
end
|
63
|
+
short_repeats, medium_repeats, long_repeats, very_long_repeats = note.stringy_durations(shortest_fractional_duration)
|
64
|
+
if note.rest?
|
65
|
+
(short_repeats).times { lines << "\ttrest 1" }
|
66
|
+
(medium_repeats).times { lines << "\ttrest 2" }
|
67
|
+
(long_repeats).times { lines << "\ttrest 3" }
|
68
|
+
(very_long_repeats).times { lines << "\ttrest 4" }
|
69
|
+
else
|
70
|
+
(short_repeats).times { lines << "\ttnote #{note.to_stringy(current_octave)},1,0 ; #{note.to_note_id}" }
|
71
|
+
(medium_repeats).times { lines << "\ttnote #{note.to_stringy(current_octave)},2,0 ; #{note.to_note_id}" }
|
72
|
+
(long_repeats).times { lines << "\ttnote #{note.to_stringy(current_octave)},3,0 ; #{note.to_note_id}" }
|
73
|
+
(very_long_repeats).times { lines << "\ttnote #{note.to_stringy(current_octave)},4,0 ; #{note.to_note_id}" }
|
74
|
+
end
|
75
|
+
end
|
76
|
+
end
|
77
|
+
lines
|
78
|
+
end
|
79
|
+
|
80
|
+
def title
|
81
|
+
xml_doc.xpath('//work/work-title').text
|
82
|
+
end
|
83
|
+
|
84
|
+
# Simplified - only supports one tempo for the piece
|
85
|
+
# assumed for quarter note
|
86
|
+
def tempo
|
87
|
+
@tempo ||= xml_doc.xpath('//measure/direction/sound/@tempo').to_s.to_i
|
88
|
+
end
|
89
|
+
|
90
|
+
def encoding_date
|
91
|
+
xml_doc.xpath('//identification/encoding/encoding-date').text
|
92
|
+
end
|
93
|
+
|
94
|
+
def encoding_software
|
95
|
+
xml_doc.xpath('//identification/encoding/software').text
|
96
|
+
end
|
97
|
+
|
98
|
+
def identification
|
99
|
+
{
|
100
|
+
title: title,
|
101
|
+
encoding: {
|
102
|
+
date: encoding_date,
|
103
|
+
software: encoding_software
|
104
|
+
}
|
105
|
+
}
|
106
|
+
end
|
107
|
+
def part_list
|
108
|
+
xml_doc.xpath('//part-list/score-part').each_with_object([]) do |part,memo|
|
109
|
+
h = {}
|
110
|
+
h[:id] = part.attr('id')
|
111
|
+
memo << h
|
112
|
+
end
|
113
|
+
end
|
114
|
+
|
115
|
+
def parts
|
116
|
+
xml_doc.xpath('//part')
|
117
|
+
end
|
118
|
+
|
119
|
+
# Returns measures for the piece.
|
120
|
+
# only converts one part (for now)
|
121
|
+
# only includes staff 1
|
122
|
+
def measures
|
123
|
+
@measures ||= begin
|
124
|
+
measures = StringyFi::Measures.new
|
125
|
+
part = parts.first
|
126
|
+
part.xpath('measure').each_with_index do |part_measure, m|
|
127
|
+
measures[m] ||= []
|
128
|
+
part_measure.xpath('note').each_with_object(measures[m]) do |note, memo|
|
129
|
+
next unless note.xpath("staff").text == "1"
|
130
|
+
next unless note.xpath("voice").text == "1"
|
131
|
+
pitch = note.xpath("pitch")
|
132
|
+
duration = note.xpath("duration").text.to_i
|
133
|
+
actual_notes = note.xpath("actual-notes").text.to_i
|
134
|
+
normal_notes = note.xpath("normal-notes").text.to_i
|
135
|
+
if actual_notes > 0 and normal_notes > 0
|
136
|
+
duration = duration * 1.0 * normal_notes / actual_notes
|
137
|
+
end
|
138
|
+
duration_type = note.xpath("type").text
|
139
|
+
memo << StringyFi::Note.new(
|
140
|
+
pitch.xpath('step').text,
|
141
|
+
pitch.xpath('octave').text,
|
142
|
+
pitch.xpath('alter').text,
|
143
|
+
duration,
|
144
|
+
duration_type
|
145
|
+
)
|
146
|
+
end
|
147
|
+
end
|
148
|
+
measures
|
149
|
+
end
|
150
|
+
end
|
151
|
+
|
152
|
+
def measure(measure_id)
|
153
|
+
measures[measure_id]
|
154
|
+
end
|
155
|
+
|
156
|
+
def xml_doc
|
157
|
+
@xml_doc ||= Nokogiri::XML(io_stream)
|
158
|
+
end
|
159
|
+
|
160
|
+
def io_stream
|
161
|
+
File.open(filename).read
|
162
|
+
end
|
163
|
+
|
164
|
+
end
|
@@ -0,0 +1,27 @@
|
|
1
|
+
class StringyFi::Measures < Array
|
2
|
+
|
3
|
+
def shortest_fractional_duration
|
4
|
+
result = 1
|
5
|
+
each do |measure|
|
6
|
+
measure.each do |note|
|
7
|
+
result = note.fractional_duration if note.fractional_duration < result
|
8
|
+
end
|
9
|
+
end
|
10
|
+
result
|
11
|
+
end
|
12
|
+
|
13
|
+
def octave_range
|
14
|
+
lo = hi = nil
|
15
|
+
each do |measure|
|
16
|
+
measure.each do |note|
|
17
|
+
next if note.rest?
|
18
|
+
lo ||= note.octave
|
19
|
+
hi ||= note.octave
|
20
|
+
lo = note.octave if note.octave < lo
|
21
|
+
hi = note.octave if note.octave > hi
|
22
|
+
end
|
23
|
+
end
|
24
|
+
[lo, hi]
|
25
|
+
end
|
26
|
+
|
27
|
+
end
|
@@ -0,0 +1,97 @@
|
|
1
|
+
class StringyFi::Note
|
2
|
+
|
3
|
+
attr_accessor :name, :octave, :alter
|
4
|
+
attr_accessor :tempo
|
5
|
+
attr_accessor :fractional_duration
|
6
|
+
|
7
|
+
def initialize(name, octave=4, alter=nil, duration=1, duration_type="quarter")
|
8
|
+
self.name = "#{name}".upcase
|
9
|
+
self.octave = "#{octave}".to_i
|
10
|
+
self.alter = alter.to_i rescue 0
|
11
|
+
self.fractional_duration = calculate_fractional_duration(duration, duration_type)
|
12
|
+
end
|
13
|
+
|
14
|
+
def rest?
|
15
|
+
name == ""
|
16
|
+
end
|
17
|
+
|
18
|
+
def calculate_fractional_duration(duration, duration_type)
|
19
|
+
divisor = {
|
20
|
+
"half" => 2.0,
|
21
|
+
"quarter" => 4.0,
|
22
|
+
"eighth" => 8.0,
|
23
|
+
"16th" => 16.0,
|
24
|
+
"32nd" => 32.0
|
25
|
+
}[duration_type] || 1.0
|
26
|
+
duration/divisor
|
27
|
+
end
|
28
|
+
|
29
|
+
# retrun [short, medium, long, very_long] repeats for the note
|
30
|
+
# TODO: scale medium, long, very_long durations correctly
|
31
|
+
def stringy_durations(shortest_fractional_duration)
|
32
|
+
time_units = (fractional_duration / shortest_fractional_duration).to_i
|
33
|
+
case
|
34
|
+
when time_units >= 8
|
35
|
+
[0, 0, 0, 1]
|
36
|
+
when time_units >= 4
|
37
|
+
[0, 0, 1, 0]
|
38
|
+
when time_units >= 2
|
39
|
+
[0, 1, 0, 0]
|
40
|
+
else
|
41
|
+
[1, 0, 0, 0]
|
42
|
+
end
|
43
|
+
end
|
44
|
+
|
45
|
+
def to_stringy(current_octave)
|
46
|
+
relative_octave = octave - current_octave + 1
|
47
|
+
case alter
|
48
|
+
when 1
|
49
|
+
"#{name}#{relative_octave}S"
|
50
|
+
when -1
|
51
|
+
sharpy_name = {
|
52
|
+
'B' => 'A',
|
53
|
+
'E' => 'D',
|
54
|
+
'A' => 'G',
|
55
|
+
'D' => 'C',
|
56
|
+
'G' => 'F',
|
57
|
+
}[name]
|
58
|
+
"#{sharpy_name}#{relative_octave}S"
|
59
|
+
else
|
60
|
+
"#{name}#{relative_octave}"
|
61
|
+
end
|
62
|
+
end
|
63
|
+
|
64
|
+
# Returns the note name, regardless of octave
|
65
|
+
def to_note_name
|
66
|
+
case alter
|
67
|
+
when 1
|
68
|
+
"#{name}#"
|
69
|
+
when -1
|
70
|
+
"#{name}b"
|
71
|
+
else
|
72
|
+
name
|
73
|
+
end
|
74
|
+
end
|
75
|
+
|
76
|
+
def to_s
|
77
|
+
to_note_name
|
78
|
+
end
|
79
|
+
|
80
|
+
# Returns the note ID - unique for each frequency
|
81
|
+
def to_note_id
|
82
|
+
"#{octave}:#{name}:#{alter}"
|
83
|
+
end
|
84
|
+
|
85
|
+
def to_str
|
86
|
+
to_note_id
|
87
|
+
end
|
88
|
+
|
89
|
+
def inspect
|
90
|
+
"\"#{to_str}\""
|
91
|
+
end
|
92
|
+
|
93
|
+
def <=>(other)
|
94
|
+
self.to_note_id <=> other.to_note_id
|
95
|
+
end
|
96
|
+
|
97
|
+
end
|