rubyboy 1.2.0 → 1.3.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.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 909f97d8e2a7de6573d735d5d949bd7a8533c6096c24f4f74cede36e2c3651bc
4
- data.tar.gz: 2a0e0b87e8053143389a6970c910859229ec9001da32f91eb805ea0ebcea6bae
3
+ metadata.gz: f641e78dedf235a5ac8bab34c18fbd624150e5e6c9df92a710d2bc458fab3d62
4
+ data.tar.gz: 7ab941409fcbec7b0eff98241231e002c249f84be16a19c28216e33f91ac14d2
5
5
  SHA512:
6
- metadata.gz: 01fa425096d955d638c55e2489642d2532af9772c3a78aa1f11797bd952396472e78b0851605e7e81abdf49adc746725fb6c96eee904811b65a4edee74204221
7
- data.tar.gz: dc45f065ed0c3e93d14ad190bdb9b0a6947b3634e2907759fe909af61a1d6cdc9acae338955ecaeefa94411aa98847369691f77c1cf242ed8c2ea77e8d0d216e
6
+ metadata.gz: f220a9aeff3fa642f334c0248c4f155cf648c53a4bb132801e86a8008039f50d3cb9d3519af07b47e44f5aa2032419ba370e8ddcbb41ffebcb7937e8ed397b92
7
+ data.tar.gz: 63065e706e48de9fc26dcd9f7bb75332876374769db0914c54843c3f60aa1dd53a950fe0f33755539331e8f02b5b71cc0d4cf0bdca31421911d23911454a8108
data/.rubocop.yml CHANGED
@@ -17,6 +17,9 @@ Metrics/AbcSize:
17
17
  Metrics/BlockNesting:
18
18
  Enabled: false
19
19
 
20
+ Metrics/BlockLength:
21
+ Enabled: false
22
+
20
23
  Metrics/CyclomaticComplexity:
21
24
  Enabled: false
22
25
 
@@ -41,6 +44,9 @@ Style/Documentation:
41
44
  Style/FormatString:
42
45
  Enabled: false
43
46
 
47
+ Style/MultipleComparison:
48
+ Enabled: false
49
+
44
50
  Style/NumericLiterals:
45
51
  Enabled: false
46
52
 
data/CHANGELOG.md CHANGED
@@ -1,5 +1,13 @@
1
1
  ## [Unreleased]
2
2
 
3
+ ## [1.3.0] - 2024-03-09
4
+
5
+ - Add ffi to dependencies
6
+ - Add SDL2 wrapper
7
+ - Add audio
8
+ - Optimize cpu
9
+ - Cache R/W methods
10
+
3
11
  ## [1.2.0] - 2024-01-09
4
12
 
5
13
  - Add logo
