stringyfi 0.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 +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
|
+
[![Build Status](https://travis-ci.org/tardate/stringyfi.svg?branch=master)](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
|