alda-rb 0.1.2 → 0.1.4

Sign up to get free protection for your applications and to get access to all the features.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: b1a40fb3e4fa0d3ae035cbaf901b0fa209676743cd36b31168b9e04af9bf6aaf
4
- data.tar.gz: df7faaf786abc7e90260864d5abaa275c3702f81b7ad9b114b287ca940d07dee
3
+ metadata.gz: fa068d3caf9d89e4c0bfb90b2bc0800f5fe2824cbd9d3674be7a4853a55e5b25
4
+ data.tar.gz: ec0c8aa26fd9f789d2bbcaef2502890a21223036779e29704eb8c30ebf08b27f
5
5
  SHA512:
6
- metadata.gz: 7378bb4040c96cf509ea800412000f5f1d676a5e2d3e62016906c928c9991fc6f2fd0358989e5ea0d0e8526767fcbc8d8942ccbb9f7742fe2106f7ab44d0596f
7
- data.tar.gz: bfb477c6af6d034c3b4b05ddf8a5e73fd2887584eae8b959487c72dddcd7deda77617e2c6b3392b2cc71574ac81a05df9825cd686a7de9f3eb79e6d8731ddb09
6
+ metadata.gz: 98860e1ec9598d9935bb59371d8621be062dcab44f524ada4ae1a1a5460f4c85bd440429b38bbdc74032fc19f18cfc0787f575f60593f1e1abceedc5d2ad06c1
7
+ data.tar.gz: 83a071c40c8e82a5d57a609c668d5f5886d02be45b1656f2a89946208eb590af940e292b550cefbcf4ed930000c1a46b518ec2a204d7f0bca8cb07293911727c
data/Gemfile CHANGED
@@ -1,7 +1,7 @@
1
1
  source "https://rubygems.org"
2
2
 
3
- # Specify your gem's dependencies in alda-rb-rb.gemspec
4
3
  gemspec
5
4
 
6
5
  gem "rake", "~> 12.0"
7
6
  gem "minitest", "~> 5.0"
7
+ gem "colorize"
data/alda-rb.gemspec CHANGED
@@ -6,19 +6,19 @@ Gem::Specification.new do |spec|
6
6
  spec.name = "alda-rb"
7
7
  spec.version = Alda::VERSION
8
8
  spec.authors = ["Ulysses Zhan"]
9
- spec.email = ["2938747508@qq.com"]
9
+ spec.email = ["UlyssesZhan@gmail.com"]
10
10
 
11
11
  spec.summary = %q{A Ruby library for live-coding music with Alda.}
12
12
  # spec.description = %q{TODO: Write a longer description or delete this line.}
13
13
  spec.homepage = "https://github.com/UlyssesZh/alda-rb"
14
14
  spec.license = "MIT"
15
- spec.required_ruby_version = Gem::Requirement.new(">= 2.3.0")
15
+ spec.required_ruby_version = Gem::Requirement.new(">= 2.7.0")
16
16
 
17
17
  # spec.metadata["allowed_push_host"] = "TODO: Set to 'http://mygemserver.com'"
18
18
 
19
19
  spec.metadata["homepage_uri"] = spec.homepage
20
20
  spec.metadata["source_code_uri"] = spec.homepage
21
- # spec.metadata["changelog_uri"] = "TODO: Put your gem's CHANGELOG.md URL here."
21
+ spec.metadata["changelog_uri"] = "https://github.com/UlyssesZh/alda-rb/releases"
22
22
 
23
23
  # Specify which files should be added to the gem when it is released.
24
24
  # The `git ls-files -z` loads the files in the RubyGem that have been added into git.
@@ -9,18 +9,18 @@ Alda::Score.new do
9
9
  vibraphone_ 'vibes-1'
10
10
  panning 10
11
11
  s do
12
- a; b8; +o; d; -o; b; g; b; +o; c
13
- e4; -o; a; +o; c; -o; g
14
- s{ g; +o; g8; f; e; c; -o; a4 }%1..2
15
- s{ b8; +o; d; g2_4 }%3
12
+ a b8 o! d o? b g b o! c
13
+ e4 o? a o! c o? g
14
+ (g o! g8 f e c o? a4) % (1..2)
15
+ (b8 o! d g2_4) % 3
16
16
  end * 3
17
17
 
18
18
  vibraphone_ 'vibes-2'
19
19
  panning 90
20
20
  s do
21
- a; +o; e; -o; a; r
22
- b; r; b; r
23
- s{ g; r; +o; g; -o; g }%1..2
24
- s{ +o; d; r; a; g }%3
21
+ a o! e o? a r
22
+ b r b r
23
+ (g r o! g o? g) % (1..2)
24
+ (o! d r a g) % 3
25
25
  end * 3
26
26
  end.play
@@ -10,16 +10,18 @@ require 'alda-rb'
10
10
  # sheet music:
11
11
  # http://www.freesheetpianomusic.com/bach/content/Well-Tempered%20Clavier_Book_1/Prelude%20and%20Fugue%20No.1%20C%20major%20BWV%20846.pdf
12
12
 
13
- include Alda
14
-
15
- Score.new do
16
- def Note.absolute event_list, pitch, duration
13
+ class Alda::Sequence
14
+ def absolute pitch, duration
17
15
  /(?<letter>[a-g][-+_]*)(?<octave>\d*)/ =~ pitch
18
- octave = @last_octave ||= '4' if octave.empty?
19
- event_list.events.push new "o#{@last_octave = octave} #{letter}", duration
16
+ octave = @@last_octave ||= '4' if octave.empty?
17
+ result = Alda::Note.new "o#{@@last_octave = octave} #{letter}", duration
18
+ @events.push result
19
+ result
20
20
  end
21
-
22
- piano_; tempo 60
21
+ end
22
+
23
+ Alda::Score.new do
24
+ piano_ tempo 60
23
25
  %w[
24
26
  c e g c5 e
25
27
  c4 d a d5 f
@@ -55,9 +57,9 @@ Score.new do
55
57
  c2 c3 g b- e4
56
58
  ].each_slice 5 do |n1, n2, *notes|
57
59
  s do
