require 'readline' require 'set' require 'stringio' require 'irb/ruby-lex' require 'alda-rb/version' require 'colorize' { Array => -> { "[#{map(&:to_alda_code).join ' '}]" }, Hash => -> { "{#{to_a.reduce(:+).map(&:to_alda_code).join ' '}}" }, String => -> { dump }, Symbol => -> { ?: + to_s }, Numeric => -> { inspect }, Range => -> { "#{first}-#{last}" }, TrueClass => -> { 'true' }, FalseClass => -> { 'false' }, NilClass => -> { 'nil' } }.each { |klass, block| klass.define_method :to_alda_code, &block } class Proc # Runs +self+ for +n+ times. def * n if !lambda? || arity == 1 n.times &self else n.times { self.() } end end end class StringIO # Equivalent to #string. def to_s string end end module Kernel # Runs the alda command. # Does not capture output. # @example # alda 'version' # alda 'play', '-c', 'piano: a' # alda 'repl' def alda *args system Alda.executable, *args end end # The module serving as a namespace. module Alda # The array of available subcommands of alda executable. # # Alda# is able to invoke +alda+ at the command line. # The subcommand is the name of the method invoked upon Alda#. # # The first argument (a hash) is interpreted as the options. # The keyword arguments are interpreted as the subcommand options. # # The return value is the string output by the command in STDOUT. # # If the exit code is nonzero, a CommandLineError# is raised. # @example # Alda.version # # => "Client version: 1.4.0\nServer version: [27713] 1.4.0\n" # Alda.parse code: 'bassoon: o3 c' # # => "{\"chord-mode\":false,\"current-instruments\":...}\n" COMMANDS = %i[ help update repl up start_server init down stop_server downup restart_server list status version play stop parse instruments export ].freeze COMMANDS.each do |command| define_method command do |*args, **opts| block = ->key, val do next unless val args.push "--#{key.to_s.tr ?_, ?-}" args.push val.to_s unless val == true end # executable args.unshift Alda.executable args.map! &:to_s # options Alda.options.each &block # subcommand args.push command.to_s # subcommand options opts.each &block # subprocess IO.popen(args, &:read).tap do raise CommandLineError.new $?, _1 if $?.exitstatus.nonzero? end end end # The path to the +alda+ executable. # # The default value is "alda", # which will depend on your PATH. singleton_class.attr_accessor :executable @executable = 'alda' singleton_class.attr_reader :options @options = {} # @return Whether the alda server is up. def up? status.include? 'up' end # @return Whether the alda server is down. def down? status.include? 'down' end module_function :up?, :down?, *COMMANDS # Start a REPL session. def self.repl REPL.new.run end # Sets the options of alda command. # Not the subcommand options. def self.[] **opts @options.merge! opts self end # Clears the command line options. def self.clear_options @options.clear end # Including this module can make your class have the ability # to have an event list. # See docs below to get an overview of its functions. module EventList # The array containing the events (Event# objects), # most of which are EventContainer# objects. attr_accessor :events # The set containing the available variable names. attr_accessor :variables def on_contained instance_eval &@block if @block end # Make the object have the ability to appending its +events+ # conveniently. # # Here is a list of sugar. When the name of a method meets certain # condition, the method is regarded as an event appended to +events+. # # 1. Ending with 2 underlines: set variable. See SetVariable#. # # 2. Starting with 2 lowercase letters and # ending with underline character: instrument. See Part#. # # 3. Starting with 2 lowercase letters: inline lisp code, # set variable, or get variable. # One of the above three is chosen intelligently. # See InlineLisp#, SetVariable#, GetVariable#. # # 4. Starting with "t": CRAM. See Cram#. # # 5. Starting with one of "a", "b", ..., "g": note. See Note#. # # 6. Starting with "r": rest. See Rest#. # # 7. "x": chord. See Chord#. # # 8. "s": sequence. See Sequence#. # # 9. Starting with "o": octave. See Octave#. # # 10. Starting with "v": voice. See Voice#. # # 11. Starting with "__" (2 underlines): at marker. See AtMarker#. # # 12. Starting with "_" (underline): marker. See Marker#. # # Notes cannot have dots. # To tie multiple durations, +_+ is used instead of +~+. # # All the appended events are contained in an EventContainer# object, # which is to be returned. # # These sugars forms a DSL. # @see #initialize. # @return an EventContainer# object. def method_missing name, *args, &block if @parent&.respond_to? name, true return @parent.__send__ name, *args, &block end sequence_sugar = ->event do if args.size == 1 joined = args.first unless (got = @events.pop) == (expected = joined) raise OrderError.new expected, got end Sequence.join event, joined else event end end case when /\A(?[a-z][a-z].*)__\z/ =~ name SetVariable.new head, *args, &block when /\A(?[a-z][a-z].*)_\z/ =~ name if args.first.is_a? String Part.new [part], args.first else sequence_sugar.(Part.new [part]) end when /\A[a-z][a-z].*\z/ =~ name if block SetVariable.new name, *args, &block elsif has_variable?(name) && (args.empty? || args.size == 1 && args.first.is_a?(Event)) sequence_sugar.(GetVariable.new name) else InlineLisp.new name, *args end when /\At(?.*)\z/ =~ name Cram.new duration, &block when /\A(?[a-g])(?.*)\z/ =~ name sequence_sugar.(Note.new pitch, duration) when /\Ar(?.*)\z/ =~ name sequence_sugar.(Rest.new duration) when /\Ax\z/ =~ name Chord.new &block when /\As\z/ =~ name Sequence.new *args, &block when /\Ao!\z/ =~ name sequence_sugar.(Octave.new('').tap { _1.up_or_down = 1}) when /\Ao\?\z/ =~ name sequence_sugar.(Octave.new('').tap { _1.up_or_down = -1}) when /\Ao(?\d*)\z/ =~ name sequence_sugar.(Octave.new num) when /\Av(?\d+)\z/ =~ name sequence_sugar.(Voice.new num) when /\A__(?.+)\z/ =~ name sequence_sugar.(AtMarker.new head) when /\A_(?.+)\z/ =~ name sequence_sugar.(Marker.new head) else super end.then do |event| EventContainer.new event, self end.tap &@events.method(:push) end def has_variable? name @variables.include?(name) || !!@parent&.has_variable?(name) end # Append the events of another EventList# object here. # This method covers the disadvantage of alda's being unable to # import scores from other files. # See https://github.com/alda-lang/alda-core/issues/8. def import event_list @events.concat event_list.events end # @param block to be passed with the EventList# object as +self+. # @example # Alda::Score.new do # tempo! 108 # inline lisp # piano_ # piano part # o4 # octave 4 # c8; d; e; f # notes # g4 g a f g e f d e c # a sequence # d4_8 # cannot have '~', use '_' instead # o3 b8 o4 c2 # a sequence # end # # => # def initialize &block @events ||= [] @variables ||= Set.new @block ||= block end # Same as #events def to_a @events end # Join the alda codes of #events with a specified delimiter. # Returns a string representing the result. def events_alda_codes delimiter = ' ' @events.map(&:to_alda_code).join delimiter end end # The class mixes in EventList# and provides methods to play or parse. class Score include EventList # Plays the score. # @return The command line output of the +alda+ command. # @example # Alda::Score.new { piano_; c; d; e }.play # # => "[27713] Parsing/evaluating...\n[27713] Playing...\n" # # (and plays the sound) # Alda::Score.new { piano_; c; d; e }.play from: 1 # # (plays only an E note) def play **opts Alda.play code: self, **opts end # Parses the score. # @return The JSON string of the parse result. # @example # Alda::Score.new { piano_; c }.parse output: :events # # => "[{\"event-type\":...}]\n" def parse **opts Alda.parse code: self, **opts end # Exports the score. # @return The command line output of the +alda+ command. # @example # Alda::Score.new { piano_; c }.export output: 'temp.mid' # # (outputs a midi file called temp.mid) def export **opts Alda.export code: self, **opts end # Saves the alda codes into a file. def save filename File.open(filename, 'w') { _1.puts to_s } end # Loads alda codes from a file. def load filename event = InlineLisp.new :alda_code, File.read(filename) @events.push event event end # @return Alda codes. def to_s events_alda_codes end # The initialization. def initialize(...) super on_contained end # Clears all the events and variables. def clear @events.clear @variables.clear self end end # An encapsulation for the REPL session for alda-rb. class REPL # The score object used in REPL. # Includes Alda#, so it can refer to alda commandline. class TempScore < Score include Alda Score.instance_methods(false).each do |meth| define_method meth, Score.instance_method(meth) end def initialize session super() @session = session end def to_s history end def history @session.history.to_s end def clear_history @session.clear_history end def get_binding binding end alias quit exit end # The history. attr_reader :history # Initialization. def initialize @score = TempScore.new self @binding = @score.get_binding @lex = RubyLex.new @history = StringIO.new end # Runs the session. Includes the start, the main loop, and the termination. def run start while code = rb_code break unless process_rb_code code end terminate end # Starts the session. def start end # Reads the next Ruby codes input in the REPL session. # It can intelligently continue reading if the code is not complete yet. def rb_code result = '' begin buf = Readline.readline '> '.green, true return unless buf result.concat buf, ?\n ltype, indent, continue, block_open = @lex.check_state result rescue Interrupt $stdout.puts retry end while ltype || indent.nonzero? || continue || block_open result end # Processes the Ruby codes read. # Sending it to a score and sending the result to alda. # @return +true+ for continue looping, +false+ for breaking the loop. def process_rb_code code @score.clear begin @binding.eval code rescue StandardError, ScriptError => e $stderr.print e.full_message return true rescue Interrupt return true rescue SystemExit return false end code = @score.events_alda_codes unless code.empty? $stdout.puts code.yellow play_score code end true end # Tries to run the block and rescue CommandLineError#. def try_command # :block: begin yield rescue CommandLineError => e puts e.message.red end end # Plays the score. def play_score code try_command do Alda.play code: code, history: @history @history.puts code end end # Terminates the REPL session. def terminate clear_history end # Clears the history. def clear_history @history = StringIO.new end end # The error is raised when one tries to # run a non-existing subcommand of +alda+. class CommandLineError < StandardError # The Process::Status object representing the status of # the process that runs +alda+ command. attr_reader :status # The port on which the problematic Alda server runs. # @example # begin # Alda.play({port: 1108}, code: "y") # rescue CommandLineError => e # e.port # => 1108 # end attr_reader :port # Create a CommandLineError# object. # @param status The status of the process running +alda+ command. # @param msg The exception message. def initialize status, msg = nil if match = msg&.match(/^\[(?\d+)\]\sERROR\s(?.*)$/) super match[:message] @port = match[:port].to_i else super msg @port = nil end @status = status end end # This error is raised when one tries to # append events in an EventList# in a wrong order. # @example # Alda::Score.new do # motif = f4 f e e d d c2 # g4 f e d c2 # It commented out, error will not occur # c4 c g g a a g2 motif # (OrderError) # end class OrderError < StandardError # The expected element gotten if it is of the correct order. # @see #got # @example # Alda::Score.new do # motif = f4 f e e d d c2 # g4 f e d c2 # p @events.size # => 2 # c4 c g g a a g2 motif # rescue OrderError => e # p @events.size # => 1 # p e.expected # => # # p e.got # => # # end attr_reader :expected # The actually gotten element. # For an example, see #expected. # @see #expected attr_reader :got def initialize expected, got super 'events are out of order' @expected = expected @got = got end end # The class of elements of EventList#events. class Event # The EventList# object that contains it. # Note that it may not be directly contained, but with an EventContainer# # object in the middle. attr_accessor :parent # The EventContainer# object that contains it. # It may be +nil+, especially probably when # it itself is an EventContainer#. attr_accessor :container # The callback invoked when it is contained in an EventContainer#. # It is overridden in InlineLisp# and EventList#. # @example # class Alda::Note # def on_contained # puts 'a note contained' # end # end # Alda::Score.new { c } # => outputs "a note contained" def on_contained end # Converts to alda code. To be overridden. def to_alda_code '' end end # The class for objects containing an event. class EventContainer < Event # The contained Event# object. attr_accessor :event # The repetition counts. +nil+ if none. attr_accessor :count # The repetition labels. Empty if none. attr_accessor :labels # @param event The Event# object to be contained. # @param parent The EventList# object containing the event. def initialize event, parent @event = event @parent = parent @labels = [] on_containing end # Make #event a Chord# object. # @example # Alda::Score.new { piano_; c/-e/g }.play # # (plays the chord Cm) # # If the contained event is a Part# object, # make #event a new Part# object. # @example # Alda::Score.new { violin_/viola_/cello_; e; f; g}.play # # (plays notes E, F, G with three instruments simultaneously) def / other unless (expected = other) == (got = @parent.events.pop) raise OrderError.new expected, got end @event = if @event.is_a? Part Part.new @event.names + other.event.names, other.event.arg else Chord.new @event, other.event end self end def to_alda_code result = @event.to_alda_code unless @labels.empty? result.concat ?', @labels.map(&:to_alda_code).join(?,) end result.concat ?*, @count.to_alda_code if @count result end # Marks repetition. def * num @count = (@count || 1) * num self end # Marks alternative repetition. def % labels labels = [labels] unless labels.is_a? Array @labels.replace labels.to_a self end def event= event @event = event on_containing @event end def on_containing if @event @event.container = self @event.parent = @parent @event.on_contained end end def method_missing name, *args result = @event.__send__ name, *args result = self if result == @event result end end # Inline lisp event. class InlineLisp < Event # The function name of the lisp function attr_accessor :head # The arguments passed to the lisp function. # Its elements can be # Array#, Hash#, Numeric#, String#, Symbol#, or Event#. attr_accessor :args # The underlines in +head+ will be converted to hyphens. def initialize head, *args @head = head.to_s.gsub ?_, ?- @args = args end def to_alda_code "(#{head} #{args.map(&:to_alda_code).join ' '})" end def on_contained super @args.reverse_each do |event| if event.is_a?(Event) && (expected = event) != (got = @parent.events.pop) raise OrderError.new expected, got end end end end # A note event. class Note < Event # The string representing the pitch attr_accessor :pitch # The string representing the duration. # It ends with +~+ if the note slurs. attr_accessor :duration # The underlines in +duration+ will be converted to +~+. # Exclamation mark and question mark in +duration+ # will be interpreted as accidentals in #pitch. # # The number of underlines at the end of +duration+ means: # neither natural nor slur if 0, # natural if 1, # slur if 2, # both natural and slur if 3. def initialize pitch, duration @pitch = pitch.to_s @duration = duration.to_s.tr ?_, ?~ case @duration[-1] when ?! # sharp @pitch.concat ?+ @duration[-1] = '' when ?? # flat @pitch.concat ?- @duration[-1] = '' end waves = /(?~+)\z/ =~ @duration ? str.size : return @duration[@duration.length - waves..] = '' if waves >= 2 waves -= 2 @duration.concat ?~ end @pitch.concat ?_ * waves end # Append a sharp sign after #pitch. # @example # Alda::Score.new { piano_; +c }.play # # (plays a C\# note) def +@ @pitch.concat ?+ self end # Append a flat sign after #pitch. # @example # Alda::Score.new { piano_; -d }.play # # (plays a Db note) def -@ @pitch.concat ?- self end # Append a natural sign after #pitch # @example # Alda::Score.new { piano_; key_sig 'f+'; ~f }.play # # (plays a F note) def ~ @pitch.concat ?_ self end def to_alda_code result = @pitch + @duration result.concat ?*, @count.to_alda_code if @count result end end # A rest event. class Rest < Event # The string representing a duration. attr_accessor :duration # Underlines in +duration+ will be converted to +~+. def initialize duration @duration = duration.to_s.tr ?_, ?~ end def to_alda_code ?r + @duration end end # An octave event. class Octave < Event # The string representing the octave's number. # It can be empty, serving for #+@ and #-@. attr_accessor :num # Positive for up, negative for down, and 0 as default. attr_accessor :up_or_down def initialize num @num = num.to_s @up_or_down = 0 end # Octave up. # @example # Alda::Score.new { piano_; c; +o; c }.play # # (plays C4, then C5) # @see #-@ def +@ @up_or_down += 1 self end # Octave down. # @see #+@. def -@ @up_or_down -= 1 self end def to_alda_code case @up_or_down <=> 0 when 0 ?o + @num when 1 ?> * @up_or_down when -1 ?< * -@up_or_down end end end # A chord event. # Includes EventList#. class Chord < Event include EventList # EventList#x invokes this method. # @see EventList#method_missing # @param events In most cases, should not be used. # @param block To be passed with the Chord# object as +self+. # @example # Alda::Score.new { piano_; x { c; -e; g } }.play # # (plays chord Cm) def initialize *events, &block @events = events super &block end def to_alda_code events_alda_codes ?/ end end # A part event. class Part < Event # The names of the part. To be joined with +/+ as delimiter. attr_accessor :names # The nickname of the part. +nil+ if none. attr_accessor :arg def initialize names, arg = nil @names = names.map { |name| name.to_s.tr ?_, ?- } @arg = arg end def to_alda_code result = @names.join ?/ result.concat " \"#{@arg}\"" if @arg result.concat ?: end # Enables dot accessor. # @example # Alda::Score.new do # violin_/viola_/cello_('strings'); g1_1_1 # strings_.cello_; -o; c1_1_1 # end.play def method_missing name, *args str = name.to_s return super unless str[-1] == ?_ str[-1] = '' @names.last.concat ?., str if args.size == 1 joined = args.first unless (got = @parent.events.pop) == (expected = joined) raise OrderError.new expected, got end unless @container @container = EventContainer.new nil, @parent @parent.events.delete self @parent.push @container end @container.event = Sequence.join self, joined end end end # A voice event. class Voice < Event # The string representing the voice's number. attr_accessor :num def initialize num @num = num end def to_alda_code ?V + num + ?: end end # A CRAM event. Includes EventList#. class Cram < Event include EventList # The string representing the duration of the CRAM. attr_accessor :duration # EventList#t invokes this method. # @see EventList#method_missing # @param block To be passed with the CRAM as +self+. # @example # Alda::Score.new { piano_; t8 { x; y; }} def initialize duration, &block @duration = duration super &block end def to_alda_code "{#{events_alda_codes}}#@duration" end end # A marker event. # @see AtMarker# class Marker < Event # The marker's name attr_accessor :name # Underlines in +name+ is converted to hyphens. def initialize name @name = name.to_s.tr ?_, ?- end def to_alda_code ?% + @name end end # An at-marker event. # @see Marker# class AtMarker < Event # The corresponding marker's name attr_accessor :name # Underlines in +name+ is converted to hyphens. def initialize name @name = name.to_s.tr ?_, ?- end def to_alda_code ?@ + @name end end # A sequence event. Includes EventList#. class Sequence < Event include EventList # Using this module can fix a bug of Array#flatten. # @example # def (a = Object.new).method_missing(...) # Object.new # end # [a].flatten rescue $! # => # # using Alda::Sequence::RefineFlatten # [a].flatten # => [#] module RefineFlatten refine Array do def flatten each_with_object [] do |element, result| if element.is_a? Array result.push *element.flatten else result.push element end end end end end using RefineFlatten def to_alda_code @events.to_alda_code end # Creates a Sequence# object by joining +events+. # The EventContainer# objects are extracted, # and the Sequence# objects are flattened. def self.join *events new do @events = events.map do |event| while event.is_a?(EventContainer) && !event.count && event.labels.empty? event = event.event end event.is_a?(Sequence) ? event.events : event end.flatten end end end # A set-variable event. # Includes EventList#. class SetVariable < Event include EventList # The name of the variable. attr_accessor :name # The events passed to it using arguments instead of a block. attr_reader :original_events def initialize name, *events, &block @name = name.to_sym @original_events = events @events = events.clone super &block end # Specially, the result ends with a newline. def to_alda_code "#@name = #{events_alda_codes}\n" end def on_contained super @parent.variables.add @name @original_events.reverse_each do |event| unless (expected = event) == (got = @parent.events.pop) raise OrderError.new expected, got end end end end # A get-variable event class GetVariable < Event # The name of the variable attr_accessor :name def initialize name @name = name end def to_alda_code @name.to_s end end end