Low latency audio output problems on iOS (aka How to beat AUAudioUnit sampleRate, maximumFramesToRender, and ioBufferDuration into submission) - ios

Okay, I'm clearly missing some important piece here. I'm trying to do low-latency audio across the network, and my fundamental frames are 10ms. I expected this to be no problem. My target phone is an iPhone X speakers--so my hardware sample rate should be locked to 48000Hz. I'm requesting 10ms which is a nice even divisor and should be 480, 960, 1920 or 3840 depending upon how you want to slice frames/samples/bytes.
Yet, for the life of me, I absolute cannot get iOS to do anything I regard as sane. I get 10.667ms buffer duration which is ludicrous--iOS is going out of it's way to give me buffer sizes that aren't integer multiples of the sampleRate. Even worse, the frame is sightly LONG which means that I have to absorb not one but two packets of latency in order to be able to fill that buffer. I can't get maximumFrameToRender to change at all, and the system is returning 0 as my sample rate even though it quite plainly is rendering at 48000Hz.
I'm clearly missing something important--what is it? Did I forget to disconnect/connect something in order to get a direct hardware mapping? (My format is 1 which pcmFormatFloat32--I would expect pcmFormatInt16 or pcmFormatInt32 for mapping directly to hardware so something in the OS is probably getting in the way) Pointers are appreciated and I'm happy to go read more. Or is AUAudioUnit simply half-baked and I need to go backward to older, more useful APIs? Or did I completely miss the plot and low-latency audio folks use a whole different set of audio management functions?
Thanks for the help--it's much appreciated.
Output from code:
2019-11-07 23:28:29.782786-0800 latencytest[3770:50382] Ready to receive user events
2019-11-07 23:28:34.727478-0800 latencytest[3770:50382] Start button pressed
2019-11-07 23:28:34.727745-0800 latencytest[3770:50382] Launching auxiliary thread
2019-11-07 23:28:34.729278-0800 latencytest[3770:50445] Thread main started
2019-11-07 23:28:35.006005-0800 latencytest[3770:50445] Sample rate: 0
2019-11-07 23:28:35.016935-0800 latencytest[3770:50445] Buffer duration: 0.010667
2019-11-07 23:28:35.016970-0800 latencytest[3770:50445] Number of output busses: 2
2019-11-07 23:28:35.016989-0800 latencytest[3770:50445] Max frames: 4096
2019-11-07 23:28:35.017010-0800 latencytest[3770:50445] Can perform output: 1
2019-11-07 23:28:35.017023-0800 latencytest[3770:50445] Output Enabled: 1
2019-11-07 23:28:35.017743-0800 latencytest[3770:50445] Bus channels: 2
2019-11-07 23:28:35.017864-0800 latencytest[3770:50445] Bus format: 1
2019-11-07 23:28:35.017962-0800 latencytest[3770:50445] Bus rate: 0
2019-11-07 23:28:35.018039-0800 latencytest[3770:50445] Sleeping 0
2019-11-07 23:28:35.018056-0800 latencytest[3770:50445] Buffer count: 2 4096
2019-11-07 23:28:36.023220-0800 latencytest[3770:50445] Sleeping 1
2019-11-07 23:28:36.023400-0800 latencytest[3770:50445] Buffer count: 190 389120
2019-11-07 23:28:37.028610-0800 latencytest[3770:50445] Sleeping 2
2019-11-07 23:28:37.028790-0800 latencytest[3770:50445] Buffer count: 378 774144
2019-11-07 23:28:38.033983-0800 latencytest[3770:50445] Sleeping 3
2019-11-07 23:28:38.034142-0800 latencytest[3770:50445] Buffer count: 566 1159168
2019-11-07 23:28:39.039333-0800 latencytest[3770:50445] Sleeping 4
2019-11-07 23:28:39.039534-0800 latencytest[3770:50445] Buffer count: 756 1548288
2019-11-07 23:28:40.041787-0800 latencytest[3770:50445] Sleeping 5
2019-11-07 23:28:40.041943-0800 latencytest[3770:50445] Buffer count: 944 1933312
2019-11-07 23:28:41.042878-0800 latencytest[3770:50445] Sleeping 6
2019-11-07 23:28:41.043037-0800 latencytest[3770:50445] Buffer count: 1132 2318336
2019-11-07 23:28:42.048219-0800 latencytest[3770:50445] Sleeping 7
2019-11-07 23:28:42.048375-0800 latencytest[3770:50445] Buffer count: 1320 2703360
2019-11-07 23:28:43.053613-0800 latencytest[3770:50445] Sleeping 8
2019-11-07 23:28:43.053771-0800 latencytest[3770:50445] Buffer count: 1508 3088384
2019-11-07 23:28:44.058961-0800 latencytest[3770:50445] Sleeping 9
2019-11-07 23:28:44.059119-0800 latencytest[3770:50445] Buffer count: 1696 3473408
Actual code:
import UIKit
import os.log
import Foundation
import AudioToolbox
import AVFoundation
class AuxiliaryWork: Thread {
let II_SAMPLE_RATE = 48000
var iiStopRequested: Int32 = 0; // Int32 is normally guaranteed to be atomic on most architectures
var iiBufferFillCount: Int32 = 0;
var iiBufferByteCount: Int32 = 0;
func requestStop() {
iiStopRequested = 1;
}
func myAVAudioSessionInterruptionNotificationHandler(notification: Notification ) -> Void {
os_log(OSLogType.info, "AVAudioSession Interrupted: %s", notification.debugDescription)
}
func myAudioUnitProvider(actionFlags: UnsafeMutablePointer<AudioUnitRenderActionFlags>, timestamp: UnsafePointer<AudioTimeStamp>,
frameCount: AUAudioFrameCount, inputBusNumber: Int, inputData: UnsafeMutablePointer<AudioBufferList>) -> AUAudioUnitStatus {
let ppInputData = UnsafeMutableAudioBufferListPointer(inputData)
let iiNumBuffers = ppInputData.count
if (iiNumBuffers > 0) {
assert(iiNumBuffers == 2)
for bbBuffer in ppInputData {
assert(Int(bbBuffer.mDataByteSize) == 2048) // FIXME: This should be 960 or 1920 ...
iiBufferFillCount += 1
iiBufferByteCount += Int32(bbBuffer.mDataByteSize)
memset(bbBuffer.mData, 0, Int(bbBuffer.mDataByteSize)) // Just send silence
}
} else {
os_log(OSLogType.error, "Zero buffers from system")
assert(iiNumBuffers != 0) // Force crash since os_log would cause an audio hiccup due to locks anyway
}
return noErr
}
override func main() {
os_log(OSLogType.info, "Thread main started")
#if os(iOS)
let kOutputUnitSubType = kAudioUnitSubType_RemoteIO
#else
let kOutputUnitSubType = kAudioUnitSubtype_HALOutput
#endif
let audioSession = AVAudioSession.sharedInstance() // FIXME: Causes the following message No Factory registered for id
try! audioSession.setCategory(AVAudioSession.Category.playback, options: [])
try! audioSession.setMode(AVAudioSession.Mode.measurement)
try! audioSession.setPreferredSampleRate(48000.0)
try! audioSession.setPreferredIOBufferDuration(0.010)
NotificationCenter.default.addObserver(
forName: AVAudioSession.interruptionNotification,
object: nil,
queue: nil,
using: myAVAudioSessionInterruptionNotificationHandler
)
let ioUnitDesc = AudioComponentDescription(
componentType: kAudioUnitType_Output,
componentSubType: kOutputUnitSubType,
componentManufacturer: kAudioUnitManufacturer_Apple,
componentFlags: 0,
componentFlagsMask: 0)
let auUnit = try! AUAudioUnit(componentDescription: ioUnitDesc,
options: AudioComponentInstantiationOptions())
auUnit.outputProvider = myAudioUnitProvider;
auUnit.maximumFramesToRender = 256
try! audioSession.setActive(true)
try! auUnit.allocateRenderResources() // Make sure audio unit has hardware resources--we could provide the buffers from the circular buffer if we want
try! auUnit.startHardware()
os_log(OSLogType.info, "Sample rate: %d", audioSession.sampleRate);
os_log(OSLogType.info, "Buffer duration: %f", audioSession.ioBufferDuration);
os_log(OSLogType.info, "Number of output busses: %d", auUnit.outputBusses.count);
os_log(OSLogType.info, "Max frames: %d", auUnit.maximumFramesToRender);
os_log(OSLogType.info, "Can perform output: %d", auUnit.canPerformOutput)
os_log(OSLogType.info, "Output Enabled: %d", auUnit.isOutputEnabled)
//os_log(OSLogType.info, "Audio Format: %p", audioFormat)
var bus0 = auUnit.outputBusses[0]
os_log(OSLogType.info, "Bus channels: %d", bus0.format.channelCount)
os_log(OSLogType.info, "Bus format: %d", bus0.format.commonFormat.rawValue)
os_log(OSLogType.info, "Bus rate: %d", bus0.format.sampleRate)
for ii in 0..<10 {
if (iiStopRequested != 0) {
os_log(OSLogType.info, "Manual stop requested");
break;
}
os_log(OSLogType.info, "Sleeping %d", ii);
os_log(OSLogType.info, "Buffer count: %d %d", iiBufferFillCount, iiBufferByteCount)
Thread.sleep(forTimeInterval: 1.0);
}
auUnit.stopHardware()
}
}
class FirstViewController: UIViewController {
var thrAuxiliaryWork: AuxiliaryWork? = nil;
override func viewDidLoad() {
super.viewDidLoad()
// Do any additional setup after loading the view.
}
#IBAction func startButtonPressed(_ sender: Any) {
os_log(OSLogType.error, "Start button pressed");
os_log(OSLogType.error, "Launching auxiliary thread");
thrAuxiliaryWork = AuxiliaryWork();
thrAuxiliaryWork?.start();
}
#IBAction func stopButtonPressed(_ sender: Any) {
os_log(OSLogType.error, "Stop button pressed");
os_log(OSLogType.error, "Manually stopping auxiliary thread");
thrAuxiliaryWork?.requestStop();
}
#IBAction func muteButtonPressed(_ sender: Any) {
os_log(OSLogType.error, "Mute button pressed");
}
#IBAction func unmuteButtonPressed(_ sender: Any) {
os_log(OSLogType.error, "Unmute button pressed");
}
}

You cannot beat iOS silicon hardware into submission by assuming the API will do it for you. You have to do your own buffering if you want to abstract the hardware.
For the very best (lowest) latencies, your software will have to (potentially dynamically) adapt to the actual hardware capabilities, which can vary from device to device, and mode to mode.
The hardware sample rate appears to be either 44.1ksps (older iOS devices), 48ksps (newer arm64 iOS devices), or an integer multiple thereof (and potentially other rates when plugging in non-AirPod Bluetooth headsets, or external ADCs). The actual hardware DMA (or equivalent) buffers seem to always be a power of 2 in size, potentially down to 64 samples on newest devices. However various iOS power saving modes will increase the buffer size (by powers of 2) up to 4k samples, especially on older iOS devices. If you request a sample rate other than the hardware rate, the OS might resample the buffers to a different size than a power of 2, and this size can change from Audio Unit callback to subsequent callback if the resampling ratio isn't an exact integer.
Audio Units are the lowest level accessible via public API on iOS devices. Everything else is built on top, and thus potentially incurs greater latencies. For instance, if you use the Audio Queue API with non-hardware buffer sizes, the OS will internally use power-of-2 audio buffers to access the hardware, and chop them up or fractionally concatenate them to return or fetch Audio Queue buffers of non-hardware sizes. Slower and jittery.
Far from being half-baked, for a long time the iOS API was the only API usable on mobile phones and tablets for live low-latency music performance. But by developing software matched to the hardware.

Related

OpenVPN v3 Dbus client not receiving all signals

I'm writing an Applet for Linux Mint/Cinnamon to manage OpenVPN v3 connections.
In order to avoid synchronous calls that can cause the DE to stutter or freeze, I'm writing a simple DBus client using the Gio and GLib libraries provided by GJS. This allows an asynchronous, partly event-driven approach and should avoid any nasty side effects. It's my first time using any of these technologies but the OpenVPN DBus API is pretty well documented and the API docs for Gio and GLib are also good.
The problem I have is with signal subscriptions, specifically the StatusChange signal published by the net.openvpn.v3.sessions service. A bunch of these signals are published whenever a connection is established, paused, resumed or closed. Most of the signals are picked up by my subscribed listener, but not all of them. In particular, I don't receive the session closed signal.
Using the dbus-monitor commandline tool, you can see all the StatusChange signals published when a connection is established (7 signals) and then closed (2 signals):
$ sudo dbus-monitor --system "type='signal',interface='net.openvpn.v3.sessions',member='StatusChange'"
...
// Connect:
signal time=1625847543.107244 sender=:1.891 -> destination=:1.892 serial=2745 path=/net/openvpn/v3/sessions/052850e7s915fs483esb3e7s3afb389a2e49; interface=net.openvpn.v3.sessions; member=StatusChange
uint32 3
uint32 27
string "session_path=/net/openvpn/v3/sessions/052850e7s915fs483esb3e7s3afb389a2e49, backend_pid=42584"
signal time=1625847543.116395 sender=:1.891 -> destination=:1.892 serial=2762 path=/net/openvpn/v3/sessions/052850e7s915fs483esb3e7s3afb389a2e49; interface=net.openvpn.v3.sessions; member=StatusChange
uint32 3
uint32 17
string "session_path=/net/openvpn/v3/sessions/052850e7s915fs483esb3e7s3afb389a2e49 backend_busname=net.openvpn.v3.backends.be42585 backend_path=/net/openvpn/v3/backends/session"
signal time=1625847543.117286 sender=:1.891 -> destination=(null destination) serial=2764 path=/net/openvpn/v3/sessions/052850e7s915fs483esb3e7s3afb389a2e49; interface=net.openvpn.v3.sessions; member=StatusChange
uint32 2
uint32 2
string "config_path=/net/openvpn/v3/configuration/9dd3fa9cxb6e0x48acxaa1ex566312bea232"
signal time=1625847543.638519 sender=:1.891 -> destination=(null destination) serial=2775 path=/net/openvpn/v3/sessions/052850e7s915fs483esb3e7s3afb389a2e49; interface=net.openvpn.v3.sessions; member=StatusChange
uint32 2
uint32 2
string "config_path=/net/openvpn/v3/configuration/9dd3fa9cxb6e0x48acxaa1ex566312bea232"
signal time=1625847543.638533 sender=:1.891 -> destination=(null destination) serial=2776 path=/net/openvpn/v3/sessions/052850e7s915fs483esb3e7s3afb389a2e49; interface=net.openvpn.v3.sessions; member=StatusChange
uint32 2
uint32 6
string ""
signal time=1625847543.735357 sender=:1.891 -> destination=(null destination) serial=2777 path=/net/openvpn/v3/sessions/052850e7s915fs483esb3e7s3afb389a2e49; interface=net.openvpn.v3.sessions; member=StatusChange
uint32 2
uint32 6
string ""
signal time=1625847543.974784 sender=:1.891 -> destination=(null destination) serial=2778 path=/net/openvpn/v3/sessions/052850e7s915fs483esb3e7s3afb389a2e49; interface=net.openvpn.v3.sessions; member=StatusChange
uint32 2
uint32 7
string ""
// Disconnect:
signal time=1625847646.846790 sender=:1.891 -> destination=:1.892 serial=2834 path=/net/openvpn/v3/sessions/052850e7s915fs483esb3e7s3afb389a2e49; interface=net.openvpn.v3.sessions; member=StatusChange
uint32 3
uint32 28
string "Session closed"
signal time=1625847646.848262 sender=:1.891 -> destination=:1.892 serial=2839 path=/net/openvpn/v3/sessions/052850e7s915fs483esb3e7s3afb389a2e49; interface=net.openvpn.v3.sessions; member=StatusChange
uint32 3
uint32 19
string ""
The following code creates a subscription that, as far as I can tell, should receive the same signals as above. Note I'm using the lower-level approach to obtain a subscription here; the subscription is made on the global connection, rather than via a DBusProxy for a specific object path. I've tried both approaches (same result) but the following should be a closer analogue to the dbus-monitor command above.
subscribeToStatusChangeSignals() {
this.statusChangeHandlerId = Gio.DBus.system.signal_subscribe(
'net.openvpn.v3.sessions',
'net.openvpn.v3.sessions',
'StatusChange',
null,
null,
Gio.DBusSignalFlags.NONE,
this._handleGlobalStatusChangeSignal
);
}
_handleGlobalStatusChangeSignal(connection, sender, path, iface, signal, params) {
let container = params.deep_unpack();
let statusMajorCode = container[0];
let statusMinorCode = container[1];
let statusMajor = lookupStatusMajor(statusMajorCode); // lookup the corresponding text
let statusMinor = lookupStatusMinor(statusMinorCode); // from something resembling an enum
let message = container[2];
global.log(`Received StatusChange signal
path: [${path}]
Status Major: [${statusMajorCode} - ${statusMajor}]
Status Minor: [${statusMinorCode} - ${statusMinor}]
Message: [${message}]`
);
}
The resulting logs when opening and closing the same connection as before:
// Connect:
Cjs-Message: 18:19:03.117: JS LOG: [LookingGlass/info] Received StatusChange signal
path: [/net/openvpn/v3/sessions/052850e7s915fs483esb3e7s3afb389a2e49]
Status Major: [2 - CONNECTION]
Status Minor: [2 - CFG_OK]
Message: [config_path=/net/openvpn/v3/configuration/9dd3fa9cxb6e0x48acxaa1ex566312bea232]
Cjs-Message: 18:19:03.638: JS LOG: [LookingGlass/info] Received StatusChange signal
path: [/net/openvpn/v3/sessions/052850e7s915fs483esb3e7s3afb389a2e49]
Status Major: [2 - CONNECTION]
Status Minor: [2 - CFG_OK]
Message: [config_path=/net/openvpn/v3/configuration/9dd3fa9cxb6e0x48acxaa1ex566312bea232]
Cjs-Message: 18:19:03.639: JS LOG: [LookingGlass/info] Received StatusChange signal
path: [/net/openvpn/v3/sessions/052850e7s915fs483esb3e7s3afb389a2e49]
Status Major: [2 - CONNECTION]
Status Minor: [6 - CONN_CONNECTING]
Message: []
Cjs-Message: 18:19:03.735: JS LOG: [LookingGlass/info] Received StatusChange signal
path: [/net/openvpn/v3/sessions/052850e7s915fs483esb3e7s3afb389a2e49]
Status Major: [2 - CONNECTION]
Status Minor: [6 - CONN_CONNECTING]
Message: []
Cjs-Message: 18:19:03.974: JS LOG: [LookingGlass/info] Received StatusChange signal
path: [/net/openvpn/v3/sessions/052850e7s915fs483esb3e7s3afb389a2e49]
Status Major: [2 - CONNECTION]
Status Minor: [7 - CONN_CONNECTED]
Message: []
// Disconnect:
<nada>
One pattern i've noticed is that the signals i do receive all have a null destination in the dbus-monitor output:
... sender=:1.891 -> destination=(null destination) ...
while the signals I don't receive have a specific destination:
... sender=:1.891 -> destination=:1.892 ...
Presumably these are direct signals intended for a particular recipient, rather than signals broadcast to all interested subscribers, but I haven't found this explained anywhere in the docs.
So the question is, why do I receive some signals but not all? Is this by design, or an issue with Gio, or (more likely) an issue with how I'm using it?
After a bit more digging it appears this behaviour is by design - Signals that carry a destination value are treated as unicast messages. Subscribers other than the intended recipient will only receive such messages if they are configured to eavesdrop. Presumably this is the case for dbus-monitor.
Source: DBus Specification (Message Routing)