58
- v1; Note.absolute self, n1, '2'
59
- v2; r16; Note.absolute self, n2, '4..'
60
- v3; r8; s{ notes.each { Note.absolute self, _1, '16' } }*2
60
+ v1 absolute n1, '2'
61
+ v2 r16 absolute n2, '4..'
62
+ v3 r8 s{ notes.each { absolute _1, '16' } }*2
61
63
  end * 2
62
64
  end
63
65
  alda_code <<~ENDING
@@ -12,14 +12,13 @@ require 'alda-rb'
12
12
 
13
13
  Alda::Score.new do
14
14
  pattern = %i[clap clap clap rest clap clap rest clap rest clap clap rest]
15
- pattern.define_singleton_method(:round_shift) { push shift }
16
15
  Alda::Sequence.class_exec do
17
16
  define_method(:clap) { +d }; define_method(:rest) { r }
18
17
  define_method(:play) { pattern.each { __send__ _1 } }
19
18
  end
20
19
 
21
20
  tempo! 172
22
- midi_percussion_; o2; set_note_length 8
23
- v1; s{ play }*(12*13)
24
- v2; 13.times { s{ play; pattern.round_shift }*12 }
21
+ midi_percussion_ o2 set_note_length 8
22
+ v1; s{ play }*12*13
23
+ v2; 13.times { s{ play; pattern.rotate! }*12 }
25
24
  end.play
@@ -4,5 +4,5 @@ require 'alda-rb'
4
4
 
5
5
  Alda::Score.new do
6
6
  violin_/viola_/cello_('strings'); g1_1_1
7
- strings_.cello_; -o; c1_1_1
7
+ strings_.cello_ o? c1_1_1
8
8
  end.play
data/examples/entropy.rb CHANGED
@@ -13,8 +13,8 @@ Alda::Score.new do
13
13
  if rand < REST_RATE
14
14
  pause ms.()
15
15
  octave rand OCTAVE_UPPER
16
- note pitch("abcdefg"[rand 7].to_sym,
17
- %i[sharp flat natural][rand 3]), ms.()
16
+ note pitch('abcdefg'[rand 7].to_sym,
17
+ %i[sharp flat natural].sample), ms.()
18
18
  end
19
19
  end
20
20
 
data/examples/hanon.rb ADDED
@@ -0,0 +1,51 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'alda-rb'
4
+
5
+ SYMBOLS = %i[c d e f g a b]
6
+ class Solfege
7
+ attr_accessor :i, :octave
8
+ def initialize i, octave
9
+ @i = i
10
+ @octave = octave
11
+ update
12
+ end
13
+ def update
14
+ old_octave = @octave
15
+ @octave, @i = @i.divmod SYMBOLS.size
16
+ @octave += old_octave
17
+ end
18
+ def letter
19
+ SYMBOLS[@i]
20
+ end
21
+ end
22
+
23
+ class Alda::Sequence
24
+ def play_solfege solfege
25
+ v1; octave solfege.octave; note pitch solfege.letter
26
+ v2; octave solfege.octave-1; note pitch solfege.letter
27
+ end
28
+ def play_hanon ary, octave, delta
29
+ solfeges = ary.map { Solfege.new _1, octave }
30
+ 14.times do
31
+ solfeges.each do |solfege|
32
+ play_solfege solfege
33
+ solfege.i += delta
34
+ solfege.update
35
+ end
36
+ end
37
+ end
38
+ end
39
+
40
+ Alda::Score.new do
41
+ piano_; set_note_length 16
42
+ def play_hanon ary1, ary2
43
+ s do
44
+ play_hanon ary1, 3, 1
45
+ play_hanon ary2, 5, -1
46
+ end * 2
47
+ end
48
+ play_hanon [0, 2, 3, 4, 5, 4, 3, 2], [4, 2, 1, 0, -1, 0, 1, 2]
49
+ play_hanon [0, 2, 5, 4, 3, 4, 3, 2], [4, 1, -1, 0, 1, 0, 1, 2]
50
+ v1; o3; c2; v2; o2; c2
51
+ end.play
@@ -3,6 +3,5 @@
3
3
  require 'alda-rb'
4
4
 
5
5
  Alda::Score.new do
6
- piano_
7
- c; d; e; f; g2; g4; f; e; d; c2
6
+ piano_ c d e f g2 g4 f e d c2
8
7
  end.play
@@ -7,11 +7,11 @@ Alda::Score.new do
7
7
  quant 200
8
8
 
9
9
  key_signature 'f+ c+ g+'
10
- a8; b; +o; c; d; e; f; g; a; -o
11
- a; b; +o; ~c; d; e; ~f; ~g; a; -o
10
+ a8 b o! c d e f g a o?
11
+ a b o! c_ d e f_ g_ a o?
12
12
 
13
13
  key_signature [:g, :minor]
14
- g; a; b; +o; +c; d; e; +f; g; -o
14
+ g a b o! c! d e f! g o?
15
15
 
16
16
  key_signature e: [:flat], b: [:flat]
17
17
  g1_1/b/d
@@ -5,5 +5,5 @@ require 'alda-rb'
5
5
  Alda::Score.new do
6
6
  midi_calliope_lead_
7
7
  set_note_length 8
8
- (10...100).step(10) { |n| note midi_note n }
8
+ (10...100).step(10) { note midi_note _1 }
9
9
  end.play
data/examples/modes2.rb CHANGED
@@ -7,6 +7,6 @@ Alda::Score.new do
7
7
  piano_
8
8
  %i[ionian dorian phrygian lydian mixolydian aeolian locrian].each do |mode|
9
9
  key_sig [:c, mode]
10
- o4; c4; d8; e; f; g; a; b; +o; c1
10
+ o4 c4 d8 e f g a b o! c1
11
11
  end
12
12
  end.play
@@ -3,12 +3,12 @@
3
3
  require 'alda-rb'
4
4
 
5
5
  Alda::Score.new do
6
- piano_; set_duration 4
7
- harp_; set_duration 2; octave 3
6
+ piano_ set_duration 4
7
+ harp_ set_duration 2; octave 3
8
8
 
