road_to_rubykaigi 0.1.0 → 0.2.0

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.
Files changed (48) hide show
  1. checksums.yaml +4 -4
  2. data/.standard.yml +11 -0
  3. data/CHANGELOG.md +6 -0
  4. data/README.md +15 -1
  5. data/Rakefile +1 -0
  6. data/lib/road_to_rubykaigi/audio/audio_engine.rb +70 -0
  7. data/lib/road_to_rubykaigi/audio/oscillator.rb +159 -0
  8. data/lib/road_to_rubykaigi/audio/sequencer.rb +263 -0
  9. data/lib/road_to_rubykaigi/audio/wav/attack_01.wav +0 -0
  10. data/lib/road_to_rubykaigi/audio/wav/attack_02.wav +0 -0
  11. data/lib/road_to_rubykaigi/audio/wav/attack_03.wav +0 -0
  12. data/lib/road_to_rubykaigi/audio/wav/attack_04.wav +0 -0
  13. data/lib/road_to_rubykaigi/audio/wav/attack_05.wav +0 -0
  14. data/lib/road_to_rubykaigi/audio/wav/bonus.wav +0 -0
  15. data/lib/road_to_rubykaigi/audio/wav/crouch.wav +0 -0
  16. data/lib/road_to_rubykaigi/audio/wav/defeat.wav +0 -0
  17. data/lib/road_to_rubykaigi/audio/wav/game_over.wav +0 -0
  18. data/lib/road_to_rubykaigi/audio/wav/jump.wav +0 -0
  19. data/lib/road_to_rubykaigi/audio/wav/laptop.wav +0 -0
  20. data/lib/road_to_rubykaigi/audio/wav/stun.wav +0 -0
  21. data/lib/road_to_rubykaigi/audio/wav/walk_01.wav +0 -0
  22. data/lib/road_to_rubykaigi/audio/wav/walk_02.wav +0 -0
  23. data/lib/road_to_rubykaigi/audio/wav_source.rb +55 -0
  24. data/lib/road_to_rubykaigi/event_dispatcher.rb +122 -0
  25. data/lib/road_to_rubykaigi/fireworks.rb +4 -4
  26. data/lib/road_to_rubykaigi/game.rb +49 -60
  27. data/lib/road_to_rubykaigi/graphics/demo-map.txt +30 -0
  28. data/lib/road_to_rubykaigi/graphics/demo-mask.txt +30 -0
  29. data/lib/road_to_rubykaigi/graphics/map.rb +6 -1
  30. data/lib/road_to_rubykaigi/graphics/mask.rb +7 -1
  31. data/lib/road_to_rubykaigi/graphics/player.rb +56 -65
  32. data/lib/road_to_rubykaigi/graphics/player.txt +26 -0
  33. data/lib/road_to_rubykaigi/manager/audio_manager.rb +81 -0
  34. data/lib/road_to_rubykaigi/manager/collision_manager.rb +23 -108
  35. data/lib/road_to_rubykaigi/manager/drawing_manager.rb +7 -6
  36. data/lib/road_to_rubykaigi/manager/game_manager.rb +50 -13
  37. data/lib/road_to_rubykaigi/manager/physics_engine.rb +21 -0
  38. data/lib/road_to_rubykaigi/manager/update_manager.rb +15 -12
  39. data/lib/road_to_rubykaigi/map.rb +1 -15
  40. data/lib/road_to_rubykaigi/score_board.rb +18 -1
  41. data/lib/road_to_rubykaigi/sprite/attack.rb +13 -6
  42. data/lib/road_to_rubykaigi/sprite/bonus.rb +16 -0
  43. data/lib/road_to_rubykaigi/sprite/deadline.rb +3 -3
  44. data/lib/road_to_rubykaigi/sprite/enemy.rb +12 -8
  45. data/lib/road_to_rubykaigi/sprite/player.rb +110 -29
  46. data/lib/road_to_rubykaigi/version.rb +1 -1
  47. data/lib/road_to_rubykaigi.rb +20 -4
  48. metadata +55 -3
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: c7a2caaae541ce4ed72641c27f4b9df735cbaf1592839a962dca1595a63158a8
4
- data.tar.gz: f01bf3be60571a84dcb809059e3630b0d58fb61b570dabb5e8c83d576164b9c5
3
+ metadata.gz: 57500f7d236ea18c22449c46e155c07d3a9a2f69d19ff94ed0667f45d37abe65
4
+ data.tar.gz: 3d0d02188814cd5a46cb0f770208ae808c908ba10c3a3901886cddf2651fe79e
5
5
  SHA512:
6
- metadata.gz: 524c0ed14385b286b2c4d201881331ee4ccde2ff3a4c7f35fd113f29f8dc813214eb60c32b9881028e8bc5f028e4b973cdaab43e5c005248cba3d7b3e64403e8
7
- data.tar.gz: 514ab937338bc63709ebbe3924b33b3b81e78e46b5b1e50f27c634c5ef04d0403312cc9446d7420e7b079f240e24555f5cd9ec902fbc1528e94d83c92624ec83
6
+ metadata.gz: 5c26abb95bc98529e89068c495b2338e6c12318ee389ef6d2933dde1df08bf0fa5bf184ad05559b9e138093273f61cdbe2a5297659e0d42e6a7a7620dd4ed73b
7
+ data.tar.gz: 568b26414a6af0c83009cc9ecc9a27835a595f73feba22a8369090076d9de29e4e6f6a6f354e6e93c97f8d4066d7a73dc529e8591903de006f40bec84055d318
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
@@ -1,3 +1,9 @@
1
+ ## [0.2.0] - 2025-04-15
2
+
3
+ - Play BGM
4
+ - Add attack action
5
+ - Add shooting action
6
+
1
7
  ## [0.1.0] - 2025-02-27
2
8
 
3
9
  - Initial release
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
- bundle exec road_to_rubykaigi
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
@@ -2,6 +2,7 @@
2
2
 
3
3
  require "bundler/gem_tasks"
4
4
  require "rspec/core/rake_task"
5
+ import "util/audio.rake"
5
6
 
6
7
  RSpec::Core::RakeTask.new(:spec)
7
8
 
@@ -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
@@ -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