diff --git a/.editorconfig b/.editorconfig new file mode 100644 index 0000000..a41b0d4 --- /dev/null +++ b/.editorconfig @@ -0,0 +1,254 @@ +# Remove the line below if you want to inherit .editorconfig settings from higher directories +root = true + +# C# files +[*.cs] + +#### Core EditorConfig Options #### + +# Indentation and spacing +indent_size = 4 +indent_style = tab +tab_width = 4 + +# New line preferences +end_of_line = crlf +insert_final_newline = false + +#### .NET Coding Conventions #### + +# Organize usings +dotnet_separate_import_directive_groups = false +dotnet_sort_system_directives_first = true +file_header_template = unset + +# this. and Me. preferences +dotnet_style_qualification_for_event = false +dotnet_style_qualification_for_field = false +dotnet_style_qualification_for_method = false +dotnet_style_qualification_for_property = false + +# Language keywords vs BCL types preferences +dotnet_style_predefined_type_for_locals_parameters_members = true +dotnet_style_predefined_type_for_member_access = true + +# Parentheses preferences +dotnet_style_parentheses_in_arithmetic_binary_operators = always_for_clarity +dotnet_style_parentheses_in_other_binary_operators = always_for_clarity +dotnet_style_parentheses_in_other_operators = never_if_unnecessary +dotnet_style_parentheses_in_relational_binary_operators = always_for_clarity + +# Modifier preferences +dotnet_style_require_accessibility_modifiers = for_non_interface_members + +# Expression-level preferences +dotnet_style_coalesce_expression = true +dotnet_style_collection_initializer = true +dotnet_style_explicit_tuple_names = true:warning +dotnet_style_namespace_match_folder = true +dotnet_style_null_propagation = true +dotnet_style_object_initializer = true +dotnet_style_operator_placement_when_wrapping = beginning_of_line +dotnet_style_prefer_auto_properties = true +dotnet_style_prefer_collection_expression = when_types_loosely_match +dotnet_style_prefer_compound_assignment = true +dotnet_style_prefer_conditional_expression_over_assignment = true +dotnet_style_prefer_conditional_expression_over_return = true +dotnet_style_prefer_foreach_explicit_cast_in_source = when_strongly_typed +dotnet_style_prefer_inferred_anonymous_type_member_names = true +dotnet_style_prefer_inferred_tuple_names = true:warning +dotnet_style_prefer_is_null_check_over_reference_equality_method = true +dotnet_style_prefer_simplified_boolean_expressions = true +dotnet_style_prefer_simplified_interpolation = true + +# Field preferences +dotnet_style_readonly_field = true + +# Parameter preferences +dotnet_code_quality_unused_parameters = all + +# Suppression preferences +dotnet_remove_unnecessary_suppression_exclusions = none + +# New line preferences +dotnet_style_allow_multiple_blank_lines_experimental = true +dotnet_style_allow_statement_immediately_after_block_experimental = false + +#### C# Coding Conventions #### + +# var preferences +csharp_style_var_elsewhere = false +csharp_style_var_for_built_in_types = false +csharp_style_var_when_type_is_apparent = false + +# Expression-bodied members +csharp_style_expression_bodied_accessors = true:silent +csharp_style_expression_bodied_constructors = false:silent +csharp_style_expression_bodied_indexers = true:silent +csharp_style_expression_bodied_lambdas = true:silent +csharp_style_expression_bodied_local_functions = false:silent +csharp_style_expression_bodied_methods = false:silent +csharp_style_expression_bodied_operators = false:silent +csharp_style_expression_bodied_properties = true:silent + +# Pattern matching preferences +csharp_style_pattern_matching_over_as_with_null_check = true +csharp_style_pattern_matching_over_is_with_cast_check = true +csharp_style_prefer_extended_property_pattern = true +csharp_style_prefer_not_pattern = true +csharp_style_prefer_pattern_matching = true:suggestion +csharp_style_prefer_switch_expression = true + +# Null-checking preferences +csharp_style_conditional_delegate_call = true + +# Modifier preferences +csharp_prefer_static_local_function = true +csharp_preferred_modifier_order = public,private,protected,internal,file,static,extern,new,virtual,abstract,sealed,override,readonly,unsafe,required,volatile,async +csharp_style_prefer_readonly_struct = true +csharp_style_prefer_readonly_struct_member = true + +# Code-block preferences +csharp_prefer_braces = true:silent +csharp_prefer_simple_using_statement = true:suggestion +csharp_style_namespace_declarations = file_scoped:suggestion +csharp_style_prefer_method_group_conversion = true:silent +csharp_style_prefer_primary_constructors = false:suggestion +csharp_style_prefer_top_level_statements = true:silent + +# Expression-level preferences +csharp_prefer_simple_default_expression = true:suggestion +csharp_style_deconstructed_variable_declaration = true +csharp_style_implicit_object_creation_when_type_is_apparent = true +csharp_style_inlined_variable_declaration = true +csharp_style_prefer_index_operator = true:suggestion +csharp_style_prefer_local_over_anonymous_function = true:suggestion +csharp_style_prefer_null_check_over_type_check = true:suggestion +csharp_style_prefer_range_operator = true +csharp_style_prefer_tuple_swap = true:warning +csharp_style_prefer_utf8_string_literals = true +csharp_style_throw_expression = true:suggestion +csharp_style_unused_value_assignment_preference = discard_variable +csharp_style_unused_value_expression_statement_preference = discard_variable + +# 'using' directive preferences +csharp_using_directive_placement = outside_namespace:silent + +# New line preferences +csharp_style_allow_blank_line_after_colon_in_constructor_initializer_experimental = false +csharp_style_allow_blank_line_after_token_in_arrow_expression_clause_experimental = true +csharp_style_allow_blank_line_after_token_in_conditional_expression_experimental = false:suggestion +csharp_style_allow_blank_lines_between_consecutive_braces_experimental = false +csharp_style_allow_embedded_statements_on_same_line_experimental = true + +#### C# Formatting Rules #### + +# New line preferences +csharp_new_line_before_catch = false +csharp_new_line_before_else = false +csharp_new_line_before_finally = false +csharp_new_line_before_members_in_anonymous_types = true +csharp_new_line_before_members_in_object_initializers = true +csharp_new_line_before_open_brace = none +csharp_new_line_between_query_expression_clauses = true + +# Indentation preferences +csharp_indent_block_contents = true +csharp_indent_braces = false +csharp_indent_case_contents = true +csharp_indent_case_contents_when_block = false +csharp_indent_labels = flush_left +csharp_indent_switch_labels = true + +# Space preferences +csharp_space_after_cast = true +csharp_space_after_colon_in_inheritance_clause = true +csharp_space_after_comma = true +csharp_space_after_dot = false +csharp_space_after_keywords_in_control_flow_statements = true +csharp_space_after_semicolon_in_for_statement = true +csharp_space_around_binary_operators = before_and_after +csharp_space_around_declaration_statements = ignore +csharp_space_before_colon_in_inheritance_clause = true +csharp_space_before_comma = false +csharp_space_before_dot = false +csharp_space_before_open_square_brackets = false +csharp_space_before_semicolon_in_for_statement = false +csharp_space_between_empty_square_brackets = false +csharp_space_between_method_call_empty_parameter_list_parentheses = false +csharp_space_between_method_call_name_and_opening_parenthesis = false +csharp_space_between_method_call_parameter_list_parentheses = false +csharp_space_between_method_declaration_empty_parameter_list_parentheses = false +csharp_space_between_method_declaration_name_and_open_parenthesis = false +csharp_space_between_method_declaration_parameter_list_parentheses = false +csharp_space_between_parentheses = false +csharp_space_between_square_brackets = false + +# Wrapping preferences +csharp_preserve_single_line_blocks = true +csharp_preserve_single_line_statements = true + +#### Naming styles #### + +# Naming rules + +dotnet_naming_rule.interface_should_be_begins_with_i.severity = suggestion +dotnet_naming_rule.interface_should_be_begins_with_i.symbols = interface +dotnet_naming_rule.interface_should_be_begins_with_i.style = begins_with_i + +dotnet_naming_rule.types_should_be_pascal_case.severity = suggestion +dotnet_naming_rule.types_should_be_pascal_case.symbols = types +dotnet_naming_rule.types_should_be_pascal_case.style = pascal_case + +dotnet_naming_rule.non_field_members_should_be_pascal_case.severity = suggestion +dotnet_naming_rule.non_field_members_should_be_pascal_case.symbols = non_field_members +dotnet_naming_rule.non_field_members_should_be_pascal_case.style = pascal_case + +# Symbol specifications + +dotnet_naming_symbols.interface.applicable_kinds = interface +dotnet_naming_symbols.interface.applicable_accessibilities = public, internal, private, protected, protected_internal, private_protected +dotnet_naming_symbols.interface.required_modifiers = + +dotnet_naming_symbols.types.applicable_kinds = class, struct, interface, enum +dotnet_naming_symbols.types.applicable_accessibilities = public, internal, private, protected, protected_internal, private_protected +dotnet_naming_symbols.types.required_modifiers = + +dotnet_naming_symbols.non_field_members.applicable_kinds = property, event, method +dotnet_naming_symbols.non_field_members.applicable_accessibilities = public, internal, private, protected, protected_internal, private_protected +dotnet_naming_symbols.non_field_members.required_modifiers = + +# Naming styles + +dotnet_naming_style.pascal_case.required_prefix = +dotnet_naming_style.pascal_case.required_suffix = +dotnet_naming_style.pascal_case.word_separator = +dotnet_naming_style.pascal_case.capitalization = pascal_case + +dotnet_naming_style.begins_with_i.required_prefix = I +dotnet_naming_style.begins_with_i.required_suffix = +dotnet_naming_style.begins_with_i.word_separator = +dotnet_naming_style.begins_with_i.capitalization = pascal_case + +[*.{cs,vb}] +dotnet_style_coalesce_expression = true:suggestion +dotnet_style_null_propagation = true:suggestion +dotnet_style_prefer_is_null_check_over_reference_equality_method = true:suggestion +dotnet_style_prefer_auto_properties = true:silent +dotnet_style_operator_placement_when_wrapping = beginning_of_line +tab_width = 4 +indent_size = 4 +end_of_line = crlf +insert_final_newline = true +dotnet_style_object_initializer = true:suggestion +dotnet_style_prefer_simplified_boolean_expressions = true:suggestion +dotnet_style_collection_initializer = true:suggestion +dotnet_style_prefer_conditional_expression_over_return = true:silent +dotnet_style_prefer_conditional_expression_over_assignment = true:silent +dotnet_style_prefer_inferred_tuple_names = true:warning +dotnet_style_explicit_tuple_names = true:warning +dotnet_style_prefer_compound_assignment = true:suggestion +dotnet_style_prefer_inferred_anonymous_type_member_names = true:suggestion +dotnet_style_prefer_collection_expression = when_types_loosely_match:suggestion +dotnet_style_prefer_simplified_interpolation = true:suggestion +dotnet_style_namespace_match_folder = true:suggestion \ No newline at end of file diff --git a/.gitattributes b/.gitattributes new file mode 100644 index 0000000..1ff0c42 --- /dev/null +++ b/.gitattributes @@ -0,0 +1,63 @@ +############################################################################### +# Set default behavior to automatically normalize line endings. +############################################################################### +* text=auto + +############################################################################### +# Set default behavior for command prompt diff. +# +# This is need for earlier builds of msysgit that does not have it on by +# default for csharp files. +# Note: This is only used by command line +############################################################################### +#*.cs diff=csharp + +############################################################################### +# Set the merge driver for project and solution files +# +# Merging from the command prompt will add diff markers to the files if there +# are conflicts (Merging from VS is not affected by the settings below, in VS +# the diff markers are never inserted). Diff markers may cause the following +# file extensions to fail to load in VS. An alternative would be to treat +# these files as binary and thus will always conflict and require user +# intervention with every merge. To do so, just uncomment the entries below +############################################################################### +#*.sln merge=binary +#*.csproj merge=binary +#*.vbproj merge=binary +#*.vcxproj merge=binary +#*.vcproj merge=binary +#*.dbproj merge=binary +#*.fsproj merge=binary +#*.lsproj merge=binary +#*.wixproj merge=binary +#*.modelproj merge=binary +#*.sqlproj merge=binary +#*.wwaproj merge=binary + +############################################################################### +# behavior for image files +# +# image files are treated as binary by default. +############################################################################### +#*.jpg binary +#*.png binary +#*.gif binary + +############################################################################### +# diff behavior for common document formats +# +# Convert binary document formats to text before diffing them. This feature +# is only available from the command line. Turn it on by uncommenting the +# entries below. +############################################################################### +#*.doc diff=astextplain +#*.DOC diff=astextplain +#*.docx diff=astextplain +#*.DOCX diff=astextplain +#*.dot diff=astextplain +#*.DOT diff=astextplain +#*.pdf diff=astextplain +#*.PDF diff=astextplain +#*.rtf diff=astextplain +#*.RTF diff=astextplain diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..2b9e6b8 --- /dev/null +++ b/.gitignore @@ -0,0 +1,9 @@ +test/* +bin/* +obj/* +*/*/bin/* +*/*/obj/* +.vs/* +*/.vs/* +*.user +*/Setup/* \ No newline at end of file diff --git a/BRRConversionException.cs b/BRRConversionException.cs new file mode 100644 index 0000000..ab3f601 --- /dev/null +++ b/BRRConversionException.cs @@ -0,0 +1,12 @@ +namespace BRRSuite; + +/// +/// The exception that is thrown when a problem occurs preventing the proper encoding or decoding of BRR or audio data. +/// +public sealed class BRRConversionException : Exception { + /// + /// Creates a new with the specified message. + /// + /// + internal BRRConversionException(string? message) : base(message) { } +} diff --git a/BRRDataIssue.cs b/BRRDataIssue.cs new file mode 100644 index 0000000..3ae5580 --- /dev/null +++ b/BRRDataIssue.cs @@ -0,0 +1,67 @@ +namespace BRRSuite; + +/// +/// Flags problems with BRR sample data. +/// +[Flags] +public enum BRRDataIssue : int { + /// + /// No issues were found with this data. + /// + None = 0, + + /// + /// Indicates raw data was not a multiple of 9. + /// + WrongAlignment = 1 << 0, + + /// + /// Indicates raw data was not a multiple of 9, and that there appears to be a loop header. + /// + WrongAlignmentForHeadered = 1 << 1, + + /// + /// Indicates that the data is not large enough to be a proper sample. This issue is never resolvable. + /// + DataTooSmall = 1 << 2, + + /// + /// Indicates that the data is far too large to reasonably be interpreted as a BRR sample. + /// + DataTooLarge = 1 << 3, + + /// + /// Indicates that the final block's header does not contain the end of sample flag. + /// + MissingEndFlag = 1 << 4, + + /// + /// Indicates that the end flag appears on any header other than the final block. + /// + EarlyEndFlags = 1 << 5, + + /// + /// Indicates that the final block's header does not contain the end of sample flag. + /// + MissingLoopFlag = 1 << 6, + + /// + /// Indicates that no loop point was specified for the data. This issue is never resolvable. + /// + MissingLoopPoint = 1 << 7, + + /// + /// Indicates that the specified loop point is not aligned to a BRR block. This issue is never resolvable for a looped sample but is technically okay for an unlooped sample. + /// + MisalignedLoopPoint = 1 << 8, + + /// + /// Indicates that the specified loop point is past the end of the data. This issue is never resolvable for a looped sample but is technically okay for an unlooped sample. + /// + OutOfRangeLoopPoint = 1 << 9, + + /// + /// Indicates issues were found with the data that cannot be fixed without more information. + /// + Unresolvable = 1 << 31, +} diff --git a/BRRSample.cs b/BRRSample.cs new file mode 100644 index 0000000..7d6071e --- /dev/null +++ b/BRRSample.cs @@ -0,0 +1,722 @@ +namespace BRRSuite; + +/// +/// Contains sample data and metadata about a Bit Rate Reduction sample. +/// +public class BRRSample { + /// + /// The preferred file extension for a raw BRR sample. + /// + public const string Extension = "brr"; + + /// + /// The preferred file extension for a headered BRR sample. + /// + public const string HeaderedExtension = "brh"; + + // a very generous limit to the size of the file. + private const int MaxSize = 0xFF00; + private const int MaxBlocks = MaxSize / BrrBlockSize; + + /// + /// Gets or sets the instrument name associated with this sample. + /// The name should be exactly 24 characters in length (padded with spaces if necessary) and only use ASCII characters. + /// These are enforced by the property setter. + /// + public string InstrumentName { + get => _name; + set { + // sanitize to ascii + value = string.Concat(value.Where(c => char.IsAscii(c) && !char.IsControl(c))); + + // encforce length + _name = value.Length switch { + SuiteSample.InstrumentNameLength => value, + < SuiteSample.InstrumentNameLength => value.PadRight(SuiteSample.InstrumentNameLength, SuiteSample.InstrumentNamePadChar), + > SuiteSample.InstrumentNameLength => value[..SuiteSample.InstrumentNameLength] + }; + } + } + + // default name + private string _name = new(SuiteSample.InstrumentNamePadChar, SuiteSample.InstrumentNameLength); + + /// + /// Gets or sets the block within this sample that will be looped to when it reaches the end. + /// An invalid value will be changed to -1. + /// + public int LoopBlock { + get => _loopblock; + set { + // prevent invalid loop points and standardize negative values + if (value < 0 || value >= BlockCount) { + _loopblock = NoLoop; + return; + } + _loopblock = value; + } + } + + private int _loopblock = NoLoop; + + /// + /// Gets the location of this sample's loop point as an offset in bytes. + /// + public int LoopPoint => _loopblock switch { + < 0 => NoLoop, + _ => _loopblock * BrrBlockSize + }; + + /// + /// Gets whether or not this sample should loop, based on the existence of a loop point. + /// + public bool IsLooping => _loopblock >= 0; + + private bool HasLoopFlag => (_data[^BrrBlockSize] & LoopFlag) == LoopFlag; + + /// + /// Gets the resampling ratio this sample was encoded at, relative its original sample. Defaults to 1.0. + /// + public decimal ResampleRatio { get; internal set; } = 1.0M; + + /// + /// Gets or sets the target frequency this sample was encoded at. Defaults to 32000. + /// + public int EncodingFrequency { get; set; } = 32000; + + /// + /// Gets the length of this sample in bytes. + /// + public int Length => Data.Length; + + /// + /// Gets the length of this sample in blocks. + /// + public int BlockCount => Data.Length / BrrBlockSize; + + /// + /// Gets or sets the SPC sound system's DSP VxPITCH value that corresponds to an audible frequency for a C note. + /// + /// The value 0x1000 corresponds to no frequency change on playback; i.e., a 32kHz sample being played at 32kHz. + /// The value 0x0000 indicates the frequency of C for this sample is not known. + /// Values of 0x4000 and higher are considered invalid. Removal of these bits is enforced by the property setter. + /// + /// + public int VxPitch { + get => _vxpitch; + set => _vxpitch = (ushort) (value & 0x3FFF); + } + + private ushort _vxpitch = 0x1000; + + /// + /// Gets the raw data stream of this BRR sample. + /// + public byte[] Data => _data; + + // Why are you reading this? Can't you see that it says "private"? + private readonly byte[] _data; + + /// + /// Gets or sets the data at a given index. + /// + public byte this[Index i] { + get => _data[i]; + set => _data[i] = value; + } + + /// + /// Creates a new instance of the class with the specified number of blocks. + /// The total size of the samples data will be * 9. + /// + /// The number of 72-bit blocks to alllocate for this sample. + /// If the number of blocks requested is 0, negative, or too many blocks. + public BRRSample(int blocks) { + ThrowIfBadBlocks(blocks); + + _data = new byte[blocks * BrrBlockSize]; + } + + /// + /// Creates a new instance of the class with a copy of the given data. + /// + /// The BRR data to use for this sample. The length of this array should be a multiple of 9. + /// If the input array is empty or too large. + public BRRSample(byte[] data) { + if ((data.Length % BrrBlockSize) is not 0) { + throw new ArgumentException("The input array is not a multiple of 9 bytes in length.", nameof(data)); + } + + ThrowIfBadBlocks(data.Length / BrrBlockSize); + + _data = data[..]; + } + + /// + /// Checks if the number of blocks being encoded is valid. + /// Throws an exception if it is not. + /// + /// The number of blocks being encoded. + /// + private static void ThrowIfBadBlocks(int blocks) { + switch (blocks) { + case 0: + throw new ArgumentException("Cannot create a BRR sample with 0 blocks.", nameof(blocks)); + + case < 0: + throw new ArgumentException("Cannot create a BRR sample with a negative number of blocks.", nameof(blocks)); + + case >= MaxBlocks: + throw new ArgumentException($"Cannot create a BRR sample with more than {MaxBlocks - 1} blocks.", nameof(blocks)); + } + } + + + /// + /// Gets a segment of data corresponding to the 9 bytes of the requested block. + /// + /// Index of block to cover. + /// A of type over the specified block. + /// If the index requested is negative or more than the number of blocks in the sample. + public Span GetBlockAt(int block) { + if (block > BlockCount || block < 0) { + throw new IndexOutOfRangeException(); + } + + return new(_data, block * BrrBlockSize, BrrBlockSize); + } + + /// + /// Adjusts the header of the final block to include the end of sample flag + /// and corrects the loop flag based on the existence of a loop point. + /// + public void FixEndBlock() { + ref byte lastHeader = ref _data[^BrrBlockSize]; + lastHeader |= EndFlag; + + if (IsLooping) { + lastHeader |= LoopFlag; + } else { + lastHeader &= LoopFlagOff; + } + } + + + /// + /// Throws an error if the given sample has issues that cannot be fixed programmatically. + /// + /// The sample to validate. + /// An unresolvable issue was found. + private static void ThrowIfUnresolvableIssues(BRRSample brr) { + BRRDataIssue issues = ValidateBRR(brr); + + if (issues.HasFlag(BRRDataIssue.Unresolvable)) { + throw new BRRConversionException("There were unresolvable issues with this object's data."); + } + + // Force validation to run on the name as a precaution + brr.InstrumentName = brr._name; + + if (brr.InstrumentName.Length is not SuiteSample.InstrumentNameLength) { + throw new BRRConversionException($"Something went terribly wrong with the name: \"{brr.InstrumentName}\" (length: {brr.InstrumentName.Length})"); + } + } + + /// + /// Exports this sample's data to a raw BRR file. + /// The extension of the exported file will be added or changed to the preferred extension defined by . + /// + /// The relative or absolute path this sample should be saved to. + /// Data being exported is malformed. + public void ExportRaw(string path) { + ThrowIfUnresolvableIssues(this); + + path = Path.ChangeExtension(path, Extension); + + using var fs = new FileStream(path, FileMode.Create, FileAccess.Write); + + fs.Write(_data); + fs.Flush(); + } + + /// + /// Exports this sample's data to a raw BRR file with a loop offset header. + /// The extension of the exported file will be added or changed to the preferred extension defined by . + /// + /// + /// + public void ExportHeadered(string path) { + ThrowIfUnresolvableIssues(this); + + path = Path.ChangeExtension(path, HeaderedExtension); + using var fs = new FileStream(path, FileMode.Create, FileAccess.Write); + + int loopPoint = IsLooping ? LoopPoint : BlockCount; + + fs.WriteByte((byte) loopPoint); + fs.WriteByte((byte) (loopPoint >> 8)); + + fs.Write(_data); + fs.Flush(); + } + + /// + /// Creates a new from data contained in a BRR Suite Sample file. + /// + /// The absolute or relative path to the file. + /// A new object containing the data. + /// + public static BRRSample ReadSuiteFile(string path) { + using var rd = new FileStream(path, FileMode.Open, FileAccess.Read); + + // Verify + var data = new byte[(int) rd.Length]; + rd.Read(data, 0, data.Length); + + rd.Close(); + + if (!VerifySuiteSample(data, out string? msg)) { + throw new BRRConversionException(msg); + } + + int blocks = ReadShort(SuiteSample.SampleBlocksLocation); + + string name = new(data[SuiteSample.InstrumentNameLocation..SuiteSample.InstrumentNameEnd].Select(c=>(char) c).ToArray()); + + BRRSample ret = new(blocks){ + InstrumentName = name, + LoopBlock = ReadShort(SuiteSample.LoopBlockLocation), + EncodingFrequency = ReadInt(SuiteSample.EncodingFrequencyLocation), + VxPitch = ReadShort(SuiteSample.PitchLocation), + }; + + Array.Copy(data, SuiteSample.SamplesDataLocation, ret._data, 0, data.Length-SuiteSample.SamplesDataLocation); + + return ret; + + int ReadShort(int i) { + return data[i] | (data[i + 1] << 8); + } + + int ReadInt(int i) { + return data[i] | (data[i + 1] << 8) | (data[i + 2] << 16) | (data[i + 3] << 24); + } + } + + /// + /// Tests a stream of data for a properly-formed BRR Suite Sample file header and valid BRR data. + /// + /// The data to validate. + /// A string containing a message about where, if at all, the data was deemed invalid. + /// if the file is valid; otherwise . + public static bool VerifySuiteSample(byte[] data, out string? message) { + // check for a valid file size + int length = data.Length; + + ushort rd16, rd16b; + + if (length < (SuiteSample.SamplesDataLocation + 9)) { + message = $"File is too small to be a {SuiteSample.FormatName} file."; + return false; + } + + // check the signatures + string? badMSG = TestSubstring(SuiteSample.FormatSignatureLocation, SuiteSample.FormatSignature); + if (badMSG is not null) { + message = badMSG; + return false; + } + + badMSG = TestSubstring(SuiteSample.MetaBlockSignatureLocation, SuiteSample.MetaBlockSignature); + if (badMSG is not null) { + message = badMSG; + return false; + } + + badMSG = TestSubstring(SuiteSample.DataBlockSignatureLocation, SuiteSample.DataBlockSignature); + if (badMSG is not null) { + message = badMSG; + return false; + } + + int samplesLength = length - SuiteSample.SamplesDataLocation; + + if ((samplesLength % BrrBlockSize) != 0) { + message = $"Sample data is not a multiple of {BrrBlockSize} bytes: {samplesLength}"; + return false; + } + + // checksums + rd16 = GetChecksum(data[SuiteSample.SamplesDataLocation..]); + rd16b = ReadShort(SuiteSample.ChecksumLocation); + if (rd16 != rd16b) { + message = $"Bad checksum: {rd16b:X4} | Expected: {rd16:X4}"; + return false; + } + + rd16b = ReadShort(SuiteSample.ChecksumComplementLocation); + rd16 ^= 0xFFFF; + if (rd16 != rd16b) { + message = $"Bad checksum complement: {rd16b:X4} | Expected: {rd16:X4}"; + return false; + } + + + rd16 = ReadShort(SuiteSample.SampleLengthLocation); + if (samplesLength != rd16) { + message = $"Incorrect length: {rd16} | Expected: {samplesLength}"; + return false; + } + + rd16 = ReadShort(SuiteSample.SampleBlocksLocation); + + if ((rd16 * BrrBlockSize) != samplesLength) { + message = $"Incorrect block count: {rd16} | Expected: {samplesLength / BrrBlockSize}"; + return false; + } + + rd16 = ReadShort(SuiteSample.LoopBlockLocation); + rd16b = ReadShort(SuiteSample.LoopPointLocation); + + if ((rd16 * BrrBlockSize) != rd16b) { + message = $"Loop block and loop point do not match: b:[{rd16}, {rd16* BrrBlockSize}] | l:[{rd16b / BrrBlockSize},{rd16b}]"; + return false; + } + + byte loopType = data[SuiteSample.LoopTypeLocation]; + + if (rd16b >= length && loopType is SuiteSample.LoopingSample) { + message = $"Invalid loop point: {rd16b}"; + return false; + } + + bool hasLoopFlag = (data[^BrrBlockSize] & LoopFlag) is LoopFlag; + + switch (loopType, hasLoopFlag) { + case (SuiteSample.NonloopingSample, true): + case (SuiteSample.LoopingSample, false): + case (SuiteSample.ForeignLoopingSample, false): + message = $"Loop type does not match final block header."; + return false; + } + + + if ((data[^BrrBlockSize] & EndFlag) is 0) { + message = $"The sample data does not contain an end flag on the final block header."; + return false; + } + + samplesLength -= BrrBlockSize; + + for (int i = 0; i < samplesLength; i += BrrBlockSize) { + if ((data[SuiteSample.SamplesDataLocation + i] & EndFlag) is EndFlag) { + message = $"The sample data contains too many end block flags."; + return false; + } + } + + // Valid file! + message = null; + + return true; + + string? TestSubstring(int start, string test) { + var s = data[start..(start + 4)]; + string result = new(s.Select(o => (char) o).ToArray()); + + if (result == test) { + return null; + } + + return $"Bad signature at {start}: {result} | Expected: {test}"; + } + + ushort ReadShort(int i) { + return (ushort) (data[i] | (data[i + 1] << 8)); + } + + } + + + /// + /// Exports this sample's data to a BRR Suite Sample file. + /// The extension of the exported file will be added or changed to the preferred extension defined by . + /// + /// + /// + public void ExportSuiteSample(string path) { + ThrowIfUnresolvableIssues(this); + + byte[] header = new byte[SuiteSample.SamplesDataLocation]; + + // header + WriteString(SuiteSample.FormatSignature, SuiteSample.FormatSignatureLocation); + + ushort cksm = GetChecksum(this); + WriteShort(cksm, SuiteSample.ChecksumLocation); + WriteShort(~cksm, SuiteSample.ChecksumComplementLocation); + + // meta block + WriteString(SuiteSample.MetaBlockSignature, SuiteSample.MetaBlockSignatureLocation); + WriteString(InstrumentName, SuiteSample.InstrumentNameLocation); + WriteShort(VxPitch, SuiteSample.PitchLocation); + WriteInt(EncodingFrequency, SuiteSample.EncodingFrequencyLocation); + + // 7 unused bytes + + // data block + WriteString(SuiteSample.DataBlockSignature, SuiteSample.DataBlockSignatureLocation); + header[SuiteSample.LoopTypeLocation] = IsLooping ? SuiteSample.LoopingSample : SuiteSample.NonloopingSample; + + int loopBlock = IsLooping ? LoopBlock : 0x0000; + + WriteShort(loopBlock, SuiteSample.LoopBlockLocation); + WriteShort(loopBlock * BrrBlockSize, SuiteSample.LoopPointLocation); + WriteShort(BlockCount, SuiteSample.SampleBlocksLocation); + WriteShort(Data.Length, SuiteSample.SampleLengthLocation); + + path = Path.ChangeExtension(path, SuiteSample.Extension); + using var fs = new FileStream(path, FileMode.Create, FileAccess.Write); + + fs.Write(header); + fs.Write(Data); + fs.Flush(); + + void WriteString(string s, int i) { + foreach (var c in s) { + header[i++] = (byte) c; + } + } + + void WriteInt(int w, int i) { + header[i + 0] = (byte) w; + header[i + 1] = (byte) (w >> 8); + header[i + 2] = (byte) (w >> 16); + header[i + 3] = (byte) (w >> 24); + } + + void WriteShort(int w, int i) { + w &= 0xFFFF; + header[i + 0] = (byte) w; + header[i + 1] = (byte) (w >> 8); + } + + } + + /// + /// Tests the given sample data for BRR validity. This method expects unheadered data. + /// + /// The data to validate. + /// A enum flagging any problems found with the data. + public static BRRDataIssue ValidateBRR(byte[] data) { + return ValidateBRRDataBasic(data); + } + + /// + public static BRRDataIssue ValidateBRR(BRRSample data) { + BRRDataIssue ret = ValidateBRRDataBasic(data._data); + + if (data.LoopBlock >= data.BlockCount) { + ret |= BRRDataIssue.OutOfRangeLoopPoint; + + if (data.HasLoopFlag) { + ret |= BRRDataIssue.Unresolvable; + } + } + + if (data.HasLoopFlag) { + // if there's a loop flag but no specified block, then there's an issue + if (!data.IsLooping) { + ret |= BRRDataIssue.Unresolvable | BRRDataIssue.MissingLoopPoint; + } + } else { + // if there's no loop flag, assume an in-range loop point means there should in fact be a loop + if (data.LoopBlock >= 0 && data.LoopBlock < data.BlockCount) { + ret |= BRRDataIssue.MissingLoopFlag; + } + } + + return ret; + } + + /// + /// Tests the given sample data for BRR validity with automatic finding of loop point if a header be present. + /// + /// + /// If the given data passes all validity checks and has a 16-bit loop offset header, + /// this will contain the index of the BRR block defined by that offset. + /// If the data is invalid, no header is present, or the sample should not loop, this will contain -1. + /// + /// + public static BRRDataIssue ValidateBRRWithHeader(byte[] data, out int loopBlock) { + // Validate alignment + int len = data.Length; + int dataStart = len % BrrBlockSize; + + loopBlock = NoLoop; + + // no header + if (dataStart is 0) { + return ValidateBRR(data); + } + + // bad size + if (dataStart is not 2) { + return BRRDataIssue.Unresolvable | BRRDataIssue.WrongAlignmentForHeadered | BRRDataIssue.WrongAlignment; + } + + // do quick bounds check before passing to the basic validater + if (len < 9) { + return BRRDataIssue.Unresolvable | BRRDataIssue.DataTooSmall; + } + + // Get loop point if from header + int loopStart = data[0] | (data[1] << 8); + + // Perform validity checks + BRRDataIssue ret = ValidateBRRDataWithLoop(new(data, 2, len - 2), loopStart); + + // give up if there are unresolvable issues + if (ret.HasFlag(BRRDataIssue.Unresolvable)) { + return ret; + } + + bool hasLoopFlag = (data[^BrrBlockSize] & LoopFlag) != LoopFlag; + + if (hasLoopFlag) { + loopBlock = loopStart / BrrBlockSize; + } + + // Valid file + return ret; + } + + /// + /// Performs a basic validity check on data for BRR compliance. + /// + /// + private static BRRDataIssue ValidateBRRDataBasic(Span data) { + int len = data.Length; + + // Files that are too small are invalid + if (len < BrrBlockSize) { + return BRRDataIssue.Unresolvable | BRRDataIssue.DataTooSmall | BRRDataIssue.WrongAlignment; + } + + if (len >= MaxSize) { + return BRRDataIssue.DataTooLarge; + } + + // must be a multiple of 9 + if ((len % BrrBlockSize) is not 0) { + return BRRDataIssue.Unresolvable | BRRDataIssue.WrongAlignment; + } + + BRRDataIssue ret = BRRDataIssue.None; + + // need end flag on last block + if ((data[^BrrBlockSize] & EndFlag) != EndFlag) { + ret |= BRRDataIssue.MissingEndFlag; + } + + // make sure no other header has an end flag + len -= BrrBlockSize; + + for (int i = 0; i < len; i += BrrBlockSize) { + if ((data[i] & EndFlag) is EndFlag) { + ret |= BRRDataIssue.EarlyEndFlags; + break; + } + } + + return ret; + } + + /// + /// Performs a validity check on data for BRR compliance with acknowledgement of a loop point. + /// + /// + private static BRRDataIssue ValidateBRRDataWithLoop(Span data, int loopPoint) { + BRRDataIssue ret = ValidateBRRDataBasic(data); + + // give up if there are unresolvable issues + if (ret.HasFlag(BRRDataIssue.Unresolvable)) { + return ret; + } + + // check for a loop flag from the final block + bool hasLoopFlag = (data[^BrrBlockSize] & LoopFlag) != LoopFlag; + + // Check if loop point is aligned to a multiple of 9 + if ((loopPoint % BrrBlockSize) is not 0) { + ret |= BRRDataIssue.MisalignedLoopPoint; + + // Unresolvable if there's a loop + if (hasLoopFlag) { + ret |= BRRDataIssue.Unresolvable; + } + } + + // check if the loop point is in range + if (loopPoint >= data.Length) { + ret |= BRRDataIssue.OutOfRangeLoopPoint; + + // Unresolvable if there's a loop + if (hasLoopFlag) { + ret |= BRRDataIssue.Unresolvable; + } + } + + return ret; + + } + + /// + public static ushort GetChecksum(BRRSample brr) { + return GetChecksum(brr.Data); + } + + /// + /// Creates a BRR Suite Sample specification checksum for the given sample. + /// + /// The sample data to checksum. + /// The checksum as a value. + /// The file is malformed. + public static ushort GetChecksum(byte[] brr) { + int length = brr.Length; + + if (length == 0) { + throw new BRRConversionException("Cannot checksum an empty set of samples."); + } + + if (length % BrrBlockSize != 0) { + throw new BRRConversionException($"Cannot checksum data whose length is not a multiple of {BrrBlockSize}."); + } + + // Step 1: Begin with a sum accumulator of 0 + int ret = 0; + + // Step 2: For each block: + for (int i = 0; i < length; i += BrrBlockSize) { + // Step 2.1: Reset the block accumulator + int accum = 0; + + // Step 2.2: Add the 8 bytes of the block, each shifted left by their index within the block minus 1 + for (int j = 1; j < BrrBlockSize; j++) { + accum += brr[i + j] << (j - 1); + } + + // Step 2.3: Shift the header byte 4 bits left + int hbs = brr[i] << 4; + + // Step 2.4: Exclusive OR the shifted header with the block accumulator + accum ^= hbs; + + // Step 2.5: Add the block accumulator to the sum accumulator + ret += accum; + } + + // Step 3: Truncate the sum accumulator to 16-bits and return. + return (ushort) ret; + } +} diff --git a/BRRSuite.csproj b/BRRSuite.csproj new file mode 100644 index 0000000..551dec1 --- /dev/null +++ b/BRRSuite.csproj @@ -0,0 +1,52 @@ + + + + net8.0 + enable + enable + True + BRRSuite + BRR Suite + Collection of tools for managing and converting Wave Sound files to Bit Rate Reduction (BRR) for the SNES audio chip. + True + README.md + git + False + 1.0.0 + 1.0.0 + LICENSE + True + False + © 2024 kan, et al + https://github.com/spannerisms/BRRSuite + logofull.png + https://github.com/spannerisms/BRRSuite + snes;brr;audio;spc700 + BRR Suite + + + + + portable + + + + none + + + + + True + \ + + + True + \ + + + True + \ + + + + diff --git a/BrrSuite.sln b/BrrSuite.sln new file mode 100644 index 0000000..b7e1ea7 --- /dev/null +++ b/BrrSuite.sln @@ -0,0 +1,36 @@ + +Microsoft Visual Studio Solution File, Format Version 12.00 +# Visual Studio Version 17 +VisualStudioVersion = 17.6.33717.318 +MinimumVisualStudioVersion = 10.0.40219.1 +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "BRRSuite", "BRRSuite.csproj", "{D8768872-634E-43A9-8353-DE8C863C53E5}" +EndProject +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "BRRSuiteGUI", "..\BRRSuiteGUI\BRRSuiteGUI.csproj", "{1F981965-19C7-4CB4-9BA5-BC8C8AE3BE92}" +EndProject +Project("{54435603-DBB4-11D2-8724-00A0C9A8B90C}") = "Setup", "..\BRRSuiteGUI\Setup\Setup.vdproj", "{DB3FED94-78E0-428A-AFF3-5279A8FC6288}" +EndProject +Global + GlobalSection(SolutionConfigurationPlatforms) = preSolution + Debug|Any CPU = Debug|Any CPU + Release|Any CPU = Release|Any CPU + EndGlobalSection + GlobalSection(ProjectConfigurationPlatforms) = postSolution + {D8768872-634E-43A9-8353-DE8C863C53E5}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {D8768872-634E-43A9-8353-DE8C863C53E5}.Debug|Any CPU.Build.0 = Debug|Any CPU + {D8768872-634E-43A9-8353-DE8C863C53E5}.Release|Any CPU.ActiveCfg = Release|Any CPU + {D8768872-634E-43A9-8353-DE8C863C53E5}.Release|Any CPU.Build.0 = Release|Any CPU + {1F981965-19C7-4CB4-9BA5-BC8C8AE3BE92}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {1F981965-19C7-4CB4-9BA5-BC8C8AE3BE92}.Debug|Any CPU.Build.0 = Debug|Any CPU + {1F981965-19C7-4CB4-9BA5-BC8C8AE3BE92}.Release|Any CPU.ActiveCfg = Release|Any CPU + {1F981965-19C7-4CB4-9BA5-BC8C8AE3BE92}.Release|Any CPU.Build.0 = Release|Any CPU + {DB3FED94-78E0-428A-AFF3-5279A8FC6288}.Debug|Any CPU.ActiveCfg = Debug + {DB3FED94-78E0-428A-AFF3-5279A8FC6288}.Release|Any CPU.ActiveCfg = Release + {DB3FED94-78E0-428A-AFF3-5279A8FC6288}.Release|Any CPU.Build.0 = Release + EndGlobalSection + GlobalSection(SolutionProperties) = preSolution + HideSolutionNode = FALSE + EndGlobalSection + GlobalSection(ExtensibilityGlobals) = postSolution + SolutionGuid = {513A467F-9FF5-429A-8DBB-0E8F8BA624F6} + EndGlobalSection +EndGlobal diff --git a/Constants.cs b/Constants.cs new file mode 100644 index 0000000..faf7b2e --- /dev/null +++ b/Constants.cs @@ -0,0 +1,73 @@ +global using static BRRSuite.Constants; + +namespace BRRSuite; + +/// +/// Provides constants related to the conversion and handling of BRR files. +/// +public static class Constants { + //***************************************************************************** + // Audio conversion + //***************************************************************************** + + /// + /// The number of bytes contained in a single block of BRR data. + /// + public const int BrrBlockSize = 9; + + /// + /// The number of audio samples represented by a single BRR block. + /// + public const int PcmBlockSize = 16; + + /// + /// The preferred fidelity of a PCM file when converting to BRR. + /// + public const int PreferredBitsPerSample = 16; + + //***************************************************************************** + // BRR block header + //***************************************************************************** + + /// + /// A bitmask for block headers to isolate the filter ID. + /// + public const byte FilterMask = 0b0000_1100; + + /// + /// The number of shifts required to normalize the filter ID. + /// + public const int FilterShift = 2; + + /// + /// A bitmask for block headers to isolate the range (sample shift). + /// + public const byte RangeMask = 0b1111_0000; + + /// + /// The number of shifts required to normalize the range (sample shift). + /// + public const int RangeShift = 4; + + /// + /// The bit marking the last block of a sample. + /// + public const byte EndFlag = 0b0000_0001; + + /// + /// The bit indicating a sample is to loop. + /// + public const byte LoopFlag = 0b0000_0010; + + /// + /// A mask that can be used to disable the loop flag. + /// + public const byte LoopFlagOff = 0b1111_1101; + + /// + /// The default value used when a sample has no loop point. + /// + public const int NoLoop = -1; + + +} diff --git a/Conversion.cs b/Conversion.cs new file mode 100644 index 0000000..ffca0c2 --- /dev/null +++ b/Conversion.cs @@ -0,0 +1,754 @@ +namespace BRRSuite; + +/// +/// Encapsulates a method for finding the optimal parameters to encoding a set of samples to BRR. +/// +/// The samples of the waveform to encode. +/// Starting point of loop in BRR blocks. +/// A new object containing the data of the converted sample. +public delegate BRRSample EncodingAlgorithm(int[] samples, int loopBlock); + +/// +/// Encapsulates one of the four sampling filters used by the BRR format. +/// +/// Amplitude of the sample 1 backwards. +/// Amplitude of the sample 2 backwards. +/// The amplitude of the next sample. +public delegate int PredictionFilter(int p1, int p2); + +/// +/// Provides methods for encoding and decoding BRR sample files to and from Wave Sound files. +/// +public static class Conversion { + /// + /// The original BRRtools brute force algorithm with silent start enabled, and no disabled filters. + /// Disable wrapping is not an option in BRR Suite. + /// + public static readonly EncodingAlgorithm BruteForce = GetBRRtoolsBruteForce(true, false, false, false, false); + + // a = 0 + // b = 0 + private static int PredictionFilter0(int p1, int p2) => 0; + + // a = 0.9375 (15/16) + // b = 0 + private static int PredictionFilter1(int p1, int p2) => p1 - (p1 >> 4); + + // a = 1.90625 (61/32) + // b = -0.9375 (-15/16) + // formula from fullsnes.txt + private static int PredictionFilter2(int p1, int p2) => p1 * 2 + ((p1 * -3) >> 5) - p2 + (p2 >> 4); + + // a = 1.796875 (115/64) + // b = -0.8125 (-13/16) + // formula from fullsnes.txt + private static int PredictionFilter3(int p1, int p2) => p1 * 2 + ((p1 * -13) >> 6) - p2 + ((p2 * 3) >> 4); + + /// + /// Gets a delegate encapsulating a filter. + /// + /// The ID of the filter to use. + /// A delegate for the filter. + /// The input value cannot be interpreted as a filter + public static PredictionFilter GetPredictionFilter(int filter) => filter switch { + 0x00 => PredictionFilter0, + 0x01 => PredictionFilter1, + 0x02 => PredictionFilter2, + 0x03 => PredictionFilter3, + + _ => throw new ArgumentOutOfRangeException($"Not a valid filter: 0x{filter:X2}", nameof(filter)) + }; + + /// + /// Clamps a signed value to 15 bits. + /// + /// The value to clamp. + /// A new 15-bit value, sign extended into bits 15..24 if necessary. + public static int Clamp(int v) { + if ((short) v != v) { + v >>= 31; + v ^= 0x7FFF; + } + + return (short) v; + } + + /// + /// Clamps a signed value to 15 bits with emulation of the hardware glitches. + /// + /// + /// A new 15-bit value. + public static int Clip(int v) => v switch { + > 0x7FFF => v - 2, // equivalent to (p + 0x7FFF) & 0x7FFF + < -0x8000 => 0, // clipped to 0 + > 0x3FFF => v - 0x8000, // [4000,7FFF] => [-4000,-1] + < -0x4000 => v + 0x8000, // [-8000,-4001] => [0,-3FFF] + _ => v, + }; + + /// + /// + /// Encodes a single block of BRR data in-place from a given set of samples. + /// + /// + /// The arguments and should be + /// of the respective type over existing data. + /// + /// + /// This method is designed to be chained with itself by using the parameters + /// to communicate samples from one block to the next, where:
+ /// • is the previous sample
+ /// • is the sample preceding
+ /// These previous samples should generally be initialized to 0 at the start of conversion. + ///
+ /// + /// Example encoding with pre-chosen filters and ranges: + /// + /// int x1 = 0; + /// int x2 = 0; + /// for (int i = 0; i < sample.Length; i += 16) { + /// var pcmSamples = new Span<int>(sample, i * 16, 16); + /// var brrSamples = brr.GetBlockAt(i); + /// Conversion.EncodeBlock(pcmSamples, brrSamples, range[i], filter[i], ref x1, ref x2); + /// } + /// + /// + ///
+ /// + /// A span of length 15 over the waveform block to encode. + /// See also: . + /// + /// + /// A span of length 9 over the BRR block that data should be written to. + /// See also: . + /// + /// The number of shifts performed on the 4-bit value of the encoded sample. [1,12] + /// The ID of the filter to encode with. [0,1,2,3] + /// + /// A reference to the 15-bit value of the most-recently encoded sample. + /// When this method returns, will contain the value of the newly encoded sample. + /// + /// + /// A reference to the 16-bit value of the second-most-recenently encoded sample. + /// When this method returns, will contain the value that held when this method was called. + /// + /// + /// + public static void EncodeBlock(Span pcmBlock, Span brrBlock, int range, int filter, ref int p1, ref int p2) { + if (pcmBlock.Length is not PcmBlockSize) { + throw new ArgumentException("The length of the input block must be 16 PCM samples.", nameof(pcmBlock)); + } + + if (brrBlock.Length is not BrrBlockSize) { + throw new ArgumentException("The length of the input block must be 16 PCM samples.", nameof(pcmBlock)); + } + + if (range is < 1 or > 12) { + throw new ArgumentOutOfRangeException("Range should be between 1 and 12, inclusive.", nameof(range)); + } + + if (filter is < 0 or > 4) { + throw new ArgumentOutOfRangeException("Filter should be between 0 and 4, inclusive.", nameof(filter)); + } + + // actual algorithm + bool odd = false; + + int brrX = 1; // start at 1 because header + + int accum = 0; + foreach (var sample in pcmBlock) { + // add in sample + accum |= EncodeSample(sample, out int _, range, filter, ref p1, ref p2); + + if (odd) { // write every other sample + brrBlock[brrX++] = (byte) accum; // write latest 2 samples (2 nibbles) + } + + odd = !odd; + + // shift everything 4 bits over + // the more significant bytes being old data is fine + // as this is cast to a byte when written + accum <<= 4; + } + + // write header + brrBlock[0] = (byte) ((range << RangeShift) | (filter << FilterShift)); + } + + /// + /// Encodes a signed 16-bit sample to a signed 4-bit value for a given set of block parameters. + /// This method uses parameters to handle the filter history. + /// + /// The signed 16-bit sample to encode. + /// When this method returns, this will contain the delta between the original sample and its encoded value. + /// + ///
+ /// Caution: this method does not include special casing or error checking for invalid range values. + /// The caller of this routine should ensure that only valid ranges are passed to this method before calling. + /// + /// + /// + /// + /// The signed 4-bit value this sample should be encoded as, given the input parameters. + /// + public static int EncodeSample(int sample, out int error, int range, int filter, ref int p1, ref int p2) { + // this method is an implementation of encoding for the formula: + // s(n) = d * 2^(r-15) + a*s(n-1) + b*s(n-2) + // where: + // s(n) indicates the n-th sample of the BRR + // d is the 4-bit, two's complement value encoded into the BRR (this is the value returned by this method) + // v = d * 2^(r-15) is sample addend + // r is the range (or shift) of the block + // a and b are constant coefficients determined by the filter: + // filter a b + // 0 0 0 + // 1 15/16 0 + // 2 61/32 -15/16 + // 3 115/64 -13/16 + // a*s(n-1) + b*s(n-2) together constitute the "filter addend" + + // Get the filter addend from the previous two samples + // inlined here for speed + int linearValue = filter switch { + 0 => 0, + 1 => p1 - (p1 >> 4), + 2 => p1 * 2 + ((p1 * -3) >> 5) - p2 + (p2 >> 4), + 3 => p1 * 2 + ((p1 * -13) >> 6) - p2 + ((p2 * 3) >> 4), + _ => throw new ArgumentOutOfRangeException("Filter should be between 0 and 4, inclusive.", nameof(filter)) + }; + + // get the difference between the sample (shifted right to normalize to 15-bit) and the filter addend + error = (sample >> 1) - linearValue; + + // some magic stuff I still don't understand + error = Clip(error) + (1 << (range + 2)) + ((1 << range) >> 2); + + // default to the lowest value + int ret = 0x8; // signed 4 bit, so this is -8, but without sign extension + + if (error > 0) { + ret = (error << 1) >> range; + + // keep the value in range + if (ret > 0xF) { + ret = 0xF; + } + + // change the domain of ret from [0,15] to [-8,7] + ret ^= 8; // same as ret -= 8, but without a sign extension into bits 4 through 31 + // this avoids an extra & 0xF + } + + // what was the previous sample is now the next previous sample + p2 = p1; + + // the previous sample is now what we just encoded + p1 = Clip(linearValue + ((ret << range) >> 1)); // TODO should this be clip or clamp? + + // calculate the error of this sample (normalizing p1 to 16-bit) + error = sample - (p1 << 1); + + // return the 4-bit encoded value + return ret; + } + + /// + /// Encodes a Wave Sound audio file to a BRR file with the given settings. + /// + /// Input PCM samples. + /// An encoding algorithm that converts a raw waveform to a BRR sample. + /// A that will convert the input wav to a resampled output + /// Desired resampling ratio. + /// Point at which input wave will be truncated; if 0 or negative, the input is not truncated. + /// Starting point of loop in samples + /// Enables the encoder to remove leading zeros before adding an initial block + /// An array of filters to apply to the sample data after it is resized and resampled. + /// A new object containing the data and metadata of the converted sample. + /// + public static BRRSample Encode(int[] wavSamples, EncodingAlgorithm encoder, ResamplingAlgorithm resampleAlgorithm, decimal resampleFactor, + int truncate = -1, int loopStart = NoLoop, bool trimLeadingZeroes = false, PreEncodingFilter[]? waveFilters = null) { + + int samplesLength = wavSamples.Length; + + if (truncate < 1 || truncate > samplesLength) { + truncate = samplesLength; + } else { + samplesLength = truncate; + } + + bool hasLoop = true; + + if (loopStart < 0) { + hasLoop = false; + loopStart = truncate; + } + + // Output buffer + int targetLength; + int loopSize = 0; + + if (!hasLoop) { + targetLength = (int) Math.Round(samplesLength / resampleFactor); + } else { + decimal oldLoopSize = (samplesLength - loopStart) / resampleFactor; + + // New loopsize is the multiple of 16 that comes after loopsize + loopSize = (int) (Math.Ceiling(oldLoopSize / PcmBlockSize) * PcmBlockSize); + + // Adjust resampling + targetLength = (int) Math.Round(samplesLength / resampleFactor * (loopSize / oldLoopSize)); + } + + decimal bsResampleRatio = (decimal) samplesLength / targetLength; + + int[] samples = resampleAlgorithm(wavSamples, samplesLength, targetLength); + + // Apply any filters to the sample now + if (waveFilters is not null) { + Array.ForEach(waveFilters, filt => { + if (filt is null) return; + + samples = filt(samples); + + if (samples.Length != samplesLength) { + throw new BRRConversionException("Something is wrong with a filter that changed the size of the sample data."); + } + }); + } + + if (trimLeadingZeroes) { + int fzero = Array.FindIndex(samples, i => i is not 0); + // if you get -1, wtf happened to your sample? + if (fzero is > 0) { + samples = samples[fzero..]; + } + } + + samplesLength = samples.Length; + + if ((samplesLength % PcmBlockSize) is not 0) { + int padding = PcmBlockSize - (samplesLength & 0xF); + + int[] padSamples = new int[samplesLength + padding]; + samples.CopyTo(padSamples, padding); + samples = padSamples; + samplesLength += padding; + } + + int loopBlock = hasLoop switch { + true => (samplesLength - loopSize) / PcmBlockSize, + false => NoLoop + }; + + BRRSample ret = encoder(samples, loopBlock); + + ret.ResampleRatio = resampleFactor; + + return ret; + } + + /// + /// Creates a brute force algorithm with specific parameters based on the original BRRtools algorithm. + /// + /// Enables the algorithm to encode a block of complete silence at the start. + /// Requests that the algorithm not perform block encoding with filter 0. This does not apply to the initial block. + /// Requests that the algorithm not perform block encoding with filter 1. + /// Requests that the algorithm not perform block encoding with filter 2. + /// Requests that the algorithm not perform block encoding with filter 3. + /// An anonymous function encoding the algorithm. + public static EncodingAlgorithm GetBRRtoolsBruteForce(bool silentStart, bool disableFilter0, bool disableFilter1, bool disableFilter2, bool disableFilter3) => + (int[] samples, int loopBlock) => { + int samplesLength = samples.Length; + int blockCount = samplesLength / PcmBlockSize; + + int blockPos = 0; + bool hasLoop = loopBlock >= 0; + + bool force0 = true; + + byte endFlags = hasLoop ? (byte) (EndFlag|LoopFlag) : EndFlag; + + int P1 = 0, P1Loop = 0; + int P2 = 0, P2Loop = 0; + int filterAtLoop = 0; + + // add 16 silent samples at the start if necessary and requested + if (silentStart) { + for (int i = 0; i < 16; i++) { + if (samples[i] is not 0) { + force0 = false; // no more need to force block 0 + loopBlock++; + blockCount++; + blockPos += BrrBlockSize; + // shouldn't need to tell the brr to initialize, since an array of bytes initializes to 0 + break; + } + } + + } + + var brrOut = new BRRSample(blockCount) { + LoopBlock = loopBlock, + }; + + // value to test for n being + int loopTest = hasLoop switch { + true => loopBlock * 16, + false => NoLoop + }; + + int endTest = samplesLength - PcmBlockSize; + + for (int n = 0; n < samplesLength; n += PcmBlockSize, blockPos += BrrBlockSize) { + // Encode BRR block, tell the encoder if we're at loop point (if loop is enabled), and if we're at end point + bool isLoopPoint = n == loopTest; + bool isEndPoint = n == endTest; + + double bestError = double.PositiveInfinity; + + PredictionFilter filterFunc; + int filter; + int bestFilter = 0; + int bestRange = 0; + + bool write = false; + + if (force0) { + MashAll(0); + force0 = false; + } else { + if (!disableFilter0) { + MashAll(0); + } + + if (!disableFilter1) { + MashAll(1); + } + + if (!disableFilter2) { + MashAll(2); + } + + if (!disableFilter3) { + MashAll(3); + } + } + + if (isLoopPoint) { + filterAtLoop = bestFilter; + P1Loop = P1; + P2Loop = P2; + } + + write = true; + filter = bestFilter; + filterFunc = GetPredictionFilter(filter); + + ADPCMMash(bestRange); + + // Local functions + // gets ugly here + void MashAll(int filteri) { + filterFunc = GetPredictionFilter(filteri); + filter = filteri; + + // fullsnes.txt says shift 0 is useless, so let's not use it + for (int sa = 1; sa < 13; sa++) { + ADPCMMash(sa); + } + } + + void ADPCMMash(int range) { + double blockError = 0.0; + + int l1 = P1; + int l2 = P2; + int step = (1 << (range + 2)) + ((1 << range) >> 2); + int sampleError, dp; + + bool even = true; + int writeAt = blockPos + 1; + + for (int i = 0; i < PcmBlockSize; i++) { + int da; + int thisSample = samples[n + i]; + int linearValue = filterFunc(l1, l2) >> 1; + + // difference between linear prediction and current sample + sampleError = (thisSample >> 1) - linearValue; + + if (sampleError < 0) { + da = -sampleError; + } else { + da = sampleError; + } + + if (da is > 16384 and < 32768) { + sampleError = (sampleError >> 9) & 0x07FF_8000; + } + + dp = sampleError + step; + + int c = 0; + + if (dp > 0) { + // not allowing shift 0 for now + c = (dp << 1) >> range; + + if (c > 0xF) { + c = 0xF; + } + } + + c -= 8; + + dp = (c << range) >> 1; // quantized estimate of samp + + l2 = l1; // shift history + + l1 = linearValue + dp; + + if (l1 is < short.MinValue or > short.MaxValue) { + l1 = (short) (0x7FFF - (l1 >> 24)); + } + + l1 <<= 1; + + sampleError = thisSample - l1; + + blockError += (double) sampleError * sampleError; + + if (write) { + if (even = !even) { + // odd samples + brrOut[writeAt++] |= (byte) (c & 0x0F); + } else { + // even samples + brrOut[writeAt] = (byte) (c << 4); + } + } + } + + if (write) { + P1 = l1; + P2 = l2; + + int header = (range << RangeShift) | (filter << FilterShift); + + if (isEndPoint) { + header |= endFlags; // Set the last block flags if we're on the last block + } + + brrOut[blockPos] = (byte) header; + } else { + if (isEndPoint) { + // Account for history points when looping is enabled & filters used + switch (filterAtLoop) { + case 0: + blockError /= 16.0; + break; + + // Filter 1 + case 1: + int temp1 = l1 - P1Loop; + blockError += (double) temp1 * temp1; + blockError /= 17.0; + break; + + // Filters 2 & 3 + default: + int temp2 = l1 - P1Loop; + blockError += (double) temp2 * temp2; + temp2 = l2 - P2Loop; + blockError += (double) temp2 * temp2; + blockError /= 18.0; + break; + } + } else { + blockError /= 16.0; + } + + if (blockError < bestError) { + bestError = blockError; + bestFilter = filter; + bestRange = range; + } + } + } + } + + return brrOut; + }; + + + + + + + + + + + + + + + // public static byte[] EncodeBlock(int samples, int offset, int filter, int ) + + /// + /// Decodes a given stream of raw BRR data into a Wave Sound audio file. + /// + /// The BRR data to decode. This should not include any loop header. + /// The loop point of this sample in blocks, or -1 if the sample does not loop. + /// The output sample rate the audio file should be played back at. + /// The minimum length of looped audio in seconds. Takes priority over . Ignored on non-looping samples. + /// The number of times this file should be looped. Defers to . Ignored on non-looping samples. + /// Allows application of a filter to the final audio to simulate the SNES Gaussian filtering. + /// A new object containing the decoded audio. + /// + public static WaveContainer Decode(byte[] brrSample, int loopBlock = NoLoop, int sampleRate = 32000, + decimal minimumLength = 0.0M, int loopCount = 1, bool applyGaussian = false) { + + const int GaussA = 372; + const int GaussB = 1304; + const int GaussShift = 11; + + int blockCount = brrSample.Length / BrrBlockSize; + + if ((brrSample.Length % BrrBlockSize) is not 0) { + throw new BRRConversionException($"Data size is not a multiple of {BrrBlockSize}: {brrSample.Length} | {blockCount * BrrBlockSize}"); + } + + if (loopBlock >= blockCount) { + loopBlock = NoLoop; + } + + if (loopBlock < 0) { + loopCount = 0; + loopBlock = blockCount; + } else { + int minSamples = (int) decimal.Ceiling(minimumLength * sampleRate / PcmBlockSize); + loopCount = Math.Max(loopCount, (minSamples - loopBlock) / (blockCount - loopBlock)); + loopCount = Math.Clamp(loopCount, 1, 777); + } + + int outBlocks = loopCount * (blockCount - loopBlock) + loopBlock; + int sampleCount = outBlocks * PcmBlockSize; + + var retWav = new WaveContainer(sampleRate, PreferredBitsPerSample, sampleCount); + + int brrPos = 0; + int wavPos = 0; + int loopAt = loopBlock * BrrBlockSize; + + int p1 = 0; + int p2 = 0; + + for (int i = 0; i < loopBlock; i++) { + DecodeNextBlock(); + } + + for (; loopCount > 0; loopCount--) { + brrPos = loopAt; + for (int i = loopBlock; i < blockCount; i++) { + DecodeNextBlock(); + } + } + + if (applyGaussian) { + int prev = GaussA * ((GaussB * retWav[0]) + retWav[1]); + int ln = blockCount * PcmBlockSize - 1; + for (int i = 1; i < ln; i++) { + int temp = (GaussB * retWav[i]) + (GaussA * (retWav[i - 1] + retWav[i + 1])); + retWav[i - 1] = (short) (prev >> GaussShift); + prev = temp; + } + int last = GaussA * ((GaussB * retWav[^2]) + retWav[^1]); + retWav[^2] = (short) (prev >> GaussShift); + retWav[^1] = (short) (last >> GaussShift); + } + + return retWav; + + void DecodeNextBlock() { + var predictor = GetPredictionFilter((brrSample[brrPos] & FilterMask) >> FilterShift); + + int shift = (brrSample[brrPos] & RangeMask) >> RangeShift; + + brrPos++; + + for (int i = 0; i < 8; i++) { + retWav[wavPos++] = (short) DecodeNextSample((byte) (brrSample[brrPos] >> 4)); + retWav[wavPos++] = (short) DecodeNextSample((byte) (brrSample[brrPos] & 0x0F)); + + brrPos++; + } + + int DecodeNextSample(byte samp) { + int a = (shift, samp) switch { + (< 13, < 8) => (samp << shift) >> 1, + (< 13, _ ) => ((samp - 16) << shift) >> 1, + (_ , < 8) => 2048, + (_ , _ ) => -2048, + }; + + a += predictor(p1, p2); + + p2 = p1; + + int ret = p1 = (a switch { + > short.MaxValue => short.MaxValue - 0x8000, + < short.MinValue => short.MinValue + 0x8000, + > 0x3FFF => a - 0x8000, + < -0x4000 => a + 0x8000, + _ => a + }); + + return ret * 2; + } + } + } + + + /// + /// Returns a new array of integers with the Gaussian interpolation table values. + /// + public static int[] GetGaussTable() => + [ // lifted directly from fullsnes.txt + 0x000, 0x000, 0x000, 0x000, 0x000, 0x000, 0x000, 0x000, 0x000, 0x000, 0x000, 0x000, 0x000, 0x000, 0x000, 0x000, + 0x001, 0x001, 0x001, 0x001, 0x001, 0x001, 0x001, 0x001, 0x001, 0x001, 0x001, 0x002, 0x002, 0x002, 0x002, 0x002, + 0x002, 0x002, 0x003, 0x003, 0x003, 0x003, 0x003, 0x004, 0x004, 0x004, 0x004, 0x004, 0x005, 0x005, 0x005, 0x005, + 0x006, 0x006, 0x006, 0x006, 0x007, 0x007, 0x007, 0x008, 0x008, 0x008, 0x009, 0x009, 0x009, 0x00A, 0x00A, 0x00A, + 0x00B, 0x00B, 0x00B, 0x00C, 0x00C, 0x00D, 0x00D, 0x00E, 0x00E, 0x00F, 0x00F, 0x00F, 0x010, 0x010, 0x011, 0x011, + 0x012, 0x013, 0x013, 0x014, 0x014, 0x015, 0x015, 0x016, 0x017, 0x017, 0x018, 0x018, 0x019, 0x01A, 0x01B, 0x01B, + 0x01C, 0x01D, 0x01D, 0x01E, 0x01F, 0x020, 0x020, 0x021, 0x022, 0x023, 0x024, 0x024, 0x025, 0x026, 0x027, 0x028, + 0x029, 0x02A, 0x02B, 0x02C, 0x02D, 0x02E, 0x02F, 0x030, 0x031, 0x032, 0x033, 0x034, 0x035, 0x036, 0x037, 0x038, + 0x03A, 0x03B, 0x03C, 0x03D, 0x03E, 0x040, 0x041, 0x042, 0x043, 0x045, 0x046, 0x047, 0x049, 0x04A, 0x04C, 0x04D, + 0x04E, 0x050, 0x051, 0x053, 0x054, 0x056, 0x057, 0x059, 0x05A, 0x05C, 0x05E, 0x05F, 0x061, 0x063, 0x064, 0x066, + 0x068, 0x06A, 0x06B, 0x06D, 0x06F, 0x071, 0x073, 0x075, 0x076, 0x078, 0x07A, 0x07C, 0x07E, 0x080, 0x082, 0x084, + 0x086, 0x089, 0x08B, 0x08D, 0x08F, 0x091, 0x093, 0x096, 0x098, 0x09A, 0x09C, 0x09F, 0x0A1, 0x0A3, 0x0A6, 0x0A8, + 0x0AB, 0x0AD, 0x0AF, 0x0B2, 0x0B4, 0x0B7, 0x0BA, 0x0BC, 0x0BF, 0x0C1, 0x0C4, 0x0C7, 0x0C9, 0x0CC, 0x0CF, 0x0D2, + 0x0D4, 0x0D7, 0x0DA, 0x0DD, 0x0E0, 0x0E3, 0x0E6, 0x0E9, 0x0EC, 0x0EF, 0x0F2, 0x0F5, 0x0F8, 0x0FB, 0x0FE, 0x101, + 0x104, 0x107, 0x10B, 0x10E, 0x111, 0x114, 0x118, 0x11B, 0x11E, 0x122, 0x125, 0x129, 0x12C, 0x130, 0x133, 0x137, + 0x13A, 0x13E, 0x141, 0x145, 0x148, 0x14C, 0x150, 0x153, 0x157, 0x15B, 0x15F, 0x162, 0x166, 0x16A, 0x16E, 0x172, + 0x176, 0x17A, 0x17D, 0x181, 0x185, 0x189, 0x18D, 0x191, 0x195, 0x19A, 0x19E, 0x1A2, 0x1A6, 0x1AA, 0x1AE, 0x1B2, + 0x1B7, 0x1BB, 0x1BF, 0x1C3, 0x1C8, 0x1CC, 0x1D0, 0x1D5, 0x1D9, 0x1DD, 0x1E2, 0x1E6, 0x1EB, 0x1EF, 0x1F3, 0x1F8, + 0x1FC, 0x201, 0x205, 0x20A, 0x20F, 0x213, 0x218, 0x21C, 0x221, 0x226, 0x22A, 0x22F, 0x233, 0x238, 0x23D, 0x241, + 0x246, 0x24B, 0x250, 0x254, 0x259, 0x25E, 0x263, 0x267, 0x26C, 0x271, 0x276, 0x27B, 0x280, 0x284, 0x289, 0x28E, + 0x293, 0x298, 0x29D, 0x2A2, 0x2A6, 0x2AB, 0x2B0, 0x2B5, 0x2BA, 0x2BF, 0x2C4, 0x2C9, 0x2CE, 0x2D3, 0x2D8, 0x2DC, + 0x2E1, 0x2E6, 0x2EB, 0x2F0, 0x2F5, 0x2FA, 0x2FF, 0x304, 0x309, 0x30E, 0x313, 0x318, 0x31D, 0x322, 0x326, 0x32B, + 0x330, 0x335, 0x33A, 0x33F, 0x344, 0x349, 0x34E, 0x353, 0x357, 0x35C, 0x361, 0x366, 0x36B, 0x370, 0x374, 0x379, + 0x37E, 0x383, 0x388, 0x38C, 0x391, 0x396, 0x39B, 0x39F, 0x3A4, 0x3A9, 0x3AD, 0x3B2, 0x3B7, 0x3BB, 0x3C0, 0x3C5, + 0x3C9, 0x3CE, 0x3D2, 0x3D7, 0x3DC, 0x3E0, 0x3E5, 0x3E9, 0x3ED, 0x3F2, 0x3F6, 0x3FB, 0x3FF, 0x403, 0x408, 0x40C, + 0x410, 0x415, 0x419, 0x41D, 0x421, 0x425, 0x42A, 0x42E, 0x432, 0x436, 0x43A, 0x43E, 0x442, 0x446, 0x44A, 0x44E, + 0x452, 0x455, 0x459, 0x45D, 0x461, 0x465, 0x468, 0x46C, 0x470, 0x473, 0x477, 0x47A, 0x47E, 0x481, 0x485, 0x488, + 0x48C, 0x48F, 0x492, 0x496, 0x499, 0x49C, 0x49F, 0x4A2, 0x4A6, 0x4A9, 0x4AC, 0x4AF, 0x4B2, 0x4B5, 0x4B7, 0x4BA, + 0x4BD, 0x4C0, 0x4C3, 0x4C5, 0x4C8, 0x4CB, 0x4CD, 0x4D0, 0x4D2, 0x4D5, 0x4D7, 0x4D9, 0x4DC, 0x4DE, 0x4E0, 0x4E3, + 0x4E5, 0x4E7, 0x4E9, 0x4EB, 0x4ED, 0x4EF, 0x4F1, 0x4F3, 0x4F5, 0x4F6, 0x4F8, 0x4FA, 0x4FB, 0x4FD, 0x4FF, 0x500, + 0x502, 0x503, 0x504, 0x506, 0x507, 0x508, 0x50A, 0x50B, 0x50C, 0x50D, 0x50E, 0x50F, 0x510, 0x511, 0x511, 0x512, + 0x513, 0x514, 0x514, 0x515, 0x516, 0x516, 0x517, 0x517, 0x517, 0x518, 0x518, 0x518, 0x518, 0x518, 0x519, 0x519 + ]; + +} diff --git a/LICENSE b/LICENSE new file mode 100644 index 0000000..cf3af33 --- /dev/null +++ b/LICENSE @@ -0,0 +1,10 @@ +Copyright (C) 2024 kan/spannerisms +Copyright (C) 2018 tewtal/total +Copyright (C) 2013 Optiroc, nyanpasu64 +Copyright (C) 2009 Bregalad, Kode54 + +Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the “Software”), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED “AS IS”, WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. diff --git a/PreEncodingFilters.cs b/PreEncodingFilters.cs new file mode 100644 index 0000000..9c2451e --- /dev/null +++ b/PreEncodingFilters.cs @@ -0,0 +1,119 @@ +namespace BRRSuite; + +/// +/// +/// Encapsulates a method that applies a filter to a waveform and returns a new array with the filtered values. +/// +/// +/// When creating a filter, it is expected that the input data not be modified, +/// thus it is required that a non-writable span be passed as the accessor to the data. +/// +/// +/// The returned array must be of the same length as the input span. +/// +/// +/// The samples data to operate on. +/// An array of integers containing the new waveform. +public delegate int[] PreEncodingFilter(ReadOnlySpan samples); + +/// +/// Contains functionality for creating filters that adjust audio waveforms before encoding to BRR. +/// +public static class PreEncodingFilters { + /// + /// Creates a filter for treble boosting a waveform to compensate for the SNES Gaussian lowpass filter. + /// + /// A convolution matrix to run over each sample. + /// An anonymous function encapsulating the filter. + public static PreEncodingFilter GetTrebleBoostFilter(double[] matrix) { + int depth = matrix.Length; + + return (samples) => { + int length = samples.Length; + int[] ret = new int[length]; + + for (int i = 0; i < length; i++) { + double acc = samples[i] * matrix[0]; + + for (int k = depth - 1; k > 0; k--) { + acc += matrix[k] * ((i + k < length) ? samples[i + k] : samples[^1]); + acc += matrix[k] * ((i - k >= 0) ? samples[i - k] : samples[0]); + } + + ret[i] = (int) acc; + } + + return ret; + }; + } + + + /// + /// Returns a new convolution matrix containing the values used by the original BRRtools treble boost filter. + /// + /// An array of values. + public static double[] GetBRRtoolsTrebleMatrix() => [ + // Tepples' coefficient multiplied by 0.6 to avoid overflow in most cases + 0.912962, -0.16199, -0.0153283, 0.0426783, -0.0372004, 0.023436, -0.0105816, 0.00250474 + ]; + + /// + /// + /// Creates a convolution matrix for Guassian compensation. + /// + /// + /// This is a formula discovered by Drexxx using the function a*b^|x|, where: + /// + /// a = 16/sqrt(70) + /// b = (16*sqrt(70)-16)/193 + /// + /// + /// + /// The desired size of the output matrix. A recommended value is + /// An array of values. + public static double[] GetDrexxxMatrix(int depth) { + int length = depth; + double[] ret = new double[length]; + + for (int i = 0; i < length; i++) { + // a b x + ret[i] = 1.9123657749350298 * Math.Pow(-0.3132730726295474, Math.Abs(i - depth)); + } + + return ret; + } + + /// + /// Creates a filter for boosting the amplitude of a waveform by a specified amount in a linear fashion. + /// + /// The value to multiply every sample by, where 1.0 indicates no change in amplitude. + /// An anonymous function encapsulating the filter. + public static PreEncodingFilter GetLinearAmplitudeFilter(decimal boost) { + return (samples) => { + int len = samples.Length; + int[] ret = new int[len]; + + for (int i = 0; i < len; i++) { + ret[i] = (int) decimal.Round(samples[i] * boost); + } + + return ret; + }; + } + + /// + /// Finds the largest linear amplitude boost ratio of a waveform such that the largest magnitude of the waveform—positive or negative—will be + /// transformed to a magnitude of 32600 (or close to it). + /// + /// The set of samples to check. + /// A ratio. + public static decimal GetLinearBoostFactor(int[] samples) { + int max = Math.Abs(samples.Max()); + int min = Math.Abs(samples.Min()); + + max = Math.Max(max, min); + + return 32600M / max; + } + +} diff --git a/README.md b/README.md new file mode 100644 index 0000000..f18a7fe --- /dev/null +++ b/README.md @@ -0,0 +1,66 @@ +# BRR Suite + +This is a C# library for converting modern, uncompressed audio files to the bit-rate reduction format (BRR) used by the Super Nintendo Entertainment System. For a user-friendly GUI employing this library, see my [BRR Suite GUI repository](https://github.com/spannerisms/BRRSuiteGUI) + +# Acknowledgements + * kan/spannerisms - me, the library author + * Bregalad - original BRRtools developer + * Kode54 - encoding algorithms + * Optiric - C version: [BRRtools](https://github.com/Optiroc/BRRtools) + * nyanpasu64 - C version + * total - [original C# encoder](https://github.com/tewtal/mITroid/blob/master/mITroid/NSPC/BRR.cs) + * _aitchFactor - whose concerns of, ideas for, and contributions to BRRtools gave me ideas and code to help future proof the implementation of this library + * Drexxx - who found some better filtering that was relayed to me through _aitchFactor + + +# File formats + +## .brr +In a raw BRR file (extension `.brr`), the entire contents of the file is the sample data. + +## .brh +In a loop-headered BRR file (extension: `.brh`), the first 2 bytes constitute the 16-bit loop offset of the sample in bytes. The rest of the file is the sample data. + +## .brs +In a BRR Suite Sample file (extension: `.brs`), the file contents are a well formed header followed by the data. + +| Offset | Size | Type | Value | Contents | +| ------:|-----:|:-----:|:------:| -------- | +| 0 | 4 | ASCII | `BRRS` | BRR Suite data file signature[^1] | +| 4 | 2 | short | - | Sample checksum[^1] | +| 6 | 2 | short | - | Sample checksum complement | +| 8 | 4 | ASCII | `META` | Start of metadata block - signature[^1] | +| 12 | 24 | ASCII | - | Sample name; padded with spaces (0x20) if fewer than 24 characters | +| 36 | 2 | short | - | The VxPITCH value that corresponds to a C; 0x0000 if unknown; default 0x1000 | +| 40 | 4 | int | - | The frequency in hertz used for resampling during encoding | +| 44 | 7 | null | 0 | Unused padding (possible expansion) +| 51 | 4 | ASCII | `DATA` | Start of data block - signature[^1] | +| 55 | 1 | byte | - | Whether this file loops (0: Non-looping, 1: Looping, 2: Loops to different sample[^2]) | +| 56 | 2 | short | - | Loop point offset of the sample in blocks (0x0000 preferred for non-looping samples) | +| 58 | 2 | short | - | Loop point offset of the sample in bytes[^3] | +| 60 | 2 | short | - | Size of sample data in blocks | +| 62 | 2 | short | - | Size of sample data in bytes[^3] | +| 64 | * | bytes | - | Sample data[^4] | + +[^1]: If this parameter is invalid, the entire file should be considered invalid. +[^2]: This is technically a valid thing to do, but please don't. +[^3]: Provided as a redundancy for file validation. Should be `9*blocks`; otherwise this parameter—and the thus entire file—is invalid. +[^4]: Should be a multiple of 9 bytes in length; otherwise this parameter—and thus the entire file—is invalid. + +### Checksum and complement +The checksum and its complement should have their bits flipped with respect to each other. In other words: `checksum XOR complement = 0x0000`. + +The checksum is calculated as such: + +1. Begin with a sum accumulator of 0 +2. For each block: + 1. Reset the block accumulator + 2. Add the 8 bytes of the block, each shifted left by their index within the block minus 1 + 3. Shift the header byte 4 bits left + 4. Exclusive OR the shifted header with the block accumulator + 5. Add the block accumulator to the sum accumulator +3. Truncate the sum accumulator to 16-bits + + +An implementation of this algorithm may be found in `BRRSample.GetChecksum(byte[])`. + diff --git a/ResamplingAlgorithms.cs b/ResamplingAlgorithms.cs new file mode 100644 index 0000000..567aa77 --- /dev/null +++ b/ResamplingAlgorithms.cs @@ -0,0 +1,256 @@ +namespace BRRSuite; + +/// +/// +/// Encapsulates a method containing an algorithm to resample a specified portion of a given set of samples to a new size. +/// +/// +/// +/// When creating an algorithm, it is expected that the input data not be modified, +/// thus it is required that a non-writable span be passed as the accessor to the data. +/// +/// +/// +/// Preferably, new algorithms should implement error checking by calling +/// +/// and include a fast copy method for when and are equal. +/// +/// +/// The data to be resampled. +/// The length of data to use for resampling. +/// The new length to resample to. +/// A new array of integers containing the resampled data. +public delegate int[] ResamplingAlgorithm(ReadOnlySpan samples, int inLength, int outLength); + +/// +/// Combines a with a human-readable name. +/// +public static class ResamplingAlgorithms { + /// + /// Throws an if any of the following occurs:
+ /// • >
+ /// • < 1
+ /// • < 1 + ///
+ /// The length of the input data. + /// + /// + /// If a problem occurs + public static void ThrowIfInvalid(int samplesLength, int inLength, int outLength) { + if (inLength > samplesLength) { + throw new ArgumentException( + $"The input length for resampling should not be larger than the size of the data: {inLength}/{samplesLength}.", + nameof(inLength)); + } + + if (inLength < 1) { + throw new ArgumentException("The input length should not be 0 or negative.", nameof(inLength)); + } + + if (outLength < 1) { + throw new ArgumentException("The output length should not be 0 or negative.", nameof(outLength)); + } + } + + /// + /// A resampling algorithm that uses nearest-neighbor interpolation. + /// + public static readonly ResamplingAlgorithm NoInterpolation = + (samples, inLength, outLength) => { + ThrowIfInvalid(samples.Length, inLength, outLength); + + // Fast copy + if (inLength == outLength) { + return samples[..inLength].ToArray(); + } + + double ratio = ((double) inLength) / outLength; + + int[] outBuf = new int[outLength]; + + for (int i = 0; i < outLength; i++) { + outBuf[i] = samples[(int) (i * ratio)]; + } + + return outBuf; + }; + + /// + /// A resampling algorithm that uses linear interpolation. + /// + public static readonly ResamplingAlgorithm LinearInterpolation = + (samples, inLength, outLength) => { + ThrowIfInvalid(samples.Length, inLength, outLength); + + // Fast copy + if (inLength == outLength) { + return samples[..inLength].ToArray(); + } + + double ratio = ((double) inLength) / outLength; + int[] outBuf = new int[outLength]; + + int lastSample = inLength - 1; + + for (int i = 0; i < outLength; i++) { + int a = (int) (i * ratio); // Whole part of index + + if (a == lastSample) { + outBuf[i] = samples[a]; + } else { + double b = i * ratio - a; // Fractional part of index + outBuf[i] = (int) ((1 - b) * samples[a] + b * samples[a + 1]); + } + } + + return outBuf; + }; + + /// + /// A resampling algorithm that uses sinusoidal interpolation. + /// + public static readonly ResamplingAlgorithm SineInterpolation = + (samples, inLength, outLength) => { + ThrowIfInvalid(samples.Length, inLength, outLength); + + // Fast copy + if (inLength == outLength) { + return samples[..inLength].ToArray(); + } + + double ratio = ((double) inLength) / outLength; + int[] outBuf = new int[outLength]; + + int lastSample = inLength - 1; + + for (int i = 0; i < outLength; i++) { + int a = (int) (i * ratio); + + if (a == lastSample) { + outBuf[i] = samples[a]; + } else { + double b = i * ratio - a; + double c = (1.0 - Math.Cos(b * Math.PI)) / 2.0; + + outBuf[i] = (int) ((1 - c) * samples[a] + c * samples[a + 1]); + } + } + + return outBuf; + }; + + /// + /// A resampling algorithm that uses cubic interpolation. + /// + public static readonly ResamplingAlgorithm CubicInterpolation = + (samples, inLength, outLength) => { + ThrowIfInvalid(samples.Length, inLength, outLength); + + // Fast copy + if (inLength == outLength) { + return samples[..inLength].ToArray(); + } + + double ratio = ((double) inLength) / outLength; + int[] outBuf = new int[outLength]; + + for (int i = 0; i < outLength; i++) { + int a = (int) (i * ratio); + + short s0 = (short) ((a == 0) ? samples[0] : samples[a - 1]); + short s1 = (short) samples[a]; + short s2 = (short) ((a + 1 >= inLength) ? samples[inLength - 1] : samples[a + 1]); + short s3 = (short) ((a + 2 >= inLength) ? samples[inLength - 1] : samples[a + 2]); + + double a0 = s3 - s2 - s0 + s1; + double a1 = s0 - s1 - a0; + double a2 = s2 - s0; + double b0 = i * ratio - a; + double b2 = b0 * b0; + double b3 = b2 * b0; + + outBuf[i] = (int) (b3 * a0 + b2 * a1 + b0 * a2 + s1); + } + + return outBuf; + }; + + /// + /// A resampling algorithm that uses band-limited interpolation. + /// + public static readonly ResamplingAlgorithm BandlimitedInterpolation = + (samples, inLength, outLength) => { + ThrowIfInvalid(samples.Length, inLength, outLength); + + // Fast copy + if (inLength == outLength) { + return samples[..inLength].ToArray(); + } + + double ratio = ((double) inLength) / outLength; + int[] outBuf = new int[outLength]; + + const int FIROrder = 15; + if (ratio > 1.0) { + int[] samples_antialiased = new int[inLength]; + double[] fir_coefs = new double[FIROrder + 1]; + + // Compute FIR coefficients + for (int k = 0; k <= FIROrder; k++) { + fir_coefs[k] = Sinc(k / ratio) / ratio; + } + + // Apply FIR filter to samples + for (int i = 0; i < inLength; i++) { + double acc = samples[i] * fir_coefs[0]; + for (int k = FIROrder; k > 0; k--) { + acc += fir_coefs[k] * ((i + k < inLength) ? samples[i + k] : samples[inLength - 1]); + acc += fir_coefs[k] * ((i - k >= 0) ? samples[i - k] : samples[0]); + } + samples_antialiased[i] = (int) acc; + } + samples = samples_antialiased; + } + + // Actual resampling using sinc interpolation + for (int i = 0; i < outLength; i++) { + double a = i * ratio; + double acc = 0.0; + int aend = (int) (a + FIROrder) + 1; + + for (int j = (int) (a - FIROrder); j < aend; j++) { + int sample; + + if (j >= 0) { + if (j < inLength) { + sample = samples[j]; + } else { + sample = samples[inLength - 1]; + } + } else { + sample = samples[0]; + } + + acc += sample * Sinc(a - j); + } + + outBuf[i] = (int) acc; + } + + return outBuf; + }; + + + /// + /// Performs the normalized sinc function on a value. + /// + /// The argument of the sinc function. + /// The result of sinc(x). + public static double Sinc(double x) { + if (x == 0D) { + return 1D; + } + + return Math.Sin(Math.PI * x) / (Math.PI * x); + } +} diff --git a/SampleRate.cs b/SampleRate.cs new file mode 100644 index 0000000..f90a29d --- /dev/null +++ b/SampleRate.cs @@ -0,0 +1,91 @@ +namespace BRRSuite; + +/// +/// Represents an audio sample rate given in hertz. +/// +public sealed class SampleRate : IComparable { + /// + /// Gets the number of samples per second expressed by this frequency. + /// + public int Frequency { get; } + + /// + /// Gets the frequency expressed as and rounded to the nearest kilohertz. + /// + public int FrequencykHz => Frequency / 1000; + + /// + /// Gets the ratio between the SNES DSP frequency of 32000 and this frequency. + /// + public decimal Cram => 32000M / Frequency; + + /// + /// Gets the value of this frequency with units (Hz). + /// + public string Name { get; } + + /// + /// Creates a new instance of the class with the specified frequency. + /// + /// The frequency of this sample. This value should be a positive, nonzero value. + /// When is 0 or negative. + public SampleRate(int frequency) { + if (frequency < 1) { + throw new ArgumentException("Frequency should be a positive, non-zero value."); + } + + Name = $"{frequency} Hz"; + Frequency = frequency; + } + + /// + public override string ToString() => Name; + + /// + /// Calculates the ratio representing this sample rate resampled to the given target frequency. + /// + /// The target frequency to resample to + /// A resampling ratio. + public decimal ResampleTo(int targetFrequency) { + return (decimal) Frequency / targetFrequency; + } + + /// + public decimal ResampleTo(SampleRate targetFrequency) { + return ResampleTo(targetFrequency.Frequency); + } + + /// + /// If is . + public int CompareTo(SampleRate? other) { + if (other is null) { + throw new ArgumentNullException(nameof(other), "Comparator argument was null."); + } + return Frequency.CompareTo(other.Frequency); + } + + /// + /// Represents a sample rate of 32000 Hz, the frequency of the SNES DSP. + /// + public static readonly SampleRate SR32000 = new(32000); + + /// + /// Represents a sample rate of 16000 Hz. + /// + public static readonly SampleRate SR16000 = new(16000); + + /// + /// Represents a sample rate of 8000 Hz. + /// + public static readonly SampleRate SR8000 = new(8000); + + /// + /// Represents a sample rate of 4000 Hz. + /// + public static readonly SampleRate SR4000 = new(4000); + + /// + /// Represents a sample rate of 44100 Hz, the standard for CD-quality audio. + /// + public static readonly SampleRate SR44100 = new(44100); +} diff --git a/SuiteSample.cs b/SuiteSample.cs new file mode 100644 index 0000000..51ab980 --- /dev/null +++ b/SuiteSample.cs @@ -0,0 +1,62 @@ +namespace BRRSuite; + +/// +/// Contains constants related to implementing the BRR Suite Sample format. +/// +public static class SuiteSample { + /// + /// The preferred file extension for a BRR Suite Sample file. + /// + public const string Extension = "brs"; + + /// + /// The preferred name for BRR Suite Sample files. + /// + public const string FormatName = "BRR Suite Sample"; + +#pragma warning disable CS1591 // Missing XML comment for publicly visible type or member + + // Header + public const string FormatSignature = "BRRS"; + public const int FormatSignatureLocation = 0; + public const int ChecksumLocation = 4; + public const int ChecksumComplementLocation = 6; + + // Metadata block + public const string MetaBlockSignature = "META"; + public const int MetaBlockLocation = 8; + + + + public const int MetaBlockSignatureLocation = MetaBlockLocation+0; + + public const int InstrumentNameLocation = MetaBlockLocation+4; + public const int InstrumentNameLength = 24; + public const int InstrumentNameEnd = InstrumentNameLocation+InstrumentNameLength; + public const char InstrumentNamePadChar = ' '; + + public const int PitchLocation = MetaBlockLocation+24; + public const int EncodingFrequencyLocation = MetaBlockLocation+28; + + + // Data block + public const string DataBlockSignature = "DATA"; + + public const int DataBlockLocation = 51; + public const int DataBlockSignatureLocation = DataBlockLocation+0; + + public const int LoopTypeLocation = DataBlockLocation+4; + public const byte NonloopingSample = 0; + public const byte LoopingSample = 1; + public const byte ForeignLoopingSample = 2; + + public const int LoopBlockLocation = DataBlockLocation+5; + public const int LoopPointLocation = DataBlockLocation+7; + public const int SampleBlocksLocation = DataBlockLocation+9; + public const int SampleLengthLocation = DataBlockLocation+11; + + public const int SamplesDataLocation = 64; + +#pragma warning restore CS1591 // Missing XML comment for publicly visible type or member + +} diff --git a/WaveContainer.cs b/WaveContainer.cs new file mode 100644 index 0000000..1b1a807 --- /dev/null +++ b/WaveContainer.cs @@ -0,0 +1,364 @@ +namespace BRRSuite; + +/// +/// Holds a Wave Sound file with functionality to easily access the data as both a valid file with header or a raw stream of samples. +/// +public sealed class WaveContainer { + /// + /// The preferred extension for Wave Sound files. + /// + public const string Extension = "wav"; + + // constants for making valid wave files + private const int WaveChunkIDOffset = 0; + private const int WaveChunkSizeOffset = 4; + private const int WaveFormatOffset = 8; + private const int WaveSubchunk1IDOffset = 12; + private const int WaveSubchunk1SizeOffset = 16; + private const int WaveAudioFormatOffset = 20; + private const int WaveChannelCountOffset = 22; + private const int WaveSampleRateOffset = 24; + private const int WaveByteRateOffset = 28; + private const int WaveBlockAlignOffset = 32; + private const int WaveBitsPerSampleOffset = 34; + private const int WaveSubchunk2IDOffset = 36; + private const int WaveSubchunk2SizeOffset = 40; + private const int WaveDataOffset = 44; + + private const string RiffChunkDescriptor = "RIFF"; + private const string WaveChunkDescriptor = "WAVE"; + private const string FormatChunkDescriptor = "fmt "; + private const string DataChunkDescriptor = "data"; + + /***************************************************************************************************************/ + + /// + /// Gets the sample rate this audio file should be played back at. + /// + public SampleRate SampleRate { get; } + + /// + /// Gets the fidelity of one sample, given as its size in bits. + /// + public int BitsPerSample { get; } = PreferredBitsPerSample; + + /// + /// Gets number of bytes required to represent each sample. + /// + public int BytesPerSample => BitsPerSample / 8; + + /// + /// Gets the number of samples contained in this audio file. + /// + public int SampleCount { get; } + +#pragma warning disable IDE0079 // Remove unnecessary suppression - get rekt +#pragma warning disable CA1822 // Mark members as static + // This might not be constant one day. Not today. + + /// + /// Gets the number of individual channels contained in this audio file. + /// + public int Channels => 1; +#pragma warning restore CA1822 // Mark members as static +#pragma warning restore IDE0079 // Remove unnecessary suppression + + /// + /// Gets the number of bytes processed per second when this audio file is played at its intended speed. + /// + public int ByteRate => SampleRate.Frequency * Channels * BytesPerSample; + + /// + /// Gets the size of chunk 2 (which contains the audio data) in bytes. + /// + public int Chunk2Size => SampleCount * Channels * BytesPerSample; + + /// + /// Gets or sets flags that indicate how this audio file was modified from its original source or after creation. + /// + public WaveFileChanges ChangesFromOriginal { get; set; } = WaveFileChanges.None; + + /// + /// Gets or sets a sample at the given index. + /// + public short this[Index i] { + get => _samples[i]; + set => _samples[i] = value; + } + + private readonly int dataSize; + + private readonly byte[] _data; + + // this will be a slice of the above + private readonly ArraySegment _samples; + + /// + /// Initializes a new instance of the class with the specified properties and an initially silent waveform. + /// + /// The sample rate of this audio. + /// The fidelity of the audio expressed as the size of the sample in bits. + /// The number of samples in this audio. + public WaveContainer(int samplerate, int bitsPerSample, int sampleCount) { + SampleCount = sampleCount; + SampleRate = new(samplerate); + BitsPerSample = bitsPerSample; + + dataSize = WaveDataOffset + Chunk2Size; + + _data = new byte[dataSize]; + + _samples = GetSamplesSlice(_data); + + FixHeader(); + } + + /// + /// Creates a slice over an audio data stream at the start of the samples data and recast as an array of values. + /// + /// A new of type . + private static ArraySegment GetSamplesSlice(byte[] fullData) { + short[] ds = System.Runtime.CompilerServices.Unsafe.As(fullData); + + // divided by 2 because byte => short + return new(ds, WaveDataOffset / 2, (fullData.Length - WaveDataOffset) / 2); + } + + /// + /// Returns a new array of signed integers copied from the samples data. + /// + /// A new array of integers containing a copy of this sample. + public int[] GetSamplesCopy() { + int[] ret = new int[SampleCount]; + + for (int i = 0; i < SampleCount; i++) { + ret[i] = _samples[i]; + } + + return ret; + } + + /// + /// Creates a read-only over the entire Wave Sound file's data. + /// + /// A covering the entire file, header included. + public MemoryStream AsMemoryStream() { + return new(_data); + } + + + /// + /// Reads a Wave Sound file from a given location. + /// If the file contains between 2 and 4 channels, they will be mixed down to mono. + /// Files with more channels will be rejected with an exception. + /// + /// A relative or absolute path to the audio file. + /// A new with a single channel of 16-bit PCM audio. + /// + public static WaveContainer ReadFromFile(string path) { + using var rd = new FileStream(path, FileMode.Open, FileAccess.Read); + + // Verify WAV + var data = new byte[(int) rd.Length]; + rd.Read(data, 0, data.Length); + + rd.Close(); + + if (!VerifyWAV(data, out string message)) { + throw new BRRConversionException(message ?? "Not a valid 16-bit PCM WAV file!"); + } + + int channels = GetShortFromPosition(data, WaveChannelCountOffset); + + if (channels > 4) { + throw new BRRConversionException("Too many channels. I'm not mixing this."); + } + + int samplerate = GetIntFromPosition(data, 24); + int sampleCount = GetIntFromPosition(data, 40) / 2; + + WaveFileChanges changes = channels == 1 ? WaveFileChanges.None : WaveFileChanges.MixedToMono; + + var ret = new WaveContainer(samplerate, PreferredBitsPerSample, sampleCount) { + ChangesFromOriginal = changes + }; + + // fast copy for 1 channel + if (channels == 1) { + Array.Copy(data, WaveDataOffset, ret._data, WaveDataOffset, ret.Chunk2Size); + } else { + var insamples = GetSamplesSlice(data); + + // average each channel + for (int i = 0, j = 0; i < sampleCount; i ++, j += channels) { + int cur = 0; + + for (int k = 0; k < channels; k++) { + cur += insamples[j++]; + } + + ret._samples[i] = (short) (cur / channels); + } + } + + return ret; + } + + /// + /// Tests a stream of data for a properly-formed WAV header that indicates it is uncompressed, 16-bit PCM audio. + /// + /// The data to validate. + /// When this method returns, this will contain a message about where, if at all, the data was deemed invalid. + /// if the header is valid; otherwise . + public static bool VerifyWAV(byte[] data, out string message) { + if (data.Length < 64) { + message = "This file is too small to be of use."; + return false; + } + + string? badMSG = TestSubstring(WaveChunkIDOffset, RiffChunkDescriptor); + if (badMSG is not null) { + message = badMSG; + return false; + } + + badMSG = TestSubstring(WaveFormatOffset, WaveChunkDescriptor); + if (badMSG is not null) { + message = badMSG; + return false; + } + + badMSG = TestSubstring(WaveSubchunk1IDOffset, FormatChunkDescriptor); + if (badMSG is not null) { + message = badMSG; + return false; + } + + badMSG = TestSubstring(WaveSubchunk2IDOffset, DataChunkDescriptor); + if (badMSG is not null) { + message = badMSG; + return false; + } + + if (GetShortFromPosition(data, WaveAudioFormatOffset) != 1) { + message = "Not an uncompressed PCM formatted wave."; + return false; + } + + if (GetShortFromPosition(data, WaveBitsPerSampleOffset) != PreferredBitsPerSample) { + message = "Not a 16-bit Wave Sound file."; + return false; + } + + if (GetIntFromPosition(data, WaveChunkSizeOffset) != (data.Length - 8)) { + message = $"Header file size does not match actual file size."; + return false; + } + + message = "Valid!"; + return true; + + string? TestSubstring(int start, string test) { + var s = data[start..(start + 4)]; + string result = new(s.Select(o => (char) o).ToArray()); + + if (result == test) { + return null; + } + + return $"Bad string at {start}: {result} | Expected: {test}"; + } + } + + /// + /// Fixes the header data to match the current properties of the wave file. + /// + private void FixHeader() { + // RIFF chunk + WriteString(RiffChunkDescriptor, WaveChunkIDOffset); + WriteInt(Chunk2Size + WaveDataOffset - 8, WaveChunkSizeOffset); + WriteString(WaveChunkDescriptor, WaveFormatOffset); + + // format subchunk + WriteString(FormatChunkDescriptor, WaveSubchunk1IDOffset); + WriteInt(16, WaveSubchunk1SizeOffset); // header size + WriteShort(1, WaveAudioFormatOffset); // audio format => 1 (PCM) + WriteShort(Channels, WaveChannelCountOffset); // number of channels + WriteInt(SampleRate.Frequency, WaveSampleRateOffset); + WriteInt(ByteRate, WaveByteRateOffset); // byte rate + WriteShort(Channels * BytesPerSample, WaveBlockAlignOffset); // block align + WriteShort(BitsPerSample, WaveBitsPerSampleOffset); + + // data subchunk + WriteString(DataChunkDescriptor, WaveSubchunk2IDOffset); + WriteInt(Chunk2Size, WaveSubchunk2SizeOffset); + + void WriteString(string s, int offset) { + foreach (var c in s) { + _data[offset++] = (byte) c; + } + } + + void WriteShort(int s, int offset) { + _data[offset++] = (byte) s; + _data[offset++] = (byte) (s >> 8); + } + + void WriteInt(int s, int offset) { + _data[offset++] = (byte) s; + _data[offset++] = (byte) (s >> 8); + _data[offset++] = (byte) (s >> 16); + _data[offset++] = (byte) (s >> 24); + } + } + + /// + /// Exports this Wave Sound file to the given location. + /// + /// The absolute or relative path this audio should be saved to. + /// Allows the export to add or change the extension of the path to the preferred extension. + public void Export(string path, bool fixPath = false) { + if (fixPath) { + path = Path.ChangeExtension(path, Extension); + } + + using var fs = File.Open(path, FileMode.Create, FileAccess.Write); + using var ws = AsMemoryStream(); + + ws.CopyTo(fs); + + fs.Flush(); + } + + private static int GetIntFromPosition(byte[] data, int position) { + return (data[position++]) | (data[position++] << 8) | (data[position++] << 16) | (data[position] << 24); + } + + private static int GetShortFromPosition(byte[] data, int position) { + return (data[position++]) | (data[position] << 8); + } + + /// + /// Gets a segment of data corresponding to the 16 samples of the requested block. + /// + /// The samples to get a block of. + /// Index of block to cover. + /// A of length 16 over the specified block. + /// If the index requested is negative or more than the number of blocks in the sample. + public static Span GetBlockAt(int[] samples, int block) { + if (block >= (samples.Length / PcmBlockSize) || block < 0) { + throw new IndexOutOfRangeException(); + } + + return new(samples, block * PcmBlockSize, PcmBlockSize); + } + + /// + public static Span GetBlockAt(short[] samples, int block) { + if (block >= (samples.Length / PcmBlockSize) || block < 0) { + throw new IndexOutOfRangeException(); + } + + return new(samples, block * PcmBlockSize, PcmBlockSize); + } +} diff --git a/WaveFileChanges.cs b/WaveFileChanges.cs new file mode 100644 index 0000000..49f2108 --- /dev/null +++ b/WaveFileChanges.cs @@ -0,0 +1,32 @@ +namespace BRRSuite; + +/// +/// Flags changes made to a WAV file during loading. +/// +[Flags] +public enum WaveFileChanges { + /// + /// No changes were made to the source audio + /// + None = 0, + + /// + /// The source audio contained multiple channels that were remixed down to a single channel. + /// + MixedToMono = 1 << 0, + + /// + /// The source audio contained too many channels, and some of them were ignored. + /// + AdditionalChannelsIgnored = 1 << 1, + + /// + /// The source audio was resampled to 16 bits per sample. + /// + ResampledTo16Bit = 1 << 8, + + /// + /// The source audio had its amplitude changed. + /// + AmplitudeAdjusted = 1 << 16, +} diff --git a/brslogo.ico b/brslogo.ico new file mode 100644 index 0000000..6585601 Binary files /dev/null and b/brslogo.ico differ diff --git a/logofull.png b/logofull.png new file mode 100644 index 0000000..5f201b1 Binary files /dev/null and b/logofull.png differ