9
9
  piano_/harp_
10
- t { e; f; g }; t { a; b; +o; c }
10
+ t{ e f g }; t{ a b o! c }
11
11
 
12
12
  harp_
13
- t { d; e; f }; t { g; a; b }
13
+ t{ d e f }; t { g a b }
14
14
  end.play
data/examples/nesting.rb CHANGED
@@ -6,21 +6,21 @@ Alda::Score.new do
6
6
  piano_
7
7
 
8
8
  v1
9
- c8; d; e; f; g2_
9
+ c8 d e f g2_
10
10
 
11
11
  v2
12
- s{ c8; d }*2; e
13
- f; g*3
12
+ (c8 d)*2; e
13
+ f g*3
14
14
  t do
15
- t{ c; c }
15
+ t{ c c }
16
16
  s{ g/e/c; s{ d }*2 }
17
- s{ c; d }*5
17
+ (c d)*5
18
18
  end * 5
19
- a/b/c; b; c/e
19
+ a/b/c; b c/e
20
20
 
21
21
  v3
22
- a8; b; +o; c2_
22
+ a8 b o! c2_
23
23
 
24
24
  clarinet_
25
- a2; e
25
+ a2 e
26
26
  end.play
data/examples/phase.rb CHANGED
@@ -10,5 +10,5 @@ Alda::Score.new do
10
10
  tempo tempo
11
11
  end
12
12
  tempos.keys.lazy.map { __send__ _1 }.inject :/
13
- s{ e8; f; g }*99
13
+ (e8 f g)*99
14
14
  end.play
@@ -5,6 +5,6 @@ require 'alda-rb'
5
5
  Alda::Score.new do
6
6
  accordion_
7
7
  c500ms/e/g
8
- c1s/f/a
8
+ x{ c1s; f; a }
9
9
  c2s/e/g
10
10
  end.play
@@ -0,0 +1,27 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'alda-rb'
4
+
5
+ Alda::Score.new do
6
+ tempo! 160
7
+
8
+ riffA__ f8 f g! a o! c c d c o?
9
+ riffB__ b8? b? o! c! d f f g f o?
10
+ riffC__ o! c8 c d! e g g a g o?
11
+ riffD do
12
+ f8 f g! a o! c c d o? b o!
13
+ c c o? b? b? a a g g
14
+ end
15
+
16
+ rockinRiff do
17
+ riffA*4
18
+ riffB*2; riffA*2
19
+ riffC riffB riffD
20
+ end
21
+
22
+ electric_guitar_distorted_ 'guitar'; o2
23
+ tenor_saxophone_ 'sax'; o3
24
+
25
+ guitar_/sax_
26
+ rockinRiff*8
27
+ end.play
data/lib/alda-rb.rb CHANGED
@@ -1,35 +1,22 @@
1
+ require 'readline'
2
+ require 'set'
3
+ require 'stringio'
4
+ require 'irb/ruby-lex'
1
5
  require 'alda-rb/version'
6
+ require 'colorize'
7
+
8
+ {
9
+ Array => -> { "[#{map(&:to_alda_code).join ' '}]" },
10
+ Hash => -> { "{#{to_a.reduce(:+).map(&:to_alda_code).join ' '}}" },
11
+ String => -> { dump },
12
+ Symbol => -> { ?: + to_s },
13
+ Numeric => -> { inspect },
14
+ Range => -> { "#{first}-#{last}" },
15
+ TrueClass => -> { 'true' },
16
+ FalseClass => -> { 'false' },
17
+ NilClass => -> { 'nil' }
18
+ }.each { |klass, block| klass.define_method :to_alda_code, &block }
2
19
 
3
- class Array
4
- def to_alda_code
5
- "[#{map(&:to_alda_code).join ' '}]"
6
- end
7
- end
8
- class Hash
9
- def to_alda_code
10
- "{#{to_a.reduce(:+).map(&:to_alda_code).join ' '}}"
11
- end
12
- end
13
- class String
14
- def to_alda_code
15
- inspect
16
- end
17
- end
18
- class Symbol
19
- def to_alda_code
20
- ?: + to_s
21
- end
22
- end
23
- class Numeric
24
- def to_alda_code
25
- inspect
26
- end
27
- end
28
- class Range
29
- def to_alda_code
30
- "#{first}-#{last}"
31
- end
32
- end
33
20
  class Proc
34
21
  # Runs +self+ for +n+ times.
35
22
  def * n
@@ -41,19 +28,35 @@ class Proc
41
28
  end
42
29
  end
43
30
 
31
+ class StringIO
32
+ # Equivalent to #string.
33
+ def to_s
34
+ string
35
+ end
36
+ end
37
+
38
+ module Kernel
39
+ # Runs the alda command.
40
+ # Does not capture output.
41
+ # @example
42
+ # alda 'version'
43
+ # alda 'play', '-c', 'piano: a'
44
+ # alda 'repl'
45
+ def alda *args
46
+ system Alda.executable, *args
47
+ end
48
+ end
49
+
44
50
  # The module serving as a namespace.
45
51
  module Alda
46
52
 
47
- # The path to the +alda+ executable.
53
+ # The array of available subcommands of alda executable.
48
54
  #
49
- # The default value is <tt>"alda"</tt>,
50
- # which will depend on your PATH.
51
- singleton_class.attr_accessor :executable
52
- @executable = 'alda'
53
-
54
- # The method give Alda# ability to invoke +alda+ at the command line,
55
- # using +name+ as subcommand and +args+ as arguments.
56
- # +opts+ are converted to command line options.
55
+ # Alda# is able to invoke +alda+ at the command line.
56
+ # The subcommand is the name of the method invoked upon Alda#.
57
+ #
58
+ # The first argument (a hash) is interpreted as the options.
59
+ # The keyword arguments are interpreted as the subcommand options.
57
60
  #
58
61
  # The return value is the string output by the command in STDOUT.
59
62
  #
@@ -63,43 +66,72 @@ module Alda
63
66
  # # => "Client version: 1.4.0\nServer version: [27713] 1.4.0\n"
64
67
  # Alda.parse code: 'bassoon: o3 c'
