stringyfi 0.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
+ 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
@@ -0,0 +1,2 @@
1
+ --color
2
+ --format documentation
data/.ruby-gemset ADDED
@@ -0,0 +1 @@
1
+ stringyfi
data/.ruby-version ADDED
@@ -0,0 +1 @@
1
+ 2.4.1
data/.travis.yml ADDED
@@ -0,0 +1,5 @@
1
+ # These are specific configuration settings required for travis-ci
2
+ # see http://travis-ci.org/tardate/stringyfi
3
+ language: ruby
4
+ rvm:
5
+ - 2.4.1
data/Gemfile ADDED
@@ -0,0 +1,4 @@
1
+ source 'https://rubygems.org'
2
+
3
+ # Specify your gem's dependencies in amuse.gemspec
4
+ gemspec
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
@@ -0,0 +1,5 @@
1
+ #!/usr/bin/env ruby
2
+ $LOAD_PATH.unshift File.join(File.dirname(__FILE__), '..', 'lib')
3
+
4
+ require 'stringyfi'
5
+ StringyFi::Shell.new(ARGV).convert!
data/lib/stringyfi.rb ADDED
@@ -0,0 +1,5 @@
1
+ require 'stringyfi/version'
2
+ require 'stringyfi/shell'
3
+ require 'stringyfi/note'
4
+ require 'stringyfi/measures'
5
+ require 'stringyfi/converter'
@@ -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