What you want is called automatic gain control or dynamic range compression. It can be implemented in a dynamics processor that takes the input audio, uses an envelope follower to analyze the volume of the input as it changes over time and modifies the audio so that the envelope of its output corresponds to the input envelope put through a non-linear function such as:

Adobe Audition Dynamics Processing
I don't know what is the best practice in how the envelope should be modified to match the target envelope, but usually there are separate parameters for attack and decay that control how fast the applied gain should be increased or decreased, respectively, to make the resultant envelope approach the target envelope. You could try the potentially unstable:
$$\text{gain}(t) = \text{target_envelope}(t)/\text{input_envelope}(t)$$
where $t$ is time. The resultant $\text{gain}(t)$ should be smoothed (lowpass filtered) before applying it, to get the wanted attack and decay behavior. The envelope detector can have additional controls for changing the (usually root mean square or peak) analysis window, and a lookahead time setting that enables the dynamics processor to respond to volume changes before they take place, by delaying the input audio after envelope detection. The root mean square calculation can be done by calculating the square of the input audio, lowpass filtering that and taking the square root of the result.

Adobe Audition Dynamics Processing attack, decay and lookahead controls
These parameters and the non-linear function should be tuned (by listening) for minimal audible artifacts such as pumping of the volume and low-frequency distortion.