-
Notifications
You must be signed in to change notification settings - Fork 1
/
stream_channel.go
180 lines (149 loc) · 3.73 KB
/
stream_channel.go
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
package xm
import (
"github.com/quasilyte/xm/xmfile"
)
const (
numRampPoints = 32
)
type streamChannel struct {
// These values are used in the hottest code path (readTick).
// Keep them closer to the head of the struct.
computedVolume [2]float64
targetVolume [2]float64
sampleOffset float64
// Note-related data.
inst *instrument
note *patternNote
period float64
sampleStep float64
effect effectKey
keyOn bool
panning float64
volume float64
fadeoutVolume float64
// Ramping state.
rampFrame uint
rampSamples [numRampPoints]float64
// Arpeggio effect state.
arpeggioRunning bool
arpeggioNoteOffset float64
panningSlideValue float64
volumeSlideValue float64
globalVolumeSlideValue float64
portamentoUpValue float64
portamentoDownValue float64
notePortamentoTargetPeriod float64
notePortamentoValue float64
// Vibrato effect state.
vibratoRunning bool
vibratoPeriodOffset float64
vibratoDepth float64
vibratoStep uint8
vibratoSpeed uint8
// Ping-pong loop state.
reverse bool
volumeEnvelope envelopeRunner
panningEnvelope envelopeRunner
// This ID is needed mostly for debugging,
// therefore we put it to the object's tail.
id int
}
type envelopeRunner struct {
envelope
value float64
frame int
}
func (ch *streamChannel) Reset() {
*ch = streamChannel{}
}
func (ch *streamChannel) resetEnvelopes() {
ch.fadeoutVolume = 1
ch.volumeEnvelope.value = 1
ch.volumeEnvelope.frame = 0
ch.panningEnvelope.value = 0.5
ch.panningEnvelope.frame = 0
}
func (ch *streamChannel) assignNote(n *patternNote) {
// Some sensible row note states:
//
// [note] [instrument]
// no no keep playing the current note (if any)
// no yes "ghost instrument" (keeps the sample offset)
// yes no "ghost note" (keeps the volume)
// yes yes normal note play
//
// In practice, it's more complicated due to various effects
// that may affect the logical consistency.
ch.note = n
ch.effect = n.effect
noteKind := n.Kind()
if noteKind == noteEmpty {
return
}
hasNotePortamento := n.flags.Contains(noteHasNotePortamento)
if !hasNotePortamento && noteKind == noteNormal {
if n.flags.Contains(noteBadInstrument) {
// Cut the current note.
ch.inst = nil
ch.volume = 0
} else {
// Read some trailing samples for the transition.
if ch.inst == nil {
ch.rampSamples = [32]float64{}
} else {
for i := range ch.rampSamples {
ch.rampSamples[i] = float64(ch.NextSample())
}
}
ch.rampFrame = 0
ch.inst = n.inst
ch.volumeEnvelope.envelope = n.inst.volumeEnvelope
ch.panningEnvelope.envelope = n.inst.panningEnvelope
}
}
ch.vibratoPeriodOffset = 0
ch.keyOn = true
ch.resetEnvelopes()
if !hasNotePortamento && n.flags.Contains(noteValid) {
if n.period == 0 {
ch.period = linearPeriod(calcRealNote(n.raw, ch.inst))
} else {
ch.period = n.period
}
}
if !hasNotePortamento && noteKind != noteGhostInstrument {
ch.sampleOffset = 0
ch.reverse = false
}
if ch.inst != nil {
if noteKind != noteGhost {
ch.volume = ch.inst.volume
}
ch.panning = ch.inst.panning
}
}
func (ch *streamChannel) NextSample() int16 {
sampleOffset := int(ch.sampleOffset)
if sampleOffset >= len(ch.inst.samples) {
return 0
}
v := ch.inst.samples[sampleOffset]
ch.sampleOffset += ch.sampleStep
if ch.sampleOffset >= ch.inst.loopEnd {
for ch.sampleOffset >= ch.inst.loopEnd {
ch.sampleOffset -= ch.inst.loopLength
}
}
return v
}
func (ch *streamChannel) IsActive() bool {
if ch.inst == nil {
return false
}
if ch.inst.loopType == xmfile.SampleLoopNone {
if int(ch.sampleOffset) >= len(ch.inst.samples) {
return false
}
}
return true
}