xi-lang 0.1.0

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: b3feae76d4f61becebc296554b3628ee870aa429
4
+ data.tar.gz: cc3ad564f9d0b4559f136dfb9f1ce93c26c01efb
5
+ SHA512:
6
+ metadata.gz: a7a6f4fea97a1600b38eaf844c847b8a34efc7fa6d21f33244aabd9a4dc6603528ba92651440db11510b40196dd835f04695c5ea83f22e30dfaeb3fd708702c9
7
+ data.tar.gz: 209c7db3b0e7cb2cd3c512756817ce7b0692bf4662658c436b9f6513651345d211dfa177732892a375d45e98ccbe6ad640ef437faf93d56c6a2cd692f96c0142
data/.gitignore ADDED
@@ -0,0 +1,11 @@
1
+ /.bundle/
2
+ /.yardoc
3
+ /Gemfile.lock
4
+ /_yardoc/
5
+ /coverage/
6
+ /doc/
7
+ /pkg/
8
+ /spec/reports/
9
+ /tmp/
10
+ *.swp
11
+ *.swo
data/.travis.yml ADDED
@@ -0,0 +1,5 @@
1
+ sudo: false
2
+ language: ruby
3
+ rvm:
4
+ - 2.3.1
5
+ before_install: gem install bundler -v 1.13.6
@@ -0,0 +1,74 @@
1
+ # Contributor Covenant Code of Conduct
2
+
3
+ ## Our Pledge
4
+
5
+ In the interest of fostering an open and welcoming environment, we as
6
+ contributors and maintainers pledge to making participation in our project and
7
+ our community a harassment-free experience for everyone, regardless of age, body
8
+ size, disability, ethnicity, gender identity and expression, level of experience,
9
+ nationality, personal appearance, race, religion, or sexual identity and
10
+ orientation.
11
+
12
+ ## Our Standards
13
+
14
+ Examples of behavior that contributes to creating a positive environment
15
+ include:
16
+
17
+ * Using welcoming and inclusive language
18
+ * Being respectful of differing viewpoints and experiences
19
+ * Gracefully accepting constructive criticism
20
+ * Focusing on what is best for the community
21
+ * Showing empathy towards other community members
22
+
23
+ Examples of unacceptable behavior by participants include:
24
+
25
+ * The use of sexualized language or imagery and unwelcome sexual attention or
26
+ advances
27
+ * Trolling, insulting/derogatory comments, and personal or political attacks
28
+ * Public or private harassment
29
+ * Publishing others' private information, such as a physical or electronic
30
+ address, without explicit permission
31
+ * Other conduct which could reasonably be considered inappropriate in a
32
+ professional setting
33
+
34
+ ## Our Responsibilities
35
+
36
+ Project maintainers are responsible for clarifying the standards of acceptable
37
+ behavior and are expected to take appropriate and fair corrective action in
38
+ response to any instances of unacceptable behavior.
39
+
40
+ Project maintainers have the right and responsibility to remove, edit, or
41
+ reject comments, commits, code, wiki edits, issues, and other contributions
42
+ that are not aligned to this Code of Conduct, or to ban temporarily or
43
+ permanently any contributor for other behaviors that they deem inappropriate,
44
+ threatening, offensive, or harmful.
45
+
46
+ ## Scope
47
+
48
+ This Code of Conduct applies both within project spaces and in public spaces
49
+ when an individual is representing the project or its community. Examples of
50
+ representing a project or community include using an official project e-mail
51
+ address, posting via an official social media account, or acting as an appointed
52
+ representative at an online or offline event. Representation of a project may be
53
+ further defined and clarified by project maintainers.
54
+
55
+ ## Enforcement
56
+
57
+ Instances of abusive, harassing, or otherwise unacceptable behavior may be
58
+ reported by contacting the project team at munshkr@gmail.com. All
59
+ complaints will be reviewed and investigated and will result in a response that
60
+ is deemed necessary and appropriate to the circumstances. The project team is
61
+ obligated to maintain confidentiality with regard to the reporter of an incident.
62
+ Further details of specific enforcement policies may be posted separately.
63
+
64
+ Project maintainers who do not follow or enforce the Code of Conduct in good
65
+ faith may face temporary or permanent repercussions as determined by other
66
+ members of the project's leadership.
67
+
68
+ ## Attribution
69
+
70
+ This Code of Conduct is adapted from the [Contributor Covenant][homepage], version 1.4,
71
+ available at [http://contributor-covenant.org/version/1/4][version]
72
+
73
+ [homepage]: http://contributor-covenant.org
74
+ [version]: http://contributor-covenant.org/version/1/4/
data/Gemfile ADDED
@@ -0,0 +1,4 @@
1
+ source 'https://rubygems.org'
2
+
3
+ # Specify your gem's dependencies in xi.gemspec
4
+ gemspec
data/Guardfile ADDED
@@ -0,0 +1,5 @@
1
+ guard :minitest do
2
+ watch(%r{^test/(.*)_test\.rb$})
3
+ watch(%r{^lib/xi/(.+)\.rb$}) { |m| "test/#{m[1]}_test.rb" }
4
+ watch(%r{^test/test_helper\.rb$}) { 'test' }
5
+ end
data/LICENSE.txt ADDED
@@ -0,0 +1,21 @@
1
+ The MIT License (MIT)
2
+
3
+ Copyright (c) 2017 Damián Silvani
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in
13
+ all copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
21
+ THE SOFTWARE.
data/README.md ADDED
@@ -0,0 +1,70 @@
1
+ # Xi
2
+
3
+ Xi (pronounced /ˈzaɪ/) is a musical pattern language inspired in Tidal and
4
+ SuperCollider for building higher-level musical constructs easily. It is
5
+ implemented on the Ruby programming language.
6
+
7
+ Xi is only a patterns library, but can talk to different backends:
8
+
9
+ - [SuperCollider](https://github.com/supercollider/supercollider)
10
+ - MIDI devices
11
+
12
+ ## Installation
13
+
14
+ You will need Ruby 2.1+ installed on your system. Check by running `ruby
15
+ -v`. You will also need Bundler. Install with `gem install bundler`.
16
+
17
+ Becase Xi is still in **alpha** stage, you will have to checkout this
18
+ repository using Git:
19
+
20
+ $ git clone https://github.com/munshkr/xi
21
+
22
+ After that, change into the new directory and install gem dependencies with
23
+ Bundler:
24
+
25
+ $ cd xi
26
+ $ bundle install
27
+
28
+ You're done! Fire up the REPL from `bin/xi`.
29
+
30
+ ## Development
31
+
32
+ After checking out the repo, run `bundle` to install dependencies. Then, run
33
+ `rake test` to run the tests.
34
+
35
+ To install this gem onto your local machine, run `bundle exec rake install`. To
36
+ release a new version, update the version number in `version.rb`, and then run
37
+ `bundle exec rake release`, which will create a git tag for the version, push
38
+ git commits and tags, and push the `.gem` file to
39
+ [rubygems.org](https://rubygems.org).
40
+
41
+ ## Contributing
42
+
43
+ Bug reports and pull requests are welcome on GitHub at
44
+ https://github.com/munshkr/xi. This project is intended to be a safe, welcoming
45
+ space for collaboration, and contributors are expected to adhere to the
46
+ [Contributor Covenant](http://contributor-covenant.org) code of conduct.
47
+
48
+ ## License
49
+
50
+ The MIT License (MIT)
51
+
52
+ Copyright (c) 2017 Damián Emiliano Silvani
53
+
54
+ Permission is hereby granted, free of charge, to any person obtaining a copy
55
+ of this software and associated documentation files (the "Software"), to deal
56
+ in the Software without restriction, including without limitation the rights
57
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
58
+ copies of the Software, and to permit persons to whom the Software is
59
+ furnished to do so, subject to the following conditions:
60
+
61
+ The above copyright notice and this permission notice shall be included in all
62
+ copies or substantial portions of the Software.
63
+
64
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
65
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
66
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
67
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
68
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
69
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
70
+ SOFTWARE.
data/Rakefile ADDED
@@ -0,0 +1,10 @@
1
+ require "bundler/gem_tasks"
2
+ require "rake/testtask"
3
+
4
+ Rake::TestTask.new(:test) do |t|
5
+ t.libs << "test"
6
+ t.libs << "lib"
7
+ t.test_files = FileList['test/**/*_test.rb']
8
+ end
9
+
10
+ task :default => :test
data/bin/xi ADDED
@@ -0,0 +1,8 @@
1
+ #!/usr/bin/env ruby
2
+ require 'bundler/setup'
3
+ require "xi"
4
+ require "xi/repl"
5
+
6
+ include Xi
7
+
8
+ REPL.start
data/lib/xi/clock.rb ADDED
@@ -0,0 +1,81 @@
1
+ require 'thread'
2
+ require 'logger'
3
+ require 'set'
4
+
5
+ Thread.abort_on_exception = true
6
+
7
+ module Xi
8
+ class Clock
9
+ DEFAULT_CPS = 1.0
10
+ INTERVAL_SEC = 10 / 1000.0
11
+
12
+ def initialize(cps: DEFAULT_CPS)
13
+ @mutex = Mutex.new
14
+ @cps = cps
15
+ @playing = true
16
+ @streams = [].to_set
17
+ @play_thread = Thread.new { thread_routine }
18
+ end
19
+
20
+ def subscribe(stream)
21
+ @mutex.synchronize { @streams << stream }
22
+ end
23
+
24
+ def unsubscribe(stream)
25
+ @mutex.synchronize { @streams.delete(stream) }
26
+ end
27
+
28
+ def cps
29
+ @mutex.synchronize { @cps }
30
+ end
31
+
32
+ def cps=(new_cps)
33
+ @mutex.synchronize { @cps = new_cps }
34
+ end
35
+
36
+ def playing?
37
+ @mutex.synchronize { @playing }
38
+ end
39
+
40
+ def stopped?
41
+ !playing?
42
+ end
43
+
44
+ def play
45
+ @mutex.synchronize { @playing = true }
46
+ self
47
+ end
48
+ alias_method :start, :play
49
+
50
+ def stop
51
+ @mutex.synchronize { @playing = false }
52
+ self
53
+ end
54
+ alias_method :pause, :play
55
+
56
+ def inspect
57
+ "#<#{self.class.name}:#{"0x%014x" % object_id} cps=#{cps.inspect} #{playing? ? :playing : :stopped}>"
58
+ end
59
+
60
+ private
61
+
62
+ def thread_routine
63
+ loop do
64
+ do_tick
65
+ sleep INTERVAL_SEC
66
+ end
67
+ end
68
+
69
+ def do_tick
70
+ cycles = Time.now.to_f * cps
71
+ return unless playing?
72
+ @streams.each { |s| s.notify(cycles) }
73
+ rescue => err
74
+ logger.error(err)
75
+ end
76
+
77
+ def logger
78
+ @logger ||= Logger.new(STDOUT)
79
+ end
80
+ end
81
+ end
@@ -0,0 +1,25 @@
1
+ require 'xi/pattern'
2
+
3
+ module Xi
4
+ module Pattern::Enumerable
5
+ def p(dur=nil, **metadata)
6
+ Pattern.new(self, event_duration: dur, **metadata)
7
+ end
8
+ end
9
+ end
10
+
11
+ class Enumerator
12
+ include Xi::Pattern::Enumerable
13
+ end
14
+
15
+ class Array
16
+ include Xi::Pattern::Enumerable
17
+ end
18
+
19
+ class Hash
20
+ include Xi::Pattern::Enumerable
21
+ end
22
+
23
+ class Range
24
+ include Xi::Pattern::Enumerable
25
+ end
@@ -0,0 +1,11 @@
1
+ module Xi
2
+ module CoerceToRational
3
+ def /(o)
4
+ super(o.to_r)
5
+ end
6
+ end
7
+ end
8
+
9
+ class Fixnum
10
+ prepend Xi::CoerceToRational
11
+ end
@@ -0,0 +1,34 @@
1
+ module Xi
2
+ module Numeric
3
+ def midi_to_cps
4
+ 440 * (2 ** ((self - 69) / 12.0))
5
+ end
6
+
7
+ def db_to_amp
8
+ 10 ** (self / 20.0)
9
+ end
10
+
11
+ def degree_to_key(scale, steps_per_octave)
12
+ accidental = (self - self.to_i) * 10.0
13
+ inner_key = scale[self % scale.size]
14
+ base_key = (self / scale.size).to_i * steps_per_octave + inner_key
15
+ if accidental != 0
16
+ base_key + accidental * (steps_per_octave / 12.0)
17
+ else
18
+ base_key
19
+ end
20
+ end
21
+ end
22
+ end
23
+
24
+ class Fixnum
25
+ include Xi::Numeric
26
+ end
27
+
28
+ class Float
29
+ include Xi::Numeric
30
+ end
31
+
32
+ class Rational
33
+ include Xi::Numeric
34
+ end
@@ -0,0 +1,15 @@
1
+ require 'xi/pattern'
2
+
3
+ module Xi
4
+ module Pattern::Simple
5
+ def p(dur=nil, **metadata)
6
+ [self].p(dur, metadata)
7
+ end
8
+ end
9
+ end
10
+
11
+ class Fixnum; include Xi::Pattern::Simple; end
12
+ class Float; include Xi::Pattern::Simple; end
13
+ class String; include Xi::Pattern::Simple; end
14
+ class Symbol; include Xi::Pattern::Simple; end
15
+ class Rational; include Xi::Pattern::Simple; end
@@ -0,0 +1,4 @@
1
+ require 'xi/core_ext/enumerable'
2
+ require 'xi/core_ext/fixnum'
3
+ require 'xi/core_ext/numeric'
4
+ require 'xi/core_ext/simple'
@@ -0,0 +1,45 @@
1
+ require 'singleton'
2
+ require 'thread'
3
+
4
+ module Xi
5
+ class ErrorLog
6
+ include Singleton
7
+
8
+ attr_accessor :max_msgs
9
+ attr_reader :more_errors
10
+ alias_method :more_errors?, :more_errors
11
+
12
+ def initialize(max_msgs: 6)
13
+ @max_msgs = max_msgs
14
+
15
+ @mutex = Mutex.new
16
+ @errors = []
17
+ @more_errors = false
18
+ end
19
+
20
+ def <<(msg)
21
+ @mutex.synchronize do
22
+ @errors.unshift(msg) unless @errors.include?(msg)
23
+ if @errors.size >= @max_msgs
24
+ @errors.slice!(@max_msgs)
25
+ @more_errors = true
26
+ end
27
+ end
28
+ end
29
+
30
+ def each
31
+ return enum_for(:each) unless block_given?
32
+
33
+ msgs = @mutex.synchronize do
34
+ res = @errors.dup
35
+ @errors.clear
36
+ @more_errors = false
37
+ res
38
+ end
39
+
40
+ while !msgs.empty?
41
+ yield msgs.shift
42
+ end
43
+ end
44
+ end
45
+ end
data/lib/xi/event.rb ADDED
@@ -0,0 +1,41 @@
1
+ module Xi
2
+ class Event
3
+ attr_reader :value, :start, :duration
4
+
5
+ def initialize(value, start=0, duration=1)
6
+ @value = value
7
+ @start = start
8
+ @duration = duration
9
+ end
10
+
11
+ def self.[](*args)
12
+ new(*args)
13
+ end
14
+
15
+ def ==(o)
16
+ self.class == o.class &&
17
+ value == o.value &&
18
+ start == o.start &&
19
+ duration == o.duration
20
+ end
21
+
22
+ def end
23
+ @start + @duration
24
+ end
25
+
26
+ def p(dur=nil, **metadata)
27
+ [self].p(dur, metadata)
28
+ end
29
+
30
+ def inspect
31
+ "E[#{@value.inspect},#{@start}" \
32
+ "#{",#{@duration}" if @duration != 1}]"
33
+ end
34
+
35
+ def to_s
36
+ inspect
37
+ end
38
+ end
39
+ end
40
+
41
+ E = Xi::Event
@@ -0,0 +1,141 @@
1
+ module Xi
2
+ class Pattern
3
+ module Generators
4
+ module ClassMethods
5
+ # Create an arithmetic series pattern of +length+ values, being +start+
6
+ # the starting value and +step+ the addition factor.
7
+ #
8
+ # @example
9
+ # peek P.series #=> [0, 1, 2, 3, 4, 5, 6, 7, 8, 9]
10
+ # peek P.series(3) #=> [3, 4, 5, 6, 7, 8, 9, 10, 11, 12]
11
+ # peek P.series(0, 2) #=> [0, 2, 4, 6, 8, 10, 12, 14, 16, 18]
12
+ # peek P.series(0, 0.25, 8) #=> [0, 0.25, 0.5, 0.75, 1.0, 1.25, 1.5, 1.75]
13
+ #
14
+ # @param start [Numeric] (default: 0)
15
+ # @param step [Numeric] (default: 1)
16
+ # @param length [Numeric, Symbol] number or inf (default: inf)
17
+ # @return [Pattern]
18
+ #
19
+ def series(start=0, step=1, length=inf)
20
+ Pattern.new(size: length) do |y|
21
+ i = start
22
+ loop_n(length) do
23
+ y << i
24
+ i += step
25
+ end
26
+ end
27
+ end
28
+
29
+ # Create a geometric series pattern of +length+ values, being +start+ the
30
+ # starting value and +step+ the multiplication factor.
31
+ #
32
+ # @example
33
+ # peek P.geom #=> [0, 0, 0, 0, 0, 0, 0, 0, 0, 0]
34
+ # peek P.geom(3) #=> [3, 3, 3, 3, 3, 3, 3, 3, 3, 3]
35
+ # peek P.geom(1, 2) #=> [1, 2, 4, 8, 16, 32, 64, 128, 256, 512]
36
+ # peek P.geom(1, 1/2, 6) #=> [1, (1/2), (1/4), (1/8), (1/16), (1/32)]
37
+ # peek P.geom(1, -1, 8) #=> [1, -1, 1, -1, 1, -1, 1, -1]
38
+ #
39
+ # @param start [Numeric] (default: 0)
40
+ # @param grow [Numeric] (default: 1)
41
+ # @param length [Numeric, Symbol] number or inf (default: inf)
42
+ # @return [Pattern]
43
+ #
44
+ def geom(start=0, grow=1, length=inf)
45
+ Pattern.new(size: length) do |y|
46
+ i = start
47
+ loop_n(length) do
48
+ y << i
49
+ i *= grow
50
+ end
51
+ end
52
+ end
53
+
54
+ # Choose items from the list randomly
55
+ #
56
+ # @example
57
+ # peek [1, 2, 3].p.rand #=> [2]
58
+ # peek [1, 2, 3, 4].p.rand(6) #=> [1, 3, 2, 2, 4, 3]
59
+ #
60
+ # @param repeats [Fixnum, Symbol] number or inf (default: 1)
61
+ # @return [Pattern]
62
+ #
63
+ def rand(list, repeats=1)
64
+ Pattern.new(size: repeats) do |y|
65
+ ls = list.to_a
66
+ loop_n(repeats) { y << ls.sample }
67
+ end
68
+ end
69
+
70
+ # Choose randomly, but only allow repeating the same item after yielding
71
+ # all items from the list.
72
+ #
73
+ # @example
74
+ # peek [1, 2, 3, 4, 5].p.xrand #=> [4]
75
+ # peek [1, 2, 3].p.xrand(8) #=> [1, 3, 2, 3, 1, 2, 3, 2]
76
+ #
77
+ # @param repeats [Fixnum, Symbol] number or inf (default: 1)
78
+ # @return [Pattern]
79
+ #
80
+ def xrand(list, repeats=1)
81
+ Pattern.new(size: repeats) do |y|
82
+ ls = list.to_a
83
+ xs = nil
84
+ loop_n(repeats) do |i|
85
+ xs = ls.shuffle if i % ls.size == 0
86
+ y << xs[i % ls.size]
87
+ end
88
+ end
89
+ end
90
+
91
+ # Shuffle the list in random order, and use the same random order
92
+ # +repeats+ times
93
+ #
94
+ # @example
95
+ # peek [1, 2, 3, 4, 5].p.xrand #=> [4]
96
+ # peek [1, 2, 3].p.xrand(8) #=> [1, 3, 2, 3, 1, 2, 3, 2]
97
+ #
98
+ # @param repeats [Fixnum, Symbol] number or inf (default: 1)
99
+ # @return [Pattern]
100
+ #
101
+ def shuf(list, repeats=1)
102
+ Pattern.new(size: list.size * repeats) do |y|
103
+ xs = list.to_a.shuffle
104
+ loop_n(repeats) do |i|
105
+ xs.each { |x| y << x }
106
+ end
107
+ end
108
+ end
109
+
110
+ # TODO Document
111
+ def sin(quant, dur=1)
112
+ Pattern.new(size: quant, dur: dur / quant) do |y|
113
+ quant.times do |i|
114
+ y << Math.sin(i / quant * 2 * Math::PI)
115
+ end
116
+ end
117
+ end
118
+
119
+ # TODO Document
120
+ def sin1(quant, dur=1)
121
+ sin(quant, dur).scale(-1, 1, 0, 1)
122
+ end
123
+
124
+ private
125
+
126
+ def loop_n(length)
127
+ i = 0
128
+ loop do
129
+ break if length != inf && i == length
130
+ yield i
131
+ i += 1
132
+ end
133
+ end
134
+ end
135
+
136
+ def self.included(receiver)
137
+ receiver.extend(ClassMethods)
138
+ end
139
+ end
140
+ end
141
+ end