Skip to content

Commit

Permalink
v1.3.0
Browse files Browse the repository at this point in the history
* New read raw audio loop:
  - captures audio samples properly when the mic position loops over the 1 second internal AudioClip
  - reduced GC allocation and better performance by moving away from a coroutine to using the Update loop instead
* Fixed IsRecording being incorrectly set to true when the Microphone fails to start.
* StartRecording now returns bool
  • Loading branch information
adrenak committed Dec 8, 2024
1 parent f657532 commit f27c10b
Show file tree
Hide file tree
Showing 5 changed files with 81 additions and 54 deletions.
3 changes: 3 additions & 0 deletions Assets/UniMic/Editor/MicEditor.cs
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,9 @@ public override void OnInspectorGUI() {

EditorGUILayout.IntField("Device Index", mic.CurrentDeviceIndex);
EditorGUILayout.LabelField("Device Name", mic.CurrentDeviceName);
Microphone.GetDeviceCaps(mic.CurrentDeviceName, out int min, out int max);
EditorGUILayout.IntField("Max Frequency", max);
EditorGUILayout.IntField("Min Frequency", min);

EditorGUILayout.Toggle("Is Recording", mic.IsRecording);
EditorGUILayout.IntField("Frequency", mic.Frequency);
Expand Down
2 changes: 1 addition & 1 deletion Assets/UniMic/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -61,7 +61,7 @@ Includes the timestamp from when the sample was captured.
- `int frequency=16000` the frequency of the inner `AudioClip`
- `int sampleDurationMS` the duration of a single sample segment in milliseconds that the instance keeps and fires on event
- `Returns`
- `void`
- `bool` if the microphone successfully started recording

- `ResumeRecording` resumes the microphone recording at the frequency and sampleDurationMS

Expand Down
117 changes: 69 additions & 48 deletions Assets/UniMic/Runtime/Mic.cs
Original file line number Diff line number Diff line change
Expand Up @@ -57,7 +57,7 @@ public int SampleLength {
/// </summary>
public string CurrentDeviceName {
get {
if (CurrentDeviceIndex < 0 || CurrentDeviceIndex >= Microphone.devices.Length)
if (CurrentDeviceIndex < 0 || CurrentDeviceIndex >= Devices.Count)
return string.Empty;
return Devices[CurrentDeviceIndex];
}
Expand Down Expand Up @@ -101,9 +101,9 @@ public string CurrentDeviceName {
static Mic m_Instance;
public static Mic Instance {
get {
if(m_Instance == null)
if (m_Instance == null)
m_Instance = FindObjectOfType<Mic>();
if (m_Instance == null)
if (m_Instance == null)
m_Instance = new GameObject("UniMic.Mic").AddComponent<Mic>();
return m_Instance;
}
Expand All @@ -121,8 +121,9 @@ public static Mic Instantiate() {
}

void Awake() {
if(Application.isPlaying)
if (Application.isPlaying)
DontDestroyOnLoad(gameObject);

if (Devices.Count > 0)
CurrentDeviceIndex = 0;
}
Expand All @@ -132,9 +133,10 @@ void Awake() {
/// </summary>
/// <param name="index">The index of the Mic device. Refer to <see cref="Devices"/> for available devices</param>
public void SetDeviceIndex(int index) {
Microphone.End(CurrentDeviceName);
bool wasRecording = IsRecording;
StopRecording();
CurrentDeviceIndex = index;
if (IsRecording)
if(wasRecording)
StartRecording(Frequency, SampleDurationMS);
}

Expand All @@ -149,19 +151,24 @@ public void ResumeRecording() {
/// <summary>
/// Starts to stream the input of the current Mic device
/// </summary>
public void StartRecording(int frequency = 16000, int sampleDurationMS = 10) {
public bool StartRecording(int frequency = 16000, int sampleDurationMS = 10) {
StopRecording();
IsRecording = true;

Frequency = frequency;
SampleDurationMS = sampleDurationMS;

AudioClip = Microphone.Start(CurrentDeviceName, true, 1, Frequency);
Sample = new float[Frequency / 1000 * SampleDurationMS * AudioClip.channels];
if (AudioClip == null) {
IsRecording = false;
return false;
}

StartCoroutine(ReadRawAudio());
IsRecording = true;

Sample = new float[Frequency / 1000 * SampleDurationMS * AudioClip.channels];

OnStartRecording?.Invoke();
return true;
}

/// <summary>
Expand All @@ -176,49 +183,63 @@ public void StopRecording() {
Destroy(AudioClip);
AudioClip = null;

StopCoroutine(ReadRawAudio());

OnStopRecording?.Invoke();
}

IEnumerator ReadRawAudio() {
int loops = 0;
int readAbsPos = 0;
int prevPos = 0;
float[] temp = new float[Sample.Length];

while (AudioClip != null && Microphone.IsRecording(CurrentDeviceName)) {
bool isNewDataAvailable = true;

while (isNewDataAvailable) {
int currPos = Microphone.GetPosition(CurrentDeviceName);
if (currPos < prevPos)
loops++;
prevPos = currPos;

var currAbsPos = loops * AudioClip.samples + currPos;
var nextReadAbsPos = readAbsPos + temp.Length;

if (nextReadAbsPos < currAbsPos) {
AudioClip.GetData(temp, readAbsPos % AudioClip.samples);

Sample = temp;
m_SampleCount++;
OnSampleReady?.Invoke(m_SampleCount, Sample);

OnTimestampedSampleReady?.Invoke(
(long)(DateTime.UtcNow.Subtract(new DateTime(1970, 1, 1)).TotalMilliseconds),
Sample
);

readAbsPos = nextReadAbsPos;
isNewDataAvailable = true;
}
else
isNewDataAvailable = false;
int currPos;
int prevPos = 0;
bool didLoop;
float[] sample;
readonly Queue<float> pcmQueue = new Queue<float>();
void Update() {
if (!IsRecording) {
sample = null;
return;
}

if(sample == null)
sample = new float[Sample.Length];

currPos = Microphone.GetPosition(CurrentDeviceName);
if (currPos == prevPos)
return;

didLoop = currPos < prevPos;

if (!didLoop) {
var samples = new float[currPos - prevPos];
AudioClip.GetData(samples, prevPos);
foreach (var t in samples)
pcmQueue.Enqueue(t);
} else {
int lastLoopSampleLen = AudioClip.samples - prevPos - 1;
int currLoopSampleLen = currPos + 1;
var lastLoopSamples = new float[lastLoopSampleLen];
var currLoopSamples = new float[currLoopSampleLen];
AudioClip.GetData(lastLoopSamples, prevPos - 1);
AudioClip.GetData(currLoopSamples, 0);

foreach (var sample in lastLoopSamples)
pcmQueue.Enqueue(sample);

foreach (var sample in currLoopSamples)
pcmQueue.Enqueue(sample);
}

while (pcmQueue.Count >= Sample.Length) {
for (int i = 0; i < sample.Length; i++) {
sample[i] = pcmQueue.Dequeue();
}
yield return null;
Sample = sample;
m_SampleCount++;
OnSampleReady?.Invoke(m_SampleCount, Sample);
OnTimestampedSampleReady?.Invoke(
(long)(DateTime.UtcNow.Subtract(new DateTime(1970, 1, 1)).TotalMilliseconds),
Sample
);
}

prevPos = currPos;
}
#endregion

Expand Down
11 changes: 7 additions & 4 deletions Assets/UniMic/Runtime/MicAudioSource.cs
Original file line number Diff line number Diff line change
Expand Up @@ -10,22 +10,25 @@ namespace Adrenak.UniMic {
public class MicAudioSource : MonoBehaviour {
public bool startRecordingAutomatically = true;
[Header("If startRecordingAutomatically is true:")]
public int recordingFrequency = 44000;
public int recordingFrequency = 48000;
public int sampleDurationMS = 100;
AudioClip clip;

void Start() {
var audioSource = gameObject.GetComponent<AudioSource>();
audioSource.loop = false;

var mic = Mic.Instance;

if(startRecordingAutomatically)
mic.StartRecording(recordingFrequency, sampleDurationMS);

mic.OnTimestampedSampleReady += (index, segment) => {
var clip = AudioClip.Create("clip", mic.SampleLength, mic.AudioClip.channels, mic.AudioClip.frequency, false);
mic.OnTimestampedSampleReady += (timestamp, segment) => {
if (clip != null)
Destroy(clip);
clip = AudioClip.Create("clip", segment.Length, mic.AudioClip.channels, mic.AudioClip.frequency, false);
clip.SetData(segment, 0);
audioSource.clip = clip;
audioSource.loop = true;
audioSource.Play();
};
}
Expand Down
2 changes: 1 addition & 1 deletion Assets/UniMic/package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "com.adrenak.unimic",
"version": "1.2.0",
"version": "1.3.0",
"displayName": "Adrenak.UniMic",
"description": "Convenience wrapper over Unity's Microphone class.",
"unity": "2018.4",
Expand Down

0 comments on commit f27c10b

Please sign in to comment.