65
68
  # # => "{\"chord-mode\":false,\"current-instruments\":...}\n"
66
- # Alda.sandwich
67
- # # => Alda::CommandLineError (Expected a command, got sandwich)
68
- def self.method_missing name, *args, **opts
69
- name = name.to_s.gsub ?_, ?-
70
- args.concat opts.map { |key, val|
71
- ["--#{key.to_s.gsub ?_, ?-}", val.to_s]
72
- }.flatten
73
- output = IO.popen [executable, name, *args], &:read
74
- raise CommandLineError.new $?, output if $?.exitstatus.nonzero?
75
- output
69
+ COMMANDS = %i[
70
+ help update repl up start_server init down stop_server
71
+ downup restart_server list status version play stop parse
72
+ instruments export
73
+ ].freeze
74
+
75
+ COMMANDS.each do |command|
76
+ define_method command do |*args, **opts|
77
+ block = ->key, val do
78
+ next unless val
79
+ args.push "--#{key.to_s.tr ?_, ?-}"
80
+ args.push val.to_s unless val == true
81
+ end
82
+ # executable
83
+ args.unshift Alda.executable
84
+ args.map! &:to_s
85
+ # options
86
+ Alda.options.each &block
87
+ # subcommand
88
+ args.push command.to_s
89
+ # subcommand options
90
+ opts.each &block
91
+ # subprocess
92
+ IO.popen(args, &:read).tap do
93
+ raise CommandLineError.new $?, _1 if $?.exitstatus.nonzero?
94
+ end
95
+ end
76
96
  end
77
97
 
98
+ # The path to the +alda+ executable.
99
+ #
100
+ # The default value is <tt>"alda"</tt>,
101
+ # which will depend on your PATH.
102
+ singleton_class.attr_accessor :executable
103
+ @executable = 'alda'
104
+
105
+ singleton_class.attr_reader :options
106
+ @options = {}
107
+
78
108
  # @return Whether the alda server is up.
79
- def self.up?
109
+ def up?
80
110
  status.include? 'up'
81
111
  end
82
112
 
83
113
  # @return Whether the alda server is down.
84
- def self.down?
114
+ def down?
85
115
  status.include? 'down'
86
116
  end
87
117
 
88
- # The error raised when one tries to run a non-existing subcommand
89
- # of +alda+.
90
- class CommandLineError < Exception
91
-
92
- # The <tt>Process::Status</tt> object representing the status of
93
- # the process that runs +alda+ command.
94
- attr_accessor :status
95
-
96
- # Create a CommandLineError# object.
97
- # @param status The status of the process running +alda+ command.
98
- # @param msg The exception message.
99
- def initialize status, msg = nil
100
- super msg
101
- @status = status
102
- end
118
+ module_function :up?, :down?, *COMMANDS
119
+
120
+ # Start a REPL session.
121
+ def self.repl
122
+ REPL.new.run
123
+ end
124
+
125
+ # Sets the options of alda command.
126
+ # Not the subcommand options.
127
+ def self.[] **opts
128
+ @options.merge! opts
129
+ self
130
+ end
131
+
132
+ # Clears the command line options.
133
+ def self.clear_options
134
+ @options.clear
103
135
  end
104
136
 
105
137
  # Including this module can make your class have the ability
@@ -111,35 +143,46 @@ module Alda
111
143
  # most of which are EventContainer# objects.
112
144
  attr_accessor :events
113
145
 
146
+ # The set containing the available variable names.
147
+ attr_accessor :variables
148
+
149
+ def on_contained
150
+ instance_eval &@block if @block
151
+ end
152
+
114
153
  # Make the object have the ability to appending its +events+
115
154
  # conveniently.
116
155
  #
117
156
  # Here is a list of sugar. When the name of a method meets certain
118
157
  # condition, the method is regarded as an event appended to +events+.
119
158
  #
120
- # 1. Starting with 2 lowercase letters and
159
+ # 1. Ending with 2 underlines: set variable. See SetVariable#.
160
+ #
161
+ # 2. Starting with 2 lowercase letters and
121
162
  # ending with underline character: instrument. See Part#.
122
163
  #
123
- # 2. Starting with 2 lowercase letters: inline lisp code.
124
- # See InlineLisp#.
164
+ # 3. Starting with 2 lowercase letters: inline lisp code,
165
+ # set variable, or get variable.
166
+ # One of the above three is chosen intelligently.
167
+ # See InlineLisp#, SetVariable#, GetVariable#.
125
168
  #
126
- # 3. Starting with "t": CRAM. See Cram#.
169
+ # 4. Starting with "t": CRAM. See Cram#.
127
170
  #
128
- # 4. Starting with one of "a", "b", ..., "g": note. See Note#.
171
+ # 5. Starting with one of "a", "b", ..., "g": note. See Note#.
129
172
  #
130
- # 5. Starting with "r": rest. See Rest#.
173
+ # 6. Starting with "r": rest. See Rest#.
131
174
  #
132
- # 6. "x": chord. See Chord#.
175
+ # 7. "x": chord. See Chord#.
133
176
  #
134
- # 7. "s": sequence. See Sequence#.
177
+ # 8. "s": sequence. See Sequence#.
135
178
  #
136
- # 8. Starting with "o": octave. See Octave#.
179
+ # 9. Starting with "o": octave. See Octave#.
137
180
  #
138
- # 9. Starting with "v": voice. See Voice#.
181
+ # 10. Starting with "v": voice. See Voice#.
139
182
  #
140
- # 10. Starting with "__" (2 underlines): at marker. See AtMarker#.
183
+ # 11. Starting with "__" (2 underlines): at marker. See AtMarker#.
141
184
  #
142
- # 11. Starting with "_" (underline): marker. See Marker#.
185
+ # 12. Starting with "_" (underline): marker. See Marker#.
143
186
  #
144
187
  # Notes cannot have dots.
145
188
  # To tie multiple durations, +_+ is used instead of +~+.
@@ -151,29 +194,59 @@ module Alda
151
194
  # @see #initialize.
152
195
  # @return an EventContainer# object.
153
196
  def method_missing name, *args, &block
197
+ if @parent&.respond_to? name, true
198
+ return @parent.__send__ name, *args, &block
199
+ end
200
+ sequence_sugar = ->event do
201
+ if args.size == 1
202
+ joined = args.first
203
+ unless (got = @events.pop) == (expected = joined)
204
+ raise OrderError.new expected, got
205
+ end
206
+ Sequence.join event, joined
207
+ else
208
+ event
209
+ end
210
+ end
154
211
  case
155
- when /^(?<part>[a-z][a-z].*)_$/ =~ name
156
- Part.new [part], args.first
157
- when /^[a-z][a-z].*$/ =~ name
158
- InlineLisp.new name, *args
159
- when /^t(?<duration>.*)$/ =~ name
212
+ when /\A(?<head>[a-z][a-z].*)__\z/ =~ name
213
+ SetVariable.new head, *args, &block
214
+ when /\A(?<part>[a-z][a-z].*)_\z/ =~ name
215
+ if args.first.is_a? String
216
+ Part.new [part], args.first
217
+ else
218
+ sequence_sugar.(Part.new [part])
219
+ end
220
+ when /\A[a-z][a-z].*\z/ =~ name
221
+ if block
222
+ SetVariable.new name, *args, &block
223
+ elsif has_variable?(name) && (args.empty? || args.size == 1 && args.first.is_a?(Event))
224
+ sequence_sugar.(GetVariable.new name)
225
+ else
226
+ InlineLisp.new name, *args
227
+ end
228
+ when /\At(?<duration>.*)\z/ =~ name
160
229
  Cram.new duration, &block
161
- when /^(?<pitch>[a-g])(?<duration>.*)$/ =~ name
162
- Note.new pitch, duration
163
- when /^r(?<duration>.*)$/ =~ name
164
- Rest.new duration
165
- when /^x$/ =~ name
230
+ when /\A(?<pitch>[a-g])(?<duration>.*)\z/ =~ name
231
+ sequence_sugar.(Note.new pitch, duration)
232
+ when /\Ar(?<duration>.*)\z/ =~ name
233
+ sequence_sugar.(Rest.new duration)
234
+ when /\Ax\z/ =~ name
166
235
  Chord.new &block
167
- when /^s$/ =~ name
236
+ when /\As\z/ =~ name
168
237
  Sequence.new *args, &block
169
- when /^o(?<num>\d*)$/ =~ name
170
- Octave.new num
171
- when /^v(?<num>\d+)$/ =~ name
172
- Voice.new num
173
- when /^__(?<head>.+)$/ =~ name
174
- AtMarker.new head
175
- when /^_(?<head>.+)$/ =~ name
176
- Marker.new head
238
+ when /\Ao!\z/ =~ name
239
+ sequence_sugar.(Octave.new('').tap { _1.up_or_down = 1})
240
+ when /\Ao\?\z/ =~ name
241
+ sequence_sugar.(Octave.new('').tap { _1.up_or_down = -1})
242
+ when /\Ao(?<num>\d*)\z/ =~ name
243
+ sequence_sugar.(Octave.new num)
244
+ when /\Av(?<num>\d+)\z/ =~ name
245
+ sequence_sugar.(Voice.new num)
246
+ when /\A__(?<head>.+)\z/ =~ name
247
+ sequence_sugar.(AtMarker.new head)
248
+ when /\A_(?<head>.+)\z/ =~ name
249
+ sequence_sugar.(Marker.new head)
177
250
  else
178
251
  super
179
252
  end.then do |event|
@@ -181,6 +254,10 @@ module Alda
181
254
  end.tap &@events.method(:push)
182
255
  end
183
256
 
257
+ def has_variable? name
258
+ @variables.include?(name) || !!@parent&.has_variable?(name)
259
+ end
260
+
184
261
  # Append the events of another EventList# object here.
185
262
  # This method covers the disadvantage of alda's being unable to
186
263
  # import scores from other files.
@@ -192,18 +269,19 @@ module Alda
192
269
  # @param block to be passed with the EventList# object as +self+.
193
270
  # @example
194
271
  # Alda::Score.new do
195
- # tempo! 108 # inline lisp
196
- # piano_ # piano part
197
- # o4 # octave 4
198
- # c8; d; e; f # notes
199
- # g4; g; a; f; g; e; f; d; e; c
200
- # d4_8 # cannot have '~', use '_' instead
201
- # o3 b8 o4 c2
272
+ # tempo! 108 # inline lisp
273
+ # piano_ # piano part
274
+ # o4 # octave 4
275
+ # c8; d; e; f # notes
276
+ # g4 g a f g e f d e c # a sequence
277
+ # d4_8 # cannot have '~', use '_' instead
278
+ # o3 b8 o4 c2 # a sequence
202
279
  # end
203
280
  # # => #<Alda::Score:0x... @events=[...]>
204
281
  def initialize &block
205
282
  @events ||= []
206
- instance_eval &block if block
283
+ @variables ||= Set.new
284
+ @block ||= block
207
285
  end
208
286
 
209
287
  # Same as #events
@@ -231,7 +309,6 @@ module Alda
231
309
  # Alda::Score.new { piano_; c; d; e }.play from: 1
232
310
  # # (plays only an E note)
233
311
  def play **opts
234
- Alda.stop
235
312
  Alda.play code: self, **opts
236
313
  end
237
314
 
@@ -253,9 +330,231 @@ module Alda
253
330
  Alda.export code: self, **opts
254
331
  end
255
332
 
333
+ # Saves the alda codes into a file.
334
+ def save filename
335
+ File.open(filename, 'w') { _1.puts to_s }
336
+ end
337
+
338
+ # Loads alda codes from a file.
339
+ def load filename
340
+ event = InlineLisp.new :alda_code, File.read(filename)
341
+ @events.push event
342
+ event
343
+ end
344
+
345
+ # @return Alda codes.
256
346
  def to_s
257
347
  events_alda_codes
258
348
  end
