Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Volume levels #419

Open
robertnhart opened this issue Oct 31, 2024 · 0 comments
Open

Volume levels #419

robertnhart opened this issue Oct 31, 2024 · 0 comments

Comments

@robertnhart
Copy link
Collaborator

In the following Discord server discussion, a user reported the volume levels heard from a MIDI file sounded louder in Signal than in another MIDI file application:

the way volume is scaled in Signal is broken

After investigating some, I think Signal's volume calculations should be updated. However, be aware that changing Signal's volume behavior may annoy existing users because it will make some parts of their existing compositions sound softer now.

Basic Problem

My understanding is MIDI channel volume, expression, and note on velocity should combine to form a relative sound wave amplitude like this:

V is the channel volume from 0 to 127
E is the expression from 0 to 127
Y is the note on velocity from 1 to 127
A is the relative sound wave amplitude from 0 (silence) to 1 (maximum)

( (V/127) × (E/127) × (Y/127) )² = A

According to some tests and peeking at Signal source code, I think Signal might be doing it like this:

(V/128) × (E/128) × (Y/128) = A

Without the square operation, the sound output in Signal will be louder than other MIDI devices. Signal is also using denominators of 128 instead of 127, which should probably also be corrected.

Formula Conversion Notes

I usually prefer to calculate a relative sound wave amplitude from 0 (silence) to 1 (maximum), and I think this is the kind of value that Signal is using during its sound wave volume calculations. However, the MIDI documents mentioned later use formulas that calculate a relative sound level in decibels from −∞ (silence) to 0 (maximum). The correspondence between a relative sound wave amplitude and a relative sound level is the following:

A is a relative sound wave amplitude from 0 (silence) to 1 (maximum).
L is a relative sound level in decibels from −∞ (silence) to 0 (maximum).

20 × log₁₀(A) = L

Also, when reading over the formulas in the MIDI documents, there are certain properties of logarithms that are useful to remember to be able to see how the different formulas are equivalent to one another:

log(a × b) = log(a) + log(b)
log(a²) = 2 × log(a)

References

In the document General MIDI System Level 1 Developer Guidelines, volume and expression are described on printed page 9, PDF page 13:

