Skip to content

Instantly share code, notes, and snippets.

@kyzentun
Last active September 21, 2023 07:37
Show Gist options
  • Star 1 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save kyzentun/33da0603a0f46989f468b480c5fd3414 to your computer and use it in GitHub Desktop.
Save kyzentun/33da0603a0f46989f468b480c5fd3414 to your computer and use it in GitHub Desktop.

Quantization Overview

Note data

A note occurs at a given absolute beat, which is its distance from the beginning of the chart, unaffected by time signatures.

Each note is quantized by two numbers: parts_per_beat and part_id.

parts_per_beat is how many parts a beat is divided into.

part_id is how many of those divisions are before this note in the beat.

Time signatures

A time signature has three relevant fields: start_beat, beats_per_measure, and note_value_per_beat.

start_beat is the beat the time signature starts on. This is distance from the beginning of the chart, unaffected by time signatures.

beats_per_measure is the number of beats in a measure.

note_value_per_beat is how many pennies a note that occurs once per beat can be exchanged for.

The time signature data is used with the note's beat value to calculate the parts_per_beat and part_id for the note like this:

local note_beat= 13 -- Fetched from note data.
local curr_time_signature= {start= 0, beats_per_measure= 4, note_value_per_beat= 4} -- Fetched from timing data.
local signature_multiple= curr_time_signature.note_value_per_beat / 4 -- Calculate relative to 4/4 time because it's the most common.
local dist_from_signature_start= (note_beat - curr_time_signature.start) * signature_multiple
local beat_in_curr_measure= dist_from_signature_start % curr_time_signature.beats_per_measure
local parts_per_beat, part_id= fractionate_number(beat_in_curr_measure % 1)

fractionate_number is a function that finds the closest fraction with a denominator less than or equal to 100 to fit the number.

Noteskins

Noteskins provide quantization data for a parts_per_beat value.

The quantization data can optionally be split to give each possible part_id value a different appearance.

When displaying a note, the notefield finds the data with a matching parts_per_beat and matching part_id. If there is no part_id match, then the first entry with a matching parts_per_beat is used.

Unknown quantizations

If there is no parts_per_beat match, there is an entry for unknowns. The note's row_id is used instead of part_id (modulus by the number of part_id entries).

Playerization (Routine/Couples mode)

In a chart meant for multiple players, each note has a player number. That player number is used instead of parts_per_beat to pick the quantization for the note.

Modifiers:

Mods can change the parts_per_beat or part_id values of a note before they are used to display the note.

parts_per_beat and part_id are fields in the mod input.

Appendix

Example tap note quantization data

This replaces the state map used to quantize and animate notes in SM 5.1.-3.

Mimimal noteskin

2D Sprite style

{
	unknown= {
		{0, 1, 2, 3}, -- Use these frames to animate a note.
		{4, 5, 6, 7},
		{8, 9, 10, 11},
		{12, 13, 14, 15},
	}
}

3D Model style

{
	unknown= {
		-- Translate the texture to this position and use the beat to set seconds into animation.
		{trans_x= 0, trans_y= 0},
		{trans_x= .25, trans_y= 0},
		{trans_x= .5, trans_y= 0},
		{trans_x= .75, trans_y= 0},
	}
}

Alternative minimal

2D Sprite style

{
	{
		{0, 1, 2, 3},
		{4, 5, 6, 7},
		{8, 9, 10, 11},
		{12, 13, 14, 15},
	}
}

3D Model style

{
	{
		{trans_x= 0, trans_y= 0},
		{trans_x= .25, trans_y= 0},
		{trans_x= .5, trans_y= 0},
		{trans_x= .75, trans_y= 0},
	}
}

Single quantization for each parts_per_beat

2D Sprite style

{
	{
		{0, 1, 2, 3},
	}
}

3D Model style

{
	{
		{trans_x= 0, trans_y= 0},
	}
}

Multiple quantizations