349
+
350
+ # The initialization.
351
+ def initialize(...)
352
+ super
353
+ on_contained
354
+ end
355
+
356
+ # Clears all the events and variables.
357
+ def clear
358
+ @events.clear
359
+ @variables.clear
360
+ self
361
+ end
362
+ end
363
+
364
+ # An encapsulation for the REPL session for alda-rb.
365
+ class REPL
366
+
367
+ # The score object used in REPL.
368
+ # Includes Alda#, so it can refer to alda commandline.
369
+ class TempScore < Score
370
+ include Alda
371
+
372
+ Score.instance_methods(false).each do |meth|
373
+ define_method meth, Score.instance_method(meth)
374
+ end
375
+
376
+ def initialize session
377
+ super()
378
+ @session = session
379
+ end
380
+
381
+ def to_s
382
+ history
383
+ end
384
+
385
+ def history
386
+ @session.history.to_s
387
+ end
388
+
389
+ def clear_history
390
+ @session.clear_history
391
+ end
392
+
393
+ def get_binding
394
+ binding
395
+ end
396
+
397
+ alias quit exit
398
+ end
399
+
400
+ # The history.
401
+ attr_reader :history
402
+
403
+ # Initialization.
404
+ def initialize
405
+ @score = TempScore.new self
406
+ @binding = @score.get_binding
407
+ @lex = RubyLex.new
408
+ @history = StringIO.new
409
+ end
410
+
411
+ # Runs the session. Includes the start, the main loop, and the termination.
412
+ def run
413
+ start
414
+ while code = rb_code
415
+ break unless process_rb_code code
416
+ end
417
+ terminate
418
+ end
419
+
420
+ # Starts the session.
421
+ def start
422
+ end
423
+
424
+ # Reads the next Ruby codes input in the REPL session.
425
+ # It can intelligently continue reading if the code is not complete yet.
426
+ def rb_code
427
+ result = ''
428
+ begin
429
+ buf = Readline.readline '> '.green, true
430
+ return unless buf
431
+ result.concat buf, ?\n
432
+ ltype, indent, continue, block_open = @lex.check_state result
433
+ rescue Interrupt
434
+ $stdout.puts
435
+ retry
436
+ end while ltype || indent.nonzero? || continue || block_open
437
+ result
438
+ end
439
+
440
+ # Processes the Ruby codes read.
441
+ # Sending it to a score and sending the result to alda.
442
+ # @return +true+ for continue looping, +false+ for breaking the loop.
443
+ def process_rb_code code
444
+ @score.clear
445
+ begin
446
+ @binding.eval code
447
+ rescue StandardError, ScriptError => e
448
+ $stderr.print e.full_message
449
+ return true
450
+ rescue Interrupt
451
+ return true
452
+ rescue SystemExit
453
+ return false
454
+ end
455
+ code = @score.events_alda_codes
456
+ unless code.empty?
457
+ $stdout.puts code.yellow
458
+ play_score code
459
+ end
460
+ true
461
+ end
462
+
463
+ # Tries to run the block and rescue CommandLineError#.
464
+ def try_command # :block:
465
+ begin
466
+ yield
467
+ rescue CommandLineError => e
468
+ puts e.message.red
469
+ end
470
+ end
471
+
472
+ # Plays the score.
473
+ def play_score code
474
+ try_command do
475
+ Alda.play code: code, history: @history
476
+ @history.puts code
477
+ end
478
+ end
479
+
480
+ # Terminates the REPL session.
481
+ def terminate
482
+ clear_history
483
+ end
484
+
485
+ # Clears the history.
486
+ def clear_history
487
+ @history = StringIO.new
488
+ end
489
+ end
490
+
491
+ # The error is raised when one tries to
492
+ # run a non-existing subcommand of +alda+.
493
+ class CommandLineError < StandardError
494
+
495
+ # The <tt>Process::Status</tt> object representing the status of
496
+ # the process that runs +alda+ command.
497
+ attr_reader :status
498
+
499
+ # The port on which the problematic Alda server runs.
500
+ # @example
501
+ # begin
502
+ # Alda.play({port: 1108}, code: "y")
503
+ # rescue CommandLineError => e
504
+ # e.port # => 1108
505
+ # end
506
+ attr_reader :port
507
+
508
+ # Create a CommandLineError# object.
509
+ # @param status The status of the process running +alda+ command.
510
+ # @param msg The exception message.
511
+ def initialize status, msg = nil
512
+ if match = msg&.match(/^\[(?<port>\d+)\]\sERROR\s(?<message>.*)$/)
513
+ super match[:message]
514
+ @port = match[:port].to_i
515
+ else
516
+ super msg
517
+ @port = nil
518
+ end
519
+ @status = status
520
+ end
521
+ end
522
+
523
+ # This error is raised when one tries to
524
+ # append events in an EventList# in a wrong order.
525
+ # @example
526
+ # Alda::Score.new do
527
+ # motif = f4 f e e d d c2
528
+ # g4 f e d c2 # It commented out, error will not occur
529
+ # c4 c g g a a g2 motif # (OrderError)
530
+ # end
531
+ class OrderError < StandardError
532
+
533
+ # The expected element gotten if it is of the correct order.
534
+ # @see #got
535
+ # @example
536
+ # Alda::Score.new do
537
+ # motif = f4 f e e d d c2
538
+ # g4 f e d c2
539
+ # p @events.size # => 2
540
+ # c4 c g g a a g2 motif
541
+ # rescue OrderError => e
542
+ # p @events.size # => 1
543
+ # p e.expected # => #<Alda::EventContainer:...>
544
+ # p e.got # => #<Alda::EventContainer:...>
545
+ # end
546
+ attr_reader :expected
547
+
548
+ # The actually gotten element.
549
+ # For an example, see #expected.
550
+ # @see #expected
551
+ attr_reader :got
552
+
553
+ def initialize expected, got
554
+ super 'events are out of order'
555
+ @expected = expected
556
+ @got = got
557
+ end
259
558
  end
260
559
 
261
560
  # The class of elements of EventList#events.
@@ -266,9 +565,13 @@ module Alda
266
565
  # object in the middle.
267
566
  attr_accessor :parent
268
567
 
