Compare commits
19 Commits
feature/os
...
master
| Author | SHA1 | Date | |
|---|---|---|---|
|
f0e2d98c12
|
|||
|
83fe94b34d
|
|||
| 8fc7fbfd30 | |||
| 2b4e3cb573 | |||
| bb3ccc296a | |||
|
868a59da0e
|
|||
|
f9ad4d844b
|
|||
| c50a8335d7 | |||
|
64fa6396bc
|
|||
|
268103d7da
|
|||
| a445fc44b3 | |||
| bcb75a65f9 | |||
|
2e4dc2c179
|
|||
| aaec53cfea | |||
| 7eb6a1755d | |||
| 97c743100a | |||
| 320a3cc8e0 | |||
|
a93278f705
|
|||
|
46ac7c9bba
|
216
.clang-format
Normal file
216
.clang-format
Normal file
@@ -0,0 +1,216 @@
|
||||
---
|
||||
Language: Cpp
|
||||
# BasedOnStyle: LLVM
|
||||
AccessModifierOffset: -2
|
||||
AlignAfterOpenBracket: Align
|
||||
AlignArrayOfStructures: None
|
||||
AlignConsecutiveAssignments:
|
||||
Enabled: false
|
||||
AcrossEmptyLines: false
|
||||
AcrossComments: false
|
||||
AlignCompound: false
|
||||
PadOperators: true
|
||||
AlignConsecutiveBitFields:
|
||||
Enabled: false
|
||||
AcrossEmptyLines: false
|
||||
AcrossComments: false
|
||||
AlignCompound: false
|
||||
PadOperators: false
|
||||
AlignConsecutiveDeclarations:
|
||||
Enabled: false
|
||||
AcrossEmptyLines: false
|
||||
AcrossComments: false
|
||||
AlignCompound: false
|
||||
PadOperators: false
|
||||
AlignConsecutiveMacros:
|
||||
Enabled: false
|
||||
AcrossEmptyLines: false
|
||||
AcrossComments: false
|
||||
AlignCompound: false
|
||||
PadOperators: false
|
||||
AlignEscapedNewlines: Right
|
||||
AlignOperands: Align
|
||||
AlignTrailingComments: true
|
||||
AllowAllArgumentsOnNextLine: true
|
||||
AllowAllParametersOfDeclarationOnNextLine: true
|
||||
AllowShortEnumsOnASingleLine: true
|
||||
AllowShortBlocksOnASingleLine: Never
|
||||
AllowShortCaseLabelsOnASingleLine: false
|
||||
AllowShortFunctionsOnASingleLine: All
|
||||
AllowShortLambdasOnASingleLine: All
|
||||
AllowShortIfStatementsOnASingleLine: Never
|
||||
AllowShortLoopsOnASingleLine: false
|
||||
AlwaysBreakAfterDefinitionReturnType: None
|
||||
AlwaysBreakAfterReturnType: None
|
||||
AlwaysBreakBeforeMultilineStrings: false
|
||||
AlwaysBreakTemplateDeclarations: MultiLine
|
||||
AttributeMacros:
|
||||
- __capability
|
||||
BinPackArguments: true
|
||||
BinPackParameters: true
|
||||
BraceWrapping:
|
||||
AfterCaseLabel: false
|
||||
AfterClass: false
|
||||
AfterControlStatement: Never
|
||||
AfterEnum: false
|
||||
AfterFunction: false
|
||||
AfterNamespace: false
|
||||
AfterObjCDeclaration: false
|
||||
AfterStruct: false
|
||||
AfterUnion: false
|
||||
AfterExternBlock: false
|
||||
BeforeCatch: false
|
||||
BeforeElse: false
|
||||
BeforeLambdaBody: false
|
||||
BeforeWhile: false
|
||||
IndentBraces: false
|
||||
SplitEmptyFunction: true
|
||||
SplitEmptyRecord: true
|
||||
SplitEmptyNamespace: true
|
||||
BreakBeforeBinaryOperators: None
|
||||
BreakBeforeConceptDeclarations: Always
|
||||
BreakBeforeBraces: Attach
|
||||
BreakBeforeInheritanceComma: false
|
||||
BreakInheritanceList: BeforeColon
|
||||
BreakBeforeTernaryOperators: true
|
||||
BreakConstructorInitializersBeforeComma: false
|
||||
BreakConstructorInitializers: BeforeColon
|
||||
BreakAfterJavaFieldAnnotations: false
|
||||
BreakStringLiterals: true
|
||||
ColumnLimit: 80
|
||||
CommentPragmas: '^ IWYU pragma:'
|
||||
QualifierAlignment: Leave
|
||||
CompactNamespaces: false
|
||||
ConstructorInitializerIndentWidth: 4
|
||||
ContinuationIndentWidth: 4
|
||||
Cpp11BracedListStyle: true
|
||||
DeriveLineEnding: true
|
||||
DerivePointerAlignment: false
|
||||
DisableFormat: false
|
||||
EmptyLineAfterAccessModifier: Never
|
||||
EmptyLineBeforeAccessModifier: LogicalBlock
|
||||
ExperimentalAutoDetectBinPacking: false
|
||||
PackConstructorInitializers: BinPack
|
||||
BasedOnStyle: ''
|
||||
ConstructorInitializerAllOnOneLineOrOnePerLine: false
|
||||
AllowAllConstructorInitializersOnNextLine: true
|
||||
FixNamespaceComments: true
|
||||
ForEachMacros:
|
||||
- foreach
|
||||
- Q_FOREACH
|
||||
- BOOST_FOREACH
|
||||
IfMacros:
|
||||
- KJ_IF_MAYBE
|
||||
IncludeBlocks: Preserve
|
||||
IncludeCategories:
|
||||
- Regex: '^"(llvm|llvm-c|clang|clang-c)/'
|
||||
Priority: 2
|
||||
SortPriority: 0
|
||||
CaseSensitive: false
|
||||
- Regex: '^(<|"(gtest|gmock|isl|json)/)'
|
||||
Priority: 3
|
||||
SortPriority: 0
|
||||
CaseSensitive: false
|
||||
- Regex: '.*'
|
||||
Priority: 1
|
||||
SortPriority: 0
|
||||
CaseSensitive: false
|
||||
IncludeIsMainRegex: '(Test)?$'
|
||||
IncludeIsMainSourceRegex: ''
|
||||
IndentAccessModifiers: false
|
||||
IndentCaseLabels: true
|
||||
IndentCaseBlocks: false
|
||||
IndentGotoLabels: true
|
||||
IndentPPDirectives: None
|
||||
IndentExternBlock: AfterExternBlock
|
||||
IndentRequiresClause: true
|
||||
IndentWidth: 4
|
||||
IndentWrappedFunctionNames: false
|
||||
InsertBraces: false
|
||||
InsertTrailingCommas: None
|
||||
JavaScriptQuotes: Leave
|
||||
JavaScriptWrapImports: true
|
||||
KeepEmptyLinesAtTheStartOfBlocks: true
|
||||
LambdaBodyIndentation: Signature
|
||||
MacroBlockBegin: ''
|
||||
MacroBlockEnd: ''
|
||||
MaxEmptyLinesToKeep: 1
|
||||
NamespaceIndentation: None
|
||||
ObjCBinPackProtocolList: Auto
|
||||
ObjCBlockIndentWidth: 2
|
||||
ObjCBreakBeforeNestedBlockParam: true
|
||||
ObjCSpaceAfterProperty: false
|
||||
ObjCSpaceBeforeProtocolList: true
|
||||
PenaltyBreakAssignment: 2
|
||||
PenaltyBreakBeforeFirstCallParameter: 19
|
||||
PenaltyBreakComment: 300
|
||||
PenaltyBreakFirstLessLess: 120
|
||||
PenaltyBreakOpenParenthesis: 0
|
||||
PenaltyBreakString: 1000
|
||||
PenaltyBreakTemplateDeclaration: 10
|
||||
PenaltyExcessCharacter: 1000000
|
||||
PenaltyReturnTypeOnItsOwnLine: 60
|
||||
PenaltyIndentedWhitespace: 0
|
||||
PointerAlignment: Left
|
||||
PPIndentWidth: -1
|
||||
ReferenceAlignment: Pointer
|
||||
ReflowComments: true
|
||||
RemoveBracesLLVM: false
|
||||
RequiresClausePosition: OwnLine
|
||||
SeparateDefinitionBlocks: Leave
|
||||
ShortNamespaceLines: 1
|
||||
SortIncludes: CaseSensitive
|
||||
SortJavaStaticImport: Before
|
||||
SortUsingDeclarations: true
|
||||
SpaceAfterCStyleCast: false
|
||||
SpaceAfterLogicalNot: false
|
||||
SpaceAfterTemplateKeyword: true
|
||||
SpaceBeforeAssignmentOperators: true
|
||||
SpaceBeforeCaseColon: false
|
||||
SpaceBeforeCpp11BracedList: false
|
||||
SpaceBeforeCtorInitializerColon: true
|
||||
SpaceBeforeInheritanceColon: true
|
||||
SpaceBeforeParens: ControlStatements
|
||||
SpaceBeforeParensOptions:
|
||||
AfterControlStatements: true
|
||||
AfterForeachMacros: true
|
||||
AfterFunctionDefinitionName: false
|
||||
AfterFunctionDeclarationName: false
|
||||
AfterIfMacros: true
|
||||
AfterOverloadedOperator: false
|
||||
AfterRequiresInClause: false
|
||||
AfterRequiresInExpression: false
|
||||
BeforeNonEmptyParentheses: false
|
||||
SpaceAroundPointerQualifiers: Default
|
||||
SpaceBeforeRangeBasedForLoopColon: true
|
||||
SpaceInEmptyBlock: false
|
||||
SpaceInEmptyParentheses: false
|
||||
SpacesBeforeTrailingComments: 1
|
||||
SpacesInAngles: Never
|
||||
SpacesInConditionalStatement: false
|
||||
SpacesInContainerLiterals: true
|
||||
SpacesInCStyleCastParentheses: false
|
||||
SpacesInLineCommentPrefix:
|
||||
Minimum: 1
|
||||
Maximum: -1
|
||||
SpacesInParentheses: false
|
||||
SpacesInSquareBrackets: false
|
||||
SpaceBeforeSquareBrackets: false
|
||||
BitFieldColonSpacing: Both
|
||||
Standard: Latest
|
||||
StatementAttributeLikeMacros:
|
||||
- Q_EMIT
|
||||
StatementMacros:
|
||||
- Q_UNUSED
|
||||
- QT_REQUIRE_VERSION
|
||||
TabWidth: 8
|
||||
UseCRLF: false
|
||||
UseTab: Never
|
||||
WhitespaceSensitiveMacros:
|
||||
- STRINGIZE
|
||||
- PP_STRINGIZE
|
||||
- BOOST_PP_STRINGIZE
|
||||
- NS_SWIFT_NAME
|
||||
- CF_SWIFT_NAME
|
||||
...
|
||||
|
||||
5
.gitignore
vendored
5
.gitignore
vendored
@@ -1,4 +1,7 @@
|
||||
/bin
|
||||
.DS_Store
|
||||
/Debug/
|
||||
*.wav
|
||||
*.wav
|
||||
*.dSYM
|
||||
/lib
|
||||
/build
|
||||
16
.vscode/launch.json
vendored
Normal file
16
.vscode/launch.json
vendored
Normal file
@@ -0,0 +1,16 @@
|
||||
{
|
||||
// Use IntelliSense to learn about possible attributes.
|
||||
// Hover to view descriptions of existing attributes.
|
||||
// For more information, visit: https://go.microsoft.com/fwlink/?linkid=830387
|
||||
"version": "0.2.0",
|
||||
"configurations": [
|
||||
{
|
||||
"type": "lldb",
|
||||
"request": "launch",
|
||||
"name": "Debug",
|
||||
"program": "${workspaceFolder}/bin/main",
|
||||
"args": [],
|
||||
"cwd": "${workspaceFolder}"
|
||||
}
|
||||
]
|
||||
}
|
||||
66
.vscode/settings.json
vendored
66
.vscode/settings.json
vendored
@@ -3,6 +3,68 @@
|
||||
"readability/casting"
|
||||
],
|
||||
"files.associations": {
|
||||
"algorithm": "c"
|
||||
}
|
||||
"algorithm": "c",
|
||||
"__bit_reference": "c",
|
||||
"bitset": "c",
|
||||
"chrono": "c",
|
||||
"unordered_map": "c",
|
||||
"__bits": "cpp",
|
||||
"__config": "cpp",
|
||||
"__debug": "cpp",
|
||||
"__errc": "cpp",
|
||||
"__hash_table": "cpp",
|
||||
"__locale": "cpp",
|
||||
"__mutex_base": "cpp",
|
||||
"__node_handle": "cpp",
|
||||
"__split_buffer": "cpp",
|
||||
"__threading_support": "cpp",
|
||||
"__tuple": "cpp",
|
||||
"__verbose_abort": "cpp",
|
||||
"array": "cpp",
|
||||
"atomic": "cpp",
|
||||
"bit": "cpp",
|
||||
"cctype": "cpp",
|
||||
"clocale": "cpp",
|
||||
"cmath": "cpp",
|
||||
"complex": "cpp",
|
||||
"cstdarg": "cpp",
|
||||
"cstddef": "cpp",
|
||||
"cstdint": "cpp",
|
||||
"cstdio": "cpp",
|
||||
"cstdlib": "cpp",
|
||||
"cstring": "cpp",
|
||||
"ctime": "cpp",
|
||||
"cwchar": "cpp",
|
||||
"cwctype": "cpp",
|
||||
"exception": "cpp",
|
||||
"initializer_list": "cpp",
|
||||
"ios": "cpp",
|
||||
"iosfwd": "cpp",
|
||||
"istream": "cpp",
|
||||
"limits": "cpp",
|
||||
"locale": "cpp",
|
||||
"memory": "cpp",
|
||||
"mutex": "cpp",
|
||||
"new": "cpp",
|
||||
"optional": "cpp",
|
||||
"ostream": "cpp",
|
||||
"ratio": "cpp",
|
||||
"sstream": "cpp",
|
||||
"stdexcept": "cpp",
|
||||
"streambuf": "cpp",
|
||||
"string": "cpp",
|
||||
"string_view": "cpp",
|
||||
"system_error": "cpp",
|
||||
"tuple": "cpp",
|
||||
"type_traits": "cpp",
|
||||
"typeinfo": "cpp",
|
||||
"variant": "cpp",
|
||||
"vector": "cpp",
|
||||
"__nullptr": "cpp",
|
||||
"__string": "cpp",
|
||||
"compare": "cpp",
|
||||
"concepts": "cpp",
|
||||
"numeric": "cpp"
|
||||
},
|
||||
"FSharp.suggestGitignore": false,
|
||||
}
|
||||
24
.vscode/tasks.json
vendored
Normal file
24
.vscode/tasks.json
vendored
Normal file
@@ -0,0 +1,24 @@
|
||||
{
|
||||
"tasks": [
|
||||
{
|
||||
"type": "cppbuild",
|
||||
"label": "C/C++: clang сборка активного файла",
|
||||
"command": "sh",
|
||||
"args": [
|
||||
"${workspaceFolder}/build.sh"
|
||||
],
|
||||
"options": {
|
||||
"cwd": "${workspaceFolder}"
|
||||
},
|
||||
"problemMatcher": [
|
||||
"$gcc"
|
||||
],
|
||||
"group": {
|
||||
"kind": "build",
|
||||
"isDefault": true
|
||||
},
|
||||
"detail": "Задача создана отладчиком."
|
||||
}
|
||||
],
|
||||
"version": "2.0.0"
|
||||
}
|
||||
37
CMakeLists.txt
Normal file
37
CMakeLists.txt
Normal file
@@ -0,0 +1,37 @@
|
||||
cmake_minimum_required(VERSION 3.5)
|
||||
project(SeeSynth)
|
||||
#set(CMAKE_C_STANDARD 99)
|
||||
|
||||
# Adding Raylib
|
||||
include(FetchContent)
|
||||
set(FETCHCONTENT_QUIET FALSE)
|
||||
set(BUILD_EXAMPLES OFF CACHE BOOL "" FORCE) # don't build the supplied examples
|
||||
set(BUILD_GAMES OFF CACHE BOOL "" FORCE) # don't build the supplied example games
|
||||
|
||||
FetchContent_Declare(
|
||||
raylib
|
||||
GIT_REPOSITORY "https://github.com/raysan5/raylib.git"
|
||||
GIT_TAG "4.5.0"
|
||||
GIT_PROGRESS TRUE
|
||||
)
|
||||
|
||||
FetchContent_MakeAvailable(raylib)
|
||||
|
||||
# Adding our source files
|
||||
file(GLOB_RECURSE PROJECT_SOURCES CONFIGURE_DEPENDS "${CMAKE_CURRENT_LIST_DIR}/src/*.cpp") # Define PROJECT_SOURCES as a list of all source files
|
||||
set(PROJECT_INCLUDE "${CMAKE_CURRENT_LIST_DIR}/inc/") # Define PROJECT_INCLUDE to be the path to the include directory of the project
|
||||
|
||||
|
||||
# Declaring our executable
|
||||
add_executable(${PROJECT_NAME})
|
||||
set_target_properties(
|
||||
${PROJECT_NAME} PROPERTIES
|
||||
CXX_STANDARD 17
|
||||
CXX_STANDARD_REQUIRED ON)
|
||||
target_sources(${PROJECT_NAME} PRIVATE ${PROJECT_SOURCES})
|
||||
target_include_directories(${PROJECT_NAME} PRIVATE ${PROJECT_INCLUDE})
|
||||
target_link_libraries(${PROJECT_NAME} PRIVATE raylib)
|
||||
|
||||
# Setting ASSETS_PATH
|
||||
target_compile_definitions(${PROJECT_NAME} PUBLIC ASSETS_PATH="${CMAKE_CURRENT_SOURCE_DIR}/assets/") # Set the asset path macro to the absolute path on the dev machine
|
||||
#target_compile_definitions(${PROJECT_NAME} PUBLIC ASSETS_PATH="./assets") # Set the asset path macro in release mode to a relative path that assumes the assets folder is in the same directory as the game executable
|
||||
6
build.sh
6
build.sh
@@ -1,3 +1,5 @@
|
||||
#!/bin/bash
|
||||
CC="${CXX:-cc}"
|
||||
$CC -Wall -std=c11 ./main.c ./parser.c -lm -o ./bin/main
|
||||
CC="${CXX:-c++}"
|
||||
LL="-lm -lraylib"
|
||||
FLAGS="-Wall -std=c++17 -I./inc/ -g"
|
||||
$CC $FLAGS $(find ./src -type f -iregex ".*\.cpp") $LL -o ./bin/main
|
||||
|
||||
BIN
docs/ADSR States.png
Normal file
BIN
docs/ADSR States.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 276 KiB |
61
docs/ADSR.md
Normal file
61
docs/ADSR.md
Normal file
@@ -0,0 +1,61 @@
|
||||
Certainly! Here's an example of generating an ADSR (Attack, Decay, Sustain, Release) envelope in Python:
|
||||
|
||||
```python
|
||||
import numpy as np
|
||||
import matplotlib.pyplot as plt
|
||||
|
||||
# Parameters
|
||||
sample_rate = 44100 # Sample rate in Hz
|
||||
duration = 5 # Duration of the envelope in seconds
|
||||
|
||||
# Time values
|
||||
num_samples = int(duration * sample_rate)
|
||||
time = np.arange(num_samples) / sample_rate
|
||||
|
||||
# ADSR parameters
|
||||
attack_time = 0.5 # Attack time in seconds
|
||||
decay_time = 0.3 # Decay time in seconds
|
||||
sustain_level = 0.6 # Sustain level (0 to 1)
|
||||
release_time = 1.0 # Release time in seconds
|
||||
|
||||
# Generate the ADSR envelope
|
||||
envelope = np.zeros(num_samples)
|
||||
attack_samples = int(attack_time * sample_rate)
|
||||
decay_samples = int(decay_time * sample_rate)
|
||||
release_samples = int(release_time * sample_rate)
|
||||
|
||||
# Attack phase
|
||||
envelope[:attack_samples] = np.linspace(0, 1, num=attack_samples)
|
||||
# 1/n * count;
|
||||
|
||||
# Decay phase
|
||||
decay_slope = (1 - sustain_level) / decay_samples
|
||||
envelope[attack_samples:attack_samples + decay_samples] = np.linspace(1, sustain_level, num=decay_samples) - decay_slope * np.arange(decay_samples)
|
||||
|
||||
# Sustain phase
|
||||
envelope[attack_samples + decay_samples:-release_samples] = sustain_level
|
||||
|
||||
# Release phase
|
||||
release_slope = sustain_level / release_samples
|
||||
envelope[-release_samples:] = sustain_level - release_slope * np.arange(release_samples)
|
||||
|
||||
# Normalize the envelope
|
||||
envelope /= np.max(envelope)
|
||||
|
||||
# Plot the envelope
|
||||
plt.plot(time, envelope)
|
||||
plt.xlabel('Time (s)')
|
||||
plt.ylabel('Amplitude')
|
||||
plt.title('ADSR Envelope')
|
||||
plt.show()
|
||||
```
|
||||
|
||||
In this example, we specify the sample rate and duration of the envelope. We define the ADSR parameters: attack time, decay time, sustain level, and release time.
|
||||
|
||||
We create an array to store the envelope values and initialize it with zeros. We calculate the number of samples for each phase based on the sample rate and duration.
|
||||
|
||||
We then calculate the envelope values for each phase. The attack phase increases linearly from 0 to 1. The decay phase decreases linearly from 1 to the sustain level. The sustain phase maintains a constant value equal to the sustain level. The release phase decreases linearly from the sustain level to 0.
|
||||
|
||||
After generating the envelope, we normalize it to ensure the maximum value is 1. Finally, we plot the envelope using Matplotlib.
|
||||
|
||||
You can modify the ADSR parameters to create different envelope shapes or experiment with adding modulation to create more dynamic and expressive sounds.
|
||||
BIN
docs/Attack Formula.png
Normal file
BIN
docs/Attack Formula.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 76 KiB |
39
docs/PhaseAccumulation.md
Normal file
39
docs/PhaseAccumulation.md
Normal file
@@ -0,0 +1,39 @@
|
||||
Certainly! Here's a simple example of phase accumulation in Python code:
|
||||
|
||||
```python
|
||||
import numpy as np
|
||||
import matplotlib.pyplot as plt
|
||||
|
||||
# Parameters
|
||||
sample_rate = 44100 # Sample rate in Hz
|
||||
frequency = 440 # Frequency of the oscillator in Hz
|
||||
duration = 1 # Duration of the generated waveform in seconds
|
||||
|
||||
# Calculate the phase increment per sample
|
||||
phase_increment = 2 * np.pi * frequency / sample_rate
|
||||
|
||||
# Initialize phase and time arrays
|
||||
num_samples = int(duration * sample_rate)
|
||||
phase = np.zeros(num_samples)
|
||||
time = np.arange(num_samples) / sample_rate
|
||||
|
||||
# Perform phase accumulation
|
||||
for i in range(1, num_samples):
|
||||
phase[i] = phase[i - 1] + phase_increment
|
||||
|
||||
# Generate the waveform (sine wave) based on the accumulated phase
|
||||
waveform = np.sin(phase)
|
||||
|
||||
# Plot the waveform
|
||||
plt.plot(time, waveform)
|
||||
plt.xlabel('Time (s)')
|
||||
plt.ylabel('Amplitude')
|
||||
plt.title('Phase Accumulation Example - Sine Wave')
|
||||
plt.show()
|
||||
```
|
||||
|
||||
In this example, we specify the sample rate, frequency, and duration of the waveform. We calculate the phase increment per sample based on the desired frequency. Then, we initialize arrays to store the phase values and time values. By iterating through each sample, we perform phase accumulation by adding the phase increment to the previous phase value.
|
||||
|
||||
Finally, we generate a sine wave by taking the sine of the accumulated phase values. The resulting waveform is plotted using Matplotlib.
|
||||
|
||||
This code demonstrates the basic concept of phase accumulation, where the phase of an oscillator is incremented over time to generate a periodic waveform. You can modify the parameters, try different waveforms, or experiment with phase modulation to create more complex sounds.
|
||||
41
docs/Resources.md
Normal file
41
docs/Resources.md
Normal file
@@ -0,0 +1,41 @@
|
||||
http://basicsynth.com/index.php?page=basic
|
||||
|
||||
Signal Generator
|
||||
The most straightforward method of sound generation in software is to evaluate a periodic function for each sample time. A periodic function is any function that repeats at a constant interval, called the period. Consider the circle in the figure below. Starting at the 3:00 position and then sweeping around the circle counter-clockwise, we make a complete cycle in 2π radians and then the movement repeats. Thus the period is 2π radians. If we plot the points on the circumference over time we produce the waveform as shown below.
|
||||
|
||||
|
||||
|
||||
For audio signals, the period is the time it takes for the waveform to repeat and is thus the inverse of the frequency. In other words, a frequency of 100Hz repeats every 1/100 second. We need to generate an amplitude value for every sample time, thus the number of samples in one period is equal to the time of the period divided by the time of one sample. Since the time of one sample is the inverse of the sample rate, and the period is the inverse of the frequency, the number of samples is also the sample rate divided by the frequency: ((1/f) / (1/fs)) = (fs/f). Since our period is also equal to 2π radians, the phase increment for one sample time (φ) is 2π divided by the number of samples in one period:
|
||||
|
||||
|
||||
|
||||
where the frequency of the signal is f and the sample rate fs. The amplitude for any given sample is the y value of the phase at that point in time multiplied by the radius of the circle. In other words, the amplitude is the sine of the phase angle and we can also derive the phase increment from the sine function.
|
||||
|
||||
Signal Generation Equation
|
||||
|
||||
The value sn is the nth sample, An the peak amplitude (volume) at sample n, and θn the phase at sample n. To calculate θn for any sample n, we can multiply the phase increment for one sample time (φ) by the sample number. To calculate φ we need to determine the radians for one sample time at a given frequency. As there are 2π radians per cycle, we multiply the frequency by 2π to get the radians per second. The phase increment for one sample time is then the radians per second multiplied by the time for one sample. Substiting for θn in the original equation yields:
|
||||
|
||||
Signal Generation Equation
|
||||
|
||||
We can implement this as a program loop.
|
||||
|
||||
totalSamples = duration * sampleRate;
|
||||
for (n = 0; n < totalSamples; n++)
|
||||
sample[n] = sin((twoPI/sampleRate) * frequency * n);
|
||||
Since 2π/fs is constant through the loop, we can calculate it once. We can also replace the multiplication of the phase with a repeated addition.
|
||||
|
||||
phaseIncr = (twoPI/sampleRate) * frequency;
|
||||
phase = 0;
|
||||
totalSamples = duration * sampleRate;
|
||||
for (n = 0; n < totalSamples; n++) {
|
||||
sample[n] = sin(phase);
|
||||
phase += phaseIncr;
|
||||
if (phase >= twoPI)
|
||||
phase -= twoPI;
|
||||
}
|
||||
|
||||
|
||||
We can replace the sin function with any periodic function that returns an amplitude for a given phase angle. Thus, this small piece of code can be used to produce a very wide range of sounds. Functionally, it is the software equivalent of an oscillator, the basic building block of almost all synthesizers.
|
||||
|
||||
|
||||
https://ccrma.stanford.edu/software/stk/index.html
|
||||
214
docs/StateVariableFilter.md
Normal file
214
docs/StateVariableFilter.md
Normal file
@@ -0,0 +1,214 @@
|
||||
https://www.earlevel.com/main/2003/03/02/the-digital-state-variable-filter/
|
||||
|
||||
The frequency control coefficient, f, is defined as
|
||||
|
||||

