road_to_rubykaigi 0.1.0 → 0.2.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +4 -4
- data/.standard.yml +11 -0
- data/CHANGELOG.md +10 -0
- data/README.md +15 -1
- data/Rakefile +1 -0
- data/lib/road_to_rubykaigi/audio/audio_engine.rb +70 -0
- data/lib/road_to_rubykaigi/audio/oscillator.rb +159 -0
- data/lib/road_to_rubykaigi/audio/sequencer.rb +263 -0
- data/lib/road_to_rubykaigi/audio/wav/attack_01.wav +0 -0
- data/lib/road_to_rubykaigi/audio/wav/attack_02.wav +0 -0
- data/lib/road_to_rubykaigi/audio/wav/attack_03.wav +0 -0
- data/lib/road_to_rubykaigi/audio/wav/attack_04.wav +0 -0
- data/lib/road_to_rubykaigi/audio/wav/attack_05.wav +0 -0
- data/lib/road_to_rubykaigi/audio/wav/bonus.wav +0 -0
- data/lib/road_to_rubykaigi/audio/wav/crouch.wav +0 -0
- data/lib/road_to_rubykaigi/audio/wav/defeat.wav +0 -0
- data/lib/road_to_rubykaigi/audio/wav/game_over.wav +0 -0
- data/lib/road_to_rubykaigi/audio/wav/jump.wav +0 -0
- data/lib/road_to_rubykaigi/audio/wav/laptop.wav +0 -0
- data/lib/road_to_rubykaigi/audio/wav/stun.wav +0 -0
- data/lib/road_to_rubykaigi/audio/wav/walk_01.wav +0 -0
- data/lib/road_to_rubykaigi/audio/wav/walk_02.wav +0 -0
- data/lib/road_to_rubykaigi/audio/wav_source.rb +55 -0
- data/lib/road_to_rubykaigi/event_dispatcher.rb +122 -0
- data/lib/road_to_rubykaigi/fireworks.rb +4 -4
- data/lib/road_to_rubykaigi/game.rb +49 -60
- data/lib/road_to_rubykaigi/graphics/demo-map.txt +30 -0
- data/lib/road_to_rubykaigi/graphics/demo-mask.txt +30 -0
- data/lib/road_to_rubykaigi/graphics/map.rb +6 -1
- data/lib/road_to_rubykaigi/graphics/mask.rb +7 -1
- data/lib/road_to_rubykaigi/graphics/player.rb +56 -65
- data/lib/road_to_rubykaigi/graphics/player.txt +26 -0
- data/lib/road_to_rubykaigi/manager/audio_manager.rb +82 -0
- data/lib/road_to_rubykaigi/manager/collision_manager.rb +23 -108
- data/lib/road_to_rubykaigi/manager/drawing_manager.rb +7 -6
- data/lib/road_to_rubykaigi/manager/game_manager.rb +50 -13
- data/lib/road_to_rubykaigi/manager/physics_engine.rb +21 -0
- data/lib/road_to_rubykaigi/manager/update_manager.rb +15 -12
- data/lib/road_to_rubykaigi/map.rb +1 -15
- data/lib/road_to_rubykaigi/score_board.rb +18 -1
- data/lib/road_to_rubykaigi/sprite/attack.rb +13 -6
- data/lib/road_to_rubykaigi/sprite/bonus.rb +16 -0
- data/lib/road_to_rubykaigi/sprite/deadline.rb +3 -3
- data/lib/road_to_rubykaigi/sprite/enemy.rb +12 -8
- data/lib/road_to_rubykaigi/sprite/player.rb +110 -29
- data/lib/road_to_rubykaigi/version.rb +1 -1
- data/lib/road_to_rubykaigi.rb +20 -4
- metadata +55 -3
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 8e1c9d75458d0964d5a7016dffb3cd8b80a7528e6dd6988400e54b37d307a2ae
|
|
4
|
+
data.tar.gz: 645ba606e44b14f7065136dc22b8dfd7f33fd8af5f0c7d0c2fcf94f928b54997
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: cc76ee396cf3a522c4aca51b02a4fc3009e7f7dea49c4ca59bcaa4161c70e8bc9a1276549e8777385a9baa4b1960f682ee0415b3534a06f22bc2f5d6bd71780a
|
|
7
|
+
data.tar.gz: a6f482e2a301be08a089b978950a1d890417653243e63b985b24e2f2c94a7c1bb64a61fde9b47d0eb885d531b778f10f94c0c280e45a81a2184dd69b6e3aa33d
|
data/.standard.yml
CHANGED
|
@@ -10,9 +10,20 @@ ignore:
|
|
|
10
10
|
- Naming/MethodParameterName
|
|
11
11
|
- Style/IfInsideElse
|
|
12
12
|
- Style/EmptyCaseCondition
|
|
13
|
+
- Style/NestedTernaryOperator
|
|
13
14
|
- Style/StringLiterals
|
|
15
|
+
- Style/TernaryParentheses
|
|
14
16
|
- Style/TrailingCommaInArguments
|
|
15
17
|
- Style/TrailingCommaInArrayLiteral
|
|
16
18
|
- Style/TrailingCommaInHashLiteral
|
|
19
|
+
- Style/WhenThen
|
|
17
20
|
- '**/**/ansi.rb':
|
|
18
21
|
- Style/RedundantSelf
|
|
22
|
+
- 'lib/road_to_rubykaigi/audio/audio_engine.rb':
|
|
23
|
+
- Naming/VariableName
|
|
24
|
+
- Security/Open
|
|
25
|
+
- 'lib/road_to_rubykaigi/audio/sequencer.rb':
|
|
26
|
+
- Layout/HashAlignment
|
|
27
|
+
- 'util/audio.rake':
|
|
28
|
+
- Lint/RedundantSplatExpansion
|
|
29
|
+
- Style/PercentLiteralDelimiters
|
data/CHANGELOG.md
CHANGED
data/README.md
CHANGED
|
@@ -19,6 +19,13 @@
|
|
|
19
19
|
|
|
20
20
|
## Installation
|
|
21
21
|
|
|
22
|
+
Install portaudio like:
|
|
23
|
+
|
|
24
|
+
```bash
|
|
25
|
+
# e.g. macOS
|
|
26
|
+
brew install portaudio
|
|
27
|
+
```
|
|
28
|
+
|
|
22
29
|
```bash
|
|
23
30
|
gem install road_to_rubykaigi
|
|
24
31
|
```
|
|
@@ -28,13 +35,20 @@ gem install road_to_rubykaigi
|
|
|
28
35
|
Run the game from your terminal:
|
|
29
36
|
|
|
30
37
|
```bash
|
|
31
|
-
|
|
38
|
+
road_to_rubykaigi
|
|
32
39
|
```
|
|
33
40
|
|
|
34
41
|
## Requirements
|
|
35
42
|
|
|
36
43
|
- Ruby 3.4.0 or later
|
|
37
44
|
- A terminal that supports ANSI escape sequences and 256-color mode
|
|
45
|
+
- PortAudio
|
|
46
|
+
|
|
47
|
+
## SoundEffects Creators
|
|
48
|
+
|
|
49
|
+
- Yasuhiro Tsuchiya / https://snd.dev
|
|
50
|
+
- PANICPUMPKIN / https://pansound.com/panicpumpkin
|
|
51
|
+
- タダノオト / https://tadanote.tokyo
|
|
38
52
|
|
|
39
53
|
## License
|
|
40
54
|
|
data/Rakefile
CHANGED
|
@@ -0,0 +1,70 @@
|
|
|
1
|
+
require 'ffi-portaudio'
|
|
2
|
+
|
|
3
|
+
module RoadToRubykaigi
|
|
4
|
+
module Audio
|
|
5
|
+
class AudioEngine < FFI::PortAudio::Stream
|
|
6
|
+
include FFI::PortAudio
|
|
7
|
+
|
|
8
|
+
def process(_input, output, framesPerBuffer, _timeInfo, _statusFlags, _userData)
|
|
9
|
+
samples = Array.new(framesPerBuffer, 0.0)
|
|
10
|
+
|
|
11
|
+
unless @muted
|
|
12
|
+
@sources.each do |source|
|
|
13
|
+
framesPerBuffer.times do |i|
|
|
14
|
+
sample = source.generate * source.gain
|
|
15
|
+
samples[i] += sample
|
|
16
|
+
end
|
|
17
|
+
remove_source(source) if source.finished?
|
|
18
|
+
end
|
|
19
|
+
end
|
|
20
|
+
|
|
21
|
+
output.write_array_of_float(samples)
|
|
22
|
+
:paContinue
|
|
23
|
+
end
|
|
24
|
+
|
|
25
|
+
def add_source(source)
|
|
26
|
+
@sources << source.rewind
|
|
27
|
+
end
|
|
28
|
+
|
|
29
|
+
def remove_source(source)
|
|
30
|
+
@sources.delete(source)
|
|
31
|
+
end
|
|
32
|
+
|
|
33
|
+
def mute
|
|
34
|
+
@muted = true
|
|
35
|
+
end
|
|
36
|
+
|
|
37
|
+
def unmute
|
|
38
|
+
@muted = false
|
|
39
|
+
end
|
|
40
|
+
|
|
41
|
+
private
|
|
42
|
+
|
|
43
|
+
def initialize(bass_sequencer, melody_sequencer)
|
|
44
|
+
frame_size = 2 ** 12 # 4096
|
|
45
|
+
@sources = [bass_sequencer, melody_sequencer]
|
|
46
|
+
@muted = false
|
|
47
|
+
API.Pa_Initialize
|
|
48
|
+
open(nil, output, bass_sequencer.sample_rate, frame_size)
|
|
49
|
+
start
|
|
50
|
+
|
|
51
|
+
at_exit do
|
|
52
|
+
@muted = true
|
|
53
|
+
sleep 0.5
|
|
54
|
+
close
|
|
55
|
+
API.Pa_Terminate
|
|
56
|
+
end
|
|
57
|
+
end
|
|
58
|
+
|
|
59
|
+
def output
|
|
60
|
+
output = API::PaStreamParameters.new
|
|
61
|
+
output[:device] = API.Pa_GetDefaultOutputDevice
|
|
62
|
+
output[:suggestedLatency] = API.Pa_GetDeviceInfo(output[:device])[:defaultHighOutputLatency]
|
|
63
|
+
output[:hostApiSpecificStreamInfo] = nil
|
|
64
|
+
output[:channelCount] = 1 # monaural
|
|
65
|
+
output[:sampleFormat] = API::Float32
|
|
66
|
+
output
|
|
67
|
+
end
|
|
68
|
+
end
|
|
69
|
+
end
|
|
70
|
+
end
|
|
@@ -0,0 +1,159 @@
|
|
|
1
|
+
module RoadToRubykaigi
|
|
2
|
+
module Audio
|
|
3
|
+
module Phasor
|
|
4
|
+
def sample_rate
|
|
5
|
+
44_100
|
|
6
|
+
end
|
|
7
|
+
|
|
8
|
+
def gain
|
|
9
|
+
0.2
|
|
10
|
+
end
|
|
11
|
+
|
|
12
|
+
private
|
|
13
|
+
|
|
14
|
+
def initialize
|
|
15
|
+
@phases = Hash.new { |h, k| h[k] = rand }
|
|
16
|
+
end
|
|
17
|
+
|
|
18
|
+
def tick(frequency:)
|
|
19
|
+
phase = @phases[frequency]
|
|
20
|
+
phase += frequency.to_f / sample_rate
|
|
21
|
+
phase -= 1.0 if phase >= 1.0
|
|
22
|
+
@phases[frequency] = phase
|
|
23
|
+
end
|
|
24
|
+
end
|
|
25
|
+
|
|
26
|
+
class TriangleOscillator
|
|
27
|
+
include Phasor
|
|
28
|
+
|
|
29
|
+
def generate(frequencies:)
|
|
30
|
+
samples = frequencies.map do |frequency|
|
|
31
|
+
phase = tick(frequency: frequency)
|
|
32
|
+
if phase < 0.5
|
|
33
|
+
4 * phase - 1
|
|
34
|
+
else
|
|
35
|
+
-4 * phase + 3
|
|
36
|
+
end
|
|
37
|
+
end
|
|
38
|
+
samples.sum / samples.size
|
|
39
|
+
end
|
|
40
|
+
end
|
|
41
|
+
|
|
42
|
+
class RoughTriangleOscillator
|
|
43
|
+
include Phasor
|
|
44
|
+
|
|
45
|
+
TABLE_SIZE = 32
|
|
46
|
+
|
|
47
|
+
def generate(frequencies:)
|
|
48
|
+
samples = frequencies.map do |frequency|
|
|
49
|
+
phase = tick(frequency: frequency)
|
|
50
|
+
index = (TABLE_SIZE * phase).floor % TABLE_SIZE
|
|
51
|
+
@table[index]
|
|
52
|
+
end
|
|
53
|
+
samples.sum / samples.size
|
|
54
|
+
end
|
|
55
|
+
|
|
56
|
+
private
|
|
57
|
+
|
|
58
|
+
def initialize
|
|
59
|
+
super
|
|
60
|
+
@table = generate_table
|
|
61
|
+
end
|
|
62
|
+
|
|
63
|
+
def generate_table
|
|
64
|
+
half = TABLE_SIZE / 2
|
|
65
|
+
# up:-1 -> +1
|
|
66
|
+
# i = 0: -1.0, i = half-1: +1.0
|
|
67
|
+
up = (0...half).map { |i| -1.0 + (2.0 * i) / (half - 1) }
|
|
68
|
+
# down:+1 -> -1
|
|
69
|
+
# i = 0: +1.0, i = half-1: -1.0
|
|
70
|
+
down = (0...half).map { |i| 1.0 - (2.0 * i) / (half - 1) }
|
|
71
|
+
up + down
|
|
72
|
+
end
|
|
73
|
+
end
|
|
74
|
+
|
|
75
|
+
class SquareOscillator
|
|
76
|
+
include Phasor
|
|
77
|
+
DUTY_CYCLE = {
|
|
78
|
+
d0: 0.125,
|
|
79
|
+
d1: 0.25,
|
|
80
|
+
d2: 0.5,
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
def generate(frequencies:)
|
|
84
|
+
samples = frequencies.map do |frequency|
|
|
85
|
+
phase = tick(frequency: frequency)
|
|
86
|
+
phase < duty_cycle ? 1.0 : -1.0
|
|
87
|
+
end
|
|
88
|
+
samples.sum / samples.size
|
|
89
|
+
end
|
|
90
|
+
|
|
91
|
+
def duty_cycle=(duty_cycle)
|
|
92
|
+
@duty_cycle = (DUTY_CYCLE.key?(duty_cycle) ? duty_cycle : :d0)
|
|
93
|
+
end
|
|
94
|
+
|
|
95
|
+
private
|
|
96
|
+
|
|
97
|
+
def initialize
|
|
98
|
+
@duty_cycle = :d1
|
|
99
|
+
super
|
|
100
|
+
end
|
|
101
|
+
|
|
102
|
+
def duty_cycle
|
|
103
|
+
DUTY_CYCLE[@duty_cycle]
|
|
104
|
+
end
|
|
105
|
+
end
|
|
106
|
+
|
|
107
|
+
class RoundedSquareOscillator
|
|
108
|
+
include Phasor
|
|
109
|
+
DUTY_CYCLE = {
|
|
110
|
+
d0: 0.125,
|
|
111
|
+
d1: 0.25,
|
|
112
|
+
d2: 0.5,
|
|
113
|
+
}
|
|
114
|
+
SMOOTH_WIDTH = 0.05
|
|
115
|
+
|
|
116
|
+
def generate(frequencies:)
|
|
117
|
+
samples = frequencies.map do |frequency|
|
|
118
|
+
phase = tick(frequency: frequency)
|
|
119
|
+
off_to_on_end = SMOOTH_WIDTH / 2.0
|
|
120
|
+
on_to_off_start = duty_cycle - SMOOTH_WIDTH / 2.0
|
|
121
|
+
on_to_off_end = duty_cycle + SMOOTH_WIDTH / 2.0
|
|
122
|
+
|
|
123
|
+
case phase
|
|
124
|
+
when 0..off_to_on_end
|
|
125
|
+
t = phase / off_to_on_end
|
|
126
|
+
smoothstep_weight = t ** 2 * (3 - 2 * t)
|
|
127
|
+
-1.0 + smoothstep_weight * 2
|
|
128
|
+
when off_to_on_end..on_to_off_start
|
|
129
|
+
1.0
|
|
130
|
+
when on_to_off_start..on_to_off_end
|
|
131
|
+
t = (phase - on_to_off_start) / SMOOTH_WIDTH
|
|
132
|
+
cos_weight = 1 - Math.cos(Math::PI * t)
|
|
133
|
+
1.0 - cos_weight
|
|
134
|
+
else
|
|
135
|
+
# We don't need interpolate off_to_on_start..1
|
|
136
|
+
# because 1 is essentially contiguous with 0.
|
|
137
|
+
-1.0
|
|
138
|
+
end
|
|
139
|
+
end
|
|
140
|
+
samples.sum / samples.size
|
|
141
|
+
end
|
|
142
|
+
|
|
143
|
+
def duty_cycle=(duty_cycle)
|
|
144
|
+
@duty_cycle = (DUTY_CYCLE.key?(duty_cycle) ? duty_cycle : :d0)
|
|
145
|
+
end
|
|
146
|
+
|
|
147
|
+
private
|
|
148
|
+
|
|
149
|
+
def initialize
|
|
150
|
+
@duty_cycle = :d1
|
|
151
|
+
super
|
|
152
|
+
end
|
|
153
|
+
|
|
154
|
+
def duty_cycle
|
|
155
|
+
DUTY_CYCLE[@duty_cycle]
|
|
156
|
+
end
|
|
157
|
+
end
|
|
158
|
+
end
|
|
159
|
+
end
|
|
@@ -0,0 +1,263 @@
|
|
|
1
|
+
module RoadToRubykaigi
|
|
2
|
+
module Audio
|
|
3
|
+
class SequencerBase
|
|
4
|
+
BPM = 100
|
|
5
|
+
NOTES = {
|
|
6
|
+
REST: 0.0,
|
|
7
|
+
C4: 261.63, C4s: 277.18, # ド
|
|
8
|
+
D4: 293.66, D4s: 311.13,
|
|
9
|
+
E4: 329.63,
|
|
10
|
+
F4: 349.23, F4s: 369.99,
|
|
11
|
+
G4: 392.00, G4s: 415.30, # ソ
|
|
12
|
+
A4: 440.00, A4s: 466.16,
|
|
13
|
+
B4: 493.88,
|
|
14
|
+
C5: 523.25, C5s: 554.37,
|
|
15
|
+
D5: 587.33, D5s: 622.25,
|
|
16
|
+
E5: 659.25,
|
|
17
|
+
F5: 698.46, F5s: 739.99,
|
|
18
|
+
G5: 783.99, G5s: 830.61,
|
|
19
|
+
A5: 880.00, A5s: 932.33,
|
|
20
|
+
B5: 987.77,
|
|
21
|
+
C6: 1046.50,
|
|
22
|
+
}
|
|
23
|
+
ENVELOPE = {
|
|
24
|
+
bass: { a: 0.2, d: 0.2, s: 0.6, sl: 0.6, rl: 0.9 },
|
|
25
|
+
bass_short: { a: 0.2, d: 0.2, s: 0.05, sl: 0.6, rl: 0.9 },
|
|
26
|
+
melody: { a: 0.05, d: 0.1, s: 0.1, sl: 0.75, rl: 1.35 },
|
|
27
|
+
melody_long: { a: 0.5, d: 0.2, s: 0.7, sl: 0.6, rl: 1.0 },
|
|
28
|
+
fanfare: { a: 0.2, d: 0.1, s: 0.35, sl: 0.6, rl: 1.5 },
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
def generate
|
|
32
|
+
process
|
|
33
|
+
env = envelope
|
|
34
|
+
sample = if (current_note[:frequency] != [:REST]) &&
|
|
35
|
+
(@current_note_sample_count < (@samples_per_note * staccato_ratio))
|
|
36
|
+
@generator.generate(frequencies: current_frequencies)
|
|
37
|
+
else
|
|
38
|
+
0.0
|
|
39
|
+
end
|
|
40
|
+
increment_current_note_sample_count
|
|
41
|
+
sample * env
|
|
42
|
+
end
|
|
43
|
+
|
|
44
|
+
def sample_rate
|
|
45
|
+
@generator.sample_rate
|
|
46
|
+
end
|
|
47
|
+
|
|
48
|
+
def gain
|
|
49
|
+
@generator.gain
|
|
50
|
+
end
|
|
51
|
+
|
|
52
|
+
def rewind
|
|
53
|
+
self
|
|
54
|
+
end
|
|
55
|
+
|
|
56
|
+
def finished?
|
|
57
|
+
!loop? && @current_note_index >= @notes.size
|
|
58
|
+
end
|
|
59
|
+
|
|
60
|
+
private
|
|
61
|
+
|
|
62
|
+
def initialize
|
|
63
|
+
@bpm = BPM
|
|
64
|
+
@notes = self.class::SCORE
|
|
65
|
+
@current_note_index = 0
|
|
66
|
+
@current_note_sample_count = 0
|
|
67
|
+
@generator = self.class::GENERATOR.new
|
|
68
|
+
change_note
|
|
69
|
+
end
|
|
70
|
+
|
|
71
|
+
def process
|
|
72
|
+
if @current_note_sample_count >= @samples_per_note
|
|
73
|
+
@current_note_index += 1
|
|
74
|
+
@current_note_sample_count = 0
|
|
75
|
+
if loop? && @current_note_index >= @notes.size
|
|
76
|
+
@current_note_index -= @notes.size
|
|
77
|
+
end
|
|
78
|
+
change_note
|
|
79
|
+
end
|
|
80
|
+
end
|
|
81
|
+
|
|
82
|
+
def envelope
|
|
83
|
+
note_progress = @current_note_sample_count.to_f / (@samples_per_note * staccato_ratio)
|
|
84
|
+
current_envelop = Hash === current_note[:envelope] ? current_note[:envelope] : ENVELOPE[current_note[:envelope] || default_envelop_key]
|
|
85
|
+
attack = current_envelop[:a]
|
|
86
|
+
decay = current_envelop[:d]
|
|
87
|
+
sustain_level = current_envelop[:sl]
|
|
88
|
+
release_level = current_envelop[:rl]
|
|
89
|
+
sustain = current_envelop[:s]
|
|
90
|
+
|
|
91
|
+
if note_progress < attack
|
|
92
|
+
note_progress / attack
|
|
93
|
+
elsif note_progress < (attack + decay)
|
|
94
|
+
1 - ((note_progress - attack) / decay) * (1 - sustain_level)
|
|
95
|
+
elsif note_progress < sustain
|
|
96
|
+
sustain_level
|
|
97
|
+
else
|
|
98
|
+
(1 - note_progress) / release_level
|
|
99
|
+
end
|
|
100
|
+
end
|
|
101
|
+
|
|
102
|
+
def current_note
|
|
103
|
+
@notes[@current_note_index]
|
|
104
|
+
end
|
|
105
|
+
|
|
106
|
+
def current_frequencies
|
|
107
|
+
current_note[:frequency].map { |frequency| NOTES[frequency] }
|
|
108
|
+
end
|
|
109
|
+
|
|
110
|
+
def staccato_ratio
|
|
111
|
+
current_note[:staccato] || self.class::STACCATO_RATIO
|
|
112
|
+
end
|
|
113
|
+
|
|
114
|
+
def change_note
|
|
115
|
+
quarter_duration = 60.0 / @bpm
|
|
116
|
+
@samples_per_note = (@generator.sample_rate * quarter_duration * current_note[:duration]).to_i
|
|
117
|
+
end
|
|
118
|
+
|
|
119
|
+
def increment_current_note_sample_count
|
|
120
|
+
@current_note_sample_count += 1
|
|
121
|
+
end
|
|
122
|
+
end
|
|
123
|
+
|
|
124
|
+
class BassSequencer < SequencerBase
|
|
125
|
+
GENERATOR = RoughTriangleOscillator
|
|
126
|
+
STACCATO_RATIO = 0.85
|
|
127
|
+
SCORE = ([
|
|
128
|
+
{ frequency: %i[F4 A4], duration: 1.0 },
|
|
129
|
+
{ frequency: %i[F4 A4], duration: 0.5, envelope: :bass_short, staccato: 0.7 },
|
|
130
|
+
{ frequency: %i[C4 F4], duration: 1.0 },
|
|
131
|
+
{ frequency: %i[C4 F4], duration: 0.5, envelope: :bass_short, staccato: 0.7 },
|
|
132
|
+
] * 5 + [
|
|
133
|
+
{ frequency: %i[F4], duration: 1.0 },
|
|
134
|
+
{ frequency: %i[C4], duration: 1.0 },
|
|
135
|
+
{ frequency: %i[E4], duration: 1.0, staccato: 1.0 },
|
|
136
|
+
] +
|
|
137
|
+
[
|
|
138
|
+
{ frequency: %i[F4 A4], duration: 1.0 },
|
|
139
|
+
{ frequency: %i[F4 A4], duration: 0.5, envelope: :bass_short, staccato: 0.7 },
|
|
140
|
+
{ frequency: %i[C4 F4], duration: 1.0 },
|
|
141
|
+
{ frequency: %i[C4 F4], duration: 0.5, envelope: :bass_short, staccato: 0.7 },
|
|
142
|
+
] * 4 + [
|
|
143
|
+
{ frequency: %i[F4 A4], duration: 1.0 },
|
|
144
|
+
{ frequency: %i[C4 F4], duration: 1.0 },
|
|
145
|
+
{ frequency: %i[F4], duration: 1.0 },
|
|
146
|
+
|
|
147
|
+
{ frequency: %i[E4], duration: 1.0 },
|
|
148
|
+
{ frequency: %i[D4], duration: 1.0 },
|
|
149
|
+
{ frequency: %i[C4], duration: 1.0, staccato: 1.0 },
|
|
150
|
+
])
|
|
151
|
+
|
|
152
|
+
private
|
|
153
|
+
|
|
154
|
+
def default_envelop_key
|
|
155
|
+
:bass
|
|
156
|
+
end
|
|
157
|
+
|
|
158
|
+
def loop?
|
|
159
|
+
true
|
|
160
|
+
end
|
|
161
|
+
end
|
|
162
|
+
|
|
163
|
+
class MelodySequencer < SequencerBase
|
|
164
|
+
GENERATOR = RoundedSquareOscillator
|
|
165
|
+
STACCATO_RATIO = 0.35
|
|
166
|
+
SCORE = [ # 6 Measures
|
|
167
|
+
{ frequency: %i[F5], duration: 0.5, envelope: { a: 0.05, d: 0.0, s: 0.5, sl: 0.4, rl: 1.0 } },
|
|
168
|
+
{ frequency: %i[C5], duration: 0.5 },
|
|
169
|
+
{ frequency: %i[F5], duration: 0.5 },
|
|
170
|
+
{ frequency: %i[F5], duration: 0.5 },
|
|
171
|
+
{ frequency: %i[C5], duration: 0.5 },
|
|
172
|
+
{ frequency: %i[F5], duration: 0.5 },
|
|
173
|
+
|
|
174
|
+
{ frequency: %i[C5], duration: 0.5 },
|
|
175
|
+
{ frequency: %i[F5], duration: 0.25 },
|
|
176
|
+
{ frequency: %i[F5], duration: 0.25 },
|
|
177
|
+
{ frequency: %i[G5], duration: 0.5 },
|
|
178
|
+
{ frequency: %i[F5], duration: 0.5 },
|
|
179
|
+
{ frequency: %i[C5], duration: 0.5 },
|
|
180
|
+
{ frequency: %i[F5], duration: 0.5 },
|
|
181
|
+
|
|
182
|
+
{ frequency: %i[REST], duration: 0.5 },
|
|
183
|
+
{ frequency: %i[A5], duration: 0.5 },
|
|
184
|
+
{ frequency: %i[G5], duration: 0.5 },
|
|
185
|
+
{ frequency: %i[F5], duration: 0.5 },
|
|
186
|
+
{ frequency: %i[E5], duration: 1.0, envelope: :melody_long, staccato: 0.95 },
|
|
187
|
+
|
|
188
|
+
{ frequency: %i[F5], duration: 0.5, envelope: { a: 0.15, d: 0.15, s: 0.5, sl: 0.4, rl: 0.9 } },
|
|
189
|
+
{ frequency: %i[C5], duration: 0.5 },
|
|
190
|
+
{ frequency: %i[A4], duration: 0.5 },
|
|
191
|
+
{ frequency: %i[C5], duration: 0.25 },
|
|
192
|
+
{ frequency: %i[F5], duration: 0.25 },
|
|
193
|
+
{ frequency: %i[D5], duration: 0.25 },
|
|
194
|
+
{ frequency: %i[F5], duration: 0.25 },
|
|
195
|
+
{ frequency: %i[C5], duration: 0.25 },
|
|
196
|
+
{ frequency: %i[F5], duration: 0.25 },
|
|
197
|
+
|
|
198
|
+
{ frequency: %i[C5], duration: 1.0, envelope: { a: 0.5, d: 0.15, s: 0.7, sl: 0.5, rl: 0.5 }, staccato: 0.95 },
|
|
199
|
+
{ frequency: %i[F5], duration: 0.25, envelope: { a: 0.05, d: 0.0, s: 0.5, sl: 0.4, rl: 1.0 } },
|
|
200
|
+
{ frequency: %i[A5], duration: 0.25 },
|
|
201
|
+
{ frequency: %i[C5], duration: 0.25 },
|
|
202
|
+
{ frequency: %i[F5], duration: 0.25 },
|
|
203
|
+
{ frequency: %i[C5], duration: 0.25 },
|
|
204
|
+
{ frequency: %i[F5], duration: 0.25 },
|
|
205
|
+
{ frequency: %i[C5], duration: 0.25 },
|
|
206
|
+
{ frequency: %i[F5], duration: 0.25 },
|
|
207
|
+
|
|
208
|
+
{ frequency: %i[C5], duration: 0.5 },
|
|
209
|
+
{ frequency: %i[F5], duration: 0.5 },
|
|
210
|
+
{ frequency: %i[A5], duration: 0.25 },
|
|
211
|
+
{ frequency: %i[G5], duration: 0.25 },
|
|
212
|
+
{ frequency: %i[F5], duration: 0.25 },
|
|
213
|
+
{ frequency: %i[E5], duration: 0.25 },
|
|
214
|
+
{ frequency: %i[F5], duration: 1.0, envelope: :melody_long, staccato: 0.95 },
|
|
215
|
+
]
|
|
216
|
+
|
|
217
|
+
def loop?
|
|
218
|
+
true
|
|
219
|
+
end
|
|
220
|
+
|
|
221
|
+
private
|
|
222
|
+
|
|
223
|
+
def default_envelop_key
|
|
224
|
+
:melody
|
|
225
|
+
end
|
|
226
|
+
end
|
|
227
|
+
|
|
228
|
+
class FanfareSequencer < SequencerBase
|
|
229
|
+
GENERATOR = RoundedSquareOscillator
|
|
230
|
+
STACCATO_RATIO = 0.35
|
|
231
|
+
SCORE = [ # 4.75 Measures
|
|
232
|
+
{ frequency: %i[REST], duration: 0.75 },
|
|
233
|
+
|
|
234
|
+
{ frequency: %i[F5], duration: 1.00 },
|
|
235
|
+
{ frequency: %i[F5], duration: 0.25 },
|
|
236
|
+
{ frequency: %i[F5], duration: 0.25 },
|
|
237
|
+
|
|
238
|
+
{ frequency: %i[G5], duration: 0.5 },
|
|
239
|
+
{ frequency: %i[F5], duration: 0.5 },
|
|
240
|
+
{ frequency: %i[G5], duration: 0.5 },
|
|
241
|
+
|
|
242
|
+
{ frequency: %i[C5], duration: 0.25 },
|
|
243
|
+
{ frequency: %i[D5], duration: 0.25 },
|
|
244
|
+
{ frequency: %i[F5], duration: 0.25 },
|
|
245
|
+
{ frequency: %i[G5], duration: 0.25 },
|
|
246
|
+
{ frequency: %i[A5], duration: 0.25 },
|
|
247
|
+
{ frequency: %i[B5], duration: 0.25 },
|
|
248
|
+
|
|
249
|
+
{ frequency: %i[C6], duration: 1.8, envelope: { a: 0.2, d: 0.2, s: 0.6, sl: 0.6, rl: 0.9 } },
|
|
250
|
+
]
|
|
251
|
+
|
|
252
|
+
private
|
|
253
|
+
|
|
254
|
+
def default_envelop_key
|
|
255
|
+
:fanfare
|
|
256
|
+
end
|
|
257
|
+
|
|
258
|
+
def loop?
|
|
259
|
+
false
|
|
260
|
+
end
|
|
261
|
+
end
|
|
262
|
+
end
|
|
263
|
+
end
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
require 'wavefile'
|
|
2
|
+
|
|
3
|
+
module RoadToRubykaigi
|
|
4
|
+
module Audio
|
|
5
|
+
class WavSource
|
|
6
|
+
include WaveFile
|
|
7
|
+
|
|
8
|
+
def sample_rate
|
|
9
|
+
44_100
|
|
10
|
+
end
|
|
11
|
+
|
|
12
|
+
def gain
|
|
13
|
+
0.3
|
|
14
|
+
end
|
|
15
|
+
|
|
16
|
+
def generate
|
|
17
|
+
if @position < @buffer.size
|
|
18
|
+
sample = @buffer[@position]
|
|
19
|
+
@position += 1
|
|
20
|
+
sample
|
|
21
|
+
else
|
|
22
|
+
0.0
|
|
23
|
+
end
|
|
24
|
+
end
|
|
25
|
+
|
|
26
|
+
def rewind
|
|
27
|
+
@position = 0
|
|
28
|
+
self
|
|
29
|
+
end
|
|
30
|
+
|
|
31
|
+
def finished?
|
|
32
|
+
@position >= @buffer.size
|
|
33
|
+
end
|
|
34
|
+
|
|
35
|
+
private
|
|
36
|
+
|
|
37
|
+
def initialize(path)
|
|
38
|
+
@buffer = read_samples(path)
|
|
39
|
+
@position = 0
|
|
40
|
+
end
|
|
41
|
+
|
|
42
|
+
def read_samples(path)
|
|
43
|
+
samples = []
|
|
44
|
+
pcm_rate = 2 ** (16 - 1)
|
|
45
|
+
Reader.new(path).each_buffer(1024) do |buffer|
|
|
46
|
+
buffer.samples.each do |sample|
|
|
47
|
+
normalized = (sample.is_a?(Array) ? sample[0] : sample).to_f / pcm_rate
|
|
48
|
+
samples << normalized
|
|
49
|
+
end
|
|
50
|
+
end
|
|
51
|
+
samples
|
|
52
|
+
end
|
|
53
|
+
end
|
|
54
|
+
end
|
|
55
|
+
end
|