@@ -0,0 +1,118 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative 'audio'
4
+ require_relative 'apu_channels/channel1'
5
+ require_relative 'apu_channels/channel2'
6
+ require_relative 'apu_channels/channel3'
7
+ require_relative 'apu_channels/channel4'
8
+
9
+ module Rubyboy
10
+ class Apu
11
+ def initialize
12
+ @audio = Audio.new
13
+ @nr50 = 0
14
+ @nr51 = 0
15
+ @cycles = 0
16
+ @sampling_cycles = 0
17
+ @fs = 0
18
+ @samples = Array.new(1024, 0.0)
19
+ @sample_idx = 0
20
+ @channel1 = ApuChannels::Channel1.new
21
+ @channel2 = ApuChannels::Channel2.new
22
+ @channel3 = ApuChannels::Channel3.new
23
+ @channel4 = ApuChannels::Channel4.new
24
+ end
25
+
26
+ def step(cycles)
27
+ @cycles += cycles
28
+ @sampling_cycles += cycles
29
+
30
+ @channel1.step(cycles)
31
+ @channel2.step(cycles)
32
+ @channel3.step(cycles)
33
+ @channel4.step(cycles)
34
+
35
+ if @cycles >= 0x1fff
36
+ @cycles -= 0x1fff
37
+
38
+ @channel1.step_fs(@fs)
39
+ @channel2.step_fs(@fs)
40
+ @channel3.step_fs(@fs)
41
+ @channel4.step_fs(@fs)
42
+
43
+ @fs = (@fs + 1) % 8
44
+ end
45
+
46
+ if @sampling_cycles >= 87
47
+ @sampling_cycles -= 87
48
+
49
+ left_sample = (
50
+ @nr51[7] * @channel4.dac_output +
51
+ @nr51[6] * @channel3.dac_output +
52
+ @nr51[5] * @channel2.dac_output +
53
+ @nr51[4] * @channel1.dac_output
54
+ ) / 4.0
55
+
56
+ right_sample = (
57
+ @nr51[3] * @channel4.dac_output +
58
+ @nr51[2] * @channel3.dac_output +
59
+ @nr51[1] * @channel2.dac_output +
60
+ @nr51[0] * @channel1.dac_output
61
+ ) / 4.0
62
+
63
+ raise "#{@nr51} #{@channel4.dac_output}, #{@channel3.dac_output}, #{@channel2.dac_output},#{@channel1.dac_output}" if left_sample.abs > 1.0 || right_sample.abs > 1.0
64
+
65
+ @samples[@sample_idx * 2] = (@nr50[4..6] / 7.0) * left_sample
66
+ @samples[@sample_idx * 2 + 1] = (@nr50[0..2] / 7.0) * right_sample
67
+ @sample_idx += 1
68
+ end
69
+
70
+ return if @sample_idx < 512
71
+
72
+ @sample_idx = 0
73
+ @audio.queue(@samples)
74
+ end
75
+
76
+ def read_byte(addr)
77
+ case addr
78
+ when 0xff10..0xff14 then @channel1.read_nr1x(addr - 0xff10)
79
+ when 0xff15..0xff19 then @channel2.read_nr2x(addr - 0xff15)
80
+ when 0xff1a..0xff1e then @channel3.read_nr3x(addr - 0xff1a)
81
+ when 0xff1f..0xff23 then @channel4.read_nr4x(addr - 0xff1f)
82
+ when 0xff24 then @nr50
83
+ when 0xff25 then @nr51
84
+ when 0xff26 then (@channel1.enabled ? 0x01 : 0x00) | (@channel2.enabled ? 0x02 : 0x00) | (@channel3.enabled ? 0x04 : 0x00) | (@channel4.enabled ? 0x08 : 0x00) | 0x70 | (@enabled ? 0x80 : 0x00)
85
+ when 0xff30..0xff3f then @channel3.wave_ram[(addr - 0xff30)]
86
+ else raise "Invalid APU read at #{addr.to_s(16)}"
87
+ end
88
+ end
89
+
90
+ def write_byte(addr, val)
91
+ return if !@enabled && ![0xff11, 0xff16, 0xff1b, 0xff20, 0xff26].include?(addr) && !(0xff30..0xff3f).include?(addr)
92
+
93
+ val &= 0x3f if !@enabled && [0xff11, 0xff16, 0xff1b, 0xff20].include?(addr)
94
+
95
+ case addr
96
+ when 0xff10..0xff14 then @channel1.write_nr1x(addr - 0xff10, val)
97
+ when 0xff15..0xff19 then @channel2.write_nr2x(addr - 0xff15, val)
98
+ when 0xff1a..0xff1e then @channel3.write_nr3x(addr - 0xff1a, val)
99
+ when 0xff1f..0xff23 then @channel4.write_nr4x(addr - 0xff1f, val)
100
+ when 0xff24 then @nr50 = val
101
+ when 0xff25 then @nr51 = val
102
+ when 0xff26
103
+ flg = val & 0x80 > 0
104
+ if !flg && @enabled
105
+ (0xff10..0xff25).each { |a| write_byte(a, 0) }
106
+ elsif flg && !@enabled
107
+ @fs = 0
108
+ @channel1.wave_duty_position = 0
109
+ @channel2.wave_duty_position = 0
110
+ @channel3.wave_duty_position = 0
111
+ end
112
+ @enabled = flg
113
+ when 0xff30..0xff3f then @channel3.wave_ram[(addr - 0xff30)] = val
114
+ else raise "Invalid APU write at #{addr.to_s(16)}"
115
+ end
116
+ end
117
+ end
118
+ end
@@ -0,0 +1,154 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Rubyboy
4
+ module ApuChannels
5
+ class Channel1
6
+ attr_accessor :enabled, :wave_duty_position
7
+
8
+ WAVE_DUTY = [
9
+ [0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 1.0], # 12.5%
10
+ [0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 1.0, 1.0], # 25%
11
+ [0.0, 0.0, 0.0, 0.0, 1.0, 1.0, 1.0, 1.0], # 50%
12
+ [0.0, 0.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0] # 75%
13
+ ].freeze
14
+
15
+ def initialize
16
+ @cycles = 0
17
+ @frequency = 0
18
+ @frequency_timer = 0
19
+ @wave_duty_position = 0
20
+
21
+ @enabled = false
22
+ @dac_enabled = false
23
+ @length_enabled = false
24
+ @is_upwards = false
25
+ @is_decrementing = false
26
+ @sweep_enabled = false
27
+ @sweep_period = 0
28
+ @sweep_shift = 0
29
+ @period = 0
30
+ @period_timer = 0
31
+ @current_volume = 0
32
+ @initial_volume = 0
33
+ @shadow_frequency = 0
34
+ @sweep_timer = 0
35
+ @length_timer = 0
36
+ @wave_duty_pattern = 0
37
+ end
38
+
39
+ def step(cycles)
40
+ @cycles += cycles
41
+
42
+ return if @cycles < @frequency_timer
43
+
44
+ @cycles -= @frequency_timer
45
+ @frequency_timer = (2048 - @frequency) * 4
46
+ @wave_duty_position = (@wave_duty_position + 1) % 8
47
+ end
48
+
49
+ def step_fs(fs)
50
+ length if fs & 0x01 == 0
51
+ envelope if fs == 7
52
+ sweep if fs == 2 || fs == 6
53
+ end
54
+
55
+ def length
56
+ return unless @length_enabled && @length_timer > 0
57
+
58
+ @length_timer -= 1
59
+ @enabled &= @length_timer > 0
60
+ end
61
+
62
+ def envelope
63
+ return if @period == 0
64
+
65
+ @period_timer -= 1 if @period_timer > 0
66
+
67
+ return if @period_timer != 0
68
+
69
+ @period_timer = @period
70
+
71
+ if @current_volume < 15 && @is_upwards
72
+ @current_volume += 1
73
+ elsif @current_volume > 0 && !@is_upwards
74
+ @current_volume -= 1
75
+ end
76
+ end
77
+
78
+ def sweep
79
+ @sweep_timer -= 1 if @sweep_timer > 0
80
+
81
+ return if @sweep_timer != 0
82
+
83
+ @sweep_timer = @sweep_period
84
+
85
+ return unless @sweep_enabled
86
+
87
+ @frequency = calculate_frequency
88
+ @shadow_frequency = @frequency
89
+ end
90
+
91
+ def calculate_frequency
92
+ if @is_decrementing
93
+ if @shadow_frequency >= (@shadow_frequency >> @sweep_shift)
94
+ @shadow_frequency - (@shadow_frequency >> @sweep_shift)
95
+ else
96
+ 0
97
+ end
98
+ else
99
+ [0x3ff, @shadow_frequency + (@shadow_frequency >> @sweep_shift)].min
100
+ end
101
+ end
102
+
103
+ def dac_output
104
+ return 0.0 unless @dac_enabled && @enabled
105
+
106
+ ret = WAVE_DUTY[@wave_duty_pattern][@wave_duty_position] * @current_volume
107
+ (ret / 7.5) - 1.0
108
+ end
109
+
110
+ def read_nr1x(x)
111
+ case x
112
+ when 0 then (@sweep_period << 4) | @sweep_shift | (@is_decrementing ? 0x08 : 0x00) | 0x80
113
+ when 1 then (@wave_duty_pattern << 6) | 0x3f
114
+ when 2 then (@initial_volume << 4) | (@is_upwards ? 0x08 : 0x00) | @period
115
+ when 3 then 0xff
116
+ when 4 then (@length_enabled ? 0x40 : 0x00) | 0xbf
117
+ else 0xff
118
+ end
119
+ end
120
+
121
+ def write_nr1x(x, val)
122
+ case x
123
+ when 0
124
+ @sweep_period = (val >> 4) & 0x07
125
+ @is_decrementing = (val & 0x08) > 0
126
+ @sweep_shift = val & 0x07
127
+ when 1
128
+ @wave_duty_pattern = (val >> 6) & 0x03
129
+ @length_timer = 64 - (val & 0x3f)
130
+ when 2
131
+ @is_upwards = (val & 0x08) > 0
132
+ @initial_volume = (val >> 4)
133
+ @period = val & 0x07
134
+ @dac_enabled = val & 0xf8 > 0
135
+ @enabled &= @dac_enabled
136
+ when 3
137
+ @frequency = (@frequency & 0x700) | val
138
+ when 4
139
+ @frequency = (@frequency & 0xff) | ((val & 0x07) << 8)
140
+ @length_enabled = (val & 0x40) > 0
141
+ @length_timer = 64 if @length_timer == 0
142
+ return unless (val & 0x80) > 0 && @dac_enabled
143
+
144
+ @enabled = true
145
+ @period_timer = @period
146
+ @current_volume = @initial_volume
147
+ @shadow_frequency = @frequency
148
+ @sweep_timer = @sweep_period
149
+ @sweep_enabled = @sweep_period > 0 || @sweep_shift > 0
150
+ end
151
+ end
152
+ end
153
+ end
154
+ end
@@ -0,0 +1,116 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Rubyboy
4
+ module ApuChannels
5
+ class Channel2
6
+ attr_accessor :enabled, :wave_duty_position
7
+
8
+ WAVE_DUTY = [
9
+ [0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 1.0], # 12.5%
10
+ [0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 1.0, 1.0], # 25%
11
+ [0.0, 0.0, 0.0, 0.0, 1.0, 1.0, 1.0, 1.0], # 50%
12
+ [0.0, 0.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0] # 75%
13
+ ].freeze
14
+
15
+ def initialize
16
+ @cycles = 0
17
+ @frequency = 0
18
+ @frequency_timer = 0
19
+ @wave_duty_position = 0
20
+
21
+ @enabled = false
22
+ @dac_enabled = false
23
+ @length_enabled = false
24
+ @is_upwards = false
25
+ @is_decrementing = false
26
+ @period = 0
27
+ @period_timer = 0
28
+ @current_volume = 0
29
+ @initial_volume = 0
30
+ @shadow_frequency = 0
31
+ @length_timer = 0
32
+ @wave_duty_pattern = 0
33
+ end
34
+
35
+ def step(cycles)
36
+ @cycles += cycles
37
+
38
+ return if @cycles < @frequency_timer
39
+
40
+ @cycles -= @frequency_timer
41
+ @frequency_timer = (2048 - @frequency) * 4
42
+ @wave_duty_position = (@wave_duty_position + 1) % 8
43
+ end
44
+
45
+ def step_fs(fs)
46
+ length if fs & 0x01 == 0
47
+ envelope if fs == 7
48
+ end
49
+
50
+ def length
51
+ return unless @length_enabled && @length_timer > 0
52
+
53
+ @length_timer -= 1
54
+ @enabled &= @length_timer > 0
55
+ end
56
+
57
+ def envelope
58
+ return if @period == 0
59
+
60
+ @period_timer -= 1 if @period_timer > 0
61
+
62
+ return if @period_timer != 0
63
+
64
+ @period_timer = @period
65
+
66
+ if @current_volume < 15 && @is_upwards
67
+ @current_volume += 1
68
+ elsif @current_volume > 0 && !@is_upwards
69
+ @current_volume -= 1
70
+ end
71
+ end
72
+
73
+ def dac_output
74
+ return 0.0 unless @dac_enabled && @enabled
75
+
76
+ ret = WAVE_DUTY[@wave_duty_pattern][@wave_duty_position] * @current_volume
77
+ (ret / 7.5) - 1.0
78
+ end
79
+
80
+ def read_nr2x(x)
81
+ case x
82
+ when 1 then (@wave_duty_pattern << 6) | 0x3f
83
+ when 2 then (@initial_volume << 4) | (@is_upwards ? 0x08 : 0x00) | @period
84
+ when 3 then 0xff
85
+ when 4 then (@length_enabled ? 0x40 : 0x00) | 0xbf
86
+ else 0xff
87
+ end
88
+ end
89
+
90
+ def write_nr2x(x, val)
91
+ case x
92
+ when 1
93
+ @wave_duty_pattern = (val >> 6) & 0x03
94
+ @length_timer = 64 - (val & 0x3f)
95
+ when 2
96
+ @is_upwards = (val & 0x08) > 0
97
+ @initial_volume = (val >> 4)
98
+ @period = val & 0x07
99
+ @dac_enabled = val & 0xf8 > 0
100
+ @enabled &= @dac_enabled
101
+ when 3
102
+ @frequency = (@frequency & 0x700) | val
103
+ when 4
104
+ @frequency = (@frequency & 0xff) | ((val & 0x07) << 8)
105
+ @length_enabled = (val & 0x40) > 0
106
+ @length_timer = 64 if @length_timer == 0
107
+ return unless (val & 0x80) > 0 && @dac_enabled
108
+
109
+ @enabled = true
110
+ @period_timer = @period
111
+ @current_volume = @initial_volume
112
+ end
113
+ end
114
+ end
115
+ end
116
+ end
@@ -0,0 +1,117 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Rubyboy
4
+ module ApuChannels
5
+ class Channel3
6
+ attr_accessor :enabled, :wave_duty_position, :wave_ram
7
+
8
+ WAVE_DUTY = [
9
+ [0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 1.0], # 12.5%
10
+ [0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 1.0, 1.0], # 25%
11
+ [0.0, 0.0, 0.0, 0.0, 1.0, 1.0, 1.0, 1.0], # 50%
12
+ [0.0, 0.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0] # 75%
13
+ ].freeze
14
+
15
+ def initialize
16
+ @cycles = 0
17
+ @frequency = 0
18
+ @frequency_timer = 0
19
+ @wave_duty_position = 0
20
+
21
+ @enabled = false
22
+ @dac_enabled = false
23
+ @length_enabled = false
24
+ @is_upwards = false
25
+ @is_decrementing = false
26
+ @sweep_enabled = false
27
+ @sweep_period = 0
28
+ @sweep_shift = 0
29
+ @period = 0
30
+ @period_timer = 0
31
+ @current_volume = 0
32
+ @initial_volume = 0
33
+ @shadow_frequency = 0
34
+ @sweep_timer = 0
35
+ @length_timer = 0
36
+ @wave_duty_pattern = 0
37
+
38
+ @output_level = 0
39
+ @volume_shift = 0
40
+ @wave_ram = Array.new(16, 0)
41
+ end
42
+
43
+ def step(cycles)
44
+ @cycles += cycles
45
+
46
+ return if @cycles < @frequency_timer
47
+
48
+ @cycles -= @frequency_timer
49
+ @frequency_timer = (2048 - @frequency) * 2
50
+ @wave_duty_position = (@wave_duty_position + 1) % 32
51
+ end
52
+
53
+ def step_fs(fs)
54
+ length if fs & 0x01 == 0
55
+ end
56
+
57
+ def length
58
+ return unless @length_enabled && @length_timer > 0
59
+
60
+ @length_timer -= 1
61
+ @enabled &= @length_timer > 0
62
+ end
63
+
64
+ def calculate_frequency
65
+ if @is_decrementing
66
+ if @shadow_frequency >= (@shadow_frequency >> @sweep_shift)
67
+ @shadow_frequency - (@shadow_frequency >> @sweep_shift)
68
+ else
69
+ 0
70
+ end
71
+ else
72
+ [0x3ff, @shadow_frequency + (@shadow_frequency >> @sweep_shift)].min
73
+ end
74
+ end
75
+
76
+ def dac_output
77
+ return 0.0 unless @dac_enabled && @enabled
78
+
79
+ ret = ((0xf & (
80
+ @wave_ram[@wave_duty_position >> 1] >> ((@wave_duty_position & 0x01) << 2)
81
+ ))) >> @volume_shift
82
+
83
+ (ret / 7.5) - 1.0
84
+ end
85
+
86
+ def read_nr3x(x)
87
+ case x
88
+ when 0 then ((@dac_enabled ? 0x80 : 0x00) | 0x7f)
89
+ when 1 then 0xff
90
+ when 2 then (@output_level << 5) | 0x9f
91
+ when 3 then 0xff
92
+ when 4 then (@length_enabled ? 0x40 : 0x00) | 0xbf
93
+ else 0xff
94
+ end
95
+ end
96
+
97
+ def write_nr3x(x, val)
98
+ case x
99
+ when 0
100
+ @dac_enabled = val & 0x80 > 0
101
+ @enabled &= @dac_enabled
102
+ when 1
103
+ @length_timer = 256 - val
104
+ when 2
105
+ @output_level = (val >> 5) & 0x03
106
+ when 3
107
+ @frequency = (@frequency & 0x700) | val
108
+ when 4
109
+ @frequency = (@frequency & 0xff) | ((val & 0x07) << 8)
110
+ @length_enabled = val & 0x40 > 0
111
+ @length_timer = 256 if @length_timer == 0
112
+ @enabled = true if (val & 0x80) > 0 && @dac_enabled
113
+ end
114
+ end
115
+ end
116
+ end
117
+ end
@@ -0,0 +1,148 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Rubyboy
4
+ module ApuChannels
5
+ class Channel4
6
+ attr_accessor :enabled, :wave_duty_position
7
+
8
+ WAVE_DUTY = [
9
+ [0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 1.0], # 12.5%
10
+ [0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 1.0, 1.0], # 25%
11
+ [0.0, 0.0, 0.0, 0.0, 1.0, 1.0, 1.0, 1.0], # 50%
12
+ [0.0, 0.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0] # 75%
13
+ ].freeze
14
+
15
+ def initialize
16
+ @cycles = 0
17
+ @frequency = 0
18
+ @frequency_timer = 0
19
+ @wave_duty_position = 0
20
+
21
+ @enabled = false
22
+ @dac_enabled = false
23
+ @length_enabled = false
24
+ @is_upwards = false
25
+ @is_decrementing = false
26
+ @sweep_enabled = false
27
+ @sweep_period = 0
28
+ @sweep_shift = 0
29
+ @period = 0
30
+ @period_timer = 0
31
+ @current_volume = 0
32
+ @initial_volume = 0
33
+ @shadow_frequency = 0
34
+ @sweep_timer = 0
35
+ @length_timer = 0
36
+ @wave_duty_pattern = 0
37
+
38
+ @lfsr = 0x7fff
39
+ @width_mode = false
40
+ @shift_amount = 0
41
+ @divisor_code = 0
42
+ end
43
+
44
+ def step(cycles)
45
+ @cycles += cycles
46
+
47
+ return if @cycles < @frequency_timer
48
+
49
+ @cycles -= @frequency_timer
50
+ @frequency_timer = [8, @divisor_code << 4].max << @shift_amount
51
+
52
+ xor = (@lfsr & 0x01) ^ ((@lfsr & 0b10) >> 1)
53
+ @lfsr = (@lfsr >> 1) | (xor << 14)
54
+ return unless @width_mode
55
+
56
+ @lfsr &= ~(1 << 6)
57
+ @lfsr |= xor << 6
58
+ end
59
+
60
+ def step_fs(fs)
61
+ length if fs & 0x01 == 0
62
+ envelope if fs == 7
63
+ end
64
+
65
+ def length
66
+ return unless @length_enabled && @length_timer > 0
67
+
68
+ @length_timer -= 1
69
+ @enabled &= @length_timer > 0
70
+ end
71
+
72
+ def envelope
73
+ return if @period == 0
74
+
75
+ @period_timer -= 1 if @period_timer > 0
76
+
77
+ return if @period_timer != 0
78
+
79
+ @period_timer = @period
80
+
81
+ if @current_volume < 15 && @is_upwards
82
+ @current_volume += 1
83
+ elsif @current_volume > 0 && !@is_upwards
84
+ @current_volume -= 1
85
+ end
86
+ end
87
+
88
+ def calculate_frequency
89
+ if @is_decrementing
90
+ if @shadow_frequency >= (@shadow_frequency >> @sweep_shift)
91
+ @shadow_frequency - (@shadow_frequency >> @sweep_shift)
92
+ else
93
+ 0
94
+ end
95
+ else
96
+ [0x3ff, @shadow_frequency + (@shadow_frequency >> @sweep_shift)].min
97
+ end
98
+ end
99
+
100
+ def dac_output
101
+ return 0.0 unless @dac_enabled && @enabled
102
+
103
+ ret = (@lfsr & 0x01) * @current_volume
104
+ (ret / 7.5) - 1.0
105
+ end
106
+
107
+ def read_nr4x(x)
108
+ case x
109
+ when 0 then 0xff
110
+ when 1 then 0xff
111
+ when 2 then (@initial_volume << 4) | (@is_upwards ? 0x08 : 0x00) | @period
112
+ when 3 then (@shift_amount << 4) | (@width_mode ? 0x08 : 0x00) | @divisor_code
113
+ when 4 then (@length_enabled ? 0x40 : 0x00) | 0xbf
114
+ else 0xff
115
+ end
116
+ end
117
+
118
+ def write_nr4x(x, val)
119
+ case x
120
+ when 0
121
+ # nop
122
+ when 1
123
+ @length_timer = 64 - (val & 0x3f)
124
+ when 2
125
+ @is_upwards = (val & 0x08) > 0
126
+ @initial_volume = val >> 4
127
+ @period = val & 0x07
128
+ @dac_enabled = val & 0xf8 > 0
129
+ @enabled &= @dac_enabled
130
+ when 3
131
+ @shift_amount = (val >> 4) & 0x0f
132
+ @width_mode = (val & 0x08) > 0
133
+ @divisor_code = val & 0x07
134
+ when 4
135
+ @length_enabled = (val & 0x40) > 0
136
+ @length_timer = 64 if @length_timer == 0
137
+ return unless (val & 0x80) > 0
138
+
139
+ @enabled = true if @dac_enabled
140
+
141
+ @lfsr = 0x7fff
142
+ @period_timer = @period
143
+ @current_volume = @initial_volume
144
+ end
145
+ end
146
+ end
147
+ end
148
+ end