aboutsummaryrefslogtreecommitdiff
path: root/audio/softsynth/mt32/LA32Ramp.cpp
blob: 454612c842bc28d106c6acba46d9bb7a8d085439 (plain)
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
/* Copyright (C) 2003, 2004, 2005, 2006, 2008, 2009 Dean Beeler, Jerome Fisher
 * Copyright (C) 2011, 2012, 2013, 2014 Dean Beeler, Jerome Fisher, Sergey V. Mikayev
 *
 *  This program is free software: you can redistribute it and/or modify
 *  it under the terms of the GNU Lesser General Public License as published by
 *  the Free Software Foundation, either version 2.1 of the License, or
 *  (at your option) any later version.
 *
 *  This program is distributed in the hope that it will be useful,
 *  but WITHOUT ANY WARRANTY; without even the implied warranty of
 *  MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 *  GNU Lesser General Public License for more details.
 *
 *  You should have received a copy of the GNU Lesser General Public License
 *  along with this program.  If not, see <http://www.gnu.org/licenses/>.
 */

/*
Some notes on this class:

This emulates the LA-32's implementation of "ramps". A ramp in this context is a smooth transition from one value to another, handled entirely within the LA-32.
The LA-32 provides this feature for amplitude and filter cutoff values.

The 8095 starts ramps on the LA-32 by setting two values in memory-mapped registers:

(1) The target value (between 0 and 255) for the ramp to end on. This is represented by the "target" argument to startRamp().
(2) The speed at which that value should be approached. This is represented by the "increment" argument to startRamp().

Once the ramp target value has been hit, the LA-32 raises an interrupt.

Note that the starting point of the ramp is whatever internal value the LA-32 had when the registers were set. This is usually the end point of a previously completed ramp.

Our handling of the "target" and "increment" values is based on sample analysis and a little guesswork.
Here's what we're pretty confident about:
 - The most significant bit of "increment" indicates the direction that the LA32's current internal value ("current" in our emulation) should change in.
   Set means downward, clear means upward.
 - The lower 7 bits of "increment" indicate how quickly "current" should be changed.
 - If "increment" is 0, no change to "current" is made and no interrupt is raised. [SEMI-CONFIRMED by sample analysis]
 - Otherwise, if the MSb is set:
    - If "current" already corresponds to a value <= "target", "current" is set immediately to the equivalent of "target" and an interrupt is raised.
    - Otherwise, "current" is gradually reduced (at a rate determined by the lower 7 bits of "increment"), and once it reaches the equivalent of "target" an interrupt is raised.
 - Otherwise (the MSb is unset):
    - If "current" already corresponds to a value >= "target", "current" is set immediately to the equivalent of "target" and an interrupt is raised.
    - Otherwise, "current" is gradually increased (at a rate determined by the lower 7 bits of "increment"), and once it reaches the equivalent of "target" an interrupt is raised.

We haven't fully explored:
 - Values when ramping between levels (though this is probably correct).
 - Transition timing (may not be 100% accurate, especially for very fast ramps).
*/
//#include <cmath>

#include "mt32emu.h"
#include "LA32Ramp.h"
#include "mmath.h"

namespace MT32Emu {

// SEMI-CONFIRMED from sample analysis.
const int TARGET_MULT = 0x40000;
const unsigned int MAX_CURRENT = 0xFF * TARGET_MULT;

// We simulate the delay in handling "target was reached" interrupts by waiting
// this many samples before setting interruptRaised.
// FIXME: This should vary with the sample rate, but doesn't.
// SEMI-CONFIRMED: Since this involves asynchronous activity between the LA32
// and the 8095, a good value is hard to pin down.
// This one matches observed behaviour on a few digital captures I had handy,
// and should be double-checked. We may also need a more sophisticated delay
// scheme eventually.
const int INTERRUPT_TIME = 7;

LA32Ramp::LA32Ramp() :
	current(0),
	largeTarget(0),
	largeIncrement(0),
	interruptCountdown(0),
	interruptRaised(false) {
}

void LA32Ramp::startRamp(Bit8u target, Bit8u increment) {
	// CONFIRMED: From sample analysis, this appears to be very accurate.
	if (increment == 0) {
		largeIncrement = 0;
	} else {
		// Three bits in the fractional part, no need to interpolate
		// (unsigned int)(EXP2F(((increment & 0x7F) + 24) / 8.0f) + 0.125f)
		Bit32u expArg = increment & 0x7F;
		largeIncrement = 8191 - Tables::getInstance().exp9[~(expArg << 6) & 511];
		largeIncrement <<= expArg >> 3;
		largeIncrement += 64;
		largeIncrement >>= 9;
	}
	descending = (increment & 0x80) != 0;
	if (descending) {
		// CONFIRMED: From sample analysis, descending increments are slightly faster
		largeIncrement++;
	}

	largeTarget = target * TARGET_MULT;
	interruptCountdown = 0;
	interruptRaised = false;
}

Bit32u LA32Ramp::nextValue() {
	if (interruptCountdown > 0) {
		if (--interruptCountdown == 0) {
			interruptRaised = true;
		}
	} else if (largeIncrement != 0) {
		// CONFIRMED from sample analysis: When increment is 0, the LA32 does *not* change the current value at all (and of course doesn't fire an interrupt).
		if (descending) {
			// Lowering current value
			if (largeIncrement > current) {
				current = largeTarget;
				interruptCountdown = INTERRUPT_TIME;
			} else {
				current -= largeIncrement;
				if (current <= largeTarget) {
					current = largeTarget;
					interruptCountdown = INTERRUPT_TIME;
				}
			}
		} else {
			// Raising current value
			if (MAX_CURRENT - current < largeIncrement) {
				current = largeTarget;
				interruptCountdown = INTERRUPT_TIME;
			} else {
				current += largeIncrement;
				if (current >= largeTarget) {
					current = largeTarget;
					interruptCountdown = INTERRUPT_TIME;
				}
			}
		}
	}
	return current;
}

bool LA32Ramp::checkInterrupt() {
	bool wasRaised = interruptRaised;
	interruptRaised = false;
	return wasRaised;
}

void LA32Ramp::reset() {
	current = 0;
	largeTarget = 0;
	largeIncrement = 0;
	descending = false;
	interruptCountdown = 0;
	interruptRaised = false;
}

}