|
||||
|
||||
where Fs is the sample rate and Fc is the filter’s corner frequency you want to set. The q coefficient is defined as
|
||||
|
||||

|
||||
|
||||
where Q normally ranges from 0.5 to inifinity (where the filter oscillates).
|
||||
|
||||
|
||||
The main drawback of the digital state variable is that it becomes unstable at higher frequencies. It depends on the Q setting, but basically the upper bound of stability is about where f reaches 1, which is at one-sixth of the sample rate (8 kHz at 48 kHz). The only way around this is to oversample. A simple way to double the filter’s sample rate (and thereby double the filter’s frequency range) is to run the filter twice with the same input sample, and discard one output sample.
|
||||
|
||||
|
||||
|
||||
example with double-sampling
|
||||
|
||||
```
|
||||
input = input buffer;
|
||||
output = output buffer;
|
||||
fs = sampling frequency;
|
||||
fc = cutoff frequency normally something like:
|
||||
440.0*pow(2.0, (midi_note - 69.0)/12.0);
|
||||
res = resonance 0 to 1;
|
||||
drive = internal distortion 0 to 0.1
|
||||
freq = 2.0*sin(PI*MIN(0.25, fc/(fs*2))); // the fs*2 is because it's double sampled
|
||||
damp = MIN(2.0*(1.0 - pow(res, 0.25)), MIN(2.0, 2.0/freq - freq*0.5));
|
||||
notch = notch output
|
||||
low = low pass output
|
||||
high = high pass output
|
||||
band = band pass output
|
||||
peak = peaking output = low - high
|
||||
--
|
||||
double sampled svf loop:
|
||||
for (i=0; i<numSamples; i++)
|
||||
{
|
||||
in = input[i];
|
||||
notch = in - damp*band;
|
||||
low = low + freq*band;
|
||||
high = notch - low;
|
||||
band = freq*high + band - drive*band*band*band;
|
||||
out = 0.5*(notch or low or high or band or peak);
|
||||
notch = in - damp*band;
|
||||
low = low + freq*band;
|
||||
high = notch - low;
|
||||
band = freq*high + band - drive*band*band*band;
|
||||
out += 0.5*(same out as above);
|
||||
output[i] = out;
|
||||
}
|
||||
```
|
||||
|
||||
|
||||
Also that could work as it's a voltage-controlled filter
|
||||
https://www.musicdsp.org/en/latest/Filters/24-moog-vcf.html
|
||||
|
||||
```
|
||||
//Init
|
||||
cutoff = cutoff freq in Hz
|
||||
fs = sampling frequency //(e.g. 44100Hz)
|
||||
res = resonance [0 - 1] //(minimum - maximum)
|
||||
|
||||
f = 2 * cutoff / fs; //[0 - 1]
|
||||
k = 3.6*f - 1.6*f*f -1; //(Empirical tunning)
|
||||
p = (k+1)*0.5;
|
||||
scale = e^((1-p)*1.386249;
|
||||
r = res*scale;
|
||||
y4 = output;
|
||||
|
||||
y1=y2=y3=y4=oldx=oldy1=oldy2=oldy3=0;
|
||||
|
||||
//Loop
|
||||
//--Inverted feed back for corner peaking
|
||||
x = input - r*y4;
|
||||
|
||||
//Four cascaded onepole filters (bilinear transform)
|
||||
y1=x*p + oldx*p - k*y1;
|
||||
y2=y1*p+oldy1*p - k*y2;
|
||||
y3=y2*p+oldy2*p - k*y3;
|
||||
y4=y3*p+oldy3*p - k*y4;
|
||||
|
||||
//Clipper band limited sigmoid
|
||||
y4 = y4 - (y4^3)/6;
|
||||
|
||||
oldx = x;
|
||||
oldy1 = y1;
|
||||
oldy2 = y2;
|
||||
oldy3 = y3;
|
||||
```
|
||||
|
||||
|
||||
```
|
||||
#pragma once
|
||||
|
||||
namespace DistoCore
|
||||
{
|
||||
template<class T>
|
||||
class MoogFilter
|
||||
{
|
||||
public:
|
||||
MoogFilter();
|
||||
~MoogFilter() {};
|
||||
|
||||
T getSampleRate() const { return sampleRate; }
|
||||
void setSampleRate(T fs) { sampleRate = fs; calc(); }
|
||||
T getResonance() const { return resonance; }
|
||||
void setResonance(T filterRezo) { resonance = filterRezo; calc(); }
|
||||
T getCutoff() const { return cutoff; }
|
||||
T getCutoffHz() const { return cutoff * sampleRate * 0.5; }
|
||||
void setCutoff(T filterCutoff) { cutoff = filterCutoff; calc(); }
|
||||
|
||||
void init();
|
||||
void calc();
|
||||
T process(T input);
|
||||
// filter an input sample using normalized params
|
||||
T filter(T input, T cutoff, T resonance);
|
||||
|
||||
protected:
|
||||
// cutoff and resonance [0 - 1]
|
||||
T cutoff;
|
||||
T resonance;
|
||||
T sampleRate;
|
||||
T fs;
|
||||
T y1,y2,y3,y4;
|
||||
T oldx;
|
||||
T oldy1,oldy2,oldy3;
|
||||
T x;
|
||||
T r;
|
||||
T p;
|
||||
T k;
|
||||
};
|
||||
|
||||
/**
|
||||
* Construct Moog-filter.
|
||||
*/
|
||||
template<class T>
|
||||
MoogFilter<T>::MoogFilter()
|
||||
: sampleRate(T(44100.0))
|
||||
, cutoff(T(1.0))
|
||||
, resonance(T(0.0))
|
||||
{
|
||||
init();
|
||||
}
|
||||
|
||||
/**
|
||||
* Initialize filter buffers.
|
||||
*/
|
||||
template<class T>
|
||||
void MoogFilter<T>::init()
|
||||
{
|
||||
// initialize values
|
||||
y1=y2=y3=y4=oldx=oldy1=oldy2=oldy3=T(0.0);
|
||||
calc();
|
||||
}
|
||||
|
||||
/**
|
||||
* Calculate coefficients.
|
||||
*/
|
||||
template<class T>
|
||||
void MoogFilter<T>::calc()
|
||||
{
|
||||
// TODO: replace with your constant
|
||||
const double kPi = 3.1415926535897931;
|
||||
|
||||
// empirical tuning
|
||||
p = cutoff * (T(1.8) - T(0.8) * cutoff);
|
||||
// k = p + p - T(1.0);
|
||||
// A much better tuning seems to be:
|
||||
k = T(2.0) * sin(cutoff * kPi * T(0.5)) - T(1.0);
|
||||
|
||||
T t1 = (T(1.0) - p) * T(1.386249);
|
||||
T t2 = T(12.0) + t1 * t1;
|
||||
r = resonance * (t2 + T(6.0) * t1) / (t2 - T(6.0) * t1);
|
||||
};
|
||||
|
||||
/**
|
||||
* Process single sample.
|
||||
*/
|
||||
template<class T>
|
||||
T MoogFilter<T>::process(T input)
|
||||
{
|
||||
// process input
|
||||
x = input - r * y4;
|
||||
|
||||
// four cascaded one-pole filters (bilinear transform)
|
||||
y1 = x * p + oldx * p - k * y1;
|
||||
y2 = y1 * p + oldy1 * p - k * y2;
|
||||
y3 = y2 * p + oldy2 * p - k * y3;
|
||||
y4 = y3 * p + oldy3 * p - k * y4;
|
||||
|
||||
// clipper band limited sigmoid
|
||||
y4 -= (y4 * y4 * y4) / T(6.0);
|
||||
|
||||
oldx = x; oldy1 = y1; oldy2 = y2; oldy3 = y3;
|
||||
|
||||
return y4;
|
||||
}
|
||||
|
||||
/**
|
||||
* Filter single sample using specified params.
|
||||
*/
|
||||
template<class T>
|
||||
T MoogFilter<T>::filter(T input, T filterCutoff, T filterRezo)
|
||||
{
|
||||
// set params first
|
||||
cutoff = filterCutoff;
|
||||
resonance = filterRezo;
|
||||
calc();
|
||||
|
||||
return process(input);
|
||||
}
|
||||
}
|
||||
```
|
||||
BIN
docs/image-1.png
Normal file
BIN
docs/image-1.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 3.0 KiB |
BIN
docs/image.png
Normal file
BIN
docs/image.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 3.3 KiB |
19
docs/matrix.md
Normal file
19
docs/matrix.md
Normal file
@@ -0,0 +1,19 @@
|
||||
|
||||
// signal -> adsr -> filter
|
||||
// ^ ^ ^
|
||||
// | | |
|
||||
// can be modulated
|
||||
// by lfo, or adsr
|
||||
|
||||
|
||||
|
||||
у каждого из них должна быть ручка для изменения состояния
|
||||
на каждом семпле?????
|
||||
|
||||
|
||||
|
||||
багует сам алгоритм изменения частоты, либо алгоритм пересчета коэфициентов фильтра
|
||||
|
||||
|
||||
|
||||
|
||||
30
inc/ADSR.h
Normal file
30
inc/ADSR.h
Normal file
@@ -0,0 +1,30 @@
|
||||
#pragma once
|
||||
#include "IEffect.h"
|
||||
#include "Ramp.h"
|
||||
#include <cstddef>
|
||||
|
||||
class ADSR : public IEffect {
|
||||
enum ADSRState { sOff, sAttack, sDecay, sSustain, sRelease };
|
||||
|
||||
private:
|
||||
float m_attack_time;
|
||||
float m_decay_time;
|
||||
float m_sustain_level;
|
||||
float m_release_time;
|
||||
ADSRState m_state;
|
||||
Ramp* m_ramp;
|
||||
|
||||
void ProcessSample(float* sample);
|
||||
bool IsAttackElapsed();
|
||||
bool IsDecayElapsed();
|
||||
bool IsReleaseElapsed();
|
||||
void RecheckState();
|
||||
|
||||
public:
|
||||
ADSR(/* args */);
|
||||
~ADSR();
|
||||
void Trigger() override;
|
||||
void Release() override;
|
||||
void Process(std::vector<float>& samples) override;
|
||||
void SetParameters(float attack, float decay, float sustain, float release);
|
||||
};
|
||||
22
inc/Adder.h
Normal file
22
inc/Adder.h
Normal file
@@ -0,0 +1,22 @@
|
||||
#pragma once
|
||||
|
||||
#include "Oscillator.h"
|
||||
#include "Settings.h"
|
||||
#include <numeric>
|
||||
#include <vector>
|
||||
|
||||
struct Adder {
|
||||
static void SumOscillators(const std::vector<Oscillator*>& oscillators,
|
||||
std::vector<float>& signal) {
|
||||
size_t sample_count = STREAM_BUFFER_SIZE;
|
||||
|
||||
for (size_t i = 0; i < sample_count; i++) {
|
||||
float sample = 0.0f;
|
||||
for (Oscillator* osc : oscillators) {
|
||||
sample += osc->Process();
|
||||
}
|
||||
|
||||
signal[i] = sample;
|
||||
}
|
||||
}
|
||||
};
|
||||
26
inc/Application.h
Normal file
26
inc/Application.h
Normal file
@@ -0,0 +1,26 @@
|
||||
#pragma once
|
||||
#include "Note.h"
|
||||
#include "Renderer.h"
|
||||
#include "Synth.h"
|
||||
#include "SynthGuiState.h"
|
||||
#include "raylib.h"
|
||||
|
||||
class Application {
|
||||
private:
|
||||
Synth m_synth;
|
||||
SynthGuiState m_synth_gui_state;
|
||||
AudioStream m_synth_stream;
|
||||
int m_sound_played_count;
|
||||
Note* m_current_note;
|
||||
Renderer m_renderer;
|
||||
bool DetectNotePressed(Note* note);
|
||||
void InitSynth();
|
||||
void InitAudio();
|
||||
void UpdateOnNoteInput();
|
||||
void PlayBufferedAudio();
|
||||
|
||||
public:
|
||||
Application(/* args */);
|
||||
~Application();
|
||||
void Run();
|
||||
};
|
||||
72
inc/Filter.h
Normal file
72
inc/Filter.h
Normal file
@@ -0,0 +1,72 @@
|
||||
#pragma once
|
||||
#include "IEffect.h"
|
||||
#include "Settings.h"
|
||||
|
||||
enum FilterType { LowPass, BandPass, HighPass };
|
||||
|
||||
class Filter : public IEffect {
|
||||
protected:
|
||||
// float* m_output; // output buffer
|
||||
float m_fs = SAMPLE_RATE; // sampling frequency;
|
||||
float m_fc; // cutoff frequency normally something like: 440.0*pow(2.0,
|
||||
// (midi_note - 69.0)/12.0);
|
||||
float m_res; // resonance 0 to 1;
|
||||
float m_drive; // internal distortion 0 to 0.1
|
||||
float m_freq;
|
||||
float m_damp;
|
||||
float m_notcho; // notch output
|
||||
float m_lowo; // low pass output
|
||||
float m_higho; // high pass output
|
||||
float m_bando; // band pass output
|
||||
float m_peako; // peaking output = low - high
|
||||
virtual float GetSampleForFilterType(){ return 0.0; };
|
||||
|
||||
public:
|
||||
Filter(/* args */);
|
||||
virtual ~Filter();
|
||||
void Trigger() override final;
|
||||
void Release() override final;
|
||||
float Process(float in);
|
||||
void Process(std::vector<float>& samples) override final;
|
||||
void SetParameters(float freq, float res, float drive);
|
||||
float GetFreq() { return m_fc; }
|
||||
float GetRes() { return m_res; }
|
||||
float GetPeakGain() { return m_drive; }
|
||||
virtual bool IsSameFilterType(FilterType type) { return false; };
|
||||
};
|
||||
|
||||
class BandPassFilter : public Filter {
|
||||
protected:
|
||||
float GetSampleForFilterType() override;
|
||||
|
||||
public:
|
||||
BandPassFilter();
|
||||
BandPassFilter(Filter* filter);
|
||||
BandPassFilter(float freq, float res, float q);
|
||||
~BandPassFilter();
|
||||
bool IsSameFilterType(FilterType type) override { return type == BandPass; };
|
||||
};
|
||||
|
||||
class HighPassFilter : public Filter {
|
||||
protected:
|
||||
float GetSampleForFilterType() override;
|
||||
|
||||
public:
|
||||
HighPassFilter();
|
||||
HighPassFilter(Filter* filter);
|
||||
HighPassFilter(float freq, float res, float q);
|
||||
~HighPassFilter();
|
||||
bool IsSameFilterType(FilterType type) override { return type == HighPass; };
|
||||
};
|
||||
|
||||
class LowPassFilter : public Filter {
|
||||
protected:
|
||||
float GetSampleForFilterType() override;
|
||||
|
||||
public:
|
||||
LowPassFilter();
|
||||
LowPassFilter(Filter* filter);
|
||||
LowPassFilter(float freq, float res, float q);
|
||||
~LowPassFilter();
|
||||
bool IsSameFilterType(FilterType type) override { return type == LowPass; };
|
||||
};
|
||||
26
inc/FilterFactory.h
Normal file
26
inc/FilterFactory.h
Normal file
@@ -0,0 +1,26 @@
|
||||
#pragma once
|
||||
#include "Filter.h"
|
||||
|
||||
struct FilterFactory {
|
||||
static Filter* CreateFilter(Filter* old_filter, FilterType new_type) {
|
||||
Filter* new_filter;
|
||||
switch (new_type) {
|
||||
case LowPass:
|
||||
new_filter = new LowPassFilter(old_filter);
|
||||
break;
|
||||
case BandPass:
|
||||
new_filter = new BandPassFilter(old_filter);
|
||||
break;
|
||||
case HighPass:
|
||||
new_filter = new HighPassFilter(old_filter);
|
||||
break;
|
||||
default:
|
||||
break;
|
||||
}
|
||||
return new_filter;
|
||||
}
|
||||
|
||||
static Filter* GetDefaultFilter() {
|
||||
return new LowPassFilter();
|
||||
}
|
||||
};
|
||||
10
inc/IEffect.h
Normal file
10
inc/IEffect.h
Normal file
@@ -0,0 +1,10 @@
|
||||
#pragma once
|
||||
#include <vector>
|
||||
class IEffect {
|
||||
private:
|
||||
/* data */
|
||||
public:
|
||||
virtual void Trigger() = 0;
|
||||
virtual void Release() = 0;
|
||||
virtual void Process(std::vector<float>& samples) = 0;
|
||||
};
|
||||
72
inc/KeyBoard.h
Normal file
72
inc/KeyBoard.h
Normal file
@@ -0,0 +1,72 @@
|
||||
#pragma once
|
||||
#include "Settings.h"
|
||||
#include <cmath>
|
||||
#include <cstdlib>
|
||||
#include <cstring>
|
||||
#include <string>
|
||||
|
||||
class KeyBoard {
|
||||
private:
|
||||
static int GetSemitoneShiftInternal(const char* root_note,
|
||||
char* target_note) {
|
||||
const char* pitch_classes[12] = {"C", "C#", "D", "D#", "E", "F",
|
||||
"F#", "G", "G#", "A", "A#", "B"};
|
||||
|
||||
// Extract the note number and pitch class for the root note
|
||||
int root_note_num = (int)root_note[strlen(root_note) - 1] - '0';
|
||||
|
||||
char* root_pitch_class_str =
|
||||
(char*)malloc((strlen(root_note) - 1) * sizeof(char));
|
||||
strncpy(root_pitch_class_str, root_note, strlen(root_note) - 1);
|
||||
|
||||
int root_pitch_class = -1;
|
||||
|
||||
for (int i = 0; i < 12; i++) {
|
||||
if (strcmp(pitch_classes[i], root_pitch_class_str) == 0) {
|
||||
root_pitch_class = i;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
free(root_pitch_class_str);
|
||||
|
||||
// Extract the note number and pitch class for the target note
|
||||
int target_note_num = (int)target_note[strlen(target_note) - 1] - '0';
|
||||
|
||||
char* target_pitch_class_str =
|
||||
(char*)malloc((strlen(target_note) - 1) * sizeof(char));
|
||||
strncpy(target_pitch_class_str, target_note, strlen(target_note) - 1);
|
||||
|
||||
int target_pitch_class = -1;
|
||||
|
||||
for (int i = 0; i < 12; i++) {
|
||||
if (strcmp(pitch_classes[i], target_pitch_class_str) == 0) {
|
||||
target_pitch_class = i;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
free(target_pitch_class_str);
|
||||
|
||||
// Calculate the semitone shift using the formula
|
||||
return (target_note_num - root_note_num) * 12 +
|
||||
(target_pitch_class - root_pitch_class);
|
||||
}
|
||||
|
||||
public:
|
||||
|
||||
static float GetHzBySemitone(float semitone) {
|
||||
//440 * Math.Pow(2, (note - 69) / 12.0) would it be better?
|
||||
return PITCH_STANDARD * powf(powf(2.f, (1.f / 12.f)), semitone);
|
||||
}
|
||||
|
||||
static int GetSemitoneShift(const std::string& target_note) {
|
||||
char* target_note_cstr = new char[target_note.length() + 1];
|
||||
strcpy(target_note_cstr, target_note.c_str());
|
||||
|
||||
int result = GetSemitoneShiftInternal("A4", target_note_cstr);
|
||||
|
||||
delete[] target_note_cstr;
|
||||
return result;
|
||||
}
|
||||
};
|
||||
13
inc/LFO.h
Normal file
13
inc/LFO.h
Normal file
@@ -0,0 +1,13 @@
|
||||
#pragma once
|
||||
#include "Oscillator.h"
|
||||
|
||||
class LFO: public Oscillator
|
||||
{
|
||||
private:
|
||||
/* data */
|
||||
public:
|
||||
LFO(/* args */);
|
||||
~LFO();
|
||||
void SetFreq(float freq) { m_phase_dt = (this->*m_dt_function)(freq); }
|
||||
};
|
||||
|
||||
7
inc/Logger.h
Normal file
7
inc/Logger.h
Normal file
@@ -0,0 +1,7 @@
|
||||
#pragma once
|
||||
#include "cstdio"
|
||||
|
||||
#define write_log(format, args...) \
|
||||
do { \
|
||||
printf(format, ##args); \
|
||||
} while (0)
|
||||
8
inc/Note.h
Normal file
8
inc/Note.h
Normal file
@@ -0,0 +1,8 @@
|
||||
#pragma once
|
||||
|
||||
#include <string>
|
||||
|
||||
struct Note {
|
||||
std::string& name;
|
||||
int length;
|
||||
};
|
||||
45
inc/Oscillator.h
Normal file
45
inc/Oscillator.h
Normal file
@@ -0,0 +1,45 @@
|
||||
#pragma once
|
||||
#include "OscillatorType.h"
|
||||
#include <vector>
|
||||
|
||||
class Oscillator {
|
||||
private:
|
||||
OscillatorType m_osc;
|
||||
float m_fine;
|
||||
float m_key;
|
||||
float m_volume;
|
||||
float m_phase;
|
||||
float (Oscillator::*m_osc_function)(void);
|
||||
void SineOscPhaseIncr();
|
||||
void SawOscPhaseIncr();
|
||||
float CalcSawPhaseDelta(float freq);
|
||||
float CalcSinePhaseDelta(float freq);
|
||||
float SawOsc();
|
||||
float TriangleOsc();
|
||||
float SquareOsc();
|
||||
float Sign(float v);
|
||||
float SineOsc();
|
||||
|
||||
protected:
|
||||
float m_phase_dt;
|
||||
float (Oscillator::*m_dt_function)(float freq);
|
||||
|
||||
public:
|
||||
Oscillator(OscillatorType osc, float fine, float volume);
|
||||
~Oscillator();
|
||||
OscillatorType GetType() { return m_osc; }
|
||||
void SetType(OscillatorType osc);
|
||||
float GetVolume() { return m_volume; }
|
||||
void SetVolume(float volume) { m_volume = volume; }
|
||||
float GetKey() { return m_key; }
|
||||
void SetKey(float key);
|
||||
float GetFine() { return m_fine; }
|
||||
void SetFine(float fine) {
|
||||
if (fine != m_fine) {
|
||||
assert(fine >= -2.f && fine <= 2.f);
|
||||
m_fine = fine;
|
||||
}
|
||||
}
|
||||
void Reset();
|
||||
float Process();
|
||||
};
|
||||
2
inc/OscillatorType.h
Normal file
2
inc/OscillatorType.h
Normal file
@@ -0,0 +1,2 @@
|
||||
#pragma once
|
||||
typedef enum { Sine, Triangle, Saw, Square } OscillatorType;
|
||||
16
inc/Ramp.h
Normal file
16
inc/Ramp.h
Normal file
@@ -0,0 +1,16 @@
|
||||
#pragma once
|
||||
|
||||
class Ramp {
|
||||
private:
|
||||
float m_level;
|
||||
float m_sample_rate;
|
||||
float m_increment;
|
||||
int m_counter;
|
||||
|
||||
public:
|
||||
Ramp(float starting_level, float sample_rate);
|
||||
~Ramp();
|
||||
void RampTo(float value, float time);
|
||||
float Process();
|
||||
bool IsCompleted();
|
||||
};
|
||||
31
inc/Renderer.h
Normal file
31
inc/Renderer.h
Normal file
@@ -0,0 +1,31 @@
|
||||
#pragma once
|
||||
#include "ADSR.h"
|
||||
#include "Filter.h"
|
||||
#include "Synth.h"
|
||||
#include "SynthGuiState.h"
|
||||
#include "raylib.h"
|
||||
#include <vector>
|
||||
|
||||
class Renderer {
|
||||
private:
|
||||
void draw_main_panel(const Rectangle& panel_bounds);
|
||||
float draw_oscillators_panels(
|
||||
const std::vector<Oscillator*>& oscillators,
|
||||
const std::vector<OscillatorGuiState*>& gui_oscillators,
|
||||
const Rectangle& panel_bounds);
|
||||
void draw_oscillators_shape_inputs(
|
||||
const std::vector<Oscillator*>& oscillators,
|
||||
const std::vector<OscillatorGuiState*>& guiOscillators);
|
||||
void draw_ui(Synth& synth, SynthGuiState& synth_gui);
|
||||
void draw_signal(Synth& synth, SynthGuiState& synth_gui);
|
||||
void draw_adsr_panel(ADSR* adsr, ADSRGuiState& gui_adsr,
|
||||
const Rectangle& panel_bounds, float panel_y_offset);
|
||||
void draw_second_panel(Rectangle& bounds);
|
||||
float DrawFilterPanel(Synth& synth, FilterGuiState& gui_filter,
|
||||
const Rectangle& panel_bounds);
|
||||
|
||||
public:
|
||||
Renderer(/* args */);
|
||||
~Renderer();
|
||||
void Draw(Synth& synth, SynthGuiState& synth_gui);
|
||||
};
|
||||
115
inc/RingBuffer.h
Normal file
115
inc/RingBuffer.h
Normal file
@@ -0,0 +1,115 @@
|
||||
#pragma once
|
||||
#include "Logger.h"
|
||||
#include <cstddef>
|
||||
template <typename T> class RingBuffer {
|
||||
private:
|
||||
T* m_items; /* data */
|
||||
std::size_t m_head;
|
||||
std::size_t m_tail;
|
||||
bool m_is_full;
|
||||
bool m_is_empty;
|
||||
std::size_t m_size;
|
||||
void advance_pointer();
|
||||
void retreat_pointer();
|
||||
|
||||
public:
|
||||
RingBuffer(std::size_t size);
|
||||
~RingBuffer();
|
||||
bool IsFull() { return m_is_full; }
|
||||
bool IsEmpty() { return m_is_empty; }
|
||||
std::size_t GetSize();
|
||||
std::size_t GetCapacity() { return m_size; }
|
||||
void Reset();
|
||||
void Write(T* data, size_t count);
|
||||
bool Read(T* output, size_t count);
|
||||
void Print();
|
||||
};
|
||||
|
||||
template <typename T> RingBuffer<T>::RingBuffer(std::size_t size) {
|
||||
m_items = new T[size];
|
||||
m_head = 0;
|
||||
m_tail = 0;
|
||||
m_is_full = 0;
|
||||
m_is_empty = 1;
|
||||
m_size = size;
|
||||
}
|
||||
|
||||
template <typename T> RingBuffer<T>::~RingBuffer() { delete[] m_items; }
|
||||
|
||||
template <typename T> void RingBuffer<T>::Reset() {
|
||||
m_head = 0;
|
||||
m_tail = 0;
|
||||
m_is_full = 0;
|
||||
}
|
||||
|
||||
template <typename T> void RingBuffer<T>::advance_pointer() {
|
||||
if (m_is_full) {
|
||||
m_tail++;
|
||||
if (m_tail == m_size) {
|
||||
m_tail = 0;
|
||||
}
|
||||
}
|
||||
m_head++;
|
||||
if (m_head == m_size) {
|
||||
m_head = 0;
|
||||
}
|
||||
std::size_t p_is_full = m_head == m_tail ? 1 : 0;
|
||||
m_is_full = p_is_full;
|
||||
}
|
||||
|
||||
template <typename T> void RingBuffer<T>::retreat_pointer() {
|
||||
m_is_full = 0;
|
||||
m_tail++;
|
||||
if (m_tail == m_size) {
|
||||
m_tail = 0;
|
||||
}
|
||||
}
|
||||
|
||||
template <typename T> void RingBuffer<T>::Write(T* data, std::size_t count) {
|
||||
if (m_is_full || m_head + count > m_size) {
|
||||
write_log("[WARN] Trying to overfill the ring buffer: "
|
||||
"\n\tIsFull:%d\n\tHead:%zu\n\tCount:%zu\n\t",
|
||||
m_is_full, m_head, count);
|
||||
return;
|
||||
}
|
||||
m_is_empty = 0;
|
||||
|
||||
for (std::size_t i = 0; i < count; i++) {
|
||||
m_items[m_head] = data[i];
|
||||
advance_pointer();
|
||||
}
|
||||
// m_is_empty = m_is_full && (m_head == m_tail);
|
||||
}
|
||||
|
||||
template <typename T> bool RingBuffer<T>::Read(T* output, std::size_t count) {
|
||||
if (m_is_empty) {
|
||||
write_log("[WARN] Trying to read empty buffer");
|
||||
return 0;
|
||||
}
|
||||
|
||||
for (std::size_t i = 0; i < count; i++) {
|
||||
output[i] = m_items[m_tail];
|
||||
retreat_pointer();
|
||||
}
|
||||
m_is_empty = !m_is_full && (m_head == m_tail);
|
||||
return 1;
|
||||
}
|
||||
|
||||
template <typename T> std::size_t RingBuffer<T>::GetSize() {
|
||||
size_t p_size = m_size;
|
||||
if (!m_is_full) {
|
||||
if (m_head >= m_tail) {
|
||||
p_size = (m_head - m_tail);
|
||||
} else {
|
||||
p_size = (m_size + m_head - m_tail);
|
||||
}
|
||||
}
|
||||
|
||||
return p_size;
|
||||
}
|
||||
|
||||
template <typename T> void RingBuffer<T>::Print() {
|
||||
write_log("[INFO] The ring buffer: "
|
||||
"\n\tIsFull:%d\n\tIsEmpty:%d\n\tHead:%zu\n\tTail:%zu\n\t",
|
||||
m_is_full, m_is_empty, m_head, m_tail);
|
||||
}
|
||||
17
inc/Settings.h
Normal file
17
inc/Settings.h
Normal file
@@ -0,0 +1,17 @@
|
||||
#pragma once
|
||||
|
||||
#define SAMPLE_RATE 44100.f
|
||||
#define BPM 120.f
|
||||
#define BEAT_DURATION 60.f / BPM
|
||||
#define PITCH_STANDARD 440.f
|
||||
#define VOLUME 0.5f
|
||||
#define ATTACK_MS 100.f
|
||||
#define STREAM_BUFFER_SIZE 1024
|
||||
#define FPS 60
|
||||
|
||||
#define SYNTH_PI 3.1415926535f
|
||||
#define SYNTH_VOLUME 0.5f
|
||||
|
||||
#define WINDOW_WIDTH 640
|
||||
#define WINDOW_HEIGHT 480
|
||||
#define OSCILLATOR_PANEL_WIDTH 200
|
||||
40
inc/Synth.h
Normal file
40
inc/Synth.h
Normal file
@@ -0,0 +1,40 @@
|
||||
#pragma once
|
||||
|
||||
#include "ADSR.h"
|
||||
#include "Filter.h"
|
||||
#include "Adder.h"
|
||||
#include "IEffect.h"
|
||||
#include "Note.h"
|
||||
#include "Oscillator.h"
|
||||
#include "Settings.h"
|
||||
#include <vector>
|
||||
#include "LFO.h"
|
||||
|
||||
class Synth {
|
||||
private:
|
||||
bool is_note_triggered;
|
||||
std::vector<Oscillator*> m_oscillators;
|
||||
std::vector<IEffect*> m_effects;
|
||||
std::vector<float> m_out_signal;
|
||||
LFO* m_lfo;
|
||||
void ZeroSignal();
|
||||
void GetNote();
|
||||
void TriggerNoteOnEffects();
|
||||
void UntriggerNoteOnEffects();
|
||||
void ApplyEffects();
|
||||
void AddOscillator();
|
||||
void ApplyFilterLfo();
|
||||
public:
|
||||
Synth(/* args */);
|
||||
~Synth();
|
||||
void Trigger(Note input);
|
||||
void Process();
|
||||
void Release();
|
||||
void AddEffect(IEffect* fx);
|
||||
const std::vector<float>& GetOutSignal() { return m_out_signal; }
|
||||
const std::vector<Oscillator*>& GetOscillators() { return m_oscillators; }
|
||||
const bool& GetIsNoteTriggered() { return is_note_triggered; }
|
||||
ADSR* GetADSR() { return (ADSR*)m_effects[0]; }
|
||||
Filter* GetFilter() { return (Filter*)m_effects[1]; }
|
||||
void SetFilter(FilterType type);
|
||||
};
|
||||
33
inc/SynthGuiState.h
Normal file
33
inc/SynthGuiState.h
Normal file
@@ -0,0 +1,33 @@
|
||||
#pragma once
|
||||
#include "OscillatorType.h"
|
||||
#include "raygui.h"
|
||||
#include <vector>
|
||||
#include "Filter.h"
|
||||
|
||||
struct OscillatorGuiState {
|
||||
float volume;
|
||||
float fine;
|
||||
OscillatorType waveshape;
|
||||
bool is_dropdown_open;
|
||||
Rectangle shape_dropdown_rect;
|
||||
};
|
||||
|
||||
struct ADSRGuiState {
|
||||
float attack;
|
||||
float decay;
|
||||
float sustain;
|
||||
float release;
|
||||
};
|
||||
|
||||
struct FilterGuiState {
|
||||
float freq;
|
||||
float res; //todo: res
|
||||
FilterType type;
|
||||
bool is_dropdown_open;
|
||||
};
|
||||
|
||||
struct SynthGuiState {
|
||||
std::vector<OscillatorGuiState*> oscillators;
|
||||
ADSRGuiState adsr;
|
||||
FilterGuiState filter;
|
||||
};
|
||||
4824
inc/raygui.h
Normal file
4824
inc/raygui.h
Normal file
File diff suppressed because it is too large
Load Diff
402
main.c
402
main.c
@@ -1,402 +0,0 @@
|
||||
#include "stdlib.h"
|
||||
#include "stdio.h"
|
||||
#include "string.h"
|
||||
#include "math.h"
|
||||
#include "parser.h"
|
||||
|
||||
#define SAMPLE_RATE 48000.f
|
||||
#define BPM 120.f
|
||||
#define BEAT_DURATION 60.f/BPM
|
||||
#define PITCH_STANDARD 440.f
|
||||
#define VOLUME 0.5f
|
||||
#define ATTACK_MS 100.f
|
||||
|
||||
#define PI 3.1415926535f
|
||||
|
||||
//------------------------------------------------------------------------------------
|
||||
// General Sound
|
||||
//------------------------------------------------------------------------------------
|
||||
|
||||
typedef struct Sound {
|
||||
float* samples;
|
||||
size_t sample_count;
|
||||
} Sound;
|
||||
|
||||
// frees the original sounds
|
||||
Sound concat_sounds(Sound* sounds, size_t count) {
|
||||
size_t total_count = 0;
|
||||
for (size_t i = 0; i < count; i++) {
|
||||
total_count += sounds[i].sample_count;
|
||||
}
|
||||
// array to hold the result
|
||||
float* total = malloc(total_count * sizeof(float));
|
||||
|
||||
size_t current_count = 0;
|
||||
for (size_t i = 0; i < count; i++) {
|
||||
memcpy(total + current_count,
|
||||
sounds[i].samples,
|
||||
sounds[i].sample_count * sizeof(float));
|
||||
current_count += sounds[i].sample_count;
|
||||
|
||||
free(sounds[i].samples);
|
||||
}
|
||||
|
||||
Sound result = {
|
||||
.samples = total,
|
||||
.sample_count = total_count
|
||||
};
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
//------------------------------------------------------------------------------------
|
||||
// Oscillator
|
||||
//------------------------------------------------------------------------------------
|
||||
|
||||
typedef enum {
|
||||
Sine,
|
||||
Triangle,
|
||||
Saw,
|
||||
Square
|
||||
} OscillatorType;
|
||||
|
||||
typedef struct OscillatorParameter {
|
||||
OscillatorType osc;
|
||||
float freq;
|
||||
} OscillatorParameter;
|
||||
|
||||
typedef struct OscillatorParameterList {
|
||||
OscillatorParameter* array;
|
||||
size_t count;
|
||||
} OscillatorParameterList;
|
||||
|
||||
typedef struct OscillatorGenerationParameter {
|
||||
OscillatorParameterList oscillators;
|
||||
float sample;
|
||||
} OscillatorGenerationParameter;
|
||||
|
||||
static Sound get_init_samples(float duration) {
|
||||
size_t sample_count = (size_t)(duration * SAMPLE_RATE);
|
||||
float* samples = malloc(sizeof(float) * sample_count);
|
||||
|
||||
for (double i = 0.0; i < duration * SAMPLE_RATE; i++) {
|
||||
samples[(int)i] = i;
|
||||
}
|
||||
|
||||
Sound res = {
|
||||
.samples = samples,
|
||||
.sample_count = sample_count
|
||||
};
|
||||
|
||||
return res;
|
||||
}
|
||||
|
||||
static float pos(float hz, float x) {
|
||||
return fmodf(hz * x / SAMPLE_RATE, 1);
|
||||
}
|
||||
|
||||
float sineosc(float hz, float x) {
|
||||
return sinf(x * (2.f * PI * hz / SAMPLE_RATE));
|
||||
}
|
||||
|
||||
static float sign(float v) {
|
||||
return (v > 0.0) ? 1.f : -1.f;
|
||||
}
|
||||
|
||||
float squareosc(float hz, float x) {
|
||||
return sign(sineosc(hz, x));
|
||||
}
|
||||
|
||||
float triangleosc(float hz, float x) {
|
||||
return 1.f - fabsf(pos(hz, x) - 0.5f) * 4.f;
|
||||
}
|
||||
|
||||
float sawosc(float hz, float x) {
|
||||
return pos(hz, x) * 2.f - 1.f;
|
||||
}
|
||||
|
||||
float multiosc(OscillatorGenerationParameter param) {
|
||||
float osc_sample = 0.f;
|
||||
for (size_t i = 0; i < param.oscillators.count; i++) {
|
||||
OscillatorParameter osc = param.oscillators.array[i];
|
||||
switch (osc.osc) {
|
||||
case Sine:
|
||||
osc_sample += sineosc(osc.freq, param.sample);
|
||||
break;
|
||||
case Triangle:
|
||||
osc_sample += triangleosc(osc.freq, param.sample);
|
||||
break;
|
||||
case Square:
|
||||
osc_sample += squareosc(osc.freq, param.sample);
|
||||
break;
|
||||
case Saw:
|
||||
osc_sample += sawosc(osc.freq, param.sample);
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
return osc_sample;
|
||||
}
|
||||
|
||||
static Sound freq(float duration, OscillatorParameterList osc) {
|
||||
Sound samples = get_init_samples(duration);
|
||||
// Sound attack = get_attack_samples();
|
||||
|
||||
float* output = malloc(sizeof(float) * samples.sample_count);
|
||||
for (int i = 0; i < samples.sample_count; i++) {
|
||||
float sample = samples.samples[i];
|
||||
OscillatorGenerationParameter param = {
|
||||
.oscillators = osc,
|
||||
.sample = sample
|
||||
};
|
||||
output[i] = multiosc(param) * VOLUME;
|
||||
}
|
||||
|
||||
// create attack and release
|
||||
/*
|
||||
let adsrLength = Seq.length output
|
||||
let attackArray = attack |> Seq.take adsrLength
|
||||
let release = Seq.rev attackArray
|
||||
*/
|
||||
/*
|
||||
todo: I will change the ADSR approach to an explicit ADSR module(with it's own state)
|
||||
size_t adsr_length = samples.sample_count;
|
||||
float *attackArray = NULL, *releaseArray = NULL;
|
||||
|
||||
if (adsr_length > 0) {
|
||||
//todo: calloc
|
||||
attackArray = malloc(sizeof(float) * adsr_length);
|
||||
size_t attack_length = attack.sample_count < adsr_length
|
||||
? attack.sample_count
|
||||
: adsr_length;
|
||||
|
||||
memcpy(attackArray, attack.samples, attack_length);
|
||||
//todo: calloc
|
||||
releaseArray = malloc(sizeof(float) * adsr_length);
|
||||
memcpy(releaseArray, attackArray, attack_length);
|
||||
reverse_array(releaseArray, 0, adsr_length);
|
||||
}
|
||||
*/
|
||||
|
||||
// return zipped array
|
||||
Sound res = {
|
||||
.samples = output,
|
||||
.sample_count = samples.sample_count
|
||||
};
|
||||
|
||||
return res;
|
||||
}
|
||||
|
||||
/*
|
||||
static Sound get_attack_samples() {
|
||||
float attack_time = 0.001 * ATTACK_MS;
|
||||
size_t sample_count = (size_t)(attack_time * SAMPLE_RATE);
|
||||
float* attack = malloc(sizeof(float) * sample_count);
|
||||
float samples_to_rise = SAMPLE_RATE * attack_time;
|
||||
float rising_delta = 1.0 / samples_to_rise;
|
||||
float i = 0.0;
|
||||
|
||||
for (int j = 0; j < sample_count; j++) {
|
||||
i += rising_delta;
|
||||
attack[j] = fmin(i, 1.0);
|
||||
}
|
||||
|
||||
Sound res = {
|
||||
.samples = attack,
|
||||
.sample_count = sample_count
|
||||
};
|
||||
|
||||
return res;
|
||||
}
|
||||
*/
|
||||
|
||||
//------------------------------------------------------------------------------------
|
||||
// Synth
|
||||
//------------------------------------------------------------------------------------
|
||||
|
||||
static size_t get_semitone_shift_internal(char* root_note, char* target_note) {
|
||||
char* pitch_classes[12] =
|
||||
{ "C", "C#", "D", "D#", "E", "F", "F#", "G", "G#", "A", "A#", "B" };
|
||||
|
||||
// Extract the note number and pitch class for the root note
|
||||
int root_note_num = (int)root_note[strlen(root_note) - 1] - '0';
|
||||
|
||||
char* root_pitch_class_str = malloc((strlen(root_note) - 1) * sizeof(char));
|
||||
strncpy(root_pitch_class_str, root_note, strlen(root_note) - 1);
|
||||
|
||||
int root_pitch_class = -1;
|
||||
|
||||
for (int i = 0; i < 12; i++) {
|
||||
if (strcmp(pitch_classes[i], root_pitch_class_str) == 0) {
|
||||
root_pitch_class = i;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
free(root_pitch_class_str);
|
||||
|
||||
// Extract the note number and pitch class for the target note
|
||||
int target_note_num = (int)target_note[strlen(target_note) - 1] - '0';
|
||||
|
||||
char* target_pitch_class_str =
|
||||
malloc((strlen(target_note) - 1) * sizeof(char));
|
||||
strncpy(target_pitch_class_str, target_note, strlen(target_note) - 1);
|
||||
|
||||
int target_pitch_class = -1;
|
||||
|
||||
for (int i = 0; i < 12; i++) {
|
||||
if (strcmp(pitch_classes[i], target_pitch_class_str) == 0) {
|
||||
target_pitch_class = i;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
free(target_pitch_class_str);
|
||||
|
||||
// Calculate the semitone shift using the formula
|
||||
return (target_note_num - root_note_num) * 12 +
|
||||
(target_pitch_class - root_pitch_class);
|
||||
}
|
||||
|
||||
static float get_hz_by_semitone(size_t semitone) {
|
||||
return PITCH_STANDARD * powf(powf(2.f, (1.f / 12.f)), semitone);
|
||||
}
|
||||
|
||||
size_t get_semitone_shift(char* target_note) {
|
||||
return get_semitone_shift_internal("A4", target_note);
|
||||
}
|
||||
|
||||
Sound note(size_t semitone, float beats) {
|
||||
float hz = get_hz_by_semitone(semitone);
|
||||
float duration = beats * BEAT_DURATION;
|
||||
|
||||
OscillatorParameter first = {
|
||||
.osc = Saw,
|
||||
.freq = hz/4.f
|
||||
};
|
||||
|
||||
OscillatorParameter second = {
|
||||
.osc = Saw,
|
||||
.freq = hz + 0.5
|
||||
};
|
||||
|
||||
OscillatorParameter third = {
|
||||
.osc = Saw,
|
||||
.freq = hz - 1.f
|
||||
};
|
||||
|
||||
OscillatorParameter oscArray[] = { first, second, third };
|
||||
OscillatorParameterList parameters = {
|
||||
.array = oscArray,
|
||||
.count = 3
|
||||
};
|
||||
|
||||
return freq(duration, parameters);
|
||||
}
|
||||
|
||||
Sound get_note_sound(Note input) {
|
||||
float length = 1.f / input.length;
|
||||
size_t semitone_shift = get_semitone_shift(input.name);
|
||||
return note(semitone_shift, length);
|
||||
}
|
||||
|
||||
|
||||
//------------------------------------------------------------------------------------
|
||||
// Wav File
|
||||
//------------------------------------------------------------------------------------
|
||||
|
||||
static uint16_t toInt16Sample(float sample) {
|
||||
return (uint16_t)(sample * 32767.f);
|
||||
}
|
||||
|
||||
static void write_file(char* filename, void* data, int size) {
|
||||
FILE* fp = fopen(filename, "wb"); // open file for writing in binary mode
|
||||
if (fp == NULL) {
|
||||
fprintf(stderr, "Cannot open file: %s\n", filename);
|
||||
exit(1);
|
||||
}
|
||||
|
||||
fwrite(data, size, 1, fp); // write data to file
|
||||
|
||||
fclose(fp); // close file
|
||||
}
|
||||
|
||||
void pack(uint16_t* d, size_t length) {
|
||||
size_t dataLength = length * 2;
|
||||
|
||||
int bytesPerSample = 2;
|
||||
int byteRate = SAMPLE_RATE * bytesPerSample;
|
||||
|
||||
size_t fileSize = 36 + dataLength;
|
||||
|
||||
uint8_t* buffer = (uint8_t*)malloc(fileSize);
|
||||
|
||||
int i = 0;
|
||||
|
||||
// RIFF header
|
||||
memcpy(buffer + i, "RIFF", 4);
|
||||
i += 4;
|
||||
memcpy(buffer + i, &fileSize, 4);
|
||||
i += 4;
|
||||
memcpy(buffer + i, "WAVE", 4);
|
||||
i += 4;
|
||||
|
||||
// fmt subchunk
|
||||
memcpy(buffer + i, "fmt ", 4);
|
||||
i += 4;
|
||||
int fmtSize = 16;
|
||||
memcpy(buffer + i, &fmtSize, 4);
|
||||
i += 4;
|
||||
uint16_t audioFormat = 1;
|
||||
memcpy(buffer + i, &audioFormat, 2);
|
||||
i += 2;
|
||||
uint16_t numChannels = 1;
|
||||
memcpy(buffer + i, &numChannels, 2);
|
||||
i += 2;
|
||||
int sampleRate = (int)SAMPLE_RATE;
|
||||
memcpy(buffer + i, &sampleRate, 4);
|
||||
i += 4;
|
||||
memcpy(buffer + i, &byteRate, 4);
|
||||
i += 4;
|
||||
memcpy(buffer + i, &bytesPerSample, 2);
|
||||
i += 2;
|
||||
int bitsPerSample = bytesPerSample * 8;
|
||||
memcpy(buffer + i, &bitsPerSample, 2);
|
||||
i += 2;
|
||||
|
||||
// data subchunk
|
||||
memcpy(buffer + i, "data", 4);
|
||||
i += 4;
|
||||
memcpy(buffer + i, &dataLength, 4);
|
||||
i += 4;
|
||||
memcpy(buffer + i, d, dataLength);
|
||||
|
||||
write_file("output.wav", buffer, fileSize);
|
||||
}
|
||||
|
||||
//------------------------------------------------------------------------------------
|
||||
// Main
|
||||
//------------------------------------------------------------------------------------
|
||||
|
||||
int main(int argc, char **argv) {
|
||||
char* input = "A4-4 A4-4 A4-4 A4-4 A4-2 A4-4 A4-4 A4-4 A4-4 A4-4 A4-2 D5-4 D5-4 D5-4 D5-4 D5-4 D5-4 D5-2 C5-4 C5-4 C5-4 C5-4 C5-4 C5-4 C5-2 G4-2 ";
|
||||
char* buf = malloc(strlen(input) + 1);
|
||||
strcpy(buf, input);
|
||||
|
||||
NoteArray note_array = parse_notes(buf, strlen(buf));
|
||||
Sound* sounds = malloc(sizeof(Sound) * note_array.count);
|
||||
for (size_t i = 0; i < note_array.count; i++) {
|
||||
Note note = note_array.notes[i];
|
||||
sounds[i] = get_note_sound(note);
|
||||
}
|
||||
|
||||
Sound song = concat_sounds(sounds, note_array.count);
|
||||
uint16_t* song_pcm = malloc(sizeof(uint16_t) * song.sample_count);
|
||||
for (size_t i = 0; i < song.sample_count; i++) {
|
||||
song_pcm[i] = toInt16Sample(song.samples[i]);
|
||||
}
|
||||
|
||||
pack(song_pcm, song.sample_count);
|
||||
|
||||
return 0;
|
||||
}
|
||||
94
parser.c
94
parser.c
@@ -1,94 +0,0 @@
|
||||
#include "parser.h"
|
||||
#include "string.h"
|
||||
#include "stdio.h"
|
||||
|
||||
struct StringArray {
|
||||
char** array;
|
||||
size_t count;
|
||||
};
|
||||
|
||||
static void trim(char* str) {
|
||||
size_t len = strlen(str);
|
||||
while (len > 0 && (str[len - 1] == '\n' || str[len - 1] == ' ')) {
|
||||
str[--len] = '\0';
|
||||
}
|
||||
}
|
||||
|
||||
static struct StringArray parse_note_parts(char* input) {
|
||||
size_t count = 0;
|
||||
size_t i = 0;
|
||||
while (input[i] != '\0') {
|
||||
if (input[i] == ' ')
|
||||
count++;
|
||||
|
||||
i++;
|
||||
}
|
||||
|
||||
char** array = malloc(sizeof(char*) * count);
|
||||
|
||||
char* sep = " ";
|
||||
char* line = strtok(input, sep);
|
||||
i = 0;
|
||||
while (line != NULL) {
|
||||
array[i] = strdup(line);
|
||||
line = strtok(NULL, sep);
|
||||
i++;
|
||||
}
|
||||
|
||||
struct StringArray result = {
|
||||
.array = array,
|
||||
.count = count
|
||||
};
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
NoteArray parse_notes(char* input, size_t len) {
|
||||
struct StringArray note_strings = parse_note_parts(input);
|
||||
|
||||
NoteArray notes;
|
||||
notes.count = note_strings.count;
|
||||
|
||||
char* end;
|
||||
for (size_t i = 0; i < note_strings.count; i++) {
|
||||
char* line = note_strings.array[i];
|
||||
trim(line);
|
||||
|
||||
char* note_name = strtok(line, "-");
|
||||
char* note_length_str = strtok(NULL, "-");
|
||||
|
||||
int note_length = strtol(note_length_str, &end, 10);
|
||||
if (*end != '\0') {
|
||||
fprintf(stderr,
|
||||
"Failed to parse note length: %s\n", note_length_str);
|
||||
return notes;
|
||||
}
|
||||
|
||||
char* buf = malloc(strlen(note_name) + 1);
|
||||
strcpy(buf, note_name);
|
||||
|
||||
Note note = {
|
||||
.length = note_length,
|
||||
.name = buf
|
||||
};
|
||||
|
||||
notes.notes[i] = note;
|
||||
}
|
||||
|
||||
return notes;
|
||||
}
|
||||
|
||||
/*
|
||||
static int test(int argc, char **argv) {
|
||||
char* input = "A4-4 A4-2 C5-8 C5-4 ";
|
||||
char* buf = malloc(strlen(input) + 1);
|
||||
strcpy(buf, input);
|
||||
|
||||
NoteArray note_array = parse_notes(buf, strlen(buf));
|
||||
for (size_t i = 0; i < note_array.count; i++) {
|
||||
Note note = note_array.notes[i];
|
||||
}
|
||||
|
||||
return 0;
|
||||
}
|
||||
*/
|
||||
20
parser.h
20
parser.h
@@ -1,20 +0,0 @@
|
||||
#ifndef PARSER_H
|
||||
#define PARSER_H
|
||||
|
||||
#include "stdlib.h"
|
||||
|
||||
#define MAX_NOTES 1024
|
||||
|
||||
typedef struct Note {
|
||||
char* name;
|
||||
int length;
|
||||
} Note;
|
||||
|
||||
typedef struct NoteArray {
|
||||
Note notes[MAX_NOTES];
|
||||
size_t count;
|
||||
} NoteArray;
|
||||
|
||||
NoteArray parse_notes(char* input, size_t len);
|
||||
|
||||
#endif
|
||||
94
src/ADSR.cpp
Normal file
94
src/ADSR.cpp
Normal file
@@ -0,0 +1,94 @@
|
||||
#include "ADSR.h"
|
||||
#include "Logger.h"
|
||||
#include "Settings.h"
|
||||
|
||||
ADSR::ADSR(/* args */) {
|
||||
m_attack_time = 1.f;
|
||||
m_decay_time = 0.4f;
|
||||
m_sustain_level = 0.6f;
|
||||
m_release_time = 0.8f;
|
||||
m_ramp = new Ramp(0, SAMPLE_RATE);
|
||||
}
|
||||
|
||||
ADSR::~ADSR() { delete m_ramp; }
|
||||
|
||||
bool ADSR::IsAttackElapsed() {
|
||||
return m_state == sAttack && m_ramp->IsCompleted();
|
||||
}
|
||||
|
||||
bool ADSR::IsDecayElapsed() {
|
||||
return m_state == sDecay && m_ramp->IsCompleted();
|
||||
}
|
||||
|
||||
bool ADSR::IsReleaseElapsed() {
|
||||
return m_state == sRelease && m_ramp->IsCompleted();
|
||||
}
|
||||
|
||||
void ADSR::RecheckState() {
|
||||
switch (m_state) {
|
||||
case sAttack:
|
||||
if (IsAttackElapsed()) {
|
||||
m_state = sDecay;
|
||||
m_ramp->RampTo(m_sustain_level, m_decay_time);
|
||||
}
|
||||
break;
|
||||
case sDecay:
|
||||
if (IsDecayElapsed()) {
|
||||
m_state = sSustain;
|
||||
}
|
||||
break;
|
||||
case sRelease:
|
||||
if (IsReleaseElapsed()) {
|
||||
m_state = sOff;
|
||||
}
|
||||
break;
|
||||
default:
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
void ADSR::ProcessSample(float* sample) {
|
||||
if (m_state == sOff) {
|
||||
(*sample) = 0;
|
||||
} else if (m_state == sAttack) {
|
||||
(*sample) = (*sample) * m_ramp->Process();
|
||||
} else if (m_state == sDecay) {
|
||||
(*sample) = (*sample) * m_ramp->Process();
|
||||
} else if (m_state == sSustain) {
|
||||
(*sample) = (*sample) * m_sustain_level;
|
||||
} else if (m_state == sRelease) {
|
||||
(*sample) = (*sample) * m_ramp->Process();
|
||||
}
|
||||
}
|
||||
|
||||
void ADSR::Trigger() {
|
||||
write_log("Set ADSR\n");
|
||||
if (m_state == sOff) {
|
||||
m_state = sAttack;
|
||||
} else if (m_state == sRelease) {
|
||||
m_state = sAttack;
|
||||
};
|
||||
|
||||
m_ramp->RampTo(1, m_attack_time);
|
||||
}
|
||||
|
||||
void ADSR::Release() {
|
||||
write_log("Unset ADSR\n");
|
||||
m_state = sRelease;
|
||||
m_ramp->RampTo(0, m_release_time);
|
||||
}
|
||||
|
||||
void ADSR::Process(std::vector<float>& samples) {
|
||||
for (std::size_t i = 0; i < samples.size(); i++) {
|
||||
RecheckState();
|
||||
ProcessSample(&samples[i]);
|
||||
}
|
||||
}
|
||||
|
||||
void ADSR::SetParameters(float attack, float decay, float sustain,
|
||||
float release) {
|
||||
m_attack_time = attack;
|
||||
m_decay_time = decay;
|
||||
m_sustain_level = sustain;
|
||||
m_release_time = release;
|
||||
}
|
||||
120
src/Application.cpp
Normal file
120
src/Application.cpp
Normal file
@@ -0,0 +1,120 @@
|
||||
#include "Application.h"
|
||||
#include "Logger.h"
|
||||
#include "Settings.h"
|
||||
#include <string>
|
||||
|
||||
Application::Application(/* args */) {
|
||||
InitSynth();
|
||||
InitAudio();
|
||||
}
|
||||
|
||||
Application::~Application() {
|
||||
StopAudioStream(m_synth_stream);
|
||||
UnloadAudioStream(m_synth_stream);
|
||||
CloseAudioDevice();
|
||||
CloseWindow();
|
||||
// todo: move to gui state class destructor (make it a class)
|
||||
for (int i = 0; i < m_synth_gui_state.oscillators.size(); i++) {
|
||||
delete m_synth_gui_state.oscillators[i];
|
||||
}
|
||||
}
|
||||
|
||||
void Application::InitAudio() {
|
||||
m_sound_played_count = 0;
|
||||
|
||||
InitAudioDevice();
|
||||
SetMasterVolume(SYNTH_VOLUME);
|
||||
SetAudioStreamBufferSizeDefault(STREAM_BUFFER_SIZE);
|
||||
m_synth_stream = LoadAudioStream(SAMPLE_RATE, sizeof(float) * 8, 1);
|
||||
SetAudioStreamVolume(m_synth_stream, 0.5f);
|
||||
|
||||
PlayAudioStream(m_synth_stream);
|
||||
}
|
||||
|
||||
void Application::InitSynth() {
|
||||
std::string* nameString = new std::string(std::string(new char[3]));
|
||||
m_current_note = new Note{.length = 1, .name = (*nameString)};
|
||||
m_current_note->name.assign("G4");
|
||||
|
||||
std::vector<Oscillator*> oscillators = m_synth.GetOscillators();
|
||||
m_synth_gui_state.oscillators.reserve(oscillators.size());
|
||||
for (size_t i = 0; i < oscillators.size(); i++) {
|
||||
Oscillator* osc = oscillators[i];
|
||||
assert(osc);
|
||||
|
||||
OscillatorGuiState* ui =
|
||||
new OscillatorGuiState{.fine = osc->GetFine(),
|
||||
.waveshape = osc->GetType(),
|
||||
.volume = osc->GetVolume()};
|
||||
m_synth_gui_state.oscillators.push_back(ui);
|
||||
}
|
||||
}
|
||||
|
||||
bool Application::DetectNotePressed(Note* note) {
|
||||
std::size_t is_pressed = 0;
|
||||
note->length = 8;
|
||||
if (IsKeyDown(KEY_A)) {
|
||||
note->name.assign("A2");
|
||||
is_pressed = 1;
|
||||
}
|
||||
if (IsKeyDown(KEY_B)) {
|
||||
note->name.assign("B2");
|
||||
is_pressed = 1;
|
||||
}
|
||||
if (IsKeyDown(KEY_C)) {
|
||||
note->name.assign("C2");
|
||||
is_pressed = 1;
|
||||
}
|
||||
if (IsKeyDown(KEY_D)) {
|
||||
note->name.assign("D2");
|
||||
is_pressed = 1;
|
||||
}
|
||||
if (IsKeyDown(KEY_E)) {
|
||||
note->name.assign("E2");
|
||||
is_pressed = 1;
|
||||
}
|
||||
if (IsKeyDown(KEY_F)) {
|
||||
note->name.assign("F2");
|
||||
is_pressed = 1;
|
||||
}
|
||||
if (IsKeyDown(KEY_G)) {
|
||||
note->name.assign("G2");
|
||||
is_pressed = 1;
|
||||
}
|
||||
return is_pressed == 1;
|
||||
}
|
||||
|
||||
bool is_note_up() {
|
||||
return IsKeyUp(KEY_A) || IsKeyUp(KEY_B) || IsKeyUp(KEY_C) ||
|
||||
IsKeyUp(KEY_D) || IsKeyUp(KEY_E) || IsKeyUp(KEY_F) || IsKeyUp(KEY_G);
|
||||
}
|
||||
|
||||
void Application::UpdateOnNoteInput() {
|
||||
if (DetectNotePressed(m_current_note)) {
|
||||
if (!m_synth.GetIsNoteTriggered()) {
|
||||
m_synth.Trigger((*m_current_note));
|
||||
}
|
||||
//write_log("Note played: %s\n", m_current_note->name.c_str());
|
||||
} else if (is_note_up() && m_synth.GetIsNoteTriggered()) {
|
||||
m_synth.Release();
|
||||
}
|
||||
// will produce 0 signal if ADSR is in off state
|
||||
m_synth.Process();
|
||||
}
|
||||
|
||||
// Play ring-buffered audio
|
||||
void Application::PlayBufferedAudio() {
|
||||
if (IsAudioStreamProcessed(m_synth_stream)) {
|
||||
UpdateOnNoteInput();
|
||||
UpdateAudioStream(m_synth_stream, m_synth.GetOutSignal().data(),
|
||||
STREAM_BUFFER_SIZE);
|
||||
}
|
||||
}
|
||||
|
||||
void Application::Run() {
|
||||
// Main game loop
|
||||
while (!WindowShouldClose()) {
|
||||
PlayBufferedAudio();
|
||||
m_renderer.Draw(m_synth, m_synth_gui_state);
|
||||
}
|
||||
}
|
||||
22
src/BandPassFilter.cpp
Normal file
22
src/BandPassFilter.cpp
Normal file
@@ -0,0 +1,22 @@
|
||||
#include "Filter.h"
|
||||
#include "Settings.h"
|
||||
|
||||
BandPassFilter::BandPassFilter() {
|
||||
SetParameters(200, 0.1, 0.001);
|
||||
}
|
||||
|
||||
BandPassFilter::BandPassFilter(
|
||||
Filter* filter) {
|
||||
SetParameters(filter->GetFreq(), filter->GetRes(), filter->GetPeakGain());
|
||||
}
|
||||
|
||||
BandPassFilter::BandPassFilter(float freq, float res,
|
||||
float q) {
|
||||
SetParameters(freq, res, q);
|
||||
}
|
||||
|
||||
BandPassFilter::~BandPassFilter() {}
|
||||
|
||||
float BandPassFilter::GetSampleForFilterType() {
|
||||
return m_lowo;
|
||||
}
|
||||
50
src/Filter.cpp
Normal file
50
src/Filter.cpp
Normal file
@@ -0,0 +1,50 @@
|
||||
#include "Filter.h"
|
||||
#include "Settings.h"
|
||||
#include <algorithm>
|
||||
#include <cmath>
|
||||
|
||||
Filter::Filter(/* args */) {}
|
||||
|
||||
Filter::~Filter() {}
|
||||
|
||||
void Filter::Trigger() {}
|
||||
|
||||
void Filter::Release() {}
|
||||
|
||||
void Filter::Process(std::vector<float>& samples) {
|
||||
for (std::size_t i = 0; i < samples.size(); i++) {
|
||||
samples[i] = Process(samples[i]);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
float Filter::Process(float in) {
|
||||
m_notcho = in - m_damp * m_bando;
|
||||
m_lowo = m_lowo + m_freq * m_bando;
|
||||
m_higho = m_notcho - m_lowo;
|
||||
m_bando =
|
||||
m_freq * m_higho + m_bando - m_drive * m_bando * m_bando * m_bando;
|
||||
// (m_notcho or m_lowo or m_higho or m_bando or m_peako)
|
||||
float out = 0.5 * GetSampleForFilterType();
|
||||
m_notcho = in - m_damp * m_bando;
|
||||
m_lowo = m_lowo + m_freq * m_bando;
|
||||
m_higho = m_notcho - m_lowo;
|
||||
m_bando =
|
||||
m_freq * m_higho + m_bando - m_drive * m_bando * m_bando * m_bando;
|
||||
out += 0.5 * GetSampleForFilterType();
|
||||
|
||||
return out;
|
||||
}
|
||||
|
||||
void Filter::SetParameters(float freq, float res, float drive) {
|
||||
m_fc = freq;
|
||||
m_res = res;
|
||||
m_drive = drive;
|
||||
|
||||
// the fs*2 is because it's double sampled
|
||||
m_freq =
|
||||
2.0 * std::sinf(SYNTH_PI * std::min(0.25f, m_fc / (m_fs * 2)));
|
||||
|
||||
m_damp = std::min(2.0f * (1.0f - std::powf(m_res, 0.25f)),
|
||||
std::min(2.0f, 2.0f / m_freq - m_freq * 0.5f));
|
||||
}
|
||||
22
src/HighPassFilter.cpp
Normal file
22
src/HighPassFilter.cpp
Normal file
@@ -0,0 +1,22 @@
|
||||
#include "Filter.h"
|
||||
#include "Settings.h"
|
||||
|
||||
HighPassFilter::HighPassFilter() {
|
||||
SetParameters(200, 0.1, 0.001);
|
||||
}
|
||||
|
||||
HighPassFilter::HighPassFilter(
|
||||
Filter* filter) {
|
||||
SetParameters(filter->GetFreq(), filter->GetRes(), filter->GetPeakGain());
|
||||
}
|
||||
|
||||
HighPassFilter::HighPassFilter(float freq, float res,
|
||||
float q) {
|
||||
SetParameters(freq, res, q);
|
||||
}
|
||||
|
||||
HighPassFilter::~HighPassFilter() {}
|
||||
|
||||
float HighPassFilter::GetSampleForFilterType() {
|
||||
return m_higho;
|
||||
}
|
||||
10
src/LFO.cpp
Normal file
10
src/LFO.cpp
Normal file
@@ -0,0 +1,10 @@
|
||||
#include "LFO.h"
|
||||
|
||||
|
||||
LFO::LFO(/* args */): Oscillator(Sine, 0.f, 0.5f)
|
||||
{
|
||||
}
|
||||
|
||||
LFO::~LFO()
|
||||
{
|
||||
}
|
||||
22
src/LowPassFilter.cpp
Normal file
22
src/LowPassFilter.cpp
Normal file
@@ -0,0 +1,22 @@
|
||||
#include "Filter.h"
|
||||
#include "Settings.h"
|
||||
|
||||
LowPassFilter::LowPassFilter() {
|
||||
SetParameters(200, 0.1, 0.001);
|
||||
}
|
||||
|
||||
LowPassFilter::LowPassFilter(
|
||||
Filter* filter) {
|
||||
SetParameters(filter->GetFreq(), filter->GetRes(), filter->GetPeakGain());
|
||||
}
|
||||
|
||||
LowPassFilter::LowPassFilter(float freq, float res,
|
||||
float q) {
|
||||
SetParameters(freq, res, q);
|
||||
}
|
||||
|
||||
LowPassFilter::~LowPassFilter() {}
|
||||
|
||||
float LowPassFilter::GetSampleForFilterType() {
|
||||
return m_lowo;
|
||||
}
|
||||
97
src/Oscillator.cpp
Normal file
97
src/Oscillator.cpp
Normal file
@@ -0,0 +1,97 @@
|
||||
#include "Oscillator.h"
|
||||
#include "Settings.h"
|
||||
#include "KeyBoard.h"
|
||||
#include "Logger.h"
|
||||
|
||||
#define TWO_PI 2 * SYNTH_PI
|
||||
|
||||
Oscillator::Oscillator(OscillatorType osc, float fine, float volume) {
|
||||
assert(fine >= -2.f && fine <= 2.f);
|
||||
assert(volume >= 0.f && volume <= 1.f);
|
||||
SetType(osc);
|
||||
m_fine = fine;
|
||||
m_volume = volume;
|
||||
}
|
||||
|
||||
Oscillator::~Oscillator() {}
|
||||
|
||||
void Oscillator::Reset() {
|
||||
m_volume = 0;
|
||||
m_phase = 0;
|
||||
m_phase_dt = 0;
|
||||
}
|
||||
|
||||
void Oscillator::SetType(OscillatorType osc) {
|
||||
m_osc = osc;
|
||||
switch (m_osc) {
|
||||
case Sine:
|
||||
m_osc_function = &Oscillator::SineOsc;
|
||||
m_dt_function = &Oscillator::CalcSinePhaseDelta;
|
||||
break;
|
||||
case Triangle:
|
||||
m_osc_function = &Oscillator::TriangleOsc;
|
||||
m_dt_function = &Oscillator::CalcSawPhaseDelta;
|
||||
break;
|
||||
case Square:
|
||||
m_osc_function = &Oscillator::SquareOsc;
|
||||
m_dt_function = &Oscillator::CalcSinePhaseDelta;
|
||||
break;
|
||||
case Saw:
|
||||
m_osc_function = &Oscillator::SawOsc;
|
||||
m_dt_function = &Oscillator::CalcSawPhaseDelta;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
void Oscillator::SetKey(float key) {
|
||||
m_key = key;
|
||||
float freq = KeyBoard::GetHzBySemitone(m_key + m_fine);
|
||||
m_phase = 0;
|
||||
m_phase_dt = (this->*m_dt_function)(freq);
|
||||
}
|
||||
|
||||
float Oscillator::Process() {
|
||||
return (this->*m_osc_function)() * m_volume;
|
||||
}
|
||||
|
||||
void Oscillator::SineOscPhaseIncr() {
|
||||
m_phase += m_phase_dt;
|
||||
if (m_phase >= TWO_PI)
|
||||
m_phase -= TWO_PI;
|
||||
}
|
||||
|
||||
void Oscillator::SawOscPhaseIncr() {
|
||||
m_phase += m_phase_dt;
|
||||
if (m_phase >= 1.0f)
|
||||
m_phase -= 1.0f;
|
||||
}
|
||||
|
||||
float Oscillator::CalcSawPhaseDelta(float freq) {
|
||||
return freq / SAMPLE_RATE;
|
||||
}
|
||||
|
||||
float Oscillator::CalcSinePhaseDelta(float freq) {
|
||||
return (TWO_PI * freq) / SAMPLE_RATE;
|
||||
}
|
||||
|
||||
float Oscillator::SineOsc() {
|
||||
float result = sinf(m_phase);
|
||||
SineOscPhaseIncr();
|
||||
return result;
|
||||
}
|
||||
|
||||
float Oscillator::Sign(float v) { return (v > 0.0) ? 1.f : -1.f; }
|
||||
|
||||
float Oscillator::SquareOsc() { return Sign(SineOsc()); }
|
||||
|
||||
float Oscillator::TriangleOsc() {
|
||||
float result = 1.f - fabsf(m_phase - 0.5f) * 4.f;
|
||||
SawOscPhaseIncr();
|
||||
return result;
|
||||
}
|
||||
|
||||
float Oscillator::SawOsc() {
|
||||
float result = m_phase * 2.f - 1.f;
|
||||
SawOscPhaseIncr();
|
||||
return result;
|
||||
}
|
||||
27
src/Ramp.cpp
Normal file
27
src/Ramp.cpp
Normal file
@@ -0,0 +1,27 @@
|
||||
#include "Ramp.h"
|
||||
#include "Logger.h"
|
||||
|
||||
Ramp::Ramp(float starting_level, float sample_rate) {
|
||||
m_level = starting_level;
|
||||
m_sample_rate = sample_rate;
|
||||
}
|
||||
|
||||
Ramp::~Ramp() {}
|
||||
|
||||
void Ramp::RampTo(float val, float time) {
|
||||
m_increment = (val - m_level) / (m_sample_rate * time);
|
||||
m_counter = (int)(m_sample_rate * time);
|
||||
write_log("Ramping from: %.1f to: %.1f by: %lf for: %d\n", m_level, val,
|
||||
m_increment, m_counter);
|
||||
}
|
||||
|
||||
float Ramp::Process() {
|
||||
if (m_counter > 0) {
|
||||
m_counter--;
|
||||
m_level += m_increment;
|
||||
}
|
||||
|
||||
return m_level;
|
||||
}
|
||||
|
||||
bool Ramp::IsCompleted() { return m_counter == 0; }
|
||||
288
src/Renderer.cpp
Normal file
288
src/Renderer.cpp
Normal file
@@ -0,0 +1,288 @@
|
||||
#include "Renderer.h"
|
||||
#define RAYGUI_IMPLEMENTATION
|
||||
#include "Logger.h"
|
||||
#include "Settings.h"
|
||||
|
||||
#pragma clang diagnostic push
|
||||
#pragma clang diagnostic ignored "-Wunused-but-set-variable"
|
||||
#pragma clang diagnostic ignored "-Wunused-variable"
|
||||
#pragma clang diagnostic ignored "-Wdeprecated-declarations"
|
||||
#include "raygui.h"
|
||||
#pragma clang diagnostic pop
|
||||
|
||||
Renderer::Renderer(/* args */) {
|
||||
InitWindow(WINDOW_WIDTH, WINDOW_HEIGHT, "SeeSynth - v0.2");
|
||||
SetTargetFPS(FPS);
|
||||
}
|
||||
|
||||
Renderer::~Renderer() {}
|
||||
|
||||
void Renderer::Draw(Synth& synth, SynthGuiState& synth_gui) {
|
||||
BeginDrawing();
|
||||
ClearBackground(RAYWHITE);
|
||||
|
||||
draw_ui(synth, synth_gui);
|
||||
draw_signal(synth, synth_gui);
|
||||
|
||||
EndDrawing();
|
||||
}
|
||||
|
||||
void Renderer::draw_signal(Synth& synth, SynthGuiState& synth_gui) {
|
||||
GuiGrid((Rectangle){0, 0, WINDOW_WIDTH, WINDOW_HEIGHT}, "",
|
||||
WINDOW_HEIGHT / 8, 2);
|
||||
auto signal = synth.GetOutSignal();
|
||||
Vector2* signal_points = new Vector2[signal.size()];
|
||||
const float screen_vertical_midpoint = (WINDOW_HEIGHT / 2);
|
||||
for (int point_idx = 0; point_idx < signal.size(); point_idx++) {
|
||||
signal_points[point_idx].x = (float)point_idx + OSCILLATOR_PANEL_WIDTH;
|
||||
signal_points[point_idx].y =
|
||||
screen_vertical_midpoint + (int)(signal[point_idx] * 300);
|
||||
}
|
||||
DrawLineStrip(signal_points, signal.size(), RED);
|
||||
delete[] signal_points;
|
||||
}
|
||||
|
||||
void Renderer::draw_oscillators_shape_inputs(
|
||||
const std::vector<Oscillator*>& oscillators,
|
||||
const std::vector<OscillatorGuiState*>& gui_oscillators) {
|
||||
#define WAVE_SHAPE_OPTIONS "Sine;Triangle;Sawtooth;Square"
|
||||
|
||||
// DRAW OSCILLATOR SHAPE INPUTS
|
||||
for (int i = 0; i < oscillators.size(); i += 1) {
|
||||
OscillatorGuiState* ui_osc = gui_oscillators[i];
|
||||
assert(ui_osc);
|
||||
|
||||
Oscillator* osc = oscillators[i];
|
||||
assert(osc);
|
||||
|
||||
// Shape select
|
||||
int shape_index = (int)(ui_osc->waveshape);
|
||||
bool is_dropdown_click =
|
||||
GuiDropdownBox(ui_osc->shape_dropdown_rect, WAVE_SHAPE_OPTIONS,
|
||||
&shape_index, ui_osc->is_dropdown_open);
|
||||
|
||||
if (is_dropdown_click) {
|
||||
write_log("Dropdown clicked!\n");
|
||||
ui_osc->is_dropdown_open = !ui_osc->is_dropdown_open;
|
||||
ui_osc->waveshape = (OscillatorType)(shape_index);
|
||||
// APPLY STATE TO REAL OSC
|
||||
osc->SetType(ui_osc->waveshape);
|
||||
}
|
||||
if (ui_osc->is_dropdown_open)
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
float Renderer::draw_oscillators_panels(
|
||||
const std::vector<Oscillator*>& oscillators,
|
||||
const std::vector<OscillatorGuiState*>& gui_oscillators,
|
||||
const Rectangle& panel_bounds) {
|
||||
float panel_y_offset = 0;
|
||||
for (int i = 0; i < oscillators.size(); i++) {
|
||||
OscillatorGuiState* ui_osc = gui_oscillators[i];
|
||||
assert(ui_osc);
|
||||
|
||||
Oscillator* osc = oscillators[i];
|
||||
assert(osc);
|
||||
|
||||
const bool has_shape_param = (ui_osc->waveshape == Square);
|
||||
|
||||
// Draw Oscillator Panel
|
||||
const int osc_panel_width = panel_bounds.width - 20;
|
||||
const int osc_panel_height = has_shape_param ? 150 : 120;
|
||||
const int osc_panel_x = panel_bounds.x + 10;
|
||||
const int osc_panel_y = panel_bounds.y + 50 + panel_y_offset;
|
||||
panel_y_offset += osc_panel_height + 5;
|
||||
GuiPanel((Rectangle){(float)osc_panel_x, (float)osc_panel_y,
|
||||
(float)osc_panel_width, (float)osc_panel_height},
|
||||
"");
|
||||
|
||||
const float slider_padding = 50.f;
|
||||
const float el_spacing = 5.f;
|
||||
Rectangle el_rect = {.x = (float)osc_panel_x + slider_padding + 30,
|
||||
.y = (float)osc_panel_y + 10,
|
||||
.width =
|
||||
(float)osc_panel_width - (slider_padding * 2),
|
||||
.height = 25.f};
|
||||
|
||||
// Volume slider
|
||||
float decibels = (20.f * log10f(osc->GetVolume()));
|
||||
char amp_slider_label[32];
|
||||
snprintf(amp_slider_label, 7, "%.1f dB", decibels);
|
||||
decibels =
|
||||
GuiSlider(el_rect, amp_slider_label, "", decibels, -60.0f, 0.0f);
|
||||
ui_osc->volume = powf(10.f, decibels * (1.f / 20.f));
|
||||
el_rect.y += el_rect.height + el_spacing;
|
||||
|
||||
// Fine slider
|
||||
float fine = osc->GetFine();
|
||||
char fine_slider_label[10];
|
||||
snprintf(fine_slider_label, 9, "%.3f u", fine);
|
||||
fine = GuiSlider(el_rect, fine_slider_label, "", fine, -2.f, 2.f);
|
||||
ui_osc->fine = fine;
|
||||
el_rect.y += el_rect.height + el_spacing;
|
||||
|
||||
// Defer shape drop-down box.
|
||||
ui_osc->shape_dropdown_rect = el_rect;
|
||||
el_rect.y += el_rect.height + el_spacing;
|
||||
|
||||
// Apply values to real
|
||||
osc->SetVolume(ui_osc->volume);
|
||||
osc->SetFine(ui_osc->fine);
|
||||
}
|
||||
|
||||
return panel_y_offset;
|
||||
}
|
||||
|
||||
void Renderer::draw_main_panel(const Rectangle& panel_bounds) {
|
||||
GuiPanel(panel_bounds, "");
|
||||
}
|
||||
|
||||
void Renderer::draw_adsr_panel(ADSR* adsr, ADSRGuiState& gui_adsr,
|
||||
const Rectangle& panel_bounds,
|
||||
float panel_y_offset) {
|
||||
// Draw ADSR Panel
|
||||
const int osc_panel_width = panel_bounds.width - 20;
|
||||
const int osc_panel_height = 120;
|
||||
const int osc_panel_x = panel_bounds.x + 10;
|
||||
const int osc_panel_y = panel_bounds.y + 50 + panel_y_offset;
|
||||
panel_y_offset += osc_panel_height + 5;
|
||||
GuiPanel((Rectangle){(float)osc_panel_x, (float)osc_panel_y,
|
||||
(float)osc_panel_width, (float)osc_panel_height},
|
||||
"ADSR");
|
||||
|
||||
const float slider_padding = 50.f;
|
||||
const float el_spacing = 5.f;
|
||||
Rectangle el_rect = {.x = (float)osc_panel_x + slider_padding + 30,
|
||||
.y = (float)osc_panel_y + 10,
|
||||
.width = (float)osc_panel_width - (slider_padding * 2),
|
||||
.height = 25.f};
|
||||
|
||||
// Attack slider
|
||||
float attack = gui_adsr.attack;
|
||||
char attack_slider_label[32];
|
||||
snprintf(attack_slider_label, 7, "%.1f s", attack);
|
||||
attack = GuiSlider(el_rect, attack_slider_label, "", attack, 0.0f, 2.0f);
|
||||
gui_adsr.attack = attack;
|
||||
el_rect.y += el_rect.height + el_spacing;
|
||||
|
||||
// Decay slider
|
||||
float decay = gui_adsr.decay;
|
||||
char decay_slider_label[32];
|
||||
snprintf(decay_slider_label, 7, "%.1f s", decay);
|
||||
decay = GuiSlider(el_rect, decay_slider_label, "", decay, 0.0f, 1.0f);
|
||||
gui_adsr.decay = decay;
|
||||
el_rect.y += el_rect.height + el_spacing;
|
||||
|
||||
// Sustain slider
|
||||
float sustain = gui_adsr.sustain;
|
||||
char sustain_slider_label[32];
|
||||
snprintf(sustain_slider_label, 7, "%.1f u", sustain);
|
||||
sustain = GuiSlider(el_rect, sustain_slider_label, "", sustain, 0.0f, 1.0f);
|
||||
gui_adsr.sustain = sustain;
|
||||
el_rect.y += el_rect.height + el_spacing;
|
||||
|
||||
// Release slider
|
||||
float release = gui_adsr.release;
|
||||
char release_slider_label[32];
|
||||
snprintf(release_slider_label, 7, "%.1f s", release);
|
||||
release = GuiSlider(el_rect, release_slider_label, "", release, 0.0f, 5.0f);
|
||||
gui_adsr.release = release;
|
||||
el_rect.y += el_rect.height + el_spacing;
|
||||
|
||||
// apply values to real one
|
||||
adsr->SetParameters(gui_adsr.attack, gui_adsr.decay, gui_adsr.sustain,
|
||||
gui_adsr.release);
|
||||
}
|
||||
|
||||
void Renderer::draw_second_panel(Rectangle& bounds) {
|
||||
bounds.x += bounds.width;
|
||||
GuiPanel(bounds, "");
|
||||
}
|
||||
|
||||
float Renderer::DrawFilterPanel(Synth& synth, FilterGuiState& gui_filter,
|
||||
const Rectangle& panel_bounds) {
|
||||
#define FILTER_TYPE_OPTIONS "LP;BP;HP"
|
||||
Filter* filter = synth.GetFilter();
|
||||
float panel_y_offset = 0;
|
||||
|
||||
// Draw Filter Panel
|
||||
const int osc_panel_width = panel_bounds.width - 20;
|
||||
const int osc_panel_height = 100;
|
||||
const int osc_panel_x = panel_bounds.x + 10;
|
||||
const int osc_panel_y = panel_bounds.y + 50;
|
||||
panel_y_offset += osc_panel_height + 5;
|
||||
GuiPanel((Rectangle){(float)osc_panel_x, (float)osc_panel_y,
|
||||
(float)osc_panel_width, (float)osc_panel_height},
|
||||
"Filter");
|
||||
|
||||
const float slider_padding = 50.f;
|
||||
const float el_spacing = 5.f;
|
||||
Rectangle el_rect = {.x = (float)osc_panel_x + slider_padding + 30,
|
||||
.y = (float)osc_panel_y + 10,
|
||||
.width = (float)osc_panel_width - (slider_padding * 2),
|
||||
.height = 25.f};
|
||||
|
||||
// Frequency slider
|
||||
float freq = log10f(gui_filter.freq);
|
||||
char freq_slider_label[32];
|
||||
snprintf(freq_slider_label, 10, "%.1f hz", powf(10.f, freq));
|
||||
freq = GuiSlider(el_rect, freq_slider_label, "", freq, 0.f, 4.f);
|
||||
gui_filter.freq = powf(10.f, freq);
|
||||
el_rect.y += el_rect.height + el_spacing;
|
||||
|
||||
// todo: implement that when Res will be fixed
|
||||
// Resonance slider
|
||||
// float res = gui_filter.res;
|
||||
// char res_slider_label[32];
|
||||
// snprintf(res_slider_label, 7, "%.1f u", res);
|
||||
// res = GuiSlider(el_rect, res_slider_label, "", res, 0.0f, 1.0f);
|
||||
// gui_filter.res = res;
|
||||
// el_rect.y += el_rect.height + el_spacing;
|
||||
|
||||
// Shape select
|
||||
int shape_index = (int)(gui_filter.type);
|
||||
bool is_dropdown_click =
|
||||
GuiDropdownBox(el_rect, FILTER_TYPE_OPTIONS, &shape_index,
|
||||
gui_filter.is_dropdown_open);
|
||||
|
||||
if (is_dropdown_click) {
|
||||
write_log("Dropdown clicked!\n");
|
||||
gui_filter.is_dropdown_open = !gui_filter.is_dropdown_open;
|
||||
gui_filter.type = (FilterType)(shape_index);
|
||||
// APPLY STATE TO REAL SYNTH
|
||||
synth.SetFilter(gui_filter.type);
|
||||
}
|
||||
|
||||
// apply values to real one
|
||||
// todo: thrid (order) parameter
|
||||
// todo: why resonance changing does not work?
|
||||
// todo: limit filter lowest frequency to ~40 hz
|
||||
if (gui_filter.freq < 40.0) {
|
||||
gui_filter.freq = 50.0;
|
||||
}
|
||||
filter->SetParameters(gui_filter.freq, filter->GetRes(),
|
||||
filter->GetPeakGain());
|
||||
|
||||
return panel_y_offset;
|
||||
}
|
||||
|
||||
void Renderer::draw_ui(Synth& synth, SynthGuiState& synth_gui) {
|
||||
Rectangle panel_bounds = {.x = 0,
|
||||
.y = 0,
|
||||
.width = OSCILLATOR_PANEL_WIDTH,
|
||||
.height = WINDOW_HEIGHT};
|
||||
draw_main_panel(panel_bounds);
|
||||
|
||||
std::vector<Oscillator*> oscillators = synth.GetOscillators();
|
||||
std::vector<OscillatorGuiState*> gui_oscillators = synth_gui.oscillators;
|
||||
|
||||
float panel_y_offset =
|
||||
draw_oscillators_panels(oscillators, gui_oscillators, panel_bounds);
|
||||
draw_adsr_panel(synth.GetADSR(), synth_gui.adsr, panel_bounds,
|
||||
panel_y_offset);
|
||||
draw_oscillators_shape_inputs(oscillators, gui_oscillators);
|
||||
|
||||
draw_second_panel(panel_bounds);
|
||||
DrawFilterPanel(synth, synth_gui.filter, panel_bounds);
|
||||
}
|
||||
8
src/SeeSynth.cpp
Normal file
8
src/SeeSynth.cpp
Normal file
@@ -0,0 +1,8 @@
|
||||
#include "Application.h"
|
||||
|
||||
int main() {
|
||||
Application* app = new Application();
|
||||
app->Run();
|
||||
|
||||
delete app;
|
||||
}
|
||||
106
src/Synth.cpp
Normal file
106
src/Synth.cpp
Normal file
@@ -0,0 +1,106 @@
|
||||
#include "Synth.h"
|
||||
#include "FilterFactory.h"
|
||||
#include "KeyBoard.h"
|
||||
#include "Logger.h"
|
||||
#include "OscillatorType.h"
|
||||
#include "Settings.h"
|
||||
|
||||
Synth::Synth(/* args */) {
|
||||
m_lfo = new LFO();
|
||||
m_lfo->SetFreq(5.0);
|
||||
AddOscillator();
|
||||
AddOscillator();
|
||||
AddEffect(new ADSR());
|
||||
AddEffect(FilterFactory::GetDefaultFilter());
|
||||
for (size_t i = 0; i < STREAM_BUFFER_SIZE; i++) {
|
||||
float sample = 0.0f;
|
||||
m_out_signal.push_back(sample);
|
||||
}
|
||||
ZeroSignal();
|
||||
}
|
||||
|
||||
Synth::~Synth() {
|
||||
m_oscillators.clear();
|
||||
m_effects.clear();
|
||||
m_out_signal.clear();
|
||||
}
|
||||
|
||||
void Synth::ZeroSignal() {
|
||||
float sample = 0.0f;
|
||||
for (size_t i = 0; i < STREAM_BUFFER_SIZE; i++) {
|
||||
m_out_signal[i] = sample;
|
||||
}
|
||||
}
|
||||
|
||||
void Synth::GetNote() {
|
||||
ZeroSignal();
|
||||
Adder::SumOscillators(m_oscillators, m_out_signal);
|
||||
}
|
||||
|
||||
void Synth::ApplyEffects() {
|
||||
auto* adsr = m_effects[0];
|
||||
adsr->Process(m_out_signal);
|
||||
|
||||
Filter* filter = (Filter*)m_effects[1];
|
||||
|
||||
for (std::size_t i = 0; i < m_out_signal.size(); i++) {
|
||||
ApplyFilterLfo();
|
||||
m_out_signal[i] = filter->Process(m_out_signal[i]);
|
||||
}
|
||||
}
|
||||
|
||||
void Synth::TriggerNoteOnEffects() {
|
||||
for (IEffect* effect : m_effects) {
|
||||
effect->Trigger();
|
||||
}
|
||||
}
|
||||
|
||||
void Synth::UntriggerNoteOnEffects() {
|
||||
for (IEffect* effect : m_effects) {
|
||||
effect->Release();
|
||||
}
|
||||
}
|
||||
|
||||
void Synth::AddOscillator() {
|
||||
m_oscillators.push_back(new Oscillator(OscillatorType::Sine, 0.0f, VOLUME));
|
||||
}
|
||||
|
||||
void Synth::Trigger(Note input) {
|
||||
int semitone_shift = KeyBoard::GetSemitoneShift(input.name);
|
||||
|
||||
for (Oscillator* osc : m_oscillators) {
|
||||
osc->SetKey(semitone_shift);
|
||||
}
|
||||
is_note_triggered = true;
|
||||
TriggerNoteOnEffects();
|
||||
}
|
||||
|
||||
void Synth::ApplyFilterLfo() {
|
||||
float dt = m_lfo->Process();
|
||||
Filter* filter = (Filter*)m_effects[1];
|
||||
float freq = filter->GetFreq();
|
||||
filter->SetParameters(freq + dt, filter->GetRes(), filter->GetPeakGain());
|
||||
}
|
||||
|
||||
void Synth::Process() {
|
||||
GetNote();
|
||||
ApplyEffects();
|
||||
}
|
||||
|
||||
void Synth::Release() {
|
||||
ZeroSignal();
|
||||
is_note_triggered = false;
|
||||
UntriggerNoteOnEffects();
|
||||
}
|
||||
|
||||
void Synth::AddEffect(IEffect* fx) { m_effects.push_back(fx); }
|
||||
|
||||
void Synth::SetFilter(FilterType type) {
|
||||
Filter* old_filter = this->GetFilter();
|
||||
if (!old_filter->IsSameFilterType(type)) {
|
||||
// todo: implement other types of state variable filters;
|
||||
Filter* new_filter = FilterFactory::CreateFilter(old_filter, type);
|
||||
delete old_filter;
|
||||
m_effects[1] = new_filter;
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user