{
	-- Notes that occur once per beat.
	-- Also used for player 1.
	[1]= {
		{0, 1, 2, 3}, -- One per beat notes for player 1.
		{4, 5, 6, 7}, -- Two per beat notes for player 1.
		{8, 9, 10, 11}, -- Four per beat notes for player 1.
		{12, 13, 14, 15}, -- Eight per beat notes for player 1.
	},
	-- Notes that occur two times per beat.
	-- Also used for player 2.
	[2]= {
		{...}, -- One per beat notes for player 2.
		{...}, -- Two per beat notes for player 2.
		{...}, -- Four per beat notes for player 2.
		{...}, -- Eight per beat notes for player 2.
	},
	-- Notes that occur four times per beat.
	-- Also used for player 2.
	[4]= {
		[1]= {...}, -- The 16th that occurs after the 4th.
			-- Also one per beat notes for player 3.
		[3]= {...}, -- The 16th that occurs before the 4th.
			-- Two per beat notes for player 3.
		[5]= {...}, -- Four per beat notes for player 3.
		[7]= {...}, -- Eight per beat notes for player 3.
	},
	-- Notes that occur eight times per beat.
	-- Also used for player 2.
	[8]= {
		[1]= {...}, -- The 32nd that occurs after the 4th.
			-- Also one per beat notes for player 4.
		[3]= {...}, -- The 32nd that occurs after the first 16th.
			-- Two per beat notes for player 4.
		[5]= {...}, -- The 32nd that occurs after the 8th.
			-- Four per beat notes for player 4.
		[7]= {...}, -- The 32nd that occurs before the 4th.
			-- Eight per beat notes for player 4.
	},
	unknown= {
		{...},
		{...},
		{...},
		{...},
	},
}

Separated playerization data

{
	-- Notes that occur once per beat.
	[1]= {{0, 1, 2, 3}},
	-- Notes that occur two times per beat.
	[2]= {{4, 5, 6, 7}},
	-- Notes that occur four times per beat.
	[4]= {{8, 9, 10, 11}},
	-- Notes that occur eight times per beat.
	[8]= {{12, 13, 14, 15}},
	unknown= {{...}},
	player= {
		-- Notes for player 1.
		{
			[1]= {16}, -- One per beat.
			[2]= {17}, -- Two per beat.
			[4]= {18}, -- Four per beat.
			[8]= {19}, -- Eight per beat.
		},
		-- Notes for player 2.
		{
			[1]= {20}, -- One per beat.
			[2]= {21}, -- Two per beat.
			[4]= {22}, -- Four per beat.
			[8]= {23}, -- Eight per beat.
		},
		-- Notes for player 3.
		{
			[1]= {24}, -- One per beat.
			[2]= {25}, -- Two per beat.
			[4]= {26}, -- Four per beat.
			[8]= {27}, -- Eight per beat.
		},
		-- Notes for player 4.
		{
			[1]= {28}, -- One per beat.
			[2]= {29}, -- Two per beat.
			[4]= {30}, -- Four per beat.
			[8]= {31}, -- Eight per beat.
		},
	},
}

The problem with separated playerization data is that it requires the noteskin author to plan for a specific number of players.

If normal quantization data is reinterpreted as playerization data, every noteskin supports as many players as it has quantizations.

So, separated playerization data is not going to be used because it would mean fewer noteskins for modes with more players.

fractionate_number C++ code

All notes in a chart are fractionated before gameplay begins (ideally by loading the data from the simfile, but that's a problem for the future). This is not performed every frame for every note.

void fractionate_number(double number, int& numenator, int& denomerator)
{
	double fraction_dist= 10.0;
	double min_dist= 1.0/1000.0; // Try to cut it short, so not every number has to go through the loop 100 times.
	for(int denom= 2; denom < 100; ++denom)
	{
		int numer= number * denom;
		double dist= std::fabs(number - (double(numer) / double(denom)));
		if(dist < fraction_dist)
		{
			numenator= numer;
			denomerator= denom;
			fraction_dist= dist;
			if(dist < min_dist) { break; }
		}
	}
}
@1033Forest
Copy link

1033Forest commented Nov 5, 2016

And when arbitrary quantizations get supported (ability to place a note on any row or beat or any millisecond) how are .sm files supposed to encode it? It would need like 600 lines for one measure, if you were looking to put the special 5 or 7/4 quants (20ths and 28ths)

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment