Is there a way to fade in a sound from an AKOperationGenerator?
E.g. in the code below .start() begins at full amplitude with a click.
let whiteNoiseGenerator = AKOperationGenerator { _ in
let white = AKOperation.whiteNoise()
return white
}
AudioKit.output = whiteNoiseGenerator
whiteNoiseGenerator.start()
There are a lot of ways to skin this cat. I'll give you a couple:
One would be to wrap the noise generator in an AKBooster:
let volumeControl = AKBooster(whiteNoiseGenerator)
volumeControl.gain = 0
volumeControl.rampTime = 1 // number of seconds to fade in/out
AudioKit.output = volumeControl
AudioKit.start() // Don't forget this, I assume you just didn't paste it in
whiteNoiseGenerator.start() // nothing will be heard
Then starting and stopping the sound will just be done by setting
volumeControl.gain = 1 // or 0 for stopping
Alternatively, I know you wanted to know how to do this in the context of an operation, so you can do that by giving your operation parameters of gain and portamento time:
let whiteNoiseGenerator = AKOperationGenerator { parameters in
let white = AKOperation.whiteNoise() * parameters[0].portamento(halfDuration: parameters[1])
return white
}
whiteNoiseGenerator.parameters = [0,0.1] // first number is gain, second is portamento time
AudioKit.output = whiteNoiseGenerator
AudioKit.start()
whiteNoiseGenerator.start()
And then, when you want to turn on the noise do this:
whiteNoiseGenerator.parameters[0] = 1
you will get a smooth variation to full volume over 0.1 seconds.
HTH!
Related
As part of a hobby project, I'm working on a 2D game engine that will draw each pixel every frame, using a color from a palette.
I am looking for a way to do that while maintaining a reasonable frame rate (60fps being the minimum).
Without any game-logic in place, I am updating the values of my pixels with some value form the palette.
I'm currently taking the mod of an index, to (hopefully) prevent the compiler from doing some loop-optimisation it could do with a fixed value.
Below is my (very naive?) implementation of updating the bytes in the pixel array.
On an iPhone 12 Pro, each run of updating all pixel values takes on average 43 ms, while on a simulator running on an M1 mac, it takes 15 ms. Both unacceptable, as that would leave not for any additional game logic (which would be much more operations than taking the mod of an Int).
I was planning to look into Metal and set up a surface, but clearly the bottleneck here is the CPU, so if I can optimize this code, I could go for a higher-level framework.
Any suggestions on a performant way to write this many bytes much, much faster (parallelisation is not an option)?
Instruments shows that most of the time is being spent in Swifts IndexingIterator.next() function. Maybe there is way to reduce the time spent there, there is quite a substantial subtree inside it.
struct BGRA
{
let blue: UInt8
let green: UInt8
let red: UInt8
let alpha: UInt8
}
let BGRAPallet =
[
BGRA(blue: 124, green: 124, red: 124, alpha: 0xff),
BGRA(blue: 252, green: 0, red: 0, alpha: 0xff),
// ... 62 more values in my code, omitting here for brevity
]
private func test()
{
let screenWidth: Int = 256
let screenHeight: Int = 240
let pixelBufferPtr = UnsafeMutableBufferPointer<BGRA>.allocate(capacity: screenWidth * screenHeight)
let runCount = 1000
let start = Date.now
for _ in 0 ..< runCount
{
for index in 0 ..< pixelBufferPtr.count
{
pixelBufferPtr[index] = BGRAPallet[index % BGRAPallet.count]
}
}
let elapsed = Date.now.timeIntervalSince(start)
print("Average time per run: \((Int(elapsed) * 1000) / runCount) ms")
}
First of all, I don't believe you're testing an optimized build for two reasons:
You say “This was measured with optimization set to Fastest [-O3].” But the Swift compiler doesn't recognize -O3 as a command-line flag. The C/C++ compiler recognizes that flag. For Swift the flags are -Onone, -O, -Osize, and -Ounchecked.
I ran your code on my M1 Max MacBook Pro in Debug configuration and it reported 15ms. Then I ran it in Release configuration and it reported 0ms. I had to increase the screen size to 2560x2400 (100x the pixels) to get it to report a time of 3ms.
Now, looking at your code, here are some things that stand out:
You're picking a color using BGRAPalette[index % BGRAPalette.count]. Since your palette size is 64, you can say BGRAPalette[index & 0b0011_1111] for the same result. I expected Swift to optimize that for me, but apparently it didn't, because making that change reduced the reported time to 2ms.
Indexing into BGRAPalette incurs a bounds check. You can avoid the bounds check by grabbing an UnsafeBufferPointer for the palette. Adding this optimization reduced the reported time to 1ms.
Here's my version:
public struct BGRA {
let blue: UInt8
let green: UInt8
let red: UInt8
let alpha: UInt8
}
func rng() -> UInt8 { UInt8.random(in: .min ... .max) }
let BGRAPalette = (0 ..< 64).map { _ in
BGRA(blue: rng(), green: rng(), red: rng(), alpha: rng())
}
public func test() {
let screenWidth: Int = 2560
let screenHeight: Int = 2400
let pixelCount = screenWidth * screenHeight
let pixelBuffer = UnsafeMutableBufferPointer<BGRA>.allocate(capacity: pixelCount)
let runCount = 1000
let start = SuspendingClock().now
BGRAPalette.withUnsafeBufferPointer { paletteBuffer in
for _ in 0 ..< runCount
{
for index in 0 ..< pixelCount
{
pixelBuffer[index] = paletteBuffer[index & 0b0011_1111]
}
}
}
let end = SuspendingClock().now
let elapsed = end - start
let msElapsed = elapsed.components.seconds * 1000 + elapsed.components.attoseconds / 1_000_000_000_000_000
print("Average time per run: \(msElapsed / Int64(runCount)) ms")
// return pixelBuffer
}
#main
struct MyMain {
static func main() {
test()
}
}
In addition to the two optimizations I described, I removed the dependency on Foundation (so I could paste the code into the compiler explorer) and corrected the spelling of ‘palette‘.
But realistically, even this isn't probably isn't particularly good test of your fill rate. You didn't say what kind of game you want to write, but given your screen size of 256x240, it's likely to use a tile-based map and sprites. If so, you shouldn't copy a pixel at a time. You can write a blitter that copies blocks of pixels at a time, using CPU instructions that operate on more than 32 bits at a time. ARM64 has 128-bit (16-byte) registers.
But even more realistically, you should use learn to use the GPU for your blitting. Not only is it faster for this sort of thing, it's probably more power-efficient too. Even though you're lighting up more of the chip, you're lighting it up for shorter intervals.
Okay so i gave this a shot. You can probably move to using single input (or instruction), multiple output. This would probably speed up the whole process by 10 - 20 percent or so.
Here is the code link (same code will be pasted below for brevity): https://codecatch.net/post/4b9683bf-8e35-4bf5-a1a9-801ab2e73805
I made two versions just in case your systems architecture doesn't support simd_uint4. Let me know if this is what you were looking for.
import simd
private func test() {
let screenWidth: Int = 256
let screenHeight: Int = 240
let pixelBufferPtr = UnsafeMutableBufferPointer<BGRA>.allocate(capacity: screenWidth * screenHeight)
let runCount = 1000
let start = Date.now
for _ in 0 ..< runCount {
var index = 0
var palletIndex = 0
let palletCount = BGRAPallet.count
while index < pixelBufferPtr.count {
let bgra = BGRAPallet[palletIndex]
let bgraVector = simd_uint4(bgra.blue, bgra.green, bgra.red, bgra.alpha)
let maxCount = min(pixelBufferPtr.count - index, 4)
let pixelBuffer = pixelBufferPtr.baseAddress! + index
pixelBuffer.storeBytes(of: bgraVector, as: simd_uint4.self)
palletIndex += 1
if palletIndex == palletCount {
palletIndex = 0
}
index += maxCount
}
}
let elapsed = Date.now.timeIntervalSince(start)
print("Average time per run: \((Int(elapsed) * 1000) / runCount) ms")
}
I am making a timer program which uses a slider to set a timer value and then updates a digital clock display to show the corresponding numerical time remaining as image files.
I am trying to run a 1 second delay function within a while loop, and update the image files each second, essentially trying to create a timer which updates the variables used to determine which images are used in real time.
I am having difficulty assigning counting down correctly: the variables used to set the image files h, min1 and min2 are set to 0 after a 1 second delay. It seems that the while loop calls the delay function once, iterates until the condition is met without delaying the timer and then displays the final values.
I've tried different methods of timing a 1 second delay including using the let timer = Timer. approach and DispatchQueue.main. approach but they don't seem to work.
#IBAction func slider(_ sender: UISlider)
{
self.x = Int(sender.value)
// Note I omitted the rest of this code as it concerned setting images while changing slider value, and used local variables.
}
var x: Int = 60
var h: Int = 1
var min1: Int = 1
var min2: Int = 7
#objc func animate2 ()
{
checkLbl.text = String(h) + ("1")
checkLbl2.text = String(min1) + ("1")
checkLbl3.text = String(min2) + ("1")
self.H2ImageView.image = UIImage(named: "\(h).png")!
self.M1ImageView.image = UIImage(named: "\(min1).png")!
self.M2ImageView.image = UIImage(named: "\(min2).png")!
}
func animate ()
{
var timeLeft = x
var seconds = 60
while timeLeft >= 0
{
(h, _) = x.quotientAndRemainder(dividingBy: 60)
(min1, min2) = x.quotientAndRemainder(dividingBy: 10)
if min1 >= 6
{
min1 = min1 - 6
if h == 2
{
min1 = 0
}
}
checkLbl.text = String(h)
checkLbl2.text = String(min1)
checkLbl3.text = String(min2)
// checkLbl used to track the variables
perform(#selector(animate2), with: nil, afterDelay: 1)
seconds = seconds - 1
if seconds == 0
{
timeLeft = timeLeft - 1
seconds = 60
}
}
}
#IBAction func watchPress(_ sender: UIButton)
{
animate()
}
To summarise: I expected the delay function to update h, min1 and min2 (and therefore update the checkLbl text) every second however these values go straight to 0.
Any help is appreciated, thanks!
You need to understand that performSelector, DispatchQueue.main.asyncAfter and Timer all run code asynchronously after a certain time. This means that line B won't run one second after line A in this example:
checkLbl3.text = String(min2) // A
perform(#selector(animate2), with: nil, afterDelay: 1)
seconds = seconds - 1 // B
Line B will run immediately after line A. And after one second, animate2 will be called.
Therefore, you should not use a while loop. Instead, you should use a Timer like this:
Timer(timeInterval: 1, repeats: true) { (timer) in
if timeLeft == 0 {
timer.invalidate()
}
seconds -= 1
if seconds == 0
{
timeLeft = timeLeft - 1
seconds = 60
}
// update the labels and image views
}
I also recommend you to only store the time left as a number of seconds. Remove your h, min1, min2 and seconds variables. Only calculate them from the time left when you update the UI.
Currently doing a SoloProject for class and decided to study SpriteKit on my own. I decided to make a top-down zombie shooter and I have a lot of questions but so far these are the two main ones I can't seem to fix or find solution for.
Problem 1
Zombies slow down the closer they get to the target, If I increase the speed they just speed in from off the screen and still slowdown as they get closer (I've read somewhere putting that function in the update is bad but I still did it...)
I want to make it where they spawn with the speed of 3 and when the player moves closer or further away it stays at 3. (Currently using an analog stick code I found that was on Youtube to move my character around)
func zombieAttack() {
let location = player.position
for node in enemies {
let followPlayer = SKAction.move(to: player.position, duration: 3)
node.run(followPlayer)
//Aim
let dx = (location.x) - node.position.x
let dy = (location.y) - node.position.y
let angle = atan2(dy, dx)
node.zRotation = angle
//Seek
let velocityX = cos(angle) * 1
let velocityY = sin(angle) * 1
node.position.x += velocityX
node.position.y += velocityY
}
}
override func update(_ currentTime: TimeInterval) {
zombieAttack()
}
Problem 2
Also when multiple zombies get close to the players (function above) they start to spazz so I allowed them to overlap on top of each other to stop the spazzing.
I want to make it where they are more solid? if that is the right way to describe it. Basically I want them to huddle up around the player**.
If I add enemy to the collision it will spazz trying to get into the same position.
private func spawnZombie() {
let xPos = randomPosition(spriteSize: gameSpace.size)
let zombie = SKSpriteNode(imageNamed: "skeleton-idle_0")
zombie.position = CGPoint(x: -1 * xPos.x, y: -1 * xPos.y)
zombie.name = "Zombie\(zombieCounter)"
zombie.zPosition = NodesZPosition.enemy.rawValue
let presetTexture = SKTexture(imageNamed: "skeleton-idle_0.png")
zombie.physicsBody = SKPhysicsBody(texture: presetTexture, size: presetTexture.size())
zombie.physicsBody?.isDynamic = true
zombie.physicsBody?.affectedByGravity = false
zombie.physicsBody?.categoryBitMask = BodyType.enemy.rawValue
zombie.physicsBody?.contactTestBitMask = BodyType.bullet.rawValue
zombie.physicsBody?.collisionBitMask = BodyType.player.rawValue
zombie.zRotation = 1.5
zombie.setScale(0.2)
enemies.append(zombie)
zombieCounter += 1
run(SKAction.playSoundFileNamed("ZombieSpawn", waitForCompletion: false))
keepEnemiesSeperated() // ADDED FROM UPDATED EDIT*
addChild(zombie)
}
Let me know if I need to post more code or explain it better, I'm a five months in on learning Swift and have only a week and a half of SpriteKit experience and first time posting on StackOverFlow. Thanks all in advance!
EDIT: I am using a code I found from having a node follow at a constant speed but I don't think I'm doing it right since it is not working. I added the following code:
private func keepEnemiesSeparated() {
// Assign constrain
for enemy in enemies {
enemy.constraints = []
let distanceBetween = CGFloat(60)
let constraint = SKConstraint.distance(SKRange(lowerLimit: distanceBetween), to: enemy)
enemy.constraints!.append(constraint)
}
}
Problem 1, your zombie is moving based on time, not at a set speed. According to your code, he will always reach the player in 3 seconds. This means if he is 1 foot away, he takes 3 seconds. If he is 100 miles away, he takes 3 seconds. You need to use a dynamic duration if you are planning to use the moveTo SKAction based on the speed of the zombie. So if your zombie moves 10 points per second, you want to calculate the distance from zombie to player, and then divide by 10. Basically, your duration formula should be distance / speed
Problem 2, if you want the zombies to form a line, you are going to have to determine who the leading zombie is, and have each zombie follow the next leading zombie as opposed to all zombies following the player. Otherwise your other option is to not allow zombies to overlap, but again, you will still end up with more of a mosh pit then a line.
I would like to know how should I detect overlapping nodes while enumerating them? Or how should I make that every random generated position in Y axis is at least some points higher or lower.
This is what I do:
1 - Generate random number between -400 and 400
2 - Add those into array
3 - Enumerate and add nodes to scene with generated positions like this:
var leftPositions = [CGPoint]()
for _ in 0..<randRange(lower: 1, upper: 5){
leftPositions.append(CGPoint(x: -295, y: Helper().randomBetweenTwoNumbers(firstNumber: leftSparkMinimumY, secondNumber: leftSparkMaximumY)))
}
leftPositions.enumerated().forEach { (index, point) in
let leftSparkNode = SKNode()
leftSparkNode.position = point
leftSparkNode.name = "LeftSparks"
let leftSparkTexture = SKTexture(imageNamed: "LeftSpark")
LeftSpark = SKSpriteNode(texture: leftSparkTexture)
LeftSpark.name = "LeftSparks"
LeftSpark.physicsBody = SKPhysicsBody(texture: leftSparkTexture, size: LeftSpark.size)
LeftSpark.physicsBody?.categoryBitMask = PhysicsCatagory.LeftSpark
LeftSpark.physicsBody?.collisionBitMask = PhysicsCatagory.Bird
LeftSpark.physicsBody?.contactTestBitMask = PhysicsCatagory.Bird
LeftSpark.physicsBody?.isDynamic = false
LeftSpark.physicsBody?.affectedByGravity = false
leftSparkNode.addChild(LeftSpark)
addChild(leftSparkNode)
}
But like this sometimes they overlap each other because the generated CGPoint is too close to the previous one.
I am trying to add some amount of triangles to the wall and those triangles are rotated by 90°
To describe in image what I want to achieve:
And I want to avoid thing like this:
Your approach to this is not the best, i would suggest only storing the Y values in your position array and check against those values to make sure your nodes will not overlap. The following will insure no two sparks are within 100 points of each other. You may want to change that value depending on your node's actual height or use case.
Now, obviously if you end up adding too many sparks within an 800 point range, this just will not work and cause an endless loop.
var leftPositions = [Int]()
var yWouldOverlap = false
for _ in 0..<randRange(lower: 1, upper: 5){
//Moved the random number generator to a function
var newY = Int(randY())
//Start a loop based on the yWouldOverlap Bool
repeat{
yWouldOverlap = false
//Nested loop to range from +- 100 from the randomly generated Y
for p in newY - 100...newY + 100{
//If array already contains one of those values
if leftPosition.contains(p){
//Set the loop Bool to true, get a new random value, and break the nested for.
yWouldOverlap = true
newY = Int(randY())
break
}
}
}while(yWouldOverlap)
//If we're here, the array does not contain the new value +- 100, so add it and move on.
leftPositions.append(newY)
}
func randY() -> CGFloat{
return Helper().randomBetweenTwoNumbers(firstNumber: leftSparkMinimumY, secondNumber: leftSparkMaximumY)
}
And here is a different version of your following code.
for (index,y) in leftPositions.enumerated() {
let leftSparkNode = SKNode()
leftSparkNode.position = CGPoint(x:-295,y:CGFloat(y))
leftSparkNode.name = "LeftSparks\(index)" //All node names should be unique
let leftSparkTexture = SKTexture(imageNamed: "LeftSpark")
LeftSpark = SKSpriteNode(texture: leftSparkTexture)
LeftSpark.name = "LeftSparks"
LeftSpark.physicsBody = SKPhysicsBody(texture: leftSparkTexture, size: LeftSpark.size)
LeftSpark.physicsBody?.categoryBitMask = PhysicsCatagory.LeftSpark
LeftSpark.physicsBody?.collisionBitMask = PhysicsCatagory.Bird
LeftSpark.physicsBody?.contactTestBitMask = PhysicsCatagory.Bird
LeftSpark.physicsBody?.isDynamic = false
LeftSpark.physicsBody?.affectedByGravity = false
leftSparkNode.addChild(LeftSpark)
addChild(leftSparkNode)
}
I am working with core audio using an AUFilePlayer to load a few mp3s into a mixer unit, everything plays great however I am unable to pause the music or rewind the music back to the start. I tried Starting and stopping the AudioGraph, but once the playback is stopped, I can't get it to restart. I also tried using AudioUnitSetProperty to set the file playback to 0
i.e something along these lines:
ScheduledAudioFileRegion rgn;
memset (&rgn.mTimeStamp, 0, sizeof(rgn.mTimeStamp));
rgn.mTimeStamp.mFlags = kAudioTimeStampSampleTimeValid;
rgn.mTimeStamp.mSampleTime = 0;
rgn.mCompletionProc = NULL;
rgn.mCompletionProcUserData = NULL;
rgn.mAudioFile = inputFile;
rgn.mLoopCount = 1;
rgn.mStartFrame = 0;
rgn.mFramesToPlay = nPackets * inputFormat.mFramesPerPacket;
AudioUnitSetProperty(fileUnit, kAudioUnitProperty_ScheduledFileRegion,
kAudioUnitScope_Global, 0,&rgn, sizeof(rgn));
Any suggestions?
In case anyone else is dealing with a similar issue, which cost me several hours searching on google, here's what I've discovered on how to specifically retrieve and set the playhead.
To get the playhead from an AUFilePlayer unit:
AudioTimeStamp timestamp;
UInt32 size = sizeof(timestamp);
err = AudioUnitGetProperty(unit, kAudioUnitProperty_CurrentPlayTime, kAudioUnitScope_Global, 0, ×tamp, &size);
The timestamp.mSampleTime is the current playhead for that file. Cast mSampleTime to a float or a double and divide by the file's sample rate to convert to seconds.
For restarting the AUFilePlayer's playhead, I had a more complex scenario where multiple AUFilePlayers pass through a mixer and can be scheduled at different times, multiple times, and with varying loop counts. This is a real-world scenario, and getting them all to restart at the correct time took a little bit of code.
There are four scenarios for each AUFilePlayer and it's schedule:
The playhead is at the beginning, so can be scheduled normally.
The playhead is past the item's duration, and doesn't need to be scheduled at all.
The playhead is before the item has started, so the start time can be moved up.
The playhead is in the middle of playing an item, so the region playing within the file needs to be adjusted, and remaining loops need to be scheduled separately (so they play in full).
Here is some code which demonstrates this (some external structures are from my own code and not Core Audio, but the mechanism should be clear):
// Add each region
for(int iItem = 0; iItem < schedule.items.count; iItem++) {
AEFileScheduleItem *scheduleItem = [schedule.items objectAtIndex:iItem];
// Setup the region
ScheduledAudioFileRegion region;
[file setRegion:®ion schedule:scheduleItem];
// Compute where we are at in it
float playheadTime = schedule.playhead / file.sampleRate;
float endOfItem = scheduleItem.startTime + (file.duration*(1+scheduleItem.loopCount));
// There are four scenarios:
// 1. The playhead is -1
// In this case, we're all done
if(schedule.playhead == -1) {
}
// 2. The playhead is past the item start time and duration*loopCount
// In this case, just ignore it and move on
else if(playheadTime > endOfItem) {
continue;
}
// 3. The playhead is less than or equal to the start time
// In this case, simply subtract the playhead from the start time
else if(playheadTime <= scheduleItem.startTime) {
region.mTimeStamp.mSampleTime -= schedule.playhead;
}
// 4. The playhead is in the middle of the file duration*loopCount
// In this case, set the start time to zero, adjust the loopCount
// startFrame and framesToPlay
else {
// First remove the start time
region.mStartFrame = 0;
double offsetTime = playheadTime - scheduleItem.startTime;
// Next, take out any expired loops
int loopsExpired = floor(offsetTime/file.duration);
int fullLoops = region.mLoopCount - loopsExpired;
region.mLoopCount = 0;
offsetTime -= (loopsExpired * file.duration);
// Then, adjust this segment of a loop accordingly
region.mStartFrame = offsetTime * file.sampleRate;
region.mFramesToPlay = region.mFramesToPlay - region.mStartFrame;
// Finally, schedule any remaining loops separately
if(fullLoops > 0) {
ScheduledAudioFileRegion loops;
[file setRegion:&loops schedule:scheduleItem];
loops.mStartFrame = region.mFramesToPlay;
loops.mLoopCount = fullLoops-1;
if(![super.errors check:AudioUnitSetProperty(unit, kAudioUnitProperty_ScheduledFileRegion, kAudioUnitScope_Global, 0, ®ion, sizeof(region))
location:#"AudioUnitSetProperty(ScheduledFileRegion)"])
return false;
}
}
// Set the region
if(![super.errors check:AudioUnitSetProperty(unit, kAudioUnitProperty_ScheduledFileRegion, kAudioUnitScope_Global, 0, ®ion, sizeof(region))
location:#"AudioUnitSetProperty(ScheduledFileRegion)"])
return false;
}
I figured it out. In case any of you ever run into the same problem, here was my solution:
On startup, I initialize the AUGraph with an array of file player audio units. I set the play head of each track in the file player audio unit array to zero.
On “pause” , first I stop the AuGraph. Then I loop through the array of file player audio units and capture the current playhead position. Each time the pause button is pressed, I make sure I add the new current playhead position to the old playhead position to get its true position.
When the user hits play, I re initialize the AuGraph just as if I was starting the app for the very first time, only I set the playhead to the number I stored when the user hit “pause” instead of telling it to play at the start of the file.
If the user clicks stop, I set the stored playhead position to zero and then stop the AuGraph.