FFMPEG making requests for each frame when decoding a stream, slow performance

I am having an issue playing MOV camera captured files from an iPhone. My FFMPEG implementation has no problem playing most file formats, this issue is exclusive only for camera captured MOV.
When trying to open the file, I can see in the logs that many requests are made, each requests decoding only one frame, before making a new request which results the video being buffered extremely slowly.
It takes roughly a minute to buffer about a few seconds of the video.
Another thing to mention is that the very same problematic file is played without an issue locally. The problem is when trying to decode while streaming.
I compiled my code on Xcode 11, iOS SDK 13, with cocoapods mobile-ffmpeg-https 4.2.
Here is a rough representation of my code, its pretty standard:
Here is how I open AVFormatContext:
AVFormatContext *context = avformat_alloc_context();
context->interrupt_callback.callback = FFMPEGFormatContextIOHandler_IO_CALLBACK;
context->interrupt_callback.opaque = (__bridge void *)(handler);
av_log_set_level(AV_LOG_TRACE);
int result = avformat_open_input(&context, [request.urlAsString UTF8String], NULL, NULL);
if (result != 0) {
if (context != NULL) {
avformat_free_context(context);
}
return nil;
}
result = avformat_find_stream_info(context, NULL);
if (result < 0) {
avformat_close_input(&context);
return nil;
}
Video decoder is opened like so, audio decoder is nearly identical
AVCodecParameters *params = context->streams[streamIndex]->codecpar;
AVCodec *codec = avcodec_find_decoder(params->codec_id);
if (codec == NULL) {
return NULL;
}
AVCodecContext *codecContext = avcodec_alloc_context3(codec);
if (codecContext == NULL) {
return NULL;
}
codecContext->thread_count = 6;
int result = avcodec_parameters_to_context(codecContext, params);
if (result < 0) {
avcodec_free_context(&codecContext);
return NULL;
}
result = avcodec_open2(codecContext, codec, NULL);
if (result < 0) {
avcodec_free_context(&codecContext);
return NULL;
}
I read the data from the server like so:
AVPacket packet;
int result = av_read_frame(formatContext, &avPacket);
if (result == 0) {
avcodec_send_packet(codecContext, &avPacket);
// .... decode ....
}
Logs after opening the decoders:
// [tls] Request is made here
// [tls] Request response headers are here
Probing mov,mp4,m4a,3gp,3g2,mj2 score:100 size:2048
Probing mp3 score:1 size:2048
[mov,mp4,m4a,3gp,3g2,mj2 # 0x115918e00] Format mov,mp4,m4a,3gp,3g2,mj2 probed with size=2048 and score=100
[mov,mp4,m4a,3gp,3g2,mj2 # 0x115918e00] type:'ftyp' parent:'root' sz: 20 8 23077123
[mov,mp4,m4a,3gp,3g2,mj2 # 0x115918e00] ISO: File Type Major Brand: qt
[mov,mp4,m4a,3gp,3g2,mj2 # 0x115918e00] type:'wide' parent:'root' sz: 8 28 23077123
[mov,mp4,m4a,3gp,3g2,mj2 # 0x115918e00] type:'mdat' parent:'root' sz: 23066642 36 23077123
// [tls] Request is made here
// [tls] Request response headers are here
[mov,mp4,m4a,3gp,3g2,mj2 # 0x115918e00] stream 0, sample 4, dts 133333
[mov,mp4,m4a,3gp,3g2,mj2 # 0x115918e00] stream 1, sample 48, dts 1114558
[mov,mp4,m4a,3gp,3g2,mj2 # 0x115918e00] stream 2, sample 1, dts 2666667
[h264 # 0x116080200] nal_unit_type: 1(Coded slice of a non-IDR picture), nal_ref_idc: 1
// [tls] Request is made here
// [tls] Request response headers are here
[mov,mp4,m4a,3gp,3g2,mj2 # 0x115918e00] stream 0, sample 4, dts 133333
[mov,mp4,m4a,3gp,3g2,mj2 # 0x115918e00] stream 1, sample 48, dts 1114558
[mov,mp4,m4a,3gp,3g2,mj2 # 0x115918e00] stream 2, sample 1, dts 2666667
[h264 # 0x116080200] nal_unit_type: 1(Coded slice of a non-IDR picture), nal_ref_idc: 1
// [tls] Request is made here
// [tls] Request response headers are here
// ...
These are some warnings I found in the log
[mov,mp4,m4a,3gp,3g2,mj2 # 0x11c030800] interrupted
[mov,mp4,m4a,3gp,3g2,mj2 # 0x11c030800] stream 0: start_time: 0.000 duration: 11.833
[mov,mp4,m4a,3gp,3g2,mj2 # 0x11c030800] stream 1: start_time: 0.000 duration: 11.832
[mov,mp4,m4a,3gp,3g2,mj2 # 0x11c030800] stream 2: start_time: 0.000 duration: 11.833
[mov,mp4,m4a,3gp,3g2,mj2 # 0x11c030800] stream 3: start_time: 0.000 duration: 11.833
[mov,mp4,m4a,3gp,3g2,mj2 # 0x11c030800] format: start_time: 0.000 duration: 11.833 bitrate=15601 kb/s
[mov,mp4,m4a,3gp,3g2,mj2 # 0x11c030800] Could not find codec parameters for stream 0 (Video: h264, 1 reference frame (avc1 / 0x31637661), none(bt709, left), 1920x1080, 1/1200, 15495 kb/s): unspecified pixel format
Consider increasing the value for the 'analyzeduration' and 'probesize' options
[mov,mp4,m4a,3gp,3g2,mj2 # 0x11c030800] After avformat_find_stream_info() pos: 23077123 bytes read:16293 seeks:1 frames:0
Also when calling avformat_open_input(...), 2 GET requests are made, before the returning.
Notice the "Probing mp3 score:1", that is not shown for other MOV files or any other files.
I have tried different versions of ffmpeg, I have tried messing around with the delays of the stream, I tried removing my custom interrupt callback, nothing have worked.
Code works fine with any other videos I have tested (mp4, mkv, avi).
Metadata of the test file:
Metadata:
major_brand : qt
minor_version : 0
compatible_brands: qt
creation_time : 2019-04-14T08:17:03.000000Z
com.apple.quicktime.make: Apple
com.apple.quicktime.model: iPhone 7
com.apple.quicktime.software: 12.2
com.apple.quicktime.creationdate: 2019-04-14T11:17:03+0300
Duration: 00:00:16.83, bitrate: N/A
Stream #0:0(und), 0, 1/600: Video: h264, 1 reference frame (avc1 / 0x31637661), none(bt709), 1920x1080 (0x0), 0/1, 15301 kb/s, 30 fps, 30 tbr, 600 tbn (default)
Metadata:
creation_time : 2019-04-14T08:17:03.000000Z
handler_name : Core Media Video
encoder : H.264
Stream #0:1(und), 0, 1/44100: Audio: aac (mp4a / 0x6134706D), 44100 Hz, mono, 100 kb/s (default)
Metadata:
creation_time : 2019-04-14T08:17:03.000000Z
handler_name : Core Media Audio
Stream #0:2(und), 0, 1/600: Data: none (mebx / 0x7862656D), 0/1, 0 kb/s (default)
Metadata:
creation_time : 2019-04-14T08:17:03.000000Z
handler_name : Core Media Metadata
Stream #0:3(und), 0, 1/600: Data: none (mebx / 0x7862656D), 0/1, 0 kb/s (default)
Metadata:
creation_time : 2019-04-14T08:17:03.000000Z
handler_name : Core Media Metadata
I found a fix for this (sorta of):
Set the io_open callback of the AVFormatContext to your own function, and then when that is called, after you call the default io_open you change the buffer size.
static int (*IO_OPEN_DEFAULT)(struct AVFormatContext *s, AVIOContext **pb, const char *url, int flags, AVDictionary **options);
int IO_OPEN_OVERRIDE(struct AVFormatContext *s, AVIOContext **pb, const char *url, int flags, AVDictionary **options) {
int result = IO_OPEN_DEFAULT(s, pb, url, flags, options);
pb[0]->buffer_size = 41239179;
return result;
}
This fixes the issue. The value you set it to is usually very large (20 to 40MB). You can get that value from the second network request bytes range when you are opening the format context (first network request is made with bytes range 0-* and then the second network request is made with bytes range XXXX-* where XXXX should be the buffer size).
The reason why this fixes the issue, is because by buffering all of that data, aviocontext no longer needs to make a new network request to get the audio data. The audio data is already buffered (or at least the first position is).
There might be a better way to fix this issue, it seems like apple MOV files separate their video and audio data with these massive chunks for some reason and that causes ffmpeg to make a million network requests, for each frame.

How to get right AVAudioFormat for connecting nodes

I've got crash with this code
// ViewController.swift
import UIKit
import AVFoundation
class ViewController: UIViewController {
var engine:AVAudioEngine!
var EQNode:AVAudioUnitEQ!
override func viewDidLoad() {
engine.reset()
let Format = engine.inputNode.outputFormat(forBus: 0)
print("channelcount:",engine.inputNode.inputFormat(forBus: 0).channelCount)
//----->Start CRASH App stoped here
engine.connect(engine.inputNode, to: EQNode, format: Format)
engine.connect(EQNode, to: engine.mainMixerNode, format: Format)
var error: NSError?
engine.prepare()
print("done prepare")
do {
try engine.start()
} catch {
print(error)
}
print("done start")
}
}
And if I change Format to nil it make my app not working but not crash.
All of this work perfectly fine on Xcode simulator with no error.
But in the real iOS device (I use iPad 2019) test it crash.
Detail about my app: Live microphone adjust in Equalizer and display Equalized sound real-time.
ERROR:
SelfhearEQ[3532:760180] [aurioc] AURemoteIO.cpp:1086:Initialize: failed: -10851
(enable 1, outf< 2 ch, 0 Hz, Float32, non-inter> inf< 1 ch, 44100 Hz, Float32>)
channelcount: 0
2019-10-22 18:01:29.891748+0700 SelfhearEQ[3532:760180] [aurioc] AURemoteIO.cpp:1086:Initialize: failed: -10851
(enable 1, outf< 2 ch, 0 Hz, Float32, non-inter> inf< 1 ch, 44100 Hz, Float32>)
2019-10-22 18:01:29.892326+0700 SelfhearEQ[3532:760180] [avae]
AVAEInternal.h:76 required condition is false: [AVAudioEngineGraph.mm:2127:_Connect: (IsFormatSampleRateAndChannelCountValid(format))]
2019-10-22 18:01:29.896270+0700 SelfhearEQ[3532:760180] *** Terminating app due to uncaught exception 'com.apple.coreaudio.avfaudio', reason: 'required condition is false: IsFormatSampleRateAndChannelCountValid(format)'
I found the answer for this it nothing about formatting causing this error.
Check it out on my other question it fixed.
avaudioengine-connect-crash-on-hardware-not-simulator

iOS app crashes while synthesising audio: buffer, frames <unavailable>

I’m synthesising audio on an iPhone using additive synthesis and my app, written in Objective C, crashes while playing audio. The synthesis engine worked in a previous version of the app and with an earlier iOS. I'm currently testing it for iOS 10.3.3 using Xcode 8.3.3.
EDIT - in the current version, the view controller sends MIDI note on messages to communicate with the synthesis engine. The previous version did not use MIDI commands.
The app synthesises two sounds: a bell and a chorus of sine tones. It does this smoothly and without transients. However it will crash if one sound is playing while attempting to play the other. Yet it never crashes if the same sound is repeated or if the user stops the chorus sound before playing the bell sound (or vice versa).
The bell tone has 11 partials of fixed frequency, each partial shaped by an amplitude envelope. The chorus tone has 5 partials each with a pitch shift envelope and an amplitude envelope. Envelope duration is calculated at runtime from when a user triggers a sound until the end of the last active state, as shown in the diagram (below).
The crash happens consistently under two conditions.
Firstly, a crash can be caused by playing a bell while a chorus sound is still playing, in which case Xcode goes into debug looking for Amplitude partial 6, a partial that would not be used if the buffer was still trying to access a chorus sound which only has 5 partials.
The diagram shows what should happen if two sound events played in the first state (between 0:00 and 0:24) are followed by a third sound played in the following state. The envelope duration of all three is calculated to end at 1:36. Normally, the bell sound (between 0:24 and 0:48) would last until 1:36, unless a player interrupts it by playing another sound as happens with the previous two sounds. However, being the first sound played in a new state, playing it sends the app into debug.
Alternately, a crash can also be caused by playing a chorus sound while a bell is still playing, in which Xcode goes into debug looking at Pitch shift partial 1, a partial that would never be used if the buffer was still trying to access a bell sound where pitch is fixed.
In both examples, it looks as if my code fails when it tries to access a buffer that was active prior to the change of state. But I can't find the reason. I tried changing the buffer size but that made little difference (it just happened to be set to 5 when I produced the logs below).
Below are two methods I use to service buffer input for bell and chorus synthesis, the typedef that defines buffer states and a debug log for a typical bell crash and chorus crash. I need some feedback on these before diving into the routine where audio is synthesised into the output buffer. I'm happy to share that too but hopefully the problem might already be obvious for someone else.
The app almost works but for the past few weeks I’ve broken the code and fixed it several times without getting closer to solving this problem.
My code is an extension of an extremely helpful AudioBufferPlayer and Synth example created by Mattias Hollejman and refined by Mario Diana.
UPDATE
I added a new method called showPark to produce a clearer log showing the state of the audio buffer as each live event arrives. When audio buffer size is 3, the log clearly shows the buffer filling after three events leaving no where to go for all subsequent events.
The problem is not evident in the first 24 seconds because the synthesis parameters remain the same. However after 24 seconds, when event 6 happens, things change and the synthesis engine cannot access parameters it needs to synthesise a bell sound. I think this explains how the crash happens. On that basis I'm trying to work out the best way to fix the problem and I welcome any suggestions from anyone who knows what to do.
Here is a crash log.
crash log (latest)
2017-08-01 13:48:59.330 SatGam2[1169:1226966] PlayView loaded (selectedFamily:1 selectedPlayerID:9)
2017-08-01 13:48:59.331 SatGam2[1169:1226966] ENABLED (player 9: state 0)
2017-08-01 13:49:01.071 SatGam2[1169:1226966]
2017-08-01 13:49:01.072 SatGam2[1169:1226966] EVENT 1
2017-08-01 13:49:01.072 SatGam2[1169:1226966]
2017-08-01 13:49:01.072 SatGam2[1169:1226966] MIDI Status: 0 MIDI Note: 0 State: ToneEventStateInactive
2017-08-01 13:49:01.073 SatGam2[1169:1226966] MIDI Status: 0 MIDI Note: 0 State: ToneEventStateInactive
2017-08-01 13:49:01.073 SatGam2[1169:1226966] MIDI Status: 0 MIDI Note: 0 State: ToneEventStateInactive
2017-08-01 13:49:01.073 SatGam2[1169:1226966] update parkPointer [1]
2017-08-01 13:49:01.073 SatGam2[1169:1226966]
2017-08-01 13:49:01.074 SatGam2[1169:1226966] Risset Chorus playing MIDI note number: 1 length: 95
2017-08-01 13:49:01.074 SatGam2[1169:1226966] [playChorus - buffer: 0 state: 0]
2017-08-01 13:49:03.873 SatGam2[1169:1226966] EVENT 2
2017-08-01 13:49:03.874 SatGam2[1169:1226966]
2017-08-01 13:49:03.874 SatGam2[1169:1226966] MIDI Status: 144 MIDI Note: 1 State: ToneEventStatePressed
2017-08-01 13:49:03.874 SatGam2[1169:1226966] MIDI Status: 0 MIDI Note: 0 State: ToneEventStateInactive
2017-08-01 13:49:03.875 SatGam2[1169:1226966] MIDI Status: 0 MIDI Note: 0 State: ToneEventStateInactive
2017-08-01 13:49:03.875 SatGam2[1169:1226966] update parkPointer [2]
2017-08-01 13:49:03.875 SatGam2[1169:1226966]
2017-08-01 13:49:03.875 SatGam2[1169:1226966] Risset Chorus playing MIDI note number: 3 length: 92
2017-08-01 13:49:03.876 SatGam2[1169:1226966] [playChorus - buffer: 1 state: 0]
2017-08-01 13:49:06.180 SatGam2[1169:1226966] EVENT 3
2017-08-01 13:49:06.180 SatGam2[1169:1226966]
2017-08-01 13:49:06.180 SatGam2[1169:1226966] MIDI Status: 144 MIDI Note: 1 State: ToneEventStatePressed
2017-08-01 13:49:06.180 SatGam2[1169:1226966] MIDI Status: 144 MIDI Note: 3 State: ToneEventStatePressed
2017-08-01 13:49:06.181 SatGam2[1169:1226966] MIDI Status: 0 MIDI Note: 0 State: ToneEventStateInactive
2017-08-01 13:49:06.181 SatGam2[1169:1226966] update parkPointer [0]
2017-08-01 13:49:06.181 SatGam2[1169:1226966]
2017-08-01 13:49:06.181 SatGam2[1169:1226966] Risset Chorus playing MIDI note number: 5 length: 90
2017-08-01 13:49:06.182 SatGam2[1169:1226966] [playChorus - buffer: 2 state: 0]
2017-08-01 13:49:10.162 SatGam2[1169:1226966] EVENT 4
2017-08-01 13:49:10.162 SatGam2[1169:1226966]
2017-08-01 13:49:10.163 SatGam2[1169:1226966] MIDI Status: 144 MIDI Note: 1 State: ToneEventStatePressed
2017-08-01 13:49:10.163 SatGam2[1169:1226966] MIDI Status: 144 MIDI Note: 3 State: ToneEventStatePressed
2017-08-01 13:49:10.163 SatGam2[1169:1226966] MIDI Status: 144 MIDI Note: 5 State: ToneEventStatePressed
2017-08-01 13:49:10.163 SatGam2[1169:1226966] update parkPointer [1]
2017-08-01 13:49:10.163 SatGam2[1169:1226966]
2017-08-01 13:49:10.164 SatGam2[1169:1226966] Risset Chorus playing MIDI note number: 7 length: 86
2017-08-01 13:49:11.352361+1000 SatGam2[1169:1228409] [aqme] 254: AQDefaultDevice (173): skipping input stream 0 0 0x0
2017-08-01 13:49:13.041 SatGam2[1169:1226966]
2017-08-01 13:49:13.042 SatGam2[1169:1226966] EVENT 5
2017-08-01 13:49:13.043 SatGam2[1169:1226966]
2017-08-01 13:49:13.043 SatGam2[1169:1226966] MIDI Status: 144 MIDI Note: 1 State: ToneEventStatePressed
2017-08-01 13:49:13.043 SatGam2[1169:1226966] MIDI Status: 144 MIDI Note: 3 State: ToneEventStatePressed
2017-08-01 13:49:13.043 SatGam2[1169:1226966] MIDI Status: 144 MIDI Note: 5 State: ToneEventStatePressed
2017-08-01 13:49:13.044 SatGam2[1169:1226966] update parkPointer [2]
2017-08-01 13:49:13.044 SatGam2[1169:1226966]
2017-08-01 13:49:13.044 SatGam2[1169:1226966] Risset Chorus playing MIDI note number: 9 length: 83
2017-08-01 13:49:23.332 SatGam2[1169:1226966] ENABLED (player 9: state 1)
2017-08-01 13:49:25.228 SatGam2[1169:1226966] EVENT 6
2017-08-01 13:49:25.228 SatGam2[1169:1226966]
2017-08-01 13:49:25.229 SatGam2[1169:1226966] MIDI Status: 144 MIDI Note: 1 State: ToneEventStatePressed
2017-08-01 13:49:25.229 SatGam2[1169:1226966] MIDI Status: 144 MIDI Note: 3 State: ToneEventStatePressed
2017-08-01 13:49:25.229 SatGam2[1169:1226966] MIDI Status: 144 MIDI Note: 5 State: ToneEventStatePressed
2017-08-01 13:49:25.229 SatGam2[1169:1226966] update parkPointer [0]
2017-08-01 13:49:25.229 SatGam2[1169:1226966]
2017-08-01 13:49:25.230 SatGam2[1169:1226966] Risset Bell playing MIDI note number: 0 length: 71
SatGam2 was compiled with optimization - stepping may behave oddly; variables may not be available.
(lldb) bt
* thread #9, name = 'com.apple.coreaudio.AQClient', stop reason = EXC_BAD_ACCESS (code=1, address=0xb174)
* frame #0: 0x000a1ea5 SatGam2`-[Synth amplitudeLookupPartial6:](self=<unavailable>, _cmd="amplitudeLookupPartial6:", n=<unavailable>) at Synth.m:988 [opt]
frame #1: 0x000a1519 SatGam2`-[Synth fillBuffer:frames:](self=0x7a831a00, _cmd="fillBuffer:frames:", buffer=<unavailable>, frames=<unavailable>) at Synth.m:805 [opt]
frame #2: 0x000a516c SatGam2`__39-[PlayViewController startAudioSession]_block_invoke((null)=0x7ce7f8e0, buffer=<unavailable>, audioFormat=AudioStreamBasicDescription # 0xb0422ab8) at PlayViewController.m:397 [opt]
frame #3: 0x000a3aa8 SatGam2`PlayCallback(inUserData=0x7946f4c0, inAudioQueue=<unavailable>, inBuffer=<unavailable>) at AudioBufferPlayer.m:30 [opt]
(the rest of the log is the same as logs shown below)
Here is the code for the new method
- (void)showPark:(uint8_t)midiStatus data1:(uint8_t)mapped_MIDINoteNumber
{
// see if MIDI events are currently active and what kind of event
eventCount++;
NSLog(#"\n\n");
NSLog(#"EVENT %i", eventCount);
NSLog(#"\n");
for (int n = 0; n < MaxToneEvents; ++n)
{
// scan buffers
int a = _tones[n].state;
int b = _tones[n].midiStatus;
int c = _tones[n].midiNote;
NSLog(#" MIDI Status: %3d MIDI Note: %3d State: %#", b, c, [self getCurrent:(int)a]);
}
// update parkPointer
parkPointer = (parkPointer + 1) % MaxToneEvents;
NSLog(#" update parkPointer [%i]", parkPointer);
NSLog(#"\n");
}
-(NSString *)getCurrent:(int)state
{
switch(state){
case ToneEventStateInactive:
return #"ToneEventStateInactive";
case ToneEventStatePressed:
return #"ToneEventStatePressed";
case ToneEventStateReleased:
return #"ToneEventStateReleased";
default:
return #"null";
}
}
showPark is called from a method not shown previously so I'll include it here.
- (void)sendMIDIEvent:(uint8_t)midiStatus
data1:(uint8_t)mapped_MIDINoteNumber // dekany buttons mapped to MIDI Note Numbers 0-9
data2:(uint8_t)data2
{
// 1. check parking bay
[self showPark:(uint8_t)midiStatus data1:(uint8_t)mapped_MIDINoteNumber];
// 2. identify MIDI status
if (midiStatus == MIDI_Status_KeyOn_RissetChorus) // 144
{
// 3. pick up duration and send a message to playChorus
[self playChorus:(uint8_t)midiStatus
chorusPitch:(uint8_t)mapped_MIDINoteNumber
length:(int)noteLength];
}
else
{
if (midiStatus == MIDI_Status_KeyOn_RissetBell) // 145
{
// 4. pick up duration and send a message to playBell
[self playBell:(uint8_t)midiStatus
bellPitch:(uint8_t)mapped_MIDINoteNumber
length:(int)noteLength];
}
}
}
playBell
-(void)playBell:(uint8_t)midiStatus
bellPitch:(uint8_t)midiNoteNumber
length:(int)length;
{
hasPitchEnvelope = FALSE;
for (int n = 0; n < MaxToneEvents; ++n)
{
if (_tones[n].state == ToneEventStateInactive) // find an empty slot
{
_tones[n].state = ToneEventStatePressed;
_tones[n].frequency = _pitches[midiNoteNumber];
_tones[n].phase1 = 0.0f;
_tones[n].phase2 = 0.0f;
_tones[n].phase3 = 0.0f;
_tones[n].phase4 = 0.0f;
_tones[n].phase5 = 0.0f;
_tones[n].phase6 = 0.0f;
_tones[n].phase7 = 0.0f;
_tones[n].phase8 = 0.0f;
_tones[n].phase9 = 0.0f;
_tones[n].phase10 = 0.0f;
_tones[n].phase11 = 0.0f;
_tones[n].levelEnvelope1 = _levelPartialEnvelope1;
_tones[n].levelEnvelope2 = _levelPartialEnvelope2;
_tones[n].levelEnvelope3 = _levelPartialEnvelope3;
_tones[n].levelEnvelope4 = _levelPartialEnvelope4;
_tones[n].levelEnvelope5 = _levelPartialEnvelope5;
_tones[n].levelEnvelope6 = _levelPartialEnvelope6;
_tones[n].levelEnvelope7 = _levelPartialEnvelope7;
_tones[n].levelEnvelope8 = _levelPartialEnvelope8;
_tones[n].levelEnvelope9 = _levelPartialEnvelope9;
_tones[n].levelEnvelope10 = _levelPartialEnvelope10;
_tones[n].levelEnvelope11 = _levelPartialEnvelope11;
_tones[n].gain = 1.0f;
_tones[n].envStep = 0.0f;
_tones[n].envDelta = ENVELOPE_LENGTH / length; // GS Envelope
_tones[n].fadeOut = 1.0f;
return;
}
}
}
playChorus
- (void)playChorus:(uint8_t)midiStatus
chorusPitch:(uint8_t)midiNoteNumber
length:(int)length;
{
hasPitchEnvelope = TRUE;
for (int n = 0; n < MaxToneEvents; ++n)
{
if (_tones[n].state == ToneEventStateInactive) // find an empty slot
{
_tones[n].state = ToneEventStatePressed;
_tones[n].frequency = _pitches[midiNoteNumber];
_tones[n].phase1 = 0.0f;
_tones[n].phase2 = 0.0f;
_tones[n].phase3 = 0.0f;
_tones[n].phase4 = 0.0f;
_tones[n].phase5 = 0.0f;
_tones[n].phase6 = 0.0f;
_tones[n].phase7 = 0.0f;
_tones[n].phase8 = 0.0f;
_tones[n].phase9 = 0.0f;
_tones[n].phase10 = 0.0f;
_tones[n].phase11 = 0.0f;
_tones[n].levelEnvelope1 = _levelEnvelopeAboveOuter;
_tones[n].levelEnvelope2 = _levelEnvelopeAboveInner;
_tones[n].levelEnvelope3 = _levelEnvelopeCentreNote;
_tones[n].levelEnvelope4 = _levelEnvelopeBelowInner;
_tones[n].levelEnvelope5 = _levelEnvelopeBelowOuter;
_tones[n].pitchEnvelope1 = _pitchEnvelopeAboveOuter;
_tones[n].pitchEnvelope2 = _pitchEnvelopeAboveInner;
_tones[n].pitchEnvelope3 = _pitchEnvelopeCentreNote;
_tones[n].pitchEnvelope4 = _pitchEnvelopeBelowInner;
_tones[n].pitchEnvelope5 = _pitchEnvelopeBelowOuter;
_tones[n].gain = 1.0f;
_tones[n].envStep = 0.0f;
_tones[n].envDelta = ENVELOPE_LENGTH / length; // GS Envelope
_tones[n].fadeOut = 1.0f;
return;
}
}
}
ToneEvent
typedef enum
{
ToneEventStateInactive = 0, // ToneEvent is not used for playing a tone
ToneEventStatePressed, // ToneEvent is still playing normally
ToneEventStateReleased, // ToneEvent is released and ringing out
}
ToneEventState;
bell crash
2017-07-28 21:05:28.438 SatGam2[897:503237] ENABLED (player 4: state 1)
2017-07-28 21:05:28.502804+1000 SatGam2[897:504929] [aqme] 254: AQDefaultDevice (173): skipping input stream 0 0 0x0
2017-07-28 21:05:30.511372+1000 SatGam2[897:504929] [aqme] 254: AQDefaultDevice (173): skipping input stream 0 0 0x0
2017-07-28 21:05:30.852 SatGam2[897:503237] ToneEventStatePressed
2017-07-28 21:05:30.853 SatGam2[897:503237] ToneEventStateReleased
2017-07-28 21:05:30.853 SatGam2[897:503237] ToneEventStatePressed
2017-07-28 21:05:30.854 SatGam2[897:503237] ToneEventStatePressed
2017-07-28 21:05:30.854 SatGam2[897:503237] ToneEventStatePressed
2017-07-28 21:05:30.854 SatGam2[897:503237] noteLength 70
2017-07-28 21:05:30.855 SatGam2[897:503237] timeToGo 70 [RissetBells, from PlayViewController]
2017-07-28 21:05:30.855 SatGam2[897:503237] Risset Bell MIDI note number 1 : length 70
2017-07-28 21:05:30.855 SatGam2[897:503237] Clapper button 7 pressed
SatGam2 was compiled with optimization - stepping may behave oddly; variables may not be available.
(lldb) bt
* thread #10, name = 'com.apple.coreaudio.AQClient', stop reason = EXC_BAD_ACCESS (code=1, address=0x9514)
* frame #0: 0x00071d82 SatGam2`-[Synth amplitudeLookupPartial6:](self=<unavailable>, _cmd="amplitudeLookupPartial6:", n=<unavailable>) at Synth.m:875 [opt]
frame #1: 0x000713f6 SatGam2`-[Synth fillBuffer:frames:](self=0x7e805a00, _cmd="fillBuffer:frames:", buffer=<unavailable>, frames=<unavailable>) at Synth.m:692 [opt]
frame #2: 0x000751e9 SatGam2`__39-[PlayViewController startAudioSession]_block_invoke((null)=0x7ae48160, buffer=<unavailable>, audioFormat=AudioStreamBasicDescription # 0xb0491ab8) at PlayViewController.m:393 [opt]
frame #3: 0x00073985 SatGam2`PlayCallback(inUserData=0x7c44ac60, inAudioQueue=<unavailable>, inBuffer=<unavailable>) at AudioBufferPlayer.m:30 [opt]
frame #4: 0x00a65e90 AudioToolbox`ClientAudioQueue::CallOutputCallback(AudioQueueBuffer*) + 336
frame #5: 0x00a69a99 AudioToolbox`ClientMessageHandler::OutputBufferComplete(unsigned int) + 137
frame #6: 0x00a5fc90 AudioToolbox`AQClientCallbackMessageReader::DispatchCallbacks(void const*, unsigned long) + 176
frame #7: 0x00a5f98d AudioToolbox`ClientAudioQueue::FetchAndDeliverPendingCallbacks(unsigned int) + 301
frame #8: 0x00a5fb78 AudioToolbox`AQCallbackReceiver_CallbackNotificationsAvailable + 152
frame #9: 0x009b071a AudioToolbox`_XCallbackNotificationsAvailable + 42
frame #10: 0x00c3b37b AudioToolbox`mshMIGPerform + 207
frame #11: 0x035e28b3 CoreFoundation`__CFRUNLOOP_IS_CALLING_OUT_TO_A_SOURCE1_PERFORM_FUNCTION__ + 35
frame #12: 0x035e2822 CoreFoundation`__CFRunLoopDoSource1 + 498
frame #13: 0x035daccd CoreFoundation`__CFRunLoopRun + 2669
frame #14: 0x035d9fd4 CoreFoundation`CFRunLoopRunSpecific + 372
frame #15: 0x035d9e4b CoreFoundation`CFRunLoopRunInMode + 123
frame #16: 0x00a6a822 AudioToolbox`GenericRunLoopThread::Entry(void*) + 178
frame #17: 0x00c406b2 AudioToolbox`CAPThread::Entry(CAPThread*) + 96
frame #18: 0x06215047 libsystem_pthread.dylib`_pthread_body + 184
frame #19: 0x06214f8f libsystem_pthread.dylib`_pthread_start + 243
frame #20: 0x0621484a libsystem_pthread.dylib`thread_start + 34
(lldb)
chorus crash
2017-07-28 20:30:13.061 SatGam2[864:436890] ENABLED (player 4: state 2)
2017-07-28 20:30:13.227994+1000 SatGam2[864:438359] [aqme] 254: AQDefaultDevice (173): skipping input stream 0 0 0x0
2017-07-28 20:30:14.878 SatGam2[864:436890] ToneEventStatePressed
2017-07-28 20:30:14.878 SatGam2[864:436890] ToneEventStateReleased
2017-07-28 20:30:14.879 SatGam2[864:436890] ToneEventStatePressed
2017-07-28 20:30:14.879 SatGam2[864:436890] ToneEventStatePressed
2017-07-28 20:30:14.879 SatGam2[864:436890] ToneEventStatePressed
2017-07-28 20:30:14.879 SatGam2[864:436890] noteLength 47
2017-07-28 20:30:14.880 SatGam2[864:436890] timeToGo 47 [RissetChorus, from PlayViewController]
2017-07-28 20:30:14.880 SatGam2[864:436890] Risset Chorus MIDI note number 3 : length 47
2017-07-28 20:30:14.880 SatGam2[864:436890] Note button 4 pressed
SatGam2 was compiled with optimization - stepping may behave oddly; variables may not be available.
(lldb) bt
* thread #10, name = 'com.apple.coreaudio.AQClient', stop reason = EXC_BAD_ACCESS (code=1, address=0xe62c)
* frame #0: 0x0007b728 SatGam2`-[Synth pitchShiftLookupPartial1:](self=0x7bb0d400, _cmd="pitchShiftLookupPartial1:", n=0) at Synth.m:762 [opt]
frame #1: 0x0007aeb6 SatGam2`-[Synth fillBuffer:frames:](self=0x7bb0d400, _cmd="fillBuffer:frames:", buffer=<unavailable>, frames=<unavailable>) at Synth.m:724 [opt]
frame #2: 0x0007f1e9 SatGam2`__39-[PlayViewController startAudioSession]_block_invoke((null)=0x7c74d5b0, buffer=<unavailable>, audioFormat=AudioStreamBasicDescription # 0xb01f9ab8) at PlayViewController.m:393 [opt]
frame #3: 0x0007d985 SatGam2`PlayCallback(inUserData=0x7b656430, inAudioQueue=<unavailable>, inBuffer=<unavailable>) at AudioBufferPlayer.m:30 [opt]
frame #4: 0x00a14e90 AudioToolbox`ClientAudioQueue::CallOutputCallback(AudioQueueBuffer*) + 336
frame #5: 0x00a18a99 AudioToolbox`ClientMessageHandler::OutputBufferComplete(unsigned int) + 137
frame #6: 0x00a0ec90 AudioToolbox`AQClientCallbackMessageReader::DispatchCallbacks(void const*, unsigned long) + 176
frame #7: 0x00a0e98d AudioToolbox`ClientAudioQueue::FetchAndDeliverPendingCallbacks(unsigned int) + 301
frame #8: 0x00a0eb78 AudioToolbox`AQCallbackReceiver_CallbackNotificationsAvailable + 152
frame #9: 0x0095f71a AudioToolbox`_XCallbackNotificationsAvailable + 42
frame #10: 0x00bea37b AudioToolbox`mshMIGPerform + 207
frame #11: 0x035988b3 CoreFoundation`__CFRUNLOOP_IS_CALLING_OUT_TO_A_SOURCE1_PERFORM_FUNCTION__ + 35
frame #12: 0x03598822 CoreFoundation`__CFRunLoopDoSource1 + 498
frame #13: 0x03590ccd CoreFoundation`__CFRunLoopRun + 2669
frame #14: 0x0358ffd4 CoreFoundation`CFRunLoopRunSpecific + 372
frame #15: 0x0358fe4b CoreFoundation`CFRunLoopRunInMode + 123
frame #16: 0x00a19822 AudioToolbox`GenericRunLoopThread::Entry(void*) + 178
frame #17: 0x00bef6b2 AudioToolbox`CAPThread::Entry(CAPThread*) + 96
frame #18: 0x0621f047 libsystem_pthread.dylib`_pthread_body + 184
frame #19: 0x0621ef8f libsystem_pthread.dylib`_pthread_start + 243
frame #20: 0x0621e84a libsystem_pthread.dylib`thread_start + 34
(lldb)

How to implement ESP8266 5 kHz PWM?

I need to realise a PWM output with 5 kHz +/- 5%. (Presumably due to a filter circuit into which this feeds over which I have no control.)
Is this realisable with a ESP8266 (ideally with NodeMCU)?
I realise that the software PWM of the ESP8266 has a maximum frequency of 1 kHz while the sigma-delta can be used to implement a PWM with a fixed frequency of about 300 kHz.
So is there a reliable way to achieve 5 kHz? I know some people experimented with the I2S peripheral for waveform output but I am unsure whether it can be used for 5kHz output.
Has anybody looked at into a similar problem before?
The essential code is:
#define qap 2 // Quick As Possible ... Duty cycle only 0, 50, 100%
#define HFreq 5150
#define pPulse D2 // a NodeMCU/ESP8266 GPIO PWM pin
analogWriteRange(qap); analogWriteFreq( HFreq ); analogWrite(pPulse, 1); // start PWM
TL;DR
Just did some crude benchamrks
// had HFreq=126400 with 75 KHz pulse at 80 MHz ESP clock, every 1 sec but proof of evidence lost
nominally with 80 MHz clock got
// 72.24 KHz 361178 2015061929
// 72.23 KHz 361163 2415062390
// 72.23 KHz 361133 2815062824
and
// 141.52 KHz 353809 2009395076
// 141.54 KHz 353846 2409395627
// 141.52 KHz 353806 2809395946
with 160 MHz clock
CAVEATS!!!
1. The duty cycle range is 2!
2. As well the system had no other conditioning, so other timing artifacts were still present from Serial IO etc. In particular instability due to the WDT and WiFi penultimately appeared. (Anyhow, presumably this is an issue only when the ESP8266 is under stress and duress.)
// 141.50 KHz 353754 619466806
// 141.52 KHz 353810 1019467038
// ...
// ad infinitum cum tempore finitum, infinitus est ad nauseum?
// ...
// 141.54 KHz 353857 735996888
//
// ets Jan 8 2013,rst cause:4, boot mode:(1,7)
//
//wdt reset
When using the following code to generate a 5 KHz square wave signal, the considerations above are not an issue and do not occur.
// ------------ test results for 80 MHz clock --------------
//
//
// PWM pulse test
//
// F_CPU: 80000000L
// ESP8266_CLOCK: 80000000UL
// PWM "freq.": 5150
//
//
// connect D1 to D2
//
//
// raw MPU
// frequency count cycle
// 0.00 KHz 1 407976267
// 4.74 KHz 9482 567976702
// 5.00 KHz 10007 727977137
// 5.00 KHz 10006 887977572
// 5.00 KHz 10006 1047978007
// 5.00 KHz 10007 1207978442
// 5.00 KHz 10006 1367978877
// 5.00 KHz 10006 1527979312
// 5.00 KHz 10007 1687979747
// 5.00 KHz 10006 1847980182
// 5.00 KHz 10006 2007980617
// 5.00 KHz 10007 2167981052
// 5.00 KHz 10006 2327981487
// 5.00 KHz 10006 2487981922
// 5.00 KHz 10007 2647982357 ...
//
// crude testing for 5KHz signal
// extracted from:
// highest frequency / shortest period pin pulse generate / detect test
//
// uses raw ESP8266 / NodeMCU V1.0 hardware primitive interface of Arduino IDE (no included libraries)
//
// timing dependencies: WDT, WiFi, I2S, I2C, one wire, UART, SPI, ...
//
// Arduino GPIO 16 5 4 0 2 14 12 13 15 3 1 0 1 2 3 4 5 ... 12 13 14 15 16
// NodeMCU D pin 0 1 2 3 4 5 6 7 8 9 10 3 10 4 9 2 1 ... 6 7 5 8 0
// | | | | | | | | | | |
// a WAKE | | F Tx1 | | Rx2 Tx2 Rx0 Tx0
// k (NO PWM or | | L blue | | | |
// a' interrupt) | S A * H | H | H | | * led's
// s red S D S S M S H
// * C A H C I I C
// L T L S M S
// K A K O O
// └ - - - - └----UART's----┘
// └--I2C--┘ └-----SPI------┘
//
// rules of engagement are obscure and vague for effects of argument values for the paramters of these functions:
// analogWriteRange(qap); analogWriteFreq( HFreq ); analogWrite(pPulse, 1);
//
// http://stackoverflow.com/questions/42112357/how-to-implement-esp8266-5-khz-pwm
//
// system #defines: F_CPU ESP8266_CLOCK
#define pInt D1 // HWI pin: NOT D0 ie. GPIO16 is not hardwared interrupt or PWM pin
#define pPulse D2 // PWM pulsed frequency source ... ditto D0 (note: D4 = blue LED)
#define countFor 160000000UL
#define gmv(p) #p // get macro value
#define em(p) gmv(p) // evaluate macro
#define qap 2 // minimal number of duty cycle levels (0, 50, 100% ) Quick As Possible ...
#define HFreq 5150 //((long int) F_CPU==80000000L ? 125000 : 250000) // ... to minimize time of a cycle period
// max values ^ and ^ found empirically
// had HFreq=126400 with 75 KHz pulse at 80 MHz ESP clock, every 1 sec but proof of evidence lost
#define infoTxt (String) \
"\n\n\t PWM pulse test " \
"\n F_CPU: " em(F_CPU) \
"\n ESP8266_CLOCK: " em(ESP8266_CLOCK) \
"\n PWM \"freq.\": " + HFreq + "\n" \
"\n\n connect " em(pInt) " to " em(pPulse) "\n" \
"\n\n raw MPU " \
" \n frequency count cycle "
long int oc=1, cntr=1;
unsigned long int tc=0;
void hwISR(){ cntr++; } // can count pulses if pInt <---> to pPulse
void anISR(){ tc=ESP.getCycleCount(); timer0_write( countFor + tc ); oc=cntr; cntr=1; }
void setup() { // need to still confirm duty cycle=50% (scope it or ...)
noInterrupts();
Serial.begin(115200); Serial.println(infoTxt); delay(10); // Serial.flush(); Serial.end(); // Serial timing?
analogWriteRange(qap); analogWriteFreq( HFreq ); analogWrite(pPulse, 1); // start PWM
pinMode( pInt, INPUT ); attachInterrupt(pInt, hwISR, RISING); // count pulses
timer0_isr_init(); timer0_attachInterrupt( anISR ); anISR(); //
interrupts();
}
void loop() { delay(10); if (oc==0) return;
Serial.println((String)" "+(oc/1000.0*F_CPU/countFor)+" KHz "+oc+" "+tc); oc=0; }
//
You can also use the hardware SPI interface which has an adjustable clock speed.
By writing continuous data, the output waveform appears on SCLK.
I've figured out the abovementioned while creating a knight rider effect with IC 74hc595 on ESP8266, using MicroPython (which is indeed slow, as being a script language).
It worked for me up to 4MHz SPI clock speed.
The disadvantage is, for permanent waveform, you need to write data to MOSI forever (as when SPI data buffer goes empty, there is no signal on SCLK anymore).

Resources