568
+ # The EventContainer# object that contains it.
569
+ # It may be +nil+, especially probably when
570
+ # it itself is an EventContainer#.
571
+ attr_accessor :container
572
+
269
573
  # The callback invoked when it is contained in an EventContainer#.
270
- # It is overridden in InlineLisp#, so be aware if you want to
271
- # override InlineLisp#on_contained.
574
+ # It is overridden in InlineLisp# and EventList#.
272
575
  # @example
273
576
  # class Alda::Note
274
577
  # def on_contained
@@ -302,9 +605,8 @@ module Alda
302
605
  def initialize event, parent
303
606
  @event = event
304
607
  @parent = parent
305
- @event.parent = @parent
306
608
  @labels = []
307
- @event.on_contained
609
+ on_containing
308
610
  end
309
611
 
310
612
  # Make #event a Chord# object.
@@ -318,7 +620,9 @@ module Alda
318
620
  # Alda::Score.new { violin_/viola_/cello_; e; f; g}.play
319
621
  # # (plays notes E, F, G with three instruments simultaneously)
320
622
  def / other
321
- raise unless other == @parent.events.pop
623
+ unless (expected = other) == (got = @parent.events.pop)
624
+ raise OrderError.new expected, got
625
+ end
322
626
  @event =
323
627
  if @event.is_a? Part
324
628
  Part.new @event.names + other.event.names, other.event.arg
@@ -339,13 +643,29 @@ module Alda
339
643
 
340
644
  # Marks repetition.
341
645
  def * num
342
- @count = num
646
+ @count = (@count || 1) * num
647
+ self
343
648
  end
344
649
 
345
650
  # Marks alternative repetition.
346
651
  def % labels
347
- labels = [labels] unless labels.respond_to? :to_a
652
+ labels = [labels] unless labels.is_a? Array
348
653
  @labels.replace labels.to_a
654
+ self
655
+ end
656
+
657
+ def event= event
658
+ @event = event
659
+ on_containing
660
+ @event
661
+ end
662
+
663
+ def on_containing
664
+ if @event
665
+ @event.container = self
666
+ @event.parent = @parent
667
+ @event.on_contained
668
+ end
349
669
  end
350
670
 
351
671
  def method_missing name, *args
@@ -363,7 +683,7 @@ module Alda
363
683
 
364
684
  # The arguments passed to the lisp function.
365
685
  # Its elements can be
366
- # Array#, Hash#, Numeric#, String#, Symbol#, or InlineLisp#.
686
+ # Array#, Hash#, Numeric#, String#, Symbol#, or Event#.
367
687
  attr_accessor :args
368
688
 
369
689
  # The underlines in +head+ will be converted to hyphens.
@@ -377,8 +697,11 @@ module Alda
377
697
  end
378
698
 
379
699
  def on_contained
700
+ super
380
701
  @args.reverse_each do |event|
381
- raise if event.is_a?(Event) && event != @parent.events.pop
702
+ if event.is_a?(Event) && (expected = event) != (got = @parent.events.pop)
703
+ raise OrderError.new expected, got
704
+ end
382
705
  end
383
706
  end
384
707
  end
@@ -394,9 +717,32 @@ module Alda
394
717
  attr_accessor :duration
395
718
 
396
719
  # The underlines in +duration+ will be converted to +~+.
720
+ # Exclamation mark and question mark in +duration+
721
+ # will be interpreted as accidentals in #pitch.
722
+ #
723
+ # The number of underlines at the end of +duration+ means:
724
+ # neither natural nor slur if 0,
725
+ # natural if 1,
726
+ # slur if 2,
727
+ # both natural and slur if 3.
397
728
  def initialize pitch, duration
398
729
  @pitch = pitch.to_s
399
- @duration = duration.to_s.gsub ?_, ?~
730
+ @duration = duration.to_s.tr ?_, ?~
731
+ case @duration[-1]
732
+ when ?! # sharp
733
+ @pitch.concat ?+
734
+ @duration[-1] = ''
735
+ when ?? # flat
736
+ @pitch.concat ?-
737
+ @duration[-1] = ''
738
+ end
739
+ waves = /(?<str>~+)\z/ =~ @duration ? str.size : return
740
+ @duration[@duration.length - waves..] = ''
741
+ if waves >= 2
742
+ waves -= 2
743
+ @duration.concat ?~
744
+ end
745
+ @pitch.concat ?_ * waves
400
746
  end
401
747
 
402
748
  # Append a sharp sign after #pitch.
@@ -441,7 +787,7 @@ module Alda
441
787
 
442
788
  # Underlines in +duration+ will be converted to +~+.
443
789
  def initialize duration
444
- @duration = duration.to_s.gsub ?_, ?~
790
+ @duration = duration.to_s.tr ?_, ?~
445
791
  end
446
792
 
447
793
  def to_alda_code
@@ -525,7 +871,7 @@ module Alda
525
871
  attr_accessor :arg
526
872
 
527
873
  def initialize names, arg = nil
528
- @names = names.map { |name| name.to_s.gsub ?_, ?- }
874
+ @names = names.map { |name| name.to_s.tr ?_, ?- }
529
875
  @arg = arg
530
876
  end
531
877
 
@@ -535,16 +881,29 @@ module Alda
535
881
  result.concat ?:
536
882
  end
537
883
 
884
+ # Enables dot accessor.
538
885
  # @example
539
886
  # Alda::Score.new do
540
887
  # violin_/viola_/cello_('strings'); g1_1_1
541
888
  # strings_.cello_; -o; c1_1_1
542
889
  # end.play
543
890
  def method_missing name, *args
544
- name = name.to_s
545
- return super unless name[-1] == ?_
546
- name[-1] = ''
547
- @names.last.concat ?., name
891
+ str = name.to_s
892
+ return super unless str[-1] == ?_
893
+ str[-1] = ''
894
+ @names.last.concat ?., str
895
+ if args.size == 1
896
+ joined = args.first
897
+ unless (got = @parent.events.pop) == (expected = joined)
898
+ raise OrderError.new expected, got
899
+ end
900
+ unless @container
901
+ @container = EventContainer.new nil, @parent
902
+ @parent.events.delete self
903
+ @parent.push @container
904
+ end
905
+ @container.event = Sequence.join self, joined
906
+ end
548
907
  end
