mirror of
https://github.com/curioustorvald/tsvm.git
synced 2026-06-17 18:04:04 +09:00
audio changes
This commit is contained in:
@@ -119,7 +119,7 @@ function mixInto(buf, lengthSec, offsetSec, op, amp, pan, sampleFn) {
|
|||||||
|
|
||||||
// ── Waveform generators ─────────────────────────────────────────────────────
|
// ── Waveform generators ─────────────────────────────────────────────────────
|
||||||
|
|
||||||
function makeSquare(buf, length, offset, freq, duty, op, amp, pan) {
|
function makeSquare(buf, length, offset, freq, duty, op, amp, pan, phaseOffset) {
|
||||||
// buffer: [Uint8Array, Uint8Array] or native buffer
|
// buffer: [Uint8Array, Uint8Array] or native buffer
|
||||||
// length: in seconds
|
// length: in seconds
|
||||||
// offset: in seconds
|
// offset: in seconds
|
||||||
@@ -128,13 +128,16 @@ function makeSquare(buf, length, offset, freq, duty, op, amp, pan) {
|
|||||||
// op: add / mul / sub; default: add
|
// op: add / mul / sub; default: add
|
||||||
// amp: 0.0 to 1.0; default: 0.5
|
// amp: 0.0 to 1.0; default: 0.5
|
||||||
// pan: -1.0 to 1.0; default: 0.0
|
// pan: -1.0 to 1.0; default: 0.0
|
||||||
|
// phaseOffset: optional absolute-time base (seconds) added to phase calc only,
|
||||||
|
// not to the buffer write position — use to ensure phase continuity
|
||||||
|
// across successive calls (e.g. frame boundaries).
|
||||||
if (duty == null) duty = 0.5
|
if (duty == null) duty = 0.5
|
||||||
if (op == null) op = 'add'
|
if (op == null) op = 'add'
|
||||||
if (amp == null) amp = 0.5
|
if (amp == null) amp = 0.5
|
||||||
if (pan == null) pan = 0.0
|
if (pan == null) pan = 0.0
|
||||||
|
const tBase = (phaseOffset || 0) + offset
|
||||||
mixInto(buf, length, offset, op, amp, pan, function(i) {
|
mixInto(buf, length, offset, op, amp, pan, function(i) {
|
||||||
const t = offset + i / HW_SAMPLING_RATE
|
const phase = ((tBase + i / HW_SAMPLING_RATE) * freq) % 1
|
||||||
const phase = (t * freq) % 1
|
|
||||||
return (phase < duty) ? 1.0 : -1.0
|
return (phase < duty) ? 1.0 : -1.0
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
@@ -225,7 +228,7 @@ function lfsrAdvance(state, steps, mode) {
|
|||||||
const LFSR_PERIOD_LONG = 32767 // mode 0
|
const LFSR_PERIOD_LONG = 32767 // mode 0
|
||||||
const LFSR_PERIOD_SHORT = 93 // mode 1
|
const LFSR_PERIOD_SHORT = 93 // mode 1
|
||||||
|
|
||||||
function makeNoise(buf, length, offset, freq, type, op, amp, pan) {
|
function makeNoise(buf, length, offset, freq, type, op, amp, pan, phaseOffset) {
|
||||||
// buffer: [Uint8Array, Uint8Array] or native buffer
|
// buffer: [Uint8Array, Uint8Array] or native buffer
|
||||||
// length: in seconds
|
// length: in seconds
|
||||||
// offset: in seconds
|
// offset: in seconds
|
||||||
@@ -238,20 +241,23 @@ function makeNoise(buf, length, offset, freq, type, op, amp, pan) {
|
|||||||
// op: add / mul / sub; default: add
|
// op: add / mul / sub; default: add
|
||||||
// amp: 0.0 to 1.0; default: 0.5
|
// amp: 0.0 to 1.0; default: 0.5
|
||||||
// pan: -1.0 to 1.0; default: 0.0
|
// pan: -1.0 to 1.0; default: 0.0
|
||||||
|
// phaseOffset: optional absolute-time base (seconds) added to phase/LFSR calc only —
|
||||||
|
// see makeSquare for details.
|
||||||
//
|
//
|
||||||
// LFSR types (1 and 2) are deterministic given (offset, freq): calling with
|
// LFSR types (1 and 2) are deterministic given (phaseOffset+offset, freq): calling
|
||||||
// monotonically advancing offset values produces a seamless noise stream
|
// with monotonically advancing phaseOffset+offset produces a seamless noise stream
|
||||||
// across frames. White noise types (-1, 0) are random per call.
|
// across frames. White noise types (-1, 0) are random per call.
|
||||||
if (op == null) op = 'add'
|
if (op == null) op = 'add'
|
||||||
if (amp == null) amp = 0.5
|
if (amp == null) amp = 0.5
|
||||||
if (pan == null) pan = 0.0
|
if (pan == null) pan = 0.0
|
||||||
|
const tBase = (phaseOffset || 0) + offset
|
||||||
|
|
||||||
if (type === -1) {
|
if (type === -1) {
|
||||||
// 8-bit white: new random float in [-1, 1] each clock period
|
// 8-bit white: new random float in [-1, 1] each clock period
|
||||||
let prevClock = -1
|
let prevClock = -1
|
||||||
let noiseVal = 0.0
|
let noiseVal = 0.0
|
||||||
mixInto(buf, length, offset, op, amp, pan, function(i) {
|
mixInto(buf, length, offset, op, amp, pan, function(i) {
|
||||||
const currentClock = Math.floor((offset + i / HW_SAMPLING_RATE) * freq) | 0
|
const currentClock = Math.floor((tBase + i / HW_SAMPLING_RATE) * freq) | 0
|
||||||
if (currentClock !== prevClock) {
|
if (currentClock !== prevClock) {
|
||||||
prevClock = currentClock
|
prevClock = currentClock
|
||||||
noiseVal = Math.random() * 2.0 - 1.0
|
noiseVal = Math.random() * 2.0 - 1.0
|
||||||
@@ -263,7 +269,7 @@ function makeNoise(buf, length, offset, freq, type, op, amp, pan) {
|
|||||||
let prevClock = -1
|
let prevClock = -1
|
||||||
let noiseVal = 1.0
|
let noiseVal = 1.0
|
||||||
mixInto(buf, length, offset, op, amp, pan, function(i) {
|
mixInto(buf, length, offset, op, amp, pan, function(i) {
|
||||||
const currentClock = Math.floor((offset + i / HW_SAMPLING_RATE) * freq) | 0
|
const currentClock = Math.floor((tBase + i / HW_SAMPLING_RATE) * freq) | 0
|
||||||
if (currentClock !== prevClock) {
|
if (currentClock !== prevClock) {
|
||||||
prevClock = currentClock
|
prevClock = currentClock
|
||||||
noiseVal = (Math.random() >= 0.5) ? 1.0 : -1.0
|
noiseVal = (Math.random() >= 0.5) ? 1.0 : -1.0
|
||||||
@@ -274,13 +280,13 @@ function makeNoise(buf, length, offset, freq, type, op, amp, pan) {
|
|||||||
// LFSR-based noise (types 1 and 2)
|
// LFSR-based noise (types 1 and 2)
|
||||||
const mode = (type === 2) ? 1 : 0
|
const mode = (type === 2) ? 1 : 0
|
||||||
const period = (mode === 0) ? LFSR_PERIOD_LONG : LFSR_PERIOD_SHORT
|
const period = (mode === 0) ? LFSR_PERIOD_LONG : LFSR_PERIOD_SHORT
|
||||||
// Advance to deterministic position for this offset so consecutive frame
|
// Advance to deterministic position for this tBase so consecutive frame
|
||||||
// calls with advancing offsets produce a seamless noise stream.
|
// calls with monotonically advancing phaseOffset produce a seamless noise stream.
|
||||||
const startClock = Math.floor(offset * freq) | 0
|
const startClock = Math.floor(tBase * freq) | 0
|
||||||
let lfsr = lfsrAdvance(1, startClock % period, mode)
|
let lfsr = lfsrAdvance(1, startClock % period, mode)
|
||||||
let prevClock = startClock
|
let prevClock = startClock
|
||||||
mixInto(buf, length, offset, op, amp, pan, function(i) {
|
mixInto(buf, length, offset, op, amp, pan, function(i) {
|
||||||
const currentClock = Math.floor((offset + i / HW_SAMPLING_RATE) * freq) | 0
|
const currentClock = Math.floor((tBase + i / HW_SAMPLING_RATE) * freq) | 0
|
||||||
const delta = currentClock - prevClock
|
const delta = currentClock - prevClock
|
||||||
if (delta > 0) {
|
if (delta > 0) {
|
||||||
const steps = delta % period
|
const steps = delta % period
|
||||||
@@ -292,17 +298,18 @@ function makeNoise(buf, length, offset, freq, type, op, amp, pan) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
function makeAliasedTriangleNES(buf, length, offset, freq, duty, op, amp, pan) {
|
function makeAliasedTriangleNES(buf, length, offset, freq, duty, op, amp, pan, phaseOffset) {
|
||||||
// NES APU triangle — quantised to the authentic 32-step, 4-bit (0..15) staircase.
|
// NES APU triangle — quantised to the authentic 32-step, 4-bit (0..15) staircase.
|
||||||
// The 32-step sequence is: 15,14,...,1,0, 0,1,...,14,15 (descending then ascending).
|
// The 32-step sequence is: 15,14,...,1,0, 0,1,...,14,15 (descending then ascending).
|
||||||
// This mirrors the real NES triangle DAC which has 32 equal-height steps per period.
|
// This mirrors the real NES triangle DAC which has 32 equal-height steps per period.
|
||||||
// duty parameter is accepted for API symmetry but ignored (NES triangle is always symmetric).
|
// duty parameter is accepted for API symmetry but ignored (NES triangle is always symmetric).
|
||||||
|
// phaseOffset: optional absolute-time base (seconds) — see makeSquare for details.
|
||||||
if (op == null) op = 'add'
|
if (op == null) op = 'add'
|
||||||
if (amp == null) amp = 0.5
|
if (amp == null) amp = 0.5
|
||||||
if (pan == null) pan = 0.0
|
if (pan == null) pan = 0.0
|
||||||
|
const tBase = (phaseOffset || 0) + offset
|
||||||
mixInto(buf, length, offset, op, amp, pan, function(i) {
|
mixInto(buf, length, offset, op, amp, pan, function(i) {
|
||||||
const t = offset + i / HW_SAMPLING_RATE
|
const phase = ((tBase + i / HW_SAMPLING_RATE) * freq) % 1
|
||||||
const phase = (t * freq) % 1
|
|
||||||
const step32 = Math.floor(phase * 32) | 0 // 0..31
|
const step32 = Math.floor(phase * 32) | 0 // 0..31
|
||||||
// step 0..15: descend from 15 to 0; step 16..31: ascend from 0 to 15
|
// step 0..15: descend from 15 to 0; step 16..31: ascend from 0 to 15
|
||||||
const level = (step32 < 16) ? (15 - step32) : (step32 - 16)
|
const level = (step32 < 16) ? (15 - step32) : (step32 - 16)
|
||||||
@@ -341,7 +348,7 @@ function sendBuffer(buf, playhead, offsetSec, lengthSec, stagingPtr) {
|
|||||||
}
|
}
|
||||||
// Wait for room in the playback queue (mirrors playwav.js idiom)
|
// Wait for room in the playback queue (mirrors playwav.js idiom)
|
||||||
// while (audio.getPosition(playhead) > 2) sys.sleep(2)
|
// while (audio.getPosition(playhead) > 2) sys.sleep(2)
|
||||||
audio.putPcmDataByPtr(stagingPtr, take * 2, playhead)
|
audio.putPcmDataByPtr(playhead, stagingPtr, take * 2, 0)
|
||||||
audio.setSampleUploadLength(playhead, take * 2)
|
audio.setSampleUploadLength(playhead, take * 2)
|
||||||
audio.startSampleUpload(playhead)
|
audio.startSampleUpload(playhead)
|
||||||
remaining -= take
|
remaining -= take
|
||||||
|
|||||||
@@ -1978,6 +1978,11 @@ Sound Adapter
|
|||||||
Endianness: little
|
Endianness: little
|
||||||
|
|
||||||
|
|
||||||
|
TSVM Sound Adapter is consisted of 4 playheads, each playhead is capable of playing one PCM or Tracker track.
|
||||||
|
|
||||||
|
Synchronisation between playheads are not guaranteed. Do not play music in multiple tracks.
|
||||||
|
|
||||||
|
|
||||||
Memory Space
|
Memory Space
|
||||||
|
|
||||||
0..114687 RW: Sample bin
|
0..114687 RW: Sample bin
|
||||||
|
|||||||
@@ -10,7 +10,7 @@ import net.torvald.tsvm.peripheral.MP2Env
|
|||||||
*
|
*
|
||||||
* NOTES:
|
* NOTES:
|
||||||
* 1. tracker mode is currently unimplemented.
|
* 1. tracker mode is currently unimplemented.
|
||||||
* 2. PCM upload buffer (accessed by `putPcmDataByPtr`) is shared between four playheads
|
* 2. Synchronisation between playheads are not guaranteed. Do not play music in multiple tracks.
|
||||||
*
|
*
|
||||||
* ## How to upload PCM audio into a playhead
|
* ## How to upload PCM audio into a playhead
|
||||||
*
|
*
|
||||||
|
|||||||
@@ -41,7 +41,7 @@ private class RenderRunnable(val playhead: AudioAdapter.Playhead) : Runnable {
|
|||||||
|
|
||||||
// printdbg("P${playhead.index+1} go back to spinning")
|
// printdbg("P${playhead.index+1} go back to spinning")
|
||||||
|
|
||||||
Thread.sleep(12)
|
Thread.sleep(6)
|
||||||
}
|
}
|
||||||
else if (playhead.isPlaying && writeQueue.isEmpty) {
|
else if (playhead.isPlaying && writeQueue.isEmpty) {
|
||||||
printdbg("!! QUEUE EXHAUSTED !! QUEUE EXHAUSTED !! QUEUE EXHAUSTED !! QUEUE EXHAUSTED !! QUEUE EXHAUSTED !! QUEUE EXHAUSTED ")
|
printdbg("!! QUEUE EXHAUSTED !! QUEUE EXHAUSTED !! QUEUE EXHAUSTED !! QUEUE EXHAUSTED !! QUEUE EXHAUSTED !! QUEUE EXHAUSTED ")
|
||||||
@@ -49,7 +49,7 @@ private class RenderRunnable(val playhead: AudioAdapter.Playhead) : Runnable {
|
|||||||
// TODO: wait for 1-2 seconds then finally stop the device
|
// TODO: wait for 1-2 seconds then finally stop the device
|
||||||
// playhead.audioDevice.stop()
|
// playhead.audioDevice.stop()
|
||||||
|
|
||||||
Thread.sleep(12)
|
Thread.sleep(6)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user