Volume (CC#7) and Expression (CC#11) should be implemented as follows:

For situations in which only CC# 7 is used (CC#11 is assumed "127"):
L(dB) = 40 log (V/127) where V= CC#7 value

For situations in which both [Volume and Expression] are used:
L(dB) = 40 log (V/127²) where V = (volume × expression)

If you re-write that last formula in the format I was using:

V is the channel volume from 0 to 127.
E is the expression from 0 to 127.
L is the relative sound level in decibels from −∞ (silence) to 0 (maximum).
A is the relative sound wave amplitude from 0 (silence) to 1 (maximum).

L = 40 × log₁₀((V × E)/127²)

then use various properties and conversions:

L = 40 × log₁₀( (V × E)/(127 × 127) )
L = 40 × log₁₀( (V/127) × (E/127) )
L = 2 × 20 × log₁₀( (V/127) × (E/127) )
L = 20 × log₁₀( ((V/127) × (E/127))² )
A = ((V/127) × (E/127))²

In the document General MIDI 2, volume is described on printed pages 6 and 7, PDF pages 10 and 11:

Regarding the curve of volume change messages, the square of the value is proportional to the volume.

The formula used is: gain in dB = 40 * log₁₀(cc7/127)

Expression is described on printed page 8, PDF page 12:

The formula used is: Gain in dB = (40 * log₁₀(cc7/127)) + (40 * log₁₀(cc11/127))

If you re-write that last formula in the format I was using:

V is the channel volume from 0 to 127.
E is the expression from 0 to 127.
L is the relative sound level in decibels from −∞ (silence) to 0 (maximum).
A is the relative sound wave amplitude from 0 (silence) to 1 (maximum).

L = 40 × log₁₀(V/127) + 40 × log₁₀(E/127)

then use various properties and conversions:

L = 40 × ( log₁₀(V/127) + log₁₀(E/127) )
L = 40 × log₁₀( (V/127) × (E/127) )
L = 2 × 20 × log₁₀( (V/127) × (E/127) )
L = 20 × log₁₀( ((V/127) × (E/127))² )
A = ((V/127) × (E/127))²

In the General MIDI 2 document, note on velocity is described on printed page 5, PDF page 9. This document just says "The velocity effect on volume is not defined", but I believe most MIDI devices combine the note on velocity in the volume calculation in the same kind of way:

V is the channel volume from 0 to 127
E is the expression from 0 to 127
Y is the note on velocity from 1 to 127
A is the relative sound wave amplitude from 0 (silence) to 1 (maximum)

A = ( (V/127) × (E/127) × (Y/127) )²

Test

I made a MIDI file that steps through the eight possible combinations of channel volume, expression, and note on velocity with values of either 127 or 90.

V: Channel volume
E: Expression
Y: Note On velocity
A1: Expected amplitude (percent of maximum) for the formula with the square operation
A2: Expected amplitude (percent of maximum) for the formula without the square operation

V E Y A1 A2
127 127 127 100 100
127 127 90 50 71
127 90 127 50 71
127 90 90 25 50
90 127 127 50 71
90 127 90 25 50
90 90 127 25 50
90 90 90 13 36

I played the test MIDI file with various output devices or software synthesizers and exported or recorded an audio file. I opened each audio file in Audacity, used the Amplify command to scale the maximum amplitude up to the 1.0 mark on the amplitude ruler, then I wrote down the rough amplitudes of the test notes by scrolling them next to the amplitude ruler.

Results so far: volume-test.zip (3.29 mebibytes, contains MIDI file, audio files, and OpenDocument spreadsheet)

In my tests, these software synthesizers matched the formula with the square operation:

  • Microsoft GS Wavetable Synth
  • Notation Player (using its Notation Software Synth)
  • Synthesia (using its Built-in MIDI Synthesizer, which uses BASS and BASSMIDI libraries, and "Built-in sounds by Voice Crystal")

From some previous testing, I also believe my Yamaha PSR-225 keyboard matches the formula with the square operation, but I don't have a way to record external devices at the moment to confirm.

These software synthesizers did not match the formula with the square operation:

  • VLC Media Player
  • Cakewalk (using its Cakewalk TTS-1 soft synth)
  • LMMS

But I think they might be using similar formulas with an exponent that is not exactly 2 (like maybe 1.75 or 1.5, or different exponents for volume, expression, and note on velocity), which is not like Signal's linear formula (or, in other words, an exponent of 1).

Additional information about LMMS: LMMS doesn't seem to use expression for anything. Also LMMS seems to have a bug where the initial volume only works the first time you play. When you play the second or later time, then the inital volume is forgotten and whatever the volume was last set to is what the volume starts out as.

I might gather more examples. I might design a different MIDI file to test with.

Code Changes

I think the following code changes will change Signal to use the formula with the square operation, but I don't know how to build changes to node_modules, so I haven't tested these changes.

file node_modules\@ryohey\wavelet\src\processor\SynthProcessorCore.ts

  • function noteOn(
    volume = velocity / 0x80 should have a denominator of 127.

  • function setMainVolume(
    state.volume = value / 0x80 should have a denominator of 127.

  • function expression(
    state.expression = value / 0x80 should have a denominator of 127.

  • function process(
    oscillator.volume = state.volume * state.expression
    note: sets the "this.volume" value used in the following code.

file node_modules\@ryohey\wavelet\src\processor\WavetableOscillator.ts

  • function process(
    const volume = this.velocity * this.volume * this.sample.volume
    I think this should be
    const volume = ((this.velocity * this.volume) ** 2) * this.sample.volume
ryohey added a commit to ryohey/wavelet that referenced this issue Nov 17, 2024
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

No branches or pull requests

1 participant