549
908
  end
550
909
 
@@ -594,7 +953,7 @@ module Alda
594
953
 
595
954
  # Underlines in +name+ is converted to hyphens.
596
955
  def initialize name
597
- @name = name.to_s.gsub ?_, ?-
956
+ @name = name.to_s.tr ?_, ?-
598
957
  end
599
958
 
600
959
  def to_alda_code
@@ -611,7 +970,7 @@ module Alda
611
970
 
612
971
  # Underlines in +name+ is converted to hyphens.
613
972
  def initialize name
614
- @name = name.to_s.gsub ?_, ?-
973
+ @name = name.to_s.tr ?_, ?-
615
974
  end
616
975
 
617
976
  def to_alda_code
@@ -623,8 +982,94 @@ module Alda
623
982
  class Sequence < Event
624
983
  include EventList
625
984
 
985
+ # Using this module can fix a bug of Array#flatten.
986
+ # @example
987
+ # def (a = Object.new).method_missing(...)
988
+ # Object.new
989
+ # end
990
+ # [a].flatten rescue $! # => #<TypeError:...>
991
+ # using Alda::Sequence::RefineFlatten
992
+ # [a].flatten # => [#<Object:...>]
993
+ module RefineFlatten
994
+ refine Array do
995
+ def flatten
996
+ each_with_object [] do |element, result|
997
+ if element.is_a? Array
998
+ result.push *element.flatten
999
+ else
1000
+ result.push element
1001
+ end
1002
+ end
1003
+ end
1004
+ end
1005
+ end
1006
+ using RefineFlatten
1007
+
626
1008
  def to_alda_code
627
1009
  @events.to_alda_code
628
1010
  end
1011
+
1012
+ # Creates a Sequence# object by joining +events+.
1013
+ # The EventContainer# objects are extracted,
1014
+ # and the Sequence# objects are flattened.
1015
+ def self.join *events
1016
+ new do
1017
+ @events = events.map do |event|
1018
+ while event.is_a?(EventContainer) && !event.count && event.labels.empty?
1019
+ event = event.event
1020
+ end
1021
+ event.is_a?(Sequence) ? event.events : event
1022
+ end.flatten
1023
+ end
1024
+ end
1025
+ end
1026
+
1027
+ # A set-variable event.
1028
+ # Includes EventList#.
1029
+ class SetVariable < Event
1030
+ include EventList
1031
+
1032
+ # The name of the variable.
1033
+ attr_accessor :name
1034
+
1035
+ # The events passed to it using arguments instead of a block.
1036
+ attr_reader :original_events
1037
+
1038
+ def initialize name, *events, &block
1039
+ @name = name.to_sym
1040
+ @original_events = events
1041
+ @events = events.clone
1042
+ super &block
1043
+ end
1044
+
1045
+ # Specially, the result ends with a newline.
1046
+ def to_alda_code
1047
+ "#@name = #{events_alda_codes}\n"
1048
+ end
1049
+
1050
+ def on_contained
1051
+ super
1052
+ @parent.variables.add @name
1053
+ @original_events.reverse_each do |event|
1054
+ unless (expected = event) == (got = @parent.events.pop)
1055
+ raise OrderError.new expected, got
1056
+ end
1057
+ end
1058
+ end
1059
+ end
1060
+
1061
+ # A get-variable event
1062
+ class GetVariable < Event
1063
+
1064
+ # The name of the variable
1065
+ attr_accessor :name
1066
+
1067
+ def initialize name
1068
+ @name = name
1069
+ end
1070
+
1071
+ def to_alda_code
1072
+ @name.to_s
1073
+ end
629
1074
  end
630
1075
  end
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module Alda
4
- VERSION = '0.1.2'
4
+ VERSION = '0.1.4'
5
5
  end
metadata CHANGED
@@ -1,18 +1,18 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: alda-rb
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.1.2
4
+ version: 0.1.4
5
5
  platform: ruby
6
6
  authors:
7
7
  - Ulysses Zhan
8
8
  autorequire:
9
9
  bindir: exe
10
10
  cert_chain: []
11
- date: 2020-04-16 00:00:00.000000000 Z
11
+ date: 2020-04-24 00:00:00.000000000 Z
12
12
  dependencies: []
13
13
  description:
14
14
  email:
15
- - 2938747508@qq.com
15
+ - UlyssesZhan@gmail.com
16
16
  executables: []
17
17
  extensions: []
18
18
  extra_rdoc_files: []
@@ -32,6 +32,7 @@ files:
32
32
  - examples/clapping_music.rb
33
33
  - examples/dot_accessor.rb
34
34
  - examples/entropy.rb
35
+ - examples/hanon.rb
35
36
  - examples/hello_world.rb
36
37
  - examples/key_signature.rb
37
38
  - examples/midi_note_numbers.rb
@@ -41,6 +42,7 @@ files:
41
42
  - examples/phase.rb
42
43
  - examples/seconds_and_milliseconds.rb
43
44
  - examples/showcase.rb
45
+ - examples/variables.rb
44
46
  - lib/alda-rb.rb
45
47
  - lib/alda-rb/version.rb
46
48
  homepage: https://github.com/UlyssesZh/alda-rb
@@ -49,6 +51,7 @@ licenses:
49
51
  metadata:
50
52
  homepage_uri: https://github.com/UlyssesZh/alda-rb
51
53
  source_code_uri: https://github.com/UlyssesZh/alda-rb
54
+ changelog_uri: https://github.com/UlyssesZh/alda-rb/releases
52
55
  post_install_message:
53
56
  rdoc_options: []
54
57
  require_paths:
@@ -57,7 +60,7 @@ required_ruby_version: !ruby/object:Gem::Requirement
57
60
  requirements:
58
61
  - - ">="
59
62
  - !ruby/object:Gem::Version
60
- version: 2.3.0
63
+ version: 2.7.0
61
64
  required_rubygems_version: !ruby/object:Gem::Requirement
62
65
  requirements:
63
66
  - - ">="