readme
This commit is contained in:
@@ -0,0 +1,2 @@
|
|||||||
|
.vs
|
||||||
|
dogtags
|
||||||
Vendored
+3
@@ -0,0 +1,3 @@
|
|||||||
|
{
|
||||||
|
"cmake.sourceDirectory": "C:/Users/yulia/Downloads/fps-gaming/dependencies/includes/glm"
|
||||||
|
}
|
||||||
+245
@@ -0,0 +1,245 @@
|
|||||||
|
#ifndef ANIMATOR_CLASS
|
||||||
|
#define ANIMATOR_CLASS
|
||||||
|
|
||||||
|
#include <glm/glm.hpp>
|
||||||
|
#include <glm/gtc/matrix_transform.hpp>
|
||||||
|
#include <map>
|
||||||
|
#include <unordered_map>
|
||||||
|
#include <vector>
|
||||||
|
#include <string>
|
||||||
|
#include <functional>
|
||||||
|
#include <assimp/scene.h>
|
||||||
|
#include <assimp/Importer.hpp>
|
||||||
|
#include "Rig.h"
|
||||||
|
|
||||||
|
// Scale of the "Head" bone (and what hangs off it), around the neck. 1 = as authored.
|
||||||
|
inline float HeadScale = 1.0f;
|
||||||
|
|
||||||
|
struct AssimpNodeData
|
||||||
|
{
|
||||||
|
glm::mat4 transformation;
|
||||||
|
std::string name;
|
||||||
|
int childrenCount;
|
||||||
|
std::vector<AssimpNodeData> children;
|
||||||
|
};
|
||||||
|
|
||||||
|
// ============================================================================
|
||||||
|
// Animation
|
||||||
|
// ============================================================================
|
||||||
|
class Animation
|
||||||
|
{
|
||||||
|
public:
|
||||||
|
Animation() = default;
|
||||||
|
|
||||||
|
Animation(const std::string& animationPath, Rig* rig = nullptr)
|
||||||
|
{
|
||||||
|
Assimp::Importer importer;
|
||||||
|
const aiScene* scene = importer.ReadFile(animationPath, aiProcess_Triangulate);
|
||||||
|
assert(scene && scene->mRootNode);
|
||||||
|
|
||||||
|
const aiAnimation* anim = scene->mAnimations[0];
|
||||||
|
m_Duration = (float)anim->mDuration;
|
||||||
|
m_TicksPerSecond = (int)(anim->mTicksPerSecond != 0 ? anim->mTicksPerSecond : 24);
|
||||||
|
|
||||||
|
ReadHierarchyData(m_RootNode, scene->mRootNode);
|
||||||
|
|
||||||
|
for (unsigned int i = 0; i < anim->mNumChannels; i++) {
|
||||||
|
const aiNodeAnim* channel = anim->mChannels[i];
|
||||||
|
m_Bones.emplace_back(channel->mNodeName.data, 0, channel);
|
||||||
|
m_BoneNames.push_back(channel->mNodeName.data);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (rig) RegisterBonesWithRig(*rig);
|
||||||
|
}
|
||||||
|
|
||||||
|
void RegisterBonesWithRig(Rig& rig)
|
||||||
|
{
|
||||||
|
for (const std::string& name : m_BoneNames) {
|
||||||
|
if (rig.m_BoneInfoMap.find(name) == rig.m_BoneInfoMap.end()) {
|
||||||
|
BoneInfo info;
|
||||||
|
info.id = rig.m_BoneCounter++;
|
||||||
|
info.offset = glm::mat4(1.0f);
|
||||||
|
rig.m_BoneInfoMap[name] = info;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Bone* FindBone(const std::string& name)
|
||||||
|
{
|
||||||
|
for (Bone& b : m_Bones)
|
||||||
|
if (b.GetBoneName() == name) return &b;
|
||||||
|
return nullptr;
|
||||||
|
}
|
||||||
|
|
||||||
|
float GetTicksPerSecond() const { return (float)m_TicksPerSecond; }
|
||||||
|
float GetDuration() const { return m_Duration; }
|
||||||
|
const AssimpNodeData& GetRootNode() const { return m_RootNode; }
|
||||||
|
|
||||||
|
public:
|
||||||
|
float m_Duration = 0.0f;
|
||||||
|
int m_TicksPerSecond = 24;
|
||||||
|
std::vector<Bone> m_Bones;
|
||||||
|
std::vector<std::string> m_BoneNames;
|
||||||
|
AssimpNodeData m_RootNode;
|
||||||
|
|
||||||
|
private:
|
||||||
|
void ReadHierarchyData(AssimpNodeData& dest, const aiNode* src)
|
||||||
|
{
|
||||||
|
assert(src);
|
||||||
|
dest.name = src->mName.data;
|
||||||
|
dest.transformation = AssimpGLMHelpers::ConvertMatrixToGLMFormat(src->mTransformation);
|
||||||
|
dest.childrenCount = (int)src->mNumChildren;
|
||||||
|
for (unsigned int i = 0; i < src->mNumChildren; i++) {
|
||||||
|
AssimpNodeData child;
|
||||||
|
ReadHierarchyData(child, src->mChildren[i]);
|
||||||
|
dest.children.push_back(child);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
// ============================================================================
|
||||||
|
// Animator
|
||||||
|
// blendWeight ramps from 0?1 over blendDuration when PlayAnimation is called,
|
||||||
|
// and back down over the end of an animation that doesn't loop.
|
||||||
|
// CharacterBody layers animators bone by bone: each one only moves the bones its animation has tracks for.
|
||||||
|
// ============================================================================
|
||||||
|
class Animator
|
||||||
|
{
|
||||||
|
public:
|
||||||
|
int weight = 0; // priority � higher wins
|
||||||
|
float blendDuration = 0.15f; // seconds to blend in
|
||||||
|
float blendWeight = 1.0f; // 0 = invisible, 1 = fully playing
|
||||||
|
bool loops = true;
|
||||||
|
bool holdsAtEnd = false; // a one-shot that stays on its last frame instead of stopping, like a jump held in the air
|
||||||
|
bool isPlaying = true;
|
||||||
|
float playbackSpeed = 1.0f;
|
||||||
|
std::function<void()> onAnimationEnd;
|
||||||
|
|
||||||
|
Animator()
|
||||||
|
{
|
||||||
|
m_FinalBoneMatrices.assign(100, glm::mat4(1.0f));
|
||||||
|
}
|
||||||
|
|
||||||
|
void SetRig(Rig* rig)
|
||||||
|
{
|
||||||
|
m_Rig = rig;
|
||||||
|
if (m_CurrentAnimation) RebuildBoneInfoMap();
|
||||||
|
}
|
||||||
|
|
||||||
|
void SetAnimation(Animation* animation)
|
||||||
|
{
|
||||||
|
m_CurrentAnimation = animation;
|
||||||
|
m_CurrentTime = 0.0f;
|
||||||
|
if (m_Rig) RebuildBoneInfoMap();
|
||||||
|
}
|
||||||
|
|
||||||
|
void PlayAnimation()
|
||||||
|
{
|
||||||
|
m_CurrentTime = 0.0f;
|
||||||
|
isPlaying = true;
|
||||||
|
blendWeight = 0.0f; // start blending in from 0
|
||||||
|
_blendTimer = 0.0f;
|
||||||
|
}
|
||||||
|
|
||||||
|
void StopAnimation() { isPlaying = false; blendWeight = 0.0f; }
|
||||||
|
void ClearAnimation() { m_CurrentAnimation = nullptr; m_CurrentTime = 0.0f; isPlaying = false; blendWeight = 0.0f; }
|
||||||
|
|
||||||
|
bool IsFinished() const
|
||||||
|
{
|
||||||
|
if (!m_CurrentAnimation || !isPlaying) return true;
|
||||||
|
return m_CurrentTime >= m_CurrentAnimation->GetDuration() - 1.0f;
|
||||||
|
}
|
||||||
|
|
||||||
|
void Step(float dt)
|
||||||
|
{
|
||||||
|
if (!m_CurrentAnimation || dt <= 0.0f || !isPlaying || !m_Rig) return;
|
||||||
|
|
||||||
|
// Ramp blend weight in
|
||||||
|
if (blendWeight < 1.0f) {
|
||||||
|
_blendTimer += dt;
|
||||||
|
blendWeight = glm::min(1.0f, blendDuration > 0.0f ? _blendTimer / blendDuration : 1.0f);
|
||||||
|
}
|
||||||
|
|
||||||
|
m_CurrentTime += m_CurrentAnimation->GetTicksPerSecond() * playbackSpeed * dt;
|
||||||
|
|
||||||
|
if (m_CurrentTime >= m_CurrentAnimation->GetDuration()) {
|
||||||
|
if (loops) {
|
||||||
|
m_CurrentTime = fmod(m_CurrentTime, m_CurrentAnimation->GetDuration());
|
||||||
|
if (onAnimationEnd) onAnimationEnd();
|
||||||
|
} else if (holdsAtEnd) {
|
||||||
|
// Just short of the end, since the bones want a keyframe after whatever time they're asked for
|
||||||
|
m_CurrentTime = m_CurrentAnimation->GetDuration() * 0.999f;
|
||||||
|
} else {
|
||||||
|
m_CurrentTime = m_CurrentAnimation->GetDuration();
|
||||||
|
isPlaying = false;
|
||||||
|
blendWeight = 0.0f;
|
||||||
|
if (onAnimationEnd) onAnimationEnd();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// A one-shot fades out over its last blendDuration instead of snapping back
|
||||||
|
if (!loops && !holdsAtEnd && blendDuration > 0.0f) {
|
||||||
|
const float remaining = (m_CurrentAnimation->GetDuration() - m_CurrentTime)
|
||||||
|
/ glm::max(m_CurrentAnimation->GetTicksPerSecond() * playbackSpeed, 1e-4f);
|
||||||
|
blendWeight = glm::min(blendWeight, glm::clamp(remaining / blendDuration, 0.0f, 1.0f));
|
||||||
|
}
|
||||||
|
|
||||||
|
CalculateBoneTransform(&m_CurrentAnimation->GetRootNode(), glm::mat4(1.0f));
|
||||||
|
}
|
||||||
|
|
||||||
|
const std::vector<glm::mat4>& GetFinalBoneMatrices() const { return m_FinalBoneMatrices; }
|
||||||
|
|
||||||
|
// This frame's transform of each bone relative to its parent, only for bones the animation has a track for
|
||||||
|
const std::unordered_map<std::string, glm::mat4>& GetLocalTransforms() const { return m_LocalTransforms; }
|
||||||
|
|
||||||
|
public:
|
||||||
|
std::vector<glm::mat4> m_FinalBoneMatrices;
|
||||||
|
Animation* m_CurrentAnimation = nullptr;
|
||||||
|
Rig* m_Rig = nullptr;
|
||||||
|
float m_CurrentTime = 0.0f;
|
||||||
|
float m_DeltaTime = 0.0f;
|
||||||
|
|
||||||
|
private:
|
||||||
|
std::map<std::string, BoneInfo> m_BoneInfoMap;
|
||||||
|
std::unordered_map<std::string, glm::mat4> m_LocalTransforms;
|
||||||
|
float _blendTimer = 0.0f;
|
||||||
|
|
||||||
|
void RebuildBoneInfoMap()
|
||||||
|
{
|
||||||
|
if (!m_CurrentAnimation || !m_Rig) return;
|
||||||
|
m_BoneInfoMap = m_Rig->m_BoneInfoMap;
|
||||||
|
m_CurrentAnimation->RegisterBonesWithRig(*m_Rig);
|
||||||
|
m_BoneInfoMap = m_Rig->m_BoneInfoMap;
|
||||||
|
}
|
||||||
|
|
||||||
|
void CalculateBoneTransform(const AssimpNodeData* node, glm::mat4 parentTransform)
|
||||||
|
{
|
||||||
|
const std::string& nodeName = node->name;
|
||||||
|
glm::mat4 nodeTransform = node->transformation;
|
||||||
|
|
||||||
|
Bone* bone = m_CurrentAnimation->FindBone(nodeName);
|
||||||
|
if (bone) {
|
||||||
|
bone->Update(m_CurrentTime);
|
||||||
|
nodeTransform = bone->GetLocalTransform();
|
||||||
|
m_LocalTransforms[nodeName] = nodeTransform; // the Animation is shared, so keep this animator's own copy
|
||||||
|
}
|
||||||
|
|
||||||
|
glm::mat4 globalTransform = parentTransform * nodeTransform;
|
||||||
|
if (nodeName == "Head")
|
||||||
|
globalTransform = glm::scale(globalTransform, glm::vec3(HeadScale));
|
||||||
|
|
||||||
|
auto it = m_BoneInfoMap.find(nodeName);
|
||||||
|
if (it != m_BoneInfoMap.end()) {
|
||||||
|
int index = it->second.id;
|
||||||
|
const glm::mat4 offset = it->second.offset;
|
||||||
|
if (index >= 0 && index < (int)m_FinalBoneMatrices.size())
|
||||||
|
m_FinalBoneMatrices[index] = globalTransform * offset;
|
||||||
|
}
|
||||||
|
|
||||||
|
for (int i = 0; i < node->childrenCount; i++)
|
||||||
|
CalculateBoneTransform(&node->children[i], globalTransform);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
#endif
|
||||||
@@ -0,0 +1,123 @@
|
|||||||
|
#ifndef CAMERA_CLASS_H
|
||||||
|
#define CAMERA_CLASS_H
|
||||||
|
#include <algorithm>
|
||||||
|
#include <glad/glad.h>
|
||||||
|
#include <GLFW/glfw3.h>
|
||||||
|
#include <glm/glm.hpp>
|
||||||
|
#include <glm/gtc/matrix_transform.hpp>
|
||||||
|
#include <glm/gtc/type_ptr.hpp>
|
||||||
|
#include <glm/gtc/quaternion.hpp>
|
||||||
|
#include <cmath>
|
||||||
|
#include <cstdlib>
|
||||||
|
#include "common.h"
|
||||||
|
#include "Window.h"
|
||||||
|
#include "t.h"
|
||||||
|
#include "tFunctions.h"
|
||||||
|
#include "shaderClass.h"
|
||||||
|
|
||||||
|
const glm::vec3 worldUp = { 0.0f, 1.0f, 0.0f };
|
||||||
|
|
||||||
|
// Default mouse sensitivity in cm of mouse travel per 360 degree turn.
|
||||||
|
inline float StartupCmPer360 = 16.0f;
|
||||||
|
inline float StartupMouseDpi = 800.0f;
|
||||||
|
|
||||||
|
class Camera {
|
||||||
|
public:
|
||||||
|
bool lockedcursor = true;
|
||||||
|
t t;
|
||||||
|
float yaw = 0.0f;
|
||||||
|
float pitch = 0.0f;
|
||||||
|
const float originalspeed = 0.08f;
|
||||||
|
float speed = 0.08f;
|
||||||
|
float sensitivity = 0.001f;
|
||||||
|
float FOV = 110.0f;
|
||||||
|
float screenshake = 0.0f; // 0 still to 1 shaking as hard as it does, wearing off over time
|
||||||
|
float shakeDecay = 1.2f; // of full strength lost a second of game time
|
||||||
|
glm::vec3 shakeMaxAngles = glm::vec3(0.08f, 0.08f, 0.04f); // radians of pitch, yaw and roll at full strength
|
||||||
|
|
||||||
|
Camera() {
|
||||||
|
if (StartupCmPer360 > 0.0f) SetSensitivityCm(StartupCmPer360, StartupMouseDpi);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Turn sensitivity into "this many cm of mouse travel per full 360 degree turn" for a mouse
|
||||||
|
// of the given DPI (mouse counts per inch).
|
||||||
|
void SetSensitivityCm(float cmPer360, float dpi) {
|
||||||
|
sensitivity = 6.2831853f / (cmPer360 / 2.54f * dpi);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Shakes the camera, amount of full strength on top of whatever shake there already is. Around 0.3 for a hit,
|
||||||
|
// 1 for something huge.
|
||||||
|
void AddScreenshake(float amount) {
|
||||||
|
screenshake = std::min(screenshake + amount, 1.0f);
|
||||||
|
}
|
||||||
|
|
||||||
|
void Matrix(float nearPlane, float farPlane, Shader& shader) {
|
||||||
|
shader.SetMat4("proj", GetProjectionMatrix(nearPlane, farPlane));
|
||||||
|
shader.SetMat4("view", GetViewMatrix());
|
||||||
|
shader.Set3F("viewPos", t.GetTranslation());
|
||||||
|
}
|
||||||
|
|
||||||
|
glm::mat4 GetViewMatrix() {
|
||||||
|
glm::vec3 front = t.GetFrontVector();
|
||||||
|
glm::vec3 up = t.GetUpVector();
|
||||||
|
|
||||||
|
if (screenshake > 0.0f) {
|
||||||
|
// Apply smooth shake offset stored in shakeOffset
|
||||||
|
glm::quat offsetRot = glm::quat(glm::vec3(
|
||||||
|
shakeOffset.x,
|
||||||
|
shakeOffset.y,
|
||||||
|
shakeOffset.z // roll
|
||||||
|
));
|
||||||
|
front = glm::normalize(offsetRot * front);
|
||||||
|
up = glm::normalize(offsetRot * up);
|
||||||
|
}
|
||||||
|
|
||||||
|
return glm::lookAt(t.GetTranslation(), t.GetTranslation() + front, up);
|
||||||
|
}
|
||||||
|
|
||||||
|
glm::mat4 GetProjectionMatrix(float nearPlane, float farPlane) {
|
||||||
|
// Quake Pro style: FOV is the horizontal angle on a 4:3 screen, so the vertical angle is fixed
|
||||||
|
// and wider windows/fullscreen reveal more to the sides instead of zooming in.
|
||||||
|
const float aspect = (float)window->width / (float)window->height;
|
||||||
|
const float vfov = 2.0f * atan(tan(glm::radians(FOV) * 0.5f) * 3.0f / 4.0f);
|
||||||
|
return glm::perspective(vfov, aspect, nearPlane, farPlane);
|
||||||
|
}
|
||||||
|
|
||||||
|
void Step(float dt) {
|
||||||
|
if (screenshake <= 0.0f) {
|
||||||
|
shakeOffset = glm::vec3(0.0f);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// A smooth wobble on each axis, out of step with each other, scaled by how strong the shake still is
|
||||||
|
shakeTime += dt;
|
||||||
|
auto Wobble = [&](float seed) {
|
||||||
|
return (std::sin(shakeTime * 31.0f + seed) + std::sin(shakeTime * 57.3f + seed * 2.7f)) * 0.5f;
|
||||||
|
};
|
||||||
|
shakeOffset = shakeMaxAngles * screenshake * glm::vec3(Wobble(0.0f), Wobble(11.0f), Wobble(23.0f));
|
||||||
|
|
||||||
|
screenshake = std::max(screenshake - shakeDecay * dt, 0.0f);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Back to looking where the mouse last had it, after something else has turned the camera
|
||||||
|
void RestoreLook() {
|
||||||
|
t.RotateToEulerAngles({ pitch, yaw, 0.0f });
|
||||||
|
t.NormalizeRotation();
|
||||||
|
}
|
||||||
|
|
||||||
|
void ProcessMouseMovement(float xoffset, float yoffset) {
|
||||||
|
xoffset *= sensitivity;
|
||||||
|
yoffset *= sensitivity;
|
||||||
|
yaw += xoffset;
|
||||||
|
pitch += yoffset;
|
||||||
|
pitch = glm::clamp(pitch, -glm::radians(85.0f), glm::radians(85.0f));
|
||||||
|
t.RotateToEulerAngles({ pitch, yaw, 0.0f });
|
||||||
|
t.NormalizeRotation();
|
||||||
|
}
|
||||||
|
|
||||||
|
private:
|
||||||
|
glm::vec3 shakeOffset = glm::vec3(0.0f); // this frame's turn from shaking, pitch, yaw and roll
|
||||||
|
float shakeTime = 0.0f; // runs the wobble along
|
||||||
|
};
|
||||||
|
|
||||||
|
#endif
|
||||||
+683
@@ -0,0 +1,683 @@
|
|||||||
|
#include "Character.h"
|
||||||
|
#include "Animator.h"
|
||||||
|
#include "CharacterMind.h"
|
||||||
|
#include "Ragdoll.h"
|
||||||
|
#include "Physics.h"
|
||||||
|
#include "tFunctions.h"
|
||||||
|
#include "Engine.h"
|
||||||
|
#include <algorithm>
|
||||||
|
#include <cfloat>
|
||||||
|
#include <cmath>
|
||||||
|
#include <glm/gtc/quaternion.hpp>
|
||||||
|
|
||||||
|
// -------------------------------------------------------
|
||||||
|
// CharacterBody
|
||||||
|
// -------------------------------------------------------
|
||||||
|
|
||||||
|
void CharacterBody::Perform(const CharacterAction& action) {
|
||||||
|
switch (action.type) {
|
||||||
|
case CharacterActionType::Move: {
|
||||||
|
sprinting = action.sprint;
|
||||||
|
if (IsDashing())
|
||||||
|
break; // the dash carries it, wherever it was going
|
||||||
|
wishDirection = glm::vec3(action.direction.x, 0.0f, action.direction.z);
|
||||||
|
if (glm::length(wishDirection) > 0.0001f) {
|
||||||
|
wishDirection = glm::normalize(wishDirection);
|
||||||
|
if (!combat.GetCurrentMove() && !combat.GetCurrentAbility() && !inAir) // mid swing it keeps facing where it swung, and in the air where it jumped
|
||||||
|
turnDirection = wishDirection;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Told where to look, it keeps looking there whichever way it walks
|
||||||
|
const glm::vec3 facing(action.facing.x, 0.0f, action.facing.z);
|
||||||
|
strafing = glm::length(facing) > 0.0001f;
|
||||||
|
if (strafing)
|
||||||
|
turnDirection = glm::normalize(facing);
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
case CharacterActionType::Jump:
|
||||||
|
if (!IsDashing())
|
||||||
|
jumpWanted = true;
|
||||||
|
break;
|
||||||
|
case CharacterActionType::Dash: {
|
||||||
|
// On the ground, between attacks and not too soon after the last one
|
||||||
|
if (IsDashing() || dashCooldownLeft > 0.0f || inAir || windup >= 0.0f || !physics
|
||||||
|
|| combat.GetCurrentMove() || combat.GetCurrentAbility())
|
||||||
|
break;
|
||||||
|
glm::vec3 direction(action.direction.x, 0.0f, action.direction.z);
|
||||||
|
direction = glm::length(direction) > 0.0001f ? glm::normalize(direction) : GetForwardDirection();
|
||||||
|
dashDirection = direction;
|
||||||
|
dashWindup = dashDelay * GetGameTimeScale();
|
||||||
|
dashLeft = dashTime * GetGameTimeScale();
|
||||||
|
Face(direction, true);
|
||||||
|
PlayAction(dashAnimation);
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
case CharacterActionType::Ability:
|
||||||
|
if (IsDashing() || !combat.IsAbilityReady(action.slot))
|
||||||
|
break;
|
||||||
|
Face(action.direction, true);
|
||||||
|
combat.UseAbility(action.slot, action.point);
|
||||||
|
break;
|
||||||
|
case CharacterActionType::Attack:
|
||||||
|
if (IsDashing())
|
||||||
|
break;
|
||||||
|
Face(action.direction, true); // round to it before the swing starts, so the step and hitbox go that way
|
||||||
|
combat.QueueAttack();
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void CharacterBody::CancelDash() {
|
||||||
|
if (!IsDashing())
|
||||||
|
return;
|
||||||
|
dashWindup = 0.0f;
|
||||||
|
dashLeft = 0.0f;
|
||||||
|
dashCooldownLeft = dashCooldown * GetGameTimeScale();
|
||||||
|
StopDashAnimation();
|
||||||
|
}
|
||||||
|
|
||||||
|
void CharacterBody::StopDashAnimation() {
|
||||||
|
if (actionAnimator && dashAnimation && actionAnimator->m_CurrentAnimation == dashAnimation && actionAnimator->isPlaying)
|
||||||
|
actionAnimator->StopAnimation();
|
||||||
|
}
|
||||||
|
|
||||||
|
void CharacterBody::ApplyMovement(bool grounded, float speedFactor, float dt, float referenceDt) {
|
||||||
|
dashCooldownLeft = std::max(dashCooldownLeft - dt, 0.0f);
|
||||||
|
|
||||||
|
// Dashing it goes where the dash sends it, fastest at the start and slowing to a stop, nothing else moves it
|
||||||
|
if (IsDashing()) {
|
||||||
|
wishDirection = glm::vec3(0.0f);
|
||||||
|
jumpWanted = false;
|
||||||
|
SetWalking(false);
|
||||||
|
if (!physics || dt <= 0.0f)
|
||||||
|
return;
|
||||||
|
|
||||||
|
// Winding up it stands where it is
|
||||||
|
if (dashWindup > 0.0f) {
|
||||||
|
physics->linearvelocity.x = 0.0f;
|
||||||
|
physics->linearvelocity.z = 0.0f;
|
||||||
|
dashWindup -= dt;
|
||||||
|
if (dashWindup > 0.0f)
|
||||||
|
return;
|
||||||
|
dt = -dashWindup; // whatever's left of this frame goes to the dash itself
|
||||||
|
dashWindup = 0.0f;
|
||||||
|
}
|
||||||
|
|
||||||
|
const float length = dashTime * GetGameTimeScale();
|
||||||
|
const float speed = 2.0f * dashDistance / length * (dashLeft / length); // falls off in a straight line, covering dashDistance
|
||||||
|
physics->linearvelocity.x = dashDirection.x * speed;
|
||||||
|
physics->linearvelocity.z = dashDirection.z * speed;
|
||||||
|
physics->WakeUp();
|
||||||
|
dashLeft -= dt;
|
||||||
|
if (dashLeft <= 0.0f) {
|
||||||
|
dashLeft = 0.0f;
|
||||||
|
dashCooldownLeft = dashCooldown * GetGameTimeScale();
|
||||||
|
}
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// An ability lunging it forward takes over, like a dash does. Once it's done, standing still brings it to a stop.
|
||||||
|
float lungeSpeed = 0.0f;
|
||||||
|
if (combat.GetAbilityLunge(lungeSpeed)) { // stunned, the ability is already cut off
|
||||||
|
wishDirection = glm::vec3(0.0f);
|
||||||
|
SetWalking(false);
|
||||||
|
if (physics) {
|
||||||
|
const glm::vec3 forward = GetForwardDirection();
|
||||||
|
physics->linearvelocity.x = forward.x * lungeSpeed;
|
||||||
|
physics->linearvelocity.z = forward.z * lungeSpeed;
|
||||||
|
physics->WakeUp();
|
||||||
|
}
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const bool moving = glm::length(wishDirection) > 0.0f && speedFactor > 0.0f;
|
||||||
|
SetWalking(moving);
|
||||||
|
// The rest of the dash animation is only there for standing still, walking or jumping out of it cuts it off
|
||||||
|
if (moving || jumpWanted)
|
||||||
|
StopDashAnimation();
|
||||||
|
// Sprinting only when it's free to go full speed, not mid attack
|
||||||
|
const float sprint = sprinting && speedFactor >= 1.0f ? sprintMultiplier : 1.0f;
|
||||||
|
|
||||||
|
// Straight along the input whichever way it's facing, turning to face it as it goes
|
||||||
|
const glm::vec3 walkDirection = wishDirection;
|
||||||
|
wishDirection = glm::vec3(0.0f); // used up, the next Move action has to ask again
|
||||||
|
const bool jump = jumpWanted;
|
||||||
|
jumpWanted = false;
|
||||||
|
|
||||||
|
// Just after taking off it's still close enough to the ground to count as on it, so it's in the air regardless
|
||||||
|
takeoffTime = std::max(takeoffTime - dt, 0.0f);
|
||||||
|
inAir = !grounded || takeoffTime > 0.0f;
|
||||||
|
|
||||||
|
// Starting a jump, if it's on the ground and not in the middle of an attack. The animation starts now,
|
||||||
|
// and it leaves the ground jumpDelay into it.
|
||||||
|
if (jump && canJump && !inAir && windup < 0.0f && physics && !combat.GetCurrentMove() && !combat.GetCurrentAbility()) {
|
||||||
|
windup = jumpDelay * GetGameTimeScale();
|
||||||
|
if (jumpAnimator && jumpAnimator->m_CurrentAnimation)
|
||||||
|
jumpAnimator->PlayAnimation();
|
||||||
|
}
|
||||||
|
if (windup >= 0.0f) {
|
||||||
|
windup -= dt;
|
||||||
|
if (windup < 0.0f && !inAir && physics) {
|
||||||
|
physics->linearvelocity.y = jumpSpeed;
|
||||||
|
physics->WakeUp();
|
||||||
|
inAir = true;
|
||||||
|
takeoffTime = 0.2f;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (walkAnimator && moving)
|
||||||
|
walkAnimator->playbackSpeed = speedFactor * statusSpeedMultiplier * (sprint > 1.0f ? sprintAnimationSpeed : 1.0f); // slower walking, slower steps
|
||||||
|
|
||||||
|
// With time stopped there's nothing to apply, and the rescaling below would divide zero by zero
|
||||||
|
if (!physics || dt <= 0.0f)
|
||||||
|
return;
|
||||||
|
|
||||||
|
const float movespeed = walkSpeed * speedFactor * sprint * statusSpeedMultiplier;
|
||||||
|
const glm::vec3 inputVel = moving ? walkDirection * movespeed : glm::vec3(0.0f);
|
||||||
|
|
||||||
|
const float accel = 40.0f;
|
||||||
|
const float damp = 0.75f;
|
||||||
|
|
||||||
|
if (grounded) {
|
||||||
|
// Damping and acceleration worked out per reference step, then rescaled so frame rate doesn't change them
|
||||||
|
const float dampStep = std::pow(damp, dt / referenceDt);
|
||||||
|
const float accelScale = referenceDt * damp * (1.0f - dampStep) / (dt * dampStep * (1.0f - damp));
|
||||||
|
physics->linearvelocity.x *= dampStep;
|
||||||
|
physics->linearvelocity.z *= dampStep;
|
||||||
|
physics->linearvelocity.x += inputVel.x * accel * dt * accelScale;
|
||||||
|
physics->linearvelocity.z += inputVel.z * accel * dt * accelScale;
|
||||||
|
}
|
||||||
|
// In the air it just carries on the way it was going when it left the ground
|
||||||
|
}
|
||||||
|
|
||||||
|
void CharacterBody::Step(float dt) {
|
||||||
|
if (glm::length(turnDirection) > 0.0001f) {
|
||||||
|
const glm::quat target = LookAt(-turnDirection);
|
||||||
|
const glm::quat current = t.GetRotationQuaternion();
|
||||||
|
const float blend = 1.0f - std::exp(-turnSpeed * dt);
|
||||||
|
t.RotateToQuaternion(glm::normalize(glm::slerp(current, target, blend)));
|
||||||
|
}
|
||||||
|
|
||||||
|
const float target = walking ? 1.0f : 0.0f;
|
||||||
|
walkBlend += (target - walkBlend) * (1.0f - std::exp(-walkFadeSpeed * dt));
|
||||||
|
const float airTarget = (inAir || windup >= 0.0f) && jumpAnimator && jumpAnimator->isPlaying ? 1.0f : 0.0f;
|
||||||
|
airBlend += (airTarget - airBlend) * (1.0f - std::exp(-airFadeSpeed * dt));
|
||||||
|
|
||||||
|
combat.Step(*this, dt);
|
||||||
|
// Clips play in real time, the way they were animated, rather than on the sped up game clock
|
||||||
|
const float animationDt = dt / GetGameTimeScale();
|
||||||
|
for (Animator* a : animators)
|
||||||
|
a->Step(animationDt);
|
||||||
|
|
||||||
|
// After stepping, since the animator ramps its own weight up to full when it plays
|
||||||
|
if (walkAnimator)
|
||||||
|
walkAnimator->blendWeight = walkBlend;
|
||||||
|
if (jumpAnimator) {
|
||||||
|
jumpAnimator->blendWeight = airBlend;
|
||||||
|
if (!inAir && windup < 0.0f && airBlend < 0.01f)
|
||||||
|
jumpAnimator->isPlaying = false; // landed and faded out, off until the next jump
|
||||||
|
}
|
||||||
|
|
||||||
|
if (rig && animator->m_CurrentAnimation) {
|
||||||
|
blendedBones = animator->GetFinalBoneMatrices();
|
||||||
|
BlendBones(animator->m_CurrentAnimation->GetRootNode(), glm::mat4(1.0f));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Blends two parent-relative transforms, position and scale linearly and rotation along the shortest arc
|
||||||
|
static glm::mat4 BlendLocalTransforms(const glm::mat4& from, const glm::mat4& to, float t) {
|
||||||
|
const glm::vec3 fromScale(glm::length(glm::vec3(from[0])), glm::length(glm::vec3(from[1])), glm::length(glm::vec3(from[2])));
|
||||||
|
const glm::vec3 toScale(glm::length(glm::vec3(to[0])), glm::length(glm::vec3(to[1])), glm::length(glm::vec3(to[2])));
|
||||||
|
const glm::mat3 fromRotation(glm::vec3(from[0]) / fromScale.x, glm::vec3(from[1]) / fromScale.y, glm::vec3(from[2]) / fromScale.z);
|
||||||
|
const glm::mat3 toRotation(glm::vec3(to[0]) / toScale.x, glm::vec3(to[1]) / toScale.y, glm::vec3(to[2]) / toScale.z);
|
||||||
|
|
||||||
|
const glm::quat rotation = glm::normalize(glm::slerp(glm::quat_cast(fromRotation), glm::quat_cast(toRotation), t));
|
||||||
|
glm::mat4 result = glm::mat4_cast(rotation) * glm::scale(glm::mat4(1.0f), glm::mix(fromScale, toScale, t));
|
||||||
|
result[3] = glm::vec4(glm::mix(glm::vec3(from[3]), glm::vec3(to[3]), t), 1.0f);
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Every node's rest transform relative to the model, and its parent's name, from one clip's hierarchy
|
||||||
|
static void CollectRestPose(const AssimpNodeData& node, const glm::mat4& parentTransform, const std::string& parentName,
|
||||||
|
std::unordered_map<std::string, glm::mat4>& globals, std::unordered_map<std::string, std::string>& parents) {
|
||||||
|
const glm::mat4 global = parentTransform * node.transformation;
|
||||||
|
globals[node.name] = global;
|
||||||
|
parents[node.name] = parentName;
|
||||||
|
for (const AssimpNodeData& child : node.children)
|
||||||
|
CollectRestPose(child, global, node.name, globals, parents);
|
||||||
|
}
|
||||||
|
|
||||||
|
// A clip exported with only some bones, like an arm on its own, has them hung off whatever ancestor made it into
|
||||||
|
// the file. Its tracks are then relative to that ancestor instead of the bone's real parent. Going through the rest
|
||||||
|
// pose of both files turns one into the other, so the arm still rides on the torso.
|
||||||
|
const std::unordered_map<std::string, glm::mat4>& CharacterBody::GetParentCorrections(const Animation* layer) {
|
||||||
|
const Animation* base = animator->m_CurrentAnimation;
|
||||||
|
const auto key = std::make_pair(base, layer);
|
||||||
|
const auto cached = parentCorrections.find(key);
|
||||||
|
if (cached != parentCorrections.end())
|
||||||
|
return cached->second;
|
||||||
|
|
||||||
|
std::unordered_map<std::string, glm::mat4>& corrections = parentCorrections[key];
|
||||||
|
if (!base || !layer || base == layer)
|
||||||
|
return corrections;
|
||||||
|
|
||||||
|
std::unordered_map<std::string, glm::mat4> baseGlobals, layerGlobals;
|
||||||
|
std::unordered_map<std::string, std::string> baseParents, layerParents;
|
||||||
|
CollectRestPose(base->GetRootNode(), glm::mat4(1.0f), "", baseGlobals, baseParents);
|
||||||
|
CollectRestPose(layer->GetRootNode(), glm::mat4(1.0f), "", layerGlobals, layerParents);
|
||||||
|
|
||||||
|
for (const std::string& bone : layer->m_BoneNames) {
|
||||||
|
const auto baseParent = baseParents.find(bone);
|
||||||
|
const auto layerParent = layerParents.find(bone);
|
||||||
|
if (baseParent == baseParents.end() || layerParent == layerParents.end())
|
||||||
|
continue;
|
||||||
|
if (baseParent->second == layerParent->second)
|
||||||
|
continue;
|
||||||
|
|
||||||
|
const auto baseParentGlobal = baseGlobals.find(baseParent->second);
|
||||||
|
const auto layerParentGlobal = layerGlobals.find(layerParent->second);
|
||||||
|
const glm::mat4 from = layerParentGlobal != layerGlobals.end() ? layerParentGlobal->second : glm::mat4(1.0f);
|
||||||
|
const glm::mat4 to = baseParentGlobal != baseGlobals.end() ? baseParentGlobal->second : glm::mat4(1.0f);
|
||||||
|
corrections[bone] = glm::inverse(to) * from;
|
||||||
|
}
|
||||||
|
return corrections;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Walks the base animation's hierarchy. Each bone starts from its rest pose, then every layer with a track for it
|
||||||
|
// blends in on top in weight order, before it's passed down to its children.
|
||||||
|
void CharacterBody::BlendBones(const AssimpNodeData& node, const glm::mat4& parentTransform) {
|
||||||
|
glm::mat4 local = node.transformation;
|
||||||
|
for (Animator* a : animators) {
|
||||||
|
if (!a->m_CurrentAnimation || a->blendWeight <= 0.0f)
|
||||||
|
continue;
|
||||||
|
if (a != animator && !a->isPlaying)
|
||||||
|
continue;
|
||||||
|
const auto& transforms = a->GetLocalTransforms();
|
||||||
|
const auto found = transforms.find(node.name);
|
||||||
|
if (found == transforms.end())
|
||||||
|
continue;
|
||||||
|
|
||||||
|
glm::mat4 layerLocal = found->second;
|
||||||
|
if (a != animator) {
|
||||||
|
const auto& corrections = GetParentCorrections(a->m_CurrentAnimation);
|
||||||
|
const auto correction = corrections.find(node.name);
|
||||||
|
if (correction != corrections.end())
|
||||||
|
layerLocal = correction->second * layerLocal;
|
||||||
|
}
|
||||||
|
|
||||||
|
local = a == animator || a->blendWeight >= 1.0f
|
||||||
|
? layerLocal
|
||||||
|
: BlendLocalTransforms(local, layerLocal, a->blendWeight);
|
||||||
|
}
|
||||||
|
|
||||||
|
glm::mat4 global = parentTransform * local;
|
||||||
|
if (node.name == "Head")
|
||||||
|
global = glm::scale(global, glm::vec3(HeadScale));
|
||||||
|
|
||||||
|
const auto info = rig->m_BoneInfoMap.find(node.name);
|
||||||
|
if (info != rig->m_BoneInfoMap.end()) {
|
||||||
|
const int index = info->second.id;
|
||||||
|
if (index >= 0 && index < (int)blendedBones.size())
|
||||||
|
blendedBones[index] = global * info->second.offset;
|
||||||
|
}
|
||||||
|
|
||||||
|
for (const AssimpNodeData& child : node.children)
|
||||||
|
BlendBones(child, global);
|
||||||
|
}
|
||||||
|
|
||||||
|
void CharacterBody::SetWalking(bool isWalking) {
|
||||||
|
walking = isWalking;
|
||||||
|
}
|
||||||
|
|
||||||
|
void CharacterBody::Face(const glm::vec3& direction, bool instantly) {
|
||||||
|
const glm::vec3 flat(direction.x, 0.0f, direction.z);
|
||||||
|
if (glm::length(flat) <= 0.0001f)
|
||||||
|
return;
|
||||||
|
turnDirection = glm::normalize(flat);
|
||||||
|
if (instantly)
|
||||||
|
t.RotateToQuaternion(LookAt(-turnDirection));
|
||||||
|
}
|
||||||
|
|
||||||
|
glm::vec3 CharacterBody::GetForwardDirection() const {
|
||||||
|
glm::vec3 forward = -t.GetFrontVector();
|
||||||
|
forward.y = 0.0f;
|
||||||
|
const float length = glm::length(forward);
|
||||||
|
if (length <= 0.0001f)
|
||||||
|
return glm::vec3(0.0f, 0.0f, -1.0f);
|
||||||
|
return forward / length;
|
||||||
|
}
|
||||||
|
|
||||||
|
// -------------------------------------------------------
|
||||||
|
// Character
|
||||||
|
// -------------------------------------------------------
|
||||||
|
|
||||||
|
void Character::Think() {
|
||||||
|
if (!mind || dead)
|
||||||
|
return;
|
||||||
|
pendingActions.clear();
|
||||||
|
mind->ProduceActions(*this, pendingActions); // still asked while stunned, so it keeps track of things
|
||||||
|
if (!CanAct())
|
||||||
|
return;
|
||||||
|
for (const CharacterAction& action : pendingActions)
|
||||||
|
body->Perform(action);
|
||||||
|
}
|
||||||
|
|
||||||
|
int Character::GetCollisionGroup() {
|
||||||
|
// Kept clear of the ragdoll's groups, which count up from 1
|
||||||
|
static int nextGroup = 1000;
|
||||||
|
if (collisionGroup == 0)
|
||||||
|
collisionGroup = nextGroup++;
|
||||||
|
return collisionGroup;
|
||||||
|
}
|
||||||
|
|
||||||
|
void Character::CreatePhysicsBody(Physics& physics, float massKg) {
|
||||||
|
if (body->physics)
|
||||||
|
return;
|
||||||
|
p* physicsBody = new p(body);
|
||||||
|
physicsBody->mass = massKg;
|
||||||
|
physicsBody->lockRotation = true; // it turns by facing where it walks, never by tipping over
|
||||||
|
physicsBody->friction = 0.0f; // walking does its own stopping
|
||||||
|
physicsBody->collisionGroup = GetCollisionGroup();
|
||||||
|
physics.AddObject(physicsBody);
|
||||||
|
body->physics = physicsBody;
|
||||||
|
world = &physics;
|
||||||
|
}
|
||||||
|
|
||||||
|
void Character::EnableRagdoll(Physics& physics) {
|
||||||
|
if (!ragdoll)
|
||||||
|
ragdoll = new Ragdoll(*body, GetRigMatrix(), physics);
|
||||||
|
}
|
||||||
|
|
||||||
|
void Character::Stun(float seconds) {
|
||||||
|
if (dead || seconds <= 0.0f)
|
||||||
|
return;
|
||||||
|
const float length = seconds * GetGameTimeScale();
|
||||||
|
if (length <= stunLeft)
|
||||||
|
return;
|
||||||
|
stunLeft = length;
|
||||||
|
body->combat.Reset();
|
||||||
|
body->CancelDash();
|
||||||
|
}
|
||||||
|
|
||||||
|
void Character::AddStatus(StatusEffect::Type type, float seconds) {
|
||||||
|
for (StatusEffect& status : statuses) {
|
||||||
|
if (status.type != type)
|
||||||
|
continue;
|
||||||
|
if (status.timeLeft >= 0.0f && (seconds < 0.0f || seconds > status.timeLeft))
|
||||||
|
status.timeLeft = seconds;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
statuses.push_back({ type, seconds });
|
||||||
|
}
|
||||||
|
|
||||||
|
void Character::RemoveStatus(StatusEffect::Type type) {
|
||||||
|
statuses.erase(std::remove_if(statuses.begin(), statuses.end(),
|
||||||
|
[type](const StatusEffect& status) { return status.type == type; }), statuses.end());
|
||||||
|
}
|
||||||
|
|
||||||
|
void Character::Knockdown(float seconds) {
|
||||||
|
if (dead || ragdoll || seconds <= 0.0f)
|
||||||
|
return;
|
||||||
|
AddStatus(StatusEffect::Type::Knockdown, seconds);
|
||||||
|
body->combat.Reset();
|
||||||
|
body->CancelDash();
|
||||||
|
body->SetWalking(false);
|
||||||
|
body->PlayAction(body->knockdownAnimation, 1.0f, true);
|
||||||
|
wasKnockedDown = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
bool Character::CanAct() const {
|
||||||
|
if (IsStunned())
|
||||||
|
return false;
|
||||||
|
for (const StatusEffect& status : statuses)
|
||||||
|
if (GetStatusEffectInfo(status.type).preventsActing)
|
||||||
|
return false;
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
bool Character::DodgesHits() const {
|
||||||
|
for (const StatusEffect& status : statuses)
|
||||||
|
if (GetStatusEffectInfo(status.type).dodgesHits)
|
||||||
|
return true;
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
bool Character::HasStatus(StatusEffect::Type type) const {
|
||||||
|
for (const StatusEffect& status : statuses)
|
||||||
|
if (status.type == type)
|
||||||
|
return true;
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
void Character::Step(float dt) {
|
||||||
|
stunLeft = std::max(stunLeft - dt, 0.0f);
|
||||||
|
const float realDt = dt / GetGameTimeScale();
|
||||||
|
for (StatusEffect& status : statuses)
|
||||||
|
if (status.timeLeft >= 0.0f)
|
||||||
|
status.timeLeft = std::max(status.timeLeft - realDt, 0.0f);
|
||||||
|
statuses.erase(std::remove_if(statuses.begin(), statuses.end(),
|
||||||
|
[](const StatusEffect& status) { return status.timeLeft == 0.0f; }), statuses.end());
|
||||||
|
|
||||||
|
// Back up, out of the pose it was lying in
|
||||||
|
const bool knockedDown = IsKnockedDown();
|
||||||
|
if (wasKnockedDown && !knockedDown && body->actionAnimator && body->knockdownAnimation
|
||||||
|
&& body->actionAnimator->m_CurrentAnimation == body->knockdownAnimation) {
|
||||||
|
body->actionAnimator->holdsAtEnd = false;
|
||||||
|
body->actionAnimator->StopAnimation();
|
||||||
|
}
|
||||||
|
wasKnockedDown = knockedDown;
|
||||||
|
if (ragdoll) {
|
||||||
|
ragdoll->Update(dt);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
body->Step(dt);
|
||||||
|
}
|
||||||
|
|
||||||
|
void Character::ApplyMovement(bool grounded, float dt, float referenceDt) {
|
||||||
|
// Swinging and abilities slow it down
|
||||||
|
const AttackMove* attack = body->combat.GetCurrentMove();
|
||||||
|
const Ability* ability = body->combat.GetCurrentAbility();
|
||||||
|
const float speed = IsStunned() ? 0.0f : ability ? ability->moveSpeedMultiplier : attack ? attack->moveSpeedMultiplier : 1.0f;
|
||||||
|
|
||||||
|
// Status effects all multiply together
|
||||||
|
float statusSpeed = 1.0f, gravity = 1.0f;
|
||||||
|
bool canJump = true;
|
||||||
|
for (const StatusEffect& status : statuses) {
|
||||||
|
const StatusEffectInfo& info = GetStatusEffectInfo(status.type);
|
||||||
|
statusSpeed *= info.speedMultiplier;
|
||||||
|
gravity *= info.gravityMultiplier;
|
||||||
|
canJump = canJump && !info.preventsJumping;
|
||||||
|
}
|
||||||
|
body->statusSpeedMultiplier = statusSpeed;
|
||||||
|
body->canJump = canJump;
|
||||||
|
body->ApplyMovement(grounded, speed, dt, referenceDt);
|
||||||
|
|
||||||
|
// Physics already pulls it down once, this is the rest
|
||||||
|
if (!grounded && gravity != 1.0f && world && body->physics)
|
||||||
|
body->physics->linearvelocity += world->GetGravity() * (gravity - 1.0f) * dt;
|
||||||
|
}
|
||||||
|
|
||||||
|
float Character::TakeDamage(float amount) {
|
||||||
|
if (dead || amount <= 0.0f)
|
||||||
|
return 0.0f;
|
||||||
|
health = std::min(health, maxHealth);
|
||||||
|
const float taken = std::min(amount, health);
|
||||||
|
health -= taken;
|
||||||
|
if (health <= 0.0f)
|
||||||
|
Die();
|
||||||
|
return taken;
|
||||||
|
}
|
||||||
|
|
||||||
|
float Character::Heal(float amount) {
|
||||||
|
if (dead || amount <= 0.0f)
|
||||||
|
return 0.0f;
|
||||||
|
const float before = health;
|
||||||
|
health = std::min(health + amount, maxHealth);
|
||||||
|
return std::max(health - before, 0.0f);
|
||||||
|
}
|
||||||
|
|
||||||
|
void Character::Flinch() {
|
||||||
|
if (dead || ragdoll || !body->flinchAnimation || IsKnockedDown()) // on the floor it stays lying there
|
||||||
|
return;
|
||||||
|
// It shares the action layer with attacks, so a punch can't carry on underneath it
|
||||||
|
body->combat.Reset();
|
||||||
|
body->CancelDash();
|
||||||
|
body->PlayAction(body->flinchAnimation);
|
||||||
|
}
|
||||||
|
|
||||||
|
void Character::Die() {
|
||||||
|
if (dead)
|
||||||
|
return;
|
||||||
|
|
||||||
|
// Some just pick themselves straight back up, like training dummies
|
||||||
|
if (healsOnDeath) {
|
||||||
|
health = maxHealth;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Stops where it is. Whatever it was doing is cut off, and it stops thinking until it's revived.
|
||||||
|
dead = true;
|
||||||
|
deaths++;
|
||||||
|
health = 0.0f;
|
||||||
|
body->SetWalking(false);
|
||||||
|
body->combat.Reset();
|
||||||
|
body->CancelDash();
|
||||||
|
}
|
||||||
|
|
||||||
|
void Character::Revive() {
|
||||||
|
if (!dead)
|
||||||
|
return;
|
||||||
|
delete ragdoll;
|
||||||
|
ragdoll = nullptr;
|
||||||
|
dead = false;
|
||||||
|
health = maxHealth;
|
||||||
|
stunLeft = 0.0f;
|
||||||
|
// Timed status effects are gone, ones that last forever stay
|
||||||
|
statuses.erase(std::remove_if(statuses.begin(), statuses.end(),
|
||||||
|
[](const StatusEffect& status) { return status.timeLeft >= 0.0f; }), statuses.end());
|
||||||
|
body->combat.Reset();
|
||||||
|
|
||||||
|
if (p* box = body->physics) {
|
||||||
|
box->EnableCollision();
|
||||||
|
box->EnableVelocity();
|
||||||
|
box->linearvelocity = glm::vec3(0.0f);
|
||||||
|
box->WakeUp();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
bool Character::GetActionBoneTransform(const Animation* animation, const std::string& bone, glm::mat4& transform) const {
|
||||||
|
const Animator* action = body ? body->actionAnimator : nullptr;
|
||||||
|
if (ragdoll || !action || !action->isPlaying || !animation || action->m_CurrentAnimation != animation || !body->rig)
|
||||||
|
return false;
|
||||||
|
const auto info = body->rig->m_BoneInfoMap.find(bone);
|
||||||
|
if (info == body->rig->m_BoneInfoMap.end())
|
||||||
|
return false;
|
||||||
|
const std::vector<glm::mat4>& bones = action->GetFinalBoneMatrices();
|
||||||
|
const int index = info->second.id;
|
||||||
|
if (index < 0 || index >= (int)bones.size())
|
||||||
|
return false;
|
||||||
|
|
||||||
|
// The skinning matrix takes the bone's rest pose to where it is now, so undoing the rest pose leaves where it is
|
||||||
|
transform = GetRigMatrix() * bones[index] * glm::inverse(info->second.offset);
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
bool Character::GetBoneTransform(const std::string& bone, glm::mat4& transform) const {
|
||||||
|
if (ragdoll || !body || !body->rig)
|
||||||
|
return false;
|
||||||
|
const auto info = body->rig->m_BoneInfoMap.find(bone);
|
||||||
|
if (info == body->rig->m_BoneInfoMap.end())
|
||||||
|
return false;
|
||||||
|
const std::vector<glm::mat4>& bones = body->GetBlendedBoneMatrices();
|
||||||
|
const int index = info->second.id;
|
||||||
|
if (index < 0 || index >= (int)bones.size())
|
||||||
|
return false;
|
||||||
|
transform = GetRigMatrix() * bones[index] * glm::inverse(info->second.offset);
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
glm::mat4 Character::GetRigMatrix() const {
|
||||||
|
// body->t's scale is the hitbox only; the rig is drawn at visualScale so it fills that hitbox
|
||||||
|
return body->t.GetTranslationMatrix() * body->t.GetRotationMatrix()
|
||||||
|
* glm::scale(glm::mat4(1.0f), glm::vec3(visualScale));
|
||||||
|
}
|
||||||
|
|
||||||
|
bool Character::GetWorldBounds(BoundingBox& bounds) const {
|
||||||
|
if (!body)
|
||||||
|
return false;
|
||||||
|
|
||||||
|
if (ragdoll && !ragdoll->GetParts().empty()) {
|
||||||
|
bounds.min = glm::vec3(FLT_MAX);
|
||||||
|
bounds.max = glm::vec3(-FLT_MAX);
|
||||||
|
for (const Ragdoll::Part* part : ragdoll->GetParts()) {
|
||||||
|
const BoundingBox partBounds = part->transform.t.GetAABB();
|
||||||
|
bounds.min = glm::min(bounds.min, partBounds.min);
|
||||||
|
bounds.max = glm::max(bounds.max, partBounds.max);
|
||||||
|
}
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
// The rig is about 2 units across natively in every direction it reaches, arms out included.
|
||||||
|
// A cube that size around the body covers any pose and any turn.
|
||||||
|
const glm::vec3 reach(visualScale);
|
||||||
|
bounds.min = body->t.GetTranslation() - reach;
|
||||||
|
bounds.max = body->t.GetTranslation() + reach;
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
void Character::ApplyTemplate(const CharacterTemplate& from) {
|
||||||
|
characterTemplate = &from;
|
||||||
|
|
||||||
|
if (from.rig)
|
||||||
|
body->SetRig(from.rig);
|
||||||
|
if (from.idleAnimation) {
|
||||||
|
body->SetAnimation(from.idleAnimation);
|
||||||
|
body->animator->loops = true;
|
||||||
|
}
|
||||||
|
if (from.walkAnimation)
|
||||||
|
body->SetWalkAnimation(from.walkAnimation);
|
||||||
|
body->jumpDelay = from.jumpDelay;
|
||||||
|
if (from.jumpAnimation)
|
||||||
|
body->SetJumpAnimation(from.jumpAnimation);
|
||||||
|
else if (body->jumpAnimator)
|
||||||
|
body->jumpAnimator->ClearAnimation(); // jumps without one
|
||||||
|
body->flinchAnimation = from.flinchAnimation;
|
||||||
|
body->knockdownAnimation = from.knockdownAnimation;
|
||||||
|
|
||||||
|
body->combat.Reset();
|
||||||
|
body->combat.combo = from.combo;
|
||||||
|
body->combat.comboName = from.comboName;
|
||||||
|
body->combat.abilities = from.abilities;
|
||||||
|
healsOnDeath = from.healsOnDeath;
|
||||||
|
body->walkSpeed = from.walkSpeed;
|
||||||
|
body->sprintMultiplier = from.sprintMultiplier;
|
||||||
|
body->dashAnimation = from.dashAnimation;
|
||||||
|
body->dashDistance = from.dashDistance;
|
||||||
|
body->dashDelay = from.dashDelay;
|
||||||
|
body->dashTime = from.dashTime;
|
||||||
|
body->dashCooldown = from.dashCooldown;
|
||||||
|
|
||||||
|
maxHealth = from.maxHealth;
|
||||||
|
if (!dead)
|
||||||
|
health = maxHealth;
|
||||||
|
}
|
||||||
|
|
||||||
|
Character::Character() {
|
||||||
|
body = new CharacterBody();
|
||||||
|
mind = nullptr;
|
||||||
|
shadertype = ShaderType::RigShader;
|
||||||
|
}
|
||||||
|
|
||||||
|
void Character::Render(Shader& shader) {
|
||||||
|
if (!body || !body->rig || !body->animator) return;
|
||||||
|
const std::vector<glm::mat4> transforms = ragdoll ? ragdoll->GetBoneMatrices()
|
||||||
|
: body->animator->m_CurrentAnimation ? body->GetBlendedBoneMatrices()
|
||||||
|
: std::vector<glm::mat4>(100, glm::mat4(1.0f));
|
||||||
|
shader.SetMat4Array("finalBonesMatrices", transforms.data(), glm::min((int)transforms.size(), 100));
|
||||||
|
shader.Set4F("colorOverride", colorOverride);
|
||||||
|
surface.Apply(shader, 1, glm::vec3(visualScale));
|
||||||
|
shader.Set1F("metallic", metallic);
|
||||||
|
shader.Set1F("roughness", roughness);
|
||||||
|
shader.Set1F("ao", ao);
|
||||||
|
body->rig->Render(shader, ragdoll ? glm::mat4(1.0f) : GetRigMatrix()); // ragdoll bones are already in world space
|
||||||
|
shader.Set4F("colorOverride", glm::vec4(0.0f)); // don't leak into other rig draws
|
||||||
|
}
|
||||||
+283
@@ -0,0 +1,283 @@
|
|||||||
|
#ifndef CHARACTER_CLASS
|
||||||
|
#define CHARACTER_CLASS
|
||||||
|
#include <vector>
|
||||||
|
#include <string>
|
||||||
|
#include <map>
|
||||||
|
#include <unordered_map>
|
||||||
|
#include <algorithm>
|
||||||
|
#include "t.h"
|
||||||
|
#include "Rig.h"
|
||||||
|
#include "Object.h"
|
||||||
|
#include "Animator.h"
|
||||||
|
#include "CharacterAction.h"
|
||||||
|
#include "Combat.h"
|
||||||
|
#include "CharacterTemplate.h"
|
||||||
|
#include "StatusEffect.h"
|
||||||
|
|
||||||
|
class Character;
|
||||||
|
class CharacterManager;
|
||||||
|
class CharacterMind;
|
||||||
|
class Ragdoll;
|
||||||
|
class Physics;
|
||||||
|
class p;
|
||||||
|
|
||||||
|
// The physical side of a character: where it is, its physics body, its animation,
|
||||||
|
// and carrying out whatever actions its mind sends it.
|
||||||
|
class CharacterBody : public t_package {
|
||||||
|
public:
|
||||||
|
Rig* rig = nullptr;
|
||||||
|
p* physics = nullptr; // made by Character::CreatePhysicsBody, its box is t's scale
|
||||||
|
|
||||||
|
std::vector<Animator*> animators;
|
||||||
|
Animator* animator = nullptr; // the base layer, always fully on: standing idle
|
||||||
|
Animator* walkAnimator = nullptr; // on top of the idles, faded in while walking
|
||||||
|
Animator* jumpAnimator = nullptr; // on top of walking, played from the start each jump and held while in the air
|
||||||
|
Animator* actionAnimator = nullptr; // one-shots on top of everything, see PlayAction
|
||||||
|
CharacterCombat combat; // its attacks, played on the action layer
|
||||||
|
Animation* flinchAnimation = nullptr; // played on the action layer when it's hit, see Character::Flinch
|
||||||
|
Animation* knockdownAnimation = nullptr; // played on the action layer when it's knocked down, held at the end
|
||||||
|
Animation* dashAnimation = nullptr; // played on the action layer while it dashes
|
||||||
|
|
||||||
|
float walkSpeed = 1.17f; // m/s on flat ground
|
||||||
|
float turnSpeed = 12.0f; // how quickly it turns to face where it's walking
|
||||||
|
float walkFadeSpeed = 8.0f; // how quickly it fades between idle and walking
|
||||||
|
float jumpSpeed = 10.0f; // m/s straight up when it jumps, about 5 m high
|
||||||
|
float jumpDelay = 0.0f; // real seconds into the jump animation it actually leaves the ground
|
||||||
|
float airFadeSpeed = 14.0f; // how quickly the jump fades in on takeoff and out on landing
|
||||||
|
float sprintMultiplier = 1.4f; // times walkSpeed while sprinting
|
||||||
|
float sprintAnimationSpeed = 1.25f; // how much faster the walk plays while sprinting
|
||||||
|
float dashDistance = 4.0f; // m covered by a dash
|
||||||
|
float dashDelay = 0.0f; // real seconds into the dash animation it starts moving
|
||||||
|
float dashTime = 0.4f; // real seconds a dash carries it for, after which it's free to do anything again
|
||||||
|
float dashCooldown = 0.5f; // real seconds after one dash ends before the next
|
||||||
|
float statusSpeedMultiplier = 1.0f; // from its status effects, set by Character every frame
|
||||||
|
bool canJump = true; // likewise
|
||||||
|
|
||||||
|
CharacterBody() {
|
||||||
|
animator = AddAnimator(0);
|
||||||
|
animator->blendWeight = 1.0f; // base always fully visible
|
||||||
|
}
|
||||||
|
|
||||||
|
Animator* AddAnimator(int weight) {
|
||||||
|
Animator* a = new Animator();
|
||||||
|
a->weight = weight;
|
||||||
|
if (rig) a->SetRig(rig);
|
||||||
|
animators.push_back(a);
|
||||||
|
std::sort(animators.begin(), animators.end(),
|
||||||
|
[](Animator* a, Animator* b) { return a->weight < b->weight; });
|
||||||
|
return a;
|
||||||
|
}
|
||||||
|
|
||||||
|
// The skinning matrices for this frame, from every animator layered together. Worked out in Step.
|
||||||
|
// Base (weight 0) is the foundation; each higher-weight animator blends in by its blendWeight,
|
||||||
|
// but only on the bones its animation has tracks for, so an arm-only clip leaves the legs walking.
|
||||||
|
const std::vector<glm::mat4>& GetBlendedBoneMatrices() const {
|
||||||
|
return blendedBones.empty() ? animator->GetFinalBoneMatrices() : blendedBones;
|
||||||
|
}
|
||||||
|
|
||||||
|
void SetRig(Rig* r) {
|
||||||
|
rig = r;
|
||||||
|
for (Animator* a : animators)
|
||||||
|
a->SetRig(r);
|
||||||
|
}
|
||||||
|
|
||||||
|
void SetAnimation(Animation* anim) {
|
||||||
|
animator->SetAnimation(anim);
|
||||||
|
animator->PlayAnimation();
|
||||||
|
animator->blendWeight = 1.0f; // base never blends, always instant
|
||||||
|
}
|
||||||
|
|
||||||
|
// Plays an animation once on top of everything else. Starts over if it's already playing.
|
||||||
|
// speed scales how fast it plays, 2 takes half as long. Holding, it stays on its last pose until stopped.
|
||||||
|
void PlayAction(Animation* anim, float speed = 1.0f, bool hold = false) {
|
||||||
|
if (!anim)
|
||||||
|
return;
|
||||||
|
if (!actionAnimator) {
|
||||||
|
actionAnimator = AddAnimator(4);
|
||||||
|
actionAnimator->blendDuration = 0.1f;
|
||||||
|
}
|
||||||
|
actionAnimator->loops = false;
|
||||||
|
actionAnimator->holdsAtEnd = hold;
|
||||||
|
actionAnimator->playbackSpeed = speed;
|
||||||
|
actionAnimator->SetAnimation(anim);
|
||||||
|
actionAnimator->PlayAnimation();
|
||||||
|
}
|
||||||
|
|
||||||
|
// The loop played while walking. It keeps running underneath so it picks up mid-stride.
|
||||||
|
void SetWalkAnimation(Animation* anim) {
|
||||||
|
if (!walkAnimator)
|
||||||
|
walkAnimator = AddAnimator(2);
|
||||||
|
walkAnimator->loops = true;
|
||||||
|
walkAnimator->SetAnimation(anim);
|
||||||
|
walkAnimator->PlayAnimation();
|
||||||
|
walkAnimator->blendWeight = walkBlend;
|
||||||
|
}
|
||||||
|
|
||||||
|
// The clip for jumping. It plays once from takeoff and holds its last pose until it lands.
|
||||||
|
void SetJumpAnimation(Animation* anim) {
|
||||||
|
if (!jumpAnimator)
|
||||||
|
jumpAnimator = AddAnimator(3);
|
||||||
|
jumpAnimator->loops = false;
|
||||||
|
jumpAnimator->holdsAtEnd = true;
|
||||||
|
jumpAnimator->SetAnimation(anim);
|
||||||
|
jumpAnimator->isPlaying = false; // until it jumps
|
||||||
|
jumpAnimator->blendWeight = 0.0f;
|
||||||
|
}
|
||||||
|
|
||||||
|
bool IsInAir() const { return inAir; }
|
||||||
|
bool IsDashing() const { return dashWindup > 0.0f || dashLeft > 0.0f; }
|
||||||
|
bool IsSprinting() const { return sprinting; }
|
||||||
|
|
||||||
|
// Stops a dash where it is, like when it's hit
|
||||||
|
void CancelDash();
|
||||||
|
void StopDashAnimation();
|
||||||
|
|
||||||
|
// Carries out one action. Movement actions only count for the frame they arrive in.
|
||||||
|
void Perform(const CharacterAction& action);
|
||||||
|
|
||||||
|
// Pushes the physics body toward walking speed along wherever it faces, from this frame's Move action.
|
||||||
|
// speedFactor scales it, 0 stops it. The walk animation plays faster or slower to match.
|
||||||
|
void ApplyMovement(bool grounded, float speedFactor, float dt, float referenceDt);
|
||||||
|
|
||||||
|
// What this frame's Move action asked for, until ApplyMovement uses it up
|
||||||
|
bool IsTryingToMove() const { return glm::length(wishDirection) > 0.0f; }
|
||||||
|
|
||||||
|
// Turning and animation
|
||||||
|
void Step(float dt);
|
||||||
|
|
||||||
|
void SetWalking(bool walking);
|
||||||
|
glm::vec3 GetForwardDirection() const;
|
||||||
|
|
||||||
|
// Turns to face along direction, flattened. Walking turns it too, this is for while it stands still.
|
||||||
|
// Instantly snaps round rather than turning over a few frames, for things that have to go the right way at once.
|
||||||
|
void Face(const glm::vec3& direction, bool instantly = false);
|
||||||
|
|
||||||
|
private:
|
||||||
|
glm::vec3 wishDirection = glm::vec3(0.0f); // from this frame's Move action
|
||||||
|
bool strafing = false; // facing somewhere it was told to, walking any way
|
||||||
|
glm::vec3 turnDirection = glm::vec3(0.0f); // where it's turning to face, kept after it stops
|
||||||
|
bool walking = false;
|
||||||
|
float walkBlend = 0.0f; // 0 = idle, 1 = walking
|
||||||
|
bool jumpWanted = false; // from this frame's Jump action, until ApplyMovement uses it up
|
||||||
|
bool inAir = false;
|
||||||
|
float takeoffTime = 0.0f; // seconds it still counts as in the air after jumping, while the ground check catches up
|
||||||
|
float windup = -1.0f; // game seconds until it leaves the ground, below 0 when it isn't about to jump
|
||||||
|
float airBlend = 0.0f; // 0 = on the ground, 1 = the jump fully showing
|
||||||
|
bool sprinting = false; // from this frame's Move action
|
||||||
|
float dashWindup = 0.0f; // game seconds until the dash starts moving it
|
||||||
|
float dashLeft = 0.0f; // game seconds of dash left, 0 when it isn't dashing
|
||||||
|
float dashCooldownLeft = 0.0f; // game seconds until it can dash again
|
||||||
|
glm::vec3 dashDirection = glm::vec3(0.0f);
|
||||||
|
std::vector<glm::mat4> blendedBones;
|
||||||
|
|
||||||
|
// Per (base clip, layer clip): bone name -> what turns the layer's track into one relative to the bone's
|
||||||
|
// real parent. Only bones whose parent differs between the two files have an entry.
|
||||||
|
std::map<std::pair<const Animation*, const Animation*>, std::unordered_map<std::string, glm::mat4>> parentCorrections;
|
||||||
|
|
||||||
|
void BlendBones(const AssimpNodeData& node, const glm::mat4& parentTransform);
|
||||||
|
const std::unordered_map<std::string, glm::mat4>& GetParentCorrections(const Animation* layer);
|
||||||
|
};
|
||||||
|
|
||||||
|
class Character : public Object, public Renderable {
|
||||||
|
public:
|
||||||
|
CharacterMind* mind = nullptr; // what decides its actions, nullptr does nothing
|
||||||
|
CharacterBody* body = nullptr;
|
||||||
|
const CharacterTemplate* characterTemplate = nullptr; // the kind of fighter it is, see ApplyTemplate
|
||||||
|
std::string displayName; // shown over its head and in menus, its name when empty
|
||||||
|
Ragdoll* ragdoll = nullptr; // when set, physics drives the rig instead of the animators
|
||||||
|
SurfaceTexture surface;
|
||||||
|
glm::vec4 colorOverride = glm::vec4(0.0f); // rgb + strength (0 = keep the rig's own texture)
|
||||||
|
float visualScale = 1.4f; // rig is ~1.8 tall natively; its physics box is 2.5
|
||||||
|
float metallic = 0.0f;
|
||||||
|
float roughness = 0.7f;
|
||||||
|
float ao = 1.0f;
|
||||||
|
|
||||||
|
float maxHealth = 100.0f;
|
||||||
|
bool healsOnDeath = false; // dying puts it straight back to full health instead
|
||||||
|
|
||||||
|
float GetHealth() const { return health; }
|
||||||
|
int GetDeaths() const { return deaths; } // how many times it's gone down, so things on it can tell it respawned
|
||||||
|
bool IsDead() const { return dead; }
|
||||||
|
|
||||||
|
// Takes away health, dying at 0. Returns how much it actually lost.
|
||||||
|
float TakeDamage(float amount);
|
||||||
|
|
||||||
|
// Gives back health up to maxHealth, nothing for the dead. Returns how much it actually gained.
|
||||||
|
float Heal(float amount);
|
||||||
|
|
||||||
|
// Can't do anything for seconds of real time: no moving, turning, attacking or jumping, and whatever move it
|
||||||
|
// was in is cut off. A longer stun replaces a shorter one, a shorter one does nothing.
|
||||||
|
void Stun(float seconds);
|
||||||
|
bool IsStunned() const { return stunLeft > 0.0f; }
|
||||||
|
|
||||||
|
// Puts a status effect on it for seconds of real time, below 0 forever. Already on, the longer of the two stays.
|
||||||
|
void AddStatus(StatusEffect::Type type, float seconds = -1.0f);
|
||||||
|
void RemoveStatus(StatusEffect::Type type);
|
||||||
|
bool HasStatus(StatusEffect::Type type) const;
|
||||||
|
|
||||||
|
// Knocked to the floor for seconds of real time, unable to do anything. Only hits that hit the knocked down land.
|
||||||
|
void Knockdown(float seconds);
|
||||||
|
bool IsKnockedDown() const { return HasStatus(StatusEffect::Type::Knockdown); }
|
||||||
|
bool CanAct() const; // not stunned or held by a status effect
|
||||||
|
bool DodgesHits() const; // a status effect keeps ordinary hits off it
|
||||||
|
const std::vector<StatusEffect>& GetStatuses() const { return statuses; }
|
||||||
|
|
||||||
|
// Recoils from a hit: whatever swing it was in is cut off and its flinch plays over everything, if it has one.
|
||||||
|
// Nothing for the dead.
|
||||||
|
void Flinch();
|
||||||
|
|
||||||
|
// Goes down, stopping where it is, until it's revived, which the game does straight away somewhere else.
|
||||||
|
// One that heals on death just goes back to full health where it is.
|
||||||
|
void Die();
|
||||||
|
|
||||||
|
// Back on its feet at full health, wherever its body box is
|
||||||
|
void Revive();
|
||||||
|
|
||||||
|
// Asks the mind for this frame's actions and hands them to the body. The dead don't think.
|
||||||
|
void Think();
|
||||||
|
|
||||||
|
// Moves the body from this frame's actions, slower while it's attacking
|
||||||
|
void ApplyMovement(bool grounded, float dt, float referenceDt);
|
||||||
|
|
||||||
|
glm::vec3 GetForwardDirection() const { return body->GetForwardDirection(); }
|
||||||
|
const std::string& GetDisplayName() const { return displayName.empty() ? name : displayName; }
|
||||||
|
|
||||||
|
// Where a bone of the clip playing on the action layer is in the world this frame, rotation and all.
|
||||||
|
// Works for bones only that clip has, like a weapon bone. False unless animation is what's playing there.
|
||||||
|
bool GetActionBoneTransform(const Animation* animation, const std::string& bone, glm::mat4& transform) const;
|
||||||
|
|
||||||
|
// Where a bone is in the world this frame with every layer blended in, rotation and all.
|
||||||
|
// False for a bone the rig doesn't have, or while it's a ragdoll.
|
||||||
|
bool GetBoneTransform(const std::string& bone, glm::mat4& transform) const;
|
||||||
|
void EnableRagdoll(Physics& physics);
|
||||||
|
|
||||||
|
// Becomes that kind of fighter: its rig, animations, combo and stats, at full health unless it's dead.
|
||||||
|
// Its looks, name and physics body are left alone.
|
||||||
|
void ApplyTemplate(const CharacterTemplate& characterTemplate);
|
||||||
|
|
||||||
|
// The box physics moves the character around with, sized by body->t's scale
|
||||||
|
void CreatePhysicsBody(Physics& physics, float massKg);
|
||||||
|
|
||||||
|
// Its physics body's group, so it never collides with anything else of its own
|
||||||
|
int GetCollisionGroup();
|
||||||
|
|
||||||
|
Character();
|
||||||
|
void Render(Shader& shader) override;
|
||||||
|
bool GetWorldBounds(BoundingBox& bounds) const override;
|
||||||
|
|
||||||
|
void Step(float dt);
|
||||||
|
|
||||||
|
private:
|
||||||
|
glm::mat4 GetRigMatrix() const;
|
||||||
|
|
||||||
|
float health = 100.0f;
|
||||||
|
int deaths = 0;
|
||||||
|
float stunLeft = 0.0f; // game seconds
|
||||||
|
std::vector<StatusEffect> statuses;
|
||||||
|
bool wasKnockedDown = false; // last frame, so getting up stops the held pose
|
||||||
|
bool dead = false;
|
||||||
|
Physics* world = nullptr; // the physics its body is in, for EnableRagdoll
|
||||||
|
int collisionGroup = 0;
|
||||||
|
std::vector<CharacterAction> pendingActions;
|
||||||
|
};
|
||||||
|
#endif
|
||||||
@@ -0,0 +1,63 @@
|
|||||||
|
#ifndef CHARACTER_ACTION_CLASS
|
||||||
|
#define CHARACTER_ACTION_CLASS
|
||||||
|
|
||||||
|
#include <glm/glm.hpp>
|
||||||
|
|
||||||
|
enum class CharacterActionType {
|
||||||
|
Move, // walk along direction, or stand still if it's zero
|
||||||
|
Attack, // the next swing of its combo along direction
|
||||||
|
Jump, // up into the air, if it's standing on something
|
||||||
|
Dash, // a quick burst along direction, or the way it faces if that's zero
|
||||||
|
Ability // its ability in slot, aimed at point
|
||||||
|
};
|
||||||
|
|
||||||
|
// Something a character's mind wants it to do this frame. Minds produce these, the body carries them out.
|
||||||
|
struct CharacterAction {
|
||||||
|
CharacterActionType type = CharacterActionType::Move;
|
||||||
|
glm::vec3 direction = glm::vec3(0.0f); // Move and Attack, world space, only x and z are used
|
||||||
|
glm::vec3 facing = glm::vec3(0.0f); // Move only, keep facing this way and walk any way. Zero turns to face where it walks.
|
||||||
|
glm::vec3 point = glm::vec3(0.0f); // Ability only, world space, what it's aimed at
|
||||||
|
int slot = -1; // Ability only, which of its abilities, 0 for the one on 1
|
||||||
|
bool sprint = false; // Move only, run instead of walking
|
||||||
|
|
||||||
|
static CharacterAction Move(const glm::vec3& direction, const glm::vec3& facing = glm::vec3(0.0f), bool sprint = false) {
|
||||||
|
CharacterAction action;
|
||||||
|
action.type = CharacterActionType::Move;
|
||||||
|
action.direction = direction;
|
||||||
|
action.facing = facing;
|
||||||
|
action.sprint = sprint;
|
||||||
|
return action;
|
||||||
|
}
|
||||||
|
|
||||||
|
static CharacterAction Attack(const glm::vec3& direction) {
|
||||||
|
CharacterAction action;
|
||||||
|
action.type = CharacterActionType::Attack;
|
||||||
|
action.direction = direction;
|
||||||
|
return action;
|
||||||
|
}
|
||||||
|
|
||||||
|
static CharacterAction Jump() {
|
||||||
|
CharacterAction action;
|
||||||
|
action.type = CharacterActionType::Jump;
|
||||||
|
return action;
|
||||||
|
}
|
||||||
|
|
||||||
|
static CharacterAction Dash(const glm::vec3& direction) {
|
||||||
|
CharacterAction action;
|
||||||
|
action.type = CharacterActionType::Dash;
|
||||||
|
action.direction = direction;
|
||||||
|
return action;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Turns to face along direction and uses the ability in slot, aimed at point
|
||||||
|
static CharacterAction UseAbility(int slot, const glm::vec3& direction, const glm::vec3& point) {
|
||||||
|
CharacterAction action;
|
||||||
|
action.type = CharacterActionType::Ability;
|
||||||
|
action.slot = slot;
|
||||||
|
action.direction = direction;
|
||||||
|
action.point = point;
|
||||||
|
return action;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
#endif
|
||||||
@@ -0,0 +1,137 @@
|
|||||||
|
#include "CharacterManager.h"
|
||||||
|
#include "Animator.h"
|
||||||
|
#include "Character.h"
|
||||||
|
#include "CharacterTemplate.h"
|
||||||
|
#include "Rig.h"
|
||||||
|
#include "Hitbox.h"
|
||||||
|
|
||||||
|
CharacterManager::CharacterManager() = default;
|
||||||
|
CharacterManager::~CharacterManager() = default;
|
||||||
|
|
||||||
|
Rig* CharacterManager::LoadRig(const std::string& name, const std::string& path) {
|
||||||
|
auto existing = rigs.find(name);
|
||||||
|
if (existing != rigs.end())
|
||||||
|
return existing->second.get();
|
||||||
|
|
||||||
|
auto rig = std::make_unique<Rig>(path);
|
||||||
|
Rig* result = rig.get();
|
||||||
|
rigs.emplace(name, std::move(rig));
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
|
Animation* CharacterManager::LoadAnimation(const std::string& name, const std::string& path,
|
||||||
|
const std::string& rigName) {
|
||||||
|
auto existing = animations.find(name);
|
||||||
|
if (existing != animations.end())
|
||||||
|
return existing->second.get();
|
||||||
|
|
||||||
|
auto animation = std::make_unique<Animation>(path, GetRig(rigName));
|
||||||
|
Animation* result = animation.get();
|
||||||
|
animations.emplace(name, std::move(animation));
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
|
Character* CharacterManager::CreateCharacter(const std::string& name,
|
||||||
|
const std::string& rigName,
|
||||||
|
const std::string& animationName) {
|
||||||
|
Character* existing = GetCharacter(name);
|
||||||
|
if (existing)
|
||||||
|
return existing;
|
||||||
|
|
||||||
|
auto character = std::make_unique<Character>();
|
||||||
|
character->name = name;
|
||||||
|
if (!rigName.empty())
|
||||||
|
character->body->SetRig(GetRig(rigName));
|
||||||
|
if (!animationName.empty())
|
||||||
|
character->body->SetAnimation(GetAnimation(animationName));
|
||||||
|
|
||||||
|
Character* result = character.get();
|
||||||
|
characters.emplace(name, std::move(character));
|
||||||
|
characterOrder.push_back(result);
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
|
CharacterTemplate* CharacterManager::AddTemplate(const std::string& name) {
|
||||||
|
if (CharacterTemplate* existing = GetTemplate(name))
|
||||||
|
return existing;
|
||||||
|
|
||||||
|
auto characterTemplate = std::make_unique<CharacterTemplate>();
|
||||||
|
characterTemplate->name = name;
|
||||||
|
CharacterTemplate* result = characterTemplate.get();
|
||||||
|
templates.emplace(name, std::move(characterTemplate));
|
||||||
|
templateOrder.push_back(result);
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
|
CharacterTemplate* CharacterManager::GetTemplate(const std::string& name) const {
|
||||||
|
auto it = templates.find(name);
|
||||||
|
return it != templates.end() ? it->second.get() : nullptr;
|
||||||
|
}
|
||||||
|
|
||||||
|
Character* CharacterManager::CreateCharacterFromTemplate(const std::string& name, const std::string& templateName) {
|
||||||
|
const CharacterTemplate* characterTemplate = GetTemplate(templateName);
|
||||||
|
if (!characterTemplate)
|
||||||
|
return nullptr;
|
||||||
|
if (Character* existing = GetCharacter(name))
|
||||||
|
return existing;
|
||||||
|
|
||||||
|
Character* character = CreateCharacter(name);
|
||||||
|
character->ApplyTemplate(*characterTemplate);
|
||||||
|
return character;
|
||||||
|
}
|
||||||
|
|
||||||
|
Rig* CharacterManager::GetRig(const std::string& name) const {
|
||||||
|
auto it = rigs.find(name);
|
||||||
|
return it != rigs.end() ? it->second.get() : nullptr;
|
||||||
|
}
|
||||||
|
|
||||||
|
Animation* CharacterManager::GetAnimation(const std::string& name) const {
|
||||||
|
auto it = animations.find(name);
|
||||||
|
return it != animations.end() ? it->second.get() : nullptr;
|
||||||
|
}
|
||||||
|
|
||||||
|
Character* CharacterManager::GetCharacter(const std::string& name) const {
|
||||||
|
auto it = characters.find(name);
|
||||||
|
return it != characters.end() ? it->second.get() : nullptr;
|
||||||
|
}
|
||||||
|
|
||||||
|
void CharacterManager::Think() {
|
||||||
|
for (Character* character : characterOrder)
|
||||||
|
character->Think();
|
||||||
|
}
|
||||||
|
|
||||||
|
void CharacterManager::Step(float dt) {
|
||||||
|
for (Character* character : characterOrder)
|
||||||
|
character->Step(dt);
|
||||||
|
}
|
||||||
|
|
||||||
|
void CharacterManager::AddAttackHitboxes(HitboxSystem& hitboxes) {
|
||||||
|
for (Character* attacker : characterOrder) {
|
||||||
|
if (attacker->IsDead() || attacker->ragdoll)
|
||||||
|
continue;
|
||||||
|
const CharacterCombat& combat = attacker->body->combat;
|
||||||
|
if (!combat.IsHitting())
|
||||||
|
continue;
|
||||||
|
const AttackMove& move = *combat.GetCurrentMove();
|
||||||
|
|
||||||
|
// In front of it, turned the way it faces
|
||||||
|
const glm::vec3 forward = attacker->GetForwardDirection();
|
||||||
|
const glm::vec3 up(0.0f, 1.0f, 0.0f);
|
||||||
|
const glm::vec3 right = glm::normalize(glm::cross(forward, up));
|
||||||
|
const glm::vec3 position = attacker->body->t.GetTranslation()
|
||||||
|
+ right * move.hitboxOffset.x + up * move.hitboxOffset.y + forward * move.hitboxOffset.z;
|
||||||
|
|
||||||
|
Hitbox hitbox;
|
||||||
|
hitbox.owner = attacker;
|
||||||
|
hitbox.swing = combat.GetSwingId();
|
||||||
|
hitbox.box.ScaleTo(move.hitboxSize);
|
||||||
|
hitbox.box.RotateToQuaternion(attacker->body->t.GetRotationQuaternion());
|
||||||
|
hitbox.box.TranslateTo(position);
|
||||||
|
hitbox.damage = move.damage;
|
||||||
|
hitbox.knockback = move.knockback;
|
||||||
|
hitbox.knockbackUp = move.knockbackUp;
|
||||||
|
hitbox.stun = move.stun;
|
||||||
|
hitbox.hitsKnockedDown = move.hitsKnockedDown;
|
||||||
|
hitboxes.Add(hitbox);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,58 @@
|
|||||||
|
#ifndef CHARACTER_MANAGER_CLASS
|
||||||
|
#define CHARACTER_MANAGER_CLASS
|
||||||
|
|
||||||
|
#include <memory>
|
||||||
|
#include <string>
|
||||||
|
#include <unordered_map>
|
||||||
|
#include <vector>
|
||||||
|
|
||||||
|
class Character;
|
||||||
|
class Rig;
|
||||||
|
class Animation;
|
||||||
|
class HitboxSystem;
|
||||||
|
struct CharacterTemplate;
|
||||||
|
|
||||||
|
class CharacterManager {
|
||||||
|
public:
|
||||||
|
CharacterManager();
|
||||||
|
~CharacterManager();
|
||||||
|
|
||||||
|
Rig* LoadRig(const std::string& name, const std::string& path);
|
||||||
|
Animation* LoadAnimation(const std::string& name, const std::string& path,
|
||||||
|
const std::string& rigName = "");
|
||||||
|
Character* CreateCharacter(const std::string& name,
|
||||||
|
const std::string& rigName = "",
|
||||||
|
const std::string& animationName = "");
|
||||||
|
|
||||||
|
// A new kind of fighter to fill in, or the one already called name
|
||||||
|
CharacterTemplate* AddTemplate(const std::string& name);
|
||||||
|
CharacterTemplate* GetTemplate(const std::string& name) const;
|
||||||
|
const std::vector<CharacterTemplate*>& GetTemplates() const { return templateOrder; } // in the order they were added
|
||||||
|
|
||||||
|
// A character that's the template called templateName, see Character::ApplyTemplate. nullptr if there's no such template.
|
||||||
|
Character* CreateCharacterFromTemplate(const std::string& name, const std::string& templateName);
|
||||||
|
|
||||||
|
Rig* GetRig(const std::string& name) const;
|
||||||
|
Animation* GetAnimation(const std::string& name) const;
|
||||||
|
Character* GetCharacter(const std::string& name) const;
|
||||||
|
|
||||||
|
// Every character's mind decides its actions for this frame
|
||||||
|
void Think();
|
||||||
|
void Step(float dt);
|
||||||
|
|
||||||
|
|
||||||
|
// Puts out the hitbox of every swing that can connect this frame, after Step
|
||||||
|
void AddAttackHitboxes(HitboxSystem& hitboxes);
|
||||||
|
|
||||||
|
const std::vector<Character*>& GetCharacters() const { return characterOrder; }
|
||||||
|
|
||||||
|
private:
|
||||||
|
std::unordered_map<std::string, std::unique_ptr<Rig>> rigs;
|
||||||
|
std::unordered_map<std::string, std::unique_ptr<Animation>> animations;
|
||||||
|
std::unordered_map<std::string, std::unique_ptr<CharacterTemplate>> templates;
|
||||||
|
std::unordered_map<std::string, std::unique_ptr<Character>> characters;
|
||||||
|
std::vector<Character*> characterOrder;
|
||||||
|
std::vector<CharacterTemplate*> templateOrder;
|
||||||
|
};
|
||||||
|
|
||||||
|
#endif
|
||||||
@@ -0,0 +1,198 @@
|
|||||||
|
#include "CharacterMind.h"
|
||||||
|
|
||||||
|
#include "Camera.h"
|
||||||
|
#include "Character.h"
|
||||||
|
#include "common.h"
|
||||||
|
#include "Keyboard.h"
|
||||||
|
#include "Mouse.h"
|
||||||
|
|
||||||
|
#include <algorithm>
|
||||||
|
#include <cmath>
|
||||||
|
|
||||||
|
void KeyboardMind::ProduceActions(const Character& character, std::vector<CharacterAction>& actions)
|
||||||
|
{
|
||||||
|
glm::vec3 direction(0.0f);
|
||||||
|
|
||||||
|
if (enabled) {
|
||||||
|
// Flattened, so looking up or down doesn't slow walking
|
||||||
|
const glm::vec3 forward = camera.t.GetFrontVector();
|
||||||
|
const glm::vec3 right = camera.t.GetRightVector();
|
||||||
|
const glm::vec3 flatForward = glm::normalize(glm::vec3(forward.x, 0.0f, forward.z));
|
||||||
|
const glm::vec3 flatRight = glm::normalize(glm::vec3(right.x, 0.0f, right.z));
|
||||||
|
|
||||||
|
// The camera's right vector points to the left of the screen, so A adds it
|
||||||
|
if (keyboard->IsKeyDown(GLFW_KEY_W)) direction += flatForward;
|
||||||
|
if (keyboard->IsKeyDown(GLFW_KEY_S)) direction -= flatForward;
|
||||||
|
if (keyboard->IsKeyDown(GLFW_KEY_A)) direction += flatRight;
|
||||||
|
if (keyboard->IsKeyDown(GLFW_KEY_D)) direction -= flatRight;
|
||||||
|
if (glm::length(direction) > 0.0f)
|
||||||
|
direction = glm::normalize(direction);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Always sent, so letting go of the keys stops the character. It turns to face where it walks,
|
||||||
|
// or in an ability that pivots, keeps turning to face where the camera looks.
|
||||||
|
glm::vec3 facing(0.0f);
|
||||||
|
if (character.body->combat.IsAbilityPivoting()) {
|
||||||
|
const glm::vec3 look = camera.t.GetFrontVector();
|
||||||
|
facing = glm::vec3(look.x, 0.0f, look.z);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Shift let go quickly dashes, held down it sprints
|
||||||
|
const bool shiftDown = enabled && (keyboard->IsKeyDown(GLFW_KEY_LEFT_SHIFT) || keyboard->IsKeyDown(GLFW_KEY_RIGHT_SHIFT));
|
||||||
|
const double now = glfwGetTime();
|
||||||
|
if (shiftDown && !prevShiftDown)
|
||||||
|
shiftPressedAt = now;
|
||||||
|
const bool sprint = shiftDown && now - shiftPressedAt > dashTapTime;
|
||||||
|
actions.push_back(CharacterAction::Move(direction, facing, sprint));
|
||||||
|
if (!shiftDown && prevShiftDown && now - shiftPressedAt <= dashTapTime)
|
||||||
|
actions.push_back(CharacterAction::Dash(direction));
|
||||||
|
prevShiftDown = shiftDown;
|
||||||
|
|
||||||
|
const bool jumpDown = enabled && keyboard->IsKeyDown(GLFW_KEY_SPACE);
|
||||||
|
if (jumpDown && !prevJumpDown)
|
||||||
|
actions.push_back(CharacterAction::Jump());
|
||||||
|
prevJumpDown = jumpDown;
|
||||||
|
|
||||||
|
// Left click swings the way the camera looks
|
||||||
|
const bool attackDown = enabled && mouseEnabled && mouse->IsLeftDown();
|
||||||
|
if (attackDown && !prevAttackDown) {
|
||||||
|
const glm::vec3 look = camera.t.GetFrontVector();
|
||||||
|
actions.push_back(CharacterAction::Attack(glm::vec3(look.x, 0.0f, look.z)));
|
||||||
|
}
|
||||||
|
prevAttackDown = attackDown;
|
||||||
|
|
||||||
|
// 1 to 9 use its abilities, the way the camera looks
|
||||||
|
const int abilityCount = std::min((int)character.body->combat.abilities.size(), 9);
|
||||||
|
for (int i = 0; i < 9; i++) {
|
||||||
|
const bool down = enabled && mouseEnabled && i < abilityCount && keyboard->IsKeyDown(GLFW_KEY_1 + i);
|
||||||
|
if (down && !prevAbilityDown[i]) {
|
||||||
|
const glm::vec3 look = camera.t.GetFrontVector();
|
||||||
|
const glm::vec3 aim(look.x, 0.0f, look.z);
|
||||||
|
const float range = character.body->combat.abilities[i].range;
|
||||||
|
actions.push_back(CharacterAction::UseAbility(i, aim, character.body->t.GetTranslation() + aim * range));
|
||||||
|
}
|
||||||
|
prevAbilityDown[i] = down;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void RiflemanMind::ProduceActions(const Character& character, std::vector<CharacterAction>& actions)
|
||||||
|
{
|
||||||
|
// Real time since it last thought, for its timers
|
||||||
|
const double now = glfwGetTime();
|
||||||
|
const float dt = lastTime < 0.0 ? 0.0f : std::min((float)(now - lastTime), 0.1f);
|
||||||
|
lastTime = now;
|
||||||
|
|
||||||
|
if (!target || target->IsDead() || target == &character) {
|
||||||
|
actions.push_back(CharacterAction::Move(glm::vec3(0.0f)));
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const CharacterCombat& combat = character.body->combat;
|
||||||
|
const glm::vec3 me = character.body->t.GetTranslation();
|
||||||
|
const glm::vec3 them = target->body->t.GetTranslation();
|
||||||
|
glm::vec3 to(them.x - me.x, 0.0f, them.z - me.z);
|
||||||
|
const float distance = glm::length(to);
|
||||||
|
const glm::vec3 toward = distance > 0.001f ? to / distance : character.GetForwardDirection();
|
||||||
|
const glm::vec3 side(-toward.z, 0.0f, toward.x);
|
||||||
|
|
||||||
|
// Mid ability it stands still, keeping on where it aimed rather than following them
|
||||||
|
if (const Ability* current = combat.GetCurrentAbility()) {
|
||||||
|
actions.push_back(CharacterAction::Move(glm::vec3(0.0f), combat.IsAbilityPivoting() ? aim : glm::vec3(0.0f)));
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
int shoot = -1, stab = -1, sweep = -1;
|
||||||
|
for (int i = 0; i < (int)combat.abilities.size(); i++) {
|
||||||
|
if (combat.abilities[i].effect == Ability::Effect::Shot && shoot < 0) shoot = i;
|
||||||
|
if (combat.abilities[i].effect == Ability::Effect::Stab && stab < 0) stab = i;
|
||||||
|
if (combat.abilities[i].effect == Ability::Effect::Sweep && sweep < 0) sweep = i;
|
||||||
|
}
|
||||||
|
// Lying there nothing it has can touch them, so it holds off until they're up
|
||||||
|
const bool canHit = !target->DodgesHits();
|
||||||
|
const bool stabReady = canHit && stab >= 0 && combat.IsAbilityReady(stab);
|
||||||
|
const bool sweepReady = canHit && sweep >= 0 && combat.IsAbilityReady(sweep);
|
||||||
|
shotWait -= dt;
|
||||||
|
dashWait -= dt;
|
||||||
|
|
||||||
|
// In reach, stab
|
||||||
|
if (stabReady && distance <= stabReach) {
|
||||||
|
aim = toward;
|
||||||
|
actions.push_back(CharacterAction::Move(glm::vec3(0.0f), toward));
|
||||||
|
actions.push_back(CharacterAction::UseAbility(stab, toward, them));
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// A little further out, sweep their legs, its step taking it in
|
||||||
|
if (sweepReady && distance >= sweepMin && distance <= sweepMax) {
|
||||||
|
aim = toward;
|
||||||
|
actions.push_back(CharacterAction::Move(glm::vec3(0.0f), toward));
|
||||||
|
actions.push_back(CharacterAction::UseAbility(sweep, toward, them));
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Which way it's strafing, now and then picked again
|
||||||
|
strafeWait -= dt;
|
||||||
|
if (strafeWait <= 0.0f) {
|
||||||
|
strafeSign = std::uniform_int_distribution<int>(0, 1)(rng) ? 1.0f : -1.0f;
|
||||||
|
strafeWait = std::uniform_real_distribution<float>(1.2f, 2.8f)(rng);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Their shot winding up at it, maybe it dashes out of the way, once for each shot
|
||||||
|
const CharacterCombat& theirs = target->body->combat;
|
||||||
|
const Ability* incoming = theirs.GetCurrentAbility();
|
||||||
|
if (incoming && incoming->effect == Ability::Effect::Shot && theirs.GetAbilityUse() != seenUse) {
|
||||||
|
seenUse = theirs.GetAbilityUse();
|
||||||
|
if (dashWait <= 0.0f && std::uniform_real_distribution<float>(0.0f, 1.0f)(rng) < dodgeChance) {
|
||||||
|
actions.push_back(CharacterAction::Dash(side * strafeSign));
|
||||||
|
dashWait = std::uniform_real_distribution<float>(dashWaitMin, dashWaitMax)(rng);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Too close with nothing to hit them with, it dashes back out
|
||||||
|
if (dashWait <= 0.0f && distance < stabReach * 1.5f && !stabReady && !sweepReady) {
|
||||||
|
actions.push_back(CharacterAction::Dash(-toward + side * strafeSign * 0.5f));
|
||||||
|
dashWait = std::uniform_real_distribution<float>(dashWaitMin, dashWaitMax)(rng);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Far off with its sweep ready, it dashes in to close the gap
|
||||||
|
if (dashWait <= 0.0f && sweepReady && distance > sweepMax + 2.0f && distance < sweepMax + 7.0f) {
|
||||||
|
actions.push_back(CharacterAction::Dash(toward));
|
||||||
|
dashWait = std::uniform_real_distribution<float>(dashWaitMin, dashWaitMax)(rng);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
glm::vec3 move(0.0f);
|
||||||
|
if (!canHit)
|
||||||
|
move = side * strafeSign; // circles them while they're down, ready for when they're up
|
||||||
|
else if ((stabReady || sweepReady) && distance < rushDistance)
|
||||||
|
move = toward; // close and its knife's ready, in it goes
|
||||||
|
else if (distance > farthest)
|
||||||
|
move = toward + side * strafeSign * 0.4f;
|
||||||
|
else if (distance < closest)
|
||||||
|
move = -toward + side * strafeSign * 0.5f;
|
||||||
|
else
|
||||||
|
move = side * strafeSign;
|
||||||
|
|
||||||
|
// Back toward the middle near the edge, rather than off it
|
||||||
|
const float edge = arenaHalfSize - 4.0f;
|
||||||
|
if (std::abs(me.x) > edge || std::abs(me.z) > edge) {
|
||||||
|
const glm::vec3 home(-me.x, 0.0f, -me.z);
|
||||||
|
if (glm::length(home) > 0.001f)
|
||||||
|
move += glm::normalize(home) * 1.5f;
|
||||||
|
}
|
||||||
|
if (glm::length(move) > 0.001f)
|
||||||
|
move = glm::normalize(move);
|
||||||
|
|
||||||
|
// Always facing them, whichever way it goes, running when they're far off
|
||||||
|
actions.push_back(CharacterAction::Move(move, toward, distance > sprintDistance));
|
||||||
|
|
||||||
|
// A shot whenever it's had a moment, if they're in range
|
||||||
|
if (canHit && shoot >= 0 && shotWait <= 0.0f && combat.IsAbilityReady(shoot) && !combat.GetCurrentMove()
|
||||||
|
&& distance <= combat.abilities[shoot].range) {
|
||||||
|
aim = toward;
|
||||||
|
actions.push_back(CharacterAction::UseAbility(shoot, toward, them));
|
||||||
|
shotWait = std::uniform_real_distribution<float>(shotWaitMin, shotWaitMax)(rng);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,85 @@
|
|||||||
|
#ifndef CHARACTER_MIND_CLASS
|
||||||
|
#define CHARACTER_MIND_CLASS
|
||||||
|
|
||||||
|
#include <random>
|
||||||
|
#include <vector>
|
||||||
|
|
||||||
|
#include <glm/glm.hpp>
|
||||||
|
|
||||||
|
#include "CharacterAction.h"
|
||||||
|
|
||||||
|
class Character;
|
||||||
|
class Camera;
|
||||||
|
|
||||||
|
// Decides what a character does, as actions. A character without a mind does nothing.
|
||||||
|
// AI goes here later, as another kind of mind.
|
||||||
|
class CharacterMind {
|
||||||
|
public:
|
||||||
|
virtual ~CharacterMind() = default;
|
||||||
|
|
||||||
|
// Called once a frame, adds whatever the character should do this frame
|
||||||
|
virtual void ProduceActions(const Character& character, std::vector<CharacterAction>& actions) = 0;
|
||||||
|
};
|
||||||
|
|
||||||
|
// The player: WASD, relative to where the camera faces, shift tapped to dash or held to sprint, space to jump, left click to attack and the number keys for abilities,
|
||||||
|
// aimed at whatever's under the crosshair. Looking around doesn't turn the
|
||||||
|
// character, it faces where it walks, but it swings the way the camera looks.
|
||||||
|
class KeyboardMind : public CharacterMind {
|
||||||
|
public:
|
||||||
|
explicit KeyboardMind(const Camera& camera) : camera(camera) {}
|
||||||
|
|
||||||
|
// Off while the keys are busy with something else, like typing in the console
|
||||||
|
bool enabled = true;
|
||||||
|
|
||||||
|
// Off while the mouse is busy with something else, like clicking on menus
|
||||||
|
bool mouseEnabled = true;
|
||||||
|
|
||||||
|
void ProduceActions(const Character& character, std::vector<CharacterAction>& actions) override;
|
||||||
|
|
||||||
|
private:
|
||||||
|
const Camera& camera;
|
||||||
|
bool prevAttackDown = false;
|
||||||
|
bool prevJumpDown = false;
|
||||||
|
bool prevShiftDown = false;
|
||||||
|
double shiftPressedAt = 0.0; // glfwGetTime when shift went down
|
||||||
|
float dashTapTime = 0.2f; // real seconds shift can be held and still count as a tap, longer is a sprint
|
||||||
|
bool prevAbilityDown[9] = {};
|
||||||
|
};
|
||||||
|
|
||||||
|
// An enemy with a rifle. It keeps its distance from its target, strafing side to side and facing them, shoots
|
||||||
|
// whenever it gets a moment and steps in to stab when they get close. Shots go where they were when it started
|
||||||
|
// aiming, so moving dodges them.
|
||||||
|
class RiflemanMind : public CharacterMind {
|
||||||
|
public:
|
||||||
|
RiflemanMind(const Character* target, float arenaHalfSize) : target(target), arenaHalfSize(arenaHalfSize) {}
|
||||||
|
|
||||||
|
const Character* target; // who it's after, nullptr just stands there
|
||||||
|
float arenaHalfSize; // how far the ground goes from the middle each way, so it doesn't back off the edge
|
||||||
|
|
||||||
|
float closest = 6.0f; // meters it backs off inside of
|
||||||
|
float farthest = 12.0f; // and closes in outside of
|
||||||
|
float stabReach = 1.6f; // meters it stabs from
|
||||||
|
float rushDistance = 3.5f; // they're this close and its stab is ready, it goes in for it
|
||||||
|
float shotWaitMin = 0.8f; // real seconds it waits between shots, picked at random between these
|
||||||
|
float shotWaitMax = 1.8f;
|
||||||
|
float sweepMin = 2.2f; // meters it sweeps from, its step carrying it the rest of the way
|
||||||
|
float sweepMax = 4.5f;
|
||||||
|
float sprintDistance = 18.0f; // farther than this it sprints to catch up
|
||||||
|
float dodgeChance = 0.5f; // of dashing aside when it sees a shot coming
|
||||||
|
float dashWaitMin = 2.0f; // real seconds between dashes, picked at random between these
|
||||||
|
float dashWaitMax = 4.5f;
|
||||||
|
|
||||||
|
void ProduceActions(const Character& character, std::vector<CharacterAction>& actions) override;
|
||||||
|
|
||||||
|
private:
|
||||||
|
std::mt19937 rng{ std::random_device{}() };
|
||||||
|
double lastTime = -1.0;
|
||||||
|
float shotWait = 1.0f;
|
||||||
|
float dashWait = 1.0f;
|
||||||
|
unsigned int seenUse = 0; // the last of their ability uses it decided whether to dodge
|
||||||
|
float strafeWait = 0.0f; // until it picks which way to strafe again
|
||||||
|
float strafeSign = 1.0f;
|
||||||
|
glm::vec3 aim = glm::vec3(0.0f, 0.0f, 1.0f); // where the ability it's in was aimed
|
||||||
|
};
|
||||||
|
|
||||||
|
#endif
|
||||||
@@ -0,0 +1,59 @@
|
|||||||
|
#ifndef CHARACTER_TEMPLATE_CLASS
|
||||||
|
#define CHARACTER_TEMPLATE_CLASS
|
||||||
|
|
||||||
|
#include <string>
|
||||||
|
#include <vector>
|
||||||
|
|
||||||
|
#include <glm/glm.hpp>
|
||||||
|
|
||||||
|
#include "Combat.h"
|
||||||
|
|
||||||
|
class Rig;
|
||||||
|
class Animation;
|
||||||
|
struct MeshData;
|
||||||
|
class Texture;
|
||||||
|
|
||||||
|
// A model a fighter always has in hand, riding one of its bones
|
||||||
|
struct HeldModel {
|
||||||
|
const MeshData* mesh = nullptr; // nothing held when nullptr. Its origin goes on the bone.
|
||||||
|
Texture* texture = nullptr;
|
||||||
|
std::string bone = "R_Weapon";
|
||||||
|
glm::vec3 turn = glm::vec3(0.0f); // degrees around x, y and z on top of the bone's own rotation
|
||||||
|
bool hasMuzzle = false; // shots come out of muzzle rather than the bone
|
||||||
|
glm::vec3 muzzle = glm::vec3(0.0f); // in the model's own space, where its Muzzle empty is
|
||||||
|
float metallic = 0.6f;
|
||||||
|
float roughness = 0.5f;
|
||||||
|
};
|
||||||
|
|
||||||
|
// A kind of fighter, like a character select pick in a fighting game: how it stands, walks, takes hits and
|
||||||
|
// what its attacks are. Any number of characters can be made from one, see CharacterManager::CreateCharacter.
|
||||||
|
struct CharacterTemplate {
|
||||||
|
std::string name;
|
||||||
|
std::string displayName; // what the character select calls it
|
||||||
|
bool selectable = true; // shown in the character select, false for ones only NPCs are
|
||||||
|
|
||||||
|
Rig* rig = nullptr;
|
||||||
|
Animation* idleAnimation = nullptr; // the stance it fights from, always on underneath everything
|
||||||
|
Animation* walkAnimation = nullptr; // faded in on top while walking
|
||||||
|
Animation* jumpAnimation = nullptr; // played from takeoff, its last pose held until it lands
|
||||||
|
float jumpDelay = 0.0f; // real seconds into jumpAnimation it leaves the ground
|
||||||
|
Animation* flinchAnimation = nullptr; // played when it's hit
|
||||||
|
Animation* knockdownAnimation = nullptr; // played when it's knocked down, its last pose held until it's up
|
||||||
|
Animation* dashAnimation = nullptr; // played over everything while it dashes
|
||||||
|
|
||||||
|
std::vector<AttackMove> combo; // left click, in order
|
||||||
|
std::string comboName; // what the moves list calls it
|
||||||
|
std::vector<Ability> abilities; // on the number keys, the first on 1
|
||||||
|
HeldModel weapon; // what it carries, like a rifle
|
||||||
|
|
||||||
|
float walkSpeed = 1.17f; // m/s on flat ground
|
||||||
|
float sprintMultiplier = 1.4f; // times walkSpeed while sprinting
|
||||||
|
float dashDistance = 4.0f; // m covered by a dash
|
||||||
|
float dashDelay = 0.0f; // real seconds into dashAnimation it starts moving
|
||||||
|
float dashTime = 0.4f; // real seconds a dash carries it for, after which it's free to do anything again
|
||||||
|
float dashCooldown = 0.5f; // real seconds after one dash ends before the next
|
||||||
|
float maxHealth = 100.0f;
|
||||||
|
bool healsOnDeath = false; // never goes down, dying puts it back to full health
|
||||||
|
};
|
||||||
|
|
||||||
|
#endif
|
||||||
@@ -0,0 +1,128 @@
|
|||||||
|
#include "CollisionMesh.h"
|
||||||
|
|
||||||
|
#include <algorithm>
|
||||||
|
#include <cfloat>
|
||||||
|
|
||||||
|
static constexpr int LEAF_TRIANGLES = 4;
|
||||||
|
|
||||||
|
static glm::vec3 TriangleCentroid(const Triangle& tri)
|
||||||
|
{
|
||||||
|
return (tri.a + tri.b + tri.c) / 3.0f;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Bounds overlap plus the triangle's plane, which is what prunes huge floor triangles
|
||||||
|
static bool TriangleOverlapsBox(const Triangle& tri, const glm::vec3& boxMin, const glm::vec3& boxMax)
|
||||||
|
{
|
||||||
|
const glm::vec3 triMin = glm::min(tri.a, glm::min(tri.b, tri.c));
|
||||||
|
const glm::vec3 triMax = glm::max(tri.a, glm::max(tri.b, tri.c));
|
||||||
|
if (glm::any(glm::lessThan(triMax, boxMin)) || glm::any(glm::greaterThan(triMin, boxMax)))
|
||||||
|
return false;
|
||||||
|
|
||||||
|
const glm::vec3 normal = glm::cross(tri.b - tri.a, tri.c - tri.a);
|
||||||
|
const glm::vec3 center = (boxMin + boxMax) * 0.5f;
|
||||||
|
const glm::vec3 halfSize = (boxMax - boxMin) * 0.5f;
|
||||||
|
const float distance = glm::dot(normal, center - tri.a);
|
||||||
|
const float radius = glm::dot(glm::abs(normal), halfSize);
|
||||||
|
return glm::abs(distance) <= radius;
|
||||||
|
}
|
||||||
|
|
||||||
|
CollisionMesh::CollisionMesh(const std::vector<Triangle>& meshTriangles)
|
||||||
|
: triangles(meshTriangles)
|
||||||
|
{
|
||||||
|
if (!triangles.empty())
|
||||||
|
Build(0, (int)triangles.size());
|
||||||
|
}
|
||||||
|
|
||||||
|
int CollisionMesh::Build(int start, int count)
|
||||||
|
{
|
||||||
|
const int index = (int)nodes.size();
|
||||||
|
nodes.push_back(Node());
|
||||||
|
|
||||||
|
glm::vec3 boundsMin(FLT_MAX);
|
||||||
|
glm::vec3 boundsMax(-FLT_MAX);
|
||||||
|
glm::vec3 centroidMin(FLT_MAX);
|
||||||
|
glm::vec3 centroidMax(-FLT_MAX);
|
||||||
|
for (int i = start; i < start + count; i++) {
|
||||||
|
const Triangle& tri = triangles[i];
|
||||||
|
boundsMin = glm::min(boundsMin, glm::min(tri.a, glm::min(tri.b, tri.c)));
|
||||||
|
boundsMax = glm::max(boundsMax, glm::max(tri.a, glm::max(tri.b, tri.c)));
|
||||||
|
const glm::vec3 centroid = TriangleCentroid(tri);
|
||||||
|
centroidMin = glm::min(centroidMin, centroid);
|
||||||
|
centroidMax = glm::max(centroidMax, centroid);
|
||||||
|
}
|
||||||
|
nodes[index].boundsMin = boundsMin;
|
||||||
|
nodes[index].boundsMax = boundsMax;
|
||||||
|
|
||||||
|
if (count <= LEAF_TRIANGLES) {
|
||||||
|
nodes[index].start = start;
|
||||||
|
nodes[index].count = count;
|
||||||
|
return index;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Split at the median along the widest axis
|
||||||
|
const glm::vec3 extent = centroidMax - centroidMin;
|
||||||
|
const int axis = extent.x > extent.y ? (extent.x > extent.z ? 0 : 2) : (extent.y > extent.z ? 1 : 2);
|
||||||
|
const int half = count / 2;
|
||||||
|
std::nth_element(triangles.begin() + start, triangles.begin() + start + half, triangles.begin() + start + count,
|
||||||
|
[axis](const Triangle& a, const Triangle& b) {
|
||||||
|
return TriangleCentroid(a)[axis] < TriangleCentroid(b)[axis];
|
||||||
|
});
|
||||||
|
|
||||||
|
// Build grows nodes, so index stays valid but references into it wouldn't
|
||||||
|
const int left = Build(start, half);
|
||||||
|
const int right = Build(start + half, count - half);
|
||||||
|
nodes[index].left = left;
|
||||||
|
nodes[index].right = right;
|
||||||
|
return index;
|
||||||
|
}
|
||||||
|
|
||||||
|
void CollisionMesh::Query(const Triangle& tri, std::vector<int>& hits) const
|
||||||
|
{
|
||||||
|
if (nodes.empty())
|
||||||
|
return;
|
||||||
|
|
||||||
|
int stack[64];
|
||||||
|
int stackSize = 0;
|
||||||
|
stack[stackSize++] = 0;
|
||||||
|
|
||||||
|
while (stackSize > 0) {
|
||||||
|
const Node& node = nodes[stack[--stackSize]];
|
||||||
|
if (!TriangleOverlapsBox(tri, node.boundsMin, node.boundsMax))
|
||||||
|
continue;
|
||||||
|
|
||||||
|
if (node.left == -1) {
|
||||||
|
for (int i = node.start; i < node.start + node.count; i++)
|
||||||
|
hits.push_back(i);
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
stack[stackSize++] = node.left;
|
||||||
|
stack[stackSize++] = node.right;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const CollisionMesh& CollisionMesh::UnitBox()
|
||||||
|
{
|
||||||
|
static const CollisionMesh box = []() {
|
||||||
|
const glm::vec3 axes[3] = { {1.0f, 0.0f, 0.0f}, {0.0f, 1.0f, 0.0f}, {0.0f, 0.0f, 1.0f} };
|
||||||
|
|
||||||
|
std::vector<Triangle> faces;
|
||||||
|
for (int axis = 0; axis < 3; axis++) {
|
||||||
|
for (float side : { 1.0f, -1.0f }) {
|
||||||
|
const glm::vec3 normal = axes[axis] * side;
|
||||||
|
const glm::vec3 u = axes[(axis + 1) % 3];
|
||||||
|
const glm::vec3 v = glm::cross(normal, u); // u x v = normal, so the winding faces outward
|
||||||
|
|
||||||
|
const glm::vec3 center = normal * 0.5f;
|
||||||
|
const glm::vec3 p0 = center - u * 0.5f - v * 0.5f;
|
||||||
|
const glm::vec3 p1 = center + u * 0.5f - v * 0.5f;
|
||||||
|
const glm::vec3 p2 = center + u * 0.5f + v * 0.5f;
|
||||||
|
const glm::vec3 p3 = center - u * 0.5f + v * 0.5f;
|
||||||
|
faces.push_back({ p0, p1, p2 });
|
||||||
|
faces.push_back({ p0, p2, p3 });
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return CollisionMesh(faces);
|
||||||
|
}();
|
||||||
|
return box;
|
||||||
|
}
|
||||||
@@ -0,0 +1,41 @@
|
|||||||
|
#ifndef COLLISION_MESH_CLASS
|
||||||
|
#define COLLISION_MESH_CLASS
|
||||||
|
|
||||||
|
#include <vector>
|
||||||
|
|
||||||
|
#include <glm/glm.hpp>
|
||||||
|
|
||||||
|
#include "Structures.h"
|
||||||
|
|
||||||
|
// Triangle soup for triangle by triangle collision, wrapped in a BVH.
|
||||||
|
// Triangles live in the body's unit space, so t.GetMatrix() takes them to world space
|
||||||
|
// the same way it does for a Mesh. Winding must be counter clockwise seen from outside.
|
||||||
|
class CollisionMesh {
|
||||||
|
public:
|
||||||
|
CollisionMesh(const std::vector<Triangle>& meshTriangles);
|
||||||
|
|
||||||
|
// The 12 triangles of a unit cube, used for any body without its own mesh
|
||||||
|
static const CollisionMesh& UnitBox();
|
||||||
|
|
||||||
|
const std::vector<Triangle>& GetTriangles() const { return triangles; }
|
||||||
|
|
||||||
|
// Appends the index of every triangle whose BVH leaf could touch tri (same space as the mesh)
|
||||||
|
void Query(const Triangle& tri, std::vector<int>& hits) const;
|
||||||
|
|
||||||
|
private:
|
||||||
|
struct Node {
|
||||||
|
glm::vec3 boundsMin;
|
||||||
|
glm::vec3 boundsMax;
|
||||||
|
int left = -1; // -1 = leaf
|
||||||
|
int right = -1;
|
||||||
|
int start = 0;
|
||||||
|
int count = 0;
|
||||||
|
};
|
||||||
|
|
||||||
|
int Build(int start, int count);
|
||||||
|
|
||||||
|
std::vector<Triangle> triangles;
|
||||||
|
std::vector<Node> nodes;
|
||||||
|
};
|
||||||
|
|
||||||
|
#endif
|
||||||
+229
@@ -0,0 +1,229 @@
|
|||||||
|
#include "Combat.h"
|
||||||
|
|
||||||
|
#include <algorithm>
|
||||||
|
|
||||||
|
#include "Animator.h"
|
||||||
|
#include "Character.h"
|
||||||
|
#include "Engine.h"
|
||||||
|
#include "Physics.h"
|
||||||
|
|
||||||
|
// How long an animation is at its own speed, in real seconds, which is what animators play in
|
||||||
|
static float ClipSeconds(const Animation* animation) {
|
||||||
|
if (!animation)
|
||||||
|
return 0.0f;
|
||||||
|
const float ticksPerSecond = animation->GetTicksPerSecond() > 0.0f ? animation->GetTicksPerSecond() : 25.0f;
|
||||||
|
return animation->GetDuration() / ticksPerSecond;
|
||||||
|
}
|
||||||
|
|
||||||
|
static float ClipSeconds(const AttackMove& move) {
|
||||||
|
return ClipSeconds(move.animation);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Game seconds a whole ability takes
|
||||||
|
static float AbilityTime(const Ability& ability) {
|
||||||
|
return (ability.duration > 0.0f ? ability.duration : ClipSeconds(ability.animation)) * GetGameTimeScale();
|
||||||
|
}
|
||||||
|
|
||||||
|
// Game seconds into an ability one of its frames is
|
||||||
|
static float AbilityFrameTime(const Ability& ability, float frame) {
|
||||||
|
if (ability.lastFrame > 0.0f)
|
||||||
|
return frame / ability.lastFrame * AbilityTime(ability);
|
||||||
|
return frame / 60.0f * GetGameTimeScale();
|
||||||
|
}
|
||||||
|
|
||||||
|
unsigned int CharacterCombat::NewSwingId() {
|
||||||
|
static unsigned int nextSwingId = 1;
|
||||||
|
return nextSwingId++;
|
||||||
|
}
|
||||||
|
|
||||||
|
void CharacterCombat::Reset() {
|
||||||
|
current = -1;
|
||||||
|
next = 0;
|
||||||
|
time = 0.0f;
|
||||||
|
queued = false;
|
||||||
|
ability = -1;
|
||||||
|
queuedAbility = -1;
|
||||||
|
}
|
||||||
|
|
||||||
|
void CharacterCombat::UseAbility(int index, const glm::vec3& aimPoint) {
|
||||||
|
if (!IsAbilityReady(index))
|
||||||
|
return;
|
||||||
|
queuedAbility = index;
|
||||||
|
queuedAim = aimPoint;
|
||||||
|
}
|
||||||
|
|
||||||
|
const Ability* CharacterCombat::GetCurrentAbility() const {
|
||||||
|
return ability >= 0 && ability < (int)abilities.size() ? &abilities[ability] : nullptr;
|
||||||
|
}
|
||||||
|
|
||||||
|
bool CharacterCombat::IsAbilityReady(int index) const {
|
||||||
|
if (index < 0 || index >= (int)abilities.size())
|
||||||
|
return false;
|
||||||
|
return index >= (int)cooldowns.size() || cooldowns[index] <= 0.0f;
|
||||||
|
}
|
||||||
|
|
||||||
|
float CharacterCombat::GetCooldownFraction(int index) const {
|
||||||
|
if (index < 0 || index >= (int)abilities.size() || index >= (int)cooldowns.size())
|
||||||
|
return 0.0f;
|
||||||
|
const float total = abilities[index].cooldown * GetGameTimeScale();
|
||||||
|
return total > 0.0f ? glm::clamp(cooldowns[index] / total, 0.0f, 1.0f) : 0.0f;
|
||||||
|
}
|
||||||
|
|
||||||
|
std::vector<AbilityFired> CharacterCombat::TakeFired() {
|
||||||
|
std::vector<AbilityFired> result;
|
||||||
|
result.swap(fired);
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
|
bool CharacterCombat::GetAbilityLunge(float& speed) const {
|
||||||
|
if (ability < 0)
|
||||||
|
return false;
|
||||||
|
const Ability& move = abilities[ability];
|
||||||
|
if (move.lungeDistance <= 0.0f || move.lungeEndFrame <= move.lungeStartFrame)
|
||||||
|
return false;
|
||||||
|
const float start = AbilityFrameTime(move, move.lungeStartFrame);
|
||||||
|
const float end = AbilityFrameTime(move, move.lungeEndFrame);
|
||||||
|
if (abilityTime < start || abilityTime >= end)
|
||||||
|
return false;
|
||||||
|
speed = move.lungeDistance / (end - start);
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
bool CharacterCombat::IsAbilityPivoting() const {
|
||||||
|
if (ability < 0)
|
||||||
|
return false;
|
||||||
|
const Ability& move = abilities[ability];
|
||||||
|
return move.pivots && (move.pivotEndFrame <= 0.0f || abilityTime < AbilityFrameTime(move, move.pivotEndFrame));
|
||||||
|
}
|
||||||
|
|
||||||
|
void CharacterCombat::StartAbility(CharacterBody& body, int index) {
|
||||||
|
// It takes over the action layer, so a swing in its recovery is over and the combo starts again after
|
||||||
|
current = -1;
|
||||||
|
next = 0;
|
||||||
|
queued = false;
|
||||||
|
|
||||||
|
ability = index;
|
||||||
|
abilityTime = 0.0f;
|
||||||
|
abilityFired = false;
|
||||||
|
abilityStatusFired = false;
|
||||||
|
abilityLanded = false;
|
||||||
|
abilityUse = NewSwingId();
|
||||||
|
abilityAim = queuedAim;
|
||||||
|
queuedAbility = -1;
|
||||||
|
|
||||||
|
const Ability& move = abilities[index];
|
||||||
|
cooldowns.resize(abilities.size(), 0.0f);
|
||||||
|
cooldowns[index] = move.cooldown * GetGameTimeScale();
|
||||||
|
const float length = AbilityTime(move);
|
||||||
|
if (move.animation)
|
||||||
|
body.PlayAction(move.animation, length > 0.0f ? ClipSeconds(move.animation) * GetGameTimeScale() / length : 1.0f);
|
||||||
|
}
|
||||||
|
|
||||||
|
void CharacterCombat::StepAbility(CharacterBody& body, float dt) {
|
||||||
|
// An ability's cooldown only starts running once it's finished, so it can't be chained straight back into itself
|
||||||
|
for (int i = 0; i < (int)cooldowns.size(); i++)
|
||||||
|
if (i != ability)
|
||||||
|
cooldowns[i] = std::max(cooldowns[i] - dt, 0.0f);
|
||||||
|
|
||||||
|
if (ability >= 0) {
|
||||||
|
abilityTime += dt;
|
||||||
|
const Ability& move = abilities[ability];
|
||||||
|
const float length = AbilityTime(move);
|
||||||
|
if (!abilityFired && abilityTime >= AbilityFrameTime(move, move.fireFrame)) {
|
||||||
|
abilityFired = true;
|
||||||
|
fired.push_back({ ability, abilityAim });
|
||||||
|
}
|
||||||
|
if (abilityFired && !abilityStatusFired && move.appliesStatus && move.statusFrame > 0.0f
|
||||||
|
&& abilityTime >= AbilityFrameTime(move, move.statusFrame)) {
|
||||||
|
abilityStatusFired = true;
|
||||||
|
fired.push_back({ ability, abilityAim, true });
|
||||||
|
}
|
||||||
|
// Landing cuts it short at its hitExitFrame, if it has one, and missing at its missExitFrame
|
||||||
|
const bool exitEarly = abilityLanded
|
||||||
|
? move.hitExitFrame > 0.0f && abilityTime >= AbilityFrameTime(move, move.hitExitFrame)
|
||||||
|
: move.missExitFrame > 0.0f && abilityTime >= AbilityFrameTime(move, move.missExitFrame);
|
||||||
|
if (abilityFired && (abilityTime >= length || exitEarly)) {
|
||||||
|
if (exitEarly && body.actionAnimator && body.actionAnimator->m_CurrentAnimation == move.animation)
|
||||||
|
body.actionAnimator->StopAnimation();
|
||||||
|
ability = -1;
|
||||||
|
abilityLanded = false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Waits for a swing to reach its recovery, like the next swing in a combo would
|
||||||
|
if (queuedAbility < 0 || ability >= 0)
|
||||||
|
return;
|
||||||
|
if (current >= 0 && time < FrameTime(combo[current], combo[current].recoveryFrame))
|
||||||
|
return;
|
||||||
|
StartAbility(body, queuedAbility);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Combat counts in game time like everything else in a character, so real seconds get scaled up to it
|
||||||
|
float CharacterCombat::MoveTime(const AttackMove& move) const {
|
||||||
|
return (move.duration > 0.0f ? move.duration : ClipSeconds(move)) * GetGameTimeScale();
|
||||||
|
}
|
||||||
|
|
||||||
|
float CharacterCombat::FrameTime(const AttackMove& move, float frame) const {
|
||||||
|
if (move.lastFrame <= 0.0f)
|
||||||
|
return 0.0f;
|
||||||
|
return frame / move.lastFrame * MoveTime(move);
|
||||||
|
}
|
||||||
|
|
||||||
|
void CharacterCombat::Start(CharacterBody& body, int index) {
|
||||||
|
current = index;
|
||||||
|
next = (index + 1) % (int)combo.size();
|
||||||
|
time = 0.0f;
|
||||||
|
queued = false;
|
||||||
|
swingId = NewSwingId();
|
||||||
|
|
||||||
|
// Sped up or slowed down to last as long as the move does
|
||||||
|
const AttackMove& move = combo[index];
|
||||||
|
const float length = MoveTime(move);
|
||||||
|
body.PlayAction(move.animation, length > 0.0f ? ClipSeconds(move) * GetGameTimeScale() / length : 1.0f);
|
||||||
|
|
||||||
|
// A little step into it
|
||||||
|
if (body.physics && move.forwardVelocity != 0.0f) {
|
||||||
|
body.physics->linearvelocity += body.GetForwardDirection() * move.forwardVelocity;
|
||||||
|
body.physics->WakeUp();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void CharacterCombat::Step(CharacterBody& body, float dt) {
|
||||||
|
StepAbility(body, dt);
|
||||||
|
if (combo.empty() || ability >= 0) {
|
||||||
|
queued = false; // no swinging in the middle of an ability
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (current >= 0) {
|
||||||
|
time += dt;
|
||||||
|
const AttackMove& move = combo[current];
|
||||||
|
if (time >= FrameTime(move, move.lastFrame)) {
|
||||||
|
// Let it play all the way out, so the combo starts over
|
||||||
|
current = -1;
|
||||||
|
next = 0;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!queued)
|
||||||
|
return;
|
||||||
|
if (current < 0) {
|
||||||
|
Start(body, next);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Cutting into the recovery carries the combo on. The last swing has to be waited out.
|
||||||
|
const AttackMove& move = combo[current];
|
||||||
|
const bool last = current == (int)combo.size() - 1;
|
||||||
|
if (!last && time >= FrameTime(move, move.recoveryFrame))
|
||||||
|
Start(body, next);
|
||||||
|
}
|
||||||
|
|
||||||
|
const AttackMove* CharacterCombat::GetCurrentMove() const {
|
||||||
|
return current >= 0 && current < (int)combo.size() ? &combo[current] : nullptr;
|
||||||
|
}
|
||||||
|
|
||||||
|
bool CharacterCombat::IsHitting() const {
|
||||||
|
const AttackMove* move = GetCurrentMove();
|
||||||
|
return move && time >= FrameTime(*move, move->hitStartFrame) && time <= FrameTime(*move, move->hitEndFrame);
|
||||||
|
}
|
||||||
@@ -0,0 +1,187 @@
|
|||||||
|
#ifndef COMBAT_CLASS
|
||||||
|
#define COMBAT_CLASS
|
||||||
|
|
||||||
|
#include <string>
|
||||||
|
|
||||||
|
#include "StatusEffect.h"
|
||||||
|
#include <vector>
|
||||||
|
|
||||||
|
#include <glm/glm.hpp>
|
||||||
|
|
||||||
|
class Animation;
|
||||||
|
class CharacterBody;
|
||||||
|
|
||||||
|
// One swing in a combo. Timings are frames of its animation, which has lastFrame of them.
|
||||||
|
struct AttackMove {
|
||||||
|
Animation* animation = nullptr;
|
||||||
|
float duration = 0.5f; // real seconds for the whole move, recovery and all. 0 plays the animation at its own speed.
|
||||||
|
float lastFrame = 120.0f;
|
||||||
|
float hitStartFrame = 40.0f; // the fist can connect from here
|
||||||
|
float hitEndFrame = 60.0f; // to here
|
||||||
|
float recoveryFrame = 60.0f; // from here it's just getting back to normal, and the next swing can cut in
|
||||||
|
float damage = 8.0f;
|
||||||
|
glm::vec3 hitboxSize = glm::vec3(0.9f, 1.2f, 1.0f); // meters across, tall and deep
|
||||||
|
glm::vec3 hitboxOffset = glm::vec3(0.0f, 0.3f, 0.85f); // its middle from the attacker's: right, up and forward. Starts at its front.
|
||||||
|
float knockback = 2.0f; // m/s away from the attacker
|
||||||
|
float knockbackUp = 0.0f; // m/s upward, enough of it and they leave the ground
|
||||||
|
float stun = 0.0f; // real seconds whoever it hits can't do anything
|
||||||
|
bool hitsKnockedDown = false; // can hit characters that are knocked down
|
||||||
|
float moveSpeedMultiplier = 0.0f; // of walking speed while it's going, recovery and all. 0 stands still.
|
||||||
|
float forwardVelocity = 4.0f; // m/s added along where it faces as the swing starts
|
||||||
|
};
|
||||||
|
|
||||||
|
// A move on a number key, like shooting. It plays on the action layer like a swing and goes off partway through,
|
||||||
|
// then can't be used again until its cooldown is up. Timings are frames of its animation, which has lastFrame of them,
|
||||||
|
// or with lastFrame 0, frames at 60 a second.
|
||||||
|
struct Ability {
|
||||||
|
enum class Effect {
|
||||||
|
Shot, // a long thin hitbox from the gun toward where it was aimed, out for the frame it fires
|
||||||
|
Stab, // a short hitbox in front of it the frame it fires. A hit leaves its prop stuck in them, bleeding them.
|
||||||
|
Sweep // a wide hitbox in front of it the frame it fires, catching everyone in it
|
||||||
|
};
|
||||||
|
|
||||||
|
std::string name;
|
||||||
|
std::string description; // what it does, shown hovering over it in the moves list
|
||||||
|
Effect effect = Effect::Shot;
|
||||||
|
Animation* animation = nullptr;
|
||||||
|
float duration = 0.0f; // real seconds for the whole move. 0 plays the animation at its own speed.
|
||||||
|
float lastFrame = 40.0f;
|
||||||
|
float fireFrame = 4.0f; // when it goes off
|
||||||
|
float hitExitFrame = 0.0f; // when it lands, it's over this far in rather than at lastFrame. 0 plays it all.
|
||||||
|
float missExitFrame = 0.0f; // when it doesn't land, it's over this far in. 0 plays it all.
|
||||||
|
float cooldown = 0.5f; // real seconds from it finishing until it can be used again
|
||||||
|
float moveSpeedMultiplier = 0.5f; // of walking speed while it's going
|
||||||
|
bool pivots = false; // keeps turning to face where the player looks while it's going, so it can be aimed
|
||||||
|
float pivotEndFrame = 0.0f; // stops pivoting here, 0 pivots all the way through
|
||||||
|
std::string sound; // played where it goes off, as it does. Empty plays nothing.
|
||||||
|
float screenshake = 0.0f; // how hard the camera shakes as it goes off, when it's the player's
|
||||||
|
|
||||||
|
float damage = 12.0f;
|
||||||
|
float range = 100.0f; // meters, how long the shot's hitbox reaches
|
||||||
|
glm::vec2 hitboxSize = glm::vec2(0.3f, 0.3f); // meters across and tall, around the line of the shot
|
||||||
|
float knockback = 1.0f; // m/s away from the shooter
|
||||||
|
float stun = 0.0f; // real seconds whoever it hits can't do anything
|
||||||
|
float knockdownTime = 0.0f; // real seconds whoever it hits is knocked down for, 0 doesn't
|
||||||
|
// Played on whoever it kills, instead of them just dropping. If it has a bone called Camera and the player is
|
||||||
|
// either of them, the camera follows that bone until it's over.
|
||||||
|
Animation* killAnimation = nullptr;
|
||||||
|
struct KillSound {
|
||||||
|
float frame = 0.0f; // into killAnimation, at 60 a second
|
||||||
|
std::string file;
|
||||||
|
};
|
||||||
|
std::vector<KillSound> killSounds; // played over the kill animation, heard when the camera's following it
|
||||||
|
bool hitsKnockedDown = false; // can hit characters that are knocked down
|
||||||
|
|
||||||
|
// A model shown on one of the animation's own bones while it's going, like a knife. Empty shows nothing.
|
||||||
|
std::string prop;
|
||||||
|
std::string propBone;
|
||||||
|
glm::vec3 propTurn = glm::vec3(0.0f); // degrees around x, y and z on top of the bone's rotation
|
||||||
|
|
||||||
|
// Stab only: how long the prop stays stuck in whoever it hits, hurting them bleedDamage a second meanwhile
|
||||||
|
float bleedTime = 0.0f; // real seconds
|
||||||
|
float bleedDamage = 0.0f;
|
||||||
|
|
||||||
|
// Carries it forward lungeDistance meters, at a steady pace from lungeStartFrame to lungeEndFrame. 0 stays put.
|
||||||
|
float lungeDistance = 0.0f;
|
||||||
|
float lungeStartFrame = 0.0f;
|
||||||
|
float lungeEndFrame = 0.0f;
|
||||||
|
|
||||||
|
// Whoever it hits gets status for statusTime real seconds, at statusFrame if it's still going then,
|
||||||
|
// or as they're hit with statusFrame 0
|
||||||
|
bool appliesStatus = false;
|
||||||
|
StatusEffect::Type status = StatusEffect::Type::HeavyGravity;
|
||||||
|
float statusTime = 0.0f;
|
||||||
|
float statusFrame = 0.0f;
|
||||||
|
};
|
||||||
|
|
||||||
|
// An ability that's just gone off, for the game to carry out
|
||||||
|
struct AbilityFired {
|
||||||
|
int index = -1; // into abilities
|
||||||
|
glm::vec3 aimPoint = glm::vec3(0.0f); // world space, what it was aimed at
|
||||||
|
bool status = false; // its statusFrame came rather than its fireFrame: whoever it hit gets its status now
|
||||||
|
};
|
||||||
|
|
||||||
|
// A character's attacks: a combo that goes through its moves in order, each click starting the next one
|
||||||
|
// while the last is recovering. Waiting out a recovery starts the combo over, so does finishing it.
|
||||||
|
// Abilities sit beside it, one per number key.
|
||||||
|
class CharacterCombat {
|
||||||
|
public:
|
||||||
|
std::vector<AttackMove> combo;
|
||||||
|
std::string comboName; // what the moves list calls the combo
|
||||||
|
std::vector<Ability> abilities; // abilities[0] is on 1, abilities[1] on 2 and so on
|
||||||
|
|
||||||
|
// Asks for the next swing, which starts as soon as the one going lets it
|
||||||
|
void QueueAttack() { queued = true; }
|
||||||
|
|
||||||
|
// Uses abilities[index] aimed at aimPoint, as soon as whatever's going lets it. Nothing while it's cooling down.
|
||||||
|
void UseAbility(int index, const glm::vec3& aimPoint);
|
||||||
|
|
||||||
|
// Stops whatever swing or ability is going, and starts the combo over. Cooldowns carry on.
|
||||||
|
void Reset();
|
||||||
|
|
||||||
|
// Once a frame, dt in game time. Starts queued swings on the body's action layer.
|
||||||
|
void Step(CharacterBody& body, float dt);
|
||||||
|
|
||||||
|
// The swing going now, nullptr between swings
|
||||||
|
const AttackMove* GetCurrentMove() const;
|
||||||
|
int GetCurrentIndex() const { return current; }
|
||||||
|
|
||||||
|
// The ability going now, nullptr if none is
|
||||||
|
const Ability* GetCurrentAbility() const;
|
||||||
|
|
||||||
|
// Different every time an ability starts, for telling one use from the next
|
||||||
|
unsigned int GetAbilityUse() const { return abilityUse; }
|
||||||
|
|
||||||
|
// The ability going now landed, so it ends early if it has a hitExitFrame
|
||||||
|
void AbilityLanded() { abilityLanded = ability >= 0; }
|
||||||
|
bool HasAbilityLanded() const { return abilityLanded; }
|
||||||
|
|
||||||
|
// Whether the ability it's in is lunging it forward right now, and if so how fast, in m/s of game time
|
||||||
|
bool GetAbilityLunge(float& speed) const;
|
||||||
|
|
||||||
|
// Whether the ability it's in turns it to face where it's aimed right now
|
||||||
|
bool IsAbilityPivoting() const;
|
||||||
|
|
||||||
|
// Whether abilities[index] can be used now, and how much of its cooldown is left, 1 just used to 0 ready
|
||||||
|
bool IsAbilityReady(int index) const;
|
||||||
|
float GetCooldownFraction(int index) const;
|
||||||
|
|
||||||
|
// Abilities that went off since the last call, emptied by it
|
||||||
|
std::vector<AbilityFired> TakeFired();
|
||||||
|
|
||||||
|
// Whether the current swing can connect right now, so its hitbox is out
|
||||||
|
bool IsHitting() const;
|
||||||
|
|
||||||
|
// Different for every swing anyone throws, so its hitbox only hits each character once
|
||||||
|
unsigned int GetSwingId() const { return swingId; }
|
||||||
|
|
||||||
|
// A swing id nothing else has, for anything else that puts out hitboxes
|
||||||
|
static unsigned int NewSwingId();
|
||||||
|
|
||||||
|
private:
|
||||||
|
float MoveTime(const AttackMove& move) const; // game seconds the whole move takes
|
||||||
|
float FrameTime(const AttackMove& move, float frame) const; // game seconds into the move
|
||||||
|
void Start(CharacterBody& body, int index);
|
||||||
|
void StartAbility(CharacterBody& body, int index);
|
||||||
|
void StepAbility(CharacterBody& body, float dt);
|
||||||
|
|
||||||
|
int current = -1; // index into combo, -1 between swings
|
||||||
|
int next = 0;
|
||||||
|
float time = 0.0f; // seconds into the current swing
|
||||||
|
bool queued = false;
|
||||||
|
unsigned int swingId = 0;
|
||||||
|
|
||||||
|
int ability = -1; // index into abilities going now, -1 if none
|
||||||
|
float abilityTime = 0.0f; // game seconds into it
|
||||||
|
bool abilityFired = false;
|
||||||
|
bool abilityStatusFired = false;
|
||||||
|
bool abilityLanded = false;
|
||||||
|
unsigned int abilityUse = 0;
|
||||||
|
int queuedAbility = -1;
|
||||||
|
glm::vec3 queuedAim = glm::vec3(0.0f);
|
||||||
|
glm::vec3 abilityAim = glm::vec3(0.0f);
|
||||||
|
std::vector<float> cooldowns; // game seconds left, by ability
|
||||||
|
std::vector<AbilityFired> fired;
|
||||||
|
};
|
||||||
|
|
||||||
|
#endif
|
||||||
+212
@@ -0,0 +1,212 @@
|
|||||||
|
#include "Console.h"
|
||||||
|
|
||||||
|
#include <algorithm>
|
||||||
|
#include <cctype>
|
||||||
|
#include <cmath>
|
||||||
|
#include <sstream>
|
||||||
|
|
||||||
|
#include "Gui.h"
|
||||||
|
|
||||||
|
static constexpr float PANEL_HEIGHT = 360.0f;
|
||||||
|
static constexpr float INPUT_HEIGHT = 36.0f;
|
||||||
|
static constexpr float MARGIN = 16.0f;
|
||||||
|
static constexpr int VISIBLE_LINES = 13;
|
||||||
|
static constexpr int MAX_LOG_LINES = 300;
|
||||||
|
static constexpr int MAX_INPUT_LENGTH = 200;
|
||||||
|
static constexpr int SCROLL_STEP = 5;
|
||||||
|
|
||||||
|
static std::string Lowercase(std::string text)
|
||||||
|
{
|
||||||
|
std::transform(text.begin(), text.end(), text.begin(), [](unsigned char c) { return (char)std::tolower(c); });
|
||||||
|
return text;
|
||||||
|
}
|
||||||
|
|
||||||
|
Console::Console(Font* newFont) : font(newFont)
|
||||||
|
{
|
||||||
|
Register("help", "help", "lists every command", [this](const Arguments&) {
|
||||||
|
for (const auto& [name, entry] : commands)
|
||||||
|
Print(" " + entry.usage + " - " + entry.description);
|
||||||
|
});
|
||||||
|
Register("clear", "clear", "empties the console", [this](const Arguments&) {
|
||||||
|
log.clear();
|
||||||
|
scroll = 0;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// GUI draws in the order things were made, so this waits until the first Update to land on top of everything
|
||||||
|
void Console::CreateDisplay()
|
||||||
|
{
|
||||||
|
background = new Box();
|
||||||
|
background->Color = { 0.02f, 0.03f, 0.05f };
|
||||||
|
background->Opacity = 0.0f;
|
||||||
|
background->t2d.position = { 0.0f, 0.0f, 0.0f, 0.0f };
|
||||||
|
background->t2d.size = { 1.0f, 0.0f, 0.0f, PANEL_HEIGHT };
|
||||||
|
background->BindToRenderSystem();
|
||||||
|
|
||||||
|
inputBackground = new Box();
|
||||||
|
inputBackground->Color = { 0.10f, 0.12f, 0.16f };
|
||||||
|
inputBackground->Opacity = 0.0f;
|
||||||
|
inputBackground->t2d.position = { 0.0f, 0.0f, 0.0f, PANEL_HEIGHT - INPUT_HEIGHT };
|
||||||
|
inputBackground->t2d.size = { 1.0f, 0.0f, 0.0f, INPUT_HEIGHT };
|
||||||
|
inputBackground->BindToRenderSystem();
|
||||||
|
|
||||||
|
// Bottom aligned, so the newest line sits just above the input and old ones get cut off at the top
|
||||||
|
logText = new TextBox();
|
||||||
|
logText->font = font;
|
||||||
|
logText->fontsize = 0.42f;
|
||||||
|
logText->Color = { 0.85f, 0.88f, 0.93f };
|
||||||
|
logText->Opacity = 0.0f;
|
||||||
|
logText->t2d.position = { 0.0f, 0.0f, MARGIN, 8.0f };
|
||||||
|
logText->t2d.size = { 1.0f, 0.0f, -2.0f * MARGIN, PANEL_HEIGHT - INPUT_HEIGHT - 12.0f };
|
||||||
|
logText->textCenter = { 0.0f, 1.0f };
|
||||||
|
logText->BindToRenderSystem();
|
||||||
|
|
||||||
|
inputText = new TextBox();
|
||||||
|
inputText->font = font;
|
||||||
|
inputText->fontsize = 0.45f;
|
||||||
|
inputText->Color = { 0.96f, 0.58f, 0.16f };
|
||||||
|
inputText->Opacity = 0.0f;
|
||||||
|
inputText->t2d.position = { 0.0f, 0.0f, MARGIN, PANEL_HEIGHT - INPUT_HEIGHT };
|
||||||
|
inputText->t2d.size = { 1.0f, 0.0f, -2.0f * MARGIN, INPUT_HEIGHT };
|
||||||
|
inputText->textCenter = { 0.0f, 0.5f };
|
||||||
|
inputText->BindToRenderSystem();
|
||||||
|
}
|
||||||
|
|
||||||
|
void Console::Register(const std::string& name, const std::string& usage, const std::string& description, Command run)
|
||||||
|
{
|
||||||
|
commands[Lowercase(name)] = { usage, description, std::move(run) };
|
||||||
|
}
|
||||||
|
|
||||||
|
void Console::Execute(const std::string& line)
|
||||||
|
{
|
||||||
|
std::istringstream words(line);
|
||||||
|
std::string name;
|
||||||
|
if (!(words >> name))
|
||||||
|
return;
|
||||||
|
|
||||||
|
Arguments arguments;
|
||||||
|
for (std::string word; words >> word; )
|
||||||
|
arguments.push_back(word);
|
||||||
|
|
||||||
|
auto command = commands.find(Lowercase(name));
|
||||||
|
if (command == commands.end()) {
|
||||||
|
Print("Unknown command '" + name + "', type help for a list");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
command->second.run(arguments);
|
||||||
|
}
|
||||||
|
|
||||||
|
void Console::Print(const std::string& message)
|
||||||
|
{
|
||||||
|
std::istringstream lines(message);
|
||||||
|
for (std::string line; std::getline(lines, line); ) {
|
||||||
|
log.push_back(line);
|
||||||
|
if ((int)log.size() > MAX_LOG_LINES)
|
||||||
|
log.pop_front();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void Console::Open()
|
||||||
|
{
|
||||||
|
open = true;
|
||||||
|
historyIndex = (int)history.size();
|
||||||
|
caretTimer = 0.0f;
|
||||||
|
}
|
||||||
|
|
||||||
|
void Console::Close()
|
||||||
|
{
|
||||||
|
open = false;
|
||||||
|
}
|
||||||
|
|
||||||
|
void Console::OnChar(unsigned int codepoint)
|
||||||
|
{
|
||||||
|
// The font only has printable ASCII, and the toggle key shouldn't end up in the line
|
||||||
|
if (codepoint < 32 || codepoint > 126 || codepoint == '`' || codepoint == '~')
|
||||||
|
return;
|
||||||
|
if ((int)input.size() < MAX_INPUT_LENGTH)
|
||||||
|
input += (char)codepoint;
|
||||||
|
caretTimer = 0.0f;
|
||||||
|
}
|
||||||
|
|
||||||
|
void Console::OnKey(int key)
|
||||||
|
{
|
||||||
|
caretTimer = 0.0f;
|
||||||
|
switch (key) {
|
||||||
|
case GLFW_KEY_ENTER:
|
||||||
|
case GLFW_KEY_KP_ENTER:
|
||||||
|
if (input.empty())
|
||||||
|
break;
|
||||||
|
Print("> " + input);
|
||||||
|
if (history.empty() || history.back() != input)
|
||||||
|
history.push_back(input);
|
||||||
|
historyIndex = (int)history.size();
|
||||||
|
scroll = 0;
|
||||||
|
{
|
||||||
|
// Copied first, a command is allowed to touch the console's own state
|
||||||
|
const std::string line = input;
|
||||||
|
input.clear();
|
||||||
|
Execute(line);
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
case GLFW_KEY_BACKSPACE:
|
||||||
|
if (!input.empty())
|
||||||
|
input.pop_back();
|
||||||
|
break;
|
||||||
|
case GLFW_KEY_UP:
|
||||||
|
if (historyIndex > 0) {
|
||||||
|
historyIndex--;
|
||||||
|
input = history[historyIndex];
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
case GLFW_KEY_DOWN:
|
||||||
|
if (historyIndex < (int)history.size()) {
|
||||||
|
historyIndex++;
|
||||||
|
input = historyIndex < (int)history.size() ? history[historyIndex] : std::string();
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
case GLFW_KEY_PAGE_UP:
|
||||||
|
Scroll(SCROLL_STEP);
|
||||||
|
break;
|
||||||
|
case GLFW_KEY_PAGE_DOWN:
|
||||||
|
Scroll(-SCROLL_STEP);
|
||||||
|
break;
|
||||||
|
case GLFW_KEY_ESCAPE:
|
||||||
|
Close();
|
||||||
|
break;
|
||||||
|
default:
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void Console::Scroll(int lines)
|
||||||
|
{
|
||||||
|
scroll = glm::clamp(scroll + lines, 0, std::max((int)log.size() - VISIBLE_LINES, 0));
|
||||||
|
}
|
||||||
|
|
||||||
|
void Console::Update(float dt)
|
||||||
|
{
|
||||||
|
if (!background)
|
||||||
|
CreateDisplay();
|
||||||
|
|
||||||
|
const float opacity = open ? 1.0f : 0.0f;
|
||||||
|
background->Opacity = open ? 0.88f : 0.0f;
|
||||||
|
inputBackground->Opacity = open ? 0.9f : 0.0f;
|
||||||
|
logText->Opacity = opacity;
|
||||||
|
inputText->Opacity = opacity;
|
||||||
|
if (!open)
|
||||||
|
return;
|
||||||
|
|
||||||
|
const int newest = (int)log.size() - scroll;
|
||||||
|
const int oldest = std::max(newest - VISIBLE_LINES, 0);
|
||||||
|
std::string visible;
|
||||||
|
for (int i = oldest; i < newest; i++) {
|
||||||
|
visible += log[i];
|
||||||
|
if (i + 1 < newest)
|
||||||
|
visible += '\n';
|
||||||
|
}
|
||||||
|
logText->text = visible;
|
||||||
|
|
||||||
|
caretTimer += dt;
|
||||||
|
const bool caretOn = std::fmod(caretTimer, 1.0f) < 0.5f;
|
||||||
|
inputText->text = "> " + input + (caretOn ? "_" : "");
|
||||||
|
}
|
||||||
@@ -0,0 +1,67 @@
|
|||||||
|
#ifndef CONSOLE_CLASS
|
||||||
|
#define CONSOLE_CLASS
|
||||||
|
|
||||||
|
#include <deque>
|
||||||
|
#include <functional>
|
||||||
|
#include <map>
|
||||||
|
#include <string>
|
||||||
|
#include <vector>
|
||||||
|
|
||||||
|
class Font;
|
||||||
|
class Box;
|
||||||
|
class TextBox;
|
||||||
|
|
||||||
|
// The in-game console. Commands get registered by name and run with whatever words follow them.
|
||||||
|
class Console {
|
||||||
|
public:
|
||||||
|
using Arguments = std::vector<std::string>; // the words after the command name
|
||||||
|
using Command = std::function<void(const Arguments&)>;
|
||||||
|
|
||||||
|
explicit Console(Font* newFont);
|
||||||
|
|
||||||
|
// usage is shown by help, like "give <item> [count]". Names aren't case sensitive.
|
||||||
|
void Register(const std::string& name, const std::string& usage, const std::string& description, Command run);
|
||||||
|
void Execute(const std::string& line);
|
||||||
|
void Print(const std::string& message);
|
||||||
|
|
||||||
|
void Open();
|
||||||
|
void Close();
|
||||||
|
void Toggle() { open ? Close() : Open(); }
|
||||||
|
bool IsOpen() const { return open; }
|
||||||
|
|
||||||
|
// Feed these from the GLFW callbacks while it's open
|
||||||
|
void OnChar(unsigned int codepoint);
|
||||||
|
void OnKey(int key);
|
||||||
|
|
||||||
|
// Positive goes back toward older lines
|
||||||
|
void Scroll(int lines);
|
||||||
|
|
||||||
|
// Once a frame, refreshes what's on screen
|
||||||
|
void Update(float dt);
|
||||||
|
|
||||||
|
private:
|
||||||
|
struct Entry {
|
||||||
|
std::string usage;
|
||||||
|
std::string description;
|
||||||
|
Command run;
|
||||||
|
};
|
||||||
|
|
||||||
|
void CreateDisplay();
|
||||||
|
|
||||||
|
Font* font = nullptr;
|
||||||
|
std::map<std::string, Entry> commands; // sorted, so help lists them alphabetically
|
||||||
|
std::deque<std::string> log;
|
||||||
|
std::vector<std::string> history;
|
||||||
|
int historyIndex = 0; // history.size() means the line being typed
|
||||||
|
std::string input;
|
||||||
|
int scroll = 0; // how many lines up from the newest
|
||||||
|
float caretTimer = 0.0f;
|
||||||
|
bool open = false;
|
||||||
|
|
||||||
|
Box* background = nullptr;
|
||||||
|
Box* inputBackground = nullptr;
|
||||||
|
TextBox* logText = nullptr;
|
||||||
|
TextBox* inputText = nullptr;
|
||||||
|
};
|
||||||
|
|
||||||
|
#endif
|
||||||
@@ -0,0 +1,67 @@
|
|||||||
|
#ifndef CUBEMAP_CLASS
|
||||||
|
#define CUBEMAP_CLASS
|
||||||
|
|
||||||
|
#include <vector>
|
||||||
|
|
||||||
|
#include "stb_image.h"
|
||||||
|
#include <glad/glad.h>
|
||||||
|
|
||||||
|
#include "shaderClass.h"
|
||||||
|
|
||||||
|
class Cubemap {
|
||||||
|
public:
|
||||||
|
GLuint ID = 0;
|
||||||
|
|
||||||
|
Cubemap() {
|
||||||
|
};
|
||||||
|
|
||||||
|
Cubemap(std::vector<std::string>& faces) {
|
||||||
|
FillCubemap(faces);
|
||||||
|
};
|
||||||
|
|
||||||
|
void FillCubemap(std::vector<std::string>& faces) {
|
||||||
|
|
||||||
|
if (ID != 0) {
|
||||||
|
Delete();
|
||||||
|
}
|
||||||
|
|
||||||
|
glGenTextures(1, &ID);
|
||||||
|
glBindTexture(GL_TEXTURE_CUBE_MAP, ID);
|
||||||
|
|
||||||
|
int width, height, nrChannels;
|
||||||
|
for (unsigned int i = 0; i < faces.size(); i++)
|
||||||
|
{
|
||||||
|
unsigned char* data = stbi_load(faces[i].c_str(), &width, &height, &nrChannels, 0);
|
||||||
|
if (data)
|
||||||
|
{
|
||||||
|
glTexImage2D(GL_TEXTURE_CUBE_MAP_POSITIVE_X + i,
|
||||||
|
0, GL_RGB, width, height, 0, GL_RGB, GL_UNSIGNED_BYTE, data
|
||||||
|
);
|
||||||
|
stbi_image_free(data);
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
std::cout << "Cubemap tex failed to load at path: " << faces[i] << std::endl;
|
||||||
|
stbi_image_free(data);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
glTexParameteri(GL_TEXTURE_CUBE_MAP, GL_TEXTURE_MIN_FILTER, GL_LINEAR_MIPMAP_LINEAR);
|
||||||
|
glTexParameteri(GL_TEXTURE_CUBE_MAP, GL_TEXTURE_MAG_FILTER, GL_LINEAR);
|
||||||
|
glTexParameteri(GL_TEXTURE_CUBE_MAP, GL_TEXTURE_WRAP_S, GL_CLAMP_TO_EDGE);
|
||||||
|
glTexParameteri(GL_TEXTURE_CUBE_MAP, GL_TEXTURE_WRAP_T, GL_CLAMP_TO_EDGE);
|
||||||
|
glTexParameteri(GL_TEXTURE_CUBE_MAP, GL_TEXTURE_WRAP_R, GL_CLAMP_TO_EDGE);
|
||||||
|
glGenerateMipmap(GL_TEXTURE_CUBE_MAP);
|
||||||
|
}
|
||||||
|
|
||||||
|
void Bind() {
|
||||||
|
glBindTexture(GL_TEXTURE_CUBE_MAP, ID);
|
||||||
|
};
|
||||||
|
void Unbind() {
|
||||||
|
glBindTexture(GL_TEXTURE_CUBE_MAP, 0);
|
||||||
|
};
|
||||||
|
void Delete() {
|
||||||
|
glDeleteTextures(1, &ID);
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
#endif
|
||||||
@@ -0,0 +1,111 @@
|
|||||||
|
|
||||||
|
#ifndef DEBUG_CLASS
|
||||||
|
#define DEBUG_CLASS
|
||||||
|
|
||||||
|
#include <iostream>
|
||||||
|
#include <chrono>
|
||||||
|
#include <string>
|
||||||
|
|
||||||
|
#include<glad/glad.h>
|
||||||
|
#include<GLFW/glfw3.h>
|
||||||
|
#include<glm/glm.hpp>
|
||||||
|
#include<glm/gtc/matrix_transform.hpp>
|
||||||
|
#include<glm/gtc/type_ptr.hpp>
|
||||||
|
|
||||||
|
#include "tFunctions.h"
|
||||||
|
#include "Mesh.h"
|
||||||
|
#include "Structures.h"
|
||||||
|
|
||||||
|
const std::vector<Vertex> CUBEVERTICES = {
|
||||||
|
{{0.5f,0.5f,0.5f}, {1.0f,0.0f,0.0f},{ 0.0f,0.0f} },
|
||||||
|
{{0.5f,0.5f,-0.5f}, {1.0f,0.0f,0.0f},{ 0.0f,1.0f}},
|
||||||
|
{{0.5f,-0.5f,-0.5f}, {1.0f,0.0f,0.0f},{ 1.0f,1.0f}},
|
||||||
|
{{0.5f,-0.5f,0.5f}, {1.0f,0.0f,0.0f}, {1.0f,0.0f}},
|
||||||
|
|
||||||
|
{{-0.5f,0.5f,0.5f}, {-1.0f,0.0f,0.0f}, {0.0f,0.0f}},
|
||||||
|
{{-0.5f,0.5f,-0.5f}, {-1.0f,0.0f,0.0f}, {0.0f,1.0f}},
|
||||||
|
{{-0.5f,-0.5f,-0.5f}, {-1.0f,0.0f,0.0f}, {1.0f,1.0f}},
|
||||||
|
{{-0.5f,-0.5f,0.5f},{ -1.0f,0.0f,0.0f}, {1.0f,0.0f}},
|
||||||
|
|
||||||
|
{{0.5f,0.5f,0.5f}, {0.0f,1.0f,0.0f},{ 0.0f,0.0f} },
|
||||||
|
{{0.5f,0.5f,-0.5f}, {0.0f,1.0f,0.0f},{ 0.0f,1.0f}},
|
||||||
|
{{-0.5f,0.5f,-0.5f}, {0.0f,1.0f,0.0f},{ 1.0f,1.0f}},
|
||||||
|
{{-0.5f,0.5f,0.5f}, {0.0f,1.0f,0.0f}, {1.0f,0.0f}},
|
||||||
|
|
||||||
|
{{0.5f,-0.5f,0.5f}, {0.0f,-1.0f,0.0f}, {0.0f,0.0f}},
|
||||||
|
{{0.5f,-0.5f,-0.5f}, {0.0f,-1.0f,0.0f}, {0.0f,1.0f}},
|
||||||
|
{{-0.5f,-0.5f,-0.5f}, {0.0f,-1.0f,0.0f}, {1.0f,1.0f}},
|
||||||
|
{{-0.5f,-0.5f,0.5f},{ 0.0f,-1.0f,0.0f}, {1.0f,0.0f}},
|
||||||
|
|
||||||
|
{{0.5f,0.5f,0.5f}, {0.0f,0.0f,1.0f},{ 0.0f,0.0f} },
|
||||||
|
{{-0.5f,0.5f,0.5f}, {0.0f,0.0f,1.0f},{ 0.0f,1.0f}},
|
||||||
|
{{-0.5f,-0.5f,0.5f}, {0.0f,0.0f,1.0f},{ 1.0f,1.0f}},
|
||||||
|
{{0.5f,-0.5f,0.5f}, {0.0f,0.0f,1.0f}, {1.0f,0.0f}},
|
||||||
|
|
||||||
|
{{0.5f,0.5f,-0.5f}, {0.0f,0.0f,-1.0f}, {0.0f,0.0f}},
|
||||||
|
{{-0.5f,0.5f,-0.5f}, {0.0f,0.0f,-1.0f}, {0.0f,1.0f}},
|
||||||
|
{{-0.5f,-0.5f,-0.5f}, {0.0f,0.0f,-1.0f}, {1.0f,1.0f}},
|
||||||
|
{{0.5f,-0.5f,-0.5f},{ 0.0f,0.0f,-1.0f}, {1.0f,0.0f}}
|
||||||
|
};
|
||||||
|
|
||||||
|
const std::vector<unsigned int> CUBEINDICES = {
|
||||||
|
0, 3, 1, 3, 2, 1,
|
||||||
|
4, 5, 7, 5, 6, 7,
|
||||||
|
|
||||||
|
8, 9, 11, 9, 10, 11,
|
||||||
|
12, 15, 13, 15, 14, 13,
|
||||||
|
|
||||||
|
16, 17, 19, 17, 18, 19,
|
||||||
|
20, 23, 21, 23, 22, 21
|
||||||
|
};
|
||||||
|
|
||||||
|
const std::vector<Vertex> PLANEVERTICES = {
|
||||||
|
{{0.5f,0.0f,0.5f}, {0.0f,1.0f,0.0f},{ 0.0f,0.0f} },
|
||||||
|
{{-0.5f,0.0f,0.5f}, {0.0f,1.0f,0.0f},{ 1.0f,0.0f}},
|
||||||
|
{{-0.5f,0.0f,-0.5f}, {0.0f,1.0f,0.0f},{ 1.0f,1.0f}},
|
||||||
|
{{0.5f,0.0f,-0.5f}, {0.0f,1.0f,0.0f}, {0.0f,1.0f}},
|
||||||
|
};
|
||||||
|
|
||||||
|
const std::vector<unsigned int> PLANEINDICES = {
|
||||||
|
0,1,2,
|
||||||
|
2,3,0,
|
||||||
|
|
||||||
|
2,1,0,
|
||||||
|
0,3,2,
|
||||||
|
};
|
||||||
|
|
||||||
|
inline void Output(std::string& to) {
|
||||||
|
std::cout << to << '\n';
|
||||||
|
}
|
||||||
|
|
||||||
|
inline void Output(float& to) {
|
||||||
|
std::cout << to << '\n';
|
||||||
|
}
|
||||||
|
|
||||||
|
inline void Output(int& to) {
|
||||||
|
std::cout << to << '\n';
|
||||||
|
}
|
||||||
|
|
||||||
|
inline void Output(glm::vec3 tooutput) {
|
||||||
|
std::cout << tooutput.x << ' ' << tooutput.y << ' ' << tooutput.z << '\n';
|
||||||
|
}
|
||||||
|
|
||||||
|
inline void Output(BoundingBox tooutput) {
|
||||||
|
std::cout << tooutput.min.x << ' ' << tooutput.min.y << ' ' << tooutput.min.z << ' ' << tooutput.max.x << ' ' << tooutput.max.y << ' ' << tooutput.max.z << '\n';
|
||||||
|
}
|
||||||
|
|
||||||
|
inline void Output(glm::mat4 tooutput) {
|
||||||
|
std::cout << '[' << tooutput[0][0] << ',' << tooutput[0][1] << ',' << tooutput[0][2] << ',' << tooutput[0][3] << ',' << '\n'
|
||||||
|
<< tooutput[1][0] << ',' << tooutput[1][1] << ',' << tooutput[1][2] << ',' << tooutput[1][3] << ',' << '\n'
|
||||||
|
<< tooutput[2][0] << ',' << tooutput[2][1] << ',' << tooutput[2][2] << ',' << tooutput[2][3] << ',' << '\n'
|
||||||
|
<< tooutput[3][0] << ',' << tooutput[3][1] << ',' << tooutput[3][2] << ',' << tooutput[3][3] << ',' << '\n' << ']' << '\n';
|
||||||
|
}
|
||||||
|
|
||||||
|
inline Mesh* CreateCubeMesh() {
|
||||||
|
return new Mesh(CUBEVERTICES, CUBEINDICES);
|
||||||
|
}
|
||||||
|
inline Mesh* CreatePlaneMesh() {
|
||||||
|
return new Mesh(PLANEVERTICES, PLANEINDICES);
|
||||||
|
}
|
||||||
|
|
||||||
|
#endif
|
||||||
+54
@@ -0,0 +1,54 @@
|
|||||||
|
|
||||||
|
#ifndef DIRLIGHT_CLASS
|
||||||
|
#define DIRLIGHT_CLASS
|
||||||
|
|
||||||
|
#include<glad/glad.h>
|
||||||
|
#include<GLFW/glfw3.h>
|
||||||
|
#include<glm/glm.hpp>
|
||||||
|
|
||||||
|
#include "shaderClass.h"
|
||||||
|
|
||||||
|
class DirLight {
|
||||||
|
|
||||||
|
public:
|
||||||
|
DirLight() {
|
||||||
|
|
||||||
|
}
|
||||||
|
DirLight(glm::vec3 ambient, glm::vec3 diffuse, glm::vec3 specular, glm::vec3 direction)
|
||||||
|
{
|
||||||
|
Initialize(ambient,diffuse,specular,direction);
|
||||||
|
}
|
||||||
|
|
||||||
|
void Initialize(glm::vec3 newambient, glm::vec3 newdiffuse, glm::vec3 newspecular, glm::vec3 newdirection) {
|
||||||
|
ambient = glm::normalize(newambient);
|
||||||
|
diffuse = glm::normalize(newdiffuse);
|
||||||
|
specular = glm::normalize(newspecular);
|
||||||
|
direction = glm::normalize(newdirection);
|
||||||
|
}
|
||||||
|
|
||||||
|
void Bind(Shader& shader) {
|
||||||
|
shader.Set3F("dirLight.ambient", ambient);
|
||||||
|
shader.Set3F("dirLight.diffuse", diffuse);
|
||||||
|
shader.Set3F("dirLight.specular", specular);
|
||||||
|
shader.Set3F("dirLight.direction", direction);
|
||||||
|
};
|
||||||
|
|
||||||
|
void Unbind(Shader shader) {
|
||||||
|
shader.Set3F("dirLight.ambient", glm::vec3(1.0f));
|
||||||
|
shader.Set3F("dirLight.diffuse", glm::vec3(0.0f));
|
||||||
|
shader.Set3F("dirLight.specular", glm::vec3(0.0f));
|
||||||
|
shader.Set3F("dirLight.direction", glm::vec3(1.0f));
|
||||||
|
//finish unbinding
|
||||||
|
};
|
||||||
|
|
||||||
|
glm::vec3 direction;
|
||||||
|
|
||||||
|
glm::vec3 ambient;
|
||||||
|
glm::vec3 diffuse;
|
||||||
|
glm::vec3 specular;
|
||||||
|
|
||||||
|
private:
|
||||||
|
|
||||||
|
};
|
||||||
|
|
||||||
|
#endif
|
||||||
@@ -0,0 +1,28 @@
|
|||||||
|
|
||||||
|
#include "EBO.h"
|
||||||
|
|
||||||
|
EBO::EBO()
|
||||||
|
{
|
||||||
|
}
|
||||||
|
|
||||||
|
void EBO::BufferData(GLuint* indices, GLsizeiptr size) {
|
||||||
|
glBindBuffer(GL_ELEMENT_ARRAY_BUFFER, ID);
|
||||||
|
glBufferData(GL_ELEMENT_ARRAY_BUFFER, size, indices, GL_STATIC_DRAW);
|
||||||
|
glBindBuffer(GL_ELEMENT_ARRAY_BUFFER, 0);
|
||||||
|
}
|
||||||
|
|
||||||
|
void EBO::Bind()
|
||||||
|
{
|
||||||
|
glBindBuffer(GL_ELEMENT_ARRAY_BUFFER, ID);
|
||||||
|
}
|
||||||
|
|
||||||
|
void EBO::Unbind()
|
||||||
|
{
|
||||||
|
glBindBuffer(GL_ELEMENT_ARRAY_BUFFER, 0);
|
||||||
|
}
|
||||||
|
|
||||||
|
void EBO::Delete()
|
||||||
|
{
|
||||||
|
glDeleteBuffers(1, &ID);
|
||||||
|
ID = 0;
|
||||||
|
}
|
||||||
@@ -0,0 +1,20 @@
|
|||||||
|
|
||||||
|
#ifndef EBO_CLASS_H
|
||||||
|
#define EBO_CLASS_H
|
||||||
|
|
||||||
|
#include <glad/glad.h>
|
||||||
|
|
||||||
|
class EBO {
|
||||||
|
public:
|
||||||
|
GLuint ID;
|
||||||
|
EBO();
|
||||||
|
void GenerateID() {
|
||||||
|
glGenBuffers(1, &ID);
|
||||||
|
};
|
||||||
|
void BufferData(GLuint* indices, GLsizeiptr size);
|
||||||
|
void Bind();
|
||||||
|
void Unbind();
|
||||||
|
void Delete();
|
||||||
|
};
|
||||||
|
|
||||||
|
#endif
|
||||||
+2138
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,73 @@
|
|||||||
|
|
||||||
|
|
||||||
|
#ifndef ENGINE_CLASS
|
||||||
|
#define ENGINE_CLASS
|
||||||
|
|
||||||
|
//OVERARCHING LIBRARIES
|
||||||
|
#include <iostream>
|
||||||
|
#include <vector>
|
||||||
|
|
||||||
|
//OPENGL LIBRARIES
|
||||||
|
#include<glad/glad.h>
|
||||||
|
#include<GLFW/glfw3.h>
|
||||||
|
|
||||||
|
//MANMADE LIBRARIES
|
||||||
|
#include "common.h"
|
||||||
|
|
||||||
|
inline int fps = 60;
|
||||||
|
|
||||||
|
// Game time runs this many times faster than real time. Every deltatime in the game loop is scaled by it.
|
||||||
|
inline float GetGameTimeScale() { return 144.0f / float(fps); }
|
||||||
|
|
||||||
|
void framebuffer_size_callback(GLFWwindow* windowe, int w, int h);
|
||||||
|
|
||||||
|
void key_callback(GLFWwindow* window, int key, int scancode, int action, int mods);
|
||||||
|
|
||||||
|
void char_callback(GLFWwindow* window, unsigned int codepoint);
|
||||||
|
|
||||||
|
void scroll_callback(GLFWwindow* window, double xoffset, double yoffset);
|
||||||
|
|
||||||
|
void mouse_callback(GLFWwindow* windowe, double xposIn, double yposIn);
|
||||||
|
|
||||||
|
void mouse_button_callback(GLFWwindow* window, int button, int action, int mods);
|
||||||
|
|
||||||
|
class Physics;
|
||||||
|
class RenderSystem;
|
||||||
|
class SoundSystem;
|
||||||
|
class Folder;
|
||||||
|
class ParticleEmitter;
|
||||||
|
class Camera;
|
||||||
|
class Animator;
|
||||||
|
class CharacterManager;
|
||||||
|
class Console;
|
||||||
|
class HitboxSystem;
|
||||||
|
|
||||||
|
class Engine {
|
||||||
|
public:
|
||||||
|
//SYSTEMS
|
||||||
|
Physics* physicsengine = nullptr;
|
||||||
|
RenderSystem* rendersystem = nullptr;
|
||||||
|
CharacterManager* charactermanager = nullptr;
|
||||||
|
Console* console = nullptr;
|
||||||
|
HitboxSystem* hitboxsystem = nullptr; // every attack's hitboxes, and what they hit
|
||||||
|
public:
|
||||||
|
//METADATA
|
||||||
|
int frame = 0;
|
||||||
|
public:
|
||||||
|
//DEBUG
|
||||||
|
bool renderwireframe = false;
|
||||||
|
bool showHitboxes = false;
|
||||||
|
public:
|
||||||
|
//RESOURCE MANAGEMENT
|
||||||
|
Folder* mainf;
|
||||||
|
public:
|
||||||
|
//CAMERA
|
||||||
|
Camera* camera;
|
||||||
|
public:
|
||||||
|
|
||||||
|
Engine();
|
||||||
|
|
||||||
|
void Initiate();
|
||||||
|
};
|
||||||
|
|
||||||
|
#endif
|
||||||
@@ -0,0 +1,26 @@
|
|||||||
|
|
||||||
|
#ifndef FBO_CLASS_H
|
||||||
|
#define FBO_CLASS_H
|
||||||
|
|
||||||
|
#include <glad/glad.h>
|
||||||
|
|
||||||
|
// cube
|
||||||
|
|
||||||
|
class FBO {
|
||||||
|
public:
|
||||||
|
GLuint ID;
|
||||||
|
FBO() {
|
||||||
|
glGenFramebuffers(1, &ID);
|
||||||
|
};
|
||||||
|
void Bind() {
|
||||||
|
glBindFramebuffer(GL_FRAMEBUFFER, ID);
|
||||||
|
};
|
||||||
|
void Unbind() {
|
||||||
|
glBindFramebuffer(GL_FRAMEBUFFER, 0);
|
||||||
|
};
|
||||||
|
void Delete() {
|
||||||
|
glDeleteFramebuffers(1, &ID);
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
#endif
|
||||||
@@ -0,0 +1,215 @@
|
|||||||
|
/*==============================================================================
|
||||||
|
3D Example
|
||||||
|
Copyright (c), Firelight Technologies Pty, Ltd 2004-2026.
|
||||||
|
|
||||||
|
This example shows how to basic 3D positioning of sounds.
|
||||||
|
|
||||||
|
For information on using FMOD example code in your own programs, visit
|
||||||
|
https://www.fmod.com/legal
|
||||||
|
==============================================================================*/
|
||||||
|
#include "fmod.hpp"
|
||||||
|
#include "common.h"
|
||||||
|
|
||||||
|
const int INTERFACE_UPDATETIME = 50; // 50ms update for interface
|
||||||
|
const float DISTANCEFACTOR = 1.0f; // Units per meter. I.e feet would = 3.28. centimeters would = 100.
|
||||||
|
|
||||||
|
int FMOD_Main()
|
||||||
|
{
|
||||||
|
FMOD::System *system;
|
||||||
|
FMOD::Sound *sound1, *sound2, *sound3;
|
||||||
|
FMOD::Channel *channel1 = 0, *channel2 = 0, *channel3 = 0;
|
||||||
|
FMOD_RESULT result;
|
||||||
|
bool listenerflag = true;
|
||||||
|
FMOD_VECTOR listenerpos = { 0.0f, 0.0f, -1.0f * DISTANCEFACTOR };
|
||||||
|
void *extradriverdata = 0;
|
||||||
|
|
||||||
|
Common_Init(&extradriverdata);
|
||||||
|
|
||||||
|
/*
|
||||||
|
Create a System object and initialize.
|
||||||
|
*/
|
||||||
|
result = FMOD::System_Create(&system);
|
||||||
|
ERRCHECK(result);
|
||||||
|
|
||||||
|
result = system->init(100, FMOD_INIT_NORMAL, extradriverdata);
|
||||||
|
ERRCHECK(result);
|
||||||
|
|
||||||
|
/*
|
||||||
|
Set the distance units. (meters/feet etc).
|
||||||
|
*/
|
||||||
|
result = system->set3DSettings(1.0, DISTANCEFACTOR, 1.0f);
|
||||||
|
ERRCHECK(result);
|
||||||
|
|
||||||
|
/*
|
||||||
|
Load some sounds
|
||||||
|
*/
|
||||||
|
result = system->createSound(Common_MediaPath("drumloop.wav"), FMOD_3D, 0, &sound1);
|
||||||
|
ERRCHECK(result);
|
||||||
|
result = sound1->set3DMinMaxDistance(0.5f * DISTANCEFACTOR, 5000.0f * DISTANCEFACTOR);
|
||||||
|
ERRCHECK(result);
|
||||||
|
result = sound1->setMode(FMOD_LOOP_NORMAL);
|
||||||
|
ERRCHECK(result);
|
||||||
|
|
||||||
|
result = system->createSound(Common_MediaPath("jaguar.wav"), FMOD_3D, 0, &sound2);
|
||||||
|
ERRCHECK(result);
|
||||||
|
result = sound2->set3DMinMaxDistance(0.5f * DISTANCEFACTOR, 5000.0f * DISTANCEFACTOR);
|
||||||
|
ERRCHECK(result);
|
||||||
|
result = sound2->setMode(FMOD_LOOP_NORMAL);
|
||||||
|
ERRCHECK(result);
|
||||||
|
|
||||||
|
result = system->createSound(Common_MediaPath("swish.wav"), FMOD_2D, 0, &sound3);
|
||||||
|
ERRCHECK(result);
|
||||||
|
|
||||||
|
/*
|
||||||
|
Play sounds at certain positions
|
||||||
|
*/
|
||||||
|
{
|
||||||
|
FMOD_VECTOR pos = { -10.0f * DISTANCEFACTOR, 0.0f, 0.0f };
|
||||||
|
FMOD_VECTOR vel = { 0.0f, 0.0f, 0.0f };
|
||||||
|
|
||||||
|
result = system->playSound(sound1, 0, true, &channel1);
|
||||||
|
ERRCHECK(result);
|
||||||
|
result = channel1->set3DAttributes(&pos, &vel);
|
||||||
|
ERRCHECK(result);
|
||||||
|
result = channel1->setPaused(false);
|
||||||
|
ERRCHECK(result);
|
||||||
|
}
|
||||||
|
|
||||||
|
{
|
||||||
|
FMOD_VECTOR pos = { 15.0f * DISTANCEFACTOR, 0.0f, 0.0f };
|
||||||
|
FMOD_VECTOR vel = { 0.0f, 0.0f, 0.0f };
|
||||||
|
|
||||||
|
result = system->playSound(sound2, 0, true, &channel2);
|
||||||
|
ERRCHECK(result);
|
||||||
|
result = channel2->set3DAttributes(&pos, &vel);
|
||||||
|
ERRCHECK(result);
|
||||||
|
result = channel2->setPaused(false);
|
||||||
|
ERRCHECK(result);
|
||||||
|
}
|
||||||
|
|
||||||
|
/*
|
||||||
|
Main loop
|
||||||
|
*/
|
||||||
|
do
|
||||||
|
{
|
||||||
|
Common_Update();
|
||||||
|
|
||||||
|
if (Common_BtnPress(BTN_ACTION1))
|
||||||
|
{
|
||||||
|
bool paused;
|
||||||
|
channel1->getPaused(&paused);
|
||||||
|
channel1->setPaused(!paused);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (Common_BtnPress(BTN_ACTION2))
|
||||||
|
{
|
||||||
|
bool paused;
|
||||||
|
channel2->getPaused(&paused);
|
||||||
|
channel2->setPaused(!paused);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (Common_BtnPress(BTN_ACTION3))
|
||||||
|
{
|
||||||
|
result = system->playSound(sound3, 0, false, &channel3);
|
||||||
|
ERRCHECK(result);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (Common_BtnPress(BTN_MORE))
|
||||||
|
{
|
||||||
|
listenerflag = !listenerflag;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!listenerflag)
|
||||||
|
{
|
||||||
|
if (Common_BtnDown(BTN_LEFT))
|
||||||
|
{
|
||||||
|
listenerpos.x -= 1.0f * DISTANCEFACTOR;
|
||||||
|
if (listenerpos.x < -24 * DISTANCEFACTOR)
|
||||||
|
{
|
||||||
|
listenerpos.x = -24 * DISTANCEFACTOR;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (Common_BtnDown(BTN_RIGHT))
|
||||||
|
{
|
||||||
|
listenerpos.x += 1.0f * DISTANCEFACTOR;
|
||||||
|
if (listenerpos.x > 23 * DISTANCEFACTOR)
|
||||||
|
{
|
||||||
|
listenerpos.x = 23 * DISTANCEFACTOR;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// ==========================================================================================
|
||||||
|
// UPDATE THE LISTENER
|
||||||
|
// ==========================================================================================
|
||||||
|
{
|
||||||
|
static float t = 0;
|
||||||
|
static FMOD_VECTOR lastpos = { 0.0f, 0.0f, 0.0f };
|
||||||
|
FMOD_VECTOR forward = { 0.0f, 0.0f, 1.0f };
|
||||||
|
FMOD_VECTOR up = { 0.0f, 1.0f, 0.0f };
|
||||||
|
FMOD_VECTOR vel;
|
||||||
|
|
||||||
|
if (listenerflag)
|
||||||
|
{
|
||||||
|
listenerpos.x = (float)sin(t * 0.05f) * 24.0f * DISTANCEFACTOR; // left right pingpong
|
||||||
|
}
|
||||||
|
|
||||||
|
// ********* NOTE ******* READ NEXT COMMENT!!!!!
|
||||||
|
// vel = how far we moved last FRAME (m/f), then time compensate it to SECONDS (m/s).
|
||||||
|
vel.x = (listenerpos.x - lastpos.x) * (1000 / INTERFACE_UPDATETIME);
|
||||||
|
vel.y = (listenerpos.y - lastpos.y) * (1000 / INTERFACE_UPDATETIME);
|
||||||
|
vel.z = (listenerpos.z - lastpos.z) * (1000 / INTERFACE_UPDATETIME);
|
||||||
|
|
||||||
|
// store pos for next time
|
||||||
|
lastpos = listenerpos;
|
||||||
|
|
||||||
|
result = system->set3DListenerAttributes(0, &listenerpos, &vel, &forward, &up);
|
||||||
|
ERRCHECK(result);
|
||||||
|
|
||||||
|
t += (30 * (1.0f / (float)INTERFACE_UPDATETIME)); // t is just a time value .. it increments in 30m/s steps in this example
|
||||||
|
}
|
||||||
|
|
||||||
|
result = system->update();
|
||||||
|
ERRCHECK(result);
|
||||||
|
|
||||||
|
// Create small visual display.
|
||||||
|
char s[80] = "|.............<1>......................<2>.......|";
|
||||||
|
s[(int)(listenerpos.x / DISTANCEFACTOR) + 25] = 'L';
|
||||||
|
|
||||||
|
Common_Draw("==================================================");
|
||||||
|
Common_Draw("3D Example.");
|
||||||
|
Common_Draw("Copyright (c) Firelight Technologies 2004-2026.");
|
||||||
|
Common_Draw("==================================================");
|
||||||
|
Common_Draw("");
|
||||||
|
Common_Draw("Press %s to toggle sound 1 (16bit Mono 3D)", Common_BtnStr(BTN_ACTION1));
|
||||||
|
Common_Draw("Press %s to toggle sound 2 (8bit Mono 3D)", Common_BtnStr(BTN_ACTION2));
|
||||||
|
Common_Draw("Press %s to play a sound (16bit Stereo 2D)", Common_BtnStr(BTN_ACTION3));
|
||||||
|
Common_Draw("Press %s or %s to move listener in still mode", Common_BtnStr(BTN_LEFT), Common_BtnStr(BTN_RIGHT));
|
||||||
|
Common_Draw("Press %s to toggle listener auto movement", Common_BtnStr(BTN_MORE));
|
||||||
|
Common_Draw("Press %s to quit", Common_BtnStr(BTN_QUIT));
|
||||||
|
Common_Draw("");
|
||||||
|
Common_Draw(s);
|
||||||
|
|
||||||
|
Common_Sleep(INTERFACE_UPDATETIME - 1);
|
||||||
|
} while (!Common_BtnPress(BTN_QUIT));
|
||||||
|
|
||||||
|
/*
|
||||||
|
Shut down
|
||||||
|
*/
|
||||||
|
result = sound1->release();
|
||||||
|
ERRCHECK(result);
|
||||||
|
result = sound2->release();
|
||||||
|
ERRCHECK(result);
|
||||||
|
result = sound3->release();
|
||||||
|
ERRCHECK(result);
|
||||||
|
|
||||||
|
result = system->close();
|
||||||
|
ERRCHECK(result);
|
||||||
|
result = system->release();
|
||||||
|
ERRCHECK(result);
|
||||||
|
|
||||||
|
Common_Close();
|
||||||
|
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
@@ -0,0 +1,348 @@
|
|||||||
|
/*===============================================================================================
|
||||||
|
Async IO Example
|
||||||
|
Copyright (c), Firelight Technologies Pty, Ltd 2004-2026.
|
||||||
|
|
||||||
|
This example shows how to play a stream and use a custom file handler that defers reads for the
|
||||||
|
streaming part. FMOD will allow the user to return straight away from a file read request and
|
||||||
|
supply the data at a later time.
|
||||||
|
===============================================================================================*/
|
||||||
|
#include "fmod.hpp"
|
||||||
|
#include "common.h"
|
||||||
|
#include <list>
|
||||||
|
|
||||||
|
struct AsyncData
|
||||||
|
{
|
||||||
|
FMOD_ASYNCREADINFO *info;
|
||||||
|
};
|
||||||
|
|
||||||
|
struct ScopedMutex
|
||||||
|
{
|
||||||
|
Common_Mutex *mMutex;
|
||||||
|
ScopedMutex(Common_Mutex *mutex) : mMutex(mutex) { Common_Mutex_Enter(mMutex); }
|
||||||
|
~ScopedMutex() { Common_Mutex_Leave(mMutex); }
|
||||||
|
};
|
||||||
|
|
||||||
|
Common_Mutex gListCrit;
|
||||||
|
std::list<AsyncData*> gList;
|
||||||
|
bool gThreadQuit = false;
|
||||||
|
bool gThreadFinished = false;
|
||||||
|
bool gSleepBreak = false;
|
||||||
|
|
||||||
|
|
||||||
|
/*
|
||||||
|
A little text buffer to allow a scrolling window
|
||||||
|
*/
|
||||||
|
const int DRAW_ROWS = NUM_ROWS - 8;
|
||||||
|
const int DRAW_COLS = NUM_COLUMNS;
|
||||||
|
|
||||||
|
char gLineData[DRAW_ROWS][DRAW_COLS];
|
||||||
|
Common_Mutex gLineCrit;
|
||||||
|
|
||||||
|
void AddLine(const char *formatString...)
|
||||||
|
{
|
||||||
|
ScopedMutex mutex(&gLineCrit);
|
||||||
|
|
||||||
|
char s[DRAW_COLS];
|
||||||
|
va_list args;
|
||||||
|
va_start(args, formatString);
|
||||||
|
Common_vsnprintf(s, DRAW_COLS, formatString, args);
|
||||||
|
va_end(args);
|
||||||
|
|
||||||
|
for (int i = 1; i < DRAW_ROWS; i++)
|
||||||
|
{
|
||||||
|
memcpy(gLineData[i-1], gLineData[i], DRAW_COLS);
|
||||||
|
}
|
||||||
|
strncpy(gLineData[DRAW_ROWS-1], s, DRAW_COLS);
|
||||||
|
}
|
||||||
|
|
||||||
|
void DrawLines()
|
||||||
|
{
|
||||||
|
ScopedMutex mutex(&gLineCrit);
|
||||||
|
|
||||||
|
for (int i = 0; i < DRAW_ROWS; i++)
|
||||||
|
{
|
||||||
|
Common_Draw(gLineData[i]);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
/*
|
||||||
|
File callbacks
|
||||||
|
*/
|
||||||
|
FMOD_RESULT F_CALL myopen(const char *name, unsigned int *filesize, void **handle, void * /*userdata*/)
|
||||||
|
{
|
||||||
|
assert(name);
|
||||||
|
assert(filesize);
|
||||||
|
assert(handle);
|
||||||
|
|
||||||
|
Common_File_Open(name, 0, filesize, handle); // mode 0 = 'read'.
|
||||||
|
|
||||||
|
if (!handle)
|
||||||
|
{
|
||||||
|
return FMOD_ERR_FILE_NOTFOUND;
|
||||||
|
}
|
||||||
|
|
||||||
|
return FMOD_OK;
|
||||||
|
}
|
||||||
|
|
||||||
|
FMOD_RESULT F_CALL myclose(void *handle, void * /*userdata*/)
|
||||||
|
{
|
||||||
|
assert(handle);
|
||||||
|
|
||||||
|
Common_File_Close(handle);
|
||||||
|
|
||||||
|
return FMOD_OK;
|
||||||
|
}
|
||||||
|
|
||||||
|
FMOD_RESULT F_CALL myread(void *handle, void *buffer, unsigned int sizebytes, unsigned int *bytesread, void * /*userdata*/)
|
||||||
|
{
|
||||||
|
assert(handle);
|
||||||
|
assert(buffer);
|
||||||
|
assert(bytesread);
|
||||||
|
|
||||||
|
Common_File_Read(handle, buffer, sizebytes, bytesread);
|
||||||
|
|
||||||
|
if (*bytesread < sizebytes)
|
||||||
|
{
|
||||||
|
return FMOD_ERR_FILE_EOF;
|
||||||
|
}
|
||||||
|
|
||||||
|
return FMOD_OK;
|
||||||
|
}
|
||||||
|
|
||||||
|
FMOD_RESULT F_CALL myseek(void *handle, unsigned int pos, void * /*userdata*/)
|
||||||
|
{
|
||||||
|
assert(handle);
|
||||||
|
|
||||||
|
Common_File_Seek(handle, pos);
|
||||||
|
|
||||||
|
return FMOD_OK;
|
||||||
|
}
|
||||||
|
|
||||||
|
FMOD_RESULT F_CALL myasyncread(FMOD_ASYNCREADINFO *info, void * /*userdata*/)
|
||||||
|
{
|
||||||
|
assert(info);
|
||||||
|
|
||||||
|
ScopedMutex mutex(&gListCrit);
|
||||||
|
|
||||||
|
AsyncData *data = (AsyncData *)malloc(sizeof(AsyncData));
|
||||||
|
if (!data)
|
||||||
|
{
|
||||||
|
/* Signal FMOD to wake up, this operation has has failed */
|
||||||
|
info->done(info, FMOD_ERR_MEMORY);
|
||||||
|
return FMOD_ERR_MEMORY;
|
||||||
|
}
|
||||||
|
|
||||||
|
AddLine("REQUEST %5d bytes, offset %5d PRIORITY = %d.", info->sizebytes, info->offset, info->priority);
|
||||||
|
data->info = info;
|
||||||
|
gList.push_back(data);
|
||||||
|
|
||||||
|
/* Example only: Use your native filesystem scheduler / priority here */
|
||||||
|
if (info->priority > 50)
|
||||||
|
{
|
||||||
|
gSleepBreak = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
return FMOD_OK;
|
||||||
|
}
|
||||||
|
|
||||||
|
FMOD_RESULT F_CALL myasynccancel(FMOD_ASYNCREADINFO *info, void * /*userdata*/)
|
||||||
|
{
|
||||||
|
assert(info);
|
||||||
|
|
||||||
|
ScopedMutex mutex(&gListCrit);
|
||||||
|
|
||||||
|
/* Find the pending IO request and remove it */
|
||||||
|
for (std::list<AsyncData*>::iterator itr = gList.begin(); itr != gList.end(); itr++)
|
||||||
|
{
|
||||||
|
AsyncData *data = *itr;
|
||||||
|
if (data->info == info)
|
||||||
|
{
|
||||||
|
gList.remove(data);
|
||||||
|
free(data);
|
||||||
|
|
||||||
|
/* Signal FMOD to wake up, this operation has been cancelled */
|
||||||
|
info->done(info, FMOD_ERR_FILE_DISKEJECTED);
|
||||||
|
return FMOD_ERR_FILE_DISKEJECTED;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/* IO request not found, it must have completed already */
|
||||||
|
return FMOD_OK;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
/*
|
||||||
|
Async file IO processing thread
|
||||||
|
*/
|
||||||
|
void ProcessQueue(void * /*param*/)
|
||||||
|
{
|
||||||
|
while (!gThreadQuit)
|
||||||
|
{
|
||||||
|
/* Grab the next IO task off the list */
|
||||||
|
FMOD_ASYNCREADINFO *info = NULL;
|
||||||
|
Common_Mutex_Enter(&gListCrit);
|
||||||
|
if (!gList.empty())
|
||||||
|
{
|
||||||
|
info = gList.front()->info;
|
||||||
|
gList.pop_front();
|
||||||
|
}
|
||||||
|
Common_Mutex_Leave(&gListCrit);
|
||||||
|
|
||||||
|
if (info)
|
||||||
|
{
|
||||||
|
/* Example only: Let's deprive the read of the whole block, only give 16kb at a time to make it re-ask for more later */
|
||||||
|
unsigned int toread = info->sizebytes;
|
||||||
|
if (toread > 16384)
|
||||||
|
{
|
||||||
|
toread = 16384;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Example only: Demonstration of priority influencing turnaround time */
|
||||||
|
for (int i = 0; i < 50; i++)
|
||||||
|
{
|
||||||
|
Common_Sleep(10);
|
||||||
|
if (gSleepBreak)
|
||||||
|
{
|
||||||
|
AddLine("URGENT REQUEST - reading now!");
|
||||||
|
gSleepBreak = false;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Process the seek and read request with EOF handling */
|
||||||
|
Common_File_Seek(info->handle, info->offset);
|
||||||
|
|
||||||
|
Common_File_Read(info->handle, info->buffer, toread, &info->bytesread);
|
||||||
|
|
||||||
|
if (info->bytesread < toread)
|
||||||
|
{
|
||||||
|
AddLine("FED %5d bytes, offset %5d (* EOF)", info->bytesread, info->offset);
|
||||||
|
info->done(info, FMOD_ERR_FILE_EOF);
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
AddLine("FED %5d bytes, offset %5d", info->bytesread, info->offset);
|
||||||
|
info->done(info, FMOD_OK);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
Common_Sleep(10); /* Example only: Use your native filesystem synchronisation to wait for more requests */
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
gThreadFinished = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
int FMOD_Main()
|
||||||
|
{
|
||||||
|
void *extradriverdata = NULL;
|
||||||
|
void *threadhandle = NULL;
|
||||||
|
|
||||||
|
Common_Init(&extradriverdata);
|
||||||
|
|
||||||
|
Common_Mutex_Create(&gLineCrit);
|
||||||
|
Common_Mutex_Create(&gListCrit);
|
||||||
|
|
||||||
|
Common_Thread_Create(ProcessQueue, NULL, &threadhandle);
|
||||||
|
|
||||||
|
/*
|
||||||
|
Create a System object and initialize.
|
||||||
|
*/
|
||||||
|
FMOD::System *system = NULL;
|
||||||
|
FMOD_RESULT result = FMOD::System_Create(&system);
|
||||||
|
ERRCHECK(result);
|
||||||
|
|
||||||
|
result = system->init(1, FMOD_INIT_NORMAL, extradriverdata);
|
||||||
|
ERRCHECK(result);
|
||||||
|
|
||||||
|
result = system->setStreamBufferSize(32768, FMOD_TIMEUNIT_RAWBYTES);
|
||||||
|
ERRCHECK(result);
|
||||||
|
|
||||||
|
result = system->setFileSystem(myopen, myclose, myread, myseek, myasyncread, myasynccancel, 2048);
|
||||||
|
ERRCHECK(result);
|
||||||
|
|
||||||
|
FMOD::Sound *sound = NULL;
|
||||||
|
result = system->createStream(Common_MediaPath("wave.mp3"), FMOD_LOOP_NORMAL | FMOD_2D | FMOD_IGNORETAGS, NULL, &sound);
|
||||||
|
ERRCHECK(result);
|
||||||
|
|
||||||
|
FMOD::Channel *channel = NULL;
|
||||||
|
result = system->playSound(sound, 0, false, &channel);
|
||||||
|
ERRCHECK(result);
|
||||||
|
|
||||||
|
/*
|
||||||
|
Main loop.
|
||||||
|
*/
|
||||||
|
do
|
||||||
|
{
|
||||||
|
Common_Update();
|
||||||
|
|
||||||
|
if (sound)
|
||||||
|
{
|
||||||
|
bool starving = false;
|
||||||
|
FMOD_OPENSTATE openstate = FMOD_OPENSTATE_READY;
|
||||||
|
result = sound->getOpenState(&openstate, NULL, &starving, NULL);
|
||||||
|
ERRCHECK(result);
|
||||||
|
|
||||||
|
if (starving)
|
||||||
|
{
|
||||||
|
AddLine("Starving");
|
||||||
|
}
|
||||||
|
|
||||||
|
result = channel->setMute(starving);
|
||||||
|
ERRCHECK(result);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (Common_BtnPress(BTN_ACTION1))
|
||||||
|
{
|
||||||
|
result = sound->release();
|
||||||
|
if (result == FMOD_OK)
|
||||||
|
{
|
||||||
|
sound = NULL;
|
||||||
|
AddLine("Released sound");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
result = system->update();
|
||||||
|
ERRCHECK(result);
|
||||||
|
|
||||||
|
Common_Draw("==================================================");
|
||||||
|
Common_Draw("Async IO Example.");
|
||||||
|
Common_Draw("Copyright (c) Firelight Technologies 2004-2026.");
|
||||||
|
Common_Draw("==================================================");
|
||||||
|
Common_Draw("");
|
||||||
|
Common_Draw("Press %s to release playing stream", Common_BtnStr(BTN_ACTION1));
|
||||||
|
Common_Draw("Press %s to quit", Common_BtnStr(BTN_QUIT));
|
||||||
|
Common_Draw("");
|
||||||
|
DrawLines();
|
||||||
|
Common_Sleep(50);
|
||||||
|
} while (!Common_BtnPress(BTN_QUIT));
|
||||||
|
|
||||||
|
/*
|
||||||
|
Shut down
|
||||||
|
*/
|
||||||
|
if (sound)
|
||||||
|
{
|
||||||
|
result = sound->release();
|
||||||
|
ERRCHECK(result);
|
||||||
|
}
|
||||||
|
result = system->close();
|
||||||
|
ERRCHECK(result);
|
||||||
|
result = system->release();
|
||||||
|
ERRCHECK(result);
|
||||||
|
|
||||||
|
gThreadQuit = true;
|
||||||
|
while (!gThreadFinished)
|
||||||
|
{
|
||||||
|
Common_Sleep(10);
|
||||||
|
}
|
||||||
|
|
||||||
|
Common_Mutex_Destroy(&gListCrit);
|
||||||
|
Common_Mutex_Destroy(&gLineCrit);
|
||||||
|
Common_Thread_Destroy(threadhandle);
|
||||||
|
Common_Close();
|
||||||
|
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
@@ -0,0 +1,189 @@
|
|||||||
|
/*==============================================================================
|
||||||
|
Channel Groups Example
|
||||||
|
Copyright (c), Firelight Technologies Pty, Ltd 2004-2026.
|
||||||
|
|
||||||
|
This example shows how to put channels into channel groups, so that you can
|
||||||
|
affect a group of channels at a time instead of just one.
|
||||||
|
|
||||||
|
For information on using FMOD example code in your own programs, visit
|
||||||
|
https://www.fmod.com/legal
|
||||||
|
==============================================================================*/
|
||||||
|
#include "fmod.hpp"
|
||||||
|
#include "common.h"
|
||||||
|
|
||||||
|
int FMOD_Main()
|
||||||
|
{
|
||||||
|
FMOD::System *system;
|
||||||
|
FMOD::Sound *sound[6];
|
||||||
|
FMOD::Channel *channel[6];
|
||||||
|
FMOD::ChannelGroup *groupA, *groupB, *masterGroup;
|
||||||
|
FMOD_RESULT result;
|
||||||
|
int count;
|
||||||
|
void *extradriverdata = 0;
|
||||||
|
|
||||||
|
Common_Init(&extradriverdata);
|
||||||
|
|
||||||
|
/*
|
||||||
|
Create a System object and initialize.
|
||||||
|
*/
|
||||||
|
result = FMOD::System_Create(&system);
|
||||||
|
ERRCHECK(result);
|
||||||
|
|
||||||
|
result = system->init(32, FMOD_INIT_NORMAL, extradriverdata);
|
||||||
|
ERRCHECK(result);
|
||||||
|
|
||||||
|
result = system->createSound(Common_MediaPath("drumloop.wav"), FMOD_LOOP_NORMAL, 0, &sound[0]);
|
||||||
|
ERRCHECK(result);
|
||||||
|
result = system->createSound(Common_MediaPath("jaguar.wav"), FMOD_LOOP_NORMAL, 0, &sound[1]);
|
||||||
|
ERRCHECK(result);
|
||||||
|
result = system->createSound(Common_MediaPath("swish.wav"), FMOD_LOOP_NORMAL, 0, &sound[2]);
|
||||||
|
ERRCHECK(result);
|
||||||
|
result = system->createSound(Common_MediaPath("c.ogg"), FMOD_LOOP_NORMAL, 0, &sound[3]);
|
||||||
|
ERRCHECK(result);
|
||||||
|
result = system->createSound(Common_MediaPath("d.ogg"), FMOD_LOOP_NORMAL, 0, &sound[4]);
|
||||||
|
ERRCHECK(result);
|
||||||
|
result = system->createSound(Common_MediaPath("e.ogg"), FMOD_LOOP_NORMAL, 0, &sound[5]);
|
||||||
|
ERRCHECK(result);
|
||||||
|
|
||||||
|
result = system->createChannelGroup("Group A", &groupA);
|
||||||
|
ERRCHECK(result);
|
||||||
|
result = system->createChannelGroup("Group B", &groupB);
|
||||||
|
ERRCHECK(result);
|
||||||
|
|
||||||
|
result = system->getMasterChannelGroup(&masterGroup);
|
||||||
|
ERRCHECK(result);
|
||||||
|
|
||||||
|
/*
|
||||||
|
Instead of being independent, set the group A and B to be children of the master group.
|
||||||
|
*/
|
||||||
|
result = masterGroup->addGroup(groupA);
|
||||||
|
ERRCHECK(result);
|
||||||
|
|
||||||
|
result = masterGroup->addGroup(groupB);
|
||||||
|
ERRCHECK(result);
|
||||||
|
|
||||||
|
/*
|
||||||
|
Start all the sounds.
|
||||||
|
*/
|
||||||
|
for (count = 0; count < 6; count++)
|
||||||
|
{
|
||||||
|
result = system->playSound(sound[count], 0, true, &channel[count]);
|
||||||
|
ERRCHECK(result);
|
||||||
|
|
||||||
|
result = channel[count]->setChannelGroup((count < 3) ? groupA : groupB);
|
||||||
|
ERRCHECK(result);
|
||||||
|
|
||||||
|
result = channel[count]->setPaused(false);
|
||||||
|
ERRCHECK(result);
|
||||||
|
}
|
||||||
|
|
||||||
|
/*
|
||||||
|
Change the volume of each group, just because we can! (reduce overall noise).
|
||||||
|
*/
|
||||||
|
result = groupA->setVolume(0.5f);
|
||||||
|
ERRCHECK(result);
|
||||||
|
result = groupB->setVolume(0.5f);
|
||||||
|
ERRCHECK(result);
|
||||||
|
|
||||||
|
/*
|
||||||
|
Main loop.
|
||||||
|
*/
|
||||||
|
do
|
||||||
|
{
|
||||||
|
Common_Update();
|
||||||
|
|
||||||
|
if (Common_BtnPress(BTN_ACTION1))
|
||||||
|
{
|
||||||
|
bool mute = true;
|
||||||
|
groupA->getMute(&mute);
|
||||||
|
groupA->setMute(!mute);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (Common_BtnPress(BTN_ACTION2))
|
||||||
|
{
|
||||||
|
bool mute = true;
|
||||||
|
groupB->getMute(&mute);
|
||||||
|
groupB->setMute(!mute);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (Common_BtnPress(BTN_ACTION3))
|
||||||
|
{
|
||||||
|
bool mute = true;
|
||||||
|
masterGroup->getMute(&mute);
|
||||||
|
masterGroup->setMute(!mute);
|
||||||
|
}
|
||||||
|
|
||||||
|
result = system->update();
|
||||||
|
ERRCHECK(result);
|
||||||
|
|
||||||
|
{
|
||||||
|
int channelsplaying = 0;
|
||||||
|
|
||||||
|
result = system->getChannelsPlaying(&channelsplaying, NULL);
|
||||||
|
ERRCHECK(result);
|
||||||
|
|
||||||
|
Common_Draw("==================================================");
|
||||||
|
Common_Draw("Channel Groups Example.");
|
||||||
|
Common_Draw("Copyright (c) Firelight Technologies 2004-2026.");
|
||||||
|
Common_Draw("==================================================");
|
||||||
|
Common_Draw("");
|
||||||
|
Common_Draw("Group A : drumloop.wav, jaguar.wav, swish.wav");
|
||||||
|
Common_Draw("Group B : c.ogg, d.ogg, e.ogg");
|
||||||
|
Common_Draw("");
|
||||||
|
Common_Draw("Press %s to mute/unmute group A", Common_BtnStr(BTN_ACTION1));
|
||||||
|
Common_Draw("Press %s to mute/unmute group B", Common_BtnStr(BTN_ACTION2));
|
||||||
|
Common_Draw("Press %s to mute/unmute master group", Common_BtnStr(BTN_ACTION3));
|
||||||
|
Common_Draw("Press %s to quit", Common_BtnStr(BTN_QUIT));
|
||||||
|
Common_Draw("");
|
||||||
|
Common_Draw("Channels playing %d", channelsplaying);
|
||||||
|
}
|
||||||
|
|
||||||
|
Common_Sleep(50);
|
||||||
|
} while (!Common_BtnPress(BTN_QUIT));
|
||||||
|
|
||||||
|
|
||||||
|
/*
|
||||||
|
A little fade out over 2 seconds.
|
||||||
|
*/
|
||||||
|
{
|
||||||
|
float pitch = 1.0f;
|
||||||
|
float vol = 1.0f;
|
||||||
|
|
||||||
|
for (count = 0; count < 200; count++)
|
||||||
|
{
|
||||||
|
masterGroup->setPitch(pitch);
|
||||||
|
masterGroup->setVolume(vol);
|
||||||
|
|
||||||
|
vol -= (1.0f / 200.0f);
|
||||||
|
pitch -= (0.5f / 200.0f);
|
||||||
|
|
||||||
|
result = system->update();
|
||||||
|
ERRCHECK(result);
|
||||||
|
|
||||||
|
Common_Sleep(10);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/*
|
||||||
|
Shut down.
|
||||||
|
*/
|
||||||
|
for (count = 0; count < 6; count++)
|
||||||
|
{
|
||||||
|
result = sound[count]->release();
|
||||||
|
ERRCHECK(result);
|
||||||
|
}
|
||||||
|
|
||||||
|
result = groupA->release();
|
||||||
|
ERRCHECK(result);
|
||||||
|
result = groupB->release();
|
||||||
|
ERRCHECK(result);
|
||||||
|
|
||||||
|
result = system->close();
|
||||||
|
ERRCHECK(result);
|
||||||
|
result = system->release();
|
||||||
|
ERRCHECK(result);
|
||||||
|
|
||||||
|
Common_Close();
|
||||||
|
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
@@ -0,0 +1,234 @@
|
|||||||
|
/*==============================================================================
|
||||||
|
FMOD Example Framework
|
||||||
|
Copyright (c), Firelight Technologies Pty, Ltd 2012-2026.
|
||||||
|
==============================================================================*/
|
||||||
|
#include "common.h"
|
||||||
|
#include "fmod_errors.h"
|
||||||
|
|
||||||
|
/* Cross platform OS Functions internal to the FMOD library, exposed for the example framework. */
|
||||||
|
#ifdef __cplusplus
|
||||||
|
extern "C" {
|
||||||
|
#endif
|
||||||
|
|
||||||
|
typedef struct FMOD_OS_FILE FMOD_OS_FILE;
|
||||||
|
typedef struct FMOD_OS_CRITICALSECTION FMOD_OS_CRITICALSECTION;
|
||||||
|
|
||||||
|
FMOD_RESULT F_API FMOD_OS_Time_GetUs(unsigned int *us);
|
||||||
|
FMOD_RESULT F_API FMOD_OS_Debug_Output(const char *format, ...);
|
||||||
|
FMOD_RESULT F_API FMOD_OS_File_Open(const char *name, int mode, unsigned int *filesize, FMOD_OS_FILE **handle);
|
||||||
|
FMOD_RESULT F_API FMOD_OS_File_Close(FMOD_OS_FILE *handle);
|
||||||
|
FMOD_RESULT F_API FMOD_OS_File_Read(FMOD_OS_FILE *handle, void *buf, unsigned int count, unsigned int *read);
|
||||||
|
FMOD_RESULT F_API FMOD_OS_File_Write(FMOD_OS_FILE *handle, const void *buffer, unsigned int bytesToWrite, bool flush);
|
||||||
|
FMOD_RESULT F_API FMOD_OS_File_Seek(FMOD_OS_FILE *handle, unsigned int offset);
|
||||||
|
FMOD_RESULT F_API FMOD_OS_Time_Sleep(unsigned int ms);
|
||||||
|
FMOD_RESULT F_API FMOD_OS_CriticalSection_Create(FMOD_OS_CRITICALSECTION **crit, bool memorycrit);
|
||||||
|
FMOD_RESULT F_API FMOD_OS_CriticalSection_Free(FMOD_OS_CRITICALSECTION *crit, bool memorycrit);
|
||||||
|
FMOD_RESULT F_API FMOD_OS_CriticalSection_Enter(FMOD_OS_CRITICALSECTION *crit);
|
||||||
|
FMOD_RESULT F_API FMOD_OS_CriticalSection_Leave(FMOD_OS_CRITICALSECTION *crit);
|
||||||
|
FMOD_RESULT F_API FMOD_OS_CriticalSection_TryEnter(FMOD_OS_CRITICALSECTION *crit, bool *entered);
|
||||||
|
FMOD_RESULT F_API FMOD_OS_CriticalSection_IsLocked(FMOD_OS_CRITICALSECTION *crit, bool *locked);
|
||||||
|
FMOD_RESULT F_API FMOD_OS_Thread_Create(const char *name, void (*callback)(void *param), void *param, FMOD_THREAD_AFFINITY affinity, FMOD_THREAD_PRIORITY priority, FMOD_THREAD_STACK_SIZE stacksize, void **handle);
|
||||||
|
FMOD_RESULT F_API FMOD_OS_Thread_Destroy(void *handle);
|
||||||
|
|
||||||
|
#ifdef __cplusplus
|
||||||
|
}
|
||||||
|
#endif
|
||||||
|
|
||||||
|
void (*Common_Private_Error)(FMOD_RESULT, const char *, int);
|
||||||
|
|
||||||
|
void ERRCHECK_fn(FMOD_RESULT result, const char *file, int line)
|
||||||
|
{
|
||||||
|
if (result != FMOD_OK)
|
||||||
|
{
|
||||||
|
if (Common_Private_Error)
|
||||||
|
{
|
||||||
|
Common_Private_Error(result, file, line);
|
||||||
|
}
|
||||||
|
Common_Fatal("%s(%d): FMOD error %d - %s", file, line, result, FMOD_ErrorString(result));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void Common_Format(char *buffer, int bufferSize, const char *formatString...)
|
||||||
|
{
|
||||||
|
va_list args;
|
||||||
|
va_start(args, formatString);
|
||||||
|
Common_vsnprintf(buffer, bufferSize, formatString, args);
|
||||||
|
va_end(args);
|
||||||
|
buffer[bufferSize-1] = '\0';
|
||||||
|
}
|
||||||
|
|
||||||
|
void Common_Fatal(const char *format, ...)
|
||||||
|
{
|
||||||
|
char error[1024];
|
||||||
|
|
||||||
|
va_list args;
|
||||||
|
va_start(args, format);
|
||||||
|
Common_vsnprintf(error, 1024, format, args);
|
||||||
|
va_end(args);
|
||||||
|
error[1023] = '\0';
|
||||||
|
|
||||||
|
do
|
||||||
|
{
|
||||||
|
Common_Draw("A fatal error has occurred...");
|
||||||
|
Common_Draw("");
|
||||||
|
Common_Draw("%s", error);
|
||||||
|
Common_Draw("");
|
||||||
|
Common_Draw("Press %s to quit", Common_BtnStr(BTN_QUIT));
|
||||||
|
|
||||||
|
Common_Update();
|
||||||
|
Common_Sleep(50);
|
||||||
|
} while (!Common_BtnPress(BTN_QUIT));
|
||||||
|
|
||||||
|
Common_Exit(0);
|
||||||
|
}
|
||||||
|
|
||||||
|
void Common_Draw(const char *format, ...)
|
||||||
|
{
|
||||||
|
char string[1024];
|
||||||
|
char *stringPtr = string;
|
||||||
|
|
||||||
|
va_list args;
|
||||||
|
va_start(args, format);
|
||||||
|
Common_vsnprintf(string, 1024, format, args);
|
||||||
|
va_end(args);
|
||||||
|
string[1023] = '\0';
|
||||||
|
|
||||||
|
unsigned int length = (unsigned int)strlen(string);
|
||||||
|
|
||||||
|
do
|
||||||
|
{
|
||||||
|
bool consumeNewLine = false;
|
||||||
|
unsigned int copyLength = length;
|
||||||
|
|
||||||
|
// Search for new line characters
|
||||||
|
char *newLinePtr = strchr(stringPtr, '\n');
|
||||||
|
if (newLinePtr)
|
||||||
|
{
|
||||||
|
consumeNewLine = true;
|
||||||
|
copyLength = (unsigned int)(newLinePtr - stringPtr);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (copyLength > NUM_COLUMNS)
|
||||||
|
{
|
||||||
|
// Hard wrap by default
|
||||||
|
copyLength = NUM_COLUMNS;
|
||||||
|
|
||||||
|
// Loop for a soft wrap
|
||||||
|
for (int i = NUM_COLUMNS - 1; i >= 0; i--)
|
||||||
|
{
|
||||||
|
if (stringPtr[i] == ' ')
|
||||||
|
{
|
||||||
|
copyLength = i + 1;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Null terminate the sub string temporarily by swapping out a char
|
||||||
|
char tempChar = stringPtr[copyLength];
|
||||||
|
stringPtr[copyLength] = 0;
|
||||||
|
Common_DrawText(stringPtr);
|
||||||
|
stringPtr[copyLength] = tempChar;
|
||||||
|
|
||||||
|
copyLength += (consumeNewLine ? 1 : 0);
|
||||||
|
length -= copyLength;
|
||||||
|
stringPtr += copyLength;
|
||||||
|
} while (length > 0);
|
||||||
|
}
|
||||||
|
|
||||||
|
void Common_Time_GetUs(unsigned int *us)
|
||||||
|
{
|
||||||
|
FMOD_OS_Time_GetUs(us);
|
||||||
|
}
|
||||||
|
|
||||||
|
void Common_Log(const char *format, ...)
|
||||||
|
{
|
||||||
|
char string[1024];
|
||||||
|
|
||||||
|
va_list args;
|
||||||
|
va_start(args, format);
|
||||||
|
Common_vsnprintf(string, 1024, format, args);
|
||||||
|
va_end(args);
|
||||||
|
string[1023] = '\0';
|
||||||
|
|
||||||
|
FMOD_OS_Debug_Output(string);
|
||||||
|
}
|
||||||
|
|
||||||
|
void Common_LoadFileMemory(const char *name, void **buff, int *length)
|
||||||
|
{
|
||||||
|
FMOD_OS_FILE *file = NULL;
|
||||||
|
unsigned int len, bytesread;
|
||||||
|
|
||||||
|
FMOD_OS_File_Open(name, 0, &len, &file);
|
||||||
|
void *mem = malloc(len);
|
||||||
|
FMOD_OS_File_Read(file, mem, len, &bytesread);
|
||||||
|
FMOD_OS_File_Close(file);
|
||||||
|
|
||||||
|
*buff = mem;
|
||||||
|
*length = bytesread;
|
||||||
|
}
|
||||||
|
|
||||||
|
void Common_UnloadFileMemory(void *buff)
|
||||||
|
{
|
||||||
|
free(buff);
|
||||||
|
}
|
||||||
|
|
||||||
|
void Common_Sleep(unsigned int ms)
|
||||||
|
{
|
||||||
|
FMOD_OS_Time_Sleep(ms);
|
||||||
|
}
|
||||||
|
|
||||||
|
void Common_File_Open(const char *name, int mode, unsigned int *filesize, void **handle)
|
||||||
|
{
|
||||||
|
FMOD_OS_File_Open(name, mode, filesize, (FMOD_OS_FILE **)handle);
|
||||||
|
}
|
||||||
|
|
||||||
|
void Common_File_Close(void *handle)
|
||||||
|
{
|
||||||
|
FMOD_OS_File_Close((FMOD_OS_FILE *)handle);
|
||||||
|
}
|
||||||
|
|
||||||
|
void Common_File_Read(void *handle, void *buf, unsigned int length, unsigned int *read)
|
||||||
|
{
|
||||||
|
FMOD_OS_File_Read((FMOD_OS_FILE *)handle, buf, length, read);
|
||||||
|
}
|
||||||
|
|
||||||
|
void Common_File_Write(void *handle, void *buf, unsigned int length)
|
||||||
|
{
|
||||||
|
FMOD_OS_File_Write((FMOD_OS_FILE *)handle, buf, length, true);
|
||||||
|
}
|
||||||
|
|
||||||
|
void Common_File_Seek(void *handle, unsigned int offset)
|
||||||
|
{
|
||||||
|
FMOD_OS_File_Seek((FMOD_OS_FILE *)handle, offset);
|
||||||
|
}
|
||||||
|
|
||||||
|
void Common_Mutex_Create(Common_Mutex *mutex)
|
||||||
|
{
|
||||||
|
FMOD_OS_CriticalSection_Create((FMOD_OS_CRITICALSECTION **)&mutex->crit, false);
|
||||||
|
}
|
||||||
|
|
||||||
|
void Common_Mutex_Destroy(Common_Mutex *mutex)
|
||||||
|
{
|
||||||
|
FMOD_OS_CriticalSection_Free((FMOD_OS_CRITICALSECTION *)mutex->crit, false);
|
||||||
|
}
|
||||||
|
|
||||||
|
void Common_Mutex_Enter(Common_Mutex *mutex)
|
||||||
|
{
|
||||||
|
FMOD_OS_CriticalSection_Enter((FMOD_OS_CRITICALSECTION *)mutex->crit);
|
||||||
|
}
|
||||||
|
|
||||||
|
void Common_Mutex_Leave(Common_Mutex *mutex)
|
||||||
|
{
|
||||||
|
FMOD_OS_CriticalSection_Leave((FMOD_OS_CRITICALSECTION *)mutex->crit);
|
||||||
|
}
|
||||||
|
|
||||||
|
void Common_Thread_Create(void (*callback)(void *param), void *param, void **handle)
|
||||||
|
{
|
||||||
|
FMOD_OS_Thread_Create("FMOD Example Thread", callback, param, FMOD_THREAD_AFFINITY_GROUP_A, FMOD_THREAD_PRIORITY_MEDIUM, (16 * 1024), handle);
|
||||||
|
}
|
||||||
|
|
||||||
|
void Common_Thread_Destroy(void *handle)
|
||||||
|
{
|
||||||
|
FMOD_OS_Thread_Destroy(handle);
|
||||||
|
}
|
||||||
@@ -0,0 +1,93 @@
|
|||||||
|
/*==============================================================================
|
||||||
|
FMOD Example Framework
|
||||||
|
Copyright (c), Firelight Technologies Pty, Ltd 2012-2026.
|
||||||
|
==============================================================================*/
|
||||||
|
#ifndef FMOD_EXAMPLES_COMMON_H
|
||||||
|
#define FMOD_EXAMPLES_COMMON_H
|
||||||
|
|
||||||
|
#include "common_platform.h"
|
||||||
|
#include "fmod.h"
|
||||||
|
|
||||||
|
#include <stddef.h>
|
||||||
|
#include <string.h>
|
||||||
|
#include <stdlib.h>
|
||||||
|
#include <math.h>
|
||||||
|
#include <stdarg.h>
|
||||||
|
#include <stdio.h>
|
||||||
|
#include <assert.h>
|
||||||
|
|
||||||
|
#define NUM_COLUMNS 50
|
||||||
|
#define NUM_ROWS 25
|
||||||
|
|
||||||
|
#ifndef Common_Sin
|
||||||
|
#define Common_Sin sin
|
||||||
|
#endif
|
||||||
|
|
||||||
|
#ifndef Common_snprintf
|
||||||
|
#define Common_snprintf snprintf
|
||||||
|
#endif
|
||||||
|
|
||||||
|
#ifndef Common_vsnprintf
|
||||||
|
#define Common_vsnprintf vsnprintf
|
||||||
|
#endif
|
||||||
|
|
||||||
|
enum Common_Button
|
||||||
|
{
|
||||||
|
BTN_ACTION1,
|
||||||
|
BTN_ACTION2,
|
||||||
|
BTN_ACTION3,
|
||||||
|
BTN_ACTION4,
|
||||||
|
BTN_LEFT,
|
||||||
|
BTN_RIGHT,
|
||||||
|
BTN_UP,
|
||||||
|
BTN_DOWN,
|
||||||
|
BTN_MORE,
|
||||||
|
BTN_QUIT
|
||||||
|
};
|
||||||
|
|
||||||
|
typedef struct
|
||||||
|
{
|
||||||
|
void *crit;
|
||||||
|
} Common_Mutex;
|
||||||
|
|
||||||
|
/* Cross platform functions (common) */
|
||||||
|
void Common_Format(char *buffer, int bufferSize, const char *formatString...);
|
||||||
|
void Common_Fatal(const char *format, ...);
|
||||||
|
void Common_Draw(const char *format, ...);
|
||||||
|
void Common_Time_GetUs(unsigned int *us);
|
||||||
|
void Common_Log(const char *format, ...);
|
||||||
|
void Common_LoadFileMemory(const char *name, void **buff, int *length);
|
||||||
|
void Common_UnloadFileMemory(void *buff);
|
||||||
|
void Common_Sleep(unsigned int ms);
|
||||||
|
void Common_File_Open(const char *name, int mode, unsigned int *filesize, void **handle); // mode : 0 = read, 1 = write.
|
||||||
|
void Common_File_Close(void *handle);
|
||||||
|
void Common_File_Read(void *handle, void *buf, unsigned int length, unsigned int *read);
|
||||||
|
void Common_File_Write(void *handle, void *buf, unsigned int length);
|
||||||
|
void Common_File_Seek(void *handle, unsigned int offset);
|
||||||
|
void Common_Mutex_Create(Common_Mutex *mutex);
|
||||||
|
void Common_Mutex_Destroy(Common_Mutex *mutex);
|
||||||
|
void Common_Mutex_Enter(Common_Mutex *mutex);
|
||||||
|
void Common_Mutex_Leave(Common_Mutex *mutex);
|
||||||
|
void Common_Thread_Create(void (*callback)(void *param), void *param, void **handle);
|
||||||
|
void Common_Thread_Destroy(void *handle);
|
||||||
|
|
||||||
|
void ERRCHECK_fn(FMOD_RESULT result, const char *file, int line);
|
||||||
|
#define ERRCHECK(_result) ERRCHECK_fn(_result, __FILE__, __LINE__)
|
||||||
|
#define Common_Max(_a, _b) ((_a) > (_b) ? (_a) : (_b))
|
||||||
|
#define Common_Min(_a, _b) ((_a) < (_b) ? (_a) : (_b))
|
||||||
|
#define Common_Clamp(_min, _val, _max) ((_val) < (_min) ? (_min) : ((_val) > (_max) ? (_max) : (_val)))
|
||||||
|
|
||||||
|
/* Functions with platform specific implementation (common_platform) */
|
||||||
|
void Common_Init(void **extraDriverData);
|
||||||
|
void Common_Close();
|
||||||
|
void Common_Update();
|
||||||
|
void Common_Exit(int returnCode);
|
||||||
|
void Common_DrawText(const char *text);
|
||||||
|
bool Common_BtnPress(Common_Button btn);
|
||||||
|
bool Common_BtnDown(Common_Button btn);
|
||||||
|
const char *Common_BtnStr(Common_Button btn);
|
||||||
|
const char *Common_MediaPath(const char *fileName);
|
||||||
|
const char *Common_WritePath(const char *fileName);
|
||||||
|
|
||||||
|
|
||||||
|
#endif
|
||||||
@@ -0,0 +1,306 @@
|
|||||||
|
/*==============================================================================
|
||||||
|
FMOD Example Framework
|
||||||
|
Copyright (c), Firelight Technologies Pty, Ltd 2012-2026.
|
||||||
|
==============================================================================*/
|
||||||
|
#define WIN32_LEAN_AND_MEAN
|
||||||
|
|
||||||
|
#include "common.h"
|
||||||
|
#include <stdio.h>
|
||||||
|
#include <conio.h>
|
||||||
|
#include <Windows.h>
|
||||||
|
#include <Objbase.h>
|
||||||
|
#include <vector>
|
||||||
|
|
||||||
|
static HWND gWindow = nullptr;
|
||||||
|
static int gScreenWidth = 0;
|
||||||
|
static int gScreenHeight = 0;
|
||||||
|
static unsigned int gPressedButtons = 0;
|
||||||
|
static unsigned int gDownButtons = 0;
|
||||||
|
static unsigned int gLastDownButtons = 0;
|
||||||
|
static char gWriteBuffer[(NUM_COLUMNS+1) * NUM_ROWS] = {0};
|
||||||
|
static char gDisplayBuffer[(NUM_COLUMNS+1) * NUM_ROWS] = {0};
|
||||||
|
static unsigned int gYPos = 0;
|
||||||
|
static bool gQuit = false;
|
||||||
|
static std::vector<char *> gPathList;
|
||||||
|
|
||||||
|
bool Common_Private_Test;
|
||||||
|
int Common_Private_Argc;
|
||||||
|
char** Common_Private_Argv;
|
||||||
|
void (*Common_Private_Update)(unsigned int*);
|
||||||
|
void (*Common_Private_Print)(const char*);
|
||||||
|
void (*Common_Private_Close)();
|
||||||
|
|
||||||
|
void Common_Init(void** /*extraDriverData*/)
|
||||||
|
{
|
||||||
|
CoInitializeEx(nullptr, COINIT_APARTMENTTHREADED);
|
||||||
|
}
|
||||||
|
|
||||||
|
void Common_Close()
|
||||||
|
{
|
||||||
|
CoUninitialize();
|
||||||
|
|
||||||
|
for (std::vector<char *>::iterator item = gPathList.begin(); item != gPathList.end(); ++item)
|
||||||
|
{
|
||||||
|
free(*item);
|
||||||
|
}
|
||||||
|
if (Common_Private_Close)
|
||||||
|
{
|
||||||
|
Common_Private_Close();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
static unsigned int translateButton(unsigned int button)
|
||||||
|
{
|
||||||
|
switch (button)
|
||||||
|
{
|
||||||
|
case '1': return (1 << BTN_ACTION1);
|
||||||
|
case '2': return (1 << BTN_ACTION2);
|
||||||
|
case '3': return (1 << BTN_ACTION3);
|
||||||
|
case '4': return (1 << BTN_ACTION4);
|
||||||
|
case VK_LEFT: return (1 << BTN_LEFT);
|
||||||
|
case VK_RIGHT: return (1 << BTN_RIGHT);
|
||||||
|
case VK_UP: return (1 << BTN_UP);
|
||||||
|
case VK_DOWN: return (1 << BTN_DOWN);
|
||||||
|
case VK_SPACE: return (1 << BTN_MORE);
|
||||||
|
case VK_ESCAPE: return (1 << BTN_QUIT);
|
||||||
|
default: return 0;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void Common_Update()
|
||||||
|
{
|
||||||
|
MSG msg = { };
|
||||||
|
while (PeekMessage(&msg, nullptr, 0, 0, PM_REMOVE))
|
||||||
|
{
|
||||||
|
TranslateMessage(&msg);
|
||||||
|
DispatchMessage(&msg);
|
||||||
|
}
|
||||||
|
|
||||||
|
gPressedButtons = (gLastDownButtons ^ gDownButtons) & gDownButtons;
|
||||||
|
gPressedButtons |= (gQuit ? (1 << BTN_QUIT) : 0);
|
||||||
|
gLastDownButtons = gDownButtons;
|
||||||
|
|
||||||
|
memcpy(gDisplayBuffer, gWriteBuffer, sizeof(gWriteBuffer));
|
||||||
|
InvalidateRect(gWindow, nullptr, FALSE);
|
||||||
|
|
||||||
|
gYPos = 0;
|
||||||
|
memset(gWriteBuffer, ' ', sizeof(gWriteBuffer));
|
||||||
|
for (int i = 0; i < NUM_ROWS; i++)
|
||||||
|
{
|
||||||
|
gWriteBuffer[(i * (NUM_COLUMNS + 1)) + NUM_COLUMNS] = '\n';
|
||||||
|
}
|
||||||
|
|
||||||
|
if (Common_Private_Update)
|
||||||
|
{
|
||||||
|
Common_Private_Update(&gPressedButtons);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void Common_Exit(int returnCode)
|
||||||
|
{
|
||||||
|
exit(returnCode);
|
||||||
|
}
|
||||||
|
|
||||||
|
void Common_DrawText(const char *text)
|
||||||
|
{
|
||||||
|
if (gYPos < NUM_ROWS)
|
||||||
|
{
|
||||||
|
char tempBuffer[NUM_COLUMNS + 1];
|
||||||
|
Common_Format(tempBuffer, sizeof(tempBuffer), "%s", text);
|
||||||
|
memcpy(&gWriteBuffer[gYPos * (NUM_COLUMNS + 1)], tempBuffer, strlen(tempBuffer));
|
||||||
|
gYPos++;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
bool Common_BtnPress(Common_Button btn)
|
||||||
|
{
|
||||||
|
return ((gPressedButtons & (1 << btn)) != 0);
|
||||||
|
}
|
||||||
|
|
||||||
|
bool Common_BtnDown(Common_Button btn)
|
||||||
|
{
|
||||||
|
return ((gDownButtons & (1 << btn)) != 0);
|
||||||
|
}
|
||||||
|
|
||||||
|
const char *Common_BtnStr(Common_Button btn)
|
||||||
|
{
|
||||||
|
switch (btn)
|
||||||
|
{
|
||||||
|
case BTN_ACTION1: return "1";
|
||||||
|
case BTN_ACTION2: return "2";
|
||||||
|
case BTN_ACTION3: return "3";
|
||||||
|
case BTN_ACTION4: return "4";
|
||||||
|
case BTN_LEFT: return "Left";
|
||||||
|
case BTN_RIGHT: return "Right";
|
||||||
|
case BTN_UP: return "Up";
|
||||||
|
case BTN_DOWN: return "Down";
|
||||||
|
case BTN_MORE: return "Space";
|
||||||
|
case BTN_QUIT: return "Escape";
|
||||||
|
default: return "Unknown";
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const char *Common_MediaPath(const char *fileName)
|
||||||
|
{
|
||||||
|
char *filePath = (char *)calloc(256, sizeof(char));
|
||||||
|
|
||||||
|
static const char* pathPrefix = nullptr;
|
||||||
|
if (!pathPrefix)
|
||||||
|
{
|
||||||
|
const char *emptyPrefix = "";
|
||||||
|
const char *mediaPrefix = "../media/";
|
||||||
|
FILE *file = fopen(fileName, "r");
|
||||||
|
if (file)
|
||||||
|
{
|
||||||
|
fclose(file);
|
||||||
|
pathPrefix = emptyPrefix;
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
pathPrefix = mediaPrefix;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
strcat(filePath, pathPrefix);
|
||||||
|
strcat(filePath, fileName);
|
||||||
|
|
||||||
|
gPathList.push_back(filePath);
|
||||||
|
|
||||||
|
return filePath;
|
||||||
|
}
|
||||||
|
|
||||||
|
const char *Common_WritePath(const char *fileName)
|
||||||
|
{
|
||||||
|
return Common_MediaPath(fileName);
|
||||||
|
}
|
||||||
|
|
||||||
|
void Common_TTY(const char *format, ...)
|
||||||
|
{
|
||||||
|
char string[1024] = {0};
|
||||||
|
|
||||||
|
va_list args;
|
||||||
|
va_start(args, format);
|
||||||
|
Common_vsnprintf(string, 1023, format, args);
|
||||||
|
va_end(args);
|
||||||
|
|
||||||
|
if (Common_Private_Print)
|
||||||
|
{
|
||||||
|
(*Common_Private_Print)(string);
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
OutputDebugStringA(string);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
HFONT CreateDisplayFont()
|
||||||
|
{
|
||||||
|
return CreateFontA(22, 0, 0, 0, FW_DONTCARE, FALSE, FALSE, FALSE, DEFAULT_CHARSET, OUT_OUTLINE_PRECIS,
|
||||||
|
CLIP_DEFAULT_PRECIS, DEFAULT_QUALITY, FIXED_PITCH, 0);
|
||||||
|
}
|
||||||
|
|
||||||
|
LRESULT CALLBACK WndProc(HWND hWnd, UINT message, WPARAM wParam, LPARAM lParam)
|
||||||
|
{
|
||||||
|
if (message == WM_PAINT)
|
||||||
|
{
|
||||||
|
PAINTSTRUCT ps;
|
||||||
|
HDC hdc = BeginPaint(hWnd, &ps);
|
||||||
|
HDC hdcBack = CreateCompatibleDC(hdc);
|
||||||
|
|
||||||
|
HBITMAP hbmBack = CreateCompatibleBitmap(hdc, gScreenWidth, gScreenHeight);
|
||||||
|
HBITMAP hbmOld = (HBITMAP)SelectObject(hdcBack, hbmBack);
|
||||||
|
|
||||||
|
HFONT hfnt = CreateDisplayFont();
|
||||||
|
HFONT hfntOld = (HFONT)SelectObject(hdcBack, hfnt);
|
||||||
|
|
||||||
|
SetBkColor(hdcBack, RGB(0x0, 0x0, 0x0));
|
||||||
|
SetTextColor(hdcBack, RGB(0xFF, 0xFF, 0xFF));
|
||||||
|
DrawTextA(hdcBack, gDisplayBuffer, -1, &ps.rcPaint, 0);
|
||||||
|
BitBlt(hdc, 0, 0, gScreenWidth, gScreenHeight, hdcBack, 0, 0, SRCCOPY);
|
||||||
|
|
||||||
|
SelectObject(hdcBack, hfntOld);
|
||||||
|
DeleteObject(hfnt);
|
||||||
|
|
||||||
|
SelectObject(hdcBack, hbmOld);
|
||||||
|
DeleteObject(hbmBack);
|
||||||
|
|
||||||
|
DeleteDC(hdcBack);
|
||||||
|
EndPaint(hWnd, &ps);
|
||||||
|
}
|
||||||
|
else if (message == WM_DESTROY)
|
||||||
|
{
|
||||||
|
gQuit = true;
|
||||||
|
}
|
||||||
|
else if (message == WM_GETMINMAXINFO)
|
||||||
|
{
|
||||||
|
if (gScreenWidth == 0)
|
||||||
|
{
|
||||||
|
HDC hdc = GetDC(hWnd);
|
||||||
|
|
||||||
|
HFONT hfnt = CreateDisplayFont();
|
||||||
|
HFONT hfntOld = (HFONT)SelectObject(hdc, hfnt);
|
||||||
|
|
||||||
|
TEXTMETRICA metrics = { };
|
||||||
|
GetTextMetricsA(hdc, &metrics);
|
||||||
|
|
||||||
|
SelectObject(hdc, hfntOld);
|
||||||
|
DeleteObject(hfnt);
|
||||||
|
|
||||||
|
ReleaseDC(hWnd, hdc);
|
||||||
|
|
||||||
|
RECT rec = { };
|
||||||
|
rec.right = metrics.tmAveCharWidth * NUM_COLUMNS;
|
||||||
|
rec.bottom = metrics.tmHeight * NUM_ROWS;
|
||||||
|
|
||||||
|
BOOL success = AdjustWindowRect(&rec, WS_CAPTION | WS_SYSMENU, FALSE);
|
||||||
|
assert(success);
|
||||||
|
|
||||||
|
gScreenWidth = rec.right - rec.left;
|
||||||
|
gScreenHeight = rec.bottom - rec.top;
|
||||||
|
}
|
||||||
|
|
||||||
|
LPMINMAXINFO lpMMI = (LPMINMAXINFO)lParam;
|
||||||
|
lpMMI->ptMinTrackSize.x = gScreenWidth;
|
||||||
|
lpMMI->ptMinTrackSize.y = gScreenHeight;
|
||||||
|
lpMMI->ptMaxTrackSize = lpMMI->ptMinTrackSize;
|
||||||
|
}
|
||||||
|
else if (message == WM_KEYDOWN)
|
||||||
|
{
|
||||||
|
gDownButtons |= translateButton((unsigned int)wParam);
|
||||||
|
}
|
||||||
|
else if (message == WM_KEYUP)
|
||||||
|
{
|
||||||
|
gDownButtons &= ~translateButton((unsigned int)wParam);
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
return DefWindowProc(hWnd, message, wParam, lParam);
|
||||||
|
}
|
||||||
|
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
int WINAPI WinMain(HINSTANCE hInstance, HINSTANCE /*hPrevInstance*/, PSTR /*pCmdLine*/, int nCmdShow)
|
||||||
|
{
|
||||||
|
const char CLASS_NAME[] = "FMOD Example Window Class";
|
||||||
|
|
||||||
|
Common_Private_Argc = __argc;
|
||||||
|
Common_Private_Argv = __argv;
|
||||||
|
|
||||||
|
WNDCLASSA wc = { };
|
||||||
|
wc.style = CS_HREDRAW | CS_VREDRAW;
|
||||||
|
wc.lpfnWndProc = WndProc;
|
||||||
|
wc.hInstance = hInstance;
|
||||||
|
wc.hbrBackground = (HBRUSH)GetStockObject(BLACK_BRUSH);
|
||||||
|
wc.lpszClassName = CLASS_NAME;
|
||||||
|
|
||||||
|
ATOM atom = RegisterClassA(&wc);
|
||||||
|
assert(atom);
|
||||||
|
|
||||||
|
gWindow = CreateWindowA(CLASS_NAME, "FMOD Example", WS_CAPTION | WS_SYSMENU, CW_USEDEFAULT, CW_USEDEFAULT, CW_USEDEFAULT, CW_USEDEFAULT, nullptr, nullptr, hInstance, nullptr);
|
||||||
|
assert(gWindow);
|
||||||
|
|
||||||
|
ShowWindow(gWindow, nCmdShow);
|
||||||
|
|
||||||
|
return FMOD_Main();
|
||||||
|
}
|
||||||
@@ -0,0 +1,16 @@
|
|||||||
|
/*==============================================================================
|
||||||
|
FMOD Example Framework
|
||||||
|
Copyright (c), Firelight Technologies Pty, Ltd 2012-2026.
|
||||||
|
==============================================================================*/
|
||||||
|
#include <windows.h>
|
||||||
|
|
||||||
|
int FMOD_Main();
|
||||||
|
|
||||||
|
#define COMMON_PLATFORM_SUPPORTS_FOPEN
|
||||||
|
|
||||||
|
#define Common_snprintf _snprintf
|
||||||
|
#define Common_vsnprintf _vsnprintf
|
||||||
|
|
||||||
|
void Common_TTY(const char *format, ...);
|
||||||
|
|
||||||
|
|
||||||
@@ -0,0 +1,239 @@
|
|||||||
|
/*==============================================================================
|
||||||
|
Convolution Reverb Example
|
||||||
|
Copyright (c), Firelight Technologies Pty, Ltd 2004-2026.
|
||||||
|
|
||||||
|
This example shows how to set up a convolution reverb DSP as a global
|
||||||
|
DSP unit that can be routed into by multiple seperate channels.
|
||||||
|
|
||||||
|
Convolution reverb uses data from a real world locations called an
|
||||||
|
"Impulse Response" to model the reflection of audio waves back
|
||||||
|
to a listener.
|
||||||
|
|
||||||
|
Impulse Response is based on "St Andrew's Church" by
|
||||||
|
|
||||||
|
www.openairlib.net
|
||||||
|
Audiolab, University of York
|
||||||
|
Damian T. Murphy
|
||||||
|
http://www.openairlib.net/auralizationdb/content/st-andrews-church
|
||||||
|
|
||||||
|
licensed under Attribution Share Alike Creative Commons license
|
||||||
|
http://creativecommons.org/licenses/by-sa/3.0/
|
||||||
|
|
||||||
|
|
||||||
|
Anechoic sample "Operatic Voice" by
|
||||||
|
|
||||||
|
www.openairlib.net
|
||||||
|
http://www.openairlib.net/anechoicdb/content/operatic-voice
|
||||||
|
|
||||||
|
licensed under Attribution Share Alike Creative Commons license
|
||||||
|
http://creativecommons.org/licenses/by-sa/3.0/
|
||||||
|
|
||||||
|
|
||||||
|
### Features Demonstrated ###
|
||||||
|
+ FMOD_DSP_CONVOLUTION_REVERB
|
||||||
|
+ DSP::addInput
|
||||||
|
|
||||||
|
For information on using FMOD example code in your own programs, visit
|
||||||
|
https://www.fmod.com/legal
|
||||||
|
|
||||||
|
==============================================================================*/
|
||||||
|
#include "fmod.hpp"
|
||||||
|
#include "common.h"
|
||||||
|
|
||||||
|
int FMOD_Main()
|
||||||
|
{
|
||||||
|
void *extradriverdata = 0;
|
||||||
|
Common_Init(&extradriverdata);
|
||||||
|
|
||||||
|
/*
|
||||||
|
Create a System object and initialize
|
||||||
|
*/
|
||||||
|
FMOD_RESULT result;
|
||||||
|
FMOD::System* system;
|
||||||
|
result = FMOD::System_Create(&system);
|
||||||
|
ERRCHECK(result);
|
||||||
|
|
||||||
|
result = system->init(32, FMOD_INIT_NORMAL, extradriverdata);
|
||||||
|
ERRCHECK(result);
|
||||||
|
|
||||||
|
|
||||||
|
/*
|
||||||
|
Create a new channel group to hold the convolution DSP unit
|
||||||
|
*/
|
||||||
|
FMOD::ChannelGroup* reverbGroup;
|
||||||
|
result = system->createChannelGroup("reverb", &reverbGroup);
|
||||||
|
ERRCHECK(result);
|
||||||
|
|
||||||
|
|
||||||
|
/*
|
||||||
|
Create a new channel group to hold all the channels and process the dry path
|
||||||
|
*/
|
||||||
|
FMOD::ChannelGroup* mainGroup;
|
||||||
|
result = system->createChannelGroup("main", &mainGroup);
|
||||||
|
ERRCHECK(result);
|
||||||
|
|
||||||
|
/*
|
||||||
|
Create the convultion DSP unit and set it as the tail of the channel group
|
||||||
|
*/
|
||||||
|
FMOD::DSP* reverbUnit;
|
||||||
|
result = system->createDSPByType(FMOD_DSP_TYPE_CONVOLUTIONREVERB, &reverbUnit);
|
||||||
|
ERRCHECK(result);
|
||||||
|
result = reverbGroup->addDSP(FMOD_CHANNELCONTROL_DSP_TAIL, reverbUnit);
|
||||||
|
ERRCHECK(result);
|
||||||
|
|
||||||
|
/*
|
||||||
|
Open the impulse response wav file, but use FMOD_OPENONLY as we want
|
||||||
|
to read the data into a seperate buffer
|
||||||
|
*/
|
||||||
|
FMOD::Sound* irSound;
|
||||||
|
result = system->createSound(Common_MediaPath("standrews.wav"), FMOD_DEFAULT | FMOD_OPENONLY, NULL, &irSound);
|
||||||
|
ERRCHECK(result);
|
||||||
|
|
||||||
|
/*
|
||||||
|
Retrieve the sound information for the Impulse Response input file
|
||||||
|
*/
|
||||||
|
FMOD_SOUND_FORMAT irSoundFormat;
|
||||||
|
FMOD_SOUND_TYPE irSoundType;
|
||||||
|
int irSoundBits, irSoundChannels;
|
||||||
|
result = irSound->getFormat(&irSoundType, &irSoundFormat, &irSoundChannels, &irSoundBits);
|
||||||
|
ERRCHECK(result);
|
||||||
|
unsigned int irSoundLength;
|
||||||
|
result = irSound->getLength(&irSoundLength, FMOD_TIMEUNIT_PCM);
|
||||||
|
ERRCHECK(result);
|
||||||
|
|
||||||
|
|
||||||
|
if (irSoundFormat != FMOD_SOUND_FORMAT_PCM16)
|
||||||
|
{
|
||||||
|
/*
|
||||||
|
For simplicity of the example, if the impulse response is the wrong format just display an error
|
||||||
|
*/
|
||||||
|
Common_Fatal("Impulse Response file is the wrong audio format");
|
||||||
|
}
|
||||||
|
|
||||||
|
/*
|
||||||
|
The reverb unit expects a block of data containing a single 16 bit int containing
|
||||||
|
the number of channels in the impulse response, followed by PCM 16 data
|
||||||
|
*/
|
||||||
|
unsigned int irDataLength = sizeof(short) * (irSoundLength * irSoundChannels + 1);
|
||||||
|
short* irData = (short*)malloc(irDataLength);
|
||||||
|
irData[0] = (short)irSoundChannels;
|
||||||
|
unsigned int irDataRead;
|
||||||
|
result = irSound->readData(&irData[1], irDataLength - sizeof(short), &irDataRead);
|
||||||
|
ERRCHECK(result);
|
||||||
|
result = reverbUnit->setParameterData(FMOD_DSP_CONVOLUTION_REVERB_PARAM_IR, irData, irDataLength);
|
||||||
|
ERRCHECK(result);
|
||||||
|
|
||||||
|
/*
|
||||||
|
Don't pass any dry signal from the reverb unit, instead take the dry part
|
||||||
|
of the mix from the main signal path
|
||||||
|
*/
|
||||||
|
result = reverbUnit->setParameterFloat(FMOD_DSP_CONVOLUTION_REVERB_PARAM_DRY, -80.0f);
|
||||||
|
ERRCHECK(result);
|
||||||
|
|
||||||
|
/*
|
||||||
|
We can now free our copy of the IR data and release the sound object, the reverb unit
|
||||||
|
has created it's internal data
|
||||||
|
*/
|
||||||
|
free(irData);
|
||||||
|
result = irSound->release();
|
||||||
|
ERRCHECK(result);
|
||||||
|
|
||||||
|
/*
|
||||||
|
Load up and play a sample clip recorded in an anechoic chamber
|
||||||
|
*/
|
||||||
|
FMOD::Sound* sound;
|
||||||
|
system->createSound(Common_MediaPath("singing.wav"), FMOD_3D | FMOD_LOOP_NORMAL, NULL, &sound);
|
||||||
|
ERRCHECK(result);
|
||||||
|
|
||||||
|
FMOD::Channel* channel;
|
||||||
|
system->playSound(sound, mainGroup, true, &channel);
|
||||||
|
ERRCHECK(result);
|
||||||
|
|
||||||
|
/*
|
||||||
|
Create a send connection between the channel head and the reverb unit
|
||||||
|
*/
|
||||||
|
FMOD::DSP* channelHead;
|
||||||
|
channel->getDSP(FMOD_CHANNELCONTROL_DSP_HEAD, &channelHead);
|
||||||
|
ERRCHECK(result);
|
||||||
|
FMOD::DSPConnection* reverbConnection;
|
||||||
|
result = reverbUnit->addInput(channelHead, &reverbConnection, FMOD_DSPCONNECTION_TYPE_SEND);
|
||||||
|
ERRCHECK(result);
|
||||||
|
|
||||||
|
result = channel->setPaused(false);
|
||||||
|
ERRCHECK(result);
|
||||||
|
|
||||||
|
|
||||||
|
float wetVolume = 1.0;
|
||||||
|
float dryVolume = 1.0;
|
||||||
|
|
||||||
|
/*
|
||||||
|
Main loop
|
||||||
|
*/
|
||||||
|
do
|
||||||
|
{
|
||||||
|
Common_Update();
|
||||||
|
|
||||||
|
if (Common_BtnPress(BTN_LEFT))
|
||||||
|
{
|
||||||
|
wetVolume = (wetVolume <= 0.0f) ? wetVolume : wetVolume - 0.05f;
|
||||||
|
}
|
||||||
|
if (Common_BtnPress(BTN_RIGHT))
|
||||||
|
{
|
||||||
|
wetVolume = (wetVolume >= 1.0f) ? wetVolume : wetVolume + 0.05f;
|
||||||
|
}
|
||||||
|
if (Common_BtnPress(BTN_DOWN))
|
||||||
|
{
|
||||||
|
dryVolume = (dryVolume <= 0.0f) ? dryVolume : dryVolume - 0.05f;
|
||||||
|
}
|
||||||
|
if (Common_BtnPress(BTN_UP))
|
||||||
|
{
|
||||||
|
dryVolume = (dryVolume >= 1.0f) ? dryVolume : dryVolume + 0.05f;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
result = system->update();
|
||||||
|
ERRCHECK(result);
|
||||||
|
|
||||||
|
result = reverbConnection->setMix(wetVolume);
|
||||||
|
ERRCHECK(result);
|
||||||
|
result = mainGroup->setVolume(dryVolume);
|
||||||
|
ERRCHECK(result);
|
||||||
|
|
||||||
|
|
||||||
|
Common_Draw("==================================================");
|
||||||
|
Common_Draw("Convolution Example.");
|
||||||
|
Common_Draw("Copyright (c) Firelight Technologies 2004-2026.");
|
||||||
|
Common_Draw("==================================================");
|
||||||
|
Common_Draw("Press %s and %s to change dry mix", Common_BtnStr(BTN_UP), Common_BtnStr(BTN_DOWN));
|
||||||
|
Common_Draw("Press %s and %s to change wet mix", Common_BtnStr(BTN_LEFT), Common_BtnStr(BTN_RIGHT));
|
||||||
|
Common_Draw("wet mix [%.2f] dry mix [%.2f]", wetVolume, dryVolume);
|
||||||
|
Common_Draw("Press %s to quit", Common_BtnStr(BTN_QUIT));
|
||||||
|
Common_Draw("");
|
||||||
|
|
||||||
|
Common_Sleep(50);
|
||||||
|
} while (!Common_BtnPress(BTN_QUIT));
|
||||||
|
|
||||||
|
/*
|
||||||
|
Shut down
|
||||||
|
*/
|
||||||
|
result = sound->release();
|
||||||
|
ERRCHECK(result);
|
||||||
|
result = mainGroup->release();
|
||||||
|
ERRCHECK(result);
|
||||||
|
result = reverbGroup->removeDSP(reverbUnit);
|
||||||
|
ERRCHECK(result);
|
||||||
|
result = reverbUnit->disconnectAll(true, true);
|
||||||
|
ERRCHECK(result);
|
||||||
|
result = reverbUnit->release();
|
||||||
|
ERRCHECK(result);
|
||||||
|
result = reverbGroup->release();
|
||||||
|
ERRCHECK(result);
|
||||||
|
result = system->close();
|
||||||
|
ERRCHECK(result);
|
||||||
|
result = system->release();
|
||||||
|
ERRCHECK(result);
|
||||||
|
|
||||||
|
Common_Close();
|
||||||
|
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
@@ -0,0 +1,362 @@
|
|||||||
|
/*==============================================================================
|
||||||
|
Custom DSP Example
|
||||||
|
Copyright (c), Firelight Technologies Pty, Ltd 2004-2026.
|
||||||
|
|
||||||
|
This example shows how to add a user created DSP callback to process audio
|
||||||
|
data. The read callback is executed at runtime, and can be added anywhere in
|
||||||
|
the DSP network.
|
||||||
|
|
||||||
|
For information on using FMOD example code in your own programs, visit
|
||||||
|
https://www.fmod.com/legal
|
||||||
|
==============================================================================*/
|
||||||
|
#include "fmod.hpp"
|
||||||
|
#include "common.h"
|
||||||
|
|
||||||
|
typedef struct
|
||||||
|
{
|
||||||
|
float *buffer;
|
||||||
|
float volume_linear;
|
||||||
|
int length_samples;
|
||||||
|
int channels;
|
||||||
|
} mydsp_data_t;
|
||||||
|
|
||||||
|
FMOD_RESULT F_CALL myDSPCallback(FMOD_DSP_STATE *dsp_state, float *inbuffer, float *outbuffer, unsigned int length, int inchannels, int *outchannels)
|
||||||
|
{
|
||||||
|
mydsp_data_t *data = (mydsp_data_t *)dsp_state->plugindata;
|
||||||
|
|
||||||
|
/*
|
||||||
|
This loop assumes inchannels = outchannels, which it will be if the DSP is created with '0'
|
||||||
|
as the number of channels in FMOD_DSP_DESCRIPTION.
|
||||||
|
Specifying an actual channel count will mean you have to take care of any number of channels coming in,
|
||||||
|
but outputting the number of channels specified. Generally it is best to keep the channel
|
||||||
|
count at 0 for maximum compatibility.
|
||||||
|
*/
|
||||||
|
for (unsigned int samp = 0; samp < length; samp++)
|
||||||
|
{
|
||||||
|
/*
|
||||||
|
Feel free to unroll this.
|
||||||
|
*/
|
||||||
|
for (int chan = 0; chan < *outchannels; chan++)
|
||||||
|
{
|
||||||
|
/*
|
||||||
|
This DSP filter just halves the volume!
|
||||||
|
Input is modified, and sent to output.
|
||||||
|
*/
|
||||||
|
data->buffer[(samp * *outchannels) + chan] = outbuffer[(samp * inchannels) + chan] = inbuffer[(samp * inchannels) + chan] * data->volume_linear;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
data->channels = inchannels;
|
||||||
|
|
||||||
|
return FMOD_OK;
|
||||||
|
}
|
||||||
|
|
||||||
|
/*
|
||||||
|
Callback called when DSP is created. This implementation creates a structure which is attached to the dsp state's 'plugindata' member.
|
||||||
|
*/
|
||||||
|
FMOD_RESULT F_CALL myDSPCreateCallback(FMOD_DSP_STATE *dsp_state)
|
||||||
|
{
|
||||||
|
unsigned int blocksize;
|
||||||
|
FMOD_RESULT result;
|
||||||
|
|
||||||
|
result = dsp_state->functions->getblocksize(dsp_state, &blocksize);
|
||||||
|
ERRCHECK(result);
|
||||||
|
|
||||||
|
mydsp_data_t *data = (mydsp_data_t *)calloc(sizeof(mydsp_data_t), 1);
|
||||||
|
if (!data)
|
||||||
|
{
|
||||||
|
return FMOD_ERR_MEMORY;
|
||||||
|
}
|
||||||
|
dsp_state->plugindata = data;
|
||||||
|
data->volume_linear = 1.0f;
|
||||||
|
data->length_samples = blocksize;
|
||||||
|
|
||||||
|
data->buffer = (float *)malloc(blocksize * 8 * sizeof(float)); // *8 = maximum size allowing room for 7.1. Could ask dsp_state->functions->getspeakermode for the right speakermode to get real speaker count.
|
||||||
|
if (!data->buffer)
|
||||||
|
{
|
||||||
|
return FMOD_ERR_MEMORY;
|
||||||
|
}
|
||||||
|
|
||||||
|
return FMOD_OK;
|
||||||
|
}
|
||||||
|
|
||||||
|
/*
|
||||||
|
Callback called when DSP is destroyed. The memory allocated in the create callback can be freed here.
|
||||||
|
*/
|
||||||
|
FMOD_RESULT F_CALL myDSPReleaseCallback(FMOD_DSP_STATE *dsp_state)
|
||||||
|
{
|
||||||
|
if (dsp_state->plugindata)
|
||||||
|
{
|
||||||
|
mydsp_data_t *data = (mydsp_data_t *)dsp_state->plugindata;
|
||||||
|
|
||||||
|
if (data->buffer)
|
||||||
|
{
|
||||||
|
free(data->buffer);
|
||||||
|
}
|
||||||
|
|
||||||
|
free(data);
|
||||||
|
}
|
||||||
|
|
||||||
|
return FMOD_OK;
|
||||||
|
}
|
||||||
|
|
||||||
|
/*
|
||||||
|
Callback called when DSP::getParameterData is called. This returns a pointer to the raw floating point PCM data.
|
||||||
|
We have set up 'parameter 0' to be the data parameter, so it checks to make sure the passed in index is 0, and nothing else.
|
||||||
|
*/
|
||||||
|
FMOD_RESULT F_CALL myDSPGetParameterDataCallback(FMOD_DSP_STATE *dsp_state, int index, void **data, unsigned int *length, char *)
|
||||||
|
{
|
||||||
|
if (index == 0)
|
||||||
|
{
|
||||||
|
unsigned int blocksize;
|
||||||
|
FMOD_RESULT result;
|
||||||
|
mydsp_data_t *mydata = (mydsp_data_t *)dsp_state->plugindata;
|
||||||
|
|
||||||
|
result = dsp_state->functions->getblocksize(dsp_state, &blocksize);
|
||||||
|
ERRCHECK(result);
|
||||||
|
|
||||||
|
*data = (void *)mydata;
|
||||||
|
*length = blocksize * 2 * sizeof(float);
|
||||||
|
|
||||||
|
return FMOD_OK;
|
||||||
|
}
|
||||||
|
|
||||||
|
return FMOD_ERR_INVALID_PARAM;
|
||||||
|
}
|
||||||
|
|
||||||
|
/*
|
||||||
|
Callback called when DSP::setParameterFloat is called. This accepts a floating point 0 to 1 volume value, and stores it.
|
||||||
|
We have set up 'parameter 1' to be the volume parameter, so it checks to make sure the passed in index is 1, and nothing else.
|
||||||
|
*/
|
||||||
|
FMOD_RESULT F_CALL myDSPSetParameterFloatCallback(FMOD_DSP_STATE *dsp_state, int index, float value)
|
||||||
|
{
|
||||||
|
if (index == 1)
|
||||||
|
{
|
||||||
|
mydsp_data_t *mydata = (mydsp_data_t *)dsp_state->plugindata;
|
||||||
|
|
||||||
|
mydata->volume_linear = value;
|
||||||
|
|
||||||
|
return FMOD_OK;
|
||||||
|
}
|
||||||
|
|
||||||
|
return FMOD_ERR_INVALID_PARAM;
|
||||||
|
}
|
||||||
|
|
||||||
|
/*
|
||||||
|
Callback called when DSP::getParameterFloat is called. This returns a floating point 0 to 1 volume value.
|
||||||
|
We have set up 'parameter 1' to be the volume parameter, so it checks to make sure the passed in index is 1, and nothing else.
|
||||||
|
An alternate way of displaying the data is provided, as a string, so the main app can use it.
|
||||||
|
*/
|
||||||
|
FMOD_RESULT F_CALL myDSPGetParameterFloatCallback(FMOD_DSP_STATE *dsp_state, int index, float *value, char *valstr)
|
||||||
|
{
|
||||||
|
if (index == 1)
|
||||||
|
{
|
||||||
|
mydsp_data_t *mydata = (mydsp_data_t *)dsp_state->plugindata;
|
||||||
|
|
||||||
|
*value = mydata->volume_linear;
|
||||||
|
if (valstr)
|
||||||
|
{
|
||||||
|
snprintf(valstr, FMOD_DSP_GETPARAM_VALUESTR_LENGTH, "%d", (int)((*value * 100.0f)+0.5f));
|
||||||
|
}
|
||||||
|
|
||||||
|
return FMOD_OK;
|
||||||
|
}
|
||||||
|
|
||||||
|
return FMOD_ERR_INVALID_PARAM;
|
||||||
|
}
|
||||||
|
|
||||||
|
int FMOD_Main()
|
||||||
|
{
|
||||||
|
FMOD::System *system;
|
||||||
|
FMOD::Sound *sound;
|
||||||
|
FMOD::Channel *channel;
|
||||||
|
FMOD::DSP *mydsp;
|
||||||
|
FMOD::ChannelGroup *mastergroup;
|
||||||
|
FMOD_RESULT result;
|
||||||
|
void *extradriverdata = 0;
|
||||||
|
|
||||||
|
Common_Init(&extradriverdata);
|
||||||
|
|
||||||
|
/*
|
||||||
|
Create a System object and initialize.
|
||||||
|
*/
|
||||||
|
result = FMOD::System_Create(&system);
|
||||||
|
ERRCHECK(result);
|
||||||
|
|
||||||
|
result = system->init(32, FMOD_INIT_NORMAL, extradriverdata);
|
||||||
|
ERRCHECK(result);
|
||||||
|
|
||||||
|
result = system->createSound(Common_MediaPath("stereo.ogg"), FMOD_LOOP_NORMAL, 0, &sound);
|
||||||
|
ERRCHECK(result);
|
||||||
|
|
||||||
|
result = system->playSound(sound, 0, false, &channel);
|
||||||
|
ERRCHECK(result);
|
||||||
|
|
||||||
|
/*
|
||||||
|
Create the DSP effect.
|
||||||
|
*/
|
||||||
|
{
|
||||||
|
FMOD_DSP_DESCRIPTION dspdesc;
|
||||||
|
memset(&dspdesc, 0, sizeof(dspdesc));
|
||||||
|
FMOD_DSP_PARAMETER_DESC wavedata_desc;
|
||||||
|
FMOD_DSP_PARAMETER_DESC volume_desc;
|
||||||
|
FMOD_DSP_PARAMETER_DESC *paramdesc[2] =
|
||||||
|
{
|
||||||
|
&wavedata_desc,
|
||||||
|
&volume_desc
|
||||||
|
};
|
||||||
|
|
||||||
|
FMOD_DSP_INIT_PARAMDESC_DATA(wavedata_desc, "wave data", "", "wave data", FMOD_DSP_PARAMETER_DATA_TYPE_USER);
|
||||||
|
FMOD_DSP_INIT_PARAMDESC_FLOAT(volume_desc, "volume", "%", "linear volume in percent", 0, 1, 1);
|
||||||
|
|
||||||
|
strncpy(dspdesc.name, "My first DSP unit", sizeof(dspdesc.name));
|
||||||
|
dspdesc.version = 0x00010000;
|
||||||
|
dspdesc.numinputbuffers = 1;
|
||||||
|
dspdesc.numoutputbuffers = 1;
|
||||||
|
dspdesc.read = myDSPCallback;
|
||||||
|
dspdesc.create = myDSPCreateCallback;
|
||||||
|
dspdesc.release = myDSPReleaseCallback;
|
||||||
|
dspdesc.getparameterdata = myDSPGetParameterDataCallback;
|
||||||
|
dspdesc.setparameterfloat = myDSPSetParameterFloatCallback;
|
||||||
|
dspdesc.getparameterfloat = myDSPGetParameterFloatCallback;
|
||||||
|
dspdesc.numparameters = 2;
|
||||||
|
dspdesc.paramdesc = paramdesc;
|
||||||
|
|
||||||
|
result = system->createDSP(&dspdesc, &mydsp);
|
||||||
|
ERRCHECK(result);
|
||||||
|
}
|
||||||
|
|
||||||
|
result = system->getMasterChannelGroup(&mastergroup);
|
||||||
|
ERRCHECK(result);
|
||||||
|
|
||||||
|
result = mastergroup->addDSP(0, mydsp);
|
||||||
|
ERRCHECK(result);
|
||||||
|
|
||||||
|
/*
|
||||||
|
Main loop.
|
||||||
|
*/
|
||||||
|
do
|
||||||
|
{
|
||||||
|
bool bypass;
|
||||||
|
|
||||||
|
Common_Update();
|
||||||
|
|
||||||
|
result = mydsp->getBypass(&bypass);
|
||||||
|
ERRCHECK(result);
|
||||||
|
|
||||||
|
if (Common_BtnPress(BTN_ACTION1))
|
||||||
|
{
|
||||||
|
bypass = !bypass;
|
||||||
|
|
||||||
|
result = mydsp->setBypass(bypass);
|
||||||
|
ERRCHECK(result);
|
||||||
|
}
|
||||||
|
if (Common_BtnPress(BTN_ACTION2))
|
||||||
|
{
|
||||||
|
float vol;
|
||||||
|
|
||||||
|
result = mydsp->getParameterFloat(1, &vol, 0, 0);
|
||||||
|
ERRCHECK(result);
|
||||||
|
|
||||||
|
if (vol > 0.0f)
|
||||||
|
{
|
||||||
|
vol -= 0.1f;
|
||||||
|
}
|
||||||
|
|
||||||
|
result = mydsp->setParameterFloat(1, vol);
|
||||||
|
ERRCHECK(result);
|
||||||
|
}
|
||||||
|
if (Common_BtnPress(BTN_ACTION3))
|
||||||
|
{
|
||||||
|
float vol;
|
||||||
|
|
||||||
|
result = mydsp->getParameterFloat(1, &vol, 0, 0);
|
||||||
|
ERRCHECK(result);
|
||||||
|
|
||||||
|
if (vol < 1.0f)
|
||||||
|
{
|
||||||
|
vol += 0.1f;
|
||||||
|
}
|
||||||
|
|
||||||
|
result = mydsp->setParameterFloat(1, vol);
|
||||||
|
ERRCHECK(result);
|
||||||
|
}
|
||||||
|
|
||||||
|
result = system->update();
|
||||||
|
ERRCHECK(result);
|
||||||
|
|
||||||
|
{
|
||||||
|
char volstr[32] = { 0 };
|
||||||
|
FMOD_DSP_PARAMETER_DESC *desc;
|
||||||
|
mydsp_data_t *data;
|
||||||
|
|
||||||
|
result = mydsp->getParameterInfo(1, &desc);
|
||||||
|
ERRCHECK(result);
|
||||||
|
result = mydsp->getParameterFloat(1, 0, volstr, 32);
|
||||||
|
ERRCHECK(result);
|
||||||
|
result = mydsp->getParameterData(0, (void **)&data, 0, 0, 0);
|
||||||
|
ERRCHECK(result);
|
||||||
|
|
||||||
|
Common_Draw("==================================================");
|
||||||
|
Common_Draw("Custom DSP Example.");
|
||||||
|
Common_Draw("Copyright (c) Firelight Technologies 2004-2026.");
|
||||||
|
Common_Draw("==================================================");
|
||||||
|
Common_Draw("");
|
||||||
|
Common_Draw("Press %s to toggle filter bypass", Common_BtnStr(BTN_ACTION1));
|
||||||
|
Common_Draw("Press %s to decrease volume 10%%", Common_BtnStr(BTN_ACTION2));
|
||||||
|
Common_Draw("Press %s to increase volume 10%%", Common_BtnStr(BTN_ACTION3));
|
||||||
|
Common_Draw("Press %s to quit", Common_BtnStr(BTN_QUIT));
|
||||||
|
Common_Draw("");
|
||||||
|
Common_Draw("Filter is %s", bypass ? "inactive" : "active");
|
||||||
|
Common_Draw("Volume is %s%s", volstr, desc->label);
|
||||||
|
|
||||||
|
if (data->channels)
|
||||||
|
{
|
||||||
|
char display[80] = { 0 };
|
||||||
|
int channel;
|
||||||
|
|
||||||
|
for (channel = 0; channel < data->channels; channel++)
|
||||||
|
{
|
||||||
|
int count,level;
|
||||||
|
float max = 0;
|
||||||
|
|
||||||
|
for (count = 0; count < data->length_samples; count++)
|
||||||
|
{
|
||||||
|
if (fabsf(data->buffer[(count * data->channels) + channel]) > max)
|
||||||
|
{
|
||||||
|
max = fabsf(data->buffer[(count * data->channels) + channel]);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
level = (int)(max * 40.0f);
|
||||||
|
|
||||||
|
snprintf(display, sizeof(display), "%2d ", channel);
|
||||||
|
for (count = 0; count < level; count++) display[count + 3] = '=';
|
||||||
|
|
||||||
|
Common_Draw(display);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Common_Sleep(50);
|
||||||
|
} while (!Common_BtnPress(BTN_QUIT));
|
||||||
|
|
||||||
|
/*
|
||||||
|
Shut down
|
||||||
|
*/
|
||||||
|
result = sound->release();
|
||||||
|
ERRCHECK(result);
|
||||||
|
|
||||||
|
result = mastergroup->removeDSP(mydsp);
|
||||||
|
ERRCHECK(result);
|
||||||
|
result = mydsp->release();
|
||||||
|
ERRCHECK(result);
|
||||||
|
|
||||||
|
result = system->close();
|
||||||
|
ERRCHECK(result);
|
||||||
|
result = system->release();
|
||||||
|
ERRCHECK(result);
|
||||||
|
|
||||||
|
Common_Close();
|
||||||
|
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
@@ -0,0 +1,276 @@
|
|||||||
|
/*==============================================================================
|
||||||
|
DSP Effect Per Speaker Example
|
||||||
|
Copyright (c), Firelight Technologies Pty, Ltd 2004-2026.
|
||||||
|
|
||||||
|
This example shows how to manipulate a DSP network and as an example, creates 2
|
||||||
|
DSP effects, splitting a single sound into 2 audio paths, which it then filters
|
||||||
|
seperately.
|
||||||
|
|
||||||
|
To only have each audio path come out of one speaker each,
|
||||||
|
DSPConnection::setMixMatrix is used just before the 2 branches merge back together
|
||||||
|
again.
|
||||||
|
|
||||||
|
For more speakers:
|
||||||
|
|
||||||
|
* Use System::setSoftwareFormat
|
||||||
|
* Create more effects, currently 2 for stereo (lowpass and highpass), create one
|
||||||
|
per speaker.
|
||||||
|
* Under the 'Now connect the 2 effects to channeldsp head.' section, connect
|
||||||
|
the extra effects by duplicating the code more times.
|
||||||
|
* Filter each effect to each speaker by calling DSPConnection::setMixMatrix.
|
||||||
|
Expand the existing code by extending the matrices from 2 in and 2 out, to the
|
||||||
|
number of speakers you require.
|
||||||
|
|
||||||
|
For information on using FMOD example code in your own programs, visit
|
||||||
|
https://www.fmod.com/legal
|
||||||
|
==============================================================================*/
|
||||||
|
#include "fmod.hpp"
|
||||||
|
#include "common.h"
|
||||||
|
|
||||||
|
int FMOD_Main()
|
||||||
|
{
|
||||||
|
FMOD::System *system;
|
||||||
|
FMOD::Sound *sound;
|
||||||
|
FMOD::Channel *channel;
|
||||||
|
FMOD::ChannelGroup *mastergroup;
|
||||||
|
FMOD::DSP *dsplowpass, *dsphighpass, *dsphead, *dspchannelmixer;
|
||||||
|
FMOD::DSPConnection *dsplowpassconnection, *dsphighpassconnection;
|
||||||
|
FMOD_RESULT result;
|
||||||
|
float pan = 0.0f;
|
||||||
|
void *extradriverdata = 0;
|
||||||
|
|
||||||
|
Common_Init(&extradriverdata);
|
||||||
|
|
||||||
|
/*
|
||||||
|
Create a System object and initialize.
|
||||||
|
*/
|
||||||
|
result = FMOD::System_Create(&system);
|
||||||
|
ERRCHECK(result);
|
||||||
|
|
||||||
|
/*
|
||||||
|
In this special case we want to use stereo output and not worry about varying matrix sizes depending on user speaker mode.
|
||||||
|
*/
|
||||||
|
system->setSoftwareFormat(48000, FMOD_SPEAKERMODE_STEREO, 0);
|
||||||
|
ERRCHECK(result);
|
||||||
|
|
||||||
|
/*
|
||||||
|
Initialize FMOD
|
||||||
|
*/
|
||||||
|
result = system->init(32, FMOD_INIT_NORMAL, extradriverdata);
|
||||||
|
ERRCHECK(result);
|
||||||
|
|
||||||
|
result = system->createSound(Common_MediaPath("drumloop.wav"), FMOD_LOOP_NORMAL, 0, &sound);
|
||||||
|
ERRCHECK(result);
|
||||||
|
|
||||||
|
result = system->playSound(sound, 0, false, &channel);
|
||||||
|
ERRCHECK(result);
|
||||||
|
|
||||||
|
/*
|
||||||
|
Create the DSP effects.
|
||||||
|
*/
|
||||||
|
result = system->createDSPByType(FMOD_DSP_TYPE_LOWPASS, &dsplowpass);
|
||||||
|
ERRCHECK(result);
|
||||||
|
|
||||||
|
result = dsplowpass->setParameterFloat(FMOD_DSP_LOWPASS_CUTOFF, 1000.0f);
|
||||||
|
ERRCHECK(result);
|
||||||
|
result = dsplowpass->setParameterFloat(FMOD_DSP_LOWPASS_RESONANCE, 4.0f);
|
||||||
|
ERRCHECK(result);
|
||||||
|
|
||||||
|
result = system->createDSPByType(FMOD_DSP_TYPE_HIGHPASS, &dsphighpass);
|
||||||
|
ERRCHECK(result);
|
||||||
|
|
||||||
|
result = dsphighpass->setParameterFloat(FMOD_DSP_HIGHPASS_CUTOFF, 4000.0f);
|
||||||
|
ERRCHECK(result);
|
||||||
|
result = dsphighpass->setParameterFloat(FMOD_DSP_HIGHPASS_RESONANCE, 4.0f);
|
||||||
|
ERRCHECK(result);
|
||||||
|
|
||||||
|
/*
|
||||||
|
Connect up the DSP network
|
||||||
|
*/
|
||||||
|
|
||||||
|
/*
|
||||||
|
When a sound is played, a subnetwork is set up in the DSP network which looks like this.
|
||||||
|
Wavetable is the drumloop sound, and it feeds its data from right to left.
|
||||||
|
|
||||||
|
[DSPHEAD]<------------[DSPCHANNELMIXER]<------------[CHANNEL HEAD]<------------[WAVETABLE - DRUMLOOP.WAV]
|
||||||
|
*/
|
||||||
|
result = system->getMasterChannelGroup(&mastergroup);
|
||||||
|
ERRCHECK(result);
|
||||||
|
|
||||||
|
result = mastergroup->getDSP(FMOD_CHANNELCONTROL_DSP_HEAD, &dsphead);
|
||||||
|
ERRCHECK(result);
|
||||||
|
|
||||||
|
result = dsphead->getInput(0, &dspchannelmixer, 0);
|
||||||
|
ERRCHECK(result);
|
||||||
|
|
||||||
|
/*
|
||||||
|
Now disconnect channeldsp head from wavetable to look like this.
|
||||||
|
|
||||||
|
[DSPHEAD] [DSPCHANNELMIXER]<------------[CHANNEL HEAD]<------------[WAVETABLE - DRUMLOOP.WAV]
|
||||||
|
*/
|
||||||
|
result = dsphead->disconnectFrom(dspchannelmixer);
|
||||||
|
ERRCHECK(result);
|
||||||
|
|
||||||
|
/*
|
||||||
|
Now connect the 2 effects to channeldsp head.
|
||||||
|
Store the 2 connections this makes so we can set their matrix later.
|
||||||
|
|
||||||
|
[DSPLOWPASS]
|
||||||
|
/x
|
||||||
|
[DSPHEAD] [DSPCHANNELMIXER]<------------[CHANNEL HEAD]<------------[WAVETABLE - DRUMLOOP.WAV]
|
||||||
|
\y
|
||||||
|
[DSPHIGHPASS]
|
||||||
|
*/
|
||||||
|
result = dsphead->addInput(dsplowpass, &dsplowpassconnection); /* x = dsplowpassconnection */
|
||||||
|
ERRCHECK(result);
|
||||||
|
result = dsphead->addInput(dsphighpass, &dsphighpassconnection); /* y = dsphighpassconnection */
|
||||||
|
ERRCHECK(result);
|
||||||
|
|
||||||
|
/*
|
||||||
|
Now connect the channelmixer to the 2 effects
|
||||||
|
|
||||||
|
[DSPLOWPASS]
|
||||||
|
/x \
|
||||||
|
[DSPHEAD] [DSPCHANNELMIXER]<------------[CHANNEL HEAD]<------------[WAVETABLE - DRUMLOOP.WAV]
|
||||||
|
\y /
|
||||||
|
[DSPHIGHPASS]
|
||||||
|
*/
|
||||||
|
result = dsplowpass->addInput(dspchannelmixer); /* Ignore connection - we dont care about it. */
|
||||||
|
ERRCHECK(result);
|
||||||
|
|
||||||
|
result = dsphighpass->addInput(dspchannelmixer); /* Ignore connection - we dont care about it. */
|
||||||
|
ERRCHECK(result);
|
||||||
|
|
||||||
|
/*
|
||||||
|
Now the drumloop will be twice as loud, because it is being split into 2, then recombined at the end.
|
||||||
|
What we really want is to only feed the dsphead<-dsplowpass through the left speaker for that effect, and
|
||||||
|
dsphead<-dsphighpass to the right speaker for that effect.
|
||||||
|
We can do that simply by setting the pan, or speaker matrix of the connections.
|
||||||
|
|
||||||
|
[DSPLOWPASS]
|
||||||
|
/x=1,0 \
|
||||||
|
[DSPHEAD] [DSPCHANNELMIXER]<------------[CHANNEL HEAD]<------------[WAVETABLE - DRUMLOOP.WAV]
|
||||||
|
\y=0,1 /
|
||||||
|
[DSPHIGHPASS]
|
||||||
|
*/
|
||||||
|
{
|
||||||
|
float lowpassmatrix[2][2] = {
|
||||||
|
{ 1.0f, 0.0f }, // <- output to front left. Take front left input signal at 1.0.
|
||||||
|
{ 0.0f, 0.0f } // <- output to front right. Silence
|
||||||
|
};
|
||||||
|
float highpassmatrix[2][2] = {
|
||||||
|
{ 0.0f, 0.0f }, // <- output to front left. Silence
|
||||||
|
{ 0.0f, 1.0f } // <- output to front right. Take front right input signal at 1.0
|
||||||
|
};
|
||||||
|
|
||||||
|
/*
|
||||||
|
Upgrade the signal coming from the channel mixer from mono to stereo. Otherwise the lowpass and highpass will get mono signals
|
||||||
|
*/
|
||||||
|
result = dspchannelmixer->setChannelFormat(0, 0, FMOD_SPEAKERMODE_STEREO);
|
||||||
|
ERRCHECK(result);
|
||||||
|
|
||||||
|
/*
|
||||||
|
Now set the above matrices.
|
||||||
|
*/
|
||||||
|
result = dsplowpassconnection->setMixMatrix(&lowpassmatrix[0][0], 2, 2);
|
||||||
|
ERRCHECK(result);
|
||||||
|
result = dsphighpassconnection->setMixMatrix(&highpassmatrix[0][0], 2, 2);
|
||||||
|
ERRCHECK(result);
|
||||||
|
}
|
||||||
|
|
||||||
|
result = dsplowpass->setBypass(true);
|
||||||
|
ERRCHECK(result);
|
||||||
|
result = dsphighpass->setBypass(true);
|
||||||
|
ERRCHECK(result);
|
||||||
|
|
||||||
|
result = dsplowpass->setActive(true);
|
||||||
|
ERRCHECK(result);
|
||||||
|
result = dsphighpass->setActive(true);
|
||||||
|
ERRCHECK(result);
|
||||||
|
|
||||||
|
/*
|
||||||
|
Main loop.
|
||||||
|
*/
|
||||||
|
do
|
||||||
|
{
|
||||||
|
bool lowpassbypass, highpassbypass;
|
||||||
|
|
||||||
|
Common_Update();
|
||||||
|
|
||||||
|
result = dsplowpass->getBypass(&lowpassbypass);
|
||||||
|
ERRCHECK(result);
|
||||||
|
result = dsphighpass->getBypass(&highpassbypass);
|
||||||
|
ERRCHECK(result);
|
||||||
|
|
||||||
|
if (Common_BtnPress(BTN_ACTION1))
|
||||||
|
{
|
||||||
|
lowpassbypass = !lowpassbypass;
|
||||||
|
|
||||||
|
result = dsplowpass->setBypass(lowpassbypass);
|
||||||
|
ERRCHECK(result);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (Common_BtnPress(BTN_ACTION2))
|
||||||
|
{
|
||||||
|
highpassbypass = !highpassbypass;
|
||||||
|
|
||||||
|
result = dsphighpass->setBypass(highpassbypass);
|
||||||
|
ERRCHECK(result);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (Common_BtnDown(BTN_LEFT))
|
||||||
|
{
|
||||||
|
pan = (pan <= -0.9f) ? -1.0f : pan - 0.1f;
|
||||||
|
|
||||||
|
result = channel->setPan(pan);
|
||||||
|
ERRCHECK(result);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (Common_BtnDown(BTN_RIGHT))
|
||||||
|
{
|
||||||
|
pan = (pan >= 0.9f) ? 1.0f : pan + 0.1f;
|
||||||
|
|
||||||
|
result = channel->setPan(pan);
|
||||||
|
ERRCHECK(result);
|
||||||
|
}
|
||||||
|
|
||||||
|
result = system->update();
|
||||||
|
ERRCHECK(result);
|
||||||
|
|
||||||
|
Common_Draw("==================================================");
|
||||||
|
Common_Draw("DSP Effect Per Speaker Example.");
|
||||||
|
Common_Draw("Copyright (c) Firelight Technologies 2004-2026.");
|
||||||
|
Common_Draw("==================================================");
|
||||||
|
Common_Draw("");
|
||||||
|
Common_Draw("Press %s to toggle lowpass (left speaker)", Common_BtnStr(BTN_ACTION1));
|
||||||
|
Common_Draw("Press %s to toggle highpass (right speaker)", Common_BtnStr(BTN_ACTION2));
|
||||||
|
Common_Draw("Press %s or %s to pan sound", Common_BtnStr(BTN_LEFT), Common_BtnStr(BTN_RIGHT));
|
||||||
|
Common_Draw("Press %s to quit", Common_BtnStr(BTN_QUIT));
|
||||||
|
Common_Draw("");
|
||||||
|
Common_Draw("Lowpass (left) is %s", lowpassbypass ? "inactive" : "active");
|
||||||
|
Common_Draw("Highpass (right) is %s", highpassbypass ? "inactive" : "active");
|
||||||
|
Common_Draw("Pan is %0.2f", pan);
|
||||||
|
|
||||||
|
Common_Sleep(50);
|
||||||
|
} while (!Common_BtnPress(BTN_QUIT));
|
||||||
|
|
||||||
|
/*
|
||||||
|
Shut down
|
||||||
|
*/
|
||||||
|
result = sound->release();
|
||||||
|
ERRCHECK(result);
|
||||||
|
|
||||||
|
result = dsplowpass->release();
|
||||||
|
ERRCHECK(result);
|
||||||
|
result = dsphighpass->release();
|
||||||
|
ERRCHECK(result);
|
||||||
|
|
||||||
|
result = system->close();
|
||||||
|
ERRCHECK(result);
|
||||||
|
result = system->release();
|
||||||
|
ERRCHECK(result);
|
||||||
|
|
||||||
|
Common_Close();
|
||||||
|
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
@@ -0,0 +1,331 @@
|
|||||||
|
/*==============================================================================
|
||||||
|
Plug-in Inspector Example
|
||||||
|
Copyright (c), Firelight Technologies Pty, Ltd 2004-2026.
|
||||||
|
|
||||||
|
This example shows how to enumerate loaded plug-ins and their parameters.
|
||||||
|
|
||||||
|
For information on using FMOD example code in your own programs, visit
|
||||||
|
https://www.fmod.com/legal
|
||||||
|
==============================================================================*/
|
||||||
|
#include "fmod.hpp"
|
||||||
|
#include "common.h"
|
||||||
|
|
||||||
|
const int INTERFACE_UPDATETIME = 50; // 50ms update for interface
|
||||||
|
const int MAX_PLUGINS_IN_VIEW = 5;
|
||||||
|
const int MAX_PARAMETERS_IN_VIEW = 14;
|
||||||
|
|
||||||
|
enum InspectorState
|
||||||
|
{
|
||||||
|
PLUGIN_SELECTOR,
|
||||||
|
PARAMETER_VIEWER
|
||||||
|
};
|
||||||
|
|
||||||
|
struct PluginSelectorState
|
||||||
|
{
|
||||||
|
FMOD::System *system;
|
||||||
|
int numplugins;
|
||||||
|
int cursor;
|
||||||
|
};
|
||||||
|
|
||||||
|
struct ParameterViewerState
|
||||||
|
{
|
||||||
|
FMOD::DSP *dsp;
|
||||||
|
int numparams;
|
||||||
|
int scroll;
|
||||||
|
};
|
||||||
|
|
||||||
|
void drawTitle()
|
||||||
|
{
|
||||||
|
Common_Draw("==================================================");
|
||||||
|
Common_Draw("Plug-in Inspector Example.");
|
||||||
|
Common_Draw("Copyright (c) Firelight Technologies 2004-2026.");
|
||||||
|
Common_Draw("==================================================");
|
||||||
|
Common_Draw("");
|
||||||
|
}
|
||||||
|
|
||||||
|
bool hasDataParameter(const FMOD_DSP_DESCRIPTION *desc, FMOD_DSP_PARAMETER_DATA_TYPE type)
|
||||||
|
{
|
||||||
|
for (int i = 0; i < desc->numparameters; i++)
|
||||||
|
{
|
||||||
|
if (desc->paramdesc[i]->type == FMOD_DSP_PARAMETER_TYPE_DATA && ((type >= 0 && desc->paramdesc[i]->datadesc.datatype >= 0) || desc->paramdesc[i]->datadesc.datatype == type))
|
||||||
|
{
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
void drawDSPInfo(const FMOD_DSP_DESCRIPTION *desc)
|
||||||
|
{
|
||||||
|
Common_Draw("Name (Version) : %s (%x)", desc->name, desc->version);
|
||||||
|
Common_Draw("SDK Version : %d", desc->pluginsdkversion);
|
||||||
|
Common_Draw("Type : %s", desc->numinputbuffers ? "Effect" : "Sound Generator");
|
||||||
|
Common_Draw("Parameters : %d", desc->numparameters);
|
||||||
|
Common_Draw("Audio Callback : %s", desc->process ? "process()" : "read()");
|
||||||
|
Common_Draw("");
|
||||||
|
Common_Draw(" Reset | Side-Chain | 3D | Audibility | User Data");
|
||||||
|
Common_Draw(" %s | %s | %s | %s | %s ",
|
||||||
|
desc->reset ? "Y " : "--",
|
||||||
|
hasDataParameter(desc, FMOD_DSP_PARAMETER_DATA_TYPE_SIDECHAIN) ? "Y " : "--",
|
||||||
|
hasDataParameter(desc, FMOD_DSP_PARAMETER_DATA_TYPE_3DATTRIBUTES) || hasDataParameter(desc, FMOD_DSP_PARAMETER_DATA_TYPE_3DATTRIBUTES_MULTI) ? "Y " : "--",
|
||||||
|
hasDataParameter(desc, FMOD_DSP_PARAMETER_DATA_TYPE_OVERALLGAIN) ? "Y " : "--",
|
||||||
|
hasDataParameter(desc, FMOD_DSP_PARAMETER_DATA_TYPE_USER) || desc->userdata ? "Y " : "--");
|
||||||
|
}
|
||||||
|
|
||||||
|
void drawDSPList(PluginSelectorState *state)
|
||||||
|
{
|
||||||
|
unsigned int pluginhandle;
|
||||||
|
char pluginname[256];
|
||||||
|
FMOD_RESULT result;
|
||||||
|
|
||||||
|
Common_Draw("Press %s to select the next plug-in", Common_BtnStr(BTN_DOWN));
|
||||||
|
Common_Draw("Press %s to select the previous plug-in", Common_BtnStr(BTN_UP));
|
||||||
|
Common_Draw("Press %s to view the plug-in parameters", Common_BtnStr(BTN_RIGHT));
|
||||||
|
Common_Draw("");
|
||||||
|
|
||||||
|
int start = Common_Clamp(0, state->cursor - (MAX_PLUGINS_IN_VIEW - 1) / 2, state->numplugins - MAX_PLUGINS_IN_VIEW);
|
||||||
|
for (int i = start; i < start + MAX_PLUGINS_IN_VIEW; i++)
|
||||||
|
{
|
||||||
|
result = state->system->getPluginHandle(FMOD_PLUGINTYPE_DSP, i, &pluginhandle);
|
||||||
|
ERRCHECK(result);
|
||||||
|
|
||||||
|
result = state->system->getPluginInfo(pluginhandle, 0, pluginname, 256, 0);
|
||||||
|
ERRCHECK(result);
|
||||||
|
|
||||||
|
Common_Draw("%s %s", i == state->cursor ? ">" : " ", pluginname);
|
||||||
|
}
|
||||||
|
|
||||||
|
Common_Draw("");
|
||||||
|
Common_Draw("==================================================");
|
||||||
|
Common_Draw("");
|
||||||
|
|
||||||
|
result = state->system->getPluginHandle(FMOD_PLUGINTYPE_DSP, state->cursor, &pluginhandle);
|
||||||
|
ERRCHECK(result);
|
||||||
|
|
||||||
|
const FMOD_DSP_DESCRIPTION *description;
|
||||||
|
result = state->system->getDSPInfoByPlugin(pluginhandle, &description);
|
||||||
|
ERRCHECK(result);
|
||||||
|
|
||||||
|
drawDSPInfo(description);
|
||||||
|
}
|
||||||
|
|
||||||
|
void drawDSPParameters(ParameterViewerState *state)
|
||||||
|
{
|
||||||
|
FMOD_RESULT result;
|
||||||
|
FMOD_DSP_PARAMETER_DESC *paramdesc;
|
||||||
|
char pluginname[256];
|
||||||
|
|
||||||
|
Common_Draw("Press %s to scroll down", Common_BtnStr(BTN_DOWN));
|
||||||
|
Common_Draw("Press %s to scroll up", Common_BtnStr(BTN_UP));
|
||||||
|
Common_Draw("Press %s to return to the plug-in list", Common_BtnStr(BTN_LEFT));
|
||||||
|
Common_Draw("");
|
||||||
|
|
||||||
|
result = state->dsp->getInfo(pluginname, 0, 0, 0, 0);
|
||||||
|
ERRCHECK(result);
|
||||||
|
|
||||||
|
Common_Draw("%s Parameters:", pluginname);
|
||||||
|
Common_Draw("--------------------------------------------------");
|
||||||
|
|
||||||
|
for (int i = state->scroll; i < state->numparams; i++)
|
||||||
|
{
|
||||||
|
result = state->dsp->getParameterInfo(i, ¶mdesc);
|
||||||
|
ERRCHECK(result);
|
||||||
|
switch (paramdesc->type)
|
||||||
|
{
|
||||||
|
case FMOD_DSP_PARAMETER_TYPE_FLOAT:
|
||||||
|
{
|
||||||
|
char *units = paramdesc->label;
|
||||||
|
Common_Draw("%2d: %-15s [%g, %g] (%.2f%s)", i, paramdesc->name, paramdesc->floatdesc.min, paramdesc->floatdesc.max, paramdesc->floatdesc.defaultval, units);
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
case FMOD_DSP_PARAMETER_TYPE_INT:
|
||||||
|
{
|
||||||
|
if (paramdesc->intdesc.valuenames)
|
||||||
|
{
|
||||||
|
int lengthremaining = 1024;
|
||||||
|
char enums[1024];
|
||||||
|
char *s = enums;
|
||||||
|
for (int j = 0; j < paramdesc->intdesc.max - paramdesc->intdesc.min; ++j)
|
||||||
|
{
|
||||||
|
int len = Common_snprintf(s, lengthremaining, "%s, ", paramdesc->intdesc.valuenames[j]);
|
||||||
|
if (!len)
|
||||||
|
{
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
s += len;
|
||||||
|
lengthremaining -= len;
|
||||||
|
}
|
||||||
|
if (lengthremaining)
|
||||||
|
{
|
||||||
|
Common_snprintf(s, lengthremaining, "%s", paramdesc->intdesc.valuenames[paramdesc->intdesc.max - paramdesc->intdesc.min]);
|
||||||
|
}
|
||||||
|
Common_Draw("%2d: %-15s [%s] (%s)", i, paramdesc->name, enums, paramdesc->intdesc.valuenames[paramdesc->intdesc.defaultval - paramdesc->intdesc.min]);
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
char *units = paramdesc->label;
|
||||||
|
Common_Draw("%2d: %-15s [%d, %d] (%d%s)", i, paramdesc->name, paramdesc->intdesc.min, paramdesc->intdesc.max, paramdesc->intdesc.defaultval, units);
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
case FMOD_DSP_PARAMETER_TYPE_BOOL:
|
||||||
|
{
|
||||||
|
if (paramdesc->booldesc.valuenames)
|
||||||
|
{
|
||||||
|
Common_Draw("%2d: %-15s [%s, %s] (%s)", i, paramdesc->name, paramdesc->booldesc.valuenames[0], paramdesc->booldesc.valuenames[1], paramdesc->booldesc.valuenames[paramdesc->booldesc.defaultval ? 1 : 0]);
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
Common_Draw("%2d: %-15s [On, Off] (%s)", i, paramdesc->name, paramdesc->booldesc.defaultval ? "On" : "Off");
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
case FMOD_DSP_PARAMETER_TYPE_DATA:
|
||||||
|
{
|
||||||
|
Common_Draw("%2d: %-15s (Data type: %d)", i, paramdesc->name, paramdesc->datadesc.datatype);
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
default:
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
InspectorState pluginSelectorDo(PluginSelectorState *state)
|
||||||
|
{
|
||||||
|
if (Common_BtnPress(BTN_UP))
|
||||||
|
{
|
||||||
|
state->cursor = (state->cursor - 1 + state->numplugins) % state->numplugins;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (Common_BtnPress(BTN_DOWN))
|
||||||
|
{
|
||||||
|
state->cursor = (state->cursor + 1) % state->numplugins;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (Common_BtnPress(BTN_RIGHT))
|
||||||
|
{
|
||||||
|
return PARAMETER_VIEWER;
|
||||||
|
}
|
||||||
|
|
||||||
|
drawTitle();
|
||||||
|
drawDSPList(state);
|
||||||
|
|
||||||
|
return PLUGIN_SELECTOR;
|
||||||
|
}
|
||||||
|
|
||||||
|
InspectorState parameterViewerDo(ParameterViewerState *state)
|
||||||
|
{
|
||||||
|
if (state->numparams > MAX_PARAMETERS_IN_VIEW)
|
||||||
|
{
|
||||||
|
if (Common_BtnPress(BTN_UP))
|
||||||
|
{
|
||||||
|
state->scroll--;
|
||||||
|
state->scroll = Common_Max(state->scroll, 0);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (Common_BtnPress(BTN_DOWN))
|
||||||
|
{
|
||||||
|
state->scroll++;
|
||||||
|
state->scroll = Common_Min(state->scroll, state->numparams - MAX_PARAMETERS_IN_VIEW);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (Common_BtnPress(BTN_LEFT))
|
||||||
|
{
|
||||||
|
return PLUGIN_SELECTOR;
|
||||||
|
}
|
||||||
|
|
||||||
|
drawTitle();
|
||||||
|
drawDSPParameters(state);
|
||||||
|
|
||||||
|
return PARAMETER_VIEWER;
|
||||||
|
}
|
||||||
|
|
||||||
|
int FMOD_Main()
|
||||||
|
{
|
||||||
|
FMOD::System *system = 0;
|
||||||
|
FMOD_RESULT result;
|
||||||
|
void *extradriverdata = 0;
|
||||||
|
unsigned int pluginhandle;
|
||||||
|
InspectorState state = PLUGIN_SELECTOR;
|
||||||
|
PluginSelectorState pluginselector = { 0 };
|
||||||
|
ParameterViewerState parameterviewer = { 0 };
|
||||||
|
|
||||||
|
Common_Init(&extradriverdata);
|
||||||
|
|
||||||
|
/*
|
||||||
|
Create a System object and initialize
|
||||||
|
*/
|
||||||
|
result = FMOD::System_Create(&system);
|
||||||
|
ERRCHECK(result);
|
||||||
|
|
||||||
|
result = system->init(32, FMOD_INIT_NORMAL, extradriverdata);
|
||||||
|
ERRCHECK(result);
|
||||||
|
|
||||||
|
result = system->getNumPlugins(FMOD_PLUGINTYPE_DSP, &pluginselector.numplugins);
|
||||||
|
ERRCHECK(result);
|
||||||
|
|
||||||
|
pluginselector.system = system;
|
||||||
|
|
||||||
|
do
|
||||||
|
{
|
||||||
|
Common_Update();
|
||||||
|
|
||||||
|
if (state == PLUGIN_SELECTOR)
|
||||||
|
{
|
||||||
|
state = pluginSelectorDo(&pluginselector);
|
||||||
|
|
||||||
|
if (state == PARAMETER_VIEWER)
|
||||||
|
{
|
||||||
|
result = pluginselector.system->getPluginHandle(FMOD_PLUGINTYPE_DSP, pluginselector.cursor, &pluginhandle);
|
||||||
|
ERRCHECK(result);
|
||||||
|
|
||||||
|
result = pluginselector.system->createDSPByPlugin(pluginhandle, ¶meterviewer.dsp);
|
||||||
|
ERRCHECK(result);
|
||||||
|
|
||||||
|
FMOD_RESULT result = parameterviewer.dsp->getNumParameters(¶meterviewer.numparams);
|
||||||
|
ERRCHECK(result);
|
||||||
|
|
||||||
|
parameterviewer.scroll = 0;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
else if (state == PARAMETER_VIEWER)
|
||||||
|
{
|
||||||
|
state = parameterViewerDo(¶meterviewer);
|
||||||
|
|
||||||
|
if (state == PLUGIN_SELECTOR)
|
||||||
|
{
|
||||||
|
result = parameterviewer.dsp->release();
|
||||||
|
ERRCHECK(result);
|
||||||
|
|
||||||
|
parameterviewer.dsp = 0;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
result = system->update();
|
||||||
|
ERRCHECK(result);
|
||||||
|
|
||||||
|
Common_Sleep(INTERFACE_UPDATETIME - 1);
|
||||||
|
} while (!Common_BtnPress(BTN_QUIT));
|
||||||
|
|
||||||
|
if (parameterviewer.dsp)
|
||||||
|
{
|
||||||
|
result = parameterviewer.dsp->release();
|
||||||
|
ERRCHECK(result);
|
||||||
|
}
|
||||||
|
|
||||||
|
result = system->close();
|
||||||
|
ERRCHECK(result);
|
||||||
|
result = system->release();
|
||||||
|
ERRCHECK(result);
|
||||||
|
|
||||||
|
Common_Close();
|
||||||
|
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
@@ -0,0 +1,237 @@
|
|||||||
|
/*==============================================================================
|
||||||
|
Effects Example
|
||||||
|
Copyright (c), Firelight Technologies Pty, Ltd 2004-2026.
|
||||||
|
|
||||||
|
This example shows how to apply some of the built in software effects to sounds
|
||||||
|
by applying them to the master channel group. All software sounds played here
|
||||||
|
would be filtered in the same way. To filter per channel, and not have other
|
||||||
|
channels affected, simply apply the same functions to the FMOD::Channel instead
|
||||||
|
of the FMOD::ChannelGroup.
|
||||||
|
|
||||||
|
For information on using FMOD example code in your own programs, visit
|
||||||
|
https://www.fmod.com/legal
|
||||||
|
==============================================================================*/
|
||||||
|
#include "fmod.hpp"
|
||||||
|
#include "common.h"
|
||||||
|
|
||||||
|
int FMOD_Main()
|
||||||
|
{
|
||||||
|
FMOD::System *system = 0;
|
||||||
|
FMOD::Sound *sound = 0;
|
||||||
|
FMOD::Channel *channel = 0;
|
||||||
|
FMOD::ChannelGroup *mastergroup = 0;
|
||||||
|
FMOD::DSP *dsplowpass = 0;
|
||||||
|
FMOD::DSP *dsphighpass = 0;
|
||||||
|
FMOD::DSP *dspecho = 0;
|
||||||
|
FMOD::DSP *dspflange = 0;
|
||||||
|
FMOD_RESULT result;
|
||||||
|
void *extradriverdata = 0;
|
||||||
|
|
||||||
|
Common_Init(&extradriverdata);
|
||||||
|
|
||||||
|
/*
|
||||||
|
Create a System object and initialize
|
||||||
|
*/
|
||||||
|
result = FMOD::System_Create(&system);
|
||||||
|
ERRCHECK(result);
|
||||||
|
|
||||||
|
result = system->init(32, FMOD_INIT_NORMAL, extradriverdata);
|
||||||
|
ERRCHECK(result);
|
||||||
|
|
||||||
|
result = system->getMasterChannelGroup(&mastergroup);
|
||||||
|
ERRCHECK(result);
|
||||||
|
|
||||||
|
result = system->createSound(Common_MediaPath("drumloop.wav"), FMOD_DEFAULT, 0, &sound);
|
||||||
|
ERRCHECK(result);
|
||||||
|
|
||||||
|
result = system->playSound(sound, 0, false, &channel);
|
||||||
|
ERRCHECK(result);
|
||||||
|
|
||||||
|
/*
|
||||||
|
Create some effects to play with
|
||||||
|
*/
|
||||||
|
result = system->createDSPByType(FMOD_DSP_TYPE_LOWPASS, &dsplowpass);
|
||||||
|
ERRCHECK(result);
|
||||||
|
result = system->createDSPByType(FMOD_DSP_TYPE_HIGHPASS, &dsphighpass);
|
||||||
|
ERRCHECK(result);
|
||||||
|
result = system->createDSPByType(FMOD_DSP_TYPE_ECHO, &dspecho);
|
||||||
|
ERRCHECK(result);
|
||||||
|
result = system->createDSPByType(FMOD_DSP_TYPE_FLANGE, &dspflange);
|
||||||
|
ERRCHECK(result);
|
||||||
|
|
||||||
|
/*
|
||||||
|
Add them to the master channel group. Each time an effect is added (to position 0) it pushes the others down the list.
|
||||||
|
*/
|
||||||
|
result = mastergroup->addDSP(0, dsplowpass);
|
||||||
|
ERRCHECK(result);
|
||||||
|
result = mastergroup->addDSP(0, dsphighpass);
|
||||||
|
ERRCHECK(result);
|
||||||
|
result = mastergroup->addDSP(0, dspecho);
|
||||||
|
ERRCHECK(result);
|
||||||
|
result = mastergroup->addDSP(0, dspflange);
|
||||||
|
ERRCHECK(result);
|
||||||
|
|
||||||
|
/*
|
||||||
|
By default, bypass all effects. This means let the original signal go through without processing.
|
||||||
|
It will sound 'dry' until effects are enabled by the user.
|
||||||
|
*/
|
||||||
|
result = dsplowpass->setBypass(true);
|
||||||
|
ERRCHECK(result);
|
||||||
|
result = dsphighpass->setBypass(true);
|
||||||
|
ERRCHECK(result);
|
||||||
|
result = dspecho->setBypass(true);
|
||||||
|
ERRCHECK(result);
|
||||||
|
result = dspflange->setBypass(true);
|
||||||
|
ERRCHECK(result);
|
||||||
|
|
||||||
|
/*
|
||||||
|
Main loop
|
||||||
|
*/
|
||||||
|
do
|
||||||
|
{
|
||||||
|
Common_Update();
|
||||||
|
|
||||||
|
if (Common_BtnPress(BTN_MORE))
|
||||||
|
{
|
||||||
|
bool paused;
|
||||||
|
|
||||||
|
result = channel->getPaused(&paused);
|
||||||
|
ERRCHECK(result);
|
||||||
|
|
||||||
|
paused = !paused;
|
||||||
|
|
||||||
|
result = channel->setPaused(paused);
|
||||||
|
ERRCHECK(result);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (Common_BtnPress(BTN_ACTION1))
|
||||||
|
{
|
||||||
|
bool bypass;
|
||||||
|
|
||||||
|
result = dsplowpass->getBypass(&bypass);
|
||||||
|
ERRCHECK(result);
|
||||||
|
|
||||||
|
bypass = !bypass;
|
||||||
|
|
||||||
|
result = dsplowpass->setBypass(bypass);
|
||||||
|
ERRCHECK(result);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (Common_BtnPress(BTN_ACTION2))
|
||||||
|
{
|
||||||
|
bool bypass;
|
||||||
|
|
||||||
|
result = dsphighpass->getBypass(&bypass);
|
||||||
|
ERRCHECK(result);
|
||||||
|
|
||||||
|
bypass = !bypass;
|
||||||
|
|
||||||
|
result = dsphighpass->setBypass(bypass);
|
||||||
|
ERRCHECK(result);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (Common_BtnPress(BTN_ACTION3))
|
||||||
|
{
|
||||||
|
bool bypass;
|
||||||
|
|
||||||
|
result = dspecho->getBypass(&bypass);
|
||||||
|
ERRCHECK(result);
|
||||||
|
|
||||||
|
bypass = !bypass;
|
||||||
|
|
||||||
|
result = dspecho->setBypass(bypass);
|
||||||
|
ERRCHECK(result);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (Common_BtnPress(BTN_ACTION4))
|
||||||
|
{
|
||||||
|
bool bypass;
|
||||||
|
|
||||||
|
result = dspflange->getBypass(&bypass);
|
||||||
|
ERRCHECK(result);
|
||||||
|
|
||||||
|
bypass = !bypass;
|
||||||
|
|
||||||
|
result = dspflange->setBypass(bypass);
|
||||||
|
ERRCHECK(result);
|
||||||
|
}
|
||||||
|
|
||||||
|
result = system->update();
|
||||||
|
ERRCHECK(result);
|
||||||
|
|
||||||
|
{
|
||||||
|
bool paused = 0;
|
||||||
|
bool dsplowpass_bypass;
|
||||||
|
bool dsphighpass_bypass;
|
||||||
|
bool dspecho_bypass;
|
||||||
|
bool dspflange_bypass;
|
||||||
|
|
||||||
|
dsplowpass ->getBypass(&dsplowpass_bypass);
|
||||||
|
dsphighpass ->getBypass(&dsphighpass_bypass);
|
||||||
|
dspecho ->getBypass(&dspecho_bypass);
|
||||||
|
dspflange ->getBypass(&dspflange_bypass);
|
||||||
|
|
||||||
|
if (channel)
|
||||||
|
{
|
||||||
|
result = channel->getPaused(&paused);
|
||||||
|
if ((result != FMOD_OK) && (result != FMOD_ERR_INVALID_HANDLE) && (result != FMOD_ERR_CHANNEL_STOLEN))
|
||||||
|
{
|
||||||
|
ERRCHECK(result);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Common_Draw("==================================================");
|
||||||
|
Common_Draw("Effects Example.");
|
||||||
|
Common_Draw("Copyright (c) Firelight Technologies 2004-2026.");
|
||||||
|
Common_Draw("==================================================");
|
||||||
|
Common_Draw("");
|
||||||
|
Common_Draw("Press %s to pause/unpause sound", Common_BtnStr(BTN_MORE));
|
||||||
|
Common_Draw("Press %s to toggle dsplowpass effect", Common_BtnStr(BTN_ACTION1));
|
||||||
|
Common_Draw("Press %s to toggle dsphighpass effect", Common_BtnStr(BTN_ACTION2));
|
||||||
|
Common_Draw("Press %s to toggle dspecho effect", Common_BtnStr(BTN_ACTION3));
|
||||||
|
Common_Draw("Press %s to toggle dspflange effect", Common_BtnStr(BTN_ACTION4));
|
||||||
|
Common_Draw("Press %s to quit", Common_BtnStr(BTN_QUIT));
|
||||||
|
Common_Draw("");
|
||||||
|
Common_Draw("%s : lowpass[%c] highpass[%c] echo[%c] flange[%c]",
|
||||||
|
paused ? "Paused " : "Playing",
|
||||||
|
dsplowpass_bypass ? ' ' : 'x',
|
||||||
|
dsphighpass_bypass ? ' ' : 'x',
|
||||||
|
dspecho_bypass ? ' ' : 'x',
|
||||||
|
dspflange_bypass ? ' ' : 'x');
|
||||||
|
}
|
||||||
|
|
||||||
|
Common_Sleep(50);
|
||||||
|
} while (!Common_BtnPress(BTN_QUIT));
|
||||||
|
|
||||||
|
/*
|
||||||
|
Shut down
|
||||||
|
*/
|
||||||
|
result = mastergroup->removeDSP(dsplowpass);
|
||||||
|
ERRCHECK(result);
|
||||||
|
result = mastergroup->removeDSP(dsphighpass);
|
||||||
|
ERRCHECK(result);
|
||||||
|
result = mastergroup->removeDSP(dspecho);
|
||||||
|
ERRCHECK(result);
|
||||||
|
result = mastergroup->removeDSP(dspflange);
|
||||||
|
ERRCHECK(result);
|
||||||
|
|
||||||
|
result = dsplowpass->release();
|
||||||
|
ERRCHECK(result);
|
||||||
|
result = dsphighpass->release();
|
||||||
|
ERRCHECK(result);
|
||||||
|
result = dspecho->release();
|
||||||
|
ERRCHECK(result);
|
||||||
|
result = dspflange->release();
|
||||||
|
ERRCHECK(result);
|
||||||
|
|
||||||
|
result = sound->release();
|
||||||
|
ERRCHECK(result);
|
||||||
|
result = system->close();
|
||||||
|
ERRCHECK(result);
|
||||||
|
result = system->release();
|
||||||
|
ERRCHECK(result);
|
||||||
|
|
||||||
|
Common_Close();
|
||||||
|
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
@@ -0,0 +1,269 @@
|
|||||||
|
/*==============================================================================
|
||||||
|
Gapless Playback Example
|
||||||
|
Copyright (c), Firelight Technologies Pty, Ltd 2004-2026.
|
||||||
|
|
||||||
|
This example shows how to schedule channel playback into the future with sample
|
||||||
|
accuracy. Use several scheduled channels to synchronize 2 or more sounds.
|
||||||
|
|
||||||
|
For information on using FMOD example code in your own programs, visit
|
||||||
|
https://www.fmod.com/legal
|
||||||
|
==============================================================================*/
|
||||||
|
#include "fmod.hpp"
|
||||||
|
#include "common.h"
|
||||||
|
|
||||||
|
enum NOTE
|
||||||
|
{
|
||||||
|
NOTE_C,
|
||||||
|
NOTE_D,
|
||||||
|
NOTE_E,
|
||||||
|
};
|
||||||
|
|
||||||
|
NOTE note[] =
|
||||||
|
{
|
||||||
|
NOTE_E, /* Ma- */
|
||||||
|
NOTE_D, /* ry */
|
||||||
|
NOTE_C, /* had */
|
||||||
|
NOTE_D, /* a */
|
||||||
|
NOTE_E, /* lit- */
|
||||||
|
NOTE_E, /* tle */
|
||||||
|
NOTE_E, /* lamb, */
|
||||||
|
NOTE_E, /* ..... */
|
||||||
|
NOTE_D, /* lit- */
|
||||||
|
NOTE_D, /* tle */
|
||||||
|
NOTE_D, /* lamb, */
|
||||||
|
NOTE_D, /* ..... */
|
||||||
|
NOTE_E, /* lit- */
|
||||||
|
NOTE_E, /* tle */
|
||||||
|
NOTE_E, /* lamb, */
|
||||||
|
NOTE_E, /* ..... */
|
||||||
|
|
||||||
|
NOTE_E, /* Ma- */
|
||||||
|
NOTE_D, /* ry */
|
||||||
|
NOTE_C, /* had */
|
||||||
|
NOTE_D, /* a */
|
||||||
|
NOTE_E, /* lit- */
|
||||||
|
NOTE_E, /* tle */
|
||||||
|
NOTE_E, /* lamb, */
|
||||||
|
NOTE_E, /* its */
|
||||||
|
NOTE_D, /* fleece */
|
||||||
|
NOTE_D, /* was */
|
||||||
|
NOTE_E, /* white */
|
||||||
|
NOTE_D, /* as */
|
||||||
|
NOTE_C, /* snow. */
|
||||||
|
NOTE_C, /* ..... */
|
||||||
|
NOTE_C, /* ..... */
|
||||||
|
NOTE_C, /* ..... */
|
||||||
|
};
|
||||||
|
|
||||||
|
int FMOD_Main()
|
||||||
|
{
|
||||||
|
FMOD::System *system;
|
||||||
|
FMOD::Sound *sound[3];
|
||||||
|
FMOD::Channel *channel = 0;
|
||||||
|
FMOD::ChannelGroup *channelgroup = 0;
|
||||||
|
FMOD_RESULT result;
|
||||||
|
unsigned int dsp_block_len, count;
|
||||||
|
int outputrate = 0;
|
||||||
|
void *extradriverdata = 0;
|
||||||
|
|
||||||
|
Common_Init(&extradriverdata);
|
||||||
|
|
||||||
|
/*
|
||||||
|
Create a System object and initialize.
|
||||||
|
*/
|
||||||
|
result = FMOD::System_Create(&system);
|
||||||
|
ERRCHECK(result);
|
||||||
|
|
||||||
|
result = system->init(100, FMOD_INIT_NORMAL, extradriverdata);
|
||||||
|
ERRCHECK(result);
|
||||||
|
|
||||||
|
/*
|
||||||
|
Get information needed later for scheduling. The mixer block size, and the output rate of the mixer.
|
||||||
|
*/
|
||||||
|
result = system->getDSPBufferSize(&dsp_block_len, 0);
|
||||||
|
ERRCHECK(result);
|
||||||
|
|
||||||
|
result = system->getSoftwareFormat(&outputrate, 0, 0);
|
||||||
|
ERRCHECK(result);
|
||||||
|
|
||||||
|
/*
|
||||||
|
Load 3 sounds - these are just sine wave tones at different frequencies. C, D and E on the musical scale.
|
||||||
|
*/
|
||||||
|
result = system->createSound(Common_MediaPath("c.ogg"), FMOD_DEFAULT, 0, &sound[NOTE_C]);
|
||||||
|
ERRCHECK(result);
|
||||||
|
result = system->createSound(Common_MediaPath("d.ogg"), FMOD_DEFAULT, 0, &sound[NOTE_D]);
|
||||||
|
ERRCHECK(result);
|
||||||
|
result = system->createSound(Common_MediaPath("e.ogg"), FMOD_DEFAULT, 0, &sound[NOTE_E]);
|
||||||
|
ERRCHECK(result);
|
||||||
|
|
||||||
|
/*
|
||||||
|
Create a channelgroup that the channels will play on. We can use this channelgroup as our clock reference.
|
||||||
|
It also means we can pause and pitch bend the channelgroup, without affecting the offsets of the delays, because the channelgroup clock
|
||||||
|
which the channels feed off, will be pausing and speeding up/slowing down and still keeping the children in sync.
|
||||||
|
*/
|
||||||
|
result = system->createChannelGroup("Parent", &channelgroup);
|
||||||
|
ERRCHECK(result);
|
||||||
|
|
||||||
|
unsigned int numsounds = sizeof(note) / sizeof(note[0]);
|
||||||
|
|
||||||
|
/*
|
||||||
|
Play all the sounds at once! Space them apart with set delay though so that they sound like they play in order.
|
||||||
|
*/
|
||||||
|
for (count = 0; count < numsounds; count++)
|
||||||
|
{
|
||||||
|
static unsigned long long clock_start = 0;
|
||||||
|
unsigned int slen;
|
||||||
|
FMOD::Sound *s = sound[note[count]]; /* Pick a note from our tune. */
|
||||||
|
|
||||||
|
result = system->playSound(s, channelgroup, true, &channel); /* Play the sound on the channelgroup we want to use as the parent clock reference (for setDelay further down) */
|
||||||
|
ERRCHECK(result);
|
||||||
|
|
||||||
|
if (!clock_start)
|
||||||
|
{
|
||||||
|
result = channel->getDSPClock(0, &clock_start);
|
||||||
|
ERRCHECK(result);
|
||||||
|
|
||||||
|
clock_start += (dsp_block_len * 2); /* Start the sound into the future, by 2 mixer blocks worth. */
|
||||||
|
/* Should be enough to avoid the mixer catching up and hitting the clock value before we've finished setting up everything. */
|
||||||
|
/* Alternatively the channelgroup we're basing the clock on could be paused to stop it ticking. */
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
float freq;
|
||||||
|
|
||||||
|
result = s->getLength(&slen, FMOD_TIMEUNIT_PCM); /* Get the length of the sound in samples. */
|
||||||
|
ERRCHECK(result);
|
||||||
|
|
||||||
|
result = s->getDefaults(&freq, 0); /* Get the default frequency that the sound was recorded at. */
|
||||||
|
ERRCHECK(result);
|
||||||
|
|
||||||
|
slen = (unsigned int)((float)slen / freq * outputrate); /* Convert the length of the sound to 'output samples' for the output timeline. */
|
||||||
|
|
||||||
|
clock_start += slen; /* Place the sound clock start time to this value after the last one. */
|
||||||
|
}
|
||||||
|
|
||||||
|
result = channel->setDelay(clock_start, 0, false); /* Schedule the channel to start in the future at the newly calculated channelgroup clock value. */
|
||||||
|
ERRCHECK(result);
|
||||||
|
|
||||||
|
result = channel->setPaused(false); /* Unpause the sound. Note that you won't hear the sounds, they are scheduled into the future. */
|
||||||
|
ERRCHECK(result);
|
||||||
|
}
|
||||||
|
|
||||||
|
/*
|
||||||
|
Main loop.
|
||||||
|
*/
|
||||||
|
do
|
||||||
|
{
|
||||||
|
Common_Update();
|
||||||
|
|
||||||
|
if (Common_BtnPress(BTN_ACTION1)) /* Pausing the channelgroup as the clock parent, will pause any scheduled sounds from continuing */
|
||||||
|
{ /* If you paused the channel, this would not stop the clock it is delayed against from ticking, */
|
||||||
|
bool paused; /* and you'd have to recalculate the delay for the channel into the future again before it was unpaused. */
|
||||||
|
result = channelgroup->getPaused(&paused);
|
||||||
|
ERRCHECK(result);
|
||||||
|
result = channelgroup->setPaused(!paused);
|
||||||
|
ERRCHECK(result);
|
||||||
|
}
|
||||||
|
if (Common_BtnPress(BTN_ACTION2))
|
||||||
|
{
|
||||||
|
for (count = 0; count < 50; count++)
|
||||||
|
{
|
||||||
|
float pitch;
|
||||||
|
result = channelgroup->getPitch(&pitch);
|
||||||
|
ERRCHECK(result);
|
||||||
|
pitch += 0.01f;
|
||||||
|
result = channelgroup->setPitch(pitch);
|
||||||
|
ERRCHECK(result);
|
||||||
|
|
||||||
|
result = system->update();
|
||||||
|
ERRCHECK(result);
|
||||||
|
|
||||||
|
Common_Sleep(10);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (Common_BtnPress(BTN_ACTION3))
|
||||||
|
{
|
||||||
|
for (count = 0; count < 50; count++)
|
||||||
|
{
|
||||||
|
float pitch;
|
||||||
|
result = channelgroup->getPitch(&pitch);
|
||||||
|
ERRCHECK(result);
|
||||||
|
|
||||||
|
if (pitch > 0.1f)
|
||||||
|
{
|
||||||
|
pitch -= 0.01f;
|
||||||
|
}
|
||||||
|
result = channelgroup->setPitch(pitch);
|
||||||
|
ERRCHECK(result);
|
||||||
|
|
||||||
|
result = system->update();
|
||||||
|
ERRCHECK(result);
|
||||||
|
|
||||||
|
Common_Sleep(10);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
result = system->update();
|
||||||
|
ERRCHECK(result);
|
||||||
|
|
||||||
|
/*
|
||||||
|
Print some information
|
||||||
|
*/
|
||||||
|
{
|
||||||
|
bool playing = false;
|
||||||
|
bool paused = false;
|
||||||
|
int chansplaying;
|
||||||
|
|
||||||
|
if (channelgroup)
|
||||||
|
{
|
||||||
|
result = channelgroup->isPlaying(&playing);
|
||||||
|
if ((result != FMOD_OK) && (result != FMOD_ERR_INVALID_HANDLE))
|
||||||
|
{
|
||||||
|
ERRCHECK(result);
|
||||||
|
}
|
||||||
|
|
||||||
|
result = channelgroup->getPaused(&paused);
|
||||||
|
if ((result != FMOD_OK) && (result != FMOD_ERR_INVALID_HANDLE))
|
||||||
|
{
|
||||||
|
ERRCHECK(result);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
result = system->getChannelsPlaying(&chansplaying, NULL);
|
||||||
|
ERRCHECK(result);
|
||||||
|
|
||||||
|
Common_Draw("==================================================");
|
||||||
|
Common_Draw("Gapless Playback example.");
|
||||||
|
Common_Draw("Copyright (c) Firelight Technologies 2004-2026.");
|
||||||
|
Common_Draw("==================================================");
|
||||||
|
Common_Draw("");
|
||||||
|
Common_Draw("Press %s to toggle pause", Common_BtnStr(BTN_ACTION1));
|
||||||
|
Common_Draw("Press %s to increase pitch", Common_BtnStr(BTN_ACTION2));
|
||||||
|
Common_Draw("Press %s to decrease pitch", Common_BtnStr(BTN_ACTION3));
|
||||||
|
Common_Draw("Press %s to quit", Common_BtnStr(BTN_QUIT));
|
||||||
|
Common_Draw("");
|
||||||
|
Common_Draw("Channels Playing %d : %s", chansplaying, paused ? "Paused " : playing ? "Playing" : "Stopped");
|
||||||
|
}
|
||||||
|
|
||||||
|
Common_Sleep(50);
|
||||||
|
} while (!Common_BtnPress(BTN_QUIT));
|
||||||
|
|
||||||
|
/*
|
||||||
|
Shut down
|
||||||
|
*/
|
||||||
|
result = sound[NOTE_C]->release();
|
||||||
|
ERRCHECK(result);
|
||||||
|
result = sound[NOTE_D]->release();
|
||||||
|
ERRCHECK(result);
|
||||||
|
result = sound[NOTE_E]->release();
|
||||||
|
ERRCHECK(result);
|
||||||
|
|
||||||
|
result = system->close();
|
||||||
|
ERRCHECK(result);
|
||||||
|
result = system->release();
|
||||||
|
ERRCHECK(result);
|
||||||
|
|
||||||
|
Common_Close();
|
||||||
|
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
@@ -0,0 +1,213 @@
|
|||||||
|
/*==============================================================================
|
||||||
|
Generate Tone Example
|
||||||
|
Copyright (c), Firelight Technologies Pty, Ltd 2004-2026.
|
||||||
|
|
||||||
|
This example shows how to play generated tones using System::playDSP
|
||||||
|
instead of manually connecting and disconnecting DSP units.
|
||||||
|
|
||||||
|
For information on using FMOD example code in your own programs, visit
|
||||||
|
https://www.fmod.com/legal
|
||||||
|
==============================================================================*/
|
||||||
|
#include "fmod.hpp"
|
||||||
|
#include "common.h"
|
||||||
|
|
||||||
|
int FMOD_Main()
|
||||||
|
{
|
||||||
|
FMOD::System *system;
|
||||||
|
FMOD::Channel *channel = 0;
|
||||||
|
FMOD::DSP *dsp;
|
||||||
|
FMOD_RESULT result;
|
||||||
|
void *extradriverdata = 0;
|
||||||
|
|
||||||
|
Common_Init(&extradriverdata);
|
||||||
|
|
||||||
|
/*
|
||||||
|
Create a System object and initialize.
|
||||||
|
*/
|
||||||
|
result = FMOD::System_Create(&system);
|
||||||
|
ERRCHECK(result);
|
||||||
|
|
||||||
|
result = system->init(32, FMOD_INIT_NORMAL, extradriverdata);
|
||||||
|
ERRCHECK(result);
|
||||||
|
|
||||||
|
/*
|
||||||
|
Create an oscillator DSP units for the tone.
|
||||||
|
*/
|
||||||
|
result = system->createDSPByType(FMOD_DSP_TYPE_OSCILLATOR, &dsp);
|
||||||
|
ERRCHECK(result);
|
||||||
|
result = dsp->setParameterFloat(FMOD_DSP_OSCILLATOR_RATE, 440.0f); /* Musical note 'A' */
|
||||||
|
ERRCHECK(result);
|
||||||
|
|
||||||
|
/*
|
||||||
|
Main loop
|
||||||
|
*/
|
||||||
|
do
|
||||||
|
{
|
||||||
|
Common_Update();
|
||||||
|
|
||||||
|
if (Common_BtnPress(BTN_ACTION1))
|
||||||
|
{
|
||||||
|
if (channel)
|
||||||
|
{
|
||||||
|
result = channel->stop();
|
||||||
|
ERRCHECK(result);
|
||||||
|
}
|
||||||
|
|
||||||
|
result = system->playDSP(dsp, 0, true, &channel);
|
||||||
|
ERRCHECK(result);
|
||||||
|
result = channel->setVolume(0.5f);
|
||||||
|
ERRCHECK(result);
|
||||||
|
result = dsp->setParameterInt(FMOD_DSP_OSCILLATOR_TYPE, 0);
|
||||||
|
ERRCHECK(result);
|
||||||
|
result = channel->setPaused(false);
|
||||||
|
ERRCHECK(result);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (Common_BtnPress(BTN_ACTION2))
|
||||||
|
{
|
||||||
|
if (channel)
|
||||||
|
{
|
||||||
|
result = channel->stop();
|
||||||
|
ERRCHECK(result);
|
||||||
|
}
|
||||||
|
|
||||||
|
result = system->playDSP(dsp, 0, true, &channel);
|
||||||
|
ERRCHECK(result);
|
||||||
|
result = channel->setVolume(0.125f);
|
||||||
|
ERRCHECK(result);
|
||||||
|
result = dsp->setParameterInt(FMOD_DSP_OSCILLATOR_TYPE, 1);
|
||||||
|
ERRCHECK(result);
|
||||||
|
result = channel->setPaused(false);
|
||||||
|
ERRCHECK(result);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (Common_BtnPress(BTN_ACTION3))
|
||||||
|
{
|
||||||
|
if (channel)
|
||||||
|
{
|
||||||
|
result = channel->stop();
|
||||||
|
ERRCHECK(result);
|
||||||
|
}
|
||||||
|
|
||||||
|
result = system->playDSP(dsp, 0, true, &channel);
|
||||||
|
ERRCHECK(result);
|
||||||
|
result = channel->setVolume(0.125f);
|
||||||
|
ERRCHECK(result);
|
||||||
|
result = dsp->setParameterInt(FMOD_DSP_OSCILLATOR_TYPE, 2);
|
||||||
|
ERRCHECK(result);
|
||||||
|
result = channel->setPaused(false);
|
||||||
|
ERRCHECK(result);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (Common_BtnPress(BTN_ACTION4))
|
||||||
|
{
|
||||||
|
if (channel)
|
||||||
|
{
|
||||||
|
result = channel->stop();
|
||||||
|
ERRCHECK(result);
|
||||||
|
}
|
||||||
|
|
||||||
|
result = system->playDSP(dsp, 0, true, &channel);
|
||||||
|
ERRCHECK(result);
|
||||||
|
result = channel->setVolume(0.5f);
|
||||||
|
ERRCHECK(result);
|
||||||
|
result = dsp->setParameterInt(FMOD_DSP_OSCILLATOR_TYPE, 4);
|
||||||
|
ERRCHECK(result);
|
||||||
|
result = channel->setPaused(false);
|
||||||
|
ERRCHECK(result);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (Common_BtnPress(BTN_MORE))
|
||||||
|
{
|
||||||
|
if (channel)
|
||||||
|
{
|
||||||
|
result = channel->stop();
|
||||||
|
ERRCHECK(result);
|
||||||
|
channel = 0;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (channel)
|
||||||
|
{
|
||||||
|
if (Common_BtnDown(BTN_UP) || Common_BtnDown(BTN_DOWN))
|
||||||
|
{
|
||||||
|
float volume;
|
||||||
|
|
||||||
|
result = channel->getVolume(&volume);
|
||||||
|
ERRCHECK(result);
|
||||||
|
|
||||||
|
volume += (Common_BtnDown(BTN_UP) ? +0.1f : -0.1f);
|
||||||
|
volume = (volume > 1.0f) ? 1.0f : volume;
|
||||||
|
volume = (volume < 0.0f) ? 0.0f : volume;
|
||||||
|
|
||||||
|
result = channel->setVolume(volume);
|
||||||
|
ERRCHECK(result);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (Common_BtnDown(BTN_LEFT) || Common_BtnDown(BTN_RIGHT))
|
||||||
|
{
|
||||||
|
float frequency;
|
||||||
|
|
||||||
|
result = channel->getFrequency(&frequency);
|
||||||
|
ERRCHECK(result);
|
||||||
|
|
||||||
|
frequency += (Common_BtnDown(BTN_RIGHT) ? +500.0f : -500.0f);
|
||||||
|
|
||||||
|
result = channel->setFrequency(frequency);
|
||||||
|
ERRCHECK(result);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
result = system->update();
|
||||||
|
ERRCHECK(result);
|
||||||
|
|
||||||
|
{
|
||||||
|
float frequency = 0.0f, volume = 0.0f;
|
||||||
|
bool playing = false;
|
||||||
|
|
||||||
|
if (channel)
|
||||||
|
{
|
||||||
|
result = channel->getFrequency(&frequency);
|
||||||
|
ERRCHECK(result);
|
||||||
|
result = channel->getVolume(&volume);
|
||||||
|
ERRCHECK(result);
|
||||||
|
result = channel->isPlaying(&playing);
|
||||||
|
ERRCHECK(result);
|
||||||
|
}
|
||||||
|
|
||||||
|
Common_Draw("==================================================");
|
||||||
|
Common_Draw("Generate Tone Example.");
|
||||||
|
Common_Draw("Copyright (c) Firelight Technologies 2004-2026.");
|
||||||
|
Common_Draw("==================================================");
|
||||||
|
Common_Draw("");
|
||||||
|
Common_Draw("Press %s to play a sine wave", Common_BtnStr(BTN_ACTION1));
|
||||||
|
Common_Draw("Press %s to play a square wave", Common_BtnStr(BTN_ACTION2));
|
||||||
|
Common_Draw("Press %s to play a saw wave", Common_BtnStr(BTN_ACTION3));
|
||||||
|
Common_Draw("Press %s to play a triangle wave", Common_BtnStr(BTN_ACTION4));
|
||||||
|
Common_Draw("Press %s to stop the channel", Common_BtnStr(BTN_MORE));
|
||||||
|
Common_Draw("Press %s and %s to change volume", Common_BtnStr(BTN_UP), Common_BtnStr(BTN_DOWN));
|
||||||
|
Common_Draw("Press %s and %s to change frequency", Common_BtnStr(BTN_LEFT), Common_BtnStr(BTN_RIGHT));
|
||||||
|
Common_Draw("Press %s to quit", Common_BtnStr(BTN_QUIT));
|
||||||
|
Common_Draw("");
|
||||||
|
Common_Draw("Channel is %s", playing ? "playing" : "stopped");
|
||||||
|
Common_Draw("Volume %0.2f", volume);
|
||||||
|
Common_Draw("Frequency %0.2f", frequency);
|
||||||
|
}
|
||||||
|
|
||||||
|
Common_Sleep(50);
|
||||||
|
} while (!Common_BtnPress(BTN_QUIT));
|
||||||
|
|
||||||
|
/*
|
||||||
|
Shut down
|
||||||
|
*/
|
||||||
|
result = dsp->release();
|
||||||
|
ERRCHECK(result);
|
||||||
|
result = system->close();
|
||||||
|
ERRCHECK(result);
|
||||||
|
result = system->release();
|
||||||
|
ERRCHECK(result);
|
||||||
|
|
||||||
|
Common_Close();
|
||||||
|
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
@@ -0,0 +1,278 @@
|
|||||||
|
/*==============================================================================
|
||||||
|
Granular Synthesis Example
|
||||||
|
Copyright (c), Firelight Technologies Pty, Ltd 2004-2026.
|
||||||
|
|
||||||
|
This example shows how you can play a string of sounds together without gaps,
|
||||||
|
using the setDelay command, to produce a granular synthesis style truck engine
|
||||||
|
effect.
|
||||||
|
|
||||||
|
The basic operation is:
|
||||||
|
|
||||||
|
* Play 2 sounds initially at the same time, the first sound immediately, and
|
||||||
|
the 2nd sound with a delay calculated by the length of the first sound.
|
||||||
|
* Call setDelay to initiate the delayed playback. setDelay is sample accurate
|
||||||
|
and uses -output- samples as the time frame, not source samples. These
|
||||||
|
samples are a fixed amount per second regardless of the source sound format,
|
||||||
|
for example, 48000 samples per second if FMOD is initialized to 48khz output.
|
||||||
|
* Output samples are calculated from source samples with a simple
|
||||||
|
source->output sample rate conversion. i.e.
|
||||||
|
sound_length *= output_rate
|
||||||
|
sound_length /= sound_frequency
|
||||||
|
* When the first sound finishes, the second one should have automatically
|
||||||
|
started. This is a good oppurtunity to queue up the next sound. Repeat
|
||||||
|
step 2.
|
||||||
|
* Make sure the framerate is high enough to queue up a new sound before the
|
||||||
|
other one finishes otherwise you will get gaps.
|
||||||
|
|
||||||
|
These sounds are not limited by format, channel count or bit depth like the
|
||||||
|
realtimestitching example is, and can also be modified to allow for overlap,
|
||||||
|
by reducing the delay from the first sound playing to the second by the overlap
|
||||||
|
amount.
|
||||||
|
|
||||||
|
#define USE_STREAMS = Use 2 stream instances, created while they play.
|
||||||
|
#define USE_STREAMS = Use 6 static wavs, all loaded into memory.
|
||||||
|
|
||||||
|
For information on using FMOD example code in your own programs, visit
|
||||||
|
https://www.fmod.com/legal
|
||||||
|
==============================================================================*/
|
||||||
|
#include "fmod.hpp"
|
||||||
|
#include "common.h"
|
||||||
|
|
||||||
|
//#define USE_STREAMS
|
||||||
|
|
||||||
|
FMOD::System *gSystem;
|
||||||
|
|
||||||
|
#ifdef USE_STREAMS
|
||||||
|
#define NUMSOUNDS 3 /* Use some longer sounds, free and load them on the fly. */
|
||||||
|
FMOD::Sound *sound[2] = { 0, 0 }; /* 2 streams active, double buffer them. */
|
||||||
|
const char *soundname[NUMSOUNDS] = { Common_MediaPath("c.ogg"),
|
||||||
|
Common_MediaPath("d.ogg"),
|
||||||
|
Common_MediaPath("e.ogg") };
|
||||||
|
#else
|
||||||
|
#define NUMSOUNDS 6 /* These sounds will be loaded into memory statically. */
|
||||||
|
FMOD::Sound *sound[NUMSOUNDS] = { 0, 0, 0, 0, 0, 0 }; /* 6 sounds active, one for each wav. */
|
||||||
|
const char *soundname[NUMSOUNDS] = { Common_MediaPath("granular/truck_idle_off_01.wav"),
|
||||||
|
Common_MediaPath("granular/truck_idle_off_02.wav"),
|
||||||
|
Common_MediaPath("granular/truck_idle_off_03.wav"),
|
||||||
|
Common_MediaPath("granular/truck_idle_off_04.wav"),
|
||||||
|
Common_MediaPath("granular/truck_idle_off_05.wav"),
|
||||||
|
Common_MediaPath("granular/truck_idle_off_06.wav") };
|
||||||
|
#endif
|
||||||
|
|
||||||
|
FMOD::Channel *queue_next_sound(int outputrate, FMOD::Channel *playingchannel, int newindex, int slot)
|
||||||
|
{
|
||||||
|
FMOD_RESULT result;
|
||||||
|
FMOD::Channel *newchannel;
|
||||||
|
FMOD::Sound *newsound;
|
||||||
|
|
||||||
|
#ifdef USE_STREAMS /* Create a new stream */
|
||||||
|
FMOD_CREATESOUNDEXINFO info;
|
||||||
|
memset(&info, 0, sizeof(FMOD_CREATESOUNDEXINFO));
|
||||||
|
info.cbsize = sizeof(FMOD_CREATESOUNDEXINFO);
|
||||||
|
info.suggestedsoundtype = FMOD_SOUND_TYPE_OGGVORBIS;
|
||||||
|
result = gSystem->createStream(soundname[newindex], FMOD_IGNORETAGS | FMOD_LOWMEM, &info, &sound[slot]);
|
||||||
|
ERRCHECK(result);
|
||||||
|
newsound = sound[slot];
|
||||||
|
#else /* Use an existing sound that was passed into us */
|
||||||
|
(void)slot;
|
||||||
|
newsound = sound[newindex];
|
||||||
|
#endif
|
||||||
|
|
||||||
|
result = gSystem->playSound(newsound, 0, true, &newchannel);
|
||||||
|
ERRCHECK(result);
|
||||||
|
|
||||||
|
if (playingchannel)
|
||||||
|
{
|
||||||
|
unsigned long long startdelay = 0;
|
||||||
|
unsigned int soundlength = 0;
|
||||||
|
float soundfrequency;
|
||||||
|
FMOD::Sound *playingsound;
|
||||||
|
|
||||||
|
/*
|
||||||
|
Get the start time of the playing channel.
|
||||||
|
*/
|
||||||
|
result = playingchannel->getDelay(&startdelay, 0);
|
||||||
|
ERRCHECK(result);
|
||||||
|
|
||||||
|
/*
|
||||||
|
Grab the length of the playing sound, and its frequency, so we can caluate where to place the new sound on the time line.
|
||||||
|
*/
|
||||||
|
result = playingchannel->getCurrentSound(&playingsound);
|
||||||
|
ERRCHECK(result);
|
||||||
|
result = playingsound->getLength(&soundlength, FMOD_TIMEUNIT_PCM);
|
||||||
|
ERRCHECK(result);
|
||||||
|
result = playingchannel->getFrequency(&soundfrequency);
|
||||||
|
ERRCHECK(result);
|
||||||
|
|
||||||
|
/*
|
||||||
|
Now calculate the length of the sound in 'output samples'.
|
||||||
|
Ie if a 44khz sound is 22050 samples long, and the output rate is 48khz, then we want to delay by 24000 output samples.
|
||||||
|
*/
|
||||||
|
soundlength *= outputrate;
|
||||||
|
soundlength /= (int)soundfrequency;
|
||||||
|
|
||||||
|
startdelay += soundlength; /* Add output rate adjusted sound length, to the clock value of the sound that is currently playing */
|
||||||
|
|
||||||
|
result = newchannel->setDelay(startdelay, 0); /* Set the delay of the new sound to the end of the old sound */
|
||||||
|
ERRCHECK(result);
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
unsigned int bufferlength;
|
||||||
|
unsigned long long startdelay;
|
||||||
|
|
||||||
|
result = gSystem->getDSPBufferSize(&bufferlength, 0);
|
||||||
|
ERRCHECK(result);
|
||||||
|
|
||||||
|
result = newchannel->getDSPClock(0, &startdelay);
|
||||||
|
ERRCHECK(result);
|
||||||
|
|
||||||
|
startdelay += (2 * bufferlength);
|
||||||
|
result = newchannel->setDelay(startdelay, 0);
|
||||||
|
ERRCHECK(result);
|
||||||
|
}
|
||||||
|
|
||||||
|
{
|
||||||
|
float val, variation;
|
||||||
|
|
||||||
|
/*
|
||||||
|
Randomize pitch/volume to make it sound more realistic / random.
|
||||||
|
*/
|
||||||
|
result = newchannel->getFrequency(&val);
|
||||||
|
ERRCHECK(result);
|
||||||
|
variation = (((float)(rand()%10000) / 5000.0f) - 1.0f); /* -1.0 to +1.0 */
|
||||||
|
val *= (1.0f + (variation * 0.02f)); /* @22khz, range fluctuates from 21509 to 22491 */
|
||||||
|
result = newchannel->setFrequency(val);
|
||||||
|
ERRCHECK(result);
|
||||||
|
|
||||||
|
result = newchannel->getVolume(&val);
|
||||||
|
ERRCHECK(result);
|
||||||
|
variation = ((float)(rand()%10000) / 10000.0f); /* 0.0 to 1.0 */
|
||||||
|
val *= (1.0f - (variation * 0.2f)); /* 0.8 to 1.0 */
|
||||||
|
result = newchannel->setVolume(val);
|
||||||
|
ERRCHECK(result);
|
||||||
|
}
|
||||||
|
|
||||||
|
result = newchannel->setPaused(false);
|
||||||
|
ERRCHECK(result);
|
||||||
|
|
||||||
|
return newchannel;
|
||||||
|
}
|
||||||
|
|
||||||
|
int FMOD_Main()
|
||||||
|
{
|
||||||
|
FMOD::Channel *channel[2] = { 0,0 };
|
||||||
|
FMOD_RESULT result;
|
||||||
|
int outputrate, slot = 0;
|
||||||
|
void *extradriverdata = 0;
|
||||||
|
bool paused = false;
|
||||||
|
|
||||||
|
Common_Init(&extradriverdata);
|
||||||
|
|
||||||
|
result = FMOD::System_Create(&gSystem);
|
||||||
|
ERRCHECK(result);
|
||||||
|
|
||||||
|
result = gSystem->init(100, FMOD_INIT_NORMAL, extradriverdata);
|
||||||
|
ERRCHECK(result);
|
||||||
|
|
||||||
|
result = gSystem->getSoftwareFormat(&outputrate, 0, 0);
|
||||||
|
ERRCHECK(result);
|
||||||
|
|
||||||
|
#if !defined(USE_STREAMS)
|
||||||
|
for (unsigned int count = 0; count < NUMSOUNDS; count++)
|
||||||
|
{
|
||||||
|
result = gSystem->createSound(soundname[count], FMOD_IGNORETAGS, 0, &sound[count]);
|
||||||
|
ERRCHECK(result);
|
||||||
|
}
|
||||||
|
#endif
|
||||||
|
|
||||||
|
/*
|
||||||
|
Kick off the first 2 sounds. First one is immediate, second one will be triggered to start after the first one.
|
||||||
|
*/
|
||||||
|
channel[slot] = queue_next_sound(outputrate, channel[1-slot], rand()%NUMSOUNDS, slot);
|
||||||
|
slot = 1-slot; /* flip */
|
||||||
|
channel[slot] = queue_next_sound(outputrate, channel[1-slot], rand()%NUMSOUNDS, slot);
|
||||||
|
slot = 1-slot; /* flip */
|
||||||
|
|
||||||
|
do
|
||||||
|
{
|
||||||
|
bool isplaying = false;
|
||||||
|
|
||||||
|
Common_Update();
|
||||||
|
|
||||||
|
if (Common_BtnPress(BTN_ACTION1))
|
||||||
|
{
|
||||||
|
FMOD::ChannelGroup *mastergroup;
|
||||||
|
|
||||||
|
paused = !paused;
|
||||||
|
|
||||||
|
result = gSystem->getMasterChannelGroup(&mastergroup);
|
||||||
|
ERRCHECK(result);
|
||||||
|
result = mastergroup->setPaused(paused);
|
||||||
|
ERRCHECK(result);
|
||||||
|
}
|
||||||
|
|
||||||
|
result = gSystem->update();
|
||||||
|
ERRCHECK(result);
|
||||||
|
|
||||||
|
/*
|
||||||
|
Replace the sound that just finished with a new sound, to create endless seamless stitching!
|
||||||
|
*/
|
||||||
|
result = channel[slot]->isPlaying(&isplaying);
|
||||||
|
if (result != FMOD_ERR_INVALID_HANDLE)
|
||||||
|
{
|
||||||
|
ERRCHECK(result);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!isplaying && !paused)
|
||||||
|
{
|
||||||
|
#ifdef USE_STREAMS
|
||||||
|
/*
|
||||||
|
Release the sound that isn't playing any more.
|
||||||
|
*/
|
||||||
|
result = sound[slot]->release();
|
||||||
|
ERRCHECK(result);
|
||||||
|
sound[slot] = 0;
|
||||||
|
#endif
|
||||||
|
|
||||||
|
/*
|
||||||
|
Replace sound that just ended with a new sound, queued up to trigger exactly after the other sound ends.
|
||||||
|
*/
|
||||||
|
channel[slot] = queue_next_sound(outputrate, channel[1-slot], rand()%NUMSOUNDS, slot);
|
||||||
|
slot = 1-slot; /* flip */
|
||||||
|
}
|
||||||
|
|
||||||
|
Common_Draw("==================================================");
|
||||||
|
Common_Draw("Granular Synthesis SetDelay Example.");
|
||||||
|
Common_Draw("Copyright (c) Firelight Technologies 2004-2026.");
|
||||||
|
Common_Draw("==================================================");
|
||||||
|
Common_Draw("");
|
||||||
|
Common_Draw("Toggle #define USE_STREAM on/off in code to switch between streams and static samples.");
|
||||||
|
Common_Draw("");
|
||||||
|
Common_Draw("Press %s to pause", Common_BtnStr(BTN_ACTION1));
|
||||||
|
Common_Draw("Press %s to quit", Common_BtnStr(BTN_QUIT));
|
||||||
|
Common_Draw("");
|
||||||
|
Common_Draw("Channels are %s", paused ? "paused" : "playing");
|
||||||
|
|
||||||
|
Common_Sleep(10); /* If you wait too long, ie longer than the length of the shortest sound, you will get gaps. */
|
||||||
|
} while (!Common_BtnPress(BTN_QUIT));
|
||||||
|
|
||||||
|
/*
|
||||||
|
Shut down
|
||||||
|
*/
|
||||||
|
for (unsigned int count = 0; count < sizeof(sound) / sizeof(sound[0]); count++)
|
||||||
|
{
|
||||||
|
if (sound[count])
|
||||||
|
{
|
||||||
|
result = sound[count]->release();
|
||||||
|
ERRCHECK(result);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
result = gSystem->release();
|
||||||
|
ERRCHECK(result);
|
||||||
|
|
||||||
|
Common_Close();
|
||||||
|
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
@@ -0,0 +1,168 @@
|
|||||||
|
/*==============================================================================
|
||||||
|
Load From Memory Example
|
||||||
|
Copyright (c), Firelight Technologies Pty, Ltd 2004-2026.
|
||||||
|
|
||||||
|
This example is simply a variant of the [Play Sound Example](play_sound.html),
|
||||||
|
but it loads the data into memory then uses the 'load from memory' feature of
|
||||||
|
System::createSound.
|
||||||
|
|
||||||
|
For information on using FMOD example code in your own programs, visit
|
||||||
|
https://www.fmod.com/legal
|
||||||
|
==============================================================================*/
|
||||||
|
#include "fmod.hpp"
|
||||||
|
#include "common.h"
|
||||||
|
|
||||||
|
int FMOD_Main()
|
||||||
|
{
|
||||||
|
FMOD::System *system;
|
||||||
|
FMOD::Sound *sound1, *sound2, *sound3;
|
||||||
|
FMOD::Channel *channel = 0;
|
||||||
|
FMOD_RESULT result;
|
||||||
|
void *extradriverdata = 0;
|
||||||
|
void *buff = 0;
|
||||||
|
int length = 0;
|
||||||
|
FMOD_CREATESOUNDEXINFO exinfo;
|
||||||
|
|
||||||
|
Common_Init(&extradriverdata);
|
||||||
|
|
||||||
|
/*
|
||||||
|
Create a System object and initialize
|
||||||
|
*/
|
||||||
|
result = FMOD::System_Create(&system);
|
||||||
|
ERRCHECK(result);
|
||||||
|
|
||||||
|
result = system->init(32, FMOD_INIT_NORMAL, extradriverdata);
|
||||||
|
ERRCHECK(result);
|
||||||
|
|
||||||
|
Common_LoadFileMemory(Common_MediaPath("drumloop.wav"), &buff, &length);
|
||||||
|
memset(&exinfo, 0, sizeof(FMOD_CREATESOUNDEXINFO));
|
||||||
|
exinfo.cbsize = sizeof(FMOD_CREATESOUNDEXINFO);
|
||||||
|
exinfo.length = length;
|
||||||
|
|
||||||
|
result = system->createSound((const char *)buff, FMOD_OPENMEMORY | FMOD_LOOP_OFF, &exinfo, &sound1);
|
||||||
|
ERRCHECK(result);
|
||||||
|
Common_UnloadFileMemory(buff); // don't need the original memory any more. Note! If loading as a stream, the memory must stay active so do not free it!
|
||||||
|
|
||||||
|
Common_LoadFileMemory(Common_MediaPath("jaguar.wav"), &buff, &length);
|
||||||
|
memset(&exinfo, 0, sizeof(FMOD_CREATESOUNDEXINFO));
|
||||||
|
exinfo.cbsize = sizeof(FMOD_CREATESOUNDEXINFO);
|
||||||
|
exinfo.length = length;
|
||||||
|
|
||||||
|
result = system->createSound((const char *)buff, FMOD_OPENMEMORY, &exinfo, &sound2);
|
||||||
|
ERRCHECK(result);
|
||||||
|
Common_UnloadFileMemory(buff); // don't need the original memory any more. Note! If loading as a stream, the memory must stay active so do not free it!
|
||||||
|
|
||||||
|
Common_LoadFileMemory(Common_MediaPath("swish.wav"), &buff, &length);
|
||||||
|
memset(&exinfo, 0, sizeof(FMOD_CREATESOUNDEXINFO));
|
||||||
|
exinfo.cbsize = sizeof(FMOD_CREATESOUNDEXINFO);
|
||||||
|
exinfo.length = length;
|
||||||
|
|
||||||
|
result = system->createSound((const char *)buff, FMOD_OPENMEMORY, &exinfo, &sound3);
|
||||||
|
ERRCHECK(result);
|
||||||
|
Common_UnloadFileMemory(buff); // don't need the original memory any more. Note! If loading as a stream, the memory must stay active so do not free it!
|
||||||
|
|
||||||
|
/*
|
||||||
|
Main loop
|
||||||
|
*/
|
||||||
|
do
|
||||||
|
{
|
||||||
|
Common_Update();
|
||||||
|
|
||||||
|
if (Common_BtnPress(BTN_ACTION1))
|
||||||
|
{
|
||||||
|
result = system->playSound(sound1, 0, false, &channel);
|
||||||
|
ERRCHECK(result);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (Common_BtnPress(BTN_ACTION2))
|
||||||
|
{
|
||||||
|
result = system->playSound(sound2, 0, false, &channel);
|
||||||
|
ERRCHECK(result);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (Common_BtnPress(BTN_ACTION3))
|
||||||
|
{
|
||||||
|
result = system->playSound(sound3, 0, false, &channel);
|
||||||
|
ERRCHECK(result);
|
||||||
|
}
|
||||||
|
|
||||||
|
result = system->update();
|
||||||
|
ERRCHECK(result);
|
||||||
|
|
||||||
|
{
|
||||||
|
unsigned int ms = 0;
|
||||||
|
unsigned int lenms = 0;
|
||||||
|
bool playing = 0;
|
||||||
|
bool paused = 0;
|
||||||
|
int channelsplaying = 0;
|
||||||
|
|
||||||
|
if (channel)
|
||||||
|
{
|
||||||
|
FMOD::Sound *currentsound = 0;
|
||||||
|
|
||||||
|
result = channel->isPlaying(&playing);
|
||||||
|
if ((result != FMOD_OK) && (result != FMOD_ERR_INVALID_HANDLE) && (result != FMOD_ERR_CHANNEL_STOLEN))
|
||||||
|
{
|
||||||
|
ERRCHECK(result);
|
||||||
|
}
|
||||||
|
|
||||||
|
result = channel->getPaused(&paused);
|
||||||
|
if ((result != FMOD_OK) && (result != FMOD_ERR_INVALID_HANDLE) && (result != FMOD_ERR_CHANNEL_STOLEN))
|
||||||
|
{
|
||||||
|
ERRCHECK(result);
|
||||||
|
}
|
||||||
|
|
||||||
|
result = channel->getPosition(&ms, FMOD_TIMEUNIT_MS);
|
||||||
|
if ((result != FMOD_OK) && (result != FMOD_ERR_INVALID_HANDLE) && (result != FMOD_ERR_CHANNEL_STOLEN))
|
||||||
|
{
|
||||||
|
ERRCHECK(result);
|
||||||
|
}
|
||||||
|
|
||||||
|
channel->getCurrentSound(¤tsound);
|
||||||
|
if (currentsound)
|
||||||
|
{
|
||||||
|
result = currentsound->getLength(&lenms, FMOD_TIMEUNIT_MS);
|
||||||
|
if ((result != FMOD_OK) && (result != FMOD_ERR_INVALID_HANDLE) && (result != FMOD_ERR_CHANNEL_STOLEN))
|
||||||
|
{
|
||||||
|
ERRCHECK(result);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
system->getChannelsPlaying(&channelsplaying, NULL);
|
||||||
|
|
||||||
|
Common_Draw("==================================================");
|
||||||
|
Common_Draw("Load From Memory Example.");
|
||||||
|
Common_Draw("Copyright (c) Firelight Technologies 2004-2026.");
|
||||||
|
Common_Draw("==================================================");
|
||||||
|
Common_Draw("");
|
||||||
|
Common_Draw("Press %s to play a mono sound (drumloop)", Common_BtnStr(BTN_ACTION1));
|
||||||
|
Common_Draw("Press %s to play a mono sound (jaguar)", Common_BtnStr(BTN_ACTION2));
|
||||||
|
Common_Draw("Press %s to play a stereo sound (swish)", Common_BtnStr(BTN_ACTION3));
|
||||||
|
Common_Draw("Press %s to quit", Common_BtnStr(BTN_QUIT));
|
||||||
|
Common_Draw("");
|
||||||
|
Common_Draw("Time %02d:%02d:%02d/%02d:%02d:%02d : %s", ms / 1000 / 60, ms / 1000 % 60, ms / 10 % 100, lenms / 1000 / 60, lenms / 1000 % 60, lenms / 10 % 100, paused ? "Paused " : playing ? "Playing" : "Stopped");
|
||||||
|
Common_Draw("Channels Playing %2d", channelsplaying);
|
||||||
|
}
|
||||||
|
|
||||||
|
Common_Sleep(50);
|
||||||
|
} while (!Common_BtnPress(BTN_QUIT));
|
||||||
|
|
||||||
|
/*
|
||||||
|
Shut down
|
||||||
|
*/
|
||||||
|
result = sound1->release();
|
||||||
|
ERRCHECK(result);
|
||||||
|
result = sound2->release();
|
||||||
|
ERRCHECK(result);
|
||||||
|
result = sound3->release();
|
||||||
|
ERRCHECK(result);
|
||||||
|
result = system->close();
|
||||||
|
ERRCHECK(result);
|
||||||
|
result = system->release();
|
||||||
|
ERRCHECK(result);
|
||||||
|
|
||||||
|
Common_Close();
|
||||||
|
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
@@ -0,0 +1,292 @@
|
|||||||
|
/*==============================================================================
|
||||||
|
Multiple Speaker Example
|
||||||
|
Copyright (c), Firelight Technologies Pty, Ltd 2004-2026.
|
||||||
|
|
||||||
|
This example shows how to play sounds in multiple speakers, and also how to even
|
||||||
|
assign sound subchannels, such as those in a stereo sound to different
|
||||||
|
individual speakers.
|
||||||
|
|
||||||
|
For information on using FMOD example code in your own programs, visit
|
||||||
|
https://www.fmod.com/legal
|
||||||
|
==============================================================================*/
|
||||||
|
#include "fmod.hpp"
|
||||||
|
#include "common.h"
|
||||||
|
|
||||||
|
const char *SPEAKERMODE_STRING[] = { "default", "raw", "mono", "stereo", "quad", "surround", "5.1", "7.1" };
|
||||||
|
const char *SELECTION_STRING[] = { "Mono from front left speaker",
|
||||||
|
"Mono from front right speaker",
|
||||||
|
"Mono from center speaker",
|
||||||
|
"Mono from surround left speaker",
|
||||||
|
"Mono from surround right speaker",
|
||||||
|
"Mono from rear left speaker",
|
||||||
|
"Mono from rear right speaker",
|
||||||
|
"Stereo from front speakers",
|
||||||
|
"Stereo from front speakers (channel swapped)",
|
||||||
|
"Stereo (right only) from center speaker" };
|
||||||
|
const unsigned int SELECTION_COUNT = sizeof(SELECTION_STRING) / sizeof(char *);
|
||||||
|
|
||||||
|
bool isSelectionAvailable(FMOD_SPEAKERMODE mode, unsigned int selection)
|
||||||
|
{
|
||||||
|
if (mode == FMOD_SPEAKERMODE_MONO || mode == FMOD_SPEAKERMODE_STEREO)
|
||||||
|
{
|
||||||
|
if (selection == 2 || selection == 3 || selection == 4 || selection == 5 || selection == 6 || selection == 9) return false;
|
||||||
|
}
|
||||||
|
else if (mode == FMOD_SPEAKERMODE_QUAD)
|
||||||
|
{
|
||||||
|
if (selection == 2 || selection == 5 || selection == 6 || selection == 9) return false;
|
||||||
|
}
|
||||||
|
else if (mode == FMOD_SPEAKERMODE_SURROUND || mode == FMOD_SPEAKERMODE_5POINT1)
|
||||||
|
{
|
||||||
|
if (selection == 5 || selection == 6) return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
int FMOD_Main()
|
||||||
|
{
|
||||||
|
FMOD::System *system;
|
||||||
|
FMOD::Sound *sound1, *sound2;
|
||||||
|
FMOD::Channel *channel = 0;
|
||||||
|
FMOD_RESULT result;
|
||||||
|
int selection = 0;
|
||||||
|
void *extradriverdata = 0;
|
||||||
|
FMOD_SPEAKERMODE speakermode = FMOD_SPEAKERMODE_STEREO;
|
||||||
|
|
||||||
|
Common_Init(&extradriverdata);
|
||||||
|
|
||||||
|
/*
|
||||||
|
Create a System object and initialize.
|
||||||
|
*/
|
||||||
|
result = FMOD::System_Create(&system);
|
||||||
|
ERRCHECK(result);
|
||||||
|
|
||||||
|
result = system->init(32, FMOD_INIT_NORMAL, extradriverdata);
|
||||||
|
ERRCHECK(result);
|
||||||
|
|
||||||
|
result = system->getSoftwareFormat(0, &speakermode, 0);
|
||||||
|
ERRCHECK(result);
|
||||||
|
|
||||||
|
result = system->createSound(Common_MediaPath("drumloop.wav"), FMOD_2D | FMOD_LOOP_OFF, 0, &sound1);
|
||||||
|
ERRCHECK(result);
|
||||||
|
|
||||||
|
result = system->createSound(Common_MediaPath("stereo.ogg"), FMOD_2D | FMOD_LOOP_OFF, 0, &sound2);
|
||||||
|
ERRCHECK(result);
|
||||||
|
|
||||||
|
/*
|
||||||
|
Main loop.
|
||||||
|
*/
|
||||||
|
do
|
||||||
|
{
|
||||||
|
Common_Update();
|
||||||
|
|
||||||
|
if (Common_BtnPress(BTN_UP) && (selection != 0))
|
||||||
|
{
|
||||||
|
selection--;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (Common_BtnPress(BTN_DOWN) && (selection != (SELECTION_COUNT - 1)))
|
||||||
|
{
|
||||||
|
selection++;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (Common_BtnPress(BTN_ACTION1) && isSelectionAvailable(speakermode, selection))
|
||||||
|
{
|
||||||
|
if (selection == 0) /* Mono front left */
|
||||||
|
{
|
||||||
|
result = system->playSound(sound1, 0, true, &channel);
|
||||||
|
ERRCHECK(result);
|
||||||
|
|
||||||
|
result = channel->setMixLevelsOutput(1.0f, 0, 0, 0, 0, 0, 0, 0);
|
||||||
|
ERRCHECK(result);
|
||||||
|
|
||||||
|
result = channel->setPaused(false);
|
||||||
|
ERRCHECK(result);
|
||||||
|
}
|
||||||
|
else if (selection == 1) /* Mono front right */
|
||||||
|
{
|
||||||
|
result = system->playSound(sound1, 0, true, &channel);
|
||||||
|
ERRCHECK(result);
|
||||||
|
|
||||||
|
result = channel->setMixLevelsOutput(0, 1.0f, 0, 0, 0, 0, 0, 0);
|
||||||
|
ERRCHECK(result);
|
||||||
|
|
||||||
|
result = channel->setPaused(false);
|
||||||
|
ERRCHECK(result);
|
||||||
|
}
|
||||||
|
else if (selection == 2) /* Mono center */
|
||||||
|
{
|
||||||
|
result = system->playSound(sound1, 0, true, &channel);
|
||||||
|
ERRCHECK(result);
|
||||||
|
|
||||||
|
result = channel->setMixLevelsOutput(0, 0, 1.0f, 0, 0, 0, 0, 0);
|
||||||
|
ERRCHECK(result);
|
||||||
|
|
||||||
|
result = channel->setPaused(false);
|
||||||
|
ERRCHECK(result);
|
||||||
|
}
|
||||||
|
else if (selection == 3) /* Mono surround left */
|
||||||
|
{
|
||||||
|
result = system->playSound(sound1, 0, true, &channel);
|
||||||
|
ERRCHECK(result);
|
||||||
|
|
||||||
|
result = channel->setMixLevelsOutput(0, 0, 0, 0, 1.0f, 0, 0, 0);
|
||||||
|
ERRCHECK(result);
|
||||||
|
|
||||||
|
result = channel->setPaused(false);
|
||||||
|
ERRCHECK(result);
|
||||||
|
}
|
||||||
|
else if (selection == 4) /* Mono surround right */
|
||||||
|
{
|
||||||
|
result = system->playSound(sound1, 0, true, &channel);
|
||||||
|
ERRCHECK(result);
|
||||||
|
|
||||||
|
result = channel->setMixLevelsOutput(0, 0, 0, 0, 0, 1.0f, 0, 0);
|
||||||
|
ERRCHECK(result);
|
||||||
|
|
||||||
|
result = channel->setPaused(false);
|
||||||
|
ERRCHECK(result);
|
||||||
|
}
|
||||||
|
else if (selection == 5) /* Mono rear left */
|
||||||
|
{
|
||||||
|
result = system->playSound(sound1, 0, true, &channel);
|
||||||
|
ERRCHECK(result);
|
||||||
|
|
||||||
|
result = channel->setMixLevelsOutput(0, 0, 0, 0, 0, 0, 1.0f, 0);
|
||||||
|
ERRCHECK(result);
|
||||||
|
|
||||||
|
result = channel->setPaused(false);
|
||||||
|
ERRCHECK(result);
|
||||||
|
}
|
||||||
|
else if (selection == 6) /* Mono rear right */
|
||||||
|
{
|
||||||
|
result = system->playSound(sound1, 0, true, &channel);
|
||||||
|
ERRCHECK(result);
|
||||||
|
|
||||||
|
result = channel->setMixLevelsOutput(0, 0, 0, 0, 0, 0, 0, 1.0f);
|
||||||
|
ERRCHECK(result);
|
||||||
|
|
||||||
|
result = channel->setPaused(false);
|
||||||
|
ERRCHECK(result);
|
||||||
|
}
|
||||||
|
else if (selection == 7) /* Stereo front */
|
||||||
|
{
|
||||||
|
result = system->playSound(sound2, 0, false, &channel);
|
||||||
|
ERRCHECK(result);
|
||||||
|
}
|
||||||
|
else if (selection == 8) /* Stereo front channel swapped */
|
||||||
|
{
|
||||||
|
float matrix[] = { 0.0f, 1.0f,
|
||||||
|
1.0f, 0.0f };
|
||||||
|
|
||||||
|
result = system->playSound(sound2, 0, true, &channel);
|
||||||
|
ERRCHECK(result);
|
||||||
|
|
||||||
|
result = channel->setMixMatrix(matrix, 2, 2);
|
||||||
|
ERRCHECK(result);
|
||||||
|
|
||||||
|
result = channel->setPaused(false);
|
||||||
|
ERRCHECK(result);
|
||||||
|
}
|
||||||
|
else if (selection == 9) /* Stereo (right only) center */
|
||||||
|
{
|
||||||
|
float matrix[] = { 0.0f, 0.0f,
|
||||||
|
0.0f, 0.0f,
|
||||||
|
0.0f, 1.0f };
|
||||||
|
|
||||||
|
result = system->playSound(sound2, 0, true, &channel);
|
||||||
|
ERRCHECK(result);
|
||||||
|
|
||||||
|
result = channel->setMixMatrix(matrix, 3, 2);
|
||||||
|
ERRCHECK(result);
|
||||||
|
|
||||||
|
result = channel->setPaused(false);
|
||||||
|
ERRCHECK(result);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
result = system->update();
|
||||||
|
ERRCHECK(result);
|
||||||
|
|
||||||
|
{
|
||||||
|
unsigned int ms = 0;
|
||||||
|
unsigned int lenms = 0;
|
||||||
|
bool playing = false;
|
||||||
|
bool paused = false;
|
||||||
|
int channelsplaying = 0;
|
||||||
|
|
||||||
|
if (channel)
|
||||||
|
{
|
||||||
|
FMOD::Sound *currentsound = 0;
|
||||||
|
|
||||||
|
result = channel->isPlaying(&playing);
|
||||||
|
if ((result != FMOD_OK) && (result != FMOD_ERR_INVALID_HANDLE) && (result != FMOD_ERR_CHANNEL_STOLEN))
|
||||||
|
{
|
||||||
|
ERRCHECK(result);
|
||||||
|
}
|
||||||
|
|
||||||
|
result = channel->getPaused(&paused);
|
||||||
|
if ((result != FMOD_OK) && (result != FMOD_ERR_INVALID_HANDLE) && (result != FMOD_ERR_CHANNEL_STOLEN))
|
||||||
|
{
|
||||||
|
ERRCHECK(result);
|
||||||
|
}
|
||||||
|
|
||||||
|
result = channel->getPosition(&ms, FMOD_TIMEUNIT_MS);
|
||||||
|
if ((result != FMOD_OK) && (result != FMOD_ERR_INVALID_HANDLE) && (result != FMOD_ERR_CHANNEL_STOLEN))
|
||||||
|
{
|
||||||
|
ERRCHECK(result);
|
||||||
|
}
|
||||||
|
|
||||||
|
channel->getCurrentSound(¤tsound);
|
||||||
|
if (currentsound)
|
||||||
|
{
|
||||||
|
result = currentsound->getLength(&lenms, FMOD_TIMEUNIT_MS);
|
||||||
|
if ((result != FMOD_OK) && (result != FMOD_ERR_INVALID_HANDLE) && (result != FMOD_ERR_CHANNEL_STOLEN))
|
||||||
|
{
|
||||||
|
ERRCHECK(result);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
result = system->getChannelsPlaying(&channelsplaying, NULL);
|
||||||
|
ERRCHECK(result);
|
||||||
|
|
||||||
|
Common_Draw("==================================================");
|
||||||
|
Common_Draw("Multiple Speaker Example.");
|
||||||
|
Common_Draw("Copyright (c) Firelight Technologies 2004-2026.");
|
||||||
|
Common_Draw("==================================================");
|
||||||
|
Common_Draw("");
|
||||||
|
Common_Draw("Speaker mode is set to %s%s", SPEAKERMODE_STRING[speakermode], speakermode < FMOD_SPEAKERMODE_7POINT1 ? " causing some speaker options to be unavailable" : "");
|
||||||
|
Common_Draw("");
|
||||||
|
Common_Draw("Press %s or %s to select mode", Common_BtnStr(BTN_UP), Common_BtnStr(BTN_DOWN));
|
||||||
|
Common_Draw("Press %s to play the sound", Common_BtnStr(BTN_ACTION1));
|
||||||
|
for (int i = 0; i < SELECTION_COUNT; i++)
|
||||||
|
{
|
||||||
|
bool disabled = !isSelectionAvailable(speakermode, i);
|
||||||
|
Common_Draw("[%c] %s%s", (selection == i) ? (disabled ? '-' : 'X') : ' ', disabled ? "[N/A] " : "", SELECTION_STRING[i]);
|
||||||
|
}
|
||||||
|
Common_Draw("Press %s to quit", Common_BtnStr(BTN_QUIT));
|
||||||
|
Common_Draw("");
|
||||||
|
Common_Draw("Time %02d:%02d:%02d/%02d:%02d:%02d : %s", ms / 1000 / 60, ms / 1000 % 60, ms / 10 % 100, lenms / 1000 / 60, lenms / 1000 % 60, lenms / 10 % 100, paused ? "Paused " : playing ? "Playing" : "Stopped");
|
||||||
|
Common_Draw("Channels playing: %d", channelsplaying);
|
||||||
|
}
|
||||||
|
|
||||||
|
Common_Sleep(50);
|
||||||
|
} while (!Common_BtnPress(BTN_QUIT));
|
||||||
|
|
||||||
|
/*
|
||||||
|
Shut down
|
||||||
|
*/
|
||||||
|
result = sound1->release();
|
||||||
|
ERRCHECK(result);
|
||||||
|
result = sound2->release();
|
||||||
|
ERRCHECK(result);
|
||||||
|
result = system->close();
|
||||||
|
ERRCHECK(result);
|
||||||
|
result = system->release();
|
||||||
|
ERRCHECK(result);
|
||||||
|
|
||||||
|
Common_Close();
|
||||||
|
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
@@ -0,0 +1,193 @@
|
|||||||
|
/*==============================================================================
|
||||||
|
Multiple System Example
|
||||||
|
Copyright (c), Firelight Technologies Pty, Ltd 2004-2026.
|
||||||
|
|
||||||
|
This example shows how to play sounds on two different output devices from the
|
||||||
|
same application. It creates two FMOD::System objects, selects a different sound
|
||||||
|
device for each, then allows the user to play one sound on each device.
|
||||||
|
|
||||||
|
Note that sounds created on device A cannot be played on device B and vice
|
||||||
|
versa.
|
||||||
|
|
||||||
|
For information on using FMOD example code in your own programs, visit
|
||||||
|
https://www.fmod.com/legal
|
||||||
|
==============================================================================*/
|
||||||
|
#include "fmod.hpp"
|
||||||
|
#include "common.h"
|
||||||
|
|
||||||
|
FMOD_RESULT fetchDriver(FMOD::System *system, int *driver)
|
||||||
|
{
|
||||||
|
FMOD_RESULT result;
|
||||||
|
int numdrivers;
|
||||||
|
int selectedindex = 0;
|
||||||
|
|
||||||
|
result = system->getNumDrivers(&numdrivers);
|
||||||
|
ERRCHECK(result);
|
||||||
|
|
||||||
|
if (numdrivers == 0)
|
||||||
|
{
|
||||||
|
result = system->setOutput(FMOD_OUTPUTTYPE_NOSOUND);
|
||||||
|
ERRCHECK(result);
|
||||||
|
}
|
||||||
|
|
||||||
|
do
|
||||||
|
{
|
||||||
|
Common_Update();
|
||||||
|
|
||||||
|
if (Common_BtnPress(BTN_UP) && (selectedindex != 0))
|
||||||
|
{
|
||||||
|
selectedindex--;
|
||||||
|
}
|
||||||
|
if (Common_BtnPress(BTN_DOWN) && (selectedindex != (numdrivers - 1)))
|
||||||
|
{
|
||||||
|
selectedindex++;
|
||||||
|
}
|
||||||
|
|
||||||
|
Common_Draw("==================================================");
|
||||||
|
Common_Draw("Multiple System Example.");
|
||||||
|
Common_Draw("Copyright (c) Firelight Technologies 2004-2026.");
|
||||||
|
Common_Draw("==================================================");
|
||||||
|
Common_Draw("");
|
||||||
|
Common_Draw("Choose a device for system: 0x%p", system);
|
||||||
|
Common_Draw("");
|
||||||
|
Common_Draw("Use %s and %s to select.", Common_BtnStr(BTN_UP), Common_BtnStr(BTN_DOWN));
|
||||||
|
Common_Draw("Press %s to confirm.", Common_BtnStr(BTN_ACTION1));
|
||||||
|
Common_Draw("");
|
||||||
|
for (int i = 0; i < numdrivers; i++)
|
||||||
|
{
|
||||||
|
char name[256];
|
||||||
|
|
||||||
|
result = system->getDriverInfo(i, name, sizeof(name), 0, 0, 0, 0);
|
||||||
|
ERRCHECK(result);
|
||||||
|
|
||||||
|
Common_Draw("[%c] - %d. %s", selectedindex == i ? 'X' : ' ', i, name);
|
||||||
|
}
|
||||||
|
|
||||||
|
Common_Sleep(50);
|
||||||
|
} while (!Common_BtnPress(BTN_ACTION1) && !Common_BtnPress(BTN_QUIT));
|
||||||
|
|
||||||
|
*driver = selectedindex;
|
||||||
|
|
||||||
|
return FMOD_OK;
|
||||||
|
}
|
||||||
|
|
||||||
|
int FMOD_Main()
|
||||||
|
{
|
||||||
|
FMOD::System *systemA, *systemB;
|
||||||
|
FMOD::Sound *soundA, *soundB;
|
||||||
|
FMOD::Channel *channelA = 0, *channelB = 0;
|
||||||
|
FMOD_RESULT result;
|
||||||
|
int driver;
|
||||||
|
void *extradriverdata = 0;
|
||||||
|
|
||||||
|
Common_Init(&extradriverdata);
|
||||||
|
|
||||||
|
/*
|
||||||
|
Create Sound Card A
|
||||||
|
*/
|
||||||
|
result = FMOD::System_Create(&systemA);
|
||||||
|
ERRCHECK(result);
|
||||||
|
|
||||||
|
result = fetchDriver(systemA, &driver);
|
||||||
|
ERRCHECK(result);
|
||||||
|
|
||||||
|
result = systemA->setDriver(driver);
|
||||||
|
ERRCHECK(result);
|
||||||
|
|
||||||
|
result = systemA->init(32, FMOD_INIT_NORMAL, extradriverdata);
|
||||||
|
ERRCHECK(result);
|
||||||
|
|
||||||
|
/*
|
||||||
|
Create Sound Card B
|
||||||
|
*/
|
||||||
|
result = FMOD::System_Create(&systemB);
|
||||||
|
ERRCHECK(result);
|
||||||
|
|
||||||
|
result = fetchDriver(systemB, &driver);
|
||||||
|
ERRCHECK(result);
|
||||||
|
|
||||||
|
result = systemB->setDriver(driver);
|
||||||
|
ERRCHECK(result);
|
||||||
|
|
||||||
|
result = systemB->init(32, FMOD_INIT_NORMAL, extradriverdata);
|
||||||
|
ERRCHECK(result);
|
||||||
|
|
||||||
|
/*
|
||||||
|
Load 1 sample into each soundcard.
|
||||||
|
*/
|
||||||
|
result = systemA->createSound(Common_MediaPath("drumloop.wav"), FMOD_LOOP_OFF, 0, &soundA);
|
||||||
|
ERRCHECK(result);
|
||||||
|
|
||||||
|
result = systemB->createSound(Common_MediaPath("jaguar.wav"), FMOD_DEFAULT, 0, &soundB);
|
||||||
|
ERRCHECK(result);
|
||||||
|
|
||||||
|
/*
|
||||||
|
Main loop
|
||||||
|
*/
|
||||||
|
do
|
||||||
|
{
|
||||||
|
Common_Update();
|
||||||
|
|
||||||
|
if (Common_BtnPress(BTN_ACTION1))
|
||||||
|
{
|
||||||
|
result = systemA->playSound(soundA, 0, 0, &channelA);
|
||||||
|
ERRCHECK(result);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (Common_BtnPress(BTN_ACTION2))
|
||||||
|
{
|
||||||
|
result = systemB->playSound(soundB, 0, 0, &channelB);
|
||||||
|
ERRCHECK(result);
|
||||||
|
}
|
||||||
|
|
||||||
|
result = systemA->update();
|
||||||
|
ERRCHECK(result);
|
||||||
|
result = systemB->update();
|
||||||
|
ERRCHECK(result);
|
||||||
|
|
||||||
|
{
|
||||||
|
int channelsplayingA = 0;
|
||||||
|
int channelsplayingB = 0;
|
||||||
|
|
||||||
|
result = systemA->getChannelsPlaying(&channelsplayingA, NULL);
|
||||||
|
ERRCHECK(result);
|
||||||
|
result = systemB->getChannelsPlaying(&channelsplayingB, NULL);
|
||||||
|
ERRCHECK(result);
|
||||||
|
|
||||||
|
Common_Draw("==================================================");
|
||||||
|
Common_Draw("Multiple System Example.");
|
||||||
|
Common_Draw("Copyright (c) Firelight Technologies 2004-2026.");
|
||||||
|
Common_Draw("==================================================");
|
||||||
|
Common_Draw("");
|
||||||
|
Common_Draw("Press %s to play a sound on device A", Common_BtnStr(BTN_ACTION1));
|
||||||
|
Common_Draw("Press %s to play a sound on device B", Common_BtnStr(BTN_ACTION2));
|
||||||
|
Common_Draw("Press %s to quit", Common_BtnStr(BTN_QUIT));
|
||||||
|
Common_Draw("");
|
||||||
|
Common_Draw("Channels playing on A: %d", channelsplayingA);
|
||||||
|
Common_Draw("Channels playing on B: %d", channelsplayingB);
|
||||||
|
}
|
||||||
|
|
||||||
|
Common_Sleep(50);
|
||||||
|
} while (!Common_BtnPress(BTN_QUIT));
|
||||||
|
|
||||||
|
/*
|
||||||
|
Shut down
|
||||||
|
*/
|
||||||
|
result = soundA->release();
|
||||||
|
ERRCHECK(result);
|
||||||
|
result = systemA->close();
|
||||||
|
ERRCHECK(result);
|
||||||
|
result = systemA->release();
|
||||||
|
ERRCHECK(result);
|
||||||
|
|
||||||
|
result = soundB->release();
|
||||||
|
ERRCHECK(result);
|
||||||
|
result = systemB->close();
|
||||||
|
ERRCHECK(result);
|
||||||
|
result = systemB->release();
|
||||||
|
ERRCHECK(result);
|
||||||
|
|
||||||
|
Common_Close();
|
||||||
|
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
@@ -0,0 +1,219 @@
|
|||||||
|
/*==============================================================================
|
||||||
|
Net Stream Example
|
||||||
|
Copyright (c), Firelight Technologies Pty, Ltd 2004-2026.
|
||||||
|
|
||||||
|
This example shows how to play streaming audio from an Internet source
|
||||||
|
|
||||||
|
For information on using FMOD example code in your own programs, visit
|
||||||
|
https://www.fmod.com/legal
|
||||||
|
==============================================================================*/
|
||||||
|
#define COMMON_REQUIRE_NETWORK
|
||||||
|
#include "fmod.hpp"
|
||||||
|
#include "common.h"
|
||||||
|
|
||||||
|
int FMOD_Main()
|
||||||
|
{
|
||||||
|
FMOD::System *system = 0;
|
||||||
|
FMOD::Sound *sound = 0;
|
||||||
|
FMOD::Channel *channel = 0;
|
||||||
|
FMOD_RESULT result = FMOD_OK;
|
||||||
|
FMOD_OPENSTATE openstate = FMOD_OPENSTATE_READY;
|
||||||
|
void *extradriverdata = 0;
|
||||||
|
const int tagcount = 4;
|
||||||
|
int tagindex = 0;
|
||||||
|
char tagstring[tagcount][128] = { };
|
||||||
|
|
||||||
|
Common_Init(&extradriverdata);
|
||||||
|
|
||||||
|
/*
|
||||||
|
Create a System object and initialize.
|
||||||
|
*/
|
||||||
|
result = FMOD::System_Create(&system);
|
||||||
|
ERRCHECK(result);
|
||||||
|
|
||||||
|
result = system->init(1, FMOD_INIT_NORMAL, extradriverdata);
|
||||||
|
ERRCHECK(result);
|
||||||
|
|
||||||
|
/* Increase the file buffer size a little bit to account for Internet lag. */
|
||||||
|
result = system->setStreamBufferSize(64*1024, FMOD_TIMEUNIT_RAWBYTES);
|
||||||
|
ERRCHECK(result);
|
||||||
|
|
||||||
|
FMOD_CREATESOUNDEXINFO exinfo;
|
||||||
|
memset(&exinfo, 0, sizeof(FMOD_CREATESOUNDEXINFO));
|
||||||
|
exinfo.cbsize = sizeof(FMOD_CREATESOUNDEXINFO);
|
||||||
|
exinfo.filebuffersize = 1024*16; /* Increase the default file chunk size to handle seeking inside large playlist files that may be over 2kb. */
|
||||||
|
|
||||||
|
result = system->createSound("https://stream.live.vc.bbcmedia.co.uk/bbc_world_service", FMOD_CREATESTREAM | FMOD_NONBLOCKING, &exinfo, &sound);
|
||||||
|
ERRCHECK(result);
|
||||||
|
|
||||||
|
/*
|
||||||
|
Main loop
|
||||||
|
*/
|
||||||
|
do
|
||||||
|
{
|
||||||
|
unsigned int pos = 0;
|
||||||
|
unsigned int percent = 0;
|
||||||
|
bool playing = false;
|
||||||
|
bool paused = false;
|
||||||
|
bool starving = false;
|
||||||
|
const char *state = "Stopped";
|
||||||
|
|
||||||
|
Common_Update();
|
||||||
|
|
||||||
|
if (Common_BtnPress(BTN_ACTION1))
|
||||||
|
{
|
||||||
|
if (channel)
|
||||||
|
{
|
||||||
|
bool paused = false;
|
||||||
|
|
||||||
|
result = channel->getPaused(&paused);
|
||||||
|
ERRCHECK(result);
|
||||||
|
result = channel->setPaused(!paused);
|
||||||
|
ERRCHECK(result);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
result = system->update();
|
||||||
|
ERRCHECK(result);
|
||||||
|
|
||||||
|
result = sound->getOpenState(&openstate, &percent, &starving, 0);
|
||||||
|
ERRCHECK(result);
|
||||||
|
|
||||||
|
{
|
||||||
|
FMOD_TAG tag;
|
||||||
|
|
||||||
|
/*
|
||||||
|
Read any tags that have arrived, this could happen if a radio station switches
|
||||||
|
to a new song.
|
||||||
|
*/
|
||||||
|
while (sound->getTag(0, -1, &tag) == FMOD_OK)
|
||||||
|
{
|
||||||
|
if (tag.datatype == FMOD_TAGDATATYPE_STRING)
|
||||||
|
{
|
||||||
|
snprintf(tagstring[tagindex], 128, "%s = '%s' (%d bytes)", tag.name, (char *)tag.data, tag.datalen);
|
||||||
|
tagindex = (tagindex + 1) % tagcount;
|
||||||
|
|
||||||
|
if (tag.type == FMOD_TAGTYPE_PLAYLIST && !strcmp(tag.name, "FILE"))
|
||||||
|
{
|
||||||
|
char url[256] = {};
|
||||||
|
|
||||||
|
strncpy(url, (const char *)tag.data, 255); /* data point to sound owned memory, copy it before the sound is released. */
|
||||||
|
|
||||||
|
result = sound->release();
|
||||||
|
ERRCHECK(result);
|
||||||
|
|
||||||
|
result = system->createSound(url, FMOD_CREATESTREAM | FMOD_NONBLOCKING, &exinfo, &sound);
|
||||||
|
ERRCHECK(result);
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
else if (tag.type == FMOD_TAGTYPE_FMOD)
|
||||||
|
{
|
||||||
|
/* When a song changes, the sample rate may also change, so compensate here. */
|
||||||
|
if (!strcmp(tag.name, "Sample Rate Change") && channel)
|
||||||
|
{
|
||||||
|
float frequency = *((float *)tag.data);
|
||||||
|
|
||||||
|
result = channel->setFrequency(frequency);
|
||||||
|
ERRCHECK(result);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (channel)
|
||||||
|
{
|
||||||
|
result = channel->getPaused(&paused);
|
||||||
|
ERRCHECK(result);
|
||||||
|
|
||||||
|
result = channel->isPlaying(&playing);
|
||||||
|
ERRCHECK(result);
|
||||||
|
|
||||||
|
result = channel->getPosition(&pos, FMOD_TIMEUNIT_MS);
|
||||||
|
ERRCHECK(result);
|
||||||
|
|
||||||
|
/* Silence the stream until we have sufficient data for smooth playback. */
|
||||||
|
result = channel->setMute(starving);
|
||||||
|
ERRCHECK(result);
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
/* This may fail if the stream isn't ready yet, so don't check the error code. */
|
||||||
|
system->playSound(sound, 0, false, &channel);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (openstate == FMOD_OPENSTATE_BUFFERING)
|
||||||
|
{
|
||||||
|
state = "Buffering...";
|
||||||
|
}
|
||||||
|
else if (openstate == FMOD_OPENSTATE_CONNECTING)
|
||||||
|
{
|
||||||
|
state = "Connecting...";
|
||||||
|
}
|
||||||
|
else if (paused)
|
||||||
|
{
|
||||||
|
state = "Paused";
|
||||||
|
}
|
||||||
|
else if (playing)
|
||||||
|
{
|
||||||
|
state = "Playing";
|
||||||
|
}
|
||||||
|
|
||||||
|
Common_Draw("==================================================");
|
||||||
|
Common_Draw("Net Stream Example.");
|
||||||
|
Common_Draw("Copyright (c) Firelight Technologies 2004-2026.");
|
||||||
|
Common_Draw("==================================================");
|
||||||
|
Common_Draw("");
|
||||||
|
Common_Draw("Press %s to toggle pause", Common_BtnStr(BTN_ACTION1));
|
||||||
|
Common_Draw("Press %s to quit", Common_BtnStr(BTN_QUIT));
|
||||||
|
Common_Draw("");
|
||||||
|
Common_Draw("Time = %02d:%02d:%02d", pos / 1000 / 60, pos / 1000 % 60, pos / 10 % 100);
|
||||||
|
Common_Draw("State = %s %s", state, starving ? "(STARVING)" : "");
|
||||||
|
Common_Draw("Buffer Percentage = %d", percent);
|
||||||
|
Common_Draw("");
|
||||||
|
Common_Draw("Tags:");
|
||||||
|
for (int i = tagindex; i < (tagindex + tagcount); i++)
|
||||||
|
{
|
||||||
|
Common_Draw("%s", tagstring[i % tagcount]);
|
||||||
|
Common_Draw("");
|
||||||
|
}
|
||||||
|
|
||||||
|
Common_Sleep(50);
|
||||||
|
} while (!Common_BtnPress(BTN_QUIT));
|
||||||
|
|
||||||
|
/*
|
||||||
|
Stop the channel, then wait for it to finish opening before we release it.
|
||||||
|
*/
|
||||||
|
if (channel)
|
||||||
|
{
|
||||||
|
result = channel->stop();
|
||||||
|
ERRCHECK(result);
|
||||||
|
}
|
||||||
|
|
||||||
|
do
|
||||||
|
{
|
||||||
|
Common_Update();
|
||||||
|
Common_Draw("Waiting for sound to finish opening before trying to release it....", Common_BtnStr(BTN_ACTION1));
|
||||||
|
Common_Sleep(50);
|
||||||
|
|
||||||
|
result = system->update();
|
||||||
|
ERRCHECK(result);
|
||||||
|
|
||||||
|
result = sound->getOpenState(&openstate, 0, 0, 0);
|
||||||
|
ERRCHECK(result);
|
||||||
|
} while (openstate != FMOD_OPENSTATE_READY);
|
||||||
|
|
||||||
|
/*
|
||||||
|
Shut down
|
||||||
|
*/
|
||||||
|
result = sound->release();
|
||||||
|
ERRCHECK(result);
|
||||||
|
result = system->close();
|
||||||
|
ERRCHECK(result);
|
||||||
|
result = system->release();
|
||||||
|
ERRCHECK(result);
|
||||||
|
|
||||||
|
Common_Close();
|
||||||
|
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
@@ -0,0 +1,152 @@
|
|||||||
|
/*==============================================================================
|
||||||
|
Play Sound Example
|
||||||
|
Copyright (c), Firelight Technologies Pty, Ltd 2004-2026.
|
||||||
|
|
||||||
|
This example shows how to simply load and play multiple sounds, the simplest
|
||||||
|
usage of FMOD. By default FMOD will decode the entire file into memory when it
|
||||||
|
loads. If the sounds are big and possibly take up a lot of RAM it would be
|
||||||
|
better to use the FMOD_CREATESTREAM flag, this will stream the file in realtime
|
||||||
|
as it plays.
|
||||||
|
|
||||||
|
For information on using FMOD example code in your own programs, visit
|
||||||
|
https://www.fmod.com/legal
|
||||||
|
==============================================================================*/
|
||||||
|
#include "fmod.hpp"
|
||||||
|
#include "common.h"
|
||||||
|
|
||||||
|
int FMOD_Main()
|
||||||
|
{
|
||||||
|
FMOD::System *system;
|
||||||
|
FMOD::Sound *sound1, *sound2, *sound3;
|
||||||
|
FMOD::Channel *channel = 0;
|
||||||
|
FMOD_RESULT result;
|
||||||
|
void *extradriverdata = 0;
|
||||||
|
|
||||||
|
Common_Init(&extradriverdata);
|
||||||
|
|
||||||
|
/*
|
||||||
|
Create a System object and initialize
|
||||||
|
*/
|
||||||
|
result = FMOD::System_Create(&system);
|
||||||
|
ERRCHECK(result);
|
||||||
|
|
||||||
|
result = system->init(32, FMOD_INIT_NORMAL, extradriverdata);
|
||||||
|
ERRCHECK(result);
|
||||||
|
|
||||||
|
result = system->createSound(Common_MediaPath("drumloop.wav"), FMOD_DEFAULT, 0, &sound1);
|
||||||
|
ERRCHECK(result);
|
||||||
|
|
||||||
|
result = sound1->setMode(FMOD_LOOP_OFF); /* drumloop.wav has embedded loop points which automatically makes looping turn on, */
|
||||||
|
ERRCHECK(result); /* so turn it off here. We could have also just put FMOD_LOOP_OFF in the above CreateSound call. */
|
||||||
|
|
||||||
|
result = system->createSound(Common_MediaPath("jaguar.wav"), FMOD_DEFAULT, 0, &sound2);
|
||||||
|
ERRCHECK(result);
|
||||||
|
|
||||||
|
result = system->createSound(Common_MediaPath("swish.wav"), FMOD_DEFAULT, 0, &sound3);
|
||||||
|
ERRCHECK(result);
|
||||||
|
|
||||||
|
/*
|
||||||
|
Main loop
|
||||||
|
*/
|
||||||
|
do
|
||||||
|
{
|
||||||
|
Common_Update();
|
||||||
|
|
||||||
|
if (Common_BtnPress(BTN_ACTION1))
|
||||||
|
{
|
||||||
|
result = system->playSound(sound1, 0, false, &channel);
|
||||||
|
ERRCHECK(result);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (Common_BtnPress(BTN_ACTION2))
|
||||||
|
{
|
||||||
|
result = system->playSound(sound2, 0, false, &channel);
|
||||||
|
ERRCHECK(result);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (Common_BtnPress(BTN_ACTION3))
|
||||||
|
{
|
||||||
|
result = system->playSound(sound3, 0, false, &channel);
|
||||||
|
ERRCHECK(result);
|
||||||
|
}
|
||||||
|
|
||||||
|
result = system->update();
|
||||||
|
ERRCHECK(result);
|
||||||
|
|
||||||
|
{
|
||||||
|
unsigned int ms = 0;
|
||||||
|
unsigned int lenms = 0;
|
||||||
|
bool playing = 0;
|
||||||
|
bool paused = 0;
|
||||||
|
int channelsplaying = 0;
|
||||||
|
|
||||||
|
if (channel)
|
||||||
|
{
|
||||||
|
FMOD::Sound *currentsound = 0;
|
||||||
|
|
||||||
|
result = channel->isPlaying(&playing);
|
||||||
|
if ((result != FMOD_OK) && (result != FMOD_ERR_INVALID_HANDLE) && (result != FMOD_ERR_CHANNEL_STOLEN))
|
||||||
|
{
|
||||||
|
ERRCHECK(result);
|
||||||
|
}
|
||||||
|
|
||||||
|
result = channel->getPaused(&paused);
|
||||||
|
if ((result != FMOD_OK) && (result != FMOD_ERR_INVALID_HANDLE) && (result != FMOD_ERR_CHANNEL_STOLEN))
|
||||||
|
{
|
||||||
|
ERRCHECK(result);
|
||||||
|
}
|
||||||
|
|
||||||
|
result = channel->getPosition(&ms, FMOD_TIMEUNIT_MS);
|
||||||
|
if ((result != FMOD_OK) && (result != FMOD_ERR_INVALID_HANDLE) && (result != FMOD_ERR_CHANNEL_STOLEN))
|
||||||
|
{
|
||||||
|
ERRCHECK(result);
|
||||||
|
}
|
||||||
|
|
||||||
|
channel->getCurrentSound(¤tsound);
|
||||||
|
if (currentsound)
|
||||||
|
{
|
||||||
|
result = currentsound->getLength(&lenms, FMOD_TIMEUNIT_MS);
|
||||||
|
if ((result != FMOD_OK) && (result != FMOD_ERR_INVALID_HANDLE) && (result != FMOD_ERR_CHANNEL_STOLEN))
|
||||||
|
{
|
||||||
|
ERRCHECK(result);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
system->getChannelsPlaying(&channelsplaying, NULL);
|
||||||
|
|
||||||
|
Common_Draw("==================================================");
|
||||||
|
Common_Draw("Play Sound Example.");
|
||||||
|
Common_Draw("Copyright (c) Firelight Technologies 2004-2026.");
|
||||||
|
Common_Draw("==================================================");
|
||||||
|
Common_Draw("");
|
||||||
|
Common_Draw("Press %s to play a mono sound (drumloop)", Common_BtnStr(BTN_ACTION1));
|
||||||
|
Common_Draw("Press %s to play a mono sound (jaguar)", Common_BtnStr(BTN_ACTION2));
|
||||||
|
Common_Draw("Press %s to play a stereo sound (swish)", Common_BtnStr(BTN_ACTION3));
|
||||||
|
Common_Draw("Press %s to quit", Common_BtnStr(BTN_QUIT));
|
||||||
|
Common_Draw("");
|
||||||
|
Common_Draw("Time %02d:%02d:%02d/%02d:%02d:%02d : %s", ms / 1000 / 60, ms / 1000 % 60, ms / 10 % 100, lenms / 1000 / 60, lenms / 1000 % 60, lenms / 10 % 100, paused ? "Paused " : playing ? "Playing" : "Stopped");
|
||||||
|
Common_Draw("Channels Playing %d", channelsplaying);
|
||||||
|
}
|
||||||
|
|
||||||
|
Common_Sleep(50);
|
||||||
|
} while (!Common_BtnPress(BTN_QUIT));
|
||||||
|
|
||||||
|
/*
|
||||||
|
Shut down
|
||||||
|
*/
|
||||||
|
result = sound1->release();
|
||||||
|
ERRCHECK(result);
|
||||||
|
result = sound2->release();
|
||||||
|
ERRCHECK(result);
|
||||||
|
result = sound3->release();
|
||||||
|
ERRCHECK(result);
|
||||||
|
result = system->close();
|
||||||
|
ERRCHECK(result);
|
||||||
|
result = system->release();
|
||||||
|
ERRCHECK(result);
|
||||||
|
|
||||||
|
Common_Close();
|
||||||
|
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
@@ -0,0 +1,143 @@
|
|||||||
|
/*==============================================================================
|
||||||
|
Play Stream Example
|
||||||
|
Copyright (c), Firelight Technologies Pty, Ltd 2004-2026.
|
||||||
|
|
||||||
|
This example shows how to simply play a stream such as an MP3 or WAV. The stream
|
||||||
|
behaviour is achieved by specifying FMOD_CREATESTREAM in the call to
|
||||||
|
System::createSound. This makes FMOD decode the file in realtime as it plays,
|
||||||
|
instead of loading it all at once which uses far less memory in exchange for a
|
||||||
|
small runtime CPU hit.
|
||||||
|
|
||||||
|
For information on using FMOD example code in your own programs, visit
|
||||||
|
https://www.fmod.com/legal
|
||||||
|
==============================================================================*/
|
||||||
|
#include "fmod.hpp"
|
||||||
|
#include "common.h"
|
||||||
|
|
||||||
|
int FMOD_Main()
|
||||||
|
{
|
||||||
|
FMOD::System *system;
|
||||||
|
FMOD::Sound *sound, *sound_to_play;
|
||||||
|
FMOD::Channel *channel = 0;
|
||||||
|
FMOD_RESULT result;
|
||||||
|
void *extradriverdata = 0;
|
||||||
|
int numsubsounds;
|
||||||
|
|
||||||
|
Common_Init(&extradriverdata);
|
||||||
|
|
||||||
|
/*
|
||||||
|
Create a System object and initialize.
|
||||||
|
*/
|
||||||
|
result = FMOD::System_Create(&system);
|
||||||
|
ERRCHECK(result);
|
||||||
|
|
||||||
|
result = system->init(32, FMOD_INIT_NORMAL, extradriverdata);
|
||||||
|
ERRCHECK(result);
|
||||||
|
|
||||||
|
/*
|
||||||
|
This example uses an FSB file, which is a preferred pack format for fmod containing multiple sounds.
|
||||||
|
This could just as easily be exchanged with a wav/mp3/ogg file for example, but in this case you wouldnt need to call getSubSound.
|
||||||
|
Because getNumSubSounds is called here the example would work with both types of sound file (packed vs single).
|
||||||
|
*/
|
||||||
|
result = system->createStream(Common_MediaPath("wave_vorbis.fsb"), FMOD_LOOP_NORMAL | FMOD_2D, 0, &sound);
|
||||||
|
ERRCHECK(result);
|
||||||
|
|
||||||
|
result = sound->getNumSubSounds(&numsubsounds);
|
||||||
|
ERRCHECK(result);
|
||||||
|
|
||||||
|
if (numsubsounds)
|
||||||
|
{
|
||||||
|
sound->getSubSound(0, &sound_to_play);
|
||||||
|
ERRCHECK(result);
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
sound_to_play = sound;
|
||||||
|
}
|
||||||
|
|
||||||
|
/*
|
||||||
|
Play the sound.
|
||||||
|
*/
|
||||||
|
result = system->playSound(sound_to_play, 0, false, &channel);
|
||||||
|
ERRCHECK(result);
|
||||||
|
|
||||||
|
/*
|
||||||
|
Main loop.
|
||||||
|
*/
|
||||||
|
do
|
||||||
|
{
|
||||||
|
Common_Update();
|
||||||
|
|
||||||
|
if (Common_BtnPress(BTN_ACTION1))
|
||||||
|
{
|
||||||
|
bool paused;
|
||||||
|
result = channel->getPaused(&paused);
|
||||||
|
ERRCHECK(result);
|
||||||
|
result = channel->setPaused(!paused);
|
||||||
|
ERRCHECK(result);
|
||||||
|
}
|
||||||
|
|
||||||
|
result = system->update();
|
||||||
|
ERRCHECK(result);
|
||||||
|
|
||||||
|
{
|
||||||
|
unsigned int ms = 0;
|
||||||
|
unsigned int lenms = 0;
|
||||||
|
bool playing = false;
|
||||||
|
bool paused = false;
|
||||||
|
|
||||||
|
if (channel)
|
||||||
|
{
|
||||||
|
result = channel->isPlaying(&playing);
|
||||||
|
if ((result != FMOD_OK) && (result != FMOD_ERR_INVALID_HANDLE))
|
||||||
|
{
|
||||||
|
ERRCHECK(result);
|
||||||
|
}
|
||||||
|
|
||||||
|
result = channel->getPaused(&paused);
|
||||||
|
if ((result != FMOD_OK) && (result != FMOD_ERR_INVALID_HANDLE))
|
||||||
|
{
|
||||||
|
ERRCHECK(result);
|
||||||
|
}
|
||||||
|
|
||||||
|
result = channel->getPosition(&ms, FMOD_TIMEUNIT_MS);
|
||||||
|
if ((result != FMOD_OK) && (result != FMOD_ERR_INVALID_HANDLE))
|
||||||
|
{
|
||||||
|
ERRCHECK(result);
|
||||||
|
}
|
||||||
|
|
||||||
|
result = sound_to_play->getLength(&lenms, FMOD_TIMEUNIT_MS);
|
||||||
|
if ((result != FMOD_OK) && (result != FMOD_ERR_INVALID_HANDLE))
|
||||||
|
{
|
||||||
|
ERRCHECK(result);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Common_Draw("==================================================");
|
||||||
|
Common_Draw("Play Stream Example.");
|
||||||
|
Common_Draw("Copyright (c) Firelight Technologies 2004-2026.");
|
||||||
|
Common_Draw("==================================================");
|
||||||
|
Common_Draw("");
|
||||||
|
Common_Draw("Press %s to toggle pause", Common_BtnStr(BTN_ACTION1));
|
||||||
|
Common_Draw("Press %s to quit", Common_BtnStr(BTN_QUIT));
|
||||||
|
Common_Draw("");
|
||||||
|
Common_Draw("Time %02d:%02d:%02d/%02d:%02d:%02d : %s", ms / 1000 / 60, ms / 1000 % 60, ms / 10 % 100, lenms / 1000 / 60, lenms / 1000 % 60, lenms / 10 % 100, paused ? "Paused " : playing ? "Playing" : "Stopped");
|
||||||
|
}
|
||||||
|
|
||||||
|
Common_Sleep(50);
|
||||||
|
} while (!Common_BtnPress(BTN_QUIT));
|
||||||
|
|
||||||
|
/*
|
||||||
|
Shut down
|
||||||
|
*/
|
||||||
|
result = sound->release(); /* Release the parent, not the sound that was retrieved with getSubSound. */
|
||||||
|
ERRCHECK(result);
|
||||||
|
result = system->close();
|
||||||
|
ERRCHECK(result);
|
||||||
|
result = system->release();
|
||||||
|
ERRCHECK(result);
|
||||||
|
|
||||||
|
Common_Close();
|
||||||
|
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
@@ -0,0 +1,278 @@
|
|||||||
|
/*
|
||||||
|
* Blade Type of DLL Interface for Lame encoder
|
||||||
|
*
|
||||||
|
* Copyright (c) 1999-2002 A.L. Faber
|
||||||
|
* Based on bladedll.h version 1.0 written by Jukka Poikolainen
|
||||||
|
*
|
||||||
|
* This library is free software; you can redistribute it and/or
|
||||||
|
* modify it under the terms of the GNU Library General Public
|
||||||
|
* License as published by the Free Software Foundation; either
|
||||||
|
* version 2 of the License, or (at your option) any later version.
|
||||||
|
*
|
||||||
|
* This library is distributed in the hope that it will be useful,
|
||||||
|
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||||
|
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
|
||||||
|
* Library General Public License for more details.
|
||||||
|
*
|
||||||
|
* You should have received a copy of the GNU Library General Public
|
||||||
|
* License along with this library; if not, write to the
|
||||||
|
* Free Software Foundation, Inc., 59 Temple Place - Suite 330,
|
||||||
|
* Boston, MA 02111-1307, USA.
|
||||||
|
*/
|
||||||
|
|
||||||
|
#pragma once
|
||||||
|
|
||||||
|
#ifdef __GNUC__
|
||||||
|
#define ATTRIBUTE_PACKED __attribute__((packed))
|
||||||
|
#else
|
||||||
|
#define ATTRIBUTE_PACKED
|
||||||
|
#pragma pack(push)
|
||||||
|
#pragma pack(1)
|
||||||
|
#endif
|
||||||
|
|
||||||
|
#ifdef __cplusplus
|
||||||
|
extern "C" {
|
||||||
|
#endif
|
||||||
|
|
||||||
|
/* encoding formats */
|
||||||
|
|
||||||
|
#define BE_CONFIG_MP3 0
|
||||||
|
#define BE_CONFIG_LAME 256
|
||||||
|
|
||||||
|
/* type definitions */
|
||||||
|
|
||||||
|
typedef void* HBE_STREAM;
|
||||||
|
typedef HBE_STREAM *PHBE_STREAM;
|
||||||
|
typedef unsigned long BE_ERR;
|
||||||
|
|
||||||
|
/* error codes */
|
||||||
|
|
||||||
|
#define BE_ERR_SUCCESSFUL 0x00000000
|
||||||
|
#define BE_ERR_INVALID_FORMAT 0x00000001
|
||||||
|
#define BE_ERR_INVALID_FORMAT_PARAMETERS 0x00000002
|
||||||
|
#define BE_ERR_NO_MORE_HANDLES 0x00000003
|
||||||
|
#define BE_ERR_INVALID_HANDLE 0x00000004
|
||||||
|
#define BE_ERR_BUFFER_TOO_SMALL 0x00000005
|
||||||
|
|
||||||
|
/* other constants */
|
||||||
|
|
||||||
|
#define BE_MAX_HOMEPAGE 128
|
||||||
|
|
||||||
|
/* format specific variables */
|
||||||
|
|
||||||
|
#define BE_MP3_MODE_STEREO 0
|
||||||
|
#define BE_MP3_MODE_JSTEREO 1
|
||||||
|
#define BE_MP3_MODE_DUALCHANNEL 2
|
||||||
|
#define BE_MP3_MODE_MONO 3
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
#define MPEG1 1
|
||||||
|
#define MPEG2 0
|
||||||
|
|
||||||
|
#ifdef _BLADEDLL
|
||||||
|
#undef FLOAT
|
||||||
|
#include <Windows.h>
|
||||||
|
#endif
|
||||||
|
|
||||||
|
#define CURRENT_STRUCT_VERSION 1
|
||||||
|
#define CURRENT_STRUCT_SIZE sizeof(BE_CONFIG) // is currently 331 bytes
|
||||||
|
|
||||||
|
|
||||||
|
typedef enum
|
||||||
|
{
|
||||||
|
VBR_METHOD_NONE = -1,
|
||||||
|
VBR_METHOD_DEFAULT = 0,
|
||||||
|
VBR_METHOD_OLD = 1,
|
||||||
|
VBR_METHOD_NEW = 2,
|
||||||
|
VBR_METHOD_MTRH = 3,
|
||||||
|
VBR_METHOD_ABR = 4
|
||||||
|
} VBRMETHOD;
|
||||||
|
|
||||||
|
typedef enum
|
||||||
|
{
|
||||||
|
LQP_NOPRESET =-1,
|
||||||
|
|
||||||
|
// QUALITY PRESETS
|
||||||
|
LQP_NORMAL_QUALITY = 0,
|
||||||
|
LQP_LOW_QUALITY = 1,
|
||||||
|
LQP_HIGH_QUALITY = 2,
|
||||||
|
LQP_VOICE_QUALITY = 3,
|
||||||
|
LQP_R3MIX = 4,
|
||||||
|
LQP_VERYHIGH_QUALITY = 5,
|
||||||
|
LQP_STANDARD = 6,
|
||||||
|
LQP_FAST_STANDARD = 7,
|
||||||
|
LQP_EXTREME = 8,
|
||||||
|
LQP_FAST_EXTREME = 9,
|
||||||
|
LQP_INSANE = 10,
|
||||||
|
LQP_ABR = 11,
|
||||||
|
LQP_CBR = 12,
|
||||||
|
LQP_MEDIUM = 13,
|
||||||
|
LQP_FAST_MEDIUM = 14,
|
||||||
|
|
||||||
|
// NEW PRESET VALUES
|
||||||
|
LQP_PHONE =1000,
|
||||||
|
LQP_SW =2000,
|
||||||
|
LQP_AM =3000,
|
||||||
|
LQP_FM =4000,
|
||||||
|
LQP_VOICE =5000,
|
||||||
|
LQP_RADIO =6000,
|
||||||
|
LQP_TAPE =7000,
|
||||||
|
LQP_HIFI =8000,
|
||||||
|
LQP_CD =9000,
|
||||||
|
LQP_STUDIO =10000
|
||||||
|
|
||||||
|
} LAME_QUALITY_PRESET;
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
typedef struct {
|
||||||
|
DWORD dwConfig; // BE_CONFIG_XXXXX
|
||||||
|
// Currently only BE_CONFIG_MP3 is supported
|
||||||
|
union {
|
||||||
|
|
||||||
|
struct {
|
||||||
|
|
||||||
|
DWORD dwSampleRate; // 48000, 44100 and 32000 allowed
|
||||||
|
BYTE byMode; // BE_MP3_MODE_STEREO, BE_MP3_MODE_DUALCHANNEL, BE_MP3_MODE_MONO
|
||||||
|
WORD wBitrate; // 32, 40, 48, 56, 64, 80, 96, 112, 128, 160, 192, 224, 256 and 320 allowed
|
||||||
|
BOOL bPrivate;
|
||||||
|
BOOL bCRC;
|
||||||
|
BOOL bCopyright;
|
||||||
|
BOOL bOriginal;
|
||||||
|
|
||||||
|
} mp3; // BE_CONFIG_MP3
|
||||||
|
|
||||||
|
struct
|
||||||
|
{
|
||||||
|
// STRUCTURE INFORMATION
|
||||||
|
DWORD dwStructVersion;
|
||||||
|
DWORD dwStructSize;
|
||||||
|
|
||||||
|
// BASIC ENCODER SETTINGS
|
||||||
|
DWORD dwSampleRate; // SAMPLERATE OF INPUT FILE
|
||||||
|
DWORD dwReSampleRate; // DOWNSAMPLERATE, 0=ENCODER DECIDES
|
||||||
|
LONG nMode; // BE_MP3_MODE_STEREO, BE_MP3_MODE_DUALCHANNEL, BE_MP3_MODE_MONO
|
||||||
|
DWORD dwBitrate; // CBR bitrate, VBR min bitrate
|
||||||
|
DWORD dwMaxBitrate; // CBR ignored, VBR Max bitrate
|
||||||
|
LONG nPreset; // Quality preset, use one of the settings of the LAME_QUALITY_PRESET enum
|
||||||
|
DWORD dwMpegVersion; // FUTURE USE, MPEG-1 OR MPEG-2
|
||||||
|
DWORD dwPsyModel; // FUTURE USE, SET TO 0
|
||||||
|
DWORD dwEmphasis; // FUTURE USE, SET TO 0
|
||||||
|
|
||||||
|
// BIT STREAM SETTINGS
|
||||||
|
BOOL bPrivate; // Set Private Bit (TRUE/FALSE)
|
||||||
|
BOOL bCRC; // Insert CRC (TRUE/FALSE)
|
||||||
|
BOOL bCopyright; // Set Copyright Bit (TRUE/FALSE)
|
||||||
|
BOOL bOriginal; // Set Original Bit (TRUE/FALSE)
|
||||||
|
|
||||||
|
// VBR STUFF
|
||||||
|
BOOL bWriteVBRHeader; // WRITE XING VBR HEADER (TRUE/FALSE)
|
||||||
|
BOOL bEnableVBR; // USE VBR ENCODING (TRUE/FALSE)
|
||||||
|
INT nVBRQuality; // VBR QUALITY 0..9
|
||||||
|
DWORD dwVbrAbr_bps; // Use ABR in stead of nVBRQuality
|
||||||
|
VBRMETHOD nVbrMethod;
|
||||||
|
BOOL bNoRes; // Disable Bit resorvoir (TRUE/FALSE)
|
||||||
|
|
||||||
|
// MISC SETTINGS
|
||||||
|
BOOL bStrictIso; // Use strict ISO encoding rules (TRUE/FALSE)
|
||||||
|
WORD nQuality; // Quality Setting, HIGH BYTE should be NOT LOW byte, otherwhise quality=5
|
||||||
|
|
||||||
|
// FUTURE USE, SET TO 0, align strucutre to 331 bytes
|
||||||
|
BYTE btReserved[255-4*sizeof(DWORD) - sizeof( WORD )];
|
||||||
|
|
||||||
|
} LHV1; // LAME header version 1
|
||||||
|
|
||||||
|
struct {
|
||||||
|
|
||||||
|
DWORD dwSampleRate;
|
||||||
|
BYTE byMode;
|
||||||
|
WORD wBitrate;
|
||||||
|
BYTE byEncodingMethod;
|
||||||
|
|
||||||
|
} aac;
|
||||||
|
|
||||||
|
} format;
|
||||||
|
|
||||||
|
} BE_CONFIG, *PBE_CONFIG ATTRIBUTE_PACKED;
|
||||||
|
|
||||||
|
|
||||||
|
typedef struct {
|
||||||
|
|
||||||
|
// BladeEnc DLL Version number
|
||||||
|
|
||||||
|
BYTE byDLLMajorVersion;
|
||||||
|
BYTE byDLLMinorVersion;
|
||||||
|
|
||||||
|
// BladeEnc Engine Version Number
|
||||||
|
|
||||||
|
BYTE byMajorVersion;
|
||||||
|
BYTE byMinorVersion;
|
||||||
|
|
||||||
|
// DLL Release date
|
||||||
|
|
||||||
|
BYTE byDay;
|
||||||
|
BYTE byMonth;
|
||||||
|
WORD wYear;
|
||||||
|
|
||||||
|
// BladeEnc Homepage URL
|
||||||
|
|
||||||
|
CHAR zHomepage[BE_MAX_HOMEPAGE + 1];
|
||||||
|
|
||||||
|
BYTE byAlphaLevel;
|
||||||
|
BYTE byBetaLevel;
|
||||||
|
BYTE byMMXEnabled;
|
||||||
|
|
||||||
|
BYTE btReserved[125];
|
||||||
|
|
||||||
|
|
||||||
|
} BE_VERSION, *PBE_VERSION ATTRIBUTE_PACKED;
|
||||||
|
|
||||||
|
#ifndef _BLADEDLL
|
||||||
|
|
||||||
|
typedef BE_ERR (*BEINITSTREAM) (PBE_CONFIG, PDWORD, PDWORD, PHBE_STREAM);
|
||||||
|
typedef BE_ERR (*BEENCODECHUNK) (HBE_STREAM, DWORD, PSHORT, PBYTE, PDWORD);
|
||||||
|
|
||||||
|
// added for floating point audio -- DSPguru, jd
|
||||||
|
typedef BE_ERR (*BEENCODECHUNKFLOATS16NI) (HBE_STREAM, DWORD, PFLOAT, PFLOAT, PBYTE, PDWORD);
|
||||||
|
typedef BE_ERR (*BEDEINITSTREAM) (HBE_STREAM, PBYTE, PDWORD);
|
||||||
|
typedef BE_ERR (*BECLOSESTREAM) (HBE_STREAM);
|
||||||
|
typedef VOID (*BEVERSION) (PBE_VERSION);
|
||||||
|
typedef BE_ERR (*BEWRITEVBRHEADER) (LPCSTR);
|
||||||
|
typedef BE_ERR (*BEWRITEINFOTAG) (HBE_STREAM, LPCSTR );
|
||||||
|
|
||||||
|
#define TEXT_BEINITSTREAM "beInitStream"
|
||||||
|
#define TEXT_BEENCODECHUNK "beEncodeChunk"
|
||||||
|
#define TEXT_BEENCODECHUNKFLOATS16NI "beEncodeChunkFloatS16NI"
|
||||||
|
#define TEXT_BEDEINITSTREAM "beDeinitStream"
|
||||||
|
#define TEXT_BECLOSESTREAM "beCloseStream"
|
||||||
|
#define TEXT_BEVERSION "beVersion"
|
||||||
|
#define TEXT_BEWRITEVBRHEADER "beWriteVBRHeader"
|
||||||
|
#define TEXT_BEFLUSHNOGAP "beFlushNoGap"
|
||||||
|
#define TEXT_BEWRITEINFOTAG "beWriteInfoTag"
|
||||||
|
|
||||||
|
|
||||||
|
#else
|
||||||
|
|
||||||
|
__declspec(dllexport) BE_ERR beInitStream(PBE_CONFIG pbeConfig, PDWORD dwSamples, PDWORD dwBufferSize, PHBE_STREAM phbeStream);
|
||||||
|
__declspec(dllexport) BE_ERR beEncodeChunk(HBE_STREAM hbeStream, DWORD nSamples, PSHORT pSamples, PBYTE pOutput, PDWORD pdwOutput);
|
||||||
|
|
||||||
|
// added for floating point audio -- DSPguru, jd
|
||||||
|
__declspec(dllexport) BE_ERR beEncodeChunkFloatS16NI(HBE_STREAM hbeStream, DWORD nSamples, PFLOAT buffer_l, PFLOAT buffer_r, PBYTE pOutput, PDWORD pdwOutput);
|
||||||
|
__declspec(dllexport) BE_ERR beDeinitStream(HBE_STREAM hbeStream, PBYTE pOutput, PDWORD pdwOutput);
|
||||||
|
__declspec(dllexport) BE_ERR beCloseStream(HBE_STREAM hbeStream);
|
||||||
|
__declspec(dllexport) VOID beVersion(PBE_VERSION pbeVersion);
|
||||||
|
__declspec(dllexport) BE_ERR beWriteVBRHeader(LPCSTR lpszFileName);
|
||||||
|
__declspec(dllexport) BE_ERR beFlushNoGap(HBE_STREAM hbeStream, PBYTE pOutput, PDWORD pdwOutput);
|
||||||
|
__declspec(dllexport) BE_ERR beWriteInfoTag( HBE_STREAM hbeStream, LPCSTR lpszFileName );
|
||||||
|
|
||||||
|
#endif
|
||||||
|
|
||||||
|
#ifdef __cplusplus
|
||||||
|
}
|
||||||
|
#endif
|
||||||
|
|
||||||
|
#ifndef __GNUC__
|
||||||
|
#pragma pack(pop)
|
||||||
|
#endif
|
||||||
|
|
||||||
@@ -0,0 +1,133 @@
|
|||||||
|
/*===============================================================================================
|
||||||
|
Raw Codec Plugin Example
|
||||||
|
Copyright (c), Firelight Technologies Pty, Ltd 2004-2026.
|
||||||
|
|
||||||
|
This example shows how to create a codec that reads raw PCM data.
|
||||||
|
|
||||||
|
1. The codec can be compiled as a DLL, using the reserved function name 'FMODGetCodecDescription'
|
||||||
|
as the only export symbol, and at runtime, the dll can be loaded in with System::loadPlugin.
|
||||||
|
|
||||||
|
2. Alternatively a codec of this type can be compiled directly into the program that uses it, and
|
||||||
|
you just register the codec into FMOD with System::registerCodec. This puts the codec into
|
||||||
|
the FMOD system, just the same way System::loadPlugin would if it was an external file.
|
||||||
|
|
||||||
|
3. The 'open' callback is the first thing called, and FMOD already has a file handle open for it.
|
||||||
|
In the open callback you can use FMOD_CODEC_STATE::fileread / FMOD_CODEC_STATE::fileseek to parse
|
||||||
|
your own file format, and return FMOD_ERR_FORMAT if it is not the format you support. Return
|
||||||
|
FMOD_OK if it succeeds your format test.
|
||||||
|
|
||||||
|
4. When an FMOD user calls System::createSound or System::createStream, the 'open' callback is called
|
||||||
|
once after FMOD tries to open it as many other types of file. If you want to override FMOD's
|
||||||
|
internal codecs then use the 'priority' parameter of System::loadPlugin or System::registerCodec.
|
||||||
|
|
||||||
|
5. In the open callback, tell FMOD what sort of PCM format the sound will produce with the
|
||||||
|
FMOD_CODEC_STATE::waveformat member.
|
||||||
|
|
||||||
|
6. The 'close' callback is called when Sound::release is called by the FMOD user.
|
||||||
|
|
||||||
|
7. The 'read' callback is called when System::createSound or System::createStream wants to receive
|
||||||
|
PCM data, in the format that you specified with FMOD_CODEC_STATE::waveformat. Data is
|
||||||
|
interleaved as decribed in the terminology section of the FMOD API documentation.
|
||||||
|
When a stream is being used, the read callback will be called repeatedly, using a size value
|
||||||
|
determined by the decode buffer size of the stream. See FMOD_CREATESOUNDEXINFO or
|
||||||
|
FMOD_ADVANCEDSETTINGS.
|
||||||
|
|
||||||
|
8. The 'seek' callback is called when Channel::setPosition is called, or when looping a sound
|
||||||
|
when it is a stream.
|
||||||
|
|
||||||
|
===============================================================================================*/
|
||||||
|
|
||||||
|
#include <stdio.h>
|
||||||
|
|
||||||
|
#include "fmod.h"
|
||||||
|
|
||||||
|
FMOD_RESULT F_CALL rawopen(FMOD_CODEC_STATE *codec, FMOD_MODE usermode, FMOD_CREATESOUNDEXINFO *userexinfo);
|
||||||
|
FMOD_RESULT F_CALL rawclose(FMOD_CODEC_STATE *codec);
|
||||||
|
FMOD_RESULT F_CALL rawread(FMOD_CODEC_STATE *codec, void *buffer, unsigned int size, unsigned int *read);
|
||||||
|
FMOD_RESULT F_CALL rawsetposition(FMOD_CODEC_STATE *codec, int subsound, unsigned int position, FMOD_TIMEUNIT postype);
|
||||||
|
|
||||||
|
FMOD_CODEC_DESCRIPTION rawcodec =
|
||||||
|
{
|
||||||
|
FMOD_CODEC_PLUGIN_VERSION, // Plugin version.
|
||||||
|
"FMOD Raw player plugin example", // Name.
|
||||||
|
0x00010000, // Version 0xAAAABBBB A = major, B = minor.
|
||||||
|
0, // Don't force everything using this codec to be a stream
|
||||||
|
FMOD_TIMEUNIT_PCMBYTES, // The time format we would like to accept into setposition/getposition.
|
||||||
|
&rawopen, // Open callback.
|
||||||
|
&rawclose, // Close callback.
|
||||||
|
&rawread, // Read callback.
|
||||||
|
0, // Getlength callback. (If not specified FMOD return the length in FMOD_TIMEUNIT_PCM, FMOD_TIMEUNIT_MS or FMOD_TIMEUNIT_PCMBYTES units based on the lengthpcm member of the FMOD_CODEC structure).
|
||||||
|
&rawsetposition, // Setposition callback.
|
||||||
|
0, // Getposition callback. (only used for timeunit types that are not FMOD_TIMEUNIT_PCM, FMOD_TIMEUNIT_MS and FMOD_TIMEUNIT_PCMBYTES).
|
||||||
|
0 // Sound create callback (don't need it)
|
||||||
|
};
|
||||||
|
|
||||||
|
|
||||||
|
/*
|
||||||
|
FMODGetCodecDescription is mandatory for every fmod plugin. This is the symbol the registerplugin function searches for.
|
||||||
|
Must be declared with F_CALL to make it export as stdcall.
|
||||||
|
MUST BE EXTERN'ED AS C! C++ functions will be mangled incorrectly and not load in fmod.
|
||||||
|
*/
|
||||||
|
#ifdef __cplusplus
|
||||||
|
extern "C" {
|
||||||
|
#endif
|
||||||
|
|
||||||
|
F_EXPORT FMOD_CODEC_DESCRIPTION * F_CALL FMODGetCodecDescription()
|
||||||
|
{
|
||||||
|
return &rawcodec;
|
||||||
|
}
|
||||||
|
|
||||||
|
#ifdef __cplusplus
|
||||||
|
}
|
||||||
|
#endif
|
||||||
|
|
||||||
|
|
||||||
|
static FMOD_CODEC_WAVEFORMAT rawwaveformat;
|
||||||
|
|
||||||
|
/*
|
||||||
|
The actual codec code.
|
||||||
|
|
||||||
|
Note that the callbacks uses FMOD's supplied file system callbacks.
|
||||||
|
|
||||||
|
This is important as even though you might want to open the file yourself, you would lose the following benefits.
|
||||||
|
1. Automatic support of memory files, CDDA based files, and HTTP/TCPIP based files.
|
||||||
|
2. "fileoffset" / "length" support when user calls System::createSound with FMOD_CREATESOUNDEXINFO structure.
|
||||||
|
3. Buffered file access.
|
||||||
|
FMOD files are high level abstracts that support all sorts of 'file', they are not just disk file handles.
|
||||||
|
If you want FMOD to use your own filesystem (and potentially lose the above benefits) use System::setFileSystem.
|
||||||
|
*/
|
||||||
|
|
||||||
|
FMOD_RESULT F_CALL rawopen(FMOD_CODEC_STATE *codec, FMOD_MODE /*usermode*/, FMOD_CREATESOUNDEXINFO * /*userexinfo*/)
|
||||||
|
{
|
||||||
|
rawwaveformat.channels = 2;
|
||||||
|
rawwaveformat.format = FMOD_SOUND_FORMAT_PCM16;
|
||||||
|
rawwaveformat.frequency = 44100;
|
||||||
|
rawwaveformat.pcmblocksize = 0;
|
||||||
|
unsigned int size;
|
||||||
|
FMOD_CODEC_FILE_SIZE(codec, &size);
|
||||||
|
rawwaveformat.lengthpcm = size / (rawwaveformat.channels * sizeof(short)); /* bytes converted to PCM samples */;
|
||||||
|
|
||||||
|
codec->numsubsounds = 0; /* number of 'subsounds' in this sound. For most codecs this is 0, only multi sound codecs such as FSB or CDDA have subsounds. */
|
||||||
|
codec->waveformat = &rawwaveformat;
|
||||||
|
codec->plugindata = 0; /* user data value */
|
||||||
|
|
||||||
|
/* If your file format needs to read data to determine the format and load metadata, do so here with codec->fileread/fileseek function pointers. This will handle reading from disk/memory or internet. */
|
||||||
|
|
||||||
|
return FMOD_OK;
|
||||||
|
}
|
||||||
|
|
||||||
|
FMOD_RESULT F_CALL rawclose(FMOD_CODEC_STATE * /*codec*/)
|
||||||
|
{
|
||||||
|
return FMOD_OK;
|
||||||
|
}
|
||||||
|
|
||||||
|
FMOD_RESULT F_CALL rawread(FMOD_CODEC_STATE *codec, void *buffer, unsigned int size, unsigned int *read)
|
||||||
|
{
|
||||||
|
return FMOD_CODEC_FILE_READ(codec, buffer, size, read);
|
||||||
|
}
|
||||||
|
|
||||||
|
FMOD_RESULT F_CALL rawsetposition(FMOD_CODEC_STATE *codec, int /*subsound*/, unsigned int position, FMOD_TIMEUNIT /*postype*/)
|
||||||
|
{
|
||||||
|
return FMOD_CODEC_FILE_SEEK(codec, position, 0);
|
||||||
|
}
|
||||||
|
|
||||||
@@ -0,0 +1,466 @@
|
|||||||
|
/*==============================================================================
|
||||||
|
Distance Filter DSP Plugin Example
|
||||||
|
Copyright (c), Firelight Technologies Pty, Ltd 2004-2026.
|
||||||
|
|
||||||
|
This example shows how to create a distance filter DSP effect.
|
||||||
|
==============================================================================*/
|
||||||
|
|
||||||
|
#ifdef WIN32
|
||||||
|
#define _CRT_SECURE_NO_WARNINGS
|
||||||
|
#endif
|
||||||
|
|
||||||
|
#include <math.h>
|
||||||
|
#include <stdio.h>
|
||||||
|
#include <string.h>
|
||||||
|
|
||||||
|
#include "fmod.hpp"
|
||||||
|
|
||||||
|
extern "C"
|
||||||
|
{
|
||||||
|
F_EXPORT FMOD_DSP_DESCRIPTION* F_CALL FMODGetDSPDescription();
|
||||||
|
}
|
||||||
|
|
||||||
|
const float FMOD_DISTANCE_FILTER_PARAM_MAX_DISTANCE_MIN = 0.0f;
|
||||||
|
const float FMOD_DISTANCE_FILTER_PARAM_MAX_DISTANCE_MAX = 10000.0f;
|
||||||
|
const float FMOD_DISTANCE_FILTER_PARAM_MAX_DISTANCE_DEFAULT = 20.0f;
|
||||||
|
const float FMOD_DISTANCE_FILTER_PARAM_BANDPASS_FREQUENCY_MIN = 10.0f;
|
||||||
|
const float FMOD_DISTANCE_FILTER_PARAM_BANDPASS_FREQUENCY_MAX = 22000.0f;
|
||||||
|
const float FMOD_DISTANCE_FILTER_PARAM_BANDPASS_FREQUENCY_DEFAULT = 1500.0f;
|
||||||
|
|
||||||
|
enum
|
||||||
|
{
|
||||||
|
FMOD_DISTANCE_FILTER_MAX_DISTANCE,
|
||||||
|
FMOD_DISTANCE_FILTER_BANDPASS_FREQUENCY,
|
||||||
|
FMOD_DISTANCE_FILTER_3D_ATTRIBUTES,
|
||||||
|
FMOD_DISTANCE_FILTER_NUM_PARAMETERS
|
||||||
|
};
|
||||||
|
|
||||||
|
FMOD_RESULT F_CALL FMOD_DistanceFilter_dspcreate (FMOD_DSP_STATE *dsp_state);
|
||||||
|
FMOD_RESULT F_CALL FMOD_DistanceFilter_dsprelease (FMOD_DSP_STATE *dsp_state);
|
||||||
|
FMOD_RESULT F_CALL FMOD_DistanceFilter_dspreset (FMOD_DSP_STATE *dsp_state);
|
||||||
|
FMOD_RESULT F_CALL FMOD_DistanceFilter_dspread (FMOD_DSP_STATE *dsp_state, float *inbuffer, float *outbuffer, unsigned int length, int inchannels, int *outchannels);
|
||||||
|
FMOD_RESULT F_CALL FMOD_DistanceFilter_dspprocess (FMOD_DSP_STATE *dsp_state, unsigned int length, const FMOD_DSP_BUFFER_ARRAY *inbufferarray, FMOD_DSP_BUFFER_ARRAY *outbufferarray, FMOD_BOOL inputsidle, FMOD_DSP_PROCESS_OPERATION op);
|
||||||
|
FMOD_RESULT F_CALL FMOD_DistanceFilter_dspsetparamfloat(FMOD_DSP_STATE *dsp_state, int index, float value);
|
||||||
|
FMOD_RESULT F_CALL FMOD_DistanceFilter_dspsetparamint (FMOD_DSP_STATE *dsp_state, int index, int value);
|
||||||
|
FMOD_RESULT F_CALL FMOD_DistanceFilter_dspsetparambool (FMOD_DSP_STATE *dsp_state, int index, FMOD_BOOL value);
|
||||||
|
FMOD_RESULT F_CALL FMOD_DistanceFilter_dspsetparamdata (FMOD_DSP_STATE *dsp_state, int index, void *data, unsigned int length);
|
||||||
|
FMOD_RESULT F_CALL FMOD_DistanceFilter_dspgetparamfloat(FMOD_DSP_STATE *dsp_state, int index, float *value, char *valuestr);
|
||||||
|
FMOD_RESULT F_CALL FMOD_DistanceFilter_dspgetparamint (FMOD_DSP_STATE *dsp_state, int index, int *value, char *valuestr);
|
||||||
|
FMOD_RESULT F_CALL FMOD_DistanceFilter_dspgetparambool (FMOD_DSP_STATE *dsp_state, int index, FMOD_BOOL *value, char *valuestr);
|
||||||
|
FMOD_RESULT F_CALL FMOD_DistanceFilter_dspgetparamdata (FMOD_DSP_STATE *dsp_state, int index, void **value, unsigned int *length, char *valuestr);
|
||||||
|
FMOD_RESULT F_CALL FMOD_DistanceFilter_shouldiprocess (FMOD_DSP_STATE *dsp_state, FMOD_BOOL inputsidle, unsigned int length, FMOD_CHANNELMASK inmask, int inchannels, FMOD_SPEAKERMODE speakermode);
|
||||||
|
|
||||||
|
static FMOD_DSP_PARAMETER_DESC p_max_distance;
|
||||||
|
static FMOD_DSP_PARAMETER_DESC p_bandpass_frequency;
|
||||||
|
static FMOD_DSP_PARAMETER_DESC p_3d_attributes;
|
||||||
|
|
||||||
|
FMOD_DSP_PARAMETER_DESC *FMOD_DistanceFilter_dspparam[FMOD_DISTANCE_FILTER_NUM_PARAMETERS] =
|
||||||
|
{
|
||||||
|
&p_max_distance,
|
||||||
|
&p_bandpass_frequency,
|
||||||
|
&p_3d_attributes
|
||||||
|
};
|
||||||
|
|
||||||
|
FMOD_DSP_DESCRIPTION FMOD_DistanceFilter_Desc =
|
||||||
|
{
|
||||||
|
FMOD_PLUGIN_SDK_VERSION,
|
||||||
|
"FMOD Distance Filter", // name
|
||||||
|
0x00010000, // plugin version
|
||||||
|
1, // number of input buffers to process
|
||||||
|
1, // number of output buffers to process
|
||||||
|
FMOD_DistanceFilter_dspcreate,
|
||||||
|
FMOD_DistanceFilter_dsprelease,
|
||||||
|
FMOD_DistanceFilter_dspreset,
|
||||||
|
FMOD_DistanceFilter_dspread,
|
||||||
|
0, // FMOD_DistanceFilter_dspprocess, // *** declare this callback instead of FMOD_DistanceFilter_dspread if the plugin sets the output channel count ***
|
||||||
|
0,
|
||||||
|
FMOD_DISTANCE_FILTER_NUM_PARAMETERS,
|
||||||
|
FMOD_DistanceFilter_dspparam,
|
||||||
|
FMOD_DistanceFilter_dspsetparamfloat,
|
||||||
|
0, // FMOD_DistanceFilter_dspsetparamint,
|
||||||
|
0, // FMOD_DistanceFilter_dspsetparambool,
|
||||||
|
FMOD_DistanceFilter_dspsetparamdata,
|
||||||
|
FMOD_DistanceFilter_dspgetparamfloat,
|
||||||
|
0, // FMOD_DistanceFilter_dspgetparamint,
|
||||||
|
0, // FMOD_DistanceFilter_dspgetparambool,
|
||||||
|
FMOD_DistanceFilter_dspgetparamdata,
|
||||||
|
FMOD_DistanceFilter_shouldiprocess,
|
||||||
|
0, // userdata
|
||||||
|
0, // sys_register
|
||||||
|
0, // sys_deregister
|
||||||
|
0 // sys_mix
|
||||||
|
};
|
||||||
|
|
||||||
|
extern "C"
|
||||||
|
{
|
||||||
|
F_EXPORT FMOD_DSP_DESCRIPTION* F_CALL FMODGetDSPDescription()
|
||||||
|
{
|
||||||
|
static float distance_mapping_values[] = { 0, 1, 5, 20, 100, 500, 10000 };
|
||||||
|
static float distance_mapping_scale[] = { 0, 1, 2, 3, 4, 4.5, 5 };
|
||||||
|
|
||||||
|
FMOD_DSP_INIT_PARAMDESC_FLOAT_WITH_MAPPING(p_max_distance, "Max Dist", "", "Distance at which bandpass stops narrowing. 0 to 1000000000. Default = 100", FMOD_DISTANCE_FILTER_PARAM_MAX_DISTANCE_DEFAULT, distance_mapping_values, distance_mapping_scale);
|
||||||
|
FMOD_DSP_INIT_PARAMDESC_FLOAT(p_bandpass_frequency, "Frequency", "Hz", "Bandpass target frequency. 100 to 10,000Hz. Default = 2000Hz", FMOD_DISTANCE_FILTER_PARAM_BANDPASS_FREQUENCY_MIN, FMOD_DISTANCE_FILTER_PARAM_BANDPASS_FREQUENCY_MAX, FMOD_DISTANCE_FILTER_PARAM_BANDPASS_FREQUENCY_DEFAULT);
|
||||||
|
FMOD_DSP_INIT_PARAMDESC_DATA(p_3d_attributes, "3D Attributes", "", "", FMOD_DSP_PARAMETER_DATA_TYPE_3DATTRIBUTES);
|
||||||
|
|
||||||
|
return &FMOD_DistanceFilter_Desc;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
class FMODDistanceFilterState
|
||||||
|
{
|
||||||
|
public:
|
||||||
|
FMODDistanceFilterState() { }
|
||||||
|
|
||||||
|
void init (FMOD_DSP_STATE *dsp_state);
|
||||||
|
void release (FMOD_DSP_STATE *dsp_state);
|
||||||
|
FMOD_RESULT process (float *inbuffer, float *outbuffer, unsigned int length, int channels);
|
||||||
|
FMOD_RESULT process (unsigned int length, const FMOD_DSP_BUFFER_ARRAY *inbufferarray, FMOD_DSP_BUFFER_ARRAY *outbufferarray, FMOD_BOOL inputsidle, FMOD_DSP_PROCESS_OPERATION op);
|
||||||
|
void reset ();
|
||||||
|
void setMaxDistance (float);
|
||||||
|
void setBandpassFrequency(float);
|
||||||
|
void setDistance (float);
|
||||||
|
float maxDistance () const { return m_max_distance; }
|
||||||
|
float bandpassFrequency () const { return m_bandpass_frequency; }
|
||||||
|
|
||||||
|
private:
|
||||||
|
void updateTimeConstants ();
|
||||||
|
|
||||||
|
float m_max_distance;
|
||||||
|
float m_bandpass_frequency;
|
||||||
|
float m_distance;
|
||||||
|
float m_target_highpass_time_const;
|
||||||
|
float m_current_highpass_time_const;
|
||||||
|
float m_target_lowpass_time_const;
|
||||||
|
float m_current_lowpass_time_const;
|
||||||
|
int m_ramp_samples_left;
|
||||||
|
float *m_previous_lp1_out;
|
||||||
|
float *m_previous_lp2_out;
|
||||||
|
float *m_previous_hp_out;
|
||||||
|
int m_sample_rate;
|
||||||
|
int m_max_channels;
|
||||||
|
};
|
||||||
|
|
||||||
|
void FMODDistanceFilterState::init(FMOD_DSP_STATE *dsp_state)
|
||||||
|
{
|
||||||
|
FMOD_DSP_GETSAMPLERATE(dsp_state, &m_sample_rate);
|
||||||
|
|
||||||
|
m_max_channels = 8;
|
||||||
|
m_max_distance = FMOD_DISTANCE_FILTER_PARAM_MAX_DISTANCE_DEFAULT;
|
||||||
|
m_bandpass_frequency = FMOD_DISTANCE_FILTER_PARAM_BANDPASS_FREQUENCY_DEFAULT;
|
||||||
|
m_distance = 0;
|
||||||
|
|
||||||
|
m_previous_lp1_out = (float*)FMOD_DSP_ALLOC(dsp_state, m_max_channels * sizeof(float));
|
||||||
|
m_previous_lp2_out = (float*)FMOD_DSP_ALLOC(dsp_state, m_max_channels * sizeof(float));
|
||||||
|
m_previous_hp_out = (float*)FMOD_DSP_ALLOC(dsp_state, m_max_channels * sizeof(float));
|
||||||
|
|
||||||
|
updateTimeConstants();
|
||||||
|
reset();
|
||||||
|
}
|
||||||
|
|
||||||
|
void FMODDistanceFilterState::release(FMOD_DSP_STATE *dsp_state)
|
||||||
|
{
|
||||||
|
FMOD_DSP_FREE(dsp_state, m_previous_lp1_out);
|
||||||
|
FMOD_DSP_FREE(dsp_state, m_previous_lp2_out);
|
||||||
|
FMOD_DSP_FREE(dsp_state, m_previous_hp_out);
|
||||||
|
}
|
||||||
|
|
||||||
|
FMOD_RESULT FMODDistanceFilterState::process(float *inbuffer, float *outbuffer, unsigned int length, int channels)
|
||||||
|
{
|
||||||
|
if(channels > m_max_channels)
|
||||||
|
{
|
||||||
|
return FMOD_ERR_INVALID_PARAM;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Note: buffers are interleaved
|
||||||
|
static float jitter = (float)1E-20;
|
||||||
|
float lp1_out, lp2_out;
|
||||||
|
int ch;
|
||||||
|
|
||||||
|
float lp_tc = m_current_lowpass_time_const;
|
||||||
|
float hp_tc = m_current_highpass_time_const;
|
||||||
|
|
||||||
|
if (m_ramp_samples_left)
|
||||||
|
{
|
||||||
|
float lp_delta = (m_target_lowpass_time_const - m_current_lowpass_time_const) / m_ramp_samples_left;
|
||||||
|
float hp_delta = (m_target_highpass_time_const - m_current_highpass_time_const) / m_ramp_samples_left;
|
||||||
|
while (length)
|
||||||
|
{
|
||||||
|
if (--m_ramp_samples_left)
|
||||||
|
{
|
||||||
|
lp_tc += lp_delta;
|
||||||
|
hp_tc += hp_delta;
|
||||||
|
for (ch = 0; ch < channels; ++ch)
|
||||||
|
{
|
||||||
|
lp1_out = m_previous_lp1_out[ch] + lp_tc * (*inbuffer++ + jitter - m_previous_lp1_out[ch]);
|
||||||
|
lp2_out = m_previous_lp2_out[ch] + lp_tc * (lp1_out - m_previous_lp2_out[ch]);
|
||||||
|
*outbuffer = hp_tc * (m_previous_hp_out[ch] + lp2_out - m_previous_lp2_out[ch]);
|
||||||
|
|
||||||
|
m_previous_lp1_out[ch] = lp1_out;
|
||||||
|
m_previous_lp2_out[ch] = lp2_out;
|
||||||
|
m_previous_hp_out[ch] = *outbuffer++;
|
||||||
|
}
|
||||||
|
jitter = -jitter;
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
lp_tc = m_target_lowpass_time_const;
|
||||||
|
hp_tc = m_target_highpass_time_const;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
--length;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
while (length--)
|
||||||
|
{
|
||||||
|
for (ch = 0; ch < channels; ++ch)
|
||||||
|
{
|
||||||
|
lp1_out = m_previous_lp1_out[ch] + lp_tc * (*inbuffer++ + jitter - m_previous_lp1_out[ch]);
|
||||||
|
lp2_out = m_previous_lp2_out[ch] + lp_tc * (lp1_out - m_previous_lp2_out[ch]);
|
||||||
|
*outbuffer = hp_tc * (m_previous_hp_out[ch] + lp2_out - m_previous_lp2_out[ch]);
|
||||||
|
|
||||||
|
m_previous_lp1_out[ch] = lp1_out;
|
||||||
|
m_previous_lp2_out[ch] = lp2_out;
|
||||||
|
m_previous_hp_out[ch] = *outbuffer++;
|
||||||
|
}
|
||||||
|
jitter = -jitter;
|
||||||
|
}
|
||||||
|
|
||||||
|
m_current_lowpass_time_const = lp_tc;
|
||||||
|
m_current_highpass_time_const = hp_tc;
|
||||||
|
|
||||||
|
return FMOD_OK;
|
||||||
|
}
|
||||||
|
|
||||||
|
FMOD_RESULT FMODDistanceFilterState::process(unsigned int length, const FMOD_DSP_BUFFER_ARRAY *inbufferarray, FMOD_DSP_BUFFER_ARRAY *outbufferarray, FMOD_BOOL inputsidle, FMOD_DSP_PROCESS_OPERATION op)
|
||||||
|
{
|
||||||
|
if (op == FMOD_DSP_PROCESS_QUERY)
|
||||||
|
{
|
||||||
|
FMOD_SPEAKERMODE outmode;
|
||||||
|
int outchannels;
|
||||||
|
|
||||||
|
#if 1 // For stereo output
|
||||||
|
{
|
||||||
|
outmode = FMOD_SPEAKERMODE_STEREO;
|
||||||
|
outchannels = 2;
|
||||||
|
}
|
||||||
|
#else // For 5.1 output
|
||||||
|
{
|
||||||
|
outmode = FMOD_SPEAKERMODE_5POINT1;
|
||||||
|
outchannels = 6;
|
||||||
|
}
|
||||||
|
#endif
|
||||||
|
|
||||||
|
if (outbufferarray)
|
||||||
|
{
|
||||||
|
outbufferarray->speakermode = outmode;
|
||||||
|
outbufferarray->buffernumchannels[0] = outchannels;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (inputsidle)
|
||||||
|
{
|
||||||
|
return FMOD_ERR_DSP_DONTPROCESS;
|
||||||
|
}
|
||||||
|
|
||||||
|
return FMOD_OK;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Do processing here
|
||||||
|
float *inbuffer = inbufferarray->buffers[0];
|
||||||
|
float *outbuffer = outbufferarray->buffers[0];
|
||||||
|
int inchannels = inbufferarray->buffernumchannels[0];
|
||||||
|
int outchannels = outbufferarray->buffernumchannels[0];
|
||||||
|
|
||||||
|
while(length--)
|
||||||
|
{
|
||||||
|
// MAIN DSP LOOP...
|
||||||
|
}
|
||||||
|
|
||||||
|
return FMOD_OK;
|
||||||
|
}
|
||||||
|
|
||||||
|
void FMODDistanceFilterState::reset()
|
||||||
|
{
|
||||||
|
m_current_lowpass_time_const = m_target_lowpass_time_const;
|
||||||
|
m_current_highpass_time_const = m_target_highpass_time_const;
|
||||||
|
m_ramp_samples_left = 0;
|
||||||
|
|
||||||
|
memset(m_previous_lp1_out, 0, m_max_channels * sizeof(float));
|
||||||
|
memset(m_previous_lp2_out, 0, m_max_channels * sizeof(float));
|
||||||
|
memset(m_previous_hp_out, 0, m_max_channels * sizeof(float));
|
||||||
|
}
|
||||||
|
|
||||||
|
void FMODDistanceFilterState::setMaxDistance(float distance)
|
||||||
|
{
|
||||||
|
m_max_distance = distance;
|
||||||
|
updateTimeConstants();
|
||||||
|
}
|
||||||
|
|
||||||
|
void FMODDistanceFilterState::setBandpassFrequency(float frequency)
|
||||||
|
{
|
||||||
|
m_bandpass_frequency = frequency;
|
||||||
|
updateTimeConstants();
|
||||||
|
}
|
||||||
|
|
||||||
|
void FMODDistanceFilterState::setDistance(float distance)
|
||||||
|
{
|
||||||
|
m_distance = distance;
|
||||||
|
updateTimeConstants();
|
||||||
|
}
|
||||||
|
|
||||||
|
void FMODDistanceFilterState::updateTimeConstants()
|
||||||
|
{
|
||||||
|
#define PI (3.14159265358979323846f)
|
||||||
|
#define MIN_CUTOFF (10.0f)
|
||||||
|
#define MAX_CUTOFF (22000.0f)
|
||||||
|
|
||||||
|
float dist_factor = m_distance >= m_max_distance ? 1.0f : m_distance / m_max_distance;
|
||||||
|
float lp_cutoff = m_bandpass_frequency + (1.0f - dist_factor) * (1.0f - dist_factor) * (MAX_CUTOFF - m_bandpass_frequency);
|
||||||
|
float hp_cutoff = MIN_CUTOFF + dist_factor * dist_factor * (m_bandpass_frequency - MIN_CUTOFF);
|
||||||
|
|
||||||
|
float dt = 1.0f / m_sample_rate;
|
||||||
|
float threshold = m_sample_rate / PI;
|
||||||
|
|
||||||
|
if (lp_cutoff >= MAX_CUTOFF)
|
||||||
|
{
|
||||||
|
m_target_lowpass_time_const = 1.0f;
|
||||||
|
}
|
||||||
|
else if (lp_cutoff <= threshold)
|
||||||
|
{
|
||||||
|
float RC = 1.0f / (2.0f * PI * lp_cutoff);
|
||||||
|
m_target_lowpass_time_const = dt / (RC + dt);
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
m_target_lowpass_time_const = 0.666666667f + (lp_cutoff - threshold) / (3.0f * (MAX_CUTOFF - threshold));
|
||||||
|
}
|
||||||
|
|
||||||
|
if (hp_cutoff >= MAX_CUTOFF)
|
||||||
|
{
|
||||||
|
m_target_highpass_time_const = 0.0f;
|
||||||
|
}
|
||||||
|
else if (hp_cutoff <= threshold)
|
||||||
|
{
|
||||||
|
float RC = 1.0f / (2.0f * PI * hp_cutoff);
|
||||||
|
m_target_highpass_time_const = RC / (RC + dt);
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
m_target_highpass_time_const = (MAX_CUTOFF - hp_cutoff) / (3.0f * (MAX_CUTOFF - threshold));
|
||||||
|
}
|
||||||
|
|
||||||
|
m_ramp_samples_left = 256;
|
||||||
|
}
|
||||||
|
|
||||||
|
FMOD_RESULT F_CALL FMOD_DistanceFilter_dspcreate(FMOD_DSP_STATE *dsp_state)
|
||||||
|
{
|
||||||
|
FMODDistanceFilterState* state = (FMODDistanceFilterState *)FMOD_DSP_ALLOC(dsp_state, sizeof(FMODDistanceFilterState));
|
||||||
|
state->init(dsp_state);
|
||||||
|
dsp_state->plugindata = state;
|
||||||
|
if (!state)
|
||||||
|
{
|
||||||
|
return FMOD_ERR_MEMORY;
|
||||||
|
}
|
||||||
|
return FMOD_OK;
|
||||||
|
}
|
||||||
|
|
||||||
|
FMOD_RESULT F_CALL FMOD_DistanceFilter_dsprelease(FMOD_DSP_STATE *dsp_state)
|
||||||
|
{
|
||||||
|
FMODDistanceFilterState *state = (FMODDistanceFilterState *)dsp_state->plugindata;
|
||||||
|
state->release(dsp_state);
|
||||||
|
FMOD_DSP_FREE(dsp_state, state);
|
||||||
|
return FMOD_OK;
|
||||||
|
}
|
||||||
|
|
||||||
|
FMOD_RESULT F_CALL FMOD_DistanceFilter_dspread(FMOD_DSP_STATE *dsp_state, float *inbuffer, float *outbuffer, unsigned int length, int inchannels, int * /*outchannels*/)
|
||||||
|
{
|
||||||
|
FMODDistanceFilterState *state = (FMODDistanceFilterState *)dsp_state->plugindata;
|
||||||
|
return state->process(inbuffer, outbuffer, length, inchannels); // input and output channels count match for this effect
|
||||||
|
}
|
||||||
|
|
||||||
|
FMOD_RESULT F_CALL FMOD_DistanceFilter_dspprocess(FMOD_DSP_STATE *dsp_state, unsigned int length, const FMOD_DSP_BUFFER_ARRAY *inbufferarray, FMOD_DSP_BUFFER_ARRAY *outbufferarray, FMOD_BOOL inputsidle, FMOD_DSP_PROCESS_OPERATION op)
|
||||||
|
{
|
||||||
|
FMODDistanceFilterState *state = (FMODDistanceFilterState *)dsp_state->plugindata;
|
||||||
|
return state->process(length, inbufferarray, outbufferarray, inputsidle, op); // as an example for plugins which set the output channel count
|
||||||
|
}
|
||||||
|
|
||||||
|
FMOD_RESULT F_CALL FMOD_DistanceFilter_dspreset(FMOD_DSP_STATE *dsp_state)
|
||||||
|
{
|
||||||
|
FMODDistanceFilterState *state = (FMODDistanceFilterState *)dsp_state->plugindata;
|
||||||
|
state->reset();
|
||||||
|
return FMOD_OK;
|
||||||
|
}
|
||||||
|
|
||||||
|
FMOD_RESULT F_CALL FMOD_DistanceFilter_dspsetparamfloat(FMOD_DSP_STATE *dsp_state, int index, float value)
|
||||||
|
{
|
||||||
|
FMODDistanceFilterState *state = (FMODDistanceFilterState *)dsp_state->plugindata;
|
||||||
|
|
||||||
|
switch (index)
|
||||||
|
{
|
||||||
|
case FMOD_DISTANCE_FILTER_MAX_DISTANCE:
|
||||||
|
state->setMaxDistance(value);
|
||||||
|
return FMOD_OK;
|
||||||
|
|
||||||
|
case FMOD_DISTANCE_FILTER_BANDPASS_FREQUENCY:
|
||||||
|
state->setBandpassFrequency(value);
|
||||||
|
return FMOD_OK;
|
||||||
|
}
|
||||||
|
|
||||||
|
return FMOD_ERR_INVALID_PARAM;
|
||||||
|
}
|
||||||
|
|
||||||
|
FMOD_RESULT F_CALL FMOD_DistanceFilter_dspgetparamfloat(FMOD_DSP_STATE *dsp_state, int index, float *value, char *valuestr)
|
||||||
|
{
|
||||||
|
FMODDistanceFilterState *state = (FMODDistanceFilterState *)dsp_state->plugindata;
|
||||||
|
|
||||||
|
switch (index)
|
||||||
|
{
|
||||||
|
case FMOD_DISTANCE_FILTER_MAX_DISTANCE:
|
||||||
|
*value = state->maxDistance();
|
||||||
|
if (valuestr) snprintf(valuestr, FMOD_DSP_GETPARAM_VALUESTR_LENGTH, "%.1f", state->maxDistance());
|
||||||
|
return FMOD_OK;
|
||||||
|
|
||||||
|
case FMOD_DISTANCE_FILTER_BANDPASS_FREQUENCY:
|
||||||
|
*value = state->bandpassFrequency();
|
||||||
|
if (valuestr) snprintf(valuestr, FMOD_DSP_GETPARAM_VALUESTR_LENGTH, "%.1f Hz", state->bandpassFrequency());
|
||||||
|
return FMOD_OK;
|
||||||
|
}
|
||||||
|
|
||||||
|
return FMOD_ERR_INVALID_PARAM;
|
||||||
|
}
|
||||||
|
|
||||||
|
FMOD_RESULT F_CALL FMOD_DistanceFilter_dspsetparamdata(FMOD_DSP_STATE *dsp_state, int index, void *data, unsigned int /*length*/)
|
||||||
|
{
|
||||||
|
FMODDistanceFilterState *state = (FMODDistanceFilterState *)dsp_state->plugindata;
|
||||||
|
|
||||||
|
switch (index)
|
||||||
|
{
|
||||||
|
case FMOD_DISTANCE_FILTER_3D_ATTRIBUTES:
|
||||||
|
FMOD_DSP_PARAMETER_3DATTRIBUTES* param = (FMOD_DSP_PARAMETER_3DATTRIBUTES*)data;
|
||||||
|
state->setDistance(sqrtf(param->relative.position.x * param->relative.position.x + param->relative.position.y * param->relative.position.y + param->relative.position.z * param->relative.position.z));
|
||||||
|
return FMOD_OK;
|
||||||
|
}
|
||||||
|
|
||||||
|
return FMOD_ERR_INVALID_PARAM;
|
||||||
|
}
|
||||||
|
|
||||||
|
FMOD_RESULT F_CALL FMOD_DistanceFilter_dspgetparamdata(FMOD_DSP_STATE * /*dsp_state*/, int index, void ** /*value*/, unsigned int * /*length*/, char * /*valuestr*/)
|
||||||
|
{
|
||||||
|
switch (index)
|
||||||
|
{
|
||||||
|
case FMOD_DISTANCE_FILTER_3D_ATTRIBUTES:
|
||||||
|
return FMOD_ERR_INVALID_PARAM;
|
||||||
|
}
|
||||||
|
|
||||||
|
return FMOD_ERR_INVALID_PARAM;
|
||||||
|
}
|
||||||
|
|
||||||
|
FMOD_RESULT F_CALL FMOD_DistanceFilter_shouldiprocess(FMOD_DSP_STATE * /*dsp_state*/, FMOD_BOOL inputsidle, unsigned int /*length*/, FMOD_CHANNELMASK /*inmask*/, int /*inchannels*/, FMOD_SPEAKERMODE /*speakermode*/)
|
||||||
|
{
|
||||||
|
if (inputsidle)
|
||||||
|
{
|
||||||
|
return FMOD_ERR_DSP_DONTPROCESS;
|
||||||
|
}
|
||||||
|
|
||||||
|
return FMOD_OK;
|
||||||
|
}
|
||||||
@@ -0,0 +1,360 @@
|
|||||||
|
/*==============================================================================
|
||||||
|
Gain DSP Plugin Example
|
||||||
|
Copyright (c), Firelight Technologies Pty, Ltd 2004-2026.
|
||||||
|
|
||||||
|
This example shows how to create a simple gain DSP effect.
|
||||||
|
==============================================================================*/
|
||||||
|
|
||||||
|
#ifdef WIN32
|
||||||
|
#define _CRT_SECURE_NO_WARNINGS
|
||||||
|
#endif
|
||||||
|
|
||||||
|
#include <math.h>
|
||||||
|
#include <stdio.h>
|
||||||
|
#include <string.h>
|
||||||
|
|
||||||
|
#include "fmod.hpp"
|
||||||
|
|
||||||
|
#define FMOD_GAIN_USEPROCESSCALLBACK /* FMOD plugins have 2 methods of processing data.
|
||||||
|
1. via a 'read' callback which is compatible with FMOD Ex but limited in functionality, or
|
||||||
|
2. via a 'process' callback which exposes more functionality, like masks and query before process early out logic. */
|
||||||
|
|
||||||
|
extern "C" {
|
||||||
|
F_EXPORT FMOD_DSP_DESCRIPTION* F_CALL FMODGetDSPDescription();
|
||||||
|
}
|
||||||
|
|
||||||
|
const float FMOD_GAIN_PARAM_GAIN_MIN = -80.0f;
|
||||||
|
const float FMOD_GAIN_PARAM_GAIN_MAX = 10.0f;
|
||||||
|
const float FMOD_GAIN_PARAM_GAIN_DEFAULT = 0.0f;
|
||||||
|
#define FMOD_GAIN_RAMPCOUNT 256
|
||||||
|
|
||||||
|
enum
|
||||||
|
{
|
||||||
|
FMOD_GAIN_PARAM_GAIN = 0,
|
||||||
|
FMOD_GAIN_PARAM_INVERT,
|
||||||
|
FMOD_GAIN_NUM_PARAMETERS
|
||||||
|
};
|
||||||
|
|
||||||
|
#define DECIBELS_TO_LINEAR(__dbval__) ((__dbval__ <= FMOD_GAIN_PARAM_GAIN_MIN) ? 0.0f : powf(10.0f, __dbval__ / 20.0f))
|
||||||
|
#define LINEAR_TO_DECIBELS(__linval__) ((__linval__ <= 0.0f) ? FMOD_GAIN_PARAM_GAIN_MIN : 20.0f * log10f((float)__linval__))
|
||||||
|
|
||||||
|
FMOD_RESULT F_CALL FMOD_Gain_dspcreate (FMOD_DSP_STATE *dsp_state);
|
||||||
|
FMOD_RESULT F_CALL FMOD_Gain_dsprelease (FMOD_DSP_STATE *dsp_state);
|
||||||
|
FMOD_RESULT F_CALL FMOD_Gain_dspreset (FMOD_DSP_STATE *dsp_state);
|
||||||
|
#ifdef FMOD_GAIN_USEPROCESSCALLBACK
|
||||||
|
FMOD_RESULT F_CALL FMOD_Gain_dspprocess (FMOD_DSP_STATE *dsp_state, unsigned int length, const FMOD_DSP_BUFFER_ARRAY *inbufferarray, FMOD_DSP_BUFFER_ARRAY *outbufferarray, FMOD_BOOL inputsidle, FMOD_DSP_PROCESS_OPERATION op);
|
||||||
|
#else
|
||||||
|
FMOD_RESULT F_CALL FMOD_Gain_dspread (FMOD_DSP_STATE *dsp_state, float *inbuffer, float *outbuffer, unsigned int length, int inchannels, int *outchannels);
|
||||||
|
#endif
|
||||||
|
FMOD_RESULT F_CALL FMOD_Gain_dspsetparamfloat(FMOD_DSP_STATE *dsp_state, int index, float value);
|
||||||
|
FMOD_RESULT F_CALL FMOD_Gain_dspsetparamint (FMOD_DSP_STATE *dsp_state, int index, int value);
|
||||||
|
FMOD_RESULT F_CALL FMOD_Gain_dspsetparambool (FMOD_DSP_STATE *dsp_state, int index, FMOD_BOOL value);
|
||||||
|
FMOD_RESULT F_CALL FMOD_Gain_dspsetparamdata (FMOD_DSP_STATE *dsp_state, int index, void *data, unsigned int length);
|
||||||
|
FMOD_RESULT F_CALL FMOD_Gain_dspgetparamfloat(FMOD_DSP_STATE *dsp_state, int index, float *value, char *valuestr);
|
||||||
|
FMOD_RESULT F_CALL FMOD_Gain_dspgetparamint (FMOD_DSP_STATE *dsp_state, int index, int *value, char *valuestr);
|
||||||
|
FMOD_RESULT F_CALL FMOD_Gain_dspgetparambool (FMOD_DSP_STATE *dsp_state, int index, FMOD_BOOL *value, char *valuestr);
|
||||||
|
FMOD_RESULT F_CALL FMOD_Gain_dspgetparamdata (FMOD_DSP_STATE *dsp_state, int index, void **value, unsigned int *length, char *valuestr);
|
||||||
|
FMOD_RESULT F_CALL FMOD_Gain_shouldiprocess (FMOD_DSP_STATE *dsp_state, FMOD_BOOL inputsidle, unsigned int length, FMOD_CHANNELMASK inmask, int inchannels, FMOD_SPEAKERMODE speakermode);
|
||||||
|
FMOD_RESULT F_CALL FMOD_Gain_sys_register (FMOD_DSP_STATE *dsp_state);
|
||||||
|
FMOD_RESULT F_CALL FMOD_Gain_sys_deregister (FMOD_DSP_STATE *dsp_state);
|
||||||
|
FMOD_RESULT F_CALL FMOD_Gain_sys_mix (FMOD_DSP_STATE *dsp_state, int stage);
|
||||||
|
|
||||||
|
static bool FMOD_Gain_Running = false;
|
||||||
|
static FMOD_DSP_PARAMETER_DESC p_gain;
|
||||||
|
static FMOD_DSP_PARAMETER_DESC p_invert;
|
||||||
|
|
||||||
|
FMOD_DSP_PARAMETER_DESC *FMOD_Gain_dspparam[FMOD_GAIN_NUM_PARAMETERS] =
|
||||||
|
{
|
||||||
|
&p_gain,
|
||||||
|
&p_invert
|
||||||
|
};
|
||||||
|
|
||||||
|
FMOD_DSP_DESCRIPTION FMOD_Gain_Desc =
|
||||||
|
{
|
||||||
|
FMOD_PLUGIN_SDK_VERSION,
|
||||||
|
"FMOD Gain", // name
|
||||||
|
0x00010000, // plug-in version
|
||||||
|
1, // number of input buffers to process
|
||||||
|
1, // number of output buffers to process
|
||||||
|
FMOD_Gain_dspcreate,
|
||||||
|
FMOD_Gain_dsprelease,
|
||||||
|
FMOD_Gain_dspreset,
|
||||||
|
#ifndef FMOD_GAIN_USEPROCESSCALLBACK
|
||||||
|
FMOD_Gain_dspread,
|
||||||
|
#else
|
||||||
|
0,
|
||||||
|
#endif
|
||||||
|
#ifdef FMOD_GAIN_USEPROCESSCALLBACK
|
||||||
|
FMOD_Gain_dspprocess,
|
||||||
|
#else
|
||||||
|
0,
|
||||||
|
#endif
|
||||||
|
0,
|
||||||
|
FMOD_GAIN_NUM_PARAMETERS,
|
||||||
|
FMOD_Gain_dspparam,
|
||||||
|
FMOD_Gain_dspsetparamfloat,
|
||||||
|
0, // FMOD_Gain_dspsetparamint,
|
||||||
|
FMOD_Gain_dspsetparambool,
|
||||||
|
0, // FMOD_Gain_dspsetparamdata,
|
||||||
|
FMOD_Gain_dspgetparamfloat,
|
||||||
|
0, // FMOD_Gain_dspgetparamint,
|
||||||
|
FMOD_Gain_dspgetparambool,
|
||||||
|
0, // FMOD_Gain_dspgetparamdata,
|
||||||
|
FMOD_Gain_shouldiprocess,
|
||||||
|
0, // userdata
|
||||||
|
FMOD_Gain_sys_register,
|
||||||
|
FMOD_Gain_sys_deregister,
|
||||||
|
FMOD_Gain_sys_mix
|
||||||
|
};
|
||||||
|
|
||||||
|
extern "C"
|
||||||
|
{
|
||||||
|
|
||||||
|
F_EXPORT FMOD_DSP_DESCRIPTION* F_CALL FMODGetDSPDescription()
|
||||||
|
{
|
||||||
|
static float gain_mapping_values[] = { -80, -50, -30, -10, 10 };
|
||||||
|
static float gain_mapping_scale[] = { 0, 2, 4, 7, 11 };
|
||||||
|
|
||||||
|
FMOD_DSP_INIT_PARAMDESC_FLOAT_WITH_MAPPING(p_gain, "Gain", "dB", "Gain in dB. -80 to 10. Default = 0", FMOD_GAIN_PARAM_GAIN_DEFAULT, gain_mapping_values, gain_mapping_scale);
|
||||||
|
FMOD_DSP_INIT_PARAMDESC_BOOL(p_invert, "Invert", "", "Invert signal. Default = off", false, 0);
|
||||||
|
return &FMOD_Gain_Desc;
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
class FMODGainState
|
||||||
|
{
|
||||||
|
public:
|
||||||
|
FMODGainState();
|
||||||
|
|
||||||
|
void read(float *inbuffer, float *outbuffer, unsigned int length, int channels);
|
||||||
|
void reset();
|
||||||
|
void setGain(float);
|
||||||
|
void setInvert(bool);
|
||||||
|
float gain() const { return LINEAR_TO_DECIBELS(m_invert ? -m_target_gain : m_target_gain); }
|
||||||
|
FMOD_BOOL invert() const { return m_invert; }
|
||||||
|
|
||||||
|
private:
|
||||||
|
float m_target_gain;
|
||||||
|
float m_current_gain;
|
||||||
|
int m_ramp_samples_left;
|
||||||
|
bool m_invert;
|
||||||
|
};
|
||||||
|
|
||||||
|
FMODGainState::FMODGainState()
|
||||||
|
{
|
||||||
|
m_target_gain = DECIBELS_TO_LINEAR(FMOD_GAIN_PARAM_GAIN_DEFAULT);
|
||||||
|
m_invert = 0;
|
||||||
|
reset();
|
||||||
|
}
|
||||||
|
|
||||||
|
void FMODGainState::read(float *inbuffer, float *outbuffer, unsigned int length, int channels)
|
||||||
|
{
|
||||||
|
// Note: buffers are interleaved
|
||||||
|
float gain = m_current_gain;
|
||||||
|
|
||||||
|
if (m_ramp_samples_left)
|
||||||
|
{
|
||||||
|
float target = m_target_gain;
|
||||||
|
float delta = (target - gain) / m_ramp_samples_left;
|
||||||
|
while (length)
|
||||||
|
{
|
||||||
|
if (--m_ramp_samples_left)
|
||||||
|
{
|
||||||
|
gain += delta;
|
||||||
|
for (int i = 0; i < channels; ++i)
|
||||||
|
{
|
||||||
|
*outbuffer++ = *inbuffer++ * gain;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
gain = target;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
--length;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
unsigned int samples = length * channels;
|
||||||
|
while (samples--)
|
||||||
|
{
|
||||||
|
*outbuffer++ = *inbuffer++ * gain;
|
||||||
|
}
|
||||||
|
|
||||||
|
m_current_gain = gain;
|
||||||
|
}
|
||||||
|
|
||||||
|
void FMODGainState::reset()
|
||||||
|
{
|
||||||
|
m_current_gain = m_target_gain;
|
||||||
|
m_ramp_samples_left = 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
void FMODGainState::setGain(float gain)
|
||||||
|
{
|
||||||
|
m_target_gain = m_invert ? -DECIBELS_TO_LINEAR(gain) : DECIBELS_TO_LINEAR(gain);
|
||||||
|
m_ramp_samples_left = FMOD_GAIN_RAMPCOUNT;
|
||||||
|
}
|
||||||
|
|
||||||
|
void FMODGainState::setInvert(bool invert)
|
||||||
|
{
|
||||||
|
if (invert != m_invert)
|
||||||
|
{
|
||||||
|
m_target_gain = -m_target_gain;
|
||||||
|
m_ramp_samples_left = FMOD_GAIN_RAMPCOUNT;
|
||||||
|
}
|
||||||
|
m_invert = invert;
|
||||||
|
}
|
||||||
|
|
||||||
|
FMOD_RESULT F_CALL FMOD_Gain_dspcreate(FMOD_DSP_STATE *dsp_state)
|
||||||
|
{
|
||||||
|
dsp_state->plugindata = (FMODGainState *)FMOD_DSP_ALLOC(dsp_state, sizeof(FMODGainState));
|
||||||
|
if (!dsp_state->plugindata)
|
||||||
|
{
|
||||||
|
return FMOD_ERR_MEMORY;
|
||||||
|
}
|
||||||
|
return FMOD_OK;
|
||||||
|
}
|
||||||
|
|
||||||
|
FMOD_RESULT F_CALL FMOD_Gain_dsprelease(FMOD_DSP_STATE *dsp_state)
|
||||||
|
{
|
||||||
|
FMODGainState *state = (FMODGainState *)dsp_state->plugindata;
|
||||||
|
FMOD_DSP_FREE(dsp_state, state);
|
||||||
|
return FMOD_OK;
|
||||||
|
}
|
||||||
|
|
||||||
|
#ifdef FMOD_GAIN_USEPROCESSCALLBACK
|
||||||
|
|
||||||
|
FMOD_RESULT F_CALL FMOD_Gain_dspprocess(FMOD_DSP_STATE *dsp_state, unsigned int length, const FMOD_DSP_BUFFER_ARRAY *inbufferarray, FMOD_DSP_BUFFER_ARRAY *outbufferarray, FMOD_BOOL inputsidle, FMOD_DSP_PROCESS_OPERATION op)
|
||||||
|
{
|
||||||
|
FMODGainState *state = (FMODGainState *)dsp_state->plugindata;
|
||||||
|
|
||||||
|
if (op == FMOD_DSP_PROCESS_QUERY)
|
||||||
|
{
|
||||||
|
if (outbufferarray && inbufferarray)
|
||||||
|
{
|
||||||
|
outbufferarray[0].buffernumchannels[0] = inbufferarray[0].buffernumchannels[0];
|
||||||
|
outbufferarray[0].speakermode = inbufferarray[0].speakermode;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (inputsidle)
|
||||||
|
{
|
||||||
|
return FMOD_ERR_DSP_DONTPROCESS;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
state->read(inbufferarray[0].buffers[0], outbufferarray[0].buffers[0], length, inbufferarray[0].buffernumchannels[0]); // input and output channels count match for this effect
|
||||||
|
}
|
||||||
|
|
||||||
|
return FMOD_OK;
|
||||||
|
}
|
||||||
|
|
||||||
|
#else
|
||||||
|
|
||||||
|
FMOD_RESULT F_CALL FMOD_Gain_dspread(FMOD_DSP_STATE *dsp_state, float *inbuffer, float *outbuffer, unsigned int length, int inchannels, int * /*outchannels*/)
|
||||||
|
{
|
||||||
|
FMODGainState *state = (FMODGainState *)dsp_state->plugindata;
|
||||||
|
state->read(inbuffer, outbuffer, length, inchannels); // input and output channels count match for this effect
|
||||||
|
return FMOD_OK;
|
||||||
|
}
|
||||||
|
|
||||||
|
#endif
|
||||||
|
|
||||||
|
FMOD_RESULT F_CALL FMOD_Gain_dspreset(FMOD_DSP_STATE *dsp_state)
|
||||||
|
{
|
||||||
|
FMODGainState *state = (FMODGainState *)dsp_state->plugindata;
|
||||||
|
state->reset();
|
||||||
|
return FMOD_OK;
|
||||||
|
}
|
||||||
|
|
||||||
|
FMOD_RESULT F_CALL FMOD_Gain_dspsetparamfloat(FMOD_DSP_STATE *dsp_state, int index, float value)
|
||||||
|
{
|
||||||
|
FMODGainState *state = (FMODGainState *)dsp_state->plugindata;
|
||||||
|
|
||||||
|
switch (index)
|
||||||
|
{
|
||||||
|
case FMOD_GAIN_PARAM_GAIN:
|
||||||
|
state->setGain(value);
|
||||||
|
return FMOD_OK;
|
||||||
|
}
|
||||||
|
|
||||||
|
return FMOD_ERR_INVALID_PARAM;
|
||||||
|
}
|
||||||
|
|
||||||
|
FMOD_RESULT F_CALL FMOD_Gain_dspgetparamfloat(FMOD_DSP_STATE *dsp_state, int index, float *value, char *valuestr)
|
||||||
|
{
|
||||||
|
FMODGainState *state = (FMODGainState *)dsp_state->plugindata;
|
||||||
|
|
||||||
|
switch (index)
|
||||||
|
{
|
||||||
|
case FMOD_GAIN_PARAM_GAIN:
|
||||||
|
*value = state->gain();
|
||||||
|
if (valuestr) snprintf(valuestr, FMOD_DSP_GETPARAM_VALUESTR_LENGTH, "%.1f dB", state->gain());
|
||||||
|
return FMOD_OK;
|
||||||
|
}
|
||||||
|
|
||||||
|
return FMOD_ERR_INVALID_PARAM;
|
||||||
|
}
|
||||||
|
|
||||||
|
FMOD_RESULT F_CALL FMOD_Gain_dspsetparambool(FMOD_DSP_STATE *dsp_state, int index, FMOD_BOOL value)
|
||||||
|
{
|
||||||
|
FMODGainState *state = (FMODGainState *)dsp_state->plugindata;
|
||||||
|
|
||||||
|
switch (index)
|
||||||
|
{
|
||||||
|
case FMOD_GAIN_PARAM_INVERT:
|
||||||
|
state->setInvert(value ? true : false);
|
||||||
|
return FMOD_OK;
|
||||||
|
}
|
||||||
|
|
||||||
|
return FMOD_ERR_INVALID_PARAM;
|
||||||
|
}
|
||||||
|
|
||||||
|
FMOD_RESULT F_CALL FMOD_Gain_dspgetparambool(FMOD_DSP_STATE *dsp_state, int index, FMOD_BOOL *value, char *valuestr)
|
||||||
|
{
|
||||||
|
FMODGainState *state = (FMODGainState *)dsp_state->plugindata;
|
||||||
|
|
||||||
|
switch (index)
|
||||||
|
{
|
||||||
|
case FMOD_GAIN_PARAM_INVERT:
|
||||||
|
*value = state->invert();
|
||||||
|
if (valuestr) snprintf(valuestr, FMOD_DSP_GETPARAM_VALUESTR_LENGTH, state->invert() ? "Inverted" : "Off" );
|
||||||
|
return FMOD_OK;
|
||||||
|
}
|
||||||
|
|
||||||
|
return FMOD_ERR_INVALID_PARAM;
|
||||||
|
}
|
||||||
|
|
||||||
|
FMOD_RESULT F_CALL FMOD_Gain_shouldiprocess(FMOD_DSP_STATE * /*dsp_state*/, FMOD_BOOL inputsidle, unsigned int /*length*/, FMOD_CHANNELMASK /*inmask*/, int /*inchannels*/, FMOD_SPEAKERMODE /*speakermode*/)
|
||||||
|
{
|
||||||
|
if (inputsidle)
|
||||||
|
{
|
||||||
|
return FMOD_ERR_DSP_DONTPROCESS;
|
||||||
|
}
|
||||||
|
|
||||||
|
return FMOD_OK;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
FMOD_RESULT F_CALL FMOD_Gain_sys_register(FMOD_DSP_STATE * /*dsp_state*/)
|
||||||
|
{
|
||||||
|
FMOD_Gain_Running = true;
|
||||||
|
// called once for this type of dsp being loaded or registered (it is not per instance)
|
||||||
|
return FMOD_OK;
|
||||||
|
}
|
||||||
|
|
||||||
|
FMOD_RESULT F_CALL FMOD_Gain_sys_deregister(FMOD_DSP_STATE * /*dsp_state*/)
|
||||||
|
{
|
||||||
|
FMOD_Gain_Running = false;
|
||||||
|
// called once for this type of dsp being unloaded or de-registered (it is not per instance)
|
||||||
|
return FMOD_OK;
|
||||||
|
}
|
||||||
|
|
||||||
|
FMOD_RESULT F_CALL FMOD_Gain_sys_mix(FMOD_DSP_STATE * /*dsp_state*/, int /*stage*/)
|
||||||
|
{
|
||||||
|
// stage == 0 , before all dsps are processed/mixed, this callback is called once for this type.
|
||||||
|
// stage == 1 , after all dsps are processed/mixed, this callback is called once for this type.
|
||||||
|
return FMOD_OK;
|
||||||
|
}
|
||||||
@@ -0,0 +1,302 @@
|
|||||||
|
/*==============================================================================
|
||||||
|
Plugin Example
|
||||||
|
Copyright (c), Firelight Technologies Pty, Ltd 2004-2026.
|
||||||
|
|
||||||
|
This example shows how to created a plugin effect.
|
||||||
|
==============================================================================*/
|
||||||
|
|
||||||
|
#include <math.h>
|
||||||
|
#include <stdlib.h>
|
||||||
|
#include <stdio.h>
|
||||||
|
#include <string.h>
|
||||||
|
|
||||||
|
#include "fmod.hpp"
|
||||||
|
|
||||||
|
extern "C" {
|
||||||
|
F_EXPORT FMOD_DSP_DESCRIPTION* F_CALL FMODGetDSPDescription();
|
||||||
|
}
|
||||||
|
|
||||||
|
const float FMOD_NOISE_PARAM_GAIN_MIN = -80.0f;
|
||||||
|
const float FMOD_NOISE_PARAM_GAIN_MAX = 10.0f;
|
||||||
|
const float FMOD_NOISE_PARAM_GAIN_DEFAULT = 0.0f;
|
||||||
|
|
||||||
|
#define FMOD_NOISE_RAMPCOUNT 256
|
||||||
|
|
||||||
|
enum
|
||||||
|
{
|
||||||
|
FMOD_NOISE_PARAM_LEVEL = 0,
|
||||||
|
FMOD_NOISE_PARAM_FORMAT,
|
||||||
|
FMOD_NOISE_NUM_PARAMETERS
|
||||||
|
};
|
||||||
|
|
||||||
|
enum FMOD_NOISE_FORMAT
|
||||||
|
{
|
||||||
|
FMOD_NOISE_FORMAT_MONO = 0,
|
||||||
|
FMOD_NOISE_FORMAT_STEREO,
|
||||||
|
FMOD_NOISE_FORMAT_5POINT1
|
||||||
|
};
|
||||||
|
|
||||||
|
#define DECIBELS_TO_LINEAR(__dbval__) ((__dbval__ <= FMOD_NOISE_PARAM_GAIN_MIN) ? 0.0f : powf(10.0f, __dbval__ / 20.0f))
|
||||||
|
#define LINEAR_TO_DECIBELS(__linval__) ((__linval__ <= 0.0f) ? FMOD_NOISE_PARAM_GAIN_MIN : 20.0f * log10f((float)__linval__))
|
||||||
|
|
||||||
|
FMOD_RESULT F_CALL FMOD_Noise_dspcreate (FMOD_DSP_STATE *dsp);
|
||||||
|
FMOD_RESULT F_CALL FMOD_Noise_dsprelease (FMOD_DSP_STATE *dsp);
|
||||||
|
FMOD_RESULT F_CALL FMOD_Noise_dspreset (FMOD_DSP_STATE *dsp);
|
||||||
|
FMOD_RESULT F_CALL FMOD_Noise_dspprocess (FMOD_DSP_STATE *dsp, unsigned int length, const FMOD_DSP_BUFFER_ARRAY *inbufferarray, FMOD_DSP_BUFFER_ARRAY *outbufferarray, FMOD_BOOL inputsidle, FMOD_DSP_PROCESS_OPERATION op);
|
||||||
|
FMOD_RESULT F_CALL FMOD_Noise_dspsetparamfloat(FMOD_DSP_STATE *dsp, int index, float value);
|
||||||
|
FMOD_RESULT F_CALL FMOD_Noise_dspsetparamint (FMOD_DSP_STATE *dsp, int index, int value);
|
||||||
|
FMOD_RESULT F_CALL FMOD_Noise_dspsetparambool (FMOD_DSP_STATE *dsp, int index, bool value);
|
||||||
|
FMOD_RESULT F_CALL FMOD_Noise_dspsetparamdata (FMOD_DSP_STATE *dsp, int index, void *data, unsigned int length);
|
||||||
|
FMOD_RESULT F_CALL FMOD_Noise_dspgetparamfloat(FMOD_DSP_STATE *dsp, int index, float *value, char *valuestr);
|
||||||
|
FMOD_RESULT F_CALL FMOD_Noise_dspgetparamint (FMOD_DSP_STATE *dsp, int index, int *value, char *valuestr);
|
||||||
|
FMOD_RESULT F_CALL FMOD_Noise_dspgetparambool (FMOD_DSP_STATE *dsp, int index, bool *value, char *valuestr);
|
||||||
|
FMOD_RESULT F_CALL FMOD_Noise_dspgetparamdata (FMOD_DSP_STATE *dsp, int index, void **value, unsigned int *length, char *valuestr);
|
||||||
|
|
||||||
|
static FMOD_DSP_PARAMETER_DESC p_level;
|
||||||
|
static FMOD_DSP_PARAMETER_DESC p_format;
|
||||||
|
|
||||||
|
FMOD_DSP_PARAMETER_DESC *FMOD_Noise_dspparam[FMOD_NOISE_NUM_PARAMETERS] =
|
||||||
|
{
|
||||||
|
&p_level,
|
||||||
|
&p_format
|
||||||
|
};
|
||||||
|
|
||||||
|
const char* FMOD_Noise_Format_Names[3] = {"Mono", "Stereo", "5.1"};
|
||||||
|
|
||||||
|
FMOD_DSP_DESCRIPTION FMOD_Noise_Desc =
|
||||||
|
{
|
||||||
|
FMOD_PLUGIN_SDK_VERSION,
|
||||||
|
"FMOD Noise", // name
|
||||||
|
0x00010000, // plug-in version
|
||||||
|
0, // number of input buffers to process
|
||||||
|
1, // number of output buffers to process
|
||||||
|
FMOD_Noise_dspcreate,
|
||||||
|
FMOD_Noise_dsprelease,
|
||||||
|
FMOD_Noise_dspreset,
|
||||||
|
0,
|
||||||
|
FMOD_Noise_dspprocess,
|
||||||
|
0,
|
||||||
|
FMOD_NOISE_NUM_PARAMETERS,
|
||||||
|
FMOD_Noise_dspparam,
|
||||||
|
FMOD_Noise_dspsetparamfloat,
|
||||||
|
FMOD_Noise_dspsetparamint,
|
||||||
|
0,
|
||||||
|
0,
|
||||||
|
FMOD_Noise_dspgetparamfloat,
|
||||||
|
FMOD_Noise_dspgetparamint,
|
||||||
|
0,
|
||||||
|
0,
|
||||||
|
0,
|
||||||
|
0, // userdata
|
||||||
|
0, // Register
|
||||||
|
0, // Deregister
|
||||||
|
0 // Mix
|
||||||
|
};
|
||||||
|
|
||||||
|
extern "C"
|
||||||
|
{
|
||||||
|
|
||||||
|
F_EXPORT FMOD_DSP_DESCRIPTION* F_CALL FMODGetDSPDescription()
|
||||||
|
{
|
||||||
|
FMOD_DSP_INIT_PARAMDESC_FLOAT(p_level, "Level", "dB", "Gain in dB. -80 to 10. Default = 0", FMOD_NOISE_PARAM_GAIN_MIN, FMOD_NOISE_PARAM_GAIN_MAX, FMOD_NOISE_PARAM_GAIN_DEFAULT);
|
||||||
|
FMOD_DSP_INIT_PARAMDESC_INT(p_format, "Format", "", "Mono, stereo or 5.1. Default = 0 (mono)", FMOD_NOISE_FORMAT_MONO, FMOD_NOISE_FORMAT_5POINT1, FMOD_NOISE_FORMAT_MONO, false, FMOD_Noise_Format_Names);
|
||||||
|
return &FMOD_Noise_Desc;
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
class FMODNoiseState
|
||||||
|
{
|
||||||
|
public:
|
||||||
|
FMODNoiseState();
|
||||||
|
|
||||||
|
void generate(float *outbuffer, unsigned int length, int channels);
|
||||||
|
void reset();
|
||||||
|
void setLevel(float);
|
||||||
|
void setFormat(FMOD_NOISE_FORMAT format) { m_format = format; }
|
||||||
|
float level() const { return LINEAR_TO_DECIBELS(m_target_level); }
|
||||||
|
FMOD_NOISE_FORMAT format() const { return m_format; }
|
||||||
|
|
||||||
|
private:
|
||||||
|
float m_target_level;
|
||||||
|
float m_current_level;
|
||||||
|
int m_ramp_samples_left;
|
||||||
|
FMOD_NOISE_FORMAT m_format;
|
||||||
|
};
|
||||||
|
|
||||||
|
FMODNoiseState::FMODNoiseState()
|
||||||
|
{
|
||||||
|
m_target_level = DECIBELS_TO_LINEAR(FMOD_NOISE_PARAM_GAIN_DEFAULT);
|
||||||
|
m_format = FMOD_NOISE_FORMAT_MONO;
|
||||||
|
reset();
|
||||||
|
}
|
||||||
|
|
||||||
|
void FMODNoiseState::generate(float *outbuffer, unsigned int length, int channels)
|
||||||
|
{
|
||||||
|
// Note: buffers are interleaved
|
||||||
|
float gain = m_current_level;
|
||||||
|
|
||||||
|
if (m_ramp_samples_left)
|
||||||
|
{
|
||||||
|
float target = m_target_level;
|
||||||
|
float delta = (target - gain) / m_ramp_samples_left;
|
||||||
|
while (length)
|
||||||
|
{
|
||||||
|
if (--m_ramp_samples_left)
|
||||||
|
{
|
||||||
|
gain += delta;
|
||||||
|
for (int i = 0; i < channels; ++i)
|
||||||
|
{
|
||||||
|
*outbuffer++ = (((float)(rand()%32768) / 16384.0f) - 1.0f) * gain;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
gain = target;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
--length;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
unsigned int samples = length * channels;
|
||||||
|
while (samples--)
|
||||||
|
{
|
||||||
|
*outbuffer++ = (((float)(rand()%32768) / 16384.0f) - 1.0f) * gain;
|
||||||
|
}
|
||||||
|
|
||||||
|
m_current_level = gain;
|
||||||
|
}
|
||||||
|
|
||||||
|
void FMODNoiseState::reset()
|
||||||
|
{
|
||||||
|
m_current_level = m_target_level;
|
||||||
|
m_ramp_samples_left = 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
void FMODNoiseState::setLevel(float level)
|
||||||
|
{
|
||||||
|
m_target_level = DECIBELS_TO_LINEAR(level);
|
||||||
|
m_ramp_samples_left = FMOD_NOISE_RAMPCOUNT;
|
||||||
|
}
|
||||||
|
|
||||||
|
FMOD_RESULT F_CALL FMOD_Noise_dspcreate(FMOD_DSP_STATE *dsp)
|
||||||
|
{
|
||||||
|
dsp->plugindata = (FMODNoiseState *)FMOD_DSP_ALLOC(dsp, sizeof(FMODNoiseState));
|
||||||
|
if (!dsp->plugindata)
|
||||||
|
{
|
||||||
|
return FMOD_ERR_MEMORY;
|
||||||
|
}
|
||||||
|
return FMOD_OK;
|
||||||
|
}
|
||||||
|
|
||||||
|
FMOD_RESULT F_CALL FMOD_Noise_dsprelease(FMOD_DSP_STATE *dsp)
|
||||||
|
{
|
||||||
|
FMODNoiseState *state = (FMODNoiseState *)dsp->plugindata;
|
||||||
|
FMOD_DSP_FREE(dsp, state);
|
||||||
|
return FMOD_OK;
|
||||||
|
}
|
||||||
|
|
||||||
|
FMOD_RESULT F_CALL FMOD_Noise_dspprocess(FMOD_DSP_STATE *dsp, unsigned int length, const FMOD_DSP_BUFFER_ARRAY * /*inbufferarray*/, FMOD_DSP_BUFFER_ARRAY *outbufferarray, FMOD_BOOL /*inputsidle*/, FMOD_DSP_PROCESS_OPERATION op)
|
||||||
|
{
|
||||||
|
FMODNoiseState *state = (FMODNoiseState *)dsp->plugindata;
|
||||||
|
|
||||||
|
if (op == FMOD_DSP_PROCESS_QUERY)
|
||||||
|
{
|
||||||
|
FMOD_SPEAKERMODE outmode = FMOD_SPEAKERMODE_DEFAULT;
|
||||||
|
int outchannels = 0;
|
||||||
|
|
||||||
|
switch(state->format())
|
||||||
|
{
|
||||||
|
case FMOD_NOISE_FORMAT_MONO:
|
||||||
|
outmode = FMOD_SPEAKERMODE_MONO;
|
||||||
|
outchannels = 1;
|
||||||
|
break;
|
||||||
|
|
||||||
|
case FMOD_NOISE_FORMAT_STEREO:
|
||||||
|
outmode = FMOD_SPEAKERMODE_STEREO;
|
||||||
|
outchannels = 2;
|
||||||
|
break;
|
||||||
|
|
||||||
|
case FMOD_NOISE_FORMAT_5POINT1:
|
||||||
|
outmode = FMOD_SPEAKERMODE_5POINT1;
|
||||||
|
outchannels = 6;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (outbufferarray)
|
||||||
|
{
|
||||||
|
outbufferarray->speakermode = outmode;
|
||||||
|
outbufferarray->buffernumchannels[0] = outchannels;
|
||||||
|
}
|
||||||
|
|
||||||
|
return FMOD_OK;
|
||||||
|
}
|
||||||
|
|
||||||
|
state->generate(outbufferarray->buffers[0], length, outbufferarray->buffernumchannels[0]);
|
||||||
|
return FMOD_OK;
|
||||||
|
}
|
||||||
|
|
||||||
|
FMOD_RESULT F_CALL FMOD_Noise_dspreset(FMOD_DSP_STATE *dsp)
|
||||||
|
{
|
||||||
|
FMODNoiseState *state = (FMODNoiseState *)dsp->plugindata;
|
||||||
|
state->reset();
|
||||||
|
return FMOD_OK;
|
||||||
|
}
|
||||||
|
|
||||||
|
FMOD_RESULT F_CALL FMOD_Noise_dspsetparamfloat(FMOD_DSP_STATE *dsp, int index, float value)
|
||||||
|
{
|
||||||
|
FMODNoiseState *state = (FMODNoiseState *)dsp->plugindata;
|
||||||
|
|
||||||
|
switch (index)
|
||||||
|
{
|
||||||
|
case FMOD_NOISE_PARAM_LEVEL:
|
||||||
|
state->setLevel(value);
|
||||||
|
return FMOD_OK;
|
||||||
|
}
|
||||||
|
|
||||||
|
return FMOD_ERR_INVALID_PARAM;
|
||||||
|
}
|
||||||
|
|
||||||
|
FMOD_RESULT F_CALL FMOD_Noise_dspgetparamfloat(FMOD_DSP_STATE *dsp, int index, float *value, char *valuestr)
|
||||||
|
{
|
||||||
|
FMODNoiseState *state = (FMODNoiseState *)dsp->plugindata;
|
||||||
|
|
||||||
|
switch (index)
|
||||||
|
{
|
||||||
|
case FMOD_NOISE_PARAM_LEVEL:
|
||||||
|
*value = state->level();
|
||||||
|
if (valuestr) snprintf(valuestr, FMOD_DSP_GETPARAM_VALUESTR_LENGTH, "%.1f dB", state->level());
|
||||||
|
return FMOD_OK;
|
||||||
|
}
|
||||||
|
|
||||||
|
return FMOD_ERR_INVALID_PARAM;
|
||||||
|
}
|
||||||
|
|
||||||
|
FMOD_RESULT F_CALL FMOD_Noise_dspsetparamint(FMOD_DSP_STATE *dsp, int index, int value)
|
||||||
|
{
|
||||||
|
FMODNoiseState *state = (FMODNoiseState *)dsp->plugindata;
|
||||||
|
|
||||||
|
switch (index)
|
||||||
|
{
|
||||||
|
case FMOD_NOISE_PARAM_FORMAT:
|
||||||
|
state->setFormat((FMOD_NOISE_FORMAT)value);
|
||||||
|
return FMOD_OK;
|
||||||
|
}
|
||||||
|
|
||||||
|
return FMOD_ERR_INVALID_PARAM;
|
||||||
|
}
|
||||||
|
|
||||||
|
FMOD_RESULT F_CALL FMOD_Noise_dspgetparamint(FMOD_DSP_STATE *dsp, int index, int *value, char *valuestr)
|
||||||
|
{
|
||||||
|
FMODNoiseState *state = (FMODNoiseState *)dsp->plugindata;
|
||||||
|
|
||||||
|
switch (index)
|
||||||
|
{
|
||||||
|
case FMOD_NOISE_PARAM_FORMAT:
|
||||||
|
*value = state->format();
|
||||||
|
if (valuestr) snprintf(valuestr, FMOD_DSP_GETPARAM_VALUESTR_LENGTH, "%s", FMOD_Noise_Format_Names[state->format()]);
|
||||||
|
return FMOD_OK;
|
||||||
|
}
|
||||||
|
|
||||||
|
return FMOD_ERR_INVALID_PARAM;
|
||||||
|
}
|
||||||
@@ -0,0 +1,189 @@
|
|||||||
|
/*==============================================================================
|
||||||
|
RNBO DSP Plugin Example
|
||||||
|
Copyright (c), Firelight Technologies Pty, Ltd 2004-2026.
|
||||||
|
|
||||||
|
This example shows how to integrate RNBO C++ source code into an FMOD effect.
|
||||||
|
1. Add the rnbo and rnbo/common directories to your include paths.
|
||||||
|
2. Add the generated RNBO source .cpp file and rnbo/RNBO.cpp files to your list
|
||||||
|
of source files.
|
||||||
|
3. Build and copy the generated library into your FMOD Studio script directory:
|
||||||
|
https://fmod.com/docs/2.03/studio/plugin-reference.html#loading-plug-ins
|
||||||
|
Note:
|
||||||
|
FMOD parameters have a max length of 15 characters. RNBO parameters greater
|
||||||
|
than 15 chracters will be truncated.
|
||||||
|
==============================================================================*/
|
||||||
|
#if __has_include("RNBO.h")
|
||||||
|
#include "RNBO.h"
|
||||||
|
|
||||||
|
#include <math.h>
|
||||||
|
#include <stdio.h>
|
||||||
|
#include <atomic>
|
||||||
|
#include <string.h>
|
||||||
|
|
||||||
|
#include "fmod.hpp"
|
||||||
|
|
||||||
|
static FMOD_DSP_PARAMETER_DESC** params = nullptr;
|
||||||
|
static int numParams;
|
||||||
|
static int numInputs;
|
||||||
|
static int numOutputs;
|
||||||
|
static std::atomic<int> gSysCount = 0;
|
||||||
|
|
||||||
|
static FMOD_RESULT F_CALL FMOD_RNBO_Create(FMOD_DSP_STATE *dsp_state)
|
||||||
|
{
|
||||||
|
dsp_state->plugindata = FMOD_DSP_ALLOC(dsp_state, sizeof(RNBO::CoreObject));
|
||||||
|
if (!dsp_state->plugindata)
|
||||||
|
{
|
||||||
|
return FMOD_ERR_MEMORY;
|
||||||
|
}
|
||||||
|
|
||||||
|
RNBO::CoreObject *rnboObject = (RNBO::CoreObject *)dsp_state->plugindata;
|
||||||
|
|
||||||
|
int sampleRate;
|
||||||
|
unsigned int blockSize;
|
||||||
|
|
||||||
|
FMOD_DSP_GETSAMPLERATE(dsp_state, &sampleRate);
|
||||||
|
FMOD_DSP_GETBLOCKSIZE(dsp_state, &blockSize);
|
||||||
|
|
||||||
|
new (rnboObject) RNBO::CoreObject();
|
||||||
|
|
||||||
|
rnboObject->prepareToProcess(sampleRate, blockSize);
|
||||||
|
|
||||||
|
return FMOD_OK;
|
||||||
|
}
|
||||||
|
|
||||||
|
static FMOD_RESULT F_CALL FMOD_RNBO_Release(FMOD_DSP_STATE *dsp_state)
|
||||||
|
{
|
||||||
|
RNBO::CoreObject *rnboObject = (RNBO::CoreObject *)dsp_state->plugindata;
|
||||||
|
rnboObject->~CoreObject();
|
||||||
|
|
||||||
|
FMOD_DSP_FREE(dsp_state, rnboObject);
|
||||||
|
|
||||||
|
return FMOD_OK;
|
||||||
|
}
|
||||||
|
|
||||||
|
static FMOD_RESULT F_CALL FMOD_RNBO_Process(FMOD_DSP_STATE *dsp_state, unsigned int length, const FMOD_DSP_BUFFER_ARRAY *inbufferarray, FMOD_DSP_BUFFER_ARRAY *outbufferarray, FMOD_BOOL inputsidle, FMOD_DSP_PROCESS_OPERATION op)
|
||||||
|
{
|
||||||
|
RNBO::CoreObject *rnboObject = (RNBO::CoreObject *)dsp_state->plugindata;
|
||||||
|
|
||||||
|
if (op == FMOD_DSP_PROCESS_QUERY)
|
||||||
|
{
|
||||||
|
if (numInputs > 0)
|
||||||
|
{
|
||||||
|
if (inputsidle)
|
||||||
|
{
|
||||||
|
return FMOD_ERR_DSP_SILENCE;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
if (outbufferarray && outbufferarray[0].buffernumchannels)
|
||||||
|
{
|
||||||
|
outbufferarray[0].buffernumchannels[0] = numOutputs;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
rnboObject->process(inbufferarray[0].buffers[0], inbufferarray[0].buffernumchannels[0],
|
||||||
|
outbufferarray[0].buffers[0], outbufferarray[0].buffernumchannels[0], length);
|
||||||
|
}
|
||||||
|
|
||||||
|
return FMOD_OK;
|
||||||
|
}
|
||||||
|
|
||||||
|
static FMOD_RESULT F_CALL FMOD_RNBO_SetParamFloat(FMOD_DSP_STATE *dsp_state, int index, float value)
|
||||||
|
{
|
||||||
|
RNBO::CoreObject *rnboObject = (RNBO::CoreObject *)dsp_state->plugindata;
|
||||||
|
|
||||||
|
rnboObject->setParameterValue(index, value);
|
||||||
|
return FMOD_OK;
|
||||||
|
}
|
||||||
|
|
||||||
|
static FMOD_RESULT F_CALL FMOD_RNBO_GetParamFloat(FMOD_DSP_STATE *dsp_state, int index, float *value, char *valuestr)
|
||||||
|
{
|
||||||
|
RNBO::CoreObject *rnboObject = (RNBO::CoreObject *)dsp_state->plugindata;
|
||||||
|
|
||||||
|
*value = (float)rnboObject->getParameterValue(index);
|
||||||
|
if (valuestr) snprintf(valuestr, FMOD_DSP_GETPARAM_VALUESTR_LENGTH, "%s", rnboObject->getParameterName(index));
|
||||||
|
|
||||||
|
return FMOD_OK;
|
||||||
|
}
|
||||||
|
|
||||||
|
static FMOD_RESULT F_CALL FMOD_RNBO_SysRegister(FMOD_DSP_STATE* dsp_state)
|
||||||
|
{
|
||||||
|
RNBO::CoreObject* rnboObject = (RNBO::CoreObject*)dsp_state->plugindata;
|
||||||
|
|
||||||
|
gSysCount++;
|
||||||
|
|
||||||
|
return FMOD_OK;
|
||||||
|
}
|
||||||
|
|
||||||
|
static FMOD_RESULT F_CALL FMOD_RNBO_SysDeregister(FMOD_DSP_STATE* dsp_state)
|
||||||
|
{
|
||||||
|
RNBO::CoreObject* rnboObject = (RNBO::CoreObject*)dsp_state->plugindata;
|
||||||
|
|
||||||
|
gSysCount--;
|
||||||
|
if (gSysCount == 0)
|
||||||
|
{
|
||||||
|
for (int i = 0; i < numParams; ++i)
|
||||||
|
{
|
||||||
|
free(params[i]);
|
||||||
|
}
|
||||||
|
free(params);
|
||||||
|
|
||||||
|
params = nullptr;
|
||||||
|
}
|
||||||
|
|
||||||
|
return FMOD_OK;
|
||||||
|
}
|
||||||
|
|
||||||
|
extern "C" F_EXPORT FMOD_DSP_DESCRIPTION *F_CALL FMODGetDSPDescription()
|
||||||
|
{
|
||||||
|
static FMOD_DSP_DESCRIPTION desc = { FMOD_PLUGIN_SDK_VERSION, "FMOD RNBO", 0x00010000 };
|
||||||
|
desc.create = FMOD_RNBO_Create;
|
||||||
|
desc.release = FMOD_RNBO_Release;
|
||||||
|
desc.process = FMOD_RNBO_Process;
|
||||||
|
desc.setparameterfloat = FMOD_RNBO_SetParamFloat;
|
||||||
|
desc.getparameterfloat = FMOD_RNBO_GetParamFloat;
|
||||||
|
desc.sys_register = FMOD_RNBO_SysRegister;
|
||||||
|
desc.sys_deregister = FMOD_RNBO_SysDeregister;
|
||||||
|
|
||||||
|
if (!params)
|
||||||
|
{
|
||||||
|
RNBO::CoreObject tmpRnboObject;
|
||||||
|
|
||||||
|
numParams = tmpRnboObject.getNumParameters();
|
||||||
|
numInputs = tmpRnboObject.getNumInputChannels();
|
||||||
|
numOutputs = tmpRnboObject.getNumOutputChannels();
|
||||||
|
|
||||||
|
desc.numparameters = numParams;
|
||||||
|
desc.numinputbuffers = numInputs > 0;
|
||||||
|
desc.numoutputbuffers = numOutputs > 0;
|
||||||
|
|
||||||
|
params = (FMOD_DSP_PARAMETER_DESC**)malloc(desc.numparameters * sizeof(FMOD_DSP_PARAMETER_DESC*));
|
||||||
|
|
||||||
|
for (int i = 0; i < numParams; ++i)
|
||||||
|
{
|
||||||
|
RNBO::ParameterInfo info = {};
|
||||||
|
tmpRnboObject.getParameterInfo(i, &info);
|
||||||
|
|
||||||
|
params[i] = (FMOD_DSP_PARAMETER_DESC *)malloc(sizeof(FMOD_DSP_PARAMETER_DESC));
|
||||||
|
memset(params[i], 0, sizeof(FMOD_DSP_PARAMETER_DESC));
|
||||||
|
|
||||||
|
// FMOD_DSP_PARAMETER_DESC::name can only hold 15 characters (excluding \0 null terminator).
|
||||||
|
// RNBO parameters greater than 15 chracters will be truncated.
|
||||||
|
snprintf(params[i]->name, sizeof(params[i]->name), "%s", tmpRnboObject.getParameterId(i));
|
||||||
|
snprintf(params[i]->label, sizeof(params[i]->label), "%s", info.unit);
|
||||||
|
|
||||||
|
params[i]->type = FMOD_DSP_PARAMETER_TYPE_FLOAT;
|
||||||
|
params[i]->floatdesc.defaultval = info.initialValue;
|
||||||
|
params[i]->floatdesc.min = info.min;
|
||||||
|
params[i]->floatdesc.max = info.max;
|
||||||
|
}
|
||||||
|
|
||||||
|
desc.paramdesc = params;
|
||||||
|
}
|
||||||
|
return &desc;
|
||||||
|
}
|
||||||
|
|
||||||
|
#endif
|
||||||
Binary file not shown.
Binary file not shown.
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user